@nxtedition/lib 27.1.9 → 28.0.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 (4) hide show
  1. package/app.d.ts +1 -1
  2. package/package.json +2 -4
  3. package/ass.js +0 -411
  4. package/wordwrap.js +0 -130
package/app.d.ts CHANGED
@@ -10,7 +10,7 @@ export function makeApp<
10
10
  Records extends Record<string, unknown> = Record<string, unknown>,
11
11
  RpcMethods extends Record<string, RpcMethodDef> = Record<string, RpcMethodDef>,
12
12
  Config = Record<string, unknown>,
13
- >(appConfig: AppConfig<Config>, meta?: { url: URL }): App<Records, RpcMethods, Config>
13
+ >(appConfig: AppConfig<Config>, meta?: { url: string }): App<Records, RpcMethods, Config>
14
14
 
15
15
  export interface AppConfig<Config = Record<string, unknown>> {
16
16
  name?: string
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@nxtedition/lib",
3
- "version": "27.1.9",
3
+ "version": "28.0.0",
4
4
  "license": "UNLICENSED",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "type": "module",
7
7
  "files": [
8
8
  "index.d.ts",
9
9
  "hash.js",
10
- "ass.js",
11
10
  "util/*",
12
11
  "cache.js",
13
12
  "fixed-queue.js",
@@ -40,7 +39,6 @@
40
39
  "stream.js",
41
40
  "transcript.js",
42
41
  "docker-secrets.js",
43
- "wordwrap.js",
44
42
  "under-pressure.js"
45
43
  ],
46
44
  "scripts": {
@@ -92,5 +90,5 @@
92
90
  "pino": ">=7.0.0",
93
91
  "rxjs": "^7.0.0"
94
92
  },
95
- "gitHead": "b9f4002d3dae6b3fd6c05c53d72d66653a697637"
93
+ "gitHead": "dca4323e9bb3624bdce93ed05e654c8892074f7d"
96
94
  }
