blogger-hbs 0.0.1 → 0.0.3

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/dist/cli/index.js CHANGED
@@ -2,6 +2,7 @@ import { CompileError } from "../compiler/errors.js";
2
2
  import { didYouMean } from "../utils/suggest.js";
3
3
  import { COMPILE_OPTION_KEYS, assertCompileOptions, assertDirectories, assertEntry, resolvePaths } from "../compiler/options.js";
4
4
  import { formatXml, validateXml } from "../xml/format.js";
5
+ import "../xml/index.js";
5
6
  import { compileTemplate } from "../compiler/compile.js";
6
7
  import { watchTemplate } from "../compiler/watch.js";
7
8
  import "../compiler/index.js";
@@ -218,7 +219,7 @@ async function watchBuild(root, flags) {
218
219
  const stop = async () => {
219
220
  stopWatchers();
220
221
  await watcher?.close();
221
- process.exit(0);
222
+ setTimeout(() => process.exit(0), 3e3).unref();
222
223
  };
223
224
  process.once("SIGINT", stop);
224
225
  process.once("SIGTERM", stop);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["import { type FSWatcher, watch } from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport { createRequire } from 'node:module';\nimport * as path from 'node:path';\nimport { styleText } from 'node:util';\nimport { cac } from 'cac';\nimport {\n\ttype CompiledTemplate,\n\tCompileError,\n\ttype CompileTemplateOptions,\n\tcompileTemplate,\n\ttype TemplateWatcher,\n\twatchTemplate,\n} from '../compiler';\nimport {\n\tassertCompileOptions,\n\tassertDirectories,\n\tassertEntry,\n\tCOMPILE_OPTION_KEYS,\n\tresolvePaths,\n} from '../compiler/options';\nimport { type LoadedConfig, loadConfig } from '../config';\nimport { renderTemplate } from '../runtime';\nimport { didYouMean } from '../utils/suggest';\nimport { formatXml, validateXml } from '../xml';\n\nconst { version } = createRequire(import.meta.url)('../../package.json') as {\n\tversion: string;\n};\n\ninterface BuildFlags {\n\tconfig?: string;\n\tentry?: string;\n\toutDir?: string;\n\tcontext?: string;\n\twatch?: boolean;\n\tclean?: boolean;\n\tvalidate?: boolean;\n\tformat?: boolean;\n}\n\n/** Everything a build needs, from the config file and the flags. */\ninterface BuildSetup {\n\toptions: CompileTemplateOptions;\n\tloaded: LoadedConfig;\n\toutDir: string;\n\tmoduleFile: string | false;\n\ttemplateFile: string | false;\n\tclean: boolean;\n\tcontext: unknown;\n\t/** Whether the rendered template is validated. */\n\tvalidate: boolean;\n\t/** Whether the rendered template is formatted, only when it is validated. */\n\tformat: boolean;\n\t/** Absolute path of the public directory, undefined when nothing is copied. */\n\tpublicDir?: string;\n}\n\nconst log = {\n\tsuccess: (message: string) =>\n\t\tconsole.log(`${styleText('green', '✓')} ${message}`),\n\tinfo: (message: string) =>\n\t\tconsole.log(`${styleText('cyan', '●')} ${message}`),\n\terror: (error: unknown) =>\n\t\tconsole.error(`${styleText('red', '✗')} ${formatError(error)}`),\n};\n\nconst cli = cac('blogger-hbs');\n\ncli\n\t.command('[root]', 'Compile the template and render it')\n\t.alias('build')\n\t.option('-c, --config <file>', 'Path of the config file')\n\t.option('--entry <file>', 'Entry template, relative to the root')\n\t.option('-o, --out-dir <dir>', 'Output directory, relative to the root')\n\t.option('--context <file>', 'JSON file with the context to render with')\n\t.option('-w, --watch', 'Build again when a file changes')\n\t.option('--no-clean', 'Keep the files in the output directory')\n\t.option(\n\t\t'--no-validate',\n\t\t'Do not validate the rendered template as XML, nor format it (templates are always validated when compiling)',\n\t)\n\t.option('--no-format', 'Do not format the rendered template')\n\t.example('blogger-hbs')\n\t.example('blogger-hbs --watch')\n\t.example('blogger-hbs ./theme --entry src/index.hbs --context context.json')\n\t.action(async (rootArg: unknown, rawFlags: BuildFlags) => {\n\t\t// cac turns values that look like numbers into numbers, `-o 2024`\n\t\tconst text = (value: unknown) =>\n\t\t\tvalue === undefined ? undefined : String(value);\n\t\tconst root = text(rootArg);\n\t\tconst flags: BuildFlags = {\n\t\t\t...rawFlags,\n\t\t\tconfig: text(rawFlags.config),\n\t\t\tentry: text(rawFlags.entry),\n\t\t\toutDir: text(rawFlags.outDir),\n\t\t\tcontext: text(rawFlags.context),\n\t\t};\n\n\t\tif (flags.watch) {\n\t\t\tawait watchBuild(root, flags);\n\t\t} else {\n\t\t\tawait build(root, flags);\n\t\t}\n\t});\n\ncli.help();\ncli.version(version);\n\ntry {\n\tcli.parse(process.argv, { run: false });\n\tawait cli.runMatchedCommand();\n} catch (error) {\n\tlog.error(describeCliError(error));\n\tprocess.exitCode = 1;\n}\n\n/** Adds a suggestion and a pointer to `--help` to the errors of the arguments. */\nfunction describeCliError(error: unknown): unknown {\n\tif (!(error instanceof Error) || error.name !== 'CACError') {\n\t\treturn error;\n\t}\n\n\tconst flags = [...cli.commands, cli.globalCommand].flatMap((command) =>\n\t\tcommand.options.flatMap(\n\t\t\t(option) => option.rawName.match(/--[\\w-]+/g) ?? [],\n\t\t),\n\t);\n\tconst unknown = /^Unknown option `(.+)`$/.exec(error.message)?.[1];\n\tconst hint = unknown ? didYouMean(unknown, flags, '`') : '';\n\n\treturn new Error(\n\t\t`${error.message}${hint}\\n Run 'blogger-hbs --help' to see the available options`,\n\t);\n}\n\nasync function setup(\n\troot: string | undefined,\n\tflags: BuildFlags,\n): Promise<BuildSetup> {\n\tconst cwd = process.cwd();\n\tconst rootFlag = root === undefined ? undefined : path.resolve(cwd, root);\n\n\t// the config is looked for in the root, which has to exist for that\n\tif (rootFlag !== undefined && !(await isDirectory(rootFlag))) {\n\t\tthrow new CompileError(\n\t\t\t(await exists(rootFlag))\n\t\t\t\t? `Root '${root}' is not a directory`\n\t\t\t\t: `Root directory '${root}' does not exist`,\n\t\t);\n\t}\n\n\tconst loaded = await loadConfig({\n\t\tcwd: rootFlag ?? cwd,\n\t\tconfigFile: flags.config && path.resolve(cwd, flags.config),\n\t\twatch: flags.watch,\n\t});\n\tconst { config } = loaded;\n\n\tconst resolvedRoot = rootFlag ?? config.root ?? cwd;\n\tconst output = config.output ?? {};\n\n\t// the config has more than the compile options\n\tconst options: CompileTemplateOptions = {};\n\tfor (const key of COMPILE_OPTION_KEYS) {\n\t\tif (key in config) {\n\t\t\tObject.assign(options, { [key]: config[key as keyof typeof config] });\n\t\t}\n\t}\n\toptions.root = resolvedRoot;\n\toptions.entry = flags.entry ?? config.entry;\n\n\tassertCompileOptions(options);\n\tawait assertDirectories(options);\n\n\tconst outDir = path.resolve(\n\t\tresolvedRoot,\n\t\tflags.outDir ?? output.dir ?? 'dist',\n\t);\n\tconst clean = flags.clean === false ? false : (output.clean ?? true);\n\tif (clean) {\n\t\tassertCleanable(outDir, options);\n\t}\n\n\treturn {\n\t\toptions,\n\t\tloaded,\n\t\toutDir,\n\t\tmoduleFile: output.module ?? 'compiled.js',\n\t\ttemplateFile: output.template ?? 'theme.xml',\n\t\tvalidate: flags.validate === false ? false : (output.validate ?? true),\n\t\tformat: flags.format === false ? false : (output.format ?? true),\n\t\tclean,\n\t\tcontext: flags.context\n\t\t\t? await readContext(path.resolve(cwd, flags.context), flags.context)\n\t\t\t: (config.context ?? {}),\n\t\tpublicDir: await resolvePublicDir(resolvedRoot, config.publicDir, outDir),\n\t};\n}\n\n/** The public directory, `public` when it exists unless set otherwise. */\nasync function resolvePublicDir(\n\troot: string,\n\tvalue: string | false | undefined,\n\toutDir: string,\n): Promise<string | undefined> {\n\tif (value === false) {\n\t\treturn;\n\t}\n\n\tconst directory = path.resolve(root, value ?? 'public');\n\tconst shown = path.relative(process.cwd(), directory) || '.';\n\n\tif (!(await isDirectory(directory))) {\n\t\tif (value === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tthrow new CompileError(\n\t\t\t(await exists(directory))\n\t\t\t\t? `Public directory '${shown}' is not a directory`\n\t\t\t\t: `Public directory '${shown}' does not exist, set 'publicDir: false' to not copy anything`,\n\t\t);\n\t}\n\n\tif (isInside(outDir, directory) || isInside(directory, outDir)) {\n\t\tthrow new CompileError(\n\t\t\t`Public directory '${shown}' and output directory '${path.relative(process.cwd(), outDir) || '.'}' overlap, they must be separate`,\n\t\t);\n\t}\n\n\treturn directory;\n}\n\n/** Copies the public directory into the output directory. */\nasync function copyPublic({\n\tpublicDir,\n\toutDir,\n\tmoduleFile,\n\ttemplateFile,\n}: BuildSetup): Promise<void> {\n\tif (publicDir === undefined) {\n\t\treturn;\n\t}\n\n\tfor (const name of await fs.readdir(publicDir)) {\n\t\tif (name === moduleFile || name === templateFile) {\n\t\t\tthrow new CompileError(\n\t\t\t\t`'${name}' in the public directory has the same name as the ${name === moduleFile ? 'compiled module' : 'rendered template'} and would be overwritten, rename it or set 'output.${name === moduleFile ? 'module' : 'template'}'`,\n\t\t\t);\n\t\t}\n\t}\n\n\tawait fs.cp(publicDir, outDir, { recursive: true, force: true });\n}\n\n/**\n * Cleaning removes everything in the output directory, which must not hold the\n * project: the root, the working directory, the templates or an alias target.\n */\nfunction assertCleanable(outDir: string, options: CompileTemplateOptions) {\n\tconst { root, entry } = resolvePaths(options);\n\tconst shown = path.relative(process.cwd(), outDir) || '.';\n\tconst kept: [string, string][] = [\n\t\t[root, 'the root'],\n\t\t[process.cwd(), 'the working directory'],\n\t\t[path.dirname(entry), 'the entry template'],\n\t\t...Object.entries(options.alias ?? {}).map(\n\t\t\t([name, target]): [string, string] => [\n\t\t\t\tpath.resolve(root, target),\n\t\t\t\t`the target of the alias '${name}'`,\n\t\t\t],\n\t\t),\n\t];\n\n\tfor (const [directory, what] of kept) {\n\t\tif (isInside(directory, outDir)) {\n\t\t\tthrow new CompileError(\n\t\t\t\t`Refusing to clean the output directory '${shown}', it contains ${what}. Use another output directory, or --no-clean / 'output.clean: false' to keep its files`,\n\t\t\t);\n\t\t}\n\t}\n}\n\nasync function readContext(file: string, shown: string): Promise<unknown> {\n\tlet text: string;\n\ttry {\n\t\ttext = await fs.readFile(file, 'utf-8');\n\t} catch (cause) {\n\t\tif ((cause as NodeJS.ErrnoException).code === 'ENOENT') {\n\t\t\tthrow new CompileError(`Context file '${shown}' not found`);\n\t\t}\n\t\tthrow new CompileError(`Failed to read the context file '${shown}'`, {\n\t\t\tcause,\n\t\t});\n\t}\n\n\ttry {\n\t\treturn JSON.parse(text);\n\t} catch (cause) {\n\t\tthrow new CompileError(\n\t\t\t`Context file '${shown}' is not valid JSON: ${(cause as Error).message}`,\n\t\t);\n\t}\n}\n\n/** Whether `child` is `parent` or inside of it. */\nfunction isInside(child: string, parent: string): boolean {\n\tconst relative = path.relative(parent, child);\n\treturn (\n\t\trelative === '' ||\n\t\t(!relative.startsWith('..') && !path.isAbsolute(relative))\n\t);\n}\n\nasync function exists(file: string): Promise<boolean> {\n\treturn fs.stat(file).then(\n\t\t() => true,\n\t\t() => false,\n\t);\n}\n\nasync function isDirectory(file: string): Promise<boolean> {\n\treturn fs.stat(file).then(\n\t\t(stats) => stats.isDirectory(),\n\t\t() => false,\n\t);\n}\n\nasync function build(root: string | undefined, flags: BuildFlags) {\n\tconst current = await setup(root, flags);\n\t// nothing is written when there is nothing to build, the watcher waits\n\t// for the entry instead\n\tawait assertEntry(current.options);\n\tawait prepareOutDir(current);\n\tawait copyPublic(current);\n\n\tconst start = performance.now();\n\tconst compiled = await compileTemplate(current.options);\n\tlog.success(`Compiled in ${elapsed(start)}`);\n\n\tawait write(current, compiled);\n}\n\n/** Watches the template and restarts when the config file changes. */\nasync function watchBuild(root: string | undefined, flags: BuildFlags) {\n\tlet current = await setup(root, flags);\n\tawait prepareOutDir(current);\n\n\tlet watcher: TemplateWatcher | undefined;\n\tlet watchers: FSWatcher[] = [];\n\tlet restarting = false;\n\n\tconst start = async () => {\n\t\tlet begin = performance.now();\n\t\tlet first = true;\n\n\t\ttry {\n\t\t\tawait copyPublic(current);\n\t\t} catch (error) {\n\t\t\tlog.error(error);\n\t\t}\n\n\t\twatcher = await watchTemplate({\n\t\t\t...current.options,\n\t\t\tonStart() {\n\t\t\t\tbegin = performance.now();\n\t\t\t},\n\t\t\tasync onCompile(compiled) {\n\t\t\t\tlog.success(`${first ? 'Compiled' : 'Rebuilt'} in ${elapsed(begin)}`);\n\t\t\t\tfirst = false;\n\t\t\t\tawait write(current, compiled);\n\t\t\t},\n\t\t\tonError: log.error,\n\t\t});\n\n\t\tfor (const file of current.loaded.dependencies) {\n\t\t\ttry {\n\t\t\t\tconst configWatcher = watch(file, () => {\n\t\t\t\t\t// saving fires more than once, a restart is enough\n\t\t\t\t\tif (!restarting) {\n\t\t\t\t\t\trestarting = true;\n\t\t\t\t\t\trestart().finally(() => {\n\t\t\t\t\t\t\trestarting = false;\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tconfigWatcher.on('error', () => configWatcher.close());\n\t\t\t\twatchers.push(configWatcher);\n\t\t\t} catch {\n\t\t\t\t// a dependency that can not be watched does not restart\n\t\t\t}\n\t\t}\n\n\t\tif (current.publicDir !== undefined) {\n\t\t\twatchers.push(watchPublic(current));\n\t\t}\n\t};\n\n\tconst stopWatchers = () => {\n\t\tfor (const fileWatcher of watchers) {\n\t\t\tfileWatcher.close();\n\t\t}\n\t\twatchers = [];\n\t};\n\n\tconst restart = async () => {\n\t\tlog.info(\n\t\t\t`Config changed, restarting ${styleText('dim', path.relative(process.cwd(), current.loaded.file ?? ''))}`,\n\t\t);\n\t\tstopWatchers();\n\t\tawait watcher?.close();\n\t\ttry {\n\t\t\tcurrent = await setup(root, flags);\n\t\t} catch (error) {\n\t\t\tlog.error(error);\n\t\t}\n\t\tawait start();\n\t};\n\n\tawait start();\n\tlog.info(\n\t\t`Watching for changes ${styleText('dim', '(press Ctrl+C to stop)')}`,\n\t);\n\n\tconst stop = async () => {\n\t\tstopWatchers();\n\t\tawait watcher?.close();\n\t\tprocess.exit(0);\n\t};\n\tprocess.once('SIGINT', stop);\n\tprocess.once('SIGTERM', stop);\n}\n\n/** Copies the public directory again when something in it changes. */\nfunction watchPublic(current: BuildSetup): FSWatcher {\n\tconst publicDir = current.publicDir as string;\n\tconst changed = new Set<string>();\n\tlet timer: NodeJS.Timeout | undefined;\n\n\tconst sync = async () => {\n\t\tconst names = [...changed];\n\t\tchanged.clear();\n\t\ttry {\n\t\t\t// what was deleted from the public directory goes from the output too\n\t\t\tfor (const name of names) {\n\t\t\t\tif (!(await exists(path.join(publicDir, name)))) {\n\t\t\t\t\tawait fs.rm(path.join(current.outDir, name), {\n\t\t\t\t\t\trecursive: true,\n\t\t\t\t\t\tforce: true,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\tawait copyPublic(current);\n\t\t\tlog.success(\n\t\t\t\t`Copied ${styleText('dim', path.relative(process.cwd(), publicDir))}`,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tlog.error(error);\n\t\t}\n\t};\n\n\tconst publicWatcher = watch(\n\t\tpublicDir,\n\t\t{ recursive: true },\n\t\t(_event, name) => {\n\t\t\tif (name) {\n\t\t\t\tchanged.add(name);\n\t\t\t}\n\t\t\tclearTimeout(timer);\n\t\t\ttimer = setTimeout(sync, 50);\n\t\t},\n\t);\n\tpublicWatcher.on('error', () => publicWatcher.close());\n\tpublicWatcher.on('close', () => clearTimeout(timer));\n\treturn publicWatcher;\n}\n\nasync function prepareOutDir({ outDir, clean }: BuildSetup) {\n\tif (clean) {\n\t\tawait fs.rm(outDir, { recursive: true, force: true });\n\t}\n\tawait fs.mkdir(outDir, { recursive: true });\n}\n\n/**\n * Writes the compiled module and the rendered template, validated and\n * formatted after rendering when enabled.\n */\nasync function write(current: BuildSetup, compiled: CompiledTemplate) {\n\tconst { outDir, moduleFile, templateFile, context, loaded } = current;\n\n\tif (moduleFile) {\n\t\tawait fs.writeFile(path.join(outDir, moduleFile), compiled.source);\n\t}\n\tif (!templateFile) {\n\t\treturn;\n\t}\n\n\tconst file = path.join(outDir, templateFile);\n\tconst shown = path.relative(process.cwd(), file);\n\n\tlet start = performance.now();\n\tconst rendered = renderTemplate({\n\t\t...compiled,\n\t\tcontext,\n\t\tplugins: loaded.config.runtimePlugins,\n\t});\n\tlog.success(`Rendered in ${elapsed(start)}`);\n\n\tstart = performance.now();\n\tlet output: string;\n\ttry {\n\t\toutput = finalizeXml(current, rendered);\n\t} catch (error) {\n\t\t// written anyway, to look at where it is invalid\n\t\tawait fs.writeFile(file, rendered);\n\t\tthrow new CompileError(\n\t\t\t`The rendered template ${shown} is not valid XML: ${(error as Error).message}`,\n\t\t);\n\t}\n\tif (current.validate) {\n\t\tlog.success(\n\t\t\t`${current.format ? 'Formatted' : 'Validated'} in ${elapsed(start)}`,\n\t\t);\n\t}\n\n\tawait fs.writeFile(file, output);\n\tlog.success(`Written ${styleText('dim', shown)}`);\n}\n\n/** Validates and formats the rendered template, as enabled. */\nfunction finalizeXml({ validate, format }: BuildSetup, xml: string): string {\n\tif (!validate) {\n\t\treturn xml;\n\t}\n\tif (!format) {\n\t\tvalidateXml(xml);\n\t\treturn xml;\n\t}\n\treturn formatXml(xml);\n}\n\nfunction elapsed(start: number): string {\n\treturn styleText('yellow', `${Math.round(performance.now() - start)}ms`);\n}\n\n/** The message of the error and of everything that caused it. */\nfunction formatError(error: unknown): string {\n\tconst lines: string[] = [];\n\tlet current: unknown = error;\n\n\twhile (current !== undefined) {\n\t\tlines.push(\n\t\t\tcurrent instanceof Error\n\t\t\t\t? `${lines.length === 0 ? '' : 'Caused by: '}${current.message}`\n\t\t\t\t: String(current),\n\t\t);\n\t\tcurrent = current instanceof Error ? current.cause : undefined;\n\t}\n\n\treturn lines.join('\\n ');\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["import { type FSWatcher, watch } from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport { createRequire } from 'node:module';\nimport * as path from 'node:path';\nimport { styleText } from 'node:util';\nimport { cac } from 'cac';\nimport {\n\ttype CompiledTemplate,\n\tCompileError,\n\ttype CompileTemplateOptions,\n\tcompileTemplate,\n\ttype TemplateWatcher,\n\twatchTemplate,\n} from '../compiler';\nimport {\n\tassertCompileOptions,\n\tassertDirectories,\n\tassertEntry,\n\tCOMPILE_OPTION_KEYS,\n\tresolvePaths,\n} from '../compiler/options';\nimport { type LoadedConfig, loadConfig } from '../config';\nimport { renderTemplate } from '../runtime';\nimport { didYouMean } from '../utils/suggest';\nimport { formatXml, validateXml } from '../xml';\n\nconst { version } = createRequire(import.meta.url)('../../package.json') as {\n\tversion: string;\n};\n\ninterface BuildFlags {\n\tconfig?: string;\n\tentry?: string;\n\toutDir?: string;\n\tcontext?: string;\n\twatch?: boolean;\n\tclean?: boolean;\n\tvalidate?: boolean;\n\tformat?: boolean;\n}\n\n/** Everything a build needs, from the config file and the flags. */\ninterface BuildSetup {\n\toptions: CompileTemplateOptions;\n\tloaded: LoadedConfig;\n\toutDir: string;\n\tmoduleFile: string | false;\n\ttemplateFile: string | false;\n\tclean: boolean;\n\tcontext: unknown;\n\t/** Whether the rendered template is validated. */\n\tvalidate: boolean;\n\t/** Whether the rendered template is formatted, only when it is validated. */\n\tformat: boolean;\n\t/** Absolute path of the public directory, undefined when nothing is copied. */\n\tpublicDir?: string;\n}\n\nconst log = {\n\tsuccess: (message: string) =>\n\t\tconsole.log(`${styleText('green', '✓')} ${message}`),\n\tinfo: (message: string) =>\n\t\tconsole.log(`${styleText('cyan', '●')} ${message}`),\n\terror: (error: unknown) =>\n\t\tconsole.error(`${styleText('red', '✗')} ${formatError(error)}`),\n};\n\nconst cli = cac('blogger-hbs');\n\ncli\n\t.command('[root]', 'Compile the template and render it')\n\t.alias('build')\n\t.option('-c, --config <file>', 'Path of the config file')\n\t.option('--entry <file>', 'Entry template, relative to the root')\n\t.option('-o, --out-dir <dir>', 'Output directory, relative to the root')\n\t.option('--context <file>', 'JSON file with the context to render with')\n\t.option('-w, --watch', 'Build again when a file changes')\n\t.option('--no-clean', 'Keep the files in the output directory')\n\t.option(\n\t\t'--no-validate',\n\t\t'Do not validate the rendered template as XML, nor format it (templates are always validated when compiling)',\n\t)\n\t.option('--no-format', 'Do not format the rendered template')\n\t.example('blogger-hbs')\n\t.example('blogger-hbs --watch')\n\t.example('blogger-hbs ./theme --entry src/index.hbs --context context.json')\n\t.action(async (rootArg: unknown, rawFlags: BuildFlags) => {\n\t\t// cac turns values that look like numbers into numbers, `-o 2024`\n\t\tconst text = (value: unknown) =>\n\t\t\tvalue === undefined ? undefined : String(value);\n\t\tconst root = text(rootArg);\n\t\tconst flags: BuildFlags = {\n\t\t\t...rawFlags,\n\t\t\tconfig: text(rawFlags.config),\n\t\t\tentry: text(rawFlags.entry),\n\t\t\toutDir: text(rawFlags.outDir),\n\t\t\tcontext: text(rawFlags.context),\n\t\t};\n\n\t\tif (flags.watch) {\n\t\t\tawait watchBuild(root, flags);\n\t\t} else {\n\t\t\tawait build(root, flags);\n\t\t}\n\t});\n\ncli.help();\ncli.version(version);\n\ntry {\n\tcli.parse(process.argv, { run: false });\n\tawait cli.runMatchedCommand();\n} catch (error) {\n\tlog.error(describeCliError(error));\n\tprocess.exitCode = 1;\n}\n\n/** Adds a suggestion and a pointer to `--help` to the errors of the arguments. */\nfunction describeCliError(error: unknown): unknown {\n\tif (!(error instanceof Error) || error.name !== 'CACError') {\n\t\treturn error;\n\t}\n\n\tconst flags = [...cli.commands, cli.globalCommand].flatMap((command) =>\n\t\tcommand.options.flatMap(\n\t\t\t(option) => option.rawName.match(/--[\\w-]+/g) ?? [],\n\t\t),\n\t);\n\tconst unknown = /^Unknown option `(.+)`$/.exec(error.message)?.[1];\n\tconst hint = unknown ? didYouMean(unknown, flags, '`') : '';\n\n\treturn new Error(\n\t\t`${error.message}${hint}\\n Run 'blogger-hbs --help' to see the available options`,\n\t);\n}\n\nasync function setup(\n\troot: string | undefined,\n\tflags: BuildFlags,\n): Promise<BuildSetup> {\n\tconst cwd = process.cwd();\n\tconst rootFlag = root === undefined ? undefined : path.resolve(cwd, root);\n\n\t// the config is looked for in the root, which has to exist for that\n\tif (rootFlag !== undefined && !(await isDirectory(rootFlag))) {\n\t\tthrow new CompileError(\n\t\t\t(await exists(rootFlag))\n\t\t\t\t? `Root '${root}' is not a directory`\n\t\t\t\t: `Root directory '${root}' does not exist`,\n\t\t);\n\t}\n\n\tconst loaded = await loadConfig({\n\t\tcwd: rootFlag ?? cwd,\n\t\tconfigFile: flags.config && path.resolve(cwd, flags.config),\n\t\twatch: flags.watch,\n\t});\n\tconst { config } = loaded;\n\n\tconst resolvedRoot = rootFlag ?? config.root ?? cwd;\n\tconst output = config.output ?? {};\n\n\t// the config has more than the compile options\n\tconst options: CompileTemplateOptions = {};\n\tfor (const key of COMPILE_OPTION_KEYS) {\n\t\tif (key in config) {\n\t\t\tObject.assign(options, { [key]: config[key as keyof typeof config] });\n\t\t}\n\t}\n\toptions.root = resolvedRoot;\n\toptions.entry = flags.entry ?? config.entry;\n\n\tassertCompileOptions(options);\n\tawait assertDirectories(options);\n\n\tconst outDir = path.resolve(\n\t\tresolvedRoot,\n\t\tflags.outDir ?? output.dir ?? 'dist',\n\t);\n\tconst clean = flags.clean === false ? false : (output.clean ?? true);\n\tif (clean) {\n\t\tassertCleanable(outDir, options);\n\t}\n\n\treturn {\n\t\toptions,\n\t\tloaded,\n\t\toutDir,\n\t\tmoduleFile: output.module ?? 'compiled.js',\n\t\ttemplateFile: output.template ?? 'theme.xml',\n\t\tvalidate: flags.validate === false ? false : (output.validate ?? true),\n\t\tformat: flags.format === false ? false : (output.format ?? true),\n\t\tclean,\n\t\tcontext: flags.context\n\t\t\t? await readContext(path.resolve(cwd, flags.context), flags.context)\n\t\t\t: (config.context ?? {}),\n\t\tpublicDir: await resolvePublicDir(resolvedRoot, config.publicDir, outDir),\n\t};\n}\n\n/** The public directory, `public` when it exists unless set otherwise. */\nasync function resolvePublicDir(\n\troot: string,\n\tvalue: string | false | undefined,\n\toutDir: string,\n): Promise<string | undefined> {\n\tif (value === false) {\n\t\treturn;\n\t}\n\n\tconst directory = path.resolve(root, value ?? 'public');\n\tconst shown = path.relative(process.cwd(), directory) || '.';\n\n\tif (!(await isDirectory(directory))) {\n\t\tif (value === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tthrow new CompileError(\n\t\t\t(await exists(directory))\n\t\t\t\t? `Public directory '${shown}' is not a directory`\n\t\t\t\t: `Public directory '${shown}' does not exist, set 'publicDir: false' to not copy anything`,\n\t\t);\n\t}\n\n\tif (isInside(outDir, directory) || isInside(directory, outDir)) {\n\t\tthrow new CompileError(\n\t\t\t`Public directory '${shown}' and output directory '${path.relative(process.cwd(), outDir) || '.'}' overlap, they must be separate`,\n\t\t);\n\t}\n\n\treturn directory;\n}\n\n/** Copies the public directory into the output directory. */\nasync function copyPublic({\n\tpublicDir,\n\toutDir,\n\tmoduleFile,\n\ttemplateFile,\n}: BuildSetup): Promise<void> {\n\tif (publicDir === undefined) {\n\t\treturn;\n\t}\n\n\tfor (const name of await fs.readdir(publicDir)) {\n\t\tif (name === moduleFile || name === templateFile) {\n\t\t\tthrow new CompileError(\n\t\t\t\t`'${name}' in the public directory has the same name as the ${name === moduleFile ? 'compiled module' : 'rendered template'} and would be overwritten, rename it or set 'output.${name === moduleFile ? 'module' : 'template'}'`,\n\t\t\t);\n\t\t}\n\t}\n\n\tawait fs.cp(publicDir, outDir, { recursive: true, force: true });\n}\n\n/**\n * Cleaning removes everything in the output directory, which must not hold the\n * project: the root, the working directory, the templates or an alias target.\n */\nfunction assertCleanable(outDir: string, options: CompileTemplateOptions) {\n\tconst { root, entry } = resolvePaths(options);\n\tconst shown = path.relative(process.cwd(), outDir) || '.';\n\tconst kept: [string, string][] = [\n\t\t[root, 'the root'],\n\t\t[process.cwd(), 'the working directory'],\n\t\t[path.dirname(entry), 'the entry template'],\n\t\t...Object.entries(options.alias ?? {}).map(\n\t\t\t([name, target]): [string, string] => [\n\t\t\t\tpath.resolve(root, target),\n\t\t\t\t`the target of the alias '${name}'`,\n\t\t\t],\n\t\t),\n\t];\n\n\tfor (const [directory, what] of kept) {\n\t\tif (isInside(directory, outDir)) {\n\t\t\tthrow new CompileError(\n\t\t\t\t`Refusing to clean the output directory '${shown}', it contains ${what}. Use another output directory, or --no-clean / 'output.clean: false' to keep its files`,\n\t\t\t);\n\t\t}\n\t}\n}\n\nasync function readContext(file: string, shown: string): Promise<unknown> {\n\tlet text: string;\n\ttry {\n\t\ttext = await fs.readFile(file, 'utf-8');\n\t} catch (cause) {\n\t\tif ((cause as NodeJS.ErrnoException).code === 'ENOENT') {\n\t\t\tthrow new CompileError(`Context file '${shown}' not found`);\n\t\t}\n\t\tthrow new CompileError(`Failed to read the context file '${shown}'`, {\n\t\t\tcause,\n\t\t});\n\t}\n\n\ttry {\n\t\treturn JSON.parse(text);\n\t} catch (cause) {\n\t\tthrow new CompileError(\n\t\t\t`Context file '${shown}' is not valid JSON: ${(cause as Error).message}`,\n\t\t);\n\t}\n}\n\n/** Whether `child` is `parent` or inside of it. */\nfunction isInside(child: string, parent: string): boolean {\n\tconst relative = path.relative(parent, child);\n\treturn (\n\t\trelative === '' ||\n\t\t(!relative.startsWith('..') && !path.isAbsolute(relative))\n\t);\n}\n\nasync function exists(file: string): Promise<boolean> {\n\treturn fs.stat(file).then(\n\t\t() => true,\n\t\t() => false,\n\t);\n}\n\nasync function isDirectory(file: string): Promise<boolean> {\n\treturn fs.stat(file).then(\n\t\t(stats) => stats.isDirectory(),\n\t\t() => false,\n\t);\n}\n\nasync function build(root: string | undefined, flags: BuildFlags) {\n\tconst current = await setup(root, flags);\n\t// nothing is written when there is nothing to build, the watcher waits\n\t// for the entry instead\n\tawait assertEntry(current.options);\n\tawait prepareOutDir(current);\n\tawait copyPublic(current);\n\n\tconst start = performance.now();\n\tconst compiled = await compileTemplate(current.options);\n\tlog.success(`Compiled in ${elapsed(start)}`);\n\n\tawait write(current, compiled);\n}\n\n/** Watches the template and restarts when the config file changes. */\nasync function watchBuild(root: string | undefined, flags: BuildFlags) {\n\tlet current = await setup(root, flags);\n\tawait prepareOutDir(current);\n\n\tlet watcher: TemplateWatcher | undefined;\n\tlet watchers: FSWatcher[] = [];\n\tlet restarting = false;\n\n\tconst start = async () => {\n\t\tlet begin = performance.now();\n\t\tlet first = true;\n\n\t\ttry {\n\t\t\tawait copyPublic(current);\n\t\t} catch (error) {\n\t\t\tlog.error(error);\n\t\t}\n\n\t\twatcher = await watchTemplate({\n\t\t\t...current.options,\n\t\t\tonStart() {\n\t\t\t\tbegin = performance.now();\n\t\t\t},\n\t\t\tasync onCompile(compiled) {\n\t\t\t\tlog.success(`${first ? 'Compiled' : 'Rebuilt'} in ${elapsed(begin)}`);\n\t\t\t\tfirst = false;\n\t\t\t\tawait write(current, compiled);\n\t\t\t},\n\t\t\tonError: log.error,\n\t\t});\n\n\t\tfor (const file of current.loaded.dependencies) {\n\t\t\ttry {\n\t\t\t\tconst configWatcher = watch(file, () => {\n\t\t\t\t\t// saving fires more than once, a restart is enough\n\t\t\t\t\tif (!restarting) {\n\t\t\t\t\t\trestarting = true;\n\t\t\t\t\t\trestart().finally(() => {\n\t\t\t\t\t\t\trestarting = false;\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tconfigWatcher.on('error', () => configWatcher.close());\n\t\t\t\twatchers.push(configWatcher);\n\t\t\t} catch {\n\t\t\t\t// a dependency that can not be watched does not restart\n\t\t\t}\n\t\t}\n\n\t\tif (current.publicDir !== undefined) {\n\t\t\twatchers.push(watchPublic(current));\n\t\t}\n\t};\n\n\tconst stopWatchers = () => {\n\t\tfor (const fileWatcher of watchers) {\n\t\t\tfileWatcher.close();\n\t\t}\n\t\twatchers = [];\n\t};\n\n\tconst restart = async () => {\n\t\tlog.info(\n\t\t\t`Config changed, restarting ${styleText('dim', path.relative(process.cwd(), current.loaded.file ?? ''))}`,\n\t\t);\n\t\tstopWatchers();\n\t\tawait watcher?.close();\n\t\ttry {\n\t\t\tcurrent = await setup(root, flags);\n\t\t} catch (error) {\n\t\t\tlog.error(error);\n\t\t}\n\t\tawait start();\n\t};\n\n\tawait start();\n\tlog.info(\n\t\t`Watching for changes ${styleText('dim', '(press Ctrl+C to stop)')}`,\n\t);\n\n\tconst stop = async () => {\n\t\tstopWatchers();\n\t\tawait watcher?.close();\n\t\t// the process ends once nothing is left running: `process.exit` would skip\n\t\t// the cleanup of rspack's native bindings, which then warn about leaks,\n\t\t// so it is only a fallback for something left running, a plugin's timer\n\t\tsetTimeout(() => process.exit(0), 3000).unref();\n\t};\n\tprocess.once('SIGINT', stop);\n\tprocess.once('SIGTERM', stop);\n}\n\n/** Copies the public directory again when something in it changes. */\nfunction watchPublic(current: BuildSetup): FSWatcher {\n\tconst publicDir = current.publicDir as string;\n\tconst changed = new Set<string>();\n\tlet timer: NodeJS.Timeout | undefined;\n\n\tconst sync = async () => {\n\t\tconst names = [...changed];\n\t\tchanged.clear();\n\t\ttry {\n\t\t\t// what was deleted from the public directory goes from the output too\n\t\t\tfor (const name of names) {\n\t\t\t\tif (!(await exists(path.join(publicDir, name)))) {\n\t\t\t\t\tawait fs.rm(path.join(current.outDir, name), {\n\t\t\t\t\t\trecursive: true,\n\t\t\t\t\t\tforce: true,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\tawait copyPublic(current);\n\t\t\tlog.success(\n\t\t\t\t`Copied ${styleText('dim', path.relative(process.cwd(), publicDir))}`,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tlog.error(error);\n\t\t}\n\t};\n\n\tconst publicWatcher = watch(\n\t\tpublicDir,\n\t\t{ recursive: true },\n\t\t(_event, name) => {\n\t\t\tif (name) {\n\t\t\t\tchanged.add(name);\n\t\t\t}\n\t\t\tclearTimeout(timer);\n\t\t\ttimer = setTimeout(sync, 50);\n\t\t},\n\t);\n\tpublicWatcher.on('error', () => publicWatcher.close());\n\tpublicWatcher.on('close', () => clearTimeout(timer));\n\treturn publicWatcher;\n}\n\nasync function prepareOutDir({ outDir, clean }: BuildSetup) {\n\tif (clean) {\n\t\tawait fs.rm(outDir, { recursive: true, force: true });\n\t}\n\tawait fs.mkdir(outDir, { recursive: true });\n}\n\n/**\n * Writes the compiled module and the rendered template, validated and\n * formatted after rendering when enabled.\n */\nasync function write(current: BuildSetup, compiled: CompiledTemplate) {\n\tconst { outDir, moduleFile, templateFile, context, loaded } = current;\n\n\tif (moduleFile) {\n\t\tawait fs.writeFile(path.join(outDir, moduleFile), compiled.source);\n\t}\n\tif (!templateFile) {\n\t\treturn;\n\t}\n\n\tconst file = path.join(outDir, templateFile);\n\tconst shown = path.relative(process.cwd(), file);\n\n\tlet start = performance.now();\n\tconst rendered = renderTemplate({\n\t\t...compiled,\n\t\tcontext,\n\t\tplugins: loaded.config.runtimePlugins,\n\t});\n\tlog.success(`Rendered in ${elapsed(start)}`);\n\n\tstart = performance.now();\n\tlet output: string;\n\ttry {\n\t\toutput = finalizeXml(current, rendered);\n\t} catch (error) {\n\t\t// written anyway, to look at where it is invalid\n\t\tawait fs.writeFile(file, rendered);\n\t\tthrow new CompileError(\n\t\t\t`The rendered template ${shown} is not valid XML: ${(error as Error).message}`,\n\t\t);\n\t}\n\tif (current.validate) {\n\t\tlog.success(\n\t\t\t`${current.format ? 'Formatted' : 'Validated'} in ${elapsed(start)}`,\n\t\t);\n\t}\n\n\tawait fs.writeFile(file, output);\n\tlog.success(`Written ${styleText('dim', shown)}`);\n}\n\n/** Validates and formats the rendered template, as enabled. */\nfunction finalizeXml({ validate, format }: BuildSetup, xml: string): string {\n\tif (!validate) {\n\t\treturn xml;\n\t}\n\tif (!format) {\n\t\tvalidateXml(xml);\n\t\treturn xml;\n\t}\n\treturn formatXml(xml);\n}\n\nfunction elapsed(start: number): string {\n\treturn styleText('yellow', `${Math.round(performance.now() - start)}ms`);\n}\n\n/** The message of the error and of everything that caused it. */\nfunction formatError(error: unknown): string {\n\tconst lines: string[] = [];\n\tlet current: unknown = error;\n\n\twhile (current !== undefined) {\n\t\tlines.push(\n\t\t\tcurrent instanceof Error\n\t\t\t\t? `${lines.length === 0 ? '' : 'Caused by: '}${current.message}`\n\t\t\t\t: String(current),\n\t\t);\n\t\tcurrent = current instanceof Error ? current.cause : undefined;\n\t}\n\n\treturn lines.join('\\n ');\n}\n"],"mappings":"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"}
@@ -13,6 +13,10 @@ import { Program } from "./ast/types.js";
13
13
  * `{{#import}}`, `{{#> partial}}` and `{{#*inline}}` blocks is rendered
14
14
  * elsewhere and checked on its own.
15
15
  *
16
+ * Blogger expressions (`b:if cond`, `expr:*`, ...) are parsed with
17
+ * `blogger-expr`, unless their value has Handlebars in it, which is only known
18
+ * once rendered.
19
+ *
16
20
  * `label` names the template in errors, a path or the name of a partial.
17
21
  */
18
22
  export declare function validateTemplateXml(program: Program, label: string): void;
@@ -1,5 +1,7 @@
1
1
  import { CompileError } from "./errors.js";
2
2
  import { XmlScanner, XmlSyntaxError, createLocate } from "../xml/scanner.js";
3
+ import { parseExpressions } from "../xml/expressions.js";
4
+ import "../xml/index.js";
3
5
  //#region src/compiler/validate.ts
4
6
  /** Helpers rendering one of their branches once, which can each open a tag. */
5
7
  const CONDITIONALS = /* @__PURE__ */ new Set(["if", "unless"]);
@@ -16,6 +18,10 @@ const CONDITIONALS = /* @__PURE__ */ new Set(["if", "unless"]);
16
18
  * `{{#import}}`, `{{#> partial}}` and `{{#*inline}}` blocks is rendered
17
19
  * elsewhere and checked on its own.
18
20
  *
21
+ * Blogger expressions (`b:if cond`, `expr:*`, ...) are parsed with
22
+ * `blogger-expr`, unless their value has Handlebars in it, which is only known
23
+ * once rendered.
24
+ *
19
25
  * `label` names the template in errors, a path or the name of a partial.
20
26
  */
21
27
  function validateTemplateXml(program, label) {
@@ -30,7 +36,7 @@ function validateTemplateXml(program, label) {
30
36
  }
31
37
  }
32
38
  function validateFragment(program) {
33
- const scanner = new XmlScanner();
39
+ const scanner = new XmlScanner({ onStartTag: parseExpressions });
34
40
  visitProgram(program, scanner);
35
41
  scanner.finish(endOf(program));
36
42
  }
@@ -91,6 +97,7 @@ function visitBlock(block, scanner) {
91
97
  }
92
98
  } else for (const [end, part] of [[program, "content"], [inverse, "'{{else}}' content"]]) if (end.signature() !== start.signature()) throw new XmlSyntaxError(`The ${part} of the ${label} has to keep the XML as it is, but it ${end.describeChange(start)}`, startOf(block));
93
99
  scanner.assign(program);
100
+ scanner.dynamicValue();
94
101
  }
95
102
  function locateContent(content) {
96
103
  const offset = Math.max(0, content.original.indexOf(content.value));
@@ -1 +1 @@
1
- {"version":3,"file":"validate.js","names":[],"sources":["../../src/compiler/validate.ts"],"sourcesContent":["import {\n\tcreateLocate,\n\ttype Locate,\n\ttype Position,\n\tXmlScanner,\n\tXmlSyntaxError,\n} from '../xml';\nimport type {\n\tBlockStatement,\n\tContentStatement,\n\tNode,\n\tProgram,\n\tStatement,\n} from './ast';\nimport { CompileError } from './errors';\n\n/** Helpers rendering one of their branches once, which can each open a tag. */\nconst CONDITIONALS = new Set(['if', 'unless']);\n\n/**\n * Checks that the XML of a template is well-formed, taking its Handlebars\n * syntax into account. The template is a fragment: anything can be at its\n * top, but every tag it opens has to be closed.\n *\n * Expressions are accepted where their output can be valid XML: in text, in\n * quoted attribute values and between attributes. The content of a block has\n * to leave the XML as it found it, except for the branches of `{{#if}}` and\n * `{{#unless}}`, which can each open the same tag, so\n * `{{#if a}}<div class='a'>{{else}}<div>{{/if}}` is fine. The content of\n * `{{#import}}`, `{{#> partial}}` and `{{#*inline}}` blocks is rendered\n * elsewhere and checked on its own.\n *\n * `label` names the template in errors, a path or the name of a partial.\n */\nexport function validateTemplateXml(program: Program, label: string): void {\n\ttry {\n\t\tvalidateFragment(program);\n\t} catch (error) {\n\t\tif (error instanceof XmlSyntaxError) {\n\t\t\tconst { line, column } = error.position;\n\t\t\tthrow new CompileError(\n\t\t\t\t`Invalid XML in ${label}:${line}:${column + 1}, ${lowerFirst(error.message)}`,\n\t\t\t);\n\t\t}\n\t\tthrow error;\n\t}\n}\n\nfunction validateFragment(program: Program): void {\n\tconst scanner = new XmlScanner();\n\tvisitProgram(program, scanner);\n\tscanner.finish(endOf(program));\n}\n\nfunction visitProgram(program: Program, scanner: XmlScanner): void {\n\tfor (const statement of program.body) {\n\t\tvisitStatement(statement, scanner);\n\t}\n}\n\nfunction visitStatement(statement: Statement, scanner: XmlScanner): void {\n\t// decorators are not in the typings\n\tconst type = statement.type as\n\t\t| Statement['type']\n\t\t| 'Decorator'\n\t\t| 'DecoratorBlock';\n\n\tswitch (type) {\n\t\tcase 'ContentStatement': {\n\t\t\tconst content = statement as ContentStatement;\n\t\t\tscanner.feed(content.value, locateContent(content));\n\t\t\treturn;\n\t\t}\n\n\t\tcase 'CommentStatement':\n\t\tcase 'Decorator':\n\t\t\treturn;\n\n\t\tcase 'DecoratorBlock':\n\t\t\t// `{{#*inline \"name\"}}` defines a partial, it renders nothing here\n\t\t\tvalidateFragment((statement as BlockStatement).program);\n\t\t\treturn;\n\n\t\tcase 'MustacheStatement':\n\t\tcase 'PartialStatement':\n\t\t\tscanner.unknown(startOf(statement), describe(statement));\n\t\t\treturn;\n\n\t\tcase 'PartialBlockStatement':\n\t\t\tif (statement.type === 'PartialBlockStatement') {\n\t\t\t\tvalidateFragment(statement.program);\n\t\t\t}\n\t\t\tscanner.unknown(startOf(statement), describe(statement));\n\t\t\treturn;\n\n\t\tcase 'BlockStatement':\n\t\t\tvisitBlock(statement as BlockStatement, scanner);\n\t\t\treturn;\n\t}\n}\n\nfunction visitBlock(block: BlockStatement, scanner: XmlScanner): void {\n\tconst name = block.path.original;\n\n\t// rendered by the imported template, in place of `{{> @partial-block}}`\n\tif (name === 'import') {\n\t\tvalidateFragment(block.program);\n\t\tscanner.unknown(startOf(block), describe(block));\n\t\treturn;\n\t}\n\n\tconst start = scanner.clone();\n\tconst program = start.clone();\n\tvisitProgram(block.program, program);\n\n\tconst inverse = start.clone();\n\tif (block.inverse) {\n\t\tvisitProgram(block.inverse, inverse);\n\t}\n\n\tconst label = `'{{#${name}}}' block`;\n\n\tif (CONDITIONALS.has(name)) {\n\t\t// one of the branches renders, so they only have to end the same way\n\t\tif (program.signature() !== inverse.signature()) {\n\t\t\tconst [changed, part, other] =\n\t\t\t\tprogram.signature() !== start.signature()\n\t\t\t\t\t? [program, 'content', \"its '{{else}}' content\"]\n\t\t\t\t\t: [inverse, \"'{{else}}' content\", 'its content'];\n\t\t\tconst fix = block.inverse\n\t\t\t\t? `${other} has to do the same`\n\t\t\t\t: `close it inside the block, or add an '{{else}}' doing the same`;\n\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t`The ${part} of the ${label} ${changed.describeChange(start)}, ${fix}`,\n\t\t\t\tstartOf(block),\n\t\t\t);\n\t\t}\n\t} else {\n\t\t// a block can render its content any number of times\n\t\tfor (const [end, part] of [\n\t\t\t[program, 'content'],\n\t\t\t[inverse, \"'{{else}}' content\"],\n\t\t] as const) {\n\t\t\tif (end.signature() !== start.signature()) {\n\t\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t\t`The ${part} of the ${label} has to keep the XML as it is, but it ${end.describeChange(start)}`,\n\t\t\t\t\tstartOf(block),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\tscanner.assign(program);\n}\n\nfunction locateContent(content: ContentStatement): Locate {\n\t// stripped whitespace is left out of the value, the location is of the original\n\tconst offset = Math.max(0, content.original.indexOf(content.value));\n\tconst locate = createLocate(content.original, startOf(content));\n\treturn (index) => locate(offset + index);\n}\n\nfunction startOf(node: Node): Position {\n\tconst start = node.loc?.start;\n\treturn start\n\t\t? { line: start.line, column: start.column }\n\t\t: { line: 1, column: 0 };\n}\n\nfunction endOf(node: Node): Position {\n\tconst end = node.loc?.end;\n\treturn end ? { line: end.line, column: end.column } : { line: 1, column: 0 };\n}\n\nfunction describe(statement: Statement): string {\n\tswitch (statement.type) {\n\t\tcase 'MustacheStatement':\n\t\t\treturn `'{{${expression(statement.path)}}}'`;\n\t\tcase 'PartialStatement':\n\t\t\treturn `'{{> ${expression(statement.name)}}}'`;\n\t\tcase 'PartialBlockStatement':\n\t\t\treturn `'{{#> ${expression(statement.name)}}}'`;\n\t\tcase 'BlockStatement':\n\t\t\treturn `'{{#${statement.path.original}}}'`;\n\t\tdefault:\n\t\t\treturn 'The expression';\n\t}\n}\n\nfunction expression(node: Node): string {\n\tif (node.type === 'PathExpression') {\n\t\treturn node.original;\n\t}\n\tif (node.type === 'SubExpression') {\n\t\treturn `(${node.path.original} ...)`;\n\t}\n\treturn 'value' in node ? String(node.value) : node.type;\n}\n\nfunction lowerFirst(text: string): string {\n\treturn /^[A-Z][a-z]/.test(text)\n\t\t? text[0].toLowerCase() + text.slice(1)\n\t\t: text;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"validate.js","names":[],"sources":["../../src/compiler/validate.ts"],"sourcesContent":["import {\n\tcreateLocate,\n\ttype Locate,\n\ttype Position,\n\tparseExpressions,\n\tXmlScanner,\n\tXmlSyntaxError,\n} from '../xml';\nimport type {\n\tBlockStatement,\n\tContentStatement,\n\tNode,\n\tProgram,\n\tStatement,\n} from './ast';\nimport { CompileError } from './errors';\n\n/** Helpers rendering one of their branches once, which can each open a tag. */\nconst CONDITIONALS = new Set(['if', 'unless']);\n\n/**\n * Checks that the XML of a template is well-formed, taking its Handlebars\n * syntax into account. The template is a fragment: anything can be at its\n * top, but every tag it opens has to be closed.\n *\n * Expressions are accepted where their output can be valid XML: in text, in\n * quoted attribute values and between attributes. The content of a block has\n * to leave the XML as it found it, except for the branches of `{{#if}}` and\n * `{{#unless}}`, which can each open the same tag, so\n * `{{#if a}}<div class='a'>{{else}}<div>{{/if}}` is fine. The content of\n * `{{#import}}`, `{{#> partial}}` and `{{#*inline}}` blocks is rendered\n * elsewhere and checked on its own.\n *\n * Blogger expressions (`b:if cond`, `expr:*`, ...) are parsed with\n * `blogger-expr`, unless their value has Handlebars in it, which is only known\n * once rendered.\n *\n * `label` names the template in errors, a path or the name of a partial.\n */\nexport function validateTemplateXml(program: Program, label: string): void {\n\ttry {\n\t\tvalidateFragment(program);\n\t} catch (error) {\n\t\tif (error instanceof XmlSyntaxError) {\n\t\t\tconst { line, column } = error.position;\n\t\t\tthrow new CompileError(\n\t\t\t\t`Invalid XML in ${label}:${line}:${column + 1}, ${lowerFirst(error.message)}`,\n\t\t\t);\n\t\t}\n\t\tthrow error;\n\t}\n}\n\nfunction validateFragment(program: Program): void {\n\tconst scanner = new XmlScanner({\n\t\tonStartTag: parseExpressions,\n\t});\n\tvisitProgram(program, scanner);\n\tscanner.finish(endOf(program));\n}\n\nfunction visitProgram(program: Program, scanner: XmlScanner): void {\n\tfor (const statement of program.body) {\n\t\tvisitStatement(statement, scanner);\n\t}\n}\n\nfunction visitStatement(statement: Statement, scanner: XmlScanner): void {\n\t// decorators are not in the typings\n\tconst type = statement.type as\n\t\t| Statement['type']\n\t\t| 'Decorator'\n\t\t| 'DecoratorBlock';\n\n\tswitch (type) {\n\t\tcase 'ContentStatement': {\n\t\t\tconst content = statement as ContentStatement;\n\t\t\tscanner.feed(content.value, locateContent(content));\n\t\t\treturn;\n\t\t}\n\n\t\tcase 'CommentStatement':\n\t\tcase 'Decorator':\n\t\t\treturn;\n\n\t\tcase 'DecoratorBlock':\n\t\t\t// `{{#*inline \"name\"}}` defines a partial, it renders nothing here\n\t\t\tvalidateFragment((statement as BlockStatement).program);\n\t\t\treturn;\n\n\t\tcase 'MustacheStatement':\n\t\tcase 'PartialStatement':\n\t\t\tscanner.unknown(startOf(statement), describe(statement));\n\t\t\treturn;\n\n\t\tcase 'PartialBlockStatement':\n\t\t\tif (statement.type === 'PartialBlockStatement') {\n\t\t\t\tvalidateFragment(statement.program);\n\t\t\t}\n\t\t\tscanner.unknown(startOf(statement), describe(statement));\n\t\t\treturn;\n\n\t\tcase 'BlockStatement':\n\t\t\tvisitBlock(statement as BlockStatement, scanner);\n\t\t\treturn;\n\t}\n}\n\nfunction visitBlock(block: BlockStatement, scanner: XmlScanner): void {\n\tconst name = block.path.original;\n\n\t// rendered by the imported template, in place of `{{> @partial-block}}`\n\tif (name === 'import') {\n\t\tvalidateFragment(block.program);\n\t\tscanner.unknown(startOf(block), describe(block));\n\t\treturn;\n\t}\n\n\tconst start = scanner.clone();\n\tconst program = start.clone();\n\tvisitProgram(block.program, program);\n\n\tconst inverse = start.clone();\n\tif (block.inverse) {\n\t\tvisitProgram(block.inverse, inverse);\n\t}\n\n\tconst label = `'{{#${name}}}' block`;\n\n\tif (CONDITIONALS.has(name)) {\n\t\t// one of the branches renders, so they only have to end the same way\n\t\tif (program.signature() !== inverse.signature()) {\n\t\t\tconst [changed, part, other] =\n\t\t\t\tprogram.signature() !== start.signature()\n\t\t\t\t\t? [program, 'content', \"its '{{else}}' content\"]\n\t\t\t\t\t: [inverse, \"'{{else}}' content\", 'its content'];\n\t\t\tconst fix = block.inverse\n\t\t\t\t? `${other} has to do the same`\n\t\t\t\t: `close it inside the block, or add an '{{else}}' doing the same`;\n\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t`The ${part} of the ${label} ${changed.describeChange(start)}, ${fix}`,\n\t\t\t\tstartOf(block),\n\t\t\t);\n\t\t}\n\t} else {\n\t\t// a block can render its content any number of times\n\t\tfor (const [end, part] of [\n\t\t\t[program, 'content'],\n\t\t\t[inverse, \"'{{else}}' content\"],\n\t\t] as const) {\n\t\t\tif (end.signature() !== start.signature()) {\n\t\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t\t`The ${part} of the ${label} has to keep the XML as it is, but it ${end.describeChange(start)}`,\n\t\t\t\t\tstartOf(block),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\tscanner.assign(program);\n\t// a block in an attribute value renders what is in it, or not\n\tscanner.dynamicValue();\n}\n\nfunction locateContent(content: ContentStatement): Locate {\n\t// stripped whitespace is left out of the value, the location is of the original\n\tconst offset = Math.max(0, content.original.indexOf(content.value));\n\tconst locate = createLocate(content.original, startOf(content));\n\treturn (index) => locate(offset + index);\n}\n\nfunction startOf(node: Node): Position {\n\tconst start = node.loc?.start;\n\treturn start\n\t\t? { line: start.line, column: start.column }\n\t\t: { line: 1, column: 0 };\n}\n\nfunction endOf(node: Node): Position {\n\tconst end = node.loc?.end;\n\treturn end ? { line: end.line, column: end.column } : { line: 1, column: 0 };\n}\n\nfunction describe(statement: Statement): string {\n\tswitch (statement.type) {\n\t\tcase 'MustacheStatement':\n\t\t\treturn `'{{${expression(statement.path)}}}'`;\n\t\tcase 'PartialStatement':\n\t\t\treturn `'{{> ${expression(statement.name)}}}'`;\n\t\tcase 'PartialBlockStatement':\n\t\t\treturn `'{{#> ${expression(statement.name)}}}'`;\n\t\tcase 'BlockStatement':\n\t\t\treturn `'{{#${statement.path.original}}}'`;\n\t\tdefault:\n\t\t\treturn 'The expression';\n\t}\n}\n\nfunction expression(node: Node): string {\n\tif (node.type === 'PathExpression') {\n\t\treturn node.original;\n\t}\n\tif (node.type === 'SubExpression') {\n\t\treturn `(${node.path.original} ...)`;\n\t}\n\treturn 'value' in node ? String(node.value) : node.type;\n}\n\nfunction lowerFirst(text: string): string {\n\treturn /^[A-Z][a-z]/.test(text)\n\t\t? text[0].toLowerCase() + text.slice(1)\n\t\t: text;\n}\n"],"mappings":"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"}
package/dist/config.d.ts CHANGED
@@ -17,7 +17,8 @@ export interface OutputConfig {
17
17
  clean?: boolean;
18
18
  /**
19
19
  * Whether the rendered template is validated as a Blogger theme: well-formed
20
- * XML with the XML declaration and the doctype on the first two lines.
20
+ * XML with the XML declaration and the doctype on the first two lines, and
21
+ * expressions (`b:if cond`, `expr:*`, ...) that parse with `blogger-expr`.
21
22
  * `true` by default, formatting needs it. The templates and the partials of
22
23
  * plugins are validated when compiling either way.
23
24
  */
@@ -25,8 +26,10 @@ export interface OutputConfig {
25
26
  /**
26
27
  * Whether the rendered template is formatted as Blogger saves it: the
27
28
  * declarations added or moved to the top, start tags on a single line, line
28
- * breaks in attribute values collapsed and the attributes sorted. `true` by
29
- * default, nothing is formatted without validation.
29
+ * breaks in attribute values collapsed, the attributes sorted and quoted
30
+ * with single quotes, and the
31
+ * expressions printed by `blogger-expr` on a single line. `true` by default,
32
+ * nothing is formatted without validation.
30
33
  */
31
34
  format?: boolean;
32
35
  }
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","names":["loadRsbuildConfig"],"sources":["../src/config.ts"],"sourcesContent":["import * as path from 'node:path';\nimport { loadConfig as loadRsbuildConfig } from '@rsbuild/core';\nimport { CompileError, type CompileTemplateOptions } from './compiler';\nimport { assertCompileOptions, invalidOption } from './compiler/options';\nimport type { RuntimePlugin } from './plugins';\nimport { didYouMean, isPlainObject } from './utils/suggest';\n\nconst CONFIG_KEYS = ['context', 'runtimePlugins', 'output', 'publicDir'];\nconst OUTPUT_KEYS = [\n\t'dir',\n\t'module',\n\t'template',\n\t'clean',\n\t'validate',\n\t'format',\n];\n\nexport const CONFIG_FILE_NAMES = [\n\t'blogger-hbs.config.ts',\n\t'blogger-hbs.config.mts',\n\t'blogger-hbs.config.js',\n\t'blogger-hbs.config.mjs',\n];\n\nexport interface OutputConfig {\n\t/** Directory of the output, relative to the root, `dist` by default. */\n\tdir?: string;\n\t/**\n\t * File name of the compiled module (the ES module consumed by\n\t * `renderTemplate`), `compiled.js` by default, `false` to not write it.\n\t */\n\tmodule?: string | false;\n\t/** File name of the rendered template, `theme.xml` by default, `false` to not render it. */\n\ttemplate?: string | false;\n\t/** Whether the directory is emptied before the first build, `true` by default. */\n\tclean?: boolean;\n\t/**\n\t * Whether the rendered template is validated as a Blogger theme: well-formed\n\t * XML with the XML declaration and the doctype on the first two lines.\n\t * `true` by default, formatting needs it. The templates and the partials of\n\t * plugins are validated when compiling either way.\n\t */\n\tvalidate?: boolean;\n\t/**\n\t * Whether the rendered template is formatted as Blogger saves it: the\n\t * declarations added or moved to the top, start tags on a single line, line\n\t * breaks in attribute values collapsed and the attributes sorted. `true` by\n\t * default, nothing is formatted without validation.\n\t */\n\tformat?: boolean;\n}\n\nexport interface UserConfig extends CompileTemplateOptions {\n\t/** The context the template is rendered with. */\n\tcontext?: unknown;\n\t/** Plugins used when rendering. */\n\truntimePlugins?: RuntimePlugin[];\n\toutput?: OutputConfig;\n\t/**\n\t * Directory whose files are copied as they are into the output directory,\n\t * relative to the root. `public` by default, when it exists, `false` to not\n\t * copy anything.\n\t */\n\tpublicDir?: string | false;\n}\n\nexport interface ConfigEnv {\n\t/** `NODE_ENV` */\n\tenv: string;\n\t/** The command of the CLI, `build`. */\n\tcommand: string;\n\tenvMode: string;\n\tmeta?: {\n\t\t/** Whether the CLI watches for changes. */\n\t\twatch?: boolean;\n\t};\n}\n\nexport type UserConfigExport =\n\t| UserConfig\n\t| ((env: ConfigEnv) => UserConfig | Promise<UserConfig>);\n\n/** Gives type checking and completions to a config file. */\nexport function defineConfig<T extends UserConfigExport>(config: T): T {\n\treturn config;\n}\n\nexport interface LoadedConfig {\n\tconfig: UserConfig;\n\t/** Path of the config file, undefined when there is none. */\n\tfile?: string;\n\t/** The config file and the files it imports. */\n\tdependencies: string[];\n}\n\n/** Throws unless `config` is a valid config. */\nexport function assertUserConfig(\n\tconfig: unknown,\n): asserts config is UserConfig {\n\tassertCompileOptions(config, CONFIG_KEYS);\n\tconst { runtimePlugins, output, publicDir } = config as UserConfig;\n\n\tif (\n\t\tpublicDir !== undefined &&\n\t\tpublicDir !== false &&\n\t\t(typeof publicDir !== 'string' || publicDir === '')\n\t) {\n\t\tthrow invalidOption('publicDir', 'a path or false', publicDir);\n\t}\n\n\tif (runtimePlugins !== undefined) {\n\t\tif (!Array.isArray(runtimePlugins)) {\n\t\t\tthrow invalidOption(\n\t\t\t\t'runtimePlugins',\n\t\t\t\t'an array of runtime plugins',\n\t\t\t\truntimePlugins,\n\t\t\t);\n\t\t}\n\t\truntimePlugins.forEach((plugin: unknown, i) => {\n\t\t\tif (typeof plugin !== 'object' || plugin === null) {\n\t\t\t\tthrow invalidOption(`runtimePlugins[${i}]`, 'a runtime plugin', plugin);\n\t\t\t}\n\t\t\tconst { helpers } = plugin as RuntimePlugin;\n\t\t\tif (helpers !== undefined && !isPlainObject(helpers)) {\n\t\t\t\tthrow invalidOption(\n\t\t\t\t\t`runtimePlugins[${i}].helpers`,\n\t\t\t\t\t'an object of helpers by name',\n\t\t\t\t\thelpers,\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\t}\n\n\tif (output === undefined) {\n\t\treturn;\n\t}\n\tif (!isPlainObject(output)) {\n\t\tthrow invalidOption('output', 'an object', output);\n\t}\n\tfor (const key of Object.keys(output)) {\n\t\tif (!OUTPUT_KEYS.includes(key)) {\n\t\t\tconst known = OUTPUT_KEYS.map((name) => `output.${name}`);\n\t\t\tconst hint = didYouMean(`output.${key}`, known);\n\t\t\tthrow new CompileError(\n\t\t\t\t`Unknown option 'output.${key}'${hint}${hint ? ' Valid' : ', valid'} options: ${known.join(', ')}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst { dir, module, template, clean, validate, format } = output;\n\tif (dir !== undefined && (typeof dir !== 'string' || dir === '')) {\n\t\tthrow invalidOption('output.dir', 'a path', dir);\n\t}\n\tfor (const [name, value] of [\n\t\t['module', module],\n\t\t['template', template],\n\t] as const) {\n\t\tif (\n\t\t\tvalue !== undefined &&\n\t\t\tvalue !== false &&\n\t\t\t(typeof value !== 'string' || value === '')\n\t\t) {\n\t\t\tthrow invalidOption(`output.${name}`, 'a file name or false', value);\n\t\t}\n\t}\n\tif (\n\t\t(module ?? 'compiled.js') !== false &&\n\t\t(module ?? 'compiled.js') === (template ?? 'theme.xml')\n\t) {\n\t\tthrow new CompileError(\n\t\t\t`'output.module' and 'output.template' are both '${module ?? template}', they would overwrite each other`,\n\t\t);\n\t}\n\tfor (const [name, value] of [\n\t\t['clean', clean],\n\t\t['validate', validate],\n\t\t['format', format],\n\t] as const) {\n\t\tif (value !== undefined && typeof value !== 'boolean') {\n\t\t\tthrow invalidOption(`output.${name}`, 'a boolean', value);\n\t\t}\n\t}\n}\n\n/**\n * Loads `blogger-hbs.config.*` from `cwd`, or the file at `configFile`. A\n * relative `root` in it is resolved from the directory of the file.\n */\nexport async function loadConfig({\n\tcwd = process.cwd(),\n\tconfigFile,\n\tcommand = 'build',\n\twatch = false,\n}: {\n\tcwd?: string;\n\tconfigFile?: string;\n\tcommand?: string;\n\twatch?: boolean;\n} = {}): Promise<LoadedConfig> {\n\tconst { content, filePath, dependencies } =\n\t\tawait loadRsbuildConfig<UserConfig>({\n\t\t\tcwd,\n\t\t\tpath: configFile,\n\t\t\tconfigFileNames: CONFIG_FILE_NAMES,\n\t\t\tcommand,\n\t\t\tmeta: { watch },\n\t\t});\n\n\tif (!filePath) {\n\t\treturn { config: {}, dependencies: [] };\n\t}\n\n\t// rsbuild records where its own config came from, which is not needed here\n\tconst { _privateMeta, ...config } = content as UserConfig & {\n\t\t_privateMeta?: unknown;\n\t};\n\n\ttry {\n\t\tassertUserConfig(config);\n\t} catch (error) {\n\t\tthrow new CompileError(\n\t\t\t`Invalid config '${path.relative(cwd, filePath) || filePath}': ${error instanceof Error ? error.message : String(error)}`,\n\t\t);\n\t}\n\n\treturn {\n\t\tconfig: {\n\t\t\t...config,\n\t\t\troot: path.resolve(path.dirname(filePath), config.root ?? '.'),\n\t\t},\n\t\tfile: filePath,\n\t\t// the loader does not always list the config file itself\n\t\tdependencies: [...new Set([filePath, ...(dependencies ?? [])])],\n\t};\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"config.js","names":["loadRsbuildConfig"],"sources":["../src/config.ts"],"sourcesContent":["import * as path from 'node:path';\nimport { loadConfig as loadRsbuildConfig } from '@rsbuild/core';\nimport { CompileError, type CompileTemplateOptions } from './compiler';\nimport { assertCompileOptions, invalidOption } from './compiler/options';\nimport type { RuntimePlugin } from './plugins';\nimport { didYouMean, isPlainObject } from './utils/suggest';\n\nconst CONFIG_KEYS = ['context', 'runtimePlugins', 'output', 'publicDir'];\nconst OUTPUT_KEYS = [\n\t'dir',\n\t'module',\n\t'template',\n\t'clean',\n\t'validate',\n\t'format',\n];\n\nexport const CONFIG_FILE_NAMES = [\n\t'blogger-hbs.config.ts',\n\t'blogger-hbs.config.mts',\n\t'blogger-hbs.config.js',\n\t'blogger-hbs.config.mjs',\n];\n\nexport interface OutputConfig {\n\t/** Directory of the output, relative to the root, `dist` by default. */\n\tdir?: string;\n\t/**\n\t * File name of the compiled module (the ES module consumed by\n\t * `renderTemplate`), `compiled.js` by default, `false` to not write it.\n\t */\n\tmodule?: string | false;\n\t/** File name of the rendered template, `theme.xml` by default, `false` to not render it. */\n\ttemplate?: string | false;\n\t/** Whether the directory is emptied before the first build, `true` by default. */\n\tclean?: boolean;\n\t/**\n\t * Whether the rendered template is validated as a Blogger theme: well-formed\n\t * XML with the XML declaration and the doctype on the first two lines, and\n\t * expressions (`b:if cond`, `expr:*`, ...) that parse with `blogger-expr`.\n\t * `true` by default, formatting needs it. The templates and the partials of\n\t * plugins are validated when compiling either way.\n\t */\n\tvalidate?: boolean;\n\t/**\n\t * Whether the rendered template is formatted as Blogger saves it: the\n\t * declarations added or moved to the top, start tags on a single line, line\n\t * breaks in attribute values collapsed, the attributes sorted and quoted\n\t * with single quotes, and the\n\t * expressions printed by `blogger-expr` on a single line. `true` by default,\n\t * nothing is formatted without validation.\n\t */\n\tformat?: boolean;\n}\n\nexport interface UserConfig extends CompileTemplateOptions {\n\t/** The context the template is rendered with. */\n\tcontext?: unknown;\n\t/** Plugins used when rendering. */\n\truntimePlugins?: RuntimePlugin[];\n\toutput?: OutputConfig;\n\t/**\n\t * Directory whose files are copied as they are into the output directory,\n\t * relative to the root. `public` by default, when it exists, `false` to not\n\t * copy anything.\n\t */\n\tpublicDir?: string | false;\n}\n\nexport interface ConfigEnv {\n\t/** `NODE_ENV` */\n\tenv: string;\n\t/** The command of the CLI, `build`. */\n\tcommand: string;\n\tenvMode: string;\n\tmeta?: {\n\t\t/** Whether the CLI watches for changes. */\n\t\twatch?: boolean;\n\t};\n}\n\nexport type UserConfigExport =\n\t| UserConfig\n\t| ((env: ConfigEnv) => UserConfig | Promise<UserConfig>);\n\n/** Gives type checking and completions to a config file. */\nexport function defineConfig<T extends UserConfigExport>(config: T): T {\n\treturn config;\n}\n\nexport interface LoadedConfig {\n\tconfig: UserConfig;\n\t/** Path of the config file, undefined when there is none. */\n\tfile?: string;\n\t/** The config file and the files it imports. */\n\tdependencies: string[];\n}\n\n/** Throws unless `config` is a valid config. */\nexport function assertUserConfig(\n\tconfig: unknown,\n): asserts config is UserConfig {\n\tassertCompileOptions(config, CONFIG_KEYS);\n\tconst { runtimePlugins, output, publicDir } = config as UserConfig;\n\n\tif (\n\t\tpublicDir !== undefined &&\n\t\tpublicDir !== false &&\n\t\t(typeof publicDir !== 'string' || publicDir === '')\n\t) {\n\t\tthrow invalidOption('publicDir', 'a path or false', publicDir);\n\t}\n\n\tif (runtimePlugins !== undefined) {\n\t\tif (!Array.isArray(runtimePlugins)) {\n\t\t\tthrow invalidOption(\n\t\t\t\t'runtimePlugins',\n\t\t\t\t'an array of runtime plugins',\n\t\t\t\truntimePlugins,\n\t\t\t);\n\t\t}\n\t\truntimePlugins.forEach((plugin: unknown, i) => {\n\t\t\tif (typeof plugin !== 'object' || plugin === null) {\n\t\t\t\tthrow invalidOption(`runtimePlugins[${i}]`, 'a runtime plugin', plugin);\n\t\t\t}\n\t\t\tconst { helpers } = plugin as RuntimePlugin;\n\t\t\tif (helpers !== undefined && !isPlainObject(helpers)) {\n\t\t\t\tthrow invalidOption(\n\t\t\t\t\t`runtimePlugins[${i}].helpers`,\n\t\t\t\t\t'an object of helpers by name',\n\t\t\t\t\thelpers,\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\t}\n\n\tif (output === undefined) {\n\t\treturn;\n\t}\n\tif (!isPlainObject(output)) {\n\t\tthrow invalidOption('output', 'an object', output);\n\t}\n\tfor (const key of Object.keys(output)) {\n\t\tif (!OUTPUT_KEYS.includes(key)) {\n\t\t\tconst known = OUTPUT_KEYS.map((name) => `output.${name}`);\n\t\t\tconst hint = didYouMean(`output.${key}`, known);\n\t\t\tthrow new CompileError(\n\t\t\t\t`Unknown option 'output.${key}'${hint}${hint ? ' Valid' : ', valid'} options: ${known.join(', ')}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst { dir, module, template, clean, validate, format } = output;\n\tif (dir !== undefined && (typeof dir !== 'string' || dir === '')) {\n\t\tthrow invalidOption('output.dir', 'a path', dir);\n\t}\n\tfor (const [name, value] of [\n\t\t['module', module],\n\t\t['template', template],\n\t] as const) {\n\t\tif (\n\t\t\tvalue !== undefined &&\n\t\t\tvalue !== false &&\n\t\t\t(typeof value !== 'string' || value === '')\n\t\t) {\n\t\t\tthrow invalidOption(`output.${name}`, 'a file name or false', value);\n\t\t}\n\t}\n\tif (\n\t\t(module ?? 'compiled.js') !== false &&\n\t\t(module ?? 'compiled.js') === (template ?? 'theme.xml')\n\t) {\n\t\tthrow new CompileError(\n\t\t\t`'output.module' and 'output.template' are both '${module ?? template}', they would overwrite each other`,\n\t\t);\n\t}\n\tfor (const [name, value] of [\n\t\t['clean', clean],\n\t\t['validate', validate],\n\t\t['format', format],\n\t] as const) {\n\t\tif (value !== undefined && typeof value !== 'boolean') {\n\t\t\tthrow invalidOption(`output.${name}`, 'a boolean', value);\n\t\t}\n\t}\n}\n\n/**\n * Loads `blogger-hbs.config.*` from `cwd`, or the file at `configFile`. A\n * relative `root` in it is resolved from the directory of the file.\n */\nexport async function loadConfig({\n\tcwd = process.cwd(),\n\tconfigFile,\n\tcommand = 'build',\n\twatch = false,\n}: {\n\tcwd?: string;\n\tconfigFile?: string;\n\tcommand?: string;\n\twatch?: boolean;\n} = {}): Promise<LoadedConfig> {\n\tconst { content, filePath, dependencies } =\n\t\tawait loadRsbuildConfig<UserConfig>({\n\t\t\tcwd,\n\t\t\tpath: configFile,\n\t\t\tconfigFileNames: CONFIG_FILE_NAMES,\n\t\t\tcommand,\n\t\t\tmeta: { watch },\n\t\t});\n\n\tif (!filePath) {\n\t\treturn { config: {}, dependencies: [] };\n\t}\n\n\t// rsbuild records where its own config came from, which is not needed here\n\tconst { _privateMeta, ...config } = content as UserConfig & {\n\t\t_privateMeta?: unknown;\n\t};\n\n\ttry {\n\t\tassertUserConfig(config);\n\t} catch (error) {\n\t\tthrow new CompileError(\n\t\t\t`Invalid config '${path.relative(cwd, filePath) || filePath}': ${error instanceof Error ? error.message : String(error)}`,\n\t\t);\n\t}\n\n\treturn {\n\t\tconfig: {\n\t\t\t...config,\n\t\t\troot: path.resolve(path.dirname(filePath), config.root ?? '.'),\n\t\t},\n\t\tfile: filePath,\n\t\t// the loader does not always list the config file itself\n\t\tdependencies: [...new Set([filePath, ...(dependencies ?? [])])],\n\t};\n}\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -10,4 +10,5 @@ import { HelperError } from "./runtime/helpers/error.js";
10
10
  import { RenderTemplateOptions, renderTemplate } from "./runtime/render.js";
11
11
  import "./runtime/index.js";
12
12
  import { XmlError, formatXml, validateXml } from "./xml/format.js";
13
+ import "./xml/index.js";
13
14
  export { CompileError, type CompilePlugin, type CompileTemplateOptions, type CompiledTemplate, type ConfigEnv, type DataContext, type DataResult, type FinalizeContext, HelperError, type OutputConfig, type PluginData, PluginError, type PluginPartials, type ProvideContext, type ProvideResult, type RenderTemplateOptions, type RsbuildOptions, type RuntimePlugin, type TemplateContext, type TemplateWatcher, type UserConfig, type UserConfigExport, type WatchTemplateOptions, XmlError, compileTemplate, defineConfig, formatXml, renderTemplate, validateXml, watchTemplate };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { CompileError } from "./compiler/errors.js";
2
2
  import { PluginError } from "./plugins/index.js";
3
3
  import { XmlError, formatXml, validateXml } from "./xml/format.js";
4
+ import "./xml/index.js";
4
5
  import { compileTemplate } from "./compiler/compile.js";
5
6
  import { watchTemplate } from "./compiler/watch.js";
6
7
  import "./compiler/index.js";
@@ -0,0 +1,28 @@
1
+ import { ScannedAttribute, ScannedStartTag } from "./scanner.js";
2
+ import { Node } from "blogger-expr";
3
+ //#region src/xml/expressions.d.ts
4
+ /** Whether the attribute `name` of `<tag>` holds an expression. */
5
+ export declare function isExpressionAttribute(tag: string, name: string): boolean;
6
+ /** A range of `b:loop values`, which `blogger-expr` does not parse. */
7
+ export interface LoopRange {
8
+ type: 'LoopRange';
9
+ from: number;
10
+ to: number;
11
+ }
12
+ /** What an expression attribute is parsed into. */
13
+ export type ParsedExpression = Node | LoopRange;
14
+ /**
15
+ * Parses the expressions in the attributes of `tag` with `blogger-expr`, by
16
+ * attribute. Dynamic attributes, with Handlebars in them, are left out, as
17
+ * are `b:include data` shorthands. The first one that does not parse throws
18
+ * an `XmlSyntaxError` where it is.
19
+ */
20
+ export declare function parseExpressions(tag: ScannedStartTag): Map<ScannedAttribute, ParsedExpression>;
21
+ /**
22
+ * A parsed expression printed by `blogger-expr` on a single line, escaped for
23
+ * single quotes, as formatted attributes have: `data:a&amp;&amp;data:b` becomes
24
+ * `data:a and data:b`. Undefined when it can not be printed.
25
+ */
26
+ export declare function formatExpression(expression: ParsedExpression): string | undefined;
27
+ //#endregion
28
+ //# sourceMappingURL=expressions.d.ts.map
@@ -0,0 +1,129 @@
1
+ import { XmlSyntaxError } from "./scanner.js";
2
+ import { LexError, ParseError, PrintError, parse, print } from "blogger-expr";
3
+ //#region src/xml/expressions.ts
4
+ /**
5
+ * Attributes holding an expression, by element, besides `expr:*` which do on
6
+ * any element.
7
+ */
8
+ const EXPRESSION_ATTRIBUTES = {
9
+ "b:if": ["cond"],
10
+ "b:elseif": ["cond"],
11
+ "b:loop": ["values"],
12
+ "b:with": ["value"],
13
+ "b:eval": ["expr"],
14
+ "b:switch": ["var"],
15
+ "b:include": ["cond", "data"],
16
+ "b:widget": ["cond"],
17
+ "b:section": ["cond"]
18
+ };
19
+ /** Whether the attribute `name` of `<tag>` holds an expression. */
20
+ function isExpressionAttribute(tag, name) {
21
+ return name.startsWith("expr:") || (EXPRESSION_ATTRIBUTES[tag]?.includes(name) ?? false);
22
+ }
23
+ /**
24
+ * `b:include data` also takes a bare name or path, `data='post'` for
25
+ * `data:post`, which is not an expression and is left as it is.
26
+ */
27
+ const INCLUDE_DATA_SHORTHAND = /^\s*[A-Za-z_][\w-]*(?:\.[\w-]+)*\s*$/;
28
+ /**
29
+ * `b:loop values` also takes a range, `values='1..20'`. Blogger only supports
30
+ * integer literals for its ends, not expressions, and no spaces.
31
+ */
32
+ const LOOP_RANGE = /^(-?\d+)\.\.(-?\d+)$/;
33
+ /** A range with spaces, rejected with its own message. */
34
+ const SPACED_LOOP_RANGE = /^\s*(-?\d+)\s*\.\.\s*(-?\d+)\s*$/;
35
+ /**
36
+ * Parses the expressions in the attributes of `tag` with `blogger-expr`, by
37
+ * attribute. Dynamic attributes, with Handlebars in them, are left out, as
38
+ * are `b:include data` shorthands. The first one that does not parse throws
39
+ * an `XmlSyntaxError` where it is.
40
+ */
41
+ function parseExpressions(tag) {
42
+ const parsed = /* @__PURE__ */ new Map();
43
+ for (const attribute of tag.attributes) {
44
+ if (attribute.dynamic || !isExpressionAttribute(tag.name, attribute.name)) continue;
45
+ const { text, offsets } = decodeValue(attribute);
46
+ if (tag.name === "b:include" && attribute.name === "data" && INCLUDE_DATA_SHORTHAND.test(text)) continue;
47
+ if (tag.name === "b:loop" && attribute.name === "values") {
48
+ const [, from, to] = LOOP_RANGE.exec(text) ?? [];
49
+ if (from !== void 0) {
50
+ parsed.set(attribute, {
51
+ type: "LoopRange",
52
+ from: Number(from),
53
+ to: Number(to)
54
+ });
55
+ continue;
56
+ }
57
+ const [, spacedFrom, spacedTo] = SPACED_LOOP_RANGE.exec(text) ?? [];
58
+ if (spacedFrom !== void 0) throw new XmlSyntaxError(`Invalid range in the attribute 'values' of <b:loop>, it can not have spaces: '${spacedFrom}..${spacedTo}'`, attribute.positionOf(0));
59
+ }
60
+ try {
61
+ parsed.set(attribute, parse(text));
62
+ } catch (error) {
63
+ if (!(error instanceof LexError || error instanceof ParseError)) throw error;
64
+ const message = error.message.replace(/ \(at position \d+\)$/, "");
65
+ throw new XmlSyntaxError(`Invalid expression in the attribute '${attribute.name}' of <${tag.name}>: ${message}`, attribute.positionOf(offsets[error.position] ?? attribute.value.length));
66
+ }
67
+ }
68
+ return parsed;
69
+ }
70
+ /**
71
+ * A parsed expression printed by `blogger-expr` on a single line, escaped for
72
+ * single quotes, as formatted attributes have: `data:a&amp;&amp;data:b` becomes
73
+ * `data:a and data:b`. Undefined when it can not be printed.
74
+ */
75
+ function formatExpression(expression) {
76
+ if (expression.type === "LoopRange") return `${expression.from}..${expression.to}`;
77
+ let printed;
78
+ try {
79
+ printed = print(expression);
80
+ } catch (error) {
81
+ if (error instanceof PrintError) return;
82
+ throw error;
83
+ }
84
+ return printed.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/'/g, "&apos;");
85
+ }
86
+ const ENTITIES = {
87
+ amp: "&",
88
+ lt: "<",
89
+ gt: ">",
90
+ quot: "\"",
91
+ apos: "'"
92
+ };
93
+ /**
94
+ * The value with its entities decoded, as Blogger parses it, and the offset
95
+ * in the written value of each character of it.
96
+ */
97
+ function decodeValue({ value }) {
98
+ let text = "";
99
+ const offsets = [];
100
+ for (let i = 0; i < value.length; i++) {
101
+ const entity = value[i] === "&" ? /^&(#x[0-9a-f]+|#[0-9]+|\w+);/i.exec(value.slice(i)) : null;
102
+ if (entity === null) {
103
+ text += value[i];
104
+ offsets.push(i);
105
+ continue;
106
+ }
107
+ const [whole, name] = entity;
108
+ const decoded = decodeEntity(name) ?? whole;
109
+ text += decoded;
110
+ for (let unit = 0; unit < decoded.length; unit++) offsets.push(i);
111
+ i += whole.length - 1;
112
+ }
113
+ offsets.push(value.length);
114
+ return {
115
+ text,
116
+ offsets
117
+ };
118
+ }
119
+ /** The character of `&name;`, undefined for an unknown name or code point. */
120
+ function decodeEntity(name) {
121
+ if (name[0] !== "#") return ENTITIES[name];
122
+ const hex = name[1].toLowerCase() === "x";
123
+ const code = Number.parseInt(name.slice(hex ? 2 : 1), hex ? 16 : 10);
124
+ return code <= 1114111 ? String.fromCodePoint(code) : void 0;
125
+ }
126
+ //#endregion
127
+ export { formatExpression, isExpressionAttribute, parseExpressions };
128
+
129
+ //# sourceMappingURL=expressions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expressions.js","names":[],"sources":["../../src/xml/expressions.ts"],"sourcesContent":["import {\n\tLexError,\n\ttype Node,\n\tParseError,\n\tPrintError,\n\tparse,\n\tprint,\n} from 'blogger-expr';\nimport {\n\ttype ScannedAttribute,\n\ttype ScannedStartTag,\n\tXmlSyntaxError,\n} from './scanner';\n\n/**\n * Attributes holding an expression, by element, besides `expr:*` which do on\n * any element.\n */\nconst EXPRESSION_ATTRIBUTES: Record<string, string[]> = {\n\t'b:if': ['cond'],\n\t'b:elseif': ['cond'],\n\t'b:loop': ['values'],\n\t'b:with': ['value'],\n\t'b:eval': ['expr'],\n\t'b:switch': ['var'],\n\t'b:include': ['cond', 'data'],\n\t'b:widget': ['cond'],\n\t'b:section': ['cond'],\n};\n\n/** Whether the attribute `name` of `<tag>` holds an expression. */\nexport function isExpressionAttribute(tag: string, name: string): boolean {\n\treturn (\n\t\tname.startsWith('expr:') ||\n\t\t(EXPRESSION_ATTRIBUTES[tag]?.includes(name) ?? false)\n\t);\n}\n\n/**\n * `b:include data` also takes a bare name or path, `data='post'` for\n * `data:post`, which is not an expression and is left as it is.\n */\nconst INCLUDE_DATA_SHORTHAND = /^\\s*[A-Za-z_][\\w-]*(?:\\.[\\w-]+)*\\s*$/;\n\n/**\n * `b:loop values` also takes a range, `values='1..20'`. Blogger only supports\n * integer literals for its ends, not expressions, and no spaces.\n */\nconst LOOP_RANGE = /^(-?\\d+)\\.\\.(-?\\d+)$/;\n/** A range with spaces, rejected with its own message. */\nconst SPACED_LOOP_RANGE = /^\\s*(-?\\d+)\\s*\\.\\.\\s*(-?\\d+)\\s*$/;\n\n/** A range of `b:loop values`, which `blogger-expr` does not parse. */\nexport interface LoopRange {\n\ttype: 'LoopRange';\n\tfrom: number;\n\tto: number;\n}\n\n/** What an expression attribute is parsed into. */\nexport type ParsedExpression = Node | LoopRange;\n\n/**\n * Parses the expressions in the attributes of `tag` with `blogger-expr`, by\n * attribute. Dynamic attributes, with Handlebars in them, are left out, as\n * are `b:include data` shorthands. The first one that does not parse throws\n * an `XmlSyntaxError` where it is.\n */\nexport function parseExpressions(\n\ttag: ScannedStartTag,\n): Map<ScannedAttribute, ParsedExpression> {\n\tconst parsed = new Map<ScannedAttribute, ParsedExpression>();\n\n\tfor (const attribute of tag.attributes) {\n\t\tif (attribute.dynamic || !isExpressionAttribute(tag.name, attribute.name)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { text, offsets } = decodeValue(attribute);\n\t\tif (\n\t\t\ttag.name === 'b:include' &&\n\t\t\tattribute.name === 'data' &&\n\t\t\tINCLUDE_DATA_SHORTHAND.test(text)\n\t\t) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (tag.name === 'b:loop' && attribute.name === 'values') {\n\t\t\tconst [, from, to] = LOOP_RANGE.exec(text) ?? [];\n\t\t\tif (from !== undefined) {\n\t\t\t\tparsed.set(attribute, {\n\t\t\t\t\ttype: 'LoopRange',\n\t\t\t\t\tfrom: Number(from),\n\t\t\t\t\tto: Number(to),\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst [, spacedFrom, spacedTo] = SPACED_LOOP_RANGE.exec(text) ?? [];\n\t\t\tif (spacedFrom !== undefined) {\n\t\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t\t`Invalid range in the attribute 'values' of <b:loop>, it can not have spaces: '${spacedFrom}..${spacedTo}'`,\n\t\t\t\t\tattribute.positionOf(0),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\tparsed.set(attribute, parse(text));\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof LexError || error instanceof ParseError)) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\t// the message of blogger-expr ends with the position in the decoded text\n\t\t\tconst message = error.message.replace(/ \\(at position \\d+\\)$/, '');\n\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t`Invalid expression in the attribute '${attribute.name}' of <${tag.name}>: ${message}`,\n\t\t\t\tattribute.positionOf(offsets[error.position] ?? attribute.value.length),\n\t\t\t);\n\t\t}\n\t}\n\n\treturn parsed;\n}\n\n/**\n * A parsed expression printed by `blogger-expr` on a single line, escaped for\n * single quotes, as formatted attributes have: `data:a&amp;&amp;data:b` becomes\n * `data:a and data:b`. Undefined when it can not be printed.\n */\nexport function formatExpression(\n\texpression: ParsedExpression,\n): string | undefined {\n\tif (expression.type === 'LoopRange') {\n\t\treturn `${expression.from}..${expression.to}`;\n\t}\n\n\tlet printed: string;\n\ttry {\n\t\tprinted = print(expression);\n\t} catch (error) {\n\t\tif (error instanceof PrintError) {\n\t\t\treturn undefined;\n\t\t}\n\t\tthrow error;\n\t}\n\n\treturn printed\n\t\t.replace(/&/g, '&amp;')\n\t\t.replace(/</g, '&lt;')\n\t\t.replace(/'/g, '&apos;');\n}\n\nconst ENTITIES: Record<string, string> = {\n\tamp: '&',\n\tlt: '<',\n\tgt: '>',\n\tquot: '\"',\n\tapos: \"'\",\n};\n\n/**\n * The value with its entities decoded, as Blogger parses it, and the offset\n * in the written value of each character of it.\n */\nfunction decodeValue({ value }: ScannedAttribute): {\n\ttext: string;\n\toffsets: number[];\n} {\n\tlet text = '';\n\tconst offsets: number[] = [];\n\n\tfor (let i = 0; i < value.length; i++) {\n\t\tconst entity =\n\t\t\tvalue[i] === '&'\n\t\t\t\t? /^&(#x[0-9a-f]+|#[0-9]+|\\w+);/i.exec(value.slice(i))\n\t\t\t\t: null;\n\t\tif (entity === null) {\n\t\t\ttext += value[i];\n\t\t\toffsets.push(i);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst [whole, name] = entity;\n\t\tconst decoded = decodeEntity(name) ?? whole;\n\t\ttext += decoded;\n\t\t// one per UTF-16 unit, as positions in `text` are\n\t\tfor (let unit = 0; unit < decoded.length; unit++) {\n\t\t\toffsets.push(i);\n\t\t}\n\t\ti += whole.length - 1;\n\t}\n\toffsets.push(value.length);\n\n\treturn { text, offsets };\n}\n\n/** The character of `&name;`, undefined for an unknown name or code point. */\nfunction decodeEntity(name: string): string | undefined {\n\tif (name[0] !== '#') {\n\t\treturn ENTITIES[name];\n\t}\n\tconst hex = name[1].toLowerCase() === 'x';\n\tconst code = Number.parseInt(name.slice(hex ? 2 : 1), hex ? 16 : 10);\n\treturn code <= 0x10ffff ? String.fromCodePoint(code) : undefined;\n}\n"],"mappings":"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"}
@@ -12,7 +12,9 @@ export declare const XML_DECLARATION = "<?xml version=\"1.0\" encoding=\"UTF-8\"
12
12
  export declare const DOCTYPE = "<!DOCTYPE html>";
13
13
  /**
14
14
  * Throws an `XmlError` unless `xml` is a well-formed document starting with
15
- * the XML declaration on the first line and the doctype on the second.
15
+ * the XML declaration on the first line and the doctype on the second, both
16
+ * as `formatXml` requires them (XML 1.0 in UTF-8, an `html` doctype), whose
17
+ * expressions (`b:if cond`, `expr:*`, ...) parse with `blogger-expr`.
16
18
  */
17
19
  export declare function validateXml(xml: string): void;
18
20
  /**
@@ -21,7 +23,9 @@ export declare function validateXml(xml: string): void;
21
23
  * - the XML declaration on the first line and the doctype on the second, added
22
24
  * when missing and moved there when elsewhere
23
25
  * - start tags on a single line, line breaks in attribute values collapsed to
24
- * a space and the attributes sorted by name
26
+ * a space, the attributes sorted by name and quoted with single quotes
27
+ * - expressions (`b:if cond`, `expr:*`, ...) printed by `blogger-expr` on a
28
+ * single line, `data:a&amp;&amp;data:b` as `data:a and data:b`
25
29
  *
26
30
  * Everything else is left as it is.
27
31
  */
@@ -1,4 +1,5 @@
1
1
  import { XmlScanner, XmlSyntaxError, createLocate } from "./scanner.js";
2
+ import { formatExpression, parseExpressions } from "./expressions.js";
2
3
  //#region src/xml/format.ts
3
4
  var XmlError = class extends Error {
4
5
  line;
@@ -17,7 +18,9 @@ const XML_DECLARATION = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>";
17
18
  const DOCTYPE = "<!DOCTYPE html>";
18
19
  /**
19
20
  * Throws an `XmlError` unless `xml` is a well-formed document starting with
20
- * the XML declaration on the first line and the doctype on the second.
21
+ * the XML declaration on the first line and the doctype on the second, both
22
+ * as `formatXml` requires them (XML 1.0 in UTF-8, an `html` doctype), whose
23
+ * expressions (`b:if cond`, `expr:*`, ...) parse with `blogger-expr`.
21
24
  */
22
25
  function validateXml(xml) {
23
26
  const { declarations } = scanDocument(xml);
@@ -36,6 +39,8 @@ function validateXml(xml) {
36
39
  line: 2,
37
40
  column: 0
38
41
  });
42
+ formatXmlDeclaration(xmlDeclaration);
43
+ formatDoctype(doctype);
39
44
  }
40
45
  /**
41
46
  * Validates the document and formats it as Blogger saves it:
@@ -43,17 +48,19 @@ function validateXml(xml) {
43
48
  * - the XML declaration on the first line and the doctype on the second, added
44
49
  * when missing and moved there when elsewhere
45
50
  * - start tags on a single line, line breaks in attribute values collapsed to
46
- * a space and the attributes sorted by name
51
+ * a space, the attributes sorted by name and quoted with single quotes
52
+ * - expressions (`b:if cond`, `expr:*`, ...) printed by `blogger-expr` on a
53
+ * single line, `data:a&amp;&amp;data:b` as `data:a and data:b`
47
54
  *
48
55
  * Everything else is left as it is.
49
56
  */
50
57
  function formatXml(xml) {
51
- const { tags, declarations } = scanDocument(xml);
58
+ const { tags, declarations, expressions } = scanDocument(xml);
52
59
  const edits = [
53
60
  ...tags.map((tag) => ({
54
61
  start: tag.start,
55
62
  end: tag.end,
56
- text: formatStartTag(tag)
63
+ text: formatStartTag(tag, expressions)
57
64
  })),
58
65
  ...indentChildren(xml, tags),
59
66
  ...declarations.map((item) => ({
@@ -110,11 +117,15 @@ function scanDocument(xml) {
110
117
  const locate = createLocate(xml);
111
118
  const scanned = {
112
119
  tags: [],
113
- declarations: []
120
+ declarations: [],
121
+ expressions: /* @__PURE__ */ new Map()
114
122
  };
115
123
  const scanner = new XmlScanner({
116
124
  document: true,
117
- onStartTag: (tag) => scanned.tags.push(tag),
125
+ onStartTag: (tag) => {
126
+ scanned.tags.push(tag);
127
+ for (const [attribute, expression] of parseExpressions(tag)) scanned.expressions.set(attribute, expression);
128
+ },
118
129
  onDeclaration: (declaration) => scanned.declarations.push(declaration)
119
130
  });
120
131
  try {
@@ -175,8 +186,19 @@ function formatDoctype({ content, position }) {
175
186
  const identifiers = rest.trim().replace(/\s+/g, " ");
176
187
  return identifiers ? `<!DOCTYPE html ${identifiers}>` : DOCTYPE;
177
188
  }
178
- function formatStartTag({ name, attributes, selfClosing }) {
179
- return `<${name}${[...attributes].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).map(({ name, value, quote }) => ` ${name}=${quote}${collapseLineBreaks(value)}${quote}`).join("")}${selfClosing ? "/>" : ">"}`;
189
+ function formatStartTag({ name: tagName, attributes, selfClosing }, expressions) {
190
+ return `<${tagName}${[...attributes].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).map((attribute) => {
191
+ const parsed = expressions.get(attribute);
192
+ const value = (parsed && formatExpression(parsed)) ?? singleQuoted(attribute, collapseLineBreaks(attribute.value));
193
+ return ` ${attribute.name}='${value}'`;
194
+ }).join("")}${selfClosing ? "/>" : ">"}`;
195
+ }
196
+ /**
197
+ * A value written in double quotes, rewritten for single quotes: `'` escaped
198
+ * as `&apos;` and `&quot;` unescaped. Other entities are left as they are.
199
+ */
200
+ function singleQuoted({ quote }, value) {
201
+ return quote === "'" ? value : value.replace(/'/g, "&apos;").replace(/&quot;/g, "\"");
180
202
  }
181
203
  /** Every line break becomes a space, the spaces around it stay as they are. */
182
204
  function collapseLineBreaks(value) {
@@ -1 +1 @@
1
- {"version":3,"file":"format.js","names":[],"sources":["../../src/xml/format.ts"],"sourcesContent":["import {\n\tcreateLocate,\n\ttype Position,\n\ttype ScannedDeclaration,\n\ttype ScannedStartTag,\n\tXmlScanner,\n\tXmlSyntaxError,\n} from './scanner';\n\nexport class XmlError extends Error {\n\treadonly line: number;\n\t/** 1-based. */\n\treadonly column: number;\n\n\tconstructor(message: string, { line, column }: Position) {\n\t\tsuper(`${message} (${line}:${column + 1})`);\n\t\tthis.name = 'XmlError';\n\t\tthis.line = line;\n\t\tthis.column = column + 1;\n\t}\n}\n\n/** The XML declaration Blogger writes, the first line of a theme. */\nexport const XML_DECLARATION = '<?xml version=\"1.0\" encoding=\"UTF-8\" ?>';\n/** The doctype Blogger writes, the second line of a theme. */\nexport const DOCTYPE = '<!DOCTYPE html>';\n\n/**\n * Throws an `XmlError` unless `xml` is a well-formed document starting with\n * the XML declaration on the first line and the doctype on the second.\n */\nexport function validateXml(xml: string): void {\n\tconst { declarations } = scanDocument(xml);\n\tconst [first, second] = [...xml.split('\\n', 2)].map((line) =>\n\t\tline.replace(/\\r$/, ''),\n\t);\n\tconst xmlDeclaration = declarations.find((item) => item.kind === 'xml');\n\tconst doctype = declarations.find((item) => item.kind === 'doctype');\n\n\tif (xmlDeclaration?.start !== 0) {\n\t\tthrow new XmlError(\n\t\t\t`The first line has to be the XML declaration, ${XML_DECLARATION}`,\n\t\t\txmlDeclaration?.position ?? { line: 1, column: 0 },\n\t\t);\n\t}\n\tif (first.trimEnd() !== xml.slice(0, xmlDeclaration.end)) {\n\t\tthrow new XmlError(\n\t\t\t'The XML declaration has to be alone on the first line',\n\t\t\t{ line: 1, column: xmlDeclaration.end },\n\t\t);\n\t}\n\tif (\n\t\tdoctype?.position.line !== 2 ||\n\t\tsecond?.trim() !== xml.slice(doctype.start, doctype.end)\n\t) {\n\t\tthrow new XmlError(\n\t\t\t`The second line has to be the doctype, ${DOCTYPE}`,\n\t\t\tdoctype?.position ?? { line: 2, column: 0 },\n\t\t);\n\t}\n}\n\n/**\n * Validates the document and formats it as Blogger saves it:\n *\n * - the XML declaration on the first line and the doctype on the second, added\n * when missing and moved there when elsewhere\n * - start tags on a single line, line breaks in attribute values collapsed to\n * a space and the attributes sorted by name\n *\n * Everything else is left as it is.\n */\nexport function formatXml(xml: string): string {\n\tconst { tags, declarations } = scanDocument(xml);\n\n\t// the declarations are taken out, they are written at the top\n\tconst edits: { start: number; end: number; text: string }[] = [\n\t\t...tags.map((tag) => ({\n\t\t\tstart: tag.start,\n\t\t\tend: tag.end,\n\t\t\ttext: formatStartTag(tag),\n\t\t})),\n\t\t...indentChildren(xml, tags),\n\t\t...declarations.map((item) => ({\n\t\t\t...lineOf(xml, item.start, item.end),\n\t\t\ttext: '',\n\t\t})),\n\t].sort((a, b) => a.start - b.start || a.end - b.end);\n\n\tlet body = '';\n\tlet last = 0;\n\tfor (const { start, end, text } of edits) {\n\t\tbody += xml.slice(last, start) + text;\n\t\tlast = end;\n\t}\n\tbody += xml.slice(last);\n\n\tconst xmlDeclaration = declarations.find((item) => item.kind === 'xml');\n\tconst doctype = declarations.find((item) => item.kind === 'doctype');\n\n\treturn [\n\t\txmlDeclaration ? formatXmlDeclaration(xmlDeclaration) : XML_DECLARATION,\n\t\tdoctype ? formatDoctype(doctype) : DOCTYPE,\n\t\tbody.replace(/^\\s+/, ''),\n\t].join('\\n');\n}\n\n/** Elements whose children Blogger writes each on a line, indented. */\nconst INDENTED_PARENTS = new Set(['b:widget', 'b:widget-settings']);\n\n/**\n * Puts the start tag of each child of an indented parent on its own line,\n * indented two spaces more than the line of the parent, by replacing the\n * whitespace before it. What is in the children is left as it is.\n */\nfunction indentChildren(\n\txml: string,\n\ttags: ScannedStartTag[],\n): { start: number; end: number; text: string }[] {\n\t// indentation by start tag offset, of the parents moved to a line of their own\n\tconst moved = new Map<number, string>();\n\tconst edits: { start: number; end: number; text: string }[] = [];\n\n\tfor (const tag of tags) {\n\t\tconst { parent } = tag;\n\t\tif (parent === undefined || !INDENTED_PARENTS.has(parent.name)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst indent = `${moved.get(parent.start) ?? indentationOf(xml, parent.start)} `;\n\t\tlet start = tag.start;\n\t\twhile (start > 0 && /\\s/.test(xml[start - 1])) {\n\t\t\tstart--;\n\t\t}\n\n\t\tmoved.set(tag.start, indent);\n\t\tedits.push({ start, end: tag.start, text: `\\n${indent}` });\n\t}\n\n\treturn edits;\n}\n\n/** The whitespace at the start of the line `offset` is on. */\nfunction indentationOf(xml: string, offset: number): string {\n\tconst lineStart = xml.lastIndexOf('\\n', offset - 1) + 1;\n\treturn /^[ \\t]*/.exec(xml.slice(lineStart, offset))?.[0] ?? '';\n}\n\ninterface Scanned {\n\ttags: ScannedStartTag[];\n\tdeclarations: ScannedDeclaration[];\n}\n\nfunction scanDocument(xml: string): Scanned {\n\tconst locate = createLocate(xml);\n\tconst scanned: Scanned = { tags: [], declarations: [] };\n\tconst scanner = new XmlScanner({\n\t\tdocument: true,\n\t\tonStartTag: (tag) => scanned.tags.push(tag),\n\t\tonDeclaration: (declaration) => scanned.declarations.push(declaration),\n\t});\n\n\ttry {\n\t\tscanner.feed(xml, locate);\n\t\tscanner.finish(locate(xml.length));\n\n\t\tfor (const kind of ['xml', 'doctype'] as const) {\n\t\t\tconst found = scanned.declarations.filter((item) => item.kind === kind);\n\t\t\tif (found.length > 1) {\n\t\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t\t`There can only be one ${kind === 'xml' ? 'XML declaration' : 'doctype'}, another one is at ${found[0].position.line}:${found[0].position.column + 1}`,\n\t\t\t\t\tfound[1].position,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t} catch (error) {\n\t\tif (error instanceof XmlSyntaxError) {\n\t\t\tthrow new XmlError(error.message, error.position);\n\t\t}\n\t\tthrow error;\n\t}\n\n\treturn scanned;\n}\n\n/**\n * The range to remove for a declaration: its whole line when nothing else is\n * on it, so no empty line is left, otherwise only the declaration.\n */\nfunction lineOf(\n\txml: string,\n\tstart: number,\n\tend: number,\n): { start: number; end: number } {\n\tlet lineStart = start;\n\twhile (lineStart > 0 && /[ \\t]/.test(xml[lineStart - 1])) {\n\t\tlineStart--;\n\t}\n\tlet lineEnd = end;\n\twhile (lineEnd < xml.length && /[ \\t\\r]/.test(xml[lineEnd])) {\n\t\tlineEnd++;\n\t}\n\n\tconst alone =\n\t\t(lineStart === 0 || xml[lineStart - 1] === '\\n') &&\n\t\t(lineEnd === xml.length || xml[lineEnd] === '\\n');\n\n\treturn alone\n\t\t? { start: lineStart, end: Math.min(lineEnd + 1, xml.length) }\n\t\t: { start, end };\n}\n\n/** `<?xml version=\"1.0\" encoding=\"UTF-8\" ?>`, it has to be XML 1.0 in UTF-8. */\nfunction formatXmlDeclaration({\n\tcontent,\n\tposition,\n}: ScannedDeclaration): string {\n\tconst attributes = new Map<string, string>();\n\tconst rest = content\n\t\t.slice('xml'.length)\n\t\t.replace(\n\t\t\t/\\s*([\\w-]+)\\s*=\\s*([\"'])(.*?)\\2/g,\n\t\t\t(_, name: string, _quote, value: string) => {\n\t\t\t\tattributes.set(name, value);\n\t\t\t\treturn '';\n\t\t\t},\n\t\t)\n\t\t.trim();\n\n\tconst fail = (message: string): never => {\n\t\tthrow new XmlError(message, position);\n\t};\n\n\tif (rest !== '') {\n\t\tfail(`Invalid XML declaration '<?${content}?>'`);\n\t}\n\tfor (const name of attributes.keys()) {\n\t\tif (!['version', 'encoding', 'standalone'].includes(name)) {\n\t\t\tfail(`Unknown '${name}' in the XML declaration`);\n\t\t}\n\t}\n\tif (attributes.get('version') !== '1.0') {\n\t\tfail(\n\t\t\t`The XML declaration has to have version=\"1.0\", got: ${attributes.has('version') ? `'${attributes.get('version')}'` : 'none'}`,\n\t\t);\n\t}\n\tconst encoding = attributes.get('encoding');\n\tif (encoding !== undefined && encoding.toUpperCase() !== 'UTF-8') {\n\t\tfail(\n\t\t\t`Blogger themes are UTF-8, the XML declaration has encoding '${encoding}'`,\n\t\t);\n\t}\n\n\treturn XML_DECLARATION;\n}\n\n/** `<!DOCTYPE html>`, an identifier after `html` is kept. */\nfunction formatDoctype({ content, position }: ScannedDeclaration): string {\n\tconst [, root, rest] = /^DOCTYPE\\s+([^\\s[>]+)([\\s\\S]*)$/i.exec(content) ?? [];\n\tif (root?.toLowerCase() !== 'html') {\n\t\tthrow new XmlError(\n\t\t\t`The doctype has to be ${DOCTYPE}, got: '<!${content}>'`,\n\t\t\tposition,\n\t\t);\n\t}\n\tconst identifiers = rest.trim().replace(/\\s+/g, ' ');\n\treturn identifiers ? `<!DOCTYPE html ${identifiers}>` : DOCTYPE;\n}\n\nfunction formatStartTag({\n\tname,\n\tattributes,\n\tselfClosing,\n}: ScannedStartTag): string {\n\tconst formatted = [...attributes]\n\t\t.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))\n\t\t.map(\n\t\t\t({ name, value, quote }) =>\n\t\t\t\t` ${name}=${quote}${collapseLineBreaks(value)}${quote}`,\n\t\t)\n\t\t.join('');\n\n\treturn `<${name}${formatted}${selfClosing ? '/>' : '>'}`;\n}\n\n/** Every line break becomes a space, the spaces around it stay as they are. */\nfunction collapseLineBreaks(value: string): string {\n\treturn value.replace(/\\r?\\n/g, ' ');\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"format.js","names":[],"sources":["../../src/xml/format.ts"],"sourcesContent":["import {\n\tformatExpression,\n\ttype ParsedExpression,\n\tparseExpressions,\n} from './expressions';\nimport {\n\tcreateLocate,\n\ttype Position,\n\ttype ScannedAttribute,\n\ttype ScannedDeclaration,\n\ttype ScannedStartTag,\n\tXmlScanner,\n\tXmlSyntaxError,\n} from './scanner';\n\nexport class XmlError extends Error {\n\treadonly line: number;\n\t/** 1-based. */\n\treadonly column: number;\n\n\tconstructor(message: string, { line, column }: Position) {\n\t\tsuper(`${message} (${line}:${column + 1})`);\n\t\tthis.name = 'XmlError';\n\t\tthis.line = line;\n\t\tthis.column = column + 1;\n\t}\n}\n\n/** The XML declaration Blogger writes, the first line of a theme. */\nexport const XML_DECLARATION = '<?xml version=\"1.0\" encoding=\"UTF-8\" ?>';\n/** The doctype Blogger writes, the second line of a theme. */\nexport const DOCTYPE = '<!DOCTYPE html>';\n\n/**\n * Throws an `XmlError` unless `xml` is a well-formed document starting with\n * the XML declaration on the first line and the doctype on the second, both\n * as `formatXml` requires them (XML 1.0 in UTF-8, an `html` doctype), whose\n * expressions (`b:if cond`, `expr:*`, ...) parse with `blogger-expr`.\n */\nexport function validateXml(xml: string): void {\n\tconst { declarations } = scanDocument(xml);\n\tconst [first, second] = [...xml.split('\\n', 2)].map((line) =>\n\t\tline.replace(/\\r$/, ''),\n\t);\n\tconst xmlDeclaration = declarations.find((item) => item.kind === 'xml');\n\tconst doctype = declarations.find((item) => item.kind === 'doctype');\n\n\tif (xmlDeclaration?.start !== 0) {\n\t\tthrow new XmlError(\n\t\t\t`The first line has to be the XML declaration, ${XML_DECLARATION}`,\n\t\t\txmlDeclaration?.position ?? { line: 1, column: 0 },\n\t\t);\n\t}\n\tif (first.trimEnd() !== xml.slice(0, xmlDeclaration.end)) {\n\t\tthrow new XmlError(\n\t\t\t'The XML declaration has to be alone on the first line',\n\t\t\t{ line: 1, column: xmlDeclaration.end },\n\t\t);\n\t}\n\tif (\n\t\tdoctype?.position.line !== 2 ||\n\t\tsecond?.trim() !== xml.slice(doctype.start, doctype.end)\n\t) {\n\t\tthrow new XmlError(\n\t\t\t`The second line has to be the doctype, ${DOCTYPE}`,\n\t\t\tdoctype?.position ?? { line: 2, column: 0 },\n\t\t);\n\t}\n\n\t// their contents are checked as when formatting\n\tformatXmlDeclaration(xmlDeclaration);\n\tformatDoctype(doctype);\n}\n\n/**\n * Validates the document and formats it as Blogger saves it:\n *\n * - the XML declaration on the first line and the doctype on the second, added\n * when missing and moved there when elsewhere\n * - start tags on a single line, line breaks in attribute values collapsed to\n * a space, the attributes sorted by name and quoted with single quotes\n * - expressions (`b:if cond`, `expr:*`, ...) printed by `blogger-expr` on a\n * single line, `data:a&amp;&amp;data:b` as `data:a and data:b`\n *\n * Everything else is left as it is.\n */\nexport function formatXml(xml: string): string {\n\tconst { tags, declarations, expressions } = scanDocument(xml);\n\n\t// the declarations are taken out, they are written at the top\n\tconst edits: { start: number; end: number; text: string }[] = [\n\t\t...tags.map((tag) => ({\n\t\t\tstart: tag.start,\n\t\t\tend: tag.end,\n\t\t\ttext: formatStartTag(tag, expressions),\n\t\t})),\n\t\t...indentChildren(xml, tags),\n\t\t...declarations.map((item) => ({\n\t\t\t...lineOf(xml, item.start, item.end),\n\t\t\ttext: '',\n\t\t})),\n\t].sort((a, b) => a.start - b.start || a.end - b.end);\n\n\tlet body = '';\n\tlet last = 0;\n\tfor (const { start, end, text } of edits) {\n\t\tbody += xml.slice(last, start) + text;\n\t\tlast = end;\n\t}\n\tbody += xml.slice(last);\n\n\tconst xmlDeclaration = declarations.find((item) => item.kind === 'xml');\n\tconst doctype = declarations.find((item) => item.kind === 'doctype');\n\n\treturn [\n\t\txmlDeclaration ? formatXmlDeclaration(xmlDeclaration) : XML_DECLARATION,\n\t\tdoctype ? formatDoctype(doctype) : DOCTYPE,\n\t\tbody.replace(/^\\s+/, ''),\n\t].join('\\n');\n}\n\n/** Elements whose children Blogger writes each on a line, indented. */\nconst INDENTED_PARENTS = new Set(['b:widget', 'b:widget-settings']);\n\n/**\n * Puts the start tag of each child of an indented parent on its own line,\n * indented two spaces more than the line of the parent, by replacing the\n * whitespace before it. What is in the children is left as it is.\n */\nfunction indentChildren(\n\txml: string,\n\ttags: ScannedStartTag[],\n): { start: number; end: number; text: string }[] {\n\t// indentation by start tag offset, of the parents moved to a line of their own\n\tconst moved = new Map<number, string>();\n\tconst edits: { start: number; end: number; text: string }[] = [];\n\n\tfor (const tag of tags) {\n\t\tconst { parent } = tag;\n\t\tif (parent === undefined || !INDENTED_PARENTS.has(parent.name)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst indent = `${moved.get(parent.start) ?? indentationOf(xml, parent.start)} `;\n\t\tlet start = tag.start;\n\t\twhile (start > 0 && /\\s/.test(xml[start - 1])) {\n\t\t\tstart--;\n\t\t}\n\n\t\tmoved.set(tag.start, indent);\n\t\tedits.push({ start, end: tag.start, text: `\\n${indent}` });\n\t}\n\n\treturn edits;\n}\n\n/** The whitespace at the start of the line `offset` is on. */\nfunction indentationOf(xml: string, offset: number): string {\n\tconst lineStart = xml.lastIndexOf('\\n', offset - 1) + 1;\n\treturn /^[ \\t]*/.exec(xml.slice(lineStart, offset))?.[0] ?? '';\n}\n\ninterface Scanned {\n\ttags: ScannedStartTag[];\n\tdeclarations: ScannedDeclaration[];\n\t/** The expressions of the attributes, parsed while scanning. */\n\texpressions: Map<ScannedAttribute, ParsedExpression>;\n}\n\nfunction scanDocument(xml: string): Scanned {\n\tconst locate = createLocate(xml);\n\tconst scanned: Scanned = {\n\t\ttags: [],\n\t\tdeclarations: [],\n\t\texpressions: new Map(),\n\t};\n\tconst scanner = new XmlScanner({\n\t\tdocument: true,\n\t\tonStartTag: (tag) => {\n\t\t\tscanned.tags.push(tag);\n\t\t\tfor (const [attribute, expression] of parseExpressions(tag)) {\n\t\t\t\tscanned.expressions.set(attribute, expression);\n\t\t\t}\n\t\t},\n\t\tonDeclaration: (declaration) => scanned.declarations.push(declaration),\n\t});\n\n\ttry {\n\t\tscanner.feed(xml, locate);\n\t\tscanner.finish(locate(xml.length));\n\n\t\tfor (const kind of ['xml', 'doctype'] as const) {\n\t\t\tconst found = scanned.declarations.filter((item) => item.kind === kind);\n\t\t\tif (found.length > 1) {\n\t\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t\t`There can only be one ${kind === 'xml' ? 'XML declaration' : 'doctype'}, another one is at ${found[0].position.line}:${found[0].position.column + 1}`,\n\t\t\t\t\tfound[1].position,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t} catch (error) {\n\t\tif (error instanceof XmlSyntaxError) {\n\t\t\tthrow new XmlError(error.message, error.position);\n\t\t}\n\t\tthrow error;\n\t}\n\n\treturn scanned;\n}\n\n/**\n * The range to remove for a declaration: its whole line when nothing else is\n * on it, so no empty line is left, otherwise only the declaration.\n */\nfunction lineOf(\n\txml: string,\n\tstart: number,\n\tend: number,\n): { start: number; end: number } {\n\tlet lineStart = start;\n\twhile (lineStart > 0 && /[ \\t]/.test(xml[lineStart - 1])) {\n\t\tlineStart--;\n\t}\n\tlet lineEnd = end;\n\twhile (lineEnd < xml.length && /[ \\t\\r]/.test(xml[lineEnd])) {\n\t\tlineEnd++;\n\t}\n\n\tconst alone =\n\t\t(lineStart === 0 || xml[lineStart - 1] === '\\n') &&\n\t\t(lineEnd === xml.length || xml[lineEnd] === '\\n');\n\n\treturn alone\n\t\t? { start: lineStart, end: Math.min(lineEnd + 1, xml.length) }\n\t\t: { start, end };\n}\n\n/** `<?xml version=\"1.0\" encoding=\"UTF-8\" ?>`, it has to be XML 1.0 in UTF-8. */\nfunction formatXmlDeclaration({\n\tcontent,\n\tposition,\n}: ScannedDeclaration): string {\n\tconst attributes = new Map<string, string>();\n\tconst rest = content\n\t\t.slice('xml'.length)\n\t\t.replace(\n\t\t\t/\\s*([\\w-]+)\\s*=\\s*([\"'])(.*?)\\2/g,\n\t\t\t(_, name: string, _quote, value: string) => {\n\t\t\t\tattributes.set(name, value);\n\t\t\t\treturn '';\n\t\t\t},\n\t\t)\n\t\t.trim();\n\n\tconst fail = (message: string): never => {\n\t\tthrow new XmlError(message, position);\n\t};\n\n\tif (rest !== '') {\n\t\tfail(`Invalid XML declaration '<?${content}?>'`);\n\t}\n\tfor (const name of attributes.keys()) {\n\t\tif (!['version', 'encoding', 'standalone'].includes(name)) {\n\t\t\tfail(`Unknown '${name}' in the XML declaration`);\n\t\t}\n\t}\n\tif (attributes.get('version') !== '1.0') {\n\t\tfail(\n\t\t\t`The XML declaration has to have version=\"1.0\", got: ${attributes.has('version') ? `'${attributes.get('version')}'` : 'none'}`,\n\t\t);\n\t}\n\tconst encoding = attributes.get('encoding');\n\tif (encoding !== undefined && encoding.toUpperCase() !== 'UTF-8') {\n\t\tfail(\n\t\t\t`Blogger themes are UTF-8, the XML declaration has encoding '${encoding}'`,\n\t\t);\n\t}\n\n\treturn XML_DECLARATION;\n}\n\n/** `<!DOCTYPE html>`, an identifier after `html` is kept. */\nfunction formatDoctype({ content, position }: ScannedDeclaration): string {\n\tconst [, root, rest] = /^DOCTYPE\\s+([^\\s[>]+)([\\s\\S]*)$/i.exec(content) ?? [];\n\tif (root?.toLowerCase() !== 'html') {\n\t\tthrow new XmlError(\n\t\t\t`The doctype has to be ${DOCTYPE}, got: '<!${content}>'`,\n\t\t\tposition,\n\t\t);\n\t}\n\tconst identifiers = rest.trim().replace(/\\s+/g, ' ');\n\treturn identifiers ? `<!DOCTYPE html ${identifiers}>` : DOCTYPE;\n}\n\nfunction formatStartTag(\n\t{ name: tagName, attributes, selfClosing }: ScannedStartTag,\n\texpressions: Map<ScannedAttribute, ParsedExpression>,\n): string {\n\tconst formatted = [...attributes]\n\t\t.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))\n\t\t.map((attribute) => {\n\t\t\tconst parsed = expressions.get(attribute);\n\t\t\tconst value =\n\t\t\t\t(parsed && formatExpression(parsed)) ??\n\t\t\t\tsingleQuoted(attribute, collapseLineBreaks(attribute.value));\n\t\t\treturn ` ${attribute.name}='${value}'`;\n\t\t})\n\t\t.join('');\n\n\treturn `<${tagName}${formatted}${selfClosing ? '/>' : '>'}`;\n}\n\n/**\n * A value written in double quotes, rewritten for single quotes: `'` escaped\n * as `&apos;` and `&quot;` unescaped. Other entities are left as they are.\n */\nfunction singleQuoted({ quote }: ScannedAttribute, value: string): string {\n\treturn quote === \"'\"\n\t\t? value\n\t\t: value.replace(/'/g, '&apos;').replace(/&quot;/g, '\"');\n}\n\n/** Every line break becomes a space, the spaces around it stay as they are. */\nfunction collapseLineBreaks(value: string): string {\n\treturn value.replace(/\\r?\\n/g, ' ');\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
1
  import { Locate, Position, XmlScanner, XmlSyntaxError, createLocate } from "./scanner.js";
2
+ import { parseExpressions } from "./expressions.js";
2
3
  import { DOCTYPE, XML_DECLARATION, XmlError, formatXml, validateXml } from "./format.js";
3
- export { DOCTYPE, type Locate, type Position, XML_DECLARATION, XmlError, XmlScanner, XmlSyntaxError, createLocate, formatXml, validateXml };
4
+ export { DOCTYPE, type Locate, type Position, XML_DECLARATION, XmlError, XmlScanner, XmlSyntaxError, createLocate, formatXml, parseExpressions, validateXml };
package/dist/xml/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  import { XmlScanner, XmlSyntaxError, createLocate } from "./scanner.js";
2
+ import { parseExpressions } from "./expressions.js";
2
3
  import { DOCTYPE, XML_DECLARATION, XmlError, formatXml, validateXml } from "./format.js";
3
- export { DOCTYPE, XML_DECLARATION, XmlError, XmlScanner, XmlSyntaxError, createLocate, formatXml, validateXml };
4
+ export { DOCTYPE, XML_DECLARATION, XmlError, XmlScanner, XmlSyntaxError, createLocate, formatXml, parseExpressions, validateXml };
@@ -25,6 +25,13 @@ export interface ScannedAttribute {
25
25
  /** The value as it is written, entities are not decoded. */
26
26
  value: string;
27
27
  quote: '"' | "'";
28
+ /**
29
+ * Whether the name or the value has output unknown until rendering, a
30
+ * Handlebars expression or block, `value` is then only the text around it.
31
+ */
32
+ dynamic: boolean;
33
+ /** Position of the character of `value` at `index`, of the closing quote at its length. */
34
+ positionOf(index: number): Position;
28
35
  }
29
36
  export interface ScannedStartTag {
30
37
  name: string;
@@ -43,7 +50,10 @@ export interface ScannerOptions {
43
50
  * is a fragment, which can have anything at its top.
44
51
  */
45
52
  document?: boolean;
46
- /** Called with every start tag, only when the text is fed in one piece. */
53
+ /**
54
+ * Called with every start tag. Its offsets are only right when the text is
55
+ * fed in one piece, `ScannedAttribute.positionOf` always is.
56
+ */
47
57
  onStartTag?(tag: ScannedStartTag): void;
48
58
  /** Called with the XML declaration and the doctype, like `onStartTag`. */
49
59
  onDeclaration?(declaration: ScannedDeclaration): void;
@@ -78,6 +88,8 @@ export declare class XmlScanner {
78
88
  private attrName;
79
89
  private attrDynamic;
80
90
  private attrValue;
91
+ private attrValueDynamic;
92
+ private attrValueSegments;
81
93
  private quote;
82
94
  /** Whether whitespace (or an expression) separates the next attribute. */
83
95
  private separated;
@@ -101,10 +113,16 @@ export declare class XmlScanner {
101
113
  * accepted where any output can be valid, otherwise it throws.
102
114
  */
103
115
  unknown(position: Position, label: string): void;
116
+ /**
117
+ * Marks the attribute value being scanned as dynamic, when a block is in
118
+ * it. Its content is scanned on a copy, so `unknown` is not called.
119
+ */
120
+ dynamicValue(): void;
104
121
  /** Throws unless everything fed is complete. */
105
122
  finish(position: Position): void;
106
123
  private step;
107
124
  private declare;
125
+ private addValueSegment;
108
126
  private startEntity;
109
127
  private missingValue;
110
128
  private endAttribute;
@@ -53,6 +53,8 @@ var XmlScanner = class XmlScanner {
53
53
  attrName = "";
54
54
  attrDynamic = false;
55
55
  attrValue = "";
56
+ attrValueDynamic = false;
57
+ attrValueSegments = [];
56
58
  quote = "\"";
57
59
  /** Whether whitespace (or an expression) separates the next attribute. */
58
60
  separated = false;
@@ -72,7 +74,8 @@ var XmlScanner = class XmlScanner {
72
74
  Object.assign(copy, this, {
73
75
  stack: [...this.stack],
74
76
  attributes: [...this.attributes],
75
- attributeNames: new Set(this.attributeNames)
77
+ attributeNames: new Set(this.attributeNames),
78
+ attrValueSegments: [...this.attrValueSegments]
76
79
  });
77
80
  return copy;
78
81
  }
@@ -95,6 +98,8 @@ var XmlScanner = class XmlScanner {
95
98
  }
96
99
  /** Scans `text`, `base` is added to the offsets of `onStartTag`. */
97
100
  feed(text, locate, base = 0) {
101
+ if (this.mode === "attrValue") this.addValueSegment(this.attrValue.length, locate, 0);
102
+ else if (this.mode === "entity" && this.entityReturn === "attrValue") this.addValueSegment(this.attrValue.length + this.buffer.length, locate, 0);
98
103
  for (let i = 0; i < text.length; i++) this.step(text[i], i, locate, base);
99
104
  }
100
105
  /**
@@ -104,8 +109,10 @@ var XmlScanner = class XmlScanner {
104
109
  unknown(position, label) {
105
110
  const error = (message) => new XmlSyntaxError(`${label} ${message}`, position);
106
111
  switch (this.mode) {
107
- case "text":
108
112
  case "attrValue":
113
+ this.attrValueDynamic = true;
114
+ return;
115
+ case "text":
109
116
  case "comment":
110
117
  case "cdata":
111
118
  case "pi":
@@ -126,6 +133,13 @@ var XmlScanner = class XmlScanner {
126
133
  default: throw error(`can not be inside ${DESCRIPTIONS[this.mode]}`);
127
134
  }
128
135
  }
136
+ /**
137
+ * Marks the attribute value being scanned as dynamic, when a block is in
138
+ * it. Its content is scanned on a copy, so `unknown` is not called.
139
+ */
140
+ dynamicValue() {
141
+ if (this.mode === "attrValue") this.attrValueDynamic = true;
142
+ }
129
143
  /** Throws unless everything fed is complete. */
130
144
  finish(position) {
131
145
  if (this.mode !== "text") throw new XmlSyntaxError(`Unexpected end inside ${DESCRIPTIONS[this.mode]}${this.tagName ? ` of <${this.tagName}>` : ""}`, this.mode === "comment" || this.mode === "cdata" || this.mode === "entity" ? this.entityPosition : this.tagPosition);
@@ -199,6 +213,9 @@ var XmlScanner = class XmlScanner {
199
213
  this.mode = "attrValue";
200
214
  this.quote = char;
201
215
  this.attrValue = "";
216
+ this.attrValueDynamic = false;
217
+ this.attrValueSegments = [];
218
+ this.addValueSegment(0, locate, i + 1);
202
219
  } else if (!WHITESPACE.test(char)) fail(`The value of the attribute '${this.attrName}' of <${this.tagName}> has to be quoted`);
203
220
  return;
204
221
  case "attrValue":
@@ -295,6 +312,13 @@ var XmlScanner = class XmlScanner {
295
312
  position: this.tagPosition
296
313
  });
297
314
  }
315
+ addValueSegment(index, locate, textIndex) {
316
+ this.attrValueSegments.push({
317
+ index,
318
+ locate,
319
+ textIndex
320
+ });
321
+ }
298
322
  startEntity(from, position) {
299
323
  this.mode = "entity";
300
324
  this.entityReturn = from;
@@ -312,7 +336,9 @@ var XmlScanner = class XmlScanner {
312
336
  this.attributes.push({
313
337
  name: this.attrName,
314
338
  value: this.attrValue,
315
- quote: this.quote
339
+ quote: this.quote,
340
+ dynamic: this.attrDynamic || this.attrValueDynamic,
341
+ positionOf: positionInSegments(this.attrValueSegments)
316
342
  });
317
343
  this.mode = "inTag";
318
344
  this.separated = false;
@@ -353,6 +379,12 @@ var XmlScanner = class XmlScanner {
353
379
  function describeElement({ name, position }) {
354
380
  return `<${name}> (opened at ${position.line}:${position.column + 1})`;
355
381
  }
382
+ function positionInSegments(segments) {
383
+ return (index) => {
384
+ const segment = segments.findLast((item) => item.index <= index) ?? segments[0];
385
+ return segment.locate(segment.textIndex + index - segment.index);
386
+ };
387
+ }
356
388
  /** Positions in a single text, computed from the offsets of its line starts. */
357
389
  function createLocate(text, start = {
358
390
  line: 1,
@@ -1 +1 @@
1
- {"version":3,"file":"scanner.js","names":[],"sources":["../../src/xml/scanner.ts"],"sourcesContent":["/**\n * An incremental XML scanner. Text is fed in pieces and parts whose content is\n * unknown (Handlebars expressions) can be marked in between, so a template can\n * be checked without being rendered. The state can be copied to follow the\n * branches of a block separately.\n *\n * It checks well-formedness only: tags, attributes, entities, comments, CDATA\n * sections, processing instructions and the doctype. The first error throws.\n */\n\nexport interface Position {\n\t/** 1-based line. */\n\tline: number;\n\t/** 0-based column, as Handlebars has it, messages show it 1-based. */\n\tcolumn: number;\n}\n\n/** Position of a character of the text fed, by its index in that text. */\nexport type Locate = (index: number) => Position;\n\nexport class XmlSyntaxError extends Error {\n\treadonly position: Position;\n\n\tconstructor(message: string, position: Position) {\n\t\tsuper(message);\n\t\tthis.name = 'XmlSyntaxError';\n\t\tthis.position = position;\n\t}\n}\n\nexport interface ScannedAttribute {\n\tname: string;\n\t/** The value as it is written, entities are not decoded. */\n\tvalue: string;\n\tquote: '\"' | \"'\";\n}\n\nexport interface ScannedStartTag {\n\tname: string;\n\t/** Offset of `<` in the text fed, see `ScannerOptions.onStartTag`. */\n\tstart: number;\n\t/** Offset right after `>`. */\n\tend: number;\n\tattributes: ScannedAttribute[];\n\tselfClosing: boolean;\n\t/** The element the tag is in, undefined at the top. */\n\tparent?: OpenElement;\n}\n\nexport interface ScannerOptions {\n\t/**\n\t * A whole document: a single root element and no text out of it. A template\n\t * is a fragment, which can have anything at its top.\n\t */\n\tdocument?: boolean;\n\t/** Called with every start tag, only when the text is fed in one piece. */\n\tonStartTag?(tag: ScannedStartTag): void;\n\t/** Called with the XML declaration and the doctype, like `onStartTag`. */\n\tonDeclaration?(declaration: ScannedDeclaration): void;\n}\n\nexport interface ScannedDeclaration {\n\tkind: 'xml' | 'doctype';\n\t/** What is between `<?` and `?>`, or `<!` and `>`. */\n\tcontent: string;\n\t/** Offset of `<`. */\n\tstart: number;\n\t/** Offset right after `>`. */\n\tend: number;\n\tposition: Position;\n}\n\ntype Mode =\n\t| 'text'\n\t| 'tagOpen'\n\t| 'startTagName'\n\t| 'inTag'\n\t| 'attrName'\n\t| 'afterAttrName'\n\t| 'beforeAttrValue'\n\t| 'attrValue'\n\t| 'selfClose'\n\t| 'endTagName'\n\t| 'afterEndTagName'\n\t| 'markup'\n\t| 'comment'\n\t| 'cdata'\n\t| 'doctype'\n\t| 'pi'\n\t| 'entity';\n\nexport interface OpenElement {\n\tname: string;\n\tposition: Position;\n\t/** Offset of the `<` of its start tag. */\n\tstart: number;\n}\n\nconst NAME_START = /[A-Za-z_:À-￿]/;\nconst NAME_CHAR = /[A-Za-z0-9_:.\\-·À-￿]/;\nconst WHITESPACE = /[ \\t\\r\\n]/;\nconst ENTITY = /^(?:amp|lt|gt|quot|apos|#[0-9]+|#x[0-9a-fA-F]+)$/;\nconst ENTITY_CHAR = /[A-Za-z0-9#]/;\nconst MARKUP = ['--', '[CDATA[', 'DOCTYPE'];\n\nconst DESCRIPTIONS: Record<Mode, string> = {\n\ttext: 'text',\n\ttagOpen: \"a tag right after '<'\",\n\tstartTagName: 'the name of a tag',\n\tinTag: 'a tag',\n\tattrName: 'the name of an attribute',\n\tafterAttrName: 'an attribute',\n\tbeforeAttrValue: 'an attribute',\n\tattrValue: 'an attribute value',\n\tselfClose: \"a tag right after '/'\",\n\tendTagName: 'a closing tag',\n\tafterEndTagName: 'a closing tag',\n\tmarkup: \"markup right after '<!'\",\n\tcomment: 'a comment',\n\tcdata: 'a CDATA section',\n\tdoctype: 'the doctype',\n\tpi: 'a processing instruction',\n\tentity: 'an entity reference',\n};\n\nexport class XmlScanner {\n\tprivate readonly options: ScannerOptions;\n\n\tprivate mode: Mode = 'text';\n\t/** The elements open, innermost last. */\n\tprivate stack: OpenElement[] = [];\n\tprivate rootClosed = false;\n\n\t// the tag being scanned\n\tprivate tagName = '';\n\tprivate tagPosition: Position = { line: 1, column: 0 };\n\tprivate tagStart = 0;\n\tprivate attributes: ScannedAttribute[] = [];\n\tprivate attributeNames = new Set<string>();\n\tprivate attrName = '';\n\tprivate attrDynamic = false;\n\tprivate attrValue = '';\n\tprivate quote: '\"' | \"'\" = '\"';\n\t/** Whether whitespace (or an expression) separates the next attribute. */\n\tprivate separated = false;\n\tprivate buffer = '';\n\tprivate count = 0;\n\tprivate entityReturn: 'text' | 'attrValue' = 'text';\n\tprivate entityPosition: Position = { line: 1, column: 0 };\n\n\tconstructor(options: ScannerOptions = {}) {\n\t\tthis.options = options;\n\t}\n\n\t/** A copy to scan a branch with. */\n\tclone(): XmlScanner {\n\t\tconst copy = new XmlScanner(this.options);\n\t\tObject.assign(copy, this, {\n\t\t\tstack: [...this.stack],\n\t\t\tattributes: [...this.attributes],\n\t\t\tattributeNames: new Set(this.attributeNames),\n\t\t});\n\t\treturn copy;\n\t}\n\n\t/** Continues from the state of `other`, a scanned branch. */\n\tassign(other: XmlScanner): void {\n\t\tObject.assign(this, other.clone());\n\t}\n\n\t/** Compares the parts of the state that make the following text valid or not. */\n\tsignature(): string {\n\t\tconst inside = this.mode === 'attrValue' ? this.quote : '';\n\t\treturn `${this.mode}${inside}|${this.stack.map((element) => element.name).join('/')}|${this.tagName}`;\n\t}\n\n\t/** Describes how this state differs from `start`, after a block. */\n\tdescribeChange(start: XmlScanner): string {\n\t\tconst opened = this.stack.slice(start.stack.length);\n\t\tconst common = this.stack.length >= start.stack.length;\n\n\t\tif (common && opened.length > 0 && this.mode === 'text') {\n\t\t\treturn `opens ${opened.map(describeElement).join(', ')} without closing ${opened.length > 1 ? 'them' : 'it'}`;\n\t\t}\n\t\tif (this.stack.length < start.stack.length) {\n\t\t\tconst closed = start.stack.slice(this.stack.length);\n\t\t\treturn `closes ${closed.map(describeElement).join(', ')}, opened before it`;\n\t\t}\n\t\tif (this.mode !== start.mode) {\n\t\t\treturn `ends inside ${DESCRIPTIONS[this.mode]}${this.tagName ? ` of <${this.tagName}>` : ''}, but starts inside ${DESCRIPTIONS[start.mode]}`;\n\t\t}\n\t\treturn 'changes the XML structure';\n\t}\n\n\t/** Scans `text`, `base` is added to the offsets of `onStartTag`. */\n\tfeed(text: string, locate: Locate, base = 0): void {\n\t\tfor (let i = 0; i < text.length; i++) {\n\t\t\tthis.step(text[i], i, locate, base);\n\t\t}\n\t}\n\n\t/**\n\t * Marks output unknown until rendering, a Handlebars expression. It is\n\t * accepted where any output can be valid, otherwise it throws.\n\t */\n\tunknown(position: Position, label: string): void {\n\t\tconst error = (message: string) =>\n\t\t\tnew XmlSyntaxError(`${label} ${message}`, position);\n\n\t\tswitch (this.mode) {\n\t\t\tcase 'text':\n\t\t\tcase 'attrValue':\n\t\t\tcase 'comment':\n\t\t\tcase 'cdata':\n\t\t\tcase 'pi':\n\t\t\tcase 'doctype':\n\t\t\t\treturn;\n\t\t\tcase 'inTag':\n\t\t\t\t// it can render attributes, which have to be separated as well\n\t\t\t\tthis.separated = true;\n\t\t\t\treturn;\n\t\t\tcase 'attrName':\n\t\t\t\tthis.attrDynamic = true;\n\t\t\t\treturn;\n\t\t\tcase 'afterAttrName':\n\t\t\tcase 'beforeAttrValue':\n\t\t\t\tthrow error(\n\t\t\t\t\t`is the value of the attribute '${this.attrName}', which has to be quoted: ${this.attrName}='...'`,\n\t\t\t\t);\n\t\t\tcase 'tagOpen':\n\t\t\tcase 'startTagName':\n\t\t\tcase 'endTagName':\n\t\t\tcase 'afterEndTagName':\n\t\t\t\tthrow error('can not be (part of) the name of a tag');\n\t\t\tcase 'entity':\n\t\t\t\tthrow error(\n\t\t\t\t\t\"can not be part of an entity reference, escape '&' as '&amp;'\",\n\t\t\t\t);\n\t\t\tdefault:\n\t\t\t\tthrow error(`can not be inside ${DESCRIPTIONS[this.mode]}`);\n\t\t}\n\t}\n\n\t/** Throws unless everything fed is complete. */\n\tfinish(position: Position): void {\n\t\tif (this.mode !== 'text') {\n\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t`Unexpected end inside ${DESCRIPTIONS[this.mode]}${this.tagName ? ` of <${this.tagName}>` : ''}`,\n\t\t\t\tthis.mode === 'comment' ||\n\t\t\t\t\tthis.mode === 'cdata' ||\n\t\t\t\t\tthis.mode === 'entity'\n\t\t\t\t\t? this.entityPosition\n\t\t\t\t\t: this.tagPosition,\n\t\t\t);\n\t\t}\n\t\tconst open = this.stack.at(-1);\n\t\tif (open) {\n\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t`${describeElement(open)} is not closed`,\n\t\t\t\topen.position,\n\t\t\t);\n\t\t}\n\t\tif (this.options.document && !this.rootClosed) {\n\t\t\tthrow new XmlSyntaxError('The document has no root element', position);\n\t\t}\n\t}\n\n\tprivate step(char: string, i: number, locate: Locate, base: number): void {\n\t\tconst fail = (message: string): never => {\n\t\t\tthrow new XmlSyntaxError(message, locate(i));\n\t\t};\n\n\t\tswitch (this.mode) {\n\t\t\tcase 'text':\n\t\t\t\tif (char === '<') {\n\t\t\t\t\tthis.mode = 'tagOpen';\n\t\t\t\t\tthis.tagStart = base + i;\n\t\t\t\t\tthis.tagPosition = locate(i);\n\t\t\t\t} else if (char === '&') {\n\t\t\t\t\tthis.startEntity('text', locate(i));\n\t\t\t\t} else if (\n\t\t\t\t\tthis.options.document &&\n\t\t\t\t\tthis.stack.length === 0 &&\n\t\t\t\t\t!WHITESPACE.test(char)\n\t\t\t\t) {\n\t\t\t\t\tfail(`Text out of the root element: '${char}'`);\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'tagOpen':\n\t\t\t\tif (char === '/') {\n\t\t\t\t\tthis.mode = 'endTagName';\n\t\t\t\t\tthis.tagName = '';\n\t\t\t\t} else if (char === '!') {\n\t\t\t\t\tthis.mode = 'markup';\n\t\t\t\t\tthis.buffer = '';\n\t\t\t\t} else if (char === '?') {\n\t\t\t\t\tthis.mode = 'pi';\n\t\t\t\t\tthis.count = 0;\n\t\t\t\t\tthis.buffer = '';\n\t\t\t\t} else if (NAME_START.test(char)) {\n\t\t\t\t\tthis.mode = 'startTagName';\n\t\t\t\t\tthis.tagName = char;\n\t\t\t\t\tthis.attributes = [];\n\t\t\t\t\tthis.attributeNames.clear();\n\t\t\t\t} else {\n\t\t\t\t\tfail(\n\t\t\t\t\t\t`Invalid character '${char}' after '<', escape a literal '<' as '&lt;'`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'startTagName':\n\t\t\t\tif (NAME_CHAR.test(char)) {\n\t\t\t\t\tthis.tagName += char;\n\t\t\t\t} else if (WHITESPACE.test(char)) {\n\t\t\t\t\tthis.mode = 'inTag';\n\t\t\t\t\tthis.separated = true;\n\t\t\t\t} else if (char === '>') {\n\t\t\t\t\tthis.endStartTag(false, base + i + 1);\n\t\t\t\t} else if (char === '/') {\n\t\t\t\t\tthis.mode = 'selfClose';\n\t\t\t\t} else {\n\t\t\t\t\tfail(`Invalid character '${char}' in the name of <${this.tagName}>`);\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'inTag':\n\t\t\t\tif (WHITESPACE.test(char)) {\n\t\t\t\t\tthis.separated = true;\n\t\t\t\t} else if (char === '>') {\n\t\t\t\t\tthis.endStartTag(false, base + i + 1);\n\t\t\t\t} else if (char === '/') {\n\t\t\t\t\tthis.mode = 'selfClose';\n\t\t\t\t} else if (NAME_START.test(char)) {\n\t\t\t\t\tif (!this.separated) {\n\t\t\t\t\t\tfail(\n\t\t\t\t\t\t\t`Missing whitespace between the attributes of <${this.tagName}>`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tthis.mode = 'attrName';\n\t\t\t\t\tthis.attrName = char;\n\t\t\t\t\tthis.attrDynamic = false;\n\t\t\t\t} else {\n\t\t\t\t\tfail(`Invalid character '${char}' in <${this.tagName}>`);\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'attrName':\n\t\t\t\tif (NAME_CHAR.test(char)) {\n\t\t\t\t\tthis.attrName += char;\n\t\t\t\t} else if (WHITESPACE.test(char)) {\n\t\t\t\t\tthis.mode = 'afterAttrName';\n\t\t\t\t} else if (char === '=') {\n\t\t\t\t\tthis.mode = 'beforeAttrValue';\n\t\t\t\t} else {\n\t\t\t\t\tthis.missingValue(fail);\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'afterAttrName':\n\t\t\t\tif (char === '=') {\n\t\t\t\t\tthis.mode = 'beforeAttrValue';\n\t\t\t\t} else if (!WHITESPACE.test(char)) {\n\t\t\t\t\tthis.missingValue(fail);\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'beforeAttrValue':\n\t\t\t\tif (char === '\"' || char === \"'\") {\n\t\t\t\t\tthis.mode = 'attrValue';\n\t\t\t\t\tthis.quote = char;\n\t\t\t\t\tthis.attrValue = '';\n\t\t\t\t} else if (!WHITESPACE.test(char)) {\n\t\t\t\t\tfail(\n\t\t\t\t\t\t`The value of the attribute '${this.attrName}' of <${this.tagName}> has to be quoted`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'attrValue':\n\t\t\t\tif (char === this.quote) {\n\t\t\t\t\tthis.endAttribute(fail);\n\t\t\t\t} else if (char === '<') {\n\t\t\t\t\tfail(\n\t\t\t\t\t\t`'<' is not allowed in the value of the attribute '${this.attrName}', escape it as '&lt;'`,\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tif (char === '&') {\n\t\t\t\t\t\tthis.startEntity('attrValue', locate(i));\n\t\t\t\t\t}\n\t\t\t\t\tthis.attrValue += char;\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'selfClose':\n\t\t\t\tif (char === '>') {\n\t\t\t\t\tthis.endStartTag(true, base + i + 1);\n\t\t\t\t} else {\n\t\t\t\t\tfail(`Expected '>' after '/' in <${this.tagName}>`);\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'endTagName':\n\t\t\t\tif (\n\t\t\t\t\tthis.tagName === '' ? NAME_START.test(char) : NAME_CHAR.test(char)\n\t\t\t\t) {\n\t\t\t\t\tthis.tagName += char;\n\t\t\t\t} else if (this.tagName !== '' && WHITESPACE.test(char)) {\n\t\t\t\t\tthis.mode = 'afterEndTagName';\n\t\t\t\t} else if (this.tagName !== '' && char === '>') {\n\t\t\t\t\tthis.endEndTag();\n\t\t\t\t} else {\n\t\t\t\t\tfail(`Invalid character '${char}' in a closing tag`);\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'afterEndTagName':\n\t\t\t\tif (char === '>') {\n\t\t\t\t\tthis.endEndTag();\n\t\t\t\t} else if (!WHITESPACE.test(char)) {\n\t\t\t\t\tfail(`Expected '>' in the closing tag </${this.tagName}>`);\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'markup': {\n\t\t\t\tthis.buffer += char;\n\t\t\t\tconst match = MARKUP.find((markup) =>\n\t\t\t\t\tmarkup.startsWith(this.buffer.toUpperCase()),\n\t\t\t\t);\n\t\t\t\tif (match === undefined) {\n\t\t\t\t\tfail(\n\t\t\t\t\t\t`Invalid markup '<!${this.buffer}', expected a comment, a CDATA section or the doctype`,\n\t\t\t\t\t);\n\t\t\t\t} else if (match === this.buffer.toUpperCase()) {\n\t\t\t\t\tif (\n\t\t\t\t\t\tmatch === 'DOCTYPE' &&\n\t\t\t\t\t\tthis.options.document &&\n\t\t\t\t\t\t(this.stack.length > 0 || this.rootClosed)\n\t\t\t\t\t) {\n\t\t\t\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t\t\t\t'The doctype has to come before the root element',\n\t\t\t\t\t\t\tthis.tagPosition,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tthis.entityPosition = this.tagPosition;\n\t\t\t\t\tthis.count = 0;\n\t\t\t\t\tthis.mode =\n\t\t\t\t\t\tmatch === '--'\n\t\t\t\t\t\t\t? 'comment'\n\t\t\t\t\t\t\t: match === 'DOCTYPE'\n\t\t\t\t\t\t\t\t? 'doctype'\n\t\t\t\t\t\t\t\t: 'cdata';\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcase 'comment':\n\t\t\t\tif (char === '-') {\n\t\t\t\t\tthis.count++;\n\t\t\t\t} else {\n\t\t\t\t\tif (this.count >= 2) {\n\t\t\t\t\t\tif (char !== '>') {\n\t\t\t\t\t\t\tfail(\"'--' is not allowed inside a comment\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.mode = 'text';\n\t\t\t\t\t}\n\t\t\t\t\tthis.count = 0;\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'cdata':\n\t\t\t\tif (char === ']') {\n\t\t\t\t\tthis.count++;\n\t\t\t\t} else {\n\t\t\t\t\tif (char === '>' && this.count >= 2) {\n\t\t\t\t\t\tthis.mode = 'text';\n\t\t\t\t\t}\n\t\t\t\t\tthis.count = 0;\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'doctype':\n\t\t\t\tif (char === '[') {\n\t\t\t\t\tthis.count++;\n\t\t\t\t} else if (char === ']') {\n\t\t\t\t\tthis.count--;\n\t\t\t\t} else if (char === '>' && this.count <= 0) {\n\t\t\t\t\tthis.mode = 'text';\n\t\t\t\t\tthis.declare('doctype', this.buffer, base + i + 1);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.buffer += char;\n\t\t\t\treturn;\n\n\t\t\tcase 'pi':\n\t\t\t\tif (char === '>' && this.count > 0) {\n\t\t\t\t\tthis.mode = 'text';\n\t\t\t\t\tconst content = this.buffer.slice(0, -1);\n\t\t\t\t\tconst target = /^[^\\s?]*/.exec(content)?.[0] ?? '';\n\t\t\t\t\tif (target.toLowerCase() === 'xml') {\n\t\t\t\t\t\tif (target !== 'xml') {\n\t\t\t\t\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t\t\t\t\t`'${target}' is reserved, the XML declaration is '<?xml ...?>'`,\n\t\t\t\t\t\t\t\tthis.tagPosition,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.declare('xml', content, base + i + 1);\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.buffer += char;\n\t\t\t\tthis.count = char === '?' ? 1 : 0;\n\t\t\t\treturn;\n\n\t\t\tcase 'entity':\n\t\t\t\tif (char === ';') {\n\t\t\t\t\tif (!ENTITY.test(this.buffer)) {\n\t\t\t\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t\t\t\tthis.buffer === ''\n\t\t\t\t\t\t\t\t? \"'&' has to be escaped as '&amp;'\"\n\t\t\t\t\t\t\t\t: `Undefined entity '&${this.buffer};', XML only has &amp; &lt; &gt; &quot; &apos;, use a numeric reference like '&#160;' for others`,\n\t\t\t\t\t\t\tthis.entityPosition,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tthis.mode = this.entityReturn;\n\t\t\t\t\tif (this.mode === 'attrValue') {\n\t\t\t\t\t\tthis.attrValue += `${this.buffer};`;\n\t\t\t\t\t}\n\t\t\t\t} else if (ENTITY_CHAR.test(char) && this.buffer.length < 32) {\n\t\t\t\t\tthis.buffer += char;\n\t\t\t\t} else {\n\t\t\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t\t\t\"'&' has to be escaped as '&amp;'\",\n\t\t\t\t\t\tthis.entityPosition,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t}\n\t}\n\n\tprivate declare(\n\t\tkind: ScannedDeclaration['kind'],\n\t\tcontent: string,\n\t\tend: number,\n\t): void {\n\t\tthis.options.onDeclaration?.({\n\t\t\tkind,\n\t\t\tcontent,\n\t\t\tstart: this.tagStart,\n\t\t\tend,\n\t\t\tposition: this.tagPosition,\n\t\t});\n\t}\n\n\tprivate startEntity(from: 'text' | 'attrValue', position: Position): void {\n\t\tthis.mode = 'entity';\n\t\tthis.entityReturn = from;\n\t\tthis.entityPosition = position;\n\t\tthis.buffer = '';\n\t}\n\n\tprivate missingValue(fail: (message: string) => never): never {\n\t\treturn fail(\n\t\t\t`The attribute '${this.attrName}' of <${this.tagName}> has no value, XML requires one: ${this.attrName}='...'`,\n\t\t);\n\t}\n\n\tprivate endAttribute(fail: (message: string) => never): void {\n\t\tif (!this.attrDynamic) {\n\t\t\tif (this.attributeNames.has(this.attrName)) {\n\t\t\t\tfail(`Duplicate attribute '${this.attrName}' in <${this.tagName}>`);\n\t\t\t}\n\t\t\tthis.attributeNames.add(this.attrName);\n\t\t}\n\t\tthis.attributes.push({\n\t\t\tname: this.attrName,\n\t\t\tvalue: this.attrValue,\n\t\t\tquote: this.quote,\n\t\t});\n\t\tthis.mode = 'inTag';\n\t\tthis.separated = false;\n\t}\n\n\tprivate endStartTag(selfClosing: boolean, end: number): void {\n\t\tif (this.options.document && this.stack.length === 0 && this.rootClosed) {\n\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t`Only one root element is allowed, found <${this.tagName}> after it`,\n\t\t\t\tthis.tagPosition,\n\t\t\t);\n\t\t}\n\n\t\tthis.options.onStartTag?.({\n\t\t\tname: this.tagName,\n\t\t\tstart: this.tagStart,\n\t\t\tend,\n\t\t\tattributes: this.attributes,\n\t\t\tselfClosing,\n\t\t\tparent: this.stack.at(-1),\n\t\t});\n\n\t\tif (selfClosing) {\n\t\t\tif (this.stack.length === 0) {\n\t\t\t\tthis.rootClosed = true;\n\t\t\t}\n\t\t} else {\n\t\t\tthis.stack.push({\n\t\t\t\tname: this.tagName,\n\t\t\t\tposition: this.tagPosition,\n\t\t\t\tstart: this.tagStart,\n\t\t\t});\n\t\t}\n\t\tthis.tagName = '';\n\t\tthis.mode = 'text';\n\t}\n\n\tprivate endEndTag(): void {\n\t\t// reported where the closing tag starts\n\t\tconst fail = (message: string): never => {\n\t\t\tthrow new XmlSyntaxError(message, this.tagPosition);\n\t\t};\n\n\t\tconst open = this.stack.at(-1);\n\t\tif (open === undefined) {\n\t\t\tfail(`Closing tag </${this.tagName}> has no opening tag`);\n\t\t} else if (open.name !== this.tagName) {\n\t\t\tfail(\n\t\t\t\t`Expected </${open.name}> to close ${describeElement(open)}, found </${this.tagName}>`,\n\t\t\t);\n\t\t}\n\t\tthis.stack.pop();\n\t\tif (this.stack.length === 0) {\n\t\t\tthis.rootClosed = true;\n\t\t}\n\t\tthis.tagName = '';\n\t\tthis.mode = 'text';\n\t}\n}\n\nfunction describeElement({ name, position }: OpenElement): string {\n\treturn `<${name}> (opened at ${position.line}:${position.column + 1})`;\n}\n\n/** Positions in a single text, computed from the offsets of its line starts. */\nexport function createLocate(\n\ttext: string,\n\tstart: Position = { line: 1, column: 0 },\n): Locate {\n\tconst lineStarts = [0];\n\tfor (let i = 0; i < text.length; i++) {\n\t\tif (text[i] === '\\n') {\n\t\t\tlineStarts.push(i + 1);\n\t\t}\n\t}\n\n\treturn (index) => {\n\t\tlet low = 0;\n\t\tlet high = lineStarts.length - 1;\n\t\twhile (low < high) {\n\t\t\tconst middle = (low + high + 1) >> 1;\n\t\t\tif (lineStarts[middle] <= index) {\n\t\t\t\tlow = middle;\n\t\t\t} else {\n\t\t\t\thigh = middle - 1;\n\t\t\t}\n\t\t}\n\t\treturn low === 0\n\t\t\t? { line: start.line, column: start.column + index }\n\t\t\t: { line: start.line + low, column: index - lineStarts[low] };\n\t};\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"scanner.js","names":[],"sources":["../../src/xml/scanner.ts"],"sourcesContent":["/**\n * An incremental XML scanner. Text is fed in pieces and parts whose content is\n * unknown (Handlebars expressions) can be marked in between, so a template can\n * be checked without being rendered. The state can be copied to follow the\n * branches of a block separately.\n *\n * It checks well-formedness only: tags, attributes, entities, comments, CDATA\n * sections, processing instructions and the doctype. The first error throws.\n */\n\nexport interface Position {\n\t/** 1-based line. */\n\tline: number;\n\t/** 0-based column, as Handlebars has it, messages show it 1-based. */\n\tcolumn: number;\n}\n\n/** Position of a character of the text fed, by its index in that text. */\nexport type Locate = (index: number) => Position;\n\nexport class XmlSyntaxError extends Error {\n\treadonly position: Position;\n\n\tconstructor(message: string, position: Position) {\n\t\tsuper(message);\n\t\tthis.name = 'XmlSyntaxError';\n\t\tthis.position = position;\n\t}\n}\n\nexport interface ScannedAttribute {\n\tname: string;\n\t/** The value as it is written, entities are not decoded. */\n\tvalue: string;\n\tquote: '\"' | \"'\";\n\t/**\n\t * Whether the name or the value has output unknown until rendering, a\n\t * Handlebars expression or block, `value` is then only the text around it.\n\t */\n\tdynamic: boolean;\n\t/** Position of the character of `value` at `index`, of the closing quote at its length. */\n\tpositionOf(index: number): Position;\n}\n\n/** Where a piece of an attribute value is in the text it was fed with. */\ninterface ValueSegment {\n\t/** Index in the value of its first character. */\n\tindex: number;\n\tlocate: Locate;\n\t/** Index in the text fed of its first character. */\n\ttextIndex: number;\n}\n\nexport interface ScannedStartTag {\n\tname: string;\n\t/** Offset of `<` in the text fed, see `ScannerOptions.onStartTag`. */\n\tstart: number;\n\t/** Offset right after `>`. */\n\tend: number;\n\tattributes: ScannedAttribute[];\n\tselfClosing: boolean;\n\t/** The element the tag is in, undefined at the top. */\n\tparent?: OpenElement;\n}\n\nexport interface ScannerOptions {\n\t/**\n\t * A whole document: a single root element and no text out of it. A template\n\t * is a fragment, which can have anything at its top.\n\t */\n\tdocument?: boolean;\n\t/**\n\t * Called with every start tag. Its offsets are only right when the text is\n\t * fed in one piece, `ScannedAttribute.positionOf` always is.\n\t */\n\tonStartTag?(tag: ScannedStartTag): void;\n\t/** Called with the XML declaration and the doctype, like `onStartTag`. */\n\tonDeclaration?(declaration: ScannedDeclaration): void;\n}\n\nexport interface ScannedDeclaration {\n\tkind: 'xml' | 'doctype';\n\t/** What is between `<?` and `?>`, or `<!` and `>`. */\n\tcontent: string;\n\t/** Offset of `<`. */\n\tstart: number;\n\t/** Offset right after `>`. */\n\tend: number;\n\tposition: Position;\n}\n\ntype Mode =\n\t| 'text'\n\t| 'tagOpen'\n\t| 'startTagName'\n\t| 'inTag'\n\t| 'attrName'\n\t| 'afterAttrName'\n\t| 'beforeAttrValue'\n\t| 'attrValue'\n\t| 'selfClose'\n\t| 'endTagName'\n\t| 'afterEndTagName'\n\t| 'markup'\n\t| 'comment'\n\t| 'cdata'\n\t| 'doctype'\n\t| 'pi'\n\t| 'entity';\n\nexport interface OpenElement {\n\tname: string;\n\tposition: Position;\n\t/** Offset of the `<` of its start tag. */\n\tstart: number;\n}\n\nconst NAME_START = /[A-Za-z_:À-￿]/;\nconst NAME_CHAR = /[A-Za-z0-9_:.\\-·À-￿]/;\nconst WHITESPACE = /[ \\t\\r\\n]/;\nconst ENTITY = /^(?:amp|lt|gt|quot|apos|#[0-9]+|#x[0-9a-fA-F]+)$/;\nconst ENTITY_CHAR = /[A-Za-z0-9#]/;\nconst MARKUP = ['--', '[CDATA[', 'DOCTYPE'];\n\nconst DESCRIPTIONS: Record<Mode, string> = {\n\ttext: 'text',\n\ttagOpen: \"a tag right after '<'\",\n\tstartTagName: 'the name of a tag',\n\tinTag: 'a tag',\n\tattrName: 'the name of an attribute',\n\tafterAttrName: 'an attribute',\n\tbeforeAttrValue: 'an attribute',\n\tattrValue: 'an attribute value',\n\tselfClose: \"a tag right after '/'\",\n\tendTagName: 'a closing tag',\n\tafterEndTagName: 'a closing tag',\n\tmarkup: \"markup right after '<!'\",\n\tcomment: 'a comment',\n\tcdata: 'a CDATA section',\n\tdoctype: 'the doctype',\n\tpi: 'a processing instruction',\n\tentity: 'an entity reference',\n};\n\nexport class XmlScanner {\n\tprivate readonly options: ScannerOptions;\n\n\tprivate mode: Mode = 'text';\n\t/** The elements open, innermost last. */\n\tprivate stack: OpenElement[] = [];\n\tprivate rootClosed = false;\n\n\t// the tag being scanned\n\tprivate tagName = '';\n\tprivate tagPosition: Position = { line: 1, column: 0 };\n\tprivate tagStart = 0;\n\tprivate attributes: ScannedAttribute[] = [];\n\tprivate attributeNames = new Set<string>();\n\tprivate attrName = '';\n\tprivate attrDynamic = false;\n\tprivate attrValue = '';\n\tprivate attrValueDynamic = false;\n\tprivate attrValueSegments: ValueSegment[] = [];\n\tprivate quote: '\"' | \"'\" = '\"';\n\t/** Whether whitespace (or an expression) separates the next attribute. */\n\tprivate separated = false;\n\tprivate buffer = '';\n\tprivate count = 0;\n\tprivate entityReturn: 'text' | 'attrValue' = 'text';\n\tprivate entityPosition: Position = { line: 1, column: 0 };\n\n\tconstructor(options: ScannerOptions = {}) {\n\t\tthis.options = options;\n\t}\n\n\t/** A copy to scan a branch with. */\n\tclone(): XmlScanner {\n\t\tconst copy = new XmlScanner(this.options);\n\t\tObject.assign(copy, this, {\n\t\t\tstack: [...this.stack],\n\t\t\tattributes: [...this.attributes],\n\t\t\tattributeNames: new Set(this.attributeNames),\n\t\t\tattrValueSegments: [...this.attrValueSegments],\n\t\t});\n\t\treturn copy;\n\t}\n\n\t/** Continues from the state of `other`, a scanned branch. */\n\tassign(other: XmlScanner): void {\n\t\tObject.assign(this, other.clone());\n\t}\n\n\t/** Compares the parts of the state that make the following text valid or not. */\n\tsignature(): string {\n\t\tconst inside = this.mode === 'attrValue' ? this.quote : '';\n\t\treturn `${this.mode}${inside}|${this.stack.map((element) => element.name).join('/')}|${this.tagName}`;\n\t}\n\n\t/** Describes how this state differs from `start`, after a block. */\n\tdescribeChange(start: XmlScanner): string {\n\t\tconst opened = this.stack.slice(start.stack.length);\n\t\tconst common = this.stack.length >= start.stack.length;\n\n\t\tif (common && opened.length > 0 && this.mode === 'text') {\n\t\t\treturn `opens ${opened.map(describeElement).join(', ')} without closing ${opened.length > 1 ? 'them' : 'it'}`;\n\t\t}\n\t\tif (this.stack.length < start.stack.length) {\n\t\t\tconst closed = start.stack.slice(this.stack.length);\n\t\t\treturn `closes ${closed.map(describeElement).join(', ')}, opened before it`;\n\t\t}\n\t\tif (this.mode !== start.mode) {\n\t\t\treturn `ends inside ${DESCRIPTIONS[this.mode]}${this.tagName ? ` of <${this.tagName}>` : ''}, but starts inside ${DESCRIPTIONS[start.mode]}`;\n\t\t}\n\t\treturn 'changes the XML structure';\n\t}\n\n\t/** Scans `text`, `base` is added to the offsets of `onStartTag`. */\n\tfeed(text: string, locate: Locate, base = 0): void {\n\t\tif (this.mode === 'attrValue') {\n\t\t\tthis.addValueSegment(this.attrValue.length, locate, 0);\n\t\t} else if (this.mode === 'entity' && this.entityReturn === 'attrValue') {\n\t\t\tthis.addValueSegment(\n\t\t\t\tthis.attrValue.length + this.buffer.length,\n\t\t\t\tlocate,\n\t\t\t\t0,\n\t\t\t);\n\t\t}\n\t\tfor (let i = 0; i < text.length; i++) {\n\t\t\tthis.step(text[i], i, locate, base);\n\t\t}\n\t}\n\n\t/**\n\t * Marks output unknown until rendering, a Handlebars expression. It is\n\t * accepted where any output can be valid, otherwise it throws.\n\t */\n\tunknown(position: Position, label: string): void {\n\t\tconst error = (message: string) =>\n\t\t\tnew XmlSyntaxError(`${label} ${message}`, position);\n\n\t\tswitch (this.mode) {\n\t\t\tcase 'attrValue':\n\t\t\t\tthis.attrValueDynamic = true;\n\t\t\t\treturn;\n\t\t\tcase 'text':\n\t\t\tcase 'comment':\n\t\t\tcase 'cdata':\n\t\t\tcase 'pi':\n\t\t\tcase 'doctype':\n\t\t\t\treturn;\n\t\t\tcase 'inTag':\n\t\t\t\t// it can render attributes, which have to be separated as well\n\t\t\t\tthis.separated = true;\n\t\t\t\treturn;\n\t\t\tcase 'attrName':\n\t\t\t\tthis.attrDynamic = true;\n\t\t\t\treturn;\n\t\t\tcase 'afterAttrName':\n\t\t\tcase 'beforeAttrValue':\n\t\t\t\tthrow error(\n\t\t\t\t\t`is the value of the attribute '${this.attrName}', which has to be quoted: ${this.attrName}='...'`,\n\t\t\t\t);\n\t\t\tcase 'tagOpen':\n\t\t\tcase 'startTagName':\n\t\t\tcase 'endTagName':\n\t\t\tcase 'afterEndTagName':\n\t\t\t\tthrow error('can not be (part of) the name of a tag');\n\t\t\tcase 'entity':\n\t\t\t\tthrow error(\n\t\t\t\t\t\"can not be part of an entity reference, escape '&' as '&amp;'\",\n\t\t\t\t);\n\t\t\tdefault:\n\t\t\t\tthrow error(`can not be inside ${DESCRIPTIONS[this.mode]}`);\n\t\t}\n\t}\n\n\t/**\n\t * Marks the attribute value being scanned as dynamic, when a block is in\n\t * it. Its content is scanned on a copy, so `unknown` is not called.\n\t */\n\tdynamicValue(): void {\n\t\tif (this.mode === 'attrValue') {\n\t\t\tthis.attrValueDynamic = true;\n\t\t}\n\t}\n\n\t/** Throws unless everything fed is complete. */\n\tfinish(position: Position): void {\n\t\tif (this.mode !== 'text') {\n\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t`Unexpected end inside ${DESCRIPTIONS[this.mode]}${this.tagName ? ` of <${this.tagName}>` : ''}`,\n\t\t\t\tthis.mode === 'comment' ||\n\t\t\t\t\tthis.mode === 'cdata' ||\n\t\t\t\t\tthis.mode === 'entity'\n\t\t\t\t\t? this.entityPosition\n\t\t\t\t\t: this.tagPosition,\n\t\t\t);\n\t\t}\n\t\tconst open = this.stack.at(-1);\n\t\tif (open) {\n\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t`${describeElement(open)} is not closed`,\n\t\t\t\topen.position,\n\t\t\t);\n\t\t}\n\t\tif (this.options.document && !this.rootClosed) {\n\t\t\tthrow new XmlSyntaxError('The document has no root element', position);\n\t\t}\n\t}\n\n\tprivate step(char: string, i: number, locate: Locate, base: number): void {\n\t\tconst fail = (message: string): never => {\n\t\t\tthrow new XmlSyntaxError(message, locate(i));\n\t\t};\n\n\t\tswitch (this.mode) {\n\t\t\tcase 'text':\n\t\t\t\tif (char === '<') {\n\t\t\t\t\tthis.mode = 'tagOpen';\n\t\t\t\t\tthis.tagStart = base + i;\n\t\t\t\t\tthis.tagPosition = locate(i);\n\t\t\t\t} else if (char === '&') {\n\t\t\t\t\tthis.startEntity('text', locate(i));\n\t\t\t\t} else if (\n\t\t\t\t\tthis.options.document &&\n\t\t\t\t\tthis.stack.length === 0 &&\n\t\t\t\t\t!WHITESPACE.test(char)\n\t\t\t\t) {\n\t\t\t\t\tfail(`Text out of the root element: '${char}'`);\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'tagOpen':\n\t\t\t\tif (char === '/') {\n\t\t\t\t\tthis.mode = 'endTagName';\n\t\t\t\t\tthis.tagName = '';\n\t\t\t\t} else if (char === '!') {\n\t\t\t\t\tthis.mode = 'markup';\n\t\t\t\t\tthis.buffer = '';\n\t\t\t\t} else if (char === '?') {\n\t\t\t\t\tthis.mode = 'pi';\n\t\t\t\t\tthis.count = 0;\n\t\t\t\t\tthis.buffer = '';\n\t\t\t\t} else if (NAME_START.test(char)) {\n\t\t\t\t\tthis.mode = 'startTagName';\n\t\t\t\t\tthis.tagName = char;\n\t\t\t\t\tthis.attributes = [];\n\t\t\t\t\tthis.attributeNames.clear();\n\t\t\t\t} else {\n\t\t\t\t\tfail(\n\t\t\t\t\t\t`Invalid character '${char}' after '<', escape a literal '<' as '&lt;'`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'startTagName':\n\t\t\t\tif (NAME_CHAR.test(char)) {\n\t\t\t\t\tthis.tagName += char;\n\t\t\t\t} else if (WHITESPACE.test(char)) {\n\t\t\t\t\tthis.mode = 'inTag';\n\t\t\t\t\tthis.separated = true;\n\t\t\t\t} else if (char === '>') {\n\t\t\t\t\tthis.endStartTag(false, base + i + 1);\n\t\t\t\t} else if (char === '/') {\n\t\t\t\t\tthis.mode = 'selfClose';\n\t\t\t\t} else {\n\t\t\t\t\tfail(`Invalid character '${char}' in the name of <${this.tagName}>`);\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'inTag':\n\t\t\t\tif (WHITESPACE.test(char)) {\n\t\t\t\t\tthis.separated = true;\n\t\t\t\t} else if (char === '>') {\n\t\t\t\t\tthis.endStartTag(false, base + i + 1);\n\t\t\t\t} else if (char === '/') {\n\t\t\t\t\tthis.mode = 'selfClose';\n\t\t\t\t} else if (NAME_START.test(char)) {\n\t\t\t\t\tif (!this.separated) {\n\t\t\t\t\t\tfail(\n\t\t\t\t\t\t\t`Missing whitespace between the attributes of <${this.tagName}>`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tthis.mode = 'attrName';\n\t\t\t\t\tthis.attrName = char;\n\t\t\t\t\tthis.attrDynamic = false;\n\t\t\t\t} else {\n\t\t\t\t\tfail(`Invalid character '${char}' in <${this.tagName}>`);\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'attrName':\n\t\t\t\tif (NAME_CHAR.test(char)) {\n\t\t\t\t\tthis.attrName += char;\n\t\t\t\t} else if (WHITESPACE.test(char)) {\n\t\t\t\t\tthis.mode = 'afterAttrName';\n\t\t\t\t} else if (char === '=') {\n\t\t\t\t\tthis.mode = 'beforeAttrValue';\n\t\t\t\t} else {\n\t\t\t\t\tthis.missingValue(fail);\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'afterAttrName':\n\t\t\t\tif (char === '=') {\n\t\t\t\t\tthis.mode = 'beforeAttrValue';\n\t\t\t\t} else if (!WHITESPACE.test(char)) {\n\t\t\t\t\tthis.missingValue(fail);\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'beforeAttrValue':\n\t\t\t\tif (char === '\"' || char === \"'\") {\n\t\t\t\t\tthis.mode = 'attrValue';\n\t\t\t\t\tthis.quote = char;\n\t\t\t\t\tthis.attrValue = '';\n\t\t\t\t\tthis.attrValueDynamic = false;\n\t\t\t\t\tthis.attrValueSegments = [];\n\t\t\t\t\tthis.addValueSegment(0, locate, i + 1);\n\t\t\t\t} else if (!WHITESPACE.test(char)) {\n\t\t\t\t\tfail(\n\t\t\t\t\t\t`The value of the attribute '${this.attrName}' of <${this.tagName}> has to be quoted`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'attrValue':\n\t\t\t\tif (char === this.quote) {\n\t\t\t\t\tthis.endAttribute(fail);\n\t\t\t\t} else if (char === '<') {\n\t\t\t\t\tfail(\n\t\t\t\t\t\t`'<' is not allowed in the value of the attribute '${this.attrName}', escape it as '&lt;'`,\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tif (char === '&') {\n\t\t\t\t\t\tthis.startEntity('attrValue', locate(i));\n\t\t\t\t\t}\n\t\t\t\t\tthis.attrValue += char;\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'selfClose':\n\t\t\t\tif (char === '>') {\n\t\t\t\t\tthis.endStartTag(true, base + i + 1);\n\t\t\t\t} else {\n\t\t\t\t\tfail(`Expected '>' after '/' in <${this.tagName}>`);\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'endTagName':\n\t\t\t\tif (\n\t\t\t\t\tthis.tagName === '' ? NAME_START.test(char) : NAME_CHAR.test(char)\n\t\t\t\t) {\n\t\t\t\t\tthis.tagName += char;\n\t\t\t\t} else if (this.tagName !== '' && WHITESPACE.test(char)) {\n\t\t\t\t\tthis.mode = 'afterEndTagName';\n\t\t\t\t} else if (this.tagName !== '' && char === '>') {\n\t\t\t\t\tthis.endEndTag();\n\t\t\t\t} else {\n\t\t\t\t\tfail(`Invalid character '${char}' in a closing tag`);\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'afterEndTagName':\n\t\t\t\tif (char === '>') {\n\t\t\t\t\tthis.endEndTag();\n\t\t\t\t} else if (!WHITESPACE.test(char)) {\n\t\t\t\t\tfail(`Expected '>' in the closing tag </${this.tagName}>`);\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'markup': {\n\t\t\t\tthis.buffer += char;\n\t\t\t\tconst match = MARKUP.find((markup) =>\n\t\t\t\t\tmarkup.startsWith(this.buffer.toUpperCase()),\n\t\t\t\t);\n\t\t\t\tif (match === undefined) {\n\t\t\t\t\tfail(\n\t\t\t\t\t\t`Invalid markup '<!${this.buffer}', expected a comment, a CDATA section or the doctype`,\n\t\t\t\t\t);\n\t\t\t\t} else if (match === this.buffer.toUpperCase()) {\n\t\t\t\t\tif (\n\t\t\t\t\t\tmatch === 'DOCTYPE' &&\n\t\t\t\t\t\tthis.options.document &&\n\t\t\t\t\t\t(this.stack.length > 0 || this.rootClosed)\n\t\t\t\t\t) {\n\t\t\t\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t\t\t\t'The doctype has to come before the root element',\n\t\t\t\t\t\t\tthis.tagPosition,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tthis.entityPosition = this.tagPosition;\n\t\t\t\t\tthis.count = 0;\n\t\t\t\t\tthis.mode =\n\t\t\t\t\t\tmatch === '--'\n\t\t\t\t\t\t\t? 'comment'\n\t\t\t\t\t\t\t: match === 'DOCTYPE'\n\t\t\t\t\t\t\t\t? 'doctype'\n\t\t\t\t\t\t\t\t: 'cdata';\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcase 'comment':\n\t\t\t\tif (char === '-') {\n\t\t\t\t\tthis.count++;\n\t\t\t\t} else {\n\t\t\t\t\tif (this.count >= 2) {\n\t\t\t\t\t\tif (char !== '>') {\n\t\t\t\t\t\t\tfail(\"'--' is not allowed inside a comment\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.mode = 'text';\n\t\t\t\t\t}\n\t\t\t\t\tthis.count = 0;\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'cdata':\n\t\t\t\tif (char === ']') {\n\t\t\t\t\tthis.count++;\n\t\t\t\t} else {\n\t\t\t\t\tif (char === '>' && this.count >= 2) {\n\t\t\t\t\t\tthis.mode = 'text';\n\t\t\t\t\t}\n\t\t\t\t\tthis.count = 0;\n\t\t\t\t}\n\t\t\t\treturn;\n\n\t\t\tcase 'doctype':\n\t\t\t\tif (char === '[') {\n\t\t\t\t\tthis.count++;\n\t\t\t\t} else if (char === ']') {\n\t\t\t\t\tthis.count--;\n\t\t\t\t} else if (char === '>' && this.count <= 0) {\n\t\t\t\t\tthis.mode = 'text';\n\t\t\t\t\tthis.declare('doctype', this.buffer, base + i + 1);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.buffer += char;\n\t\t\t\treturn;\n\n\t\t\tcase 'pi':\n\t\t\t\tif (char === '>' && this.count > 0) {\n\t\t\t\t\tthis.mode = 'text';\n\t\t\t\t\tconst content = this.buffer.slice(0, -1);\n\t\t\t\t\tconst target = /^[^\\s?]*/.exec(content)?.[0] ?? '';\n\t\t\t\t\tif (target.toLowerCase() === 'xml') {\n\t\t\t\t\t\tif (target !== 'xml') {\n\t\t\t\t\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t\t\t\t\t`'${target}' is reserved, the XML declaration is '<?xml ...?>'`,\n\t\t\t\t\t\t\t\tthis.tagPosition,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.declare('xml', content, base + i + 1);\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.buffer += char;\n\t\t\t\tthis.count = char === '?' ? 1 : 0;\n\t\t\t\treturn;\n\n\t\t\tcase 'entity':\n\t\t\t\tif (char === ';') {\n\t\t\t\t\tif (!ENTITY.test(this.buffer)) {\n\t\t\t\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t\t\t\tthis.buffer === ''\n\t\t\t\t\t\t\t\t? \"'&' has to be escaped as '&amp;'\"\n\t\t\t\t\t\t\t\t: `Undefined entity '&${this.buffer};', XML only has &amp; &lt; &gt; &quot; &apos;, use a numeric reference like '&#160;' for others`,\n\t\t\t\t\t\t\tthis.entityPosition,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tthis.mode = this.entityReturn;\n\t\t\t\t\tif (this.mode === 'attrValue') {\n\t\t\t\t\t\tthis.attrValue += `${this.buffer};`;\n\t\t\t\t\t}\n\t\t\t\t} else if (ENTITY_CHAR.test(char) && this.buffer.length < 32) {\n\t\t\t\t\tthis.buffer += char;\n\t\t\t\t} else {\n\t\t\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t\t\t\"'&' has to be escaped as '&amp;'\",\n\t\t\t\t\t\tthis.entityPosition,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t}\n\t}\n\n\tprivate declare(\n\t\tkind: ScannedDeclaration['kind'],\n\t\tcontent: string,\n\t\tend: number,\n\t): void {\n\t\tthis.options.onDeclaration?.({\n\t\t\tkind,\n\t\t\tcontent,\n\t\t\tstart: this.tagStart,\n\t\t\tend,\n\t\t\tposition: this.tagPosition,\n\t\t});\n\t}\n\n\tprivate addValueSegment(index: number, locate: Locate, textIndex: number) {\n\t\tthis.attrValueSegments.push({ index, locate, textIndex });\n\t}\n\n\tprivate startEntity(from: 'text' | 'attrValue', position: Position): void {\n\t\tthis.mode = 'entity';\n\t\tthis.entityReturn = from;\n\t\tthis.entityPosition = position;\n\t\tthis.buffer = '';\n\t}\n\n\tprivate missingValue(fail: (message: string) => never): never {\n\t\treturn fail(\n\t\t\t`The attribute '${this.attrName}' of <${this.tagName}> has no value, XML requires one: ${this.attrName}='...'`,\n\t\t);\n\t}\n\n\tprivate endAttribute(fail: (message: string) => never): void {\n\t\tif (!this.attrDynamic) {\n\t\t\tif (this.attributeNames.has(this.attrName)) {\n\t\t\t\tfail(`Duplicate attribute '${this.attrName}' in <${this.tagName}>`);\n\t\t\t}\n\t\t\tthis.attributeNames.add(this.attrName);\n\t\t}\n\t\tthis.attributes.push({\n\t\t\tname: this.attrName,\n\t\t\tvalue: this.attrValue,\n\t\t\tquote: this.quote,\n\t\t\tdynamic: this.attrDynamic || this.attrValueDynamic,\n\t\t\tpositionOf: positionInSegments(this.attrValueSegments),\n\t\t});\n\t\tthis.mode = 'inTag';\n\t\tthis.separated = false;\n\t}\n\n\tprivate endStartTag(selfClosing: boolean, end: number): void {\n\t\tif (this.options.document && this.stack.length === 0 && this.rootClosed) {\n\t\t\tthrow new XmlSyntaxError(\n\t\t\t\t`Only one root element is allowed, found <${this.tagName}> after it`,\n\t\t\t\tthis.tagPosition,\n\t\t\t);\n\t\t}\n\n\t\tthis.options.onStartTag?.({\n\t\t\tname: this.tagName,\n\t\t\tstart: this.tagStart,\n\t\t\tend,\n\t\t\tattributes: this.attributes,\n\t\t\tselfClosing,\n\t\t\tparent: this.stack.at(-1),\n\t\t});\n\n\t\tif (selfClosing) {\n\t\t\tif (this.stack.length === 0) {\n\t\t\t\tthis.rootClosed = true;\n\t\t\t}\n\t\t} else {\n\t\t\tthis.stack.push({\n\t\t\t\tname: this.tagName,\n\t\t\t\tposition: this.tagPosition,\n\t\t\t\tstart: this.tagStart,\n\t\t\t});\n\t\t}\n\t\tthis.tagName = '';\n\t\tthis.mode = 'text';\n\t}\n\n\tprivate endEndTag(): void {\n\t\t// reported where the closing tag starts\n\t\tconst fail = (message: string): never => {\n\t\t\tthrow new XmlSyntaxError(message, this.tagPosition);\n\t\t};\n\n\t\tconst open = this.stack.at(-1);\n\t\tif (open === undefined) {\n\t\t\tfail(`Closing tag </${this.tagName}> has no opening tag`);\n\t\t} else if (open.name !== this.tagName) {\n\t\t\tfail(\n\t\t\t\t`Expected </${open.name}> to close ${describeElement(open)}, found </${this.tagName}>`,\n\t\t\t);\n\t\t}\n\t\tthis.stack.pop();\n\t\tif (this.stack.length === 0) {\n\t\t\tthis.rootClosed = true;\n\t\t}\n\t\tthis.tagName = '';\n\t\tthis.mode = 'text';\n\t}\n}\n\nfunction describeElement({ name, position }: OpenElement): string {\n\treturn `<${name}> (opened at ${position.line}:${position.column + 1})`;\n}\n\nfunction positionInSegments(segments: ValueSegment[]) {\n\treturn (index: number): Position => {\n\t\tconst segment =\n\t\t\tsegments.findLast((item) => item.index <= index) ?? segments[0];\n\t\treturn segment.locate(segment.textIndex + index - segment.index);\n\t};\n}\n\n/** Positions in a single text, computed from the offsets of its line starts. */\nexport function createLocate(\n\ttext: string,\n\tstart: Position = { line: 1, column: 0 },\n): Locate {\n\tconst lineStarts = [0];\n\tfor (let i = 0; i < text.length; i++) {\n\t\tif (text[i] === '\\n') {\n\t\t\tlineStarts.push(i + 1);\n\t\t}\n\t}\n\n\treturn (index) => {\n\t\tlet low = 0;\n\t\tlet high = lineStarts.length - 1;\n\t\twhile (low < high) {\n\t\t\tconst middle = (low + high + 1) >> 1;\n\t\t\tif (lineStarts[middle] <= index) {\n\t\t\t\tlow = middle;\n\t\t\t} else {\n\t\t\t\thigh = middle - 1;\n\t\t\t}\n\t\t}\n\t\treturn low === 0\n\t\t\t? { line: start.line, column: start.column + index }\n\t\t\t: { line: start.line + low, column: index - lineStarts[low] };\n\t};\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blogger-hbs",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Build Blogger themes from Handlebars templates, with bundled scripts and styles.",
5
5
  "keywords": [
6
6
  "blogger",
@@ -46,6 +46,7 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@rsbuild/core": "^2.2.11",
49
+ "blogger-expr": "0.0.1",
49
50
  "cac": "^7.1.0",
50
51
  "handlebars": "^4.7.9",
51
52
  "normalize-path": "^3.0.0"