1
0

main.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. const lib = {};
  2. lib.dedent_lines = lines => {
  3. // If any lines are just spaces, remove the spaces
  4. for ( let i=0 ; i < lines.length ; i++ ) {
  5. if ( /^\s+$/.test(lines[i]) ) lines[i] = '';
  6. }
  7. // Remove leading and trailing blanks
  8. while ( lines[0] === '' ) lines.shift();
  9. while ( lines[lines.length-1] === '' ) lines.pop();
  10. let min_indent = Number.MAX_SAFE_INTEGER;
  11. for ( let i=0 ; i < lines.length ; i++ ) {
  12. if ( lines[i] === '' ) continue;
  13. let n_spaces = 0;
  14. for ( let j=0 ; j < lines[i].length ; j++ ) {
  15. if ( lines[i][j] === ' ' ) n_spaces++;
  16. else break;
  17. }
  18. if ( n_spaces < min_indent ) min_indent = n_spaces;
  19. }
  20. for ( let i=0 ; i < lines.length ; i++ ) {
  21. if ( lines[i] === '' ) continue;
  22. lines[i] = lines[i].slice(min_indent);
  23. }
  24. };
  25. const StringStream = (str, { state_ } = {}) => {
  26. const state = state_ ?? { pos: 0 };
  27. return {
  28. skip_whitespace () {
  29. while ( /^\s/.test(str[state.pos]) ) state.pos++;
  30. },
  31. // INCOMPLETE: only handles single chars
  32. skip_matching (items) {
  33. while ( items.some(item => {
  34. return str[state.pos] === item;
  35. }) ) state.pos++;
  36. },
  37. fwd (amount) {
  38. state.pos += amount ?? 1;
  39. },
  40. fork () {
  41. return StringStream(str, { state_: { pos: state.pos } });
  42. },
  43. async get_pos () {
  44. return state.pos;
  45. },
  46. async get_char () {
  47. return str[state.pos];
  48. },
  49. async matches (re_or_lit) {
  50. if ( re_or_lit instanceof RegExp ) {
  51. const re = re_or_lit;
  52. return re.test(str.slice(state.pos));
  53. }
  54. const lit = re_or_lit;
  55. return lit === str.slice(state.pos, state.pos + lit.length);
  56. },
  57. async get_until (re_or_lit) {
  58. let index;
  59. if ( re_or_lit instanceof RegExp ) {
  60. const re = re_or_lit;
  61. const result = re.exec(str.slice(state.pos));
  62. if ( ! result ) return;
  63. index = state.pos + result.index;
  64. } else {
  65. const lit = re_or_lit;
  66. const ind = str.slice(state.pos).indexOf(lit);
  67. // TODO: parser warnings?
  68. if ( ind === -1 ) return;
  69. index = state.pos + ind;
  70. }
  71. const start_pos = state.pos;
  72. state.pos = index;
  73. return str.slice(start_pos, index);
  74. },
  75. async debug () {
  76. const l1 = str.length;
  77. const l2 = str.length - state.pos;
  78. const clean = s => s.replace(/\n/, '{LF}');
  79. return `[stream : "${
  80. clean(str.slice(0, Math.min(6, l1)))
  81. }"... |${state.pos}| ..."${
  82. clean(str.slice(state.pos, state.pos + Math.min(6, l2)))
  83. }"]`
  84. }
  85. };
  86. };
  87. const LinesCommentParser = ({
  88. prefix
  89. }) => {
  90. return {
  91. parse: async (stream) => {
  92. stream.skip_whitespace();
  93. const lines = [];
  94. while ( await stream.matches(prefix) ) {
  95. const line = await stream.get_until('\n');
  96. if ( ! line ) return;
  97. lines.push(line);
  98. stream.fwd();
  99. stream.skip_matching([' ', '\t']);
  100. if ( await stream.get_char() === '\n' ){
  101. stream.fwd();
  102. break;
  103. }
  104. stream.skip_whitespace();
  105. }
  106. if ( lines.length === 0 ) return;
  107. for ( let i=0 ; i < lines.length ; i++ ) {
  108. lines[i] = lines[i].slice(prefix.length);
  109. }
  110. lib.dedent_lines(lines);
  111. return {
  112. lines,
  113. };
  114. }
  115. };
  116. };
  117. const BlockCommentParser = ({
  118. start,
  119. end,
  120. ignore_line_prefix,
  121. }) => {
  122. return {
  123. parse: async (stream) => {
  124. stream.skip_whitespace();
  125. stream.debug('starting at', await stream.debug())
  126. if ( ! stream.matches(start) ) return;
  127. stream.fwd(start.length);
  128. const contents = await stream.get_until(end);
  129. if ( ! contents ) return;
  130. stream.fwd(end.length);
  131. // console.log('ending at', await stream.debug())
  132. const lines = contents.split('\n');
  133. // === Formatting Time! === //
  134. // Special case: remove the last '*' after '/**'
  135. if ( lines[0].trim() === ignore_line_prefix ) {
  136. lines.shift();
  137. }
  138. // First dedent pass
  139. lib.dedent_lines(lines);
  140. // If all the lines start with asterisks, remove
  141. let allofem = true;
  142. for ( let i=0 ; i < lines.length ; i++ ) {
  143. if ( lines[i] === '' ) continue;
  144. if ( ! lines[i].startsWith(ignore_line_prefix) ) {
  145. allofem = false;
  146. break
  147. }
  148. }
  149. if ( allofem ) {
  150. for ( let i=0 ; i < lines.length ; i++ ) {
  151. if ( lines[i] === '' ) continue;
  152. lines[i] = lines[i].slice(ignore_line_prefix.length);
  153. }
  154. // Second dedent pass
  155. lib.dedent_lines(lines);
  156. }
  157. return { lines };
  158. }
  159. };
  160. };
  161. const LinesCommentWriter = ({ prefix }) => {
  162. return {
  163. write: (lines) => {
  164. lib.dedent_lines(lines);
  165. for ( let i=0 ; i < lines.length ; i++ ) {
  166. lines[i] = prefix + lines[i];
  167. }
  168. return lines.join('\n') + '\n';
  169. }
  170. };
  171. };
  172. const BlockCommentWriter = ({ start, end, prefix }) => {
  173. return {
  174. write: (lines) => {
  175. lib.dedent_lines(lines);
  176. for ( let i=0 ; i < lines.length ; i++ ) {
  177. lines[i] = prefix + lines[i];
  178. }
  179. let s = start + '\n';
  180. s += lines.join('\n') + '\n';
  181. s += end + '\n';
  182. return s;
  183. }
  184. };
  185. };
  186. const CommentParser = () => {
  187. const registry_ = {
  188. object: {
  189. parsers: {
  190. lines: LinesCommentParser,
  191. block: BlockCommentParser,
  192. },
  193. writers: {
  194. lines: LinesCommentWriter,
  195. block: BlockCommentWriter,
  196. },
  197. },
  198. data: {
  199. extensions: {
  200. js: 'javascript',
  201. cjs: 'javascript',
  202. mjs: 'javascript',
  203. },
  204. languages: {
  205. javascript: {
  206. parsers: [
  207. ['lines', {
  208. prefix: '// ',
  209. }],
  210. ['block', {
  211. start: '/*',
  212. end: '*/',
  213. ignore_line_prefix: '*',
  214. }],
  215. ],
  216. writers: {
  217. lines: ['lines', {
  218. prefix: '//'
  219. }],
  220. block: ['block', {
  221. start: '/*',
  222. end: '*/',
  223. prefix: ' * ',
  224. }]
  225. },
  226. }
  227. },
  228. }
  229. };
  230. const get_language_by_filename = ({ filename }) => {
  231. const { language } = (({ filename }) => {
  232. const { language_id } = (({ filename }) => {
  233. const { extension } = (({ filename }) => {
  234. const components = ('' + filename).split('.');
  235. const extension = components[components.length - 1];
  236. return { extension };
  237. })({ filename });
  238. const language_id = registry_.data.extensions[extension];
  239. if ( ! language_id ) {
  240. throw new Error(`unrecognized language id: ` +
  241. language_id);
  242. }
  243. return { language_id };
  244. })({ filename });
  245. const language = registry_.data.languages[language_id];
  246. return { language };
  247. })({ filename });
  248. if ( ! language ) {
  249. // TODO: use strutil quot here
  250. throw new Error(`unrecognized language: ${language}`)
  251. }
  252. return { language };
  253. }
  254. const supports = ({ filename }) => {
  255. try {
  256. get_language_by_filename({ filename });
  257. } catch (e) {
  258. return false;
  259. }
  260. return true;
  261. };
  262. const extract_top_comments = async ({ filename, source }) => {
  263. const { language } = get_language_by_filename({ filename });
  264. // TODO: registry has `data` and `object`...
  265. // ... maybe add `virt` (virtual), which will
  266. // behave in the way the above code is written.
  267. const inst_ = spec => registry_.object.parsers[spec[0]](spec[1]);
  268. let ss = StringStream(source);
  269. const results = [];
  270. for (;;) {
  271. let comment;
  272. for ( let parser of language.parsers ) {
  273. const parser_name = parser[0];
  274. parser = inst_(parser);
  275. const ss_ = ss.fork();
  276. const start_pos = await ss_.get_pos();
  277. comment = await parser.parse(ss_);
  278. const end_pos = await ss_.get_pos();
  279. if ( comment ) {
  280. ss = ss_;
  281. comment.type = parser_name;
  282. comment.range = [start_pos, end_pos];
  283. break;
  284. }
  285. }
  286. if ( ! comment ) break;
  287. results.push(comment);
  288. }
  289. return results;
  290. }
  291. const output_comment = ({ filename, style, text }) => {
  292. const { language } = get_language_by_filename({ filename });
  293. const inst_ = spec => registry_.object.writers[spec[0]](spec[1]);
  294. let writer = language.writers[style];
  295. writer = inst_(writer);
  296. const lines = text.split('\n');
  297. const s = writer.write(lines);
  298. return s;
  299. }
  300. return {
  301. supports,
  302. extract_top_comments,
  303. output_comment,
  304. };
  305. };
  306. module.exports = {
  307. StringStream,
  308. LinesCommentParser,
  309. BlockCommentParser,
  310. CommentParser,
  311. };