diff --git a/.gitattributes b/.gitattributes index d8b0880..57bcb04 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,8 @@ * text=auto -/test/fixtures/**/* text eol=lf \ No newline at end of file +/test/results/** linguist-generated +/test/fixtures/** linguist-generated text eol=lf + +*.lockb binary diff=lockb +biome.json linguist-language=JSON-with-Comments +.vscode/*.json linguist-language=JSON-with-Comments diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index de0c2ce..82b0ef7 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -29,7 +29,7 @@ jobs: bun-version: 'latest' - name: 'Setup Biome' - uses: biomejs/setup-biome@v1 + uses: biomejs/setup-biome@v2 with: version: 'latest' diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 72a3b16..09b30f2 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,9 +1,9 @@ { "recommendations": [ - // "biomejs.biome", - "orta.vscode-twoslash-queries", - "bradlc.vscode-tailwindcss" + "bradlc.vscode-tailwindcss", + "tobermory.es6-string-html", + "orta.vscode-twoslash-queries" ], "unwantedRecommendations": [ // we use biome for linting and formatting diff --git a/.vscode/settings.json b/.vscode/settings.json index 10f3196..7995805 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,17 +4,15 @@ "typescript.preferences.importModuleSpecifier": "non-relative", "typescript.inlayHints.parameterNames.enabled": "all", "typescript.preferences.preferTypeOnlyAutoImports": true, - "files.associations": { - // tells vscode that the file can take comments - "*.css": "tailwindcss", - "biome.json": "jsonc" - }, "editor.quickSuggestions": { "strings": "on" }, "tailwindCSS.includeLanguages": { "plaintext": "html" }, + "biome.enabled": true, + // "biome.rename": true, + // "biome.searchInPath": true, // format & lint settings "editor.formatOnSave": true, "editor.codeActionsOnSave": { @@ -29,6 +27,12 @@ "[javascriptreact]": { "editor.defaultFormatter": "biomejs.biome" }, "[json]": { "editor.defaultFormatter": "biomejs.biome" }, "[jsonc]": { "editor.defaultFormatter": "biomejs.biome" }, + "[astro]": { "editor.defaultFormatter": "biomejs.biome" }, + "[svelte]": { "editor.defaultFormatter": "biomejs.biome" }, + "files.associations": { + "biome.json": "jsonc", + "*.css": "tailwindcss" + }, "search.exclude": { "_": true, "**/dist": true, diff --git a/biome.json b/biome.json index aba704f..6c7a583 100644 --- a/biome.json +++ b/biome.json @@ -3,19 +3,22 @@ "vcs": { "root": ".", "enabled": true, - "clientKind": "git" + "clientKind": "git", + "useIgnoreFile": true }, "files": { "include": [ - "./**/*.ts", - "./**/*.js", - "./**/*.cjs", - "./**/*.mjs", - "./**/*.jsx", - "./**/*.tsx", - "./**/*.d.ts", - "./**/*.json", - "./**/*.jsonc" + "*.ts", + "*.js", + "*.cjs", + "*.mjs", + "*.jsx", + "*.tsx", + "*.d.ts", + "*.json", + "*.jsonc", + "*.astro", + "*.svelte" ], "ignore": ["node_modules", "dist", "_"], "ignoreUnknown": true @@ -23,6 +26,18 @@ "organizeImports": { "enabled": false }, + "css": { + "formatter": { + "enabled": true, + "indentWidth": 2, + "lineWidth": 100, + "quoteStyle": "double", + "indentStyle": "space" + }, + "parser": { + "allowWrongLineComments": true + } + }, "formatter": { "enabled": true, "lineWidth": 100, @@ -30,31 +45,54 @@ "indentStyle": "space", "formatWithErrors": true, "include": [ - "./**/*.ts", - "./**/*.js", - "./**/*.cjs", - "./**/*.mjs", - "./**/*.jsx", - "./**/*.tsx", - "./**/*.d.ts", - "./**/*.json", - "./**/*.jsonc" + "*.ts", + "*.js", + "*.cjs", + "*.mjs", + "*.jsx", + "*.tsx", + "*.d.ts", + "*.json", + "*.jsonc", + "*.astro", + "*.svelte" ] }, "linter": { + "include": [ + "*.ts", + "*.js", + "*.cjs", + "*.mjs", + "*.jsx", + "*.tsx", + "*.d.ts", + "*.json", + "*.jsonc", + "*.astro", + "*.svelte" + ], "enabled": true, "rules": { "all": true, "style": { + "noDefaultExport": "off", + "useFragmentSyntax": "off", "useBlockStatements": "off", + "useNamingConvention": "off", + "useShorthandArrayType": "off", "useSelfClosingElements": "off", - "noUnusedTemplateLiteral": "off" + "noUnusedTemplateLiteral": "off", + "useConsistentArrayType": { "level": "warn", "options": { "syntax": "generic" } } }, "a11y": { "noSvgWithoutTitle": "off" }, - "nursery": { "noUnusedImports": "off" }, "performance": { "noAccumulatingSpread": "off" }, "correctness": { "noUndeclaredVariables": "off" }, + "nursery": { + "noNodejsModules": "off" + }, "suspicious": { + "useAwait": "off", "noExplicitAny": "off", "noEmptyInterface": "off", "noConfusingVoidType": "off" @@ -79,6 +117,7 @@ } }, "javascript": { + "globals": ["NodeJS", "Astro", "NEXT_TELEMETRY_DISABLED"], "parser": { "unsafeParameterDecoratorsEnabled": true }, @@ -91,8 +130,54 @@ "trailingComma": "all", "semicolons": "always", "jsxQuoteStyle": "double", - "quoteProperties": "asNeeded", - "arrowParentheses": "always" + "arrowParentheses": "always", + "quoteProperties": "asNeeded" + } + }, + "overrides": [ + { + "include": ["test", "scripts"], + "linter": { + "rules": { + "suspicious": { + "noConsoleLog": "off" + } + } + } + }, + { + "include": ["*.astro"], + "linter": { + "rules": { + "correctness": { + "noUnusedImports": "off" + }, + "style": { + "useImportType": "off", + "useFilenamingConvention": "off" + } + } + } + }, + { + "include": ["*.svelte"], + "linter": { + "rules": { + "correctness": { + "noUnusedLabels": "off", + "noUnusedImports": "off", + "useHookAtTopLevel": "off" + }, + "style": { + "useConst": "off", + "useImportType": "off", + "useFilenamingConvention": "off" + }, + "suspicious": { + "noConfusingLabels": "off" + } + } + } } - } + ] } diff --git a/bun.lockb b/bun.lockb index 0468cdc..c482220 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/examples/astro/astro.config.ts b/examples/astro/astro.config.ts index 56b2265..a0e14ab 100644 --- a/examples/astro/astro.config.ts +++ b/examples/astro/astro.config.ts @@ -7,14 +7,7 @@ import moonlightTheme from './public/theme/moonlight-ii.json'; export default defineConfig({ markdown: { syntaxHighlight: false, - rehypePlugins: [ - [ - rehypePrettyCode, - { - theme: moonlightTheme, - }, - ], - ], + rehypePlugins: [[rehypePrettyCode, { theme: moonlightTheme }]], }, integrations: [mdx(), tailwind()], }); diff --git a/examples/astro/bun.lockb b/examples/astro/bun.lockb index 6838ebf..9c441fb 100755 Binary files a/examples/astro/bun.lockb and b/examples/astro/bun.lockb differ diff --git a/examples/astro/package.json b/examples/astro/package.json index 69e8ad2..f6820e3 100644 --- a/examples/astro/package.json +++ b/examples/astro/package.json @@ -10,16 +10,16 @@ "astro": "astro" }, "dependencies": { - "@astrojs/mdx": "^2.1.1", + "@astrojs/mdx": "^2.2.1", "@astrojs/tailwind": "^5.1.0", - "astro": "^4.4.4", + "astro": "^4.5.9", "rehype-pretty-code": "^0.13.0", - "shiki": "^1.1.7", + "shiki": "^1.2.0", "tailwindcss": "^3.4.1" }, "devDependencies": { - "@types/node": "^20.11.20", - "@astrojs/check": "^0.5.5", - "typescript": "^5.3.3" + "@astrojs/check": "^0.5.10", + "@types/node": "^20.11.30", + "typescript": "^5.4.3" } } diff --git a/examples/astro/tsconfig.json b/examples/astro/tsconfig.json index 894364a..57c12dc 100644 --- a/examples/astro/tsconfig.json +++ b/examples/astro/tsconfig.json @@ -5,5 +5,7 @@ "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "Bundler" - } + }, + "include": ["src"], + "files": ["astro.config.ts", "tailwind.config.ts"] } diff --git a/package.json b/package.json index 66f014c..08a84fc 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,7 @@ }, "./package.json": "./package.json" }, - "files": [ - "dist", - "package.json" - ], + "files": ["dist", "package.json"], "scripts": { "build": "tsup --config=tsup.config.ts", "test": "vitest --run", @@ -31,7 +28,7 @@ "release": "bumpp package.json --commit --push --tag", "prebuild": "rm -rf dist", "prepublishOnly": "NODE_ENV='production' bun run build", - "check-package": "bunx publint@latest --strict && bunx @arethetypeswrong/cli --pack --ignore-rules cjs-resolves-to-esm" + "check-package": "bunx publint@latest --strict && bunx attw --pack --ignore-rules cjs-resolves-to-esm" }, "dependencies": { "@types/hast": "^3.0.4", @@ -42,22 +39,24 @@ "unist-util-visit": "^5.0.0" }, "devDependencies": { - "@biomejs/biome": "^1.5.3", - "@types/bun": "^1.0.7", - "@types/node": "^20.11.20", - "bumpp": "^9.3.0", - "bun": "^1.0.29", + "@arethetypeswrong/cli": "^0.15.2", + "@biomejs/biome": "^1.6.3", + "@shikijs/transformers": "^1.2.0", + "@types/bun": "^1.0.10", + "@types/node": "^20.11.30", + "bumpp": "^9.4.0", + "bun": "^1.0.35", "hast-util-to-html": "^9.0.0", "husky": "^9.0.11", "mdast-util-to-hast": "^13.1.0", "prettier": "^3.2.5", "remark": "^15.0.1", + "shiki": "^1.2.0", "tsup": "^8.0.2", - "typescript": "^5.3.3", - "vite": "^5.1.4", - "vitest": "^1.3.1", - "@shikijs/transformers": "^1.1.7", - "shiki": "^1.1.7" + "tsx": "^4.7.1", + "typescript": "^5.4.3", + "vite": "^5.2.6", + "vitest": "^1.4.0" }, "peerDependencies": { "shiki": "^1.0.0" @@ -66,9 +65,7 @@ "node": ">=18" }, "repository": "github:rehype-pretty/rehype-pretty-code", - "browserslist": [ - "node 18" - ], + "browserslist": ["node 18"], "author": "https://github.com/atomiks", "license": "MIT" } diff --git a/scripts/workspace.ts b/scripts/workspace.ts index 446d196..b366a60 100644 --- a/scripts/workspace.ts +++ b/scripts/workspace.ts @@ -36,7 +36,7 @@ else { await file(join(import.meta.dir, '../package.json')).text(), ); - const workspaces = packageJson.workspaces as string[]; + const workspaces = packageJson.workspaces as Array; for (const w of workspaces) { if (w === excluded) continue; await runScriptForWorkspace(w); diff --git a/src/chars/charsHighlighter.ts b/src/chars/charsHighlighter.ts index 1fdeb39..ba3c1f0 100644 --- a/src/chars/charsHighlighter.ts +++ b/src/chars/charsHighlighter.ts @@ -16,7 +16,7 @@ import { isElement } from '../utils'; */ export function charsHighlighter( element: Element, - charsList: string[], + charsList: Array, options: CharsHighlighterOptions, onVisitHighlightedChars?: ( element: CharsElement, diff --git a/src/chars/getElementsToHighlight.ts b/src/chars/getElementsToHighlight.ts index 6ca6e01..1ea4a6b 100644 --- a/src/chars/getElementsToHighlight.ts +++ b/src/chars/getElementsToHighlight.ts @@ -16,7 +16,7 @@ export function getElementsToHighlight( let charsSoFar = ''; if (element.children) { - const elements = element.children as Element[]; + const elements = element.children as Array; for (let i = startIndex; i < elements.length; i++) { const remaining = charsSoFar ? chars.replace(charsSoFar, '') : chars; diff --git a/src/chars/splitElement.ts b/src/chars/splitElement.ts index 7db346c..9d0e591 100644 --- a/src/chars/splitElement.ts +++ b/src/chars/splitElement.ts @@ -2,12 +2,12 @@ import type { Element } from 'hast'; import { isElement, isText } from '../utils'; interface SplitElementProps { - elements: Element[]; + elements: Array; elementToWrap: Element; innerString: string; rightString: string; leftString: string; - rest: string[]; + rest: Array; nextElementContinues: boolean; index: number; ignoreChars: boolean; diff --git a/src/chars/utils.ts b/src/chars/utils.ts index f3d7334..bd2788b 100644 --- a/src/chars/utils.ts +++ b/src/chars/utils.ts @@ -9,7 +9,7 @@ export function nextElementMaybeContinuesChars({ nextIndex, remainingPart, }: { - elements: Element[]; + elements: Array; nextIndex: number; remainingPart: string; }): boolean { diff --git a/src/index.ts b/src/index.ts index 4d4d5b5..402126f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -68,7 +68,7 @@ function apply( const themeNames = getThemeNames(theme); const themeNamesString = themeNames.join(' '); - if (!isElement(pre) || !pre.properties) { + if (!(isElement(pre) && pre.properties)) { return []; } @@ -95,7 +95,7 @@ function apply( pre.properties['data-language'] = lang; pre.properties['data-theme'] = themeNamesString; - if (!isElement(code) || !code.properties) { + if (!(isElement(code) && code.properties)) { return []; } @@ -123,7 +123,7 @@ function apply( lineNumbersMaxDigits.toString().length; } - const fragments: ElementContent[] = []; + const fragments: Array = []; if (title) { const elementContent: Element = { @@ -290,7 +290,17 @@ export default function rehypePrettyCode( let codeTree: Root; - if (!isLang) { + if (isLang) { + try { + codeTree = hastParser.parse( + highlighter.codeToHtml(strippedValue, getOptions(lang)), + ); + } catch { + codeTree = hastParser.parse( + highlighter.codeToHtml(strippedValue, getOptions('plaintext')), + ); + } + } else { const themeNames = getThemeNames(theme); const isMultiTheme = typeof theme === 'object' && !isJSONTheme(theme); const themeKeys = isMultiTheme ? Object.keys(theme) : null; @@ -315,16 +325,6 @@ export default function rehypePrettyCode( `
${strippedValue}
`, ); } - } else { - try { - codeTree = hastParser.parse( - highlighter.codeToHtml(strippedValue, getOptions(lang)), - ); - } catch (e) { - codeTree = hastParser.parse( - highlighter.codeToHtml(strippedValue, getOptions('plaintext')), - ); - } } visit(codeTree, 'element', replaceLineClass); @@ -351,7 +351,7 @@ export default function rehypePrettyCode( if (!lang || lang === 'math') return; - const lineNumbers: number[] = []; + const lineNumbers: Array = []; if (meta) { const matches = meta.matchAll(/\{(.*?)\}/g); for (const match of matches) { @@ -363,8 +363,8 @@ export default function rehypePrettyCode( let lineNumbersMaxDigits = 0; const lineIdMap = new Map(); - const charsList: string[] = []; - const charsListNumbers: Array = []; + const charsList: Array = []; + const charsListNumbers: Array> = []; const charsListIdMap = new Map(); const charsMatches = meta ? [ @@ -405,7 +405,7 @@ export default function rehypePrettyCode( codeTree = hastParser.parse( highlighter.codeToHtml(strippedValue, getOptions(lang, meta)), ); - } catch (e) { + } catch { codeTree = hastParser.parse( highlighter.codeToHtml( strippedValue, diff --git a/src/types.ts b/src/types.ts index 91ec0a1..ccfd207 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,11 +8,11 @@ import type { import type { Element, Properties, Text } from 'hast'; export type LineElement = Omit & { - properties: Properties & { className?: string[] }; + properties: Properties & { className?: Array }; }; export type CharsElement = Omit & { - properties: Properties & { className?: string[] }; + properties: Properties & { className?: Array }; children: Array; }; @@ -24,7 +24,7 @@ export interface Options { keepBackground?: boolean; defaultLang?: string | { block?: string; inline?: string }; tokensMap?: Record; - transformers?: ShikiTransformer[]; + transformers?: Array; filterMetaString?(str: string): string; getHighlighter?( options: BundledHighlighterOptions, @@ -37,7 +37,7 @@ export interface Options { } export interface CharsHighlighterOptions { - ranges: Array; + ranges: Array>; idsMap: Map; counterMap: Map; } diff --git a/src/utils.ts b/src/utils.ts index d5df199..42d2dae 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -114,7 +114,7 @@ export function getLineId(lineNumber: number, meta: string) { for (const segment of segments) { const [range, id] = segment.split('#'); - if (!range || !id) continue; + if (!(range && id)) continue; const match = range.match(/\{(.*?)\}/); const capture = match?.[1]; diff --git a/test/fixtures.test.js b/test/fixtures.test.ts similarity index 65% rename from test/fixtures.test.js rename to test/fixtures.test.ts index e91fc1b..db59500 100644 --- a/test/fixtures.test.js +++ b/test/fixtures.test.ts @@ -1,61 +1,86 @@ -import { expect, describe, it, vi } from 'vitest'; -import rehypePrettyCode from '../src'; -import { lstatSync, readFileSync, readdirSync } from 'node:fs'; +import { + lstatSync, + readdirSync, + readFileSync, + type PathOrFileDescriptor, +} from 'node:fs'; +import { + type BundledTheme, + type BundledLanguage, + type HighlighterGeneric, + type BundledHighlighterOptions, + getHighlighter as shikiHighlighter, +} from 'shiki'; +import prettier from 'prettier'; +import { remark } from 'remark'; +import { join, parse } from 'node:path'; +import type { Compatible } from 'vfile'; import { toHtml } from 'hast-util-to-html'; import { toHast } from 'mdast-util-to-hast'; -import { dirname, join, parse } from 'node:path'; -import { remark } from 'remark'; -import { getHighlighter as shikiHighlighter } from 'shiki'; -import { fileURLToPath } from 'node:url'; -import qs from 'node:querystring'; +import rehypePrettyCode, { type Options } from '../src'; +import { expect, describe, it, vi, type Mock } from 'vitest'; import { transformerNotationDiff } from '@shikijs/transformers'; -import prettier from 'prettier'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const fixturesFolder = join(__dirname, 'fixtures'); -const resultsFolder = join(__dirname, 'results'); - -const getHTML = async (code, options) => { - const hAST = toHast(remark().parse(code), { allowDangerousHtml: true }); - await rehypePrettyCode(options)(hAST); - return toHtml(hAST, { allowDangerousHtml: true }); +const resultsFolder = join(import.meta.dirname, 'results'); +const fixturesFolder = join(import.meta.dirname, 'fixtures'); + +const getHtml = async ( + code: Compatible | undefined, + options: Options | undefined, +) => { + const hAst = toHast(remark().parse(code), { allowDangerousHtml: true }); + // @ts-expect-error + await rehypePrettyCode(options)(hAst); + return toHtml(hAst, { allowDangerousHtml: true }); }; -const getTheme = (multiple) => { +export const parseQueryParameters = (query: string) => + Object.fromEntries(new URLSearchParams(query).entries()); + +const getTheme = (multiple: boolean): Options['theme'] => { return multiple ? { dark: 'github-dark', light: 'github-light' } : 'github-dark'; }; -const isMultipleThemeTest = (fixtureName) => { +const isMultipleThemeTest = (fixtureName: string) => { return fixtureName.toLowerCase().includes('multipletheme'); }; // To add a test, create a markdown file in the fixtures folder -const runFixture = async (fixture, fixtureName, getHighlighter) => { +const runFixture = async ( + fixture: PathOrFileDescriptor, + fixtureName: string, + getHighlighter: Mock< + [ + options?: + | BundledHighlighterOptions + | undefined, + ], + Promise> + >, +) => { const testName = parse(fixtureName).name; - const resultHTMLName = `${testName}.html`; - const resultHTMLPath = join(resultsFolder, resultHTMLName); + const resultHtmlName = `${testName}.html`; + const resultHtmlPath = join(resultsFolder, resultHtmlName); const code = readFileSync(fixture, 'utf8'); - const html = await getHTML(code, { - keepBackground: !resultHTMLName.includes('keepBackground'), + const html = await getHtml(code, { + keepBackground: !resultHtmlName.includes('keepBackground'), defaultLang: (() => { if (testName === 'no-highlighting') { return undefined; } - const lang = testName.split('.')[1]; + const [, lang] = testName.split('.'); if (!lang) { return undefined; } if (lang === 'js') { return 'js'; } - return qs.parse(lang); + return parseQueryParameters(lang); })(), filterMetaString: (string) => string?.replace(/filename=".*"/, ''), theme: getTheme(isMultipleThemeTest(testName)), @@ -66,8 +91,13 @@ const runFixture = async (fixture, fixtureName, getHighlighter) => { node.properties.className = ['word']; if (id) { - const textColor = { a: 'pink', b: 'cyan', c: 'lightblue', id: 'white' }; - const backgroundColor = { + const textColor: Record = { + a: 'pink', + b: 'cyan', + c: 'lightblue', + id: 'white', + }; + const backgroundColor: Record = { a: 'rgba(255, 100, 200, 0.35)', b: 'rgba(0, 255, 100, 0.25)', c: 'rgba(100, 200, 255, 0.25)', @@ -79,8 +109,8 @@ const runFixture = async (fixture, fixtureName, getHighlighter) => { `; } }, - onVisitLine(node) { - node; + onVisitLine(_node) { + _node; }, onVisitTitle(node) { node.properties.style = 'font-weight: bold;'; @@ -93,7 +123,7 @@ const runFixture = async (fixture, fixtureName, getHighlighter) => { }); const htmlString = await prettier.format(html, { parser: 'html' }); - return { htmlString, resultHTMLPath }; + return { htmlString, resultHtmlPath }; }; describe('Single theme', () => { @@ -108,13 +138,13 @@ describe('Single theme', () => { } it(`Fixture: ${fixtureName}`, async () => { - const { htmlString, resultHTMLPath } = await runFixture( + const { htmlString, resultHtmlPath } = await runFixture( fixture, fixtureName, getHighlighter, ); - expect(defaultStyle + htmlString).toMatchFileSnapshot(resultHTMLPath); + expect(defaultStyle + htmlString).toMatchFileSnapshot(resultHtmlPath); }); }); }); @@ -131,28 +161,28 @@ describe('Multiple theme', () => { } it(`Fixture: ${fixtureName}`, async () => { - const { htmlString, resultHTMLPath } = await runFixture( + const { htmlString, resultHtmlPath } = await runFixture( fixture, fixtureName, getHighlighter, ); - expect(defaultStyle + htmlString).toMatchFileSnapshot(resultHTMLPath); + expect(defaultStyle + htmlString).toMatchFileSnapshot(resultHtmlPath); }); }); }); it("highlighter caches don't overwrite each other", async () => { const [html1, html2] = await Promise.all([ - getHTML('`[1, 2, 3]{:js}`', { theme: 'github-light' }), - getHTML('`[1, 2, 3]{:js}`', { theme: 'github-dark' }), + getHtml('`[1, 2, 3]{:js}`', { theme: 'github-light' }), + getHtml('`[1, 2, 3]{:js}`', { theme: 'github-dark' }), ]); // both highlighters are being cached under the same key, but in separate caches, // that's what we're testing here by asserting that they yield different results expect(html1).not.toBe(html2); }); -const defaultStyle = ` +const defaultStyle = /* html */ `