index.mjs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  1. /*
  2. * md ast - pluggable markdown parser
  3. */
  4. const getSecondsFromTimeCode = timeCode => {
  5. let times = timeCode.split(':').map(x => +x).filter(x => !isNaN(x))
  6. return times.length === 2 ? times[0]*60 + times[1] :
  7. (times.length === 3 ? times[0]*60*60 + times[1]*60 + times[2] : NaN )
  8. }
  9. const getTimeCodes = ast =>{
  10. let timeCodes = {}
  11. const walker = node => ((node.tag && node.tag.startsWith('heading') && (timeCodes[node.startMatch[4] || node.startMatch[3]] = node.startMatch[3])),
  12. (node.children instanceof Array && node.children.forEach(walker)))
  13. walker(ast)
  14. delete timeCodes.undefined
  15. let result = {}
  16. for (let timeCode in timeCodes){
  17. result[getSecondsFromTimeCode(timeCode)] = timeCodes[timeCode]
  18. }
  19. return result;
  20. }
  21. const syntax = {
  22. img:{
  23. paired: true,
  24. recursive: false,
  25. startRegexp: /!(\S*)\[(.*)\]\(/,
  26. endRegexp: /\)/,
  27. content: {
  28. start: {
  29. point: 'end',
  30. offset: 0
  31. },
  32. end: {
  33. point: 'start',
  34. offset: 0
  35. }
  36. },
  37. begin: 0,
  38. forward: {
  39. point: 'end', //start, startEnd, end, endEnd
  40. offset: 1
  41. },
  42. title: {
  43. index: 2,
  44. recursive: false,
  45. },
  46. onbuild(md, mdTags, buildAST){ //this = {tag: }
  47. }
  48. },
  49. a:{
  50. paired: true,
  51. recursive: false,
  52. startRegexp: /\[(.*)\]\(/,
  53. endRegexp: /\)/,
  54. content: {
  55. start: {
  56. point: 'end',
  57. offset: 0
  58. },
  59. end: {
  60. point: 'start',
  61. offset: 0
  62. }
  63. },
  64. begin: 0,
  65. forward: {
  66. point: 'end', //start, startEnd, end, endEnd
  67. offset: 1
  68. },
  69. title: {
  70. //index: 1,
  71. recursive: true,
  72. },
  73. onbuild(md, mdTags, buildAST){ //this = {tag: }
  74. }
  75. },
  76. strike: {
  77. paired: true,
  78. recursive: true,
  79. startRegexp: /\~\~\S.*/,
  80. endRegexp: /\~\~\W/,
  81. content: {
  82. start: {
  83. point: 'start',
  84. offset: 2
  85. },
  86. end: {
  87. point: 'start',
  88. offset: 0
  89. }
  90. },
  91. begin: 0,
  92. forward: {
  93. point: 'endEnd', //start, startEnd, end, endEnd
  94. offset: -1
  95. }
  96. },
  97. bold1: {
  98. paired: true,
  99. recursive: true,
  100. startRegexp: /\*\*\S.*/,
  101. endRegexp: /\*\*\W/,
  102. content: {
  103. start: {
  104. point: 'start',
  105. offset: 2
  106. },
  107. end: {
  108. point: 'start',
  109. offset: 0
  110. }
  111. },
  112. begin: 0,
  113. forward: {
  114. point: 'endEnd', //start, startEnd, end, endEnd
  115. offset: -1
  116. }
  117. },
  118. bold2: {
  119. paired: true,
  120. recursive: true,
  121. startRegexp: /\s__\S.*/,
  122. endRegexp: /__\W/,
  123. content: {
  124. start: {
  125. point: 'start',
  126. offset: 3
  127. },
  128. end: {
  129. point: 'start',
  130. offset: 0
  131. }
  132. },
  133. begin: 1,
  134. forward: {
  135. point: 'endEnd', //start, startEnd, end, endEnd
  136. offset: -1
  137. }
  138. },
  139. italic1: {
  140. paired: true,
  141. recursive: true,
  142. startRegexp: /\*\S.*/,
  143. endRegexp: /\S\*[^*]/,
  144. content: {
  145. start: {
  146. point: 'start',
  147. offset: 1
  148. },
  149. end: {
  150. point: 'start',
  151. offset: 1
  152. }
  153. },
  154. begin: 0,
  155. forward: {
  156. point: 'endEnd', //start, startEnd, end, endEnd
  157. offset: -1
  158. }
  159. },
  160. italic2: {
  161. paired: true,
  162. recursive: true,
  163. startRegexp: /\s_\S.*/,
  164. endRegexp: /\S_\W/,
  165. content: {
  166. start: {
  167. point: 'start',
  168. offset: 2
  169. },
  170. end: {
  171. point: 'start',
  172. offset: 1
  173. }
  174. },
  175. begin: 1,
  176. forward: {
  177. point: 'endEnd', //start, startEnd, end, endEnd
  178. offset: -1
  179. }
  180. },
  181. root: {
  182. paired: true,
  183. recursive: true,
  184. startRegexp: /^/,
  185. endRegexp: /$/,
  186. content: {
  187. start: {
  188. point: 'start',
  189. offset: 1
  190. },
  191. end: {
  192. point: 'end',
  193. offset: 0
  194. }
  195. },
  196. begin: 0,
  197. forward: {
  198. point: 'endEnd', //start, startEnd, end, endEnd
  199. offset: -1
  200. }
  201. },
  202. heading6: {
  203. paired: true,
  204. recursive: true,
  205. startRegexp: /\n######[ \t]*([^\[\n]*)(\[#(\S*)[ \t]*(.*)\])?\n/,
  206. endRegexp: /\n#+/,
  207. content: {
  208. start: {
  209. point: 'end',
  210. offset: -1
  211. },
  212. end: {
  213. point: 'start',
  214. offset: 0
  215. }
  216. },
  217. begin: 0,
  218. forward: {
  219. point: 'end', //start, startEnd, end, endEnd
  220. offset: 0
  221. },
  222. title: {
  223. //index: 1,
  224. recursive: true,
  225. },
  226. onbuild(md, mdTags, buildAST){ //this = {tag: }
  227. }
  228. },
  229. heading5: {
  230. paired: true,
  231. recursive: true,
  232. startRegexp: /\n#####[ \t]*([^\[\n]*)(\[#(\S*)[ \t]*(.*)\])?\n/,
  233. endRegexp: /\n#{1,5}\s/,
  234. content: {
  235. start: {
  236. point: 'end',
  237. offset: -1
  238. },
  239. end: {
  240. point: 'start',
  241. offset: 0
  242. }
  243. },
  244. begin: 0,
  245. forward: {
  246. point: 'end', //start, startEnd, end, endEnd
  247. offset: 0
  248. },
  249. title: {
  250. //index: 1,
  251. recursive: true,
  252. },
  253. onbuild(md, mdTags, buildAST){ //this = {tag: }
  254. }
  255. },
  256. heading4: {
  257. paired: true,
  258. recursive: true,
  259. startRegexp: /\n####[ \t]*([^\[\n]*)(\[#(\S*)[ \t]*(.*)\])?\n/,
  260. endRegexp: /\n#{1,4}\s/,
  261. content: {
  262. start: {
  263. point: 'end',
  264. offset: -1
  265. },
  266. end: {
  267. point: 'start',
  268. offset: 0
  269. }
  270. },
  271. begin: 0,
  272. forward: {
  273. point: 'end', //start, startEnd, end, endEnd
  274. offset: 0
  275. },
  276. title: {
  277. //index: 1,
  278. recursive: true,
  279. },
  280. onbuild(md, mdTags, buildAST){ //this = {tag: }
  281. }
  282. },
  283. heading3: {
  284. paired: true,
  285. recursive: true,
  286. startRegexp: /\n###[ \t]*([^\[\n]*)(\[#(\S*)[ \t]*(.*)\])?\n/,
  287. endRegexp: /\n#{1,3}\s/,
  288. content: {
  289. start: {
  290. point: 'end',
  291. offset: -1
  292. },
  293. end: {
  294. point: 'start',
  295. offset: 0
  296. }
  297. },
  298. begin: 0,
  299. forward: {
  300. point: 'end', //start, startEnd, end, endEnd
  301. offset: 0
  302. },
  303. title: {
  304. //index: 1,
  305. recursive: true,
  306. },
  307. onbuild(md, mdTags, buildAST){ //this = {tag: }
  308. }
  309. },
  310. heading2: {
  311. paired: true,
  312. recursive: true,
  313. startRegexp: /\n##[ \t]*([^\[\n]*)(\[#(\S*)[ \t]*(.*)\])?\n/,
  314. endRegexp: /\n#{1,2}\s/,
  315. content: {
  316. start: {
  317. point: 'end',
  318. offset: -1
  319. },
  320. end: {
  321. point: 'start',
  322. offset: 0
  323. }
  324. },
  325. begin: 0,
  326. forward: {
  327. point: 'end', //start, startEnd, end, endEnd
  328. offset: 0
  329. },
  330. title: {
  331. //index: 1,
  332. recursive: true,
  333. },
  334. onbuild(md, mdTags, buildAST){ //this = {tag: }
  335. }
  336. },
  337. heading1: {
  338. paired: true,
  339. recursive: true,
  340. startRegexp: /\n#[ \t]*([^\[\n]*)(\[#(\S*)[ \t]*(.*)\])?\n/,
  341. endRegexp: /\n#\s/,
  342. content: {
  343. start: {
  344. point: 'end',
  345. offset: -1
  346. },
  347. end: {
  348. point: 'start',
  349. offset: 0
  350. }
  351. },
  352. begin: 0,
  353. forward: {
  354. point: 'end', //start, startEnd, end, endEnd
  355. offset: 0
  356. },
  357. title: {
  358. //index: 1,
  359. recursive: true,
  360. },
  361. onbuild(md, mdTags, buildAST){ //this = {tag: }
  362. }
  363. },
  364. code: {
  365. paired: true,
  366. recursive: false,
  367. startRegexp: /`/,
  368. endRegexp: /`/,
  369. content: {
  370. start: {
  371. point: 'start',
  372. offset: 1
  373. },
  374. end: {
  375. point: 'start',
  376. offset: 0
  377. }
  378. },
  379. begin: 0,
  380. forward: {
  381. point: 'end', //start, startEnd, end, endEnd
  382. offset: 1
  383. }
  384. },
  385. codeMultiLine: {
  386. paired: true,
  387. recursive: false,
  388. startRegexp: /\n```\s*\n/,
  389. endRegexp: /\n```[ \t]*\n/,
  390. content:{
  391. start:{
  392. point: 'end',
  393. offset: 0
  394. },
  395. end:{
  396. point: 'start',
  397. offset: 0
  398. }
  399. },
  400. begin: 1,
  401. forward: {
  402. point: 'endEnd',
  403. offset: -1
  404. },
  405. },
  406. codeLanguage: {
  407. paired: true,
  408. recursive: false,
  409. startRegexp: /\n```(\w+)\s*\n/,
  410. endRegexp: /\n```[ \t]*\n/,
  411. title: {
  412. recursive: false
  413. },
  414. content:{
  415. start:{
  416. point: 'end',
  417. offset: 0
  418. },
  419. end:{
  420. point: 'start',
  421. offset: 0
  422. }
  423. },
  424. begin: 1,
  425. forward: {
  426. point: 'endEnd',
  427. offset: -1
  428. },
  429. },
  430. unOrderedList: {
  431. indent: true,
  432. childName: 'unOrderedListItem',
  433. //paired: true,
  434. recursive: true,
  435. regexp: /-\s*\S/,
  436. content:{
  437. start:{
  438. point: 'end',
  439. offset: -1
  440. },
  441. end:{
  442. point: 'start',
  443. offset: 0
  444. }
  445. },
  446. begin: 1,
  447. forward: {
  448. point: 'end',
  449. offset: 0
  450. }
  451. },
  452. orderedList: {
  453. indent: true,
  454. childName: 'orderedListItem',
  455. //paired: true,
  456. recursive: true,
  457. regexp: /\d+\.\s*\S/,
  458. content:{
  459. start:{
  460. point: 'end',
  461. offset: -1
  462. },
  463. end:{
  464. point: 'start',
  465. offset: 0
  466. }
  467. },
  468. begin: 1,
  469. forward: {
  470. point: 'end',
  471. offset: 0
  472. }
  473. },
  474. p:{
  475. paired: true,
  476. recursive: true,
  477. startRegexp: /\n\s*\S/,
  478. endRegexp: /\n/,
  479. content: {
  480. start: {
  481. point: 'start',
  482. offset: 1
  483. },
  484. end: {
  485. point: 'start',
  486. offset: 0
  487. }
  488. },
  489. begin: 1,
  490. forward: {
  491. point: 'end', //start, startEnd, end, endEnd
  492. offset: 0
  493. },
  494. },
  495. }
  496. const indentRegexp = (regexp,count) => new RegExp(`\\n([ \\t]${count === undefined ? '*' : `{${count}}` })` + regexp.toString().slice(1,-1))
  497. const indentEndRegexp = (count) => new RegExp(`\\n([ \\t]${count === undefined ? '*' : `{0,${count}}` })\\S`)
  498. function findNearest(md, mdTags, offset=0){
  499. let nearest, nearestMatch = {index: Infinity};
  500. for (let [mdTag, {paired,
  501. startRegexp,
  502. regexp, indent}] of Object.entries(mdTags)) {
  503. if (mdTag === 'root') continue;
  504. regexp = startRegexp || regexp
  505. regexp = indent ? indentRegexp(regexp) : regexp
  506. let match = md.offsetMatch(offset, regexp)
  507. if (match && match.index < nearestMatch.index){
  508. nearestMatch = match
  509. nearest = mdTag
  510. }
  511. }
  512. return [nearest, nearestMatch]
  513. }
  514. //node:
  515. //{
  516. // tag: 'keyFromSyntax',
  517. // children: [String, Node]
  518. // parent: node
  519. //}
  520. //
  521. String.prototype.offsetMatch = function(offset, ...params){
  522. return this.slice(offset).match(...params)
  523. }
  524. Array.prototype.last = function(amount=-1){
  525. return this[this.length +amount]
  526. }
  527. String.prototype.cutIndent = function(indent){
  528. let lines = this.split('\n').map(line => line.slice(0, indent).match(/^\s*$/) ? line.slice(indent) : line)
  529. return lines.join('\n')
  530. }
  531. function buildAST(md, mdTags=syntax, offset=0, tree={tag: 'root'}, stack=[]){
  532. let currentNode = stack.last() || tree
  533. if (currentNode.tag === 'root') md = '\n' + md + '\n'
  534. currentNode.children = currentNode.children || []
  535. const { children } = currentNode
  536. let {indent, childName, title, recursive, regexp, endRegexp, content: {end: {offset: offsetEnd, point} }, forward } = mdTags[currentNode.tag]
  537. if (indent){
  538. if (currentNode.parent.tag !== currentNode.tag){ //li
  539. let { parent: {children: siblings} } = currentNode
  540. if (siblings.length > 1 && siblings.last(-2).tag === currentNode.tag){
  541. siblings.pop()
  542. currentNode = siblings.last()
  543. }
  544. const { children } = currentNode
  545. const indentLength = currentNode.startMatch[1].length
  546. console.log(indentLength)
  547. currentNode.indentLength = indentLength
  548. endRegexp = indentEndRegexp(indentLength)
  549. let endMatch = md.offsetMatch(offset, endRegexp) || {index: md.length +1, 0: 'zzz'}
  550. let listMD = md.slice(offset, endMatch.index + offset).cutIndent(currentNode.startMatch[0].length -2)
  551. const newNode = {tag: childName, startOffset: offset, parent: currentNode, startMatch: currentNode.startMatch}
  552. children.push(newNode)
  553. newNode.children = buildAST(listMD, mdTags).children
  554. newNode.children.forEach(item => ((typeof item === 'object') && (item.parent = currentNode)))
  555. offset = newNode.endOffset = currentNode.endOffset = endMatch.index + offset
  556. }
  557. }
  558. if (title){
  559. const {index=1, recursive} = title
  560. const {[index]: titleContent } = currentNode.startMatch
  561. if (titleContent && recursive){
  562. currentNode.title = buildAST(titleContent, mdTags).children
  563. currentNode.title.forEach(item => ((typeof item === 'object') && (item.parent = currentNode)))
  564. }
  565. else {
  566. currentNode.title = [titleContent]
  567. }
  568. }
  569. while(offset < md.length){
  570. const [nearest, nearestMatch] = findNearest(md, mdTags, offset)
  571. let endMatch = md.offsetMatch(offset, endRegexp)
  572. if (!recursive || endMatch) { //if we (should) find closing tag
  573. if (!recursive || !nearest || endMatch.index <= nearestMatch.index ){ //if closing tag closer than new nested tag
  574. endMatch = endMatch || {index: md.length - offset, 0: "zzz"}
  575. currentNode.endContent = offset + endMatch.index + offsetEnd + (point === 'end' ? endMatch[0].length : 0)
  576. offset !== currentNode.endContent && children.push(md.slice(offset, currentNode.endContent))
  577. offset += endMatch.index + forward.offset + (forward.point === 'endEnd' ? endMatch[0].length : 0)
  578. currentNode.endOffset = offset
  579. currentNode.endMatch = endMatch
  580. return currentNode
  581. }
  582. }
  583. if (nearest){ //new nested tag
  584. const {begin,content: {start}} = mdTags[nearest]
  585. if (nearestMatch.index){ //if just text before nested tag
  586. nearestMatch.index + begin > 0 && children.push(md.slice(offset, offset + nearestMatch.index + begin))
  587. offset += nearestMatch.index
  588. }
  589. else { //if new tag right under cursor (offset)
  590. let newNode = {tag: nearest, startOffset: offset, parent: currentNode, startMatch: nearestMatch}
  591. children.push(newNode)
  592. newNode = buildAST(md, mdTags, offset + start.offset + (start.point === 'end' ? nearestMatch[0].length : 0), tree, [...stack, newNode])
  593. offset = newNode.endOffset
  594. }
  595. }
  596. else { //no nearest - rest of line to children as text
  597. children.push(md.slice(offset))
  598. offset = md.length
  599. }
  600. }
  601. return currentNode
  602. }
  603. const Heading = ({react:React, children, title, node: {tag, startMatch}}) => {
  604. const level = +tag.slice(-1)
  605. const _ = React.createElement.bind(React)
  606. const [m, m1,m2, id, rest] = startMatch
  607. if (isNaN(level)) throw new SyntaxError('wrong heading name')
  608. return _(React.Fragment, null,
  609. _(`h${level}`, id && {id}, ...title),
  610. _(`div`, null, ...children)
  611. )
  612. }
  613. const A = ({react:React, children, title}) =>{
  614. const _ = React.createElement.bind(React)
  615. return _("a", {children: title, href: children})
  616. }
  617. const Img = ({react:React, children, title, node}) =>{
  618. const _ = React.createElement.bind(React)
  619. return _("img", {alt: title, src: children, className: node.startMatch[1] || undefined})
  620. }
  621. export const defaultMapMDToComponents = {
  622. heading1: Heading,
  623. heading2: Heading,
  624. heading3: Heading,
  625. heading4: Heading,
  626. heading5: Heading,
  627. heading6: Heading,
  628. strike: "strike",
  629. bold1: "strong",
  630. bold2: "strong",
  631. p: "p",
  632. a: A,
  633. img: Img,
  634. italic1: "i",
  635. italic2: "i",
  636. unOrderedList: 'ul',
  637. orderedList: 'ol',
  638. unOrderedListItem: 'li',
  639. orderedListItem: 'li',
  640. code: 'code',
  641. codeMultiLine: 'pre',
  642. codeLanguage: 'pre',
  643. root: ""
  644. }
  645. function toReact(ast, React, mapMDToComponents=defaultMapMDToComponents){
  646. mapMDToComponents = {...defaultMapMDToComponents, ...mapMDToComponents}
  647. const gC = (tag, c) => (c = mapMDToComponents[tag]) ? c : (c === "" ? React.Fragment : "span")
  648. const RenderComponent = gC(ast.tag)
  649. const _ = React.createElement.bind(React)
  650. const childToReact = child => typeof child === 'string' ? child :
  651. toReact(child, React, mapMDToComponents)
  652. const props = {key: Math.random(),
  653. children: ast.children.map(childToReact)}
  654. if (typeof RenderComponent !== 'string' && RenderComponent !== React.Fragment)
  655. Object.assign(props,{node: ast,
  656. title: ast.title && ast.title.map(childToReact),
  657. react: React})
  658. return _(RenderComponent, props)
  659. }
  660. export {buildAST, toReact, getSecondsFromTimeCode, getTimeCodes, defaultMapMDToComponents}