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
22 changes: 0 additions & 22 deletions src/content-linter/lib/init-test.js

This file was deleted.

34 changes: 34 additions & 0 deletions src/content-linter/lib/init-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import markdownlint from 'markdownlint'
import type { Configuration, Options } from 'markdownlint'

import { defaultConfig } from '@/content-linter/lib/default-markdownlint-options'
import type { Rule } from '@/content-linter/types'

interface RunRuleOptions {
strings?: { [key: string]: string }
files?: string[]
ruleConfig?: boolean | object
markdownlintOptions?: Partial<Options>
}

export async function runRule(
module: Rule,
{ strings, files, ruleConfig, markdownlintOptions = {} }: RunRuleOptions,
) {
if ((!strings && !files) || (strings && files))
throw new Error('Must provide either Markdown strings or files to run a rule')

const testConfig: Configuration = {
[module.names[0]]: ruleConfig || true,
}

const testOptions: Partial<Options> = {
customRules: [module as any],
config: { ...defaultConfig, ...testConfig },
}
if (strings) testOptions.strings = strings
if (files) testOptions.files = files

const options: Options = { ...markdownlintOptions, ...testOptions }
return await markdownlint.promises.markdownlint(options)
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations
import { addError, filterTokens } from 'markdownlint-rule-helpers'

import { getFrontmatter } from '../helpers/utils'
import { getFrontmatter } from '@/content-linter/lib/helpers/utils'
import type { RuleParams, RuleErrorCallback, MarkdownToken } from '@/content-linter/types'

interface Frontmatter {
layout?: string
[key: string]: any
}

export const codeAnnotations = {
names: ['GHD007', 'code-annotations'],
description:
'Code annotations defined in Markdown must contain a specific layout frontmatter property',
tags: ['code', 'feature', 'annotate', 'frontmatter'],
parser: 'markdownit',
function: (params, onError) => {
filterTokens(params, 'fence', (token) => {
if (!token.info.includes('annotate')) return
const fm = getFrontmatter(params.frontMatterLines)
parser: 'markdownit' as const,
function: (params: RuleParams, onError: RuleErrorCallback) => {
filterTokens(params, 'fence', (token: MarkdownToken) => {
if (!token.info?.includes('annotate')) return
const fm: Frontmatter | null = getFrontmatter(params.frontMatterLines)
if (!fm || (fm.layout && fm.layout === 'inline')) return

addError(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { addFixErrorDetail, forEachInlineChild } from '../helpers/utils'
import { addFixErrorDetail, forEachInlineChild } from '@/content-linter/lib/helpers/utils'
import type { RuleParams, RuleErrorCallback, MarkdownToken } from '@/content-linter/types'

export const imageFileKebabCase = {
names: ['GHD004', 'image-file-kebab-case'],
description: 'Image file names must use kebab-case',
tags: ['images'],
parser: 'markdownit',
function: (params, onError) => {
forEachInlineChild(params, 'image', async function forToken(token) {
const imageFileName = token.attrs[0][1].split('/').pop().split('.')[0]
parser: 'markdownit' as const,
function: (params: RuleParams, onError: RuleErrorCallback) => {
forEachInlineChild(params, 'image', async function forToken(token: MarkdownToken) {
const imageFileName = token.attrs?.[0]?.[1]?.split('/').pop()?.split('.')[0] || ''
const nonKebabRegex = /([A-Z]|_)/
const suggestedFileName = imageFileName.toLowerCase().replace(/_/g, '-')
if (imageFileName.match(nonKebabRegex)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations
import { addError } from 'markdownlint-rule-helpers'

import { forEachInlineChild } from '../helpers/utils'
import { forEachInlineChild } from '@/content-linter/lib/helpers/utils'
import type { RuleParams, RuleErrorCallback, MarkdownToken } from '@/content-linter/types'

export const imageNoGif = {
names: ['GHD036', 'image-no-gif'],
description:
'Image must not be a gif, styleguide reference: contributing/style-guide-and-content-model/style-guide.md#images',
tags: ['images'],
parser: 'markdownit',
function: (params, onError) => {
forEachInlineChild(params, 'image', function forToken(token) {
const imageFileName = token.attrs[0][1]
if (imageFileName.endsWith('.gif')) {
parser: 'markdownit' as const,
function: (params: RuleParams, onError: RuleErrorCallback) => {
forEachInlineChild(params, 'image', function forToken(token: MarkdownToken) {
const imageFileName = token.attrs?.[0]?.[1]
if (imageFileName && imageFileName.endsWith('.gif')) {
addError(
onError,
token.lineNumber,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { describe, expect, test } from 'vitest'

import { runRule } from '../../lib/init-test'
import { hardcodedDataVariable } from '../../lib/linting-rules/hardcoded-data-variable'
import { runRule } from '@/content-linter/lib/init-test'
import { hardcodedDataVariable } from '@/content-linter/lib/linting-rules/hardcoded-data-variable'

describe(hardcodedDataVariable.names.join(' - '), () => {
test('Using hardcoded personal access token string causes error', async () => {
const markdown = [
test('Using hardcoded personal access token string causes error', async (): Promise<void> => {
const markdown: string = [
'Hello personal access token for apps.',
'A Personal access token for apps.',
'Lots of PERSONAL ACCESS TOKENS for apps.',
'access tokens for apps.',
'personal access token and a Personal Access Tokens',
].join('\n')
const result = await runRule(hardcodedDataVariable, { strings: { markdown } })
const result = await runRule(hardcodedDataVariable, {
strings: { markdown },
files: undefined,
ruleConfig: true,
})
const errors = result.markdown
expect(errors.length).toBe(5)
expect(errors[errors.length - 2].lineNumber).toBe(5)
Expand Down
37 changes: 36 additions & 1 deletion src/content-linter/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,44 @@
// Interfaces for content linter rule parameters and callbacks
export interface MarkdownToken {
type: string
tag?: string
attrs?: [string, string][]
content?: string
info?: string
lineNumber: number
line: string
children?: MarkdownToken[]
}

export interface RuleParams {
name: string // file path
lines: string[] // array of lines from the file
frontMatterLines: string[] // array of frontmatter lines
tokens?: MarkdownToken[] // markdown tokens (when using markdownit parser)
config?: {
[key: string]: any // rule-specific configuration
}
}

export interface RuleErrorCallback {
(
lineNumber: number,
detail?: string,
context?: string,
range?: [number, number],
fixInfo?: any,
): void
}

export type Rule = {
names: string[]
description: string
tags: string[]
information?: URL
function: Function[]
parser?: 'markdownit' | 'micromark' | 'none'
asynchronous?: boolean
severity?: string
function: (params: RuleParams, onError: RuleErrorCallback) => void | Promise<void>
}

type RuleDetail = Rule & {
Expand Down
23 changes: 0 additions & 23 deletions src/content-render/unified/heading-links.js

This file was deleted.

34 changes: 34 additions & 0 deletions src/content-render/unified/heading-links.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { visit } from 'unist-util-visit'
import { h } from 'hastscript'

interface HeadingNode {
type: 'element'
tagName: string
properties: {
id?: string
tabIndex?: number
[key: string]: any
}
children: any[]
}

const matcher = (node: any): node is HeadingNode =>
node.type === 'element' &&
['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName) &&
node.properties?.id

export default function headingLinks() {
return (tree: any) => {
visit(tree, matcher, (node: HeadingNode) => {
const { id } = node.properties
const text = node.children
node.properties.tabIndex = -1
node.children = [
h('a', { className: 'heading-link', href: `#${id}` }, [
...text,
h('span', { className: 'heading-link-symbol', ariaHidden: 'true' }),
]),
]
})
}
}
22 changes: 0 additions & 22 deletions src/content-render/unified/index.js

This file was deleted.

26 changes: 26 additions & 0 deletions src/content-render/unified/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { fastTextOnly } from '@/content-render/unified/text-only'
import { createProcessor, createMarkdownOnlyProcessor } from '@/content-render/unified/processor'

interface RenderOptions {
textOnly?: boolean
}

export async function renderUnified(template: string, context: any, options: RenderOptions = {}) {
const processor = createProcessor(context)
const vFile = await processor.process(template)
let html = vFile.toString()

if (options.textOnly) {
html = fastTextOnly(html)
}

return html.trim()
}

export async function renderMarkdown(template: string, context: any) {
const processor = createMarkdownOnlyProcessor(context)
const vFile = await processor.process(template)
const markdown = vFile.toString()

return markdown.trim()
}
23 changes: 0 additions & 23 deletions src/frame/lib/find-page.js

This file was deleted.

31 changes: 31 additions & 0 deletions src/frame/lib/find-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { getLanguageCode } from '@/frame/lib/patterns'
import getRedirect from '@/redirects/lib/get-redirect'
import type { Context, Page } from '@/types'

export default function findPage(
href: string,
pages: Record<string, Page> | undefined,
redirects: Record<string, string> | undefined,
): Page | undefined {
if (Array.isArray(pages)) throw new Error("'pages' is not supposed to be an array")
if (pages === undefined) return undefined

// remove any fragments
href = new URL(href, 'http://example.com').pathname

const redirectsContext: Context = { redirects: redirects || {}, pages }

// find the page
const redirectedHref = getRedirect(href, redirectsContext)
const page = pages[href] || (redirectedHref ? pages[redirectedHref] : undefined)
if (page) return page

// get the current language
const languageMatch = href.match(getLanguageCode)
const currentLang = getLanguageCode.test(href) && languageMatch ? languageMatch[1] : 'en'

// try to fall back to English if the translated page can't be found
const englishHref = href.replace(`/${currentLang}/`, '/en/')
const redirectedEnglishHref = getRedirect(englishHref, redirectsContext)
return pages[englishHref] || (redirectedEnglishHref ? pages[redirectedEnglishHref] : undefined)
}
Loading
Loading