@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.
Files changed (97) hide show
  1. package/README.en.md +151 -0
  2. package/README.md +80 -127
  3. package/benchmarks/results/layout.json +2593 -0
  4. package/benchmarks/results/markstream-code.json +1823 -0
  5. package/benchmarks/results/markstream-reading-repeat.json +1152 -0
  6. package/benchmarks/results/markstream.json +12486 -0
  7. package/benchmarks/results/streaming-browser.json +77 -0
  8. package/benchmarks/results/streaming.json +357 -0
  9. package/dist/base.css +93 -10
  10. package/dist/{chunk-7JF5HHVF.js → chunk-BG5QUQFR.js} +340 -72
  11. package/dist/chunk-BG5QUQFR.js.map +1 -0
  12. package/dist/chunk-BVHHOVMT.js +179 -0
  13. package/dist/chunk-BVHHOVMT.js.map +1 -0
  14. package/dist/chunk-FJZCQY2S.js +155 -0
  15. package/dist/chunk-FJZCQY2S.js.map +1 -0
  16. package/dist/chunk-FSI5OTBW.js +1 -0
  17. package/dist/{chunk-ETHLPBKQ.js → chunk-GU2RPESF.js} +238 -131
  18. package/dist/chunk-GU2RPESF.js.map +1 -0
  19. package/dist/{chunk-65V5OZ4G.js → chunk-SUAAUGAX.js} +1350 -511
  20. package/dist/chunk-SUAAUGAX.js.map +1 -0
  21. package/dist/core/index.d.ts +6 -3
  22. package/dist/core/index.js +15 -5
  23. package/dist/extensions/index.d.ts +30 -5
  24. package/dist/extensions/index.js +15 -3
  25. package/dist/index.d.ts +4 -4
  26. package/dist/index.js +31 -8
  27. package/dist/{engine-43u8B4yt.d.ts → knuth-plass-C8mgFm7W.d.ts} +67 -1
  28. package/dist/knuth-plass-layout-CHQCWKB5.js +449 -0
  29. package/dist/knuth-plass-layout-CHQCWKB5.js.map +1 -0
  30. package/dist/react/index.d.ts +7 -5
  31. package/dist/react/index.js +7 -3
  32. package/dist/styles.css +93 -10
  33. package/dist/{translations-BliabrPd.d.ts → translations-BXzSwLBJ.d.ts} +49 -4
  34. package/docs/README.md +13 -0
  35. package/docs/api.md +5 -158
  36. package/docs/assets/rendered.svg +9 -0
  37. package/docs/assets/source.svg +9 -0
  38. package/docs/en/README.md +28 -0
  39. package/docs/en/ai-skills.md +57 -0
  40. package/docs/en/annotations.md +86 -0
  41. package/docs/en/api.md +115 -0
  42. package/docs/en/architecture.md +41 -0
  43. package/docs/en/extensions.md +208 -0
  44. package/docs/en/getting-started.md +69 -0
  45. package/docs/en/integration.md +154 -0
  46. package/docs/en/modules.md +84 -0
  47. package/docs/en/performance.md +93 -0
  48. package/docs/en/plugin-packages.md +68 -0
  49. package/docs/en/styles.md +99 -0
  50. package/docs/en/syntax.md +455 -0
  51. package/docs/extensions.md +5 -306
  52. package/docs/implementation.md +5 -76
  53. package/docs/performance.md +5 -81
  54. package/docs/screenshots/blocks.png +0 -0
  55. package/docs/screenshots/dark.png +0 -0
  56. package/docs/screenshots/desktop.png +0 -0
  57. package/docs/screenshots/plugin-mobile.png +0 -0
  58. package/docs/styles.md +5 -100
  59. package/docs/visual-upgrade.md +5 -145
  60. package/docs/zh-CN/README.md +28 -0
  61. package/docs/zh-CN/ai-skills.md +57 -0
  62. package/docs/zh-CN/annotations.md +86 -0
  63. package/docs/zh-CN/api.md +115 -0
  64. package/docs/zh-CN/architecture.md +41 -0
  65. package/docs/zh-CN/extensions.md +210 -0
  66. package/docs/zh-CN/getting-started.md +69 -0
  67. package/docs/zh-CN/integration.md +154 -0
  68. package/docs/zh-CN/modules.md +84 -0
  69. package/docs/zh-CN/performance.md +93 -0
  70. package/docs/zh-CN/plugin-packages.md +68 -0
  71. package/docs/zh-CN/styles.md +99 -0
  72. package/docs/zh-CN/syntax.md +457 -0
  73. package/examples/notes.tsx +18 -11
  74. package/examples/packages/paper-theme/index.css +25 -0
  75. package/examples/packages/paper-theme/package.json +7 -0
  76. package/examples/packages/reading-tools/index.d.ts +2 -0
  77. package/examples/packages/reading-tools/index.js +25 -0
  78. package/examples/packages/reading-tools/package.json +16 -0
  79. package/package.json +17 -5
  80. package/skills/limr-plugin-development/SKILL.md +22 -0
  81. package/skills/limr-plugin-development/agents/openai.yaml +4 -0
  82. package/skills/limr-plugin-development/assets/plugin-template/README.md +30 -0
  83. package/skills/limr-plugin-development/assets/plugin-template/package.json +36 -0
  84. package/skills/limr-plugin-development/assets/plugin-template/src/index.tsx +50 -0
  85. package/skills/limr-plugin-development/assets/plugin-template/src/styles.css +9 -0
  86. package/skills/limr-plugin-development/assets/plugin-template/tsconfig.json +20 -0
  87. package/skills/limr-plugin-development/assets/plugin-template/tsup.config.ts +8 -0
  88. package/skills/limr-plugin-development/references/contract.md +51 -0
  89. package/skills/limr-use/SKILL.md +23 -0
  90. package/skills/limr-use/agents/openai.yaml +4 -0
  91. package/skills/limr-use/references/integration.md +49 -0
  92. package/skills/limr-use/references/syntax.md +20 -0
  93. package/dist/chunk-65V5OZ4G.js.map +0 -1
  94. package/dist/chunk-7JF5HHVF.js.map +0 -1
  95. package/dist/chunk-ETHLPBKQ.js.map +0 -1
  96. package/dist/chunk-V2OG4Y3F.js +0 -1
  97. /package/dist/{chunk-V2OG4Y3F.js.map → chunk-FSI5OTBW.js.map} +0 -0
