@portone/docx-editor 0.1.0 → 0.1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @portone/docx-editor
2
2
 
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#36](https://github.com/portone-io/docx-editor/pull/36) [`ab518b9`](https://github.com/portone-io/docx-editor/commit/ab518b9c551afc1d647ad612b265ec5895601240) Thanks [@Deea222](https://github.com/Deea222)! - Read two Unicode spellings of one font name as the same font.
8
+
9
+ A name a document writes down can be composed or decomposed (NFC or NFD) while the same name elsewhere in the document, or among the toolbar's presets, is written the other way.
10
+ Those spellings used to compare unequal, so the font dropdown listed the same font twice and a selection that all carried one font could report as mixed and blank the dropdown.
11
+ Names are now compared in composed form, and the spelling the document wrote is what stays stored, exported, and shown.
12
+
13
+ A list marker cut to its length cap is also cut between characters now, instead of possibly splitting an emoji or a combining sequence in half and drawing a replacement glyph.
14
+
3
15
  ## 0.1.0
4
16
 
5
17
  The first release.
@@ -5,6 +5,7 @@ import {
5
5
  toRunFormat
6
6
  } from "../../../model/format.js";
7
7
  import {
8
+ comparableFontName,
8
9
  DEFAULT_FONT_FALLBACKS
9
10
  } from "../../../styles/fontStack.js";
