@pallasoft/ps-utilities 2.21.0 → 2.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/client/http-service.d.ts +1 -0
  2. package/dist/client/http-service.js +1 -1
  3. package/dist/common/constants/html-entity-names.d.ts +1 -0
  4. package/dist/common/constants/html-entity-names.js +34 -0
  5. package/dist/common/constants/html-heading-styles.d.ts +1 -0
  6. package/dist/common/constants/html-heading-styles.js +11 -0
  7. package/dist/common/constants/html-text-config.d.ts +76 -0
  8. package/dist/common/constants/html-text-config.js +136 -0
  9. package/dist/common/constants/html-token-kind.d.ts +5 -0
  10. package/dist/common/constants/html-token-kind.js +8 -0
  11. package/dist/common/constants/portable-text-config.d.ts +16 -0
  12. package/dist/common/constants/portable-text-config.js +15 -1
  13. package/dist/common/html-entity-service.d.ts +6 -0
  14. package/dist/common/html-entity-service.js +34 -0
  15. package/dist/common/html-style-service.d.ts +12 -0
  16. package/dist/common/html-style-service.js +92 -0
  17. package/dist/common/html-token-service.d.ts +8 -0
  18. package/dist/common/html-token-service.js +90 -0
  19. package/dist/common/index.d.ts +1 -0
  20. package/dist/common/index.js +1 -0
  21. package/dist/common/portable-text-html-service.d.ts +28 -0
  22. package/dist/common/portable-text-html-service.js +392 -0
  23. package/dist/common/portable-text-link-service.d.ts +7 -0
  24. package/dist/common/portable-text-link-service.js +42 -0
  25. package/dist/common/portable-text-service.d.ts +7 -4
  26. package/dist/common/portable-text-service.js +15 -10
  27. package/dist/common/portable-text-tracking-service.d.ts +7 -0
  28. package/dist/common/portable-text-tracking-service.js +45 -0
  29. package/dist/esm/client/http-service.js +1 -1
  30. package/dist/esm/common/constants/html-entity-names.js +31 -0
  31. package/dist/esm/common/constants/html-heading-styles.js +8 -0
  32. package/dist/esm/common/constants/html-text-config.js +133 -0
  33. package/dist/esm/common/constants/html-token-kind.js +5 -0
  34. package/dist/esm/common/constants/portable-text-config.js +15 -1
  35. package/dist/esm/common/html-entity-service.js +25 -0
  36. package/dist/esm/common/html-style-service.js +85 -0
  37. package/dist/esm/common/html-token-service.js +83 -0
  38. package/dist/esm/common/index.js +1 -0
  39. package/dist/esm/common/portable-text-html-service.js +375 -0
  40. package/dist/esm/common/portable-text-link-service.js +35 -0
  41. package/dist/esm/common/portable-text-service.js +14 -9
  42. package/dist/esm/common/portable-text-tracking-service.js +34 -0
  43. package/dist/esm/models/html-attributes-data.js +1 -0
  44. package/dist/esm/models/html-close-token-data.js +1 -0
  45. package/dist/esm/models/html-context-data.js +1 -0
  46. package/dist/esm/models/html-conversion-data.js +1 -0
  47. package/dist/esm/models/html-declarations-data.js +1 -0
  48. package/dist/esm/models/html-frame-data.js +1 -0
  49. package/dist/esm/models/html-open-token-data.js +1 -0
  50. package/dist/esm/models/html-run-data.js +1 -0
  51. package/dist/esm/models/html-text-token-data.js +1 -0
  52. package/dist/esm/models/html-token-data.js +1 -0
  53. package/dist/esm/models/portable-text-block-link.js +1 -0
  54. package/dist/esm/models/portable-text-content-data.js +1 -0
  55. package/dist/esm/models/portable-text-image-data.js +1 -0
  56. package/dist/index.d.ts +3 -0
  57. package/dist/models/html-attributes-data.d.ts +1 -0
  58. package/dist/models/html-attributes-data.js +2 -0
  59. package/dist/models/html-close-token-data.d.ts +5 -0
  60. package/dist/models/html-close-token-data.js +2 -0
  61. package/dist/models/html-context-data.d.ts +6 -0
  62. package/dist/models/html-context-data.js +2 -0
  63. package/dist/models/html-conversion-data.d.ts +12 -0
  64. package/dist/models/html-conversion-data.js +2 -0
  65. package/dist/models/html-declarations-data.d.ts +1 -0
  66. package/dist/models/html-declarations-data.js +2 -0
  67. package/dist/models/html-frame-data.d.ts +13 -0
  68. package/dist/models/html-frame-data.js +2 -0
  69. package/dist/models/html-open-token-data.d.ts +8 -0
  70. package/dist/models/html-open-token-data.js +2 -0
  71. package/dist/models/html-run-data.d.ts +9 -0
  72. package/dist/models/html-run-data.js +2 -0
  73. package/dist/models/html-text-token-data.d.ts +5 -0
  74. package/dist/models/html-text-token-data.js +2 -0
  75. package/dist/models/html-token-data.d.ts +4 -0
  76. package/dist/models/html-token-data.js +2 -0
  77. package/dist/models/portable-text-block-link.d.ts +7 -0
  78. package/dist/models/portable-text-block-link.js +2 -0
  79. package/dist/models/portable-text-content-data.d.ts +3 -0
  80. package/dist/models/portable-text-content-data.js +2 -0
  81. package/dist/models/portable-text-image-data.d.ts +7 -0
  82. package/dist/models/portable-text-image-data.js +2 -0
  83. package/package.json +2 -2
