@tarhnama/core 0.1.0 → 0.2.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/README.md +1 -1
- package/package.json +4 -4
- package/src/context-menu.ts +62 -2
- package/src/index.ts +3 -2
- package/src/ops.ts +7 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ npm install @tarhnama/core
|
|
|
10
10
|
|
|
11
11
|
Part of **طرحنما**, a Persian-first outline editor. Everything is optional — features,
|
|
12
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/
|
|
13
|
+
See the [consumer guide](https://github.com/smhosseini72/tarhnama-editor/blob/main/docs/CONSUMING.md) for the styling levels,
|
|
14
14
|
the feature switches and the i18n contract.
|
|
15
15
|
|
|
16
16
|
Licensed MIT.
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tarhnama/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Framework-agnostic outline document model: flat blocks with outline levels, plus a derived tree index. No DOM, no editor dependency.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/
|
|
8
|
+
"url": "git+https://github.com/smhosseini72/tarhnama-editor.git",
|
|
9
9
|
"directory": "packages/core"
|
|
10
10
|
},
|
|
11
|
-
"homepage": "https://github.com/
|
|
12
|
-
"bugs": "https://github.com/
|
|
11
|
+
"homepage": "https://github.com/smhosseini72/tarhnama-editor#readme",
|
|
12
|
+
"bugs": "https://github.com/smhosseini72/tarhnama-editor/issues",
|
|
13
13
|
"keywords": [
|
|
14
14
|
"outline",
|
|
15
15
|
"editor",
|
package/src/context-menu.ts
CHANGED
|
@@ -50,8 +50,42 @@ export interface ContextTarget {
|
|
|
50
50
|
readonly canMoveDown: boolean;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* A host's own command, spelled `host:<id>` so it can never collide with a built-in one and a
|
|
55
|
+
* renderer can tell the two apart with a prefix check (`hostCommandId`).
|
|
56
|
+
*/
|
|
57
|
+
export type HostCommand = `host:${string}`;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A command the HOST adds to the right-click menu — «send to my app», «open in the CRM».
|
|
61
|
+
*
|
|
62
|
+
* Data, like everything else here: the host decides per right-click which items exist (it is
|
|
63
|
+
* handed the same `ContextTarget`), and runs the one chosen by its id. No closure goes in, so
|
|
64
|
+
* the descriptor stays serializable and a host can build it anywhere.
|
|
65
|
+
*/
|
|
66
|
+
export interface HostMenuItem {
|
|
67
|
+
/** Unique among the host's items. Comes back as `host:<id>` on the chosen item. */
|
|
68
|
+
readonly id: string;
|
|
69
|
+
/** Already translated: the host owns its own words. */
|
|
70
|
+
readonly label: string;
|
|
71
|
+
/** Defaults to true. Dimmed, not removed, when false — the menu keeps its shape. */
|
|
72
|
+
readonly enabled?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Order among the HOST's items: higher first, ties keep the given order. Never moves one
|
|
75
|
+
* above the editor's own items — those stay where a writer learned them.
|
|
76
|
+
*/
|
|
77
|
+
readonly priority?: number;
|
|
78
|
+
/** A rule before this item, to group the host's own commands. */
|
|
79
|
+
readonly separator?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The host id inside a `host:` command, or null for one of the editor's own. */
|
|
83
|
+
export function hostCommandId(command: string): string | null {
|
|
84
|
+
return command.startsWith('host:') ? command.slice('host:'.length) : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
53
87
|
export interface ContextMenuItem {
|
|
54
|
-
readonly command: ContextCommand;
|
|
88
|
+
readonly command: ContextCommand | HostCommand;
|
|
55
89
|
/** English; a host translates through their own label pack. */
|
|
56
90
|
readonly label: string;
|
|
57
91
|
readonly enabled: boolean;
|
|
@@ -76,6 +110,7 @@ export interface ContextMenuItem {
|
|
|
76
110
|
export function contextMenuFor(
|
|
77
111
|
target: ContextTarget,
|
|
78
112
|
features: OutlineFeatures,
|
|
113
|
+
hostItems: readonly HostMenuItem[] = [],
|
|
79
114
|
): readonly ContextMenuItem[] {
|
|
80
115
|
// A lock stops EDITING, never reading — so copy survives it and the mutating pair does not.
|
|
81
116
|
const editable = !target.locked;
|
|
@@ -135,5 +170,30 @@ export function contextMenuFor(
|
|
|
135
170
|
},
|
|
136
171
|
];
|
|
137
172
|
|
|
138
|
-
|
|
173
|
+
const builtIn = all.filter((item) => item.feature === undefined || features[item.feature]);
|
|
174
|
+
return hostItems.length === 0 ? builtIn : [...builtIn, ...hostEntries(hostItems)];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/*
|
|
178
|
+
* The host's items go BELOW the editor's own, behind one rule, ordered by priority. Below,
|
|
179
|
+
* because the built-in geometry is what a writer learns ("cut is first, lock is last") and a
|
|
180
|
+
* host item above it would move every one of them; ordered, because a host with several
|
|
181
|
+
* commands knows which matters most and the order it happened to build the array in does not.
|
|
182
|
+
*/
|
|
183
|
+
function hostEntries(items: readonly HostMenuItem[]): ContextMenuItem[] {
|
|
184
|
+
const seen = new Set<string>();
|
|
185
|
+
for (const item of items) {
|
|
186
|
+
// A host item that cannot be told apart from another runs the wrong command, silently.
|
|
187
|
+
if (item.id === '') throw new Error('contextMenuFor: a host menu item has an empty id');
|
|
188
|
+
if (seen.has(item.id)) throw new Error(`contextMenuFor: duplicate host menu item id "${item.id}"`);
|
|
189
|
+
seen.add(item.id);
|
|
190
|
+
}
|
|
191
|
+
// `sort` is stable, so equal priorities keep the order the host gave.
|
|
192
|
+
const ordered = [...items].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
193
|
+
return ordered.map((item, i) => ({
|
|
194
|
+
command: `host:${item.id}` as const,
|
|
195
|
+
label: item.label,
|
|
196
|
+
enabled: item.enabled ?? true,
|
|
197
|
+
...(i === 0 || item.separator === true ? { separator: true } : {}),
|
|
198
|
+
}));
|
|
139
199
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** The public surface of @tarhnama/core: the flat block model, tree derivation, pure structural ops, features, shortcuts, context menu and the nested-tree adapter. */
|
|
1
2
|
export * from './types.js';
|
|
2
3
|
export * from './tree.js';
|
|
3
4
|
export * from './ops.js';
|
|
@@ -48,8 +49,8 @@ export type {
|
|
|
48
49
|
} from './shortcuts.js';
|
|
49
50
|
|
|
50
51
|
export { skippedLevelAt } from './ops.js';
|
|
51
|
-
export { contextMenuFor } from './context-menu.js';
|
|
52
|
-
export type { ContextCommand, ContextMenuItem, ContextTarget } from './context-menu.js';
|
|
52
|
+
export { contextMenuFor, hostCommandId } from './context-menu.js';
|
|
53
|
+
export type { ContextCommand, ContextMenuItem, ContextTarget, HostCommand, HostMenuItem } from './context-menu.js';
|
|
53
54
|
|
|
54
55
|
export { blocksToNested, nestedToBlocks } from './nested.js';
|
|
55
56
|
export type { NestedNode, NestedOptions, NestedToBlocksResult } from './nested.js';
|
package/src/ops.ts
CHANGED
|
@@ -167,7 +167,13 @@ export function canMoveSubtree(blocks: Blocks, from: number, to: number): boolea
|
|
|
167
167
|
const [, end] = subtreeRange(index, before);
|
|
168
168
|
if (to < end) return false;
|
|
169
169
|
}
|
|
170
|
-
|
|
170
|
+
/*
|
|
171
|
+
* Landing AT a locked block's index puts the subtree just before it. That is inside the lock
|
|
172
|
+
* only when the block is locked through an ancestor (a locked chapter's own content); before
|
|
173
|
+
* a locked chapter's own heading the move changes nothing read-only, and refusing it dimmed
|
|
174
|
+
* «پایین بردن گره» for any heading whose next sibling preceded a locked one.
|
|
175
|
+
*/
|
|
176
|
+
if (to < blocks.length && isLocked(blocks, index, to) && blocks[to]?.locked !== true) return false;
|
|
171
177
|
return true;
|
|
172
178
|
}
|
|
173
179
|
|