|
1 |
| -// To do next major: use `structuredClone` (or so?) to deep clone `properties` |
2 |
| -// and the like: the return value has to be a clone (not shallow copy) of the |
3 |
| -// passed tree. |
4 |
| - |
5 |
| -/** |
6 |
| - * @typedef {import('hast').Root} Root |
7 |
| - * @typedef {import('hast').Content} Content |
8 |
| - * @typedef {import('hast').Text} Text |
9 |
| - */ |
10 |
| - |
11 |
| -/** |
12 |
| - * @typedef {Root | Content} Node |
13 |
| - * |
14 |
| - * @typedef Options |
15 |
| - * Configuration. |
16 |
| - * @property {number | null | undefined} [size=140] |
17 |
| - * Number of characters to truncate to. |
18 |
| - * @property {string | null | undefined} [ellipsis] |
19 |
| - * Value to use at truncation point. |
20 |
| - * @property {number | null | undefined} [maxCharacterStrip=30] |
21 |
| - * How far to walk back. |
22 |
| - * The algorithm attempts to break right after a word rather than the exact |
23 |
| - * `size`. |
24 |
| - * Take for example the `|`, which is the actual break defined by `size`, and |
25 |
| - * the `…` is the location where the ellipsis is placed: `This… an|d that`. |
26 |
| - * Breaking at `|` would at best look bad but could likely result in things |
27 |
| - * such as `ass…` for `assignment` — which is not ideal. |
28 |
| - * `maxCharacterStrip` defines how far back the algorithm will walk to find |
29 |
| - * a pretty word break. |
30 |
| - * This prevents a potential slow operation on larger `size`s without any |
31 |
| - * whitespace. |
32 |
| - * If `maxCharacterStrip` characters are walked back and no nice break point |
33 |
| - * is found, the bad break point is used. |
34 |
| - * Set `maxCharacterStrip: 0` to not find a nice break. |
35 |
| - * @property {Array<Content> | null | undefined} [ignore=[]] |
36 |
| - * Nodes to exclude from the resulting tree. |
37 |
| - * These are not counted towards `size`. |
38 |
| - */ |
39 |
| - |
40 |
| -import {unicodeWhitespace, unicodePunctuation} from 'micromark-util-character' |
41 |
| - |
42 |
| -/** @type {ReadonlyArray<void>} */ |
43 |
| -const empty = [] |
44 |
| - |
45 | 1 | /**
|
46 |
| - * Truncate the tree to a certain number of characters. |
47 |
| - * |
48 |
| - * @template {Node} Tree |
49 |
| - * @param {Tree} tree |
50 |
| - * @param {Options | null | undefined} [options] |
51 |
| - * @returns {Tree} |
52 |
| - * A shallow copy of `tree`, truncated. |
| 2 | + * @typedef {import('./lib/index.js').Options} Options |
53 | 3 | */
|
54 |
| -export function truncate(tree, options) { |
55 |
| - // To do: support units. |
56 |
| - const config = options || {} |
57 |
| - const size = typeof config.size === 'number' ? config.size : 140 |
58 |
| - const maxCharacterStrip = |
59 |
| - typeof config.maxCharacterStrip === 'number' ? config.maxCharacterStrip : 30 |
60 |
| - const ignore = config.ignore || empty |
61 |
| - const ellipsis = config.ellipsis |
62 |
| - let searchSize = 0 |
63 |
| - /** @type {Text | undefined} */ |
64 |
| - let overflowingText |
65 |
| - |
66 |
| - const result = preorder(tree) |
67 |
| - |
68 |
| - if (overflowingText) { |
69 |
| - const uglyBreakpoint = size - searchSize |
70 |
| - let breakpoint = uglyBreakpoint |
71 |
| - |
72 |
| - // If the number at the break is not an alphanumerical… |
73 |
| - if (unicodeAlphanumeric(overflowingText.value.charCodeAt(breakpoint))) { |
74 |
| - let remove = -1 |
75 |
| - |
76 |
| - // Move back while the character before breakpoint is an alphanumerical. |
77 |
| - while ( |
78 |
| - breakpoint && |
79 |
| - ++remove < maxCharacterStrip && |
80 |
| - unicodeAlphanumeric(overflowingText.value.charCodeAt(breakpoint - 1)) |
81 |
| - ) { |
82 |
| - breakpoint-- |
83 |
| - } |
84 |
| - |
85 |
| - // Move back while the character before breakpoint is *not* an alphanumerical. |
86 |
| - while ( |
87 |
| - breakpoint && |
88 |
| - ++remove < maxCharacterStrip && |
89 |
| - !unicodeAlphanumeric(overflowingText.value.charCodeAt(breakpoint - 1)) |
90 |
| - ) { |
91 |
| - breakpoint-- |
92 |
| - } |
93 |
| - } |
94 | 4 |
|
95 |
| - overflowingText.value = overflowingText.value.slice( |
96 |
| - 0, |
97 |
| - breakpoint || uglyBreakpoint |
98 |
| - ) |
99 |
| - |
100 |
| - if (ellipsis) { |
101 |
| - overflowingText.value += ellipsis |
102 |
| - } |
103 |
| - } |
104 |
| - |
105 |
| - // @ts-expect-error: `preorder` for the top node always returns itself. |
106 |
| - return result |
107 |
| - |
108 |
| - /** |
109 |
| - * Transform in `preorder`. |
110 |
| - * |
111 |
| - * @param {Node} node |
112 |
| - * Node to truncate. |
113 |
| - * @returns {Node} |
114 |
| - * Shallow copy of `node`. |
115 |
| - */ |
116 |
| - function preorder(node) { |
117 |
| - if (node.type === 'text') { |
118 |
| - if (searchSize + node.value.length > size) { |
119 |
| - overflowingText = {...node} |
120 |
| - return overflowingText |
121 |
| - } |
122 |
| - |
123 |
| - searchSize += node.value.length |
124 |
| - } |
125 |
| - |
126 |
| - /** @type {Node} */ |
127 |
| - const replacement = {...node} |
128 |
| - |
129 |
| - if ('children' in node) { |
130 |
| - /** @type {Array<Content>} */ |
131 |
| - const children = [] |
132 |
| - let index = -1 |
133 |
| - |
134 |
| - while (++index < node.children.length) { |
135 |
| - const child = node.children[index] |
136 |
| - |
137 |
| - if (!ignore.includes(child)) { |
138 |
| - const result = preorder(child) |
139 |
| - // @ts-expect-error: assume content matches. |
140 |
| - if (result) children.push(result) |
141 |
| - } |
142 |
| - |
143 |
| - // One of the descendant texts included the breakpoint. |
144 |
| - if (overflowingText) { |
145 |
| - break |
146 |
| - } |
147 |
| - } |
148 |
| - |
149 |
| - // @ts-expect-error: assume content matches. |
150 |
| - replacement.children = children |
151 |
| - } |
152 |
| - |
153 |
| - return replacement |
154 |
| - } |
155 |
| -} |
156 |
| - |
157 |
| -/** |
158 |
| - * @param {number} code |
159 |
| - * Character code. |
160 |
| - * @returns {boolean} |
161 |
| - * Whether `code` is not whitespace and not punctuation. |
162 |
| - */ |
163 |
| -function unicodeAlphanumeric(code) { |
164 |
| - return !unicodeWhitespace(code) && !unicodePunctuation(code) |
165 |
| -} |
| 5 | +export {truncate} from './lib/index.js' |
0 commit comments