@glw907/cairn-cms 0.51.0 → 0.52.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 (31) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/components/EditPage.svelte +38 -2
  3. package/dist/components/EditorToolbar.svelte +60 -6
  4. package/dist/components/EditorToolbar.svelte.d.ts +10 -1
  5. package/dist/components/MarkdownEditor.svelte +114 -24
  6. package/dist/components/MarkdownEditor.svelte.d.ts +4 -0
  7. package/dist/components/cairn-admin.css +27 -11
  8. package/dist/components/editor-highlight.d.ts +2 -1
  9. package/dist/components/editor-highlight.js +60 -19
  10. package/dist/components/editor-modes.d.ts +26 -0
  11. package/dist/components/editor-modes.js +92 -0
  12. package/dist/components/fonts/iAWriterMono-OFL.txt +100 -0
  13. package/dist/components/fonts/ia-writer-mono-latin-400-italic.woff2 +0 -0
  14. package/dist/components/fonts/ia-writer-mono-latin-400-normal.woff2 +0 -0
  15. package/dist/components/fonts/ia-writer-mono-latin-700-italic.woff2 +0 -0
  16. package/dist/components/fonts/ia-writer-mono-latin-700-normal.woff2 +0 -0
  17. package/dist/components/markdown-directives.d.ts +48 -7
  18. package/dist/components/markdown-directives.js +89 -13
  19. package/package.json +1 -1
  20. package/src/lib/components/EditPage.svelte +38 -2
  21. package/src/lib/components/EditorToolbar.svelte +60 -6
  22. package/src/lib/components/MarkdownEditor.svelte +114 -24
  23. package/src/lib/components/cairn-admin.css +62 -31
  24. package/src/lib/components/editor-highlight.ts +72 -20
  25. package/src/lib/components/editor-modes.ts +106 -0
  26. package/src/lib/components/fonts/iAWriterMono-OFL.txt +100 -0
  27. package/src/lib/components/fonts/ia-writer-mono-latin-400-italic.woff2 +0 -0
  28. package/src/lib/components/fonts/ia-writer-mono-latin-400-normal.woff2 +0 -0
  29. package/src/lib/components/fonts/ia-writer-mono-latin-700-italic.woff2 +0 -0
  30. package/src/lib/components/fonts/ia-writer-mono-latin-700-normal.woff2 +0 -0
  31. package/src/lib/components/markdown-directives.ts +113 -13
