My dotfiles
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 

372 Zeilen
8.7 KiB

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