UNPKG

cspell-config-lib

Version:
573 lines 17.9 kB
import assert from 'node:assert'; import { Document as YamlDocument, isAlias, isMap, isNode, isPair, isScalar, isSeq, parseDocument, Scalar, stringify, visit as yamlWalkAst, YAMLMap, YAMLSeq, } from 'yaml'; import { MutableCSpellConfigFile } from '../CSpellConfigFile.js'; import { detectIndentAsNum } from '../serializers/util.js'; import { isNodeValue } from '../UpdateConfig/CfgTree.js'; import { ParseError } from './Errors.js'; export class CSpellConfigFileYaml extends MutableCSpellConfigFile { url; yamlDoc; indent; #settings = undefined; constructor(url, yamlDoc, indent) { super(url); this.url = url; this.yamlDoc = yamlDoc; this.indent = indent; // Set the initial settings from the YAML document. this.#settings = this.yamlDoc.toJS(); } get settings() { return this.#settings ?? this.yamlDoc.toJS(); } addWords(wordsToAdd) { const cfgWords = this.yamlDoc.get('words') || new YAMLSeq(); assert(isSeq(cfgWords), 'Expected words to be a YAML sequence'); const knownWords = new Set(cfgWords.items.map((item) => getScalarValue(item))); wordsToAdd.forEach((w) => { if (knownWords.has(w)) return; cfgWords.add(w); knownWords.add(w); }); const sorted = sortWords(cfgWords.items); sorted.forEach((item, index) => cfgWords.set(index, item)); cfgWords.items.length = sorted.length; this.#setValue('words', cfgWords); this.#markAsMutable(); return this; } serialize() { return stringify(this.yamlDoc, { indent: this.indent }); } setValue(key, value) { if (isNodeValue(value)) { let node = this.#getNode(key); if (!node) { node = this.yamlDoc.createNode(value.value); setYamlNodeComments(node, value); this.#setValue(key, node); } else { setYamlNodeValue(node, value); } } else { this.#setValue(key, value); } this.#markAsMutable(); return this; } getValue(key) { const node = this.#getNode(key); return node?.toJS(this.yamlDoc); } #getNode(key) { return getYamlNode(this.yamlDoc, key); } getNode(key, defaultValue) { let yNode = this.#getNode(key); if (!yNode) { if (defaultValue === undefined) { return undefined; } yNode = this.yamlDoc.createNode(defaultValue); this.#setValue(key, yNode); } this.#markAsMutable(); return toConfigNode(this.yamlDoc, yNode); } getFieldNode(key) { const contents = this.yamlDoc.contents; if (!isMap(contents)) { return undefined; } const found = findPair(contents, key); const pair = found && this.#fixPair(found); if (!pair) { return undefined; } return toConfigNode(this.yamlDoc, pair.key); } /** * Removes a value from the document. * @returns `true` if the item was found and removed. */ delete(key) { const removed = this.yamlDoc.delete(key); if (removed) { this.#markAsMutable(); } return removed; } get comment() { return this.yamlDoc.comment ?? undefined; } set comment(comment) { // eslint-disable-next-line unicorn/no-null this.yamlDoc.comment = comment ?? null; } setSchema(schemaRef) { removeSchemaComment(this.yamlDoc); let commentBefore = this.yamlDoc.commentBefore || ''; commentBefore = commentBefore.replace(/^ yaml-language-server: \$schema=.*\n?/m, ''); commentBefore = ` yaml-language-server: $schema=${schemaRef}` + (commentBefore ? '\n' + commentBefore : ''); this.yamlDoc.commentBefore = commentBefore; // Remove any existing comment references that might be attached to the first field. const contents = this.#getContentsMap(); const firstPair = contents.items[0]; if (firstPair && isPair(firstPair)) { const key = firstPair.key; if (isNode(key)) { removeSchemaComment(key); } } if (this.getNode('$schema')) { this.setValue('$schema', schemaRef); } return this; } removeAllComments() { const doc = this.yamlDoc; // eslint-disable-next-line unicorn/no-null doc.comment = null; // eslint-disable-next-line unicorn/no-null doc.commentBefore = null; yamlWalkAst(this.yamlDoc, (_, node) => { if (!(isScalar(node) || isMap(node) || isSeq(node))) return; // eslint-disable-next-line unicorn/no-null node.comment = null; // eslint-disable-next-line unicorn/no-null node.commentBefore = null; }); return this; } setComment(key, comment, inline) { const node = this.getFieldNode(key); if (!node) return this; if (inline) { node.comment = comment; } else { node.commentBefore = comment; } return this; } /** * Marks the config file as mutable. Any access to settings will the settings to be regenerated * from the YAML document. */ #markAsMutable() { this.#settings = undefined; } #setValue(key, value) { this.yamlDoc.set(key, value); const contents = this.#getContentsMap(); const pair = findPair(contents, key); assert(pair, `Expected pair for key: ${String(key)}`); this.#fixPair(pair); } #toNode(value) { return (isNode(value) ? value : this.yamlDoc.createNode(value)); } #fixPair(pair) { assert(isPair(pair), 'Expected pair to be a Pair'); pair.key = this.#toNode(pair.key); pair.value = this.#toNode(pair.value); return pair; } #getContentsMap() { const contents = this.yamlDoc.contents; assert(isMap(contents), 'Expected contents to be a YAMLMap'); return contents; } static parse(file) { return parseCSpellConfigFileYaml(file); } static from(url, settings, indent = 2) { const yamlDoc = new YamlDocument(settings); return new CSpellConfigFileYaml(url, yamlDoc, indent); } } export function parseCSpellConfigFileYaml(file) { const { url, content } = file; try { const doc = parseDocument(content); // Force empty content to be a map. if (doc.contents === null || (isScalar(doc.contents) && !doc.contents.value)) { doc.contents = new YAMLMap(); } if (!isMap(doc.contents)) { throw new ParseError(url, `Invalid YAML content ${url}`); } const indent = detectIndentAsNum(content); return new CSpellConfigFileYaml(url, doc, indent); } catch (e) { if (e instanceof ParseError) { throw e; } throw new ParseError(url, undefined, { cause: e }); } } function getScalarValue(node) { if (isScalar(node)) { return node.value; } return node; } function toScalar(node) { if (isScalar(node)) { return node; } return new Scalar(node); } function groupWords(words) { const groups = []; if (words.length === 0) { return groups; } let currentGroup = []; groups.push(currentGroup); for (const word of words) { if (isSectionHeader(word)) { currentGroup = []; groups.push(currentGroup); } currentGroup.push(cloneWord(word)); } return groups; } function isSectionHeader(word) { if (!isScalar(word) || (!word.commentBefore && !word.spaceBefore)) return false; if (word.spaceBefore) return true; if (!word.commentBefore) return false; return word.commentBefore.includes('\n\n'); } function adjustSectionHeader(word, prev, isFirstSection) { // console.log('adjustSectionHeader %o', { word, prev, isFirstSection }); if (!isScalar(prev)) return; let captureComment = isFirstSection; if (prev.spaceBefore) { word.spaceBefore = true; captureComment = true; delete prev.spaceBefore; } if (!prev.commentBefore) return; const originalComment = prev.commentBefore; const lines = originalComment.split(/^\n/gm); const lastLine = lines[lines.length - 1]; // console.log('adjustSectionHeader lines %o', { lines, isFirstSection, lastLine, originalComment }); captureComment = (captureComment && originalComment.trim() === lastLine.trim()) || originalComment.endsWith('\n'); let header = originalComment; if (captureComment) { delete prev.commentBefore; } else { prev.commentBefore = lastLine; lines.pop(); header = lines.join('\n'); } if (word.commentBefore) { header += header.endsWith('\n\n') ? '' : '\n'; header += header.endsWith('\n\n') ? '' : '\n'; header += word.commentBefore; } word.commentBefore = header; // console.log('adjustSectionHeader after %o', { word, prev, isFirstSection, originalComment, lastLine, lines }); } function sortWords(words) { const compare = new Intl.Collator().compare; const groups = groupWords(words); let firstGroup = true; for (const group of groups) { const head = group[0]; group.sort((a, b) => { return compare(getScalarValue(a), getScalarValue(b)); }); if (group[0] !== head && isScalar(head)) { const first = (group[0] = toScalar(group[0])); adjustSectionHeader(first, head, firstGroup); } firstGroup = false; } const result = groups.flat(); return result.map((w) => toScalar(w)); } function cloneWord(word) { if (isScalar(word)) { return word.clone(); } return word; } function getYamlNode(yamlDoc, key) { return (Array.isArray(key) ? yamlDoc.getIn(key, true) : yamlDoc.get(key, true)); } function toConfigNode(doc, yNode) { if (isYamlSeq(yNode)) { return toConfigArrayNode(doc, yNode); } if (isMap(yNode)) { return toConfigObjectNode(doc, yNode); } if (isScalar(yNode)) { return toConfigScalarNode(doc, yNode); } throw new Error(`Unsupported YAML node type: ${yamlNodeType(yNode)}`); } class ConfigNodeBase { type; constructor(type) { this.type = type; } } class ConfigArrayNode extends ConfigNodeBase { #doc; #yNode; constructor(doc, yNode) { super('array'); this.#doc = doc; this.#yNode = yNode; } get value() { return this.#yNode.toJS(this.#doc); } get comment() { return this.#yNode.comment ?? undefined; } set comment(comment) { // eslint-disable-next-line unicorn/no-null this.#yNode.comment = comment ?? null; } get commentBefore() { return this.#yNode.commentBefore ?? undefined; } set commentBefore(comment) { // eslint-disable-next-line unicorn/no-null this.#yNode.commentBefore = comment ?? null; } getNode(key) { const node = getYamlNode(this.#yNode, key); if (!node) return undefined; return toConfigNode(this.#doc, node); } getValue(key) { const node = getYamlNode(this.#yNode, key); if (!node) return undefined; return node.toJS(this.#doc); } setValue(key, value) { if (!isNodeValue(value)) { this.#yNode.set(key, value); return; } this.#yNode.set(key, value.value); const yNodeValue = getYamlNode(this.#yNode, key); assert(yNodeValue); // eslint-disable-next-line unicorn/no-null yNodeValue.comment = value.comment ?? null; // eslint-disable-next-line unicorn/no-null yNodeValue.commentBefore = value.commentBefore ?? null; } delete(key) { return this.#yNode.delete(key); } push(value) { if (!isNodeValue(value)) { this.#yNode.add(value); return this.#yNode.items.length; } this.#yNode.add(value.value); setYamlNodeComments(getYamlNode(this.#yNode, this.#yNode.items.length - 1), value); return this.#yNode.items.length; } get length() { return this.#yNode.items.length; } } function toConfigArrayNode(doc, yNode) { return new ConfigArrayNode(doc, yNode); } class ConfigObjectNode extends ConfigNodeBase { #doc; #yNode; constructor(doc, yNode) { super('object'); this.#doc = doc; this.#yNode = yNode; } get value() { return this.#yNode.toJS(this.#doc); } get comment() { return this.#yNode.comment ?? undefined; } set comment(comment) { // eslint-disable-next-line unicorn/no-null this.#yNode.comment = comment ?? null; } get commentBefore() { return this.#yNode.commentBefore ?? undefined; } set commentBefore(comment) { // eslint-disable-next-line unicorn/no-null this.#yNode.commentBefore = comment ?? null; } getValue(key) { const node = getYamlNode(this.#yNode, key); if (!node) return undefined; return node.toJS(this.#doc); } getNode(key) { const node = getYamlNode(this.#yNode, key); if (!node) return undefined; return toConfigNode(this.#doc, node); } setValue(key, value) { if (!isNodeValue(value)) { this.#yNode.set(key, value); return; } this.#yNode.set(key, value.value); const yNodeValue = getYamlNode(this.#yNode, key); assert(yNodeValue); // eslint-disable-next-line unicorn/no-null yNodeValue.comment = value.comment ?? null; // eslint-disable-next-line unicorn/no-null yNodeValue.commentBefore = value.commentBefore ?? null; } delete(key) { return this.#yNode.delete(key); } } function toConfigObjectNode(doc, yNode) { return new ConfigObjectNode(doc, yNode); } class ConfigScalarNode extends ConfigNodeBase { $doc; $yNode; type = 'scalar'; constructor(doc, yNode) { super('scalar'); this.$doc = doc; this.$yNode = yNode; assert(isScalar(yNode), 'Expected yNode to be a Scalar'); } get value() { return this.$yNode.toJS(this.$doc); } set value(value) { this.$yNode.value = value; } get comment() { return this.$yNode.comment ?? undefined; } set comment(comment) { // eslint-disable-next-line unicorn/no-null this.$yNode.comment = comment ?? null; } get commentBefore() { return this.$yNode.commentBefore ?? undefined; } set commentBefore(comment) { // eslint-disable-next-line unicorn/no-null this.$yNode.commentBefore = comment ?? null; } toJSON() { return { type: this.type, value: this.value, comment: this.comment, commentBefore: this.commentBefore, }; } } function toConfigScalarNode(doc, yNode) { return new ConfigScalarNode(doc, yNode); } function isYamlSeq(node) { return isSeq(node); } function yamlNodeType(node) { if (isScalar(node)) return 'scalar'; if (isSeq(node)) return 'seq'; if (isMap(node)) return 'map'; if (isAlias(node)) return 'alias'; return 'unknown'; } function setYamlNodeComments(yamlNode, comments) { if (!yamlNode) return; if ('comment' in comments) { // eslint-disable-next-line unicorn/no-null yamlNode.comment = comments.comment ?? null; } if ('commentBefore' in comments) { // eslint-disable-next-line unicorn/no-null yamlNode.commentBefore = comments.commentBefore ?? null; } } function setYamlNodeValue(yamlNode, nodeValue) { setYamlNodeComments(yamlNode, nodeValue); if (isScalar(yamlNode)) { yamlNode.value = nodeValue.value; return; } const value = nodeValue.value; if (isSeq(yamlNode)) { assert(Array.isArray(value), 'Expected value to be an array for YAMLSeq'); yamlNode.items = []; for (let i = 0; i < value.length; ++i) { yamlNode.set(i, value[i]); } return; } if (isMap(yamlNode)) { assert(typeof value === 'object' && value !== null, 'Expected value to be an object for YAMLMap'); yamlNode.items = []; for (const [key, val] of Object.entries(value)) { yamlNode.set(key, val); } return; } throw new Error(`Unsupported YAML node type: ${yamlNodeType(yamlNode)}`); } function findPair(yNode, yKey) { const key = isScalar(yKey) ? yKey.value : yKey; if (!isMap(yNode)) return undefined; const items = yNode.items; for (const item of items) { if (!isPair(item)) continue; if (item.key === key) { return item; } if (isScalar(item.key) && item.key.value === key) { return item; } } return undefined; } function removeSchemaComment(node) { if (!node.commentBefore) return; // eslint-disable-next-line unicorn/no-null node.commentBefore = node.commentBefore?.replaceAll(/^ yaml-language-server: \$schema=.*\n?/gm, '') ?? null; } //# sourceMappingURL=CSpellConfigFileYaml.js.map