123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712 |
- /*
- * md ast - pluggable markdown parser
- */
- const getSecondsFromTimeCode = timeCode => {
- let times = timeCode.split(':').map(x => +x).filter(x => !isNaN(x))
- return times.length === 2 ? times[0]*60 + times[1] :
- (times.length === 3 ? times[0]*60*60 + times[1]*60 + times[2] : NaN )
- }
- const getTimeCodeFromSeconds = seconds => {
- let hours = Math.floor(seconds / 3600)
- let minutes = Math.floor((seconds % 3600) / 60)
- let secs = Math.floor((seconds % 3600) % 60)
- return `${hours}:${minutes}:${secs}`
- }
- const getTimeCodes = ast =>{
- let timeCodes = {}
- const walker = node => ((node.tag && node.tag.startsWith('heading') && (timeCodes[node.startMatch[4] || node.startMatch[3]] = node.startMatch[3])),
- (node.children instanceof Array && node.children.forEach(walker)))
- walker(ast)
- delete timeCodes.undefined
- let result = {}
- for (let timeCode in timeCodes){
- result[getSecondsFromTimeCode(timeCode)] = timeCodes[timeCode]
- }
- return result;
- }
- const syntax = {
- img:{
- paired: true,
- recursive: false,
- startRegexp: /!(\S*)\[(.*)\]\(/,
- endRegexp: /\)/,
- content: {
- start: {
- point: 'end',
- offset: 0
- },
- end: {
- point: 'start',
- offset: 0
- }
- },
- begin: 0,
- forward: {
- point: 'end', //start, startEnd, end, endEnd
- offset: 1
- },
- title: {
- index: 2,
- recursive: false,
- },
- onbuild(md, mdTags, buildAST){ //this = {tag: }
- }
- },
- a:{
- paired: true,
- recursive: false,
- startRegexp: /\[(.*)\]\(/,
- endRegexp: /\)/,
- content: {
- start: {
- point: 'end',
- offset: 0
- },
- end: {
- point: 'start',
- offset: 0
- }
- },
- begin: 0,
- forward: {
- point: 'end', //start, startEnd, end, endEnd
- offset: 1
- },
- title: {
- //index: 1,
- recursive: true,
- },
- onbuild(md, mdTags, buildAST){ //this = {tag: }
- }
- },
- strike: {
- paired: true,
- recursive: true,
- startRegexp: /\~\~\S.*/,
- endRegexp: /\~\~\W/,
- content: {
- start: {
- point: 'start',
- offset: 2
- },
- end: {
- point: 'start',
- offset: 0
- }
- },
- begin: 0,
- forward: {
- point: 'endEnd', //start, startEnd, end, endEnd
- offset: -1
- }
- },
- bold1: {
- paired: true,
- recursive: true,
- startRegexp: /\*\*\S.*/,
- endRegexp: /\*\*\W/,
- content: {
- start: {
- point: 'start',
- offset: 2
- },
- end: {
- point: 'start',
- offset: 0
- }
- },
- begin: 0,
- forward: {
- point: 'endEnd', //start, startEnd, end, endEnd
- offset: -1
- }
- },
- bold2: {
- paired: true,
- recursive: true,
- startRegexp: /\s__\S.*/,
- endRegexp: /__\W/,
- content: {
- start: {
- point: 'start',
- offset: 3
- },
- end: {
- point: 'start',
- offset: 0
- }
- },
- begin: 1,
- forward: {
- point: 'endEnd', //start, startEnd, end, endEnd
- offset: -1
- }
- },
- italic1: {
- paired: true,
- recursive: true,
- startRegexp: /\*\S.*/,
- endRegexp: /\S\*[^*]/,
- content: {
- start: {
- point: 'start',
- offset: 1
- },
- end: {
- point: 'start',
- offset: 1
- }
- },
- begin: 0,
- forward: {
- point: 'endEnd', //start, startEnd, end, endEnd
- offset: -1
- }
- },
- italic2: {
- paired: true,
- recursive: true,
- startRegexp: /\s_\S.*/,
- endRegexp: /\S_\W/,
- content: {
- start: {
- point: 'start',
- offset: 2
- },
- end: {
- point: 'start',
- offset: 1
- }
- },
- begin: 1,
- forward: {
- point: 'endEnd', //start, startEnd, end, endEnd
- offset: -1
- }
- },
- root: {
- paired: true,
- recursive: true,
- startRegexp: /^/,
- endRegexp: /$/,
- content: {
- start: {
- point: 'start',
- offset: 1
- },
- end: {
- point: 'end',
- offset: 0
- }
- },
- begin: 0,
- forward: {
- point: 'endEnd', //start, startEnd, end, endEnd
- offset: -1
- }
- },
- heading6: {
- paired: true,
- recursive: true,
- startRegexp: /\n######[ \t]*([^\[\n]*)(\[#(\S*)[ \t]*(.*)\])?\n/,
- endRegexp: /\n#+/,
- content: {
- start: {
- point: 'end',
- offset: -1
- },
- end: {
- point: 'start',
- offset: 0
- }
- },
- begin: 0,
- forward: {
- point: 'end', //start, startEnd, end, endEnd
- offset: 0
- },
- title: {
- //index: 1,
- recursive: true,
- },
- onbuild(md, mdTags, buildAST){ //this = {tag: }
- }
- },
- heading5: {
- paired: true,
- recursive: true,
- startRegexp: /\n#####[ \t]*([^\[\n]*)(\[#(\S*)[ \t]*(.*)\])?\n/,
- endRegexp: /\n#{1,5}\s/,
- content: {
- start: {
- point: 'end',
- offset: -1
- },
- end: {
- point: 'start',
- offset: 0
- }
- },
- begin: 0,
- forward: {
- point: 'end', //start, startEnd, end, endEnd
- offset: 0
- },
- title: {
- //index: 1,
- recursive: true,
- },
- onbuild(md, mdTags, buildAST){ //this = {tag: }
- }
- },
- heading4: {
- paired: true,
- recursive: true,
- startRegexp: /\n####[ \t]*([^\[\n]*)(\[#(\S*)[ \t]*(.*)\])?\n/,
- endRegexp: /\n#{1,4}\s/,
- content: {
- start: {
- point: 'end',
- offset: -1
- },
- end: {
- point: 'start',
- offset: 0
- }
- },
- begin: 0,
- forward: {
- point: 'end', //start, startEnd, end, endEnd
- offset: 0
- },
- title: {
- //index: 1,
- recursive: true,
- },
- onbuild(md, mdTags, buildAST){ //this = {tag: }
- }
- },
- heading3: {
- paired: true,
- recursive: true,
- startRegexp: /\n###[ \t]*([^\[\n]*)(\[#(\S*)[ \t]*(.*)\])?\n/,
- endRegexp: /\n#{1,3}\s/,
- content: {
- start: {
- point: 'end',
- offset: -1
- },
- end: {
- point: 'start',
- offset: 0
- }
- },
- begin: 0,
- forward: {
- point: 'end', //start, startEnd, end, endEnd
- offset: 0
- },
- title: {
- //index: 1,
- recursive: true,
- },
- onbuild(md, mdTags, buildAST){ //this = {tag: }
- }
- },
- heading2: {
- paired: true,
- recursive: true,
- startRegexp: /\n##[ \t]*([^\[\n]*)(\[#(\S*)[ \t]*(.*)\])?\n/,
- endRegexp: /\n#{1,2}\s/,
- content: {
- start: {
- point: 'end',
- offset: -1
- },
- end: {
- point: 'start',
- offset: 0
- }
- },
- begin: 0,
- forward: {
- point: 'end', //start, startEnd, end, endEnd
- offset: 0
- },
- title: {
- //index: 1,
- recursive: true,
- },
- onbuild(md, mdTags, buildAST){ //this = {tag: }
- }
- },
- heading1: {
- paired: true,
- recursive: true,
- startRegexp: /\n#[ \t]*([^\[\n]*)(\[#(\S*)[ \t]*(.*)\])?\n/,
- endRegexp: /\n#\s/,
- content: {
- start: {
- point: 'end',
- offset: -1
- },
- end: {
- point: 'start',
- offset: 0
- }
- },
- begin: 0,
- forward: {
- point: 'end', //start, startEnd, end, endEnd
- offset: 0
- },
- title: {
- //index: 1,
- recursive: true,
- },
- onbuild(md, mdTags, buildAST){ //this = {tag: }
- }
- },
- code: {
- paired: true,
- recursive: false,
- startRegexp: /`/,
- endRegexp: /`/,
- content: {
- start: {
- point: 'start',
- offset: 1
- },
- end: {
- point: 'start',
- offset: 0
- }
- },
- begin: 0,
- forward: {
- point: 'end', //start, startEnd, end, endEnd
- offset: 1
- }
- },
- codeMultiLine: {
- paired: true,
- recursive: false,
- startRegexp: /\n```\s*\n/,
- endRegexp: /\n```[ \t]*\n/,
- content:{
- start:{
- point: 'end',
- offset: 0
- },
- end:{
- point: 'start',
- offset: 0
- }
- },
- begin: 1,
- forward: {
- point: 'endEnd',
- offset: -1
- },
- },
- codeLanguage: {
- paired: true,
- recursive: false,
- startRegexp: /\n```(\w+)\s*\n/,
- endRegexp: /\n```[ \t]*\n/,
- title: {
- recursive: false
- },
- content:{
- start:{
- point: 'end',
- offset: 0
- },
- end:{
- point: 'start',
- offset: 0
- }
- },
- begin: 1,
- forward: {
- point: 'endEnd',
- offset: -1
- },
- },
- unOrderedList: {
- indent: true,
- childName: 'unOrderedListItem',
- //paired: true,
- recursive: true,
- regexp: /-\s*\S/,
- content:{
- start:{
- point: 'end',
- offset: -1
- },
- end:{
- point: 'start',
- offset: 0
- }
- },
- begin: 1,
- forward: {
- point: 'end',
- offset: 0
- }
- },
- orderedList: {
- indent: true,
- childName: 'orderedListItem',
- //paired: true,
- recursive: true,
- regexp: /\d+\.\s+\S/,
- content:{
- start:{
- point: 'end',
- offset: -1
- },
- end:{
- point: 'start',
- offset: 0
- }
- },
- begin: 1,
- forward: {
- point: 'end',
- offset: 0
- }
- },
- p:{
- paired: true,
- recursive: true,
- startRegexp: /\n\s*\S/,
- endRegexp: /\n/,
- content: {
- start: {
- point: 'start',
- offset: 1
- },
- end: {
- point: 'start',
- offset: 0
- }
- },
- begin: 1,
- forward: {
- point: 'end', //start, startEnd, end, endEnd
- offset: 0
- },
- },
- }
- const indentRegexp = (regexp,count) => new RegExp(`\\n([ \\t]${count === undefined ? '*' : `{${count}}` })` + regexp.toString().slice(1,-1))
- const indentEndRegexp = (count) => new RegExp(`\\n([ \\t]${count === undefined ? '*' : `{0,${count}}` })\\S`)
- function findNearest(md, mdTags, offset=0){
- let nearest, nearestMatch = {index: Infinity};
- for (let [mdTag, {paired,
- startRegexp,
- regexp, indent}] of Object.entries(mdTags)) {
- if (mdTag === 'root') continue;
- regexp = startRegexp || regexp
- regexp = indent ? indentRegexp(regexp) : regexp
- let match = md.offsetMatch(offset, regexp)
- if (match && match.index < nearestMatch.index){
- nearestMatch = match
- nearest = mdTag
- }
- }
- return [nearest, nearestMatch]
- }
- //node:
- //{
- // tag: 'keyFromSyntax',
- // children: [String, Node]
- // parent: node
- //}
- //
- String.prototype.offsetMatch = function(offset, ...params){
- return this.slice(offset).match(...params)
- }
- Array.prototype.last = function(amount=-1){
- return this[this.length +amount]
- }
- String.prototype.cutIndent = function(indent){
- let lines = this.split('\n').map(line => line.slice(0, indent).match(/^\s*$/) ? line.slice(indent) : line)
- return lines.join('\n')
- }
- function buildAST(md, mdTags=syntax, offset=0, tree={tag: 'root'}, stack=[]){
- let currentNode = stack.last() || tree
- if (currentNode.tag === 'root') md = '\n' + md + '\n'
- currentNode.children = currentNode.children || []
- const { children } = currentNode
- let {indent, childName, title, recursive, regexp, endRegexp, content: {end: {offset: offsetEnd, point} }, forward } = mdTags[currentNode.tag]
- if (indent){
- if (currentNode.parent.tag !== currentNode.tag){
- let { parent: {children: siblings} } = currentNode //current node are ol/ul
- if (siblings.length > 1 && siblings.last(-2).tag === currentNode.tag){ //two ol/ul one after another should be concated as li
- siblings.pop() //so remove last ol/ul
- currentNode = siblings.last() //make previous ol/ul currentNode to add new LI into previous ol/li
- }
- const { children } = currentNode
- const indentLength = currentNode.startMatch[1].length
- console.log(indentLength)
- currentNode.indentLength = indentLength
- endRegexp = indentEndRegexp(indentLength)
- let endMatch = md.offsetMatch(offset, endRegexp) || {index: md.length +1, 0: 'zzz'}
- let listMD = md.slice(offset, endMatch.index + offset).cutIndent(currentNode.startMatch[0].length -2)
- const newNode = {tag: childName, startOffset: offset, parent: currentNode, startMatch: currentNode.startMatch}
- children.push(newNode) //create LI and add it into children of ol/ul
- newNode.children = buildAST(listMD, mdTags).children
- newNode.children.forEach(item => ((typeof item === 'object') && (item.parent = currentNode)))
- offset = newNode.endOffset = currentNode.endOffset = endMatch.index + offset
- }
- }
-
- if (title){
- const {index=1, recursive} = title
- const {[index]: titleContent } = currentNode.startMatch
- if (titleContent && recursive){
- currentNode.title = buildAST(titleContent, mdTags).children
- currentNode.title.forEach(item => ((typeof item === 'object') && (item.parent = currentNode)))
- }
- else {
- currentNode.title = [titleContent]
- }
- }
- while(offset < md.length){
- const [nearest, nearestMatch] = findNearest(md, mdTags, offset)
- let endMatch = md.offsetMatch(offset, endRegexp)
- if (!recursive || endMatch) { //if we (should) find closing tag
- if (!recursive || !nearest || endMatch.index <= nearestMatch.index ){ //if closing tag closer than new nested tag
- endMatch = endMatch || {index: md.length - offset, 0: "zzz"}
- currentNode.endContent = offset + endMatch.index + offsetEnd + (point === 'end' ? endMatch[0].length : 0)
- offset !== currentNode.endContent && children.push(md.slice(offset, currentNode.endContent))
- offset += endMatch.index + forward.offset + (forward.point === 'endEnd' ? endMatch[0].length : 0)
- currentNode.endOffset = offset
- currentNode.endMatch = endMatch
- return currentNode
- }
- }
- if (nearest){ //new nested tag
- const {begin,content: {start}} = mdTags[nearest]
- if (nearestMatch.index){ //if just text before nested tag
- nearestMatch.index + begin > 0 && children.push(md.slice(offset, offset + nearestMatch.index + begin))
- offset += nearestMatch.index
- }
- else { //if new tag right under cursor (offset)
- let newNode = {tag: nearest, startOffset: offset, parent: currentNode, startMatch: nearestMatch}
- children.push(newNode)
- newNode = buildAST(md, mdTags, offset + start.offset + (start.point === 'end' ? nearestMatch[0].length : 0), tree, [...stack, newNode])
- offset = newNode.endOffset
- }
- }
- else { //no nearest - rest of line to children as text
- children.push(md.slice(offset))
- offset = md.length
- }
- }
- return currentNode
- }
- const Heading = ({react:React, children, title, node: {tag, startMatch}}) => {
- const level = +tag.slice(-1)
- const _ = React.createElement.bind(React)
- const [m, m1,m2, id, rest] = startMatch
- if (isNaN(level)) throw new SyntaxError('wrong heading name')
- return _(React.Fragment, null,
- _(`h${level}`, id && {id}, ...title),
- _(`div`, null, ...children)
- )
- }
- const A = ({react:React, children, title}) =>{
- const _ = React.createElement.bind(React)
- return _("a", {children: title, href: children})
- }
- const Img = ({react:React, children, title, node}) =>{
- const _ = React.createElement.bind(React)
- return _("img", {alt: title, src: children, className: node.startMatch[1] || undefined})
- }
- const defaultMapMDToComponents = {
- heading1: Heading,
- heading2: Heading,
- heading3: Heading,
- heading4: Heading,
- heading5: Heading,
- heading6: Heading,
- strike: "strike",
- bold1: "strong",
- bold2: "strong",
- p: "p",
- a: A,
- img: Img,
- italic1: "i",
- italic2: "i",
- unOrderedList: 'ul',
- orderedList: 'ol',
- unOrderedListItem: 'li',
- orderedListItem: 'li',
- code: 'code',
- codeMultiLine: 'pre',
- codeLanguage: 'pre',
- root: ""
- }
- function toReact(ast, React, mapMDToComponents=defaultMapMDToComponents){
- mapMDToComponents = {...defaultMapMDToComponents, ...mapMDToComponents}
- const gC = (tag, c) => (c = mapMDToComponents[tag]) ? c : (c === "" ? React.Fragment : "span")
- const RenderComponent = gC(ast.tag)
- const _ = React.createElement.bind(React)
- const childToReact = child => typeof child === 'string' ? child :
- toReact(child, React, mapMDToComponents)
- const props = {key: Math.random(),
- children: ast.children && ast.children.map(childToReact)}
- if (typeof RenderComponent !== 'string' && RenderComponent !== React.Fragment)
- Object.assign(props,{node: ast,
- title: ast.title && ast.title.map(childToReact),
- react: React})
-
- return _(RenderComponent, props)
- }
- export {buildAST, toReact, getSecondsFromTimeCode, getTimeCodes, defaultMapMDToComponents , getTimeCodeFromSeconds}
|