index.mjs 19 KB

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