@tarhnama/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/dist/code-tokens.d.ts +39 -0
- package/dist/code-tokens.d.ts.map +1 -0
- package/dist/code-tokens.js +189 -0
- package/dist/code-tokens.js.map +1 -0
- package/dist/context-menu.d.ts +59 -0
- package/dist/context-menu.d.ts.map +1 -0
- package/dist/context-menu.js +69 -0
- package/dist/context-menu.js.map +1 -0
- package/dist/features.d.ts +114 -0
- package/dist/features.d.ts.map +1 -0
- package/dist/features.js +128 -0
- package/dist/features.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/nested.d.ts +66 -0
- package/dist/nested.d.ts.map +1 -0
- package/dist/nested.js +194 -0
- package/dist/nested.js.map +1 -0
- package/dist/ops.d.ts +120 -0
- package/dist/ops.d.ts.map +1 -0
- package/dist/ops.js +333 -0
- package/dist/ops.js.map +1 -0
- package/dist/shortcuts.d.ts +90 -0
- package/dist/shortcuts.d.ts.map +1 -0
- package/dist/shortcuts.js +179 -0
- package/dist/shortcuts.js.map +1 -0
- package/dist/spine.d.ts +92 -0
- package/dist/spine.d.ts.map +1 -0
- package/dist/spine.js +213 -0
- package/dist/spine.js.map +1 -0
- package/dist/styles.d.ts +63 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +169 -0
- package/dist/styles.js.map +1 -0
- package/dist/tree.d.ts +70 -0
- package/dist/tree.d.ts.map +1 -0
- package/dist/tree.js +190 -0
- package/dist/tree.js.map +1 -0
- package/dist/types.d.ts +239 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +47 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -0
- package/src/code-tokens.ts +236 -0
- package/src/context-menu.ts +139 -0
- package/src/features.ts +173 -0
- package/src/index.ts +58 -0
- package/src/nested.ts +271 -0
- package/src/ops.ts +341 -0
- package/src/shortcuts.ts +280 -0
- package/src/spine.ts +214 -0
- package/src/styles.ts +201 -0
- package/src/tree.ts +189 -0
- package/src/types.ts +275 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mahdi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# @tarhnama/core
|
|
2
|
+
|
|
3
|
+
Framework-agnostic outline document model: flat blocks with outline levels, plus a derived tree index. No DOM, no editor dependency.
|
|
4
|
+
|
|
5
|
+
No DOM and no editor dependency, so it runs in Node — batch conversion, indexing, tests.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @tarhnama/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Part of **طرحنما**, a Persian-first outline editor. Everything is optional — features,
|
|
12
|
+
styling, and copy — so it can be embedded without bringing opinions you did not ask for.
|
|
13
|
+
See the [consumer guide](https://github.com/mahdi/tarhnama/blob/main/docs/CONSUMING.md) for the styling levels,
|
|
14
|
+
the feature switches and the i18n contract.
|
|
15
|
+
|
|
16
|
+
Licensed MIT.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The colouring 1a draws in a code block — keywords in the accent, strings in green —
|
|
3
|
+
* as a pure function over text, so the editor's decorations and both readers colour the
|
|
4
|
+
* same characters.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately small. It is not a parser and does not try to be: a full grammar per
|
|
7
|
+
* language is a dependency this package must not force on a host, and a host that wants
|
|
8
|
+
* one passes its own tokenizer with the same signature. What it does, it does exactly —
|
|
9
|
+
* whole words only, escapes honoured, comment markers inside strings left alone — because
|
|
10
|
+
* a highlighter that is right most of the time teaches the reader to distrust the colours.
|
|
11
|
+
*
|
|
12
|
+
* Numbers and identifiers are not tokens. The prototype colours neither consistently
|
|
13
|
+
* (`editor` is accent once in 1a and plain on the next line; `const` is plain in 2a-06),
|
|
14
|
+
* so there is no rule to measure — see docs/DESIGN-REQUESTS.md.
|
|
15
|
+
*/
|
|
16
|
+
export type CodeTokenKind = 'keyword' | 'string' | 'comment';
|
|
17
|
+
export interface CodeToken {
|
|
18
|
+
/** Offset of the first character, in UTF-16 code units of the block's text. */
|
|
19
|
+
readonly from: number;
|
|
20
|
+
/** Offset after the last character. */
|
|
21
|
+
readonly to: number;
|
|
22
|
+
readonly kind: CodeTokenKind;
|
|
23
|
+
}
|
|
24
|
+
/** A host-supplied replacement for `tokenizeCode`, e.g. one backed by a real grammar. */
|
|
25
|
+
export type CodeTokenizer = (code: string, lang: string | undefined) => readonly CodeToken[];
|
|
26
|
+
export declare function tokenizeCode(code: string, lang: string | undefined): CodeToken[];
|
|
27
|
+
export interface CodeSegment {
|
|
28
|
+
readonly text: string;
|
|
29
|
+
/** Absent for the text between tokens. */
|
|
30
|
+
readonly kind?: CodeTokenKind;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The text cut into runs, each token its own. Every renderer goes through this, so the
|
|
34
|
+
* editor and both readers colour the same characters — including when a HOST tokenizer
|
|
35
|
+
* returns something malformed: a token that is empty, out of range or overlaps the one
|
|
36
|
+
* before is skipped, and the text itself always comes back whole.
|
|
37
|
+
*/
|
|
38
|
+
export declare function codeSegments(code: string, tokens: readonly CodeToken[]): CodeSegment[];
|
|
39
|
+
//# sourceMappingURL=code-tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"code-tokens.d.ts","sourceRoot":"","sources":["../src/code-tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE7D,MAAM,WAAW,SAAS;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;CAC9B;AAED,yFAAyF;AACzF,MAAM,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,KAAK,SAAS,SAAS,EAAE,CAAC;AAiG7F,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,EAAE,CA4DhF;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,SAAS,EAAE,GAAG,WAAW,EAAE,CAWtF"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The colouring 1a draws in a code block — keywords in the accent, strings in green —
|
|
3
|
+
* as a pure function over text, so the editor's decorations and both readers colour the
|
|
4
|
+
* same characters.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately small. It is not a parser and does not try to be: a full grammar per
|
|
7
|
+
* language is a dependency this package must not force on a host, and a host that wants
|
|
8
|
+
* one passes its own tokenizer with the same signature. What it does, it does exactly —
|
|
9
|
+
* whole words only, escapes honoured, comment markers inside strings left alone — because
|
|
10
|
+
* a highlighter that is right most of the time teaches the reader to distrust the colours.
|
|
11
|
+
*
|
|
12
|
+
* Numbers and identifiers are not tokens. The prototype colours neither consistently
|
|
13
|
+
* (`editor` is accent once in 1a and plain on the next line; `const` is plain in 2a-06),
|
|
14
|
+
* so there is no rule to measure — see docs/DESIGN-REQUESTS.md.
|
|
15
|
+
*/
|
|
16
|
+
const words = (list) => new Set(list.split(/\s+/).filter(Boolean));
|
|
17
|
+
const C_LIKE = {
|
|
18
|
+
lineComment: '//',
|
|
19
|
+
blockComment: true,
|
|
20
|
+
quotes: `'"\``,
|
|
21
|
+
rawQuotes: '',
|
|
22
|
+
multilineQuotes: '`',
|
|
23
|
+
tripleQuotes: false,
|
|
24
|
+
hashNeedsBoundary: false,
|
|
25
|
+
memberAccessIsNotKeyword: true,
|
|
26
|
+
};
|
|
27
|
+
/*
|
|
28
|
+
* Contextual words that are routinely identifiers (`type`, `from`, `of`, `as`, `get`, `set`)
|
|
29
|
+
* are left out: colouring `const type = …` as a keyword is wrong far more often than
|
|
30
|
+
* leaving `import x from` plain is.
|
|
31
|
+
*/
|
|
32
|
+
const JS = {
|
|
33
|
+
...C_LIKE,
|
|
34
|
+
keywords: words(`
|
|
35
|
+
abstract async await break case catch class const continue debugger declare default delete
|
|
36
|
+
do else enum export extends false finally for function if implements import in instanceof
|
|
37
|
+
interface keyof let namespace new null private protected public readonly return satisfies
|
|
38
|
+
static super switch this throw true try typeof undefined var void while with yield
|
|
39
|
+
`),
|
|
40
|
+
};
|
|
41
|
+
const JSON_GRAMMAR = {
|
|
42
|
+
keywords: words('true false null'),
|
|
43
|
+
lineComment: null,
|
|
44
|
+
blockComment: false,
|
|
45
|
+
quotes: '"',
|
|
46
|
+
rawQuotes: '',
|
|
47
|
+
multilineQuotes: '',
|
|
48
|
+
tripleQuotes: false,
|
|
49
|
+
hashNeedsBoundary: false,
|
|
50
|
+
memberAccessIsNotKeyword: false,
|
|
51
|
+
};
|
|
52
|
+
const PYTHON = {
|
|
53
|
+
keywords: words(`
|
|
54
|
+
False None True and as assert async await break class continue def del elif else except
|
|
55
|
+
finally for from global if import in is lambda nonlocal not or pass raise return try while
|
|
56
|
+
with yield
|
|
57
|
+
`),
|
|
58
|
+
lineComment: '#',
|
|
59
|
+
blockComment: false,
|
|
60
|
+
quotes: `'"`,
|
|
61
|
+
rawQuotes: '',
|
|
62
|
+
multilineQuotes: '',
|
|
63
|
+
tripleQuotes: true,
|
|
64
|
+
hashNeedsBoundary: false,
|
|
65
|
+
memberAccessIsNotKeyword: true,
|
|
66
|
+
};
|
|
67
|
+
const SHELL = {
|
|
68
|
+
keywords: words('if then else elif fi case esac for while until do done in function return exit local export select'),
|
|
69
|
+
lineComment: '#',
|
|
70
|
+
blockComment: false,
|
|
71
|
+
quotes: `'"`,
|
|
72
|
+
rawQuotes: "'",
|
|
73
|
+
multilineQuotes: `'"`,
|
|
74
|
+
tripleQuotes: false,
|
|
75
|
+
hashNeedsBoundary: true,
|
|
76
|
+
memberAccessIsNotKeyword: false,
|
|
77
|
+
};
|
|
78
|
+
const GRAMMARS = {
|
|
79
|
+
js: JS, jsx: JS, mjs: JS, cjs: JS, javascript: JS,
|
|
80
|
+
ts: JS, tsx: JS, mts: JS, cts: JS, typescript: JS,
|
|
81
|
+
json: JSON_GRAMMAR, jsonc: { ...JSON_GRAMMAR, lineComment: '//', blockComment: true },
|
|
82
|
+
py: PYTHON, python: PYTHON, python3: PYTHON,
|
|
83
|
+
sh: SHELL, bash: SHELL, zsh: SHELL, shell: SHELL, console: SHELL,
|
|
84
|
+
};
|
|
85
|
+
const WORD_START = /[\p{L}_$]/u;
|
|
86
|
+
const WORD_PART = /[\p{L}\p{N}_$]/u;
|
|
87
|
+
export function tokenizeCode(code, lang) {
|
|
88
|
+
const grammar = lang ? GRAMMARS[lang.trim().toLowerCase()] : undefined;
|
|
89
|
+
if (!grammar)
|
|
90
|
+
return [];
|
|
91
|
+
const tokens = [];
|
|
92
|
+
const length = code.length;
|
|
93
|
+
let i = 0;
|
|
94
|
+
while (i < length) {
|
|
95
|
+
const ch = code[i];
|
|
96
|
+
if (grammar.lineComment && code.startsWith(grammar.lineComment, i)) {
|
|
97
|
+
const atBoundary = i === 0 || /\s/.test(code[i - 1]);
|
|
98
|
+
if (!grammar.hashNeedsBoundary || atBoundary) {
|
|
99
|
+
const end = lineEnd(code, i);
|
|
100
|
+
tokens.push({ from: i, to: end, kind: 'comment' });
|
|
101
|
+
i = end;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (grammar.blockComment && code.startsWith('/*', i)) {
|
|
106
|
+
const close = code.indexOf('*/', i + 2);
|
|
107
|
+
const end = close === -1 ? length : close + 2;
|
|
108
|
+
tokens.push({ from: i, to: end, kind: 'comment' });
|
|
109
|
+
i = end;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (grammar.quotes.includes(ch)) {
|
|
113
|
+
const end = grammar.tripleQuotes && code.startsWith(ch.repeat(3), i)
|
|
114
|
+
? tripleQuoteEnd(code, i, ch)
|
|
115
|
+
: quoteEnd(code, i, ch, grammar.multilineQuotes.includes(ch), !grammar.rawQuotes.includes(ch));
|
|
116
|
+
tokens.push({ from: i, to: end, kind: 'string' });
|
|
117
|
+
i = end;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (WORD_START.test(ch)) {
|
|
121
|
+
let end = i + 1;
|
|
122
|
+
while (end < length && WORD_PART.test(code[end]))
|
|
123
|
+
end += 1;
|
|
124
|
+
const word = code.slice(i, end);
|
|
125
|
+
const member = grammar.memberAccessIsNotKeyword && i > 0 && code[i - 1] === '.';
|
|
126
|
+
if (!member && grammar.keywords.has(word))
|
|
127
|
+
tokens.push({ from: i, to: end, kind: 'keyword' });
|
|
128
|
+
i = end;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// A digit run is skipped whole, so `1e5` or `0x1f` never starts a word in its middle.
|
|
132
|
+
if (/\p{N}/u.test(ch)) {
|
|
133
|
+
let end = i + 1;
|
|
134
|
+
while (end < length && WORD_PART.test(code[end]))
|
|
135
|
+
end += 1;
|
|
136
|
+
i = end;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
i += 1;
|
|
140
|
+
}
|
|
141
|
+
return tokens;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The text cut into runs, each token its own. Every renderer goes through this, so the
|
|
145
|
+
* editor and both readers colour the same characters — including when a HOST tokenizer
|
|
146
|
+
* returns something malformed: a token that is empty, out of range or overlaps the one
|
|
147
|
+
* before is skipped, and the text itself always comes back whole.
|
|
148
|
+
*/
|
|
149
|
+
export function codeSegments(code, tokens) {
|
|
150
|
+
const segments = [];
|
|
151
|
+
let at = 0;
|
|
152
|
+
for (const token of tokens) {
|
|
153
|
+
if (token.from < at || token.to <= token.from || token.to > code.length)
|
|
154
|
+
continue;
|
|
155
|
+
if (token.from > at)
|
|
156
|
+
segments.push({ text: code.slice(at, token.from) });
|
|
157
|
+
segments.push({ text: code.slice(token.from, token.to), kind: token.kind });
|
|
158
|
+
at = token.to;
|
|
159
|
+
}
|
|
160
|
+
if (at < code.length)
|
|
161
|
+
segments.push({ text: code.slice(at) });
|
|
162
|
+
return segments;
|
|
163
|
+
}
|
|
164
|
+
function lineEnd(code, from) {
|
|
165
|
+
const newline = code.indexOf('\n', from);
|
|
166
|
+
return newline === -1 ? code.length : newline;
|
|
167
|
+
}
|
|
168
|
+
/** After the closing quote; an unterminated single-line quote stops at the end of its line. */
|
|
169
|
+
function quoteEnd(code, from, quote, multiline, escapes) {
|
|
170
|
+
let i = from + 1;
|
|
171
|
+
while (i < code.length) {
|
|
172
|
+
const ch = code[i];
|
|
173
|
+
if (escapes && ch === '\\') {
|
|
174
|
+
i += 2;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (ch === quote)
|
|
178
|
+
return i + 1;
|
|
179
|
+
if (ch === '\n' && !multiline)
|
|
180
|
+
return i;
|
|
181
|
+
i += 1;
|
|
182
|
+
}
|
|
183
|
+
return code.length;
|
|
184
|
+
}
|
|
185
|
+
function tripleQuoteEnd(code, from, quote) {
|
|
186
|
+
const close = code.indexOf(quote.repeat(3), from + 3);
|
|
187
|
+
return close === -1 ? code.length : close + 3;
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=code-tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"code-tokens.js","sourceRoot":"","sources":["../src/code-tokens.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { OutlineFeatureName, OutlineFeatures } from './features.js';
|
|
2
|
+
/**
|
|
3
|
+
* What a right-click on the document offers.
|
|
4
|
+
*
|
|
5
|
+
* The spine's handle already has a node menu. The TEXT does not, so right-clicking a
|
|
6
|
+
* paragraph gets the browser's own menu — spell-check and "view source", nothing about the
|
|
7
|
+
* document you are writing.
|
|
8
|
+
*
|
|
9
|
+
* Computed as DATA, like the shortcut registry and for the same reasons: a host can render
|
|
10
|
+
* it in their own menu component, translate it through their own label pack, filter it, or
|
|
11
|
+
* ignore it entirely. Nothing here holds a closure or touches the DOM.
|
|
12
|
+
*/
|
|
13
|
+
export type ContextCommand = 'cut' | 'copy' | 'paste' | 'undo' | 'redo' | 'indent' | 'outdent' | 'moveUp' | 'moveDown' | 'toggleCollapse' | 'isolate' | 'focus' | 'toggleLock';
|
|
14
|
+
/** What the host knows about the place that was right-clicked. */
|
|
15
|
+
export interface ContextTarget {
|
|
16
|
+
readonly blockId: string;
|
|
17
|
+
readonly level: number;
|
|
18
|
+
readonly hasSelection: boolean;
|
|
19
|
+
readonly locked: boolean;
|
|
20
|
+
readonly collapsed: boolean;
|
|
21
|
+
/** Whether this heading governs anything — a fold with nothing under it does nothing. */
|
|
22
|
+
readonly governsSubtree: boolean;
|
|
23
|
+
readonly canUndo: boolean;
|
|
24
|
+
readonly canRedo: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Whether a sibling exists to move past.
|
|
27
|
+
*
|
|
28
|
+
* Part of the target rather than derived here, for the same reason as the undo stack:
|
|
29
|
+
* this function is handed one block, and "is there a previous sibling" is a question only
|
|
30
|
+
* the whole document can answer. Getting it wrong is not cosmetic — "move down" was live
|
|
31
|
+
* on a heading with nowhere to go, and choosing it did nothing.
|
|
32
|
+
*/
|
|
33
|
+
readonly canMoveUp: boolean;
|
|
34
|
+
readonly canMoveDown: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface ContextMenuItem {
|
|
37
|
+
readonly command: ContextCommand;
|
|
38
|
+
/** English; a host translates through their own label pack. */
|
|
39
|
+
readonly label: string;
|
|
40
|
+
readonly enabled: boolean;
|
|
41
|
+
/** A rule drawn before this item. Named here so a renderer does not invent grouping. */
|
|
42
|
+
readonly separator?: boolean;
|
|
43
|
+
readonly feature?: OutlineFeatureName;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Build the menu for one right-click.
|
|
47
|
+
*
|
|
48
|
+
* Two rules, and the difference between them is the whole design:
|
|
49
|
+
*
|
|
50
|
+
* **A command that cannot act right now is DIMMED.** The menu has the same shape every
|
|
51
|
+
* time it opens, so its geometry can be learned and the third item is always the third
|
|
52
|
+
* item. This is the rule the toolbars follow.
|
|
53
|
+
*
|
|
54
|
+
* **A command whose FEATURE is off is REMOVED.** Dimming says "not now", which is a
|
|
55
|
+
* promise. A feature the host switched off is never coming back, and leaving it there
|
|
56
|
+
* spends the reader's attention on it at every opening, forever.
|
|
57
|
+
*/
|
|
58
|
+
export declare function contextMenuFor(target: ContextTarget, features: OutlineFeatures): readonly ContextMenuItem[];
|
|
59
|
+
//# sourceMappingURL=context-menu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-menu.d.ts","sourceRoot":"","sources":["../src/context-menu.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEzE;;;;;;;;;;GAUG;AAEH,MAAM,MAAM,cAAc,GACtB,KAAK,GACL,MAAM,GACN,OAAO,GACP,MAAM,GACN,MAAM,GACN,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,UAAU,GACV,gBAAgB,GAChB,SAAS,GACT,OAAO,GACP,YAAY,CAAC;AAEjB,kEAAkE;AAClE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,yFAAyF;IACzF,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,wFAAwF;IACxF,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,kBAAkB,CAAC;CACvC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,aAAa,EACrB,QAAQ,EAAE,eAAe,GACxB,SAAS,eAAe,EAAE,CA4D5B"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the menu for one right-click.
|
|
3
|
+
*
|
|
4
|
+
* Two rules, and the difference between them is the whole design:
|
|
5
|
+
*
|
|
6
|
+
* **A command that cannot act right now is DIMMED.** The menu has the same shape every
|
|
7
|
+
* time it opens, so its geometry can be learned and the third item is always the third
|
|
8
|
+
* item. This is the rule the toolbars follow.
|
|
9
|
+
*
|
|
10
|
+
* **A command whose FEATURE is off is REMOVED.** Dimming says "not now", which is a
|
|
11
|
+
* promise. A feature the host switched off is never coming back, and leaving it there
|
|
12
|
+
* spends the reader's attention on it at every opening, forever.
|
|
13
|
+
*/
|
|
14
|
+
export function contextMenuFor(target, features) {
|
|
15
|
+
// A lock stops EDITING, never reading — so copy survives it and the mutating pair does not.
|
|
16
|
+
const editable = !target.locked;
|
|
17
|
+
const all = [
|
|
18
|
+
{
|
|
19
|
+
command: 'cut',
|
|
20
|
+
label: 'Cut',
|
|
21
|
+
enabled: target.hasSelection && editable,
|
|
22
|
+
feature: 'clipboard',
|
|
23
|
+
},
|
|
24
|
+
{ command: 'copy', label: 'Copy', enabled: target.hasSelection, feature: 'clipboard' },
|
|
25
|
+
{ command: 'paste', label: 'Paste', enabled: editable, feature: 'clipboard' },
|
|
26
|
+
{
|
|
27
|
+
command: 'undo',
|
|
28
|
+
label: 'Undo',
|
|
29
|
+
enabled: target.canUndo,
|
|
30
|
+
separator: true,
|
|
31
|
+
feature: 'history',
|
|
32
|
+
},
|
|
33
|
+
{ command: 'redo', label: 'Redo', enabled: target.canRedo, feature: 'history' },
|
|
34
|
+
/*
|
|
35
|
+
* Structural commands carry no feature: indent and outdent ARE the outline, not a
|
|
36
|
+
* feature of it, and an editor without them is a different product rather than this
|
|
37
|
+
* one configured down.
|
|
38
|
+
*/
|
|
39
|
+
{ command: 'indent', label: 'Indent', enabled: editable && target.level > 0, separator: true },
|
|
40
|
+
{ command: 'outdent', label: 'Outdent', enabled: editable && target.level > 1 },
|
|
41
|
+
{ command: 'moveUp', label: 'Move up', enabled: editable && target.canMoveUp },
|
|
42
|
+
{ command: 'moveDown', label: 'Move down', enabled: editable && target.canMoveDown },
|
|
43
|
+
{
|
|
44
|
+
command: 'toggleCollapse',
|
|
45
|
+
label: target.collapsed ? 'Expand' : 'Collapse',
|
|
46
|
+
// A heading that governs nothing has nothing to fold — the spine's eighth state,
|
|
47
|
+
// in menu form.
|
|
48
|
+
enabled: target.governsSubtree,
|
|
49
|
+
separator: true,
|
|
50
|
+
feature: 'collapse',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
command: 'isolate',
|
|
54
|
+
label: 'Keep only this subtree open',
|
|
55
|
+
enabled: target.governsSubtree,
|
|
56
|
+
feature: 'collapse',
|
|
57
|
+
},
|
|
58
|
+
{ command: 'focus', label: 'Focus this subtree', enabled: target.governsSubtree },
|
|
59
|
+
{
|
|
60
|
+
command: 'toggleLock',
|
|
61
|
+
label: target.locked ? 'Unlock' : 'Lock',
|
|
62
|
+
enabled: true,
|
|
63
|
+
separator: true,
|
|
64
|
+
feature: 'lock',
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
return all.filter((item) => item.feature === undefined || features[item.feature]);
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=context-menu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-menu.js","sourceRoot":"","sources":["../src/context-menu.ts"],"names":[],"mappings":"AA8DA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc,CAC5B,MAAqB,EACrB,QAAyB;IAEzB,4FAA4F;IAC5F,MAAM,QAAQ,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC;IAEhC,MAAM,GAAG,GAAsB;QAC7B;YACE,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,KAAK;YACZ,OAAO,EAAE,MAAM,CAAC,YAAY,IAAI,QAAQ;YACxC,OAAO,EAAE,WAAW;SACrB;QACD,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE;QACtF,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE;QAE7E;YACE,OAAO,EAAE,MAAM;YACf,KAAK,EAAE,MAAM;YACb,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,SAAS,EAAE,IAAI;YACf,OAAO,EAAE,SAAS;SACnB;QACD,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE;QAE/E;;;;WAIG;QACH,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,IAAI,MAAM,CAAC,KAAK,GAAG,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE;QAC9F,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,IAAI,MAAM,CAAC,KAAK,GAAG,CAAC,EAAE;QAC/E,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,IAAI,MAAM,CAAC,SAAS,EAAE;QAC9E,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,IAAI,MAAM,CAAC,WAAW,EAAE;QAEpF;YACE,OAAO,EAAE,gBAAgB;YACzB,KAAK,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU;YAC/C,iFAAiF;YACjF,gBAAgB;YAChB,OAAO,EAAE,MAAM,CAAC,cAAc;YAC9B,SAAS,EAAE,IAAI;YACf,OAAO,EAAE,UAAU;SACpB;QACD;YACE,OAAO,EAAE,SAAS;YAClB,KAAK,EAAE,6BAA6B;YACpC,OAAO,EAAE,MAAM,CAAC,cAAc;YAC9B,OAAO,EAAE,UAAU;SACpB;QACD,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,CAAC,cAAc,EAAE;QAEjF;YACE,OAAO,EAAE,YAAY;YACrB,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM;YACxC,OAAO,EAAE,IAAI;YACb,SAAS,EAAE,IAAI;YACf,OAAO,EAAE,MAAM;SAChB;KACF,CAAC;IAEF,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;AACpF,CAAC"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a host turns on.
|
|
3
|
+
*
|
|
4
|
+
* This editor is embedded in projects that want very different subsets of it: a note pane
|
|
5
|
+
* that needs no cross-document links, a review surface that needs no formatting, a viewer
|
|
6
|
+
* that needs no editing at all. Every one of those is a supported configuration rather
|
|
7
|
+
* than a fork.
|
|
8
|
+
*
|
|
9
|
+
* ── The rule that shapes this whole file ───────────────────────────────────────
|
|
10
|
+
*
|
|
11
|
+
* **Features gate BEHAVIOUR, never the schema.**
|
|
12
|
+
*
|
|
13
|
+
* It is tempting to make `links: false` remove the link mark from the document schema —
|
|
14
|
+
* it would be smaller, and it would tree-shake. It is also data loss: ProseMirror strips
|
|
15
|
+
* any mark its schema does not know, so a host with links disabled that opens a document
|
|
16
|
+
* containing links **destroys them on load**, silently, and writes the damaged version
|
|
17
|
+
* back on the next save. A document must round-trip through every configuration
|
|
18
|
+
* unchanged.
|
|
19
|
+
*
|
|
20
|
+
* So the schema is always complete, and a disabled feature removes its input rules, its
|
|
21
|
+
* keymap entries, its commands, its decorations and its UI. Bundle size is addressed the
|
|
22
|
+
* honest way instead — subpath exports, so a host that never imports the link picker
|
|
23
|
+
* never ships it.
|
|
24
|
+
*/
|
|
25
|
+
/** Every switchable behaviour. Deliberately flat and small; this is a public contract. */
|
|
26
|
+
export interface OutlineFeatures {
|
|
27
|
+
/**
|
|
28
|
+
* `[[` autocomplete, anchor commands, link input rules, reference previews.
|
|
29
|
+
* Existing link marks still render and still round-trip when this is off.
|
|
30
|
+
*/
|
|
31
|
+
readonly links: boolean;
|
|
32
|
+
/** Bold/italic/colour/highlight commands and the formatting keymap (⌘B and friends). */
|
|
33
|
+
readonly formatting: boolean;
|
|
34
|
+
/** Collapse state, the spine, the hidden-count decorations, ⌘. and the ladder. */
|
|
35
|
+
readonly collapse: boolean;
|
|
36
|
+
/** Read-only subtrees, the lock guard, and the padlock affordance. */
|
|
37
|
+
readonly lock: boolean;
|
|
38
|
+
/** Undo/redo. Turn OFF when adopting Yjs: two undo managers fight (see CLAUDE.md §4). */
|
|
39
|
+
readonly history: boolean;
|
|
40
|
+
/** Table editing commands. Existing tables still render either way. */
|
|
41
|
+
readonly tables: boolean;
|
|
42
|
+
/** Image insertion. Existing images still render either way. */
|
|
43
|
+
readonly images: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Outline-aware copy: the host's serializer decides the `text/plain` flavour.
|
|
46
|
+
* Off leaves ProseMirror's default, which loses every level.
|
|
47
|
+
*/
|
|
48
|
+
readonly clipboard: boolean;
|
|
49
|
+
}
|
|
50
|
+
export type OutlineFeatureName = keyof OutlineFeatures;
|
|
51
|
+
/**
|
|
52
|
+
* The default: everything.
|
|
53
|
+
*
|
|
54
|
+
* A host that has not thought about features yet should get the whole editor, not an empty
|
|
55
|
+
* shell to debug. "Off unless named" reads as safer and is worse — the failure it produces
|
|
56
|
+
* is a missing feature with no error, which is the hardest kind to diagnose.
|
|
57
|
+
*/
|
|
58
|
+
export declare const ALL_FEATURES: OutlineFeatures;
|
|
59
|
+
/** Nothing but text. Useful as a base to switch individual features back on. */
|
|
60
|
+
export declare const NO_FEATURES: OutlineFeatures;
|
|
61
|
+
/**
|
|
62
|
+
* Normalize whatever the host passed into a complete record.
|
|
63
|
+
*
|
|
64
|
+
* Three input shapes, because hosts reach for different ones and all three are reasonable:
|
|
65
|
+
*
|
|
66
|
+
* undefined → everything (the default)
|
|
67
|
+
* { links: false } → everything except links (a partial override)
|
|
68
|
+
* ['collapse', 'formatting'] → ONLY those two (an allowlist)
|
|
69
|
+
*
|
|
70
|
+
* The array form flips the default deliberately: writing a list reads as "these are the
|
|
71
|
+
* features I want", and having it mean "these, plus the six I did not mention" would be a
|
|
72
|
+
* trap. The object form keeps the opposite reading, which is why both exist.
|
|
73
|
+
*
|
|
74
|
+
* An unknown name in the array throws rather than being ignored — a typo'd feature that
|
|
75
|
+
* silently does nothing is the exact failure this API exists to prevent.
|
|
76
|
+
*/
|
|
77
|
+
export declare function resolveFeatures(input?: Partial<OutlineFeatures> | readonly OutlineFeatureName[] | undefined): OutlineFeatures;
|
|
78
|
+
/**
|
|
79
|
+
* How the document is being used.
|
|
80
|
+
*
|
|
81
|
+
* A separate axis from features, not another flag among them: `read` is not "formatting
|
|
82
|
+
* off plus links off", it is a different contract — nothing may mutate the document, and
|
|
83
|
+
* the machinery that exists to mutate it does not need to be constructed at all.
|
|
84
|
+
*/
|
|
85
|
+
export type OutlineMode = 'edit' | 'read';
|
|
86
|
+
/**
|
|
87
|
+
* The features a mode can actually support.
|
|
88
|
+
*
|
|
89
|
+
* Read mode keeps `collapse` and `links`, and that distinction is the point: **a lock stops
|
|
90
|
+
* editing, never reading.** A reader who cannot fold a section or follow a reference has
|
|
91
|
+
* been given a screenshot, not a document. Everything that mutates content is off, and no
|
|
92
|
+
* host flag can turn it back on — an editing command reachable in read mode is a bug, not
|
|
93
|
+
* a configuration.
|
|
94
|
+
*/
|
|
95
|
+
export declare function featuresForMode(features: OutlineFeatures, mode: OutlineMode): OutlineFeatures;
|
|
96
|
+
/**
|
|
97
|
+
* A stable string for a resolved feature set.
|
|
98
|
+
*
|
|
99
|
+
* Exists because of how React memoization actually gets used. The natural way for a host to
|
|
100
|
+
* configure this editor is an inline literal:
|
|
101
|
+
*
|
|
102
|
+
* <OutlineEditor features={{ links: hasLinks }} />
|
|
103
|
+
*
|
|
104
|
+
* That object has a new identity on every render. Memoizing the extension list on it
|
|
105
|
+
* rebuilds the Tiptap editor on every render — the document is torn down and recreated
|
|
106
|
+
* between keystrokes, and the symptom is not an error but an editor that never settles:
|
|
107
|
+
* the spine's handles move continuously and a click can never land.
|
|
108
|
+
*
|
|
109
|
+
* An API that is only correct when the caller remembers to `useMemo` is a bad API, so the
|
|
110
|
+
* comparison is by CONTENT here rather than by reference. The key is ordered by
|
|
111
|
+
* `ALL_FEATURES` so two equal sets always produce the same string.
|
|
112
|
+
*/
|
|
113
|
+
export declare function featureKey(features: OutlineFeatures): string;
|
|
114
|
+
//# sourceMappingURL=features.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features.d.ts","sourceRoot":"","sources":["../src/features.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,0FAA0F;AAC1F,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,wFAAwF;IACxF,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,yFAAyF;IACzF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,uEAAuE;IACvE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,gEAAgE;IAChE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,MAAM,kBAAkB,GAAG,MAAM,eAAe,CAAC;AAEvD;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,EAAE,eAS1B,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,WAAW,EAAE,eASzB,CAAC;AAIF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAC7B,KAAK,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,SAAS,kBAAkB,EAAE,GAAG,SAAS,GAC3E,eAAe,CAkBjB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1C;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,eAAe,EAAE,IAAI,EAAE,WAAW,GAAG,eAAe,CAU7F;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,CAE5D"}
|