@peaske7/readit 0.1.6 → 0.1.8
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/biome.json +1 -1
- package/bun.lock +86 -72
- package/package.json +12 -11
- package/src/App.tsx +36 -16
- package/src/cli/index.ts +338 -70
- package/src/components/ActionsMenu.tsx +12 -10
- package/src/components/DocumentViewer/CodeBlock.tsx +131 -54
- package/src/components/DocumentViewer/DocumentViewer.tsx +10 -8
- package/src/components/DocumentViewer/InlineCode.tsx +60 -0
- package/src/components/FloatingTOC.tsx +4 -2
- package/src/components/Header.tsx +3 -1
- package/src/components/InlineEditor.tsx +4 -2
- package/src/components/MarginNote.tsx +17 -8
- package/src/components/RawModal.tsx +9 -7
- package/src/components/ReanchorConfirm.tsx +6 -3
- package/src/components/SettingsModal.tsx +112 -23
- package/src/components/ShortcutCapture.tsx +4 -1
- package/src/components/ShortcutList.tsx +50 -9
- package/src/components/comments/CommentBadge.tsx +7 -1
- package/src/components/comments/CommentInput.tsx +13 -18
- package/src/components/comments/CommentListItem.tsx +15 -5
- package/src/components/comments/CommentManager.tsx +14 -7
- package/src/components/comments/CommentNav.tsx +8 -3
- package/src/contexts/CommentContext.tsx +16 -9
- package/src/contexts/LayoutContext.tsx +17 -5
- package/src/contexts/LocaleContext.tsx +35 -0
- package/src/hooks/useClipboard.ts +11 -8
- package/src/hooks/useDocument.ts +33 -18
- package/src/hooks/useEditorScheme.ts +51 -0
- package/src/hooks/useFontPreference.ts +5 -22
- package/src/hooks/useKeybindings.ts +6 -18
- package/src/hooks/useLocalePreference.ts +42 -0
- package/src/index.css +87 -26
- package/src/lib/editor-links.ts +59 -0
- package/src/lib/highlight/dom.ts +126 -54
- package/src/lib/highlight/highlighter.ts +10 -10
- package/src/lib/i18n/completeness.test.ts +51 -0
- package/src/lib/i18n/en.ts +139 -0
- package/src/lib/i18n/index.ts +3 -0
- package/src/lib/i18n/ja.ts +141 -0
- package/src/lib/i18n/translations.test.ts +39 -0
- package/src/lib/i18n/translations.ts +27 -0
- package/src/lib/i18n/types.ts +145 -0
- package/src/lib/shortcut-registry.ts +1 -1
- package/src/main.tsx +4 -1
- package/src/server/index.ts +197 -124
- package/src/store/index.test.ts +22 -0
- package/src/store/index.ts +24 -4
- package/src/types/index.ts +12 -0
package/src/index.css
CHANGED
|
@@ -56,17 +56,18 @@ html {
|
|
|
56
56
|
--scrollbar-thumb-hover: #aaaaaa;
|
|
57
57
|
|
|
58
58
|
/* Prose */
|
|
59
|
-
--prose-body: #
|
|
60
|
-
--prose-headings: #
|
|
59
|
+
--prose-body: #374151;
|
|
60
|
+
--prose-headings: #111827;
|
|
61
61
|
--prose-links: #2563eb;
|
|
62
62
|
--prose-links-hover: #1d4ed8;
|
|
63
|
-
--prose-bold: #
|
|
64
|
-
--prose-code-bg: #
|
|
65
|
-
--prose-border: #
|
|
66
|
-
--prose-table-header-bg: #
|
|
67
|
-
--prose-table-even-bg: #
|
|
68
|
-
--prose-blockquote: #
|
|
69
|
-
--prose-
|
|
63
|
+
--prose-bold: #111827;
|
|
64
|
+
--prose-code-bg: #e8e8ec;
|
|
65
|
+
--prose-border: #e5e7eb;
|
|
66
|
+
--prose-table-header-bg: #f9fafb;
|
|
67
|
+
--prose-table-even-bg: #f9fafb;
|
|
68
|
+
--prose-blockquote: #6b7280;
|
|
69
|
+
--prose-blockquote-border: #d1d5db;
|
|
70
|
+
--prose-summary-marker: #9ca3af;
|
|
70
71
|
|
|
71
72
|
/* TOC */
|
|
72
73
|
--toc-text: rgba(26, 26, 26, 0.5);
|
|
@@ -113,15 +114,16 @@ html {
|
|
|
113
114
|
--scrollbar-thumb-hover: #71717a;
|
|
114
115
|
|
|
115
116
|
--prose-body: #d4d4d8;
|
|
116
|
-
--prose-headings: #
|
|
117
|
+
--prose-headings: #f4f4f5;
|
|
117
118
|
--prose-links: #60a5fa;
|
|
118
119
|
--prose-links-hover: #93c5fd;
|
|
119
|
-
--prose-bold: #
|
|
120
|
-
--prose-code-bg: #
|
|
120
|
+
--prose-bold: #f4f4f5;
|
|
121
|
+
--prose-code-bg: #323238;
|
|
121
122
|
--prose-border: #3f3f46;
|
|
122
123
|
--prose-table-header-bg: #27272a;
|
|
123
124
|
--prose-table-even-bg: #1f1f23;
|
|
124
125
|
--prose-blockquote: #a1a1aa;
|
|
126
|
+
--prose-blockquote-border: #52525b;
|
|
125
127
|
--prose-summary-marker: #71717a;
|
|
126
128
|
|
|
127
129
|
--toc-text: rgba(228, 228, 231, 0.5);
|
|
@@ -467,6 +469,7 @@ mark[data-pending] {
|
|
|
467
469
|
color: var(--prose-body);
|
|
468
470
|
user-select: text;
|
|
469
471
|
-webkit-user-select: text;
|
|
472
|
+
letter-spacing: -0.011em;
|
|
470
473
|
}
|
|
471
474
|
|
|
472
475
|
/* Headings */
|
|
@@ -477,27 +480,43 @@ mark[data-pending] {
|
|
|
477
480
|
.prose h5,
|
|
478
481
|
.prose h6 {
|
|
479
482
|
font-weight: 600;
|
|
480
|
-
line-height: 1.
|
|
483
|
+
line-height: 1.3;
|
|
481
484
|
margin-top: 2em;
|
|
482
485
|
margin-bottom: 0.75em;
|
|
483
486
|
color: var(--prose-headings);
|
|
487
|
+
letter-spacing: -0.02em;
|
|
484
488
|
}
|
|
485
489
|
|
|
486
490
|
.prose h1 {
|
|
487
491
|
font-size: 2.25em;
|
|
488
492
|
margin-top: 0;
|
|
493
|
+
margin-bottom: 0.889em;
|
|
494
|
+
font-weight: 800;
|
|
495
|
+
line-height: 1.111;
|
|
489
496
|
}
|
|
490
497
|
|
|
491
498
|
.prose h2 {
|
|
492
499
|
font-size: 1.5em;
|
|
500
|
+
margin-top: 2em;
|
|
501
|
+
margin-bottom: 1em;
|
|
502
|
+
font-weight: 700;
|
|
503
|
+
line-height: 1.333;
|
|
504
|
+
padding-bottom: 0.4em;
|
|
505
|
+
border-bottom: 1px solid var(--prose-border);
|
|
493
506
|
}
|
|
494
507
|
|
|
495
508
|
.prose h3 {
|
|
496
509
|
font-size: 1.25em;
|
|
510
|
+
margin-top: 1.6em;
|
|
511
|
+
margin-bottom: 0.6em;
|
|
512
|
+
line-height: 1.6;
|
|
497
513
|
}
|
|
498
514
|
|
|
499
515
|
.prose h4 {
|
|
500
|
-
font-size: 1.
|
|
516
|
+
font-size: 1.1em;
|
|
517
|
+
margin-top: 1.5em;
|
|
518
|
+
margin-bottom: 0.5em;
|
|
519
|
+
line-height: 1.5;
|
|
501
520
|
}
|
|
502
521
|
|
|
503
522
|
/* Paragraphs and spacing */
|
|
@@ -516,12 +535,21 @@ mark[data-pending] {
|
|
|
516
535
|
/* Lists */
|
|
517
536
|
.prose ul,
|
|
518
537
|
.prose ol {
|
|
519
|
-
padding-left: 1.
|
|
538
|
+
padding-left: 1.625em;
|
|
520
539
|
margin: 1.25em 0;
|
|
521
540
|
}
|
|
522
541
|
|
|
523
542
|
.prose li {
|
|
524
543
|
margin: 0.5em 0;
|
|
544
|
+
padding-left: 0.375em;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.prose li::marker {
|
|
548
|
+
color: #9ca3af;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.dark .prose li::marker {
|
|
552
|
+
color: #6b7280;
|
|
525
553
|
}
|
|
526
554
|
|
|
527
555
|
.prose ul {
|
|
@@ -535,46 +563,67 @@ mark[data-pending] {
|
|
|
535
563
|
/* Nested lists */
|
|
536
564
|
.prose li > ul,
|
|
537
565
|
.prose li > ol {
|
|
538
|
-
margin: 0.
|
|
566
|
+
margin: 0.35em 0;
|
|
539
567
|
}
|
|
540
568
|
|
|
541
569
|
/* Code */
|
|
542
570
|
.prose code {
|
|
543
571
|
background: var(--prose-code-bg);
|
|
544
|
-
padding: 0.
|
|
545
|
-
border-radius: 0.
|
|
546
|
-
font-size: 0.
|
|
572
|
+
padding: 0.15em 0.4em;
|
|
573
|
+
border-radius: 0.3em;
|
|
574
|
+
font-size: 0.85em;
|
|
575
|
+
font-weight: 500;
|
|
547
576
|
}
|
|
548
577
|
|
|
549
578
|
.prose pre {
|
|
550
|
-
margin: 1.
|
|
579
|
+
margin: 1.75em 0;
|
|
551
580
|
}
|
|
552
581
|
|
|
553
582
|
.prose pre code {
|
|
554
583
|
background: transparent;
|
|
555
584
|
padding: 0;
|
|
556
|
-
font-size: 0.
|
|
585
|
+
font-size: 0.85em;
|
|
586
|
+
font-weight: 400;
|
|
557
587
|
color: inherit;
|
|
558
588
|
}
|
|
559
589
|
|
|
590
|
+
/* Editor links — clickable file paths */
|
|
591
|
+
.prose a.editor-link {
|
|
592
|
+
text-decoration: none;
|
|
593
|
+
color: inherit;
|
|
594
|
+
border-bottom: 1px dashed var(--prose-links);
|
|
595
|
+
transition: border-color 0.15s;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.prose a.editor-link:hover {
|
|
599
|
+
border-bottom-style: solid;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.prose a.editor-link code {
|
|
603
|
+
cursor: pointer;
|
|
604
|
+
}
|
|
605
|
+
|
|
560
606
|
/* Tables */
|
|
561
607
|
.prose table {
|
|
562
608
|
width: 100%;
|
|
563
609
|
border-collapse: collapse;
|
|
564
|
-
margin: 1.
|
|
610
|
+
margin: 1.75em 0;
|
|
565
611
|
font-size: 0.9em;
|
|
566
612
|
}
|
|
567
613
|
|
|
568
614
|
.prose th,
|
|
569
615
|
.prose td {
|
|
570
616
|
border: 1px solid var(--prose-border);
|
|
571
|
-
padding: 0.
|
|
617
|
+
padding: 0.625em 1em;
|
|
572
618
|
text-align: left;
|
|
619
|
+
line-height: 1.6;
|
|
573
620
|
}
|
|
574
621
|
|
|
575
622
|
.prose th {
|
|
576
623
|
background: var(--prose-table-header-bg);
|
|
577
624
|
font-weight: 600;
|
|
625
|
+
font-size: 0.875em;
|
|
626
|
+
letter-spacing: 0.01em;
|
|
578
627
|
}
|
|
579
628
|
|
|
580
629
|
.prose tr:nth-child(even) {
|
|
@@ -583,8 +632,8 @@ mark[data-pending] {
|
|
|
583
632
|
|
|
584
633
|
/* Blockquotes */
|
|
585
634
|
.prose blockquote {
|
|
586
|
-
border-left:
|
|
587
|
-
padding-left:
|
|
635
|
+
border-left: 3px solid var(--prose-blockquote-border);
|
|
636
|
+
padding-left: 1.125em;
|
|
588
637
|
margin: 1.5em 0;
|
|
589
638
|
color: var(--prose-blockquote);
|
|
590
639
|
font-style: italic;
|
|
@@ -597,7 +646,10 @@ mark[data-pending] {
|
|
|
597
646
|
/* Links */
|
|
598
647
|
.prose a {
|
|
599
648
|
color: var(--prose-links);
|
|
649
|
+
font-weight: 500;
|
|
600
650
|
text-decoration: underline;
|
|
651
|
+
text-underline-offset: 2px;
|
|
652
|
+
text-decoration-thickness: 1px;
|
|
601
653
|
}
|
|
602
654
|
|
|
603
655
|
.prose a:hover {
|
|
@@ -608,7 +660,7 @@ mark[data-pending] {
|
|
|
608
660
|
.prose hr {
|
|
609
661
|
border: none;
|
|
610
662
|
border-top: 1px solid var(--prose-border);
|
|
611
|
-
margin:
|
|
663
|
+
margin: 3em 0;
|
|
612
664
|
}
|
|
613
665
|
|
|
614
666
|
/* Task lists (checkboxes) */
|
|
@@ -628,12 +680,21 @@ mark[data-pending] {
|
|
|
628
680
|
.prose strong {
|
|
629
681
|
font-weight: 600;
|
|
630
682
|
color: var(--prose-bold);
|
|
683
|
+
letter-spacing: -0.01em;
|
|
631
684
|
}
|
|
632
685
|
|
|
633
686
|
.prose em {
|
|
634
687
|
font-style: italic;
|
|
635
688
|
}
|
|
636
689
|
|
|
690
|
+
/* First paragraph after heading - tighter spacing */
|
|
691
|
+
.prose h1 + p,
|
|
692
|
+
.prose h2 + p,
|
|
693
|
+
.prose h3 + p,
|
|
694
|
+
.prose h4 + p {
|
|
695
|
+
margin-top: 0.75em;
|
|
696
|
+
}
|
|
697
|
+
|
|
637
698
|
/* Fullscreen prose - left-aligned, full width */
|
|
638
699
|
.prose-fullscreen {
|
|
639
700
|
max-width: none;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type EditorScheme, EditorSchemes } from "../types";
|
|
2
|
+
|
|
3
|
+
// Known source file extensions — kept broad to cover common languages
|
|
4
|
+
const EXTENSIONS =
|
|
5
|
+
"ts|tsx|js|jsx|mjs|cjs|json|md|mdx|css|scss|html|htm|py|rs|go|rb|java|kt|yml|yaml|toml|sh|bash|zsh|sql|graphql|gql|vue|svelte|astro|c|cpp|h|hpp|cs|swift|zig|lua|ex|exs|erl|hrl|elm|clj|cljs|ml|mli|fs|fsx|r|jl|dart|tf|hcl|proto|xml|svg";
|
|
6
|
+
|
|
7
|
+
// Match file paths with known extensions and optional :line[:col] suffix
|
|
8
|
+
// Requires at least one `/` to avoid matching bare filenames like "README.md"
|
|
9
|
+
const FILE_PATH_RE = new RegExp(
|
|
10
|
+
`^(?:\\.\\.\\/|\\.\\/)?((?:[\\w@.-]+\\/)+[\\w.-]+\\.(?:${EXTENSIONS}))(?::(\\d+)(?::(\\d+))?)?$`,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export interface FilePathMatch {
|
|
14
|
+
path: string;
|
|
15
|
+
line?: number;
|
|
16
|
+
col?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function parseFilePath(text: string): FilePathMatch | undefined {
|
|
20
|
+
const match = FILE_PATH_RE.exec(text.trim());
|
|
21
|
+
if (!match) return undefined;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
path: match[1],
|
|
25
|
+
line: match[2] ? Number.parseInt(match[2], 10) : undefined,
|
|
26
|
+
col: match[3] ? Number.parseInt(match[3], 10) : undefined,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildEditorUri(
|
|
31
|
+
scheme: EditorScheme,
|
|
32
|
+
absolutePath: string,
|
|
33
|
+
line?: number,
|
|
34
|
+
col?: number,
|
|
35
|
+
): string {
|
|
36
|
+
if (scheme === EditorSchemes.NONE) return "";
|
|
37
|
+
|
|
38
|
+
// vscode://file/path:line:col
|
|
39
|
+
let uri = `${scheme}://file/${absolutePath}`;
|
|
40
|
+
if (line !== undefined) {
|
|
41
|
+
uri += `:${line}`;
|
|
42
|
+
if (col !== undefined) {
|
|
43
|
+
uri += `:${col}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return uri;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resolveAbsolutePath(
|
|
50
|
+
relativePath: string,
|
|
51
|
+
workingDirectory: string,
|
|
52
|
+
): string {
|
|
53
|
+
if (relativePath.startsWith("/")) return relativePath;
|
|
54
|
+
|
|
55
|
+
const base = workingDirectory.endsWith("/")
|
|
56
|
+
? workingDirectory
|
|
57
|
+
: `${workingDirectory}/`;
|
|
58
|
+
return `${base}${relativePath}`;
|
|
59
|
+
}
|
package/src/lib/highlight/dom.ts
CHANGED
|
@@ -147,6 +147,21 @@ export function collectTextNodes(root: Node): TextNodeInfo[] {
|
|
|
147
147
|
export interface ExtendedHighlightStyle extends HighlightStyle {
|
|
148
148
|
colorIndex?: number;
|
|
149
149
|
isBracketMode?: boolean;
|
|
150
|
+
isBracketStart?: boolean;
|
|
151
|
+
isBracketEnd?: boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
interface BatchedHighlightSegment {
|
|
155
|
+
startOffset: number;
|
|
156
|
+
endOffset: number;
|
|
157
|
+
style: ExtendedHighlightStyle;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface NodeSegment {
|
|
161
|
+
start: number;
|
|
162
|
+
end: number;
|
|
163
|
+
style: ExtendedHighlightStyle;
|
|
164
|
+
order: number;
|
|
150
165
|
}
|
|
151
166
|
|
|
152
167
|
/**
|
|
@@ -217,78 +232,135 @@ export function applyHighlightToRange(
|
|
|
217
232
|
}
|
|
218
233
|
}
|
|
219
234
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
*/
|
|
223
|
-
export function applyHighlightWithStyle(
|
|
224
|
-
root: HTMLElement,
|
|
225
|
-
textContent: string,
|
|
226
|
-
startOffset: number,
|
|
227
|
-
endOffset: number,
|
|
235
|
+
function createStyledMark(
|
|
236
|
+
text: string,
|
|
228
237
|
style: ExtendedHighlightStyle,
|
|
229
|
-
):
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
(n) => n.end > startOffset && n.start < endOffset,
|
|
233
|
-
);
|
|
238
|
+
): HTMLElement {
|
|
239
|
+
const mark = document.createElement("mark");
|
|
240
|
+
mark.setAttribute(style.attribute, style.attributeValue);
|
|
234
241
|
|
|
235
|
-
if (
|
|
236
|
-
|
|
242
|
+
if (style.colorIndex !== undefined) {
|
|
243
|
+
mark.setAttribute("data-color-index", String(style.colorIndex % 4));
|
|
237
244
|
}
|
|
238
245
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
style.
|
|
246
|
+
if (style.isBracketMode) {
|
|
247
|
+
mark.setAttribute("data-bracket-mode", "true");
|
|
248
|
+
if (style.isBracketStart) {
|
|
249
|
+
mark.setAttribute("data-bracket-start", "true");
|
|
250
|
+
}
|
|
251
|
+
if (style.isBracketEnd) {
|
|
252
|
+
mark.setAttribute("data-bracket-end", "true");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
242
255
|
|
|
243
|
-
|
|
256
|
+
mark.textContent = text;
|
|
257
|
+
return mark;
|
|
258
|
+
}
|
|
244
259
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
260
|
+
function normalizeNodeSegments(segments: NodeSegment[]): NodeSegment[] {
|
|
261
|
+
const sorted = [...segments].sort((a, b) => {
|
|
262
|
+
if (a.start !== b.start) return a.start - b.start;
|
|
263
|
+
if (a.end !== b.end) return b.end - a.end;
|
|
264
|
+
return a.order - b.order;
|
|
265
|
+
});
|
|
248
266
|
|
|
249
|
-
|
|
250
|
-
|
|
267
|
+
const normalized: NodeSegment[] = [];
|
|
268
|
+
let coveredUntil = 0;
|
|
251
269
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
270
|
+
for (const segment of sorted) {
|
|
271
|
+
const start = Math.max(segment.start, coveredUntil);
|
|
272
|
+
if (start >= segment.end) continue;
|
|
255
273
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
274
|
+
normalized.push({ ...segment, start });
|
|
275
|
+
coveredUntil = segment.end;
|
|
276
|
+
}
|
|
259
277
|
|
|
260
|
-
|
|
261
|
-
|
|
278
|
+
return normalized;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function applyHighlightBatch(
|
|
282
|
+
root: HTMLElement,
|
|
283
|
+
textContent: string,
|
|
284
|
+
highlights: BatchedHighlightSegment[],
|
|
285
|
+
): void {
|
|
286
|
+
if (highlights.length === 0) return;
|
|
262
287
|
|
|
263
|
-
|
|
264
|
-
|
|
288
|
+
const textNodes = collectTextNodes(root);
|
|
289
|
+
const segmentsByNode = new Map<Text, NodeSegment[]>();
|
|
290
|
+
|
|
291
|
+
for (
|
|
292
|
+
let highlightIndex = 0;
|
|
293
|
+
highlightIndex < highlights.length;
|
|
294
|
+
highlightIndex++
|
|
295
|
+
) {
|
|
296
|
+
const highlight = highlights[highlightIndex];
|
|
297
|
+
const overlappingNodes = textNodes.filter(
|
|
298
|
+
(n) => n.end > highlight.startOffset && n.start < highlight.endOffset,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
if (overlappingNodes.length === 0) continue;
|
|
302
|
+
|
|
303
|
+
const lineCount = countLinesInRange(
|
|
304
|
+
textContent,
|
|
305
|
+
highlight.startOffset,
|
|
306
|
+
highlight.endOffset,
|
|
307
|
+
);
|
|
308
|
+
const useBracketMode =
|
|
309
|
+
highlight.style.isBracketMode ?? lineCount >= BRACKET_MODE_LINE_THRESHOLD;
|
|
310
|
+
|
|
311
|
+
for (let nodeIndex = 0; nodeIndex < overlappingNodes.length; nodeIndex++) {
|
|
312
|
+
const { node, start } = overlappingNodes[nodeIndex];
|
|
313
|
+
const localStart = Math.max(0, highlight.startOffset - start);
|
|
314
|
+
const localEnd = Math.min(node.length, highlight.endOffset - start);
|
|
315
|
+
|
|
316
|
+
if (localStart >= localEnd) continue;
|
|
317
|
+
|
|
318
|
+
const nodeSegments = segmentsByNode.get(node) ?? [];
|
|
319
|
+
nodeSegments.push({
|
|
320
|
+
start: localStart,
|
|
321
|
+
end: localEnd,
|
|
322
|
+
order: highlightIndex,
|
|
323
|
+
style: {
|
|
324
|
+
...highlight.style,
|
|
325
|
+
isBracketMode: useBracketMode,
|
|
326
|
+
isBracketStart: useBracketMode && nodeIndex === 0,
|
|
327
|
+
isBracketEnd:
|
|
328
|
+
useBracketMode && nodeIndex === overlappingNodes.length - 1,
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
segmentsByNode.set(node, nodeSegments);
|
|
265
332
|
}
|
|
333
|
+
}
|
|
266
334
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
335
|
+
for (const { node } of textNodes) {
|
|
336
|
+
const segments = segmentsByNode.get(node);
|
|
337
|
+
if (!segments || segments.length === 0) continue;
|
|
338
|
+
|
|
339
|
+
const normalized = normalizeNodeSegments(segments);
|
|
340
|
+
if (normalized.length === 0) continue;
|
|
341
|
+
|
|
342
|
+
const text = node.textContent ?? "";
|
|
343
|
+
const fragment = document.createDocumentFragment();
|
|
344
|
+
let cursor = 0;
|
|
345
|
+
|
|
346
|
+
for (const segment of normalized) {
|
|
347
|
+
if (cursor < segment.start) {
|
|
348
|
+
fragment.appendChild(
|
|
349
|
+
document.createTextNode(text.slice(cursor, segment.start)),
|
|
350
|
+
);
|
|
274
351
|
}
|
|
352
|
+
|
|
353
|
+
fragment.appendChild(
|
|
354
|
+
createStyledMark(text.slice(segment.start, segment.end), segment.style),
|
|
355
|
+
);
|
|
356
|
+
cursor = segment.end;
|
|
275
357
|
}
|
|
276
358
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
} catch {
|
|
280
|
-
// Range crosses element boundaries - use extractContents fallback
|
|
281
|
-
try {
|
|
282
|
-
const fragment = range.extractContents();
|
|
283
|
-
mark.appendChild(fragment);
|
|
284
|
-
range.insertNode(mark);
|
|
285
|
-
} catch (err) {
|
|
286
|
-
// Skip if fallback also fails, but log for debugging
|
|
287
|
-
console.warn("[highlight] Failed to apply styled highlight:", err);
|
|
288
|
-
}
|
|
359
|
+
if (cursor < text.length) {
|
|
360
|
+
fragment.appendChild(document.createTextNode(text.slice(cursor)));
|
|
289
361
|
}
|
|
290
362
|
|
|
291
|
-
|
|
363
|
+
node.replaceWith(fragment);
|
|
292
364
|
}
|
|
293
365
|
}
|
|
294
366
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { findTextPosition } from "./core";
|
|
2
2
|
import {
|
|
3
|
+
applyHighlightBatch,
|
|
3
4
|
applyHighlightToRange,
|
|
4
|
-
applyHighlightWithStyle,
|
|
5
5
|
clearHighlights,
|
|
6
6
|
collectHighlightPositions,
|
|
7
7
|
getDOMTextContent,
|
|
@@ -195,19 +195,19 @@ function createMarkdownHighlighter(options: MarkdownOptions): Highlighter {
|
|
|
195
195
|
.filter((c): c is HighlightComment => c !== null)
|
|
196
196
|
.sort((a, b) => a.startOffset - b.startOffset);
|
|
197
197
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
comment.startOffset,
|
|
203
|
-
comment.endOffset,
|
|
204
|
-
{
|
|
198
|
+
applyHighlightBatch(
|
|
199
|
+
root,
|
|
200
|
+
textContent,
|
|
201
|
+
resolved.map((comment) => ({
|
|
202
|
+
startOffset: comment.startOffset,
|
|
203
|
+
endOffset: comment.endOffset,
|
|
204
|
+
style: {
|
|
205
205
|
attribute: "data-comment-id",
|
|
206
206
|
attributeValue: comment.id,
|
|
207
207
|
colorIndex: 0,
|
|
208
208
|
},
|
|
209
|
-
)
|
|
210
|
-
|
|
209
|
+
})),
|
|
210
|
+
);
|
|
211
211
|
|
|
212
212
|
// Defer position update to next frame to ensure browser has completed layout
|
|
213
213
|
// after DOM changes from highlight application
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { en } from "./en";
|
|
3
|
+
import { ja } from "./ja";
|
|
4
|
+
|
|
5
|
+
describe("translation completeness", () => {
|
|
6
|
+
const enKeys = Object.keys(en).sort();
|
|
7
|
+
const jaKeys = Object.keys(ja).sort();
|
|
8
|
+
|
|
9
|
+
it("en and ja have the same keys", () => {
|
|
10
|
+
expect(enKeys).toEqual(jaKeys);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Prefix/suffix keys may be intentionally empty in some locales
|
|
14
|
+
// (e.g., Japanese has no prefix before the command)
|
|
15
|
+
const ALLOW_EMPTY = new Set(["app.noDocumentsHintPrefix"]);
|
|
16
|
+
|
|
17
|
+
it("no empty string values in en", () => {
|
|
18
|
+
for (const [key, value] of Object.entries(en)) {
|
|
19
|
+
if (ALLOW_EMPTY.has(key)) continue;
|
|
20
|
+
expect(value, `en.${key} is empty`).not.toBe("");
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("no empty string values in ja", () => {
|
|
25
|
+
for (const [key, value] of Object.entries(ja)) {
|
|
26
|
+
if (ALLOW_EMPTY.has(key)) continue;
|
|
27
|
+
expect(value, `ja.${key} is empty`).not.toBe("");
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("interpolation placeholders match between locales", () => {
|
|
32
|
+
const placeholderPattern = /\{\{(\w+)\}\}/g;
|
|
33
|
+
|
|
34
|
+
for (const key of enKeys) {
|
|
35
|
+
const enValue = en[key as keyof typeof en];
|
|
36
|
+
const jaValue = ja[key as keyof typeof ja];
|
|
37
|
+
|
|
38
|
+
const enPlaceholders = [...enValue.matchAll(placeholderPattern)]
|
|
39
|
+
.map((m) => m[1])
|
|
40
|
+
.sort();
|
|
41
|
+
const jaPlaceholders = [...jaValue.matchAll(placeholderPattern)]
|
|
42
|
+
.map((m) => m[1])
|
|
43
|
+
.sort();
|
|
44
|
+
|
|
45
|
+
expect(
|
|
46
|
+
enPlaceholders,
|
|
47
|
+
`Placeholder mismatch for key "${key}": en has ${JSON.stringify(enPlaceholders)}, ja has ${JSON.stringify(jaPlaceholders)}`,
|
|
48
|
+
).toEqual(jaPlaceholders);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|