@yanqirenshi/model.sitemap 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/README.md CHANGED
@@ -25,8 +25,8 @@ d3.sitemap は、このパッケージの既定値(ラベル・枠線・背景
25
25
 
26
26
  model.sitemap・colonoscope の間に横の依存はありません。組み合わせるのは利用側だけです。
27
27
  これは [Foolsgolds/Assholes#138](https://github.com/Foolsgolds/Assholes/issues/138) の派生([#145](https://github.com/Foolsgolds/Assholes/issues/145))です。
28
- ノードと線の図なので [model.unchi](../model.unchi) と形が近く、`check` / `movePatch` などは model.unchi から複製しています
29
- (graph 系の共通部分の切り出しは #146 のあとに判断します)。
28
+ ノードと線の図なので [model.unchi](../model.unchi) と形が近く、`check` / `movePatch` / `mergePatch` と入力の読み取り部品は
29
+ 共通部品 [model.core](../model.core) のものに model.sitemap の型を付けて公開しています([#151](https://github.com/Foolsgolds/Assholes/issues/151))。
30
30
 
31
31
  ## レコードの形
32
32
 
@@ -1 +1 @@
1
- {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5F,wCAAwC;AACxC,wBAAgB,OAAO,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,iBAAiB,EAAE,CAEvE;AAgBD;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,SAAK,GAAG,MAAM,EAAE,CAa/D;AAED,qCAAqC;AACrC,wBAAgB,QAAQ,CAAC,CAAC,SAAS,aAAa,EAAE,IAAI,EAAE,SAAS,CAAC,EAAE,GAAG,SAAS,EAAE,EAAE,EAAE,SAAS,GAAG,CAAC,GAAG,IAAI,CAazG"}
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5F,wCAAwC;AACxC,wBAAgB,OAAO,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,iBAAiB,EAAE,CAEvE;AAED;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,SAAK,GAAG,MAAM,EAAE,CAE/D;AAED,qCAAqC;AACrC,wBAAgB,QAAQ,CAAC,CAAC,SAAS,aAAa,EAAE,IAAI,EAAE,SAAS,CAAC,EAAE,GAAG,SAAS,EAAE,EAAE,EAAE,SAAS,GAAG,CAAC,GAAG,IAAI,CAEzG"}
package/dist/check.js CHANGED
@@ -2,21 +2,13 @@
2
2
  // スキーマ(schema.ts)に合っているデータを渡す。戻り値はエラーのメッセージの配列(空なら問題なし)。
3
3
  //
4
4
  // iHub の server/validation/rules.mjs の checkGraph('sitemap', data) と同じ検査・同じ文言。
5
- // model.unchi の check.ts の複製(graph 系の共通化は #146 のあとに判断する — #145 の「やらないこと」)。
5
+ // 検査そのものは graph 系(unchi / sitemap / deployment)で共通なので model.core の checkGraph にあり、
6
+ // ここでは model.sitemap の型を付けて公開する(#151)。
6
7
  // メッセージの先頭はコレクション名で、prefix を渡すと 'sitemap/nodes: ...' のように付けられる。
8
+ import { checkGraph, flattenNodes, findById as findInList } from '@yanqirenshi/model.core';
7
9
  /** 入れ子のノード(children)を平らにする(親 → 子 の順) */
8
10
  export function flatten(nodes) {
9
- return nodes.flatMap(node => [node, ...flatten(node.children || [])]);
10
- }
11
- // list の中で id が重複していればエラーにする。id の集合を返す。
12
- function checkUnique(errors, label, list) {
13
- const values = new Set();
14
- for (const d of list) {
15
- if (values.has(d.id))
16
- errors.push(`${label}: id が重複しています。(id=${String(d.id)})`);
17
- values.add(d.id);
18
- }
19
- return values;
11
+ return flattenNodes(nodes);
20
12
  }
21
13
  /**
22
14
  * 図全体を検査する。
@@ -27,24 +19,9 @@ function checkUnique(errors, label, list) {
27
19
  * @param prefix メッセージのコレクション名の前に付ける文字列(例: 'sitemap/')
28
20
  */
29
21
  export function check(data, prefix = '') {
30
- const errors = [];
31
- const node_ids = checkUnique(errors, `${prefix}nodes`, flatten(data.nodes || []));
32
- checkUnique(errors, `${prefix}edges`, data.edges || []);
33
- for (const edge of data.edges || [])
34
- for (const end of ['from', 'to'])
35
- if (!edge[end] || !node_ids.has(edge[end].id))
36
- errors.push(`${prefix}edges: ${end}.id のノードが見つかりません。(id=${String(edge.id)}, ${end}.id=${String(edge[end]?.id)})`);
37
- return errors;
22
+ return checkGraph(data, prefix);
38
23
  }
39
24
  /** ノード(子を含む)か線を id で探す(無ければ null) */
40
25
  export function findById(list, id) {
41
- for (const d of list || []) {
42
- if (d.id === id)
43
- return d;
44
- const children = d.children;
45
- const hit = children ? findById(children, id) : null;
46
- if (hit)
47
- return hit;
48
- }
49
- return null;
26
+ return findInList(list, id);
50
27
  }
package/dist/cjs/check.js CHANGED
@@ -3,25 +3,17 @@
3
3
  // スキーマ(schema.ts)に合っているデータを渡す。戻り値はエラーのメッセージの配列(空なら問題なし)。
4
4
  //
5
5
  // iHub の server/validation/rules.mjs の checkGraph('sitemap', data) と同じ検査・同じ文言。
6
- // model.unchi の check.ts の複製(graph 系の共通化は #146 のあとに判断する — #145 の「やらないこと」)。
6
+ // 検査そのものは graph 系(unchi / sitemap / deployment)で共通なので model.core の checkGraph にあり、
7
+ // ここでは model.sitemap の型を付けて公開する(#151)。
7
8
  // メッセージの先頭はコレクション名で、prefix を渡すと 'sitemap/nodes: ...' のように付けられる。
8
9
  Object.defineProperty(exports, "__esModule", { value: true });
9
10
  exports.flatten = flatten;
10
11
  exports.check = check;
11
12
  exports.findById = findById;
13
+ const model_core_1 = require("@yanqirenshi/model.core");
12
14
  /** 入れ子のノード(children)を平らにする(親 → 子 の順) */
13
15
  function flatten(nodes) {
14
- return nodes.flatMap(node => [node, ...flatten(node.children || [])]);
15
- }
16
- // list の中で id が重複していればエラーにする。id の集合を返す。
17
- function checkUnique(errors, label, list) {
18
- const values = new Set();
19
- for (const d of list) {
20
- if (values.has(d.id))
21
- errors.push(`${label}: id が重複しています。(id=${String(d.id)})`);
22
- values.add(d.id);
23
- }
24
- return values;
16
+ return (0, model_core_1.flattenNodes)(nodes);
25
17
  }
26
18
  /**
27
19
  * 図全体を検査する。
@@ -32,24 +24,9 @@ function checkUnique(errors, label, list) {
32
24
  * @param prefix メッセージのコレクション名の前に付ける文字列(例: 'sitemap/')
33
25
  */
34
26
  function check(data, prefix = '') {
35
- const errors = [];
36
- const node_ids = checkUnique(errors, `${prefix}nodes`, flatten(data.nodes || []));
37
- checkUnique(errors, `${prefix}edges`, data.edges || []);
38
- for (const edge of data.edges || [])
39
- for (const end of ['from', 'to'])
40
- if (!edge[end] || !node_ids.has(edge[end].id))
41
- errors.push(`${prefix}edges: ${end}.id のノードが見つかりません。(id=${String(edge.id)}, ${end}.id=${String(edge[end]?.id)})`);
42
- return errors;
27
+ return (0, model_core_1.checkGraph)(data, prefix);
43
28
  }
44
29
  /** ノード(子を含む)か線を id で探す(無ければ null) */
45
30
  function findById(list, id) {
46
- for (const d of list || []) {
47
- if (d.id === id)
48
- return d;
49
- const children = d.children;
50
- const hit = children ? findById(children, id) : null;
51
- if (hit)
52
- return hit;
53
- }
54
- return null;
31
+ return (0, model_core_1.findById)(list, id);
55
32
  }
package/dist/cjs/index.js CHANGED
@@ -8,7 +8,8 @@
8
8
  // 編集できる項目: fieldsOf(record) / NODE_FIELDS / EDGE_FIELDS
9
9
  // patch への変換: movePatch(node, move) / applyPatch(record, values) / mergePatch(record, patch)
10
10
  //
11
- // graph 系(unchi / sitemap / deployment)の共通部分はまだ切り出していない(#145 の「やらないこと」。#146 のあとに判断)。
11
+ // graph 系(unchi / sitemap / deployment)で共通の check / flatten / findById / movePatch / mergePatch / getByPath / isColor と
12
+ // 入力の読み取り部品は model.core(#151)のもので、ここでは model.sitemap の型を付けて公開している。
12
13
  Object.defineProperty(exports, "__esModule", { value: true });
13
14
  exports.isColor = exports.applyPatch = exports.movePatch = exports.mergePatch = exports.getByPath = exports.titleOf = exports.collectionOf = exports.ModelError = exports.fieldsOf = exports.EDGE_FIELDS = exports.NODE_FIELDS = exports.findById = exports.flatten = exports.check = exports.SCHEMAS = exports.EDGES_SCHEMA = exports.NODES_SCHEMA = exports.withDefaults = exports.degreeOf = exports.edgeStyle = exports.nodeStyle = exports.isEdge = exports.EDGE_DEFAULTS = exports.NODE_DEFAULTS = exports.LABEL_DEFAULTS = exports.NODE_STYLE_DEFAULTS = void 0;
14
15
  var defaults_js_1 = require("./defaults.js");
package/dist/cjs/patch.js CHANGED
@@ -4,25 +4,22 @@
4
4
  // 戻り値は { collection, id, patch }。利用側(iHub など)は図の種類を知らずにこれを保存すればよい。
5
5
  // 入力の検証に失敗したときは ModelError を投げる(メッセージは利用者にそのまま見せられる日本語)。
6
6
  // model.unchi の patch.ts を元にしている(ラベルは label.contents、リンクは link.url、マーカーと border.type は無い)。
7
+ // 読み取り部品(toColor など)と patch の道具は model.core にある(#151)。
7
8
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.ModelError = void 0;
9
+ exports.isColor = exports.mergePatch = exports.getByPath = exports.ModelError = void 0;
9
10
  exports.collectionOf = collectionOf;
10
11
  exports.titleOf = titleOf;
11
- exports.getByPath = getByPath;
12
- exports.mergePatch = mergePatch;
13
12
  exports.movePatch = movePatch;
14
- exports.isColor = isColor;
15
13
  exports.applyPatch = applyPatch;
14
+ const model_core_1 = require("@yanqirenshi/model.core");
16
15
  const defaults_js_1 = require("./defaults.js");
17
16
  const fields_js_1 = require("./fields.js");
18
- /** 入力の検証エラー。message は利用者向け */
19
- class ModelError extends Error {
20
- constructor(message) {
21
- super(message);
22
- this.name = 'ModelError';
23
- }
24
- }
25
- exports.ModelError = ModelError;
17
+ // model.core のものをそのまま公開する(利用側が model.sitemap だけを読めばよいように)
18
+ var model_core_2 = require("@yanqirenshi/model.core");
19
+ Object.defineProperty(exports, "ModelError", { enumerable: true, get: function () { return model_core_2.ModelError; } });
20
+ Object.defineProperty(exports, "getByPath", { enumerable: true, get: function () { return model_core_2.getByPath; } });
21
+ Object.defineProperty(exports, "mergePatch", { enumerable: true, get: function () { return model_core_2.mergePatch; } });
22
+ Object.defineProperty(exports, "isColor", { enumerable: true, get: function () { return model_core_2.isColor; } });
26
23
  /** レコードを置くコレクション */
27
24
  function collectionOf(record) {
28
25
  return (0, defaults_js_1.isEdge)(record) ? 'edges' : 'nodes';
@@ -34,128 +31,21 @@ function titleOf(record) {
34
31
  const contents = record.label && typeof record.label === 'object' ? record.label.contents : null;
35
32
  return typeof contents === 'string' && contents !== '' ? contents : String(record.id);
36
33
  }
37
- /** ドット区切りの path で値を取り出す */
38
- function getByPath(obj, path) {
39
- return path.split('.').reduce((acc, key) => (acc == null ? acc : acc[key]), obj);
40
- }
41
- /**
42
- * JSON Merge Patch をレコードに重ねたものを返す(入力は書き換えない)。
43
- * null の項目は除く。保存したあと、表示を先に更新するときに使う。
44
- */
45
- function mergePatch(record, patch) {
46
- const out = { ...record };
47
- for (const [key, value] of Object.entries(patch)) {
48
- if (value === null) {
49
- delete out[key];
50
- }
51
- else if (typeof value === 'object' && !Array.isArray(value)) {
52
- const base = out[key];
53
- out[key] = mergePatch(base && typeof base === 'object' && !Array.isArray(base) ? base : {}, value);
54
- }
55
- else {
56
- out[key] = value;
57
- }
58
- }
59
- return out;
60
- }
61
34
  /**
62
35
  * ドラッグで動かしたノードの位置を保存する patch。
63
36
  * 保存してある position に移動量を足し、整数に丸める(position が無いときは 0, 0 から)。
64
37
  * 子のノードの position は親の中での位置なので、親を動かしても子の patch は要らない。
65
38
  */
66
39
  function movePatch(node, move) {
67
- const base = node.position || { x: 0, y: 0 };
68
- return {
69
- collection: 'nodes',
70
- id: node.id,
71
- patch: {
72
- position: {
73
- x: Math.round(base.x + move.dx),
74
- y: Math.round(base.y + move.dy),
75
- },
76
- },
77
- };
78
- }
79
- // ---- 入力の読み取り ------------------------------------------------------------
80
- function toNumber(value) {
81
- const n = Number(value);
82
- return value !== '' && Number.isFinite(n) ? n : null;
40
+ return (0, model_core_1.movePatch)(node, move, 'nodes');
83
41
  }
84
- /**
85
- * 色として読める文字列か。
86
- * ブラウザでは CSS.supports('color', v) で判定する。Node など CSS の無い環境では、
87
- * #rgb / #rrggbb / #rrggbbaa・rgb() などの関数表記・英字だけの色名、を通す緩い判定にする。
88
- */
89
- function isColor(value) {
90
- if (value === '')
91
- return false;
92
- const css = globalThis.CSS;
93
- if (css && typeof css.supports === 'function')
94
- return css.supports('color', value);
95
- return /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(value)
96
- || /^(rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\(.+\)$/i.test(value)
97
- || /^[a-z]+$/i.test(value);
98
- }
99
- function toColor(value, label) {
100
- if (!isColor(value))
101
- throw new ModelError(`${label}は、色 (#ff0000 や red など) で入力してください。`);
102
- return value;
103
- }
104
- // 0 以上の数(枠線・線の太さ。0 は線なし)
105
- function toWidth(value, label) {
106
- const n = toNumber(value);
107
- if (n === null || n < 0)
108
- throw new ModelError(`${label}は、0 以上の数で入力してください。`);
109
- return n;
110
- }
111
- // 0 以上の整数(幅・高さ。0 は「子に合わせる」)
112
- function toLength(value, label) {
113
- const n = toNumber(value);
114
- if (n === null || n < 0)
115
- throw new ModelError(`${label}は 0 以上の数で入力してください。(0 で子に合わせる)`);
116
- return Math.round(n);
117
- }
118
- // 角度(0 以上 360 未満の整数に正規化)
119
- function toDegree(value, label) {
120
- const n = toNumber(value);
121
- if (n === null)
122
- throw new ModelError(`${label}は、角度 (数) で入力してください。`);
123
- return ((Math.round(n) % 360) + 360) % 360;
124
- }
125
- // 空にできる文字列。空なら null(JSON Merge Patch で項目を除く)
126
- function toOptionalText(value) {
127
- return value === '' ? null : value;
128
- }
129
- // patch の入れ子に値を置く(patch.border.color = v のように)
130
- function put(patch, path, value) {
131
- const keys = path.split('.');
132
- let cur = patch;
133
- for (const key of keys.slice(0, -1)) {
134
- if (!cur[key] || typeof cur[key] !== 'object')
135
- cur[key] = {};
136
- cur = cur[key];
137
- }
138
- cur[keys[keys.length - 1]] = value;
139
- }
140
- /**
141
- * インスペクタの入力のうち、図に描いている値(withDefaults で埋めた値)から変わった項目だけを返す。
142
- * 前後の空白は除く。読み取り専用の項目と、values に無い項目は含めない。
143
- */
42
+ // インスペクタの入力のうち、図に描いている値(withDefaults)から変わった項目だけ
144
43
  function changedValues(record, values) {
145
- const effective = (0, defaults_js_1.withDefaults)(record);
146
- const out = new Map();
147
- for (const field of (0, fields_js_1.fieldsOf)(record)) {
148
- if (field.type === 'readonly')
149
- continue;
150
- const value = values[field.path];
151
- if (value === undefined)
152
- continue;
153
- const current = getByPath(effective, field.path);
154
- if (value.trim() !== String(current ?? ''))
155
- out.set(field.path, value.trim());
156
- }
157
- return out;
44
+ return (0, model_core_1.changedValues)((0, defaults_js_1.withDefaults)(record), values, (0, fields_js_1.fieldsOf)(record));
158
45
  }
46
+ // 幅・高さは 0 以上(0 は「子に合わせる」)。枠線・線の太さは 0 以上(0 は線なし)
47
+ const LENGTH = { hint: '(0 で子に合わせる)' };
48
+ const WIDTH = { allowZero: true };
159
49
  // ノードの入力 → patch(label.contents / node_type / link.url / size / background / border)
160
50
  function nodePatch(node, values) {
161
51
  const patch = {};
@@ -164,33 +54,33 @@ function nodePatch(node, values) {
164
54
  case 'label.contents':
165
55
  // 名称は空にできない(画面の箱に文字が無くなる)
166
56
  if (value === '')
167
- throw new ModelError('名称を入力してください。');
168
- put(patch, path, value);
57
+ throw new model_core_1.ModelError('名称を入力してください。');
58
+ (0, model_core_1.put)(patch, path, value);
169
59
  break;
170
60
  case 'node_type':
171
- put(patch, path, toOptionalText(value));
61
+ (0, model_core_1.put)(patch, path, (0, model_core_1.toOptionalText)(value));
172
62
  break;
173
63
  case 'link.url':
174
64
  // URL を空にするとリンクごと除く(d3.sitemap は url の無い link を描かない)
175
65
  if (value === '')
176
66
  patch.link = null;
177
67
  else
178
- put(patch, path, value);
68
+ (0, model_core_1.put)(patch, path, value);
179
69
  break;
180
70
  case 'size.w':
181
- put(patch, path, toLength(value, '幅'));
71
+ (0, model_core_1.put)(patch, path, (0, model_core_1.toLength)(value, '幅', LENGTH));
182
72
  break;
183
73
  case 'size.h':
184
- put(patch, path, toLength(value, '高さ'));
74
+ (0, model_core_1.put)(patch, path, (0, model_core_1.toLength)(value, '高さ', LENGTH));
185
75
  break;
186
76
  case 'background.color':
187
- put(patch, path, toColor(value, '背景色'));
77
+ (0, model_core_1.put)(patch, path, (0, model_core_1.toColor)(value, '背景色'));
188
78
  break;
189
79
  case 'border.color':
190
- put(patch, path, toColor(value, '枠線の色'));
80
+ (0, model_core_1.put)(patch, path, (0, model_core_1.toColor)(value, '枠線の色'));
191
81
  break;
192
82
  case 'border.width':
193
- put(patch, path, toWidth(value, '枠線の太さ'));
83
+ (0, model_core_1.put)(patch, path, (0, model_core_1.toWidth)(value, '枠線の太さ', WIDTH));
194
84
  break;
195
85
  }
196
86
  }
@@ -204,13 +94,13 @@ function edgePatch(edge, values) {
204
94
  switch (path) {
205
95
  case 'from.position':
206
96
  case 'to.position':
207
- put(patch, path, toDegree(value, `${END_LABELS[path.split('.')[0]]}の位置`));
97
+ (0, model_core_1.put)(patch, path, (0, model_core_1.toDegree)(value, `${END_LABELS[path.split('.')[0]]}の位置`));
208
98
  break;
209
99
  case 'stroke.color':
210
- put(patch, path, toColor(value, '線の色'));
100
+ (0, model_core_1.put)(patch, path, (0, model_core_1.toColor)(value, '線の色'));
211
101
  break;
212
102
  case 'stroke.width':
213
- put(patch, path, toWidth(value, '線の太さ'));
103
+ (0, model_core_1.put)(patch, path, (0, model_core_1.toWidth)(value, '線の太さ', WIDTH));
214
104
  break;
215
105
  }
216
106
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAWA,YAAY,EACV,SAAS,EAAE,gBAAgB,EAAE,eAAe,EAAE,iBAAiB,EAAE,cAAc,EAAE,iBAAiB,EAAE,aAAa,EACjH,YAAY,EAAE,iBAAiB,EAAE,UAAU,EAAE,aAAa,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,EAAE,aAAa,GACvH,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,mBAAmB,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EACjE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,GACrD,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAYA,YAAY,EACV,SAAS,EAAE,gBAAgB,EAAE,eAAe,EAAE,iBAAiB,EAAE,cAAc,EAAE,iBAAiB,EAAE,aAAa,EACjH,YAAY,EAAE,iBAAiB,EAAE,UAAU,EAAE,aAAa,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,EAAE,aAAa,GACvH,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,mBAAmB,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EACjE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,GACrD,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -7,7 +7,8 @@
7
7
  // 編集できる項目: fieldsOf(record) / NODE_FIELDS / EDGE_FIELDS
8
8
  // patch への変換: movePatch(node, move) / applyPatch(record, values) / mergePatch(record, patch)
9
9
  //
10
- // graph 系(unchi / sitemap / deployment)の共通部分はまだ切り出していない(#145 の「やらないこと」。#146 のあとに判断)。
10
+ // graph 系(unchi / sitemap / deployment)で共通の check / flatten / findById / movePatch / mergePatch / getByPath / isColor と
11
+ // 入力の読み取り部品は model.core(#151)のもので、ここでは model.sitemap の型を付けて公開している。
11
12
  export { NODE_STYLE_DEFAULTS, LABEL_DEFAULTS, NODE_DEFAULTS, EDGE_DEFAULTS, isEdge, nodeStyle, edgeStyle, degreeOf, withDefaults, } from './defaults.js';
12
13
  export { NODES_SCHEMA, EDGES_SCHEMA, SCHEMAS } from './schema.js';
13
14
  export { check, flatten, findById } from './check.js';
package/dist/patch.d.ts CHANGED
@@ -1,31 +1,15 @@
1
- import type { MergePatch, SitemapChange, SitemapCollection, SitemapMove, SitemapNodeRecord, SitemapRecord, SitemapValues } from './types.js';
2
- /** 入力の検証エラー。message は利用者向け */
3
- export declare class ModelError extends Error {
4
- constructor(message: string);
5
- }
1
+ import type { SitemapChange, SitemapCollection, SitemapMove, SitemapNodeRecord, SitemapRecord, SitemapValues } from './types.js';
2
+ export { ModelError, getByPath, mergePatch, isColor } from '@yanqirenshi/model.core';
6
3
  /** レコードを置くコレクション */
7
4
  export declare function collectionOf(record: SitemapRecord): SitemapCollection;
8
5
  /** インスペクタの見出し。ラベルの文字、無ければ id(線は「遷移元 → 遷移先」) */
9
6
  export declare function titleOf(record: SitemapRecord): string;
10
- /** ドット区切りの path で値を取り出す */
11
- export declare function getByPath(obj: unknown, path: string): unknown;
12
- /**
13
- * JSON Merge Patch をレコードに重ねたものを返す(入力は書き換えない)。
14
- * null の項目は除く。保存したあと、表示を先に更新するときに使う。
15
- */
16
- export declare function mergePatch<T extends object>(record: T, patch: MergePatch): T;
17
7
  /**
18
8
  * ドラッグで動かしたノードの位置を保存する patch。
19
9
  * 保存してある position に移動量を足し、整数に丸める(position が無いときは 0, 0 から)。
20
10
  * 子のノードの position は親の中での位置なので、親を動かしても子の patch は要らない。
21
11
  */
22
12
  export declare function movePatch(node: SitemapNodeRecord, move: SitemapMove): SitemapChange;
23
- /**
24
- * 色として読める文字列か。
25
- * ブラウザでは CSS.supports('color', v) で判定する。Node など CSS の無い環境では、
26
- * #rgb / #rrggbb / #rrggbbaa・rgb() などの関数表記・英字だけの色名、を通す緩い判定にする。
27
- */
28
- export declare function isColor(value: string): boolean;
29
13
  /**
30
14
  * インスペクタの「適用」の値を、保存する patch に変換する。
31
15
  * 図に描いている値から変えた項目だけを patch に入れる(変えていない既定の値まで書き込まないため)。
@@ -1 +1 @@
1
- {"version":3,"file":"patch.d.ts","sourceRoot":"","sources":["../src/patch.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,UAAU,EAAE,aAAa,EAAE,iBAAiB,EAAqB,WAAW,EAAE,iBAAiB,EAAE,aAAa,EAC9G,aAAa,EACd,MAAM,YAAY,CAAC;AAEpB,8BAA8B;AAC9B,qBAAa,UAAW,SAAQ,KAAK;gBACvB,OAAO,EAAE,MAAM;CAI5B;AAED,oBAAoB;AACpB,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,iBAAiB,CAErE;AAED,+CAA+C;AAC/C,wBAAgB,OAAO,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAOrD;AAED,2BAA2B;AAC3B,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAK7D;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,UAAU,GAAG,CAAC,CAmB5E;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,WAAW,GAAG,aAAa,CAanF;AAUD;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAY9C;AAyJD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,aAAa,GAAG,aAAa,CAMtF"}
1
+ {"version":3,"file":"patch.d.ts","sourceRoot":"","sources":["../src/patch.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACE,aAAa,EAAE,iBAAiB,EAAqB,WAAW,EAAE,iBAAiB,EAAE,aAAa,EAC9G,aAAa,EACd,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAErF,oBAAoB;AACpB,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,iBAAiB,CAErE;AAED,+CAA+C;AAC/C,wBAAgB,OAAO,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAOrD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,WAAW,GAAG,aAAa,CAEnF;AA+ED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,aAAa,GAAG,aAAa,CAMtF"}
package/dist/patch.js CHANGED
@@ -3,15 +3,12 @@
3
3
  // 戻り値は { collection, id, patch }。利用側(iHub など)は図の種類を知らずにこれを保存すればよい。
4
4
  // 入力の検証に失敗したときは ModelError を投げる(メッセージは利用者にそのまま見せられる日本語)。
5
5
  // model.unchi の patch.ts を元にしている(ラベルは label.contents、リンクは link.url、マーカーと border.type は無い)。
6
+ // 読み取り部品(toColor など)と patch の道具は model.core にある(#151)。
7
+ import { ModelError, changedValues as changedValuesOf, movePatch as movePatchTo, put, toColor, toDegree, toLength, toOptionalText, toWidth, } from '@yanqirenshi/model.core';
6
8
  import { isEdge, withDefaults } from './defaults.js';
7
9
  import { fieldsOf } from './fields.js';
8
- /** 入力の検証エラー。message は利用者向け */
9
- export class ModelError extends Error {
10
- constructor(message) {
11
- super(message);
12
- this.name = 'ModelError';
13
- }
14
- }
10
+ // model.core のものをそのまま公開する(利用側が model.sitemap だけを読めばよいように)
11
+ export { ModelError, getByPath, mergePatch, isColor } from '@yanqirenshi/model.core';
15
12
  /** レコードを置くコレクション */
16
13
  export function collectionOf(record) {
17
14
  return isEdge(record) ? 'edges' : 'nodes';
@@ -23,128 +20,21 @@ export function titleOf(record) {
23
20
  const contents = record.label && typeof record.label === 'object' ? record.label.contents : null;
24
21
  return typeof contents === 'string' && contents !== '' ? contents : String(record.id);
25
22
  }
26
- /** ドット区切りの path で値を取り出す */
27
- export function getByPath(obj, path) {
28
- return path.split('.').reduce((acc, key) => (acc == null ? acc : acc[key]), obj);
29
- }
30
- /**
31
- * JSON Merge Patch をレコードに重ねたものを返す(入力は書き換えない)。
32
- * null の項目は除く。保存したあと、表示を先に更新するときに使う。
33
- */
34
- export function mergePatch(record, patch) {
35
- const out = { ...record };
36
- for (const [key, value] of Object.entries(patch)) {
37
- if (value === null) {
38
- delete out[key];
39
- }
40
- else if (typeof value === 'object' && !Array.isArray(value)) {
41
- const base = out[key];
42
- out[key] = mergePatch(base && typeof base === 'object' && !Array.isArray(base) ? base : {}, value);
43
- }
44
- else {
45
- out[key] = value;
46
- }
47
- }
48
- return out;
49
- }
50
23
  /**
51
24
  * ドラッグで動かしたノードの位置を保存する patch。
52
25
  * 保存してある position に移動量を足し、整数に丸める(position が無いときは 0, 0 から)。
53
26
  * 子のノードの position は親の中での位置なので、親を動かしても子の patch は要らない。
54
27
  */
55
28
  export function movePatch(node, move) {
56
- const base = node.position || { x: 0, y: 0 };
57
- return {
58
- collection: 'nodes',
59
- id: node.id,
60
- patch: {
61
- position: {
62
- x: Math.round(base.x + move.dx),
63
- y: Math.round(base.y + move.dy),
64
- },
65
- },
66
- };
67
- }
68
- // ---- 入力の読み取り ------------------------------------------------------------
69
- function toNumber(value) {
70
- const n = Number(value);
71
- return value !== '' && Number.isFinite(n) ? n : null;
29
+ return movePatchTo(node, move, 'nodes');
72
30
  }
73
- /**
74
- * 色として読める文字列か。
75
- * ブラウザでは CSS.supports('color', v) で判定する。Node など CSS の無い環境では、
76
- * #rgb / #rrggbb / #rrggbbaa・rgb() などの関数表記・英字だけの色名、を通す緩い判定にする。
77
- */
78
- export function isColor(value) {
79
- if (value === '')
80
- return false;
81
- const css = globalThis.CSS;
82
- if (css && typeof css.supports === 'function')
83
- return css.supports('color', value);
84
- return /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(value)
85
- || /^(rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\(.+\)$/i.test(value)
86
- || /^[a-z]+$/i.test(value);
87
- }
88
- function toColor(value, label) {
89
- if (!isColor(value))
90
- throw new ModelError(`${label}は、色 (#ff0000 や red など) で入力してください。`);
91
- return value;
92
- }
93
- // 0 以上の数(枠線・線の太さ。0 は線なし)
94
- function toWidth(value, label) {
95
- const n = toNumber(value);
96
- if (n === null || n < 0)
97
- throw new ModelError(`${label}は、0 以上の数で入力してください。`);
98
- return n;
99
- }
100
- // 0 以上の整数(幅・高さ。0 は「子に合わせる」)
101
- function toLength(value, label) {
102
- const n = toNumber(value);
103
- if (n === null || n < 0)
104
- throw new ModelError(`${label}は 0 以上の数で入力してください。(0 で子に合わせる)`);
105
- return Math.round(n);
106
- }
107
- // 角度(0 以上 360 未満の整数に正規化)
108
- function toDegree(value, label) {
109
- const n = toNumber(value);
110
- if (n === null)
111
- throw new ModelError(`${label}は、角度 (数) で入力してください。`);
112
- return ((Math.round(n) % 360) + 360) % 360;
113
- }
114
- // 空にできる文字列。空なら null(JSON Merge Patch で項目を除く)
115
- function toOptionalText(value) {
116
- return value === '' ? null : value;
117
- }
118
- // patch の入れ子に値を置く(patch.border.color = v のように)
119
- function put(patch, path, value) {
120
- const keys = path.split('.');
121
- let cur = patch;
122
- for (const key of keys.slice(0, -1)) {
123
- if (!cur[key] || typeof cur[key] !== 'object')
124
- cur[key] = {};
125
- cur = cur[key];
126
- }
127
- cur[keys[keys.length - 1]] = value;
128
- }
129
- /**
130
- * インスペクタの入力のうち、図に描いている値(withDefaults で埋めた値)から変わった項目だけを返す。
131
- * 前後の空白は除く。読み取り専用の項目と、values に無い項目は含めない。
132
- */
31
+ // インスペクタの入力のうち、図に描いている値(withDefaults)から変わった項目だけ
133
32
  function changedValues(record, values) {
134
- const effective = withDefaults(record);
135
- const out = new Map();
136
- for (const field of fieldsOf(record)) {
137
- if (field.type === 'readonly')
138
- continue;
139
- const value = values[field.path];
140
- if (value === undefined)
141
- continue;
142
- const current = getByPath(effective, field.path);
143
- if (value.trim() !== String(current ?? ''))
144
- out.set(field.path, value.trim());
145
- }
146
- return out;
33
+ return changedValuesOf(withDefaults(record), values, fieldsOf(record));
147
34
  }
35
+ // 幅・高さは 0 以上(0 は「子に合わせる」)。枠線・線の太さは 0 以上(0 は線なし)
36
+ const LENGTH = { hint: '(0 で子に合わせる)' };
37
+ const WIDTH = { allowZero: true };
148
38
  // ノードの入力 → patch(label.contents / node_type / link.url / size / background / border)
149
39
  function nodePatch(node, values) {
150
40
  const patch = {};
@@ -167,10 +57,10 @@ function nodePatch(node, values) {
167
57
  put(patch, path, value);
168
58
  break;
169
59
  case 'size.w':
170
- put(patch, path, toLength(value, '幅'));
60
+ put(patch, path, toLength(value, '幅', LENGTH));
171
61
  break;
172
62
  case 'size.h':
173
- put(patch, path, toLength(value, '高さ'));
63
+ put(patch, path, toLength(value, '高さ', LENGTH));
174
64
  break;
175
65
  case 'background.color':
176
66
  put(patch, path, toColor(value, '背景色'));
@@ -179,7 +69,7 @@ function nodePatch(node, values) {
179
69
  put(patch, path, toColor(value, '枠線の色'));
180
70
  break;
181
71
  case 'border.width':
182
- put(patch, path, toWidth(value, '枠線の太さ'));
72
+ put(patch, path, toWidth(value, '枠線の太さ', WIDTH));
183
73
  break;
184
74
  }
185
75
  }
@@ -199,7 +89,7 @@ function edgePatch(edge, values) {
199
89
  put(patch, path, toColor(value, '線の色'));
200
90
  break;
201
91
  case 'stroke.width':
202
- put(patch, path, toWidth(value, '線の太さ'));
92
+ put(patch, path, toWidth(value, '線の太さ', WIDTH));
203
93
  break;
204
94
  }
205
95
  }
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { MergePatch, ModelField, ModelFieldType, ModelMove, ModelValues } from '@yanqirenshi/model.core';
1
2
  import type { Background, Border, Font, Position, Size, Stroke } from '@yanqirenshi/types';
2
3
  /** レコードの id(nodes / edges とも。ノードは子も含めて一意) */
3
4
  export type SitemapId = number | string;
@@ -61,10 +62,8 @@ export interface SitemapGraph {
61
62
  }
62
63
  /** レコードを置くコレクション */
63
64
  export type SitemapCollection = 'nodes' | 'edges';
64
- /** JSON Merge Patch(RFC 7386)。null は「その項目を除く」 */
65
- export type MergePatch = {
66
- [key: string]: unknown;
67
- };
65
+ /** JSON Merge Patch(RFC 7386)。null は「その項目を除く」。model.core の MergePatch と同じ(iHub の PATCH /{collection}/{id} が受け取る形) */
66
+ export type { MergePatch };
68
67
  /** 保存する変更。利用側は図の種類を知らずにこれを保存すればよい */
69
68
  export interface SitemapChange {
70
69
  collection: SitemapCollection;
@@ -72,22 +71,13 @@ export interface SitemapChange {
72
71
  patch: MergePatch;
73
72
  }
74
73
  /** ドラッグの移動量(世界座標。d3.sitemap の callbacks.node.dragend が渡す move と同じ) */
75
- export interface SitemapMove {
76
- dx: number;
77
- dy: number;
78
- }
74
+ export type SitemapMove = ModelMove;
79
75
  /**
80
76
  * 編集できる項目。colonoscope の ColonoscopeField(path / label / type)と同じ形で、
81
77
  * 取りうる値の一覧 options だけ足している(型は構造的に互換。colonoscope には依存しない)。
82
78
  */
83
- export type SitemapFieldType = 'text' | 'textarea' | 'number' | 'readonly';
84
- export interface SitemapField {
85
- /** レコードの項目のパス(ドット区切り。例: 'link.url')。写し先ではなく実際の項目を指す */
86
- path: string;
87
- label: string;
88
- type: SitemapFieldType;
89
- options?: readonly string[];
90
- }
79
+ export type SitemapFieldType = ModelFieldType;
80
+ export type SitemapField = ModelField;
91
81
  /** インスペクタの「適用」が渡す値(path → 入力した文字列) */
92
- export type SitemapValues = Record<string, string>;
82
+ export type SitemapValues = ModelValues;
93
83
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yanqirenshi/model.sitemap",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Diagram model for sitemap diagrams (d3.sitemap) — JSON Schema, consistency rules, drawing defaults, editable fields and patch conversion. Pure data and functions: no React / d3 dependency",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -37,6 +37,7 @@
37
37
  "access": "public"
38
38
  },
39
39
  "dependencies": {
40
+ "@yanqirenshi/model.core": "^0.1.0",
40
41
  "@yanqirenshi/types": "^0.1.2"
41
42
  },
42
43
  "devDependencies": {
package/src/check.ts CHANGED
@@ -2,28 +2,17 @@
2
2
  // スキーマ(schema.ts)に合っているデータを渡す。戻り値はエラーのメッセージの配列(空なら問題なし)。
3
3
  //
4
4
  // iHub の server/validation/rules.mjs の checkGraph('sitemap', data) と同じ検査・同じ文言。
5
- // model.unchi の check.ts の複製(graph 系の共通化は #146 のあとに判断する — #145 の「やらないこと」)。
5
+ // 検査そのものは graph 系(unchi / sitemap / deployment)で共通なので model.core の checkGraph にあり、
6
+ // ここでは model.sitemap の型を付けて公開する(#151)。
6
7
  // メッセージの先頭はコレクション名で、prefix を渡すと 'sitemap/nodes: ...' のように付けられる。
7
8
 
9
+ import { checkGraph, flattenNodes, findById as findInList } from '@yanqirenshi/model.core';
10
+
8
11
  import type { SitemapGraph, SitemapId, SitemapNodeRecord, SitemapRecord } from './types.js';
9
12
 
10
13
  /** 入れ子のノード(children)を平らにする(親 → 子 の順) */
11
14
  export function flatten(nodes: SitemapNodeRecord[]): SitemapNodeRecord[] {
12
- return nodes.flatMap(node => [node, ...flatten(node.children || [])]);
13
- }
14
-
15
- // list の中で id が重複していればエラーにする。id の集合を返す。
16
- function checkUnique(errors: string[], label: string, list: { id: SitemapId }[]): Set<SitemapId> {
17
- const values = new Set<SitemapId>();
18
-
19
- for (const d of list) {
20
- if (values.has(d.id))
21
- errors.push(`${label}: id が重複しています。(id=${String(d.id)})`);
22
-
23
- values.add(d.id);
24
- }
25
-
26
- return values;
15
+ return flattenNodes(nodes);
27
16
  }
28
17
 
29
18
  /**
@@ -35,32 +24,10 @@ function checkUnique(errors: string[], label: string, list: { id: SitemapId }[])
35
24
  * @param prefix メッセージのコレクション名の前に付ける文字列(例: 'sitemap/')
36
25
  */
37
26
  export function check(data: SitemapGraph, prefix = ''): string[] {
38
- const errors: string[] = [];
39
-
40
- const node_ids = checkUnique(errors, `${prefix}nodes`, flatten(data.nodes || []));
41
-
42
- checkUnique(errors, `${prefix}edges`, data.edges || []);
43
-
44
- for (const edge of data.edges || [])
45
- for (const end of ['from', 'to'] as const)
46
- if (!edge[end] || !node_ids.has(edge[end].id))
47
- errors.push(`${prefix}edges: ${end}.id のノードが見つかりません。(id=${String(edge.id)}, ${end}.id=${String(edge[end]?.id)})`);
48
-
49
- return errors;
27
+ return checkGraph(data, prefix);
50
28
  }
51
29
 
52
30
  /** ノード(子を含む)か線を id で探す(無ければ null) */
53
31
  export function findById<T extends SitemapRecord>(list: readonly T[] | undefined, id: SitemapId): T | null {
54
- for (const d of list || []) {
55
- if (d.id === id)
56
- return d;
57
-
58
- const children = (d as SitemapNodeRecord).children;
59
- const hit = children ? findById(children as T[], id) : null;
60
-
61
- if (hit)
62
- return hit;
63
- }
64
-
65
- return null;
32
+ return findInList(list, id);
66
33
  }
package/src/index.ts CHANGED
@@ -7,7 +7,8 @@
7
7
  // 編集できる項目: fieldsOf(record) / NODE_FIELDS / EDGE_FIELDS
8
8
  // patch への変換: movePatch(node, move) / applyPatch(record, values) / mergePatch(record, patch)
9
9
  //
10
- // graph 系(unchi / sitemap / deployment)の共通部分はまだ切り出していない(#145 の「やらないこと」。#146 のあとに判断)。
10
+ // graph 系(unchi / sitemap / deployment)で共通の check / flatten / findById / movePatch / mergePatch / getByPath / isColor と
11
+ // 入力の読み取り部品は model.core(#151)のもので、ここでは model.sitemap の型を付けて公開している。
11
12
 
12
13
  export type {
13
14
  SitemapId, SitemapNodeLabel, SitemapNodeLink, SitemapNodeRecord, SitemapEdgeEnd, SitemapEdgeRecord, SitemapRecord,
package/src/patch.ts CHANGED
@@ -3,6 +3,12 @@
3
3
  // 戻り値は { collection, id, patch }。利用側(iHub など)は図の種類を知らずにこれを保存すればよい。
4
4
  // 入力の検証に失敗したときは ModelError を投げる(メッセージは利用者にそのまま見せられる日本語)。
5
5
  // model.unchi の patch.ts を元にしている(ラベルは label.contents、リンクは link.url、マーカーと border.type は無い)。
6
+ // 読み取り部品(toColor など)と patch の道具は model.core にある(#151)。
7
+
8
+ import {
9
+ ModelError, changedValues as changedValuesOf, movePatch as movePatchTo, put,
10
+ toColor, toDegree, toLength, toOptionalText, toWidth,
11
+ } from '@yanqirenshi/model.core';
6
12
 
7
13
  import { isEdge, withDefaults } from './defaults.js';
8
14
  import { fieldsOf } from './fields.js';
@@ -11,13 +17,8 @@ import type {
11
17
  SitemapValues,
12
18
  } from './types.js';
13
19
 
14
- /** 入力の検証エラー。message は利用者向け */
15
- export class ModelError extends Error {
16
- constructor(message: string) {
17
- super(message);
18
- this.name = 'ModelError';
19
- }
20
- }
20
+ // model.core のものをそのまま公開する(利用側が model.sitemap だけを読めばよいように)
21
+ export { ModelError, getByPath, mergePatch, isColor } from '@yanqirenshi/model.core';
21
22
 
22
23
  /** レコードを置くコレクション */
23
24
  export function collectionOf(record: SitemapRecord): SitemapCollection {
@@ -34,169 +35,24 @@ export function titleOf(record: SitemapRecord): string {
34
35
  return typeof contents === 'string' && contents !== '' ? contents : String(record.id);
35
36
  }
36
37
 
37
- /** ドット区切りの path で値を取り出す */
38
- export function getByPath(obj: unknown, path: string): unknown {
39
- return path.split('.').reduce<unknown>(
40
- (acc, key) => (acc == null ? acc : (acc as Record<string, unknown>)[key]),
41
- obj,
42
- );
43
- }
44
-
45
- /**
46
- * JSON Merge Patch をレコードに重ねたものを返す(入力は書き換えない)。
47
- * null の項目は除く。保存したあと、表示を先に更新するときに使う。
48
- */
49
- export function mergePatch<T extends object>(record: T, patch: MergePatch): T {
50
- const out: Record<string, unknown> = { ...(record as Record<string, unknown>) };
51
-
52
- for (const [key, value] of Object.entries(patch)) {
53
- if (value === null) {
54
- delete out[key];
55
- } else if (typeof value === 'object' && !Array.isArray(value)) {
56
- const base = out[key];
57
-
58
- out[key] = mergePatch(
59
- base && typeof base === 'object' && !Array.isArray(base) ? (base as object) : {},
60
- value as MergePatch,
61
- );
62
- } else {
63
- out[key] = value;
64
- }
65
- }
66
-
67
- return out as T;
68
- }
69
-
70
38
  /**
71
39
  * ドラッグで動かしたノードの位置を保存する patch。
72
40
  * 保存してある position に移動量を足し、整数に丸める(position が無いときは 0, 0 から)。
73
41
  * 子のノードの position は親の中での位置なので、親を動かしても子の patch は要らない。
74
42
  */
75
43
  export function movePatch(node: SitemapNodeRecord, move: SitemapMove): SitemapChange {
76
- const base = node.position || { x: 0, y: 0 };
77
-
78
- return {
79
- collection: 'nodes',
80
- id: node.id,
81
- patch: {
82
- position: {
83
- x: Math.round(base.x + move.dx),
84
- y: Math.round(base.y + move.dy),
85
- },
86
- },
87
- };
88
- }
89
-
90
- // ---- 入力の読み取り ------------------------------------------------------------
91
-
92
- function toNumber(value: string): number | null {
93
- const n = Number(value);
94
-
95
- return value !== '' && Number.isFinite(n) ? n : null;
96
- }
97
-
98
- /**
99
- * 色として読める文字列か。
100
- * ブラウザでは CSS.supports('color', v) で判定する。Node など CSS の無い環境では、
101
- * #rgb / #rrggbb / #rrggbbaa・rgb() などの関数表記・英字だけの色名、を通す緩い判定にする。
102
- */
103
- export function isColor(value: string): boolean {
104
- if (value === '')
105
- return false;
106
-
107
- const css = (globalThis as { CSS?: { supports?: (p: string, v: string) => boolean } }).CSS;
108
-
109
- if (css && typeof css.supports === 'function')
110
- return css.supports('color', value);
111
-
112
- return /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(value)
113
- || /^(rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\(.+\)$/i.test(value)
114
- || /^[a-z]+$/i.test(value);
115
- }
116
-
117
- function toColor(value: string, label: string): string {
118
- if (!isColor(value))
119
- throw new ModelError(`${label}は、色 (#ff0000 や red など) で入力してください。`);
120
-
121
- return value;
44
+ return movePatchTo(node, move, 'nodes');
122
45
  }
123
46
 
124
- // 0 以上の数(枠線・線の太さ。0 は線なし)
125
- function toWidth(value: string, label: string): number {
126
- const n = toNumber(value);
127
-
128
- if (n === null || n < 0)
129
- throw new ModelError(`${label}は、0 以上の数で入力してください。`);
130
-
131
- return n;
132
- }
133
-
134
- // 0 以上の整数(幅・高さ。0 は「子に合わせる」)
135
- function toLength(value: string, label: string): number {
136
- const n = toNumber(value);
137
-
138
- if (n === null || n < 0)
139
- throw new ModelError(`${label}は 0 以上の数で入力してください。(0 で子に合わせる)`);
140
-
141
- return Math.round(n);
142
- }
143
-
144
- // 角度(0 以上 360 未満の整数に正規化)
145
- function toDegree(value: string, label: string): number {
146
- const n = toNumber(value);
147
-
148
- if (n === null)
149
- throw new ModelError(`${label}は、角度 (数) で入力してください。`);
150
-
151
- return ((Math.round(n) % 360) + 360) % 360;
152
- }
153
-
154
- // 空にできる文字列。空なら null(JSON Merge Patch で項目を除く)
155
- function toOptionalText(value: string): string | null {
156
- return value === '' ? null : value;
157
- }
158
-
159
- // patch の入れ子に値を置く(patch.border.color = v のように)
160
- function put(patch: MergePatch, path: string, value: unknown): void {
161
- const keys = path.split('.');
162
- let cur = patch;
163
-
164
- for (const key of keys.slice(0, -1)) {
165
- if (!cur[key] || typeof cur[key] !== 'object')
166
- cur[key] = {};
167
-
168
- cur = cur[key] as MergePatch;
169
- }
170
-
171
- cur[keys[keys.length - 1]] = value;
172
- }
173
-
174
- /**
175
- * インスペクタの入力のうち、図に描いている値(withDefaults で埋めた値)から変わった項目だけを返す。
176
- * 前後の空白は除く。読み取り専用の項目と、values に無い項目は含めない。
177
- */
47
+ // インスペクタの入力のうち、図に描いている値(withDefaults)から変わった項目だけ
178
48
  function changedValues(record: SitemapRecord, values: SitemapValues): Map<string, string> {
179
- const effective = withDefaults(record);
180
- const out = new Map<string, string>();
181
-
182
- for (const field of fieldsOf(record)) {
183
- if (field.type === 'readonly')
184
- continue;
185
-
186
- const value = values[field.path];
187
-
188
- if (value === undefined)
189
- continue;
190
-
191
- const current = getByPath(effective, field.path);
192
-
193
- if (value.trim() !== String(current ?? ''))
194
- out.set(field.path, value.trim());
195
- }
196
-
197
- return out;
49
+ return changedValuesOf(withDefaults(record), values, fieldsOf(record));
198
50
  }
199
51
 
52
+ // 幅・高さは 0 以上(0 は「子に合わせる」)。枠線・線の太さは 0 以上(0 は線なし)
53
+ const LENGTH = { hint: '(0 で子に合わせる)' };
54
+ const WIDTH = { allowZero: true };
55
+
200
56
  // ノードの入力 → patch(label.contents / node_type / link.url / size / background / border)
201
57
  function nodePatch(node: SitemapNodeRecord, values: SitemapValues): MergePatch {
202
58
  const patch: MergePatch = {};
@@ -221,10 +77,10 @@ function nodePatch(node: SitemapNodeRecord, values: SitemapValues): MergePatch {
221
77
  put(patch, path, value);
222
78
  break;
223
79
  case 'size.w':
224
- put(patch, path, toLength(value, '幅'));
80
+ put(patch, path, toLength(value, '幅', LENGTH));
225
81
  break;
226
82
  case 'size.h':
227
- put(patch, path, toLength(value, '高さ'));
83
+ put(patch, path, toLength(value, '高さ', LENGTH));
228
84
  break;
229
85
  case 'background.color':
230
86
  put(patch, path, toColor(value, '背景色'));
@@ -233,7 +89,7 @@ function nodePatch(node: SitemapNodeRecord, values: SitemapValues): MergePatch {
233
89
  put(patch, path, toColor(value, '枠線の色'));
234
90
  break;
235
91
  case 'border.width':
236
- put(patch, path, toWidth(value, '枠線の太さ'));
92
+ put(patch, path, toWidth(value, '枠線の太さ', WIDTH));
237
93
  break;
238
94
  }
239
95
  }
@@ -257,7 +113,7 @@ function edgePatch(edge: SitemapEdgeRecord, values: SitemapValues): MergePatch {
257
113
  put(patch, path, toColor(value, '線の色'));
258
114
  break;
259
115
  case 'stroke.width':
260
- put(patch, path, toWidth(value, '線の太さ'));
116
+ put(patch, path, toWidth(value, '線の太さ', WIDTH));
261
117
  break;
262
118
  }
263
119
  }
package/src/types.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  // ノードと線の図なので model.unchi と形が近いが、ラベルは label.contents(unchi は label.text)で、
6
6
  // ノードにリンク(link.url)と URL パス(path)、画面の種類(node_type)がある。
7
7
 
8
+ import type { MergePatch, ModelField, ModelFieldType, ModelMove, ModelValues } from '@yanqirenshi/model.core';
8
9
  import type { Background, Border, Font, Position, Size, Stroke } from '@yanqirenshi/types';
9
10
 
10
11
  /** レコードの id(nodes / edges とも。ノードは子も含めて一意) */
@@ -73,8 +74,8 @@ export interface SitemapGraph {
73
74
  /** レコードを置くコレクション */
74
75
  export type SitemapCollection = 'nodes' | 'edges';
75
76
 
76
- /** JSON Merge Patch(RFC 7386)。null は「その項目を除く」 */
77
- export type MergePatch = { [key: string]: unknown };
77
+ /** JSON Merge Patch(RFC 7386)。null は「その項目を除く」。model.core の MergePatch と同じ(iHub の PATCH /{collection}/{id} が受け取る形) */
78
+ export type { MergePatch };
78
79
 
79
80
  /** 保存する変更。利用側は図の種類を知らずにこれを保存すればよい */
80
81
  export interface SitemapChange {
@@ -84,24 +85,15 @@ export interface SitemapChange {
84
85
  }
85
86
 
86
87
  /** ドラッグの移動量(世界座標。d3.sitemap の callbacks.node.dragend が渡す move と同じ) */
87
- export interface SitemapMove {
88
- dx: number;
89
- dy: number;
90
- }
88
+ export type SitemapMove = ModelMove;
91
89
 
92
90
  /**
93
91
  * 編集できる項目。colonoscope の ColonoscopeField(path / label / type)と同じ形で、
94
92
  * 取りうる値の一覧 options だけ足している(型は構造的に互換。colonoscope には依存しない)。
95
93
  */
96
- export type SitemapFieldType = 'text' | 'textarea' | 'number' | 'readonly';
97
-
98
- export interface SitemapField {
99
- /** レコードの項目のパス(ドット区切り。例: 'link.url')。写し先ではなく実際の項目を指す */
100
- path: string;
101
- label: string;
102
- type: SitemapFieldType;
103
- options?: readonly string[];
104
- }
94
+ export type SitemapFieldType = ModelFieldType;
95
+
96
+ export type SitemapField = ModelField;
105
97
 
106
98
  /** インスペクタの「適用」が渡す値(path → 入力した文字列) */
107
- export type SitemapValues = Record<string, string>;
99
+ export type SitemapValues = ModelValues;