My dotfiles
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 

374 lignes
8.7 KiB

  1. 'use strict';
  2. /**
  3. * Minimalistic backwards stream reader
  4. */
  5. class StreamReader {
  6. constructor(string) {
  7. this.string = string;
  8. this.pos = this.string.length;
  9. }
  10. sol() {
  11. return this.pos === 0;
  12. }
  13. peek(offset) {
  14. return this.string.charCodeAt(this.pos - 1 + (offset || 0));
  15. }
  16. prev() {
  17. if (!this.sol()) {
  18. return this.string.charCodeAt(--this.pos);
  19. }
  20. }
  21. eat(match) {
  22. const ok = typeof match === 'function'
  23. ? match(this.peek())
  24. : match === this.peek();
  25. if (ok) {
  26. this.pos--;
  27. }
  28. return ok;
  29. }
  30. eatWhile(match) {
  31. const start = this.pos;
  32. while (this.eat(match)) {}
  33. return this.pos < start;
  34. }
  35. }
  36. /**
  37. * Quotes-related utilities
  38. */
  39. const SINGLE_QUOTE = 39; // '
  40. const DOUBLE_QUOTE = 34; // "
  41. const ESCAPE = 92; // \
  42. /**
  43. * Check if given character code is a quote
  44. * @param {Number} c
  45. * @return {Boolean}
  46. */
  47. function isQuote(c) {
  48. return c === SINGLE_QUOTE || c === DOUBLE_QUOTE;
  49. }
  50. /**
  51. * Consumes quoted value, if possible
  52. * @param {StreamReader} stream
  53. * @return {Boolean} Returns `true` is value was consumed
  54. */
  55. function eatQuoted(stream) {
  56. const start = stream.pos;
  57. const quote = stream.prev();
  58. if (isQuote(quote)) {
  59. while (!stream.sol()) {
  60. if (stream.prev() === quote && stream.peek() !== ESCAPE) {
  61. return true;
  62. }
  63. }
  64. }
  65. stream.pos = start;
  66. return false;
  67. }
  68. const TAB = 9;
  69. const SPACE = 32;
  70. const DASH = 45; // -
  71. const SLASH = 47; // /
  72. const COLON = 58; // :
  73. const EQUALS = 61; // =
  74. const ANGLE_LEFT = 60; // <
  75. const ANGLE_RIGHT = 62; // >
  76. /**
  77. * Check if given reader’s current position points at the end of HTML tag
  78. * @param {StreamReader} stream
  79. * @return {Boolean}
  80. */
  81. var isAtHTMLTag = function (stream) {
  82. const start = stream.pos;
  83. if (!stream.eat(ANGLE_RIGHT)) {
  84. return false;
  85. }
  86. let ok = false;
  87. stream.eat(SLASH); // possibly self-closed element
  88. while (!stream.sol()) {
  89. stream.eatWhile(isWhiteSpace);
  90. if (eatIdent(stream)) {
  91. // ate identifier: could be a tag name, boolean attribute or unquoted
  92. // attribute value
  93. if (stream.eat(SLASH)) {
  94. // either closing tag or invalid tag
  95. ok = stream.eat(ANGLE_LEFT);
  96. break;
  97. } else if (stream.eat(ANGLE_LEFT)) {
  98. // opening tag
  99. ok = true;
  100. break;
  101. } else if (stream.eat(isWhiteSpace)) {
  102. // boolean attribute
  103. continue;
  104. } else if (stream.eat(EQUALS)) {
  105. // simple unquoted value or invalid attribute
  106. if (eatIdent(stream)) {
  107. continue;
  108. }
  109. break;
  110. } else if (eatAttributeWithUnquotedValue(stream)) {
  111. // identifier was a part of unquoted value
  112. ok = true;
  113. break;
  114. }
  115. // invalid tag
  116. break;
  117. }
  118. if (eatAttribute(stream)) {
  119. continue;
  120. }
  121. break;
  122. }
  123. stream.pos = start;
  124. return ok;
  125. };
  126. /**
  127. * Eats HTML attribute from given string.
  128. * @param {StreamReader} state
  129. * @return {Boolean} `true` if attribute was consumed.
  130. */
  131. function eatAttribute(stream) {
  132. return eatAttributeWithQuotedValue(stream) || eatAttributeWithUnquotedValue(stream);
  133. }
  134. /**
  135. * @param {StreamReader} stream
  136. * @return {Boolean}
  137. */
  138. function eatAttributeWithQuotedValue(stream) {
  139. const start = stream.pos;
  140. if (eatQuoted(stream) && stream.eat(EQUALS) && eatIdent(stream)) {
  141. return true;
  142. }
  143. stream.pos = start;
  144. return false;
  145. }
  146. /**
  147. * @param {StreamReader} stream
  148. * @return {Boolean}
  149. */
  150. function eatAttributeWithUnquotedValue(stream) {
  151. const start = stream.pos;
  152. if (stream.eatWhile(isUnquotedValue) && stream.eat(EQUALS) && eatIdent(stream)) {
  153. return true;
  154. }
  155. stream.pos = start;
  156. return false;
  157. }
  158. /**
  159. * Eats HTML identifier from stream
  160. * @param {StreamReader} stream
  161. * @return {Boolean}
  162. */
  163. function eatIdent(stream) {
  164. return stream.eatWhile(isIdent);
  165. }
  166. /**
  167. * Check if given character code belongs to HTML identifier
  168. * @param {Number} c
  169. * @return {Boolean}
  170. */
  171. function isIdent(c) {
  172. return c === COLON || c === DASH || isAlpha(c) || isNumber(c);
  173. }
  174. /**
  175. * Check if given character code is alpha code (letter though A to Z)
  176. * @param {Number} c
  177. * @return {Boolean}
  178. */
  179. function isAlpha(c) {
  180. c &= ~32; // quick hack to convert any char code to uppercase char code
  181. return c >= 65 && c <= 90; // A-Z
  182. }
  183. /**
  184. * Check if given code is a number
  185. * @param {Number} c
  186. * @return {Boolean}
  187. */
  188. function isNumber(c) {
  189. return c > 47 && c < 58;
  190. }
  191. /**
  192. * Check if given code is a whitespace
  193. * @param {Number} c
  194. * @return {Boolean}
  195. */
  196. function isWhiteSpace(c) {
  197. return c === SPACE || c === TAB;
  198. }
  199. /**
  200. * Check if given code may belong to unquoted attribute value
  201. * @param {Number} c
  202. * @return {Boolean}
  203. */
  204. function isUnquotedValue(c) {
  205. return c && c !== EQUALS && !isWhiteSpace(c) && !isQuote(c);
  206. }
  207. const code = ch => ch.charCodeAt(0);
  208. const SQUARE_BRACE_L = code('[');
  209. const SQUARE_BRACE_R = code(']');
  210. const ROUND_BRACE_L = code('(');
  211. const ROUND_BRACE_R = code(')');
  212. const CURLY_BRACE_L = code('{');
  213. const CURLY_BRACE_R = code('}');
  214. const specialChars = new Set('#.*:$-_!@%^+>/'.split('').map(code));
  215. const bracePairs = new Map()
  216. .set(SQUARE_BRACE_L, SQUARE_BRACE_R)
  217. .set(ROUND_BRACE_L, ROUND_BRACE_R)
  218. .set(CURLY_BRACE_L, CURLY_BRACE_R);
  219. const defaultOptions = {
  220. syntax: 'markup',
  221. lookAhead: null
  222. };
  223. /**
  224. * Extracts Emmet abbreviation from given string.
  225. * The goal of this module is to extract abbreviation from current editor’s line,
  226. * e.g. like this: `<span>.foo[title=bar|]</span>` -> `.foo[title=bar]`, where
  227. * `|` is a current caret position.
  228. * @param {String} line A text line where abbreviation should be expanded
  229. * @param {Number} [pos] Caret position in line. If not given, uses end-of-line
  230. * @param {Object} [options]
  231. * @param {Boolean} [options.lookAhead] Allow parser to look ahead of `pos` index for
  232. * searching of missing abbreviation parts. Most editors automatically inserts
  233. * closing braces for `[`, `{` and `(`, which will most likely be right after
  234. * current caret position. So in order to properly expand abbreviation, user
  235. * must explicitly move caret right after auto-inserted braces. Whith this option
  236. * enabled, parser will search for closing braces right after `pos`. Default is `true`
  237. * @param {String} [options.syntax] Name of context syntax of expanded abbreviation.
  238. * Either 'markup' (default) or 'stylesheet'. In 'stylesheet' syntax, braces `[]`
  239. * and `{}` are not supported thus not extracted.
  240. * @return {Object} Object with `abbreviation` and its `location` in given line
  241. * if abbreviation can be extracted, `null` otherwise
  242. */
  243. function extractAbbreviation(line, pos, options) {
  244. // make sure `pos` is within line range
  245. pos = Math.min(line.length, Math.max(0, pos == null ? line.length : pos));
  246. if (typeof options === 'boolean') {
  247. options = Object.assign(defaultOptions, { lookAhead: options });
  248. } else {
  249. options = Object.assign(defaultOptions, options);
  250. }
  251. if (options.lookAhead == null || options.lookAhead === true) {
  252. pos = offsetPastAutoClosed(line, pos, options);
  253. }
  254. let c;
  255. const stream = new StreamReader(line);
  256. stream.pos = pos;
  257. const stack = [];
  258. while (!stream.sol()) {
  259. c = stream.peek();
  260. if (isCloseBrace(c, options.syntax)) {
  261. stack.push(c);
  262. } else if (isOpenBrace(c, options.syntax)) {
  263. if (stack.pop() !== bracePairs.get(c)) {
  264. // unexpected brace
  265. break;
  266. }
  267. } else if (has(stack, SQUARE_BRACE_R) || has(stack, CURLY_BRACE_R)) {
  268. // respect all characters inside attribute sets or text nodes
  269. stream.pos--;
  270. continue;
  271. } else if (isAtHTMLTag(stream) || !isAbbreviation(c)) {
  272. break;
  273. }
  274. stream.pos--;
  275. }
  276. if (!stack.length && stream.pos !== pos) {
  277. // found something, remove some invalid symbols from the
  278. // beginning and return abbreviation
  279. const abbreviation = line.slice(stream.pos, pos).replace(/^[*+>^]+/, '');
  280. return {
  281. abbreviation,
  282. location: pos - abbreviation.length
  283. };
  284. }
  285. }
  286. /**
  287. * Returns new `line` index which is right after characters beyound `pos` that
  288. * edditor will likely automatically close, e.g. }, ], and quotes
  289. * @param {String} line
  290. * @param {Number} pos
  291. * @return {Number}
  292. */
  293. function offsetPastAutoClosed(line, pos, options) {
  294. // closing quote is allowed only as a next character
  295. if (isQuote(line.charCodeAt(pos))) {
  296. pos++;
  297. }
  298. // offset pointer until non-autoclosed character is found
  299. while (isCloseBrace(line.charCodeAt(pos), options.syntax)) {
  300. pos++;
  301. }
  302. return pos;
  303. }
  304. function has(arr, value) {
  305. return arr.indexOf(value) !== -1;
  306. }
  307. function isAbbreviation(c) {
  308. return (c > 64 && c < 91) // uppercase letter
  309. || (c > 96 && c < 123) // lowercase letter
  310. || (c > 47 && c < 58) // number
  311. || specialChars.has(c); // special character
  312. }
  313. function isOpenBrace(c, syntax) {
  314. return c === ROUND_BRACE_L || (syntax === 'markup' && (c === SQUARE_BRACE_L || c === CURLY_BRACE_L));
  315. }
  316. function isCloseBrace(c, syntax) {
  317. return c === ROUND_BRACE_R || (syntax === 'markup' && (c === SQUARE_BRACE_R || c === CURLY_BRACE_R));
  318. }
  319. module.exports = extractAbbreviation;