@linloir/limr 0.1.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +151 -0
- package/README.md +80 -127
- package/benchmarks/results/layout.json +2593 -0
- package/benchmarks/results/markstream-code.json +1823 -0
- package/benchmarks/results/markstream-reading-repeat.json +1152 -0
- package/benchmarks/results/markstream.json +12486 -0
- package/benchmarks/results/streaming-browser.json +77 -0
- package/benchmarks/results/streaming.json +357 -0
- package/dist/base.css +93 -10
- package/dist/{chunk-7JF5HHVF.js → chunk-BG5QUQFR.js} +340 -72
- package/dist/chunk-BG5QUQFR.js.map +1 -0
- package/dist/chunk-BVHHOVMT.js +179 -0
- package/dist/chunk-BVHHOVMT.js.map +1 -0
- package/dist/chunk-FJZCQY2S.js +155 -0
- package/dist/chunk-FJZCQY2S.js.map +1 -0
- package/dist/chunk-FSI5OTBW.js +1 -0
- package/dist/{chunk-ETHLPBKQ.js → chunk-GU2RPESF.js} +238 -131
- package/dist/chunk-GU2RPESF.js.map +1 -0
- package/dist/{chunk-65V5OZ4G.js → chunk-SUAAUGAX.js} +1350 -511
- package/dist/chunk-SUAAUGAX.js.map +1 -0
- package/dist/core/index.d.ts +6 -3
- package/dist/core/index.js +15 -5
- package/dist/extensions/index.d.ts +30 -5
- package/dist/extensions/index.js +15 -3
- package/dist/index.d.ts +4 -4
- package/dist/index.js +31 -8
- package/dist/{engine-43u8B4yt.d.ts → knuth-plass-C8mgFm7W.d.ts} +67 -1
- package/dist/knuth-plass-layout-CHQCWKB5.js +449 -0
- package/dist/knuth-plass-layout-CHQCWKB5.js.map +1 -0
- package/dist/react/index.d.ts +7 -5
- package/dist/react/index.js +7 -3
- package/dist/styles.css +93 -10
- package/dist/{translations-BliabrPd.d.ts → translations-BXzSwLBJ.d.ts} +49 -4
- package/docs/README.md +13 -0
- package/docs/api.md +5 -158
- package/docs/assets/rendered.svg +9 -0
- package/docs/assets/source.svg +9 -0
- package/docs/en/README.md +28 -0
- package/docs/en/ai-skills.md +57 -0
- package/docs/en/annotations.md +86 -0
- package/docs/en/api.md +115 -0
- package/docs/en/architecture.md +41 -0
- package/docs/en/extensions.md +208 -0
- package/docs/en/getting-started.md +69 -0
- package/docs/en/integration.md +154 -0
- package/docs/en/modules.md +84 -0
- package/docs/en/performance.md +93 -0
- package/docs/en/plugin-packages.md +68 -0
- package/docs/en/styles.md +99 -0
- package/docs/en/syntax.md +455 -0
- package/docs/extensions.md +5 -306
- package/docs/implementation.md +5 -76
- package/docs/performance.md +5 -81
- package/docs/screenshots/blocks.png +0 -0
- package/docs/screenshots/dark.png +0 -0
- package/docs/screenshots/desktop.png +0 -0
- package/docs/screenshots/plugin-mobile.png +0 -0
- package/docs/styles.md +5 -100
- package/docs/visual-upgrade.md +5 -145
- package/docs/zh-CN/README.md +28 -0
- package/docs/zh-CN/ai-skills.md +57 -0
- package/docs/zh-CN/annotations.md +86 -0
- package/docs/zh-CN/api.md +115 -0
- package/docs/zh-CN/architecture.md +41 -0
- package/docs/zh-CN/extensions.md +210 -0
- package/docs/zh-CN/getting-started.md +69 -0
- package/docs/zh-CN/integration.md +154 -0
- package/docs/zh-CN/modules.md +84 -0
- package/docs/zh-CN/performance.md +93 -0
- package/docs/zh-CN/plugin-packages.md +68 -0
- package/docs/zh-CN/styles.md +99 -0
- package/docs/zh-CN/syntax.md +457 -0
- package/examples/notes.tsx +18 -11
- package/examples/packages/paper-theme/index.css +25 -0
- package/examples/packages/paper-theme/package.json +7 -0
- package/examples/packages/reading-tools/index.d.ts +2 -0
- package/examples/packages/reading-tools/index.js +25 -0
- package/examples/packages/reading-tools/package.json +16 -0
- package/package.json +17 -5
- package/skills/limr-plugin-development/SKILL.md +22 -0
- package/skills/limr-plugin-development/agents/openai.yaml +4 -0
- package/skills/limr-plugin-development/assets/plugin-template/README.md +30 -0
- package/skills/limr-plugin-development/assets/plugin-template/package.json +36 -0
- package/skills/limr-plugin-development/assets/plugin-template/src/index.tsx +50 -0
- package/skills/limr-plugin-development/assets/plugin-template/src/styles.css +9 -0
- package/skills/limr-plugin-development/assets/plugin-template/tsconfig.json +20 -0
- package/skills/limr-plugin-development/assets/plugin-template/tsup.config.ts +8 -0
- package/skills/limr-plugin-development/references/contract.md +51 -0
- package/skills/limr-use/SKILL.md +23 -0
- package/skills/limr-use/agents/openai.yaml +4 -0
- package/skills/limr-use/references/integration.md +49 -0
- package/skills/limr-use/references/syntax.md +20 -0
- package/dist/chunk-65V5OZ4G.js.map +0 -1
- package/dist/chunk-7JF5HHVF.js.map +0 -1
- package/dist/chunk-ETHLPBKQ.js.map +0 -1
- package/dist/chunk-V2OG4Y3F.js +0 -1
- /package/dist/{chunk-V2OG4Y3F.js.map → chunk-FSI5OTBW.js.map} +0 -0
package/examples/notes.tsx
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { useEffect, useLayoutEffect, useRef } from 'react';
|
|
2
|
-
import { AnnotationMarkdown, ToolIcon, defineExtension, useLiMR, type AnnotationContentProps, type RendererExtension } from '@linloir/limr';
|
|
2
|
+
import { AnnotationMarkdown, ToolIcon, defineExtension, useLiMR, useLocale, type AnnotationContentProps, type RendererExtension } from '@linloir/limr';
|
|
3
3
|
|
|
4
4
|
interface NoteData { text: string; draft?: string }
|
|
5
5
|
const kinds = [
|
|
6
|
-
{ type: 'margin-note', label: '旁注', icon: 'comment', color: '#81B29A', hint: '在段落旁留下一个想法' },
|
|
7
|
-
{ type: 'reading-note', label: '笔记', icon: 'note', color: '#6B9BD2', hint: '整理这一段的阅读笔记' },
|
|
8
|
-
{ type: 'reader-question', label: '提问', icon: 'question', color: '#E5A06E', hint: '写下还想进一步理解的问题' },
|
|
9
|
-
{ type: 'reader-footnote', label: '脚注', icon: 'footnote', color: '#B39DDB', hint: '补充解释或参考来源' },
|
|
6
|
+
{ type: 'margin-note', label: '旁注', icon: 'comment', color: '#81B29A', hint: '在段落旁留下一个想法', labelEn: 'Margin note', hintEn: 'Leave a thought beside this passage' },
|
|
7
|
+
{ type: 'reading-note', label: '笔记', icon: 'note', color: '#6B9BD2', hint: '整理这一段的阅读笔记', labelEn: 'Note', hintEn: 'Write a reading note' },
|
|
8
|
+
{ type: 'reader-question', label: '提问', icon: 'question', color: '#E5A06E', hint: '写下还想进一步理解的问题', labelEn: 'Question', hintEn: 'What would you like to understand?' },
|
|
9
|
+
{ type: 'reader-footnote', label: '脚注', icon: 'footnote', color: '#B39DDB', hint: '补充解释或参考来源', labelEn: 'Footnote', hintEn: 'Add an explanation or a source' },
|
|
10
10
|
] as const;
|
|
11
11
|
|
|
12
12
|
function NoteContent({ annotation, data, editing, presentation }: AnnotationContentProps<NoteData>) {
|
|
13
13
|
const commands = useLiMR();
|
|
14
|
+
const locale = useLocale();
|
|
15
|
+
const zh = locale.language?.startsWith('zh') ?? false;
|
|
14
16
|
const text = data?.draft ?? data?.text ?? '';
|
|
15
17
|
const isEditing = editing || data?.draft !== undefined;
|
|
16
18
|
const input = useRef<HTMLTextAreaElement>(null);
|
|
@@ -45,25 +47,30 @@ function NoteContent({ annotation, data, editing, presentation }: AnnotationCont
|
|
|
45
47
|
};
|
|
46
48
|
return <div className="demo-note-content" data-limr-card-interactive="true">
|
|
47
49
|
{isEditing ? <form onSubmit={event => { event.preventDefault(); save(); }}>
|
|
48
|
-
<textarea className="demo-note-editor" ref={input} aria-label={`${kind.label}内容`} rows={1} value={text} onChange={event => commands.updateAnnotation(annotation.id, { data: { text: data?.text ?? '', draft: event.target.value } })} placeholder={kind.hint} onKeyDown={event => {
|
|
50
|
+
<textarea className="demo-note-editor" ref={input} aria-label={zh ? `${kind.label}内容` : `${kind.labelEn} content`} rows={1} value={text} onChange={event => commands.updateAnnotation(annotation.id, { data: { text: data?.text ?? '', draft: event.target.value } })} placeholder={zh ? kind.hint : kind.hintEn} onKeyDown={event => {
|
|
49
51
|
if (event.nativeEvent.isComposing) return;
|
|
50
52
|
if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); cancel(); }
|
|
51
53
|
else if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { event.preventDefault(); save(); }
|
|
52
54
|
}} />
|
|
53
|
-
<div className="demo-note-actions"><button type="button" onClick={cancel}><ToolIcon name="close" /><span className="limr-underline"
|
|
54
|
-
</form> : <><AnnotationMarkdown source={data?.text || '还没有内容。'} /><div className="demo-note-actions"><button type="button" onClick={() => { commands.updateAnnotation(annotation.id, { data: { text: data?.text ?? '', draft: data?.text ?? '' } }); commands.editAnnotation(annotation.id); }}><ToolIcon name="edit" /><span className="limr-underline"
|
|
55
|
+
<div className="demo-note-actions"><button type="button" onClick={cancel}><ToolIcon name="close" /><span className="limr-underline">{zh ? '取消' : 'Cancel'}</span></button><button type="submit" className="demo-note-submit" disabled={!text.trim()}><ToolIcon name="check" /><span className="limr-underline">{locale.submit}</span></button></div>
|
|
56
|
+
</form> : <><AnnotationMarkdown source={data?.text || (zh ? '还没有内容。' : 'No content yet.')} /><div className="demo-note-actions"><button type="button" onClick={() => { commands.updateAnnotation(annotation.id, { data: { text: data?.text ?? '', draft: data?.text ?? '' } }); commands.editAnnotation(annotation.id); }}><ToolIcon name="edit" /><span className="limr-underline">{zh ? '修改' : 'Edit'}</span></button><button type="button" className="demo-note-delete" onClick={() => commands.removeAnnotation(annotation.id)}><ToolIcon name="trash" /><span className="limr-underline">{locale.deleteAnnotations}</span></button></div></>}
|
|
55
57
|
</div>;
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
function NotePreview({ data }: { data?: NoteData }) {
|
|
61
|
+
const locale = useLocale();
|
|
62
|
+
return <span>{data?.text || (locale.language?.startsWith('zh') ? '正在编辑…' : 'Editing…')}</span>;
|
|
63
|
+
}
|
|
64
|
+
|
|
58
65
|
/** Modules bind every footnote/note/question through the same annotation API. */
|
|
59
66
|
export function readingNotes(): RendererExtension {
|
|
60
67
|
return defineExtension({
|
|
61
68
|
name: 'reading-notes',
|
|
62
|
-
annotationTypes: kinds.map(kind => ({ type: kind.type, label: kind.label, color: kind.color, icon: <ToolIcon name={kind.icon} />, Content: NoteContent,
|
|
63
|
-
sidebar: { Preview:
|
|
69
|
+
annotationTypes: kinds.map(kind => ({ type: kind.type, label: locale => locale.language?.startsWith('zh') ? kind.label : kind.labelEn, color: kind.color, icon: <ToolIcon name={kind.icon} />, Content: NoteContent,
|
|
70
|
+
sidebar: { Preview: NotePreview },
|
|
64
71
|
overlay: { previewWidth: 300, expand: { placement: 'center' as const, width: 400, maxHeight: '75dvh' } },
|
|
65
72
|
})),
|
|
66
|
-
actions: kinds.map(kind => ({ id: `reading-notes:${kind.type}`, label: kind.label, icon: <ToolIcon name={kind.icon} />, run: context => {
|
|
73
|
+
actions: kinds.map(kind => ({ id: `reading-notes:${kind.type}`, label: context => context.locale.language?.startsWith('zh') ? kind.label : kind.labelEn, icon: <ToolIcon name={kind.icon} />, run: context => {
|
|
67
74
|
context.addAnnotation({ id: `note-${crypto.randomUUID()}`, type: kind.type, data: { text: '', draft: '' } satisfies NoteData,
|
|
68
75
|
anchors: context.selection.syntax.ranges.map(range => ({ range, documentId: context.document.id, quote: context.document.source.slice(range.start, range.end) })),
|
|
69
76
|
}, { edit: true });
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* Unlayered application/theme CSS overrides the built-in @layer limr. */
|
|
2
|
+
[data-limr-appearance='paper'] {
|
|
3
|
+
--limr-accent: #185145;
|
|
4
|
+
--limr-link: #185145;
|
|
5
|
+
--limr-selection: #548b7440;
|
|
6
|
+
--limr-background: #fcfaf4;
|
|
7
|
+
--limr-heading: #283b34;
|
|
8
|
+
}
|
|
9
|
+
[data-limr-appearance='paper'][data-limr-theme='dark'] {
|
|
10
|
+
--limr-background: #1e2421;
|
|
11
|
+
--limr-heading: #d8e4db;
|
|
12
|
+
--limr-link: #a8cbb8;
|
|
13
|
+
}
|
|
14
|
+
[data-limr-appearance='paper'] .reading-tip {
|
|
15
|
+
border-inline-start: 3px solid var(--limr-accent);
|
|
16
|
+
padding: .8rem 1rem;
|
|
17
|
+
background: #548b7410;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* The same appearance selector is applied to portalled toolbars. */
|
|
21
|
+
.limr-selection-toolbar[data-limr-appearance='paper'] {
|
|
22
|
+
--limr-toolbar-radius: 3px;
|
|
23
|
+
--limr-toolbar-panel-max-height: 190px;
|
|
24
|
+
--limr-toolbar-accent: var(--limr-accent);
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createElement as h } from 'react';
|
|
2
|
+
import { AnnotationCard, AnnotationMarkdown, defineBlock, defineExtension } from '@linloir/limr';
|
|
3
|
+
|
|
4
|
+
// No renderer internals or application services are needed by an npm plugin.
|
|
5
|
+
export function readingTools() {
|
|
6
|
+
const tip = defineBlock({ name: 'reading-tip', component: ({ children }) => h('aside', { className: 'reading-tip' }, children) });
|
|
7
|
+
return defineExtension({
|
|
8
|
+
...tip,
|
|
9
|
+
name: 'reading-tools',
|
|
10
|
+
annotationTypes: [{
|
|
11
|
+
type: 'reading-note', label: 'Reading note', icon: '◇', color: '#548b74',
|
|
12
|
+
overlay: { Card: props => h(AnnotationCard, { ...props, title: 'Reading note' }), expand: { placement: 'anchor' } },
|
|
13
|
+
Content: ({ data }) => h(AnnotationMarkdown, { source: data?.text ?? '' }),
|
|
14
|
+
}],
|
|
15
|
+
actions: [{
|
|
16
|
+
id: 'reading-note:add', label: 'Save passage', icon: h('svg', { viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.7, strokeLinecap: 'round', strokeLinejoin: 'round' }, h('rect', { x: 5, y: 3, width: 14, height: 18, rx: 3 }), h('path', { d: 'M9 8h6M9 12h6M9 16h4' })),
|
|
17
|
+
run(context) {
|
|
18
|
+
const id = `reading-note-${crypto.randomUUID()}`;
|
|
19
|
+
context.addAnnotation({ id, type: 'reading-note', range: context.selection.syntax.ranges[0], data: { text: `**Saved passage**\n\n${context.selection.text}` } });
|
|
20
|
+
context.activateAnnotation(id);
|
|
21
|
+
context.clearSelection();
|
|
22
|
+
},
|
|
23
|
+
}],
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@limr-example/reading-tools",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./index.d.ts",
|
|
9
|
+
"import": "./index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"@linloir/limr": "^1.0.0",
|
|
14
|
+
"react": "^18.3.0 || ^19.0.0"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linloir/limr",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Linloir's limr Is a Markdown Renderer — extensible reading and streaming components for React",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,9 +23,11 @@
|
|
|
23
23
|
"files": [
|
|
24
24
|
"dist",
|
|
25
25
|
"README.md",
|
|
26
|
+
"README.en.md",
|
|
26
27
|
"LICENSE",
|
|
27
|
-
"docs
|
|
28
|
-
"
|
|
28
|
+
"docs",
|
|
29
|
+
"skills",
|
|
30
|
+
"examples",
|
|
29
31
|
"benchmarks/results/*.json"
|
|
30
32
|
],
|
|
31
33
|
"exports": {
|
|
@@ -62,12 +64,16 @@
|
|
|
62
64
|
},
|
|
63
65
|
"dependencies": {
|
|
64
66
|
"@floating-ui/react": "^0.27.20",
|
|
67
|
+
"hyphenation.en-us": "^0.2.1",
|
|
68
|
+
"hypher": "^0.2.5",
|
|
65
69
|
"katex": "^0.19.0",
|
|
70
|
+
"linebreak": "^1.1.0",
|
|
66
71
|
"mdast-util-directive": "^3.1.1",
|
|
67
72
|
"mdast-util-from-markdown": "^2.1.0",
|
|
68
73
|
"mdast-util-gfm": "^3.1.0",
|
|
69
74
|
"mdast-util-math": "^3.0.0",
|
|
70
75
|
"mdast-util-mdx-jsx": "^3.2.0",
|
|
76
|
+
"mermaid": "^12.2.0",
|
|
71
77
|
"micromark-extension-directive": "^4.0.0",
|
|
72
78
|
"micromark-extension-gfm": "^3.0.0",
|
|
73
79
|
"micromark-extension-math": "^3.1.0",
|
|
@@ -84,12 +90,14 @@
|
|
|
84
90
|
"@types/react": "^19.3.0",
|
|
85
91
|
"@types/react-dom": "^19.3.0",
|
|
86
92
|
"@vitejs/plugin-react": "^5.2.0",
|
|
93
|
+
"markstream-react": "2.0.19",
|
|
87
94
|
"react": "^19",
|
|
88
95
|
"react-dom": "^19",
|
|
89
96
|
"react-markdown": "^10.1.0",
|
|
90
97
|
"rehype-katex": "^7.0.1",
|
|
91
98
|
"remark-gfm": "^4.0.1",
|
|
92
99
|
"remark-math": "^6.0.0",
|
|
100
|
+
"stream-diffs": "0.0.2",
|
|
93
101
|
"tsup": "^8.5.1",
|
|
94
102
|
"tsx": "^4.23.15",
|
|
95
103
|
"typescript": "^5.9.3",
|
|
@@ -97,17 +105,21 @@
|
|
|
97
105
|
"vitest": "^3.2.7"
|
|
98
106
|
},
|
|
99
107
|
"scripts": {
|
|
100
|
-
"dev": "vite --host 127.0.0.1",
|
|
108
|
+
"dev": "pnpm pack:skills && vite --host 127.0.0.1",
|
|
101
109
|
"build": "tsup && node scripts/build-assets.mjs",
|
|
102
|
-
"build:playground": "vite build --base=/LiMR/",
|
|
110
|
+
"build:playground": "pnpm pack:skills && vite build --base=/LiMR/",
|
|
103
111
|
"preview:playground": "vite preview --host 127.0.0.1 --base=/LiMR/",
|
|
104
112
|
"typecheck": "tsc --noEmit",
|
|
105
113
|
"test": "vitest run",
|
|
106
114
|
"test:browser": "playwright test",
|
|
107
115
|
"test:package": "node scripts/package-smoke.mjs",
|
|
116
|
+
"test:docs": "node scripts/check-docs.mjs",
|
|
117
|
+
"pack:skills": "python3 scripts/package-skills.py",
|
|
108
118
|
"bench": "tsx benchmarks/run.ts",
|
|
119
|
+
"bench:streaming": "tsx benchmarks/streaming.ts",
|
|
109
120
|
"bench:browser": "tsx benchmarks/browser.ts",
|
|
110
121
|
"bench:selection": "tsx benchmarks/selection.ts",
|
|
122
|
+
"bench:markstream": "tsx benchmarks/markstream/run.ts",
|
|
111
123
|
"check": "pnpm typecheck && pnpm test && pnpm build"
|
|
112
124
|
}
|
|
113
125
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: limr-plugin-development
|
|
3
|
+
description: Develop, package, and integrate new @linloir/limr extension modules with custom syntax, annotation cards, selection actions, parameters, or CSS themes. Use for reusable LiMR plugin packages; ordinary application integration belongs to limr-use.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Develop a LiMR plugin
|
|
7
|
+
|
|
8
|
+
Deliver the requested extension as a package plus a working host import. Inspect the installed LiMR version and public declarations before choosing APIs. This bundle describes 1.0.0, including its locale APIs.
|
|
9
|
+
|
|
10
|
+
Read [the contract](references/contract.md) for source mapping, annotation presentation, actions, resources, and localization. Start from [plugin-template](assets/plugin-template/README.md) when a standalone TypeScript/ESM package is useful. Copy the complete template to the requested project destination, rename its example package and IDs, and remove capabilities not needed by the task. Do not edit the installed skill as the project implementation.
|
|
11
|
+
|
|
12
|
+
Choose the smallest public contract: defineBlock for a block; defineAnnotation for a type; defineExtension to combine capabilities. Keep React/LiMR as peers and build externals, export declarations, and expose CSS separately with sideEffects. Import no renderer internals and bundle no second React or LiMR runtime.
|
|
13
|
+
|
|
14
|
+
Preserve original source positions. Reuse children/renderChildren, renderValue, and renderAttribute. Store every selected range with documentId and its original quote. A transform defaults to document scope; scope:local is valid only for subtree-local, prefix-independent work.
|
|
15
|
+
|
|
16
|
+
Let the module own card meaning and controls through Content, overlay.Card, and sidebar.Preview. Reuse AnnotationCard and AnnotationMarkdown where suitable. LiMR owns shared positioning, portals, focus, and motion. Preview must not fetch; long tasks honor context.signal. The application owns services and credentials.
|
|
17
|
+
|
|
18
|
+
Support host localization when requested: on 1.0.0 use useLocale() in components, locale.language for plugin dictionaries, label(locale) for annotation names, and context.locale in actions. Preserve stored user text across language changes. Older hosts require explicit plugin options instead; reflect the actual minimum in peerDependencies.
|
|
19
|
+
|
|
20
|
+
Integrate with a stable plugins array and explicit CSS imports. Use extensions + replaceExtension for default replacements; duplicate names/tags/directives are errors. Do not silently discard persisted unknown annotations.
|
|
21
|
+
|
|
22
|
+
Validate the built package in an independent consumer, not just a source alias. Check TypeScript, SSR, retained built-ins, syntax/source mapping/finalized streaming where relevant, and desktop/mobile card interactions for UI features. Exercise supported React major versions and both languages/themes when claimed. Report the package path and verified limits. Publishing remains a separate user action unless explicitly requested.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# LiMR reading-tools starter / 插件模板
|
|
2
|
+
|
|
3
|
+
Copy this entire folder into your project, rename the example package/extension/type/action IDs, and adapt the implementation. It requires LiMR 1.0.0 for useLocale and localized annotation labels. The @example package is not on npm. private: true permits local packing while preventing accidental publication under the sample name.
|
|
4
|
+
|
|
5
|
+
复制完整目录到项目,替换示例包名和各 ID。模板需要 LiMR 1.0.0 的语言接口;未发布版本可先安装维护者提供的本地 LiMR tgz。
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install
|
|
9
|
+
npm run typecheck
|
|
10
|
+
npm run build
|
|
11
|
+
npm pack
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Install the generated tgz in a separate host application, then:
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import { Reader, chinese } from '@linloir/limr';
|
|
18
|
+
import { readingTools } from '@example/limr-reading-tools';
|
|
19
|
+
import '@linloir/limr/styles.css';
|
|
20
|
+
import '@example/limr-reading-tools/styles.css';
|
|
21
|
+
|
|
22
|
+
const plugins = [readingTools()];
|
|
23
|
+
export function Article({ source }: { source: string }) {
|
|
24
|
+
return <Reader source={source} plugins={plugins} locale={chinese} />;
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Supports <reading-tip title="Tip"> with Markdown inside, plus a selection action creating an example:note annotation with original-source anchors. English/Chinese labels follow the reader locale; options.label explicitly overrides both. User note text is never translated. Persist annotations in the host with annotations/onAnnotationsChange. The plugin includes no service connection or storage.
|
|
29
|
+
|
|
30
|
+
Test the actual archive in each supported React major, SSR, desktop/mobile cards, and both locales before claiming compatibility. Keep React and LiMR external and declare them as peers. The CSS import is explicit.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@example/limr-reading-tools",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"sideEffects": [
|
|
12
|
+
"**/*.css"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./styles.css": "./dist/styles.css"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"build": "tsup",
|
|
24
|
+
"prepack": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@linloir/limr": "^1.0.0",
|
|
28
|
+
"react": "^18.3.0 || ^19.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.0.0",
|
|
32
|
+
"@types/react": "^19.0.0",
|
|
33
|
+
"tsup": "^8.5.0",
|
|
34
|
+
"typescript": "^5.9.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { AnnotationCard, AnnotationMarkdown, defineBlock, defineExtension, ToolIcon, useLocale,
|
|
2
|
+
type AnnotationContentProps, type AnnotationOverlayCardProps, type AnnotationSidebarPreviewProps,
|
|
3
|
+
type AnnotationType, type LiMRLocale, type RendererExtension } from '@linloir/limr';
|
|
4
|
+
|
|
5
|
+
export interface ReadingNoteData { text: string }
|
|
6
|
+
export interface ReadingToolsOptions { label?: string }
|
|
7
|
+
const isChinese = (locale: LiMRLocale) => locale.language?.startsWith('zh') ?? false;
|
|
8
|
+
|
|
9
|
+
export function readingTools(options: ReadingToolsOptions = {}): RendererExtension {
|
|
10
|
+
const label = (locale: LiMRLocale) => options.label ?? (isChinese(locale) ? '阅读笔记' : 'Reading note');
|
|
11
|
+
function Content({ data }: AnnotationContentProps<ReadingNoteData>) {
|
|
12
|
+
return <AnnotationMarkdown source={data?.text ?? ''} />;
|
|
13
|
+
}
|
|
14
|
+
function Card(props: AnnotationOverlayCardProps<ReadingNoteData>) {
|
|
15
|
+
return <AnnotationCard {...props} title={label(useLocale())} />;
|
|
16
|
+
}
|
|
17
|
+
function Preview({ data }: AnnotationSidebarPreviewProps<ReadingNoteData>) {
|
|
18
|
+
return data?.text ? <span>{data.text}</span> : null;
|
|
19
|
+
}
|
|
20
|
+
const note: AnnotationType<ReadingNoteData> = {
|
|
21
|
+
type: 'example:note', label, icon: <ToolIcon name="note" />, color: '#548b74', Content,
|
|
22
|
+
sidebar: { Preview },
|
|
23
|
+
overlay: { Card, previewWidth: 320, expand: { placement: 'center', width: 600, maxHeight: '80dvh' } },
|
|
24
|
+
};
|
|
25
|
+
const tip = defineBlock({
|
|
26
|
+
name: 'reading-tip',
|
|
27
|
+
component: ({ children, renderAttribute, node }) => <aside className="example-reading-tip">
|
|
28
|
+
{node.attributes?.title && <header>{renderAttribute('title')}</header>}
|
|
29
|
+
{children}
|
|
30
|
+
</aside>,
|
|
31
|
+
});
|
|
32
|
+
return defineExtension({
|
|
33
|
+
...tip, name: 'example:reading-tools', annotationTypes: [note],
|
|
34
|
+
actions: [{
|
|
35
|
+
id: 'example:save-passage', label: context => label(context.locale), icon: <ToolIcon name="note" />,
|
|
36
|
+
visible: ({ selection }) => selection.settled && selection.quality !== 'unmapped'
|
|
37
|
+
&& selection.syntax.ranges.some(range => range.end > range.start),
|
|
38
|
+
run(context) {
|
|
39
|
+
const id = `example-note-${crypto.randomUUID()}`;
|
|
40
|
+
context.addAnnotation({ id, type: note.type, data: { text: context.selection.text } satisfies ReadingNoteData,
|
|
41
|
+
anchors: context.selection.syntax.ranges.map(range => ({
|
|
42
|
+
range, documentId: context.document.id, quote: context.document.source.slice(range.start, range.end),
|
|
43
|
+
})),
|
|
44
|
+
});
|
|
45
|
+
context.activateAnnotation(id);
|
|
46
|
+
context.clearSelection();
|
|
47
|
+
},
|
|
48
|
+
}],
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
.example-reading-tip {
|
|
2
|
+
padding: 1rem;
|
|
3
|
+
border-inline-start: 3px solid var(--limr-accent, #548b74);
|
|
4
|
+
background: var(--limr-surface, #f3f1ee);
|
|
5
|
+
color: var(--limr-text, #3d3a36);
|
|
6
|
+
}
|
|
7
|
+
.example-reading-tip > header { font-weight: 600; }
|
|
8
|
+
.example-reading-tip > :first-child { margin-block-start: 0; }
|
|
9
|
+
.example-reading-tip > :last-child { margin-block-end: 0; }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": [
|
|
5
|
+
"ES2022",
|
|
6
|
+
"DOM"
|
|
7
|
+
],
|
|
8
|
+
"module": "ESNext",
|
|
9
|
+
"moduleResolution": "Bundler",
|
|
10
|
+
"jsx": "react-jsx",
|
|
11
|
+
"strict": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"declaration": true
|
|
15
|
+
},
|
|
16
|
+
"include": [
|
|
17
|
+
"src",
|
|
18
|
+
"tsup.config.ts"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { cp } from 'node:fs/promises';
|
|
2
|
+
import { defineConfig } from 'tsup';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
entry: ['src/index.tsx'], format: ['esm'], target: 'es2022', dts: true, clean: true,
|
|
6
|
+
external: ['@linloir/limr', 'react', 'react/jsx-runtime', 'react-dom'],
|
|
7
|
+
onSuccess: async () => { await cp('src/styles.css', 'dist/styles.css'); },
|
|
8
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Public plugin contract
|
|
2
|
+
|
|
3
|
+
## Syntax and rendering
|
|
4
|
+
|
|
5
|
+
RendererExtension extends SyntaxExtension with components, rendererOverrides, streaming, annotationTypes, actions, and parameters. SyntaxExtension has name, micromark, mdast, tags, directives, transform(root, context), display, selection, scope, and literal. Extension/tag/directive names are validated independently; registered tags also accept matching component directives. Check the installed package's exports before depending on repository additions.
|
|
6
|
+
|
|
7
|
+
defineBlock({ name, component, tags?, directives?, selection?, streaming? }) registers a block with default tag/directive [name], content selection, and local scope. Paired ::name components support nesting, as do existing ::: containers. defineAnnotation<T>(type) returns an extension named annotation:<type>. defineExtension returns the supplied RendererExtension.
|
|
8
|
+
|
|
9
|
+
NodeRendererProps contains node, children, pending, incomplete, source (node slice), renderChildren(nodes, {trailing?}), renderValue(tokens?), and renderAttribute(name). pending describes a growing top-level block; incomplete identifies this particular open node. Do not reparse children or replace the original source. Token offsets and node positions are UTF-16. Use atomic selection for widgets without character correspondence. A custom visual boundary can use data-limr-selection-surface. InlineMarker inserts action-only markers; it is not an annotation.
|
|
10
|
+
|
|
11
|
+
streaming.Loading renders a module-owned intermediate view; omit it to render progressive content using incomplete. streaming.effect supplies className, keyframes, and timing for new text/blocks. The last configured extension effect wins; Reader.streamEffect overrides it and false disables it. Default motion uses opacity alone and respects reduced motion. Reuse children/renderValue/renderChildren to retain text selection and control identity. literal keeps directive bodies from being interpreted as Markdown/JSX; parse original source without changing its public offsets.
|
|
12
|
+
|
|
13
|
+
rendererOverrides rules contain type, component, and optional when(node,{pending,incomplete,ancestors}). The last matching rule wins, with host rules taking final precedence over extensions and components. Explicit replacements own intermediate rendering and keep LiMR's mapping/error boundaries. These are scoped matching rules, not a sandbox for third-party code.
|
|
14
|
+
|
|
15
|
+
Unknown tags are inert literal source, unknown directives under standard() render child Markdown, and malformed JSX can fall back with diagnostics. A document-scoped transform disables tail-only parsing. Declaring local means no dependence on prior/future blocks and no mutation of retained prefixes.
|
|
16
|
+
|
|
17
|
+
## Annotation UI
|
|
18
|
+
|
|
19
|
+
AnnotationType<T> has type, label, optional icon/color/highlight, Content, Marker, overlay, sidebar, and load. highlight:'hover' paints temporary referenced-text feedback and suppresses the automatic persistent highlight unless explicitly requested. In 1.0.0 label can be a string or (locale: LiMRLocale) => string; stable factory instances can switch names without reparsing.
|
|
20
|
+
|
|
21
|
+
Content receives annotation, data, loading, error, retry, presentation ('preview'|'expanded'|'sidebar'), expanded, and editing. It renders the resource/loading/error/editor states. AnnotationMarkdown renders nested Markdown with an isolated selection scope.
|
|
22
|
+
|
|
23
|
+
overlay.Card receives annotation, occurrence, presentation, expanded, pinned, canExpand, expand, collapse, close, and children. It owns the full interior; children contains shared Content. AnnotationCard is an optional layout requiring title. Expansion is opt-in: {placement:'anchor'|'center',width?,height?,maxHeight?}, false, or an annotation resolver. previewWidth is independent. Preserve keyboard-accessible controls when drawing a custom card.
|
|
24
|
+
|
|
25
|
+
sidebar.Preview receives local annotation/data, occurrence, all annotations in the group, and referenceText. It must not trigger load. Return null or set Preview:false to omit it. previewLines defaults to 2. Color fields include previewColor, color, borderColor, titleColor, textColor, excerptColor. collapseOnContentClick defaults to false. The first annotation type owns grouped preview/presentation. Do not duplicate shell padding; use shared CSS variables.
|
|
26
|
+
|
|
27
|
+
load(annotation,{signal}) returns Promise<T> on visible content. The reader deduplicates requests and cancels when unused. Data changes/invalidation refresh resources. Do not put credentials in source, Markdown, or shipped templates. Implement service adapters in the host.
|
|
28
|
+
|
|
29
|
+
## Selection actions and storage
|
|
30
|
+
|
|
31
|
+
SelectionAction requires id, label, icon, and may provide run/visible/disabled/onHover/renderExpanded/render. label can resolve from ActionContext; icon can resolve from context. Custom panel operations use ToolbarActionContext.run for shared busy/error state. Expanded panels inherit the current action-row width; they cannot enlarge the toolbar. Use min-width: 0 and wrapping layouts for controls, with left-aligned whole-item wrapping.
|
|
32
|
+
|
|
33
|
+
ActionContext includes selection, document, annotations, highlight, locale, clearSelection, signal, and document commands. The signal aborts on reader disposal, not menu close. selection.raw/syntax/blocks contain ranges and sourceText; quality is exact/atomic/unmapped, settled is false during adjustment. Keep all selected ranges and use original source slices as quote, not rendered text.
|
|
34
|
+
|
|
35
|
+
For atomic widgets, selection.text and default/native copy preserve the entire original range, including delimiters, mapped attributes, and nested content. Outermost atomic ownership takes precedence. Ordinary text adjacent to an atom keeps rendered spelling. Partial code selections remain supported. A single atomic block anchors the toolbar below the block; custom block-local copy actions define their own copied value. Default copy feedback measures changing button width and uses eased width/opacity/scale transitions with reduced-motion support.
|
|
36
|
+
|
|
37
|
+
Commands: addAnnotation(annotation,{highlight?,edit?}), updateAnnotation(id,partial), removeAnnotation(id), removeAnnotationsAt(ranges), editAnnotation(id|null), activateAnnotation(id|null). Updates shallow-merge annotation fields and replace data. Preserve immutable data and fields such as the initial question during answer streaming. Controlled hosts must return onAnnotationsChange results.
|
|
38
|
+
|
|
39
|
+
Unknown types still produce generic/unavailable UI. Never delete hidden records to simulate plugin uninstallation. A plugin-only action should not create data for a type it fails to register.
|
|
40
|
+
|
|
41
|
+
## Parameters, locales, and styles
|
|
42
|
+
|
|
43
|
+
Augment RendererParameters through declare module '@linloir/limr', register defaults in extension.parameters, and read useRendererParameter. Host parameters override defaults without reparsing. Export augmentation in .d.ts.
|
|
44
|
+
|
|
45
|
+
On 1.0.0 useLocale() exposes the merged locale. language is an optional BCP-47-style tag, with en and zh-CN in built-ins. Use it to select plugin-owned dictionaries, with a sensible fallback. Existing string labels remain valid. Plugins are responsible for translating their own editor buttons, placeholders, summaries, prompts, and status text; do not translate saved data.
|
|
46
|
+
|
|
47
|
+
CSS is explicitly imported. For themes use [data-limr-appearance='your-theme']; reader ancestor selectors miss portals. Keep annotation IDs and CSS names unique. Use the provided .limr-underline on an inline span inside a layout wrapper: a direct flex child becomes a block and cannot paint an underline per text line. Labels that must remain on one line need intrinsic-width buttons; allow button groups to wrap on narrow screens.
|
|
48
|
+
|
|
49
|
+
## Consumer verification
|
|
50
|
+
|
|
51
|
+
Build ESM and .d.ts, pack, install into a temporary React consumer without source aliases, and exercise exported CSS. Test supported React versions rather than claiming compatibility from one host. Check final streaming vs one-shot parse equivalence for syntax changes, source offsets for entities/emoji, and actual browser states for card/toolbar/portal behavior. Test loader cancellation and isolation if adding remote data. No real network service is required by the starter.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: limr-use
|
|
3
|
+
description: Integrate @linloir/limr into React applications, author its supported Markdown, configure streaming, annotations, locales and themes, or import existing LiMR plugins. Use for LiMR consumers; new reusable plugin packages belong to limr-plugin-development.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Use LiMR
|
|
7
|
+
|
|
8
|
+
Deliver a working integration using the host application's installed LiMR API. These references cover the 1.0.0 contract; inspect package.json and exported declarations before assuming a newer or older version behaves identically.
|
|
9
|
+
|
|
10
|
+
- Read [integration.md](references/integration.md) for components, styles, state, streaming, and installed plugin imports.
|
|
11
|
+
- Read [syntax.md](references/syntax.md) when authoring or converting content. Preserve original source and do not invent tag/attribute support.
|
|
12
|
+
|
|
13
|
+
Choose Reader for the complete UI, Markdown for body-only rendering, or one DocumentProvider with one primary Markdown for a custom layout. Inside a provider, omit Markdown.source to share the document.
|
|
14
|
+
|
|
15
|
+
Append third-party factories through a stable plugins array. extensions replaces the entire list; use standard() and replaceExtension() to change defaults. Verify package existence and public exports before suggesting an npm install. Repository @limr-example packages are unpublished examples.
|
|
16
|
+
|
|
17
|
+
Persist controlled annotations immutably and return every onAnnotationsChange value. Keep unknown types in storage even if the host filters them from a view. Unknown annotation types currently show generic/unavailable UI, not silent omission. Use original UTF-16 ranges, stable document IDs, and quotes; avoid DOM-derived offsets.
|
|
18
|
+
|
|
19
|
+
Use locale={chinese} or english for built-in labels. locale does not translate source. In 1.0.0, plugins can use the public useLocale() hook and locale.language, and AnnotationType.label may resolve from the locale. Confirm these exports before using them against 0.1.0.
|
|
20
|
+
|
|
21
|
+
For streaming, pass the accumulated source and finalize with streaming=false. Transport, authentication, storage, and stopping generation belong to the app. No AI provider is installed by this skill.
|
|
22
|
+
|
|
23
|
+
Follow the existing project layout and stylesheet conventions. Validate a real rendered page, relevant interactions, and type/build checks. When changing portals or responsive layouts, inspect desktop and narrow containers. Report what was verified locally and what still needs the host environment; do not publish or deploy unless requested.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Consumer contract
|
|
2
|
+
|
|
3
|
+
## Minimal integration
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { Reader, chinese } from '@linloir/limr';
|
|
7
|
+
import '@linloir/limr/styles.css';
|
|
8
|
+
import '@linloir/limr/math.css';
|
|
9
|
+
|
|
10
|
+
export function Article({ source }: { source: string }) {
|
|
11
|
+
return <Reader source={source} locale={chinese} theme="auto" />;
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
React/React DOM 18.3 or 19, Node 20+, ESM. Choose styles.css or styles/base.css; the latter avoids browser requests for Maple but leaves fonts in the installed package. math.css is separate. CSS themes are imported separately and selected with appearance; that scope reaches portals.
|
|
16
|
+
|
|
17
|
+
Markdown is body-only, with no automatic toolbar. A composed DocumentProvider contains one primary <Markdown /> plus TableOfContents, AnnotationSidebar or AnnotationOverlay, and SelectionToolbar. A Markdown with source creates another document. Synchronized body/sidebar share one scroll container and unscaled coordinates; the host/grid supplies sidebar height. Use sync="none" for a separately scrolling annotation list.
|
|
18
|
+
|
|
19
|
+
## State and commands
|
|
20
|
+
|
|
21
|
+
Reader supports annotations/onAnnotationsChange. Supplying annotations makes it controlled; updates require a callback and a new array. Store data per article and supply a stable documentId. Changing Reader.key resets its internal state, not host state.
|
|
22
|
+
|
|
23
|
+
Annotation is { id, type, data?, range?, anchors? }. Anchors are { range, documentId?, revision?, quote?, id? }. All offsets use the unmodified source's UTF-16 indices with an exclusive end. A quote relocates only when it matches uniquely; ambiguous or mismatched anchors can be omitted.
|
|
24
|
+
|
|
25
|
+
Selection exposes text, quality, settled, and raw/syntax/blocks (each with ranges and sourceText). Preserve all selected ranges and quote each original slice. Syntax absorbs adjacent standard delimiters, not unselected words or custom outer tags. Code can be selected partially; math/images/Mermaid and selection:atomic widgets select as a whole. Toolbar/native copying serializes each atom's complete original source, including $ delimiters, fences, or custom tags; surrounding ordinary text remains rendered text. A single atomic block anchors its toolbar below the block. Block-local copy buttons may deliberately copy only code, a URL, or another module-specific value.
|
|
26
|
+
|
|
27
|
+
useLiMR() exposes addAnnotation, updateAnnotation, removeAnnotation, removeAnnotationsAt, editAnnotation, activateAnnotation, clearSelection, invalidateAnnotation, and the engine. data updates replace the entire data object. addAnnotation normally adds a shared persistent highlight; highlight:false disables that. highlight:'hover' types skip automatic persistent highlights unless an explicit appearance is supplied. highlight annotations need no plugin and support background/underline/dotted/wavy plus hidden.
|
|
28
|
+
|
|
29
|
+
## Streaming and resources
|
|
30
|
+
|
|
31
|
+
Pass the complete accumulated text to source and set streaming=false at completion. followOutput is opt-in and pauses after upward scrolling. Keep extensions/plugins stable. Appends and eligible finalization reuse stable prefixes; full-document transforms, changed definitions, and middle edits may require complete parsing. Growing code fences have a literal fast path.
|
|
32
|
+
|
|
33
|
+
New text and special blocks fade in without a background tint. streamEffect overrides animation keyframes/timing/className; false disables it. Extensions can provide streaming.effect and streaming.Loading. Incomplete code/math may render progressively while Mermaid waits for closure. Existing control state and selected text are retained; honor reduced motion in custom components.
|
|
34
|
+
|
|
35
|
+
typography defaults to standard; choose justify for browser justification or knuth-plass for TeX paragraph optimization with English Knuth–Liang hyphenation. The client adapter caches preparation, retains offscreen DOM, and protects active selections on width changes. knuthPlass configures tolerances, penalties, emergencyStretch, looseness, and hyphenate. Browser font shaping is not pixel-identical to TeX PDF; compare workloads using the repository performance report rather than promising universal timing parity.
|
|
36
|
+
|
|
37
|
+
SSR renders code text without client Shiki and a loading view for Mermaid; KP begins with standard layout and runs on the client. Await preloadMath() before synchronous SSR when complete formula HTML is needed. Framework client boundaries still apply to interactive components. Annotation loaders start when content is shown on the client; Preview only uses local data. Do not infer physical-device acceptance from Chromium mobile emulation.
|
|
38
|
+
|
|
39
|
+
## Importing a plugin
|
|
40
|
+
|
|
41
|
+
Read the actual installed package exports/peer dependencies. Import its factory, create a stable array, pass plugins, and import its CSS if required. Plugins add to defaults; extensions replaces them. Duplicate extension/tag/directive names throw. Parameter overrides belong in parameters and do not reparse source.
|
|
42
|
+
|
|
43
|
+
rendererOverrides replaces views without changing syntax: each rule has type, component, and optional when(node,{pending,incomplete,ancestors}). Last matching rules win; host rules override extension rules. This restricts where a renderer applies, not what third-party JavaScript can execute.
|
|
44
|
+
|
|
45
|
+
Missing plugin annotations retain data but show generic markers/unavailable cards. The Playground's plugin switches disable action entries while leaving renderers installed. If an application wants to hide unsupported types, merge untouched hidden records back into persistence on edits.
|
|
46
|
+
|
|
47
|
+
Use docs shipped under node_modules/@linloir/limr/docs when available. Public source: https://github.com/Linloir/LiMR . Do not rely on internal src/dist imports.
|
|
48
|
+
|
|
49
|
+
The Playground always refreshes to its default document and standard typography. Edits are stored only through explicit Save, then opened manually from the saved-draft menu; locale preference persists separately. Reader itself supplies no persistence.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Built-in syntax
|
|
2
|
+
|
|
3
|
+
React defaults install gfm, math, code-highlight, mermaid, callout, definition, question, extras, video, figures, citation, translations, inline-highlights, and footnotes. Core createDocument defaults to basic CommonMark. Check the installed package's exports and version before using new APIs; these references describe the repository implementation.
|
|
4
|
+
|
|
5
|
+
- Standard Markdown: headings, emphasis, lists, links, images, fenced/inline code, blockquotes. GFM adds tables, tasks, strikethrough, autolinks, and named footnote parsing; the default footnotes module presents notes as comment markers and sidebar/floating cards instead of superscripts/endnotes.
|
|
6
|
+
- Math uses $…$ and $$ fences; import math.css.
|
|
7
|
+
- Mermaid uses mermaid code fences or a paired ::mermaid component. Open streaming diagrams show loading; completed diagrams render on the client and fade in. One shared toolbar copies the diagram body, zooms, and switches source/image views. Drag to pan; arrow keys and wheel scroll, Shift+wheel scrolls horizontally. Selection is atomic in both views, and selection-toolbar/native copying retains the entire original fence/component. Override --limr-mermaid-max-height and --limr-mermaid-scrollbar through CSS.
|
|
8
|
+
- Obsidian callouts use > [!type] Title. Types: note, abstract, info, todo, tip, success, question, warning, failure, danger, bug, example, quote; common Obsidian aliases work. A + suffix starts expanded, - starts collapsed. Blockquotes and callouts can nest. ::callout{type="tip" title="Title" fold="-"} is the component form.
|
|
9
|
+
- ==highlight== creates a read-only blue source highlight; its toolbar offers copy plus a localized hint. Override --limr-inline-highlight or replace inlineHighlights({ color }). Named [^id] definitions and inline ^[Markdown content] create footnote cards. ==highlight==[^id] and ==highlight==^[note] attach the footnote to the highlight. Removing footnotes() restores GFM superscripts/endnotes.
|
|
10
|
+
- <definition title="Term"> supports Markdown body. <title> and <content> child tags support richer content.
|
|
11
|
+
- <question> supports question-content, question-hint, question-answer, question-sel-grp and question-sel. Use string attributes multi="true" and answer="[0,2]"; indices start at zero. This is an exercise, not an AI annotation.
|
|
12
|
+
- <extras> contains <extra title="Title" url="https://example.com" desc="Description" />.
|
|
13
|
+
- <citation id="a" title="Source" url="https://example.com" description="Markdown body" summary="Short preview" /> defines data; <ref id="a" /> or <ref ids='["a","b"]' /> inserts a marker. Legacy cite-id, cite-from-ids, and desc work. Standard [^footnote] syntax is a separate feature.
|
|
14
|
+
- <figure src="/a.png" alt="A" caption="Caption" /> works alone or inside <figures columns="2">. Use figcaption children for Markdown. Limits: min-width/min-height 220/120, max-width/max-height 560/480; none/false removes maxima. Columns/rows, responsive, and gap control grouping. Media-local copy buttons copy image URLs or group Markdown; selection copying follows the selected atomic source range, including Markdown image syntax when present.
|
|
15
|
+
- <video src="/clip.mp4" provider="native" /> and <md-video url="…" /> support native, YouTube, and Bilibili. loading defaults to visible (200px proximity), not click. Use loading="click" to defer until interaction. Remote playback is not guaranteed by local iframe tests.
|
|
16
|
+
- <translations default="both"> contains <origin> and <translation>. Modes: origin/translation/both; both shows translation first. It displays supplied content, not an automatic translation service. Global parameters.translationMode overrides local mode; onTranslationModeChange accepts switch requests.
|
|
17
|
+
|
|
18
|
+
Use static string/boolean attributes, never JavaScript expressions or spread props. Separate block tags and Markdown with blank lines. Only registered tags execute React renderers. Unknown tags show the full original text; unknown directives under standard() render children without their outer marker. Streaming defers partial headers and inline delimiters; recognized unclosed components carry incomplete and use module-owned loading/progressive rendering. Finish with streaming=false to apply final Markdown interpretation.
|
|
19
|
+
|
|
20
|
+
Every registered tag also supports its matching directive, including child tags and citation/ref. Paired ::name{key="value"} and a closing :: support nested Markdown components; ::: containers and single-colon inline directives remain supported. Code/math/Mermaid directive bodies are literal. An unpaired two-colon line in a finalized document keeps its legacy leaf-directive meaning. This compatibility covers scalar attributes and nesting, not CoMark YAML properties, named slots, or frontmatter.
|