@texuli/core 0.1.0-next.0 → 0.1.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/editor/document.d.ts +65 -0
- package/dist/editor/document.js +394 -0
- package/dist/editor/edit.d.ts +7 -0
- package/dist/editor/edit.js +7 -0
- package/dist/editor/graphemes.d.ts +17 -0
- package/dist/editor/graphemes.js +71 -0
- package/dist/editor/history.d.ts +57 -0
- package/dist/editor/history.js +77 -0
- package/dist/editor/inline.d.ts +56 -0
- package/dist/editor/inline.js +166 -0
- package/dist/editor/input-rules.d.ts +17 -0
- package/dist/editor/input-rules.js +151 -0
- package/dist/editor/keymap.d.ts +30 -0
- package/dist/editor/keymap.js +69 -0
- package/dist/editor/load.d.ts +44 -0
- package/dist/editor/load.js +248 -0
- package/dist/editor/markdown.d.ts +33 -0
- package/dist/editor/markdown.js +705 -0
- package/dist/editor/model.d.ts +13 -0
- package/dist/editor/model.js +13 -0
- package/dist/editor/selection.d.ts +27 -0
- package/dist/editor/selection.js +85 -0
- package/dist/editor/transforms.d.ts +93 -0
- package/dist/editor/transforms.js +847 -0
- package/dist/editor/tree.d.ts +35 -0
- package/dist/editor/tree.js +103 -0
- package/dist/editor/types.d.ts +147 -0
- package/dist/editor/types.js +39 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/url/safe-href.d.ts +1 -20
- package/dist/url/safe-href.js +24 -1
- package/dist/url-policy/index.d.ts +1 -0
- package/dist/url-policy/index.js +1 -0
- package/dist/url-policy/safe-image-src.d.ts +1 -1
- package/dist/url-policy/safe-image-src.js +4 -4
- package/dist/url-policy/safe-image-srcset.d.ts +1 -1
- package/dist/url-policy/safe-image-srcset.js +3 -1
- package/dist/url-policy/safe-media-src.d.ts +1 -1
- package/dist/url-policy/safe-media-src.js +4 -4
- package/dist/url-policy/shared.d.ts +0 -2
- package/dist/url-policy/shared.js +0 -3
- package/dist/url-policy/trust.d.ts +8 -0
- package/dist/url-policy/trust.js +9 -0
- package/package.json +22 -6
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@texuli/core/editor-model`: the rich text editor document model (ADR-003).
|
|
3
|
+
* Types and caps, the closed vocabularies, constructors, normalization,
|
|
4
|
+
* validation, the untrusted loader, selection helpers, grapheme and word
|
|
5
|
+
* boundaries, and the nested inline interchange shape. Framework-free.
|
|
6
|
+
*/
|
|
7
|
+
export { blockLength, blockText, createEditorIds, type EditorAncestor, type EditorIdSource, type EditorLocation, editorBlockCount, editorCharCount, editorLeaves, editorPlainText, emptyEditorDocument, isTextBlock, isVoidBlock, locateBlock, nextEditorId, normalizeDocument, paragraph, removeBlocks, replaceBlock, replaceItem, validateEditorDocument, } from './document.js';
|
|
8
|
+
export { graphemeBoundaries, nextGrapheme, nextWord, previousGrapheme, previousWord, snapToGrapheme, } from './graphemes.js';
|
|
9
|
+
export { cleanText, hasMark, inheritedMarks, inlineLength, inlineText, markKey, normalizeInline, sliceInline, sortMarks, textToInline, } from './inline.js';
|
|
10
|
+
export { type EditorLoadOptions, type EditorLoadResult, loadEditorDocument } from './load.js';
|
|
11
|
+
export { caret, clampSelection, type EditorRangePoint, isCollapsed, nodeSelection, selectAll, selectionRange, startSelection, textSelection, } from './selection.js';
|
|
12
|
+
export { type EditorInlineNode, inlineFromTree, inlineToTree } from './tree.js';
|
|
13
|
+
export { EDITOR_BLOCK_TYPES, EDITOR_LIMITS, EDITOR_MARK_ORDER, type EditorBlock, type EditorBlockId, type EditorBlockType, type EditorCodeBlock, type EditorDivider, type EditorDocument, type EditorHeading, type EditorHeadingLevel, type EditorInline, type EditorIssue, type EditorIssueCode, type EditorLimits, type EditorList, type EditorListItem, type EditorListKind, type EditorMark, type EditorMarkType, type EditorMedia, type EditorParagraph, type EditorPoint, type EditorQuote, type EditorRichBlock, type EditorSelection, type EditorState, type EditorTextBlock, type EditorTransformResult, } from './types.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@texuli/core/editor-model`: the rich text editor document model (ADR-003).
|
|
3
|
+
* Types and caps, the closed vocabularies, constructors, normalization,
|
|
4
|
+
* validation, the untrusted loader, selection helpers, grapheme and word
|
|
5
|
+
* boundaries, and the nested inline interchange shape. Framework-free.
|
|
6
|
+
*/
|
|
7
|
+
export { blockLength, blockText, createEditorIds, editorBlockCount, editorCharCount, editorLeaves, editorPlainText, emptyEditorDocument, isTextBlock, isVoidBlock, locateBlock, nextEditorId, normalizeDocument, paragraph, removeBlocks, replaceBlock, replaceItem, validateEditorDocument, } from "./document.js";
|
|
8
|
+
export { graphemeBoundaries, nextGrapheme, nextWord, previousGrapheme, previousWord, snapToGrapheme, } from "./graphemes.js";
|
|
9
|
+
export { cleanText, hasMark, inheritedMarks, inlineLength, inlineText, markKey, normalizeInline, sliceInline, sortMarks, textToInline, } from "./inline.js";
|
|
10
|
+
export { loadEditorDocument } from "./load.js";
|
|
11
|
+
export { caret, clampSelection, isCollapsed, nodeSelection, selectAll, selectionRange, startSelection, textSelection, } from "./selection.js";
|
|
12
|
+
export { inlineFromTree, inlineToTree } from "./tree.js";
|
|
13
|
+
export { EDITOR_BLOCK_TYPES, EDITOR_LIMITS, EDITOR_MARK_ORDER, } from "./types.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { EditorBlockId, EditorDocument, EditorPoint, EditorSelection } from './types.js';
|
|
2
|
+
/** A collapsed text selection (a caret) at `offset` in `block`. */
|
|
3
|
+
export declare const caret: (block: EditorBlockId, offset?: number) => EditorSelection;
|
|
4
|
+
/** A text selection from `anchor` to `focus` (either order). */
|
|
5
|
+
export declare const textSelection: (anchor: EditorPoint, focus?: EditorPoint) => EditorSelection;
|
|
6
|
+
/** Select the void block (divider, media) `block`. */
|
|
7
|
+
export declare const nodeSelection: (block: EditorBlockId) => EditorSelection;
|
|
8
|
+
export declare const isCollapsed: (selection: EditorSelection) => boolean;
|
|
9
|
+
/** A point in document order: the leaf index, then the offset. */
|
|
10
|
+
export interface EditorRangePoint extends EditorPoint {
|
|
11
|
+
index: number;
|
|
12
|
+
}
|
|
13
|
+
/** The ordered ends of a selection, or `null` when it points outside the document. */
|
|
14
|
+
export declare function selectionRange(doc: EditorDocument, selection: EditorSelection): {
|
|
15
|
+
from: EditorRangePoint;
|
|
16
|
+
to: EditorRangePoint;
|
|
17
|
+
} | null;
|
|
18
|
+
/**
|
|
19
|
+
* A selection that is valid in `doc`: points inside text blocks, offsets
|
|
20
|
+
* clamped and snapped to grapheme boundaries. A selection on a missing block
|
|
21
|
+
* falls back to the start of the first text block (or the first leaf).
|
|
22
|
+
*/
|
|
23
|
+
export declare function clampSelection(doc: EditorDocument, selection: EditorSelection): EditorSelection;
|
|
24
|
+
/** A caret at the start of the document. */
|
|
25
|
+
export declare function startSelection(doc: EditorDocument): EditorSelection;
|
|
26
|
+
/** Select the whole document (first text block start to last text block end). */
|
|
27
|
+
export declare function selectAll(doc: EditorDocument): EditorSelection;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { blockLength, blockText, editorLeaves, isTextBlock } from "./document.js";
|
|
2
|
+
import { snapToGrapheme } from "./graphemes.js";
|
|
3
|
+
/** A collapsed text selection (a caret) at `offset` in `block`. */
|
|
4
|
+
export const caret = (block, offset = 0) => ({
|
|
5
|
+
kind: 'text',
|
|
6
|
+
anchor: { block, offset },
|
|
7
|
+
focus: { block, offset },
|
|
8
|
+
});
|
|
9
|
+
/** A text selection from `anchor` to `focus` (either order). */
|
|
10
|
+
export const textSelection = (anchor, focus = anchor) => ({
|
|
11
|
+
kind: 'text',
|
|
12
|
+
anchor,
|
|
13
|
+
focus,
|
|
14
|
+
});
|
|
15
|
+
/** Select the void block (divider, media) `block`. */
|
|
16
|
+
export const nodeSelection = (block) => ({ kind: 'node', block });
|
|
17
|
+
export const isCollapsed = (selection) => selection.kind === 'text' &&
|
|
18
|
+
selection.anchor.block === selection.focus.block &&
|
|
19
|
+
selection.anchor.offset === selection.focus.offset;
|
|
20
|
+
/** The ordered ends of a selection, or `null` when it points outside the document. */
|
|
21
|
+
export function selectionRange(doc, selection) {
|
|
22
|
+
const leaves = editorLeaves(doc);
|
|
23
|
+
const indexOf = (id) => leaves.findIndex(({ block }) => block.id === id);
|
|
24
|
+
if (selection.kind === 'node') {
|
|
25
|
+
const index = indexOf(selection.block);
|
|
26
|
+
if (index === -1)
|
|
27
|
+
return null;
|
|
28
|
+
const point = { block: selection.block, offset: 0, index };
|
|
29
|
+
return { from: point, to: point };
|
|
30
|
+
}
|
|
31
|
+
const a = { ...selection.anchor, index: indexOf(selection.anchor.block) };
|
|
32
|
+
const f = { ...selection.focus, index: indexOf(selection.focus.block) };
|
|
33
|
+
if (a.index === -1 || f.index === -1)
|
|
34
|
+
return null;
|
|
35
|
+
const forward = a.index < f.index || (a.index === f.index && a.offset <= f.offset);
|
|
36
|
+
return forward ? { from: a, to: f } : { from: f, to: a };
|
|
37
|
+
}
|
|
38
|
+
const firstText = (doc) => editorLeaves(doc).find(({ block }) => isTextBlock(block))?.block;
|
|
39
|
+
function clampPoint(doc, point) {
|
|
40
|
+
const leaf = editorLeaves(doc).find(({ block }) => block.id === point.block)?.block;
|
|
41
|
+
if (!leaf || !isTextBlock(leaf))
|
|
42
|
+
return null;
|
|
43
|
+
const offset = Math.max(0, Math.min(blockLength(leaf), Math.trunc(point.offset) || 0));
|
|
44
|
+
return { block: leaf.id, offset: snapToGrapheme(blockText(leaf), offset) };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A selection that is valid in `doc`: points inside text blocks, offsets
|
|
48
|
+
* clamped and snapped to grapheme boundaries. A selection on a missing block
|
|
49
|
+
* falls back to the start of the first text block (or the first leaf).
|
|
50
|
+
*/
|
|
51
|
+
export function clampSelection(doc, selection) {
|
|
52
|
+
if (selection.kind === 'node') {
|
|
53
|
+
const leaf = editorLeaves(doc).find(({ block }) => block.id === selection.block)?.block;
|
|
54
|
+
if (leaf && !isTextBlock(leaf))
|
|
55
|
+
return selection;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
const anchor = clampPoint(doc, selection.anchor);
|
|
59
|
+
const focus = clampPoint(doc, selection.focus);
|
|
60
|
+
if (anchor && focus) {
|
|
61
|
+
const same = anchor.offset === selection.anchor.offset && focus.offset === selection.focus.offset;
|
|
62
|
+
return same ? selection : textSelection(anchor, focus);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return startSelection(doc);
|
|
66
|
+
}
|
|
67
|
+
/** A caret at the start of the document. */
|
|
68
|
+
export function startSelection(doc) {
|
|
69
|
+
const text = firstText(doc);
|
|
70
|
+
if (text)
|
|
71
|
+
return caret(text.id);
|
|
72
|
+
const leaf = editorLeaves(doc)[0]?.block;
|
|
73
|
+
return nodeSelection(leaf.id);
|
|
74
|
+
}
|
|
75
|
+
/** Select the whole document (first text block start to last text block end). */
|
|
76
|
+
export function selectAll(doc) {
|
|
77
|
+
const texts = editorLeaves(doc)
|
|
78
|
+
.map(({ block }) => block)
|
|
79
|
+
.filter(isTextBlock);
|
|
80
|
+
const first = texts[0];
|
|
81
|
+
const last = texts.at(-1);
|
|
82
|
+
if (!first || !last)
|
|
83
|
+
return startSelection(doc);
|
|
84
|
+
return textSelection({ block: first.id, offset: 0 }, { block: last.id, offset: blockLength(last) });
|
|
85
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type HrefTrust } from '../url/safe-href.js';
|
|
2
|
+
import { type EditorIdSource } from './document.js';
|
|
3
|
+
import { type EditorBlock, type EditorBlockId, type EditorDocument, type EditorHeadingLevel, type EditorLimits, type EditorListKind, type EditorMarkType, type EditorState, type EditorTextBlock, type EditorTransformResult } from './types.js';
|
|
4
|
+
export interface EditorTransformOptions {
|
|
5
|
+
newId?: EditorIdSource;
|
|
6
|
+
/** Lower caps for this editor (a blocks `text` field holds 4,000 characters). */
|
|
7
|
+
limits?: Partial<EditorLimits>;
|
|
8
|
+
}
|
|
9
|
+
declare const textLeaf: (doc: EditorDocument, id: EditorBlockId) => EditorTextBlock | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Type `text` at the selection (replacing a range). Rich blocks give the text
|
|
12
|
+
* the stored marks, else the marks before the caret; `\n` becomes a break.
|
|
13
|
+
* Code keeps newlines and tabs. Unsafe characters are stripped.
|
|
14
|
+
*/
|
|
15
|
+
export declare function insertText(state: EditorState, text: string, options?: EditorTransformOptions): EditorTransformResult;
|
|
16
|
+
/** Shift+Enter: a break in rich blocks, a newline in code. */
|
|
17
|
+
export declare function insertBreak(state: EditorState, options?: EditorTransformOptions): EditorTransformResult;
|
|
18
|
+
/** Delete the selection (a range or a selected void block). */
|
|
19
|
+
export declare function deleteSelection(state: EditorState, options?: EditorTransformOptions): EditorTransformResult;
|
|
20
|
+
export type EditorDeleteUnit = 'grapheme' | 'word' | 'line';
|
|
21
|
+
/** Backspace: delete before the caret, or apply the block-start rules (lift, join, select a void block). */
|
|
22
|
+
export declare const deleteBackward: (state: EditorState, unit?: EditorDeleteUnit, options?: EditorTransformOptions) => EditorTransformResult;
|
|
23
|
+
/** Delete: delete after the caret, or join the next block. */
|
|
24
|
+
export declare const deleteForward: (state: EditorState, unit?: EditorDeleteUnit, options?: EditorTransformOptions) => EditorTransformResult;
|
|
25
|
+
/** Shift+Tab in a list: move the caret's item out one level. */
|
|
26
|
+
export declare function liftListItem(state: EditorState, options?: EditorTransformOptions): EditorTransformResult;
|
|
27
|
+
/** Tab in a list: nest the caret's item under the previous item. */
|
|
28
|
+
export declare function sinkListItem(state: EditorState, options?: EditorTransformOptions): EditorTransformResult;
|
|
29
|
+
/** Mod+Enter: check or uncheck the caret's todo item. */
|
|
30
|
+
export declare function toggleTodo(state: EditorState, options?: EditorTransformOptions): EditorTransformResult;
|
|
31
|
+
/**
|
|
32
|
+
* Enter. Splits the block; in list items continues the list (an empty item
|
|
33
|
+
* lifts out); an empty last paragraph of a quote leaves it; at the end of a
|
|
34
|
+
* heading the new block is a paragraph; in code it inserts a newline, and an
|
|
35
|
+
* empty last line leaves the code block.
|
|
36
|
+
*/
|
|
37
|
+
export declare function splitBlock(state: EditorState, options?: EditorTransformOptions): EditorTransformResult;
|
|
38
|
+
/** Make the selected text blocks paragraphs, headings or code. A list item's first block stays a paragraph. */
|
|
39
|
+
export declare function setBlockType(state: EditorState, type: 'paragraph' | 'heading' | 'code', level?: EditorHeadingLevel, options?: EditorTransformOptions): EditorTransformResult;
|
|
40
|
+
/**
|
|
41
|
+
* Bullet, ordered or todo list. Inside a list of that kind the selected items
|
|
42
|
+
* leave it; inside another kind the list changes kind; elsewhere the selected
|
|
43
|
+
* paragraphs and headings become items.
|
|
44
|
+
*/
|
|
45
|
+
export declare function toggleList(state: EditorState, kind: EditorListKind, options?: EditorTransformOptions): EditorTransformResult;
|
|
46
|
+
/** Wrap the selected blocks in a quote, or unwrap the quote around the caret. */
|
|
47
|
+
export declare function toggleQuote(state: EditorState, options?: EditorTransformOptions): EditorTransformResult;
|
|
48
|
+
/**
|
|
49
|
+
* Toggle a mark over the selection (removed when every eligible character has
|
|
50
|
+
* it). At a caret it toggles the stored marks for the next typed text. Code
|
|
51
|
+
* text takes only `link` besides `code`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function toggleMark(state: EditorState, type: Exclude<EditorMarkType, 'link'>, options?: EditorTransformOptions): EditorTransformResult;
|
|
54
|
+
export interface SetLinkOptions extends EditorTransformOptions {
|
|
55
|
+
/** URL policy tier. Default `untrusted`; pasted and typed URLs stay untrusted. */
|
|
56
|
+
trust?: HrefTrust;
|
|
57
|
+
/** At a caret: insert this text as the link. */
|
|
58
|
+
text?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Link the selection to `href`, which must pass `safeHref` at `trust`
|
|
62
|
+
* (`untrusted` by default) or the edit is refused. At a caret, updates the
|
|
63
|
+
* link around it, or inserts `options.text` as a new link.
|
|
64
|
+
*/
|
|
65
|
+
export declare function setLink(state: EditorState, href: string, options?: SetLinkOptions): EditorTransformResult;
|
|
66
|
+
/** Remove links from the selection, or the whole link around the caret. */
|
|
67
|
+
export declare function removeLink(state: EditorState, options?: EditorTransformOptions): EditorTransformResult;
|
|
68
|
+
export type EditorVoidInsert = {
|
|
69
|
+
type: 'divider';
|
|
70
|
+
} | {
|
|
71
|
+
type: 'media';
|
|
72
|
+
ref: string;
|
|
73
|
+
alt?: string;
|
|
74
|
+
};
|
|
75
|
+
/** Insert a divider or a media reference at the caret; the caret moves to the text after it. */
|
|
76
|
+
export declare function insertBlock(state: EditorState, insert: EditorVoidInsert, options?: EditorTransformOptions): EditorTransformResult;
|
|
77
|
+
/**
|
|
78
|
+
* Paste: replace the selection with `fragment` (a loaded document or blocks,
|
|
79
|
+
* already sanitized by `loadEditorDocument`, `fromMarkdown` or the paste
|
|
80
|
+
* import). One paragraph pastes inline; several blocks split the caret block
|
|
81
|
+
* around them. Blocks that would nest too deep become plain paragraphs, and
|
|
82
|
+
* blocks past the size caps are cut, each with a diagnostic.
|
|
83
|
+
*/
|
|
84
|
+
export declare function replaceSelection(state: EditorState, fragment: EditorDocument | readonly EditorBlock[], options?: EditorTransformOptions): EditorTransformResult;
|
|
85
|
+
/**
|
|
86
|
+
* Apply what an input method composed: the block's text as the DOM shows it
|
|
87
|
+
* after `compositionend` (breaks as `\n`) is diffed against the model, and the
|
|
88
|
+
* one changed span is replaced as a single edit.
|
|
89
|
+
*/
|
|
90
|
+
export declare function applyComposition(state: EditorState, blockId: EditorBlockId, domText: string, options?: EditorTransformOptions): EditorTransformResult;
|
|
91
|
+
/** Move the caret's block (or its list item) one place up or down among its siblings. */
|
|
92
|
+
export declare function moveBlock(state: EditorState, direction: 'up' | 'down', options?: EditorTransformOptions): EditorTransformResult;
|
|
93
|
+
export { textLeaf as editorTextBlock };
|