directives.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  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(/^(.*!)([^!]*)$/);
  120. if (!suffix)
  121. onError(`The ${source} tag has no suffix`);
  122. const prefix = this.tags[handle];
  123. if (prefix)
  124. return prefix + decodeURIComponent(suffix);
  125. if (handle === '!')
  126. return source; // local tag
  127. onError(`Could not resolve tag: ${source}`);
  128. return null;
  129. }
  130. /**
  131. * Given a fully resolved tag, returns its printable string form,
  132. * taking into account current tag prefixes and defaults.
  133. */
  134. tagString(tag) {
  135. for (const [handle, prefix] of Object.entries(this.tags)) {
  136. if (tag.startsWith(prefix))
  137. return handle + escapeTagName(tag.substring(prefix.length));
  138. }
  139. return tag[0] === '!' ? tag : `!<${tag}>`;
  140. }
  141. toString(doc) {
  142. const lines = this.yaml.explicit
  143. ? [`%YAML ${this.yaml.version || '1.2'}`]
  144. : [];
  145. const tagEntries = Object.entries(this.tags);
  146. let tagNames;
  147. if (doc && tagEntries.length > 0 && identity.isNode(doc.contents)) {
  148. const tags = {};
  149. visit.visit(doc.contents, (_key, node) => {
  150. if (identity.isNode(node) && node.tag)
  151. tags[node.tag] = true;
  152. });
  153. tagNames = Object.keys(tags);
  154. }
  155. else
  156. tagNames = [];
  157. for (const [handle, prefix] of tagEntries) {
  158. if (handle === '!!' && prefix === 'tag:yaml.org,2002:')
  159. continue;
  160. if (!doc || tagNames.some(tn => tn.startsWith(prefix)))
  161. lines.push(`%TAG ${handle} ${prefix}`);
  162. }
  163. return lines.join('\n');
  164. }
  165. }
  166. Directives.defaultYaml = { explicit: false, version: '1.2' };
  167. Directives.defaultTags = { '!!': 'tag:yaml.org,2002:' };
  168. exports.Directives = Directives;