@@ -0,0 +1,106 @@
1
+ // The iA writing modes: focus mode dims every line outside the caret's paragraph, and typewriter
2
+ // scroll holds the cursor line at vertical center while typing. Client-only, like editor-highlight:
3
+ // MarkdownEditor reaches this module through a dynamic import, so the static @codemirror imports
4
+ // here never enter a server bundle (guarded by the editor-boundary test).
5
+ import {
6
+ Decoration,
7
+ EditorView,
8
+ ViewPlugin,
9
+ type DecorationSet,
10
+ type ViewUpdate,
11
+ } from '@codemirror/view';
12
+ import { RangeSetBuilder, type Extension } from '@codemirror/state';
13
+
14
+ /** An inclusive 0-based line range. */
15
+ export interface LineRange {
16
+ fromLine: number;
17
+ toLine: number;
18
+ }
19
+
20
+ const isBlank = (line: string | undefined) => !line || /^\s*$/.test(line);
21
+
22
+ /**
23
+ * The contiguous non-blank block around the caret line, the unit focus mode keeps at full ink.
24
+ * On a blank line the caret stands alone; the walk clamps at the document edges, and an
25
+ * out-of-range caret clamps into the document first.
26
+ */
27
+ export function paragraphRange(lines: string[], caretLine: number): LineRange {
28
+ const last = Math.max(lines.length - 1, 0);
29
+ const caret = Math.min(Math.max(caretLine, 0), last);
30
+ if (isBlank(lines[caret])) return { fromLine: caret, toLine: caret };
31
+ let fromLine = caret;
32
+ while (fromLine > 0 && !isBlank(lines[fromLine - 1])) fromLine--;
33
+ let toLine = caret;
34
+ while (toLine < last && !isBlank(lines[toLine + 1])) toLine++;
35
+ return { fromLine, toLine };
36
+ }
37
+
38
+ const dimLine = Decoration.line({ class: 'cm-cairn-focus-dim' });
39
+
40
+ // The line cache mirrors editor-highlight's: one full-document read per doc change, so a caret
41
+ // move or scroll rebuilds the viewport decorations from the cached array.
42
+ function docLines(view: EditorView): string[] {
43
+ const doc = view.state.doc;
44
+ const lines: string[] = [];
45
+ for (let n = 1; n <= doc.lines; n++) lines.push(doc.line(n).text);
46
+ return lines;
47
+ }
48
+
49
+ function buildFocusDecorations(view: EditorView, lines: string[]): DecorationSet {
50
+ const builder = new RangeSetBuilder<Decoration>();
51
+ const caretLine = view.state.doc.lineAt(view.state.selection.main.head).number - 1;
52
+ const paragraph = paragraphRange(lines, caretLine);
53
+ for (const { from, to } of view.visibleRanges) {
54
+ for (let pos = from; pos <= to; ) {
55
+ const line = view.state.doc.lineAt(pos);
56
+ const n = line.number - 1;
57
+ if (n < paragraph.fromLine || n > paragraph.toLine) builder.add(line.from, line.from, dimLine);
58
+ pos = line.to + 1;
59
+ }
60
+ }
61
+ return builder.finish();
62
+ }
63
+
64
+ /**
65
+ * Focus mode: a line class (`cm-cairn-focus-dim`) on every line outside the caret's paragraph.
66
+ * The class only marks the lines; the dim ink itself lives in the editor theme so the per-theme
67
+ * `--cairn-focus-dim-ink` variable resolves it.
68
+ */
69
+ export function focusMode(): Extension {
70
+ return ViewPlugin.fromClass(
71
+ class {
72
+ decorations: DecorationSet;
73
+ lines: string[];
74
+ constructor(view: EditorView) {
75
+ this.lines = docLines(view);
76
+ this.decorations = buildFocusDecorations(view, this.lines);
77
+ }
78
+ update(update: ViewUpdate) {
79
+ if (update.docChanged) this.lines = docLines(update.view);
80
+ if (update.docChanged || update.viewportChanged || update.selectionSet)
81
+ this.decorations = buildFocusDecorations(update.view, this.lines);
82
+ }
83
+ },
84
+ { decorations: (v) => v.decorations },
85
+ );
86
+ }
87
+
88
+ /**
89
+ * Typewriter scroll: on every doc change, recenter the selection head vertically. An update
90
+ * listener may not dispatch while its update runs, so the recenter is queued as a microtask:
91
+ * it fires as soon as the update finishes, still ahead of the next paint, where an animation
92
+ * frame would trail the edit by a frame. The isConnected guard skips a view destroyed in the
93
+ * queue window.
94
+ */
95
+ export function typewriterScroll(): Extension {
96
+ return EditorView.updateListener.of((update) => {
97
+ if (!update.docChanged) return;
98
+ const view = update.view;
99
+ queueMicrotask(() => {
100
+ if (!view.dom.isConnected) return;
101
+ view.dispatch({
102
+ effects: EditorView.scrollIntoView(view.state.selection.main.head, { y: 'center' }),
103
+ });
104
+ });
105
+ });
106
+ }
@@ -0,0 +1,100 @@
1
+ # iA Writer Typeface
2
+
3
+ Copyright © 2018 Information Architects Inc. with Reserved Font Name "iA Writer"
4
+
5
+ # Based on IBM Plex Typeface
6
+
7
+ Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
8
+
9
+ # License
10
+
11
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
12
+ This license is copied below, and is also available with a FAQ at:
13
+ http://scripts.sil.org/OFL
14
+
15
+ -----------------------------------------------------------
16
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
17
+ -----------------------------------------------------------
18
+
19
+ PREAMBLE
20
+ The goals of the Open Font License (OFL) are to stimulate worldwide
21
+ development of collaborative font projects, to support the font creation
22
+ efforts of academic and linguistic communities, and to provide a free and
23
+ open framework in which fonts may be shared and improved in partnership
24
+ with others.
25
+
26
+ The OFL allows the licensed fonts to be used, studied, modified and
27
+ redistributed freely as long as they are not sold by themselves. The
28
+ fonts, including any derivative works, can be bundled, embedded,
29
+ redistributed and/or sold with any software provided that any reserved
30
+ names are not used by derivative works. The fonts and derivatives,
31
+ however, cannot be released under any other type of license. The
32
+ requirement for fonts to remain under this license does not apply
33
+ to any document created using the fonts or their derivatives.
34
+
35
+ DEFINITIONS
36
+ "Font Software" refers to the set of files released by the Copyright
37
+ Holder(s) under this license and clearly marked as such. This may
38
+ include source files, build scripts and documentation.
39
+
40
+ "Reserved Font Name" refers to any names specified as such after the
41
+ copyright statement(s).
42
+
43
+ "Original Version" refers to the collection of Font Software components as
44
+ distributed by the Copyright Holder(s).
45
+
46
+ "Modified Version" refers to any derivative made by adding to, deleting,
47
+ or substituting -- in part or in whole -- any of the components of the
48
+ Original Version, by changing formats or by porting the Font Software to a
49
+ new environment.
50
+
51
+ "Author" refers to any designer, engineer, programmer, technical
52
+ writer or other person who contributed to the Font Software.
53
+
54
+ PERMISSION & CONDITIONS
55
+ Permission is hereby granted, free of charge, to any person obtaining
56
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
57
+ redistribute, and sell modified and unmodified copies of the Font
58
+ Software, subject to the following conditions:
59
+
60
+ 1) Neither the Font Software nor any of its individual components,
61
+ in Original or Modified Versions, may be sold by itself.
62
+
63
+ 2) Original or Modified Versions of the Font Software may be bundled,
64
+ redistributed and/or sold with any software, provided that each copy
65
+ contains the above copyright notice and this license. These can be
66
+ included either as stand-alone text files, human-readable headers or
67
+ in the appropriate machine-readable metadata fields within text or
68
+ binary files as long as those fields can be easily viewed by the user.
69
+
70
+ 3) No Modified Version of the Font Software may use the Reserved Font
71
+ Name(s) unless explicit written permission is granted by the corresponding
72
+ Copyright Holder. This restriction only applies to the primary font name as
73
+ presented to the users.
74
+
75
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
76
+ Software shall not be used to promote, endorse or advertise any
77
+ Modified Version, except to acknowledge the contribution(s) of the
78
+ Copyright Holder(s) and the Author(s) or with their explicit written
79
+ permission.
80
+
81
+ 5) The Font Software, modified or unmodified, in part or in whole,
82
+ must be distributed entirely under this license, and must not be
83
+ distributed under any other license. The requirement for fonts to
84
+ remain under this license does not apply to any document created
85
+ using the Font Software.
86
+
87
+ TERMINATION
88
+ This license becomes null and void if any of the above conditions are
89
+ not met.
90
+
91
+ DISCLAIMER
92
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
93
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
94
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
95
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
96
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
97
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
98
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
99
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
100
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -4,9 +4,11 @@
4
4
 