@@ -0,0 +1,375 @@
1
+ import { HtmlHeadingStyles } from './constants/html-heading-styles.js';
2
+ import { HtmlTextConfig } from './constants/html-text-config.js';
3
+ import { HtmlTokenKind } from './constants/html-token-kind.js';
4
+ import { PortableTextConfig } from './constants/portable-text-config.js';
5
+ import { HtmlEntityService } from './html-entity-service.js';
6
+ import { HtmlStyleService } from './html-style-service.js';
7
+ import { HtmlTokenService } from './html-token-service.js';
8
+ import { PortableTextLinkService } from './portable-text-link-service.js';
9
+ import { PortableTextTrackingService } from './portable-text-tracking-service.js';
10
+ export class PortableTextHtmlService {
11
+ static fromHtml(html) {
12
+ const state = {
13
+ blocks: [],
14
+ counter: 0,
15
+ lists: [],
16
+ pending: [],
17
+ skipDepth: 0,
18
+ skipName: null,
19
+ stack: [PortableTextHtmlService.rootFrame()],
20
+ };
21
+ HtmlTokenService.tokenize(html).forEach((token) => PortableTextHtmlService.consume(state, token));
22
+ PortableTextHtmlService.flush(state);
23
+ return PortableTextTrackingService.stripUrlLines(state.blocks);
24
+ }
25
+ static rootFrame() {
26
+ return {
27
+ code: false,
28
+ em: false,
29
+ heading: null,
30
+ href: null,
31
+ isQuote: false,
32
+ listLevel: null,
33
+ name: HtmlTextConfig.rootName,
34
+ ordered: false,
35
+ size: null,
36
+ strong: false,
37
+ underline: false,
38
+ };
39
+ }
40
+ static consume(state, token) {
41
+ if (token.kind === HtmlTokenKind.Text) {
42
+ if (state.skipName === null)
43
+ PortableTextHtmlService.addText(state, token.text);
44
+ return;
45
+ }
46
+ if (token.kind === HtmlTokenKind.Open)
47
+ PortableTextHtmlService.open(state, token);
48
+ else
49
+ PortableTextHtmlService.close(state, token.name);
50
+ }
51
+ static isVoid(token) {
52
+ return HtmlTextConfig.voidTags.includes(token.name) || token.selfClosing;
53
+ }
54
+ static isBlockTag(name) {
55
+ return (HtmlTextConfig.blockTags.includes(name) ||
56
+ Object.prototype.hasOwnProperty.call(HtmlHeadingStyles, name) ||
57
+ name === HtmlTextConfig.quoteTag ||
58
+ name === HtmlTextConfig.listItemTag ||
59
+ name === HtmlTextConfig.orderedListTag ||
60
+ name === HtmlTextConfig.unorderedListTag);
61
+ }
62
+ static isHidden(token, declarations) {
63
+ const { attributes } = token;
64
+ return (HtmlTextConfig.skipTags.includes(token.name) ||
65
+ Object.prototype.hasOwnProperty.call(attributes, HtmlTextConfig.hiddenAttribute) ||
66
+ attributes[HtmlTextConfig.aria] === HtmlTextConfig.hiddenAria ||
67
+ HtmlTextConfig.hiddenClassPattern.test(PortableTextHtmlService.attribute(attributes, HtmlTextConfig.classAttribute)) ||
68
+ HtmlStyleService.isHidden(declarations));
69
+ }
70
+ static attribute(attributes, name) {
71
+ return Object.prototype.hasOwnProperty.call(attributes, name) ? attributes[name] : '';
72
+ }
73
+ static open(state, token) {
74
+ if (state.skipName !== null) {
75
+ if (token.name === state.skipName && !PortableTextHtmlService.isVoid(token))
76
+ state.skipDepth += 1;
77
+ return;
78
+ }
79
+ const declarations = HtmlStyleService.parse(PortableTextHtmlService.attribute(token.attributes, HtmlTextConfig.styleAttribute));
80
+ if (PortableTextHtmlService.isHidden(token, declarations)) {
81
+ if (!PortableTextHtmlService.isVoid(token)) {
82
+ state.skipName = token.name;
83
+ state.skipDepth = 1;
84
+ }
85
+ return;
86
+ }
87
+ if (token.name === HtmlTextConfig.breakTag) {
88
+ PortableTextHtmlService.lineBreak(state);
89
+ return;
90
+ }
91
+ if (token.name === HtmlTextConfig.imageTag) {
92
+ PortableTextHtmlService.image(state, token, declarations);
93
+ return;
94
+ }
95
+ const isBlock = PortableTextHtmlService.isBlockTag(token.name) || token.name === HtmlTextConfig.ruleTag;
96
+ if (isBlock)
97
+ PortableTextHtmlService.flush(state);
98
+ if (PortableTextHtmlService.isVoid(token))
99
+ return;
100
+ if (token.name === HtmlTextConfig.listItemTag)
101
+ PortableTextHtmlService.closeOpenItem(state);
102
+ if (token.name === HtmlTextConfig.orderedListTag)
103
+ state.lists.push(true);
104
+ if (token.name === HtmlTextConfig.unorderedListTag)
105
+ state.lists.push(false);
106
+ state.stack.push(PortableTextHtmlService.frame(state, token, declarations));
107
+ }
108
+ static closeOpenItem(state) {
109
+ while (state.stack[state.stack.length - 1].name === HtmlTextConfig.listItemTag)
110
+ state.stack.pop();
111
+ }
112
+ static frame(state, token, declarations) {
113
+ const parent = state.stack[state.stack.length - 1];
114
+ const name = token.name;
115
+ const isLink = name === HtmlTextConfig.linkTag;
116
+ const hasHref = Object.prototype.hasOwnProperty.call(token.attributes, HtmlTextConfig.hrefAttribute);
117
+ const size = HtmlStyleService.fontSize(declarations);
118
+ const isItem = name === HtmlTextConfig.listItemTag;
119
+ return {
120
+ code: HtmlTextConfig.codeTags.includes(name) || parent.code,
121
+ em: PortableTextHtmlService.resolve(parent.em, HtmlTextConfig.emTags.includes(name), HtmlStyleService.italic(declarations)),
122
+ heading: Object.prototype.hasOwnProperty.call(HtmlHeadingStyles, name)
123
+ ? HtmlHeadingStyles[name]
124
+ : null,
125
+ href: isLink && hasHref
126
+ ? PortableTextLinkService.normalize(token.attributes[HtmlTextConfig.hrefAttribute])
127
+ : parent.href,
128
+ isQuote: name === HtmlTextConfig.quoteTag,
129
+ listLevel: isItem
130
+ ? Math.min(Math.max(state.lists.length, HtmlTextConfig.bodyListLevel), PortableTextConfig.maxListLevel)
131
+ : null,
132
+ name,
133
+ ordered: isItem && state.lists.length > 0 && state.lists[state.lists.length - 1],
134
+ size: size !== null ? size : parent.size,
135
+ strong: PortableTextHtmlService.resolve(parent.strong, HtmlTextConfig.strongTags.includes(name), HtmlStyleService.bold(declarations)),
136
+ underline: isLink
137
+ ? parent.underline
138
+ : PortableTextHtmlService.resolve(parent.underline, HtmlTextConfig.underlineTags.includes(name), HtmlStyleService.underline(declarations)),
139
+ };
140
+ }
141
+ static resolve(inherited, byTag, byStyle) {
142
+ if (byStyle !== null)
143
+ return byStyle;
144
+ return byTag || inherited;
145
+ }
146
+ static close(state, name) {
147
+ if (state.skipName !== null) {
148
+ if (name === state.skipName) {
149
+ state.skipDepth -= 1;
150
+ if (state.skipDepth === 0)
151
+ state.skipName = null;
152
+ }
153
+ return;
154
+ }
155
+ let index = state.stack.length - 1;
156
+ while (index > 0 && state.stack[index].name !== name)
157
+ index -= 1;
158
+ if (index === 0)
159
+ return;
160
+ if (PortableTextHtmlService.isBlockTag(name))
161
+ PortableTextHtmlService.flush(state);
162
+ if (name === HtmlTextConfig.orderedListTag || name === HtmlTextConfig.unorderedListTag)
163
+ state.lists.pop();
164
+ state.stack.length = index;
165
+ }
166
+ static addText(state, raw) {
167
+ const isPre = state.stack.some((frame) => frame.name === HtmlTextConfig.preTag);
168
+ const cleaned = HtmlEntityService.decode(raw).replace(HtmlTextConfig.invisiblePattern, '');
169
+ let text = isPre
170
+ ? cleaned
171
+ .replace(HtmlTextConfig.preLineBreakPattern, HtmlTextConfig.newline)
172
+ .replace(HtmlTextConfig.nonBreakingPattern, HtmlTextConfig.space)
173
+ : cleaned.replace(HtmlTextConfig.whitespacePattern, HtmlTextConfig.space);
174
+ if (!isPre) {
175
+ const last = state.pending[state.pending.length - 1];
176
+ const atStart = last === undefined ||
177
+ last.text.endsWith(HtmlTextConfig.space) ||
178
+ last.text.endsWith(HtmlTextConfig.newline);
179
+ if (atStart)
180
+ text = text.replace(HtmlTextConfig.leadingSpacePattern, '');
181
+ }
182
+ if (text !== '')
183
+ PortableTextHtmlService.push(state, text);
184
+ }
185
+ static push(state, text) {
186
+ const frame = state.stack[state.stack.length - 1];
187
+ state.pending.push({
188
+ code: frame.code,
189
+ em: frame.em,
190
+ href: frame.href,
191
+ size: frame.size,
192
+ strong: frame.strong,
193
+ text,
194
+ underline: frame.underline,
195
+ });
196
+ }
197
+ static lineBreak(state) {
198
+ const last = state.pending[state.pending.length - 1];
199
+ if (last === undefined)
200
+ return;
201
+ if (last.text.endsWith(HtmlTextConfig.newline)) {
202
+ PortableTextHtmlService.flush(state);
203
+ return;
204
+ }
205
+ last.text = last.text.replace(HtmlTextConfig.trailingSpacePattern, '');
206
+ PortableTextHtmlService.push(state, HtmlTextConfig.newline);
207
+ }
208
+ static image(state, token, declarations) {
209
+ const src = PortableTextLinkService.normalizeImage(PortableTextHtmlService.attribute(token.attributes, HtmlTextConfig.srcAttribute));
210
+ if (src === null || PortableTextHtmlService.isTiny(token.attributes, declarations))
211
+ return;
212
+ const alt = HtmlEntityService.decode(PortableTextHtmlService.attribute(token.attributes, HtmlTextConfig.altAttribute))
213
+ .replace(HtmlTextConfig.invisiblePattern, '')
214
+ .replace(HtmlTextConfig.whitespacePattern, HtmlTextConfig.space)
215
+ .trim()
216
+ .slice(0, PortableTextConfig.maxImageAltLength);
217
+ PortableTextHtmlService.flush(state);
218
+ state.blocks.push({
219
+ _key: PortableTextHtmlService.key(state, PortableTextConfig.keyPrefix.image),
220
+ _type: PortableTextConfig.imageType,
221
+ alt,
222
+ src,
223
+ });
224
+ }
225
+ static isTiny(attributes, declarations) {
226
+ const sizes = [HtmlTextConfig.widthAttribute, HtmlTextConfig.heightAttribute].map((name) => Number.parseFloat(PortableTextHtmlService.attribute(attributes, name)));
227
+ return (sizes.some((size) => !Number.isNaN(size) && size <= HtmlTextConfig.tinyImageSize) ||
228
+ HtmlStyleService.isTiny(declarations));
229
+ }
230
+ static key(state, prefix) {
231
+ const key = `${prefix}${state.counter}`;
232
+ state.counter += 1;
233
+ return key;
234
+ }
235
+ static context(state) {
236
+ const context = {
237
+ heading: null,
238
+ isQuote: false,
239
+ listLevel: null,
240
+ ordered: false,
241
+ };
242
+ for (let index = state.stack.length - 1; index > 0; index -= 1) {
243
+ const frame = state.stack[index];
244
+ if (context.heading === null && frame.heading !== null)
245
+ context.heading = frame.heading;
246
+ if (frame.isQuote)
247
+ context.isQuote = true;
248
+ if (context.listLevel === null && frame.listLevel !== null) {
249
+ context.listLevel = frame.listLevel;
250
+ context.ordered = frame.ordered;
251
+ }
252
+ }
253
+ return context;
254
+ }
255
+ static trimmed(runs) {
256
+ const kept = runs.filter((run) => run.text !== '');
257
+ while (kept.length > 0) {
258
+ const last = kept[kept.length - 1];
259
+ last.text = last.text.replace(HtmlTextConfig.trailingBreakPattern, '');
260
+ if (last.text !== '')
261
+ break;
262
+ kept.pop();
263
+ }
264
+ while (kept.length > 0) {
265
+ const first = kept[0];
266
+ first.text = first.text.replace(HtmlTextConfig.leadingBreakPattern, '');
267
+ if (first.text !== '')
268
+ break;
269
+ kept.shift();
270
+ }
271
+ return kept;
272
+ }
273
+ static flush(state) {
274
+ const runs = PortableTextHtmlService.trimmed(state.pending);
275
+ state.pending = [];
276
+ if (runs.length === 0)
277
+ return;
278
+ const context = PortableTextHtmlService.context(state);
279
+ const heading = PortableTextHtmlService.headingOf(runs, context);
280
+ const markDefs = [];
281
+ const children = PortableTextHtmlService.merge(runs, heading !== null).map((span) => ({
282
+ _key: PortableTextHtmlService.key(state, PortableTextConfig.keyPrefix.span),
283
+ _type: PortableTextConfig.spanType,
284
+ marks: PortableTextHtmlService.marks(state, span, markDefs),
285
+ text: span.text,
286
+ }));
287
+ const level = context.listLevel;
288
+ const base = {
289
+ _key: PortableTextHtmlService.key(state, PortableTextConfig.keyPrefix.block),
290
+ _type: PortableTextConfig.blockType,
291
+ children,
292
+ markDefs,
293
+ style: PortableTextHtmlService.styleOf(context, heading),
294
+ };
295
+ state.blocks.push(level !== null && heading === null
296
+ ? {
297
+ ...base,
298
+ level,
299
+ listItem: context.ordered
300
+ ? PortableTextConfig.numberItem
301
+ : PortableTextConfig.bulletItem,
302
+ }
303
+ : base);
304
+ }
305
+ static styleOf(context, heading) {
306
+ if (heading !== null)
307
+ return heading;
308
+ if (context.listLevel !== null)
309
+ return PortableTextConfig.normalStyle;
310
+ return context.isQuote ? PortableTextConfig.quoteStyle : PortableTextConfig.normalStyle;
311
+ }
312
+ static headingOf(runs, context) {
313
+ if (context.heading !== null)
314
+ return context.heading;
315
+ if (context.listLevel !== null || context.isQuote)
316
+ return null;
317
+ const text = runs.map((run) => run.text).join('');
318
+ if (text.includes(HtmlTextConfig.newline) || text.length > HtmlTextConfig.largeText.maxLength)
319
+ return null;
320
+ const filled = runs.filter((run) => run.text.trim() !== '');
321
+ const sizes = filled.map((run) => run.size);
322
+ if (sizes.every((size) => size !== null)) {
323
+ const smallest = Math.min(...sizes);
324
+ if (smallest >= HtmlTextConfig.largeText.heading1)
325
+ return PortableTextConfig.headingStyles[0];
326
+ if (smallest >= HtmlTextConfig.largeText.heading2)
327
+ return PortableTextConfig.headingStyles[1];
328
+ if (smallest >= HtmlTextConfig.largeText.heading3)
329
+ return PortableTextConfig.headingStyles[2];
330
+ }
331
+ const isBold = filled.every((run) => run.strong && run.href === null) &&
332
+ text.length <= HtmlTextConfig.boldHeadingMaxLength &&
333
+ !HtmlTextConfig.headingEndPattern.test(text.trim());
334
+ return isBold ? PortableTextConfig.headingStyles[2] : null;
335
+ }
336
+ static merge(runs, dropStrong) {
337
+ const merged = [];
338
+ runs.forEach((run) => {
339
+ const next = dropStrong ? { ...run, strong: false } : { ...run };
340
+ const last = merged[merged.length - 1];
341
+ if (last !== undefined &&
342
+ last.strong === next.strong &&
343
+ last.em === next.em &&
344
+ last.underline === next.underline &&
345
+ last.code === next.code &&
346
+ last.href === next.href) {
347
+ last.text += next.text;
348
+ return;
349
+ }
350
+ merged.push(next);
351
+ });
352
+ return merged;
353
+ }
354
+ static marks(state, span, markDefs) {
355
+ const marks = [];
356
+ if (span.strong)
357
+ marks.push(PortableTextConfig.decorators[0]);
358
+ if (span.em)
359
+ marks.push(PortableTextConfig.decorators[1]);
360
+ if (span.underline)
361
+ marks.push(PortableTextConfig.decorators[2]);
362
+ if (span.code)
363
+ marks.push(PortableTextConfig.decorators[3]);
364
+ if (span.href === null)
365
+ return marks;
366
+ const existing = markDefs.find((definition) => definition.href === span.href);
367
+ const key = existing !== undefined
368
+ ? existing._key
369
+ : PortableTextHtmlService.key(state, PortableTextConfig.keyPrefix.link);
370
+ if (existing === undefined)
371
+ markDefs.push({ _key: key, _type: PortableTextConfig.linkType, href: span.href });
372
+ marks.push(key);
373
+ return marks;
374
+ }
375
+ }
@@ -0,0 +1,35 @@
1
+ import { PortableTextConfig } from './constants/portable-text-config.js';
2
+ export class PortableTextLinkService {
3
+ static isSafe(href) {
4
+ try {
5
+ return PortableTextConfig.linkProtocols.includes(new URL(href).protocol);
6
+ }
7
+ catch {
8
+ return false;
9
+ }
10
+ }
11
+ static normalize(raw) {
12
+ if (typeof raw !== 'string')
13
+ return null;
14
+ const href = Array.from(raw)
15
+ .filter((character) => !PortableTextLinkService.isControl(character))
16
+ .join('')
17
+ .trim();
18
+ return href !== '' && PortableTextLinkService.isSafe(href) ? href : null;
19
+ }
20
+ static isSafeImage(src) {
21
+ return PortableTextLinkService.normalizeImage(src) === src;
22
+ }
23
+ static normalizeImage(raw) {
24
+ const src = PortableTextLinkService.normalize(raw);
25
+ return src !== null &&
26
+ PortableTextConfig.imageProtocolPattern.test(src) &&
27
+ src.length <= PortableTextConfig.maxImageSrcLength
28
+ ? src
29
+ : null;
30
+ }
31
+ static isControl(character) {
32
+ const code = character.charCodeAt(0);
33
+ return code <= PortableTextConfig.maxControlCode || code === PortableTextConfig.deleteCode;
34
+ }
35
+ }
@@ -1,13 +1,15 @@
1
1
  import { Common } from '@pallasoft/ps-models';
