@pallasoft/ps-utilities 2.22.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.
- package/dist/common/constants/html-entity-names.d.ts +1 -0
- package/dist/common/constants/html-entity-names.js +34 -0
- package/dist/common/constants/html-heading-styles.d.ts +1 -0
- package/dist/common/constants/html-heading-styles.js +11 -0
- package/dist/common/constants/html-text-config.d.ts +76 -0
- package/dist/common/constants/html-text-config.js +136 -0
- package/dist/common/constants/html-token-kind.d.ts +5 -0
- package/dist/common/constants/html-token-kind.js +8 -0
- package/dist/common/constants/portable-text-config.d.ts +16 -0
- package/dist/common/constants/portable-text-config.js +15 -1
- package/dist/common/html-entity-service.d.ts +6 -0
- package/dist/common/html-entity-service.js +34 -0
- package/dist/common/html-style-service.d.ts +12 -0
- package/dist/common/html-style-service.js +92 -0
- package/dist/common/html-token-service.d.ts +8 -0
- package/dist/common/html-token-service.js +90 -0
- package/dist/common/index.d.ts +1 -0
- package/dist/common/index.js +1 -0
- package/dist/common/portable-text-html-service.d.ts +28 -0
- package/dist/common/portable-text-html-service.js +392 -0
- package/dist/common/portable-text-link-service.d.ts +7 -0
- package/dist/common/portable-text-link-service.js +42 -0
- package/dist/common/portable-text-service.d.ts +7 -4
- package/dist/common/portable-text-service.js +15 -10
- package/dist/common/portable-text-tracking-service.d.ts +7 -0
- package/dist/common/portable-text-tracking-service.js +45 -0
- package/dist/esm/common/constants/html-entity-names.js +31 -0
- package/dist/esm/common/constants/html-heading-styles.js +8 -0
- package/dist/esm/common/constants/html-text-config.js +133 -0
- package/dist/esm/common/constants/html-token-kind.js +5 -0
- package/dist/esm/common/constants/portable-text-config.js +15 -1
- package/dist/esm/common/html-entity-service.js +25 -0
- package/dist/esm/common/html-style-service.js +85 -0
- package/dist/esm/common/html-token-service.js +83 -0
- package/dist/esm/common/index.js +1 -0
- package/dist/esm/common/portable-text-html-service.js +375 -0
- package/dist/esm/common/portable-text-link-service.js +35 -0
- package/dist/esm/common/portable-text-service.js +14 -9
- package/dist/esm/common/portable-text-tracking-service.js +34 -0
- package/dist/esm/models/html-attributes-data.js +1 -0
- package/dist/esm/models/html-close-token-data.js +1 -0
- package/dist/esm/models/html-context-data.js +1 -0
- package/dist/esm/models/html-conversion-data.js +1 -0
- package/dist/esm/models/html-declarations-data.js +1 -0
- package/dist/esm/models/html-frame-data.js +1 -0
- package/dist/esm/models/html-open-token-data.js +1 -0
- package/dist/esm/models/html-run-data.js +1 -0
- package/dist/esm/models/html-text-token-data.js +1 -0
- package/dist/esm/models/html-token-data.js +1 -0
- package/dist/esm/models/portable-text-block-link.js +1 -0
- package/dist/esm/models/portable-text-content-data.js +1 -0
- package/dist/esm/models/portable-text-image-data.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/models/html-attributes-data.d.ts +1 -0
- package/dist/models/html-attributes-data.js +2 -0
- package/dist/models/html-close-token-data.d.ts +5 -0
- package/dist/models/html-close-token-data.js +2 -0
- package/dist/models/html-context-data.d.ts +6 -0
- package/dist/models/html-context-data.js +2 -0
- package/dist/models/html-conversion-data.d.ts +12 -0
- package/dist/models/html-conversion-data.js +2 -0
- package/dist/models/html-declarations-data.d.ts +1 -0
- package/dist/models/html-declarations-data.js +2 -0
- package/dist/models/html-frame-data.d.ts +13 -0
- package/dist/models/html-frame-data.js +2 -0
- package/dist/models/html-open-token-data.d.ts +8 -0
- package/dist/models/html-open-token-data.js +2 -0
- package/dist/models/html-run-data.d.ts +9 -0
- package/dist/models/html-run-data.js +2 -0
- package/dist/models/html-text-token-data.d.ts +5 -0
- package/dist/models/html-text-token-data.js +2 -0
- package/dist/models/html-token-data.d.ts +4 -0
- package/dist/models/html-token-data.js +2 -0
- package/dist/models/portable-text-block-link.d.ts +7 -0
- package/dist/models/portable-text-block-link.js +2 -0
- package/dist/models/portable-text-content-data.d.ts +3 -0
- package/dist/models/portable-text-content-data.js +2 -0
- package/dist/models/portable-text-image-data.d.ts +7 -0
- package/dist/models/portable-text-image-data.js +2 -0
- 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) ||
|
|
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
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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,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 @@
|
|
|
1
|
+
export type HtmlDeclarationsData = Record<string, string>;
|
|
@@ -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,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
|
+
}
|