5
5
  // A container fence: three or more colons, then an optional name, an optional [label], and
6
6
  // optional {attrs}, in remark-directive order. The name is captured so the depth scan below can
7
- // tell an opener (named) from a closer (bare colons). Matching is tolerant of stray whitespace,
8
- // the same posture as the leaf form: a slightly off fence should still read as machinery.
9
- const FENCE = /^\s{0,3}:{3,}\s*([\w-]*)\s*(\[[^\]]*\])?\s*(\{[^}]*\})?\s*$/;
7
+ // tell an opener (named) from a closer (bare colons), and the d flag records each group's span
8
+ // so fenceTokens can split the line without re-parsing. Matching is tolerant of stray
9
+ // whitespace, the same posture as the leaf form: a slightly off fence should still read as
10
+ // machinery.
11
+ const FENCE = /^\s{0,3}(:{3,})\s*([\w-]*)\s*(\[[^\]]*\])?\s*(\{[^}]*\})?\s*$/d;
10
12
  const LEAF = /^\s{0,3}::[\w-]+(\[[^\]]*\])?(\{[^}]*\})?\s*$/;
11
13
  const INLINE = /(?<![:\w]):[\w-]+\[[^\]]*\](\{[^}]*\})?/g;
12
14
 
@@ -22,17 +24,28 @@ export function directiveLineKind(line: string): 'fence' | 'leaf' | null {
22
24
  return null;
23
25
  }
