You are my hero! I´m trying to build a markdown parser from scratch. Ok, there are many implementations, but non of them fits my needs. I know that I should use a syntax tree, but I´m not sure about the implementation. By the way, I want to use the same markdown syntax most parsers use (e.g. this), but some elements are tricky to capture:
Ordered Lists do not have a fixed delimiter, any number followed by a dot can be used
A square bracket can be a link, if it is followed by a round bracket, other wise it´s just a square bracket.
Any hints how to get the right approach? For some reason I do not want to use a parser generator, so I´m building anything from scratch.
Ordered lists in Markdown can have any number followed by a dot (1., 2., etc.).
Tokenization Strategy:
Look for lines starting with a pattern \d+\. (one or more digits followed by a dot).
Capture the number and the list item content.
Example:
1. First item
2. Second item
10. Tenth item
Parsing Strategy:
Create a ListNode for the ordered list.
Create ListItemNode children for each list item.
Links
Links in Markdown have the format [text](url).
Tokenization Strategy:
Look for patterns matching \[.*?\]\(.*?\).
Capture the link text and URL separately.
Example:
[OpenAI](https://www.openai.com)
Parsing Strategy:
Create a LinkNode with text and url attributes.
Example Implementation in Python
Here is a simplified example of how you might start implementing this in Python:
importreclassTokenType:TEXT='TEXT'HEADING='HEADING'ORDERED_LIST_ITEM='ORDERED_LIST_ITEM'LINK='LINK'classToken:def__init__(self,type,value):self.type=typeself.value=valuedeftokenize(markdown):tokens=[]lines=markdown.split('\n')forlineinlines:ifre.match(r'^\d+\.\s',line):tokens.append(Token(TokenType.ORDERED_LIST_ITEM,line))elifre.match(r'^\[.*?\]\(.*?\)$',line):tokens.append(Token(TokenType.LINK,line))else:tokens.append(Token(TokenType.TEXT,line))returntokensclassNodeType:DOCUMENT='DOCUMENT'PARAGRAPH='PARAGRAPH'ORDERED_LIST='ORDERED_LIST'LIST_ITEM='LIST_ITEM'LINK='LINK'classNode:def__init__(self,type,value=None):self.type=typeself.value=valueself.children=[]defparse(tokens):root=Node(NodeType.DOCUMENT)current_node=rootfortokenintokens:iftoken.type==TokenType.ORDERED_LIST_ITEM:list_item_node=Node(NodeType.LIST_ITEM,token.value)ifcurrent_node.type!=NodeType.ORDERED_LIST:current_node=Node(NodeType.ORDERED_LIST)root.children.append(current_node)current_node.children.append(list_item_node)eliftoken.type==TokenType.LINK:match=re.match(r'^\[(.*?)\]\((.*?)\)$',token.value)link_node=Node(NodeType.LINK,{'text':match.group(1),'url':match.group(2)})root.children.append(link_node)else:paragraph_node=Node(NodeType.PARAGRAPH,token.value)root.children.append(paragraph_node)returnrootdefrender_html(node):ifnode.type==NodeType.DOCUMENT:return''.join(render_html(child)forchildinnode.children)elifnode.type==NodeType.PARAGRAPH:returnf'<p>{node.value}</p>'elifnode.type==NodeType.ORDERED_LIST:returnf'<ol>{"".join(render_html(child)forchildinnode.children)}</ol>'elifnode.type==NodeType.LIST_ITEM:returnf'<li>{node.value}</li>'elifnode.type==NodeType.LINK:returnf'<a href="{node.value["url"]}">{node.value["text"]}</a>'# Example usage
markdown_text="""1. First item
2. Second item
[OpenAI](https://www.openai.com)"""tokens=tokenize(markdown_text)ast=parse(tokens)html=render_html(ast)print(html)
Explanation
Tokenize: The tokenize function processes each line of the Markdown input and generates tokens based on patterns.
Parse: The parse function converts tokens into an AST. It recognizes ordered list items and links, nesting them appropriately.
Render: The render_html function traverses the AST to generate HTML output.
This is a basic framework. You can extend it by adding more sophisticated handling for other Markdown features like unordered lists, blockquotes, code blocks, etc. Additionally, refining the tokenization and parsing logic will help in accurately capturing and rendering all Markdown syntax.
Please correct me, I´m not used to Python and not very good at reading RegEx, but If I understand it right, you might get trouble with nested brackets like this (?)
You're correct. The regular expression I provided for links doesn't handle nested brackets properly. Nested brackets can indeed cause issues because the regex pattern \[.*?\]\(.*?\) will greedily match the first closing bracket, leading to incorrect tokenization.
To handle nested brackets correctly, we need a more sophisticated approach. One way is to use a stack to track the brackets during the tokenization phase. This approach can manage nested brackets by ensuring each opening bracket has a corresponding closing bracket.
Here is the code example in typescript :
enumTokenType{TEXT='TEXT',HEADING='HEADING',ORDERED_LIST_ITEM='ORDERED_LIST_ITEM',LINK='LINK'}classToken{type:TokenType;value:string;constructor(type:TokenType,value:string){this.type=type;this.value=value;}}functiontokenize(markdown:string):Token[]{consttokens:Token[]=[];constlines=markdown.split('\n');for (constlineoflines){if (/^\d+\.\s/.test(line)){tokens.push(newToken(TokenType.ORDERED_LIST_ITEM,line));}elseif (/\[.*?\]\(.*?\)/.test(line)){tokens.push(newToken(TokenType.LINK,line));}else{tokens.push(newToken(TokenType.TEXT,line));}}returntokens;}enumNodeType{DOCUMENT='DOCUMENT',PARAGRAPH='PARAGRAPH',ORDERED_LIST='ORDERED_LIST',LIST_ITEM='LIST_ITEM',LINK='LINK'}classNode{type:NodeType;value:any;children:Node[];constructor(type:NodeType,value:any=null){this.type=type;this.value=value;this.children=[];}}functionparse(tokens:Token[]):Node{constroot=newNode(NodeType.DOCUMENT);letcurrentNode=root;for (consttokenoftokens){if (token.type===TokenType.ORDERED_LIST_ITEM){constlistItemNode=newNode(NodeType.LIST_ITEM,token.value);if (currentNode.type!==NodeType.ORDERED_LIST){currentNode=newNode(NodeType.ORDERED_LIST);root.children.push(currentNode);}currentNode.children.push(listItemNode);}elseif (token.type===TokenType.LINK){constlinkNode=parseLink(token.value);root.children.push(linkNode);}else{constparagraphNode=newNode(NodeType.PARAGRAPH,token.value);root.children.push(paragraphNode);}}returnroot;}functionparseLink(text:string):Node{conststack:string[]=[];letlinkText='';leturl='';leti=0;letinsideLinkText=false;while (i<text.length){constchar=text[i];if (char==='['){stack.push(char);if (!insideLinkText){insideLinkText=true;i++;continue;}}elseif (char===']'&&stack[stack.length-1]==='['){stack.pop();if (stack.length===0){insideLinkText=false;i+=2;// Skip "]("continue;}}elseif (char==='('&&!insideLinkText){stack.push(char);}elseif (char===')'&&stack[stack.length-1]==='('){stack.pop();if (stack.length===0){break;}}if (insideLinkText){linkText+=char;}else{url+=char;}i++;}returnnewNode(NodeType.LINK,{text:linkText,url:url});}functionrenderHTML(node:Node):string{switch (node.type){caseNodeType.DOCUMENT:returnnode.children.map(renderHTML).join('');caseNodeType.PARAGRAPH:return`<p>${node.value}</p>`;caseNodeType.ORDERED_LIST:return`<ol>${node.children.map(renderHTML).join('')}</ol>`;caseNodeType.LIST_ITEM:return`<li>${node.value}</li>`;caseNodeType.LINK:return`<a href="${node.value.url}">${node.value.text}</a>`;default:return'';}}// Example usageconstmarkdownText=`1. First item
2. Second item
[[Link-Text]](https://www.example.com)`;consttokens=tokenize(markdownText);constast=parse(tokens);consthtml=renderHTML(ast);console.log(html);
The parseLink function uses a stack to handle nested brackets, ensuring that link text and URLs are correctly identified even when nested brackets are present.
You are my hero! I´m trying to build a markdown parser from scratch. Ok, there are many implementations, but non of them fits my needs. I know that I should use a syntax tree, but I´m not sure about the implementation. By the way, I want to use the same markdown syntax most parsers use (e.g. this), but some elements are tricky to capture:
Ordered Lists
Ordered lists in Markdown can have any number followed by a dot (
1.,2., etc.).Tokenization Strategy:
\d+\.(one or more digits followed by a dot).Example:
Parsing Strategy:
ListNodefor the ordered list.ListItemNodechildren for each list item.Links
Links in Markdown have the format
[text](url).Tokenization Strategy:
\[.*?\]\(.*?\).Example:
Parsing Strategy:
LinkNodewithtextandurlattributes.Example Implementation in Python
Here is a simplified example of how you might start implementing this in Python:
Explanation
tokenizefunction processes each line of the Markdown input and generates tokens based on patterns.parsefunction converts tokens into an AST. It recognizes ordered list items and links, nesting them appropriately.render_htmlfunction traverses the AST to generate HTML output.This is a basic framework. You can extend it by adding more sophisticated handling for other Markdown features like unordered lists, blockquotes, code blocks, etc. Additionally, refining the tokenization and parsing logic will help in accurately capturing and rendering all Markdown syntax.
Thank you so much for your answer!
Please correct me, I´m not used to Python and not very good at reading RegEx, but If I understand it right, you might get trouble with nested brackets like this (?)
[[Link-Text]](URL.com)
You're correct. The regular expression I provided for links doesn't handle nested brackets properly. Nested brackets can indeed cause issues because the regex pattern
\[.*?\]\(.*?\)will greedily match the first closing bracket, leading to incorrect tokenization.To handle nested brackets correctly, we need a more sophisticated approach. One way is to use a stack to track the brackets during the tokenization phase. This approach can manage nested brackets by ensuring each opening bracket has a corresponding closing bracket.
Here is the code example in typescript :
The parseLink function uses a stack to handle nested brackets, ensuring that link text and URLs are correctly identified even when nested brackets are present.
Was this post created with ChatGPT?
At least he gave you information, bro. take it or shut up.
Thanks
This code didn't work for me. I had to modify the traverse function to pass source file into node.getText().