@linloir/limr 0.1.0 → 1.0.0

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.
Files changed (80) hide show
  1. package/README.en.md +123 -0
  2. package/README.md +63 -138
  3. package/dist/base.css +10 -7
  4. package/dist/{chunk-65V5OZ4G.js → chunk-3ILUZKDB.js} +77 -17
  5. package/dist/chunk-3ILUZKDB.js.map +1 -0
  6. package/dist/{chunk-ETHLPBKQ.js → chunk-N77VD2YR.js} +96 -51
  7. package/dist/chunk-N77VD2YR.js.map +1 -0
  8. package/dist/{chunk-7JF5HHVF.js → chunk-TTISGBST.js} +14 -1
  9. package/dist/{chunk-7JF5HHVF.js.map → chunk-TTISGBST.js.map} +1 -1
  10. package/dist/core/index.d.ts +6 -3
  11. package/dist/core/index.js +3 -1
  12. package/dist/extensions/index.d.ts +2 -2
  13. package/dist/extensions/index.js +2 -2
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.js +7 -3
  16. package/dist/react/index.d.ts +4 -3
  17. package/dist/react/index.js +5 -3
  18. package/dist/styles.css +10 -7
  19. package/dist/{translations-BliabrPd.d.ts → translations-DHnYMg4h.d.ts} +11 -3
  20. package/docs/README.md +13 -0
  21. package/docs/api.md +5 -158
  22. package/docs/assets/rendered.svg +9 -0
  23. package/docs/assets/source.svg +9 -0
  24. package/docs/en/README.md +26 -0
  25. package/docs/en/ai-skills.md +57 -0
  26. package/docs/en/annotations.md +84 -0
  27. package/docs/en/api.md +107 -0
  28. package/docs/en/architecture.md +33 -0
  29. package/docs/en/extensions.md +159 -0
  30. package/docs/en/getting-started.md +67 -0
  31. package/docs/en/integration.md +130 -0
  32. package/docs/en/modules.md +80 -0
  33. package/docs/en/performance.md +53 -0
  34. package/docs/en/plugin-packages.md +68 -0
  35. package/docs/en/styles.md +80 -0
  36. package/docs/en/syntax.md +352 -0
  37. package/docs/extensions.md +5 -306
  38. package/docs/implementation.md +5 -76
  39. package/docs/performance.md +5 -81
  40. package/docs/screenshots/blocks.png +0 -0
  41. package/docs/screenshots/dark.png +0 -0
  42. package/docs/screenshots/desktop.png +0 -0
  43. package/docs/screenshots/plugin-mobile.png +0 -0
  44. package/docs/styles.md +5 -100
  45. package/docs/visual-upgrade.md +5 -145
  46. package/docs/zh-CN/README.md +26 -0
  47. package/docs/zh-CN/ai-skills.md +57 -0
  48. package/docs/zh-CN/annotations.md +84 -0
  49. package/docs/zh-CN/api.md +107 -0
  50. package/docs/zh-CN/architecture.md +33 -0
  51. package/docs/zh-CN/extensions.md +159 -0
  52. package/docs/zh-CN/getting-started.md +67 -0
  53. package/docs/zh-CN/integration.md +130 -0
  54. package/docs/zh-CN/modules.md +80 -0
  55. package/docs/zh-CN/performance.md +53 -0
  56. package/docs/zh-CN/plugin-packages.md +68 -0
  57. package/docs/zh-CN/styles.md +80 -0
  58. package/docs/zh-CN/syntax.md +352 -0
  59. package/examples/notes.tsx +18 -11
  60. package/examples/packages/paper-theme/index.css +25 -0
  61. package/examples/packages/paper-theme/package.json +7 -0
  62. package/examples/packages/reading-tools/index.d.ts +2 -0
  63. package/examples/packages/reading-tools/index.js +25 -0
  64. package/examples/packages/reading-tools/package.json +16 -0
  65. package/package.json +9 -5
  66. package/skills/limr-plugin-development/SKILL.md +22 -0
  67. package/skills/limr-plugin-development/agents/openai.yaml +4 -0
  68. package/skills/limr-plugin-development/assets/plugin-template/README.md +30 -0
  69. package/skills/limr-plugin-development/assets/plugin-template/package.json +36 -0
  70. package/skills/limr-plugin-development/assets/plugin-template/src/index.tsx +50 -0
  71. package/skills/limr-plugin-development/assets/plugin-template/src/styles.css +9 -0
  72. package/skills/limr-plugin-development/assets/plugin-template/tsconfig.json +20 -0
  73. package/skills/limr-plugin-development/assets/plugin-template/tsup.config.ts +8 -0
  74. package/skills/limr-plugin-development/references/contract.md +45 -0
  75. package/skills/limr-use/SKILL.md +23 -0
  76. package/skills/limr-use/agents/openai.yaml +4 -0
  77. package/skills/limr-use/references/integration.md +41 -0
  78. package/skills/limr-use/references/syntax.md +17 -0
  79. package/dist/chunk-65V5OZ4G.js.map +0 -1
  80. package/dist/chunk-ETHLPBKQ.js.map +0 -1
