From 66b2af3f301b18bc02d4502812d6eaca10a271a1 Mon Sep 17 00:00:00 2001 From: JSer Date: Thu, 11 Apr 2024 22:15:01 +0900 Subject: [PATCH] support @ data close #65 --- examples/web/app/page.tsx | 17 +++++ .../web/components/CodePreview/sanitize.ts | 1 + examples/web/components/Playground.tsx | 16 +++++ .../__snapshots__/parse.test.ts.snap | 53 ++++++++++++++++ .../src/__tests__/parse.test.ts | 3 + .../shaku-code-annotate-core/src/parser.ts | 44 ++++++++++++- .../__snapshots__/codeToHtml.test.ts.snap | 14 +++++ .../src/__tests__/codeToHtml.test.ts | 11 ++++ .../src/codeToShakuHtml.ts | 23 ++++++- .../src/defaultCode.ts | 4 ++ .../src/escapeHtml.ts | 8 +++ .../shaku-code-annotate-shiki/src/render.ts | 10 +-- .../__snapshots__/highlight.spec.mts.snap | 19 ++++++ .../src/__tests__/highlight.spec.mts | 18 ++++++ .../src/escapeHtml.mts | 8 +++ .../src/index.mts | 63 ++++++++++++++++++- .../src/render.mts | 10 +-- 17 files changed, 298 insertions(+), 24 deletions(-) create mode 100644 packages/shaku-code-annotate-shiki/src/escapeHtml.ts create mode 100644 packages/shaku-code-annotate-sugar-high/src/escapeHtml.mts diff --git a/examples/web/app/page.tsx b/examples/web/app/page.tsx index bb5cb42..a9def20 100644 --- a/examples/web/app/page.tsx +++ b/examples/web/app/page.tsx @@ -719,6 +719,23 @@ const Hello = "World!" lang="js" shakuEnabled /> +

Custom data attributes

+

+ You can also use @data to add custom data attributes to a + line, which could be useful if you are building something on top of + Shaku. +

+ <$.h2 $textAlign="center">Dev Tools