package/ass.js DELETED
@@ -1,411 +0,0 @@
1
- // @ts-check
2
- import { wordwrap } from './wordwrap.js'
3
-
4
- const BASE_WIDTH = 1920
5
- const BASE_HEIGHT = 1080
6
-
7
- /**
8
- * @typedef {object} SubtitleEvent
9
- * @property {number} start
10
- * @property {number | null} [duration]
11
- * @property {number | null} [end]
12
- * @property {string | null} [style]
13
- * @property {string | null} [text]
14
- * @property {SubtitleEventStyleOverrides} [styleOverrides]
15
- *
16
- * @typedef {object} SubtitleEventStyleOverrides
17
- * @property {string} [marginL]
18
- * @property {string} [marginR]
19
- * @property {string} [marginV]
20
- * @property {string} [alignment]
21
- * @property {string} [fontname]
22
- * @property {string} [fontsize]
23
- * @property {string} [primaryColour]
24
- * @property {string} [secondaryColour]
25
- * @property {string} [outlineColour]
26
- * @property {string} [backColour]
27
- * @property {string} [bold]
28
- * @property {string} [italic]
29
- * @property {string} [underline]
30
- * @property {string} [strikeOut]
31
- *
32
- * @typedef {object} EmbeddableSubtitleFontFace
33
- * @property {string} family
34
- * @property {string} name
35
- * @property {'normal'|'bold'} [weight]
36
- * @property {'normal'|'italic'} [style]
37
- * @property {Uint8Array} data
38
- *
39
- * @typedef {object} Styles
40
- * @property {string} [name]
41
- * @property {string} [fontname]
42
- * @property {string} [fontsize]
43
- * @property {string} [primaryColour]
44
- * @property {string} [secondaryColour]
45
- * @property {string} [outlineColour]
46
- * @property {string} [backColour]
47
- * @property {string} [bold]
48
- * @property {string} [italic]
49
- * @property {string} [underline]
50
- * @property {string} [strikeOut]
51
- * @property {string} [scaleX]
52
- * @property {string} [scaleY]
53
- * @property {string} [spacing]
54
- * @property {string} [angle]
55
- * @property {string} [borderStyle]
56
- * @property {string} [outline]
57
- * @property {string} [shadow]
58
- * @property {string} [alignment]
59
- * @property {string} [marginL]
60
- * @property {string} [marginR]
61
- * @property {string} [marginV]
62
- * @property {string} [encoding]
63
- *
64
- * @typedef {object} Options
65
- * @property {{ [styleName: string]: Styles }} [styles]
66
- * @property {number} [width]
67
- * @property {number} [height]
68
- * @property {boolean} [scaledBorderAndShadow] TODO: default to true when all client styles depend on it?
69
- * @property {boolean} [futureWordWrapping]
70
- * @property {EmbeddableSubtitleFontFace[]} [fonts]
71
- */
72
-
73
- /**
74
- * @param {SubtitleEvent[]} events
75
- * @param {Options} [options]
76
- * @returns {string}
77
- */
78
- export function encodeASS(
79
- events,
80
- {
81
- styles = {},
82
- width = BASE_WIDTH,
83
- height = BASE_HEIGHT,
84
- scaledBorderAndShadow = false,
85
- futureWordWrapping = false,
86
- fonts = [],
87
- } = {},
88
- ) {
89
- const scale = typeof width === 'number' ? BASE_HEIGHT / height : 1
90
- const playResX = scale * (width || BASE_WIDTH)
91
- const playResY = scale * (height || BASE_HEIGHT)
92
- return [
93
- encASSHeader({ playResX, playResY, scaledBorderAndShadow, futureWordWrapping }),
94
- encFontFaces(fonts),
95
- encASSStyles(styles),
96
- '',
97
- encASSEvents(events, styles, playResX, futureWordWrapping),
98
- ]
99
- .filter((x) => typeof x === 'string')
100
- .join('\n')
101
- }
102
-
103
- /**
104
- * @param {SubtitleEvent[]} events
105
- * @param {{ [styleName: string]: Styles }} styles
106
- * @param {number} scriptResX
107
- * @param {boolean} futureWordWrapping
108
- */
109
- function formatDialogues(events, styles, scriptResX, futureWordWrapping) {
110
- return [...events]
111
- .sort((a, b) => a.start - b.start)
112
- .map((event) => {
113
- const {
114
- start,
115
- duration,
116
- end = duration != null ? start + duration : null,
117
- text,
118
- style,
119
- styleOverrides,
120
- } = event
121
-
122
- if (typeof text !== 'string' || text.length === 0 || typeof start !== 'number') {
123
- return null
124
- }
125
-
126
- const styleName = style || 'nxt-default'
127
- const eventStyle = styles[styleName] ?? {}
128
- const textOptions = {
129
- fontSize: parseFloat(eventStyle.fontsize || '32'),
130
- fontFamily: eventStyle.fontname ?? 'Arial',
131
- maxWidth: Math.max(
132
- 0,
133
- scriptResX -
134
- parseFloat(eventStyle.marginL || '0') -
135
- parseFloat(eventStyle.marginR || '0'),
136
- ),
137
- styleOverrides,
138
- futureWordWrapping,
139
- }
140
-
141
- return `Dialogue: ${[
142
- /* Layer */ '0',
143
- /* Start */ formatASSTime(start) || '0:00:00.00',
144
- /* End */ formatASSTime(end) || '9:59:59.00',
145
- /* Style */ styleName,
146
- /* Name */ '',
147
- /* MarginL */ (styleOverrides?.marginL ?? '0').padStart(4, '0'),
148
- /* MarginR */ (styleOverrides?.marginR ?? '0').padStart(4, '0'),
149
- /* MarginV */ (styleOverrides?.marginV ?? '0').padStart(4, '0'),
150
- /* Effect */ '',
151
- /* Text */ encodeText(text, textOptions),
152
- ]
153
- .filter((s) => typeof s === 'string')
154
- .join(',')}`
155
- })
156
- .join('\n')
157
- }
158
-
159
- /**
160
- * @param {string} text
161
- * @param {object} options
162
- * @param {number} options.fontSize
163
- * @param {string} options.fontFamily
164
- * @param {number} options.maxWidth
165
- * @param {SubtitleEventStyleOverrides} [options.styleOverrides]
166
- * @param {boolean} options.futureWordWrapping
167
- */
168
- function encodeText(text, { styleOverrides, futureWordWrapping, ...options }) {
169
- return `${getOverrideTags(styleOverrides)}${text
170
- .split(/\\n|\n|\\N/)
171
- .map((line) => line.trim())
172
- .flatMap((line) =>
173
- futureWordWrapping ? wordwrap(line, { ...options, breakWord: false }) : [line],
174
- )
175
- .map((line) => line.trim())
176
- .join('\\N')}`
177
- }
178
-
179
- /**
180
- * @param {SubtitleEventStyleOverrides} [styleOverrides]
181
- */
182
- function getOverrideTags(styleOverrides) {
183
- return Object.entries(styleOverrides ?? {})
184
- .map(([key, value]) => {
185
- switch (key) {
186
- case 'alignment':
187
- return `an${value}`
188
- case 'primaryColour':
189
- return `c${value}`
190
- case 'secondaryColour':
191
- return `2c${value}`
192
- case 'outlineColour':
193
- return `3c${value}`
194
- case 'backColour':
195
- return `4c${value}`
196
- case 'bold':
197
- return `b${value === '0' ? '0' : '1'}`
198
- case 'italic':
199
- return `i${value === '0' ? '0' : '1'}`
200
- case 'underline':
201
- return `u${value === '0' ? '0' : '1'}`
202
- case 'strikeOut':
203
- return `s${value === '0' ? '0' : '1'}`
204
- case 'fontname':
205
- return `fn${value}`
206
- case 'fontsize':
207
- return `fs${value}`
208
- default:
209
- return null
210
- }
211
- })
212
- .filter((tag) => typeof tag === 'string')
213
- .map((tag) => `{\\${tag}}`)
214
- .join('')
215
- }
216
-
217
- /**
218
- * @param {SubtitleEvent[]} events
219
- * @param {{ [styleName: string]: Styles }} styles
220
- * @param {number} scriptResX
221
- * @param {boolean} futureWordWrapping
222
- */
223
- function encASSEvents(events, styles, scriptResX, futureWordWrapping) {
224
- return `[Events]
225
- Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
226
- ${formatDialogues(events, styles, scriptResX, futureWordWrapping)}\n`
227
- }
228
-
229
- /**
230
- * @param {{ [styleName: string]: Styles }} styles
231
- * @returns {string}
232
- */
233
- const formatStyles = (styles) =>
234
- Object.entries(styles)
235
- .map(([id, style]) =>
236
- [
237
- `Style: ${id}`,
238
- style.fontname,
239
- style.fontsize,
240
- style.primaryColour,
241
- style.secondaryColour,
242
- style.outlineColour,
243
- style.backColour,
244
- style.bold,
245
- style.italic,
246
- style.underline,
247
- style.strikeOut,
248
- style.scaleX,
249
- style.scaleY,
250
- style.spacing,
251
- style.angle,
252
- style.borderStyle,
253
- style.outline,
254
- style.shadow,
255
- style.alignment,
256
- style.marginL,
257
- style.marginR,
258
- style.marginV,
259
- style.encoding,
260
- ]
261
- .map((s) => s ?? '')
262
- .join(','),
263
- )
264
- .join('\n')
265
-
266
- /**
267
- * @param {{ [styleName: string]: Styles }} styles
268
- * @returns {string}
269
- */
270
- function encASSStyles(styles) {
271
- return `[V4+ Styles]
272
- Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
273
- ${formatStyles(styles)}`
274
- }
275
-
276
- /**
277
- * @param {object} params
278
- * @param {number} params.playResX
279
- * @param {number} params.playResY
280
- * @param {boolean} [params.scaledBorderAndShadow]
281
- * @param {boolean} params.futureWordWrapping
282
- * @returns {string}
283
- */
284
- function encASSHeader({ playResX, playResY, scaledBorderAndShadow, futureWordWrapping }) {
285
- // WrapStyle
286
- // 0: smart wrapping, lines are evenly broken
287
- // 1: end-of-line word wrapping, only \N breaks
288
- // 2: no word wrapping, \n \N both breaks
289
- // 3: same as 0, but lower line gets wider.
290
- const header = [
291
- '[Script Info]',
292
- 'ScriptType: v4.00+',
293
- `PlayResX: ${playResX}`,
294
- `PlayResY: ${playResY}`,
295
- // new way is to not let ass wrap (2) -- we handle it ourselves
296
- // to make sure hub and render gets the same result
297
- `WrapStyle: ${futureWordWrapping ? 2 : 1}`,
298
- ]
299
- if (scaledBorderAndShadow) {
300
- header.push('ScaledBorderAndShadow: Yes')
301
- }
302
- return `${header.join('\n')}\n`
303
- }
304
-
305
- /**
306
- * @param {number | null | undefined} seconds
307
- * @returns {string}
308
- */
309
- function formatASSTime(seconds) {
310
- if (seconds == null) {
311
- return ''
312
- }
313
-
314
- const h = Math.floor(seconds / 3600)
315
- seconds -= h * 3600
316
- const m = Math.floor(seconds / 60)
317
- seconds -= m * 60
318
- const s = Math.floor(seconds)
319
- const cs = Math.floor((seconds - s) * 100)
320
-
321
- return (
322
- h +
323
- ':' +
324
- (m === 0 ? '00' : m < 10 ? '0' + m : m) +
325
- ':' +
326
- (s === 0 ? '00' : s < 10 ? '0' + s : s) +
327
- '.' +
328
- (cs === 0 ? '00' : cs < 10 ? '0' + cs : cs)
329
- )
330
- }
331
-
332
- /**
333
- * @param {EmbeddableSubtitleFontFace[]} fontFaces
334
- * @returns {string | undefined}
335
- */
336
- function encFontFaces(fontFaces) {
337
- if (fontFaces.length === 0) {
338
- return
339
- }
340
- return [
341
- '[Fonts]',
342
- ...fontFaces.map((fontFace) => {
343
- return [`fontname: ${getFontName(fontFace)}`, ...uuencodeForSSA(fontFace.data)]
344
- }),
345
- ]
346
- .filter((x) => x !== undefined)
347
- .flat()
348
- .join('\n')
349
- }
350
-
351
- /**
352
- * @param {EmbeddableSubtitleFontFace} fontFace
353
- */
354
- function getFontName(fontFace) {
355
- const { name, family, weight, style } = fontFace
356
- // NOTE: Seems like libass doesn't care about the font name as specified in the original spec.
357
- // That is good, because then we can register more faces than the original spec allowed.
358
- return name ?? `${family}_${weight === 'bold' ? 'B' : ''}${style === 'italic' ? 'I' : ''}0`
359
- }
360
-
361
- /**
362
- * UUEncode a binary file (e.g. font file) for SSA/ASS embedding.
363
- * @param {Uint8Array} data - Binary content of the file.
364
- * @returns {string[]} - Encoded lines, each max 80 characters.
365
- */
366
- function uuencodeForSSA(data) {
367
- const result = []
368
- const outputLineLength = 80
369
-
370
- let i = 0
371
- const fullChunks = Math.floor(data.length / 3)
372
-
373
- // Encode 3-byte chunks
374
- while (i < fullChunks * 3) {
375
- const b1 = data[i++]
376
- const b2 = data[i++]
377
- const b3 = data[i++]
378
- const n = (b1 << 16) | (b2 << 8) | b3
379
-
380
- const c1 = ((n >> 18) & 0x3f) + 33
381
- const c2 = ((n >> 12) & 0x3f) + 33
382
- const c3 = ((n >> 6) & 0x3f) + 33
383
- const c4 = (n & 0x3f) + 33
384
-
385
- result.push(String.fromCharCode(c1, c2, c3, c4))
386
- }
387
-
388
- // Handle remainder
389
- const remaining = data.length % 3
390
- if (remaining === 1) {
391
- const last = data[i] * 0x100
392
- const c1 = ((last >> 10) & 0x3f) + 33
393
- const c2 = ((last >> 4) & 0x3f) + 33
394
- result.push(String.fromCharCode(c1, c2))
395
- } else if (remaining === 2) {
396
- const last = ((data[i] << 8) | data[i + 1]) * 0x10000
397
- const c1 = ((last >> 16) & 0x3f) + 33
398
- const c2 = ((last >> 10) & 0x3f) + 33
399
- const c3 = ((last >> 4) & 0x3f) + 33
400
- result.push(String.fromCharCode(c1, c2, c3))
401
- }
402
-
403
- // Break into 80-char lines
404
- const finalLines = []
405
- const allChars = result.join('')
406
- for (let j = 0; j < allChars.length; j += outputLineLength) {
407
- finalLines.push(allChars.slice(j, j + outputLineLength))
408
- }
409
-
410
- return finalLines
411
- }
package/wordwrap.js DELETED
@@ -1,130 +0,0 @@
1
- // @ts-check
2
- import { createCanvas } from 'canvas'
3
-
4
- const canvas = createCanvas(0, 0)
5
- const ctx = canvas.getContext('2d')
6
-
7
- /**
8
- * Takes a string and a maxWidth and splits the text into lines.
9
- *
10
- * @param {string} text
11
- * @param {object} options
12
- * @param {number} options.fontSize
13
- * @param {string} options.fontFamily
14
- * @param {number} options.maxWidth
15
- * @param {boolean} [options.breakWord]
16
- */
17
- export function wordwrap(text, { fontSize, fontFamily, maxWidth, breakWord = true }) {
18
- // TODO: Register fonts
19
- // const fontFile = name =>
20
- // path.join(path.dirname(__dirname), 'fonts', `${name}.ttf`)
21
- // const ptSans = new Canvas.Font('PTSans', fontFile('ptsans-regular'))
22
- // ctx.addFont(ptSans)
23
-
24
- ctx.font = `${fontSize}px ${fontFamily}`
25
- const emMeasure = ctx.measureText('M').width
26
- const spaceMeasure = ctx.measureText(' ').width
27
-
28
- if (maxWidth < emMeasure) {
29
- // To prevent weird looping anamolies farther on.
30
- return text.split('')
31
- }
32
-
33
- if (ctx.measureText(text).width < maxWidth) {
34
- return [text]
35
- }
36
-
37
- const words = text.split(' ')
38
- const metawords = []
39
- const lines = []
40
-
41
- // measure first.
42
- for (const w in words) {
43
- const word = words[w]
44
- const measure = ctx.measureText(word).width
45
-
46
- // Edge case - If the current word is too long for one line, break it into maximized pieces.
47
- if (breakWord && measure > maxWidth) {
48
- // TODO - a divide and conquer method might be nicer.
49
- const edgewords = splitWord(word, maxWidth)
50
-
51
- // could use metawords = metawords.concat(edgwords)
52
- for (const ew in edgewords) {
53
- metawords.push(edgewords[ew])
54
- }
55
- } else {
56
- metawords.push({ word, len: word.length, measure })
57
- }
58
- }
59
-
60
- // build array of lines second.
61
- let cline = ''
62
- let cmeasure = 0
63
- for (const mw in metawords) {
64
- const metaword = metawords[mw]
65
-
66
- // If current word doesn't fit on current line, push the current line and start a new one.
67
- // Unless (edge-case): this is a new line and the current word is one character.
68
- if (cmeasure + metaword.measure > maxWidth && cmeasure > 0 && metaword.len > 1) {
69
- lines.push(cline)
70
- cline = ''
71
- cmeasure = 0
72
- }
73
-
74
- cline += metaword.word
75
- cmeasure += metaword.measure
76
-
77
- // If there's room, append a space, else push the current line and start a new one.
78
- if (cmeasure + spaceMeasure < maxWidth) {
79
- cline += ' '
80
- cmeasure += spaceMeasure
81
- } else {
82
- lines.push(cline)
83
- cline = ''
84
- cmeasure = 0
85
- }
86
- }
87
- if (cmeasure > 0) {
88
- lines.push(cline)
89
- }
90
-
91
- return lines
92
- }
93
-
94
- /**
95
- * @param {string} word
96
- * @param {number} maxWidth
97
- */
98
- function splitWord(word, maxWidth) {
99
- const wlen = word.length
100
- if (wlen === 0) {
101
- return []
102
- }
103
-
104
- const awords = []
105
- const letters = []
106
- let cword = ''
107
- let cmeasure = 0
108
-
109
- // Measure each letter.
110
- for (let l = 0; l < wlen; l += 1) {
111
- letters.push({ letter: word[l], measure: ctx.measureText(word[l]).width })
112
- }
113
-
114
- // Assemble the letters into words of maximized length.
115
- for (const ml in letters) {
116
- const metaletter = letters[ml]
117
-
118
- if (cmeasure + metaletter.measure > maxWidth) {
119
- awords.push({ word: cword, len: cword.length, measure: cmeasure })
120
- cword = ''
121
- cmeasure = 0
122
- }
123
-
124
- cword += metaletter.letter
125
- cmeasure += metaletter.measure
126
- }
127
- // there will always be one more word to push.
128
- awords.push({ word: cword, len: cword.length, measure: cmeasure })
129
- return awords
130
- }