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. if ( ! await stream.matches(start) ) return;
  126. stream.fwd(start.length);
  127. const contents = await stream.get_until(end);
  128. if ( ! contents ) return;
  129. stream.fwd(end.length);
  130. // console.log('ending at', await stream.debug())
  131. const lines = contents.split('\n');
  132. // === Formatting Time! === //
  133. // Special case: remove the last '*' after '/**'
  134. if ( lines[0].trim() === ignore_line_prefix ) {
  135. lines.shift();
  136. }
  137. // First dedent pass
  138. lib.dedent_lines(lines);
  139. // If all the lines start with asterisks, remove
  140. let allofem = true;
  141. for ( let i=0 ; i < lines.length ; i++ ) {
  142. if ( lines[i] === '' ) continue;
  143. if ( ! lines[i].startsWith(ignore_line_prefix) ) {
  144. allofem = false;
  145. break
  146. }
  147. }
  148. if ( allofem ) {
  149. for ( let i=0 ; i < lines.length ; i++ ) {
  150. if ( lines[i] === '' ) continue;
  151. lines[i] = lines[i].slice(ignore_line_prefix.length);
  152. }
  153. // Second dedent pass
  154. lib.dedent_lines(lines);
  155. }
  156. return { lines };
  157. }
  158. };
  159. };
  160. const LinesCommentWriter = ({ prefix }) => {
  161. return {
  162. write: (lines) => {
  163. lib.dedent_lines(lines);
  164. for ( let i=0 ; i < lines.length ; i++ ) {
  165. lines[i] = prefix + lines[i];
  166. }
  167. return lines.join('\n') + '\n';
  168. }
  169. };
  170. };
  171. const BlockCommentWriter = ({ start, end, prefix }) => {
  172. return {
  173. write: (lines) => {
  174. lib.dedent_lines(lines);
  175. for ( let i=0 ; i < lines.length ; i++ ) {
  176. lines[i] = prefix + lines[i];
  177. }
  178. let s = start + '\n';
  179. s += lines.join('\n') + '\n';
  180. s += end + '\n';
  181. return s;
  182. }
  183. };
  184. };
  185. const CommentParser = () => {
  186. const registry_ = {
  187. object: {
  188. parsers: {
  189. lines: LinesCommentParser,
  190. block: BlockCommentParser,
  191. },
  192. writers: {
  193. lines: LinesCommentWriter,
  194. block: BlockCommentWriter,
  195. },
  196. },
  197. data: {
  198. extensions: {
  199. js: 'javascript',
  200. cjs: 'javascript',
  201. mjs: 'javascript',
  202. },
  203. languages: {
  204. javascript: {
  205. parsers: [
  206. ['lines', {
  207. prefix: '//',
  208. }],
  209. ['block', {
  210. start: '/*',
  211. end: '*/',
  212. ignore_line_prefix: '*',
  213. }],
  214. ],
  215. writers: {
  216. lines: ['lines', {
  217. prefix: '// '
  218. }],
  219. block: ['block', {
  220. start: '/*',
  221. end: ' */',
  222. prefix: ' * ',
  223. }]
  224. },
  225. }
  226. },
  227. }
  228. };
  229. const get_language_by_filename = ({ filename }) => {
  230. const { language } = (({ filename }) => {
  231. const { language_id } = (({ filename }) => {
  232. const { extension } = (({ filename }) => {
  233. const components = ('' + filename).split('.');
  234. const extension = components[components.length - 1];
  235. return { extension };
  236. })({ filename });
  237. const language_id = registry_.data.extensions[extension];
  238. if ( ! language_id ) {
  239. throw new Error(`unrecognized language id: ` +
  240. language_id);
  241. }
  242. return { language_id };
  243. })({ filename });
  244. const language = registry_.data.languages[language_id];
  245. return { language };
  246. })({ filename });
  247. if ( ! language ) {
  248. // TODO: use strutil quot here
  249. throw new Error(`unrecognized language: ${language}`)
  250. }
  251. return { language };
  252. }
  253. const supports = ({ filename }) => {
  254. try {
  255. get_language_by_filename({ filename });
  256. } catch (e) {
  257. return false;
  258. }
  259. return true;
  260. };
  261. const extract_top_comments = async ({ filename, source }) => {
  262. const { language } = get_language_by_filename({ filename });
  263. // TODO: registry has `data` and `object`...
  264. // ... maybe add `virt` (virtual), which will
  265. // behave in the way the above code is written.
  266. const inst_ = spec => registry_.object.parsers[spec[0]](spec[1]);
  267. let ss = StringStream(source);
  268. const results = [];
  269. for (;;) {
  270. let comment;
  271. for ( let parser of language.parsers ) {
  272. const parser_name = parser[0];
  273. parser = inst_(parser);
  274. const ss_ = ss.fork();
  275. const start_pos = await ss_.get_pos();
  276. comment = await parser.parse(ss_);
  277. const end_pos = await ss_.get_pos();
  278. if ( comment ) {
  279. ss = ss_;
  280. comment.type = parser_name;
  281. comment.range = [start_pos, end_pos];
  282. break;
  283. }
  284. }
  285. console.log('comment?', comment);
  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. };