@hyperfrontend/versioning 0.1.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/ARCHITECTURE.md +593 -0
- package/CHANGELOG.md +35 -0
- package/FUNDING.md +141 -0
- package/LICENSE.md +21 -0
- package/README.md +195 -0
- package/SECURITY.md +82 -0
- package/changelog/compare/diff.d.ts +128 -0
- package/changelog/compare/diff.d.ts.map +1 -0
- package/changelog/compare/index.cjs.js +628 -0
- package/changelog/compare/index.cjs.js.map +1 -0
- package/changelog/compare/index.d.ts +4 -0
- package/changelog/compare/index.d.ts.map +1 -0
- package/changelog/compare/index.esm.js +612 -0
- package/changelog/compare/index.esm.js.map +1 -0
- package/changelog/compare/is-equal.d.ts +114 -0
- package/changelog/compare/is-equal.d.ts.map +1 -0
- package/changelog/index.cjs.js +6448 -0
- package/changelog/index.cjs.js.map +1 -0
- package/changelog/index.d.ts +6 -0
- package/changelog/index.d.ts.map +1 -0
- package/changelog/index.esm.js +6358 -0
- package/changelog/index.esm.js.map +1 -0
- package/changelog/models/changelog.d.ts +86 -0
- package/changelog/models/changelog.d.ts.map +1 -0
- package/changelog/models/commit-ref.d.ts +51 -0
- package/changelog/models/commit-ref.d.ts.map +1 -0
- package/changelog/models/entry.d.ts +84 -0
- package/changelog/models/entry.d.ts.map +1 -0
- package/changelog/models/index.cjs.js +2043 -0
- package/changelog/models/index.cjs.js.map +1 -0
- package/changelog/models/index.d.ts +11 -0
- package/changelog/models/index.d.ts.map +1 -0
- package/changelog/models/index.esm.js +2026 -0
- package/changelog/models/index.esm.js.map +1 -0
- package/changelog/models/schema.d.ts +68 -0
- package/changelog/models/schema.d.ts.map +1 -0
- package/changelog/models/section.d.ts +25 -0
- package/changelog/models/section.d.ts.map +1 -0
- package/changelog/operations/add-entry.d.ts +56 -0
- package/changelog/operations/add-entry.d.ts.map +1 -0
- package/changelog/operations/add-item.d.ts +18 -0
- package/changelog/operations/add-item.d.ts.map +1 -0
- package/changelog/operations/filter-by-predicate.d.ts +81 -0
- package/changelog/operations/filter-by-predicate.d.ts.map +1 -0
- package/changelog/operations/filter-by-range.d.ts +63 -0
- package/changelog/operations/filter-by-range.d.ts.map +1 -0
- package/changelog/operations/filter-entries.d.ts +9 -0
- package/changelog/operations/filter-entries.d.ts.map +1 -0
- package/changelog/operations/index.cjs.js +2455 -0
- package/changelog/operations/index.cjs.js.map +1 -0
- package/changelog/operations/index.d.ts +15 -0
- package/changelog/operations/index.d.ts.map +1 -0
- package/changelog/operations/index.esm.js +2411 -0
- package/changelog/operations/index.esm.js.map +1 -0
- package/changelog/operations/merge.d.ts +88 -0
- package/changelog/operations/merge.d.ts.map +1 -0
- package/changelog/operations/remove-entry.d.ts +45 -0
- package/changelog/operations/remove-entry.d.ts.map +1 -0
- package/changelog/operations/remove-section.d.ts +50 -0
- package/changelog/operations/remove-section.d.ts.map +1 -0
- package/changelog/operations/transform.d.ts +143 -0
- package/changelog/operations/transform.d.ts.map +1 -0
- package/changelog/parse/index.cjs.js +1282 -0
- package/changelog/parse/index.cjs.js.map +1 -0
- package/changelog/parse/index.d.ts +5 -0
- package/changelog/parse/index.d.ts.map +1 -0
- package/changelog/parse/index.esm.js +1275 -0
- package/changelog/parse/index.esm.js.map +1 -0
- package/changelog/parse/line.d.ts +48 -0
- package/changelog/parse/line.d.ts.map +1 -0
- package/changelog/parse/parser.d.ts +16 -0
- package/changelog/parse/parser.d.ts.map +1 -0
- package/changelog/parse/tokenizer.d.ts +49 -0
- package/changelog/parse/tokenizer.d.ts.map +1 -0
- package/changelog/serialize/index.cjs.js +574 -0
- package/changelog/serialize/index.cjs.js.map +1 -0
- package/changelog/serialize/index.d.ts +6 -0
- package/changelog/serialize/index.d.ts.map +1 -0
- package/changelog/serialize/index.esm.js +564 -0
- package/changelog/serialize/index.esm.js.map +1 -0
- package/changelog/serialize/templates.d.ts +81 -0
- package/changelog/serialize/templates.d.ts.map +1 -0
- package/changelog/serialize/to-json.d.ts +57 -0
- package/changelog/serialize/to-json.d.ts.map +1 -0
- package/changelog/serialize/to-string.d.ts +30 -0
- package/changelog/serialize/to-string.d.ts.map +1 -0
- package/commits/index.cjs.js +648 -0
- package/commits/index.cjs.js.map +1 -0
- package/commits/index.d.ts +3 -0
- package/commits/index.d.ts.map +1 -0
- package/commits/index.esm.js +629 -0
- package/commits/index.esm.js.map +1 -0
- package/commits/models/breaking.d.ts +39 -0
- package/commits/models/breaking.d.ts.map +1 -0
- package/commits/models/commit-type.d.ts +32 -0
- package/commits/models/commit-type.d.ts.map +1 -0
- package/commits/models/conventional.d.ts +49 -0
- package/commits/models/conventional.d.ts.map +1 -0
- package/commits/models/index.cjs.js +207 -0
- package/commits/models/index.cjs.js.map +1 -0
- package/commits/models/index.d.ts +7 -0
- package/commits/models/index.d.ts.map +1 -0
- package/commits/models/index.esm.js +193 -0
- package/commits/models/index.esm.js.map +1 -0
- package/commits/parse/body.d.ts +18 -0
- package/commits/parse/body.d.ts.map +1 -0
- package/commits/parse/footer.d.ts +16 -0
- package/commits/parse/footer.d.ts.map +1 -0
- package/commits/parse/header.d.ts +15 -0
- package/commits/parse/header.d.ts.map +1 -0
- package/commits/parse/index.cjs.js +505 -0
- package/commits/parse/index.cjs.js.map +1 -0
- package/commits/parse/index.d.ts +5 -0
- package/commits/parse/index.d.ts.map +1 -0
- package/commits/parse/index.esm.js +499 -0
- package/commits/parse/index.esm.js.map +1 -0
- package/commits/parse/message.d.ts +17 -0
- package/commits/parse/message.d.ts.map +1 -0
- package/commits/utils/replace-char.d.ts +19 -0
- package/commits/utils/replace-char.d.ts.map +1 -0
- package/flow/executor/execute.d.ts +72 -0
- package/flow/executor/execute.d.ts.map +1 -0
- package/flow/executor/index.cjs.js +4402 -0
- package/flow/executor/index.cjs.js.map +1 -0
- package/flow/executor/index.d.ts +3 -0
- package/flow/executor/index.d.ts.map +1 -0
- package/flow/executor/index.esm.js +4398 -0
- package/flow/executor/index.esm.js.map +1 -0
- package/flow/factory.d.ts +58 -0
- package/flow/factory.d.ts.map +1 -0
- package/flow/index.cjs.js +8506 -0
- package/flow/index.cjs.js.map +1 -0
- package/flow/index.d.ts +7 -0
- package/flow/index.d.ts.map +1 -0
- package/flow/index.esm.js +8451 -0
- package/flow/index.esm.js.map +1 -0
- package/flow/models/flow.d.ts +130 -0
- package/flow/models/flow.d.ts.map +1 -0
- package/flow/models/index.cjs.js +285 -0
- package/flow/models/index.cjs.js.map +1 -0
- package/flow/models/index.d.ts +7 -0
- package/flow/models/index.d.ts.map +1 -0
- package/flow/models/index.esm.js +268 -0
- package/flow/models/index.esm.js.map +1 -0
- package/flow/models/step.d.ts +108 -0
- package/flow/models/step.d.ts.map +1 -0
- package/flow/models/types.d.ts +150 -0
- package/flow/models/types.d.ts.map +1 -0
- package/flow/presets/conventional.d.ts +59 -0
- package/flow/presets/conventional.d.ts.map +1 -0
- package/flow/presets/independent.d.ts +61 -0
- package/flow/presets/independent.d.ts.map +1 -0
- package/flow/presets/index.cjs.js +3903 -0
- package/flow/presets/index.cjs.js.map +1 -0
- package/flow/presets/index.d.ts +4 -0
- package/flow/presets/index.d.ts.map +1 -0
- package/flow/presets/index.esm.js +3889 -0
- package/flow/presets/index.esm.js.map +1 -0
- package/flow/presets/synced.d.ts +65 -0
- package/flow/presets/synced.d.ts.map +1 -0
- package/flow/steps/analyze-commits.d.ts +19 -0
- package/flow/steps/analyze-commits.d.ts.map +1 -0
- package/flow/steps/calculate-bump.d.ts +27 -0
- package/flow/steps/calculate-bump.d.ts.map +1 -0
- package/flow/steps/create-commit.d.ts +16 -0
- package/flow/steps/create-commit.d.ts.map +1 -0
- package/flow/steps/create-tag.d.ts +22 -0
- package/flow/steps/create-tag.d.ts.map +1 -0
- package/flow/steps/fetch-registry.d.ts +19 -0
- package/flow/steps/fetch-registry.d.ts.map +1 -0
- package/flow/steps/generate-changelog.d.ts +25 -0
- package/flow/steps/generate-changelog.d.ts.map +1 -0
- package/flow/steps/index.cjs.js +3523 -0
- package/flow/steps/index.cjs.js.map +1 -0
- package/flow/steps/index.d.ts +8 -0
- package/flow/steps/index.d.ts.map +1 -0
- package/flow/steps/index.esm.js +3504 -0
- package/flow/steps/index.esm.js.map +1 -0
- package/flow/steps/update-packages.d.ts +25 -0
- package/flow/steps/update-packages.d.ts.map +1 -0
- package/flow/utils/interpolate.d.ts +11 -0
- package/flow/utils/interpolate.d.ts.map +1 -0
- package/git/factory.d.ts +233 -0
- package/git/factory.d.ts.map +1 -0
- package/git/index.cjs.js +2863 -0
- package/git/index.cjs.js.map +1 -0
- package/git/index.d.ts +5 -0
- package/git/index.d.ts.map +1 -0
- package/git/index.esm.js +2785 -0
- package/git/index.esm.js.map +1 -0
- package/git/models/commit.d.ts +129 -0
- package/git/models/commit.d.ts.map +1 -0
- package/git/models/index.cjs.js +755 -0
- package/git/models/index.cjs.js.map +1 -0
- package/git/models/index.d.ts +7 -0
- package/git/models/index.d.ts.map +1 -0
- package/git/models/index.esm.js +729 -0
- package/git/models/index.esm.js.map +1 -0
- package/git/models/ref.d.ts +120 -0
- package/git/models/ref.d.ts.map +1 -0
- package/git/models/tag.d.ts +141 -0
- package/git/models/tag.d.ts.map +1 -0
- package/git/operations/commit.d.ts +97 -0
- package/git/operations/commit.d.ts.map +1 -0
- package/git/operations/head-info.d.ts +29 -0
- package/git/operations/head-info.d.ts.map +1 -0
- package/git/operations/index.cjs.js +1954 -0
- package/git/operations/index.cjs.js.map +1 -0
- package/git/operations/index.d.ts +14 -0
- package/git/operations/index.d.ts.map +1 -0
- package/git/operations/index.esm.js +1903 -0
- package/git/operations/index.esm.js.map +1 -0
- package/git/operations/log.d.ts +104 -0
- package/git/operations/log.d.ts.map +1 -0
- package/git/operations/manage-tags.d.ts +60 -0
- package/git/operations/manage-tags.d.ts.map +1 -0
- package/git/operations/query-tags.d.ts +88 -0
- package/git/operations/query-tags.d.ts.map +1 -0
- package/git/operations/stage.d.ts +66 -0
- package/git/operations/stage.d.ts.map +1 -0
- package/git/operations/status.d.ts +173 -0
- package/git/operations/status.d.ts.map +1 -0
- package/index.cjs.js +16761 -0
- package/index.cjs.js.map +1 -0
- package/index.d.ts +102 -0
- package/index.d.ts.map +1 -0
- package/index.esm.js +16427 -0
- package/index.esm.js.map +1 -0
- package/package.json +200 -0
- package/registry/factory.d.ts +18 -0
- package/registry/factory.d.ts.map +1 -0
- package/registry/index.cjs.js +543 -0
- package/registry/index.cjs.js.map +1 -0
- package/registry/index.d.ts +5 -0
- package/registry/index.d.ts.map +1 -0
- package/registry/index.esm.js +535 -0
- package/registry/index.esm.js.map +1 -0
- package/registry/models/index.cjs.js +69 -0
- package/registry/models/index.cjs.js.map +1 -0
- package/registry/models/index.d.ts +6 -0
- package/registry/models/index.d.ts.map +1 -0
- package/registry/models/index.esm.js +66 -0
- package/registry/models/index.esm.js.map +1 -0
- package/registry/models/package-info.d.ts +55 -0
- package/registry/models/package-info.d.ts.map +1 -0
- package/registry/models/registry.d.ts +62 -0
- package/registry/models/registry.d.ts.map +1 -0
- package/registry/models/version-info.d.ts +67 -0
- package/registry/models/version-info.d.ts.map +1 -0
- package/registry/npm/cache.d.ts +50 -0
- package/registry/npm/cache.d.ts.map +1 -0
- package/registry/npm/client.d.ts +30 -0
- package/registry/npm/client.d.ts.map +1 -0
- package/registry/npm/index.cjs.js +456 -0
- package/registry/npm/index.cjs.js.map +1 -0
- package/registry/npm/index.d.ts +4 -0
- package/registry/npm/index.d.ts.map +1 -0
- package/registry/npm/index.esm.js +451 -0
- package/registry/npm/index.esm.js.map +1 -0
- package/semver/compare/compare.d.ts +100 -0
- package/semver/compare/compare.d.ts.map +1 -0
- package/semver/compare/index.cjs.js +386 -0
- package/semver/compare/index.cjs.js.map +1 -0
- package/semver/compare/index.d.ts +3 -0
- package/semver/compare/index.d.ts.map +1 -0
- package/semver/compare/index.esm.js +370 -0
- package/semver/compare/index.esm.js.map +1 -0
- package/semver/compare/sort.d.ts +36 -0
- package/semver/compare/sort.d.ts.map +1 -0
- package/semver/format/index.cjs.js +58 -0
- package/semver/format/index.cjs.js.map +1 -0
- package/semver/format/index.d.ts +2 -0
- package/semver/format/index.d.ts.map +1 -0
- package/semver/format/index.esm.js +53 -0
- package/semver/format/index.esm.js.map +1 -0
- package/semver/format/to-string.d.ts +31 -0
- package/semver/format/to-string.d.ts.map +1 -0
- package/semver/increment/bump.d.ts +37 -0
- package/semver/increment/bump.d.ts.map +1 -0
- package/semver/increment/index.cjs.js +223 -0
- package/semver/increment/index.cjs.js.map +1 -0
- package/semver/increment/index.d.ts +2 -0
- package/semver/increment/index.d.ts.map +1 -0
- package/semver/increment/index.esm.js +219 -0
- package/semver/increment/index.esm.js.map +1 -0
- package/semver/index.cjs.js +1499 -0
- package/semver/index.cjs.js.map +1 -0
- package/semver/index.d.ts +6 -0
- package/semver/index.d.ts.map +1 -0
- package/semver/index.esm.js +1458 -0
- package/semver/index.esm.js.map +1 -0
- package/semver/models/index.cjs.js +153 -0
- package/semver/models/index.cjs.js.map +1 -0
- package/semver/models/index.d.ts +5 -0
- package/semver/models/index.d.ts.map +1 -0
- package/semver/models/index.esm.js +139 -0
- package/semver/models/index.esm.js.map +1 -0
- package/semver/models/range.d.ts +83 -0
- package/semver/models/range.d.ts.map +1 -0
- package/semver/models/version.d.ts +78 -0
- package/semver/models/version.d.ts.map +1 -0
- package/semver/parse/index.cjs.js +799 -0
- package/semver/parse/index.cjs.js.map +1 -0
- package/semver/parse/index.d.ts +5 -0
- package/semver/parse/index.d.ts.map +1 -0
- package/semver/parse/index.esm.js +793 -0
- package/semver/parse/index.esm.js.map +1 -0
- package/semver/parse/range.d.ts +38 -0
- package/semver/parse/range.d.ts.map +1 -0
- package/semver/parse/version.d.ts +49 -0
- package/semver/parse/version.d.ts.map +1 -0
- package/workspace/discovery/changelog-path.d.ts +21 -0
- package/workspace/discovery/changelog-path.d.ts.map +1 -0
- package/workspace/discovery/dependencies.d.ts +145 -0
- package/workspace/discovery/dependencies.d.ts.map +1 -0
- package/workspace/discovery/discover-changelogs.d.ts +76 -0
- package/workspace/discovery/discover-changelogs.d.ts.map +1 -0
- package/workspace/discovery/index.cjs.js +2300 -0
- package/workspace/discovery/index.cjs.js.map +1 -0
- package/workspace/discovery/index.d.ts +13 -0
- package/workspace/discovery/index.d.ts.map +1 -0
- package/workspace/discovery/index.esm.js +2283 -0
- package/workspace/discovery/index.esm.js.map +1 -0
- package/workspace/discovery/packages.d.ts +83 -0
- package/workspace/discovery/packages.d.ts.map +1 -0
- package/workspace/index.cjs.js +4445 -0
- package/workspace/index.cjs.js.map +1 -0
- package/workspace/index.d.ts +52 -0
- package/workspace/index.d.ts.map +1 -0
- package/workspace/index.esm.js +4394 -0
- package/workspace/index.esm.js.map +1 -0
- package/workspace/models/index.cjs.js +284 -0
- package/workspace/models/index.cjs.js.map +1 -0
- package/workspace/models/index.d.ts +10 -0
- package/workspace/models/index.d.ts.map +1 -0
- package/workspace/models/index.esm.js +261 -0
- package/workspace/models/index.esm.js.map +1 -0
- package/workspace/models/project.d.ts +118 -0
- package/workspace/models/project.d.ts.map +1 -0
- package/workspace/models/workspace.d.ts +139 -0
- package/workspace/models/workspace.d.ts.map +1 -0
- package/workspace/operations/batch-update.d.ts +99 -0
- package/workspace/operations/batch-update.d.ts.map +1 -0
- package/workspace/operations/cascade-bump.d.ts +125 -0
- package/workspace/operations/cascade-bump.d.ts.map +1 -0
- package/workspace/operations/index.cjs.js +2675 -0
- package/workspace/operations/index.cjs.js.map +1 -0
- package/workspace/operations/index.d.ts +12 -0
- package/workspace/operations/index.d.ts.map +1 -0
- package/workspace/operations/index.esm.js +2663 -0
- package/workspace/operations/index.esm.js.map +1 -0
- package/workspace/operations/validate.d.ts +85 -0
- package/workspace/operations/validate.d.ts.map +1 -0
|
@@ -0,0 +1,1275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a new changelog item.
|
|
3
|
+
*
|
|
4
|
+
* @param description - The description text of the change
|
|
5
|
+
* @param options - Optional configuration for scope, commits, references, and breaking flag
|
|
6
|
+
* @returns A new ChangelogItem object
|
|
7
|
+
*/
|
|
8
|
+
function createChangelogItem(description, options) {
|
|
9
|
+
return {
|
|
10
|
+
description,
|
|
11
|
+
scope: options?.scope,
|
|
12
|
+
commits: options?.commits ?? [],
|
|
13
|
+
references: options?.references ?? [],
|
|
14
|
+
breaking: options?.breaking ?? false,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Maps section headings to their canonical types.
|
|
20
|
+
* Used during parsing to normalize different heading styles.
|
|
21
|
+
*/
|
|
22
|
+
const SECTION_TYPE_MAP = {
|
|
23
|
+
// Breaking changes
|
|
24
|
+
'breaking changes': 'breaking',
|
|
25
|
+
breaking: 'breaking',
|
|
26
|
+
'breaking change': 'breaking',
|
|
27
|
+
// Features
|
|
28
|
+
features: 'features',
|
|
29
|
+
feature: 'features',
|
|
30
|
+
added: 'features',
|
|
31
|
+
new: 'features',
|
|
32
|
+
// Fixes
|
|
33
|
+
fixes: 'fixes',
|
|
34
|
+
fix: 'fixes',
|
|
35
|
+
'bug fixes': 'fixes',
|
|
36
|
+
bugfixes: 'fixes',
|
|
37
|
+
fixed: 'fixes',
|
|
38
|
+
// Performance
|
|
39
|
+
performance: 'performance',
|
|
40
|
+
'performance improvements': 'performance',
|
|
41
|
+
perf: 'performance',
|
|
42
|
+
// Documentation
|
|
43
|
+
documentation: 'documentation',
|
|
44
|
+
docs: 'documentation',
|
|
45
|
+
// Deprecations
|
|
46
|
+
deprecations: 'deprecations',
|
|
47
|
+
deprecated: 'deprecations',
|
|
48
|
+
// Refactoring
|
|
49
|
+
refactoring: 'refactoring',
|
|
50
|
+
refactor: 'refactoring',
|
|
51
|
+
'code refactoring': 'refactoring',
|
|
52
|
+
// Tests
|
|
53
|
+
tests: 'tests',
|
|
54
|
+
test: 'tests',
|
|
55
|
+
testing: 'tests',
|
|
56
|
+
// Build
|
|
57
|
+
build: 'build',
|
|
58
|
+
'build system': 'build',
|
|
59
|
+
dependencies: 'build',
|
|
60
|
+
// CI
|
|
61
|
+
ci: 'ci',
|
|
62
|
+
'continuous integration': 'ci',
|
|
63
|
+
// Chores
|
|
64
|
+
chores: 'chores',
|
|
65
|
+
chore: 'chores',
|
|
66
|
+
maintenance: 'chores',
|
|
67
|
+
// Other
|
|
68
|
+
other: 'other',
|
|
69
|
+
miscellaneous: 'other',
|
|
70
|
+
misc: 'other',
|
|
71
|
+
changed: 'other',
|
|
72
|
+
changes: 'other',
|
|
73
|
+
removed: 'other',
|
|
74
|
+
security: 'other',
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Determines the section type from a heading string.
|
|
78
|
+
* Returns 'other' if the heading is not recognized.
|
|
79
|
+
*
|
|
80
|
+
* @param heading - The heading string to parse
|
|
81
|
+
* @returns The corresponding ChangelogSectionType
|
|
82
|
+
*/
|
|
83
|
+
function getSectionType(heading) {
|
|
84
|
+
const normalized = heading.toLowerCase().trim();
|
|
85
|
+
return SECTION_TYPE_MAP[normalized] ?? 'other';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Safe copies of Math built-in methods.
|
|
90
|
+
*
|
|
91
|
+
* These references are captured at module initialization time to protect against
|
|
92
|
+
* prototype pollution attacks. Import only what you need for tree-shaking.
|
|
93
|
+
*
|
|
94
|
+
* @module @hyperfrontend/immutable-api-utils/built-in-copy/math
|
|
95
|
+
*/
|
|
96
|
+
// Capture references at module initialization time
|
|
97
|
+
const _Math = globalThis.Math;
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// Min/Max
|
|
100
|
+
// ============================================================================
|
|
101
|
+
/**
|
|
102
|
+
* (Safe copy) Returns the larger of zero or more numbers.
|
|
103
|
+
*/
|
|
104
|
+
const max = _Math.max;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Safe copies of Number built-in methods and constants.
|
|
108
|
+
*
|
|
109
|
+
* These references are captured at module initialization time to protect against
|
|
110
|
+
* prototype pollution attacks. Import only what you need for tree-shaking.
|
|
111
|
+
*
|
|
112
|
+
* @module @hyperfrontend/immutable-api-utils/built-in-copy/number
|
|
113
|
+
*/
|
|
114
|
+
// Capture references at module initialization time
|
|
115
|
+
const _parseInt = globalThis.parseInt;
|
|
116
|
+
// ============================================================================
|
|
117
|
+
// Parsing
|
|
118
|
+
// ============================================================================
|
|
119
|
+
/**
|
|
120
|
+
* (Safe copy) Parses a string and returns an integer.
|
|
121
|
+
*/
|
|
122
|
+
const parseInt = _parseInt;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Line Parser
|
|
126
|
+
*
|
|
127
|
+
* Utilities for parsing individual changelog lines without regex.
|
|
128
|
+
*/
|
|
129
|
+
/**
|
|
130
|
+
* Parses a version string from a heading.
|
|
131
|
+
* Examples: "1.2.3", "v1.2.3", "[1.2.3]", "1.2.3 - 2024-01-01"
|
|
132
|
+
*
|
|
133
|
+
* @param heading - The heading string to parse
|
|
134
|
+
* @returns An object containing the parsed version, date, and optional compareUrl
|
|
135
|
+
*/
|
|
136
|
+
function parseVersionFromHeading(heading) {
|
|
137
|
+
const trimmed = heading.trim();
|
|
138
|
+
// Check for unreleased
|
|
139
|
+
const lowerHeading = trimmed.toLowerCase();
|
|
140
|
+
if (lowerHeading === 'unreleased' || lowerHeading === '[unreleased]') {
|
|
141
|
+
return { version: 'Unreleased', date: null };
|
|
142
|
+
}
|
|
143
|
+
let pos = 0;
|
|
144
|
+
let version = '';
|
|
145
|
+
let date = null;
|
|
146
|
+
let compareUrl;
|
|
147
|
+
// Skip leading [ if present
|
|
148
|
+
if (trimmed[pos] === '[') {
|
|
149
|
+
pos++;
|
|
150
|
+
}
|
|
151
|
+
// Skip leading 'v' if present
|
|
152
|
+
if (trimmed[pos] === 'v' || trimmed[pos] === 'V') {
|
|
153
|
+
pos++;
|
|
154
|
+
}
|
|
155
|
+
// Parse version number (digits and dots)
|
|
156
|
+
const versionStart = pos;
|
|
157
|
+
while (pos < trimmed.length) {
|
|
158
|
+
const char = trimmed[pos];
|
|
159
|
+
const code = char.charCodeAt(0);
|
|
160
|
+
// Allow digits, dots, hyphens (for prerelease), plus signs
|
|
161
|
+
if ((code >= 48 && code <= 57) || // 0-9
|
|
162
|
+
char === '.' ||
|
|
163
|
+
char === '-' ||
|
|
164
|
+
char === '+' ||
|
|
165
|
+
(code >= 97 && code <= 122) || // a-z (for prerelease tags like alpha, beta, rc)
|
|
166
|
+
(code >= 65 && code <= 90) // A-Z
|
|
167
|
+
) {
|
|
168
|
+
pos++;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
version = trimmed.slice(versionStart, pos);
|
|
175
|
+
// Skip trailing ] if present
|
|
176
|
+
if (trimmed[pos] === ']') {
|
|
177
|
+
pos++;
|
|
178
|
+
}
|
|
179
|
+
// Skip whitespace and separator
|
|
180
|
+
while (pos < trimmed.length && (trimmed[pos] === ' ' || trimmed[pos] === '-' || trimmed[pos] === '–')) {
|
|
181
|
+
pos++;
|
|
182
|
+
}
|
|
183
|
+
// Try to parse date (YYYY-MM-DD format)
|
|
184
|
+
if (pos < trimmed.length) {
|
|
185
|
+
const dateMatch = extractDate(trimmed.slice(pos));
|
|
186
|
+
if (dateMatch) {
|
|
187
|
+
date = dateMatch.date;
|
|
188
|
+
pos += dateMatch.length;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Skip to check for compare URL (in parentheses or link)
|
|
192
|
+
while (pos < trimmed.length && trimmed[pos] === ' ') {
|
|
193
|
+
pos++;
|
|
194
|
+
}
|
|
195
|
+
// Check for link at end: [compare](url)
|
|
196
|
+
if (pos < trimmed.length) {
|
|
197
|
+
const linkMatch = extractLink(trimmed.slice(pos));
|
|
198
|
+
if (linkMatch?.url) {
|
|
199
|
+
compareUrl = linkMatch.url;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { version, date, compareUrl };
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Extracts a date in YYYY-MM-DD format from a string.
|
|
206
|
+
*
|
|
207
|
+
* @param str - The string to extract a date from
|
|
208
|
+
* @returns The extracted date and its length, or null if no date found
|
|
209
|
+
*/
|
|
210
|
+
function extractDate(str) {
|
|
211
|
+
let pos = 0;
|
|
212
|
+
// Skip optional parentheses
|
|
213
|
+
if (str[pos] === '(')
|
|
214
|
+
pos++;
|
|
215
|
+
// Parse year (4 digits)
|
|
216
|
+
const yearStart = pos;
|
|
217
|
+
while (pos < str.length && pos - yearStart < 4) {
|
|
218
|
+
const code = str.charCodeAt(pos);
|
|
219
|
+
if (code >= 48 && code <= 57) {
|
|
220
|
+
pos++;
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (pos - yearStart !== 4)
|
|
227
|
+
return null;
|
|
228
|
+
// Expect - or /
|
|
229
|
+
if (str[pos] !== '-' && str[pos] !== '/')
|
|
230
|
+
return null;
|
|
231
|
+
const separator = str[pos];
|
|
232
|
+
pos++;
|
|
233
|
+
// Parse month (2 digits)
|
|
234
|
+
const monthStart = pos;
|
|
235
|
+
while (pos < str.length && pos - monthStart < 2) {
|
|
236
|
+
const code = str.charCodeAt(pos);
|
|
237
|
+
if (code >= 48 && code <= 57) {
|
|
238
|
+
pos++;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (pos - monthStart !== 2)
|
|
245
|
+
return null;
|
|
246
|
+
// Expect same separator
|
|
247
|
+
if (str[pos] !== separator)
|
|
248
|
+
return null;
|
|
249
|
+
pos++;
|
|
250
|
+
// Parse day (2 digits)
|
|
251
|
+
const dayStart = pos;
|
|
252
|
+
while (pos < str.length && pos - dayStart < 2) {
|
|
253
|
+
const code = str.charCodeAt(pos);
|
|
254
|
+
if (code >= 48 && code <= 57) {
|
|
255
|
+
pos++;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (pos - dayStart !== 2)
|
|
262
|
+
return null;
|
|
263
|
+
// Skip optional closing parenthesis
|
|
264
|
+
if (str[pos] === ')')
|
|
265
|
+
pos++;
|
|
266
|
+
const dateStr = str.slice(yearStart, dayStart + 2);
|
|
267
|
+
const date = slashToHyphen(dateStr);
|
|
268
|
+
return { date, length: pos };
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Replaces forward slashes with hyphens (ReDoS-safe).
|
|
272
|
+
*
|
|
273
|
+
* @param input - The input string
|
|
274
|
+
* @returns String with forward slashes replaced by hyphens
|
|
275
|
+
*/
|
|
276
|
+
function slashToHyphen(input) {
|
|
277
|
+
const result = [];
|
|
278
|
+
for (let i = 0; i < input.length; i++) {
|
|
279
|
+
result.push(input[i] === '/' ? '-' : input[i]);
|
|
280
|
+
}
|
|
281
|
+
return result.join('');
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Extracts a markdown link from a string.
|
|
285
|
+
*
|
|
286
|
+
* @param str - The string to extract a link from
|
|
287
|
+
* @returns The extracted link text, url, and length, or null if no link found
|
|
288
|
+
*/
|
|
289
|
+
function extractLink(str) {
|
|
290
|
+
if (str[0] !== '[')
|
|
291
|
+
return null;
|
|
292
|
+
let pos = 1;
|
|
293
|
+
let depth = 1;
|
|
294
|
+
// Find closing ]
|
|
295
|
+
while (pos < str.length && depth > 0) {
|
|
296
|
+
if (str[pos] === '[')
|
|
297
|
+
depth++;
|
|
298
|
+
else if (str[pos] === ']')
|
|
299
|
+
depth--;
|
|
300
|
+
pos++;
|
|
301
|
+
}
|
|
302
|
+
if (depth !== 0)
|
|
303
|
+
return null;
|
|
304
|
+
const text = str.slice(1, pos - 1);
|
|
305
|
+
// Expect (
|
|
306
|
+
if (str[pos] !== '(')
|
|
307
|
+
return null;
|
|
308
|
+
pos++;
|
|
309
|
+
const urlStart = pos;
|
|
310
|
+
depth = 1;
|
|
311
|
+
while (pos < str.length && depth > 0) {
|
|
312
|
+
if (str[pos] === '(')
|
|
313
|
+
depth++;
|
|
314
|
+
else if (str[pos] === ')')
|
|
315
|
+
depth--;
|
|
316
|
+
pos++;
|
|
317
|
+
}
|
|
318
|
+
if (depth !== 0)
|
|
319
|
+
return null;
|
|
320
|
+
const url = str.slice(urlStart, pos - 1);
|
|
321
|
+
return { text, url, length: pos };
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Parses commit references from a line.
|
|
325
|
+
* Examples: (abc1234), [abc1234], commit abc1234
|
|
326
|
+
*
|
|
327
|
+
* @param text - The text to parse for commit references
|
|
328
|
+
* @param baseUrl - Optional base URL for constructing commit links
|
|
329
|
+
* @returns An array of parsed CommitRef objects
|
|
330
|
+
*/
|
|
331
|
+
function parseCommitRefs(text, baseUrl) {
|
|
332
|
+
const refs = [];
|
|
333
|
+
let pos = 0;
|
|
334
|
+
while (pos < text.length) {
|
|
335
|
+
// Look for potential hash patterns
|
|
336
|
+
// Common formats: (abc1234), [abc1234], abc1234fabcdef
|
|
337
|
+
// Check for parenthetical hash
|
|
338
|
+
if (text[pos] === '(' || text[pos] === '[') {
|
|
339
|
+
const closeChar = text[pos] === '(' ? ')' : ']';
|
|
340
|
+
const start = pos + 1;
|
|
341
|
+
pos++;
|
|
342
|
+
// Read potential hash
|
|
343
|
+
while (pos < text.length && isHexDigit(text[pos])) {
|
|
344
|
+
pos++;
|
|
345
|
+
}
|
|
346
|
+
// Check if valid hash (7-40 hex chars)
|
|
347
|
+
const hash = text.slice(start, pos);
|
|
348
|
+
if (hash.length >= 7 && hash.length <= 40 && text[pos] === closeChar) {
|
|
349
|
+
refs.push({
|
|
350
|
+
hash,
|
|
351
|
+
shortHash: hash.slice(0, 7),
|
|
352
|
+
url: baseUrl ? `${baseUrl}/commit/${hash}` : undefined,
|
|
353
|
+
});
|
|
354
|
+
pos++; // skip closing bracket
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
pos++;
|
|
359
|
+
}
|
|
360
|
+
return refs;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Parses issue/PR references from a line.
|
|
364
|
+
* Examples: #123, GH-123, closes #123
|
|
365
|
+
*
|
|
366
|
+
* @param text - The text to parse for issue references
|
|
367
|
+
* @param baseUrl - Optional base URL for constructing issue links
|
|
368
|
+
* @returns An array of parsed IssueRef objects
|
|
369
|
+
*/
|
|
370
|
+
function parseIssueRefs(text, baseUrl) {
|
|
371
|
+
const refs = [];
|
|
372
|
+
let pos = 0;
|
|
373
|
+
while (pos < text.length) {
|
|
374
|
+
// Look for # followed by digits
|
|
375
|
+
if (text[pos] === '#') {
|
|
376
|
+
pos++;
|
|
377
|
+
const numStart = pos;
|
|
378
|
+
while (pos < text.length && isDigitChar(text[pos])) {
|
|
379
|
+
pos++;
|
|
380
|
+
}
|
|
381
|
+
if (pos > numStart) {
|
|
382
|
+
const number = parseInt(text.slice(numStart, pos), 10);
|
|
383
|
+
// Check context for PR vs issue
|
|
384
|
+
const beforeHash = text.slice(max(0, numStart - 10), numStart - 1).toLowerCase();
|
|
385
|
+
const type = beforeHash.includes('pr') || beforeHash.includes('pull') ? 'pull-request' : 'issue';
|
|
386
|
+
refs.push({
|
|
387
|
+
number,
|
|
388
|
+
type,
|
|
389
|
+
url: baseUrl ? `${baseUrl}/issues/${number}` : undefined,
|
|
390
|
+
});
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
pos++;
|
|
395
|
+
}
|
|
396
|
+
return refs;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Parses the scope from a changelog item.
|
|
400
|
+
* Example: "**scope:** description" -> { scope: "scope", description: "description" }
|
|
401
|
+
*
|
|
402
|
+
* @param text - The text to parse for scope
|
|
403
|
+
* @returns An object with optional scope and the description
|
|
404
|
+
*/
|
|
405
|
+
function parseScopeFromItem(text) {
|
|
406
|
+
const trimmed = text.trim();
|
|
407
|
+
// Check for **scope:** pattern (colon inside or outside bold)
|
|
408
|
+
if (trimmed.startsWith('**')) {
|
|
409
|
+
let pos = 2;
|
|
410
|
+
const scopeStart = pos;
|
|
411
|
+
// Read until ** or :
|
|
412
|
+
while (pos < trimmed.length && trimmed[pos] !== '*' && trimmed[pos] !== ':') {
|
|
413
|
+
pos++;
|
|
414
|
+
}
|
|
415
|
+
// Handle **scope:** pattern (colon before closing **)
|
|
416
|
+
if (trimmed[pos] === ':' && trimmed[pos + 1] === '*' && trimmed[pos + 2] === '*') {
|
|
417
|
+
const scope = trimmed.slice(scopeStart, pos);
|
|
418
|
+
pos += 3; // skip :**
|
|
419
|
+
// Skip whitespace
|
|
420
|
+
while (trimmed[pos] === ' ')
|
|
421
|
+
pos++;
|
|
422
|
+
return { scope, description: trimmed.slice(pos) };
|
|
423
|
+
}
|
|
424
|
+
// Handle **scope**: pattern (colon after closing **)
|
|
425
|
+
if (trimmed[pos] === '*' && trimmed[pos + 1] === '*') {
|
|
426
|
+
const scope = trimmed.slice(scopeStart, pos);
|
|
427
|
+
pos += 2; // skip **
|
|
428
|
+
// Skip : if present
|
|
429
|
+
if (trimmed[pos] === ':')
|
|
430
|
+
pos++;
|
|
431
|
+
// Skip whitespace
|
|
432
|
+
while (trimmed[pos] === ' ')
|
|
433
|
+
pos++;
|
|
434
|
+
return { scope, description: trimmed.slice(pos) };
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// Check for scope: pattern (without bold)
|
|
438
|
+
const colonPos = trimmed.indexOf(':');
|
|
439
|
+
if (colonPos > 0 && colonPos < 30) {
|
|
440
|
+
// scope shouldn't be too long
|
|
441
|
+
const potentialScope = trimmed.slice(0, colonPos);
|
|
442
|
+
// Scope should be a simple identifier (letters, numbers, hyphens)
|
|
443
|
+
if (isValidScope(potentialScope)) {
|
|
444
|
+
const description = trimmed.slice(colonPos + 1).trim();
|
|
445
|
+
return { scope: potentialScope, description };
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return { description: trimmed };
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Checks if a string is a valid scope (alphanumeric with hyphens).
|
|
452
|
+
*
|
|
453
|
+
* @param str - The string to check
|
|
454
|
+
* @returns True if the string is a valid scope identifier
|
|
455
|
+
*/
|
|
456
|
+
function isValidScope(str) {
|
|
457
|
+
if (!str || str.length === 0)
|
|
458
|
+
return false;
|
|
459
|
+
for (let i = 0; i < str.length; i++) {
|
|
460
|
+
const code = str.charCodeAt(i);
|
|
461
|
+
if (!(code >= 48 && code <= 57) && // 0-9
|
|
462
|
+
!(code >= 65 && code <= 90) && // A-Z
|
|
463
|
+
!(code >= 97 && code <= 122) && // a-z
|
|
464
|
+
code !== 45 // -
|
|
465
|
+
) {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Checks if a character is a hex digit.
|
|
473
|
+
*
|
|
474
|
+
* @param char - The character to check
|
|
475
|
+
* @returns True if the character is a hex digit (0-9, A-F, a-f)
|
|
476
|
+
*/
|
|
477
|
+
function isHexDigit(char) {
|
|
478
|
+
const code = char.charCodeAt(0);
|
|
479
|
+
return ((code >= 48 && code <= 57) || // 0-9
|
|
480
|
+
(code >= 65 && code <= 70) || // A-F
|
|
481
|
+
(code >= 97 && code <= 102) // a-f
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Checks if a character is a digit.
|
|
486
|
+
*
|
|
487
|
+
* @param char - The character to check
|
|
488
|
+
* @returns True if the character is a digit (0-9)
|
|
489
|
+
*/
|
|
490
|
+
function isDigitChar(char) {
|
|
491
|
+
const code = char.charCodeAt(0);
|
|
492
|
+
return code >= 48 && code <= 57;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Safe copies of Error built-ins via factory functions.
|
|
497
|
+
*
|
|
498
|
+
* Since constructors cannot be safely captured via Object.assign, this module
|
|
499
|
+
* provides factory functions that use Reflect.construct internally.
|
|
500
|
+
*
|
|
501
|
+
* These references are captured at module initialization time to protect against
|
|
502
|
+
* prototype pollution attacks. Import only what you need for tree-shaking.
|
|
503
|
+
*
|
|
504
|
+
* @module @hyperfrontend/immutable-api-utils/built-in-copy/error
|
|
505
|
+
*/
|
|
506
|
+
// Capture references at module initialization time
|
|
507
|
+
const _Error = globalThis.Error;
|
|
508
|
+
const _Reflect = globalThis.Reflect;
|
|
509
|
+
/**
|
|
510
|
+
* (Safe copy) Creates a new Error using the captured Error constructor.
|
|
511
|
+
* Use this instead of `new Error()`.
|
|
512
|
+
*
|
|
513
|
+
* @param message - Optional error message.
|
|
514
|
+
* @param options - Optional error options.
|
|
515
|
+
* @returns A new Error instance.
|
|
516
|
+
*/
|
|
517
|
+
const createError = (message, options) => _Reflect.construct(_Error, [message, options]);
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Changelog Tokenizer
|
|
521
|
+
*
|
|
522
|
+
* A state machine tokenizer that processes markdown character-by-character.
|
|
523
|
+
* No regex is used to ensure ReDoS safety.
|
|
524
|
+
*/
|
|
525
|
+
/**
|
|
526
|
+
* Maximum input length to prevent memory exhaustion (1MB)
|
|
527
|
+
*/
|
|
528
|
+
const MAX_INPUT_LENGTH = 1024 * 1024;
|
|
529
|
+
/**
|
|
530
|
+
* Tokenizes a changelog markdown string into tokens.
|
|
531
|
+
*
|
|
532
|
+
* @param input - The markdown content to tokenize
|
|
533
|
+
* @returns Array of tokens
|
|
534
|
+
* @throws {Error} If input exceeds maximum length
|
|
535
|
+
*/
|
|
536
|
+
function tokenize(input) {
|
|
537
|
+
if (input.length > MAX_INPUT_LENGTH) {
|
|
538
|
+
throw createError(`Input exceeds maximum length of ${MAX_INPUT_LENGTH} characters`);
|
|
539
|
+
}
|
|
540
|
+
const state = {
|
|
541
|
+
pos: 0,
|
|
542
|
+
line: 1,
|
|
543
|
+
column: 1,
|
|
544
|
+
input,
|
|
545
|
+
tokens: [],
|
|
546
|
+
};
|
|
547
|
+
while (state.pos < state.input.length) {
|
|
548
|
+
const char = state.input[state.pos];
|
|
549
|
+
// Check for newline
|
|
550
|
+
if (char === '\n') {
|
|
551
|
+
consumeNewline(state);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
// Check for carriage return (handle \r\n)
|
|
555
|
+
if (char === '\r') {
|
|
556
|
+
state.pos++;
|
|
557
|
+
if (state.input[state.pos] === '\n') {
|
|
558
|
+
consumeNewline(state);
|
|
559
|
+
}
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
// At start of line, check for special markers
|
|
563
|
+
if (state.column === 1) {
|
|
564
|
+
// Check for heading
|
|
565
|
+
if (char === '#') {
|
|
566
|
+
consumeHeading(state);
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
// Check for list item
|
|
570
|
+
if ((char === '-' || char === '*') && isWhitespace(state.input[state.pos + 1])) {
|
|
571
|
+
consumeListItem(state);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
// Check for inline markdown elements
|
|
576
|
+
if (char === '[') {
|
|
577
|
+
consumeLink(state);
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (char === '`') {
|
|
581
|
+
consumeCode(state);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (char === '*' && state.input[state.pos + 1] === '*') {
|
|
585
|
+
consumeBold(state);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
// Default: consume as text
|
|
589
|
+
consumeText(state);
|
|
590
|
+
}
|
|
591
|
+
// Add EOF token
|
|
592
|
+
pushToken(state, 'eof', '');
|
|
593
|
+
return state.tokens;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Consumes a newline character.
|
|
597
|
+
*
|
|
598
|
+
* @param state - The tokenizer state to update
|
|
599
|
+
*/
|
|
600
|
+
function consumeNewline(state) {
|
|
601
|
+
// Check if this is a blank line (previous token was also newline or at start)
|
|
602
|
+
const prevToken = state.tokens[state.tokens.length - 1];
|
|
603
|
+
const isBlank = prevToken?.type === 'newline' || prevToken?.type === 'blank-line' || state.tokens.length === 0;
|
|
604
|
+
pushToken(state, isBlank ? 'blank-line' : 'newline', '\n');
|
|
605
|
+
state.pos++;
|
|
606
|
+
state.line++;
|
|
607
|
+
state.column = 1;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Consumes a heading (# through ####).
|
|
611
|
+
*
|
|
612
|
+
* @param state - The tokenizer state to update
|
|
613
|
+
*/
|
|
614
|
+
function consumeHeading(state) {
|
|
615
|
+
const startColumn = state.column;
|
|
616
|
+
let level = 0;
|
|
617
|
+
// Count # characters
|
|
618
|
+
while (state.input[state.pos] === '#' && level < 4) {
|
|
619
|
+
state.pos++;
|
|
620
|
+
state.column++;
|
|
621
|
+
level++;
|
|
622
|
+
}
|
|
623
|
+
// Skip whitespace after #
|
|
624
|
+
while (isWhitespace(state.input[state.pos]) && state.input[state.pos] !== '\n') {
|
|
625
|
+
state.pos++;
|
|
626
|
+
state.column++;
|
|
627
|
+
}
|
|
628
|
+
// Consume the rest of the line as heading content
|
|
629
|
+
const contentStart = state.pos;
|
|
630
|
+
while (state.pos < state.input.length && state.input[state.pos] !== '\n') {
|
|
631
|
+
state.pos++;
|
|
632
|
+
state.column++;
|
|
633
|
+
}
|
|
634
|
+
const content = state.input.slice(contentStart, state.pos);
|
|
635
|
+
const type = `heading-${level}`;
|
|
636
|
+
state.tokens.push({
|
|
637
|
+
type,
|
|
638
|
+
value: content.trim(),
|
|
639
|
+
line: state.line,
|
|
640
|
+
column: startColumn,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Consumes a list item (- or *).
|
|
645
|
+
*
|
|
646
|
+
* @param state - The tokenizer state to update
|
|
647
|
+
*/
|
|
648
|
+
function consumeListItem(state) {
|
|
649
|
+
const startColumn = state.column;
|
|
650
|
+
// Skip the marker and whitespace
|
|
651
|
+
state.pos++; // skip - or *
|
|
652
|
+
state.column++;
|
|
653
|
+
while (isWhitespace(state.input[state.pos]) && state.input[state.pos] !== '\n') {
|
|
654
|
+
state.pos++;
|
|
655
|
+
state.column++;
|
|
656
|
+
}
|
|
657
|
+
// Consume the rest of the line as list item content
|
|
658
|
+
const contentStart = state.pos;
|
|
659
|
+
while (state.pos < state.input.length && state.input[state.pos] !== '\n') {
|
|
660
|
+
state.pos++;
|
|
661
|
+
state.column++;
|
|
662
|
+
}
|
|
663
|
+
const content = state.input.slice(contentStart, state.pos);
|
|
664
|
+
state.tokens.push({
|
|
665
|
+
type: 'list-item',
|
|
666
|
+
value: content,
|
|
667
|
+
line: state.line,
|
|
668
|
+
column: startColumn,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Consumes a markdown link [text](url).
|
|
673
|
+
*
|
|
674
|
+
* @param state - The tokenizer state to update
|
|
675
|
+
*/
|
|
676
|
+
function consumeLink(state) {
|
|
677
|
+
const startColumn = state.column;
|
|
678
|
+
const startLine = state.line;
|
|
679
|
+
// Skip opening [
|
|
680
|
+
state.pos++;
|
|
681
|
+
state.column++;
|
|
682
|
+
// Find closing ]
|
|
683
|
+
const textStart = state.pos;
|
|
684
|
+
let depth = 1;
|
|
685
|
+
while (state.pos < state.input.length && depth > 0) {
|
|
686
|
+
const char = state.input[state.pos];
|
|
687
|
+
if (char === '[')
|
|
688
|
+
depth++;
|
|
689
|
+
else if (char === ']')
|
|
690
|
+
depth--;
|
|
691
|
+
else if (char === '\n') {
|
|
692
|
+
// Link text shouldn't span lines; emit '[' as text and reset
|
|
693
|
+
state.tokens.push({
|
|
694
|
+
type: 'text',
|
|
695
|
+
value: '[',
|
|
696
|
+
line: startLine,
|
|
697
|
+
column: startColumn,
|
|
698
|
+
});
|
|
699
|
+
state.pos = textStart;
|
|
700
|
+
state.column = startColumn + 1;
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (depth > 0) {
|
|
704
|
+
state.pos++;
|
|
705
|
+
state.column++;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (depth !== 0) {
|
|
709
|
+
// No closing ], emit '[' as text and reset
|
|
710
|
+
state.tokens.push({
|
|
711
|
+
type: 'text',
|
|
712
|
+
value: '[',
|
|
713
|
+
line: startLine,
|
|
714
|
+
column: startColumn,
|
|
715
|
+
});
|
|
716
|
+
state.pos = textStart;
|
|
717
|
+
state.column = startColumn + 1;
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const linkText = state.input.slice(textStart, state.pos);
|
|
721
|
+
state.pos++; // skip ]
|
|
722
|
+
state.column++;
|
|
723
|
+
// Check for (url)
|
|
724
|
+
if (state.input[state.pos] === '(') {
|
|
725
|
+
state.pos++; // skip (
|
|
726
|
+
state.column++;
|
|
727
|
+
const urlStart = state.pos;
|
|
728
|
+
depth = 1;
|
|
729
|
+
while (state.pos < state.input.length && depth > 0) {
|
|
730
|
+
const char = state.input[state.pos];
|
|
731
|
+
if (char === '(')
|
|
732
|
+
depth++;
|
|
733
|
+
else if (char === ')')
|
|
734
|
+
depth--;
|
|
735
|
+
else if (char === '\n') {
|
|
736
|
+
// URL shouldn't span lines
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
if (depth > 0) {
|
|
740
|
+
state.pos++;
|
|
741
|
+
state.column++;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (depth === 0) {
|
|
745
|
+
const linkUrl = state.input.slice(urlStart, state.pos);
|
|
746
|
+
state.pos++; // skip )
|
|
747
|
+
state.column++;
|
|
748
|
+
// Emit link-text token
|
|
749
|
+
state.tokens.push({
|
|
750
|
+
type: 'link-text',
|
|
751
|
+
value: linkText,
|
|
752
|
+
line: startLine,
|
|
753
|
+
column: startColumn,
|
|
754
|
+
});
|
|
755
|
+
// Emit link-url token
|
|
756
|
+
state.tokens.push({
|
|
757
|
+
type: 'link-url',
|
|
758
|
+
value: linkUrl,
|
|
759
|
+
line: startLine,
|
|
760
|
+
column: startColumn + linkText.length + 3,
|
|
761
|
+
});
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
// No URL part, emit as text
|
|
766
|
+
state.tokens.push({
|
|
767
|
+
type: 'text',
|
|
768
|
+
value: `[${linkText}]`,
|
|
769
|
+
line: startLine,
|
|
770
|
+
column: startColumn,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Consumes a code span `code`.
|
|
775
|
+
*
|
|
776
|
+
* @param state - The tokenizer state to update
|
|
777
|
+
*/
|
|
778
|
+
function consumeCode(state) {
|
|
779
|
+
const startColumn = state.column;
|
|
780
|
+
const startLine = state.line;
|
|
781
|
+
state.pos++; // skip opening `
|
|
782
|
+
state.column++;
|
|
783
|
+
const contentStart = state.pos;
|
|
784
|
+
while (state.pos < state.input.length && state.input[state.pos] !== '`' && state.input[state.pos] !== '\n') {
|
|
785
|
+
state.pos++;
|
|
786
|
+
state.column++;
|
|
787
|
+
}
|
|
788
|
+
if (state.input[state.pos] === '`') {
|
|
789
|
+
const content = state.input.slice(contentStart, state.pos);
|
|
790
|
+
state.pos++; // skip closing `
|
|
791
|
+
state.column++;
|
|
792
|
+
state.tokens.push({
|
|
793
|
+
type: 'code',
|
|
794
|
+
value: content,
|
|
795
|
+
line: startLine,
|
|
796
|
+
column: startColumn,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
// No closing `, emit as text
|
|
801
|
+
state.tokens.push({
|
|
802
|
+
type: 'text',
|
|
803
|
+
value: '`' + state.input.slice(contentStart, state.pos),
|
|
804
|
+
line: startLine,
|
|
805
|
+
column: startColumn,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Consumes bold text **text**.
|
|
811
|
+
*
|
|
812
|
+
* @param state - The tokenizer state to update
|
|
813
|
+
*/
|
|
814
|
+
function consumeBold(state) {
|
|
815
|
+
const startColumn = state.column;
|
|
816
|
+
const startLine = state.line;
|
|
817
|
+
state.pos += 2; // skip opening **
|
|
818
|
+
state.column += 2;
|
|
819
|
+
const contentStart = state.pos;
|
|
820
|
+
while (state.pos < state.input.length) {
|
|
821
|
+
// Check for closing **
|
|
822
|
+
if (state.input[state.pos] === '*' && state.input[state.pos + 1] === '*') {
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
if (state.input[state.pos] === '\n') {
|
|
826
|
+
state.line++;
|
|
827
|
+
state.column = 0;
|
|
828
|
+
}
|
|
829
|
+
state.pos++;
|
|
830
|
+
state.column++;
|
|
831
|
+
}
|
|
832
|
+
if (state.input[state.pos] === '*' && state.input[state.pos + 1] === '*') {
|
|
833
|
+
const content = state.input.slice(contentStart, state.pos);
|
|
834
|
+
state.pos += 2; // skip closing **
|
|
835
|
+
state.column += 2;
|
|
836
|
+
state.tokens.push({
|
|
837
|
+
type: 'bold',
|
|
838
|
+
value: content,
|
|
839
|
+
line: startLine,
|
|
840
|
+
column: startColumn,
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
// No closing **, emit as text
|
|
845
|
+
state.tokens.push({
|
|
846
|
+
type: 'text',
|
|
847
|
+
value: '**' + state.input.slice(contentStart, state.pos),
|
|
848
|
+
line: startLine,
|
|
849
|
+
column: startColumn,
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Consumes plain text until a special character or end of line.
|
|
855
|
+
*
|
|
856
|
+
* @param state - The tokenizer state to update
|
|
857
|
+
*/
|
|
858
|
+
function consumeText(state) {
|
|
859
|
+
const startColumn = state.column;
|
|
860
|
+
const startLine = state.line;
|
|
861
|
+
const startPos = state.pos;
|
|
862
|
+
while (state.pos < state.input.length) {
|
|
863
|
+
const char = state.input[state.pos];
|
|
864
|
+
// Stop at newline
|
|
865
|
+
if (char === '\n' || char === '\r') {
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
// Stop at special characters (but only if they start a pattern)
|
|
869
|
+
if (char === '[')
|
|
870
|
+
break;
|
|
871
|
+
if (char === '`')
|
|
872
|
+
break;
|
|
873
|
+
if (char === '*' && state.input[state.pos + 1] === '*')
|
|
874
|
+
break;
|
|
875
|
+
state.pos++;
|
|
876
|
+
state.column++;
|
|
877
|
+
}
|
|
878
|
+
const content = state.input.slice(startPos, state.pos);
|
|
879
|
+
if (content.length > 0) {
|
|
880
|
+
state.tokens.push({
|
|
881
|
+
type: 'text',
|
|
882
|
+
value: content,
|
|
883
|
+
line: startLine,
|
|
884
|
+
column: startColumn,
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Pushes a token to the state.
|
|
890
|
+
*
|
|
891
|
+
* @param state - The tokenizer state to update
|
|
892
|
+
* @param type - Token classification (e.g., 'heading-1', 'text', 'link-url')
|
|
893
|
+
* @param value - Text content associated with the token
|
|
894
|
+
*/
|
|
895
|
+
function pushToken(state, type, value) {
|
|
896
|
+
state.tokens.push({
|
|
897
|
+
type,
|
|
898
|
+
value,
|
|
899
|
+
line: state.line,
|
|
900
|
+
column: state.column,
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Checks if a character is whitespace (but not newline).
|
|
905
|
+
*
|
|
906
|
+
* @param char - The character to check
|
|
907
|
+
* @returns True if the character is whitespace (space or tab)
|
|
908
|
+
*/
|
|
909
|
+
function isWhitespace(char) {
|
|
910
|
+
return char === ' ' || char === '\t';
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Changelog Parser
|
|
915
|
+
*
|
|
916
|
+
* Parses a changelog markdown string into a structured Changelog object.
|
|
917
|
+
* Uses a state machine tokenizer for ReDoS-safe parsing.
|
|
918
|
+
*/
|
|
919
|
+
/**
|
|
920
|
+
* Parses a changelog markdown string into a Changelog object.
|
|
921
|
+
*
|
|
922
|
+
* @param content - The markdown content to parse
|
|
923
|
+
* @param source - Optional source file path
|
|
924
|
+
* @returns Parsed Changelog object
|
|
925
|
+
*/
|
|
926
|
+
function parseChangelog(content, source) {
|
|
927
|
+
const tokens = tokenize(content);
|
|
928
|
+
const state = {
|
|
929
|
+
tokens,
|
|
930
|
+
pos: 0,
|
|
931
|
+
warnings: [],
|
|
932
|
+
};
|
|
933
|
+
// Parse header
|
|
934
|
+
const header = parseHeader(state);
|
|
935
|
+
// Parse entries
|
|
936
|
+
const entries = parseEntries(state);
|
|
937
|
+
// Detect format
|
|
938
|
+
const format = detectFormat(header, entries);
|
|
939
|
+
// Build metadata
|
|
940
|
+
const metadata = {
|
|
941
|
+
format,
|
|
942
|
+
isConventional: format === 'conventional',
|
|
943
|
+
repositoryUrl: state.repositoryUrl,
|
|
944
|
+
warnings: state.warnings,
|
|
945
|
+
};
|
|
946
|
+
return {
|
|
947
|
+
source,
|
|
948
|
+
header,
|
|
949
|
+
entries,
|
|
950
|
+
metadata,
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Parses the changelog header section.
|
|
955
|
+
*
|
|
956
|
+
* @param state - The parser state containing tokens and position
|
|
957
|
+
* @returns The parsed ChangelogHeader with title, description, and links
|
|
958
|
+
*/
|
|
959
|
+
function parseHeader(state) {
|
|
960
|
+
let title = '# Changelog';
|
|
961
|
+
const description = [];
|
|
962
|
+
const links = [];
|
|
963
|
+
// Look for h1 title
|
|
964
|
+
const headingToken = currentToken(state);
|
|
965
|
+
if (headingToken?.type === 'heading-1') {
|
|
966
|
+
title = `# ${headingToken.value}`;
|
|
967
|
+
advance(state);
|
|
968
|
+
}
|
|
969
|
+
// Skip newlines
|
|
970
|
+
skipNewlines(state);
|
|
971
|
+
// Collect description lines until we hit h2 (version entry)
|
|
972
|
+
while (!isEOF(state) && currentToken(state)?.type !== 'heading-2') {
|
|
973
|
+
const token = currentToken(state);
|
|
974
|
+
if (!token)
|
|
975
|
+
break;
|
|
976
|
+
if (token.type === 'text') {
|
|
977
|
+
description.push(token.value);
|
|
978
|
+
}
|
|
979
|
+
else if (token.type === 'link-text') {
|
|
980
|
+
// Check for link definition
|
|
981
|
+
const nextToken = peek(state, 1);
|
|
982
|
+
if (nextToken?.type === 'link-url') {
|
|
983
|
+
description.push(`[${token.value}](${nextToken.value})`);
|
|
984
|
+
links.push({ label: token.value, url: nextToken.value });
|
|
985
|
+
// Try to detect repository URL
|
|
986
|
+
if (!state.repositoryUrl && nextToken.value.includes('github.com')) {
|
|
987
|
+
state.repositoryUrl = extractRepoUrl(nextToken.value);
|
|
988
|
+
}
|
|
989
|
+
advance(state); // skip link-text
|
|
990
|
+
advance(state); // skip link-url
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
else if (token.type === 'newline' || token.type === 'blank-line') {
|
|
995
|
+
if (description.length > 0 && description[description.length - 1] !== '') {
|
|
996
|
+
description.push('');
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
advance(state);
|
|
1000
|
+
}
|
|
1001
|
+
// Trim trailing empty lines
|
|
1002
|
+
while (description.length > 0 && description[description.length - 1] === '') {
|
|
1003
|
+
description.pop();
|
|
1004
|
+
}
|
|
1005
|
+
return { title, description, links };
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Parses all changelog entries.
|
|
1009
|
+
*
|
|
1010
|
+
* @param state - The parser state containing tokens and position
|
|
1011
|
+
* @returns An array of parsed ChangelogEntry objects
|
|
1012
|
+
*/
|
|
1013
|
+
function parseEntries(state) {
|
|
1014
|
+
const entries = [];
|
|
1015
|
+
while (!isEOF(state)) {
|
|
1016
|
+
// Look for h2 heading (version entry)
|
|
1017
|
+
if (currentToken(state)?.type === 'heading-2') {
|
|
1018
|
+
const entry = parseEntry(state);
|
|
1019
|
+
if (entry) {
|
|
1020
|
+
entries.push(entry);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
else {
|
|
1024
|
+
advance(state);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return entries;
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Parses a single changelog entry.
|
|
1031
|
+
*
|
|
1032
|
+
* @param state - The parser state containing tokens and position
|
|
1033
|
+
* @returns The parsed ChangelogEntry or null if parsing fails
|
|
1034
|
+
*/
|
|
1035
|
+
function parseEntry(state) {
|
|
1036
|
+
const headingToken = currentToken(state);
|
|
1037
|
+
if (headingToken?.type !== 'heading-2') {
|
|
1038
|
+
return null;
|
|
1039
|
+
}
|
|
1040
|
+
const { version, date, compareUrl } = parseVersionFromHeading(headingToken.value);
|
|
1041
|
+
const unreleased = version.toLowerCase() === 'unreleased';
|
|
1042
|
+
advance(state); // skip h2
|
|
1043
|
+
skipNewlines(state);
|
|
1044
|
+
// Parse sections
|
|
1045
|
+
const sections = parseSections(state);
|
|
1046
|
+
return {
|
|
1047
|
+
version,
|
|
1048
|
+
date,
|
|
1049
|
+
unreleased,
|
|
1050
|
+
compareUrl,
|
|
1051
|
+
sections,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Parses sections within an entry.
|
|
1056
|
+
*
|
|
1057
|
+
* @param state - The parser state containing tokens and position
|
|
1058
|
+
* @returns An array of parsed ChangelogSection objects
|
|
1059
|
+
*/
|
|
1060
|
+
function parseSections(state) {
|
|
1061
|
+
const sections = [];
|
|
1062
|
+
while (!isEOF(state)) {
|
|
1063
|
+
const token = currentToken(state);
|
|
1064
|
+
// Stop at next version entry (h2)
|
|
1065
|
+
if (token?.type === 'heading-2') {
|
|
1066
|
+
break;
|
|
1067
|
+
}
|
|
1068
|
+
// Parse section (h3)
|
|
1069
|
+
if (token?.type === 'heading-3') {
|
|
1070
|
+
const section = parseSection(state);
|
|
1071
|
+
if (section) {
|
|
1072
|
+
sections.push(section);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
else if (token?.type === 'list-item') {
|
|
1076
|
+
// Items without section heading - create "other" section
|
|
1077
|
+
const items = parseItems(state);
|
|
1078
|
+
if (items.length > 0) {
|
|
1079
|
+
sections.push({
|
|
1080
|
+
type: 'other',
|
|
1081
|
+
heading: 'Changes',
|
|
1082
|
+
items,
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
else {
|
|
1087
|
+
advance(state);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return sections;
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Parses a single section.
|
|
1094
|
+
*
|
|
1095
|
+
* @param state - The parser state containing tokens and position
|
|
1096
|
+
* @returns The parsed ChangelogSection or null if parsing fails
|
|
1097
|
+
*/
|
|
1098
|
+
function parseSection(state) {
|
|
1099
|
+
const headingToken = currentToken(state);
|
|
1100
|
+
if (headingToken?.type !== 'heading-3') {
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
const heading = headingToken.value;
|
|
1104
|
+
const type = getSectionType(heading);
|
|
1105
|
+
advance(state); // skip h3
|
|
1106
|
+
skipNewlines(state);
|
|
1107
|
+
// Parse items
|
|
1108
|
+
const items = parseItems(state);
|
|
1109
|
+
return {
|
|
1110
|
+
type,
|
|
1111
|
+
heading,
|
|
1112
|
+
items,
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Parses list items.
|
|
1117
|
+
*
|
|
1118
|
+
* @param state - The parser state containing tokens and position
|
|
1119
|
+
* @returns An array of parsed ChangelogItem objects
|
|
1120
|
+
*/
|
|
1121
|
+
function parseItems(state) {
|
|
1122
|
+
const items = [];
|
|
1123
|
+
while (!isEOF(state)) {
|
|
1124
|
+
const token = currentToken(state);
|
|
1125
|
+
// Stop at headings
|
|
1126
|
+
if (token?.type === 'heading-2' || token?.type === 'heading-3') {
|
|
1127
|
+
break;
|
|
1128
|
+
}
|
|
1129
|
+
// Parse list item
|
|
1130
|
+
if (token?.type === 'list-item') {
|
|
1131
|
+
const item = parseItem(state);
|
|
1132
|
+
if (item) {
|
|
1133
|
+
items.push(item);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
advance(state);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return items;
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Parses a single list item.
|
|
1144
|
+
*
|
|
1145
|
+
* @param state - The parser state containing tokens and position
|
|
1146
|
+
* @returns The parsed ChangelogItem or null if parsing fails
|
|
1147
|
+
*/
|
|
1148
|
+
function parseItem(state) {
|
|
1149
|
+
const token = currentToken(state);
|
|
1150
|
+
if (token?.type !== 'list-item') {
|
|
1151
|
+
return null;
|
|
1152
|
+
}
|
|
1153
|
+
const text = token.value;
|
|
1154
|
+
const { scope, description } = parseScopeFromItem(text);
|
|
1155
|
+
const commits = parseCommitRefs(text, state.repositoryUrl);
|
|
1156
|
+
const references = parseIssueRefs(text, state.repositoryUrl);
|
|
1157
|
+
// Check for breaking change indicators
|
|
1158
|
+
const breaking = isBreakingItem(text);
|
|
1159
|
+
advance(state);
|
|
1160
|
+
return createChangelogItem(description, {
|
|
1161
|
+
scope,
|
|
1162
|
+
commits,
|
|
1163
|
+
references,
|
|
1164
|
+
breaking,
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Checks if an item indicates a breaking change.
|
|
1169
|
+
*
|
|
1170
|
+
* @param text - The text content of the item
|
|
1171
|
+
* @returns True if the item indicates a breaking change
|
|
1172
|
+
*/
|
|
1173
|
+
function isBreakingItem(text) {
|
|
1174
|
+
const lower = text.toLowerCase();
|
|
1175
|
+
return lower.includes('breaking change') || lower.includes('breaking:') || lower.startsWith('!') || lower.includes('[breaking]');
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Detects the changelog format.
|
|
1179
|
+
*
|
|
1180
|
+
* @param header - The parsed header of the changelog
|
|
1181
|
+
* @param entries - The parsed changelog entries
|
|
1182
|
+
* @returns The detected ChangelogFormat
|
|
1183
|
+
*/
|
|
1184
|
+
function detectFormat(header, entries) {
|
|
1185
|
+
const descriptionText = header.description.join(' ').toLowerCase();
|
|
1186
|
+
// Check for Keep a Changelog
|
|
1187
|
+
if (descriptionText.includes('keep a changelog') || descriptionText.includes('keepachangelog')) {
|
|
1188
|
+
return 'keep-a-changelog';
|
|
1189
|
+
}
|
|
1190
|
+
// Check for conventional changelog patterns
|
|
1191
|
+
const hasConventionalSections = entries.some((entry) => entry.sections.some((section) => ['features', 'fixes', 'performance'].includes(section.type)));
|
|
1192
|
+
if (hasConventionalSections) {
|
|
1193
|
+
return 'conventional';
|
|
1194
|
+
}
|
|
1195
|
+
// Check if we have entries with structured sections
|
|
1196
|
+
if (entries.some((entry) => entry.sections.length > 0)) {
|
|
1197
|
+
return 'custom';
|
|
1198
|
+
}
|
|
1199
|
+
return 'unknown';
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Extracts repository URL from a GitHub URL.
|
|
1203
|
+
*
|
|
1204
|
+
* @param url - The URL to extract the repository from
|
|
1205
|
+
* @returns The repository URL or undefined if not found
|
|
1206
|
+
*/
|
|
1207
|
+
function extractRepoUrl(url) {
|
|
1208
|
+
// Try to extract base repo URL from various GitHub URL patterns
|
|
1209
|
+
const githubIndex = url.indexOf('github.com/');
|
|
1210
|
+
if (githubIndex !== -1) {
|
|
1211
|
+
const afterGithub = url.slice(githubIndex + 11);
|
|
1212
|
+
const parts = afterGithub.split('/');
|
|
1213
|
+
if (parts.length >= 2) {
|
|
1214
|
+
return `https://github.com/${parts[0]}/${parts[1]}`;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return undefined;
|
|
1218
|
+
}
|
|
1219
|
+
// ============================================================================
|
|
1220
|
+
// Parser utilities
|
|
1221
|
+
// ============================================================================
|
|
1222
|
+
/**
|
|
1223
|
+
* Gets the current token at the parser position.
|
|
1224
|
+
*
|
|
1225
|
+
* @param state - The parser state
|
|
1226
|
+
* @returns The current token or undefined if at end
|
|
1227
|
+
*/
|
|
1228
|
+
function currentToken(state) {
|
|
1229
|
+
return state.tokens[state.pos];
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Peeks at a token at an offset from the current position.
|
|
1233
|
+
*
|
|
1234
|
+
* @param state - The parser state
|
|
1235
|
+
* @param offset - The offset from current position
|
|
1236
|
+
* @returns The token at the offset or undefined if out of bounds
|
|
1237
|
+
*/
|
|
1238
|
+
function peek(state, offset) {
|
|
1239
|
+
return state.tokens[state.pos + offset];
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Advances the parser position by one token.
|
|
1243
|
+
*
|
|
1244
|
+
* @param state - The parser state to advance
|
|
1245
|
+
*/
|
|
1246
|
+
function advance(state) {
|
|
1247
|
+
state.pos++;
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Checks if the parser has reached the end of the token stream.
|
|
1251
|
+
*
|
|
1252
|
+
* @param state - The parser state
|
|
1253
|
+
* @returns True if at end of file
|
|
1254
|
+
*/
|
|
1255
|
+
function isEOF(state) {
|
|
1256
|
+
const token = currentToken(state);
|
|
1257
|
+
return !token || token.type === 'eof';
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Skips newline tokens until a non-newline token is found.
|
|
1261
|
+
*
|
|
1262
|
+
* @param state - The parser state
|
|
1263
|
+
*/
|
|
1264
|
+
function skipNewlines(state) {
|
|
1265
|
+
while (!isEOF(state)) {
|
|
1266
|
+
const token = currentToken(state);
|
|
1267
|
+
if (token?.type !== 'newline' && token?.type !== 'blank-line') {
|
|
1268
|
+
break;
|
|
1269
|
+
}
|
|
1270
|
+
advance(state);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
export { parseChangelog, parseCommitRefs, parseIssueRefs, parseScopeFromItem, parseVersionFromHeading, tokenize };
|
|
1275
|
+
//# sourceMappingURL=index.esm.js.map
|