@sametalmdr/word-editor 0.2.0-alpha → 0.3.0-alpha
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 +108 -70
- package/LICENSE +21 -21
- package/README.md +286 -247
- package/dist/canvas/box-clipboard.js +238 -238
- package/dist/canvas/box-selection.js +80 -80
- package/dist/canvas/canvas-editor.js +502 -502
- package/dist/canvas/controller.js +717 -717
- package/dist/canvas/geometry.js +131 -131
- package/dist/canvas/gesture.d.ts +0 -1
- package/dist/canvas/gesture.js +289 -289
- package/dist/canvas/handles.js +212 -212
- package/dist/canvas/menu.js +82 -82
- package/dist/canvas/mode-controller.js +81 -81
- package/dist/canvas/nudge.js +108 -108
- package/dist/canvas/overlay.js +149 -149
- package/dist/canvas/snap.js +132 -132
- package/dist/canvas/viewport.js +112 -112
- package/dist/canvas/z-order.js +171 -171
- package/dist/commands/alignment.js +150 -150
- package/dist/commands/box.js +272 -272
- package/dist/commands/clipboard.js +93 -93
- package/dist/commands/graphemes.js +112 -112
- package/dist/commands/marks.js +153 -153
- package/dist/commands/paragraph-props.js +176 -176
- package/dist/commands/size.js +368 -368
- package/dist/commands/text.js +291 -291
- package/dist/docx.d.ts +1 -0
- package/dist/docx.js +15 -13
- package/dist/editor/editing-host.js +434 -434
- package/dist/element.js +41 -41
- package/dist/fonts/OFL-Carlito.txt +94 -0
- package/dist/fonts/calibri-metrics.d.ts +27 -0
- package/dist/fonts/calibri-metrics.js +88 -0
- package/dist/history/history.js +433 -433
- package/dist/index.js +63 -63
- package/dist/input/beforeinput.js +224 -224
- package/dist/input/clipboard.js +107 -107
- package/dist/input/composition.js +341 -341
- package/dist/input/keymap.js +54 -54
- package/dist/io/ooxml/custgeom.js +72 -72
- package/dist/io/ooxml/drawingml.js +232 -232
- package/dist/io/ooxml/edits.js +67 -67
- package/dist/io/ooxml/export-splice.js +880 -880
- package/dist/io/ooxml/export.js +365 -365
- package/dist/io/ooxml/fill.d.ts +93 -8
- package/dist/io/ooxml/fill.js +618 -562
- package/dist/io/ooxml/fit.d.ts +252 -0
- package/dist/io/ooxml/fit.js +598 -0
- package/dist/io/ooxml/import-gate.js +200 -200
- package/dist/io/ooxml/import.js +691 -691
- package/dist/io/ooxml/inflate.js +126 -126
- package/dist/io/ooxml/media.js +87 -87
- package/dist/io/ooxml/ns.d.ts +2 -0
- package/dist/io/ooxml/ns.js +26 -21
- package/dist/io/ooxml/numbering.js +290 -290
- package/dist/io/ooxml/opc-write.js +171 -171
- package/dist/io/ooxml/provenance.js +93 -93
- package/dist/io/ooxml/recipe.js +708 -708
- package/dist/io/ooxml/relationships.js +59 -59
- package/dist/io/ooxml/run-fonts.d.ts +89 -0
- package/dist/io/ooxml/run-fonts.js +378 -0
- package/dist/io/ooxml/slots.d.ts +4 -0
- package/dist/io/ooxml/slots.js +582 -581
- package/dist/io/ooxml/styles-resolve.js +257 -257
- package/dist/io/ooxml/units.js +43 -43
- package/dist/io/ooxml/value.js +120 -120
- package/dist/io/ooxml/xml-scan.js +616 -616
- package/dist/io/ooxml/zip-container.js +228 -228
- package/dist/io/ooxml/zip-reject.js +106 -106
- package/dist/measure/placement.js +83 -83
- package/dist/measure/report.js +75 -75
- package/dist/model/ids.js +70 -70
- package/dist/model/normalize.js +146 -146
- package/dist/model/position.js +283 -283
- package/dist/model/transaction.js +209 -209
- package/dist/platform/capabilities.js +25 -25
- package/dist/react/index.js +21 -21
- package/dist/selection/controller.js +315 -315
- package/dist/selection/dom-map.js +334 -334
- package/dist/state/editor-state.js +109 -109
- package/dist/styles.css +66 -66
- package/package.json +94 -93
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
import { createRun, locateParagraph, paragraphText } from "../model/node.js";
|
|
2
|
-
import {
|
|
3
|
-
collapsedSelection,
|
|
4
|
-
containerIdOf,
|
|
5
|
-
createPosition,
|
|
6
|
-
isCollapsed,
|
|
7
|
-
paragraphIdOf,
|
|
8
|
-
selectionRange,
|
|
9
|
-
withOffset,
|
|
10
|
-
} from "../model/position.js";
|
|
11
|
-
import { insertStep, splitParagraphStep } from "../model/step.js";
|
|
12
|
-
import { createTransaction } from "../model/transaction.js";
|
|
13
|
-
import { marksAt } from "./marks.js";
|
|
14
|
-
import { deleteRange } from "./text.js";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Plain-text clipboard commands. ARCHITECTURE.md §2: the browser's HTML is
|
|
18
|
-
* never the data model, which cuts the other way too — we never *write* HTML
|
|
19
|
-
* to the clipboard either. Everything here is `text/plain`, in and out.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Pastes pre-split lines at the cursor, replacing the selection first in the
|
|
24
|
-
* same transaction — same single-transaction rule as `insertText` (§2.5):
|
|
25
|
-
* pasting over a selection is one user action, not two.
|
|
26
|
-
*
|
|
27
|
-
* Lines after the first each start a new paragraph, exactly as if the user
|
|
28
|
-
* had typed the text with Enter between lines. Splitting on `\r\n|\r|\n` is
|
|
29
|
-
* the caller's job (`input/clipboard.js`) — this command only knows about
|
|
30
|
-
* already-separated lines, so it stays testable without a `ClipboardEvent`.
|
|
31
|
-
*
|
|
32
|
-
* @param {import("../state/editor-state.js").EditorState} state
|
|
33
|
-
* @param {string[]} lines
|
|
34
|
-
* @param {import("../model/ids.js").IdFactory} ids
|
|
35
|
-
* @returns {import("../model/transaction.js").Transaction | null}
|
|
36
|
-
*/
|
|
37
|
-
export function pasteText(state, lines, ids) {
|
|
38
|
-
if (lines.length === 0 || (lines.length === 1 && lines[0] === "")) {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const tr = createTransaction(state.doc, state.selection, "paste");
|
|
43
|
-
let at = isCollapsed(state.selection) ? state.selection.head : deleteRange(tr, state.selection);
|
|
44
|
-
|
|
45
|
-
const marks = state.storedMarks ?? marksAt(tr.doc, at);
|
|
46
|
-
|
|
47
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
48
|
-
if (lines[i] !== "") {
|
|
49
|
-
tr.step(insertStep(at, [createRun(lines[i], marks)]));
|
|
50
|
-
at = withOffset(at, at.offset + lines[i].length);
|
|
51
|
-
}
|
|
52
|
-
if (i < lines.length - 1) {
|
|
53
|
-
const newParagraphId = ids.next();
|
|
54
|
-
tr.step(splitParagraphStep(at, newParagraphId));
|
|
55
|
-
// The pasted paragraphs stay inside the container the cursor was in —
|
|
56
|
-
// pasting multi-line text never spills into the next box.
|
|
57
|
-
at = createPosition(containerIdOf(at), newParagraphId, 0);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
tr.setSelection(collapsedSelection(at));
|
|
62
|
-
return tr.build();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* The selected text as a plain string, paragraphs joined by `\n`. Used by
|
|
67
|
-
* `copy` and `cut` alike — cut is this plus a deletion, not a different
|
|
68
|
-
* serialisation.
|
|
69
|
-
* @param {import("../model/node.js").Doc} doc
|
|
70
|
-
* @param {import("../model/position.js").Selection} selection
|
|
71
|
-
* @returns {string}
|
|
72
|
-
*/
|
|
73
|
-
export function serializeToText(doc, selection) {
|
|
74
|
-
const range = selectionRange(doc, selection);
|
|
75
|
-
if (range === null) {
|
|
76
|
-
return "";
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const fromAt = locateParagraph(doc, paragraphIdOf(range.from));
|
|
80
|
-
const toAt = locateParagraph(doc, paragraphIdOf(range.to));
|
|
81
|
-
if (fromAt === null || toAt === null || fromAt.containerIndex !== toAt.containerIndex) {
|
|
82
|
-
return "";
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const parts = [];
|
|
86
|
-
for (let i = fromAt.paraIndex; i <= toAt.paraIndex; i += 1) {
|
|
87
|
-
const text = paragraphText(fromAt.container.content[i]);
|
|
88
|
-
const start = i === fromAt.paraIndex ? range.from.offset : 0;
|
|
89
|
-
const end = i === toAt.paraIndex ? range.to.offset : text.length;
|
|
90
|
-
parts.push(text.slice(start, end));
|
|
91
|
-
}
|
|
92
|
-
return parts.join("\n");
|
|
93
|
-
}
|
|
1
|
+
import { createRun, locateParagraph, paragraphText } from "../model/node.js";
|
|
2
|
+
import {
|
|
3
|
+
collapsedSelection,
|
|
4
|
+
containerIdOf,
|
|
5
|
+
createPosition,
|
|
6
|
+
isCollapsed,
|
|
7
|
+
paragraphIdOf,
|
|
8
|
+
selectionRange,
|
|
9
|
+
withOffset,
|
|
10
|
+
} from "../model/position.js";
|
|
11
|
+
import { insertStep, splitParagraphStep } from "../model/step.js";
|
|
12
|
+
import { createTransaction } from "../model/transaction.js";
|
|
13
|
+
import { marksAt } from "./marks.js";
|
|
14
|
+
import { deleteRange } from "./text.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Plain-text clipboard commands. ARCHITECTURE.md §2: the browser's HTML is
|
|
18
|
+
* never the data model, which cuts the other way too — we never *write* HTML
|
|
19
|
+
* to the clipboard either. Everything here is `text/plain`, in and out.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Pastes pre-split lines at the cursor, replacing the selection first in the
|
|
24
|
+
* same transaction — same single-transaction rule as `insertText` (§2.5):
|
|
25
|
+
* pasting over a selection is one user action, not two.
|
|
26
|
+
*
|
|
27
|
+
* Lines after the first each start a new paragraph, exactly as if the user
|
|
28
|
+
* had typed the text with Enter between lines. Splitting on `\r\n|\r|\n` is
|
|
29
|
+
* the caller's job (`input/clipboard.js`) — this command only knows about
|
|
30
|
+
* already-separated lines, so it stays testable without a `ClipboardEvent`.
|
|
31
|
+
*
|
|
32
|
+
* @param {import("../state/editor-state.js").EditorState} state
|
|
33
|
+
* @param {string[]} lines
|
|
34
|
+
* @param {import("../model/ids.js").IdFactory} ids
|
|
35
|
+
* @returns {import("../model/transaction.js").Transaction | null}
|
|
36
|
+
*/
|
|
37
|
+
export function pasteText(state, lines, ids) {
|
|
38
|
+
if (lines.length === 0 || (lines.length === 1 && lines[0] === "")) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const tr = createTransaction(state.doc, state.selection, "paste");
|
|
43
|
+
let at = isCollapsed(state.selection) ? state.selection.head : deleteRange(tr, state.selection);
|
|
44
|
+
|
|
45
|
+
const marks = state.storedMarks ?? marksAt(tr.doc, at);
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
48
|
+
if (lines[i] !== "") {
|
|
49
|
+
tr.step(insertStep(at, [createRun(lines[i], marks)]));
|
|
50
|
+
at = withOffset(at, at.offset + lines[i].length);
|
|
51
|
+
}
|
|
52
|
+
if (i < lines.length - 1) {
|
|
53
|
+
const newParagraphId = ids.next();
|
|
54
|
+
tr.step(splitParagraphStep(at, newParagraphId));
|
|
55
|
+
// The pasted paragraphs stay inside the container the cursor was in —
|
|
56
|
+
// pasting multi-line text never spills into the next box.
|
|
57
|
+
at = createPosition(containerIdOf(at), newParagraphId, 0);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
tr.setSelection(collapsedSelection(at));
|
|
62
|
+
return tr.build();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The selected text as a plain string, paragraphs joined by `\n`. Used by
|
|
67
|
+
* `copy` and `cut` alike — cut is this plus a deletion, not a different
|
|
68
|
+
* serialisation.
|
|
69
|
+
* @param {import("../model/node.js").Doc} doc
|
|
70
|
+
* @param {import("../model/position.js").Selection} selection
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
export function serializeToText(doc, selection) {
|
|
74
|
+
const range = selectionRange(doc, selection);
|
|
75
|
+
if (range === null) {
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const fromAt = locateParagraph(doc, paragraphIdOf(range.from));
|
|
80
|
+
const toAt = locateParagraph(doc, paragraphIdOf(range.to));
|
|
81
|
+
if (fromAt === null || toAt === null || fromAt.containerIndex !== toAt.containerIndex) {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const parts = [];
|
|
86
|
+
for (let i = fromAt.paraIndex; i <= toAt.paraIndex; i += 1) {
|
|
87
|
+
const text = paragraphText(fromAt.container.content[i]);
|
|
88
|
+
const start = i === fromAt.paraIndex ? range.from.offset : 0;
|
|
89
|
+
const end = i === toAt.paraIndex ? range.to.offset : text.length;
|
|
90
|
+
parts.push(text.slice(start, end));
|
|
91
|
+
}
|
|
92
|
+
return parts.join("\n");
|
|
93
|
+
}
|
|
@@ -1,112 +1,112 @@
|
|
|
1
|
-
// Backspace must remove a whole grapheme cluster, never a UTF-16 code unit.
|
|
2
|
-
// "👍" is two code units; "e" plus a combining acute is two code points; Turkish
|
|
3
|
-
// text composed with a dead key can be either. Deleting half of one corrupts the
|
|
4
|
-
// document.
|
|
5
|
-
//
|
|
6
|
-
// The input layer prefers the browser's own answer from `getTargetRanges()`.
|
|
7
|
-
// This module is the fallback chain behind that: Intl.Segmenter, then surrogate
|
|
8
|
-
// pair handling on its own.
|
|
9
|
-
|
|
10
|
-
/** @type {Intl.Segmenter | null | undefined} */
|
|
11
|
-
let cachedSegmenter;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @returns {Intl.Segmenter | null}
|
|
15
|
-
*/
|
|
16
|
-
function graphemeSegmenter() {
|
|
17
|
-
if (cachedSegmenter !== undefined) {
|
|
18
|
-
return cachedSegmenter;
|
|
19
|
-
}
|
|
20
|
-
cachedSegmenter =
|
|
21
|
-
typeof Intl !== "undefined" && typeof Intl.Segmenter === "function"
|
|
22
|
-
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
|
23
|
-
: null;
|
|
24
|
-
return cachedSegmenter;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Start index of the grapheme that ends at `offset`.
|
|
29
|
-
* @param {string} text
|
|
30
|
-
* @param {number} offset
|
|
31
|
-
* @returns {number}
|
|
32
|
-
*/
|
|
33
|
-
export function previousGraphemeStart(text, offset) {
|
|
34
|
-
if (offset <= 0) {
|
|
35
|
-
return 0;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const segmenter = graphemeSegmenter();
|
|
39
|
-
if (segmenter === null) {
|
|
40
|
-
return previousCodePointStart(text, offset);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
let start = 0;
|
|
44
|
-
for (const { index } of segmenter.segment(text)) {
|
|
45
|
-
if (index >= offset) {
|
|
46
|
-
break;
|
|
47
|
-
}
|
|
48
|
-
start = index;
|
|
49
|
-
}
|
|
50
|
-
return start;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* End index of the grapheme that starts at `offset`.
|
|
55
|
-
* @param {string} text
|
|
56
|
-
* @param {number} offset
|
|
57
|
-
* @returns {number}
|
|
58
|
-
*/
|
|
59
|
-
export function nextGraphemeEnd(text, offset) {
|
|
60
|
-
if (offset >= text.length) {
|
|
61
|
-
return text.length;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const segmenter = graphemeSegmenter();
|
|
65
|
-
if (segmenter === null) {
|
|
66
|
-
return nextCodePointEnd(text, offset);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
for (const { index, segment } of segmenter.segment(text)) {
|
|
70
|
-
const end = index + segment.length;
|
|
71
|
-
if (end > offset) {
|
|
72
|
-
return end;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return text.length;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Surrogate-aware fallback. Handles astral characters but not combining marks —
|
|
80
|
-
* it exists only for engines without `Intl.Segmenter`.
|
|
81
|
-
* @param {string} text
|
|
82
|
-
* @param {number} offset
|
|
83
|
-
* @returns {number}
|
|
84
|
-
*/
|
|
85
|
-
export function previousCodePointStart(text, offset) {
|
|
86
|
-
let index = offset - 1;
|
|
87
|
-
if (index > 0) {
|
|
88
|
-
const code = text.charCodeAt(index);
|
|
89
|
-
const previous = text.charCodeAt(index - 1);
|
|
90
|
-
if (code >= 0xdc00 && code <= 0xdfff && previous >= 0xd800 && previous <= 0xdbff) {
|
|
91
|
-
index -= 1;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return Math.max(0, index);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* @param {string} text
|
|
99
|
-
* @param {number} offset
|
|
100
|
-
* @returns {number}
|
|
101
|
-
*/
|
|
102
|
-
export function nextCodePointEnd(text, offset) {
|
|
103
|
-
let index = offset + 1;
|
|
104
|
-
if (index < text.length) {
|
|
105
|
-
const code = text.charCodeAt(offset);
|
|
106
|
-
const next = text.charCodeAt(index);
|
|
107
|
-
if (code >= 0xd800 && code <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) {
|
|
108
|
-
index += 1;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return Math.min(text.length, index);
|
|
112
|
-
}
|
|
1
|
+
// Backspace must remove a whole grapheme cluster, never a UTF-16 code unit.
|
|
2
|
+
// "👍" is two code units; "e" plus a combining acute is two code points; Turkish
|
|
3
|
+
// text composed with a dead key can be either. Deleting half of one corrupts the
|
|
4
|
+
// document.
|
|
5
|
+
//
|
|
6
|
+
// The input layer prefers the browser's own answer from `getTargetRanges()`.
|
|
7
|
+
// This module is the fallback chain behind that: Intl.Segmenter, then surrogate
|
|
8
|
+
// pair handling on its own.
|
|
9
|
+
|
|
10
|
+
/** @type {Intl.Segmenter | null | undefined} */
|
|
11
|
+
let cachedSegmenter;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @returns {Intl.Segmenter | null}
|
|
15
|
+
*/
|
|
16
|
+
function graphemeSegmenter() {
|
|
17
|
+
if (cachedSegmenter !== undefined) {
|
|
18
|
+
return cachedSegmenter;
|
|
19
|
+
}
|
|
20
|
+
cachedSegmenter =
|
|
21
|
+
typeof Intl !== "undefined" && typeof Intl.Segmenter === "function"
|
|
22
|
+
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
|
23
|
+
: null;
|
|
24
|
+
return cachedSegmenter;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Start index of the grapheme that ends at `offset`.
|
|
29
|
+
* @param {string} text
|
|
30
|
+
* @param {number} offset
|
|
31
|
+
* @returns {number}
|
|
32
|
+
*/
|
|
33
|
+
export function previousGraphemeStart(text, offset) {
|
|
34
|
+
if (offset <= 0) {
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const segmenter = graphemeSegmenter();
|
|
39
|
+
if (segmenter === null) {
|
|
40
|
+
return previousCodePointStart(text, offset);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let start = 0;
|
|
44
|
+
for (const { index } of segmenter.segment(text)) {
|
|
45
|
+
if (index >= offset) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
start = index;
|
|
49
|
+
}
|
|
50
|
+
return start;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* End index of the grapheme that starts at `offset`.
|
|
55
|
+
* @param {string} text
|
|
56
|
+
* @param {number} offset
|
|
57
|
+
* @returns {number}
|
|
58
|
+
*/
|
|
59
|
+
export function nextGraphemeEnd(text, offset) {
|
|
60
|
+
if (offset >= text.length) {
|
|
61
|
+
return text.length;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const segmenter = graphemeSegmenter();
|
|
65
|
+
if (segmenter === null) {
|
|
66
|
+
return nextCodePointEnd(text, offset);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const { index, segment } of segmenter.segment(text)) {
|
|
70
|
+
const end = index + segment.length;
|
|
71
|
+
if (end > offset) {
|
|
72
|
+
return end;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return text.length;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Surrogate-aware fallback. Handles astral characters but not combining marks —
|
|
80
|
+
* it exists only for engines without `Intl.Segmenter`.
|
|
81
|
+
* @param {string} text
|
|
82
|
+
* @param {number} offset
|
|
83
|
+
* @returns {number}
|
|
84
|
+
*/
|
|
85
|
+
export function previousCodePointStart(text, offset) {
|
|
86
|
+
let index = offset - 1;
|
|
87
|
+
if (index > 0) {
|
|
88
|
+
const code = text.charCodeAt(index);
|
|
89
|
+
const previous = text.charCodeAt(index - 1);
|
|
90
|
+
if (code >= 0xdc00 && code <= 0xdfff && previous >= 0xd800 && previous <= 0xdbff) {
|
|
91
|
+
index -= 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return Math.max(0, index);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {string} text
|
|
99
|
+
* @param {number} offset
|
|
100
|
+
* @returns {number}
|
|
101
|
+
*/
|
|
102
|
+
export function nextCodePointEnd(text, offset) {
|
|
103
|
+
let index = offset + 1;
|
|
104
|
+
if (index < text.length) {
|
|
105
|
+
const code = text.charCodeAt(offset);
|
|
106
|
+
const next = text.charCodeAt(index);
|
|
107
|
+
if (code >= 0xd800 && code <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) {
|
|
108
|
+
index += 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return Math.min(text.length, index);
|
|
112
|
+
}
|