24
26
 
27
+ /** One pass over the document: each line's container depth alongside its fence role. */
28
+ export interface FenceScan {
29
+ /** The 1-based container depth per line, or null outside any container. */
30
+ depths: (number | null)[];
31
+ /** Whether a line opened or closed a container, or null for everything else. A fence-shaped
32
+ * line the code-block tracking disowned is null too, so the role array is the one source of
33
+ * truth for pairing and no caller re-parses a line the scan already judged. */
34
+ roles: ('opener' | 'closer' | null)[];
35
+ }
36
+
25
37
  /**
26
- * The 1-based container depth each line sits at, or null outside any container. A named fence
27
- * opens a container; a bare fence closes the most recent one (colon counts are not trusted for
28
- * pairing, since authors vary them). An opener and its closer share the opener's depth, and a
29
- * line between them carries the depth of its innermost container. Lines inside a fenced code
30
- * block are plain content, so a documented ::: example cannot open a phantom container running
31
- * to end of document. Author errors are tolerated: an unmatched closer reads as depth 1 and the
32
- * count never goes below zero.
38
+ * Scan the document's container structure in one pass. A named fence opens a container; a bare
39
+ * fence closes the most recent one (colon counts are not trusted for pairing, since authors
40
+ * vary them). An opener and its closer share the opener's depth, and a line between them
41
+ * carries the depth of its innermost container. Lines inside a fenced code block are plain
42
+ * content, so a documented ::: example cannot open a phantom container running to end of
43
+ * document. Author errors are tolerated: an unmatched closer reads as depth 1 and the count
44
+ * never goes below zero.
33
45
  */
