Linker.ts 5.3 KB

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