index.mjs 17 KB

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