Skip to content

Commit 79ad667

Browse files
committed
Refactor to move implementation to lib/
1 parent 644926d commit 79ad667

File tree

3 files changed

+168
-162
lines changed

3 files changed

+168
-162
lines changed

index.js

Lines changed: 2 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,165 +1,5 @@
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-
451
/**
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
533
*/
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-
}
944

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'

lib/index.js

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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+
/**
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.
53+
*/
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+
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+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"main": "index.js",
3030
"types": "index.d.ts",
3131
"files": [
32+
"lib/",
3233
"index.d.ts",
3334
"index.js"
3435
],

0 commit comments

Comments
 (0)