@@ -0,0 +1,159 @@
1
+ # Extension development
2
+
3
+ > Reading note: this page uses LiMR syntax and interactive examples. Some content cannot be fully displayed by ordinary Markdown viewers such as GitHub; visit the [GitHub Pages documentation](https://linloir.github.io/LiMR/#/docs/en/extensions) for the best reading experience.
4
+
5
+ [Documentation](README.md) · [简体中文](../zh-CN/extensions.md)
6
+
7
+ Start with [built-in modules](modules.md) for module objects, factories, names, Markdown tags, and the difference between `extensions` and `plugins`.
8
+
9
+ ## Choose an interface
10
+
11
+ | Need | Interface |
12
+ | --- | --- |
13
+ | A new block with Markdown content | defineBlock |
14
+ | A standalone annotation type | defineAnnotation |
15
+ | Combined syntax, annotations, actions, and parameters | defineExtension / RendererExtension |
16
+ | A different view for an existing node | components |
17
+ | New parsing rules | micromark + mdast, with transform when needed |
18
+ | Visual changes only | CSS theme package + appearance |
19
+
20
+ These APIs are publicly exported from @linloir/limr. Third-party packages must not import src/dist internals or non-public helpers such as useRuntime. See [plugin packages](plugin-packages.md) for a complete standalone starter.
21
+
22
+ ## Append, remove, and replace defaults
23
+
24
+ ```tsx
25
+ import { Reader, standard, replaceExtension, video, defineBlock } from '@linloir/limr';
26
+
27
+ const extensions = replaceExtension(standard(), video({ loading: 'click' }));
28
+ const plugins = [defineBlock({
29
+ name: 'reading-tip',
30
+ component: ({ children }) => <aside className="reading-tip">{children}</aside>,
31
+ })];
32
+
33
+ export function Article({ source }: { source: string }) {
34
+ return <Reader source={source} extensions={extensions} plugins={plugins} />;
35
+ }
36
+ ```
37
+
38
+ Filter by name to remove a module. Extension names, tag names, and directive names must each be unique; duplicates throw. Do not append a second citation or video through plugins. replaceExtension replaces a matching name or appends if absent. Keep configurations stable to avoid rebuilding the parser during streaming.
39
+
40
+ ## Custom content blocks
41
+
42
+ ```tsx
43
+ import { defineBlock } from '@linloir/limr';
44
+
45
+ export const notice = defineBlock({
46
+ name: 'notice',
47
+ component: ({ children, renderAttribute }) => <aside className="notice">
48
+ <header>{renderAttribute('title')}</header>
49
+ {children}
50
+ </aside>,
51
+ });
52
+ ```
53
+
54
+ This supports `<notice title="Tip">…</notice>` and `:::notice{title="Tip"}`. name becomes the tag and directive name unless tags/directives overrides them. defineBlock defaults to display: block, selection: content, and scope: local.
55
+
56
+ | NodeRendererProps | Use |
57
+ | --- | --- |
58
+ | node | AST node and static attributes |
59
+ | children | Already-rendered subtree; reuse it where possible |
60
+ | source | Source for **this node**, not the entire document |
61
+ | pending | Whether its tail block is still streaming |
62
+ | renderChildren(nodes, { trailing? }) | Rearrange an existing subtree or append an action-only marker |
63
+ | renderValue(tokens?) | Render literals/tokens while preserving selection and annotation insertion |
64
+ | renderAttribute(name) | Render attribute text with original source coordinates |
65
+
66
+ Do not stringify/reparse children or render only node.value when source mapping is required. Use selection: atomic for charts/media without character correspondence, and data-limr-selection-surface for a visual boundary. Public InlineMarker supports action-only markers without creating annotations.
67
+
68
+ ## Plugins own card presentation
69
+
70
+ ```tsx
71
+ import { AnnotationCard, AnnotationMarkdown, defineAnnotation, ToolIcon } from '@linloir/limr';
72
+
73
+ export const note = defineAnnotation<{ text: string }>({
74
+ type: 'example:note',
75
+ label: 'Note',
76
+ icon: <ToolIcon name="note" />,
77
+ color: '#6B9BD2',
78
+ Content: ({ data, loading, error, retry }) => loading ? <p>Loading…</p>
79
+ : error ? <button onClick={retry}>Retry</button>
80
+ : <AnnotationMarkdown source={data?.text ?? ''} />,
81
+ sidebar: {
82
+ Preview: ({ data }) => data?.text ? <span>{data.text}</span> : null,
83
+ previewLines: 2,
84
+ },
85
+ overlay: {
86
+ Card: props => <AnnotationCard {...props} title="Note" />,
87
+ previewWidth: 320,
88
+ expand: { placement: 'center', width: 640, maxHeight: '80dvh' },
89
+ },
90
+ });
91
+ ```
92
+
93
+ Content handles presentation (preview / expanded / sidebar), editing, data, and resource state. overlay.Card owns title, body, and controls; use AnnotationCard or render everything yourself. children contains the shared loaded content. LiMR owns positioning, portals, focus, animation, and exit lifecycle. Keep the plugin's semantic title.
94
+
95
+ Expansion is opt-in. placement accepts anchor / center, sizes accept pixels or CSS lengths, and a resolver can vary the policy by annotation. sidebar.Preview reads local data only; do not fetch for a summary. Preview: false or returning null hides it. The first type in an occurrence owns grouped appearance and summary; annotations provides the group.
96
+
97
+ sidebar also accepts previewLines, previewColor, color, borderColor, titleColor, textColor, excerptColor, and collapseOnContentClick. Do not repeat card padding at the content root. Use --limr-sidebar-padding-inline and manage internal paragraph spacing.
98
+
99
+ ## Selection actions
100
+
101
+ ```tsx
102
+ import { defineExtension, ToolIcon } from '@linloir/limr';
103
+
104
+ export const savePassage = defineExtension({
105
+ name: 'example:save-passage',
106
+ actions: [{
107
+ id: 'example:save-passage', label: 'Save passage', icon: <ToolIcon name="note" />,
108
+ visible: ({ selection }) => selection.settled && selection.quality !== 'unmapped',
109
+ run(context) {
110
+ const id = `example-note-${crypto.randomUUID()}`;
111
+ context.addAnnotation({
112
+ id, type: 'example:note', data: { text: context.selection.text },
113
+ anchors: context.selection.syntax.ranges.map(range => ({
114
+ range, documentId: context.document.id,
115
+ quote: context.document.source.slice(range.start, range.end),
116
+ })),
117
+ });
118
+ context.activateAnnotation(id);
119
+ context.clearSelection();
120
+ },
121
+ }],
122
+ });
123
+ ```
124
+
125
+ Install this alongside the note extension above. SelectionAction requires id, label, and icon, and can define visible, disabled, run, onHover, renderExpanded, or render. Use context.run for panel operations to share toolbar busy/error handling. Context contains the selection, full snapshot, annotations/highlight for the fragment, locale, cancellation signal, and commands. Later actions override matching IDs; explicit host actions take final precedence.
126
+
127
+ ## Parameters
128
+
129
+ ```tsx
130
+ import { defineExtension, useRendererParameter } from '@linloir/limr';
131
+
132
+ declare module '@linloir/limr' {
133
+ interface RendererParameters { exampleDetail?: 'short' | 'full' }
134
+ }
135
+
136
+ export const glossary = defineExtension({
137
+ name: 'example:glossary', tags: ['glossary'], scope: 'local',
138
+ parameters: { exampleDetail: 'short' },
139
+ components: {
140
+ glossary: ({ children }) => <aside data-detail={useRendererParameter('exampleDetail')}>
141
+ {children}
142
+ </aside>,
143
+ },
144
+ });
145
+ ```
146
+
147
+ The host overrides defaults with parameters={{ exampleDetail: 'full' }}. Parameter updates affect views without reparsing; later defaults win. A standalone package must include declaration merging in its exported .d.ts.
148
+
149
+ ## Parsing and streaming constraints
150
+
151
+ SyntaxExtension supports micromark, mdast, tags, directives, display, selection, transform, and scope. Transforms default to document scope. Declare scope: local only if the transform depends on the current subtree and never revises earlier content or needs document-wide semantics. context.source is the original whole document; positions remain original UTF-16 offsets. Do not regex-rewrite source before parsing.
152
+
153
+ Unknown custom tags display their complete original text, including Markdown inside. Unknown directives in the default configuration render their inner Markdown without outer directive markers; with no directive extension installed, basic Markdown parsing applies. Incomplete tags fall back with diagnostics and retry on append. Finish with finish() or streaming=false. Transform exceptions become diagnostics. Client renderer boundaries can fall back to source, but React error boundaries do not catch server-rendering exceptions.
154
+
155
+ Validate tags/directives, Chinese/emoji, offsets, equivalence between finalized streams and one-shot parsing, SSR, and browser behavior for new interactions. For distribution, test actual packed consumption as described in [plugin packages](plugin-packages.md).
156
+
157
+ ## Plugin localization
158
+
159
+ From 1.0.0, components can read the merged locale through useLocale() and select their own dictionary by locale.language. AnnotationType.label accepts a string or `(locale) => string`; actions use context.locale. Citation titles switch between Citation and 引用. A stable plugin instance can update labels without rewriting source. Saved user notes are never translated.
@@ -0,0 +1,67 @@
1
+ # Getting started
2
+
3
+ > Reading note: this page uses LiMR syntax and interactive examples. Some content cannot be fully displayed by ordinary Markdown viewers such as GitHub; visit the [GitHub Pages documentation](https://linloir.github.io/LiMR/#/docs/en/getting-started) for the best reading experience.
4
+
5
+ [Documentation](README.md) · [简体中文](../zh-CN/getting-started.md)
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install @linloir/limr
11
+ ```
12
+
13
+ The host needs React and React DOM 18.3 or 19. LiMR is ESM and requires Node.js 20+. Use an ESM-capable build tool such as Vite. The application must import CSS explicitly.
14
+
15
+ ## Complete reader
16
+
17
+ ```tsx
18
+ import { Reader } from '@linloir/limr';
19
+ import '@linloir/limr/styles.css';
20
+ import '@linloir/limr/math.css';
21
+
22
+ const source = `# LiMR
23
+
24
+ Supports **emphasis**, tables, code, and $x^2$.
25
+
26
+ ## References
27
+
28
+ Read this passage<ref id="guide" />.
29
+
30
+ <citation id="guide" title="Guide" url="https://example.com/guide" />`;
31
+
32
+ export function Article() {
33
+ return <Reader source={source} theme="auto" />;
34
+ }
35
+ ```
36
+
37
+ Wide containers show contents, body, and annotations. Below `sidebarBreakpoint` (1120px by default), annotations use an overlay. Contents become a collapsible list on narrow screens.
38
+
39
+ ## Body only
40
+
41
+ ```tsx
42
+ import { Markdown } from '@linloir/limr';
43
+ import '@linloir/limr/styles/base.css';
44
+
45
+ export function Excerpt() {
46
+ return <Markdown source="Some **Markdown**." />;
47
+ }
48
+ ```
49
+
50
+ Standalone `Markdown` retains parsing and selection support but does not mount a toolbar. Set `interactive={false}` to disable selection tracking for static content. Inside a `DocumentProvider`, use `<Markdown />` without `source` to share its document.
51
+
52
+ ## Choose styles
53
+
54
+ | Import | Includes |
55
+ | --- | --- |
56
+ | `@linloir/limr/styles.css` | Default theme, layout, interaction, and Maple code fonts |
57
+ | `@linloir/limr/styles/base.css` | The same styling without importing Maple font CSS |
58
+ | `@linloir/limr/fonts.css` | Maple fonts only, for separate use with base.css |
59
+ | `@linloir/limr/math.css` | KaTeX styles and local math fonts |
60
+
61
+ Choose either styles.css or styles/base.css. Selecting base does not remove font files from the npm download. Body fonts come from the host or system; the Playground's Noto Serif SC is not automatically loaded by the library.
62
+
63
+ ## Next steps
64
+
65
+ - Read [default syntax](syntax.md) to distinguish built-in content modules from application annotation plugins.
66
+ - Read [application integration](integration.md) for streaming, scroll containers, and SSR.
67
+ - Install the [limr-use Skill](ai-skills.md) for AI-assisted integration.
@@ -0,0 +1,130 @@
1
+ # Application integration
2
+
3
+ > Reading note: this page uses LiMR syntax and interactive examples. Some content cannot be fully displayed by ordinary Markdown viewers such as GitHub; visit the [GitHub Pages documentation](https://linloir.github.io/LiMR/#/docs/en/integration) for the best reading experience.
4
+
5
+ [Documentation](README.md) · [简体中文](../zh-CN/integration.md)
6
+
7
+ ## Choose a component
8
+
9
+ Use `Reader` for the complete experience and `Markdown` for standalone content. For a custom layout, one DocumentProvider manages one primary Markdown plus its navigation, cards, and toolbar. Passing source to Markdown inside a provider creates an independent document instead of sharing the parent state.
10
+
11
+ ## Streaming content
12
+
13
+ ```tsx
14
+ import { Markdown } from '@linloir/limr';
15
+
16
+ export function StreamingAnswer({ answer, generating }: {
17
+ answer: string;
18
+ generating: boolean;
19
+ }) {
20
+ return <Markdown source={answer} streaming={generating} followOutput />;
21
+ }
22
+ ```
23
+
24
+ The application owns transport and appends each chunk to the complete answer. Do not pass only the newest chunk as source. The final update must set streaming to false to complete semantic correction. followOutput follows only while the reader remains near the bottom. The core engine supports append(chunk) / finish(); React props take the accumulated string.
25
+
26
+ ## Nested scroll containers
27
+
28
+ ```tsx
29
+ import { useState } from 'react';
30
+ import { Reader } from '@linloir/limr';
31
+
32
+ export function ScrollPanel({ source }: { source: string }) {
33
+ const [scrollRoot, setScrollRoot] = useState<HTMLDivElement | null>(null);
34
+ return <div ref={setScrollRoot} style={{ height: '80vh', overflowY: 'auto' }}>
35
+ <Reader source={source} scrollRoot={scrollRoot} scrollOffset={32} />
36
+ </div>;
37
+ }
38
+ ```
39
+
40
+ Window scrolling needs no scrollRoot. Set scrollOffset for a fixed header and --limr-sticky-top for sticky navigation. Give Reader a new React key when switching to an independent document, plus a stable documentId unique on the page, to recreate task and interaction state.
41
+
42
+ ## Custom composition
43
+
44
+ This is a fixed desktop layout. Prefer Reader for responsive sidebar switching, collapsible contents, and automatic height. With your own breakpoints, decide when to mount the sidebar or overlay.
45
+
46
+ ```tsx
47
+ import { DocumentProvider, Markdown, TableOfContents, AnnotationSidebar,
48
+ SelectionToolbar } from '@linloir/limr';
49
+
50
+ export function DesktopReader({ source }: { source: string }) {
51
+ return <DocumentProvider source={source}>
52
+ <div style={{ display: 'grid', gridTemplateColumns: '180px minmax(0, 1fr) 280px', gap: 24 }}>
53
+ <TableOfContents style={{ position: 'sticky', top: 24, alignSelf: 'start', maxHeight: '80vh' }} />
54
+ <main className="limr-reader-content" style={{ minHeight: '70vh', overflow: 'clip' }}>
55
+ <Markdown />
56
+ </main>
57
+ <AnnotationSidebar />
58
+ </div>
59
+ <SelectionToolbar />
60
+ </DocumentProvider>;
61
+ }
62
+ ```
63
+
64
+ Keep body and synchronized sidebar in the same scroll container and unscaled coordinate system. A grid row or the host supplies sidebar height. Do not grow the page to the total card height or add a separate scroll container to the synchronized list. Long card bodies can scroll independently. For a drawer or ordinary list, use AnnotationSidebar with sync="none" and let the host constrain height and overflow.
65
+
66
+ For an overlay layout, mount Markdown, AnnotationOverlay, and SelectionToolbar under the same provider. The two floating components accept portalRoot; Reader does not expose that prop directly, so compose components when it is needed.
67
+
68
+ ## Controlled data and document switching
69
+
70
+ ```tsx
71
+ import { useState } from 'react';
72
+ import { Reader, type Annotation } from '@linloir/limr';
73
+
74
+ export function SavedArticle({ id, source }: { id: string; source: string }) {
75
+ const [annotations, setAnnotations] = useState<readonly Annotation[]>([]);
76
+ return <Reader key={id} documentId={id} source={source}
77
+ annotations={annotations} onAnnotationsChange={setAnnotations} />;
78
+ }
79
+ ```
80
+
81
+ This illustrates state for one article. In an application, load/store annotations by article ID and replace the array on navigation; a React key does not clear host state. Omit annotations for internal state. When supplied, accept edits through onAnnotationsChange and pass the new array back, or editing throws. Do not mutate arrays or data in place. See [annotations and selection](annotations.md) for persistence and relocation rules.
82
+
83
+ ## Global translation mode
84
+
85
+ ```tsx
86
+ import { useState } from 'react';
87
+ import { Reader, type TranslationMode } from '@linloir/limr';
88
+
89
+ export function BilingualArticle({ source }: { source: string }) {
90
+ const [mode, setMode] = useState<TranslationMode | undefined>();
91
+ return <Reader source={source}
92
+ parameters={{ translationMode: mode, onTranslationModeChange: setMode }} />;
93
+ }
94
+ ```
95
+
96
+ Parameters update views without reparsing. Other plugins can register parameters as described in [extension development](extensions.md).
97
+
98
+ ## SSR and client boundaries
99
+
100
+ Place interactive components inside your framework's client component boundary (for example, 'use client' where required) and let the framework arrange SSR/hydration. Avoid window/document at module initialization. Keep initial source, documentId, and configuration consistent between server and client.
101
+
102
+ Synchronous SSR falls back to formula source until math is loaded. To include complete KaTeX output in the first HTML, preload it on the server:
103
+
104
+ ```tsx
105
+ import { renderToString } from 'react-dom/server';
106
+ import { Markdown, preloadMath } from '@linloir/limr';
107
+
108
+ export async function renderArticle(source: string) {
109
+ await preloadMath();
110
+ return renderToString(<Markdown source={source} documentId="server-article" />);
111
+ }
112
+ ```
113
+
114
+ React streaming SSR can also handle Suspense. Shiki adds highlighting on the client; SSR already includes code text. Annotation load requests start when a card is shown on the client. Put preloaded data in annotation.data if needed. The host must still serve CSS and fonts.
115
+
116
+ ## Existing plugins and themes
117
+
118
+ Import a factory from a real plugin package and create a stable plugins array outside rendering or with useMemo. Import plugin CSS explicitly as documented by its author. Themes are selected with appearance, not passed through plugins. See [plugin packages](plugin-packages.md) for installation and local tgz consumption.
119
+
120
+ ## Troubleshooting
121
+
122
+ | Symptom | Check |
123
+ | --- | --- |
124
+ | Missing layout or malformed formulas | Import styles or base, plus math.css |
125
+ | Default modules disappear after adding a plugin | Use plugins to append, rather than replacing extensions |
126
+ | Annotation edits do not persist | Return a new array from the controlled callback |
127
+ | Body and navigation disagree | A Markdown source prop may have created a second provider |
128
+ | Nested scrolling targets the wrong place | Pass the actual scrolling DOM element as scrollRoot |
129
+ | Portal colors differ from the reader | Scope CSS by data-limr-appearance |
130
+ | Streaming repeatedly reparses everything | Keep extensions/plugins stable and check document-scoped transforms |
@@ -0,0 +1,80 @@
1
+ # Built-in modules
2
+
3
+ > Reading note: this page uses LiMR syntax and interactive examples. Some content cannot be fully displayed by ordinary Markdown viewers such as GitHub; visit the [GitHub Pages documentation](https://linloir.github.io/LiMR/#/docs/en/modules) for the best reading experience.
4
+
5
+ [Documentation](README.md) · [简体中文](../zh-CN/modules.md)
6
+
7
+ ## What is a module?
8
+
9
+ LiMR's base parser understands CommonMark: headings, paragraphs, lists, links, images, and code fences. A **module** is a configuration object that adds capabilities, such as formulas, video rendering, or citation cards. Its public TypeScript type is `RendererExtension`, so the API also calls it an extension.
10
+
11
+ A module can combine parsing rules, React components, annotation types, toolbar actions, and default parameters. It need not include all of them. For example, `math` adds formula syntax and rendering, `code-highlight` colors existing code fences, and `citation` provides both reference syntax and annotation cards.
12
+
13
+ A **plugin** usually means a module added by your application or supplied by a third-party package. It uses the same interface as built-in modules. An npm package distributes code and can export several modules. Passing a module to `plugins` appends it to the existing configuration.
14
+
15
+ ## Functions, module names, and tag names
16
+
17
+ Consider video:
18
+
19
+ ```tsx
20
+ import { Reader, standard, replaceExtension, video } from '@linloir/limr';
21
+
22
+ const videoModule = video({ loading: 'click' });
23
+ // videoModule.name === 'video'
24
+ const extensions = replaceExtension(standard(), videoModule);
25
+
26
+ export function Article() {
27
+ return <Reader extensions={extensions} source={'<video src="/lesson.mp4" />'} />;
28
+ }
29
+ ```
30
+
31
+ | Form | Where it belongs | Meaning |
32
+ | --- | --- | --- |
33
+ | `video` / `video(...)` | Application JavaScript / TypeScript | The imported function / a call that creates a module configuration object |
34
+ | `videoModule` | Application code | Your variable name for that object; you can rename it |
35
+ | `'video'` | The module object's `name` property | A stable identifier used to distinguish, replace, and remove modules |
36
+ | `<video … />` / `<md-video … />` | Markdown content | Two tags registered by the module, using the same video renderer |
37
+ | `::video{…}` | Markdown content | A registered directive; a `:::video` container also works |
38
+
39
+ The names need not match. The code-highlighting factory is `codeHighlight()`, its module name is `'code-highlight'`, and its content uses ordinary code fences without a `<code-highlight>` tag. Writing `video()` or the string `'video'` inside Markdown does not enable a module. Pass the returned object to `plugins`, not a string.
40
+
41
+ `video({ loading: 'click' })` sets module defaults for that Reader. A `<video loading="eager" … />` tag overrides the setting for one video. Available options vary by module; see [default syntax](syntax.md).
42
+
43
+ ## Ten default modules included with LiMR
44
+
45
+ These modules ship in `@linloir/limr`; they do not require ten separate npm installations. `standard()` returns an array containing the following ten module objects, each with its default settings.
46
+
47
+ | Factory function | Object's `name` | Capability and content syntax |
48
+ | --- | --- | --- |
49
+ | `gfm()` | `gfm` | Tables, `~~strikethrough~~`, tasks, autolinks, `[^id]` endnotes |
50
+ | `math()` | `math` | `$…$` inline and `$$…$$` display math; also import math.css |
51
+ | `codeHighlight()` | `code-highlight` | Shiki colors for code fences with a language identifier |
52
+ | `definition()` | `definition` | `<definition>` cards with a title and Markdown body |
53
+ | `question()` | `question` | `<question>` exercises with choices, hints, answers, and local scoring |
54
+ | `extras()` | `extras` | `<extras>` / `<extra>` further-reading links |
55
+ | `video()` | `video` | `<video>` / `<md-video>` native video, YouTube, and Bilibili embeds |
56
+ | `figures()` | `figures` | `<figures>` / `<figure>` responsive groups, single figures, and captions |
57
+ | `citation()` | `citation` | `<citation>` source definitions and `<ref>` markers with cards |
58
+ | `translations()` | `translations` | `<translations>` original, translated, and parallel views |
59
+
60
+ Import these functions from `@linloir/limr` or `@linloir/limr/extensions`. `standard()` is a convenience function returning an array, not an eleventh module.
61
+
62
+ `Reader`, standalone `Markdown`, and `DocumentProvider` automatically use `standard()` when `extensions` is omitted. Your first `<Reader source={source} />` therefore supports these capabilities. Components inside a shared Provider inherit its configuration. Core-only `createDocument()` starts with CommonMark; extensions must be explicit, and it does not display React UI.
63
+
64
+ Highlight, copy, and annotation deletion are built-in toolbar actions. Highlights are saved as annotation data, not a separate Markdown tag. The Playground's margin notes, reading notes, questions, footnote annotations, and simulated assistant come from example plugins and **are not automatically installed in every Reader**. The `question()` exercise module is also separate from question annotations.
65
+
66
+ ## Choosing a configuration
67
+
68
+ You do not need to call `standard()` when the defaults suffice. For customization, distinguish three operations:
69
+
70
+ | Goal | Configuration | Result |
71
+ | --- | --- | --- |
72
+ | Add your own capability | `plugins={[myPlugin]}` | Keep default modules and append your plugin object |
73
+ | Keep only selected capabilities | `extensions={[gfm(), math()]}` | Replace the complete default module list; CommonMark remains available |
74
+ | Configure a built-in module | `extensions={replaceExtension(standard(), video({ loading: 'click' }))}` | Replace video by module `name`, preserving the other modules |
75
+
76
+ To remove one module, filter the array: `standard().filter(module => module.name !== 'video')`. Use `extensions={[]}` when extension syntax is unnecessary.
77
+
78
+ Do not append `plugins={[video()]}` while the default video module is present: duplicate module names, tags, or directives throw. Without the corresponding module, unknown tags render as text, unknown directives may retain only their inner Markdown, and unknown annotation data remains with generic fallback UI. See [architecture and behavior](architecture.md) for the precise boundaries.
79
+
80
+ Next, try the [syntax examples](syntax.md), create modules with [extension development](extensions.md), or distribute them using [plugin packages](plugin-packages.md).
@@ -0,0 +1,53 @@
1
+ # Performance and limits
2
+
3
+ > Reading note: this page uses LiMR syntax and interactive examples. Some content cannot be fully displayed by ordinary Markdown viewers such as GitHub; visit the [GitHub Pages documentation](https://linloir.github.io/LiMR/#/docs/en/performance) for the best reading experience.
4
+
5
+ [Documentation](README.md) · [简体中文](../zh-CN/performance.md)
6
+
7
+ These are the repository's **2026-10-12 historical measurements**, retained for comparison. They were not rerun for this documentation/Playground update and are not guarantees.
8
+
9
+ Environment: Intel Core i7-13700, WSL2/Linux, Node 20.19.5, Chromium 156.0.8078.4, React 19 production. Baseline: react-markdown 10.1.0 + remark-gfm 4.0.1.
10
+
11
+ ## Browser
12
+
13
+ Both render equivalent GFM with the same typography, body only, excluding Reader navigation/sidebar. Values are medians of three isolated pages after warmup and font readiness, alternating renderer order. Timing includes synchronous React commits and forced layout, excluding network, asynchronous heavy resources, and idle frame time.
14
+
15
+ | Scenario | LiMR | react-markdown |
16
+ | --- | ---: | ---: |
17
+ | 87,381 characters, 200 sections, initial mount | 126.1 ms | 134.0 ms |
18
+ | 877,782 characters, 2,000 sections, initial mount | 1,345.4 ms | 1,437.6 ms |
19
+ | 34,860 characters, 138 streaming commits, total synchronous time | 306.1 ms | 1,864.1 ms |
20
+ | Same stream, per-commit P95 | 2.9 ms | 23.7 ms |
21
+
22
+ The final full parse is included. P95 does not mean every frame stays below 16ms. Raw data: [browser.json](../../benchmarks/results/browser.json).
23
+
24
+ ## Selection and SSR
25
+
26
+ Historical long-document selection paint latency P95 was 25.2–28.5ms; code was 18.9–19.4ms. This measures selectionchange to paint-layer updates, not hardware input-to-display latency. The same automated drag took 3.23s in LiMR and 1.52s in the baseline; similar rAF intervals do not imply identical processing cost. Data: [selection.json](../../benchmarks/results/selection.json).
27
+
28
+ | Node scenario | LiMR | Baseline |
29
+ | --- | ---: | ---: |
30
+ | 87,381 characters, SSR | 123.85 ms | 135.80 ms |
31
+ | 877,782 characters, SSR | 5,508.29 ms | 2,720.70 ms |
32
+ | 34,860 characters, 137 core appends/finalization | 164.04 ms | 2,673.74 ms |
33
+
34
+ Large-document SSR varied substantially: LiMR samples were 2.64–9.10s and baseline 2.70–5.03s. These results show no SSR advantage. The append baseline reparses the entire prefix each time. Data: [node.json](../../benchmarks/results/node.json).
35
+
36
+ ## Practical limits
37
+
38
+ Keeping the full DOM supports search, selection, and printing, but initial long-document rendering still costs time. Incremental parsing reuses stable prefixes; long tail blocks, document-scoped transforms, definition changes, middle edits, and finalization can remain expensive. Keep extensions/plugins stable and limit highlighting languages/length when appropriate.
39
+
40
+ The measurements do not cover large formula collections, Shiki cold starts, dense annotations, third-party plugins, full Reader layouts, or physical mobile devices. Installed font size also differs from browser transfers; see [styles and fonts](styles.md).
41
+
42
+ ## Reproduce in the repository
43
+
44
+ ```sh
45
+ NODE_ENV=production pnpm bench
46
+ pnpm exec vite build
47
+ pnpm exec vite preview --host 127.0.0.1 --port 4174
48
+ # In another terminal:
49
+ pnpm bench:browser
50
+ pnpm bench:selection
51
+ ```
52
+
53
+ Avoid concurrent builds/benchmarks. Scripts write benchmarks/results. Reevaluate using your hardware, content, and built version rather than treating historical numbers as current-package acceptance.
@@ -0,0 +1,68 @@
1
+ # Plugin packages
2
+
3
+ > Reading note: this page uses LiMR syntax and interactive examples. Some content cannot be fully displayed by ordinary Markdown viewers such as GitHub; visit the [GitHub Pages documentation](https://linloir.github.io/LiMR/#/docs/en/plugin-packages) for the best reading experience.
4
+
5
+ [Documentation](README.md) · [简体中文](../zh-CN/plugin-packages.md)
6
+
7
+ A plugin exports a factory returning RendererExtension. Consumers append it through plugins. One package can provide syntax, annotations, actions, and parameters, with a separate CSS export, without editing LiMR itself.
8
+
9
+ ## Copyable starter
10
+
11
+ The complete [plugin-template](../../skills/limr-plugin-development/assets/plugin-template/README.md) includes TypeScript, declaration output, CSS, package.json, and tsup configuration. It demonstrates a reading-tip block, an example:note card, and a save-selection action.
12
+
13
+ After installing LiMR, run from your application:
14
+
15
+ ```sh
16
+ mkdir -p packages/limr-reading-tools
17
+ cp -R node_modules/@linloir/limr/skills/limr-plugin-development/assets/plugin-template/. packages/limr-reading-tools/
18
+ cd packages/limr-reading-tools
19
+ npm install
20
+ npm run typecheck
21
+ npm run build
22
+ npm pack
23
+ ```
24
+
25
+ In a source checkout, use skills/limr-plugin-development/assets/plugin-template. Replace the @example name, extension/type/action IDs, and implement your data/UI. The starter's private: true prevents accidental publication under an example name and still permits local packing.
26
+
27
+ ## Package contract
28
+
29
+ | Field | Requirement |
30
+ | --- | --- |
31
+ | type | module |
32
+ | exports["."] | import points to ESM; types points to .d.ts |
33
+ | exports["./styles.css"] | Explicit export when the plugin needs CSS |
34
+ | files | Built output and usage instructions |
35
+ | sideEffects | ["**/*.css"] to retain imported styles |
36
+ | peerDependencies | @linloir/limr and react; add react-dom when directly using it |
37
+ | Build external | LiMR, React, react/jsx-runtime, and any React DOM entry used |
38
+
39
+ Bundling another copy of LiMR or React breaks shared contexts/hooks. Externalize host dependencies and avoid internal deep imports. Ship parameter declaration merging with exported types.
40
+
41
+ ## Import into an application
42
+
43
+ The @example package below is the starter, not a published npm package. Install the generated local archive; use the actual filename printed by npm pack:
44
+
45
+ ```sh
46
+ npm install ./packages/limr-reading-tools/example-limr-reading-tools-0.1.0.tgz
47
+ ```
48
+
49
+ ```tsx
50
+ import { Reader } from '@linloir/limr';
51
+ import { readingTools } from '@example/limr-reading-tools';
52
+ import '@linloir/limr/styles.css';
53
+ import '@example/limr-reading-tools/styles.css';
54
+
55
+ const plugins = [readingTools({ label: 'Reading note' })];
56
+
57
+ export function Article({ source }: { source: string }) {
58
+ return <Reader source={source} plugins={plugins} />;
59
+ }
60
+ ```
61
+
62
+ Use the real name and peer version requirements for a third-party package. Replace default modules through extensions + replaceExtension; do not register the same tag twice. Hosts still configure fonts, math CSS, persistence, and application services.
63
+
64
+ ## Validate distribution
65
+
66
+ After type checking, install the actual tgz into an independent consumer. Check public imports, SSR, CSS resources, preserved defaults, and new interactions. Packages supporting React 18/19 should verify both. Syntax plugins should compare finalized streams with one-shot parsing. Interactive plugins should cover desktop sidebars, narrow overlays, keyboard behavior, cancellation, and both themes.
67
+
68
+ The repository also includes packable [reading-tools](../../examples/packages/reading-tools/index.js) and [paper-theme](../../examples/packages/paper-theme/index.css) examples. The [plugin development Skill](ai-skills.md) includes a portable template and references that do not require this repository.
@@ -0,0 +1,80 @@
1
+ # Styles and fonts
2
+
3
+ > Reading note: this page uses LiMR syntax and interactive examples. Some content cannot be fully displayed by ordinary Markdown viewers such as GitHub; visit the [GitHub Pages documentation](https://linloir.github.io/LiMR/#/docs/en/styles) for the best reading experience.
4
+
5
+ [Documentation](README.md) · [简体中文](../zh-CN/styles.md)
6
+
7
+ ## Base styles and themes
8
+
9
+ Default rules live in @layer limr, so ordinary host CSS can override them. Choose styles.css (with Maple) or styles/base.css (without loading Maple), and add math.css for formulas.
10
+
11
+ ```tsx
12
+ import { Reader } from '@linloir/limr';
13
+ import '@linloir/limr/styles/base.css';
14
+ import './paper.css';
15
+
16
+ export function Paper({ source }: { source: string }) {
17
+ return <Reader source={source} appearance="paper" theme="auto" />;
18
+ }
19
+ ```
20
+
21
+ Example paper.css:
22
+
23
+ ```css
24
+ [data-limr-appearance='paper'] {
25
+ --limr-font-text: Georgia, 'Noto Serif SC', serif;
26
+ --limr-content-width: 820px;
27
+ --limr-accent: #8b6548;
28
+ }
29
+ [data-limr-appearance='paper'][data-limr-theme='dark'] {
30
+ --limr-accent: #d1ab84;
31
+ }
32
+ ```
33
+
34
+ appearance propagates to portalled controls. Do not rely only on .limr-reader ancestry. theme: auto selects built-in colors with the system preference; custom dark overrides for auto also need a prefers-color-scheme media rule. See the standalone [paper-theme](../../examples/packages/paper-theme/index.css) example.
35
+
36
+ ## Common variables
37
+
38
+ | Variable | Purpose |
39
+ | --- | --- |
40
+ | --limr-font-text / --limr-font-code | Body / code fonts |
41
+ | --limr-font-size / --limr-line-height | Base typography |
42
+ | --limr-font-features | Defaults to 'palt' 1 for proportional punctuation where supported |
43
+ | --limr-emphasis-spacing / --limr-list-item-gap | Trailing emphasis / list spacing |
44
+ | --limr-text / --limr-heading / --limr-secondary / --limr-muted | Text hierarchy |
45
+ | --limr-background / --limr-surface / --limr-border | Backgrounds and borders |
46
+ | --limr-link / --limr-accent / --limr-selection | Links, active state, selection |
47
+ | --limr-content-width / --limr-reader-width | Body / overall width; default 900px / 1800px |
48
+ | --limr-toc-width / --limr-sidebar-width / --limr-column-gap | Side column widths and gaps |
49
+ | --limr-sticky-top | Sticky offset |
50
+ | --limr-reader-bottom-space / --limr-reader-min-height | Short-document height and bottom space |
51
+ | --limr-sidebar-padding-inline / --limr-sidebar-reference-gap | Shared padding (10px) and reference separation (1px) |
52
+ | --limr-overlay-duration / --limr-overlay-morph-duration | Enter/exit (560ms), preview/expanded morph (480ms) |
53
+ | --limr-overlay-z / --limr-toolbar-z | Floating control stacking |
54
+ | --limr-overflow-fade-size | Overflow fade, default 24px; 0px disables |
55
+ | --limr-toolbar-panel-max-height | Maximum toolbar panel height |
56
+ | --limr-tool-icon-size / --limr-mark-size | Toolbar icons / inline markers |
57
+
58
+ --limr-toolbar-available-height, --limr-toolbar-panel-height, and --limr-toc-available-height are runtime measurements, not theme inputs. AnnotationType.sidebar usually supplies local card colors; see [extensions](extensions.md).
59
+
60
+ ## Fonts and package size
61
+
62
+ Local WOFF2 subsets of Maple Mono NF CN v7.9 ship in the package under the derivative name LiMR Maple Mono NF CN, with the OFL license. unicode-range and font-display: swap limit runtime requests, but the complete font assets still occupy about 31MB. base.css avoids browser loading, not npm installation size.
63
+
64
+ Body text does not automatically load Chinese fonts. Use system fallbacks, self-hosted fonts, or a host-chosen service. The Playground's Noto Serif SC belongs to the demo. No font should be assumed to cover every script.
65
+
66
+ Default font/math styles do not require a third-party CDN. Hosts can apply long-lived immutable caching to hashed font URLs; the library cannot set server HTTP headers. Remote image/video/plugin requests depend on content and host configuration.
67
+
68
+ ## Layout and interaction conventions
69
+
70
+ The reading region determines synchronized sidebar height; do not size the page to all cards combined. Modules own content while the shared layout owns references, summaries, and card shells. Avoid duplicate padding or empty-paragraph compensation. Expanded cards animate from preview geometry; reduced-motion preferences disable motion.
71
+
72
+ Dense footer references borrow spare space above them at rest. Hovered or active items take alignment priority, allowing neighboring items to clip at the edges. Marker hover can compress the optional summary to leave room for the reference and heading. Expanded content is bounded by the visible reading region and scrolls within its card. Packed sidebar items still align the prose on hover. Deactivation smoothly restores the resting layout using current card heights, `--limr-motion-slow`, and `--limr-ease`, preserving gaps and page height.
73
+
74
+ Selection retains native browser ranges and keyboard behavior. Paint layers calculate visible fragments only. Code allows partial highlights, math is atomic, and image highlights use borders. Printing hides paint layers while keeping document content. Mobile automation does not certify native iOS/Android handles or physical-device behavior.
75
+
76
+ ## CSS-only themes
77
+
78
+ Export CSS, include it in files, and mark sideEffects as ["**/*.css"]. A theme does not need React or parser runtime dependencies. Scope rules to an appearance name and check light, dark, auto, portals, and two differently themed readers on the same page.
79
+
80
+ Expanded toolbar panels use the full available toolbar width. The action row and available viewport determine its width; expanded content does not contribute intrinsic width or enlarge the toolbar. Highlight options use intrinsic text widths and wrap as whole items, aligned to the start. Multiline underlines need an inner inline span rather than a direct flex child.