directives.js 6.1 KB

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