2
2
  import { escapeHTML, toHTML } from '@portabletext/to-html';
3
3
  import { PortableTextConfig } from './constants/portable-text-config.js';
4
+ import { PortableTextHtmlService } from './portable-text-html-service.js';
5
+ import { PortableTextLinkService } from './portable-text-link-service.js';
4
6
  export class PortableTextService {
5
7
  static toPlainText(blocks) {
6
8
  const lines = [];
7
9
  const counters = {};
8
10
  let previousWasList = false;
9
11
  blocks.forEach((block) => {
10
- if (block._type !== Common.PortableTextConfig.blockType)
12
+ if (!('children' in block) || block._type !== Common.PortableTextConfig.blockType)
11
13
  return;
12
14
  const text = this.blockText(block);
13
15
  const level = typeof block.level === 'number' ? block.level : 1;
@@ -59,17 +61,22 @@ export class PortableTextService {
59
61
  ],
60
62
  }));
61
63
  }
64
+ static fromHtml(html) {
65
+ return PortableTextHtmlService.fromHtml(html);
66
+ }
62
67
  static isEmpty(blocks) {
63
- return (!Array.isArray(blocks) || blocks.every((block) => this.blockText(block).trim().length === 0));
68
+ return (!Array.isArray(blocks) ||
69
+ blocks.every((block) => !('children' in block) || this.blockText(block).trim().length === 0));
64
70
  }
