mirror of
				https://github.com/Theodor-Springmann-Stiftung/kgpz_web.git
				synced 2025-10-31 09:55:30 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			177 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			177 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import { isNode } from '../nodes/identity.js';
 | |
| import { visit } from '../visit.js';
 | |
| 
 | |
| const escapeChars = {
 | |
|     '!': '%21',
 | |
|     ',': '%2C',
 | |
|     '[': '%5B',
 | |
|     ']': '%5D',
 | |
|     '{': '%7B',
 | |
|     '}': '%7D'
 | |
| };
 | |
| const escapeTagName = (tn) => tn.replace(/[!,[\]{}]/g, ch => escapeChars[ch]);
 | |
| class Directives {
 | |
|     constructor(yaml, tags) {
 | |
|         /**
 | |
|          * The directives-end/doc-start marker `---`. If `null`, a marker may still be
 | |
|          * included in the document's stringified representation.
 | |
|          */
 | |
|         this.docStart = null;
 | |
|         /** The doc-end marker `...`.  */
 | |
|         this.docEnd = false;
 | |
|         this.yaml = Object.assign({}, Directives.defaultYaml, yaml);
 | |
|         this.tags = Object.assign({}, Directives.defaultTags, tags);
 | |
|     }
 | |
|     clone() {
 | |
|         const copy = new Directives(this.yaml, this.tags);
 | |
|         copy.docStart = this.docStart;
 | |
|         return copy;
 | |
|     }
 | |
|     /**
 | |
|      * During parsing, get a Directives instance for the current document and
 | |
|      * update the stream state according to the current version's spec.
 | |
|      */
 | |
|     atDocument() {
 | |
|         const res = new Directives(this.yaml, this.tags);
 | |
|         switch (this.yaml.version) {
 | |
|             case '1.1':
 | |
|                 this.atNextDocument = true;
 | |
|                 break;
 | |
|             case '1.2':
 | |
|                 this.atNextDocument = false;
 | |
|                 this.yaml = {
 | |
|                     explicit: Directives.defaultYaml.explicit,
 | |
|                     version: '1.2'
 | |
|                 };
 | |
|                 this.tags = Object.assign({}, Directives.defaultTags);
 | |
|                 break;
 | |
|         }
 | |
|         return res;
 | |
|     }
 | |
|     /**
 | |
|      * @param onError - May be called even if the action was successful
 | |
|      * @returns `true` on success
 | |
|      */
 | |
|     add(line, onError) {
 | |
|         if (this.atNextDocument) {
 | |
|             this.yaml = { explicit: Directives.defaultYaml.explicit, version: '1.1' };
 | |
|             this.tags = Object.assign({}, Directives.defaultTags);
 | |
|             this.atNextDocument = false;
 | |
|         }
 | |
|         const parts = line.trim().split(/[ \t]+/);
 | |
|         const name = parts.shift();
 | |
|         switch (name) {
 | |
|             case '%TAG': {
 | |
|                 if (parts.length !== 2) {
 | |
|                     onError(0, '%TAG directive should contain exactly two parts');
 | |
|                     if (parts.length < 2)
 | |
|                         return false;
 | |
|                 }
 | |
|                 const [handle, prefix] = parts;
 | |
|                 this.tags[handle] = prefix;
 | |
|                 return true;
 | |
|             }
 | |
|             case '%YAML': {
 | |
|                 this.yaml.explicit = true;
 | |
|                 if (parts.length !== 1) {
 | |
|                     onError(0, '%YAML directive should contain exactly one part');
 | |
|                     return false;
 | |
|                 }
 | |
|                 const [version] = parts;
 | |
|                 if (version === '1.1' || version === '1.2') {
 | |
|                     this.yaml.version = version;
 | |
|                     return true;
 | |
|                 }
 | |
|                 else {
 | |
|                     const isValid = /^\d+\.\d+$/.test(version);
 | |
|                     onError(6, `Unsupported YAML version ${version}`, isValid);
 | |
|                     return false;
 | |
|                 }
 | |
|             }
 | |
|             default:
 | |
|                 onError(0, `Unknown directive ${name}`, true);
 | |
|                 return false;
 | |
|         }
 | |
|     }
 | |
|     /**
 | |
|      * Resolves a tag, matching handles to those defined in %TAG directives.
 | |
|      *
 | |
|      * @returns Resolved tag, which may also be the non-specific tag `'!'` or a
 | |
|      *   `'!local'` tag, or `null` if unresolvable.
 | |
|      */
 | |
|     tagName(source, onError) {
 | |
|         if (source === '!')
 | |
|             return '!'; // non-specific tag
 | |
|         if (source[0] !== '!') {
 | |
|             onError(`Not a valid tag: ${source}`);
 | |
|             return null;
 | |
|         }
 | |
|         if (source[1] === '<') {
 | |
|             const verbatim = source.slice(2, -1);
 | |
|             if (verbatim === '!' || verbatim === '!!') {
 | |
|                 onError(`Verbatim tags aren't resolved, so ${source} is invalid.`);
 | |
|                 return null;
 | |
|             }
 | |
|             if (source[source.length - 1] !== '>')
 | |
|                 onError('Verbatim tags must end with a >');
 | |
|             return verbatim;
 | |
|         }
 | |
|         const [, handle, suffix] = source.match(/^(.*!)([^!]*)$/s);
 | |
|         if (!suffix)
 | |
|             onError(`The ${source} tag has no suffix`);
 | |
|         const prefix = this.tags[handle];
 | |
|         if (prefix) {
 | |
|             try {
 | |
|                 return prefix + decodeURIComponent(suffix);
 | |
|             }
 | |
|             catch (error) {
 | |
|                 onError(String(error));
 | |
|                 return null;
 | |
|             }
 | |
|         }
 | |
|         if (handle === '!')
 | |
|             return source; // local tag
 | |
|         onError(`Could not resolve tag: ${source}`);
 | |
|         return null;
 | |
|     }
 | |
|     /**
 | |
|      * Given a fully resolved tag, returns its printable string form,
 | |
|      * taking into account current tag prefixes and defaults.
 | |
|      */
 | |
|     tagString(tag) {
 | |
|         for (const [handle, prefix] of Object.entries(this.tags)) {
 | |
|             if (tag.startsWith(prefix))
 | |
|                 return handle + escapeTagName(tag.substring(prefix.length));
 | |
|         }
 | |
|         return tag[0] === '!' ? tag : `!<${tag}>`;
 | |
|     }
 | |
|     toString(doc) {
 | |
|         const lines = this.yaml.explicit
 | |
|             ? [`%YAML ${this.yaml.version || '1.2'}`]
 | |
|             : [];
 | |
|         const tagEntries = Object.entries(this.tags);
 | |
|         let tagNames;
 | |
|         if (doc && tagEntries.length > 0 && isNode(doc.contents)) {
 | |
|             const tags = {};
 | |
|             visit(doc.contents, (_key, node) => {
 | |
|                 if (isNode(node) && node.tag)
 | |
|                     tags[node.tag] = true;
 | |
|             });
 | |
|             tagNames = Object.keys(tags);
 | |
|         }
 | |
|         else
 | |
|             tagNames = [];
 | |
|         for (const [handle, prefix] of tagEntries) {
 | |
|             if (handle === '!!' && prefix === 'tag:yaml.org,2002:')
 | |
|                 continue;
 | |
|             if (!doc || tagNames.some(tn => tn.startsWith(prefix)))
 | |
|                 lines.push(`%TAG ${handle} ${prefix}`);
 | |
|         }
 | |
|         return lines.join('\n');
 | |
|     }
 | |
| }
 | |
| Directives.defaultYaml = { explicit: false, version: '1.2' };
 | |
| Directives.defaultTags = { '!!': 'tag:yaml.org,2002:' };
 | |
| 
 | |
| export { Directives };
 | 
