@linloir/limr 1.0.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 +36 -8
- package/README.md +36 -8
- 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 +83 -3
- package/dist/{chunk-TTISGBST.js → chunk-BG5QUQFR.js} +327 -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-N77VD2YR.js → chunk-GU2RPESF.js} +145 -83
- package/dist/chunk-GU2RPESF.js.map +1 -0
- package/dist/{chunk-3ILUZKDB.js → chunk-SUAAUGAX.js} +1280 -501
- package/dist/chunk-SUAAUGAX.js.map +1 -0
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +13 -5
- package/dist/extensions/index.d.ts +30 -5
- package/dist/extensions/index.js +15 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +27 -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 +5 -4
- package/dist/react/index.js +5 -3
- package/dist/styles.css +83 -3
- package/dist/{translations-DHnYMg4h.d.ts → translations-BXzSwLBJ.d.ts} +39 -2
- package/docs/en/README.md +8 -6
- package/docs/en/annotations.md +3 -1
- package/docs/en/api.md +9 -1
- package/docs/en/architecture.md +11 -3
- package/docs/en/extensions.md +53 -4
- package/docs/en/getting-started.md +2 -0
- package/docs/en/integration.md +26 -2
- package/docs/en/modules.md +9 -5
- package/docs/en/performance.md +42 -2
- package/docs/en/styles.md +21 -2
- package/docs/en/syntax.md +118 -15
- package/docs/zh-CN/README.md +8 -6
- package/docs/zh-CN/annotations.md +3 -1
- package/docs/zh-CN/api.md +9 -1
- package/docs/zh-CN/architecture.md +11 -3
- package/docs/zh-CN/extensions.md +55 -4
- package/docs/zh-CN/getting-started.md +2 -0
- package/docs/zh-CN/integration.md +26 -2
- package/docs/zh-CN/modules.md +9 -5
- package/docs/zh-CN/performance.md +42 -2
- package/docs/zh-CN/styles.md +21 -2
- package/docs/zh-CN/syntax.md +120 -15
- package/package.json +9 -1
- package/skills/limr-plugin-development/references/contract.md +9 -3
- package/skills/limr-use/references/integration.md +11 -3
- package/skills/limr-use/references/syntax.md +8 -5
- package/dist/chunk-3ILUZKDB.js.map +0 -1
- package/dist/chunk-N77VD2YR.js.map +0 -1
- package/dist/chunk-TTISGBST.js.map +0 -1
- package/dist/chunk-V2OG4Y3F.js +0 -1
- /package/dist/{chunk-V2OG4Y3F.js.map → chunk-FSI5OTBW.js.map} +0 -0
package/dist/styles.css
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/* LiMR classic skin. Local assets only; unlayered application CSS wins. */
|
|
2
2
|
@import "./fonts/maple.css";
|
|
3
|
+
/* Viewport coordinates change on every scroll. They belong to the mask host,
|
|
4
|
+
never its text descendants; inheriting them invalidates the entire article. */
|
|
5
|
+
@property --limr-fade-start { syntax: '<length>'; inherits: false; initial-value: 0px; }
|
|
6
|
+
@property --limr-fade-end { syntax: '<length>'; inherits: false; initial-value: 0px; }
|
|
7
|
+
|
|
3
8
|
@layer limr {
|
|
4
9
|
:where([data-limr-appearance]) {
|
|
5
10
|
--limr-ease: cubic-bezier(.16, 1, .3, 1);
|
|
@@ -62,8 +67,21 @@
|
|
|
62
67
|
--limr-link: #8bb8e3; --limr-selection: #6e88c955; --limr-shadow: #0006;
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
|
-
.limr-prose { width: 100%; min-width: 0; font-size: var(--limr-font-size, 1rem); line-height: var(--limr-line-height, 1.8); font-feature-settings: var(--limr-font-features,
|
|
70
|
+
.limr-prose { width: 100%; min-width: 0; font-size: var(--limr-font-size, 1rem); line-height: var(--limr-line-height, 1.8); font-feature-settings: var(--limr-font-features, normal); text-spacing-trim: var(--limr-punctuation-spacing, space-all); overflow-wrap: break-word; --limr-paragraph-align: start; }
|
|
66
71
|
.limr-prose[data-limr-justify='true'] { --limr-paragraph-align: justify; }
|
|
72
|
+
.limr-prose[data-limr-typography='pretty'] :where(p,.limr-paragraph,li,figcaption) { text-wrap: pretty; }
|
|
73
|
+
.limr-prose[data-limr-typography='pretty'] :where(h1,h2,h3,h4,h5,h6) { text-wrap: balance; }
|
|
74
|
+
/* Stable native wrapping prevents earlier lines moving for every token. */
|
|
75
|
+
.limr-prose[data-limr-typography='pretty'] :where([data-pending], [data-pending] p, [data-pending] li, [data-pending] .limr-paragraph) { text-wrap: wrap; text-wrap: stable; }
|
|
76
|
+
.limr-block-loading { display: flex; align-items: center; min-height: 3rem; padding-block: .5rem; }
|
|
77
|
+
.limr-loading { display: inline-flex; flex: none; width: 22px; height: 22px; line-height: 0; color: var(--limr-secondary, #7b7975); }
|
|
78
|
+
.limr-loading svg { display: block; width: 100%; height: 100%; overflow: visible; }
|
|
79
|
+
.limr-loading circle { fill: none; stroke-width: 4; }
|
|
80
|
+
.limr-loading-track { stroke: currentColor; opacity: .17; }
|
|
81
|
+
.limr-loading-orbit { transform-box: view-box; transform-origin: 16px 16px; animation: limr-loading-orbit 1.55s cubic-bezier(.65,0,.35,1) infinite; }
|
|
82
|
+
.limr-loading-arc { stroke: currentColor; stroke-linecap: round; stroke-dasharray: 3 97; transform: rotate(-90deg); transform-box: view-box; transform-origin: 16px 16px; animation: limr-loading-arc 1.55s cubic-bezier(.65,0,.35,1) infinite; }
|
|
83
|
+
@keyframes limr-loading-orbit { 0% { transform: rotate(0deg); } 50% { transform: rotate(300deg); } 100% { transform: rotate(720deg); } }
|
|
84
|
+
@keyframes limr-loading-arc { 0% { stroke-dasharray: 3 97; stroke-dashoffset: 0; } 46%,57% { stroke-dasharray: 92 8; stroke-dashoffset: -4; } 100% { stroke-dasharray: 3 97; stroke-dashoffset: -100; } }
|
|
67
85
|
.limr-prose :where(p,.limr-paragraph,li) { text-align: var(--limr-paragraph-align); text-align-last: start; text-justify: auto; }
|
|
68
86
|
.limr-prose ::selection { background: var(--limr-selection, #667eea4d); }
|
|
69
87
|
.limr-prose { position: relative; isolation: isolate; transition: translate var(--limr-motion) var(--limr-ease); }
|
|
@@ -399,8 +417,9 @@
|
|
|
399
417
|
.limr-toolbar-item { display: flex; align-items: center; border-radius: 3px; transition: color 240ms var(--limr-ease); }
|
|
400
418
|
[data-ready] .limr-toolbar-item { animation: limr-tool-float 330ms cubic-bezier(.16,1,.3,1) calc(30ms + var(--limr-tool-index) * 22ms) both, limr-tool-fade 210ms cubic-bezier(.22,.7,.3,1) calc(65ms + var(--limr-tool-index) * 22ms) both; }
|
|
401
419
|
.limr-toolbar-action { display: inline-grid; place-items: center; flex: none; min-height: 34px; padding: 5px 7px; border: 0; border-radius: 3px; background: transparent; white-space: nowrap; line-height: 1.5; }
|
|
402
|
-
.limr-tool-
|
|
403
|
-
.limr-tool-face
|
|
420
|
+
.limr-tool-faces { display: inline-grid; min-width: 0; transition: width 320ms var(--limr-ease); }
|
|
421
|
+
.limr-tool-face { display: flex; align-items: center; justify-self: start; grid-area: 1 / 1; gap: 5px; width: max-content; min-width: 0; line-height: 1.5; opacity: 0; transform: translateY(2px) scale(.94); transform-origin: left center; pointer-events: none; transition: opacity 220ms var(--limr-ease), transform 320ms var(--limr-ease); }
|
|
422
|
+
.limr-tool-face[data-visible] { opacity: 1; transform: none; }
|
|
404
423
|
.limr-tool-label { display: block; flex: none; padding-block-end: 1px; }
|
|
405
424
|
.limr-tool-face > .limr-tool-icon { animation: limr-tool-fade 240ms var(--limr-ease) both; }
|
|
406
425
|
.limr-toolbar-item[data-status='success'] { color: var(--limr-success, #52885e); }
|
|
@@ -423,6 +442,8 @@
|
|
|
423
442
|
.limr-toolbar-delete-warning[aria-hidden='true'] { visibility: hidden; }
|
|
424
443
|
[data-placement^='top'] .limr-toolbar-delete-warning { order: -1; }
|
|
425
444
|
.limr-toolbar-delete-hint { margin: 4px 8px 5px; font-size: 11px; color: var(--limr-error, #b45858); }
|
|
445
|
+
.limr-selection-toolbar[data-readonly] { width: max-content; }
|
|
446
|
+
.limr-toolbar-readonly-hint { margin: 4px 8px 5px; font-size: 11px; line-height: 1.6; color: var(--limr-secondary, #7b756e); white-space: nowrap; text-align: start; }
|
|
426
447
|
.limr-highlight-settings fieldset { border: 0; padding: 0; margin: 0 0 12px; min-width: 0; }
|
|
427
448
|
.limr-highlight-settings legend { margin-bottom: 7px; padding: 0; color: var(--limr-secondary, #7b756e); font-size: 11px; }
|
|
428
449
|
.limr-highlight-colors { display: flex; flex-wrap: wrap; justify-content: flex-start; gap: 9px; }
|
|
@@ -452,3 +473,62 @@
|
|
|
452
473
|
.limr-prose pre { white-space: pre-wrap; }
|
|
453
474
|
}
|
|
454
475
|
}
|
|
476
|
+
|
|
477
|
+
@layer limr { .limr-stream-block { display: contents; } }
|
|
478
|
+
|
|
479
|
+
@layer limr {
|
|
480
|
+
.limr-mermaid-display { padding: 0; overflow: hidden; }
|
|
481
|
+
.limr-mermaid-loading { display: grid; place-items: center; min-height: 80px; }
|
|
482
|
+
.limr-mermaid-viewport { overflow: auto; max-width: 100%; max-height: var(--limr-mermaid-max-height, min(70vh, 36rem)); padding: .75rem 1rem; box-sizing: border-box; scrollbar-width: thin; scrollbar-color: var(--limr-mermaid-scrollbar, #87837866) transparent; cursor: grab; user-select: none; -webkit-user-select: none; }
|
|
483
|
+
.limr-mermaid-viewport * { user-select: none; -webkit-user-select: none; }
|
|
484
|
+
.limr-mermaid-viewport[data-dragging] { cursor: grabbing; }
|
|
485
|
+
.limr-mermaid-viewport:focus-visible { outline-offset: -2px; }
|
|
486
|
+
.limr-mermaid-viewport::-webkit-scrollbar { width: 5px; height: 5px; }
|
|
487
|
+
.limr-mermaid-viewport::-webkit-scrollbar-track,.limr-mermaid-viewport::-webkit-scrollbar-corner { background: transparent; }
|
|
488
|
+
.limr-mermaid-viewport::-webkit-scrollbar-thumb { background: var(--limr-mermaid-scrollbar, #87837866); border-radius: 8px; }
|
|
489
|
+
.limr-mermaid-viewport::-webkit-scrollbar-button { display: none; }
|
|
490
|
+
.limr-mermaid-viewport[hidden],.limr-prose .limr-mermaid-source[hidden] { display: none; }
|
|
491
|
+
.limr-mermaid-canvas { margin-inline: auto; min-width: 0; transition: width var(--limr-motion) var(--limr-ease); }
|
|
492
|
+
.limr-mermaid-canvas > svg { display: block; width: 100%; height: auto; max-width: none !important; }
|
|
493
|
+
.limr-prose .limr-mermaid-source { background: transparent; padding: .75rem 1rem; }
|
|
494
|
+
.limr-mermaid-error { color: var(--limr-secondary); font-size: .8em; margin-bottom: .5rem; }
|
|
495
|
+
.limr-mermaid-error button { border: 0; background: transparent; text-decoration: underline; }
|
|
496
|
+
.limr-block-tools { position: absolute; inset-block-start: .45rem; inset-inline-end: .5rem; display: flex; align-items: center; padding: 2px; gap: 1px; border-radius: 9px; background: #ffffff99; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); box-shadow: 0 1px 5px #29231d0a, inset 0 0 0 1px #ffffff80; color: #6b6760; z-index: 1; opacity: 0; transition: opacity var(--limr-motion-fast) var(--limr-ease); }
|
|
497
|
+
.limr-mermaid-block:hover .limr-block-tools,.limr-mermaid-block:focus-within .limr-block-tools { opacity: 1; }
|
|
498
|
+
@media (hover: none) { .limr-block-tools { opacity: 1; } }
|
|
499
|
+
.limr-block-tools > button { display: inline-grid; place-items: center; position: relative; width: 28px; height: 28px; padding: 0; border: 0; border-radius: 7px; background: transparent; color: inherit; transition: color var(--limr-motion-fast) var(--limr-ease), background-color var(--limr-motion-fast) var(--limr-ease), opacity var(--limr-motion-fast) var(--limr-ease); }
|
|
500
|
+
.limr-block-tools > button > svg { width: 19px; height: 19px; }
|
|
501
|
+
.limr-block-tools > button:enabled:hover { background: #87837822; color: #2c2925; }
|
|
502
|
+
.limr-block-tools > button:enabled:active { background: #87837838; }
|
|
503
|
+
.limr-block-tools > button[aria-pressed='true'] { color: var(--limr-heading); }
|
|
504
|
+
[data-limr-theme='dark'] .limr-block-tools { background: #37332dcc; box-shadow: 0 1px 5px #0002, inset 0 0 0 1px #ffffff10; color: #c7c0b6; }
|
|
505
|
+
[data-limr-theme='dark'] .limr-block-tools > button:enabled:hover { color: #f0ebe4; }
|
|
506
|
+
@media (prefers-color-scheme: dark) {
|
|
507
|
+
[data-limr-theme='auto'] .limr-block-tools { background: #37332dcc; box-shadow: 0 1px 5px #0002, inset 0 0 0 1px #ffffff10; color: #c7c0b6; }
|
|
508
|
+
[data-limr-theme='auto'] .limr-block-tools > button:enabled:hover { color: #f0ebe4; }
|
|
509
|
+
}
|
|
510
|
+
.limr-callout { border-color: var(--limr-callout-color); background: color-mix(in srgb, var(--limr-callout-color) 8%, transparent); }
|
|
511
|
+
.limr-callout-title { display: flex; align-items: center; gap: .5em; color: var(--limr-callout-color); text-transform: none; letter-spacing: .02em; font-size: .9em; }
|
|
512
|
+
.limr-callout-title > svg { width: 18px; height: 18px; flex: none; }
|
|
513
|
+
.limr-callout-title > span { flex: 1; }
|
|
514
|
+
.limr-callout-toggle { border: 0; border-radius: 5px; background: transparent; padding: 2px; display: inline-flex; }
|
|
515
|
+
.limr-callout-toggle:hover { background: color-mix(in srgb, var(--limr-callout-color) 12%, transparent); }
|
|
516
|
+
.limr-callout-toggle > svg { width: 18px; height: 18px; transition: transform var(--limr-motion) var(--limr-ease); }
|
|
517
|
+
.limr-callout-toggle[aria-expanded='false'] > svg { transform: rotate(-90deg); }
|
|
518
|
+
.limr-callout > .limr-collapse > .limr-collapse-inner > .limr-block-content { min-height: 0; }
|
|
519
|
+
@media (prefers-reduced-motion: reduce) { .limr-mermaid-canvas,.limr-block-tools,.limr-block-tools > button,.limr-callout-toggle > svg { transition: none; } }
|
|
520
|
+
@media print {
|
|
521
|
+
.limr-mermaid-viewport { max-height: none; overflow: visible; }
|
|
522
|
+
.limr-mermaid-canvas { width: 100% !important; }
|
|
523
|
+
.limr-block-tools { display: none; }
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
@layer limr {
|
|
528
|
+
[data-limr-kp] { white-space: nowrap; text-align: start; text-wrap: nowrap !important; }
|
|
529
|
+
[data-limr-kp][data-limr-kp-stale='clip'] { overflow-x: clip; }
|
|
530
|
+
[data-limr-kp][data-limr-kp-stale='wrap'] { white-space: normal; text-wrap: wrap !important; }
|
|
531
|
+
@media print { [data-limr-kp][data-limr-kp-stale] { overflow: visible; white-space: normal; text-wrap: wrap !important; } }
|
|
532
|
+
.limr-kp-glue { display: inline-block; font-size: 0; line-height: 0; white-space: pre; }
|
|
533
|
+
.limr-kp-hyphen::before { content: '-'; }
|
|
534
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ComponentType, ReactNode, CSSProperties } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { n as SyntaxExtension, M as MarkdownNode, A as Annotation, b as AnnotationOccurrence, g as HighlightData, m as SourceRange, j as SelectionSnapshot, f as DocumentSnapshot, K as KnuthPlassOptions, c as DocumentEngine } from './knuth-plass-C8mgFm7W.js';
|
|
4
4
|
|
|
5
5
|
interface NodeRendererProps {
|
|
6
6
|
node: MarkdownNode;
|
|
7
7
|
children: ReactNode;
|
|
8
8
|
pending: boolean;
|
|
9
|
+
/** True only for this unclosed construct, not every child of a growing block. */
|
|
10
|
+
incomplete?: boolean;
|
|
9
11
|
source: string;
|
|
10
12
|
/** Render an existing subtree without parsing strings again. */
|
|
11
13
|
renderChildren: (nodes: readonly MarkdownNode[], options?: {
|
|
@@ -23,6 +25,18 @@ interface TextToken {
|
|
|
23
25
|
style?: CSSProperties;
|
|
24
26
|
}
|
|
25
27
|
type NodeRenderer = ComponentType<NodeRendererProps>;
|
|
28
|
+
interface RendererOverride {
|
|
29
|
+
/** AST type, or the registered name of a component. */
|
|
30
|
+
type: string;
|
|
31
|
+
component: NodeRenderer;
|
|
32
|
+
/** Limits the replacement; false falls back to the next match or built-in. */
|
|
33
|
+
when?: (node: MarkdownNode, context: {
|
|
34
|
+
pending: boolean;
|
|
35
|
+
incomplete: boolean;
|
|
36
|
+
ancestors: readonly MarkdownNode[];
|
|
37
|
+
}) => boolean;
|
|
38
|
+
}
|
|
39
|
+
type TypographyMode = 'knuth-plass' | 'standard' | 'justify';
|
|
26
40
|
type AnnotationPresentation = 'preview' | 'expanded' | 'sidebar';
|
|
27
41
|
interface AnnotationExpansion {
|
|
28
42
|
/** Grow beside the marker, or center within the page's visual viewport. */
|
|
@@ -111,13 +125,28 @@ interface AnnotationType<T = unknown> {
|
|
|
111
125
|
}) => Promise<T>;
|
|
112
126
|
}
|
|
113
127
|
interface RendererExtension extends SyntaxExtension {
|
|
128
|
+
streaming?: {
|
|
129
|
+
/** Optional module-owned intermediate state. Omit to render progressively. */
|
|
130
|
+
Loading?: NodeRenderer;
|
|
131
|
+
/** Last configured effect wins. The reader may override it. */
|
|
132
|
+
effect?: StreamEffect | false;
|
|
133
|
+
};
|
|
114
134
|
/** Register document parameter defaults. Plugins extend RendererParameters
|
|
115
135
|
* through TypeScript declaration merging and read them with useRendererParameter. */
|
|
116
136
|
parameters?: Readonly<RendererParameters>;
|
|
117
137
|
components?: Readonly<Record<string, NodeRenderer>>;
|
|
138
|
+
/** Ordered, reader-local overrides. Last matching entry wins. */
|
|
139
|
+
rendererOverrides?: readonly RendererOverride[];
|
|
118
140
|
annotationTypes?: readonly AnnotationType<any>[];
|
|
119
141
|
actions?: readonly SelectionAction[];
|
|
120
142
|
}
|
|
143
|
+
/** Animation applies to newly appended text and newly appearing blocks. CSS can style the supplied
|
|
144
|
+
* class; keyframes/timing use the Web Animations API. No source rewriting. */
|
|
145
|
+
interface StreamEffect {
|
|
146
|
+
className?: string;
|
|
147
|
+
keyframes?: Keyframe[];
|
|
148
|
+
timing?: KeyframeAnimationOptions;
|
|
149
|
+
}
|
|
121
150
|
/** Open to module augmentation; parameter names and semantics belong to plugins. */
|
|
122
151
|
interface RendererParameters {
|
|
123
152
|
}
|
|
@@ -214,6 +243,7 @@ interface LiMRLocale {
|
|
|
214
243
|
highlightWavy: string;
|
|
215
244
|
hideHighlight: string;
|
|
216
245
|
showHighlight: string;
|
|
246
|
+
inlineHighlightHint?: string;
|
|
217
247
|
deleteAnnotations: string;
|
|
218
248
|
deleteAnnotationsHint: string;
|
|
219
249
|
}
|
|
@@ -226,11 +256,18 @@ interface ViewConfiguration {
|
|
|
226
256
|
/** CSS skin name, applied to every view including portalled controls. */
|
|
227
257
|
appearance?: string;
|
|
228
258
|
components?: Readonly<Record<string, NodeRenderer>>;
|
|
259
|
+
/** Reader rules take precedence over extension rules and component mappings. */
|
|
260
|
+
rendererOverrides?: readonly RendererOverride[];
|
|
229
261
|
annotationTypes?: readonly AnnotationType<any>[];
|
|
230
262
|
annotations?: readonly Annotation[];
|
|
231
263
|
theme?: 'light' | 'dark' | 'auto';
|
|
232
264
|
/** Justify prose paragraphs and list items, leaving the last line start-aligned. */
|
|
233
265
|
justify?: boolean;
|
|
266
|
+
/** TeX Knuth–Plass, standard (default), or browser justification.
|
|
267
|
+
* pretty/normal remain compatible native-CSS aliases. */
|
|
268
|
+
typography?: TypographyMode | 'pretty' | 'normal';
|
|
269
|
+
knuthPlass?: KnuthPlassOptions;
|
|
270
|
+
streamEffect?: StreamEffect | false;
|
|
234
271
|
locale?: Partial<LiMRLocale>;
|
|
235
272
|
scrollRoot?: HTMLElement | null;
|
|
236
273
|
scrollOffset?: number;
|
|
@@ -315,4 +352,4 @@ declare module '@linloir/limr' {
|
|
|
315
352
|
}
|
|
316
353
|
declare function translations(options?: TranslationsOptions): RendererExtension;
|
|
317
354
|
|
|
318
|
-
export { type ActionContext as A, type DocumentCommands as D, type LiMRLocale as L, Markdown as M, type NodeRenderer as N, type RendererExtension as R, type SelectionAction as S, type TextToken as T, type ViewConfiguration as V, type AnnotationContentProps as a, type AnnotationExpansion as b, type AnnotationOverlayCardProps as c, type AnnotationPresentation as d, type AnnotationSidebarPreviewProps as e, type AnnotationType as f, DocumentProvider as g, type DocumentProviderProps as h, type MarkdownProps as i, type NodeRendererProps as j, type
|
|
355
|
+
export { type ActionContext as A, useLocale as B, useRendererParameter as C, type DocumentCommands as D, useSelection as E, type LiMRLocale as L, Markdown as M, type NodeRenderer as N, type RendererExtension as R, type SelectionAction as S, type TextToken as T, type ViewConfiguration as V, type AnnotationContentProps as a, type AnnotationExpansion as b, type AnnotationOverlayCardProps as c, type AnnotationPresentation as d, type AnnotationSidebarPreviewProps as e, type AnnotationType as f, DocumentProvider as g, type DocumentProviderProps as h, type MarkdownProps as i, type NodeRendererProps as j, type RendererOverride as k, type RendererParameters as l, type StreamEffect as m, type StyledProps as n, type ToolbarActionContext as o, type TranslationMode as p, type TranslationsOptions as q, type TypographyMode as r, useAnnotation as s, translations as t, useAnchor as u, useAnnotationState as v, useAnnotations as w, useDocument as x, useDocumentSelector as y, useLiMR as z };
|
package/docs/en/README.md
CHANGED
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
[Documentation](README.md) · [简体中文](../zh-CN/README.md)
|
|
6
6
|
|
|
7
|
-
These pages describe the current
|
|
7
|
+
These pages describe the current repository implementation, including the new streaming, typography, and module capabilities. Check installed exports and types when consuming an npm release. Chinese is the default documentation language; every English page has a matching Chinese page.
|
|
8
8
|
|
|
9
9
|
| What you want to do | Read |
|
|
10
10
|
| --- | --- |
|
|
11
11
|
| Install and render a first document | [Getting started](getting-started.md) |
|
|
12
12
|
| Understand modules, included capabilities, and configuration | [Built-in modules](modules.md) |
|
|
13
|
-
| Write Markdown,
|
|
14
|
-
| Integrate React, streaming, custom layouts, or SSR | [Application integration](integration.md) |
|
|
15
|
-
| Persist selections, highlights, citations, and
|
|
16
|
-
| Add syntax, cards,
|
|
13
|
+
| Write Markdown, Mermaid, callouts, highlights, footnotes, and two-colon components | [Default syntax](syntax.md) |
|
|
14
|
+
| Integrate React, typography modes, streaming, custom layouts, or SSR | [Application integration](integration.md) |
|
|
15
|
+
| Persist selections, highlights, citations, and annotations; understand atomic copying | [Annotations and selection](annotations.md) |
|
|
16
|
+
| Add syntax, cards, actions, streaming effects, or conditional renderers | [Extension development](extensions.md) |
|
|
17
17
|
| Build a standalone npm plugin and import it into an app | [Plugin packages](plugin-packages.md) |
|
|
18
|
-
| Customize themes, fonts,
|
|
18
|
+
| Customize themes, fonts, KP layout, animation, and diagrams | [Styles and fonts](styles.md) |
|
|
19
19
|
| Look up component props, hooks, and core APIs | [API reference](api.md) |
|
|
20
20
|
| Understand parsing, state, and resource boundaries | [Architecture and behavior](architecture.md) |
|
|
21
21
|
| Understand measurements and validation boundaries | [Performance and limits](performance.md) |
|
|
@@ -23,4 +23,6 @@ These pages describe the current public LiMR 1.0.0 implementation. Chinese is th
|
|
|
23
23
|
|
|
24
24
|
Start with `Reader`, split components when a custom layout is needed, and add plugins for application features. Local persistence, import/export, and simulated assistant responses in the Playground belong to the demo application, not to Reader itself.
|
|
25
25
|
|
|
26
|
+
Playground uses standard typography by default; its menu also offers Knuth–Plass and justification, and the streaming demo runs at a slower pace. Refresh opens the default document without restoring the previous article or edits. Explicitly saved drafts can be loaded manually; locale preference persists separately. Docs links open the selected page directly, and switching between Docs and Playground preserves edits during the current session.
|
|
27
|
+
|
|
26
28
|
[Back to README](../../README.en.md) · [Live Playground](https://linloir.github.io/LiMR/)
|
package/docs/en/annotations.md
CHANGED
|
@@ -36,7 +36,7 @@ Persist source, a stable documentId, annotation ID/type/data/anchors, and your a
|
|
|
36
36
|
|
|
37
37
|
| Field | Meaning |
|
|
38
38
|
| --- | --- |
|
|
39
|
-
| text | Readable selected text |
|
|
39
|
+
| text | Readable selected text; settled atomic content uses its complete original source |
|
|
40
40
|
| raw | Mapped original ranges and source |
|
|
41
41
|
| syntax | Absorbs adjacent standard Markdown delimiters at each edge, without selecting extra words or outer custom tags |
|
|
42
42
|
| blocks | Ranges and source for touched blocks |
|
|
@@ -46,6 +46,8 @@ Persist source, a stable documentId, annotation ID/type/data/anchors, and your a
|
|
|
46
46
|
|
|
47
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
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
|
+
|
|
49
51
|
## Highlights and commands
|
|
50
52
|
|
|
51
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.
|
package/docs/en/api.md
CHANGED
|
@@ -37,6 +37,10 @@ Reader, Markdown, and DocumentProvider accept:
|
|
|
37
37
|
| appearance | classic; CSS scope propagated to portals |
|
|
38
38
|
| locale | english; accepts chinese or Partial<LiMRLocale> |
|
|
39
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 |
|
|
40
44
|
| scrollRoot | null/omitted means window |
|
|
41
45
|
| scrollOffset | 24px |
|
|
42
46
|
| onDocumentChange | DocumentSnapshot after updates |
|
|
@@ -63,7 +67,7 @@ The default locale is english. Missing partial keys fall back to English. locale
|
|
|
63
67
|
| AnnotationGroup | occurrence, presentation, expanded=true, enabled=true |
|
|
64
68
|
| AnnotationMarkdown | MarkdownProps with required source; interactive=false by default |
|
|
65
69
|
| InlineMarker | Required label, color, children, button props/ref |
|
|
66
|
-
| ToolIcon | name and SVG props; e.g. note, copy,
|
|
70
|
+
| ToolIcon | name and SVG props; e.g. note, copy, highlight, comment, zoom-in, zoom-out, source, image |
|
|
67
71
|
|
|
68
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.
|
|
69
73
|
|
|
@@ -105,3 +109,7 @@ Core defaults to extensions=[]. Use SyntaxExtension with micromark/mdast for a s
|
|
|
105
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.
|
|
106
110
|
|
|
107
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.
|
package/docs/en/architecture.md
CHANGED
|
@@ -14,15 +14,23 @@ Docs navigation and Playground persistence/loading/language menus belong to the
|
|
|
14
14
|
|
|
15
15
|
## Append parsing
|
|
16
16
|
|
|
17
|
-
Streaming reparses from the unstable final top-level block and
|
|
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
18
|
|
|
19
|
-
|
|
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.
|
|
20
28
|
|
|
21
29
|
## State and resources
|
|
22
30
|
|
|
23
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.
|
|
24
32
|
|
|
25
|
-
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. The library supplies no model service, authentication, database, or automatic persistence.
|
|
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.
|
|
26
34
|
|
|
27
35
|
## Fallback behavior
|
|
28
36
|
|
package/docs/en/extensions.md
CHANGED
|
@@ -13,7 +13,7 @@ Start with [built-in modules](modules.md) for module objects, factories, names,
|
|
|
13
13
|
| A new block with Markdown content | defineBlock |
|
|
14
14
|
| A standalone annotation type | defineAnnotation |
|
|
15
15
|
| Combined syntax, annotations, actions, and parameters | defineExtension / RendererExtension |
|
|
16
|
-
| A different view for
|
|
16
|
+
| A different view for existing nodes or a matching subset | components / rendererOverrides |
|
|
17
17
|
| New parsing rules | micromark + mdast, with transform when needed |
|
|
18
18
|
| Visual changes only | CSS theme package + appearance |
|
|
19
19
|
|
|
@@ -51,7 +51,7 @@ export const notice = defineBlock({
|
|
|
51
51
|
});
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
This supports `<notice title="Tip">…</notice
|
|
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
55
|
|
|
56
56
|
| NodeRendererProps | Use |
|
|
57
57
|
| --- | --- |
|
|
@@ -59,6 +59,7 @@ This supports `<notice title="Tip">…</notice>` and `:::notice{title="Tip"}`. n
|
|
|
59
59
|
| children | Already-rendered subtree; reuse it where possible |
|
|
60
60
|
| source | Source for **this node**, not the entire document |
|
|
61
61
|
| pending | Whether its tail block is still streaming |
|
|
62
|
+
| incomplete | Whether this particular syntax node is unclosed |
|
|
62
63
|
| renderChildren(nodes, { trailing? }) | Rearrange an existing subtree or append an action-only marker |
|
|
63
64
|
| renderValue(tokens?) | Render literals/tokens while preserving selection and annotation insertion |
|
|
64
65
|
| renderAttribute(name) | Render attribute text with original source coordinates |
|
|
@@ -148,12 +149,60 @@ The host overrides defaults with parameters={{ exampleDetail: 'full' }}. Paramet
|
|
|
148
149
|
|
|
149
150
|
## Parsing and streaming constraints
|
|
150
151
|
|
|
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.
|
|
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.
|
|
152
153
|
|
|
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.
|
|
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.
|
|
154
155
|
|
|
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).
|
|
156
157
|
|
|
157
158
|
## Plugin localization
|
|
158
159
|
|
|
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.
|
|
@@ -36,6 +36,8 @@ export function Article() {
|
|
|
36
36
|
|
|
37
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
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
|
+
|
|
39
41
|
## Body only
|
|
40
42
|
|
|
41
43
|
```tsx
|
package/docs/en/integration.md
CHANGED
|
@@ -21,7 +21,31 @@ export function StreamingAnswer({ answer, generating }: {
|
|
|
21
21
|
}
|
|
22
22
|
```
|
|
23
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
|
|
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 finalize syntax and semantics. Finalization can reuse stable prefixes; definition changes and other global dependencies may still require a full parse. 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
|
+
New text and special blocks fade through opacity by default, without replaying the entrance when a paragraph finishes. Use `streamEffect={false}` to disable it or supply `className`, `keyframes`, and `timing`; reduced-motion preferences take precedence. Unclosed modules can show loading states while code/math render progressively. Reuse `children`, `renderChildren`, and `renderValue` to preserve controls and selections. See [extensions](extensions.md).
|
|
27
|
+
|
|
28
|
+
## Paragraph typography
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { useState } from 'react';
|
|
32
|
+
import { Reader, type TypographyMode } from '@linloir/limr';
|
|
33
|
+
|
|
34
|
+
export function TypographyReader({ source }: { source: string }) {
|
|
35
|
+
const [mode, setMode] = useState<TypographyMode>('standard');
|
|
36
|
+
return <>
|
|
37
|
+
<select aria-label="Typography" value={mode}
|
|
38
|
+
onChange={event => setMode(event.target.value as TypographyMode)}>
|
|
39
|
+
<option value="standard">Standard</option>
|
|
40
|
+
<option value="knuth-plass">Knuth–Plass</option>
|
|
41
|
+
<option value="justify">Justified</option>
|
|
42
|
+
</select>
|
|
43
|
+
<Reader source={source} typography={mode} />
|
|
44
|
+
</>;
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`standard` is the default; `justify` uses browser justification; `knuth-plass` enables the paragraph optimizer. `knuthPlass` configures TeX parameters and hyphenation. Streaming tails participate in KP once stable. See [styles](styles.md) and [performance](performance.md) for behavior and measurement boundaries.
|
|
25
49
|
|
|
26
50
|
## Nested scroll containers
|
|
27
51
|
|
|
@@ -111,7 +135,7 @@ export async function renderArticle(source: string) {
|
|
|
111
135
|
}
|
|
112
136
|
```
|
|
113
137
|
|
|
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.
|
|
138
|
+
React streaming SSR can also handle Suspense. Shiki adds highlighting on the client; SSR already includes code text. Mermaid emits a loading state on the server and fades in after client rendering. KP measures fonts and typesets on the client; SSR retains ordinary text layout. 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
139
|
|
|
116
140
|
## Existing plugins and themes
|
|
117
141
|
|
package/docs/en/modules.md
CHANGED
|
@@ -40,13 +40,13 @@ The names need not match. The code-highlighting factory is `codeHighlight()`, it
|
|
|
40
40
|
|
|
41
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
42
|
|
|
43
|
-
##
|
|
43
|
+
## Default modules included with LiMR
|
|
44
44
|
|
|
45
|
-
These modules ship in `@linloir/limr`;
|
|
45
|
+
These modules ship in `@linloir/limr`; no separate npm installations are required. `standard()` returns the following module objects with their default settings.
|
|
46
46
|
|
|
47
47
|
| Factory function | Object's `name` | Capability and content syntax |
|
|
48
48
|
| --- | --- | --- |
|
|
49
|
-
| `gfm()` | `gfm` | Tables, `~~strikethrough~~`, tasks, autolinks, `[^id]`
|
|
49
|
+
| `gfm()` | `gfm` | Tables, `~~strikethrough~~`, tasks, autolinks, `[^id]` footnote parsing |
|
|
50
50
|
| `math()` | `math` | `$…$` inline and `$$…$$` display math; also import math.css |
|
|
51
51
|
| `codeHighlight()` | `code-highlight` | Shiki colors for code fences with a language identifier |
|
|
52
52
|
| `definition()` | `definition` | `<definition>` cards with a title and Markdown body |
|
|
@@ -56,12 +56,16 @@ These modules ship in `@linloir/limr`; they do not require ten separate npm inst
|
|
|
56
56
|
| `figures()` | `figures` | `<figures>` / `<figure>` responsive groups, single figures, and captions |
|
|
57
57
|
| `citation()` | `citation` | `<citation>` source definitions and `<ref>` markers with cards |
|
|
58
58
|
| `translations()` | `translations` | `<translations>` original, translated, and parallel views |
|
|
59
|
+
| `mermaid()` | `mermaid` | Mermaid fences / `::mermaid`, image/source, zoom and copy |
|
|
60
|
+
| `callout()` | `callout` | Obsidian callout types, aliases and nested quotes / `::callout` |
|
|
61
|
+
| `inlineHighlights()` | `inline-highlights` | `==text==` and `::highlight`, blue read-only highlights |
|
|
62
|
+
| `footnotes()` | `footnotes` | `[^id]` / `^[body]`, comment markers and cards; `::footnote` / `::footnote-ref` |
|
|
59
63
|
|
|
60
|
-
Import these functions from `@linloir/limr` or `@linloir/limr/extensions`. `standard()` is a convenience function returning an array, not an
|
|
64
|
+
Import these functions from `@linloir/limr` or `@linloir/limr/extensions`. `standard()` is a convenience function returning an array, not an additional module.
|
|
61
65
|
|
|
62
66
|
`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
67
|
|
|
64
|
-
Highlight, copy, and annotation deletion are built-in toolbar actions.
|
|
68
|
+
Highlight, copy, and annotation deletion are built-in toolbar actions. Default modules recognize `==text==` and Markdown footnotes. Source-authored highlights offer copying but no toolbar editing. The Playground's editable 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
69
|
|
|
66
70
|
## Choosing a configuration
|
|
67
71
|
|
package/docs/en/performance.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[Documentation](README.md) · [简体中文](../zh-CN/performance.md)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The first sections retain the repository's **2026-10-12 historical measurements** for comparison. The **2026-10-13** section below records this optimization's measurements. Neither set is a performance guarantee.
|
|
8
8
|
|
|
9
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
10
|
|
|
@@ -37,7 +37,7 @@ Large-document SSR varied substantially: LiMR samples were 2.64–9.10s and base
|
|
|
37
37
|
|
|
38
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
39
|
|
|
40
|
-
The measurements
|
|
40
|
+
The historical measurements above exclude full Reader layouts; the newer KP comparison below includes one complete Reader workload. Neither set covers large formula collections, Shiki cold starts, arbitrary dense annotations/plugins, or physical mobile devices. Installed font size also differs from browser transfers; see [styles and fonts](styles.md).
|
|
41
41
|
|
|
42
42
|
## Reproduce in the repository
|
|
43
43
|
|
|
@@ -51,3 +51,43 @@ pnpm bench:selection
|
|
|
51
51
|
```
|
|
52
52
|
|
|
53
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.
|
|
54
|
+
|
|
55
|
+
## 2026-10-13: streaming and layout measurements
|
|
56
|
+
|
|
57
|
+
This compares the working implementation with pre-change commit `ebab991` in one process, using the same dependencies, UTF-16 input and chunk boundaries. Five warmed trials alternate order; values are the sample with median total time. Core append and finalization only: React, layout, async highlighting and network are excluded. Environment and raw samples: [streaming.json](../../benchmarks/results/streaming.json).
|
|
58
|
+
|
|
59
|
+
| Scenario | Characters | Before total | After total |
|
|
60
|
+
| --- | ---: | ---: | ---: |
|
|
61
|
+
| GFM | 34,860 | 139.22ms | 89.07ms |
|
|
62
|
+
| Default modules | 34,860 | 141.40ms | 94.63ms |
|
|
63
|
+
| Growing code fence | 43,209 | 1335.87ms | 29.05ms |
|
|
64
|
+
| Single long paragraph | 7,840 | 72.43ms | 74.32ms |
|
|
65
|
+
|
|
66
|
+
Changes include incremental line indexes, cached semantic facts for retained nodes, local finalization, an append path for growing code fences, and lazy code character maps. Complex growing paragraphs still need reparsing. Differences for a single long paragraph are small and subject to runtime variance; these results do not establish equal improvements for all content.
|
|
67
|
+
|
|
68
|
+
Run `pnpm bench:streaming` to measure the current core. For the alternating comparison, set `LIMR_BENCH_BASELINE` to a separate untouched checkout of `ebab991`, with the same dependencies available there. The script writes `benchmarks/results/streaming.json`; it does not recreate the historical hardware or timings.
|
|
69
|
+
|
|
70
|
+
Production Chromium 156, including synchronous React commits, forced layout, default typography and increment animations: a 34,860-character stream with default modules had **3.9ms P95**, 7.3ms maximum; a 43,211-character code stream had **2.6ms P95**, 12.4ms maximum. Three isolated font-ready pages per scenario, 256-character chunks; idle rAF time and async highlighting excluded. No old-browser-implementation comparison was made, and this is not physical-device input-to-display latency. Data: [streaming-browser.json](../../benchmarks/results/streaming-browser.json).
|
|
71
|
+
|
|
72
|
+
KP has a separate production benchmark using the complete scikit-learn Playground, including its TOC, sidebar and annotations. Three fresh pages per mode run three downward/upward sweeps each, then 32 viewport changes across 1100–1420px. The first sweep includes newly visible paragraphs; later sweeps reuse their typesetting. The table pools 1,080 scroll samples and 96 resize samples per mode, without CPU throttling.
|
|
73
|
+
|
|
74
|
+
| Measurement | Standard | Knuth–Plass |
|
|
75
|
+
| --- | ---: | ---: |
|
|
76
|
+
| Scroll median / P95 / maximum | 16.6 / 17.0 / 18.5ms | 16.6 / 19.8 / 46.5ms |
|
|
77
|
+
| Resize median / P95 / maximum | 11.2 / 24.1 / 31.9ms | 17.7 / 41.6 / 49.9ms |
|
|
78
|
+
| Scroll samples over 50ms | 0 / 1,080 | 0 / 1,080 |
|
|
79
|
+
| Resize samples over 50ms | 0 / 96 | 0 / 96 |
|
|
80
|
+
|
|
81
|
+
Source anchor lookup prunes unrelated source bindings and stops at the first visible character, avoiding a full KP DOM traversal for every sidebar item.
|
|
82
|
+
|
|
83
|
+
Scroll timings span `scrollTo` through the next rAF; resize timings include the viewport update, protocol overhead and the next rAF. KP still costs more during resizing, but this run did not reproduce the earlier long stalls. These are local Chromium measurements, not physical-device input latency or a guarantee for arbitrary plugins. Raw samples and exact boundaries: [layout.json](../../benchmarks/results/layout.json).
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
pnpm build:playground
|
|
87
|
+
pnpm preview:playground --port 4174
|
|
88
|
+
# In another terminal, with no concurrent build or test:
|
|
89
|
+
pnpm exec tsx benchmarks/layout.ts
|
|
90
|
+
BENCH_URL=http://127.0.0.1:4174/LiMR pnpm exec tsx benchmarks/streaming-browser.ts
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Preparation caches font measurements, hyphenation, legal breakpoints and prefix sums independently of line width. Rendering uses a word-spacing run per line instead of an element per space. Width changes patch glue in place when the breaks match, or reuse text containers when they change. A shared synchronous measurement Range avoids retaining thousands of live ranges across DOM mutations. Highlight geometry is cached in paragraph coordinates, adjacent KP paint fragments merge into lines, and paint writes do not force a layout per rectangle. Viewport-mask coordinates do not inherit into the article. Scrolling retains typeset nodes and does not rerun the paragraph solver.
|