206 lines
5.4 KiB
JavaScript
206 lines
5.4 KiB
JavaScript
import { promises as fs } from 'node:fs'
|
|
import { basename, dirname, join, resolve } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const scriptDir = dirname(fileURLToPath(import.meta.url))
|
|
const repoRoot = resolve(scriptDir, '..', '..')
|
|
const tmpDir = join(repoRoot, '.tmp')
|
|
|
|
const evidenceFiles = {
|
|
home: join(tmpDir, 'rea-four-modules-library-stable-20261017.json'),
|
|
local: join(tmpDir, 'rea-four-modules-library-local-20261017.json')
|
|
}
|
|
|
|
const prefixes = [
|
|
'knowledge-sidebar',
|
|
'main-layout-content',
|
|
'workspace-tree',
|
|
'wb-home',
|
|
'doc-feed',
|
|
'wb-feed',
|
|
'my-files'
|
|
]
|
|
|
|
const styleKeys = [
|
|
'display',
|
|
'position',
|
|
'font-family',
|
|
'font-size',
|
|
'font-weight',
|
|
'line-height',
|
|
'color',
|
|
'background-color',
|
|
'border-radius',
|
|
'border-top-width',
|
|
'border-top-color',
|
|
'padding-top',
|
|
'padding-right',
|
|
'padding-bottom',
|
|
'padding-left',
|
|
'margin-top',
|
|
'margin-right',
|
|
'margin-bottom',
|
|
'margin-left',
|
|
'gap',
|
|
'align-items',
|
|
'justify-content',
|
|
'box-shadow',
|
|
'overflow',
|
|
'max-width',
|
|
'min-height',
|
|
'transition-property',
|
|
'transition-duration',
|
|
'transition-timing-function',
|
|
'opacity',
|
|
'transform'
|
|
]
|
|
|
|
function stripComments(source) {
|
|
return source.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
}
|
|
|
|
function cssRules(source) {
|
|
const css = stripComments(source)
|
|
const stack = []
|
|
const rules = []
|
|
let boundary = 0
|
|
let quote = ''
|
|
let escaped = false
|
|
|
|
for (let index = 0; index < css.length; index += 1) {
|
|
const char = css[index]
|
|
if (quote) {
|
|
if (escaped) escaped = false
|
|
else if (char === '\\') escaped = true
|
|
else if (char === quote) quote = ''
|
|
continue
|
|
}
|
|
if (char === '"' || char === "'") {
|
|
quote = char
|
|
continue
|
|
}
|
|
if (char === '{') {
|
|
const prelude = css.slice(boundary, index).trim()
|
|
stack.push({ prelude, bodyStart: index + 1 })
|
|
boundary = index + 1
|
|
continue
|
|
}
|
|
if (char === '}') {
|
|
const entry = stack.pop()
|
|
if (entry) {
|
|
const body = css.slice(entry.bodyStart, index).trim()
|
|
if (body && !entry.prelude.startsWith('@')) {
|
|
rules.push({
|
|
context: stack.map((item) => item.prelude).filter((item) => item.startsWith('@')),
|
|
selector: entry.prelude,
|
|
body
|
|
})
|
|
}
|
|
}
|
|
boundary = index + 1
|
|
continue
|
|
}
|
|
if (char === ';' && stack.length === 0) boundary = index + 1
|
|
}
|
|
return rules
|
|
}
|
|
|
|
function matchesPrefix(selector) {
|
|
return prefixes.some((prefix) => selector.includes(prefix))
|
|
}
|
|
|
|
function relevantNode(node) {
|
|
const bounds = node.bounds ?? []
|
|
if (bounds.length < 4 || bounds[2] <= 0 || bounds[3] <= 0) return false
|
|
if (!node.attributes || !Object.keys(node.attributes).length) return false
|
|
return true
|
|
}
|
|
|
|
function layoutNode(node) {
|
|
const styles = node.styles ?? {}
|
|
return {
|
|
nodeIndex: node.node_index,
|
|
parentIndex: node.parent_index,
|
|
tag: node.node_name,
|
|
attributes: node.attributes,
|
|
bounds: node.bounds,
|
|
styles: Object.fromEntries(styleKeys.map((key) => [key, styles[key]]))
|
|
}
|
|
}
|
|
|
|
async function readEvidence(path) {
|
|
return JSON.parse(await fs.readFile(path, 'utf8'))
|
|
}
|
|
|
|
async function main() {
|
|
const evidence = {}
|
|
for (const [name, path] of Object.entries(evidenceFiles)) {
|
|
evidence[name] = await readEvidence(path)
|
|
}
|
|
|
|
const seenRules = new Set()
|
|
const recovered = []
|
|
const styleSources = new Map()
|
|
|
|
for (const [name, entry] of Object.entries(evidence)) {
|
|
for (const sheet of entry.normalized_result.styles.items ?? []) {
|
|
const text = sheet.source?.artifact?.text
|
|
if (!text || !prefixes.some((prefix) => text.includes(prefix))) continue
|
|
const sheetName = basename(sheet.file_path ?? 'inline.css')
|
|
styleSources.set(sheetName, {
|
|
path: sheet.file_path,
|
|
bytes: sheet.source.artifact.bytes,
|
|
sha256: sheet.source.artifact.sha256
|
|
})
|
|
for (const rule of cssRules(text)) {
|
|
if (!matchesPrefix(rule.selector)) continue
|
|
const key = `${rule.context.join('>')}|${rule.selector}|${rule.body}`
|
|
if (seenRules.has(key)) continue
|
|
seenRules.add(key)
|
|
recovered.push({
|
|
evidence: name,
|
|
sheet: sheetName,
|
|
...rule
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
const css = recovered
|
|
.map((rule) => {
|
|
const prefix = rule.context.length ? `${rule.context.join(' { ')} { ` : ''
|
|
const suffix = rule.context.length ? ` ${'}'.repeat(rule.context.length)}` : ''
|
|
return `/* ${rule.evidence}:${rule.sheet} */\n${prefix}${rule.selector} { ${rule.body} }${suffix}`
|
|
})
|
|
.join('\n\n')
|
|
|
|
const layout = {}
|
|
for (const [name, entry] of Object.entries(evidence)) {
|
|
layout[name] = (entry.normalized_result.visuals.layout.documents ?? [])
|
|
.flatMap((document) => document.nodes ?? [])
|
|
.filter(relevantNode)
|
|
.map(layoutNode)
|
|
}
|
|
|
|
await fs.mkdir(tmpDir, { recursive: true })
|
|
await Promise.all([
|
|
fs.writeFile(join(tmpDir, 'library-recovered-rules.css'), `${css}\n`, 'utf8'),
|
|
fs.writeFile(join(tmpDir, 'library-layout-summary.json'), JSON.stringify({
|
|
styleSources: Object.fromEntries(styleSources),
|
|
layout
|
|
}, null, 2), 'utf8')
|
|
])
|
|
|
|
process.stdout.write(JSON.stringify({
|
|
rules: recovered.length,
|
|
css: join(tmpDir, 'library-recovered-rules.css'),
|
|
layout: join(tmpDir, 'library-layout-summary.json'),
|
|
styleSources
|
|
}, null, 2))
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error)
|
|
process.exitCode = 1
|
|
})
|