linker.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import { encodeSpaces } from '@growi/core/dist/utils/page-path-utils';
  2. export class Linker {
  3. type: string;
  4. label: string | undefined;
  5. link: string | undefined;
  6. constructor(type = Linker.types.markdownLink, label = '', link = '') {
  7. this.type = type;
  8. this.label = label;
  9. this.link = link;
  10. if (type === Linker.types.markdownLink) {
  11. this.initWhenMarkdownLink();
  12. }
  13. this.generateMarkdownText = this.generateMarkdownText.bind(this);
  14. }
  15. static types = {
  16. markdownLink: 'mdLink',
  17. growiLink: 'growiLink',
  18. pukiwikiLink: 'pukiwikiLink',
  19. };
  20. static patterns = {
  21. pukiwikiLinkWithLabel: /^\[\[(?<label>.+)>(?<link>.+)\]\]$/, // https://regex101.com/r/2fNmUN/2
  22. pukiwikiLinkWithoutLabel: /^\[\[(?<label>.+)\]\]$/, // https://regex101.com/r/S7w5Xu/1
  23. markdownLink: /^\[(?<label>.*)\]\((?<link>.*)\)$/, // https://regex101.com/r/DZCKP3/2
  24. };
  25. initWhenMarkdownLink(): void {
  26. // fill label with link if empty
  27. if (this.label === '') {
  28. this.label = this.link;
  29. }
  30. // encode spaces
  31. this.link = encodeSpaces(this.link);
  32. }
  33. generateMarkdownText(): string | undefined {
  34. if (this.type === Linker.types.pukiwikiLink) {
  35. if (this.label === '') return `[[${this.link}]]`;
  36. return `[[${this.label}>${this.link}]]`;
  37. }
  38. if (this.type === Linker.types.growiLink) {
  39. return `[${this.link}]`;
  40. }
  41. if (this.type === Linker.types.markdownLink) {
  42. return `[${this.label}](${this.link})`;
  43. }
  44. }
  45. // create an instance of Linker from string
  46. static fromMarkdownString(str: string): Linker {
  47. // if str doesn't mean a linker, create a link whose label is str
  48. let label = str;
  49. let link = '';
  50. let type = Linker.types.markdownLink;
  51. const patterns = [
  52. // pukiwiki with separator ">".
  53. {
  54. type: Linker.types.pukiwikiLink,
  55. pattern: Linker.patterns.pukiwikiLinkWithLabel,
  56. },
  57. // pukiwiki without separator ">".
  58. {
  59. type: Linker.types.pukiwikiLink,
  60. pattern: Linker.patterns.pukiwikiLinkWithoutLabel,
  61. },
  62. // markdown link.
  63. {
  64. type: Linker.types.markdownLink,
  65. pattern: Linker.patterns.markdownLink,
  66. },
  67. ];
  68. // evaluate patterns
  69. for (const { type: patternType, pattern } of patterns) {
  70. const match = str.match(pattern);
  71. if (match?.groups) {
  72. type = patternType;
  73. label = match.groups.label;
  74. link = match.groups.link ?? label;
  75. break;
  76. }
  77. }
  78. return new Linker(type, label, link);
  79. }
  80. // create an instance of Linker from text with index
  81. static fromLineWithIndex(line: string, index: number): Linker {
  82. const { beginningOfLink, endOfLink } = Linker.getBeginningAndEndIndexOfLink(
  83. line,
  84. index,
  85. );
  86. // if index is in a link, extract it from line
  87. let linkStr = '';
  88. if (beginningOfLink >= 0 && endOfLink >= 0) {
  89. linkStr = line.substring(beginningOfLink, endOfLink);
  90. }
  91. return Linker.fromMarkdownString(linkStr);
  92. }
  93. // return beginning and end indices of link
  94. // if index is not in a link, return { beginningOfLink: -1, endOfLink: -1 }
  95. static getBeginningAndEndIndexOfLink(
  96. line: string,
  97. index: number,
  98. ): { beginningOfLink: number; endOfLink: number } {
  99. let beginningOfLink: number;
  100. let endOfLink: number;
  101. // pukiwiki link ('[[link]]')
  102. [beginningOfLink, endOfLink] =
  103. Linker.getBeginningAndEndIndexWithPrefixAndSuffix(
  104. line,
  105. index,
  106. '[[',
  107. ']]',
  108. );
  109. // markdown link ('[label](link)')
  110. if (
  111. beginningOfLink < 0 ||
  112. endOfLink < 0 ||
  113. beginningOfLink > index ||
  114. endOfLink < index
  115. ) {
  116. [beginningOfLink, endOfLink] =
  117. Linker.getBeginningAndEndIndexWithPrefixAndSuffix(
  118. line,
  119. index,
  120. '[',
  121. ')',
  122. '](',
  123. );
  124. }
  125. // growi link ('[/link]')
  126. if (
  127. beginningOfLink < 0 ||
  128. endOfLink < 0 ||
  129. beginningOfLink > index ||
  130. endOfLink < index
  131. ) {
  132. [beginningOfLink, endOfLink] =
  133. Linker.getBeginningAndEndIndexWithPrefixAndSuffix(
  134. line,
  135. index,
  136. '[/',
  137. ']',
  138. );
  139. }
  140. // return { beginningOfLink: -1, endOfLink: -1 }
  141. if (
  142. beginningOfLink < 0 ||
  143. endOfLink < 0 ||
  144. beginningOfLink > index ||
  145. endOfLink < index
  146. ) {
  147. [beginningOfLink, endOfLink] = [-1, -1];
  148. }
  149. return { beginningOfLink, endOfLink };
  150. }
  151. // return begin and end indices as an array only when index is between prefix and suffix and link contains containText.
  152. static getBeginningAndEndIndexWithPrefixAndSuffix(
  153. line: string,
  154. index: number,
  155. prefix: string,
  156. suffix: string,
  157. containText = '',
  158. ): [number, number] {
  159. const beginningIndex = line.lastIndexOf(prefix, index);
  160. const indexOfContainText = line.indexOf(
  161. containText,
  162. beginningIndex + prefix.length,
  163. );
  164. const endIndex = line.indexOf(
  165. suffix,
  166. indexOfContainText + containText.length,
  167. );
  168. if (beginningIndex < 0 || indexOfContainText < 0 || endIndex < 0) {
  169. return [-1, -1];
  170. }
  171. return [beginningIndex, endIndex + suffix.length];
  172. }
  173. }