@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/dist/features.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
/**
|
|
26
|
+
* The default: everything.
|
|
27
|
+
*
|
|
28
|
+
* A host that has not thought about features yet should get the whole editor, not an empty
|
|
29
|
+
* shell to debug. "Off unless named" reads as safer and is worse — the failure it produces
|
|
30
|
+
* is a missing feature with no error, which is the hardest kind to diagnose.
|
|
31
|
+
*/
|
|
32
|
+
export const ALL_FEATURES = {
|
|
33
|
+
links: true,
|
|
34
|
+
formatting: true,
|
|
35
|
+
collapse: true,
|
|
36
|
+
lock: true,
|
|
37
|
+
history: true,
|
|
38
|
+
tables: true,
|
|
39
|
+
images: true,
|
|
40
|
+
clipboard: true,
|
|
41
|
+
};
|
|
42
|
+
/** Nothing but text. Useful as a base to switch individual features back on. */
|
|
43
|
+
export const NO_FEATURES = {
|
|
44
|
+
links: false,
|
|
45
|
+
formatting: false,
|
|
46
|
+
collapse: false,
|
|
47
|
+
lock: false,
|
|
48
|
+
history: false,
|
|
49
|
+
tables: false,
|
|
50
|
+
images: false,
|
|
51
|
+
clipboard: false,
|
|
52
|
+
};
|
|
53
|
+
const NAMES = Object.keys(ALL_FEATURES);
|
|
54
|
+
/**
|
|
55
|
+
* Normalize whatever the host passed into a complete record.
|
|
56
|
+
*
|
|
57
|
+
* Three input shapes, because hosts reach for different ones and all three are reasonable:
|
|
58
|
+
*
|
|
59
|
+
* undefined → everything (the default)
|
|
60
|
+
* { links: false } → everything except links (a partial override)
|
|
61
|
+
* ['collapse', 'formatting'] → ONLY those two (an allowlist)
|
|
62
|
+
*
|
|
63
|
+
* The array form flips the default deliberately: writing a list reads as "these are the
|
|
64
|
+
* features I want", and having it mean "these, plus the six I did not mention" would be a
|
|
65
|
+
* trap. The object form keeps the opposite reading, which is why both exist.
|
|
66
|
+
*
|
|
67
|
+
* An unknown name in the array throws rather than being ignored — a typo'd feature that
|
|
68
|
+
* silently does nothing is the exact failure this API exists to prevent.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveFeatures(input) {
|
|
71
|
+
if (input === undefined)
|
|
72
|
+
return ALL_FEATURES;
|
|
73
|
+
if (Array.isArray(input)) {
|
|
74
|
+
const wanted = new Set(input);
|
|
75
|
+
for (const name of wanted) {
|
|
76
|
+
if (!NAMES.includes(name)) {
|
|
77
|
+
throw new Error(`Unknown outline feature ${JSON.stringify(name)}. Known: ${NAMES.join(', ')}.`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const out = { ...NO_FEATURES };
|
|
81
|
+
for (const name of wanted)
|
|
82
|
+
out[name] = true;
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
return { ...ALL_FEATURES, ...input };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The features a mode can actually support.
|
|
89
|
+
*
|
|
90
|
+
* Read mode keeps `collapse` and `links`, and that distinction is the point: **a lock stops
|
|
91
|
+
* editing, never reading.** A reader who cannot fold a section or follow a reference has
|
|
92
|
+
* been given a screenshot, not a document. Everything that mutates content is off, and no
|
|
93
|
+
* host flag can turn it back on — an editing command reachable in read mode is a bug, not
|
|
94
|
+
* a configuration.
|
|
95
|
+
*/
|
|
96
|
+
export function featuresForMode(features, mode) {
|
|
97
|
+
if (mode === 'edit')
|
|
98
|
+
return features;
|
|
99
|
+
return {
|
|
100
|
+
...features,
|
|
101
|
+
formatting: false,
|
|
102
|
+
history: false,
|
|
103
|
+
tables: false,
|
|
104
|
+
images: false,
|
|
105
|
+
lock: false,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* A stable string for a resolved feature set.
|
|
110
|
+
*
|
|
111
|
+
* Exists because of how React memoization actually gets used. The natural way for a host to
|
|
112
|
+
* configure this editor is an inline literal:
|
|
113
|
+
*
|
|
114
|
+
* <OutlineEditor features={{ links: hasLinks }} />
|
|
115
|
+
*
|
|
116
|
+
* That object has a new identity on every render. Memoizing the extension list on it
|
|
117
|
+
* rebuilds the Tiptap editor on every render — the document is torn down and recreated
|
|
118
|
+
* between keystrokes, and the symptom is not an error but an editor that never settles:
|
|
119
|
+
* the spine's handles move continuously and a click can never land.
|
|
120
|
+
*
|
|
121
|
+
* An API that is only correct when the caller remembers to `useMemo` is a bad API, so the
|
|
122
|
+
* comparison is by CONTENT here rather than by reference. The key is ordered by
|
|
123
|
+
* `ALL_FEATURES` so two equal sets always produce the same string.
|
|
124
|
+
*/
|
|
125
|
+
export function featureKey(features) {
|
|
126
|
+
return NAMES.filter((name) => features[name]).join(',');
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=features.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features.js","sourceRoot":"","sources":["../src/features.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AA8BH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,YAAY,GAAoB;IAC3C,KAAK,EAAE,IAAI;IACX,UAAU,EAAE,IAAI;IAChB,QAAQ,EAAE,IAAI;IACd,IAAI,EAAE,IAAI;IACV,OAAO,EAAE,IAAI;IACb,MAAM,EAAE,IAAI;IACZ,MAAM,EAAE,IAAI;IACZ,SAAS,EAAE,IAAI;CAChB,CAAC;AAEF,gFAAgF;AAChF,MAAM,CAAC,MAAM,WAAW,GAAoB;IAC1C,KAAK,EAAE,KAAK;IACZ,UAAU,EAAE,KAAK;IACjB,QAAQ,EAAE,KAAK;IACf,IAAI,EAAE,KAAK;IACX,OAAO,EAAE,KAAK;IACd,MAAM,EAAE,KAAK;IACb,MAAM,EAAE,KAAK;IACb,SAAS,EAAE,KAAK;CACjB,CAAC;AAEF,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAAkC,CAAC;AAEzE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,eAAe,CAC7B,KAA4E;IAE5E,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,YAAY,CAAC;IAE7C,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,KAAsC,CAAC,CAAC;QAC/D,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;YAC1B,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC1B,MAAM,IAAI,KAAK,CACb,2BAA2B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,YAAY,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC/E,CAAC;YACJ,CAAC;QACH,CAAC;QACD,MAAM,GAAG,GAAG,EAAE,GAAG,WAAW,EAAyC,CAAC;QACtE,KAAK,MAAM,IAAI,IAAI,MAAM;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;QAC5C,OAAO,GAAsB,CAAC;IAChC,CAAC;IAED,OAAO,EAAE,GAAG,YAAY,EAAE,GAAI,KAAkC,EAAE,CAAC;AACrE,CAAC;AAWD;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,QAAyB,EAAE,IAAiB;IAC1E,IAAI,IAAI,KAAK,MAAM;QAAE,OAAO,QAAQ,CAAC;IACrC,OAAO;QACL,GAAG,QAAQ;QACX,UAAU,EAAE,KAAK;QACjB,OAAO,EAAE,KAAK;QACd,MAAM,EAAE,KAAK;QACb,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,KAAK;KACZ,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,UAAU,CAAC,QAAyB;IAClD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC1D,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './tree.js';
|
|
3
|
+
export * from './ops.js';
|
|
4
|
+
export { clearDirectFormat, effectiveFormat, effectiveRunStyle, redefineStyle, styleChain, styleOutlineLevel, styleUsage, } from './styles.js';
|
|
5
|
+
export type { StyleUsage } from './styles.js';
|
|
6
|
+
export { collapseSiblings, currentOpenLevel, deepestParentLevel, isHidden, isolateSubtree, openToFit, openToLevel, subtreeSize, } from './spine.js';
|
|
7
|
+
export { ALL_FEATURES, NO_FEATURES, featureKey, featuresForMode, resolveFeatures, } from './features.js';
|
|
8
|
+
export type { OutlineFeatureName, OutlineFeatures, OutlineMode } from './features.js';
|
|
9
|
+
export { DEFAULT_SHORTCUTS, formatShortcut, resolveShortcuts, shortcutsFor, } from './shortcuts.js';
|
|
10
|
+
export type { ShortcutBinding, ShortcutCommand, ShortcutGroup, ShortcutOptions, ShortcutPlatform, } from './shortcuts.js';
|
|
11
|
+
export { skippedLevelAt } from './ops.js';
|
|
12
|
+
export { contextMenuFor } from './context-menu.js';
|
|
13
|
+
export type { ContextCommand, ContextMenuItem, ContextTarget } from './context-menu.js';
|
|
14
|
+
export { blocksToNested, nestedToBlocks } from './nested.js';
|
|
15
|
+
export type { NestedNode, NestedOptions, NestedToBlocksResult } from './nested.js';
|
|
16
|
+
export { codeSegments, tokenizeCode } from './code-tokens.js';
|
|
17
|
+
export type { CodeSegment, CodeToken, CodeTokenKind, CodeTokenizer } from './code-tokens.js';
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AAEzB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,UAAU,GACX,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,QAAQ,EACR,cAAc,EACd,SAAS,EACT,WAAW,EACX,WAAW,GACZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,YAAY,EACZ,WAAW,EACX,UAAU,EACV,eAAe,EACf,eAAe,GAChB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEtF,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,gBAAgB,EAChB,YAAY,GACb,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,eAAe,EACf,eAAe,EACf,aAAa,EACb,eAAe,EACf,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAExF,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7D,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnF,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC9D,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './tree.js';
|
|
3
|
+
export * from './ops.js';
|
|
4
|
+
export { clearDirectFormat, effectiveFormat, effectiveRunStyle, redefineStyle, styleChain, styleOutlineLevel, styleUsage, } from './styles.js';
|
|
5
|
+
export { collapseSiblings, currentOpenLevel, deepestParentLevel, isHidden, isolateSubtree, openToFit, openToLevel, subtreeSize, } from './spine.js';
|
|
6
|
+
export { ALL_FEATURES, NO_FEATURES, featureKey, featuresForMode, resolveFeatures, } from './features.js';
|
|
7
|
+
export { DEFAULT_SHORTCUTS, formatShortcut, resolveShortcuts, shortcutsFor, } from './shortcuts.js';
|
|
8
|
+
export { skippedLevelAt } from './ops.js';
|
|
9
|
+
export { contextMenuFor } from './context-menu.js';
|
|
10
|
+
export { blocksToNested, nestedToBlocks } from './nested.js';
|
|
11
|
+
export { codeSegments, tokenizeCode } from './code-tokens.js';
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AAEzB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,UAAU,GACX,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,QAAQ,EACR,cAAc,EACd,SAAS,EACT,WAAW,EACX,WAAW,GACZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,YAAY,EACZ,WAAW,EACX,UAAU,EACV,eAAe,EACf,eAAe,GAChB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,gBAAgB,EAChB,YAAY,GACb,MAAM,gBAAgB,CAAC;AASxB,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGnD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG7D,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/nested.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type Blocks } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Nested tree <-> flat blocks.
|
|
4
|
+
*
|
|
5
|
+
* Almost every system that stores a tree stores it as ADJACENCY: one row per node with a
|
|
6
|
+
* `parentId` and an ordinal. This editor's model is deliberately the opposite — a flat list
|
|
7
|
+
* where each block carries an explicit level, which is what lets it import the Word and
|
|
8
|
+
* Markdown documents that skip levels constantly (CLAUDE.md §1).
|
|
9
|
+
*
|
|
10
|
+
* Binding the two is the entire cost of embedding this editor in a host that already has a
|
|
11
|
+
* tree, and every host has to do it. Doing it here once, with tests, beats each of them
|
|
12
|
+
* writing the awkward parts again — the awkward parts being ordering, depth beyond what a
|
|
13
|
+
* level can express, cycles, orphans, and the fields a tree carries that an outline cannot.
|
|
14
|
+
*/
|
|
15
|
+
/** A host's nested node. The shape is the common denominator, not any one product's schema. */
|
|
16
|
+
export interface NestedNode<T = unknown> {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly parentId: string | null;
|
|
19
|
+
readonly title: string;
|
|
20
|
+
/** Order among siblings. Absent sorts last, stably. */
|
|
21
|
+
readonly ordinal?: number;
|
|
22
|
+
/** Prose under the heading. */
|
|
23
|
+
readonly body?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Anything the host keeps that the outline cannot represent — evidence, a source, a
|
|
26
|
+
* foreign key. Carried through untouched so a round trip never costs the host a field.
|
|
27
|
+
*/
|
|
28
|
+
readonly data?: T;
|
|
29
|
+
}
|
|
30
|
+
export interface NestedToBlocksResult<C> {
|
|
31
|
+
readonly blocks: Blocks<C>;
|
|
32
|
+
/**
|
|
33
|
+
* What could not be represented faithfully.
|
|
34
|
+
*
|
|
35
|
+
* Returned rather than thrown, and never silent: a 12-deep branch that quietly flattens
|
|
36
|
+
* to 9 loses three levels of someone's tree with nothing reporting a problem, and they
|
|
37
|
+
* find out when they save.
|
|
38
|
+
*/
|
|
39
|
+
readonly warnings: readonly string[];
|
|
40
|
+
}
|
|
41
|
+
/** How a title and a body become block content. Defaults to a single plain run. */
|
|
42
|
+
export interface NestedOptions<C> {
|
|
43
|
+
readonly toContent?: (text: string) => C;
|
|
44
|
+
readonly fromContent?: (content: C | undefined) => string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Flatten a nested tree into blocks.
|
|
48
|
+
*
|
|
49
|
+
* Depth becomes level. Where depth exceeds `MAX_LEVEL` the level CLAMPS — a block with
|
|
50
|
+
* level 12 is not representable and would be rejected downstream — but the real depth and
|
|
51
|
+
* the real parent ride along in `attrs`, so `blocksToNested` restores the exact tree. The
|
|
52
|
+
* clamp is a display limit, never a data loss, and it is reported either way.
|
|
53
|
+
*/
|
|
54
|
+
export declare function nestedToBlocks<C = unknown, T = unknown>(nodes: readonly NestedNode<T>[], options?: NestedOptions<C>): NestedToBlocksResult<C>;
|
|
55
|
+
/**
|
|
56
|
+
* Rebuild the nested tree from blocks.
|
|
57
|
+
*
|
|
58
|
+
* The inverse of `nestedToBlocks`, and the direction that has to be right on save: a host
|
|
59
|
+
* writes what this returns back into their database, so anything it gets wrong is data loss
|
|
60
|
+
* rather than a rendering bug.
|
|
61
|
+
*
|
|
62
|
+
* Ordinals are RENUMBERED from document order. That is the point of editing an outline —
|
|
63
|
+
* the user reordered it — and preserving the incoming ordinals would throw their edit away.
|
|
64
|
+
*/
|
|
65
|
+
export declare function blocksToNested<C = unknown, T = unknown>(blocks: Blocks<C>, options?: NestedOptions<C>): readonly NestedNode<T>[];
|
|
66
|
+
//# sourceMappingURL=nested.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nested.d.ts","sourceRoot":"","sources":["../src/nested.ts"],"names":[],"mappings":"AACA,OAAO,EAAmB,KAAK,MAAM,EAAqB,MAAM,YAAY,CAAC;AAE7E;;;;;;;;;;;;GAYG;AAEH,+FAA+F;AAC/F,MAAM,WAAW,UAAU,CAAC,CAAC,GAAG,OAAO;IACrC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uDAAuD;IACvD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB,CAAC,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAED,mFAAmF;AACnF,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,CAAC,CAAC;IACzC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,SAAS,KAAK,MAAM,CAAC;CAC3D;AAgBD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,EACrD,KAAK,EAAE,SAAS,UAAU,CAAC,CAAC,CAAC,EAAE,EAC/B,OAAO,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,GACzB,oBAAoB,CAAC,CAAC,CAAC,CA8GzB;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,EACrD,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EACjB,OAAO,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,GACzB,SAAS,UAAU,CAAC,CAAC,CAAC,EAAE,CAqD1B"}
|
package/dist/nested.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { buildIndex, subtreeRange } from './tree.js';
|
|
2
|
+
import { BODY, MAX_LEVEL } from './types.js';
|
|
3
|
+
const defaultToContent = (text) => [{ text }];
|
|
4
|
+
const defaultFromContent = (content) => {
|
|
5
|
+
if (!Array.isArray(content))
|
|
6
|
+
return '';
|
|
7
|
+
return content.map((run) => run.text ?? '').join('');
|
|
8
|
+
};
|
|
9
|
+
/** Where the true depth is stashed when a level cannot hold it. See `nestedToBlocks`. */
|
|
10
|
+
const DEPTH_ATTR = 'nestedDepth';
|
|
11
|
+
/** Where the host's own payload rides. */
|
|
12
|
+
const DATA_ATTR = 'data';
|
|
13
|
+
/** The parent, recorded so the reverse mapping never has to guess at a clamped level. */
|
|
14
|
+
const PARENT_ATTR = 'nestedParentId';
|
|
15
|
+
/**
|
|
16
|
+
* Flatten a nested tree into blocks.
|
|
17
|
+
*
|
|
18
|
+
* Depth becomes level. Where depth exceeds `MAX_LEVEL` the level CLAMPS — a block with
|
|
19
|
+
* level 12 is not representable and would be rejected downstream — but the real depth and
|
|
20
|
+
* the real parent ride along in `attrs`, so `blocksToNested` restores the exact tree. The
|
|
21
|
+
* clamp is a display limit, never a data loss, and it is reported either way.
|
|
22
|
+
*/
|
|
23
|
+
export function nestedToBlocks(nodes, options) {
|
|
24
|
+
const toContent = (options?.toContent ?? defaultToContent);
|
|
25
|
+
const warnings = [];
|
|
26
|
+
const byId = new Map();
|
|
27
|
+
for (const node of nodes)
|
|
28
|
+
byId.set(node.id, node);
|
|
29
|
+
// Children by parent, in sibling order. Built once: doing it per node is the quadratic
|
|
30
|
+
// version of this function and a knowledge tree can be large.
|
|
31
|
+
const children = new Map();
|
|
32
|
+
for (const node of nodes) {
|
|
33
|
+
/*
|
|
34
|
+
* A parent that is not in the set makes this node a ROOT rather than dropping it.
|
|
35
|
+
* Dropping deletes the user's content on the way in; promoting keeps it where they can
|
|
36
|
+
* see it and fix it.
|
|
37
|
+
*/
|
|
38
|
+
const missing = node.parentId !== null && !byId.has(node.parentId);
|
|
39
|
+
if (missing) {
|
|
40
|
+
warnings.push(`Node ${JSON.stringify(node.id)} has parent ${JSON.stringify(node.parentId)}, which is not in the tree — treated as a root.`);
|
|
41
|
+
}
|
|
42
|
+
const key = missing ? null : node.parentId;
|
|
43
|
+
const bucket = children.get(key);
|
|
44
|
+
if (bucket)
|
|
45
|
+
bucket.push(node);
|
|
46
|
+
else
|
|
47
|
+
children.set(key, [node]);
|
|
48
|
+
}
|
|
49
|
+
for (const bucket of children.values()) {
|
|
50
|
+
bucket.sort((a, b) => {
|
|
51
|
+
// Absent ordinals sort last and keep their relative order, so a host that never set
|
|
52
|
+
// them gets its array order back rather than an arbitrary shuffle.
|
|
53
|
+
const left = a.ordinal ?? Number.MAX_SAFE_INTEGER;
|
|
54
|
+
const right = b.ordinal ?? Number.MAX_SAFE_INTEGER;
|
|
55
|
+
return left - right;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const blocks = [];
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
/* Iterative, not recursive: a knowledge tree deep enough to blow the stack is a crash
|
|
61
|
+
with no message, and this runs on data the library did not create. */
|
|
62
|
+
const stack = [];
|
|
63
|
+
for (const root of [...(children.get(null) ?? [])].reverse()) {
|
|
64
|
+
stack.push({ node: root, depth: 1 });
|
|
65
|
+
}
|
|
66
|
+
while (stack.length > 0) {
|
|
67
|
+
const { node, depth } = stack.pop();
|
|
68
|
+
if (seen.has(node.id))
|
|
69
|
+
continue;
|
|
70
|
+
seen.add(node.id);
|
|
71
|
+
const level = Math.min(depth, MAX_LEVEL);
|
|
72
|
+
if (depth > MAX_LEVEL) {
|
|
73
|
+
warnings.push(`Node ${JSON.stringify(node.id)} is ${depth} deep; levels stop at ${MAX_LEVEL}, so it renders at ${MAX_LEVEL}. Its real depth is preserved.`);
|
|
74
|
+
}
|
|
75
|
+
const attrs = {};
|
|
76
|
+
if (node.data !== undefined)
|
|
77
|
+
attrs[DATA_ATTR] = node.data;
|
|
78
|
+
// Only recorded when the level cannot express the truth on its own.
|
|
79
|
+
if (depth > MAX_LEVEL) {
|
|
80
|
+
attrs[DEPTH_ATTR] = depth;
|
|
81
|
+
attrs[PARENT_ATTR] = node.parentId;
|
|
82
|
+
}
|
|
83
|
+
blocks.push({
|
|
84
|
+
id: node.id,
|
|
85
|
+
level,
|
|
86
|
+
type: 'heading',
|
|
87
|
+
dir: 'auto',
|
|
88
|
+
collapsed: false,
|
|
89
|
+
content: toContent(node.title),
|
|
90
|
+
...(Object.keys(attrs).length > 0 ? { attrs } : {}),
|
|
91
|
+
});
|
|
92
|
+
if (node.body !== undefined && node.body !== '') {
|
|
93
|
+
blocks.push({
|
|
94
|
+
// Body text is not a node of its own, so it gets no node id to collide with.
|
|
95
|
+
id: '',
|
|
96
|
+
level: BODY,
|
|
97
|
+
type: 'paragraph',
|
|
98
|
+
dir: 'auto',
|
|
99
|
+
collapsed: false,
|
|
100
|
+
content: toContent(node.body),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
for (const child of [...(children.get(node.id) ?? [])].reverse()) {
|
|
104
|
+
stack.push({ node: child, depth: depth + 1 });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/*
|
|
108
|
+
* Anything never reached is in a cycle, or hangs off one. Reported rather than emitted:
|
|
109
|
+
* a cycle has no depth, so there is no level to give it, and inventing one would put the
|
|
110
|
+
* nodes somewhere arbitrary in the user's document.
|
|
111
|
+
*/
|
|
112
|
+
const unreachable = nodes.filter((node) => !seen.has(node.id));
|
|
113
|
+
if (unreachable.length > 0) {
|
|
114
|
+
warnings.push(`${unreachable.length} node(s) are unreachable from any root — a parent cycle: ${unreachable
|
|
115
|
+
.map((n) => JSON.stringify(n.id))
|
|
116
|
+
.join(', ')}.`);
|
|
117
|
+
}
|
|
118
|
+
return { blocks: blocks, warnings };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Rebuild the nested tree from blocks.
|
|
122
|
+
*
|
|
123
|
+
* The inverse of `nestedToBlocks`, and the direction that has to be right on save: a host
|
|
124
|
+
* writes what this returns back into their database, so anything it gets wrong is data loss
|
|
125
|
+
* rather than a rendering bug.
|
|
126
|
+
*
|
|
127
|
+
* Ordinals are RENUMBERED from document order. That is the point of editing an outline —
|
|
128
|
+
* the user reordered it — and preserving the incoming ordinals would throw their edit away.
|
|
129
|
+
*/
|
|
130
|
+
export function blocksToNested(blocks, options) {
|
|
131
|
+
const fromContent = (options?.fromContent ?? defaultFromContent);
|
|
132
|
+
const index = buildIndex(blocks);
|
|
133
|
+
const out = [];
|
|
134
|
+
const ordinals = new Map();
|
|
135
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
136
|
+
const block = blocks[i];
|
|
137
|
+
if (!block || block.level === BODY)
|
|
138
|
+
continue;
|
|
139
|
+
/*
|
|
140
|
+
* The parent is the nearest ancestor HEADING. Taken from the tree index rather than
|
|
141
|
+
* recomputed, so this agrees with collapse, indent and every other consumer of the
|
|
142
|
+
* hierarchy — a second implementation of "who is my parent" is a second answer.
|
|
143
|
+
*/
|
|
144
|
+
const attrs = block.attrs;
|
|
145
|
+
const clampedParent = attrs?.[PARENT_ATTR];
|
|
146
|
+
let parentId;
|
|
147
|
+
if (typeof clampedParent === 'string' || clampedParent === null) {
|
|
148
|
+
// Beyond MAX_LEVEL the level cannot express the parent, so the recorded one wins.
|
|
149
|
+
parentId = clampedParent;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const parent = parentOf(blocks, index, i);
|
|
153
|
+
parentId = parent === -1 ? null : (blocks[parent]?.id ?? null);
|
|
154
|
+
}
|
|
155
|
+
const ordinal = ordinals.get(parentId) ?? 0;
|
|
156
|
+
ordinals.set(parentId, ordinal + 1);
|
|
157
|
+
// Body text belonging to this heading: the level-0 blocks that immediately follow it.
|
|
158
|
+
const bodyParts = [];
|
|
159
|
+
for (let j = i + 1; j < blocks.length; j++) {
|
|
160
|
+
const next = blocks[j];
|
|
161
|
+
if (!next || next.level !== BODY)
|
|
162
|
+
break;
|
|
163
|
+
bodyParts.push(fromContent(next.content));
|
|
164
|
+
}
|
|
165
|
+
const data = attrs?.[DATA_ATTR];
|
|
166
|
+
out.push({
|
|
167
|
+
id: block.id,
|
|
168
|
+
parentId,
|
|
169
|
+
title: fromContent(block.content),
|
|
170
|
+
ordinal,
|
|
171
|
+
...(bodyParts.length > 0 ? { body: bodyParts.join('\n\n') } : {}),
|
|
172
|
+
...(data !== undefined ? { data: data } : {}),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
/** Nearest preceding block at a shallower level — the parent heading, or -1. */
|
|
178
|
+
function parentOf(blocks, index, i) {
|
|
179
|
+
const level = blocks[i]?.level;
|
|
180
|
+
if (level === undefined || level <= 1)
|
|
181
|
+
return -1;
|
|
182
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
183
|
+
const other = blocks[j]?.level;
|
|
184
|
+
if (other === undefined || other === BODY)
|
|
185
|
+
continue;
|
|
186
|
+
if (other < level) {
|
|
187
|
+
// Confirm containment against the shared index rather than trusting the scan alone.
|
|
188
|
+
const [, end] = subtreeRange(index, j);
|
|
189
|
+
return i < end ? j : -1;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return -1;
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=nested.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nested.js","sourceRoot":"","sources":["../src/nested.ts"],"names":[],"mappings":"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"}
|
package/dist/ops.d.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { type Block, type Blocks, type OutlineLevel } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Structural operations. All are pure: they take a flat list and return a new one,
|
|
4
|
+
* sharing unchanged blocks by reference so downstream memoization works.
|
|
5
|
+
*
|
|
6
|
+
* The defining rule of this module: an operation on a heading applies to its whole
|
|
7
|
+
* SUBTREE. Indenting a heading and leaving its children behind — the bug in the
|
|
8
|
+
* earlier Tiptap scaffold — produces an orphaned document.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Can `i` be indented?
|
|
12
|
+
*
|
|
13
|
+
* The rule prevents skipped levels: a heading may go one deeper than the nearest
|
|
14
|
+
* preceding heading, never more. Without this you can indent from level 1 straight
|
|
15
|
+
* to level 3 and create a block with no representable parent.
|
|
16
|
+
*
|
|
17
|
+
* Body text has no level, so indenting it is meaningless — use `setLevel` to promote
|
|
18
|
+
* body text into a heading instead.
|
|
19
|
+
*/
|
|
20
|
+
export declare function canIndent(blocks: Blocks, i: number): boolean;
|
|
21
|
+
export declare function indent<C>(blocks: Blocks<C>, i: number): Blocks<C>;
|
|
22
|
+
/** Level 1 cannot outdent — there is nothing above it. Body text has no level. */
|
|
23
|
+
export declare function canOutdent(blocks: Blocks, i: number): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Outdent a heading and its subtree.
|
|
26
|
+
*
|
|
27
|
+
* Pleasant property of the flat model: the following siblings that were at the
|
|
28
|
+
* outdented block's old level automatically become its children, which is exactly
|
|
29
|
+
* what Workflowy and Word's outline view do — no extra code needed.
|
|
30
|
+
*/
|
|
31
|
+
export declare function outdent<C>(blocks: Blocks<C>, i: number): Blocks<C>;
|
|
32
|
+
/** Set a block's level directly — used to promote body text to a heading and back. */
|
|
33
|
+
export declare function canSetLevel(blocks: Blocks, i: number): boolean;
|
|
34
|
+
export declare function setLevel<C>(blocks: Blocks<C>, i: number, level: OutlineLevel): Blocks<C>;
|
|
35
|
+
export declare function toggleCollapse<C>(blocks: Blocks<C>, i: number): Blocks<C>;
|
|
36
|
+
/**
|
|
37
|
+
* Expand every collapsed ancestor of a block, so it can actually be reached.
|
|
38
|
+
*
|
|
39
|
+
* Needed wherever the app navigates to a block the user did not click: a search result, a
|
|
40
|
+
* cross-document link, a backlink. A collapsed subtree is ABSENT from the DOM, not hidden,
|
|
41
|
+
* so scrolling to it finds nothing and the jump silently does nothing at all — which reads
|
|
42
|
+
* as a broken feature rather than as "that match is folded away".
|
|
43
|
+
*
|
|
44
|
+
* Only ancestors are touched. The block's own collapse state is left alone: revealing a
|
|
45
|
+
* heading should not also unfold everything beneath it.
|
|
46
|
+
*
|
|
47
|
+
* Returns the SAME list when nothing was collapsed.
|
|
48
|
+
*/
|
|
49
|
+
export declare function expandAncestors<C>(blocks: Blocks<C>, blockId: string): Blocks<C>;
|
|
50
|
+
export declare function setDirection<C>(blocks: Blocks<C>, i: number, dir: Block['dir']): Blocks<C>;
|
|
51
|
+
/**
|
|
52
|
+
* Move the subtree rooted at `from` so it begins at document position `to`.
|
|
53
|
+
*
|
|
54
|
+
* `to` is interpreted against the ORIGINAL list. Moving a subtree into itself is a
|
|
55
|
+
* no-op rather than an error, because drag-and-drop will attempt it constantly.
|
|
56
|
+
*/
|
|
57
|
+
/**
|
|
58
|
+
* Can the subtree at `from` be moved to `to`?
|
|
59
|
+
*
|
|
60
|
+
* Both ends matter: a locked subtree cannot be dragged away, and nothing may be dropped
|
|
61
|
+
* INTO a locked one. Checking only the source is the easy mistake — it lets a reader
|
|
62
|
+
* append into a chapter they are not allowed to change.
|
|
63
|
+
*/
|
|
64
|
+
export declare function canMoveSubtree(blocks: Blocks, from: number, to: number): boolean;
|
|
65
|
+
export declare function moveSubtree<C>(blocks: Blocks<C>, from: number, to: number): Blocks<C>;
|
|
66
|
+
/**
|
|
67
|
+
* Repair skipped levels — the import path for real Word documents, which jump from
|
|
68
|
+
* H1 to H3 constantly.
|
|
69
|
+
*
|
|
70
|
+
* Each heading is clamped to at most one deeper than the preceding heading, and the
|
|
71
|
+
* first heading is clamped to 1. Body text is untouched.
|
|
72
|
+
*/
|
|
73
|
+
export declare function normalizeLevels<C>(blocks: Blocks<C>): Blocks<C>;
|
|
74
|
+
/** True when no heading is more than one level deeper than the one before it. */
|
|
75
|
+
export declare function isNormalized(blocks: Blocks): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Lock or unlock a block. Locking a heading makes its whole subtree read-only by
|
|
78
|
+
* derivation — only the heading itself carries the flag.
|
|
79
|
+
*/
|
|
80
|
+
export declare function setLocked<C>(blocks: Blocks<C>, i: number, locked: boolean): Blocks<C>;
|
|
81
|
+
/**
|
|
82
|
+
* Move a node before its previous sibling, subtree and all.
|
|
83
|
+
*
|
|
84
|
+
* "Up" is **a sibling**, not a row. A heading with forty blocks under it must jump the
|
|
85
|
+
* whole of its neighbour's subtree; swapping single rows would tear both apart, and the
|
|
86
|
+
* design's درخت menu calls this "move node" for that reason.
|
|
87
|
+
*
|
|
88
|
+
* A first sibling does not move. The tempting alternative — promote it above its parent —
|
|
89
|
+
* changes the node's LEVEL as a side effect of a move, and a command that quietly does two
|
|
90
|
+
* things is one users stop trusting. Indent and outdent are how a level changes.
|
|
91
|
+
*/
|
|
92
|
+
export declare function moveBlockUp<C>(blocks: Blocks<C>, blockId: string): Blocks<C>;
|
|
93
|
+
/**
|
|
94
|
+
* Move a node after its next sibling, subtree and all.
|
|
95
|
+
*
|
|
96
|
+
* The target is the END of the sibling's subtree, in ORIGINAL indices — `moveSubtree`
|
|
97
|
+
* interprets `to` against the list it was given and does the shift arithmetic itself.
|
|
98
|
+
* Pre-adjusting by the moved subtree's size (the obvious defensive move) lands the target
|
|
99
|
+
* back inside the node being moved, where `moveSubtree`'s "dropping inside itself" guard
|
|
100
|
+
* correctly refuses it and the command silently does nothing.
|
|
101
|
+
*/
|
|
102
|
+
export declare function moveBlockDown<C>(blocks: Blocks<C>, blockId: string): Blocks<C>;
|
|
103
|
+
/**
|
|
104
|
+
* The level this heading skipped over, or `null`.
|
|
105
|
+
*
|
|
106
|
+
* `isNormalized` answers "does this document skip anywhere", which is enough to enable a
|
|
107
|
+
* repair command and useless for showing the reader WHERE. Frame 1a puts an amber
|
|
108
|
+
* «سطح ۳ جا افتاده» chip on the offending heading, so the question has to be asked of a
|
|
109
|
+
* single block.
|
|
110
|
+
*
|
|
111
|
+
* Returns the FIRST missing level, not the deepest. When 2 → 4 skips only 3 that is the
|
|
112
|
+
* same answer either way, but 1 → 4 skips both 2 and 3, and 2 is the one to add first —
|
|
113
|
+
* naming 3 would send someone to fix the wrong end of the gap.
|
|
114
|
+
*
|
|
115
|
+
* Body text is never an answer and never breaks the chain: level 0 is not a hierarchy
|
|
116
|
+
* level, it belongs to the nearest preceding heading, and a paragraph between two headings
|
|
117
|
+
* does not make the second one a skip.
|
|
118
|
+
*/
|
|
119
|
+
export declare function skippedLevelAt(blocks: Blocks, i: number): OutlineLevel | null;
|
|
120
|
+
//# sourceMappingURL=ops.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ops.d.ts","sourceRoot":"","sources":["../src/ops.ts"],"names":[],"mappings":"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"}
|