Skip to content

Commit 1c7a483

Browse files
authored
Merge pull request #40881 from github/repo-sync
Repo sync
2 parents 3874439 + 1c8a215 commit 1c7a483

File tree

7 files changed

+287
-138
lines changed

7 files changed

+287
-138
lines changed

content/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ Links to docs in the `docs-internal` repository must start with a product ID (li
375375
376376
Image paths must start with `/assets` and contain the entire filepath including the file extension. For example, `/assets/images/help/settings/settings-account-delete.png`.
377377
378-
The links to Markdown pages undergo some transformations on the server side to match the current page's language and version. The handling for these transformations lives in [`src/content-render/unified/rewrite-local-links.js`](/src/content-render/unified/rewrite-local-links.js).
378+
The links to Markdown pages undergo some transformations on the server side to match the current page's language and version. The handling for these transformations lives in [`src/content-render/unified/rewrite-local-links.ts`](/src/content-render/unified/rewrite-local-links.ts).
379379
380380
For example, if you include the following link in a content file:
381381

content/contributing/writing-for-github-docs/using-markdown-and-liquid-in-github-docs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ Links to docs in the `docs` repository must start with a product ID (like `/acti
442442

443443
Image paths must start with `/assets` and contain the entire filepath including the file extension. For example, `/assets/images/help/settings/settings-account-delete.png`.
444444

445-
The links to Markdown pages undergo some transformations on the server side to match the current page's language and version. The handling for these transformations lives in [`lib/render-content/plugins/rewrite-local-links`](https://github.com/github/docs/blob/main/src/content-render/unified/rewrite-local-links.js).
445+
The links to Markdown pages undergo some transformations on the server side to match the current page's language and version. The handling for these transformations lives in [`lib/render-content/plugins/rewrite-local-links`](https://github.com/github/docs/blob/main/src/content-render/unified/rewrite-local-links.ts).
446446

447447
For example, if you include the following link in a content file:
448448

src/content-linter/lib/linting-rules/liquid-versioning.js renamed to src/content-linter/lib/linting-rules/liquid-versioning.ts

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import semver from 'semver'
22
import { TokenKind } from 'liquidjs'
3+
// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations
34
import { addError } from 'markdownlint-rule-helpers'
45

56
import { getRange, addFixErrorDetail } from '../helpers/utils'
@@ -9,9 +10,17 @@ import allowedVersionOperators from '@/content-render/liquid/ifversion-supported
910
import { getDeepDataByLanguage } from '@/data-directory/lib/get-data'
1011
import getApplicableVersions from '@/versions/lib/get-applicable-versions'
1112
import { getLiquidTokens, getPositionData } from '../helpers/liquid-utils'
13+
import type { RuleParams, RuleErrorCallback } from '@/content-linter/types'
1214

13-
const allShortnames = Object.keys(allVersionShortnames)
14-
const getAllPossibleVersionNames = memoize(() => {
15+
interface Feature {
16+
versions: Record<string, string>
17+
[key: string]: any
18+
}
19+
20+
type AllFeatures = Record<string, Feature>
21+
22+
const allShortnames: string[] = Object.keys(allVersionShortnames)
23+
const getAllPossibleVersionNames = memoize((): Set<string> => {
1524
// This function might appear "slow" but it's wrapped in a memoizer
1625
// so it's only every executed once for all files that the
1726
// Liquid linting rule functions on.
@@ -21,20 +30,22 @@ const getAllPossibleVersionNames = memoize(() => {
2130
return new Set([...Object.keys(getAllFeatures()), ...allShortnames])
2231
})
2332

24-
const getAllFeatures = memoize(() => getDeepDataByLanguage('features', 'en', process.env.ROOT))
33+
const getAllFeatures = memoize(
34+
(): AllFeatures => getDeepDataByLanguage('features', 'en', process.env.ROOT) as AllFeatures,
35+
)
2536

26-
const allVersionNames = Object.keys(allVersions)
37+
const allVersionNames: string[] = Object.keys(allVersions)
2738

28-
function isAllVersions(versions) {
39+
function isAllVersions(versions: string[]): boolean {
2940
if (versions.length === allVersionNames.length) {
3041
return versions.every((version) => allVersionNames.includes(version))
3142
}
3243
return false
3344
}
3445

35-
function memoize(func) {
36-
let cached = null
37-
return () => {
46+
function memoize<T>(func: () => T): () => T {
47+
let cached: T | null = null
48+
return (): T => {
3849
if (!cached) {
3950
cached = func()
4051
}
@@ -47,14 +58,14 @@ export const liquidIfTags = {
4758
description:
4859
'Liquid `ifversion` tags should be used instead of `if` tags when the argument is a valid version',
4960
tags: ['liquid', 'versioning'],
50-
function: (params, onError) => {
61+
function: (params: RuleParams, onError: RuleErrorCallback) => {
5162
const content = params.lines.join('\n')
5263

5364
const tokens = getLiquidTokens(content).filter(
5465
(token) =>
5566
token.kind === TokenKind.Tag &&
5667
token.name === 'if' &&
57-
token.args.split(/\s+/).some((arg) => getAllPossibleVersionNames().has(arg)),
68+
token.args.split(/\s+/).some((arg: string) => getAllPossibleVersionNames().has(arg)),
5869
)
5970

6071
for (const token of tokens) {
@@ -77,7 +88,7 @@ export const liquidIfVersionTags = {
7788
names: ['GHD020', 'liquid-ifversion-tags'],
7889
description: 'Liquid `ifversion` tags should contain valid version names as arguments',
7990
tags: ['liquid', 'versioning'],
80-
function: (params, onError) => {
91+
function: (params: RuleParams, onError: RuleErrorCallback) => {
8192
const content = params.lines.join('\n')
8293
const tokens = getLiquidTokens(content)
8394
.filter((token) => token.kind === TokenKind.Tag)
@@ -105,10 +116,10 @@ export const liquidIfVersionTags = {
105116
},
106117
}
107118

108-
function validateIfversionConditionals(cond, possibleVersionNames) {
109-
const validateVersion = (version) => possibleVersionNames.has(version)
119+
function validateIfversionConditionals(cond: string, possibleVersionNames: Set<string>): string[] {
120+
const validateVersion = (version: string): boolean => possibleVersionNames.has(version)
110121

111-
const errors = []
122+
const errors: string[] = []
112123

113124
// Where `cond` is an array of strings, where each string may have one of the following space-separated formats:
114125
// * Length 1: `<version>` (example: `fpt`)
@@ -150,14 +161,16 @@ function validateIfversionConditionals(cond, possibleVersionNames) {
150161
if (strParts.length === 3) {
151162
const [version, operator, release] = strParts
152163
const hasSemanticVersioning = Object.values(allVersions).some(
153-
(v) => (v.hasNumberedReleases || v.internalLatestRelease) && v.shortName === version,
164+
(v) => v.hasNumberedReleases && v.shortName === version,
154165
)
155166
if (!hasSemanticVersioning) {
156167
errors.push(
157168
`Found "${version}" inside "${cond}" with a "${operator}" operator, but "${version}" does not support semantic comparisons"`,
158169
)
159170
}
160-
if (!allowedVersionOperators.includes(operator)) {
171+
// Using 'as any' because the operator is a runtime string value that we validate,
172+
// but the allowedVersionOperators array has a more specific type that TypeScript can't infer
173+
if (!allowedVersionOperators.includes(operator as any)) {
161174
errors.push(
162175
`Found a "${operator}" operator inside "${cond}", but "${operator}" is not supported`,
163176
)
@@ -187,7 +200,10 @@ function validateIfversionConditionals(cond, possibleVersionNames) {
187200

188201
// The reason this function is exported is because it's sufficiently
189202
// complex that it needs to be tested in isolation.
190-
export function validateIfversionConditionalsVersions(cond, allFeatures) {
203+
export function validateIfversionConditionalsVersions(
204+
cond: string,
205+
allFeatures: AllFeatures,
206+
): string[] {
191207
// Suppose the cond is `ghes >3.1 or some-cool-feature` we need to open
192208
// that `some-cool-feature` and if that has `{ghes:'>3.0', ghec:'*', fpt:'*'}`
193209
// then *combined* versions will be `{ghes:'>3.0', ghec:'*', fpt:'*'}`.
@@ -198,9 +214,9 @@ export function validateIfversionConditionalsVersions(cond, allFeatures) {
198214
return []
199215
}
200216

201-
const errors = []
202-
const versions = {}
203-
let hasFutureLessThan = false
217+
const errors: string[] = []
218+
const versions: Record<string, string> = {}
219+
let hasFutureLessThan: boolean = false
204220
for (const part of cond.split(/\sor\s/)) {
205221
// For example `fpt or not ghec` or `not ghes or ghec or not fpt`
206222
if (/(^|\s)not(\s|$)/.test(part)) {
@@ -223,7 +239,7 @@ export function validateIfversionConditionalsVersions(cond, allFeatures) {
223239
}
224240
}
225241

226-
const applicableVersions = []
242+
const applicableVersions: string[] = []
227243
try {
228244
applicableVersions.push(...getApplicableVersions(versions))
229245
} catch {
@@ -238,12 +254,16 @@ export function validateIfversionConditionalsVersions(cond, allFeatures) {
238254
return errors
239255
}
240256

241-
function getVersionsObject(part, allFeatures) {
242-
const versions = {}
257+
function getVersionsObject(part: string, allFeatures: AllFeatures): Record<string, string> {
258+
const versions: Record<string, string> = {}
243259
if (part in allFeatures) {
244260
for (const [shortName, version] of Object.entries(allFeatures[part].versions)) {
245-
const versionOperator =
246-
version in allFeatures ? getVersionsObject(version, allFeatures) : version
261+
// Using 'as any' for recursive getVersionsObject call because it can return either
262+
// a string or a nested Record<string, string>, but we flatten it to string for this context
263+
const versionOperator: string =
264+
version in allFeatures
265+
? (getVersionsObject(version, allFeatures) as any)
266+
: (version as string)
247267
if (shortName in versions) {
248268
versions[shortName] = lowestVersion(versionOperator, versions[shortName])
249269
} else {
@@ -254,19 +274,23 @@ function getVersionsObject(part, allFeatures) {
254274
versions[part] = '*'
255275
} else if (allShortnames.some((v) => part.startsWith(v))) {
256276
const shortNamed = allShortnames.find((v) => part.startsWith(v))
257-
const rest = part.replace(shortNamed, '').trim()
258-
versions[shortNamed] = rest
277+
if (shortNamed) {
278+
const rest = part.replace(shortNamed, '').trim()
279+
versions[shortNamed] = rest
280+
}
259281
} else {
260282
throw new Error(`The version '${part}' is neither a short version name or a feature name`)
261283
}
262284
return versions
263285
}
264286

265-
function lowestVersion(version1, version2) {
287+
function lowestVersion(version1: string, version2: string): string {
266288
if (version1 === '*' || version2 === '*') {
267289
return '*'
268290
}
269-
if (semver.lt(semver.minVersion(version1), semver.minVersion(version2))) {
291+
const min1 = semver.minVersion(version1)
292+
const min2 = semver.minVersion(version2)
293+
if (min1 && min2 && semver.lt(min1, min2)) {
270294
return version1
271295
} else {
272296
return version2

src/content-render/unified/processor.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ export function createProcessor(context: Context): UnifiedProcessor {
4040
.use(gfm)
4141
// Markdown AST below vvv
4242
.use(parseInfoString)
43-
.use(rewriteLocalLinks, context)
43+
// Using 'as any' because rewriteLocalLinks is a factory function that takes context
44+
// and returns a transformer, but TypeScript's unified plugin types don't handle this pattern
45+
.use(rewriteLocalLinks as any, context)
4446
.use(emoji)
4547
// Markdown AST above ^^^
4648
.use(remark2rehype, { allowDangerousHtml: true })
@@ -87,20 +89,28 @@ export function createProcessor(context: Context): UnifiedProcessor {
8789
}
8890

8991
export function createMarkdownOnlyProcessor(context: Context): UnifiedProcessor {
90-
return unified()
91-
.use(remarkParse)
92-
.use(gfm)
93-
.use(rewriteLocalLinks, context)
94-
.use(remarkStringify) as UnifiedProcessor
92+
return (
93+
unified()
94+
.use(remarkParse)
95+
.use(gfm)
96+
// Using 'as any' because rewriteLocalLinks is a factory function that takes context
97+
// and returns a transformer, but TypeScript's unified plugin types don't handle this pattern
98+
.use(rewriteLocalLinks as any, context)
99+
.use(remarkStringify) as UnifiedProcessor
100+
)
95101
}
96102

97103
export function createMinimalProcessor(context: Context): UnifiedProcessor {
98-
return unified()
99-
.use(remarkParse)
100-
.use(gfm)
101-
.use(rewriteLocalLinks, context)
102-
.use(remark2rehype, { allowDangerousHtml: true })
103-
.use(slug)
104-
.use(raw)
105-
.use(html) as UnifiedProcessor
104+
return (
105+
unified()
106+
.use(remarkParse)
107+
.use(gfm)
108+
// Using 'as any' because rewriteLocalLinks is a factory function that takes context
109+
// and returns a transformer, but TypeScript's unified plugin types don't handle this pattern
110+
.use(rewriteLocalLinks as any, context)
111+
.use(remark2rehype, { allowDangerousHtml: true })
112+
.use(slug)
113+
.use(raw)
114+
.use(html) as UnifiedProcessor
115+
)
106116
}

0 commit comments

Comments
 (0)