@stackline/wcwidth 1.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.
- package/CHANGELOG.md +16 -0
- package/COMPATIBILITY.md +5 -0
- package/COMPATIBILITY_CONTRACT.md +75 -0
- package/CONTRIBUTING.md +18 -0
- package/LICENSE +21 -0
- package/MIGRATION.md +39 -0
- package/NOTICE +15 -0
- package/PUBLISHING.md +103 -0
- package/README.md +112 -0
- package/SECURITY.md +21 -0
- package/THIRD_PARTY_LICENSES.md +90 -0
- package/VERIFICATION.md +29 -0
- package/combining.d.ts +3 -0
- package/combining.js +5 -0
- package/dist/index.mjs +1 -0
- package/examples/commonjs.cjs +6 -0
- package/examples/esm.mjs +4 -0
- package/examples/install.sh +1 -0
- package/examples/legacy-alias.json +5 -0
- package/index.d.cts +17 -0
- package/index.d.mts +19 -0
- package/index.d.ts +17 -0
- package/index.js +28 -0
- package/index.mjs +7 -0
- package/lib/defaults.js +12 -0
- package/lib/graphemes.js +147 -0
- package/lib/ranges.js +22 -0
- package/lib/unicode-tables.js +653 -0
- package/lib/width.js +91 -0
- package/package.json +149 -0
- package/scripts/test-package.mjs +38 -0
- package/tools/generate-unicode-tables.mjs +234 -0
- package/unicode-sources.json +33 -0
package/lib/width.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
EMOJI,
|
|
5
|
+
EMOJI_PRESENTATION,
|
|
6
|
+
EXTENDED_PICTOGRAPHIC,
|
|
7
|
+
UNICODE_VERSION,
|
|
8
|
+
WIDE,
|
|
9
|
+
ZERO_WIDTH
|
|
10
|
+
} = require('./unicode-tables')
|
|
11
|
+
const { splitGraphemeCodePoints } = require('./graphemes')
|
|
12
|
+
const { inRanges } = require('./ranges')
|
|
13
|
+
|
|
14
|
+
const TEXT_VARIATION_SELECTOR = 0xfe0e
|
|
15
|
+
const EMOJI_VARIATION_SELECTOR = 0xfe0f
|
|
16
|
+
const ZERO_WIDTH_JOINER = 0x200d
|
|
17
|
+
const COMBINING_ENCLOSING_KEYCAP = 0x20e3
|
|
18
|
+
|
|
19
|
+
function measure(value, options) {
|
|
20
|
+
if (typeof value !== 'string') return codePointWidth(value, options)
|
|
21
|
+
|
|
22
|
+
const asciiWidth = printableAsciiWidth(value)
|
|
23
|
+
if (asciiWidth !== -1) return asciiWidth
|
|
24
|
+
|
|
25
|
+
let total = 0
|
|
26
|
+
for (const cluster of splitGraphemeCodePoints(value)) {
|
|
27
|
+
const width = graphemeWidth(cluster, options)
|
|
28
|
+
if (width < 0) return -1
|
|
29
|
+
total += width
|
|
30
|
+
}
|
|
31
|
+
return total
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function printableAsciiWidth(value) {
|
|
35
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
36
|
+
const codeUnit = value.charCodeAt(index)
|
|
37
|
+
if (codeUnit < 0x20 || codeUnit > 0x7e) return -1
|
|
38
|
+
}
|
|
39
|
+
return value.length
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function graphemeWidth(points, options) {
|
|
43
|
+
if (isEmojiCluster(points)) return 2
|
|
44
|
+
|
|
45
|
+
let total = 0
|
|
46
|
+
for (const point of points) {
|
|
47
|
+
const width = codePointWidth(point, options)
|
|
48
|
+
if (width < 0) return -1
|
|
49
|
+
total += width
|
|
50
|
+
}
|
|
51
|
+
return total
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function codePointWidth(point, options) {
|
|
55
|
+
if (point === 0) return options.nul
|
|
56
|
+
if (point < 32 || (point >= 0x7f && point < 0xa0)) return options.control
|
|
57
|
+
if (inRanges(point, ZERO_WIDTH)) return 0
|
|
58
|
+
if (inRanges(point, WIDE)) return 2
|
|
59
|
+
return 1
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isEmojiCluster(points) {
|
|
63
|
+
if (points.includes(TEXT_VARIATION_SELECTOR) && !points.includes(EMOJI_VARIATION_SELECTOR)) {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const hasEmoji = points.some((point) => inRanges(point, EMOJI))
|
|
68
|
+
if (hasEmoji && points.includes(EMOJI_VARIATION_SELECTOR)) return true
|
|
69
|
+
if (points.includes(COMBINING_ENCLOSING_KEYCAP) && points.some(isKeycapBase)) return true
|
|
70
|
+
if (points.some((point) => inRanges(point, EMOJI_PRESENTATION))) return true
|
|
71
|
+
|
|
72
|
+
if (points.includes(ZERO_WIDTH_JOINER)) {
|
|
73
|
+
let pictographs = 0
|
|
74
|
+
for (const point of points) {
|
|
75
|
+
if (inRanges(point, EXTENDED_PICTOGRAPHIC)) pictographs += 1
|
|
76
|
+
}
|
|
77
|
+
if (pictographs >= 2) return true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isKeycapBase(point) {
|
|
84
|
+
return point === 0x23 || point === 0x2a || (point >= 0x30 && point <= 0x39)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
UNICODE_VERSION,
|
|
89
|
+
codePointWidth,
|
|
90
|
+
measure
|
|
91
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stackline/wcwidth",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Terminal column widths with modern Unicode grapheme support and wcwidth compatibility",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"terminal",
|
|
7
|
+
"unicode",
|
|
8
|
+
"width",
|
|
9
|
+
"wcwidth",
|
|
10
|
+
"grapheme",
|
|
11
|
+
"emoji"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "commonjs",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"main": "./index.js",
|
|
17
|
+
"module": "./dist/index.mjs",
|
|
18
|
+
"types": "./index.d.ts",
|
|
19
|
+
"typings": "./index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": {
|
|
23
|
+
"import": "./index.d.mts",
|
|
24
|
+
"require": "./index.d.cts",
|
|
25
|
+
"default": "./index.d.ts"
|
|
26
|
+
},
|
|
27
|
+
"browser": {
|
|
28
|
+
"import": "./dist/index.mjs",
|
|
29
|
+
"require": "./index.js",
|
|
30
|
+
"default": "./index.js"
|
|
31
|
+
},
|
|
32
|
+
"import": "./dist/index.mjs",
|
|
33
|
+
"require": "./index.js",
|
|
34
|
+
"default": "./index.js"
|
|
35
|
+
},
|
|
36
|
+
"./index.js": {
|
|
37
|
+
"types": {
|
|
38
|
+
"import": "./index.d.mts",
|
|
39
|
+
"require": "./index.d.cts",
|
|
40
|
+
"default": "./index.d.ts"
|
|
41
|
+
},
|
|
42
|
+
"import": "./dist/index.mjs",
|
|
43
|
+
"require": "./index.js",
|
|
44
|
+
"default": "./index.js"
|
|
45
|
+
},
|
|
46
|
+
"./combining": {
|
|
47
|
+
"types": "./combining.d.ts",
|
|
48
|
+
"require": "./combining.js",
|
|
49
|
+
"default": "./combining.js"
|
|
50
|
+
},
|
|
51
|
+
"./combining.js": {
|
|
52
|
+
"types": "./combining.d.ts",
|
|
53
|
+
"require": "./combining.js",
|
|
54
|
+
"default": "./combining.js"
|
|
55
|
+
},
|
|
56
|
+
"./package.json": "./package.json"
|
|
57
|
+
},
|
|
58
|
+
"files": [
|
|
59
|
+
"index.js",
|
|
60
|
+
"index.mjs",
|
|
61
|
+
"index.d.ts",
|
|
62
|
+
"index.d.cts",
|
|
63
|
+
"index.d.mts",
|
|
64
|
+
"combining.js",
|
|
65
|
+
"combining.d.ts",
|
|
66
|
+
"dist",
|
|
67
|
+
"lib",
|
|
68
|
+
"tools",
|
|
69
|
+
"examples",
|
|
70
|
+
"scripts/test-package.mjs",
|
|
71
|
+
"CHANGELOG.md",
|
|
72
|
+
"COMPATIBILITY.md",
|
|
73
|
+
"COMPATIBILITY_CONTRACT.md",
|
|
74
|
+
"CONTRIBUTING.md",
|
|
75
|
+
"LICENSE",
|
|
76
|
+
"MIGRATION.md",
|
|
77
|
+
"NOTICE",
|
|
78
|
+
"PUBLISHING.md",
|
|
79
|
+
"README.md",
|
|
80
|
+
"SECURITY.md",
|
|
81
|
+
"THIRD_PARTY_LICENSES.md",
|
|
82
|
+
"VERIFICATION.md",
|
|
83
|
+
"unicode-sources.json"
|
|
84
|
+
],
|
|
85
|
+
"scripts": {
|
|
86
|
+
"clean": "node scripts/clean.mjs",
|
|
87
|
+
"build": "node scripts/build.mjs",
|
|
88
|
+
"lint": "eslint index.js index.mjs lib scripts test examples docs-site/*.js docs-site/*.mjs",
|
|
89
|
+
"docs:prepare": "node docs-site/prepare.mjs",
|
|
90
|
+
"docs:check": "node docs-site/check.mjs",
|
|
91
|
+
"unicode:generate": "node tools/generate-unicode-tables.mjs",
|
|
92
|
+
"unicode:check": "node tools/generate-unicode-tables.mjs --check",
|
|
93
|
+
"test:source": "npm run test:core && npm run test:release-helpers && npm run test:browser && npm run test:types",
|
|
94
|
+
"test:core": "node --test test/*.test.js",
|
|
95
|
+
"test:release-helpers": "node --test test/registry-verification.test.cjs",
|
|
96
|
+
"test:browser": "node scripts/check-browser.mjs",
|
|
97
|
+
"test:types": "tsc -p test/types/modern/tsconfig.json && node node_modules/typescript-3-9/bin/tsc -p test/types/legacy/tsconfig.json --typeRoots ./test/types/no-global-types",
|
|
98
|
+
"test": "node scripts/test-package.mjs",
|
|
99
|
+
"test:coverage": "c8 --all --include=index.js --include=lib/*.js --exclude=lib/unicode-tables.js --check-coverage --lines 95 --functions 95 --branches 90 node --test test/*.test.js",
|
|
100
|
+
"test:smoke": "node scripts/smoke-install.mjs",
|
|
101
|
+
"test:closure": "node scripts/check-production-closure.mjs",
|
|
102
|
+
"check:package": "publint --strict && attw --pack .",
|
|
103
|
+
"check:licenses": "node scripts/check-licenses.mjs",
|
|
104
|
+
"check:sbom": "node scripts/check-sbom.mjs",
|
|
105
|
+
"check:release": "node scripts/check-release.mjs",
|
|
106
|
+
"evidence:assemble": "node scripts/assemble-release-assets.mjs",
|
|
107
|
+
"evidence:check": "node scripts/check-release-assets.mjs",
|
|
108
|
+
"pack:check": "npm pack --dry-run",
|
|
109
|
+
"audit:dependencies": "npm audit --omit=dev --audit-level=low",
|
|
110
|
+
"audit:all": "npm audit --audit-level=low",
|
|
111
|
+
"audit:signatures": "npm audit signatures",
|
|
112
|
+
"verify": "npm run clean && npm run lint && npm run build && npm run docs:prepare && npm run docs:check && npm run unicode:check && npm test && npm run test:coverage && npm run test:smoke && npm run test:closure && npm run check:package && npm run check:licenses && npm run check:sbom && npm run check:release && npm run pack:check && npm run audit:dependencies && npm run audit:all && npm run audit:signatures",
|
|
113
|
+
"artifact:prepare": "node scripts/create-artifact.mjs",
|
|
114
|
+
"prepack": "npm run build"
|
|
115
|
+
},
|
|
116
|
+
"engines": {
|
|
117
|
+
"node": ">=18.0.0"
|
|
118
|
+
},
|
|
119
|
+
"publishConfig": {
|
|
120
|
+
"access": "public",
|
|
121
|
+
"provenance": true
|
|
122
|
+
},
|
|
123
|
+
"dependencies": {},
|
|
124
|
+
"devDependencies": {
|
|
125
|
+
"@arethetypeswrong/cli": "0.18.5",
|
|
126
|
+
"@eslint/js": "10.0.1",
|
|
127
|
+
"@rollup/plugin-node-resolve": "16.0.3",
|
|
128
|
+
"c8": "12.0.0",
|
|
129
|
+
"esbuild": "0.28.2",
|
|
130
|
+
"eslint": "10.9.1",
|
|
131
|
+
"publint": "0.3.24",
|
|
132
|
+
"rollup": "4.63.1",
|
|
133
|
+
"typescript": "7.0.2",
|
|
134
|
+
"typescript-3-9": "npm:typescript@3.9.10",
|
|
135
|
+
"wcwidth-upstream": "npm:wcwidth@1.0.1"
|
|
136
|
+
},
|
|
137
|
+
"author": "Stackline maintainers",
|
|
138
|
+
"contributors": [
|
|
139
|
+
"Jun Woong (original wcwidth JavaScript port)"
|
|
140
|
+
],
|
|
141
|
+
"homepage": "https://alexandro.net/docs/vanilla/wcwidth/",
|
|
142
|
+
"repository": {
|
|
143
|
+
"type": "git",
|
|
144
|
+
"url": "git+https://github.com/alexandroit/stackline-wcwidth.git"
|
|
145
|
+
},
|
|
146
|
+
"bugs": {
|
|
147
|
+
"url": "https://github.com/alexandroit/stackline-wcwidth/issues"
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { spawnSync } from 'node:child_process'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
|
|
6
|
+
const root = new URL('../', import.meta.url)
|
|
7
|
+
|
|
8
|
+
if (existsSync(new URL('../test/api.test.js', import.meta.url))) {
|
|
9
|
+
if (!existsSync(new URL('../dist/index.mjs', import.meta.url))) runNpm(['run', 'build'])
|
|
10
|
+
runNpm(['run', 'test:source'])
|
|
11
|
+
} else {
|
|
12
|
+
const require = createRequire(import.meta.url)
|
|
13
|
+
const wcwidth = require('../index.js')
|
|
14
|
+
const esm = await import(new URL('../dist/index.mjs', import.meta.url))
|
|
15
|
+
|
|
16
|
+
assert.equal(typeof wcwidth, 'function')
|
|
17
|
+
assert.deepEqual(Object.keys(wcwidth), ['config'])
|
|
18
|
+
assert.equal(wcwidth('A字🤦🏼♂️e\u0301'), 6)
|
|
19
|
+
assert.equal(wcwidth.config({ control: -1 })('a\nb'), -1)
|
|
20
|
+
assert.equal(esm.default('🇨🇦'), 2)
|
|
21
|
+
assert.equal(esm.config({ nul: 3 })('\0'), 3)
|
|
22
|
+
assert.equal(esm.unicodeVersion, '17.0.0')
|
|
23
|
+
process.stdout.write('Installed package CJS and ESM smoke tests passed.\n')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function runNpm(arguments_) {
|
|
27
|
+
const command = process.env.npm_execpath || 'npm'
|
|
28
|
+
const invocation = process.env.npm_execpath
|
|
29
|
+
? [command, ...arguments_]
|
|
30
|
+
: arguments_
|
|
31
|
+
const result = spawnSync(process.env.npm_execpath ? process.execPath : command, invocation, {
|
|
32
|
+
cwd: root,
|
|
33
|
+
encoding: 'utf8',
|
|
34
|
+
stdio: 'inherit'
|
|
35
|
+
})
|
|
36
|
+
if (result.error) throw result.error
|
|
37
|
+
assert.equal(result.status, 0, `npm ${arguments_.join(' ')} failed`)
|
|
38
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { access, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import { dirname, join } from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
10
|
+
const manifestPath = join(root, 'unicode-sources.json')
|
|
11
|
+
const outputPath = join(root, 'lib', 'unicode-tables.js')
|
|
12
|
+
const graphemeTestPath = join(root, 'test', 'fixtures', 'GraphemeBreakTest.txt')
|
|
13
|
+
const emojiTestPath = join(root, 'test', 'fixtures', 'emoji-test.txt')
|
|
14
|
+
const check = process.argv.includes('--check')
|
|
15
|
+
|
|
16
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
17
|
+
const scratch = await mkdtemp(join(tmpdir(), 'stackline-wcwidth-unicode-'))
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const contents = {}
|
|
21
|
+
for (const [name, source] of Object.entries(manifest.sources)) {
|
|
22
|
+
const response = await fetch(source.url)
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(`Unable to download ${source.url}: HTTP ${response.status}`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const bytes = Buffer.from(await response.arrayBuffer())
|
|
28
|
+
const actual = createHash('sha256').update(bytes).digest('hex')
|
|
29
|
+
if (actual !== source.sha256) {
|
|
30
|
+
throw new Error(`${name} checksum mismatch: expected ${source.sha256}, got ${actual}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await writeFile(join(scratch, name), bytes)
|
|
34
|
+
contents[name] = bytes.toString('utf8')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const generated = generateTables(manifest.version, contents)
|
|
38
|
+
const graphemeTests = contents['GraphemeBreakTest.txt']
|
|
39
|
+
const emojiTests = contents['emoji-test.txt']
|
|
40
|
+
const sourceTreeIncludesTests = await exists(dirname(graphemeTestPath))
|
|
41
|
+
|
|
42
|
+
if (check) {
|
|
43
|
+
const current = await readFile(outputPath, 'utf8')
|
|
44
|
+
if (current !== generated) {
|
|
45
|
+
throw new Error('lib/unicode-tables.js is not reproducible from unicode-sources.json')
|
|
46
|
+
}
|
|
47
|
+
if (sourceTreeIncludesTests) {
|
|
48
|
+
const currentGraphemeTests = await readFile(graphemeTestPath, 'utf8')
|
|
49
|
+
if (currentGraphemeTests !== graphemeTests) {
|
|
50
|
+
throw new Error('GraphemeBreakTest.txt does not match its pinned Unicode source')
|
|
51
|
+
}
|
|
52
|
+
const currentEmojiTests = await readFile(emojiTestPath, 'utf8')
|
|
53
|
+
if (currentEmojiTests !== emojiTests) {
|
|
54
|
+
throw new Error('emoji-test.txt does not match its pinned Unicode source')
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
process.stdout.write(`Unicode ${manifest.version} tables are current.\n`)
|
|
58
|
+
} else {
|
|
59
|
+
await writeFile(outputPath, generated)
|
|
60
|
+
if (sourceTreeIncludesTests) {
|
|
61
|
+
await writeFile(graphemeTestPath, graphemeTests)
|
|
62
|
+
await writeFile(emojiTestPath, emojiTests)
|
|
63
|
+
}
|
|
64
|
+
process.stdout.write(`Generated ${outputPath} from Unicode ${manifest.version}.\n`)
|
|
65
|
+
}
|
|
66
|
+
} finally {
|
|
67
|
+
await rm(scratch, { recursive: true, force: true })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function exists(pathname) {
|
|
71
|
+
try {
|
|
72
|
+
await access(pathname)
|
|
73
|
+
return true
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error && error.code === 'ENOENT') return false
|
|
76
|
+
throw error
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function generateTables(version, files) {
|
|
81
|
+
const categories = parseUnicodeData(files['UnicodeData.txt'])
|
|
82
|
+
const zeroWidth = mergeRanges([
|
|
83
|
+
...categories.Mn,
|
|
84
|
+
...categories.Me,
|
|
85
|
+
...categories.Cf,
|
|
86
|
+
[0x1160, 0x11ff],
|
|
87
|
+
[0xd7b0, 0xd7c6],
|
|
88
|
+
[0xd7cb, 0xd7fb]
|
|
89
|
+
].flatMap((range) => subtractCodePoint(range, 0x00ad)))
|
|
90
|
+
|
|
91
|
+
const eastAsian = parseProperties(files['EastAsianWidth.txt'])
|
|
92
|
+
const wide = mergeRanges([...(eastAsian.W || []), ...(eastAsian.F || [])])
|
|
93
|
+
|
|
94
|
+
const grapheme = parseProperties(files['GraphemeBreakProperty.txt'])
|
|
95
|
+
const emoji = parseProperties(files['emoji-data.txt'])
|
|
96
|
+
const derived = parseProperties(files['DerivedCoreProperties.txt'], true)
|
|
97
|
+
|
|
98
|
+
const tables = {
|
|
99
|
+
ZERO_WIDTH: zeroWidth,
|
|
100
|
+
WIDE: wide,
|
|
101
|
+
EMOJI: emoji.Emoji || [],
|
|
102
|
+
EMOJI_PRESENTATION: emoji.Emoji_Presentation || [],
|
|
103
|
+
EXTENDED_PICTOGRAPHIC: emoji.Extended_Pictographic || [],
|
|
104
|
+
GCB_CR: grapheme.CR || [],
|
|
105
|
+
GCB_LF: grapheme.LF || [],
|
|
106
|
+
GCB_CONTROL: grapheme.Control || [],
|
|
107
|
+
GCB_EXTEND: grapheme.Extend || [],
|
|
108
|
+
GCB_ZWJ: grapheme.ZWJ || [],
|
|
109
|
+
GCB_REGIONAL_INDICATOR: grapheme.Regional_Indicator || [],
|
|
110
|
+
GCB_PREPEND: grapheme.Prepend || [],
|
|
111
|
+
GCB_SPACING_MARK: grapheme.SpacingMark || [],
|
|
112
|
+
GCB_L: grapheme.L || [],
|
|
113
|
+
GCB_V: grapheme.V || [],
|
|
114
|
+
GCB_T: grapheme.T || [],
|
|
115
|
+
GCB_LV: grapheme.LV || [],
|
|
116
|
+
GCB_LVT: grapheme.LVT || [],
|
|
117
|
+
INCB_CONSONANT: derived['InCB=Consonant'] || [],
|
|
118
|
+
INCB_EXTEND: derived['InCB=Extend'] || [],
|
|
119
|
+
INCB_LINKER: derived['InCB=Linker'] || []
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const lines = [
|
|
123
|
+
"'use strict'",
|
|
124
|
+
'',
|
|
125
|
+
'// Generated by tools/generate-unicode-tables.mjs. Do not edit by hand.',
|
|
126
|
+
`const UNICODE_VERSION = ${JSON.stringify(version)}`,
|
|
127
|
+
''
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
for (const [name, ranges] of Object.entries(tables)) {
|
|
131
|
+
lines.push(`const ${name} = ${formatRanges(ranges)}`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
lines.push(
|
|
135
|
+
'',
|
|
136
|
+
'module.exports = {',
|
|
137
|
+
' UNICODE_VERSION,',
|
|
138
|
+
...Object.keys(tables).map((name) => ` ${name},`),
|
|
139
|
+
'}',
|
|
140
|
+
''
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return lines.join('\n')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function parseUnicodeData(text) {
|
|
147
|
+
const wanted = { Mn: [], Me: [], Cf: [] }
|
|
148
|
+
let pending = null
|
|
149
|
+
|
|
150
|
+
for (const line of text.split(/\r?\n/)) {
|
|
151
|
+
if (!line) continue
|
|
152
|
+
const fields = line.split(';')
|
|
153
|
+
const point = Number.parseInt(fields[0], 16)
|
|
154
|
+
const name = fields[1]
|
|
155
|
+
const category = fields[2]
|
|
156
|
+
|
|
157
|
+
if (name.endsWith(', First>')) {
|
|
158
|
+
pending = { point, category }
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
if (name.endsWith(', Last>')) {
|
|
162
|
+
if (!pending || pending.category !== category) {
|
|
163
|
+
throw new Error(`Malformed UnicodeData range ending at ${fields[0]}`)
|
|
164
|
+
}
|
|
165
|
+
if (wanted[category]) wanted[category].push([pending.point, point])
|
|
166
|
+
pending = null
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
if (wanted[category]) wanted[category].push([point, point])
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (pending) throw new Error('Unclosed UnicodeData range')
|
|
173
|
+
return wanted
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function parseProperties(text, compound = false) {
|
|
177
|
+
const properties = {}
|
|
178
|
+
|
|
179
|
+
for (const original of text.split(/\r?\n/)) {
|
|
180
|
+
const line = original.replace(/#.*/, '').trim()
|
|
181
|
+
if (!line || line.startsWith('@missing:')) continue
|
|
182
|
+
const fields = line.split(';').map((field) => field.trim())
|
|
183
|
+
if (fields.length < 2) continue
|
|
184
|
+
|
|
185
|
+
const property = compound && fields[1] === 'InCB'
|
|
186
|
+
? `${fields[1]}=${fields[2]}`
|
|
187
|
+
: fields[1]
|
|
188
|
+
if (!property) continue
|
|
189
|
+
|
|
190
|
+
const [first, last = first] = fields[0].split('..')
|
|
191
|
+
const range = [Number.parseInt(first, 16), Number.parseInt(last, 16)]
|
|
192
|
+
;(properties[property] ||= []).push(range)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (const [name, ranges] of Object.entries(properties)) {
|
|
196
|
+
properties[name] = mergeRanges(ranges)
|
|
197
|
+
}
|
|
198
|
+
return properties
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function subtractCodePoint([start, end], point) {
|
|
202
|
+
if (point < start || point > end) return [[start, end]]
|
|
203
|
+
const result = []
|
|
204
|
+
if (start < point) result.push([start, point - 1])
|
|
205
|
+
if (point < end) result.push([point + 1, end])
|
|
206
|
+
return result
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function mergeRanges(ranges) {
|
|
210
|
+
const sorted = ranges
|
|
211
|
+
.map(([start, end]) => [start, end])
|
|
212
|
+
.sort((a, b) => a[0] - b[0] || a[1] - b[1])
|
|
213
|
+
const merged = []
|
|
214
|
+
|
|
215
|
+
for (const range of sorted) {
|
|
216
|
+
const previous = merged.at(-1)
|
|
217
|
+
if (previous && range[0] <= previous[1] + 1) {
|
|
218
|
+
previous[1] = Math.max(previous[1], range[1])
|
|
219
|
+
} else {
|
|
220
|
+
merged.push(range)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return merged
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function formatRanges(ranges) {
|
|
227
|
+
if (ranges.length === 0) return '[]\n'
|
|
228
|
+
const values = ranges.map(([start, end]) => `[0x${start.toString(16)},0x${end.toString(16)}]`)
|
|
229
|
+
const rows = []
|
|
230
|
+
for (let index = 0; index < values.length; index += 5) {
|
|
231
|
+
rows.push(` ${values.slice(index, index + 5).join(',')}`)
|
|
232
|
+
}
|
|
233
|
+
return `[\n${rows.join(',\n')}\n]\n`
|
|
234
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "17.0.0",
|
|
3
|
+
"sources": {
|
|
4
|
+
"UnicodeData.txt": {
|
|
5
|
+
"url": "https://www.unicode.org/Public/17.0.0/ucd/UnicodeData.txt",
|
|
6
|
+
"sha256": "2e1efc1dcb59c575eedf5ccae60f95229f706ee6d031835247d843c11d96470c"
|
|
7
|
+
},
|
|
8
|
+
"EastAsianWidth.txt": {
|
|
9
|
+
"url": "https://www.unicode.org/Public/17.0.0/ucd/EastAsianWidth.txt",
|
|
10
|
+
"sha256": "ea7ce50f3444a050333448dffef1cadd9325af55cbb764b4a2280faf52170a33"
|
|
11
|
+
},
|
|
12
|
+
"GraphemeBreakProperty.txt": {
|
|
13
|
+
"url": "https://www.unicode.org/Public/17.0.0/ucd/auxiliary/GraphemeBreakProperty.txt",
|
|
14
|
+
"sha256": "d6b51d1d2ae5c33b451b7ed994b48f1f4dc62b2272a5831e7fd418514a6bae89"
|
|
15
|
+
},
|
|
16
|
+
"emoji-data.txt": {
|
|
17
|
+
"url": "https://www.unicode.org/Public/17.0.0/ucd/emoji/emoji-data.txt",
|
|
18
|
+
"sha256": "2cb2bb9455cda83e8481541ecf5b6dfda66a3bb89efa3fa7c5297eccf607b72b"
|
|
19
|
+
},
|
|
20
|
+
"DerivedCoreProperties.txt": {
|
|
21
|
+
"url": "https://www.unicode.org/Public/17.0.0/ucd/DerivedCoreProperties.txt",
|
|
22
|
+
"sha256": "24c7fed1195c482faaefd5c1e7eb821c5ee1fb6de07ecdbaa64b56a99da22c08"
|
|
23
|
+
},
|
|
24
|
+
"GraphemeBreakTest.txt": {
|
|
25
|
+
"url": "https://www.unicode.org/Public/17.0.0/ucd/auxiliary/GraphemeBreakTest.txt",
|
|
26
|
+
"sha256": "e2d134d2c52919bace503ebb6a551c1855fe1a1faec18478c78fff254a1793ec"
|
|
27
|
+
},
|
|
28
|
+
"emoji-test.txt": {
|
|
29
|
+
"url": "https://www.unicode.org/Public/17.0.0/emoji/emoji-test.txt",
|
|
30
|
+
"sha256": "1d8a944f88d7952f7ef7c5167fef3c67995bcae24543949710231b03a201acda"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|