34
- export function fenceDepths(lines: string[]): (number | null)[] {
46
+ export function fenceScan(lines: string[]): FenceScan {
35
47
  const depths: (number | null)[] = [];
48
+ const roles: ('opener' | 'closer' | null)[] = [];
36
49
  let open = 0;
37
50
  // The marker character that opened the current code block, or null outside one. Only a line
38
51
  // opening with the same character closes it, so tildes inside a backtick block stay literal.
@@ -43,24 +56,111 @@ export function fenceDepths(lines: string[]): (number | null)[] {
43
56
  if (codeMarker === null) codeMarker = code[1][0];
44
57
  else if (code[1][0] === codeMarker) codeMarker = null;
45
58
  depths.push(open > 0 ? open : null);
59
+ roles.push(null);
46
60
  continue;
47
61
  }
48
62
  if (codeMarker !== null) {
49
63
  depths.push(open > 0 ? open : null);
64
+ roles.push(null);
50
65
  continue;
51
66
  }
52
67
  const fence = FENCE.exec(line);
53
68
  if (!fence) {
54
69
  depths.push(open > 0 ? open : null);
55
- } else if (fence[1]) {
70
+ roles.push(null);
71
+ } else if (fence[2]) {
56
72
  open += 1;
57
73
  depths.push(open);
74
+ roles.push('opener');
58
75
  } else {
59
76
  depths.push(Math.max(open, 1));
77
+ roles.push('closer');
60
78
  if (open > 0) open -= 1;
61
79
  }
62
80
  }
63
- return depths;
81
+ return { depths, roles };
82
+ }
83
+
84
+ /** The depth half of {@link fenceScan}, for callers that need no roles. */
85
+ export function fenceDepths(lines: string[]): (number | null)[] {
86
+ return fenceScan(lines).depths;
87
+ }
88
+
89
+ /** The inclusive line span of one directive container. */
90
+ export interface ContainerRange {
91
+ fromLine: number;
92
+ toLine: number;
93
+ depth: number;
94
+ }
95
+
96
+ /**
97
+ * The innermost container around a caret line, as an inclusive line range, or null outside any
98
+ * container. Works from the cached scan without re-parsing: the caret line's own depth names
99
+ * the container (fence rows carry the depth they delimit, so a caret on a fence belongs to
100
+ * that fence's container), and within a container the only same-depth real fences are its
101
+ * opener and closer (nested containers sit deeper, siblings sit outside), so the nearest
102
+ * opener above and the nearest closer below bound the range. The scan's roles already disown a
103
+ * fence-shaped line inside a code block, so a documented example can never clip the range. An
104
+ * unclosed container runs to the document end.
105
+ */
106
+ export function caretContainerRange(scan: FenceScan, caretLine: number): ContainerRange | null {
107
+ const { depths, roles } = scan;
108
+ const depth = depths[caretLine] ?? null;
109
+ if (depth === null) return null;
110
+ let fromLine = caretLine;
111
+ for (let i = caretLine; i >= 0; i--) {
112
+ if (depths[i] === depth && roles[i] === 'opener') {
113
+ fromLine = i;
114
+ break;
115
+ }
116
+ }
117
+ let toLine = depths.length - 1;
118
+ for (let i = caretLine; i < depths.length; i++) {
119
+ if (depths[i] === depth && roles[i] === 'closer') {
120
+ toLine = i;
121
+ break;
122
+ }
123
+ }
124
+ return { fromLine, toLine, depth };
125
+ }
126
+
127
+ /** One span of a fence line, in line-local offsets: machinery (`mark`) or meaning (`label`). */
128
+ export interface FenceToken {
129
+ from: number;
130
+ to: number;
131
+ kind: 'mark' | 'label';
132
+ }
133
+
134
+ /**
135
+ * Split a fence line into machinery and meaning. The colon run, the label's brackets, and the
136
+ * whole {attrs} group are machinery; the directive name and the label text are meaning, the
137
+ * parts an editor reads. A bare closer is a single machinery span, and a non-fence line yields
138
+ * no spans at all.
139
+ */
140
+ export function fenceTokens(line: string): FenceToken[] {
141
+ const m = FENCE.exec(line);
142
+ if (!m?.indices) return [];
143
+ // A group's span exists whenever the group matched: group 1 (the colons) always does on a
144
+ // fence, and the optional groups are read only behind their own m[n] guard.
145
+ const indices = m.indices;
146
+ const out: FenceToken[] = [];
147
+ const [colonFrom, colonTo] = indices[1]!;
148
+ out.push({ from: colonFrom, to: colonTo, kind: 'mark' });
149
+ if (m[2]) {
150
+ const [from, to] = indices[2]!;
151
+ out.push({ from, to, kind: 'label' });
152
+ }
153
+ if (m[3]) {
154
+ const [from, to] = indices[3]!;
155
+ out.push({ from, to: from + 1, kind: 'mark' });
156
+ if (to - from > 2) out.push({ from: from + 1, to: to - 1, kind: 'label' });
157
+ out.push({ from: to - 1, to, kind: 'mark' });
158
+ }
159
+ if (m[4]) {
160
+ const [from, to] = indices[4]!;
161
+ out.push({ from, to, kind: 'mark' });
162
+ }
163
+ return out;
64
164
  }
65
165
 
66
166
  /** Inline directive ranges (`:name[...]{...}`) within a line of text. */