You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

358 lines
11 KiB

  1. 'use strict';
  2. import { createHash, HexBase64Latin1Encoding } from 'crypto';
  3. const emptyStr = '';
  4. export namespace Strings {
  5. export const enum CharCode {
  6. /**
  7. * The `/` character.
  8. */
  9. Slash = 47,
  10. /**
  11. * The `\` character.
  12. */
  13. Backslash = 92
  14. }
  15. const escapeMarkdownRegex = /[\\`*_{}[\]()#+\-.!]/g;
  16. const escapeMarkdownHeaderRegex = /^===/gm;
  17. // const sampleMarkdown = '## message `not code` *not important* _no underline_ \n> don\'t quote me \n- don\'t list me \n+ don\'t list me \n1. don\'t list me \nnot h1 \n=== \nnot h2 \n---\n***\n---\n___';
  18. const markdownQuotedRegex = /\n/g;
  19. export function escapeMarkdown(s: string, options: { quoted?: boolean } = {}): string {
  20. s = s
  21. // Escape markdown
  22. .replace(escapeMarkdownRegex, '\\$&')
  23. // Escape markdown header (since the above regex won't match it)
  24. .replace(escapeMarkdownHeaderRegex, '\u200b===');
  25. if (!options.quoted) return s;
  26. // Keep under the same block-quote but with line breaks
  27. return s.replace(markdownQuotedRegex, '\t\n> ');
  28. }
  29. export function getCommonBase(s1: string, s2: string, delimiter: string) {
  30. let char;
  31. let index = 0;
  32. for (let i = 0; i < s1.length; i++) {
  33. char = s1[i];
  34. if (char !== s2[i]) break;
  35. if (char === delimiter) {
  36. index = i;
  37. }
  38. }
  39. return index > 0 ? s1.substring(0, index + 1) : undefined;
  40. }
  41. export function getDurationMilliseconds(start: [number, number]) {
  42. const [secs, nanosecs] = process.hrtime(start);
  43. return secs * 1000 + Math.floor(nanosecs / 1000000);
  44. }
  45. const pathNormalizeRegex = /\\/g;
  46. const pathStripTrailingSlashRegex = /\/$/g;
  47. const tokenRegex = /\$\{(\W*)?([^|]*?)(?:\|(\d+)(-|\?)?)?(\W*)?\}/g;
  48. const tokenSanitizeRegex = /\$\{(?:\W*)?(\w*?)(?:[\W\d]*)\}/g;
  49. // eslint-disable-next-line no-template-curly-in-string
  50. const tokenSanitizeReplacement = '$${this.$1}';
  51. export interface TokenOptions {
  52. collapseWhitespace: boolean;
  53. padDirection: 'left' | 'right';
  54. prefix: string | undefined;
  55. suffix: string | undefined;
  56. truncateTo: number | undefined;
  57. }
  58. export function getTokensFromTemplate(template: string) {
  59. const tokens: { key: string; options: TokenOptions }[] = [];
  60. let match;
  61. do {
  62. match = tokenRegex.exec(template);
  63. if (match == null) break;
  64. const [, prefix, key, truncateTo, option, suffix] = match;
  65. tokens.push({
  66. key: key,
  67. options: {
  68. collapseWhitespace: option === '?',
  69. padDirection: option === '-' ? 'left' : 'right',
  70. prefix: prefix,
  71. suffix: suffix,
  72. truncateTo: truncateTo == null ? undefined : parseInt(truncateTo, 10)
  73. }
  74. });
  75. } while (match != null);
  76. return tokens;
  77. }
  78. const interpolationMap = new Map<string, Function>();
  79. export function interpolate(template: string, context: object | undefined): string {
  80. if (template == null || template.length === 0) return template;
  81. if (context === undefined) return template.replace(tokenSanitizeRegex, emptyStr);
  82. let fn = interpolationMap.get(template);
  83. if (fn === undefined) {
  84. fn = new Function(`return \`${template.replace(tokenSanitizeRegex, tokenSanitizeReplacement)}\`;`);
  85. interpolationMap.set(template, fn);
  86. }
  87. return fn.call(context);
  88. }
  89. export function* lines(s: string): IterableIterator<string> {
  90. let i = 0;
  91. while (i < s.length) {
  92. let j = s.indexOf('\n', i);
  93. if (j === -1) {
  94. j = s.length;
  95. }
  96. yield s.substring(i, j);
  97. i = j + 1;
  98. }
  99. }
  100. export function md5(s: string, encoding: HexBase64Latin1Encoding = 'base64'): string {
  101. return createHash('md5')
  102. .update(s)
  103. .digest(encoding);
  104. }
  105. export function normalizePath(
  106. fileName: string,
  107. options: { addLeadingSlash?: boolean; stripTrailingSlash?: boolean } = { stripTrailingSlash: true }
  108. ) {
  109. if (fileName == null || fileName.length === 0) return fileName;
  110. let normalized = fileName.replace(pathNormalizeRegex, '/');
  111. const { addLeadingSlash, stripTrailingSlash } = { stripTrailingSlash: true, ...options };
  112. if (stripTrailingSlash) {
  113. normalized = normalized.replace(pathStripTrailingSlashRegex, emptyStr);
  114. }
  115. if (addLeadingSlash && normalized.charCodeAt(0) !== CharCode.Slash) {
  116. normalized = `/${normalized}`;
  117. }
  118. return normalized;
  119. }
  120. export function pad(s: string, before: number = 0, after: number = 0, padding: string = '\u00a0') {
  121. if (before === 0 && after === 0) return s;
  122. return `${before === 0 ? emptyStr : padding.repeat(before)}${s}${
  123. after === 0 ? emptyStr : padding.repeat(after)
  124. }`;
  125. }
  126. export function padLeft(s: string, padTo: number, padding: string = '\u00a0', width?: number) {
  127. const diff = padTo - (width || getWidth(s));
  128. return diff <= 0 ? s : padding.repeat(diff) + s;
  129. }
  130. export function padLeftOrTruncate(s: string, max: number, padding?: string, width?: number) {
  131. width = width || getWidth(s);
  132. if (width < max) return padLeft(s, max, padding, width);
  133. if (width > max) return truncate(s, max, undefined, width);
  134. return s;
  135. }
  136. export function padRight(s: string, padTo: number, padding: string = '\u00a0', width?: number) {
  137. const diff = padTo - (width || getWidth(s));
  138. return diff <= 0 ? s : s + padding.repeat(diff);
  139. }
  140. export function padOrTruncate(s: string, max: number, padding?: string, width?: number) {
  141. const left = max < 0;
  142. max = Math.abs(max);
  143. width = width || getWidth(s);
  144. if (width < max) return left ? padLeft(s, max, padding, width) : padRight(s, max, padding, width);
  145. if (width > max) return truncate(s, max, undefined, width);
  146. return s;
  147. }
  148. export function padRightOrTruncate(s: string, max: number, padding?: string, width?: number) {
  149. width = width || getWidth(s);
  150. if (width < max) return padRight(s, max, padding, width);
  151. if (width > max) return truncate(s, max);
  152. return s;
  153. }
  154. export function pluralize(
  155. s: string,
  156. count: number,
  157. options?: { number?: string; plural?: string; suffix?: string; zero?: string }
  158. ) {
  159. if (options === undefined) return `${count} ${s}${count === 1 ? emptyStr : 's'}`;
  160. return `${count === 0 ? options.zero || count : options.number || count} ${
  161. count === 1 ? s : options.plural || `${s}${options.suffix || 's'}`
  162. }`;
  163. }
  164. // Removes \ / : * ? " < > | and C0 and C1 control codes
  165. // eslint-disable-next-line no-control-regex
  166. const illegalCharsForFSRegex = /[\\/:*?"<>|\x00-\x1f\x80-\x9f]/g;
  167. export function sanitizeForFileSystem(s: string, replacement: string = '_') {
  168. if (!s) return s;
  169. return s.replace(illegalCharsForFSRegex, replacement);
  170. }
  171. export function sha1(s: string, encoding: HexBase64Latin1Encoding = 'base64'): string {
  172. return createHash('sha1')
  173. .update(s)
  174. .digest(encoding);
  175. }
  176. export function splitLast(s: string, splitter: string) {
  177. const index = s.lastIndexOf(splitter);
  178. if (index === -1) return [s];
  179. return [s.substr(index), s.substring(0, index - 1)];
  180. }
  181. export function splitSingle(s: string, splitter: string) {
  182. const parts = s.split(splitter, 1);
  183. const first = parts[0];
  184. return first.length === s.length ? parts : [first, s.substr(first.length + 1)];
  185. }
  186. export function truncate(s: string, truncateTo: number, ellipsis: string = '\u2026', width?: number) {
  187. if (!s) return s;
  188. width = width || getWidth(s);
  189. if (width <= truncateTo) return s;
  190. if (width === s.length) return `${s.substring(0, truncateTo - 1)}${ellipsis}`;
  191. // Skip ahead to start as far as we can by assuming all the double-width characters won't be truncated
  192. let chars = Math.floor(truncateTo / (width / s.length));
  193. let count = getWidth(s.substring(0, chars));
  194. while (count < truncateTo) {
  195. count += getWidth(s[chars++]);
  196. }
  197. if (count >= truncateTo) {
  198. chars--;
  199. }
  200. return `${s.substring(0, chars)}${ellipsis}`;
  201. }
  202. const ansiRegex = /[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))/g;
  203. const containsNonAsciiRegex = /[^\x20-\x7F\u00a0\u2026]/;
  204. export function getWidth(s: string): number {
  205. if (s == null || s.length === 0) return 0;
  206. // Shortcut to avoid needless string `RegExp`s, replacements, and allocations
  207. if (!containsNonAsciiRegex.test(s)) return s.length;
  208. s = s.replace(ansiRegex, emptyStr);
  209. let count = 0;
  210. let emoji = 0;
  211. let joiners = 0;
  212. const graphemes = [...s];
  213. for (let i = 0; i < graphemes.length; i++) {
  214. const code = graphemes[i].codePointAt(0)!;
  215. // Ignore control characters
  216. if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) continue;
  217. // Ignore combining characters
  218. if (code >= 0x300 && code <= 0x36f) continue;
  219. // https://stackoverflow.com/questions/30757193/find-out-if-character-in-string-is-emoji
  220. if (
  221. (code >= 0x1f600 && code <= 0x1f64f) || // Emoticons
  222. (code >= 0x1f300 && code <= 0x1f5ff) || // Misc Symbols and Pictographs
  223. (code >= 0x1f680 && code <= 0x1f6ff) || // Transport and Map
  224. (code >= 0x2600 && code <= 0x26ff) || // Misc symbols
  225. (code >= 0x2700 && code <= 0x27bf) || // Dingbats
  226. (code >= 0xfe00 && code <= 0xfe0f) || // Variation Selectors
  227. (code >= 0x1f900 && code <= 0x1f9ff) || // Supplemental Symbols and Pictographs
  228. (code >= 65024 && code <= 65039) || // Variation selector
  229. (code >= 8400 && code <= 8447) // Combining Diacritical Marks for Symbols
  230. ) {
  231. if (code >= 0x1f3fb && code <= 0x1f3ff) continue; // emoji modifier fitzpatrick type
  232. emoji++;
  233. count += 2;
  234. continue;
  235. }
  236. // Ignore zero-width joiners '\u200d'
  237. if (code === 8205) {
  238. joiners++;
  239. count -= 2;
  240. continue;
  241. }
  242. // Surrogates
  243. if (code > 0xffff) {
  244. i++;
  245. }
  246. count += isFullwidthCodePoint(code) ? 2 : 1;
  247. }
  248. const offset = emoji - joiners;
  249. if (offset > 1) {
  250. count += offset - 1;
  251. }
  252. return count;
  253. }
  254. function isFullwidthCodePoint(cp: number) {
  255. // code points are derived from:
  256. // http://www.unix.org/Public/UNIDATA/EastAsianWidth.txt
  257. if (
  258. cp >= 0x1100 &&
  259. (cp <= 0x115f || // Hangul Jamo
  260. cp === 0x2329 || // LEFT-POINTING ANGLE BRACKET
  261. cp === 0x232a || // RIGHT-POINTING ANGLE BRACKET
  262. // CJK Radicals Supplement .. Enclosed CJK Letters and Months
  263. (cp >= 0x2e80 && cp <= 0x3247 && cp !== 0x303f) ||
  264. // Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A
  265. (cp >= 0x3250 && cp <= 0x4dbf) ||
  266. // CJK Unified Ideographs .. Yi Radicals
  267. (cp >= 0x4e00 && cp <= 0xa4c6) ||
  268. // Hangul Jamo Extended-A
  269. (cp >= 0xa960 && cp <= 0xa97c) ||
  270. // Hangul Syllables
  271. (cp >= 0xac00 && cp <= 0xd7a3) ||
  272. // CJK Compatibility Ideographs
  273. (cp >= 0xf900 && cp <= 0xfaff) ||
  274. // Vertical Forms
  275. (cp >= 0xfe10 && cp <= 0xfe19) ||
  276. // CJK Compatibility Forms .. Small Form Variants
  277. (cp >= 0xfe30 && cp <= 0xfe6b) ||
  278. // Halfwidth and Fullwidth Forms
  279. (cp >= 0xff01 && cp <= 0xff60) ||
  280. (cp >= 0xffe0 && cp <= 0xffe6) ||
  281. // Kana Supplement
  282. (cp >= 0x1b000 && cp <= 0x1b001) ||
  283. // Enclosed Ideographic Supplement
  284. (cp >= 0x1f200 && cp <= 0x1f251) ||
  285. // CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane
  286. (cp >= 0x20000 && cp <= 0x3fffd))
  287. ) {
  288. return true;
  289. }
  290. return false;
  291. }
  292. }