fix: align v2 publisher review with HMDesign stories and tokens
This commit is contained in:
1 parent
1c4e0bb9a1
commit
7b22fe88a1
27 files changed
+413
-49
No files matched your search
@@ -3,7 +3,70 @@ import path from 'node:path'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
|
||||
const root = process.cwd()
|
||||
const files = [...new Set(execFileSync('git', ['ls-files', '--cached', '--others', '--exclude-standard', '-z'], { encoding: 'utf8' }).split('\0'))].filter(file => file.endsWith('.md'))
|
||||
// A portable checkout must not accidentally use a parent directory's Git repository.
|
||||
let collectionMode
|
||||
try {
|
||||
fs.lstatSync(path.join(root, '.git'))
|
||||
collectionMode = 'git'
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') collectionMode = 'filesystem'
|
||||
else {
|
||||
console.error(JSON.stringify({ error: 'Git metadata detection failed', detail: error.message }))
|
||||
process.exit(2)
|
||||
}
|
||||
}
|
||||
function ignoreRules(directory) {
|
||||
const ignoreFile = path.join(directory, '.gitignore')
|
||||
let source
|
||||
try { source = fs.readFileSync(ignoreFile, 'utf8') }
|
||||
catch (error) { if (error.code === 'ENOENT') return []; throw error }
|
||||
return source.split(/\r?\n/).flatMap(line => {
|
||||
if (!line || line.startsWith('#')) return []
|
||||
const negate = line.startsWith('!')
|
||||
let pattern = (negate ? line.slice(1) : line).trimEnd()
|
||||
const directoryOnly = pattern.endsWith('/')
|
||||
if (directoryOnly) pattern = pattern.slice(0, -1)
|
||||
const anchored = pattern.startsWith('/') || pattern.includes('/')
|
||||
pattern = pattern.replace(/^\//, '')
|
||||
// Repository ignore rules use literal paths, *, **, ? and negation.
|
||||
let expression = ''
|
||||
for (let i = 0; i < pattern.length; i++) {
|
||||
const char = pattern[i]
|
||||
if (char === '*' && pattern[i + 1] === '*') {
|
||||
i++
|
||||
if (pattern[i + 1] === '/') { expression += '(?:.*/)?'; i++ }
|
||||
else expression += '.*'
|
||||
} else if (char === '*') expression += '[^/]*'
|
||||
else if (char === '?') expression += '[^/]'
|
||||
else expression += char.replace(/[\\^$+?.()|{}\[\]]/g, '\\$&')
|
||||
}
|
||||
return [{ directory, negate, directoryOnly, regex: new RegExp(`${anchored ? '^' : '(?:^|/)'}${expression}$`) }]
|
||||
})
|
||||
}
|
||||
function filesystemMarkdown(directory = root, inherited = []) {
|
||||
const rules = [...inherited, ...ignoreRules(directory)]
|
||||
return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
|
||||
if (entry.name === '.git') return []
|
||||
const absolute = path.join(directory, entry.name)
|
||||
let ignored = false
|
||||
for (const rule of rules) {
|
||||
const relative = path.relative(rule.directory, absolute).replaceAll('\\', '/')
|
||||
if ((!rule.directoryOnly || entry.isDirectory()) && rule.regex.test(relative)) ignored = !rule.negate
|
||||
}
|
||||
if (ignored) return []
|
||||
if (entry.isDirectory()) return filesystemMarkdown(absolute, rules)
|
||||
return entry.isFile() && entry.name.endsWith('.md') ? [path.relative(root, absolute).replaceAll('\\', '/')] : []
|
||||
})
|
||||
}
|
||||
let files
|
||||
try {
|
||||
files = collectionMode === 'git'
|
||||
? [...new Set(execFileSync('git', ['ls-files', '--cached', '--others', '--exclude-standard', '-z'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).split('\0'))].filter(file => file.endsWith('.md'))
|
||||
: filesystemMarkdown().sort()
|
||||
} catch (error) {
|
||||
console.error(JSON.stringify({ collectionMode, error: 'Markdown collection failed', detail: error.message }))
|
||||
process.exit(2)
|
||||
}
|
||||
const stripCode = text => text.replace(/^```[^\n]*\n[\s\S]*?^```[^\n]*$/gm, '')
|
||||
const slug = text => text.toLowerCase().replace(/`/g, '').replace(/[^\p{Letter}\p{Number}\p{Mark}_\- ]/gu, '').replace(/ /g, '-')
|
||||
const anchors = file => {
|
||||
@@ -30,5 +93,5 @@ for (const file of files) for (const [, raw] of stripCode(fs.readFileSync(file,
|
||||
if (reason) problems.push({ file, href, reason })
|
||||
} catch (error) { problems.push({ file, href, reason: error.message }) }
|
||||
}
|
||||
console.log(JSON.stringify({ files: files.length, internalLinks, problems }, null, 2))
|
||||
console.log(JSON.stringify({ collectionMode, files: files.length, internalLinks, problems }, null, 2))
|
||||
if (problems.length) process.exitCode = 1
|
||||
@@ -0,0 +1,76 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import os from 'node:os'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { spawnSync, execFileSync } from 'node:child_process'
|
||||
|
||||
const script = fileURLToPath(new URL('./check-doc-links.mjs', import.meta.url))
|
||||
function fixture(t, contents) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hm-doc-links-'))
|
||||
t.after(() => {
|
||||
const relative = path.relative(path.resolve(os.tmpdir()), path.resolve(root))
|
||||
assert.ok(relative.startsWith('hm-doc-links-') && !relative.includes(path.sep), 'cleanup must stay in its own temporary fixture')
|
||||
fs.rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
for (const [name, source] of Object.entries(contents)) {
|
||||
const target = path.join(root, name)
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true })
|
||||
fs.writeFileSync(target, source)
|
||||
}
|
||||
return root
|
||||
}
|
||||
function run(root) {
|
||||
const result = spawnSync(process.execPath, [script], { cwd: root, encoding: 'utf8' })
|
||||
return { status: result.status, report: JSON.parse(result.stdout || result.stderr) }
|
||||
}
|
||||
|
||||
test('Git and filesystem modes preserve current ignore scope and link counts', t => {
|
||||
const root = fixture(t, {
|
||||
'.gitignore': 'node_modules/\n_local/\n**/dist-dashboard/\n*.log\n.env.*\n!.env.example\n',
|
||||
'README.md': '[한글](docs/a%20b.md#한글-제목) [asset](.env.example)\n```md\n[x](missing.md)\n```\n',
|
||||
'docs/a b.md': '# 한글 제목\n# 한글 제목\n[duplicate](#한글-제목-1) [id](#custom)\n<a id="custom"></a>\n',
|
||||
'.env.example': '',
|
||||
'node_modules/package/bad.md': '[x](missing.md)',
|
||||
'_local/bad.md': '[x](missing.md)',
|
||||
'app/dist-dashboard/bad.md': '[x](missing.md)',
|
||||
})
|
||||
const portable = run(root)
|
||||
assert.equal(portable.status, 0)
|
||||
assert.equal(portable.report.collectionMode, 'filesystem')
|
||||
assert.equal(portable.report.files, 2)
|
||||
assert.equal(portable.report.internalLinks, 4)
|
||||
execFileSync('git', ['init', '--quiet', root])
|
||||
execFileSync('git', ['add', '.'], { cwd: root })
|
||||
const repository = run(root)
|
||||
assert.equal(repository.status, 0)
|
||||
assert.equal(repository.report.collectionMode, 'git')
|
||||
assert.deepEqual({ ...repository.report, collectionMode: 'filesystem' }, portable.report)
|
||||
})
|
||||
|
||||
test('missing paths, headings and excluded local evidence return exit 1', t => {
|
||||
const root = fixture(t, {
|
||||
'README.md': '[path](missing.md) [heading](doc.md#missing) [local](_local/proof.json)',
|
||||
'doc.md': '# Present', '_local/proof.json': '{}',
|
||||
})
|
||||
const result = run(root)
|
||||
assert.equal(result.status, 1)
|
||||
assert.deepEqual(result.report.problems.map(problem => problem.reason), ['missing path', 'missing heading', 'local evidence is excluded from Git Clone'])
|
||||
})
|
||||
|
||||
test('nested ignore rules and negation retain the intended Markdown files', t => {
|
||||
const root = fixture(t, {
|
||||
'.gitignore': '*.md\n!README.md\n', 'README.md': '# Readme', 'ignored.md': '[x](missing)',
|
||||
'docs/.gitignore': '!keep.md\n', 'docs/keep.md': '# Kept', 'docs/drop.md': '[x](missing)',
|
||||
})
|
||||
assert.equal(run(root).report.files, 2)
|
||||
})
|
||||
|
||||
test('broken Git metadata is an operational error, never a silent fallback', t => {
|
||||
const root = fixture(t, { '.git': 'gitdir: missing-metadata', 'README.md': '# Readme' })
|
||||
const result = run(root)
|
||||
assert.equal(result.status, 2)
|
||||
assert.equal(result.report.collectionMode, 'git')
|
||||
assert.equal(result.report.error, 'Markdown collection failed')
|
||||
})
|
||||
@@ -200,3 +200,15 @@
|
||||
--ring: var(--hm-sem-stroke-input-focus);
|
||||
--radius: var(--hm-radius-container);
|
||||
}
|
||||
|
||||
/* Additive brand palette; does not change existing component aliases. */
|
||||
:root, [data-hm-theme] {
|
||||
--hm-sem-palette-green-dark-3: {{color.palette.green-dark-3|var(--hm-green-dark-3)}};
|
||||
--hm-sem-palette-brown-light-3: {{color.palette.brown-light-3|var(--hm-brown-light-3)}};
|
||||
--hm-sem-palette-brown-dark-2: {{color.palette.brown-dark-2|var(--hm-brown-dark-2)}};
|
||||
--hm-sem-palette-brown-dark-3: {{color.palette.brown-dark-3|var(--hm-brown-dark-3)}};
|
||||
--hm-sem-palette-orange-light-3: {{color.palette.orange-light-3|var(--hm-orange-light-3)}};
|
||||
--hm-sem-palette-orange-light-2: {{color.palette.orange-light-2|var(--hm-orange-light-2)}};
|
||||
--hm-sem-palette-orange-light-1: {{color.palette.orange-light-1|var(--hm-orange-light-1)}};
|
||||
--hm-sem-palette-orange-dark-3: {{color.palette.orange-dark-3|var(--hm-orange-dark-3)}};
|
||||
}
|
||||
@@ -43,3 +43,15 @@
|
||||
:root {
|
||||
--hm-yellow-accent: {{color.palette.yellow-accent}};
|
||||
}
|
||||
|
||||
/* Confirmed brand palette additions (ui_viz Color 4.1). */
|
||||
:root {
|
||||
--hm-green-dark-3: {{color.palette.green-dark-3}};
|
||||
--hm-brown-light-3: {{color.palette.brown-light-3}};
|
||||
--hm-brown-dark-2: {{color.palette.brown-dark-2}};
|
||||
--hm-brown-dark-3: {{color.palette.brown-dark-3}};
|
||||
--hm-orange-light-3: {{color.palette.orange-light-3}};
|
||||
--hm-orange-light-2: {{color.palette.orange-light-2}};
|
||||
--hm-orange-light-1: {{color.palette.orange-light-1}};
|
||||
--hm-orange-dark-3: {{color.palette.orange-dark-3}};
|
||||
}
|
||||
Reference in new issue
Block a user