md-to-adf
Version:
Translate Markdown (Github) into Atlassian Document Format (ADF)
260 lines (218 loc) • 9.34 kB
JavaScript
/***********************************************************************************************************************
*
* Atlassian Document Format Handling
*
* @author bruno.morel@b-yond.com
* ---------------------------------------------------------------------------------------------------------------------
*
* This transform a Intermediate Representation Tree (see markdownHandling) into the equivalent ADF nodes.
* It also remove non-compatible hierarchy that ADF doesn't support
*
**********************************************************************************************************************/
const { marks, Heading, Text, Emoji, BulletList, OrderedList, ListItem, CodeBlock, BlockQuote, Paragraph, Rule } = require( 'adf-builder' )
const attachTextToNodeSliceEmphasis = require( __dirname + '/adfEmphasisParsing' )
// /**
// * @typedef { import("./markdownParsing").IRElement } IRElement
// * @typedef { import("./markdownHandling").IRTreeNode } IRTreeNode
// */
/**
* Browse the tree recursively to add each node to the ADF Document
* It also treat special cases between top-level node and generic ones
*
* @param currentParentNode {Document} ADF document to add to
* @param currentArrayOfNodesOfSameIndent {IRTreeNode}
*/
function fillADFNodesWithMarkdown( currentParentNode, currentArrayOfNodesOfSameIndent ){
currentArrayOfNodesOfSameIndent.reduce( ( lastListNode, currentNode ) => {
const nodeOrListNode = lastListNode !== null
&& ( currentNode.node.adfType === 'orderedList' || currentNode.node.adfType === 'bulletList' )
&& lastListNode.content.type === currentNode.node.adfType
? lastListNode
: addTypeToNode( currentParentNode, currentNode.node.adfType, currentNode.node.typeParam )
const nodeOrListItem = currentNode.node.adfType === 'orderedList' || currentNode.node.adfType === 'bulletList'
? nodeOrListNode.content.add( new ListItem() )
: nodeOrListNode
const nodeToAttachTextTo = currentNode.node.adfType === 'orderedList' || currentNode.node.adfType === 'bulletList' || currentNode.node.adfType === 'blockQuote'
? typeof currentNode.node.textToEmphasis !== 'undefined' || currentNode.children.length === 0
? nodeOrListItem.content.add( new Paragraph() )
: nodeOrListItem
: nodeOrListItem
if( currentNode.node.adfType === 'divider' )
return lastListNode
else if( currentNode.node.adfType !== 'codeBlock'
&& currentNode.node.textToEmphasis )
attachItemNode( nodeToAttachTextTo, currentNode.node.textToEmphasis )
else if( currentNode.node.adfType !== 'codeBlock'
&& currentNode.node.textToEmphasis === '' )
attachItemNode( nodeToAttachTextTo, ' ' )
else if( currentNode.node.adfType === 'codeBlock' )
attachTextToNodeRaw( nodeToAttachTextTo, currentNode.node.textToEmphasis )
if( currentNode.children )
fillADFNodesWithMarkdown( nodeOrListItem, currentNode.children )
return ( currentNode.node.adfType !== 'orderedList' && currentNode.node.adfType !== 'bulletList' )
|| ( !lastListNode || currentNode.node.adfType === lastListNode.content.type )
? nodeOrListNode
: lastListNode
}, null )
}
/**
* Adding a Top-Level ADF element
*
* @param adfNodeToAttachTo {Node} ADF node to attach this element to
* @param adfType {String} ADF Type of the element we want to attach
* @param typeParams {String} extra params for special top-level nodes
*
* @returns {Node} the node added
*/
function addTypeToNode( adfNodeToAttachTo, adfType, typeParams ){
switch( adfType ) {
case "heading":
return adfNodeToAttachTo.content.add( new Heading( typeParams ) )
case "divider":
return adfNodeToAttachTo.content.add( new Rule() )
case "bulletList":
return adfNodeToAttachTo.content.add( new BulletList() )
case "orderedList": {
const orderedListNode = new OrderedList( )
if( typeParams ) orderedListNode.attrs = { order: typeParams }
return adfNodeToAttachTo.content.add( orderedListNode )
}
case "codeBlock":
return adfNodeToAttachTo.content.add( new CodeBlock( typeParams ) )
case "blockQuote":
return adfNodeToAttachTo.content.add( new BlockQuote() )
case "paragraph":
return adfNodeToAttachTo.content.add( new Paragraph() )
default:
throw 'incompatible type'
}
}
/**
* Adding a non-top-level ADF node
*
* @param nodeToAttachTo {Node} ADF Node to attach to
* @param rawText {String} text content of the node to add
*/
function attachItemNode( nodeToAttachTo, rawText ) {
const slicedInline = sliceInLineCode( rawText )
const { slicedInlineAndEmoji } = slicedInline.reduce( ( { slicedInlineAndEmoji }, currentSlice ) => {
if( !currentSlice.isMatching ){
const slicedEmoji = sliceEmoji( currentSlice.text )
return { slicedInlineAndEmoji: slicedInlineAndEmoji.concat( slicedEmoji ) }
}
slicedInlineAndEmoji.push( currentSlice )
return { slicedInlineAndEmoji }
}, { slicedInlineAndEmoji: [] } )
const { slicedInlineAndEmojiAndLink } = slicedInlineAndEmoji.reduce( ( { slicedInlineAndEmojiAndLink }, currentSlice ) => {
if( !currentSlice.isMatching ){
const slicedLink = sliceLink( currentSlice.text )
return { slicedInlineAndEmojiAndLink: slicedInlineAndEmojiAndLink.concat( slicedLink ) }
}
slicedInlineAndEmojiAndLink.push( currentSlice )
return { slicedInlineAndEmojiAndLink }
}, { slicedInlineAndEmojiAndLink: [] } )
for( const currentSlice of slicedInlineAndEmojiAndLink ) {
switch( currentSlice.type ){
case 'inline':
const inlineCodeNode = new Text( currentSlice.text, marks().code() )
nodeToAttachTo.content.add( inlineCodeNode )
break
case 'emoji':
const emojiNode = new Emoji( {shortName: currentSlice.text } )
nodeToAttachTo.content.add( emojiNode )
break
case 'link':
const linkNode = new Text( currentSlice.text,
marks().link( currentSlice.optionalText1,
currentSlice.optionalText2 ) )
nodeToAttachTo.content.add( linkNode )
break
case 'image':
const imageNode = new Text( currentSlice.text,
marks().link( currentSlice.optionalText1,
currentSlice.optionalText2 ) )
nodeToAttachTo.content.add( imageNode )
break
default:
attachTextToNodeSliceEmphasis( nodeToAttachTo, currentSlice.text )
// const textNode = new Text( currentSlice.text, marksToUse )
// nodeToAttachTo.content.add( textNode )
}
}
}
/**
* Match text content with and ADF inline type
*
* @param rawText {String} the text content to try to match
*
* @returns {String[]} the different slice matching an inline style
*/
function sliceInLineCode( rawText ){
return sliceOneMatchFromRegexp( rawText, 'inline', /(?<nonMatchBefore>[^`]*)(?:`(?<match>[^`]+)`)(?<nonMatchAfter>[^`]*)/g )
}
/**
* Match text content with and ADF emoji type
*
* @param rawText {String} the text content to try to match
*
* @returns {String[]} the different slice matching an emoji style
*/
function sliceEmoji( rawText ){
return sliceOneMatchFromRegexp( rawText, 'emoji',/(?<nonMatchBefore>[^`]*)(?::(?<match>[^`\s]+):)(?<nonMatchAfter>[^`]*)/g )
}
/**
* Match text content with and ADF link type
*
* @param rawText {String} the text content to try to match
*
* @returns {String[]} the different slice matching a link style
*/
function sliceLink( rawText ){
return sliceOneMatchFromRegexp( rawText, 'link',/(?<nonMatchBefore>[^`]*)(?:\[(?<match>[^\[\]]+)\]\((?<matchOptional>[^\(\)"]+)(?: "(?<matchOptional2>[^"]*)")?\))(?<nonMatchAfter>[^`]*)/g )
}
/**
* Match text content with and regular expression with one match
*
* @param rawText {String} the text content to try to match
* @param typeTag {String} the ADF Type to return if it matches
* @param regexpToSliceWith {RegExp} the regexp with a match group and a non-match group to use
*
* @returns {String[]} the different slice matching the specified regexp
*/
function sliceOneMatchFromRegexp( rawText, typeTag, regexpToSliceWith ){
let slicesResult = [ ]
let snippet = null
let hasAtLeastOneExpression = false
while( ( snippet = regexpToSliceWith.exec( rawText ) ) ) {
hasAtLeastOneExpression = true
if( snippet.groups.nonMatchBefore ){
slicesResult.push( { isMatching: false, text: snippet.groups.nonMatchBefore } )
}
if( snippet.groups.match ){
slicesResult.push( {
isMatching: true,
type: typeTag,
text: snippet.groups.match,
optionalText1: snippet.groups.matchOptional,
optionalText2: snippet.groups.matchOptional2
} )
}
if( snippet.groups.nonMatchAfter ){
slicesResult.push( { isMatching: false, text: snippet.groups.nonMatchAfter } )
}
}
if( !hasAtLeastOneExpression )
slicesResult.push( { isMatching: false, text: rawText } )
return slicesResult
}
/**
* Attach a raw simple text node to the parent
*
* @param nodeToAttachTo {Node} ADF node to attach to
* @param textToAttach {String} text to use for the Text node
*/
function attachTextToNodeRaw( nodeToAttachTo, textToAttach ){
const textNode = new Text( textToAttach )
nodeToAttachTo.content.add( textNode )
}
module.exports = fillADFNodesWithMarkdown