Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion content/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ Links to docs in the `docs-internal` repository must start with a product ID (li

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`.

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).
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).

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ Links to docs in the `docs` repository must start with a product ID (like `/acti

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`.

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).
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).

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import semver from 'semver'
import { TokenKind } from 'liquidjs'
// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations
import { addError } from 'markdownlint-rule-helpers'

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

const allShortnames = Object.keys(allVersionShortnames)
const getAllPossibleVersionNames = memoize(() => {
interface Feature {
versions: Record<string, string>
[key: string]: any
}

type AllFeatures = Record<string, Feature>

const allShortnames: string[] = Object.keys(allVersionShortnames)
const getAllPossibleVersionNames = memoize((): Set<string> => {
// This function might appear "slow" but it's wrapped in a memoizer
// so it's only every executed once for all files that the
// Liquid linting rule functions on.
Expand All @@ -21,20 +30,22 @@ const getAllPossibleVersionNames = memoize(() => {
return new Set([...Object.keys(getAllFeatures()), ...allShortnames])
})

const getAllFeatures = memoize(() => getDeepDataByLanguage('features', 'en', process.env.ROOT))
const getAllFeatures = memoize(
(): AllFeatures => getDeepDataByLanguage('features', 'en', process.env.ROOT) as AllFeatures,
)

const allVersionNames = Object.keys(allVersions)
const allVersionNames: string[] = Object.keys(allVersions)

function isAllVersions(versions) {
function isAllVersions(versions: string[]): boolean {
if (versions.length === allVersionNames.length) {
return versions.every((version) => allVersionNames.includes(version))
}
return false
}

function memoize(func) {
let cached = null
return () => {
function memoize<T>(func: () => T): () => T {
let cached: T | null = null
return (): T => {
if (!cached) {
cached = func()
}
Expand All @@ -47,14 +58,14 @@ export const liquidIfTags = {
description:
'Liquid `ifversion` tags should be used instead of `if` tags when the argument is a valid version',
tags: ['liquid', 'versioning'],
function: (params, onError) => {
function: (params: RuleParams, onError: RuleErrorCallback) => {
const content = params.lines.join('\n')

const tokens = getLiquidTokens(content).filter(
(token) =>
token.kind === TokenKind.Tag &&
token.name === 'if' &&
token.args.split(/\s+/).some((arg) => getAllPossibleVersionNames().has(arg)),
token.args.split(/\s+/).some((arg: string) => getAllPossibleVersionNames().has(arg)),
)

for (const token of tokens) {
Expand All @@ -77,7 +88,7 @@ export const liquidIfVersionTags = {
names: ['GHD020', 'liquid-ifversion-tags'],
description: 'Liquid `ifversion` tags should contain valid version names as arguments',
tags: ['liquid', 'versioning'],
function: (params, onError) => {
function: (params: RuleParams, onError: RuleErrorCallback) => {
const content = params.lines.join('\n')
const tokens = getLiquidTokens(content)
.filter((token) => token.kind === TokenKind.Tag)
Expand Down Expand Up @@ -105,10 +116,10 @@ export const liquidIfVersionTags = {
},
}

function validateIfversionConditionals(cond, possibleVersionNames) {
const validateVersion = (version) => possibleVersionNames.has(version)
function validateIfversionConditionals(cond: string, possibleVersionNames: Set<string>): string[] {
const validateVersion = (version: string): boolean => possibleVersionNames.has(version)

const errors = []
const errors: string[] = []

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

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

const errors = []
const versions = {}
let hasFutureLessThan = false
const errors: string[] = []
const versions: Record<string, string> = {}
let hasFutureLessThan: boolean = false
for (const part of cond.split(/\sor\s/)) {
// For example `fpt or not ghec` or `not ghes or ghec or not fpt`
if (/(^|\s)not(\s|$)/.test(part)) {
Expand All @@ -223,7 +239,7 @@ export function validateIfversionConditionalsVersions(cond, allFeatures) {
}
}

const applicableVersions = []
const applicableVersions: string[] = []
try {
applicableVersions.push(...getApplicableVersions(versions))
} catch {
Expand All @@ -238,12 +254,16 @@ export function validateIfversionConditionalsVersions(cond, allFeatures) {
return errors
}

function getVersionsObject(part, allFeatures) {
const versions = {}
function getVersionsObject(part: string, allFeatures: AllFeatures): Record<string, string> {
const versions: Record<string, string> = {}
if (part in allFeatures) {
for (const [shortName, version] of Object.entries(allFeatures[part].versions)) {
const versionOperator =
version in allFeatures ? getVersionsObject(version, allFeatures) : version
// Using 'as any' for recursive getVersionsObject call because it can return either
// a string or a nested Record<string, string>, but we flatten it to string for this context
const versionOperator: string =
version in allFeatures
? (getVersionsObject(version, allFeatures) as any)
: (version as string)
if (shortName in versions) {
versions[shortName] = lowestVersion(versionOperator, versions[shortName])
} else {
Expand All @@ -254,19 +274,23 @@ function getVersionsObject(part, allFeatures) {
versions[part] = '*'
} else if (allShortnames.some((v) => part.startsWith(v))) {
const shortNamed = allShortnames.find((v) => part.startsWith(v))
const rest = part.replace(shortNamed, '').trim()
versions[shortNamed] = rest
if (shortNamed) {
const rest = part.replace(shortNamed, '').trim()
versions[shortNamed] = rest
}
} else {
throw new Error(`The version '${part}' is neither a short version name or a feature name`)
}
return versions
}

function lowestVersion(version1, version2) {
function lowestVersion(version1: string, version2: string): string {
if (version1 === '*' || version2 === '*') {
return '*'
}
if (semver.lt(semver.minVersion(version1), semver.minVersion(version2))) {
const min1 = semver.minVersion(version1)
const min2 = semver.minVersion(version2)
if (min1 && min2 && semver.lt(min1, min2)) {
return version1
} else {
return version2
Expand Down
38 changes: 24 additions & 14 deletions src/content-render/unified/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ export function createProcessor(context: Context): UnifiedProcessor {
.use(gfm)
// Markdown AST below vvv
.use(parseInfoString)
.use(rewriteLocalLinks, context)
// Using 'as any' because rewriteLocalLinks is a factory function that takes context
// and returns a transformer, but TypeScript's unified plugin types don't handle this pattern
.use(rewriteLocalLinks as any, context)
.use(emoji)
// Markdown AST above ^^^
.use(remark2rehype, { allowDangerousHtml: true })
Expand Down Expand Up @@ -87,20 +89,28 @@ export function createProcessor(context: Context): UnifiedProcessor {
}

export function createMarkdownOnlyProcessor(context: Context): UnifiedProcessor {
return unified()
.use(remarkParse)
.use(gfm)
.use(rewriteLocalLinks, context)
.use(remarkStringify) as UnifiedProcessor
return (
unified()
.use(remarkParse)
.use(gfm)
// Using 'as any' because rewriteLocalLinks is a factory function that takes context
// and returns a transformer, but TypeScript's unified plugin types don't handle this pattern
.use(rewriteLocalLinks as any, context)
.use(remarkStringify) as UnifiedProcessor
)
}

export function createMinimalProcessor(context: Context): UnifiedProcessor {
return unified()
.use(remarkParse)
.use(gfm)
.use(rewriteLocalLinks, context)
.use(remark2rehype, { allowDangerousHtml: true })
.use(slug)
.use(raw)
.use(html) as UnifiedProcessor
return (
unified()
.use(remarkParse)
.use(gfm)
// Using 'as any' because rewriteLocalLinks is a factory function that takes context
// and returns a transformer, but TypeScript's unified plugin types don't handle this pattern
.use(rewriteLocalLinks as any, context)
.use(remark2rehype, { allowDangerousHtml: true })
.use(slug)
.use(raw)
.use(html) as UnifiedProcessor
)
}
Loading
Loading