We got some tools to understand how Shaku works, such as{" "} diff --git a/examples/web/components/CodePreview/sanitize.ts b/examples/web/components/CodePreview/sanitize.ts index 5dfc7e9..f8f8332 100644 --- a/examples/web/components/CodePreview/sanitize.ts +++ b/examples/web/components/CodePreview/sanitize.ts @@ -44,6 +44,7 @@ export const sanitize = (html: string) => { mark: ["data-*", "class"], details: ["class"], summary: ["style"], + "*": ["data-*"], }, }); }; diff --git a/examples/web/components/Playground.tsx b/examples/web/components/Playground.tsx index 4adb979..d15a7bc 100644 --- a/examples/web/components/Playground.tsx +++ b/examples/web/components/Playground.tsx @@ -227,6 +227,22 @@ const Hello = "World!" // [you'll see this line is rendered with the custom class names!] \`\`\` +## Custom data attributes + +You can also use \`@data\` to add custom data attributes to a + line, which could be useful if you are building something on top of + Shaku. + +\`\`\`tsx annotate +// @data hello=world jser=dev ! +// @data hello=world jser=dev +// @highlight +const Hello = "World!" +// ^ +// [Open the dev console and inspect this line,] +// [you'll see this line is rendered with the custom data attributes!] +\`\`\` + ## How to Use diff --git a/packages/shaku-code-annotate-core/src/__tests__/__snapshots__/parse.test.ts.snap b/packages/shaku-code-annotate-core/src/__tests__/__snapshots__/parse.test.ts.snap index 8979726..5aff817 100644 --- a/packages/shaku-code-annotate-core/src/__tests__/__snapshots__/parse.test.ts.snap +++ b/packages/shaku-code-annotate-core/src/__tests__/__snapshots__/parse.test.ts.snap @@ -1324,6 +1324,59 @@ exports[`parseLine() can parse comment lines > @class abc-1 efg-2 h123! 1`] = ` } `; +exports[`parseLine() can parse comment lines > @data abc=123 1`] = ` +{ + "config": { + "entries": [ + { + "key": "abc", + "value": "123", + }, + ], + "isEscaped": false, + }, + "type": "DirectiveData", +} +`; + +exports[`parseLine() can parse comment lines > @data abc=123 ed1=abc 1`] = ` +{ + "config": { + "entries": [ + { + "key": "abc", + "value": "123", + }, + { + "key": "ed1", + "value": "abc", + }, + ], + "isEscaped": false, + }, + "type": "DirectiveData", +} +`; + +exports[`parseLine() can parse comment lines > @data abc=123 ed1=abc ! 1`] = ` +{ + "config": { + "entries": [ + { + "key": "abc", + "value": "123", + }, + { + "key": "ed1", + "value": "abc", + }, + ], + "isEscaped": true, + }, + "type": "DirectiveData", +} +`; + exports[`parseLine() can parse comment lines > @diff 1`] = `null`; exports[`parseLine() can parse comment lines > @diff - ^ 1`] = ` diff --git a/packages/shaku-code-annotate-core/src/__tests__/parse.test.ts b/packages/shaku-code-annotate-core/src/__tests__/parse.test.ts index 341603a..a786084 100644 --- a/packages/shaku-code-annotate-core/src/__tests__/parse.test.ts +++ b/packages/shaku-code-annotate-core/src/__tests__/parse.test.ts @@ -119,6 +119,9 @@ describe("parseLine() can parse comment lines", () => { "@class abc efg h123 ", "@class abc efg h123!", "@class abc-1 efg-2 h123!", + "@data abc=123", + "@data abc=123 ed1=abc ", + "@data abc=123 ed1=abc !", ]; for (const input of inputs) { test(input, () => { diff --git a/packages/shaku-code-annotate-core/src/parser.ts b/packages/shaku-code-annotate-core/src/parser.ts index 119cc97..32816fe 100644 --- a/packages/shaku-code-annotate-core/src/parser.ts +++ b/packages/shaku-code-annotate-core/src/parser.ts @@ -141,6 +141,17 @@ export type ShakuDirectiveClass = { const RegShakuDirectiveClass = /^(?\s*)@class\s+(?([a-zA-Z0-9 \-_]+))\s*(?!?)\s*$/; +export type ShakuDirectiveData = { + type: "DirectiveData"; + config: { + isEscaped: boolean; + entries: Array<{ key: string; value: string }>; + }; +}; + +const RegShakuDirectiveData = + /^(?\s*)@data\s+(?([a-zA-Z0-9 \-=_]+))\s*(?!?)\s*$/; + export type ShakuLine = | ShakuDirectiveUnderline | ShakuAnnotationLine @@ -151,7 +162,8 @@ export type ShakuLine = | ShakuDirectiveFocus | ShakuDirectiveHighlightInline | ShakuDirectiveDiff - | ShakuDirectiveClass; + | ShakuDirectiveClass + | ShakuDirectiveData; export const parseLine = (line: string): ShakuLine | null => { const matchShakuDirectiveUnderlineSolid = line.match( @@ -347,6 +359,32 @@ export const parseLine = (line: string): ShakuLine | null => { }; } + const matchShakuDirectiveData = line.match(RegShakuDirectiveData); + if (matchShakuDirectiveData) { + const entriesStr = matchShakuDirectiveData.groups?.entries ?? ""; + const entries = filterNonNull( + entriesStr + .trim() + .split(/\s+/) + .map((entry) => { + const segs = entry.split("="); + if (segs.length !== 2) return null; + return { + key: segs[0], + value: segs[1], + }; + }) + ); + + return { + type: "DirectiveData", + config: { + isEscaped: !!matchShakuDirectiveData.groups?.escape, + entries, + }, + }; + } + return null; }; @@ -445,3 +483,7 @@ function getCanonicalMark(mark: string) { if (mark === "^") return "end"; return mark; } + +function filterNonNull(arr: (T | null)[]): T[] { + return arr.filter((item): item is T => item !== null); +} diff --git a/packages/shaku-code-annotate-shiki/src/__tests__/__snapshots__/codeToHtml.test.ts.snap b/packages/shaku-code-annotate-shiki/src/__tests__/__snapshots__/codeToHtml.test.ts.snap index 61005a5..8b5ec09 100644 --- a/packages/shaku-code-annotate-shiki/src/__tests__/__snapshots__/codeToHtml.test.ts.snap +++ b/packages/shaku-code-annotate-shiki/src/__tests__/__snapshots__/codeToHtml.test.ts.snap @@ -28,6 +28,13 @@ exports[`codeToHtml() + raw HTML 4`] = ` } `; +exports[`codeToHtml() + raw HTML 5`] = ` +{ + "html": "