@@ -0,0 +1,86 @@
1
+ # Annotations and selection
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/annotations) for the best reading experience.
4
+
5
+ [Documentation](README.md) · [简体中文](../zh-CN/annotations.md)
6
+
7
+ ## Data model
8
+
9
+ Annotation stores data, AnnotationType registers presentation/loading, and AnnotationOccurrence describes one location. One annotation can have multiple anchors; one citation marker can refer to several annotations.
10
+
11
+ ```ts
12
+ import type { Annotation } from '@linloir/limr';
13
+
14
+ export const annotations: readonly Annotation[] = [{
15
+ id: 'paper-reference',
16
+ type: 'citation',
17
+ data: { title: 'Paper', url: 'https://example.com/paper' },
18
+ anchors: [{
19
+ range: { start: 0, end: 4 },
20
+ documentId: 'article',
21
+ quote: 'LiMR',
22
+ }],
23
+ }, {
24
+ id: 'highlight-1',
25
+ type: 'highlight',
26
+ range: { start: 0, end: 4 },
27
+ data: { color: '#F2C744', style: 'background' },
28
+ }];
29
+ ```
30
+
31
+ The example requires source to begin with LiMR and documentId to be article. range is shorthand for one anchor; anchors takes precedence. All ranges address the **unmodified input**, using UTF-16 offsets with an exclusive end, not UTF-8 bytes or Unicode code points.
32
+
33
+ Persist source, a stable documentId, annotation ID/type/data/anchors, and your application document version when needed. anchor.revision belongs to the current engine session, not a cross-session identity. If quote no longer matches the range, relocation succeeds only for one unique match in the source. Missing/ambiguous matches or a mismatched documentId skip that anchor. Without quote, a mismatched revision also skips it. This is not automatic position transformation for collaborative editing.
34
+
35
+ ## Three selection forms
36
+
37
+ | Field | Meaning |
38
+ | --- | --- |
39
+ | text | Readable selected text; settled atomic content uses its complete original source |
40
+ | raw | Mapped original ranges and source |
41
+ | syntax | Absorbs adjacent standard Markdown delimiters at each edge, without selecting extra words or outer custom tags |
42
+ | blocks | Ranges and source for touched blocks |
43
+ | quality | exact, atomic, or unmapped |
44
+ | settled | False while a pointer selection is changing |
45
+ | documentId / revision | Owning document snapshot |
46
+
47
+ Each result has ranges and sourceText. Notes usually use syntax.ranges. Store document.source.slice(start, end) as quote for **every range**. Code supports partial selection; math and images are atomic. Do not infer offsets from DOM textContent lengths: entities, escapes, and emoji make those coordinates differ.
48
+
49
+ The default selection toolbar and browser copy serialize atomic content as a whole, retaining its original delimiters (`$…$`, `$$…$$`, image syntax, Mermaid fences/directives, or plugin block tags). Surrounding ordinary text keeps its rendered spelling and explicit line breaks. This also applies when reopening an existing highlight. A plugin's `selection: 'atomic'` covers its mapped attributes, children, and nested atoms; block-local copy buttons may offer their own specialized format.
50
+
51
+ ## Highlights and commands
52
+
53
+ highlight needs no extra AnnotationType. Its data supports color, style (background / underline / dotted / wavy), and hidden. Deleting content annotations differs from hiding their highlight.
54
+
55
+ | Command | Behavior |
56
+ | --- | --- |
57
+ | addAnnotation(annotation, options?) | Adds/replaces an ID; ordinary content annotations also get a shared persistent highlight by default |
58
+ | updateAnnotation(id, partial) | Shallowly merges fields; data is replaced as a whole, so retain needed fields |
59
+ | removeAnnotation(id) | Removes that annotation, preserving other annotations and shared highlights |
60
+ | removeAnnotationsAt(ranges) | Removes annotations/highlights at exact matching fragments, retaining other anchors |
61
+ | editAnnotation(id / null) | Changes shared editing state; setting an ID also activates its card |
62
+ | activateAnnotation(id / null) | Opens the annotation's first occurrence, or accepts an explicit occurrence ID |
63
+
64
+ addAnnotation accepts highlight: false to suppress automatic persistent highlighting, explicit HighlightData to choose its style, and edit: true to enter editing. Types configured with highlight: 'hover' do not add persistent highlights by default, but an explicit appearance still creates one. Default toolbar actions are palette, add/hide highlight, copy, and delete. actions replaces built-in actions; plugin actions are still merged in.
65
+
66
+ ## Citation hover highlights
67
+
68
+ Citations default to highlight: 'hover'. Hovering a marker or sidebar item, or activating a reference, paints the referenced text with an 18% tint of the type's color behind the text. Clicking pins the highlight after the pointer leaves; it disappears once the reference is neither active nor hovered. Active and hovered references to the same paragraph do not stack their tints. This feedback never updates annotations or saved hidden settings. Embedded refs use their nearest paragraph, heading, list item, or table cell. External references use their own range; the first line anchors sidebar alignment.
69
+
70
+ Sidebar alignment translates the prose without changing the page's scroll range. Paint updates throughout that animation and on completion or cancellation, so text entering from outside the viewport receives the current hover or active feedback.
71
+
72
+ Disable it with replaceExtension(standard(), citation({ highlight: false })). Other types can opt into highlight: 'hover'.
73
+
74
+ ## Missing plugins
75
+
76
+ The current implementation **does not silently ignore unknown annotation types**. It retains data/ranges and may show a generic dot marker. Without Content, the card shows the localized unavailable label and type name. A missing citation definition may also show unavailable content. Install the matching plugin, or let the application decide which annotations to hide temporarily.
77
+
78
+ If filtering unsupported types, retain the full persisted array and merge hidden data back when accepting edits, so disabling a plugin cannot discard data. Playground plugin checkboxes currently toggle action entries while retaining card renderers; they do not simulate uninstalling a plugin.
79
+
80
+ ## Cards and async resources
81
+
82
+ Modules register Content for annotation content, overlay.Card for the whole floating card interior, and sidebar.Preview for a local collapsed summary. AnnotationMarkdown reuses Markdown configuration with an isolated selection scope.
83
+
84
+ load(annotation, { signal }) runs when content is displayed, sharing requests and a bounded cache within one reader. Separate readers are isolated. Preview never invokes load. Content receives data, loading, error, retry, editing, presentation, and expanded; the module renders loading/error UI. Refresh with useLiMR().invalidateAnnotation(id).
85
+
86
+ Action signals abort when the reader is disposed, not when the menu closes. Preserve question fields when replacing data with streamed answers. Stopping generation, retries, authentication, conversation state, and storage belong to the application adapter. See the [assistant example](../../examples/assistant.tsx) and [extension development](extensions.md).
package/docs/en/api.md ADDED
@@ -0,0 +1,115 @@
1
+ # API reference
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/api) for the best reading experience.
4
+
5
+ [Documentation](README.md) · [简体中文](../zh-CN/api.md)
6
+
7
+ ## Entry points
8
+
9
+ | Entry | Contents |
10
+ | --- | --- |
11
+ | @linloir/limr | All public core, React, and extension APIs |
12
+ | @linloir/limr/core | DOM/React-free engine, selection calculations, tree helpers, caches, data types |
13
+ | @linloir/limr/react | Components, hooks, locales, React types |
14
+ | @linloir/limr/extensions | Module factories, definitions, selection actions, AnnotationMarkdown |
15
+ | styles.css / styles/base.css / fonts.css / math.css | Public styles, prefixed by the package name |
16
+ | @linloir/limr/package.json | Package metadata |
17
+
18
+ This is an API guide. Exact TypeScript declarations ship with the package; plugins should use the installed version's declarations.
19
+
20
+ ## Shared document configuration
21
+
22
+ Reader, Markdown, and DocumentProvider accept:
23
+
24
+ | Prop | Default / meaning |
25
+ | --- | --- |
26
+ | source | Empty string; original Markdown |
27
+ | streaming | false |
28
+ | documentId | Generated; explicit values should be stable and unique on the page |
29
+ | extensions | standard(); the complete extension list |
30
+ | plugins | Appended to the default or explicit list |
31
+ | parameters | Document-wide module parameter overrides |
32
+ | components | NodeRenderer overrides by AST type/custom tag |
33
+ | annotationTypes | Add/override presentation by type |
34
+ | annotations | Internal state if omitted; controlled if supplied |
35
+ | onAnnotationsChange | Next external annotation array, including highlights; required to accept controlled edits |
36
+ | theme | light, dark, or auto |
37
+ | appearance | classic; CSS scope propagated to portals |
38
+ | locale | english; accepts chinese or Partial<LiMRLocale> |
39
+ | justify | false; justify prose/list text |
40
+ | typography | standard (default), knuth-plass, or justify; legacy CSS modes pretty / normal remain available |
41
+ | knuthPlass | TeX paragraph parameters; see [styles](styles.md) |
42
+ | rendererOverrides | Ordered, reader-scoped rules with type, component and optional when predicate |
43
+ | streamEffect | Increment animation override; false disables |
44
+ | scrollRoot | null/omitted means window |
45
+ | scrollOffset | 24px |
46
+ | onDocumentChange | DocumentSnapshot after updates |
47
+ | onSelectionChange | SelectionSnapshot or null |
48
+ | onAnnotationClick | Activated AnnotationOccurrence |
49
+ | onAnnotationHover | Occurrence or null |
50
+ | onHighlightClick / onHighlightHover | Highlight Annotation; hover exit is null |
51
+
52
+ The default locale is english. Missing partial keys fall back to English. locale affects built-in controls that use LiMRLocale, not document text or third-party copy. Some auxiliary labels (for example Copy LaTeX and translation switch hints) are currently fixed by their modules. The Playground reader-language control demonstrates Chinese/English switching and saves that preference separately in browser storage.
53
+
54
+ ## Components
55
+
56
+ | Component | Extra props and defaults |
57
+ | --- | --- |
58
+ | Reader | toc=true, sidebar=true, fillHeight=true, sidebarBreakpoint=1120, actions, followOutput=false, className/style |
59
+ | Markdown | interactive=true, followOutput=false, className/style |
60
+ | DocumentProvider | children; supplies context to composed views |
61
+ | TableOfContents | title, maxDepth=6, followActive=true, topInset=24, updateHash=false, className/style |
62
+ | AnnotationSidebar | sync='document' / 'none', gap=14, positioner=alignSidebar, renderCard, className/style |
63
+ | AnnotationOverlay | disabled=false, portalRoot, previewWidth=340, maxHeight='80dvh' |
64
+ | SelectionToolbar | actions, portalRoot, maxPanelHeight=260, render, className/style |
65
+ | AnnotationCard | AnnotationOverlayCardProps plus required title and optional bodyLabel |
66
+ | AnnotationContent | annotationId, presentation, expanded=true, enabled=true |
67
+ | AnnotationGroup | occurrence, presentation, expanded=true, enabled=true |
68
+ | AnnotationMarkdown | MarkdownProps with required source; interactive=false by default |
69
+ | InlineMarker | Required label, color, children, button props/ref |
70
+ | ToolIcon | name and SVG props; e.g. note, copy, highlight, comment, zoom-in, zoom-out, source, image |
71
+
72
+ Shared views/hooks require a provider, except standalone Markdown and Reader. AnnotationCard is an interior template, not its own portal. sidebar.renderCard receives occurrence/active/activate; toolbar.render receives context/actions/run.
73
+
74
+ `SidebarPositioner` receives `{ id, anchorTop, height }[]` and `{ activeId, gap, height?, prioritizeActive? }`, returning a map of IDs to top positions. anchorTop is the first line of referenced text; prioritizeActive distinguishes hovered or active items from the item selected by reading position. The default alignSidebar packs footer cards at rest and prioritizes the current item during interaction.
75
+
76
+ ## Hooks
77
+
78
+ | Hook | Result |
79
+ | --- | --- |
80
+ | useDocument() | Complete snapshot |
81
+ | useDocumentSelector(select) | Selected value; unchanged references avoid rerenders |
82
+ | useLocale() | Merged locale; built-in language is en / zh-CN for plugin dictionaries |
83
+ | useSelection() | Selection or null |
84
+ | useAnnotation(id) | Annotation or undefined |
85
+ | useAnnotations() | Annotations Map and occurrences, combining embedded and external data |
86
+ | useAnnotationState() | Active/hovered occurrence IDs plus activate/hover |
87
+ | useAnchor(occurrenceId) | Element, viewport rect, contentRect; null until mounted |
88
+ | useRendererParameter(name) | Host override or last registered default |
89
+ | useLiMR() | Engine, commands, signal, clearSelection, scrollToHeading, scrollToAnnotation, invalidateAnnotation |
90
+
91
+ useRuntime and useTheme are not public APIs. useLocale, locale.language, and localized annotation labels are available from 1.0.0. See [annotations](annotations.md) for commands and [extensions](extensions.md) for renderer/action contracts.
92
+
93
+ ## Core engine
94
+
95
+ ```ts
96
+ import { createDocument, selectionFromRange } from '@linloir/limr/core';
97
+
98
+ const engine = createDocument('# Hello\n\nText', { id: 'core-doc', streaming: true });
99
+ engine.append('\n\nMore text.');
100
+ const document = engine.finish();
101
+ const selection = selectionFromRange(document, { start: 9, end: 13 });
102
+ console.log(document.headings, selection.syntax.sourceText);
103
+ ```
104
+
105
+ createDocument(source?, { id?, extensions?, streaming? }) returns DocumentEngine. Read with getSnapshot(); subscribe(listener) returns an unsubscribe function. update(source, { streaming? }) replaces the complete input, append(chunk) appends in streaming mode, finish() finalizes, and setExtensions(list) changes parsing configuration.
106
+
107
+ Core defaults to extensions=[]. Use SyntaxExtension with micromark/mdast for a strictly React-free parser; importing standard from the React extension entry introduces React dependencies.
108
+
109
+ DocumentSnapshot contains id, revision, source, streaming, blocks, headings, definitions, footnotes, footnoteOrder, annotations, occurrences, diagnostics, and metrics. Its annotations are **parsed from source**. Use useAnnotations for merged external data.
110
+
111
+ Other core exports include walk, textContent, rangeOf, intersects, safeUrl, mapText, selectionFromRange, replaceExtension, resolveAnnotationAnchors, annotationReferenceRange, sameRange, defaultHighlight, LruCache, and ResourceCache. ResourceCache.acquire returns promise/release; invalidate clears a key or everything.
112
+
113
+ `inlineHighlights({ color?, component? })` and `footnotes(annotationTypeOptions?)` are included in `standard()`. `HighlightData.readOnly` limits the default highlight toolbar to copying and a hint; `AnnotationOccurrence.referenceRange` separates a marker's source location from the text it references.
114
+
115
+ Core also exports `knuthPlass(items, lengths, options?)`, `prepareKnuthPlass(items)`, `texBadness`, and their types. The prepared function reuses prefix sums when solving the same paragraph at multiple widths.
@@ -0,0 +1,41 @@
1
+ # Architecture and behavior
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/architecture) for the best reading experience.
4
+
5
+ [Documentation](README.md) · [简体中文](../zh-CN/architecture.md)
6
+
7
+ ## Three public layers
8
+
9
+ 1. core owns original source, AST, blocks, headings, embedded references, diagnostics, and selection coordinates, without a React/DOM runtime dependency.
10
+ 2. React connects document, interaction, layout, external annotations, and resource state to views.
11
+ 3. extensions implements defaults through the same public contract available to applications; modules can be appended, replaced, or removed.
12
+
13
+ Docs navigation and Playground persistence/loading/language menus belong to the demo application, not the LiMR core.
14
+
15
+ ## Append parsing
16
+
17
+ Streaming reparses from the unstable final top-level block, reusing stable prefixes, line indexes, and node semantic indexes. Growing code fences can use a literal fast path. Finalization can also reparse only the tail rather than always parsing the whole document. Late Markdown reference/footnote definitions, middle edits, document-scoped transforms, or extension changes can still require a full parse. Long paragraphs, lists, and complex unclosed tags retain tail-parsing costs.
18
+
19
+ `incomplete` identifies an unclosed node; `pending` describes its growing top-level block. Modules can render progressively or provide `streaming.Loading`. Incomplete headers and inline syntax markers are deferred. Increment animations affect new content; temporary wrappers merge after animation completion once native selection leaves them. Streaming code retains existing text nodes, and final highlighting waits while its text is selected.
20
+
21
+ Public source always retains the original input. Internally, the parser inserts markers for two-colon components and incomplete syntax, then maps positions back through sparse offsets. These internal markers never enter copied text, annotations, or persisted data. Custom transforms must preserve original UTF-16 positions and honor locality. Embedded references and application annotations enter separately and merge in the React layer.
22
+
23
+ ## Typography and source mapping
24
+
25
+ Standard mode uses browser layout by default. KP loads on the client: preparation caches glyph measurements, hyphenation, legal breakpoints, and prefix sums; solving runs TeX paragraph dynamic programming. Stable paragraphs near the viewport take priority and retain their DOM offscreen. Width changes reuse preparation and text containers. Selected paragraphs keep their native Range and defer layout until selection clears. Original source offsets and annotations remain unchanged; see [styles](styles.md) and [performance](performance.md).
26
+
27
+ Atomic selection text serializes complete original source; surrounding ordinary text retains its rendered spelling and explicit line breaks. Mapped attributes, children, and nested atoms follow the enclosing plugin block’s atomic selection contract.
28
+
29
+ ## State and resources
30
+
31
+ Update configuration immutably and keep static references stable. Scrolling and parameter updates do not require parsing. A provider manages one primary body; document state and annotation requests are isolated between providers.
32
+
33
+ Annotation loading uses bounded per-reader caching; releasing consumers can cancel unused requests. Pure Shiki token results use a bounded module-level cache, separate from user annotation data. Mermaid uses bounded result caching and serializes global initialization with rendering so readers using different themes cannot interfere. The library supplies no model service, authentication, database, or automatic persistence.
34
+
35
+ ## Fallback behavior
36
+
37
+ Unknown HTML/custom tags remain inert source text. In the default configuration, unknown directives render their children. Unknown annotation types retain data and show generic markers/unavailable content rather than silently disappearing. Syntax recovery, transform diagnostics, and client renderer boundaries have distinct scopes; see [extensions](extensions.md).
38
+
39
+ ## Validation boundaries
40
+
41
+ The repository includes parser/offset/SSR unit tests, real Chromium interaction regressions, and separate React 18/19 npm-consumer checks. Intercepted media, mobile viewport simulation, and performance fixtures are not proof of remote playback, physical-device acceptance, or universal performance.
@@ -0,0 +1,208 @@
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 existing nodes or a matching subset | components / rendererOverrides |
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>`, paired `::notice{title="Tip"}` / `::`, and `:::` containers. 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
+ | incomplete | Whether this particular syntax node is unclosed |
63
+ | renderChildren(nodes, { trailing? }) | Rearrange an existing subtree or append an action-only marker |
64
+ | renderValue(tokens?) | Render literals/tokens while preserving selection and annotation insertion |
65
+ | renderAttribute(name) | Render attribute text with original source coordinates |
66
+
67
+ 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.
68
+
69
+ ## Plugins own card presentation
70
+
71
+ ```tsx
72
+ import { AnnotationCard, AnnotationMarkdown, defineAnnotation, ToolIcon } from '@linloir/limr';
73
+
74
+ export const note = defineAnnotation<{ text: string }>({
75
+ type: 'example:note',
76
+ label: 'Note',
77
+ icon: <ToolIcon name="note" />,
78
+ color: '#6B9BD2',
79
+ Content: ({ data, loading, error, retry }) => loading ? <p>Loading…</p>
80
+ : error ? <button onClick={retry}>Retry</button>
81
+ : <AnnotationMarkdown source={data?.text ?? ''} />,
82
+ sidebar: {
83
+ Preview: ({ data }) => data?.text ? <span>{data.text}</span> : null,
84
+ previewLines: 2,
85
+ },
86
+ overlay: {
87
+ Card: props => <AnnotationCard {...props} title="Note" />,
88
+ previewWidth: 320,
89
+ expand: { placement: 'center', width: 640, maxHeight: '80dvh' },
90
+ },
91
+ });
92
+ ```
93
+
94
+ 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.
95
+
96
+ 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.
97
+
98
+ 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.
99
+
100
+ ## Selection actions
101
+
102
+ ```tsx
103
+ import { defineExtension, ToolIcon } from '@linloir/limr';
104
+
105
+ export const savePassage = defineExtension({
106
+ name: 'example:save-passage',
107
+ actions: [{
108
+ id: 'example:save-passage', label: 'Save passage', icon: <ToolIcon name="note" />,
109
+ visible: ({ selection }) => selection.settled && selection.quality !== 'unmapped',
110
+ run(context) {
111
+ const id = `example-note-${crypto.randomUUID()}`;
112
+ context.addAnnotation({
113
+ id, type: 'example:note', data: { text: context.selection.text },
114
+ anchors: context.selection.syntax.ranges.map(range => ({
115
+ range, documentId: context.document.id,
116
+ quote: context.document.source.slice(range.start, range.end),
117
+ })),
118
+ });
119
+ context.activateAnnotation(id);
120
+ context.clearSelection();
121
+ },
122
+ }],
123
+ });
124
+ ```
125
+
126
+ 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.
127
+
128
+ ## Parameters
129
+
130
+ ```tsx
131
+ import { defineExtension, useRendererParameter } from '@linloir/limr';
132
+
133
+ declare module '@linloir/limr' {
134
+ interface RendererParameters { exampleDetail?: 'short' | 'full' }
135
+ }
136
+
137
+ export const glossary = defineExtension({
138
+ name: 'example:glossary', tags: ['glossary'], scope: 'local',
139
+ parameters: { exampleDetail: 'short' },
140
+ components: {
141
+ glossary: ({ children }) => <aside data-detail={useRendererParameter('exampleDetail')}>
142
+ {children}
143
+ </aside>,
144
+ },
145
+ });
146
+ ```
147
+
148
+ 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.
149
+
150
+ ## Parsing and streaming constraints
151
+
152
+ SyntaxExtension supports micromark, mdast, tags, directives, display, selection, literal, 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. Plugins must not regex-rewrite source before parsing.
153
+
154
+ 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. Registered incomplete tags and containers carry node.incomplete and are retried on append; unfinished headers stay hidden or use the module loading view. Invalid JSX can still fall back with diagnostics. 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.
155
+
156
+ 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).
157
+
158
+ ## Plugin localization
159
+
160
+ 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.
161
+
162
+ ## Streaming presentation
163
+
164
+ `pending` describes a growing top-level block; `incomplete` describes this particular unclosed syntax node. Every registered tag also accepts component directive syntax. `defineBlock` accepts `streaming.Loading`; omit it to render progressively, using `incomplete` in your component. Component identity is retained while children grow. Default definition, question, extras, video, figure and callout directives use a small themed ring; Mermaid waits for closure before asynchronously drawing. Code, math, GFM and translations show usable partial content. Blockquote-style callouts can grow progressively once their header is complete. Unclosed citation definitions do not register incomplete data.
165
+
166
+ ```tsx
167
+ import { defineBlock, defineExtension } from '@linloir/limr';
168
+
169
+ export const chart = defineBlock({
170
+ name: 'chart',
171
+ component: ({ children }) => <section>{children}</section>,
172
+ streaming: { Loading: () => <span role="status">Preparing chart…</span> },
173
+ });
174
+ export const streamMotion = defineExtension({
175
+ name: 'example:stream-motion',
176
+ streaming: {
177
+ effect: {
178
+ className: 'my-stream-increment',
179
+ keyframes: [{ opacity: 0.25 }, { opacity: 1 }],
180
+ timing: { duration: 320, easing: 'ease-out' },
181
+ },
182
+ },
183
+ });
184
+ ```
185
+
186
+ The last extension providing `streaming.effect` wins. The reader's `streamEffect` overrides it; `false` disables the effect. Keyframes and timing use the Web Animations API. Reduced-motion preferences suppress new animations. Default text/block entrances change opacity without adding a background tint. Text wrappers are compacted after completion, once the native selection leaves them. Use `children`, `renderChildren` and `renderValue` to retain this behavior; a plugin that replaces its own DOM owns its selection and animation behavior.
187
+
188
+ `SyntaxExtension.literal` protects directive bodies from Markdown/JSX interpretation. Literal modules read their original `source` or transform the resulting component. It does not execute attribute expressions, YAML or JavaScript.
189
+
190
+ ## Conditional component replacement
191
+
192
+ A module's `rendererOverrides` can restrict replacement by node type, language, ancestors or streaming state. Rules apply only inside that Reader. The last matching rule wins; Reader rules take precedence over extension rules and component mappings. Unmatched nodes retain their existing renderer.
193
+
194
+ ```tsx
195
+ import { defineExtension, type NodeRenderer } from '@linloir/limr';
196
+
197
+ const JavaScriptCode: NodeRenderer = ({ renderValue }) => <pre>{renderValue()}</pre>;
198
+ export const customCode = defineExtension({
199
+ name: 'example:javascript-code',
200
+ rendererOverrides: [{
201
+ type: 'code',
202
+ when: (node, context) => node.lang === 'js' && !context.incomplete,
203
+ component: JavaScriptCode,
204
+ }],
205
+ });
206
+ ```
207
+
208
+ `when` also receives `pending` and `ancestors`. An explicit replacement owns its intermediate rendering while retaining LiMR's source wrapper, atomic selection and error boundary. Use `renderValue`, `renderAttribute` and `renderChildren` to preserve mappings. These restrictions scope rendering rules; they are not a JavaScript security sandbox.
@@ -0,0 +1,69 @@
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
+ Typography defaults to `standard`; `knuth-plass` and `justify` are also available, as shown in [integration](integration.md). Defaults include Mermaid, nested callouts, `==highlights==`, and footnote cards; do not append duplicate modules. See [built-in modules](modules.md).
40
+
41
+ ## Body only
42
+
43
+ ```tsx
44
+ import { Markdown } from '@linloir/limr';
45
+ import '@linloir/limr/styles/base.css';
46
+
47
+ export function Excerpt() {
48
+ return <Markdown source="Some **Markdown**." />;
49
+ }
50
+ ```
51
+
52
+ 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.
53
+
54
+ ## Choose styles
55
+
56
+ | Import | Includes |
57
+ | --- | --- |
58
+ | `@linloir/limr/styles.css` | Default theme, layout, interaction, and Maple code fonts |
59
+ | `@linloir/limr/styles/base.css` | The same styling without importing Maple font CSS |
60
+ | `@linloir/limr/fonts.css` | Maple fonts only, for separate use with base.css |
61
+ | `@linloir/limr/math.css` | KaTeX styles and local math fonts |
62
+
63
+ 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.
64
+
65
+ ## Next steps
66
+
67
+ - Read [default syntax](syntax.md) to distinguish built-in content modules from application annotation plugins.
68
+ - Read [application integration](integration.md) for streaming, scroll containers, and SSR.
69
+ - Install the [limr-use Skill](ai-skills.md) for AI-assisted integration.