directives.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import { isNode } from '../nodes/identity.js';
  2. import { visit } from '../visit.js';
  3. const escapeChars = {
  4. '!': '%21',
  5. ',': '%2C',
  6. '[': '%5B',
  7. ']': '%5D',
  8. '{': '%7B',
  9. '}': '%7D'
  10. };
  11. const escapeTagName = (tn) => tn.replace(/[!,[\]{}]/g, ch => escapeChars[ch]);
  12. class Directives {
  13. constructor(yaml, tags) {
  14. /**
  15. * The directives-end/doc-start marker `---`. If `null`, a marker may still be
  16. * included in the document's stringified representation.
  17. */
  18. this.docStart = null;
  19. /** The doc-end marker `...`. */
  20. this.docEnd = false;
  21. this.yaml = Object.assign({}, Directives.defaultYaml, yaml);
  22. this.tags = Object.assign({}, Directives.defaultTags, tags);
  23. }
  24. clone() {
  25. const copy = new Directives(this.yaml, this.tags);
  26. copy.docStart = this.docStart;
  27. return copy;
  28. }
  29. /**
  30. * During parsing, get a Directives instance for the current document and
  31. * update the stream state according to the current version's spec.
  32. */
  33. atDocument() {
  34. const res = new Directives(this.yaml, this.tags);
  35. switch (this.yaml.version) {
  36. case '1.1':
  37. this.atNextDocument = true;
  38. break;
  39. case '1.2':
  40. this.atNextDocument = false;
  41. this.yaml = {
  42. explicit: Directives.defaultYaml.explicit,
  43. version: '1.2'
  44. };
  45. this.tags = Object.assign({}, Directives.defaultTags);
  46. break;
  47. }
  48. return res;
  49. }
  50. /**
  51. * @param onError - May be called even if the action was successful
  52. * @returns `true` on success
  53. */
  54. add(line, onError) {
  55. if (this.atNextDocument) {
  56. this.yaml = { explicit: Directives.defaultYaml.explicit, version: '1.1' };
  57. this.tags = Object.assign({}, Directives.defaultTags);
  58. this.atNextDocument = false;
  59. }
  60. const parts = line.trim().split(/[ \t]+/);
  61. const name = parts.shift();
  62. switch (name) {
  63. case '%TAG': {
  64. if (parts.length !== 2) {
  65. onError(0, '%TAG directive should contain exactly two parts');
  66. if (parts.length < 2)
  67. return false;
  68. }
  69. const [handle, prefix] = parts;
  70. this.tags[handle] = prefix;
  71. return true;
  72. }
  73. case '%YAML': {
  74. this.yaml.explicit = true;
  75. if (parts.length !== 1) {
  76. onError(0, '%YAML directive should contain exactly one part');
  77. return false;
  78. }
  79. const [version] = parts;
  80. if (version === '1.1' || version === '1.2') {
  81. this.yaml.version = version;
  82. return true;
  83. }
  84. else {
  85. const isValid = /^\d+\.\d+$/.test(version);
  86. onError(6, `Unsupported YAML version ${version}`, isValid);
  87. return false;
  88. }
  89. }
  90. default:
  91. onError(0, `Unknown directive ${name}`, true);
  92. return false;
  93. }
  94. }
  95. /**
  96. * Resolves a tag, matching handles to those defined in %TAG directives.
  97. *
  98. * @returns Resolved tag, which may also be the non-specific tag `'!'` or a
  99. * `'!local'` tag, or `null` if unresolvable.
  100. */
  101. tagName(source, onError) {
  102. if (source === '!')
  103. return '!'; // non-specific tag
  104. if (source[0] !== '!') {
  105. onError(`Not a valid tag: ${source}`);
  106. return null;
  107. }
  108. if (source[1] === '<') {
  109. const verbatim = source.slice(2, -1);
  110. if (verbatim === '!' || verbatim === '!!') {
  111. onError(`Verbatim tags aren't resolved, so ${source} is invalid.`);
  112. return null;
  113. }
  114. if (source[source.length - 1] !== '>')
  115. onError('Verbatim tags must end with a >');
  116. return verbatim;
  117. }
  118. const [, handle, suffix] = source.match(/^(.*!)([^!]*)$/s);
  119. if (!suffix)
  120. onError(`The ${source} tag has no suffix`);
  121. const prefix = this.tags[handle];
  122. if (prefix) {
  123. try {
  124. return prefix + decodeURIComponent(suffix);
  125. }
  126. catch (error) {
  127. onError(String(error));
  128. return null;
  129. }
  130. }
  131. if (handle === '!')
  132. return source; // local tag
  133. onError(`Could not resolve tag: ${source}`);
  134. return null;
  135. }
  136. /**
  137. * Given a fully resolved tag, returns its printable string form,
  138. * taking into account current tag prefixes and defaults.
  139. */
  140. tagString(tag) {
  141. for (const [handle, prefix] of Object.entries(this.tags)) {
  142. if (tag.startsWith(prefix))
  143. return handle + escapeTagName(tag.substring(prefix.length));
  144. }
  145. return tag[0] === '!' ? tag : `!<${tag}>`;
  146. }
  147. toString(doc) {
  148. const lines = this.yaml.explicit
  149. ? [`%YAML ${this.yaml.version || '1.2'}`]
  150. : [];
  151. const tagEntries = Object.entries(this.tags);
  152. let tagNames;
  153. if (doc && tagEntries.length > 0 && isNode(doc.contents)) {
  154. const tags = {};
  155. visit(doc.contents, (_key, node) => {
  156. if (isNode(node) && node.tag)
  157. tags[node.tag] = true;
  158. });
  159. tagNames = Object.keys(tags);
  160. }
  161. else
  162. tagNames = [];
  163. for (const [handle, prefix] of tagEntries) {
  164. if (handle === '!!' && prefix === 'tag:yaml.org,2002:')
  165. continue;
  166. if (!doc || tagNames.some(tn => tn.startsWith(prefix)))
  167. lines.push(`%TAG ${handle} ${prefix}`);
  168. }
  169. return lines.join('\n');
  170. }
  171. }
  172. Directives.defaultYaml = { explicit: false, version: '1.2' };
  173. Directives.defaultTags = { '!!': 'tag:yaml.org,2002:' };
  174. export { Directives };