function useSomeEffect({blog}) {
useEffect(() => {
return () => {
location.href = 'https://jser.dev'
}
}, [blog])
}
", + "skipped": false, +} +`; + exports[`codeToHtml() 1`] = ` { "html": "
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
<a href="https:jser.dev">jser.dev</a>
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
}
", @@ -55,3 +62,10 @@ exports[`codeToHtml() 4`] = ` "skipped": false, } `; + +exports[`codeToHtml() 5`] = ` +{ + "html": "
function useSomeEffect({blog}) {
useEffect(() => {
return () => {
location.href = 'https://jser.dev'
}
}, [blog])
}
", + "skipped": false, +} +`; diff --git a/packages/shaku-code-annotate-shiki/src/__tests__/codeToHtml.test.ts b/packages/shaku-code-annotate-shiki/src/__tests__/codeToHtml.test.ts index 89fc0d8..eedabd8 100644 --- a/packages/shaku-code-annotate-shiki/src/__tests__/codeToHtml.test.ts +++ b/packages/shaku-code-annotate-shiki/src/__tests__/codeToHtml.test.ts @@ -54,6 +54,17 @@ function useSomeEffect({blog}) { location.href = 'https://jser.dev' } }, [blog]) +} + `, + ` +function useSomeEffect({blog}) { + useEffect(() => { + // @data a=1 + return () => { + // @data a-b-c=1-2-3 beg-1=hello-2 + location.href = 'https://jser.dev' + } + }, [blog]) } `, ]; diff --git a/packages/shaku-code-annotate-shiki/src/codeToShakuHtml.ts b/packages/shaku-code-annotate-shiki/src/codeToShakuHtml.ts index 21531ef..439501b 100644 --- a/packages/shaku-code-annotate-shiki/src/codeToShakuHtml.ts +++ b/packages/shaku-code-annotate-shiki/src/codeToShakuHtml.ts @@ -10,6 +10,7 @@ import { renderSourceLine, renderSourceLineWithInlineHighlight, } from "./render"; +import { escapeHtml } from "./escapeHtml"; type StringLiteralUnion = T | (U & {}); interface CodeToShakuHtmlOptions { @@ -94,6 +95,9 @@ export let codeToShakuHtml = function ( let diffBlock: false | "+" | "-" = false; let isFoldBlock = false; let classNamesForNextSourceLine = ""; + let dataAttrsForNextSourceLine: + | false + | Array<{ key: string; value: string }> = false; for (let i = 0; i < parsedLines.length; i++) { const line = parsedLines[i]; @@ -167,7 +171,6 @@ export let codeToShakuHtml = function ( } j += 1; } - console.log(indent); html += `
{...}`; break; } @@ -310,6 +313,10 @@ export let codeToShakuHtml = function ( classNamesForNextSourceLine = shakuLine.config.classNames; break; } + case "DirectiveData": { + dataAttrsForNextSourceLine = shakuLine.config.entries; + break; + } default: assertsNever(shakuLine); } @@ -323,12 +330,14 @@ export let codeToShakuHtml = function ( const classNames = classNamesForNextSourceLine ? " " + classNamesForNextSourceLine : ""; + const dataAttrs = dataAttrsForNextSourceLine; shouldHighlighNextSourceLine = false; shouldFocusNextSourceLine = false; shouldDimNextSourceLine = false; diffNextSourceLine = false; classNamesForNextSourceLine = ""; + dataAttrsForNextSourceLine = false; const sourceLine = line.type === "default" ? line.line : line.sourceLine; @@ -341,7 +350,17 @@ export let codeToShakuHtml = function ( ? " diff diff-delete" : ""; - const prefix = `
`; + const classString = `line${highlightClass}${dimClass}${diffClass}${classNames}`; + const dataString = dataAttrs + ? " " + + dataAttrs + .map( + ({ key, value }) => + `${escapeHtml(`data-${key}`)}="${escapeHtml(value)}"` + ) + .join(" ") + : ""; + const prefix = `
`; html += prefix; if (shakuDirectiveHighlightInline) { diff --git a/packages/shaku-code-annotate-shiki/src/defaultCode.ts b/packages/shaku-code-annotate-shiki/src/defaultCode.ts index 0867c90..8899e71 100644 --- a/packages/shaku-code-annotate-shiki/src/defaultCode.ts +++ b/packages/shaku-code-annotate-shiki/src/defaultCode.ts @@ -483,6 +483,10 @@ export default function Counter() { //[Underline and callout!] } + // @class clickable + // @data jser=dev + const blog = "https://jser.dev" + // @diff - console.log('jser.dev') return ( diff --git a/packages/shaku-code-annotate-shiki/src/escapeHtml.ts b/packages/shaku-code-annotate-shiki/src/escapeHtml.ts new file mode 100644 index 0000000..3d58a6e --- /dev/null +++ b/packages/shaku-code-annotate-shiki/src/escapeHtml.ts @@ -0,0 +1,8 @@ +export function escapeHtml(html: string) { + return html + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} diff --git a/packages/shaku-code-annotate-shiki/src/render.ts b/packages/shaku-code-annotate-shiki/src/render.ts index e3dc53a..2e230d2 100644 --- a/packages/shaku-code-annotate-shiki/src/render.ts +++ b/packages/shaku-code-annotate-shiki/src/render.ts @@ -1,5 +1,6 @@ import { IThemedToken } from "shiki"; import { ShakuDirectiveHighlightInline } from "shaku-code-annotate-core"; +import { escapeHtml } from "./escapeHtml"; export const renderMarkStart = (id?: number) => `/g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -} diff --git a/packages/shaku-code-annotate-sugar-high/src/__tests__/__snapshots__/highlight.spec.mts.snap b/packages/shaku-code-annotate-sugar-high/src/__tests__/__snapshots__/highlight.spec.mts.snap index 48240c5..2b6859e 100644 --- a/packages/shaku-code-annotate-sugar-high/src/__tests__/__snapshots__/highlight.spec.mts.snap +++ b/packages/shaku-code-annotate-sugar-high/src/__tests__/__snapshots__/highlight.spec.mts.snap @@ -54,3 +54,22 @@ exports[`highlight() 4`] = `
}
" `; + +exports[`highlight() 5`] = ` +"
+
{...}
function hello() { +
{...}
const blog = "https://jser.dev" +
return blog +
+
} +
" +`; + +exports[`highlight() 6`] = ` +"
+
function hello() { +
const blog = "https://jser.dev" +
return blog +
} +
" +`; diff --git a/packages/shaku-code-annotate-sugar-high/src/__tests__/highlight.spec.mts b/packages/shaku-code-annotate-sugar-high/src/__tests__/highlight.spec.mts index a46b78c..d566d69 100644 --- a/packages/shaku-code-annotate-sugar-high/src/__tests__/highlight.spec.mts +++ b/packages/shaku-code-annotate-sugar-high/src/__tests__/highlight.spec.mts @@ -64,6 +64,24 @@ function hello() {
} `, + ` + // @fold start + function hello() { + // @fold end + // @fold v + const blog = "https://jser.dev" + return blog + // @fold ^ + + } + `, + ` + function hello() { + // @data a-1=a-1 b-2=hello + const blog = "https://jser.dev" + return blog + } + `, ]; test("highlight()", async () => { diff --git a/packages/shaku-code-annotate-sugar-high/src/escapeHtml.mts b/packages/shaku-code-annotate-sugar-high/src/escapeHtml.mts new file mode 100644 index 0000000..3d58a6e --- /dev/null +++ b/packages/shaku-code-annotate-sugar-high/src/escapeHtml.mts @@ -0,0 +1,8 @@ +export function escapeHtml(html: string) { + return html + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} diff --git a/packages/shaku-code-annotate-sugar-high/src/index.mts b/packages/shaku-code-annotate-sugar-high/src/index.mts index b982d7f..3f7510c 100644 --- a/packages/shaku-code-annotate-sugar-high/src/index.mts +++ b/packages/shaku-code-annotate-sugar-high/src/index.mts @@ -9,6 +9,7 @@ import { renderSourceLineWithInlineHighlight, } from "./render.mjs"; import { Token } from "./types.mjs"; +import { escapeHtml } from "./escapeHtml.mjs"; // @ts-ignore // this type is missing from sugar-high @@ -34,7 +35,11 @@ export function highlight(code: string) { } = null; let diffNextSourceLine: false | "+" | "-" = false; let diffBlock: false | "+" | "-" = false; + let isFoldBlock = false; let classNamesForNextSourceLine = ""; + let dataAttrsForNextSourceLine: + | false + | Array<{ key: string; value: string }> = false; for (let i = 0; i < parsedLines.length; i++) { const line = parsedLines[i]; @@ -85,7 +90,35 @@ export function highlight(code: string) { // TODO break; case "DirectiveFold": - // TODO + const mark = shakuLine.config.mark; + switch (mark) { + case "start": { + isFoldBlock = true; + // find the next non-shaku-line to determine the indent + let j = i + 1; + let indent = 0; + while (j < parsedLines.length) { + const parsedLine = parsedLines[j]; + if (parsedLine.type !== "shaku") { + indent = getLeadingSpaceCount( + parsedLine.line.map((token) => token.content).join("") + ); + break; + } + j += 1; + } + html += `
{...}`; + break; + } + case "end": + if (isFoldBlock) { + isFocusBlock = false; + html += "
"; + } + break; + default: + assertsNever(mark); + } break; case "DirectiveHighlight": { const mark = shakuLine.config.mark; @@ -209,6 +242,10 @@ export function highlight(code: string) { classNamesForNextSourceLine = shakuLine.config.classNames; break; } + case "DirectiveData": { + dataAttrsForNextSourceLine = shakuLine.config.entries; + break; + } default: assertsNever(shakuLine); } @@ -222,12 +259,13 @@ export function highlight(code: string) { const classNames = classNamesForNextSourceLine ? " " + classNamesForNextSourceLine : ""; - + const dataAttrs = dataAttrsForNextSourceLine; shouldHighlighNextSourceLine = false; shouldFocusNextSourceLine = false; shouldDimNextSourceLine = false; diffNextSourceLine = false; classNamesForNextSourceLine = ""; + dataAttrsForNextSourceLine = false; const sourceLine = line.type === "default" ? line.line : line.sourceLine; @@ -239,7 +277,17 @@ export function highlight(code: string) { : diff === "-" ? " diff diff-delete" : ""; - const prefix = `
`; + const classString = `sh__line${highlightClass}${dimClass}${diffClass}${classNames}`; + const dataString = dataAttrs + ? " " + + dataAttrs + .map( + ({ key, value }) => + `${escapeHtml(`data-${key}`)}="${escapeHtml(value)}"` + ) + .join(" ") + : ""; + const prefix = `
`; html += prefix; if (shakuDirectiveHighlightInline) { @@ -423,3 +471,12 @@ function hasShakuDirectiveFocus(lines: ReturnType) { function assertsNever(data: never) { throw new Error("expected never but got: " + data); } + +function getLeadingSpaceCount(str: string) { + for (let i = 0; i < str.length; i++) { + if (!/\s/.test(str[i])) { + return i; + } + } + return 0; +} diff --git a/packages/shaku-code-annotate-sugar-high/src/render.mts b/packages/shaku-code-annotate-sugar-high/src/render.mts index 361a5ca..7594762 100644 --- a/packages/shaku-code-annotate-sugar-high/src/render.mts +++ b/packages/shaku-code-annotate-sugar-high/src/render.mts @@ -1,5 +1,6 @@ import { ShakuDirectiveHighlightInline } from "shaku-code-annotate-core"; import { Token } from "./types.mjs"; +import { escapeHtml } from "./escapeHtml.mjs"; export const renderMarkStart = (id?: number) => `/g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -}