@skaile/forge-editor 5.0.2 → 5.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/CHANGELOG.md +16 -0
- package/dist/code/CodeSurface.vue.d.ts +3 -3
- package/dist/code/index.js +1 -1
- package/dist/core/toolbar-fit.js +99 -0
- package/dist/core/toolbar-fit.js.map +1 -0
- package/dist/document/DocumentSurface.js +1 -1
- package/dist/document/DocumentSurface.vue.d.ts +3 -3
- package/dist/features/ai/agent-ops.js +1 -1
- package/dist/features/ai/agent-ops.js.map +1 -1
- package/dist/features/ai/document-chat.d.ts +3 -2
- package/dist/features/ai/document-chat.js +6 -4
- package/dist/features/ai/document-chat.js.map +1 -1
- package/dist/features/ai/protocol/block-lines.d.ts +2 -1
- package/dist/features/ai/protocol/block-lines.js +2 -1
- package/dist/features/ai/protocol/block-lines.js.map +1 -1
- package/dist/features/composites/components/CompositeInspector.js +8 -8
- package/dist/features/composites/vue.js +1 -1
- package/dist/features/themes/index.d.ts +2 -2
- package/dist/features/themes/index.js +2 -2
- package/dist/features/themes/theme-runtime.d.ts +15 -4
- package/dist/features/themes/theme-runtime.js +76 -11
- package/dist/features/themes/theme-runtime.js.map +1 -1
- package/dist/markdown/MarkdownSurface.vue.d.ts +5 -5
- package/dist/react/EditorToolbar.d.ts +5 -1
- package/dist/react/EditorToolbar.js +76 -18
- package/dist/react/EditorToolbar.js.map +1 -1
- package/dist/server/document-blocks.js +5 -1
- package/dist/server/document-blocks.js.map +1 -1
- package/dist/styles/themes.css +10 -8
- package/dist/tree/DocumentTree.js +1 -1
- package/dist/tree/use-tree-model.js +1 -1
- package/dist/vue/AdvancedEditor.vue.d.ts +4 -4
- package/dist/vue/SimpleEditor.vue.d.ts +4 -4
- package/dist/vue/components/EditorSurface.vue.d.ts +4 -4
- package/dist/vue/components/EditorToolbar.js +81 -9
- package/dist/vue/components/EditorToolbar.js.map +1 -1
- package/dist/vue/components/EditorToolbar.vue.d.ts +7 -1
- package/dist/vue/compose.js +1 -1
- package/dist/vue/index.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.1.0 - 2026-10-27
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `pinObsidianTheme(ctx, id)` (`@skaile/forge-editor/themes`): shows an Obsidian theme (or `null`, the Default appearance) over the user's `appearance.theme` until the returned release runs, without changing or persisting the setting. Pins stack per app, the most recent wins; a pin holds the theme runtime, so it also applies on a page without an editor surface. Taking the next pin before releasing the previous one in the same task loads only the next theme. Also exported: the `ThemeHost` type (what the runtime reads: `settings` and `backend`).
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Document chat with tables (`tablesFeature`): `documentChatText` sends a table as one block, `[<table id>]` followed by its pipe-table Markdown (rows on their own lines, like a code block), instead of one line per cell paragraph. The model therefore targets the table itself; before, an edit after accepting a table aimed at cell IDs and came back `missing` ("Block fehlt"). An operation's snippet (`agentOpSnippet`, and the server's change-set target via `blockExcerptOf`) separates nested blocks with a space, so a table reads „Datum Therapeut/in …“ instead of „DatumTherapeut/in…“.
|
|
12
|
+
|
|
13
|
+
## 5.0.3 - 2026-10-26
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Released through npm trusted publishing (GitHub OIDC from `publish.yml`); the `NPM_TOKEN` repository secret is no longer used. No package changes.
|
|
18
|
+
|
|
3
19
|
## 5.0.2 - 2026-10-26
|
|
4
20
|
|
|
5
21
|
### Added
|
|
@@ -11,16 +11,16 @@ type __VLS_Props = {
|
|
|
11
11
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
12
12
|
view: import("vue").ShallowRef<EditorView | undefined, EditorView | undefined>;
|
|
13
13
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
14
|
+
ready: (view: EditorView) => any;
|
|
14
15
|
"update:modelValue": (text: string) => any;
|
|
15
16
|
save: (text: string) => any;
|
|
16
|
-
ready: (view: EditorView) => any;
|
|
17
17
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
18
|
+
onReady?: ((view: EditorView) => any) | undefined;
|
|
18
19
|
"onUpdate:modelValue"?: ((text: string) => any) | undefined;
|
|
19
20
|
onSave?: ((text: string) => any) | undefined;
|
|
20
|
-
onReady?: ((view: EditorView) => any) | undefined;
|
|
21
21
|
}>, {
|
|
22
|
-
modelValue: string;
|
|
23
22
|
editable: boolean;
|
|
23
|
+
modelValue: string;
|
|
24
24
|
wordWrap: boolean;
|
|
25
25
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
26
26
|
declare const _default: typeof __VLS_export;
|
package/dist/code/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsonl, jsonlLanguage } from "./jsonl-language.js";
|
|
2
2
|
import { isCoreLang, registerLanguage, resolveLanguage } from "./lang-registry.js";
|
|
3
3
|
import { createCodeEditor, loadLanguage, setDocument, setLanguage, setReadOnly, setWordWrap } from "./editor-factory.js";
|
|
4
|
-
import { formatJsonInEditor, formatJsonText, isJsonFormattablePath } from "./format-json.js";
|
|
5
4
|
import { kindOf, languageOf } from "./kind.js";
|
|
5
|
+
import { formatJsonInEditor, formatJsonText, isJsonFormattablePath } from "./format-json.js";
|
|
6
6
|
export { createCodeEditor, formatJsonInEditor, formatJsonText, isCoreLang, isJsonFormattablePath, jsonl, jsonlLanguage, kindOf, languageOf, loadLanguage, registerLanguage, resolveLanguage, setDocument, setLanguage, setReadOnly, setWordWrap };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
//#region src/core/toolbar-fit.ts
|
|
2
|
+
/**
|
|
3
|
+
* How many leading groups stay expanded so that the row fits `available` px on one line, with `gap` px between groups;
|
|
4
|
+
* the remaining groups collapse. Collapsed groups are never expanded out of order, so the result is a prefix length.
|
|
5
|
+
* When even all-collapsed does not fit the result is 0 (the row overflows; nothing smaller exists).
|
|
6
|
+
*/
|
|
7
|
+
function expandedGroupCount(groups, available, gap) {
|
|
8
|
+
let total = groups.reduce((sum, group) => sum + group.collapsed, 0) + gap * Math.max(0, groups.length - 1);
|
|
9
|
+
let count = 0;
|
|
10
|
+
for (const group of groups) {
|
|
11
|
+
const next = total - group.collapsed + group.full;
|
|
12
|
+
if (next > available + .5) break;
|
|
13
|
+
total = next;
|
|
14
|
+
count++;
|
|
15
|
+
}
|
|
16
|
+
return count;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The dropdown rows of a collapsed group: each `run` command as a row, each `menu` command as a labelled section of its
|
|
20
|
+
* own rows. `component` commands render themselves and are left to the caller (shown next to the dropdown).
|
|
21
|
+
*/
|
|
22
|
+
function collapsedGroupMenu(items, editor) {
|
|
23
|
+
const rows = [];
|
|
24
|
+
const sections = [];
|
|
25
|
+
for (const { cmd, state } of items) if ("run" in cmd) rows.push({
|
|
26
|
+
label: cmd.label,
|
|
27
|
+
icon: cmd.icon,
|
|
28
|
+
disabled: !state.enabled,
|
|
29
|
+
onSelect: () => editor && cmd.run(editor)
|
|
30
|
+
});
|
|
31
|
+
else if ("menu" in cmd) {
|
|
32
|
+
const entries = editor && state.enabled ? cmd.menu(editor) : [];
|
|
33
|
+
sections.push([{
|
|
34
|
+
type: "label",
|
|
35
|
+
label: cmd.label
|
|
36
|
+
}, ...entries]);
|
|
37
|
+
}
|
|
38
|
+
return [...rows, ...sections.flatMap((section, index) => rows.length || index ? [{ type: "separator" }, ...section] : section)];
|
|
39
|
+
}
|
|
40
|
+
/** Estimated widths of a group never seen in that state (an icon button is 28-32px). */
|
|
41
|
+
const ESTIMATED_ITEM = 32;
|
|
42
|
+
const ESTIMATED_COLLAPSED = 48;
|
|
43
|
+
/**
|
|
44
|
+
* Watches a toolbar row and reports how many leading groups fit expanded (`onFit`, called only on change). Measuring
|
|
45
|
+
* happens only when the row or a group changes size (a newly observed group counts), one frame later. Widths are
|
|
46
|
+
* remembered per group, state and item count, so a collapsed group knows its expanded width from its last render; a
|
|
47
|
+
* group never rendered expanded is estimated until it is.
|
|
48
|
+
*/
|
|
49
|
+
function fitToolbar(row, onFit) {
|
|
50
|
+
const widths = /* @__PURE__ */ new Map();
|
|
51
|
+
const observed = /* @__PURE__ */ new Set();
|
|
52
|
+
let reported = -1;
|
|
53
|
+
let frame = 0;
|
|
54
|
+
const keyOf = (element, collapsed) => `${collapsed ? "c" : "f"}:${element.dataset.toolbarGroup}:${element.dataset.items}`;
|
|
55
|
+
const groupsOf = () => [...row.children].filter((child) => child instanceof HTMLElement && child.dataset.toolbarGroup !== void 0);
|
|
56
|
+
function measure() {
|
|
57
|
+
const groups = groupsOf();
|
|
58
|
+
for (const element of groups) widths.set(keyOf(element, element.dataset.collapsed !== void 0), element.getBoundingClientRect().width);
|
|
59
|
+
const available = row.clientWidth;
|
|
60
|
+
if (!available) return;
|
|
61
|
+
const gap = Number.parseFloat(getComputedStyle(row).columnGap) || 0;
|
|
62
|
+
const count = expandedGroupCount(groups.map((element) => ({
|
|
63
|
+
full: widths.get(keyOf(element, false)) ?? (Number(element.dataset.items) || 1) * ESTIMATED_ITEM,
|
|
64
|
+
collapsed: widths.get(keyOf(element, true)) ?? ESTIMATED_COLLAPSED
|
|
65
|
+
})), available, gap);
|
|
66
|
+
if (count === reported) return;
|
|
67
|
+
reported = count;
|
|
68
|
+
onFit(count);
|
|
69
|
+
}
|
|
70
|
+
const observer = new ResizeObserver(() => {
|
|
71
|
+
cancelAnimationFrame(frame);
|
|
72
|
+
frame = requestAnimationFrame(measure);
|
|
73
|
+
});
|
|
74
|
+
function refresh() {
|
|
75
|
+
for (const element of observed) {
|
|
76
|
+
if (element.isConnected && element.parentElement === row) continue;
|
|
77
|
+
observer.unobserve(element);
|
|
78
|
+
observed.delete(element);
|
|
79
|
+
}
|
|
80
|
+
for (const element of groupsOf()) {
|
|
81
|
+
if (observed.has(element)) continue;
|
|
82
|
+
observer.observe(element);
|
|
83
|
+
observed.add(element);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
observer.observe(row);
|
|
87
|
+
refresh();
|
|
88
|
+
return {
|
|
89
|
+
refresh,
|
|
90
|
+
dispose() {
|
|
91
|
+
cancelAnimationFrame(frame);
|
|
92
|
+
observer.disconnect();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
export { collapsedGroupMenu, expandedGroupCount, fitToolbar };
|
|
98
|
+
|
|
99
|
+
//# sourceMappingURL=toolbar-fit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toolbar-fit.js","names":[],"sources":["../../src/core/toolbar-fit.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport type { CommandDef, CommandState, MenuItem } from './command-types'\n\n// One-line toolbar (`overflow: 'collapse'` of the Vue and React `EditorToolbar`): groups that do not fit collapse, from\n// the end, into one dropdown each. The arithmetic and the menu rows are shared; the DOM part measures what the framework\n// rendered. A group element is a direct child of the toolbar row carrying `data-toolbar-group=\"<id>\"`,\n// `data-items=\"<count of run/menu commands>\"` and, while collapsed, `data-collapsed`; its width includes its leading\n// separator.\n\n/** Rendered widths (px) of one group: expanded, and collapsed to its dropdown (plus any `component` commands). */\nexport interface GroupWidths { full: number, collapsed: number }\n\n/**\n * How many leading groups stay expanded so that the row fits `available` px on one line, with `gap` px between groups;\n * the remaining groups collapse. Collapsed groups are never expanded out of order, so the result is a prefix length.\n * When even all-collapsed does not fit the result is 0 (the row overflows; nothing smaller exists).\n */\nexport function expandedGroupCount(groups: readonly GroupWidths[], available: number, gap: number): number {\n let total = groups.reduce((sum, group) => sum + group.collapsed, 0) + gap * Math.max(0, groups.length - 1)\n let count = 0\n for (const group of groups) {\n const next = total - group.collapsed + group.full\n // Half a pixel of slack: measured widths are fractional, the row's `clientWidth` is rounded.\n if (next > available + 0.5) break\n total = next\n count++\n }\n return count\n}\n\n/**\n * The dropdown rows of a collapsed group: each `run` command as a row, each `menu` command as a labelled section of its\n * own rows. `component` commands render themselves and are left to the caller (shown next to the dropdown).\n */\nexport function collapsedGroupMenu<C>(items: readonly { cmd: CommandDef<C>, state: CommandState }[], editor: Editor | undefined): MenuItem[] {\n const rows: MenuItem[] = []\n const sections: MenuItem[][] = []\n for (const { cmd, state } of items) {\n if ('run' in cmd) {\n rows.push({ label: cmd.label, icon: cmd.icon, disabled: !state.enabled, onSelect: () => editor && cmd.run(editor) })\n }\n else if ('menu' in cmd) {\n const entries = editor && state.enabled ? cmd.menu(editor) : []\n sections.push([{ type: 'label', label: cmd.label }, ...entries])\n }\n }\n return [...rows, ...sections.flatMap((section, index) => rows.length || index ? [{ type: 'separator' as const }, ...section] : section)]\n}\n\n/** Estimated widths of a group never seen in that state (an icon button is 28-32px). */\nconst ESTIMATED_ITEM = 32\nconst ESTIMATED_COLLAPSED = 48\n\nexport interface ToolbarFit {\n /** Picks up group elements added since the last call (cheap: no layout read); call after every render of the row. */\n refresh(): void\n dispose(): void\n}\n\n/**\n * Watches a toolbar row and reports how many leading groups fit expanded (`onFit`, called only on change). Measuring\n * happens only when the row or a group changes size (a newly observed group counts), one frame later. Widths are\n * remembered per group, state and item count, so a collapsed group knows its expanded width from its last render; a\n * group never rendered expanded is estimated until it is.\n */\nexport function fitToolbar(row: HTMLElement, onFit: (expanded: number) => void): ToolbarFit {\n const widths = new Map<string, number>()\n const observed = new Set<Element>()\n let reported = -1\n let frame = 0\n\n const keyOf = (element: HTMLElement, collapsed: boolean) =>\n `${collapsed ? 'c' : 'f'}:${element.dataset.toolbarGroup}:${element.dataset.items}`\n const groupsOf = () => [...row.children].filter((child): child is HTMLElement =>\n child instanceof HTMLElement && child.dataset.toolbarGroup !== undefined)\n\n function measure() {\n const groups = groupsOf()\n for (const element of groups) widths.set(keyOf(element, element.dataset.collapsed !== undefined), element.getBoundingClientRect().width)\n const available = row.clientWidth\n if (!available) return\n const gap = Number.parseFloat(getComputedStyle(row).columnGap) || 0\n const count = expandedGroupCount(groups.map(element => ({\n full: widths.get(keyOf(element, false)) ?? (Number(element.dataset.items) || 1) * ESTIMATED_ITEM,\n collapsed: widths.get(keyOf(element, true)) ?? ESTIMATED_COLLAPSED,\n })), available, gap)\n if (count === reported) return\n reported = count\n onFit(count)\n }\n\n // Next frame: reporting inside the observer callback would resize the observed elements in the same frame.\n const observer = new ResizeObserver(() => {\n cancelAnimationFrame(frame)\n frame = requestAnimationFrame(measure)\n })\n\n function refresh() {\n for (const element of observed) {\n if (element.isConnected && element.parentElement === row) continue\n observer.unobserve(element)\n observed.delete(element)\n }\n for (const element of groupsOf()) {\n if (observed.has(element)) continue\n observer.observe(element)\n observed.add(element)\n }\n }\n\n observer.observe(row)\n refresh()\n\n return {\n refresh,\n dispose() {\n cancelAnimationFrame(frame)\n observer.disconnect()\n },\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { kindOf, languageOf } from "../code/kind.js";
|
|
2
1
|
import { CodeSurface } from "../code/CodeSurface.js";
|
|
2
|
+
import { kindOf, languageOf } from "../code/kind.js";
|
|
3
3
|
import { MarkdownSurface } from "../markdown/MarkdownSurface.js";
|
|
4
4
|
import { Fragment, jsx } from "react/jsx-runtime";
|
|
5
5
|
//#region src/document/DocumentSurface.tsx
|
|
@@ -32,16 +32,16 @@ type __VLS_Slots = {} & { [K in NonNullable<typeof __VLS_13>]?: (props: typeof _
|
|
|
32
32
|
binary?: (props: typeof __VLS_26) => any;
|
|
33
33
|
};
|
|
34
34
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
35
|
+
ready: (target: DocumentSurfaceTarget) => any;
|
|
35
36
|
"update:modelValue": (text: string) => any;
|
|
36
37
|
save: (text: string) => any;
|
|
37
|
-
ready: (target: DocumentSurfaceTarget) => any;
|
|
38
38
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
39
|
+
onReady?: ((target: DocumentSurfaceTarget) => any) | undefined;
|
|
39
40
|
"onUpdate:modelValue"?: ((text: string) => any) | undefined;
|
|
40
41
|
onSave?: ((text: string) => any) | undefined;
|
|
41
|
-
onReady?: ((target: DocumentSurfaceTarget) => any) | undefined;
|
|
42
42
|
}>, {
|
|
43
|
-
modelValue: string;
|
|
44
43
|
editable: boolean;
|
|
44
|
+
modelValue: string;
|
|
45
45
|
wordWrap: boolean;
|
|
46
46
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
47
47
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -84,7 +84,7 @@ function agentOpSnippet(doc, op, t) {
|
|
|
84
84
|
const node = target === null ? void 0 : findBlockById(doc, target)?.node;
|
|
85
85
|
return blockSnippet(node && {
|
|
86
86
|
type: node.type.name,
|
|
87
|
-
text: node.
|
|
87
|
+
text: node.textBetween(0, node.content.size, " ")
|
|
88
88
|
}, target, t);
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-ops.js","names":[],"sources":["../../../src/features/ai/agent-ops.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { Fragment, type Node as PMNode } from '@tiptap/pm/model'\nimport type { EditorState } from '@tiptap/pm/state'\nimport { blockConversion, blockSnippet, conversionsOf, isWrapperBlock, parseBlocks, parseConversionTarget, selectionRange } from '../../core'\nimport { BLOCK_ID_ATTR, blockIdOf, findBlockById, isBlockContainer, isChromeNode } from '../../core/schema'\nimport type { MessageKey, Translate } from '../../i18n'\nimport type { AiReviewChange } from './ai-review'\nimport { type AgentOp, type AgentOpOutcome, agentOpTarget, stripListMarkerEcho } from './protocol'\n\n/** Edit scope of a non-empty selection: the top-level blocks it touches, and every block ID inside them. */\nexport interface AgentScope { blocks: number, ids: string[] }\n\n/** The scope chat edits are limited to: the top-level blocks the selection touches; null without a selection (whole document). */\nexport function selectionScope(state: EditorState): AgentScope | null {\n if (state.selection.empty) return null\n const { from, to } = selectionRange(state)\n const ids: string[] = []\n let blocks = 0\n state.doc.forEach((node, offset) => {\n if (offset >= to || offset + node.nodeSize <= from) return\n blocks++\n const own = blockIdOf(node)\n if (own) ids.push(own)\n node.descendants((child) => {\n const id = blockIdOf(child)\n if (id) ids.push(id)\n return !child.isTextblock\n })\n })\n return { blocks, ids }\n}\n\n/** Catalog keys of how the chat shows an outcome: the badge label and the badge's hover text. */\nexport const AGENT_OP_OUTCOME_KEYS: Record<AgentOpOutcome, { badge: MessageKey, hover: MessageKey }> = {\n pending: { badge: 'ai.opBadge.pending', hover: 'ai.opTitle.pending' },\n accepted: { badge: 'ai.opBadge.accepted', hover: 'ai.opTitle.accepted' },\n rejected: { badge: 'ai.opBadge.rejected', hover: 'ai.opTitle.rejected' },\n partial: { badge: 'ai.opBadge.partial', hover: 'ai.opTitle.partial' },\n invalid: { badge: 'ai.opBadge.invalid', hover: 'ai.opTitle.invalid' },\n unchanged: { badge: 'ai.opBadge.unchanged', hover: 'ai.opTitle.unchanged' },\n missing: { badge: 'ai.opBadge.missing', hover: 'ai.opTitle.missing' },\n stale: { badge: 'ai.opBadge.stale', hover: 'ai.opTitle.stale' },\n overlap: { badge: 'ai.opBadge.overlap', hover: 'ai.opTitle.overlap' },\n empty: { badge: 'ai.opBadge.empty', hover: 'ai.opTitle.empty' },\n blocked: { badge: 'ai.opBadge.blocked', hover: 'ai.opTitle.blocked' },\n}\n\n/** Short text of the block an operation targets, for the chat („Dokumentanfang“ for an insertion at the start). */\nexport function agentOpSnippet(doc: PMNode, op: AgentOp, t: Translate): string {\n const target = agentOpTarget(op)\n const node = target === null ? undefined : findBlockById(doc, target)?.node\n return blockSnippet(node && { type: node.type.name, text: node.textContent }, target, t)\n}\n\n/**\n * The review change of an operation, re-checked against the scope the request was sent with (`allowed`, null: whole\n * document): 'blocked' when it points outside, 'stale' when its block is gone (deleted in the browser while the model was\n * answering), 'unchanged' for a conversion that changes nothing or is impossible there.\n * - update: a single paragraph, or a single block of the same type and attributes (a speaker line of the same\n * speaker), is diffed within the block's content, so the block keeps its type and speaker; anything else (another\n * speaker's `**Name:**`, several blocks, a different block type) replaces the whole block. On a composite, a\n * `::: kind` fence (exactly one composite) replaces the whole composite, anything else only its body.\n * - delete: the block, or the smallest list item, list or quote around it that would be invalid without it.\n * - add: after the target's block in the document, in a composite body or in the body of a host block holding blocks\n * (a callout); a list of the same type after a list item goes after that item, a host block of the same type after\n * one of its body blocks after that whole block.\n * - convert: the block (a heading becoming a composite with its group) replaced by `convertBlockTo`'s result.\n */\nexport function agentOpChange(editor: Editor, op: AgentOp, allowed: ReadonlySet<string> | null): AiReviewChange | 'blocked' | 'stale' | 'unchanged' {\n const target = agentOpTarget(op)\n if (allowed && (target === null || !allowed.has(target))) return 'blocked'\n const { state } = editor\n const { doc, schema } = state\n\n if (op.type === 'add' && target === null) return { id: op.op, kind: 'insert', at: 0, markdown: op.markdown }\n const found = target === null ? null : findBlockById(doc, target)\n if (!found) return 'stale'\n const { node, pos } = found\n const $pos = doc.resolve(pos)\n\n if (op.type === 'add') {\n const firstType = parseBlocks(editor, op.markdown).blocks[0]?.type\n // After the block around the target that sits in the document, in a composite body or in a host block that holds\n // blocks (a callout), so an insertion after a body block stays inside its wrapper, unless it adds a wrapper of the\n // same type (then after the whole wrapper). Lists and quotes insert after the whole list or quote.\n const holdsBody = (n: PMNode) => isBlockContainer(n) || isWrapperBlock(n)\n let container = $pos.depth\n while (container > 0 && !holdsBody($pos.node(container))) container--\n let at = container === $pos.depth ? pos + node.nodeSize : $pos.after(container + 1)\n if (container > 0 && isWrapperBlock($pos.node(container)) && $pos.node(container).type.name === firstType) at = $pos.after(container)\n for (let depth = $pos.depth; depth > 0; depth--) {\n if ($pos.node(depth).type.name !== 'listItem') continue\n if ($pos.node(depth - 1).type.name === firstType) at = $pos.after(depth)\n break\n }\n return { id: op.op, kind: 'insert', at, markdown: op.markdown }\n }\n\n if (op.type === 'delete') {\n let from = pos\n let to = pos + node.nodeSize\n for (let $from = $pos; $from.depth > 0; $from = doc.resolve(from)) {\n const parent = $from.parent\n const start = $from.start()\n if (parent.type.validContent(parent.content.cut(0, from - start).append(parent.content.cut(to - start)))) break\n from = $from.before()\n to = $from.after()\n }\n return { id: op.op, kind: 'delete', range: { from, to } }\n }\n\n if (op.type === 'convert') {\n const providers = conversionsOf(editor)\n const to = parseConversionTarget(providers, op.to)\n const conversion = to && blockConversion(providers, state, $pos, to)\n if (!conversion) return 'unchanged'\n return { id: op.op, kind: 'blocks', range: { from: conversion.from, to: conversion.to }, blocks: Fragment.from(conversion.blocks) }\n }\n\n const whole = { from: pos, to: pos + node.nodeSize }\n if (isBlockContainer(node)) {\n const { blocks } = parseBlocks(editor, op.markdown)\n if (blocks.length === 1 && blocks[0]!.type === node.type.name) return { id: op.op, range: whole, markdown: op.markdown }\n let body = pos + 1\n node.forEach((child) => {\n if (isChromeNode(child)) body += child.nodeSize\n })\n return { id: op.op, range: { from: body, to: pos + node.nodeSize - 1 }, markdown: op.markdown }\n }\n if (!node.isTextblock) return { id: op.op, range: whole, markdown: op.markdown }\n // Lines of blocks in lists and quotes carry their markers, which the model may echo; composite bodies carry none (a\n // fence the model writes there keeps its `> description` and list items).\n let marked = false\n for (let depth = $pos.depth; depth > 0 && !marked; depth--) marked = ['listItem', 'blockquote'].includes($pos.node(depth).type.name)\n const markdown = marked ? stripListMarkerEcho(op.markdown) : op.markdown\n const { blocks } = parseBlocks(editor, markdown)\n const result = blocks.length === 1 ? schema.nodeFromJSON(blocks[0]!) : null\n const sameKind = result?.type === node.type && Object.keys(node.attrs).every(name => name === BLOCK_ID_ATTR || result.attrs[name] === node.attrs[name])\n const inside = node.content.size > 0 && (sameKind || result?.type.name === 'paragraph')\n const range = inside ? { from: pos + 1, to: pos + 1 + node.content.size } : whole\n return { id: op.op, range, markdown }\n}\n"],"mappings":";;;;;;;;;;;;AAaA,SAAgB,eAAe,OAAuC;CACpE,IAAI,MAAM,UAAU,OAAO,OAAO;CAClC,MAAM,EAAE,MAAM,OAAO,eAAe,KAAK;CACzC,MAAM,MAAgB,CAAC;CACvB,IAAI,SAAS;CACb,MAAM,IAAI,SAAS,MAAM,WAAW;EAClC,IAAI,UAAU,MAAM,SAAS,KAAK,YAAY,MAAM;EACpD;EACA,MAAM,MAAM,UAAU,IAAI;EAC1B,IAAI,KAAK,IAAI,KAAK,GAAG;EACrB,KAAK,aAAa,UAAU;GAC1B,MAAM,KAAK,UAAU,KAAK;GAC1B,IAAI,IAAI,IAAI,KAAK,EAAE;GACnB,OAAO,CAAC,MAAM;EAChB,CAAC;CACH,CAAC;CACD,OAAO;EAAE;EAAQ;CAAI;AACvB;;AAGA,MAAa,wBAA0F;CACrG,SAAS;EAAE,OAAO;EAAsB,OAAO;CAAqB;CACpE,UAAU;EAAE,OAAO;EAAuB,OAAO;CAAsB;CACvE,UAAU;EAAE,OAAO;EAAuB,OAAO;CAAsB;CACvE,SAAS;EAAE,OAAO;EAAsB,OAAO;CAAqB;CACpE,SAAS;EAAE,OAAO;EAAsB,OAAO;CAAqB;CACpE,WAAW;EAAE,OAAO;EAAwB,OAAO;CAAuB;CAC1E,SAAS;EAAE,OAAO;EAAsB,OAAO;CAAqB;CACpE,OAAO;EAAE,OAAO;EAAoB,OAAO;CAAmB;CAC9D,SAAS;EAAE,OAAO;EAAsB,OAAO;CAAqB;CACpE,OAAO;EAAE,OAAO;EAAoB,OAAO;CAAmB;CAC9D,SAAS;EAAE,OAAO;EAAsB,OAAO;CAAqB;AACtE;;AAGA,SAAgB,eAAe,KAAa,IAAa,GAAsB;CAC7E,MAAM,SAAS,cAAc,EAAE;CAC/B,MAAM,OAAO,WAAW,OAAO,KAAA,IAAY,cAAc,KAAK,MAAM,CAAC,EAAE;CACvE,OAAO,aAAa,QAAQ;EAAE,MAAM,KAAK,KAAK;EAAM,MAAM,KAAK;CAAY,GAAG,QAAQ,CAAC;AACzF;;;;;;;;;;;;;;;AAgBA,SAAgB,cAAc,QAAgB,IAAa,SAAyF;CAClJ,MAAM,SAAS,cAAc,EAAE;CAC/B,IAAI,YAAY,WAAW,QAAQ,CAAC,QAAQ,IAAI,MAAM,IAAI,OAAO;CACjE,MAAM,EAAE,UAAU;CAClB,MAAM,EAAE,KAAK,WAAW;CAExB,IAAI,GAAG,SAAS,SAAS,WAAW,MAAM,OAAO;EAAE,IAAI,GAAG;EAAI,MAAM;EAAU,IAAI;EAAG,UAAU,GAAG;CAAS;CAC3G,MAAM,QAAQ,WAAW,OAAO,OAAO,cAAc,KAAK,MAAM;CAChE,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,EAAE,MAAM,QAAQ;CACtB,MAAM,OAAO,IAAI,QAAQ,GAAG;CAE5B,IAAI,GAAG,SAAS,OAAO;EACrB,MAAM,YAAY,YAAY,QAAQ,GAAG,QAAQ,CAAC,CAAC,OAAO,EAAE,EAAE;EAI9D,MAAM,aAAa,MAAc,iBAAiB,CAAC,KAAK,eAAe,CAAC;EACxE,IAAI,YAAY,KAAK;EACrB,OAAO,YAAY,KAAK,CAAC,UAAU,KAAK,KAAK,SAAS,CAAC,GAAG;EAC1D,IAAI,KAAK,cAAc,KAAK,QAAQ,MAAM,KAAK,WAAW,KAAK,MAAM,YAAY,CAAC;EAClF,IAAI,YAAY,KAAK,eAAe,KAAK,KAAK,SAAS,CAAC,KAAK,KAAK,KAAK,SAAS,CAAC,CAAC,KAAK,SAAS,WAAW,KAAK,KAAK,MAAM,SAAS;EACpI,KAAK,IAAI,QAAQ,KAAK,OAAO,QAAQ,GAAG,SAAS;GAC/C,IAAI,KAAK,KAAK,KAAK,CAAC,CAAC,KAAK,SAAS,YAAY;GAC/C,IAAI,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,SAAS,WAAW,KAAK,KAAK,MAAM,KAAK;GACvE;EACF;EACA,OAAO;GAAE,IAAI,GAAG;GAAI,MAAM;GAAU;GAAI,UAAU,GAAG;EAAS;CAChE;CAEA,IAAI,GAAG,SAAS,UAAU;EACxB,IAAI,OAAO;EACX,IAAI,KAAK,MAAM,KAAK;EACpB,KAAK,IAAI,QAAQ,MAAM,MAAM,QAAQ,GAAG,QAAQ,IAAI,QAAQ,IAAI,GAAG;GACjE,MAAM,SAAS,MAAM;GACrB,MAAM,QAAQ,MAAM,MAAM;GAC1B,IAAI,OAAO,KAAK,aAAa,OAAO,QAAQ,IAAI,GAAG,OAAO,KAAK,CAAC,CAAC,OAAO,OAAO,QAAQ,IAAI,KAAK,KAAK,CAAC,CAAC,GAAG;GAC1G,OAAO,MAAM,OAAO;GACpB,KAAK,MAAM,MAAM;EACnB;EACA,OAAO;GAAE,IAAI,GAAG;GAAI,MAAM;GAAU,OAAO;IAAE;IAAM;GAAG;EAAE;CAC1D;CAEA,IAAI,GAAG,SAAS,WAAW;EACzB,MAAM,YAAY,cAAc,MAAM;EACtC,MAAM,KAAK,sBAAsB,WAAW,GAAG,EAAE;EACjD,MAAM,aAAa,MAAM,gBAAgB,WAAW,OAAO,MAAM,EAAE;EACnE,IAAI,CAAC,YAAY,OAAO;EACxB,OAAO;GAAE,IAAI,GAAG;GAAI,MAAM;GAAU,OAAO;IAAE,MAAM,WAAW;IAAM,IAAI,WAAW;GAAG;GAAG,QAAQ,SAAS,KAAK,WAAW,MAAM;EAAE;CACpI;CAEA,MAAM,QAAQ;EAAE,MAAM;EAAK,IAAI,MAAM,KAAK;CAAS;CACnD,IAAI,iBAAiB,IAAI,GAAG;EAC1B,MAAM,EAAE,WAAW,YAAY,QAAQ,GAAG,QAAQ;EAClD,IAAI,OAAO,WAAW,KAAK,OAAO,EAAE,CAAE,SAAS,KAAK,KAAK,MAAM,OAAO;GAAE,IAAI,GAAG;GAAI,OAAO;GAAO,UAAU,GAAG;EAAS;EACvH,IAAI,OAAO,MAAM;EACjB,KAAK,SAAS,UAAU;GACtB,IAAI,aAAa,KAAK,GAAG,QAAQ,MAAM;EACzC,CAAC;EACD,OAAO;GAAE,IAAI,GAAG;GAAI,OAAO;IAAE,MAAM;IAAM,IAAI,MAAM,KAAK,WAAW;GAAE;GAAG,UAAU,GAAG;EAAS;CAChG;CACA,IAAI,CAAC,KAAK,aAAa,OAAO;EAAE,IAAI,GAAG;EAAI,OAAO;EAAO,UAAU,GAAG;CAAS;CAG/E,IAAI,SAAS;CACb,KAAK,IAAI,QAAQ,KAAK,OAAO,QAAQ,KAAK,CAAC,QAAQ,SAAS,SAAS,CAAC,YAAY,YAAY,CAAC,CAAC,SAAS,KAAK,KAAK,KAAK,CAAC,CAAC,KAAK,IAAI;CACnI,MAAM,WAAW,SAAS,oBAAoB,GAAG,QAAQ,IAAI,GAAG;CAChE,MAAM,EAAE,WAAW,YAAY,QAAQ,QAAQ;CAC/C,MAAM,SAAS,OAAO,WAAW,IAAI,OAAO,aAAa,OAAO,EAAG,IAAI;CACvE,MAAM,WAAW,QAAQ,SAAS,KAAK,QAAQ,OAAO,KAAK,KAAK,KAAK,CAAC,CAAC,OAAM,SAAQ,SAAA,QAA0B,OAAO,MAAM,UAAU,KAAK,MAAM,KAAK;CAEtJ,MAAM,QADS,KAAK,QAAQ,OAAO,MAAM,YAAY,QAAQ,KAAK,SAAS,eACpD;EAAE,MAAM,MAAM;EAAG,IAAI,MAAM,IAAI,KAAK,QAAQ;CAAK,IAAI;CAC5E,OAAO;EAAE,IAAI,GAAG;EAAI;EAAO;CAAS;AACtC"}
|
|
1
|
+
{"version":3,"file":"agent-ops.js","names":[],"sources":["../../../src/features/ai/agent-ops.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { Fragment, type Node as PMNode } from '@tiptap/pm/model'\nimport type { EditorState } from '@tiptap/pm/state'\nimport { blockConversion, blockSnippet, conversionsOf, isWrapperBlock, parseBlocks, parseConversionTarget, selectionRange } from '../../core'\nimport { BLOCK_ID_ATTR, blockIdOf, findBlockById, isBlockContainer, isChromeNode } from '../../core/schema'\nimport type { MessageKey, Translate } from '../../i18n'\nimport type { AiReviewChange } from './ai-review'\nimport { type AgentOp, type AgentOpOutcome, agentOpTarget, stripListMarkerEcho } from './protocol'\n\n/** Edit scope of a non-empty selection: the top-level blocks it touches, and every block ID inside them. */\nexport interface AgentScope { blocks: number, ids: string[] }\n\n/** The scope chat edits are limited to: the top-level blocks the selection touches; null without a selection (whole document). */\nexport function selectionScope(state: EditorState): AgentScope | null {\n if (state.selection.empty) return null\n const { from, to } = selectionRange(state)\n const ids: string[] = []\n let blocks = 0\n state.doc.forEach((node, offset) => {\n if (offset >= to || offset + node.nodeSize <= from) return\n blocks++\n const own = blockIdOf(node)\n if (own) ids.push(own)\n node.descendants((child) => {\n const id = blockIdOf(child)\n if (id) ids.push(id)\n return !child.isTextblock\n })\n })\n return { blocks, ids }\n}\n\n/** Catalog keys of how the chat shows an outcome: the badge label and the badge's hover text. */\nexport const AGENT_OP_OUTCOME_KEYS: Record<AgentOpOutcome, { badge: MessageKey, hover: MessageKey }> = {\n pending: { badge: 'ai.opBadge.pending', hover: 'ai.opTitle.pending' },\n accepted: { badge: 'ai.opBadge.accepted', hover: 'ai.opTitle.accepted' },\n rejected: { badge: 'ai.opBadge.rejected', hover: 'ai.opTitle.rejected' },\n partial: { badge: 'ai.opBadge.partial', hover: 'ai.opTitle.partial' },\n invalid: { badge: 'ai.opBadge.invalid', hover: 'ai.opTitle.invalid' },\n unchanged: { badge: 'ai.opBadge.unchanged', hover: 'ai.opTitle.unchanged' },\n missing: { badge: 'ai.opBadge.missing', hover: 'ai.opTitle.missing' },\n stale: { badge: 'ai.opBadge.stale', hover: 'ai.opTitle.stale' },\n overlap: { badge: 'ai.opBadge.overlap', hover: 'ai.opTitle.overlap' },\n empty: { badge: 'ai.opBadge.empty', hover: 'ai.opTitle.empty' },\n blocked: { badge: 'ai.opBadge.blocked', hover: 'ai.opTitle.blocked' },\n}\n\n/** Short text of the block an operation targets, for the chat („Dokumentanfang“ for an insertion at the start). */\nexport function agentOpSnippet(doc: PMNode, op: AgentOp, t: Translate): string {\n const target = agentOpTarget(op)\n const node = target === null ? undefined : findBlockById(doc, target)?.node\n // Cells and other nested blocks are separated by a space, so a table's snippet reads „Datum Therapeut/in …“.\n return blockSnippet(node && { type: node.type.name, text: node.textBetween(0, node.content.size, ' ') }, target, t)\n}\n\n/**\n * The review change of an operation, re-checked against the scope the request was sent with (`allowed`, null: whole\n * document): 'blocked' when it points outside, 'stale' when its block is gone (deleted in the browser while the model was\n * answering), 'unchanged' for a conversion that changes nothing or is impossible there.\n * - update: a single paragraph, or a single block of the same type and attributes (a speaker line of the same\n * speaker), is diffed within the block's content, so the block keeps its type and speaker; anything else (another\n * speaker's `**Name:**`, several blocks, a different block type) replaces the whole block. On a composite, a\n * `::: kind` fence (exactly one composite) replaces the whole composite, anything else only its body.\n * - delete: the block, or the smallest list item, list or quote around it that would be invalid without it.\n * - add: after the target's block in the document, in a composite body or in the body of a host block holding blocks\n * (a callout); a list of the same type after a list item goes after that item, a host block of the same type after\n * one of its body blocks after that whole block.\n * - convert: the block (a heading becoming a composite with its group) replaced by `convertBlockTo`'s result.\n */\nexport function agentOpChange(editor: Editor, op: AgentOp, allowed: ReadonlySet<string> | null): AiReviewChange | 'blocked' | 'stale' | 'unchanged' {\n const target = agentOpTarget(op)\n if (allowed && (target === null || !allowed.has(target))) return 'blocked'\n const { state } = editor\n const { doc, schema } = state\n\n if (op.type === 'add' && target === null) return { id: op.op, kind: 'insert', at: 0, markdown: op.markdown }\n const found = target === null ? null : findBlockById(doc, target)\n if (!found) return 'stale'\n const { node, pos } = found\n const $pos = doc.resolve(pos)\n\n if (op.type === 'add') {\n const firstType = parseBlocks(editor, op.markdown).blocks[0]?.type\n // After the block around the target that sits in the document, in a composite body or in a host block that holds\n // blocks (a callout), so an insertion after a body block stays inside its wrapper, unless it adds a wrapper of the\n // same type (then after the whole wrapper). Lists and quotes insert after the whole list or quote.\n const holdsBody = (n: PMNode) => isBlockContainer(n) || isWrapperBlock(n)\n let container = $pos.depth\n while (container > 0 && !holdsBody($pos.node(container))) container--\n let at = container === $pos.depth ? pos + node.nodeSize : $pos.after(container + 1)\n if (container > 0 && isWrapperBlock($pos.node(container)) && $pos.node(container).type.name === firstType) at = $pos.after(container)\n for (let depth = $pos.depth; depth > 0; depth--) {\n if ($pos.node(depth).type.name !== 'listItem') continue\n if ($pos.node(depth - 1).type.name === firstType) at = $pos.after(depth)\n break\n }\n return { id: op.op, kind: 'insert', at, markdown: op.markdown }\n }\n\n if (op.type === 'delete') {\n let from = pos\n let to = pos + node.nodeSize\n for (let $from = $pos; $from.depth > 0; $from = doc.resolve(from)) {\n const parent = $from.parent\n const start = $from.start()\n if (parent.type.validContent(parent.content.cut(0, from - start).append(parent.content.cut(to - start)))) break\n from = $from.before()\n to = $from.after()\n }\n return { id: op.op, kind: 'delete', range: { from, to } }\n }\n\n if (op.type === 'convert') {\n const providers = conversionsOf(editor)\n const to = parseConversionTarget(providers, op.to)\n const conversion = to && blockConversion(providers, state, $pos, to)\n if (!conversion) return 'unchanged'\n return { id: op.op, kind: 'blocks', range: { from: conversion.from, to: conversion.to }, blocks: Fragment.from(conversion.blocks) }\n }\n\n const whole = { from: pos, to: pos + node.nodeSize }\n if (isBlockContainer(node)) {\n const { blocks } = parseBlocks(editor, op.markdown)\n if (blocks.length === 1 && blocks[0]!.type === node.type.name) return { id: op.op, range: whole, markdown: op.markdown }\n let body = pos + 1\n node.forEach((child) => {\n if (isChromeNode(child)) body += child.nodeSize\n })\n return { id: op.op, range: { from: body, to: pos + node.nodeSize - 1 }, markdown: op.markdown }\n }\n if (!node.isTextblock) return { id: op.op, range: whole, markdown: op.markdown }\n // Lines of blocks in lists and quotes carry their markers, which the model may echo; composite bodies carry none (a\n // fence the model writes there keeps its `> description` and list items).\n let marked = false\n for (let depth = $pos.depth; depth > 0 && !marked; depth--) marked = ['listItem', 'blockquote'].includes($pos.node(depth).type.name)\n const markdown = marked ? stripListMarkerEcho(op.markdown) : op.markdown\n const { blocks } = parseBlocks(editor, markdown)\n const result = blocks.length === 1 ? schema.nodeFromJSON(blocks[0]!) : null\n const sameKind = result?.type === node.type && Object.keys(node.attrs).every(name => name === BLOCK_ID_ATTR || result.attrs[name] === node.attrs[name])\n const inside = node.content.size > 0 && (sameKind || result?.type.name === 'paragraph')\n const range = inside ? { from: pos + 1, to: pos + 1 + node.content.size } : whole\n return { id: op.op, range, markdown }\n}\n"],"mappings":";;;;;;;;;;;;AAaA,SAAgB,eAAe,OAAuC;CACpE,IAAI,MAAM,UAAU,OAAO,OAAO;CAClC,MAAM,EAAE,MAAM,OAAO,eAAe,KAAK;CACzC,MAAM,MAAgB,CAAC;CACvB,IAAI,SAAS;CACb,MAAM,IAAI,SAAS,MAAM,WAAW;EAClC,IAAI,UAAU,MAAM,SAAS,KAAK,YAAY,MAAM;EACpD;EACA,MAAM,MAAM,UAAU,IAAI;EAC1B,IAAI,KAAK,IAAI,KAAK,GAAG;EACrB,KAAK,aAAa,UAAU;GAC1B,MAAM,KAAK,UAAU,KAAK;GAC1B,IAAI,IAAI,IAAI,KAAK,EAAE;GACnB,OAAO,CAAC,MAAM;EAChB,CAAC;CACH,CAAC;CACD,OAAO;EAAE;EAAQ;CAAI;AACvB;;AAGA,MAAa,wBAA0F;CACrG,SAAS;EAAE,OAAO;EAAsB,OAAO;CAAqB;CACpE,UAAU;EAAE,OAAO;EAAuB,OAAO;CAAsB;CACvE,UAAU;EAAE,OAAO;EAAuB,OAAO;CAAsB;CACvE,SAAS;EAAE,OAAO;EAAsB,OAAO;CAAqB;CACpE,SAAS;EAAE,OAAO;EAAsB,OAAO;CAAqB;CACpE,WAAW;EAAE,OAAO;EAAwB,OAAO;CAAuB;CAC1E,SAAS;EAAE,OAAO;EAAsB,OAAO;CAAqB;CACpE,OAAO;EAAE,OAAO;EAAoB,OAAO;CAAmB;CAC9D,SAAS;EAAE,OAAO;EAAsB,OAAO;CAAqB;CACpE,OAAO;EAAE,OAAO;EAAoB,OAAO;CAAmB;CAC9D,SAAS;EAAE,OAAO;EAAsB,OAAO;CAAqB;AACtE;;AAGA,SAAgB,eAAe,KAAa,IAAa,GAAsB;CAC7E,MAAM,SAAS,cAAc,EAAE;CAC/B,MAAM,OAAO,WAAW,OAAO,KAAA,IAAY,cAAc,KAAK,MAAM,CAAC,EAAE;CAEvE,OAAO,aAAa,QAAQ;EAAE,MAAM,KAAK,KAAK;EAAM,MAAM,KAAK,YAAY,GAAG,KAAK,QAAQ,MAAM,GAAG;CAAE,GAAG,QAAQ,CAAC;AACpH;;;;;;;;;;;;;;;AAgBA,SAAgB,cAAc,QAAgB,IAAa,SAAyF;CAClJ,MAAM,SAAS,cAAc,EAAE;CAC/B,IAAI,YAAY,WAAW,QAAQ,CAAC,QAAQ,IAAI,MAAM,IAAI,OAAO;CACjE,MAAM,EAAE,UAAU;CAClB,MAAM,EAAE,KAAK,WAAW;CAExB,IAAI,GAAG,SAAS,SAAS,WAAW,MAAM,OAAO;EAAE,IAAI,GAAG;EAAI,MAAM;EAAU,IAAI;EAAG,UAAU,GAAG;CAAS;CAC3G,MAAM,QAAQ,WAAW,OAAO,OAAO,cAAc,KAAK,MAAM;CAChE,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,EAAE,MAAM,QAAQ;CACtB,MAAM,OAAO,IAAI,QAAQ,GAAG;CAE5B,IAAI,GAAG,SAAS,OAAO;EACrB,MAAM,YAAY,YAAY,QAAQ,GAAG,QAAQ,CAAC,CAAC,OAAO,EAAE,EAAE;EAI9D,MAAM,aAAa,MAAc,iBAAiB,CAAC,KAAK,eAAe,CAAC;EACxE,IAAI,YAAY,KAAK;EACrB,OAAO,YAAY,KAAK,CAAC,UAAU,KAAK,KAAK,SAAS,CAAC,GAAG;EAC1D,IAAI,KAAK,cAAc,KAAK,QAAQ,MAAM,KAAK,WAAW,KAAK,MAAM,YAAY,CAAC;EAClF,IAAI,YAAY,KAAK,eAAe,KAAK,KAAK,SAAS,CAAC,KAAK,KAAK,KAAK,SAAS,CAAC,CAAC,KAAK,SAAS,WAAW,KAAK,KAAK,MAAM,SAAS;EACpI,KAAK,IAAI,QAAQ,KAAK,OAAO,QAAQ,GAAG,SAAS;GAC/C,IAAI,KAAK,KAAK,KAAK,CAAC,CAAC,KAAK,SAAS,YAAY;GAC/C,IAAI,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,SAAS,WAAW,KAAK,KAAK,MAAM,KAAK;GACvE;EACF;EACA,OAAO;GAAE,IAAI,GAAG;GAAI,MAAM;GAAU;GAAI,UAAU,GAAG;EAAS;CAChE;CAEA,IAAI,GAAG,SAAS,UAAU;EACxB,IAAI,OAAO;EACX,IAAI,KAAK,MAAM,KAAK;EACpB,KAAK,IAAI,QAAQ,MAAM,MAAM,QAAQ,GAAG,QAAQ,IAAI,QAAQ,IAAI,GAAG;GACjE,MAAM,SAAS,MAAM;GACrB,MAAM,QAAQ,MAAM,MAAM;GAC1B,IAAI,OAAO,KAAK,aAAa,OAAO,QAAQ,IAAI,GAAG,OAAO,KAAK,CAAC,CAAC,OAAO,OAAO,QAAQ,IAAI,KAAK,KAAK,CAAC,CAAC,GAAG;GAC1G,OAAO,MAAM,OAAO;GACpB,KAAK,MAAM,MAAM;EACnB;EACA,OAAO;GAAE,IAAI,GAAG;GAAI,MAAM;GAAU,OAAO;IAAE;IAAM;GAAG;EAAE;CAC1D;CAEA,IAAI,GAAG,SAAS,WAAW;EACzB,MAAM,YAAY,cAAc,MAAM;EACtC,MAAM,KAAK,sBAAsB,WAAW,GAAG,EAAE;EACjD,MAAM,aAAa,MAAM,gBAAgB,WAAW,OAAO,MAAM,EAAE;EACnE,IAAI,CAAC,YAAY,OAAO;EACxB,OAAO;GAAE,IAAI,GAAG;GAAI,MAAM;GAAU,OAAO;IAAE,MAAM,WAAW;IAAM,IAAI,WAAW;GAAG;GAAG,QAAQ,SAAS,KAAK,WAAW,MAAM;EAAE;CACpI;CAEA,MAAM,QAAQ;EAAE,MAAM;EAAK,IAAI,MAAM,KAAK;CAAS;CACnD,IAAI,iBAAiB,IAAI,GAAG;EAC1B,MAAM,EAAE,WAAW,YAAY,QAAQ,GAAG,QAAQ;EAClD,IAAI,OAAO,WAAW,KAAK,OAAO,EAAE,CAAE,SAAS,KAAK,KAAK,MAAM,OAAO;GAAE,IAAI,GAAG;GAAI,OAAO;GAAO,UAAU,GAAG;EAAS;EACvH,IAAI,OAAO,MAAM;EACjB,KAAK,SAAS,UAAU;GACtB,IAAI,aAAa,KAAK,GAAG,QAAQ,MAAM;EACzC,CAAC;EACD,OAAO;GAAE,IAAI,GAAG;GAAI,OAAO;IAAE,MAAM;IAAM,IAAI,MAAM,KAAK,WAAW;GAAE;GAAG,UAAU,GAAG;EAAS;CAChG;CACA,IAAI,CAAC,KAAK,aAAa,OAAO;EAAE,IAAI,GAAG;EAAI,OAAO;EAAO,UAAU,GAAG;CAAS;CAG/E,IAAI,SAAS;CACb,KAAK,IAAI,QAAQ,KAAK,OAAO,QAAQ,KAAK,CAAC,QAAQ,SAAS,SAAS,CAAC,YAAY,YAAY,CAAC,CAAC,SAAS,KAAK,KAAK,KAAK,CAAC,CAAC,KAAK,IAAI;CACnI,MAAM,WAAW,SAAS,oBAAoB,GAAG,QAAQ,IAAI,GAAG;CAChE,MAAM,EAAE,WAAW,YAAY,QAAQ,QAAQ;CAC/C,MAAM,SAAS,OAAO,WAAW,IAAI,OAAO,aAAa,OAAO,EAAG,IAAI;CACvE,MAAM,WAAW,QAAQ,SAAS,KAAK,QAAQ,OAAO,KAAK,KAAK,KAAK,CAAC,CAAC,OAAM,SAAQ,SAAA,QAA0B,OAAO,MAAM,UAAU,KAAK,MAAM,KAAK;CAEtJ,MAAM,QADS,KAAK,QAAQ,OAAO,MAAM,YAAY,QAAQ,KAAK,SAAS,eACpD;EAAE,MAAM,MAAM;EAAG,IAAI,MAAM,IAAI,KAAK,QAAQ;CAAK,IAAI;CAC5E,OAAO;EAAE,IAAI,GAAG;EAAI;EAAO;CAAS;AACtC"}
|
|
@@ -10,8 +10,9 @@ import { Node as Node$1 } from "@tiptap/pm/model";
|
|
|
10
10
|
* <title> | <description> | Tags: #a` (empty parts left out), its body blocks two spaces further in, and a closing
|
|
11
11
|
* `:::` line without ID at that indent. Any other non-list block with block content (a host block such as a callout) is
|
|
12
12
|
* a header line `[<id>] <first line of its Markdown form without content>` (`!!! info`) with its body blocks two spaces
|
|
13
|
-
* further in, each with its own ID and no closing line; if that header is empty, only the body blocks appear.
|
|
14
|
-
*
|
|
13
|
+
* further in, each with its own ID and no closing line; if that header is empty, only the body blocks appear. A table
|
|
14
|
+
* is one block: its pipe-table Markdown, the first row on the line with the table's ID, the others below it (as a code
|
|
15
|
+
* block keeps its lines), never a line per cell. Cut at whole lines to fit `CHAT_LIMITS.document`.
|
|
15
16
|
*/
|
|
16
17
|
export declare function documentChatText(editor: Editor): string;
|
|
17
18
|
//#endregion
|
|
@@ -16,8 +16,9 @@ import { Fragment } from "@tiptap/pm/model";
|
|
|
16
16
|
* <title> | <description> | Tags: #a` (empty parts left out), its body blocks two spaces further in, and a closing
|
|
17
17
|
* `:::` line without ID at that indent. Any other non-list block with block content (a host block such as a callout) is
|
|
18
18
|
* a header line `[<id>] <first line of its Markdown form without content>` (`!!! info`) with its body blocks two spaces
|
|
19
|
-
* further in, each with its own ID and no closing line; if that header is empty, only the body blocks appear.
|
|
20
|
-
*
|
|
19
|
+
* further in, each with its own ID and no closing line; if that header is empty, only the body blocks appear. A table
|
|
20
|
+
* is one block: its pipe-table Markdown, the first row on the line with the table's ID, the others below it (as a code
|
|
21
|
+
* block keeps its lines), never a line per cell. Cut at whole lines to fit `CHAT_LIMITS.document`.
|
|
21
22
|
*/
|
|
22
23
|
function documentChatText(editor) {
|
|
23
24
|
const lines = [];
|
|
@@ -38,7 +39,8 @@ function documentChatText(editor) {
|
|
|
38
39
|
const emit = (node, prefix, id) => {
|
|
39
40
|
const markdown = markdownOf(node);
|
|
40
41
|
if (!markdown) return;
|
|
41
|
-
|
|
42
|
+
const keepsLines = node.type.name === "codeBlock" || node.type.name === "table";
|
|
43
|
+
push(blockLine(id, prefix, keepsLines ? markdown : singleBlockLine(markdown)));
|
|
42
44
|
};
|
|
43
45
|
const container = (node, prefix, id) => {
|
|
44
46
|
const chrome = [];
|
|
@@ -59,7 +61,7 @@ function documentChatText(editor) {
|
|
|
59
61
|
node.forEach((child, _offset, index) => {
|
|
60
62
|
if (full || isChromeNode(child)) return;
|
|
61
63
|
const id = blockIdOf(child) ?? parentId;
|
|
62
|
-
if (child.isTextblock || child.isLeaf) emit(child, current, id);
|
|
64
|
+
if (child.isTextblock || child.isLeaf || child.type.name === "table") emit(child, current, id);
|
|
63
65
|
else if (isBlockContainer(child)) container(child, current, id);
|
|
64
66
|
else if (child.type.name === "blockquote") visit(child, quoteLinePrefix(current), id);
|
|
65
67
|
else if (child.type.name === "bulletList" || child.type.name === "orderedList") {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document-chat.js","names":[],"sources":["../../../src/features/ai/document-chat.ts"],"sourcesContent":["import type { Editor, JSONContent } from '@tiptap/core'\nimport { Fragment, type Node as PMNode } from '@tiptap/pm/model'\nimport { isWrapperBlock, metaOf, parseBlocks, typeIdOf } from '../../core'\nimport { blockIdOf, findBlockById, isBlockContainer, isChromeNode } from '../../core/schema'\nimport { blockLine, CHAT_LIMITS, CITATION_HREF_PREFIX, citationBlockId, compositeChromePart, compositeLineClose, compositeLineHeader, continuedLinePrefix, listLinePrefix, nestedLinePrefix, quoteLinePrefix, singleBlockLine, TRUNCATED_LINE } from './protocol'\n\n/**\n * The document as sent to the chat, in the line format of `protocol/block-lines.ts` (which also reads and describes\n * it): one line per text block, `[<block id>] <Markdown>` (e.g. `[…] **Anna:** Text`), with list markers and quote\n * prefixes after the ID. Nested blocks carry their own IDs, so quotes from list items or\n * block quotes link to exactly that paragraph. A block container below the document (a composite: the schema flags it\n * `editorBlockContainer`, its title/description/tags rows `editorChrome`) is a header line `[<id>] ::: <kind> {meta} |\n * <title> | <description> | Tags: #a` (empty parts left out), its body blocks two spaces further in, and a closing\n * `:::` line without ID at that indent. Any other non-list block with block content (a host block such as a callout) is\n * a header line `[<id>] <first line of its Markdown form without content>` (`!!! info`) with its body blocks two spaces\n * further in, each with its own ID and no closing line; if that header is empty, only the body blocks appear.
|
|
1
|
+
{"version":3,"file":"document-chat.js","names":[],"sources":["../../../src/features/ai/document-chat.ts"],"sourcesContent":["import type { Editor, JSONContent } from '@tiptap/core'\nimport { Fragment, type Node as PMNode } from '@tiptap/pm/model'\nimport { isWrapperBlock, metaOf, parseBlocks, typeIdOf } from '../../core'\nimport { blockIdOf, findBlockById, isBlockContainer, isChromeNode } from '../../core/schema'\nimport { blockLine, CHAT_LIMITS, CITATION_HREF_PREFIX, citationBlockId, compositeChromePart, compositeLineClose, compositeLineHeader, continuedLinePrefix, listLinePrefix, nestedLinePrefix, quoteLinePrefix, singleBlockLine, TRUNCATED_LINE } from './protocol'\n\n/**\n * The document as sent to the chat, in the line format of `protocol/block-lines.ts` (which also reads and describes\n * it): one line per text block, `[<block id>] <Markdown>` (e.g. `[…] **Anna:** Text`), with list markers and quote\n * prefixes after the ID. Nested blocks carry their own IDs, so quotes from list items or\n * block quotes link to exactly that paragraph. A block container below the document (a composite: the schema flags it\n * `editorBlockContainer`, its title/description/tags rows `editorChrome`) is a header line `[<id>] ::: <kind> {meta} |\n * <title> | <description> | Tags: #a` (empty parts left out), its body blocks two spaces further in, and a closing\n * `:::` line without ID at that indent. Any other non-list block with block content (a host block such as a callout) is\n * a header line `[<id>] <first line of its Markdown form without content>` (`!!! info`) with its body blocks two spaces\n * further in, each with its own ID and no closing line; if that header is empty, only the body blocks appear. A table\n * is one block: its pipe-table Markdown, the first row on the line with the table's ID, the others below it (as a code\n * block keeps its lines), never a line per cell. Cut at whole lines to fit `CHAT_LIMITS.document`.\n */\nexport function documentChatText(editor: Editor): string {\n const lines: string[] = []\n let size = 0\n let full = false\n\n const push = (line: string) => {\n if (full || size + line.length + 1 > CHAT_LIMITS.document - TRUNCATED_LINE.length - 1) {\n full = true\n return\n }\n lines.push(line)\n size += line.length + 1\n }\n const markdownOf = (node: PMNode) => editor.markdown!.serialize({ type: 'doc', content: [node.toJSON()] }).trim()\n\n const emit = (node: PMNode, prefix: string, id: string | null) => {\n const markdown = markdownOf(node)\n if (!markdown) return\n // Hard breaks and wrapped lines would start new \"blocks\"; code keeps its lines, and so does a table: its pipe rows\n // are one block under the table's ID, not one line per cell.\n const keepsLines = node.type.name === 'codeBlock' || node.type.name === 'table'\n push(blockLine(id, prefix, keepsLines ? markdown : singleBlockLine(markdown)))\n }\n\n const container = (node: PMNode, prefix: string, id: string | null) => {\n const chrome: string[] = []\n // The chrome's own Markdown (`## title`, `> description`, `Tags: #a`) without the block markers.\n node.forEach((child) => {\n if (isChromeNode(child)) chrome.push(compositeChromePart(markdownOf(child)))\n })\n push(blockLine(id, prefix, compositeLineHeader(typeIdOf(node), metaOf(node), chrome)))\n visit(node, nestedLinePrefix(prefix), id)\n push(compositeLineClose(prefix))\n }\n\n // A host block with block content (a callout): its own Markdown form without content as the header line, the body\n // blocks two spaces further in, each with its own ID, no closing line (the form has none).\n const wrapper = (node: PMNode, prefix: string, id: string | null) => {\n const header = markdownOf(node.copy(Fragment.empty)).split('\\n')[0] ?? ''\n if (header) push(blockLine(id, prefix, header))\n visit(node, header ? nestedLinePrefix(prefix) : prefix, id)\n }\n\n const visit = (node: PMNode, prefix: string, parentId: string | null) => {\n let current = prefix\n node.forEach((child, _offset, index) => {\n if (full || isChromeNode(child)) return\n const id = blockIdOf(child) ?? parentId\n if (child.isTextblock || child.isLeaf || child.type.name === 'table') emit(child, current, id)\n else if (isBlockContainer(child)) container(child, current, id)\n else if (child.type.name === 'blockquote') visit(child, quoteLinePrefix(current), id)\n else if (child.type.name === 'bulletList' || child.type.name === 'orderedList') {\n const start = Number(child.attrs.start ?? 1)\n child.forEach((item, _o, i) => visit(item, listLinePrefix(current, child.type.name === 'orderedList', start + i), id))\n }\n else if (isWrapperBlock(child)) wrapper(child, current, id)\n else visit(child, current, id)\n // Later blocks of the same list item continue under its marker instead of repeating it.\n if (index === 0) current = continuedLinePrefix(prefix)\n })\n }\n\n visit(editor.state.doc, '', null)\n if (full) lines.push(TRUNCATED_LINE)\n return lines.join('\\n')\n}\n\n/**\n * Chat answer Markdown parsed for display with the pipeline of `editor` (speaker lines become segments where a feature\n * contributes them): quote links whose block exists in `doc` point to `#block-<id>`, and quote links to unknown blocks\n * lose the link (plain text).\n */\nexport function documentChatAnswerContent(editor: Editor, markdown: string, doc: PMNode | undefined): JSONContent {\n const resolve = (node: JSONContent): JSONContent => {\n const marks = node.marks?.flatMap((mark) => {\n if (mark.type !== 'link') return [mark]\n const id = citationBlockId(mark.attrs?.href)\n if (id === null) return [mark]\n return doc && findBlockById(doc, id) ? [{ ...mark, attrs: { ...mark.attrs, href: `${CITATION_HREF_PREFIX}${id}` } }] : []\n })\n return { ...node, ...(marks && { marks }), ...(node.content && { content: node.content.map(resolve) }) }\n }\n return resolve({ type: 'doc', content: parseBlocks(editor, markdown).blocks })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,iBAAiB,QAAwB;CACvD,MAAM,QAAkB,CAAC;CACzB,IAAI,OAAO;CACX,IAAI,OAAO;CAEX,MAAM,QAAQ,SAAiB;EAC7B,IAAI,QAAQ,OAAO,KAAK,SAAS,IAAI,YAAY,WAAA,KAAmC,GAAG;GACrF,OAAO;GACP;EACF;EACA,MAAM,KAAK,IAAI;EACf,QAAQ,KAAK,SAAS;CACxB;CACA,MAAM,cAAc,SAAiB,OAAO,SAAU,UAAU;EAAE,MAAM;EAAO,SAAS,CAAC,KAAK,OAAO,CAAC;CAAE,CAAC,CAAC,CAAC,KAAK;CAEhH,MAAM,QAAQ,MAAc,QAAgB,OAAsB;EAChE,MAAM,WAAW,WAAW,IAAI;EAChC,IAAI,CAAC,UAAU;EAGf,MAAM,aAAa,KAAK,KAAK,SAAS,eAAe,KAAK,KAAK,SAAS;EACxE,KAAK,UAAU,IAAI,QAAQ,aAAa,WAAW,gBAAgB,QAAQ,CAAC,CAAC;CAC/E;CAEA,MAAM,aAAa,MAAc,QAAgB,OAAsB;EACrE,MAAM,SAAmB,CAAC;EAE1B,KAAK,SAAS,UAAU;GACtB,IAAI,aAAa,KAAK,GAAG,OAAO,KAAK,oBAAoB,WAAW,KAAK,CAAC,CAAC;EAC7E,CAAC;EACD,KAAK,UAAU,IAAI,QAAQ,oBAAoB,SAAS,IAAI,GAAG,OAAO,IAAI,GAAG,MAAM,CAAC,CAAC;EACrF,MAAM,MAAM,iBAAiB,MAAM,GAAG,EAAE;EACxC,KAAK,mBAAmB,MAAM,CAAC;CACjC;CAIA,MAAM,WAAW,MAAc,QAAgB,OAAsB;EACnE,MAAM,SAAS,WAAW,KAAK,KAAK,SAAS,KAAK,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM;EACvE,IAAI,QAAQ,KAAK,UAAU,IAAI,QAAQ,MAAM,CAAC;EAC9C,MAAM,MAAM,SAAS,iBAAiB,MAAM,IAAI,QAAQ,EAAE;CAC5D;CAEA,MAAM,SAAS,MAAc,QAAgB,aAA4B;EACvE,IAAI,UAAU;EACd,KAAK,SAAS,OAAO,SAAS,UAAU;GACtC,IAAI,QAAQ,aAAa,KAAK,GAAG;GACjC,MAAM,KAAK,UAAU,KAAK,KAAK;GAC/B,IAAI,MAAM,eAAe,MAAM,UAAU,MAAM,KAAK,SAAS,SAAS,KAAK,OAAO,SAAS,EAAE;QACxF,IAAI,iBAAiB,KAAK,GAAG,UAAU,OAAO,SAAS,EAAE;QACzD,IAAI,MAAM,KAAK,SAAS,cAAc,MAAM,OAAO,gBAAgB,OAAO,GAAG,EAAE;QAC/E,IAAI,MAAM,KAAK,SAAS,gBAAgB,MAAM,KAAK,SAAS,eAAe;IAC9E,MAAM,QAAQ,OAAO,MAAM,MAAM,SAAS,CAAC;IAC3C,MAAM,SAAS,MAAM,IAAI,MAAM,MAAM,MAAM,eAAe,SAAS,MAAM,KAAK,SAAS,eAAe,QAAQ,CAAC,GAAG,EAAE,CAAC;GACvH,OACK,IAAI,eAAe,KAAK,GAAG,QAAQ,OAAO,SAAS,EAAE;QACrD,MAAM,OAAO,SAAS,EAAE;GAE7B,IAAI,UAAU,GAAG,UAAU,oBAAoB,MAAM;EACvD,CAAC;CACH;CAEA,MAAM,OAAO,MAAM,KAAK,IAAI,IAAI;CAChC,IAAI,MAAM,MAAM,KAAK,cAAc;CACnC,OAAO,MAAM,KAAK,IAAI;AACxB;;;;;;AAOA,SAAgB,0BAA0B,QAAgB,UAAkB,KAAsC;CAChH,MAAM,WAAW,SAAmC;EAClD,MAAM,QAAQ,KAAK,OAAO,SAAS,SAAS;GAC1C,IAAI,KAAK,SAAS,QAAQ,OAAO,CAAC,IAAI;GACtC,MAAM,KAAK,gBAAgB,KAAK,OAAO,IAAI;GAC3C,IAAI,OAAO,MAAM,OAAO,CAAC,IAAI;GAC7B,OAAO,OAAO,cAAc,KAAK,EAAE,IAAI,CAAC;IAAE,GAAG;IAAM,OAAO;KAAE,GAAG,KAAK;KAAO,MAAM,GAAG,uBAAuB;IAAK;GAAE,CAAC,IAAI,CAAC;EAC1H,CAAC;EACD,OAAO;GAAE,GAAG;GAAM,GAAI,SAAS,EAAE,MAAM;GAAI,GAAI,KAAK,WAAW,EAAE,SAAS,KAAK,QAAQ,IAAI,OAAO,EAAE;EAAG;CACzG;CACA,OAAO,QAAQ;EAAE,MAAM;EAAO,SAAS,YAAY,QAAQ,QAAQ,CAAC,CAAC;CAAO,CAAC;AAC/E"}
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* A line is `[<block id>] <prefix><text>`. The prefix carries list items (`- `, `1. `), quotes (`> `) and the nesting
|
|
9
9
|
* of a block inside a Typed block or host block (two spaces per level). A Typed block is a header line, its body lines and a
|
|
10
10
|
* closing line `:::` without ID. Code blocks keep their own lines: the first one carries the ID, the others start at
|
|
11
|
-
* the left edge, so their content can look like anything.
|
|
11
|
+
* the left edge, so their content can look like anything. A table does the same with its pipe rows (one block, no
|
|
12
|
+
* line per cell).
|
|
12
13
|
*/
|
|
13
14
|
/** The line shown instead of the rest of a document that did not fit. */
|
|
14
15
|
export declare const TRUNCATED_LINE = "[…] (Rest des Dokuments gekürzt)";
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* A line is `[<block id>] <prefix><text>`. The prefix carries list items (`- `, `1. `), quotes (`> `) and the nesting
|
|
9
9
|
* of a block inside a Typed block or host block (two spaces per level). A Typed block is a header line, its body lines and a
|
|
10
10
|
* closing line `:::` without ID. Code blocks keep their own lines: the first one carries the ID, the others start at
|
|
11
|
-
* the left edge, so their content can look like anything.
|
|
11
|
+
* the left edge, so their content can look like anything. A table does the same with its pipe rows (one block, no
|
|
12
|
+
* line per cell).
|
|
12
13
|
*/
|
|
13
14
|
/** One level of nesting inside a Typed block or host block. */
|
|
14
15
|
const INDENT = " ";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"block-lines.js","names":[],"sources":["../../../../src/features/ai/protocol/block-lines.ts"],"sourcesContent":["// i18n-ignore-file: the document lines and the prompt texts describing them are model-facing (German; the model's language is not the editor's)\n/**\n * The line format of the document as the chat model sees it, in one place: how the lines are written (the primitives\n * `documentChatText` formats its lines with), how they are read back (the block IDs of a text and which blocks lie\n * inside which, ignoring fenced code), how an echoed line prefix is removed from the model's Markdown, and how the\n * prompt describes all of that. Framework-free and free of ProseMirror: the browser writes the text, the server reads it.\n *\n * A line is `[<block id>] <prefix><text>`. The prefix carries list items (`- `, `1. `), quotes (`> `) and the nesting\n * of a block inside a Typed block or host block (two spaces per level). A Typed block is a header line, its body lines and a\n * closing line `:::` without ID. Code blocks keep their own lines: the first one carries the ID, the others start at\n * the left edge, so their content can look like anything.\n */\n\n// ---------- Writing ----------------------------------------------------------\n\n/** One level of nesting inside a Typed block or host block. */\nconst INDENT = ' '\n\n/** The line shown instead of the rest of a document that did not fit. */\nexport const TRUNCATED_LINE = '[…] (Rest des Dokuments gekürzt)'\n\n/** A line of block `id` (none: an ID-less line), indented by `prefix`, holding `text`. */\nexport function blockLine(id: string | null, prefix: string, text: string): string {\n return `${id ? `[${id}] ` : ''}${prefix}${text}`\n}\n\n/** `markdown` on one line: hard breaks and wrapped lines would start new \"blocks\". */\nexport function singleBlockLine(markdown: string): string {\n return markdown.replace(/\\s*\\n\\s*/g, ' ')\n}\n\n/** The prefix of blocks inside the block whose lines carry `prefix` (the body of a Typed block or host block). */\nexport function nestedLinePrefix(prefix: string): string {\n return `${prefix}${INDENT}`\n}\n\n/** The prefix of the blocks of a quote. */\nexport function quoteLinePrefix(prefix: string): string {\n return `${prefix}> `\n}\n\n/** The prefix of the blocks of list item `number` (1-based; ignored in a bullet list). */\nexport function listLinePrefix(prefix: string, ordered: boolean, number: number): string {\n return `${prefix}${ordered ? `${number}. ` : '- '}`\n}\n\n/** The prefix of the blocks of a list item after its first: they continue under its marker instead of repeating it. */\nexport function continuedLinePrefix(prefix: string): string {\n return prefix.replace(/(?:- |\\d+\\. )$/, marker => ' '.repeat(marker.length))\n}\n\n/** One chrome row of a Typed block (`## title`, `> description`, `Tags: #a`) without its block marker, on one line. */\nexport function compositeChromePart(markdown: string): string {\n return singleBlockLine(markdown.replace(/^(?:#+|>) ?/, ''))\n}\n\n/**\n * The header text of a Typed block: `::: <kind> {meta}` (the meta as JSON only when it has entries), then every non-empty\n * chrome part (title, description, tags) separated by ` | `.\n */\nexport function compositeLineHeader(kind: string, meta: unknown, chrome: readonly string[]): string {\n const fields = meta && typeof meta === 'object' && Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : ''\n return [`::: ${kind}${fields}`, ...chrome.filter(Boolean)].join(' | ')\n}\n\n/** The closing line of a Typed block whose header line is indented by `prefix`: no ID. */\nexport function compositeLineClose(prefix: string): string {\n return `${nestedLinePrefix(prefix)}:::`\n}\n\n// ---------- Reading ----------------------------------------------------------\n\n/** The block IDs of a document text and which blocks lie inside which. */\nexport interface BlockLines {\n /** Every block ID that starts a line (lines inside fenced code do not count). */\n ids: ReadonlySet<string>\n /** The IDs of all blocks nested below block `id`: the lines indented under its header line (empty for a block without nested blocks). */\n below: (id: string) => ReadonlySet<string>\n}\n\nconst ID_PREFIX = /^\\[([^\\]\\s]+)\\] /\n/** An opening code fence after optional list/quote markers and indentation: three or more backticks, no backtick in the info string. */\nconst FENCE_OPEN = /^(?:[ \\t]|>|[-*+][ \\t]|\\d+[.)][ \\t])*(`{3,})[^`]*$/\nconst FENCE_CLOSE = /^[ \\t]*(`{3,})[ \\t]*$/\n/** Indentation and list/quote markers at the start of a line's content. */\nconst LINE_PREFIX = /^(?:[ \\t]|>|[-*+][ \\t]|\\d+[.)][ \\t])*/\n\n/**\n * The fence length after `line` when a fence of `fence` backticks was open before it (0: none). A line of at least as\n * many backticks closes the fence; `opens` says whether a line outside a fence may open one.\n */\nfunction fenceAfter(fence: number, line: string, opens: boolean): number {\n if (fence) {\n const closing = FENCE_CLOSE.exec(line)\n return closing && closing[1]!.length >= fence ? 0 : fence\n }\n const opening = opens ? FENCE_OPEN.exec(line) : null\n return opening ? opening[1]!.length : 0\n}\n\nconst NOTHING: ReadonlySet<string> = new Set()\n\n/**\n * Reads the lines of `text` (as `documentChatText` writes them). Lines inside a fenced code block are code, never\n * block lines, so a code line like `[ghost] text` does not become an ID; only a line that carries an ID opens a fence\n * (a stray line of backticks inside the code cannot swallow what follows).\n *\n * Nesting follows the indentation: the blocks on the lines under a header line that are indented further belong to its\n * block. Lines of list items (their marker, and what continues under it) never head a block of their own: a paragraph\n * and its nested list are siblings, nothing lies \"inside\" the paragraph. A block without lines of its own (a list, a\n * quote) has no nested blocks; a block's own ID on a nested line (a child without an ID of its own) is not \"inside\".\n */\nexport function readBlockLines(text: string): BlockLines {\n const ids = new Set<string>()\n const nested = new Map<string, Set<string>>()\n // The blocks whose lines enclose the current one, outermost first; `head`: the block can have nested blocks.\n const open: { id: string, indent: number, head: boolean }[] = []\n let fence = 0\n\n for (const line of text.split('\\n')) {\n const id = ID_PREFIX.exec(line)?.[1]\n if (fence) {\n fence = fenceAfter(fence, line, false)\n continue\n }\n // The truncation notice reads like a line with the ID `…`.\n if (!id || line === TRUNCATED_LINE) continue\n const content = line.slice(id.length + 3)\n fence = fenceAfter(0, content, true)\n ids.add(id)\n\n const indent = /^[ \\t]*/.exec(content)![0].length\n const listItem = /[-*+\\d]/.test(LINE_PREFIX.exec(content)![0])\n while (open.length && open.at(-1)!.indent >= indent) open.pop()\n for (const outer of open) {\n if (!outer.head || outer.id === id) continue\n const inside = nested.get(outer.id)\n if (inside) inside.add(id)\n else nested.set(outer.id, new Set([id]))\n }\n open.push({ id, indent, head: !listItem && (open.at(-1)?.head ?? true) })\n }\n return { ids, below: id => nested.get(id) ?? NOTHING }\n}\n\n/**\n * Removes what a model echoes from the document lines into its Markdown: a `[<id>] ` prefix of a known block at the\n * start of a line (not inside a fenced code block of its own Markdown, which keeps lines that look like one), and the\n * indentation all non-blank lines share (lines echoed from a Typed block's body carry its indent, which would turn deeper\n * nested lines into indented code).\n */\nexport function stripBlockLineEcho(markdown: string, known: (id: string) => boolean): string {\n let fence = 0\n const stripped = markdown.split('\\n').map((line) => {\n if (fence) {\n fence = fenceAfter(fence, line, false)\n return line\n }\n const bare = line.replace(/^\\[([^\\]\\s]+)\\] ?/, (prefix, id: string) => known(id) ? '' : prefix)\n fence = fenceAfter(0, bare, true)\n return bare\n })\n const indents = stripped.filter(line => line.trim()).map(line => /^[ \\t]*/.exec(line)![0].length)\n const common = indents.length ? Math.min(...indents) : 0\n return (common ? stripped.map(line => line.replace(new RegExp(`^[ \\\\t]{0,${common}}`), '')) : stripped).join('\\n')\n}\n\n/**\n * Removes the list and quote markers (`- `, `1. `, `> `) a model echoes from the lines of list items and quotes at the\n * start of each line of its Markdown.\n */\nexport function stripListMarkerEcho(markdown: string): string {\n return markdown.replace(/^(?:[ \\t]*(?:[-*+]|\\d+[.)])[ \\t]+|[ \\t]*>[ \\t]?)+/gm, '')\n}\n\n// ---------- Prompt -----------------------------------------------------------\n\n/** The block ID of the prompt's example line (also the target of its citation example). */\nexport const BLOCK_LINE_EXAMPLE_ID = '0f3c9a52-7d1e-4b8a-9c2f-5e6d7a8b9c0d'\n\n/** The text of the prompt's example line: a speaker line. */\nexport const BLOCK_LINE_EXAMPLE_TEXT = '**Anna:** Wir starten im Mai.'\n\n/** The example line of the prompt, written by `blockLine` like every line of the document. */\nexport const BLOCK_LINE_EXAMPLE = blockLine(BLOCK_LINE_EXAMPLE_ID, '', BLOCK_LINE_EXAMPLE_TEXT)\n\n/** The label of the document in the user message. */\nexport const BLOCK_LINES_LABEL = 'Dokument (aktueller Stand, eine Zeile pro Block, beginnend mit der Block-ID)'\n\n/** What the first lines of the system prompt say about the document's lines. */\nexport const BLOCK_LINE_INTRO = `Jede Zeile des Dokuments beginnt mit der ID ihres Blocks in eckigen Klammern, z. B. \\`${BLOCK_LINE_EXAMPLE}\\`. Redebeiträge beginnen nach der ID mit dem fett gesetzten Namen der Sprecherin bzw. des Sprechers und einem Doppelpunkt.`\n\n/** The list and quote blocks with the characters their lines carry. */\nexport const BLOCK_LINE_MARKERS = `Aufzählung („${listLinePrefix('', false, 1)}“), nummerierte Liste („${listLinePrefix('', true, 1)}“), Zitat („${quoteLinePrefix('')}“)`\n\n/** What the prompt says about lists and quotes in the document. */\nexport const BLOCK_LINE_LIST_TEXT = 'Listenpunkte und Zitate stehen nach der ID mit ihrem Zeichen; jeder Absatz darin hat eine eigene ID.'\n\n/** What the prompt says about the lines of a host block with block content (a callout). */\nexport const BLOCK_LINE_HOST_TEXT = `Im Dokument beginnt ein eigener Block mit Inhaltsblöcken mit der Zeile „${blockLine('ID', '', '<erste Zeile seiner Markdown-Form>')}“; seine Inhaltsblöcke folgen als eigene Zeilen mit IDs, um zwei Leerzeichen eingerückt.`\n\n/** What the prompt says about the lines of a Typed block. */\nexport const BLOCK_LINE_COMPOSITE_TEXT = `Im Dokument beginnt ein typisierter Block mit der Kopfzeile „${blockLine('ID', '', compositeLineHeader('Blocktyp', { feld: 'wert' }, ['Titel', 'Beschreibung', 'Tags: #a #b']))}“ (die Felder als JSON nur, wenn welche gesetzt sind; leere Teile fehlen samt ihrem „|“). Seine Inhaltsblöcke folgen als eigene Zeilen mit ihren IDs, um zwei Leerzeichen eingerückt; eine Zeile „${compositeLineClose('')}“ ohne ID schließt ihn. Das Feld „bib“ ist eine Literaturangabe als CSL-JSON, „derivation“ beschreibt, woraus der Block erzeugt wurde. Ein Verweis auf einen Block des Dokuments steht im Text als „[@<Block-ID>]“, mit Fundstelle als „[@<Block-ID>, S. 12]“.`\n\n/** What the prompt says about the Markdown of an operation: no echo of the line prefix (`stripBlockLineEcho` removes it when it comes anyway). */\nexport const BLOCK_LINE_ECHO_TEXT = '„markdown“ enthält nur den neuen Inhalt, ohne Block-ID und ohne das Listen- oder Zitatzeichen der Zeile.'\n\n/** What the retry message says about IDs. */\nexport const BLOCK_LINE_IDS_HINT = 'Verwende nur IDs, die im Dokument in eckigen Klammern stehen; erfinde keine IDs.'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"block-lines.js","names":[],"sources":["../../../../src/features/ai/protocol/block-lines.ts"],"sourcesContent":["// i18n-ignore-file: the document lines and the prompt texts describing them are model-facing (German; the model's language is not the editor's)\n/**\n * The line format of the document as the chat model sees it, in one place: how the lines are written (the primitives\n * `documentChatText` formats its lines with), how they are read back (the block IDs of a text and which blocks lie\n * inside which, ignoring fenced code), how an echoed line prefix is removed from the model's Markdown, and how the\n * prompt describes all of that. Framework-free and free of ProseMirror: the browser writes the text, the server reads it.\n *\n * A line is `[<block id>] <prefix><text>`. The prefix carries list items (`- `, `1. `), quotes (`> `) and the nesting\n * of a block inside a Typed block or host block (two spaces per level). A Typed block is a header line, its body lines and a\n * closing line `:::` without ID. Code blocks keep their own lines: the first one carries the ID, the others start at\n * the left edge, so their content can look like anything. A table does the same with its pipe rows (one block, no\n * line per cell).\n */\n\n// ---------- Writing ----------------------------------------------------------\n\n/** One level of nesting inside a Typed block or host block. */\nconst INDENT = ' '\n\n/** The line shown instead of the rest of a document that did not fit. */\nexport const TRUNCATED_LINE = '[…] (Rest des Dokuments gekürzt)'\n\n/** A line of block `id` (none: an ID-less line), indented by `prefix`, holding `text`. */\nexport function blockLine(id: string | null, prefix: string, text: string): string {\n return `${id ? `[${id}] ` : ''}${prefix}${text}`\n}\n\n/** `markdown` on one line: hard breaks and wrapped lines would start new \"blocks\". */\nexport function singleBlockLine(markdown: string): string {\n return markdown.replace(/\\s*\\n\\s*/g, ' ')\n}\n\n/** The prefix of blocks inside the block whose lines carry `prefix` (the body of a Typed block or host block). */\nexport function nestedLinePrefix(prefix: string): string {\n return `${prefix}${INDENT}`\n}\n\n/** The prefix of the blocks of a quote. */\nexport function quoteLinePrefix(prefix: string): string {\n return `${prefix}> `\n}\n\n/** The prefix of the blocks of list item `number` (1-based; ignored in a bullet list). */\nexport function listLinePrefix(prefix: string, ordered: boolean, number: number): string {\n return `${prefix}${ordered ? `${number}. ` : '- '}`\n}\n\n/** The prefix of the blocks of a list item after its first: they continue under its marker instead of repeating it. */\nexport function continuedLinePrefix(prefix: string): string {\n return prefix.replace(/(?:- |\\d+\\. )$/, marker => ' '.repeat(marker.length))\n}\n\n/** One chrome row of a Typed block (`## title`, `> description`, `Tags: #a`) without its block marker, on one line. */\nexport function compositeChromePart(markdown: string): string {\n return singleBlockLine(markdown.replace(/^(?:#+|>) ?/, ''))\n}\n\n/**\n * The header text of a Typed block: `::: <kind> {meta}` (the meta as JSON only when it has entries), then every non-empty\n * chrome part (title, description, tags) separated by ` | `.\n */\nexport function compositeLineHeader(kind: string, meta: unknown, chrome: readonly string[]): string {\n const fields = meta && typeof meta === 'object' && Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : ''\n return [`::: ${kind}${fields}`, ...chrome.filter(Boolean)].join(' | ')\n}\n\n/** The closing line of a Typed block whose header line is indented by `prefix`: no ID. */\nexport function compositeLineClose(prefix: string): string {\n return `${nestedLinePrefix(prefix)}:::`\n}\n\n// ---------- Reading ----------------------------------------------------------\n\n/** The block IDs of a document text and which blocks lie inside which. */\nexport interface BlockLines {\n /** Every block ID that starts a line (lines inside fenced code do not count). */\n ids: ReadonlySet<string>\n /** The IDs of all blocks nested below block `id`: the lines indented under its header line (empty for a block without nested blocks). */\n below: (id: string) => ReadonlySet<string>\n}\n\nconst ID_PREFIX = /^\\[([^\\]\\s]+)\\] /\n/** An opening code fence after optional list/quote markers and indentation: three or more backticks, no backtick in the info string. */\nconst FENCE_OPEN = /^(?:[ \\t]|>|[-*+][ \\t]|\\d+[.)][ \\t])*(`{3,})[^`]*$/\nconst FENCE_CLOSE = /^[ \\t]*(`{3,})[ \\t]*$/\n/** Indentation and list/quote markers at the start of a line's content. */\nconst LINE_PREFIX = /^(?:[ \\t]|>|[-*+][ \\t]|\\d+[.)][ \\t])*/\n\n/**\n * The fence length after `line` when a fence of `fence` backticks was open before it (0: none). A line of at least as\n * many backticks closes the fence; `opens` says whether a line outside a fence may open one.\n */\nfunction fenceAfter(fence: number, line: string, opens: boolean): number {\n if (fence) {\n const closing = FENCE_CLOSE.exec(line)\n return closing && closing[1]!.length >= fence ? 0 : fence\n }\n const opening = opens ? FENCE_OPEN.exec(line) : null\n return opening ? opening[1]!.length : 0\n}\n\nconst NOTHING: ReadonlySet<string> = new Set()\n\n/**\n * Reads the lines of `text` (as `documentChatText` writes them). Lines inside a fenced code block are code, never\n * block lines, so a code line like `[ghost] text` does not become an ID; only a line that carries an ID opens a fence\n * (a stray line of backticks inside the code cannot swallow what follows).\n *\n * Nesting follows the indentation: the blocks on the lines under a header line that are indented further belong to its\n * block. Lines of list items (their marker, and what continues under it) never head a block of their own: a paragraph\n * and its nested list are siblings, nothing lies \"inside\" the paragraph. A block without lines of its own (a list, a\n * quote) has no nested blocks; a block's own ID on a nested line (a child without an ID of its own) is not \"inside\".\n */\nexport function readBlockLines(text: string): BlockLines {\n const ids = new Set<string>()\n const nested = new Map<string, Set<string>>()\n // The blocks whose lines enclose the current one, outermost first; `head`: the block can have nested blocks.\n const open: { id: string, indent: number, head: boolean }[] = []\n let fence = 0\n\n for (const line of text.split('\\n')) {\n const id = ID_PREFIX.exec(line)?.[1]\n if (fence) {\n fence = fenceAfter(fence, line, false)\n continue\n }\n // The truncation notice reads like a line with the ID `…`.\n if (!id || line === TRUNCATED_LINE) continue\n const content = line.slice(id.length + 3)\n fence = fenceAfter(0, content, true)\n ids.add(id)\n\n const indent = /^[ \\t]*/.exec(content)![0].length\n const listItem = /[-*+\\d]/.test(LINE_PREFIX.exec(content)![0])\n while (open.length && open.at(-1)!.indent >= indent) open.pop()\n for (const outer of open) {\n if (!outer.head || outer.id === id) continue\n const inside = nested.get(outer.id)\n if (inside) inside.add(id)\n else nested.set(outer.id, new Set([id]))\n }\n open.push({ id, indent, head: !listItem && (open.at(-1)?.head ?? true) })\n }\n return { ids, below: id => nested.get(id) ?? NOTHING }\n}\n\n/**\n * Removes what a model echoes from the document lines into its Markdown: a `[<id>] ` prefix of a known block at the\n * start of a line (not inside a fenced code block of its own Markdown, which keeps lines that look like one), and the\n * indentation all non-blank lines share (lines echoed from a Typed block's body carry its indent, which would turn deeper\n * nested lines into indented code).\n */\nexport function stripBlockLineEcho(markdown: string, known: (id: string) => boolean): string {\n let fence = 0\n const stripped = markdown.split('\\n').map((line) => {\n if (fence) {\n fence = fenceAfter(fence, line, false)\n return line\n }\n const bare = line.replace(/^\\[([^\\]\\s]+)\\] ?/, (prefix, id: string) => known(id) ? '' : prefix)\n fence = fenceAfter(0, bare, true)\n return bare\n })\n const indents = stripped.filter(line => line.trim()).map(line => /^[ \\t]*/.exec(line)![0].length)\n const common = indents.length ? Math.min(...indents) : 0\n return (common ? stripped.map(line => line.replace(new RegExp(`^[ \\\\t]{0,${common}}`), '')) : stripped).join('\\n')\n}\n\n/**\n * Removes the list and quote markers (`- `, `1. `, `> `) a model echoes from the lines of list items and quotes at the\n * start of each line of its Markdown.\n */\nexport function stripListMarkerEcho(markdown: string): string {\n return markdown.replace(/^(?:[ \\t]*(?:[-*+]|\\d+[.)])[ \\t]+|[ \\t]*>[ \\t]?)+/gm, '')\n}\n\n// ---------- Prompt -----------------------------------------------------------\n\n/** The block ID of the prompt's example line (also the target of its citation example). */\nexport const BLOCK_LINE_EXAMPLE_ID = '0f3c9a52-7d1e-4b8a-9c2f-5e6d7a8b9c0d'\n\n/** The text of the prompt's example line: a speaker line. */\nexport const BLOCK_LINE_EXAMPLE_TEXT = '**Anna:** Wir starten im Mai.'\n\n/** The example line of the prompt, written by `blockLine` like every line of the document. */\nexport const BLOCK_LINE_EXAMPLE = blockLine(BLOCK_LINE_EXAMPLE_ID, '', BLOCK_LINE_EXAMPLE_TEXT)\n\n/** The label of the document in the user message. */\nexport const BLOCK_LINES_LABEL = 'Dokument (aktueller Stand, eine Zeile pro Block, beginnend mit der Block-ID)'\n\n/** What the first lines of the system prompt say about the document's lines. */\nexport const BLOCK_LINE_INTRO = `Jede Zeile des Dokuments beginnt mit der ID ihres Blocks in eckigen Klammern, z. B. \\`${BLOCK_LINE_EXAMPLE}\\`. Redebeiträge beginnen nach der ID mit dem fett gesetzten Namen der Sprecherin bzw. des Sprechers und einem Doppelpunkt.`\n\n/** The list and quote blocks with the characters their lines carry. */\nexport const BLOCK_LINE_MARKERS = `Aufzählung („${listLinePrefix('', false, 1)}“), nummerierte Liste („${listLinePrefix('', true, 1)}“), Zitat („${quoteLinePrefix('')}“)`\n\n/** What the prompt says about lists and quotes in the document. */\nexport const BLOCK_LINE_LIST_TEXT = 'Listenpunkte und Zitate stehen nach der ID mit ihrem Zeichen; jeder Absatz darin hat eine eigene ID.'\n\n/** What the prompt says about the lines of a host block with block content (a callout). */\nexport const BLOCK_LINE_HOST_TEXT = `Im Dokument beginnt ein eigener Block mit Inhaltsblöcken mit der Zeile „${blockLine('ID', '', '<erste Zeile seiner Markdown-Form>')}“; seine Inhaltsblöcke folgen als eigene Zeilen mit IDs, um zwei Leerzeichen eingerückt.`\n\n/** What the prompt says about the lines of a Typed block. */\nexport const BLOCK_LINE_COMPOSITE_TEXT = `Im Dokument beginnt ein typisierter Block mit der Kopfzeile „${blockLine('ID', '', compositeLineHeader('Blocktyp', { feld: 'wert' }, ['Titel', 'Beschreibung', 'Tags: #a #b']))}“ (die Felder als JSON nur, wenn welche gesetzt sind; leere Teile fehlen samt ihrem „|“). Seine Inhaltsblöcke folgen als eigene Zeilen mit ihren IDs, um zwei Leerzeichen eingerückt; eine Zeile „${compositeLineClose('')}“ ohne ID schließt ihn. Das Feld „bib“ ist eine Literaturangabe als CSL-JSON, „derivation“ beschreibt, woraus der Block erzeugt wurde. Ein Verweis auf einen Block des Dokuments steht im Text als „[@<Block-ID>]“, mit Fundstelle als „[@<Block-ID>, S. 12]“.`\n\n/** What the prompt says about the Markdown of an operation: no echo of the line prefix (`stripBlockLineEcho` removes it when it comes anyway). */\nexport const BLOCK_LINE_ECHO_TEXT = '„markdown“ enthält nur den neuen Inhalt, ohne Block-ID und ohne das Listen- oder Zitatzeichen der Zeile.'\n\n/** What the retry message says about IDs. */\nexport const BLOCK_LINE_IDS_HINT = 'Verwende nur IDs, die im Dokument in eckigen Klammern stehen; erfinde keine IDs.'\n"],"mappings":"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"}
|
|
@@ -4,10 +4,10 @@ import { propertyHostOf } from "../properties.js";
|
|
|
4
4
|
import { useUiKit } from "../../../vue/kit/use-ui-kit.js";
|
|
5
5
|
import { useEditorI18n } from "../../../vue/use-editor-i18n.js";
|
|
6
6
|
import { compositeFieldText, isStructuredField } from "../inspector-model.js";
|
|
7
|
-
import _sfc_main$1 from "./
|
|
8
|
-
import _sfc_main$2 from "./
|
|
9
|
-
import _sfc_main$3 from "./
|
|
10
|
-
import _sfc_main$4 from "./
|
|
7
|
+
import _sfc_main$1 from "./DerivationBadge.js";
|
|
8
|
+
import _sfc_main$2 from "./CompositeFieldEditor.js";
|
|
9
|
+
import _sfc_main$3 from "./InspectorSections.js";
|
|
10
|
+
import _sfc_main$4 from "./DocumentBadges.js";
|
|
11
11
|
import { useCompositeInspector } from "../use-composite-inspector.js";
|
|
12
12
|
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, normalizeClass, normalizeStyle, openBlock, renderList, renderSlot, toDisplayString, unref, useId, withCtx } from "vue";
|
|
13
13
|
//#region src/features/composites/components/CompositeInspector.vue
|
|
@@ -109,7 +109,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
109
109
|
class: "text-sm font-semibold text-[var(--sk-fg-highlighted)]"
|
|
110
110
|
}, toDisplayString(unref(t)("composites.inspector.document")), 9, _hoisted_4),
|
|
111
111
|
createCommentVNode(" Chips of the whole document other features add (`EditorFeature.documentBadges`), e.g. the Freigabestatus. "),
|
|
112
|
-
createVNode(_sfc_main$
|
|
112
|
+
createVNode(_sfc_main$4, { editor: __props.editor }, null, 8, ["editor"])
|
|
113
113
|
]), createElementVNode("div", _hoisted_5, [renderSlot(_ctx.$slots, "document"), createElementVNode("dl", _hoisted_6, [
|
|
114
114
|
createElementVNode("dt", _hoisted_7, toDisplayString(unref(t)("composites.inspector.composites")), 1),
|
|
115
115
|
unref(kinds).length ? (openBlock(), createElementBlock("dd", _hoisted_8, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(kinds), (kind) => {
|
|
@@ -267,7 +267,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
267
267
|
label: field.label,
|
|
268
268
|
size: "sm"
|
|
269
269
|
}, {
|
|
270
|
-
default: withCtx(() => [createCommentVNode(" Fields whose value is not text (reference, bibliography, derivation) have their own editors, as in the details card; a text input would overwrite the stored object with a string. "), unref(isStructuredField)(field) ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [__props.editor ? (openBlock(), createBlock(_sfc_main$
|
|
270
|
+
default: withCtx(() => [createCommentVNode(" Fields whose value is not text (reference, bibliography, derivation) have their own editors, as in the details card; a text input would overwrite the stored object with a string. "), unref(isStructuredField)(field) ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [__props.editor ? (openBlock(), createBlock(_sfc_main$2, {
|
|
271
271
|
key: 0,
|
|
272
272
|
editor: __props.editor,
|
|
273
273
|
row: unref(properties),
|
|
@@ -335,14 +335,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
335
335
|
label: unref(t)("composites.inspector.derivation"),
|
|
336
336
|
size: "sm"
|
|
337
337
|
}, {
|
|
338
|
-
default: withCtx(() => [createVNode(_sfc_main$
|
|
338
|
+
default: withCtx(() => [createVNode(_sfc_main$1, {
|
|
339
339
|
editor: __props.editor,
|
|
340
340
|
node: unref(derivation).node
|
|
341
341
|
}, null, 8, ["editor", "node"])]),
|
|
342
342
|
_: 1
|
|
343
343
|
}, 8, ["label"])) : createCommentVNode("v-if", true),
|
|
344
344
|
createCommentVNode(" Sections other features add (`EditorFeature.inspectorSections`), e.g. the Prüfstand. "),
|
|
345
|
-
__props.editor ? (openBlock(), createBlock(_sfc_main$
|
|
345
|
+
__props.editor ? (openBlock(), createBlock(_sfc_main$3, {
|
|
346
346
|
key: 3,
|
|
347
347
|
editor: __props.editor,
|
|
348
348
|
node: blockNode.value
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { embedViewOptions } from "./embed-dom.js";
|
|
2
2
|
import { typedBlockViewOptions } from "./node-views.js";
|
|
3
3
|
import { compositeFeatureCore } from "./feature.js";
|
|
4
|
-
import _sfc_main$13 from "./components/DocumentBadges.js";
|
|
5
4
|
import _sfc_main from "./components/BlockEmbedView.js";
|
|
6
5
|
import _sfc_main$1 from "./components/CiteView.js";
|
|
7
6
|
import _sfc_main$11 from "./components/CompositeView.js";
|
|
@@ -21,6 +20,7 @@ import _sfc_main$14 from "./components/EdgesSection.js";
|
|
|
21
20
|
import _sfc_main$16 from "./components/FocusChip.js";
|
|
22
21
|
import _sfc_main$18 from "./components/ReferenceView.js";
|
|
23
22
|
import _sfc_main$19 from "./components/SettingsEditingTab.js";
|
|
23
|
+
import _sfc_main$13 from "./components/DocumentBadges.js";
|
|
24
24
|
import _sfc_main$6 from "./components/CompositeInspector.js";
|
|
25
25
|
import _sfc_main$9 from "./components/CompositeTable.js";
|
|
26
26
|
import { VueNodeViewRenderer } from "@tiptap/vue-3";
|
|
@@ -3,5 +3,5 @@ import { ThemeRecord } from "../../core/backend-types.js";
|
|
|
3
3
|
import { MAX_THEME_BYTES, ThemeManifest, themeManifestSchema } from "./schema.js";
|
|
4
4
|
import { ThemesUi, themesFeatureCore } from "./feature.js";
|
|
5
5
|
import { scopeThemeCss } from "./scope-css.js";
|
|
6
|
-
import { THEME_SCOPE_CLASS, startObsidianTheme } from "./theme-runtime.js";
|
|
7
|
-
export { MAX_THEME_BYTES, THEME_ID, THEME_SCOPE_CLASS, type ThemeManifest, type ThemeRecord, type ThemesUi, scopeThemeCss, startObsidianTheme, themeManifestSchema, themesFeatureCore };
|
|
6
|
+
import { THEME_SCOPE_CLASS, ThemeHost, pinObsidianTheme, startObsidianTheme } from "./theme-runtime.js";
|
|
7
|
+
export { MAX_THEME_BYTES, THEME_ID, THEME_SCOPE_CLASS, type ThemeHost, type ThemeManifest, type ThemeRecord, type ThemesUi, pinObsidianTheme, scopeThemeCss, startObsidianTheme, themeManifestSchema, themesFeatureCore };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { THEME_ID } from "../../settings/index.js";
|
|
2
2
|
import { MAX_THEME_BYTES, themeManifestSchema } from "./schema.js";
|
|
3
3
|
import { scopeThemeCss } from "./scope-css.js";
|
|
4
|
-
import { THEME_SCOPE_CLASS, startObsidianTheme } from "./theme-runtime.js";
|
|
4
|
+
import { THEME_SCOPE_CLASS, pinObsidianTheme, startObsidianTheme } from "./theme-runtime.js";
|
|
5
5
|
import { themesFeatureCore } from "./feature.js";
|
|
6
|
-
export { MAX_THEME_BYTES, THEME_ID, THEME_SCOPE_CLASS, scopeThemeCss, startObsidianTheme, themeManifestSchema, themesFeatureCore };
|
|
6
|
+
export { MAX_THEME_BYTES, THEME_ID, THEME_SCOPE_CLASS, pinObsidianTheme, scopeThemeCss, startObsidianTheme, themeManifestSchema, themesFeatureCore };
|
|
@@ -2,13 +2,24 @@ import { FeatureContext } from "../../core/feature.js";
|
|
|
2
2
|
//#region src/features/themes/theme-runtime.d.ts
|
|
3
3
|
/** Class that scopes a theme's rules (`scopeThemeCss`) and the bridge stylesheet; the runtime puts it on `<html>`. */
|
|
4
4
|
export declare const THEME_SCOPE_CLASS = "obsidian-theme";
|
|
5
|
+
/** What the runtime reads of a surface's `FeatureContext` (an app's `EditorContext` has both, too). */
|
|
6
|
+
export type ThemeHost = Pick<FeatureContext, 'settings' | 'backend'>;
|
|
5
7
|
/**
|
|
6
|
-
* The theme runtime of the app `ctx` belongs to: applies `settings.appearance.theme` (
|
|
7
|
-
* `ctx.backend.themes`, scopes it, keeps one
|
|
8
|
-
* on `<html>`
|
|
8
|
+
* The theme runtime of the app `ctx` belongs to: applies `settings.appearance.theme` (or the most recent
|
|
9
|
+
* `pinObsidianTheme`), fetches the stylesheet through `ctx.backend.themes`, scopes it, keeps one
|
|
10
|
+
* `<style data-tiptap-theme>` in the document head, puts `obsidian-theme theme-dark|theme-light` on `<html>` and follows
|
|
11
|
+
* the app's colour mode.
|
|
9
12
|
* Reference-counted per app: the first call starts the runtime, every call returns an idempotent release, the last release
|
|
10
13
|
* stops it (removing the style element and the root classes). Surfaces of one app share a single runtime.
|
|
11
14
|
*/
|
|
12
|
-
export declare function startObsidianTheme(ctx:
|
|
15
|
+
export declare function startObsidianTheme(ctx: ThemeHost): () => void;
|
|
16
|
+
/**
|
|
17
|
+
* Shows theme `id` (`null` = the standard look) instead of the user's `settings.appearance.theme` until the returned
|
|
18
|
+
* release runs; the setting itself is not touched, so nothing is persisted and the rest of the app keeps the user's
|
|
19
|
+
* choice. For a page or view with a look of its own. Pins stack per app: the most recent one wins, releasing it falls
|
|
20
|
+
* back to the one before, the last release back to the setting. A pin holds the app's theme runtime (`startObsidianTheme`),
|
|
21
|
+
* so it also applies on a page without a themes-feature surface. The release is idempotent.
|
|
22
|
+
*/
|
|
23
|
+
export declare function pinObsidianTheme(host: ThemeHost, id: string | null): () => void;
|
|
13
24
|
//#endregion
|
|
14
25
|
//# sourceMappingURL=theme-runtime.d.ts.map
|