10
11
  import {
@@ -40,22 +41,30 @@ function defaultFontName(state, fontFallbacks) {
40
41
  function activeFontFamily(state, fontFallbacks = DEFAULT_FONT_FALLBACKS) {
41
42
  const names = activePieces(state).map((target) => fontNameOf(target.format));
42
43
  const first = names[0] ?? null;
43
- if (names.some((name) => name !== first)) return { kind: "mixed" };
44
+ const keys = names.map(
45
+ (name) => name === null ? null : comparableFontName(name)
46
+ );
47
+ if (keys.some((key) => key !== keys[0])) return { kind: "mixed" };
44
48
  if (first !== null) return { kind: "font", name: first };
45
49
  return { kind: "default", name: defaultFontName(state, fontFallbacks) };
46
50
  }
47
51
  function documentFontNames(doc, defaults) {
48
- const names = new Set(fontNamesOf(defaults.fontFamily));
52
+ const names = /* @__PURE__ */ new Map();
53
+ const collect = (name) => {
54
+ const comparable = comparableFontName(name);
55
+ if (!names.has(comparable)) names.set(comparable, name);
56
+ };
57
+ for (const name of fontNamesOf(defaults.fontFamily)) collect(name);
49
58
  doc.descendants((node) => {
50
59
  const mark = runMarkOf(node.marks);
51
60
  for (const name of fontNamesOf(
52
61
  toRunFormat(mark?.attrs.format)?.fontFamily
53
62
  )) {
54
- names.add(name);
63
+ collect(name);
55
64
  }
56
65
  return true;
57
66
  });
58
- return Array.from(names).sort((a, b) => a.localeCompare(b));
67
+ return Array.from(names.values()).sort((a, b) => a.localeCompare(b));
59
68
  }
60
69
  function activeTextColor(state) {
61
70
  return sharedValue(
@@ -70,6 +70,18 @@ function advance(counters, list, ilvl, start) {
70
70
  if (deeper > ilvl) counters.delete(deeper);
71
71
  }
72
72
  }
73
+ var MARKER_SEGMENTER = new Intl.Segmenter();
74
+ function capped(shape) {
75
+ if (shape.length <= MAX_MARKER_CHARS) return shape;
76
+ let text = "";
77
+ for (const { segment } of MARKER_SEGMENTER.segment(
78
+ shape.slice(0, MAX_MARKER_CHARS * 2)
79
+ )) {
80
+ if (text.length + segment.length > MAX_MARKER_CHARS) break;
81
+ text += segment;
82
+ }
83
+ return text;
84
+ }
73
85
  function computeMarkers(paragraphs, numbering) {
74
86
  const countersByList = /* @__PURE__ */ new Map();
75
87
  return paragraphs.map((ref) => {
@@ -84,7 +96,7 @@ function computeMarkers(paragraphs, numbering) {
84
96
  }
85
97
  advance(counters, list, ref.ilvl, level.start);
86
98
  const shape = level.format === "bullet" ? level.text : fillLevels(level.text, list, counters);
87
- const text = shape.slice(0, MAX_MARKER_CHARS);
99
+ const text = capped(shape);
88
100
  return text ? { text, indent: levelIndentPt(level.indent) } : null;
89
101
  });
90
102
  }
@@ -36,6 +36,15 @@ export interface FontFallbacks {
36
36
  * own set (`fontFallbacks` on `DocxEditor`).
37
37
  */
38
38
  export declare const DEFAULT_FONT_FALLBACKS: FontFallbacks;
39
+ /**
40
+ * The form a name is compared in. Quotes, letter case, and Unicode normalization all vary
41
+ * from document to document, so two spellings of one name have to compare equal.
42
+ *
43
+ * NFC only. An NFK* form is lossy and would fold the fullwidth Latin of a name such as
44
+ * `DFKaiShu` into ASCII. The result is a comparison key and is never
45
+ * written into a document or shown: the original spelling is what gets stored and exported.
46
+ */
47
+ export declare function comparableFontName(name: string): string;
39
48
  /**
40
49
  * Whether this font name belongs to a script written in Han characters, kana or
41
50
  * Hangul.
@@ -189,15 +189,15 @@ var DEFAULT_FONT_FALLBACKS = {
189
189
  defaultStack: "Arial, Helvetica, sans-serif",
190
190
  defaultFontName: "Arial"
191
191
  };
192
- function comparableName(name) {
193
- return name.trim().replace(/^["']|["']$/g, "").toLowerCase();
192
+ function comparableFontName(name) {
193
+ return name.trim().replace(/^["']|["']$/g, "").toLowerCase().normalize("NFC");
194
194
  }
195
195
  var EAST_ASIAN_CHARACTER = /[\u1100-\u11ff\u3000-\u30ff\u3130-\u318f\u31f0-\u31ff\u3400-\u4dbf\u4e00-\u9fff\ua960-\ua97f\uac00-\ud7ff\uf900-\ufaff\uff00-\uffef\u{20000}-\u{2fa1f}]/u;
196
196
  var eastAsianNames = new Set(
197
- EAST_ASIAN_GROUPS.flatMap((group) => group.names.map(comparableName))
197
+ EAST_ASIAN_GROUPS.flatMap((group) => group.names.map(comparableFontName))
198
198
  );
199
199
  function isEastAsianFontName(name) {
200
- return EAST_ASIAN_CHARACTER.test(name) || eastAsianNames.has(comparableName(name));
200
+ return EAST_ASIAN_CHARACTER.test(name) || eastAsianNames.has(comparableFontName(name));
201
201
  }
202
202
  var stacksByFallbacks = /* @__PURE__ */ new WeakMap();
203
203
  function stackByName(fallbacks) {
@@ -206,7 +206,7 @@ function stackByName(fallbacks) {
206
206
  const stacks = /* @__PURE__ */ new Map();
207
207
  for (const group of fallbacks.groups) {
208
208
  for (const name of group.names)
209
- stacks.set(comparableName(name), group.stack);
209
+ stacks.set(comparableFontName(name), group.stack);
210
210
  }
211
211
  stacksByFallbacks.set(fallbacks, stacks);
212
212
  return stacks;
@@ -215,7 +215,7 @@ function matchedStacks(cssNames, fallbacks) {
215
215
  const stacks = stackByName(fallbacks);
216
216
  const matched = [];
217
217
  for (const name of cssNames.split(",")) {
218
- const stack = stacks.get(comparableName(name));
218
+ const stack = stacks.get(comparableFontName(name));
219
219
  if (stack !== void 0 && !matched.includes(stack)) matched.push(stack);
220
220
  }
221
221
  return matched;
@@ -235,12 +235,12 @@ var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
235
235
  "emoji"
236
236
  ]);
237
237
  function isGenericFamily(name) {
238
- return GENERIC_FAMILIES.has(comparableName(name));
238
+ return GENERIC_FAMILIES.has(comparableFontName(name));
239
239
  }
240
240
  function distinctNames(names) {
241
241
  const seen = /* @__PURE__ */ new Set();
242
242
  return names.filter((name) => {
243
- const comparable = comparableName(name);
243
+ const comparable = comparableFontName(name);
244
244
  if (seen.has(comparable)) return false;
245
245
  seen.add(comparable);
246
246
  return true;
@@ -257,6 +257,7 @@ function withFontFallback(cssNames, fallbacks = DEFAULT_FONT_FALLBACKS) {
257
257
  }
258
258
  export {
259
259
  DEFAULT_FONT_FALLBACKS,
260
+ comparableFontName,
260
261
  isEastAsianFontName,
261
262
  withFontFallback
262
263
  };
@@ -3,7 +3,10 @@ import {
3
3
  setFontFamily
4
4
  } from "../editor/commands/formattingCommands.js";
5
5
  import { editorClassNames } from "../styles/classNames.js";
6
- import { withFontFallback } from "../styles/fontStack.js";
6
+ import {
7
+ comparableFontName,
8
+ withFontFallback
9
+ } from "../styles/fontStack.js";
7
10
  import { DEFAULT_FONTS } from "../styles/fonts.js";
8
11
  import { Tooltip } from "./Tooltip.js";
9
12
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -11,11 +14,21 @@ var LABEL = "Font";
11
14
  var CLEAR = ";default";
12
15
  var MIXED = "";
13
16
  function optionNames(current, fonts, documentFonts) {
14
- const names = [...fonts, ...documentFonts];
15
- if (current.kind !== "mixed" && !names.includes(current.name)) {
16
- names.push(current.name);
17
+ const names = [
18
+ ...fonts,
19
+ ...documentFonts,
20
+ ...current.kind === "mixed" ? [] : [current.name]
21
+ ];
22
+ const byComparable = /* @__PURE__ */ new Map();
23
+ for (const name of names) {
24
+ const comparable = comparableFontName(name);
25
+ if (!byComparable.has(comparable)) byComparable.set(comparable, name);
17
26
  }
18
- return Array.from(new Set(names));
27
+ return Array.from(byComparable.values());
28
+ }
29
+ function selectedValue(options, name) {
30
+ const comparable = comparableFontName(name);
31
+ return options.find((option) => comparableFontName(option) === comparable) ?? name;
19
32
  }
20
33
  function FontFamilySelect({
21
34
  current,
@@ -25,7 +38,8 @@ function FontFamilySelect({
25
38
  disabled,
26
39
  run
27
40
  }) {
28
- const value = current.kind === "mixed" ? MIXED : current.name;
41
+ const options = optionNames(current, fonts, documentFonts);
42
+ const value = current.kind === "mixed" ? MIXED : selectedValue(options, current.name);
29
43
  return /* @__PURE__ */ jsx(Tooltip, { label: LABEL, children: /* @__PURE__ */ jsxs(
30
44
  "select",
31
45
  {
@@ -41,7 +55,7 @@ function FontFamilySelect({
41
55
  children: [
42
56
  current.kind === "mixed" && /* @__PURE__ */ jsx("option", { value: MIXED }),
43
57
  /* @__PURE__ */ jsx("option", { value: CLEAR, children: "Default" }),
44
- optionNames(current, fonts, documentFonts).map((name) => /* @__PURE__ */ jsx(
58
+ options.map((name) => /* @__PURE__ */ jsx(
45
59
  "option",
46
60
  {
47
61
  value: name,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portone/docx-editor",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "A simple DOCX editor for React, built directly on OOXML.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",