main.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. 'use strict';
  2. var obsidian = require('obsidian');
  3. /*! *****************************************************************************
  4. Copyright (c) Microsoft Corporation.
  5. Permission to use, copy, modify, and/or distribute this software for any
  6. purpose with or without fee is hereby granted.
  7. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
  8. REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
  9. AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
  10. INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
  11. LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
  12. OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
  13. PERFORMANCE OF THIS SOFTWARE.
  14. ***************************************************************************** */
  15. function __awaiter(thisArg, _arguments, P, generator) {
  16. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  17. return new (P || (P = Promise))(function (resolve, reject) {
  18. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  19. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  20. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  21. step((generator = generator.apply(thisArg, _arguments || [])).next());
  22. });
  23. }
  24. const reKey = /\[\^(.+?(?=\]))\]/gi;
  25. const reDefinition = /\[\^(.+)\]\:/;
  26. // https://stackoverflow.com/a/1830844
  27. function isNumeric(value) {
  28. return !isNaN(value - parseFloat(value));
  29. }
  30. function tidyFootnotes(editor) {
  31. let markers = [];
  32. let definitions = new Map();
  33. let firstDefinitionLine = -1;
  34. let definitionsIndexed = new Map();
  35. // Iterate through each line
  36. const lineCount = editor.lineCount();
  37. let prevKey = '';
  38. for (let i = 0; i < lineCount; i++) {
  39. const line = editor.getLine(i);
  40. let isDefinition = false;
  41. let match;
  42. if (prevKey.length) {
  43. const hasIndent = /^[ \t]/.test(line);
  44. const isLastLine = i === (lineCount - 1);
  45. if (hasIndent || (line.length === 0 && !isLastLine)) {
  46. // Append line to the previous footnote definition
  47. const value = definitions.get(prevKey);
  48. definitions.set(prevKey, value + "\n" + line);
  49. markers[markers.length - 1].length++;
  50. continue;
  51. }
  52. else {
  53. prevKey = '';
  54. }
  55. }
  56. // Look for footnote definition
  57. while ((match = reDefinition.exec(line)) !== null) {
  58. if (match.length < 1)
  59. return;
  60. isDefinition = true;
  61. // Remember definition and where it is
  62. let key = match[1];
  63. let value = line.substring(match[0].length);
  64. definitions.set(key, value);
  65. prevKey = key;
  66. let marker = {
  67. key,
  68. line: i,
  69. index: 0,
  70. length: 0,
  71. isDefinition: true
  72. };
  73. markers.push(marker);
  74. // Remember first definition line to insert combined list later
  75. if (firstDefinitionLine === -1) {
  76. firstDefinitionLine = i;
  77. }
  78. break;
  79. }
  80. if (isDefinition)
  81. continue;
  82. // Look for footnote key
  83. while ((match = reKey.exec(line)) !== null) {
  84. if (match.length < 1)
  85. return;
  86. // Remember where footnote key is
  87. let key = match[1];
  88. let marker = {
  89. key,
  90. line: i,
  91. index: match.index,
  92. length: match[0].length,
  93. isDefinition: false
  94. };
  95. markers.push(marker);
  96. if (!definitionsIndexed.has(key)) {
  97. // Add key into index
  98. definitionsIndexed.set(key, {
  99. key,
  100. newKey: key,
  101. isNumber: isNumeric(key),
  102. value: ''
  103. });
  104. }
  105. }
  106. }
  107. // Assign definition to key in index
  108. // If definition has no key, it will be appended with its current key
  109. definitions.forEach((value, key) => {
  110. definitionsIndexed.set(key, {
  111. key,
  112. newKey: key,
  113. isNumber: isNumeric(key),
  114. value
  115. });
  116. });
  117. // Re-index numbers and construct combined definitions output
  118. let count = 1;
  119. let definitionsStr = '';
  120. definitionsIndexed.forEach((definition, marker) => {
  121. let key = definition.key;
  122. if (definition.isNumber) {
  123. const current = definitionsIndexed.get(marker);
  124. key = count.toString();
  125. definitionsIndexed.set(marker, Object.assign(Object.assign({}, current), { newKey: key }));
  126. count++;
  127. }
  128. definitionsStr += `[^${key}]:${definition.value}\n`;
  129. });
  130. const markersCount = markers.length;
  131. for (let i = markersCount - 1; i >= 0; i--) {
  132. const marker = markers[i];
  133. const markerLine = marker.line;
  134. if (marker.isDefinition) {
  135. let rangeStart, rangeEnd;
  136. const lineEnd = markerLine + 1 + marker.length;
  137. if (lineEnd === editor.lineCount()) {
  138. // Replace from previous to current line to fix CodeMirror 6 error
  139. rangeStart = { line: markerLine, ch: 0 };
  140. rangeEnd = { line: lineEnd - 1, ch: Infinity };
  141. }
  142. else {
  143. // Replace from current to next line
  144. rangeStart = { line: markerLine, ch: 0 };
  145. rangeEnd = { line: lineEnd, ch: 0 };
  146. }
  147. if (markerLine === firstDefinitionLine) {
  148. // Replace first definition line with list of indexed definitions
  149. editor.replaceRange(definitionsStr, rangeStart, rangeEnd);
  150. continue;
  151. }
  152. // Remove line(s)
  153. editor.replaceRange('', rangeStart, rangeEnd);
  154. continue;
  155. }
  156. // Check if key has changed
  157. const definition = definitionsIndexed.get(marker.key);
  158. const newKey = definition.newKey;
  159. if (marker.key === newKey)
  160. continue;
  161. // Replace footnote key in line with the new one
  162. const line = editor.getLine(markerLine);
  163. const prefix = line.substring(0, marker.index);
  164. const newMarker = `[^${newKey}]`;
  165. const suffix = line.substr(marker.index + marker.length);
  166. const newLine = prefix + newMarker + suffix;
  167. editor.replaceRange(newLine, { line: markerLine, ch: 0 }, { line: markerLine, ch: Infinity });
  168. }
  169. if (firstDefinitionLine == -1) {
  170. // If there are no definitions, add definitions list at the end
  171. const lineCount = editor.lineCount();
  172. editor.replaceRange("\n\n" + definitionsStr, { line: lineCount, ch: 0 }, { line: lineCount, ch: Infinity });
  173. }
  174. // console.log(markers, definitions, definitionsIndexed, definitionsStr);
  175. }
  176. class TidyFootnotes extends obsidian.Plugin {
  177. onload() {
  178. return __awaiter(this, void 0, void 0, function* () {
  179. this.addCommand({
  180. id: 'tidy-footnotes',
  181. name: 'Tidy Footnotes',
  182. checkCallback: (checking) => {
  183. // Ensure the active view is a Markdown editor
  184. const view = this.app.workspace.getActiveViewOfType(obsidian.MarkdownView);
  185. if (checking)
  186. return !!view;
  187. if (!view || view.sourceMode == undefined)
  188. return false;
  189. let editor = view.editor;
  190. tidyFootnotes(editor);
  191. }
  192. });
  193. });
  194. }
  195. }
  196. module.exports = TidyFootnotes;