65
71
  static toHtml(blocks) {
66
72
  return toHTML(blocks, {
67
73
  onMissingComponent: false,
68
74
  components: {
75
+ types: { [PortableTextConfig.imageType]: () => '' },
69
76
  marks: {
70
77
  link: ({ children, value }) => {
71
78
  const href = typeof value?.href === 'string' ? value.href : '';
72
- return this.isSafeLink(href)
79
+ return PortableTextService.isSafeLink(href)
73
80
  ? `<a href="${escapeHTML(href)}" rel="${PortableTextConfig.linkRel}" target="${PortableTextConfig.linkTarget}">${children}</a>`
74
81
  : children;
75
82
  },
@@ -87,11 +94,9 @@ export class PortableTextService {
87
94
  : '';
88
95
  }
89
96
  static isSafeLink(href) {
90
- try {
91
- return Common.PortableTextConfig.linkProtocols.includes(new URL(href).protocol);
92
- }
93
- catch {
94
- return false;
95
- }
97
+ return PortableTextLinkService.isSafe(href);
98
+ }
99
+ static isSafeImage(src) {
100
+ return PortableTextLinkService.isSafeImage(src);
96
101
  }
97
102
  }
@@ -0,0 +1,34 @@
1
+ import { HtmlTextConfig } from './constants/html-text-config.js';
2
+ import { PortableTextConfig } from './constants/portable-text-config.js';
3
+ export class PortableTextTrackingService {
4
+ static stripUrlLines(blocks) {
5
+ const hrefs = PortableTextTrackingService.hrefs(blocks);
6
+ return blocks.filter((block) => !PortableTextTrackingService.isDuplicate(block, hrefs));
7
+ }
8
+ static hrefs(blocks) {
9
+ return blocks.flatMap((block) => 'children' in block && Array.isArray(block.markDefs)
10
+ ? block.markDefs.flatMap((definition) => typeof definition.href === 'string' ? [definition.href] : [])
11
+ : []);
12
+ }
13
+ static isDuplicate(block, hrefs) {
14
+ if (!('children' in block) || !Array.isArray(block.children))
15
+ return false;
16
+ if (block.style !== PortableTextConfig.normalStyle || typeof block.listItem === 'string')
17
+ return false;
18
+ const text = block.children
19
+ .map((child) => (typeof child.text === 'string' ? child.text : ''))
20
+ .join('')
21
+ .trim();
22
+ const match = HtmlTextConfig.trackingLinePattern.exec(text);
23
+ return (match !== null && hrefs.some((href) => PortableTextTrackingService.matches(href, match[1])));
24
+ }
25
+ static matches(href, shown) {
26
+ const ascii = shown.indexOf(HtmlTextConfig.trackingEllipsis);
27
+ const unicode = shown.indexOf(HtmlTextConfig.trackingEllipsisCharacter);
28
+ const index = ascii !== -1 ? ascii : unicode;
29
+ if (index === -1)
30
+ return href === shown || href.startsWith(shown);
31
+ const width = ascii !== -1 ? HtmlTextConfig.trackingEllipsis.length : 1;
32
+ return href.startsWith(shown.slice(0, index)) && href.endsWith(shown.slice(index + width));
33
+ }
34
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -5,6 +5,9 @@ export { HttpMethod } from './common/constants/http-method';
5
5
  export type { CodedErrorData } from './models/coded-error-data';
6
6
  export type { DeviceData } from './models/device-data';
7
7
  export type { EmailPreviewInput } from './models/email-preview-input';
8
+ export type { PortableTextBlockLink } from './models/portable-text-block-link';
9
+ export type { PortableTextContentData } from './models/portable-text-content-data';
10
+ export type { PortableTextImageData } from './models/portable-text-image-data';
8
11
  export type { JsonValueGuard } from './models/json-value-guard';
9
12
  export type { SignedTokenHeader } from './models/signed-token-header';
10
13
  export type { SignedTokenKeyPair } from './models/signed-token-key-pair';
@@ -0,0 +1 @@
1
+ export type HtmlAttributesData = Record<string, string>;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,5 @@
1
+ import type { HtmlTokenKind } from '../common/constants/html-token-kind';
2
+ export interface HtmlCloseTokenData {
3
+ kind: typeof HtmlTokenKind.Close;
4
+ name: string;
5
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,6 @@
1
+ export interface HtmlContextData {
2
+ heading: string | null;
3
+ isQuote: boolean;
4
+ listLevel: number | null;
5
+ ordered: boolean;
6
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,12 @@
1
+ import type { HtmlFrameData } from './html-frame-data';
2
+ import type { HtmlRunData } from './html-run-data';
3
+ import type { PortableTextContentData } from './portable-text-content-data';
4
+ export interface HtmlConversionData {
5
+ blocks: PortableTextContentData[];
6
+ counter: number;
7
+ lists: boolean[];
8
+ pending: HtmlRunData[];
9
+ skipDepth: number;
10
+ skipName: string | null;
11
+ stack: HtmlFrameData[];
12
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1 @@
1
+ export type HtmlDeclarationsData = Record<string, string>;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,13 @@
1
+ export interface HtmlFrameData {
2
+ code: boolean;
3
+ em: boolean;
4
+ heading: string | null;
5
+ href: string | null;
6
+ isQuote: boolean;
7
+ listLevel: number | null;
8
+ name: string;
9
+ ordered: boolean;
10
+ size: number | null;
11
+ strong: boolean;
12
+ underline: boolean;
13
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,8 @@
1
+ import type { HtmlTokenKind } from '../common/constants/html-token-kind';
2
+ import type { HtmlAttributesData } from './html-attributes-data';
3
+ export interface HtmlOpenTokenData {
4
+ attributes: HtmlAttributesData;
5
+ kind: typeof HtmlTokenKind.Open;
6
+ name: string;
7
+ selfClosing: boolean;
8
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,9 @@
1
+ export interface HtmlRunData {
2
+ code: boolean;
3
+ em: boolean;
4
+ href: string | null;
5
+ size: number | null;
6
+ strong: boolean;
7
+ text: string;
8
+ underline: boolean;
9
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });