diff --git a/examples/webtools/mdbook_markdown/content/cybercity.md b/examples/webtools/mdbook_markdown/content/cybercity.md new file mode 100644 index 00000000..866ba40e --- /dev/null +++ b/examples/webtools/mdbook_markdown/content/cybercity.md @@ -0,0 +1,29 @@ +--- +sidebar_position: 10 +title: 'Dunia CyberCity' +description: 'Co-create the Future' +--- + +![alt text](img/cybercity2.png) + + +We are building a 700,000 m2 Regenerative Startup Cyber City + +- 100% co-owned +- regenerative +- autonomous zone + +a city for startups and its creators + +- build a system for augmented collective intelligence +- operate business wise from a digital freezone +- (co)own assets (shares, digital currencies) safely and privately + + +## More Info + +> see [https://friends.threefold.info/cybercity](https://friends.threefold.info/cybercity) + +- login:```planet``` +- passwd:```first``` + diff --git a/examples/webtools/mdbook_markdown/markdown_example.vsh b/examples/webtools/mdbook_markdown/markdown_example.vsh index dbfd3675..b14d3fb3 100755 --- a/examples/webtools/mdbook_markdown/markdown_example.vsh +++ b/examples/webtools/mdbook_markdown/markdown_example.vsh @@ -1,4 +1,4 @@ -#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run +#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run // import freeflowuniverse.herolib.core.texttools import freeflowuniverse.herolib.ui.console diff --git a/examples/webtools/vmarkdown/markdown_example_v.vsh b/examples/webtools/vmarkdown/markdown_example_v.vsh new file mode 100755 index 00000000..43be864c --- /dev/null +++ b/examples/webtools/vmarkdown/markdown_example_v.vsh @@ -0,0 +1,27 @@ +#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run + +// import freeflowuniverse.herolib.core.texttools +import freeflowuniverse.herolib.ui.console +import log +import os +import markdown +import mlib + +path2:="${os.home_dir()}/code/github/freeflowuniverse/herolib/examples/webtools/mdbook_markdown/content/links.md" +path1:="${os.home_dir()}/code/github/freeflowuniverse/herolib/examples/webtools/mdbook_markdown/content/test.md" + +text := os.read_file(path1)! + +// Example 1: Using the built-in plaintext renderer +println('=== PLAINTEXT RENDERING ===') +println(markdown.to_plain(text)) +println('') + +// Example 2: Using our custom structure renderer to show markdown structure +println('=== STRUCTURE RENDERING ===') +println(mlib.to_structure(text)) + +// // Example 3: Using a simple markdown example to demonstrate structure +// println('\n=== STRUCTURE OF A SIMPLE MARKDOWN EXAMPLE ===') +// simple_md := '# Heading 1\n\nThis is a paragraph with **bold** and *italic* text.\n\n- List item 1\n- List item 2\n\n```v\nfn main() {\n\tprintln("Hello, world!")\n}\n```\n\n[Link to V language](https://vlang.io)' +// println(markdown.to_structure(simple_md)) diff --git a/examples/webtools/vmarkdown/markdown_parser.vsh b/examples/webtools/vmarkdown/markdown_parser.vsh new file mode 100755 index 00000000..0fa44370 --- /dev/null +++ b/examples/webtools/vmarkdown/markdown_parser.vsh @@ -0,0 +1,95 @@ +#!/usr/bin/env -S v -n -w -gc none run + +import mlib2 + +fn main() { + // Sample markdown text + text := '# Heading 1 + +This is a paragraph with **bold** and *italic* text. + +## Heading 2 + +- List item 1 +- List item 2 + - Nested item +- List item 3 + +```v +fn main() { + println("Hello, world!") +} +``` + +> This is a blockquote +> with multiple lines + +| Column 1 | Column 2 | Column 3 | +|----------|:--------:|---------:| +| Left | Center | Right | +| Cell 1 | Cell 2 | Cell 3 | + +[Link to V language](https://vlang.io) + +![Image](https://vlang.io/img/v-logo.png) + +Footnote reference[^1] + +[^1]: This is a footnote. +' + + // Example 1: Using the plain text renderer + println('=== PLAINTEXT RENDERING ===') + println(mlib2.to_plain(text)) + println('') + + // Example 2: Using the structure renderer to show markdown structure + println('=== STRUCTURE RENDERING ===') + println(mlib2.to_structure(text)) + + // Example 3: Using the navigator to find specific elements + println('\n=== NAVIGATION EXAMPLE ===') + + // Parse the markdown text + doc := mlib2.parse(text) + + // Create a navigator + mut nav := mlib2.new_navigator(doc) + + // Find all headings + headings := nav.find_all_by_type(.heading) + println('Found ${headings.len} headings:') + for heading in headings { + level := heading.attributes['level'] + println(' ${'#'.repeat(level.int())} ${heading.content}') + } + + // Find all code blocks + code_blocks := nav.find_all_by_type(.code_block) + println('\nFound ${code_blocks.len} code blocks:') + for block in code_blocks { + language := block.attributes['language'] + println(' Language: ${language}') + println(' Content length: ${block.content.len} characters') + } + + // Find all list items + list_items := nav.find_all_by_type(.list_item) + println('\nFound ${list_items.len} list items:') + for item in list_items { + println(' - ${item.content}') + } + + // Find content containing specific text + if element := nav.find_by_content('blockquote') { + println('\nFound element containing "blockquote":') + println(' Type: ${element.typ}') + println(' Content: ${element.content}') + } + + // Find all footnotes + println('\nFootnotes:') + for id, footnote in nav.footnotes() { + println(' [^${id}]: ${footnote.content}') + } +} diff --git a/examples/webtools/vmarkdown/mlib/structure_renderer.v b/examples/webtools/vmarkdown/mlib/structure_renderer.v new file mode 100644 index 00000000..1abc6aaa --- /dev/null +++ b/examples/webtools/vmarkdown/mlib/structure_renderer.v @@ -0,0 +1,194 @@ +module mlib + +import markdown + +import strings + +// Helper functions to extract information from C structs +fn get_md_attribute_string(attr C.MD_ATTRIBUTE) ?string { + unsafe { + if attr.text == nil || attr.size == 0 { + return none + } + return attr.text.vstring_with_len(int(attr.size)) + } +} + +fn get_heading_level(detail voidptr) int { + unsafe { + h_detail := &C.MD_BLOCK_H_DETAIL(detail) + return int(h_detail.level) + } +} + +fn get_code_language(detail voidptr) ?string { + unsafe { + code_detail := &C.MD_BLOCK_CODE_DETAIL(detail) + return get_md_attribute_string(code_detail.lang) + } +} + +fn get_ul_details(detail voidptr) (bool, string) { + unsafe { + ul_detail := &C.MD_BLOCK_UL_DETAIL(detail) + is_tight := ul_detail.is_tight != 0 + mark := ul_detail.mark.ascii_str() + return is_tight, mark + } +} + +fn get_ol_details(detail voidptr) (int, bool) { + unsafe { + ol_detail := &C.MD_BLOCK_OL_DETAIL(detail) + start := int(ol_detail.start) + is_tight := ol_detail.is_tight != 0 + return start, is_tight + } +} + +fn get_link_details(detail voidptr) (?string, ?string) { + unsafe { + a_detail := &C.MD_SPAN_A_DETAIL(detail) + href := get_md_attribute_string(a_detail.href) + title := get_md_attribute_string(a_detail.title) + return href, title + } +} + +fn get_image_details(detail voidptr) (?string, ?string) { + unsafe { + img_detail := &C.MD_SPAN_IMG_DETAIL(detail) + src := get_md_attribute_string(img_detail.src) + title := get_md_attribute_string(img_detail.title) + return src, title + } +} + +fn get_wikilink_target(detail voidptr) ?string { + unsafe { + wl_detail := &C.MD_SPAN_WIKILINK_DETAIL(detail) + return get_md_attribute_string(wl_detail.target) + } +} + +// StructureRenderer is a custom renderer that outputs the structure of a markdown document +pub struct StructureRenderer { +mut: + writer strings.Builder = strings.new_builder(200) + indent int // Track indentation level for nested elements +} + +pub fn (mut sr StructureRenderer) str() string { + return sr.writer.str() +} + +fn (mut sr StructureRenderer) enter_block(typ markdown.MD_BLOCKTYPE, detail voidptr) ? { + // Add indentation based on current level + sr.writer.write_string(strings.repeat(` `, sr.indent * 2)) + + // Output the block type + sr.writer.write_string('BLOCK[${typ}]: ') + + // Add specific details based on block type + match typ { + .md_block_h { + level := get_heading_level(detail) + sr.writer.write_string('Level ${level}') + } + .md_block_code { + if lang := get_code_language(detail) { + sr.writer.write_string('Language: ${lang}') + } else { + sr.writer.write_string('No language specified') + } + } + .md_block_ul { + is_tight, mark := get_ul_details(detail) + sr.writer.write_string('Tight: ${is_tight}, Mark: ${mark}') + } + .md_block_ol { + start, is_tight := get_ol_details(detail) + sr.writer.write_string('Start: ${start}, Tight: ${is_tight}') + } + else {} + } + + sr.writer.write_u8(`\n`) + sr.indent++ +} + +fn (mut sr StructureRenderer) leave_block(typ markdown.MD_BLOCKTYPE, _ voidptr) ? { + sr.indent-- +} + +fn (mut sr StructureRenderer) enter_span(typ markdown.MD_SPANTYPE, detail voidptr) ? { + // Add indentation based on current level + sr.writer.write_string(strings.repeat(` `, sr.indent * 2)) + + // Output the span type + sr.writer.write_string('SPAN[${typ}]: ') + + // Add specific details based on span type + match typ { + .md_span_a { + href, title := get_link_details(detail) + if href != none { + sr.writer.write_string('Link: ${href}') + } + if title != none { + sr.writer.write_string(', Title: ${title}') + } + } + .md_span_img { + src, title := get_image_details(detail) + if src != none { + sr.writer.write_string('Source: ${src}') + } + if title != none { + sr.writer.write_string(', Title: ${title}') + } + } + .md_span_wikilink { + if target := get_wikilink_target(detail) { + sr.writer.write_string('Target: ${target}') + } + } + else {} + } + + sr.writer.write_u8(`\n`) + sr.indent++ +} + +fn (mut sr StructureRenderer) leave_span(typ markdown.MD_SPANTYPE, _ voidptr) ? { + sr.indent-- +} + +fn (mut sr StructureRenderer) text(typ markdown.MD_TEXTTYPE, text string) ? { + if text.trim_space() == '' { + return + } + + // Add indentation based on current level + sr.writer.write_string(strings.repeat(` `, sr.indent * 2)) + + // Output the text type + sr.writer.write_string('TEXT[${typ}]: ') + + // Add the text content (truncate if too long) + content := if text.len > 50 { text[..50] + '...' } else { text } + sr.writer.write_string(content.replace('\n', '\\n')) + + sr.writer.write_u8(`\n`) +} + +fn (mut sr StructureRenderer) debug_log(msg string) { + println(msg) +} + +// to_structure renders a markdown string and returns its structure +pub fn to_structure(input string) string { + mut structure_renderer := StructureRenderer{} + out := markdown.render(input, mut structure_renderer) or { '' } + return out +} diff --git a/examples/webtools/vmarkdown/mlib2/README.md b/examples/webtools/vmarkdown/mlib2/README.md new file mode 100644 index 00000000..34750df8 --- /dev/null +++ b/examples/webtools/vmarkdown/mlib2/README.md @@ -0,0 +1,133 @@ +# V Markdown Parser + +A pure V implementation of a Markdown parser that supports extended Markdown syntax and provides an easy way to navigate through the document structure. + +## Features + +- Parses Markdown text into a structured representation +- Supports both basic and extended Markdown syntax +- Provides an easy way to navigate through the document structure +- Includes renderers for different output formats +- No external dependencies + +## Supported Markdown Syntax + +- Headings (# to ######) +- Paragraphs +- Blockquotes +- Lists (ordered and unordered) +- Task lists +- Code blocks (fenced with language support) +- Tables with alignment +- Horizontal rules +- Footnotes +- Basic text elements (currently as plain text, with planned support for inline formatting) + +## Usage + +### Parsing Markdown + +```v +import mlib2 + +// Parse Markdown text +md_text := '# Hello World\n\nThis is a paragraph.' +doc := mlib2.parse(md_text) + +// Access the document structure +root := doc.root +for child in root.children { + println(child.typ) +} +``` + +### Navigating the Document + +```v +import mlib2 + +// Parse Markdown text +md_text := '# Hello World\n\nThis is a paragraph.' +doc := mlib2.parse(md_text) + +// Create a navigator +mut nav := mlib2.new_navigator(doc) + +// Find elements by type +headings := nav.find_all_by_type(.heading) +for heading in headings { + level := heading.attributes['level'] + println('Heading level ${level}: ${heading.content}') +} + +// Find elements by content +if para := nav.find_by_content('paragraph') { + println('Found paragraph: ${para.content}') +} + +// Navigate through the document +if first_heading := nav.find_by_type(.heading) { + println('First heading: ${first_heading.content}') + + // Move to next sibling + if next := nav.next_sibling() { + println('Next element after heading: ${next.typ}') + } +} +``` + +### Rendering the Document + +```v +import mlib2 + +// Parse Markdown text +md_text := '# Hello World\n\nThis is a paragraph.' + +// Render as structure (for debugging) +structure := mlib2.to_structure(md_text) +println(structure) + +// Render as plain text +plain_text := mlib2.to_plain(md_text) +println(plain_text) +``` + +## Element Types + +The parser recognizes the following element types: + +- `document`: The root element of the document +- `heading`: A heading element (h1-h6) +- `paragraph`: A paragraph of text +- `blockquote`: A blockquote +- `code_block`: A code block +- `list`: A list (ordered or unordered) +- `list_item`: An item in a list +- `task_list_item`: A task list item with checkbox +- `table`: A table +- `table_row`: A row in a table +- `table_cell`: A cell in a table +- `horizontal_rule`: A horizontal rule +- `footnote`: A footnote definition +- `footnote_ref`: A reference to a footnote +- `text`: A text element +- `link`, `image`, `emphasis`, `strong`, `strikethrough`, `inline_code`: Inline formatting elements (planned for future implementation) + +## Element Structure + +Each Markdown element has the following properties: + +- `typ`: The type of the element +- `content`: The text content of the element +- `children`: Child elements +- `attributes`: Additional attributes specific to the element type +- `line_number`: The line number where the element starts in the source +- `column`: The column number where the element starts in the source + +## Future Improvements + +- Implement parsing of inline elements (bold, italic, links, etc.) +- Add HTML renderer +- Support for more extended Markdown syntax +- Performance optimizations diff --git a/examples/webtools/vmarkdown/mlib2/example.v b/examples/webtools/vmarkdown/mlib2/example.v new file mode 100644 index 00000000..2c8eb62e --- /dev/null +++ b/examples/webtools/vmarkdown/mlib2/example.v @@ -0,0 +1,143 @@ +module mlib2 + +// This file contains examples of how to use the Markdown parser + +// Example of parsing and navigating a markdown document +pub fn example_navigation() { + md_text := '# Heading 1 + +This is a paragraph with **bold** and *italic* text. + +## Heading 2 + +- List item 1 +- List item 2 + - Nested item +- List item 3 + +```v +fn main() { + println("Hello, world!") +} +``` + +> This is a blockquote +> with multiple lines + +| Column 1 | Column 2 | Column 3 | +|----------|:--------:|---------:| +| Left | Center | Right | +| Cell 1 | Cell 2 | Cell 3 | + +[Link to V language](https://vlang.io) + +![Image](https://vlang.io/img/v-logo.png) + +Footnote reference[^1] + +[^1]: This is a footnote. +' + + // Parse the markdown text + doc := parse(md_text) + + // Create a navigator + mut nav := new_navigator(doc) + + // Find all headings + headings := nav.find_all_by_type(.heading) + println('Found ${headings.len} headings:') + for heading in headings { + level := heading.attributes['level'] + println(' ${'#'.repeat(level.int())} ${heading.content}') + } + + // Find the first code block + if code_block := nav.find_by_type(.code_block) { + language := code_block.attributes['language'] + println('\nFound code block in language: ${language}') + println('```${language}\n${code_block.content}```') + } + + // Find all list items + list_items := nav.find_all_by_type(.list_item) + println('\nFound ${list_items.len} list items:') + for item in list_items { + println(' - ${item.content}') + } + + // Find content containing specific text + if element := nav.find_by_content('blockquote') { + println('\nFound element containing "blockquote":') + println(' Type: ${element.typ}') + println(' Content: ${element.content}') + } + + // Find table cells + table_cells := nav.find_all_by_type(.table_cell) + println('\nFound ${table_cells.len} table cells:') + for cell in table_cells { + alignment := cell.attributes['align'] or { 'left' } + is_header := cell.attributes['is_header'] or { 'false' } + println(' Cell: "${cell.content}" (align: ${alignment}, header: ${is_header})') + } + + // Find footnotes + println('\nFootnotes:') + for id, footnote in nav.footnotes() { + println(' [^${id}]: ${footnote.content}') + } +} + +// Example of rendering a markdown document +pub fn example_rendering() { + md_text := '# Heading 1 + +This is a paragraph with **bold** and *italic* text. + +## Heading 2 + +- List item 1 +- List item 2 + - Nested item +- List item 3 + +```v +fn main() { + println("Hello, world!") +} +``` + +> This is a blockquote +> with multiple lines +' + + // Parse the markdown text + doc := parse(md_text) + + // Render as structure + mut structure_renderer := new_structure_renderer() + structure := structure_renderer.render(doc) + println('=== STRUCTURE RENDERING ===') + println(structure) + + // Render as plain text + mut plain_text_renderer := new_plain_text_renderer() + plain_text := plain_text_renderer.render(doc) + println('=== PLAIN TEXT RENDERING ===') + println(plain_text) + + // Using convenience functions + println('=== USING CONVENIENCE FUNCTIONS ===') + println(to_structure(md_text)) + println(to_plain(md_text)) +} + +// Main function to run the examples +pub fn main() { + println('=== NAVIGATION EXAMPLE ===') + example_navigation() + + println('\n=== RENDERING EXAMPLE ===') + example_rendering() +} diff --git a/examples/webtools/vmarkdown/mlib2/markdown.v b/examples/webtools/vmarkdown/mlib2/markdown.v new file mode 100644 index 00000000..3f5319e7 --- /dev/null +++ b/examples/webtools/vmarkdown/mlib2/markdown.v @@ -0,0 +1,70 @@ +module mlib2 + +// MarkdownElement represents a single element in a markdown document +pub struct MarkdownElement { +pub: + typ ElementType + content string + children []&MarkdownElement + attributes map[string]string + line_number int + column int +} + +// ElementType represents the type of a markdown element +pub enum ElementType { + document + heading + paragraph + blockquote + code_block + list + list_item + table + table_row + table_cell + horizontal_rule + link + image + emphasis + strong + strikethrough + inline_code + html + text + footnote + footnote_ref + task_list_item +} + +// MarkdownDocument represents a parsed markdown document +pub struct MarkdownDocument { +pub mut: + root &MarkdownElement + footnotes map[string]&MarkdownElement +} + +// Creates a new markdown document +pub fn new_document() MarkdownDocument { + root := &MarkdownElement{ + typ: .document + content: '' + children: [] + } + return MarkdownDocument{ + root: root + footnotes: map[string]&MarkdownElement{} + } +} + +// Parses markdown text and returns a MarkdownDocument +pub fn parse(text string) MarkdownDocument { + mut parser := Parser{ + text: text + pos: 0 + line: 1 + column: 1 + } + return parser.parse() +} + diff --git a/examples/webtools/vmarkdown/mlib2/navigator.v b/examples/webtools/vmarkdown/mlib2/navigator.v new file mode 100644 index 00000000..48508c06 --- /dev/null +++ b/examples/webtools/vmarkdown/mlib2/navigator.v @@ -0,0 +1,269 @@ +module mlib2 + +// Navigator provides an easy way to navigate through the document structure +@[heap] +pub struct Navigator { +pub: + doc MarkdownDocument +pub mut: + current_element &MarkdownElement +} + +// Creates a new navigator for a markdown document +pub fn new_navigator(doc MarkdownDocument) Navigator { + return Navigator{ + doc: doc + current_element: doc.root + } +} + +// Reset the navigator to the root element +pub fn (mut n Navigator) reset() { + n.current_element = n.doc.root +} + +// Find an element by type +pub fn (mut n Navigator) find_by_type(typ ElementType) ?&MarkdownElement { + return n.find_by_type_from(n.doc.root, typ) +} + +// Find an element by type starting from a specific element +fn (mut n Navigator) find_by_type_from(element &MarkdownElement, typ ElementType) ?&MarkdownElement { + if element.typ == typ { + n.current_element = element + return element + } + + for child in element.children { + if child.typ == typ { + n.current_element = child + return child + } + + if result := n.find_by_type_from(child, typ) { + return result + } + } + + return none +} + +// Find all elements by type +pub fn (mut n Navigator) find_all_by_type(typ ElementType) []&MarkdownElement { + return n.find_all_by_type_from(n.doc.root, typ) +} + +// Find all elements by type starting from a specific element +fn (mut n Navigator) find_all_by_type_from(element &MarkdownElement, typ ElementType) []&MarkdownElement { + mut results := []&MarkdownElement{} + + if element.typ == typ { + results << element + } + + for child in element.children { + if child.typ == typ { + results << child + } + + results << n.find_all_by_type_from(child, typ) + } + + return results +} + +// Find an element by content +pub fn (mut n Navigator) find_by_content(text string) ?&MarkdownElement { + return n.find_by_content_from(n.doc.root, text) +} + +// Find an element by content starting from a specific element +fn (mut n Navigator) find_by_content_from(element &MarkdownElement, text string) ?&MarkdownElement { + if element.content.contains(text) { + n.current_element = element + return element + } + + for child in element.children { + if child.content.contains(text) { + n.current_element = child + return child + } + + if result := n.find_by_content_from(child, text) { + return result + } + } + + return none +} + +// Find all elements by content +pub fn (mut n Navigator) find_all_by_content(text string) []&MarkdownElement { + return n.find_all_by_content_from(n.doc.root, text) +} + +// Find all elements by content starting from a specific element +fn (mut n Navigator) find_all_by_content_from(element &MarkdownElement, text string) []&MarkdownElement { + mut results := []&MarkdownElement{} + + if element.content.contains(text) { + results << element + } + + for child in element.children { + if child.content.contains(text) { + results << child + } + + results << n.find_all_by_content_from(child, text) + } + + return results +} + +// Find an element by attribute +pub fn (mut n Navigator) find_by_attribute(key string, value string) ?&MarkdownElement { + return n.find_by_attribute_from(n.doc.root, key, value) +} + +// Find an element by attribute starting from a specific element +fn (mut n Navigator) find_by_attribute_from(element &MarkdownElement, key string, value string) ?&MarkdownElement { + if element.attributes[key] == value { + n.current_element = element + return element + } + + for child in element.children { + if child.attributes[key] == value { + n.current_element = child + return child + } + + if result := n.find_by_attribute_from(child, key, value) { + return result + } + } + + return none +} + +// Find all elements by attribute +pub fn (mut n Navigator) find_all_by_attribute(key string, value string) []&MarkdownElement { + return n.find_all_by_attribute_from(n.doc.root, key, value) +} + +// Find all elements by attribute starting from a specific element +fn (mut n Navigator) find_all_by_attribute_from(element &MarkdownElement, key string, value string) []&MarkdownElement { + mut results := []&MarkdownElement{} + + if element.attributes[key] == value { + results << element + } + + for child in element.children { + if child.attributes[key] == value { + results << child + } + + results << n.find_all_by_attribute_from(child, key, value) + } + + return results +} + +// Find the parent of an element +pub fn (mut n Navigator) find_parent(target &MarkdownElement) ?&MarkdownElement { + return n.find_parent_from(n.doc.root, target) +} + +// Find the parent of an element starting from a specific element +fn (mut n Navigator) find_parent_from(root &MarkdownElement, target &MarkdownElement) ?&MarkdownElement { + for child in root.children { + if child == target { + n.current_element = root + return root + } + + if result := n.find_parent_from(child, target) { + return result + } + } + + return none +} + +// Get the parent of the current element +pub fn (mut n Navigator) parent() ?&MarkdownElement { + return n.find_parent(n.current_element) +} + +// Get the next sibling of the current element +pub fn (mut n Navigator) next_sibling() ?&MarkdownElement { + parent := n.parent() or { return none } + + mut found := false + for child in parent.children { + if found { + n.current_element = child + return child + } + + if child == n.current_element { + found = true + } + } + + return none +} + +// Get the previous sibling of the current element +pub fn (mut n Navigator) prev_sibling() ?&MarkdownElement { + parent := n.parent() or { return none } + + mut prev := &MarkdownElement(0) + for child in parent.children { + if child == n.current_element && prev != 0 { + n.current_element = prev + return prev + } + + prev = child + } + + return none +} + +// Get the first child of the current element +pub fn (mut n Navigator) first_child() ?&MarkdownElement { + if n.current_element.children.len == 0 { + return none + } + + n.current_element = n.current_element.children[0] + return n.current_element +} + +// Get the last child of the current element +pub fn (mut n Navigator) last_child() ?&MarkdownElement { + if n.current_element.children.len == 0 { + return none + } + + n.current_element = n.current_element.children[n.current_element.children.len - 1] + return n.current_element +} + +// Get all footnotes in the document +pub fn (n Navigator) footnotes() map[string]&MarkdownElement { + return n.doc.footnotes +} + +// Get a footnote by identifier +pub fn (n Navigator) footnote(id string) ?&MarkdownElement { + if id in n.doc.footnotes { + return n.doc.footnotes[id] + } + + return none +} diff --git a/examples/webtools/vmarkdown/mlib2/parser_block.v b/examples/webtools/vmarkdown/mlib2/parser_block.v new file mode 100644 index 00000000..43f50087 --- /dev/null +++ b/examples/webtools/vmarkdown/mlib2/parser_block.v @@ -0,0 +1,31 @@ +module mlib2 + +// Parse a block-level element +fn (mut p Parser) parse_block() ?&MarkdownElement { + // Skip whitespace at the beginning of a line + p.skip_whitespace() + + // Check for end of input + if p.pos >= p.text.len { + return none + } + + // Check for different block types + if p.text[p.pos] == `#` { + return p.parse_heading() + } else if p.text[p.pos] == `>` { + return p.parse_blockquote() + } else if p.text[p.pos] == `-` && p.peek(1) == `-` && p.peek(2) == `-` { + return p.parse_horizontal_rule() + } else if p.text[p.pos] == '`' && p.peek(1) == '`' && p.peek(2) == '`' { + return p.parse_fenced_code_block() + } else if p.is_list_start() { + return p.parse_list() + } else if p.is_table_start() { + return p.parse_table() + } else if p.is_footnote_definition() { + return p.parse_footnote_definition() + } else { + return p.parse_paragraph() + } +} diff --git a/examples/webtools/vmarkdown/mlib2/parser_blockquote.v b/examples/webtools/vmarkdown/mlib2/parser_blockquote.v new file mode 100644 index 00000000..bb087935 --- /dev/null +++ b/examples/webtools/vmarkdown/mlib2/parser_blockquote.v @@ -0,0 +1,97 @@ +module mlib2 + +// Parse a blockquote element +fn (mut p Parser) parse_blockquote() ?&MarkdownElement { + start_pos := p.pos + start_line := p.line + start_column := p.column + + // Skip the > character + p.pos++ + p.column++ + + // Skip whitespace after > + p.skip_whitespace() + + mut content := '' + mut lines := []string{} + + // Read the first line + for p.pos < p.text.len && p.text[p.pos] != `\n` { + content += p.text[p.pos].ascii_str() + p.pos++ + p.column++ + } + lines << content + + // Skip the newline + if p.pos < p.text.len && p.text[p.pos] == `\n` { + p.pos++ + p.line++ + p.column = 1 + } + + // Read additional lines of the blockquote + for p.pos < p.text.len { + // Check if the line starts with > + if p.text[p.pos] == `>` { + p.pos++ + p.column++ + p.skip_whitespace() + + mut line := '' + for p.pos < p.text.len && p.text[p.pos] != `\n` { + line += p.text[p.pos].ascii_str() + p.pos++ + p.column++ + } + lines << line + + // Skip the newline + if p.pos < p.text.len && p.text[p.pos] == `\n` { + p.pos++ + p.line++ + p.column = 1 + } + } else if p.text[p.pos] == `\n` { + // Empty line - could be a continuation or the end of the blockquote + p.pos++ + p.line++ + p.column = 1 + + // Check if the next line is part of the blockquote + if p.pos < p.text.len && p.text[p.pos] == `>` { + lines << '' + } else { + break + } + } else { + // Not a blockquote line, end of blockquote + break + } + } + + // Join the lines with newlines + content = lines.join('\n') + + // Create the blockquote element + mut blockquote := &MarkdownElement{ + typ: .blockquote + content: content + line_number: start_line + column: start_column + } + + // Parse nested blocks within the blockquote + mut nested_parser := Parser{ + text: content + pos: 0 + line: start_line + column: start_column + } + + nested_doc := nested_parser.parse() + blockquote.children = nested_doc.root.children + + return blockquote +} diff --git a/examples/webtools/vmarkdown/mlib2/parser_fenced_code_block.v b/examples/webtools/vmarkdown/mlib2/parser_fenced_code_block.v new file mode 100644 index 00000000..8ad71104 --- /dev/null +++ b/examples/webtools/vmarkdown/mlib2/parser_fenced_code_block.v @@ -0,0 +1,117 @@ +module mlib2 + +// Parse a fenced code block element +fn (mut p Parser) parse_fenced_code_block() ?&MarkdownElement { + start_pos := p.pos + start_line := p.line + start_column := p.column + + // Check for opening fence (``` or ~~~) + fence_char := p.text[p.pos] + if fence_char != '`' && fence_char != '~' { + p.pos = start_pos + p.line = start_line + p.column = start_column + return p.parse_paragraph() + } + + // Count fence characters + mut fence_len := 0 + for p.pos < p.text.len && p.text[p.pos] == fence_char { + fence_len++ + p.pos++ + p.column++ + } + + // Must have at least 3 characters + if fence_len < 3 { + p.pos = start_pos + p.line = start_line + p.column = start_column + return p.parse_paragraph() + } + + // Read language identifier + mut language := '' + for p.pos < p.text.len && p.text[p.pos] != `\n` { + language += p.text[p.pos].ascii_str() + p.pos++ + p.column++ + } + language = language.trim_space() + + // Skip the newline + if p.pos < p.text.len && p.text[p.pos] == `\n` { + p.pos++ + p.line++ + p.column = 1 + } + + // Read code content until closing fence + mut content := '' + mut found_closing_fence := false + + for p.pos < p.text.len { + // Check for closing fence + if p.text[p.pos] == fence_char { + mut i := p.pos + mut count := 0 + + // Count fence characters + for i < p.text.len && p.text[i] == fence_char { + count++ + i++ + } + + // Check if it's a valid closing fence + if count >= fence_len { + // Skip to end of line + for i < p.text.len && p.text[i] != `\n` { + i++ + } + + // Update position + p.pos = i + if p.pos < p.text.len && p.text[p.pos] == `\n` { + p.pos++ + p.line++ + p.column = 1 + } + + found_closing_fence = true + break + } + } + + // Add character to content + content += p.text[p.pos].ascii_str() + + // Move to next character + if p.text[p.pos] == `\n` { + p.line++ + p.column = 1 + } else { + p.column++ + } + p.pos++ + } + + // If no closing fence was found, treat as paragraph + if !found_closing_fence { + p.pos = start_pos + p.line = start_line + p.column = start_column + return p.parse_paragraph() + } + + // Create the code block element + return &MarkdownElement{ + typ: .code_block + content: content + line_number: start_line + column: start_column + attributes: { + 'language': language + } + } +} diff --git a/examples/webtools/vmarkdown/mlib2/parser_footnote_definition.v b/examples/webtools/vmarkdown/mlib2/parser_footnote_definition.v new file mode 100644 index 00000000..59a27fdf --- /dev/null +++ b/examples/webtools/vmarkdown/mlib2/parser_footnote_definition.v @@ -0,0 +1,140 @@ +module mlib2 + +// Parse a footnote definition +fn (mut p Parser) parse_footnote_definition() ?&MarkdownElement { + start_pos := p.pos + start_line := p.line + start_column := p.column + + // Skip the [ character + p.pos++ + p.column++ + + // Skip the ^ character + p.pos++ + p.column++ + + // Read the footnote identifier + mut identifier := '' + for p.pos < p.text.len && p.text[p.pos] != `]` { + identifier += p.text[p.pos].ascii_str() + p.pos++ + p.column++ + } + + // Skip the ] character + if p.pos < p.text.len && p.text[p.pos] == `]` { + p.pos++ + p.column++ + } else { + p.pos = start_pos + p.line = start_line + p.column = start_column + return p.parse_paragraph() + } + + // Skip the : character + if p.pos < p.text.len && p.text[p.pos] == `:` { + p.pos++ + p.column++ + } else { + p.pos = start_pos + p.line = start_line + p.column = start_column + return p.parse_paragraph() + } + + // Skip whitespace + p.skip_whitespace() + + // Read the footnote content + mut content := '' + mut lines := []string{} + + // Read the first line + for p.pos < p.text.len && p.text[p.pos] != `\n` { + content += p.text[p.pos].ascii_str() + p.pos++ + p.column++ + } + lines << content + + // Skip the newline + if p.pos < p.text.len && p.text[p.pos] == `\n` { + p.pos++ + p.line++ + p.column = 1 + } + + // Read additional lines of the footnote + for p.pos < p.text.len { + // Check if the line is indented (part of the current footnote) + if p.text[p.pos] == ` ` || p.text[p.pos] == `\t` { + // Count indentation + mut indent := 0 + for p.pos < p.text.len && (p.text[p.pos] == ` ` || p.text[p.pos] == `\t`) { + indent++ + p.pos++ + p.column++ + } + + // If indented enough, it's part of the current footnote + if indent >= 2 { + mut line := '' + for p.pos < p.text.len && p.text[p.pos] != `\n` { + line += p.text[p.pos].ascii_str() + p.pos++ + p.column++ + } + lines << line + + // Skip the newline + if p.pos < p.text.len && p.text[p.pos] == `\n` { + p.pos++ + p.line++ + p.column = 1 + } + } else { + // Not indented enough, end of footnote + break + } + } else if p.text[p.pos] == `\n` { + // Empty line - could be a continuation or the end of the footnote + p.pos++ + p.line++ + p.column = 1 + + // Check if the next line is indented + if p.pos < p.text.len && (p.text[p.pos] == ` ` || p.text[p.pos] == `\t`) { + lines << '' + } else { + break + } + } else { + // Not an indented line, end of footnote + break + } + } + + // Join the lines with newlines + content = lines.join('\n') + + // Create the footnote element + mut footnote := &MarkdownElement{ + typ: .footnote + content: content + line_number: start_line + column: start_column + attributes: { + 'identifier': identifier + } + } + + // Parse inline elements within the footnote + footnote.children = p.parse_inline(content) + + // Add the footnote to the document + p.doc.footnotes[identifier] = footnote + + return footnote +} diff --git a/examples/webtools/vmarkdown/mlib2/parser_heading.v b/examples/webtools/vmarkdown/mlib2/parser_heading.v new file mode 100644 index 00000000..e3c0c35c --- /dev/null +++ b/examples/webtools/vmarkdown/mlib2/parser_heading.v @@ -0,0 +1,64 @@ +module mlib2 + +// Parse a heading element +fn (mut p Parser) parse_heading() ?&MarkdownElement { + start_pos := p.pos + start_line := p.line + start_column := p.column + + // Count the number of # characters + mut level := 0 + for p.pos < p.text.len && p.text[p.pos] == `#` && level < 6 { + level++ + p.pos++ + p.column++ + } + + // Must be followed by a space + if p.pos >= p.text.len || (p.text[p.pos] != ` ` && p.text[p.pos] != `\t`) { + p.pos = start_pos + p.line = start_line + p.column = start_column + return p.parse_paragraph() + } + + // Skip whitespace after # + p.skip_whitespace() + + // Read the heading text until end of line + mut content := '' + for p.pos < p.text.len && p.text[p.pos] != `\n` { + content += p.text[p.pos].ascii_str() + p.pos++ + p.column++ + } + + // Skip the newline + if p.pos < p.text.len && p.text[p.pos] == `\n` { + p.pos++ + p.line++ + p.column = 1 + } + + // Trim trailing whitespace and optional closing #s + content = content.trim_right() + for content.ends_with('#') { + content = content.trim_right('#').trim_right() + } + + // Create the heading element + mut heading := &MarkdownElement{ + typ: .heading + content: content + line_number: start_line + column: start_column + attributes: { + 'level': level.str() + } + } + + // Parse inline elements within the heading + heading.children = p.parse_inline(content) + + return heading +} diff --git a/examples/webtools/vmarkdown/mlib2/parser_helpers.v b/examples/webtools/vmarkdown/mlib2/parser_helpers.v new file mode 100644 index 00000000..b83f5e43 --- /dev/null +++ b/examples/webtools/vmarkdown/mlib2/parser_helpers.v @@ -0,0 +1,119 @@ +module mlib2 + +// Helper function to peek ahead in the text +fn (p Parser) peek(offset int) byte { + if p.pos + offset >= p.text.len { + return 0 + } + return p.text[p.pos + offset] +} + +// Skip whitespace characters +fn (mut p Parser) skip_whitespace() { + for p.pos < p.text.len && (p.text[p.pos] == ` ` || p.text[p.pos] == `\t`) { + p.pos++ + p.column++ + } +} + +// Check if current position is the start of a list +fn (p Parser) is_list_start() bool { + if p.pos >= p.text.len { + return false + } + + // Unordered list: *, -, + + if (p.text[p.pos] == `*` || p.text[p.pos] == `-` || p.text[p.pos] == `+`) && + (p.peek(1) == ` ` || p.peek(1) == `\t`) { + return true + } + + // Ordered list: 1., 2., etc. + if p.pos + 2 < p.text.len && p.text[p.pos].is_digit() { + mut i := p.pos + 1 + for i < p.text.len && p.text[i].is_digit() { + i++ + } + if i < p.text.len && p.text[i] == `.` && i + 1 < p.text.len && (p.text[i + 1] == ` ` || p.text[i + 1] == `\t`) { + return true + } + } + + // Task list: - [ ], - [x], etc. + if p.pos + 4 < p.text.len && + (p.text[p.pos] == `-` || p.text[p.pos] == `*` || p.text[p.pos] == `+`) && + p.text[p.pos + 1] == ` ` && p.text[p.pos + 2] == `[` && + (p.text[p.pos + 3] == ` ` || p.text[p.pos + 3] == `x` || p.text[p.pos + 3] == `X`) && + p.text[p.pos + 4] == `]` { + return true + } + + return false +} + +// Check if current position is the start of a table +fn (p Parser) is_table_start() bool { + if p.pos >= p.text.len || p.text[p.pos] != `|` { + return false + } + + // Look for a pipe character at the beginning of the line + // and check if there's at least one more pipe in the line + mut has_second_pipe := false + mut i := p.pos + 1 + for i < p.text.len && p.text[i] != `\n` { + if p.text[i] == `|` { + has_second_pipe = true + break + } + i++ + } + + if !has_second_pipe { + return false + } + + // Check if the next line has a header separator (---|---|...) + mut next_line_start := i + 1 + if next_line_start >= p.text.len { + return false + } + + // Skip whitespace at the beginning of the next line + for next_line_start < p.text.len && (p.text[next_line_start] == ` ` || p.text[next_line_start] == `\t`) { + next_line_start++ + } + + if next_line_start >= p.text.len || p.text[next_line_start] != `|` { + return false + } + + // Check for pattern like |---|---|... + mut has_separator := false + mut j := next_line_start + 1 + for j < p.text.len && p.text[j] != `\n` { + if p.text[j] == `-` { + has_separator = true + } else if p.text[j] == `|` { + // Reset for next column + has_separator = false + } else if p.text[j] != `:` && p.text[j] != ` ` && p.text[j] != `\t` { + // Only allow :, space, or tab besides - and | + return false + } + j++ + } + + return true +} + +// Check if current position is a footnote definition +fn (p Parser) is_footnote_definition() bool { + if p.pos + 3 >= p.text.len { + return false + } + + // Check for pattern like [^id]: + return p.text[p.pos] == `[` && p.text[p.pos + 1] == `^` && + p.text[p.pos + 2] != `]` && p.text.index_after(']:', p.pos + 2) > p.pos + 2 +} diff --git a/examples/webtools/vmarkdown/mlib2/parser_horizontal_rule.v b/examples/webtools/vmarkdown/mlib2/parser_horizontal_rule.v new file mode 100644 index 00000000..a22c37af --- /dev/null +++ b/examples/webtools/vmarkdown/mlib2/parser_horizontal_rule.v @@ -0,0 +1,58 @@ +module mlib2 + +// Parse a horizontal rule element +fn (mut p Parser) parse_horizontal_rule() ?&MarkdownElement { + start_pos := p.pos + start_line := p.line + start_column := p.column + + // Check for at least 3 of the same character (-, *, _) + char := p.text[p.pos] + if char != `-` && char != `*` && char != `_` { + p.pos = start_pos + p.line = start_line + p.column = start_column + return p.parse_paragraph() + } + + mut count := 0 + for p.pos < p.text.len && p.text[p.pos] == char { + count++ + p.pos++ + p.column++ + } + + // Must have at least 3 characters + if count < 3 { + p.pos = start_pos + p.line = start_line + p.column = start_column + return p.parse_paragraph() + } + + // Skip whitespace + p.skip_whitespace() + + // Must be at end of line + if p.pos < p.text.len && p.text[p.pos] != `\n` { + p.pos = start_pos + p.line = start_line + p.column = start_column + return p.parse_paragraph() + } + + // Skip the newline + if p.pos < p.text.len && p.text[p.pos] == `\n` { + p.pos++ + p.line++ + p.column = 1 + } + + // Create the horizontal rule element + return &MarkdownElement{ + typ: .horizontal_rule + content: '' + line_number: start_line + column: start_column + } +} diff --git a/examples/webtools/vmarkdown/mlib2/parser_inline.v b/examples/webtools/vmarkdown/mlib2/parser_inline.v new file mode 100644 index 00000000..120bea27 --- /dev/null +++ b/examples/webtools/vmarkdown/mlib2/parser_inline.v @@ -0,0 +1,22 @@ +module mlib2 + +// Parse inline elements within a block +fn (mut p Parser) parse_inline(text string) []&MarkdownElement { + mut elements := []&MarkdownElement{} + + // Simple implementation for now - just create a text element + if text.trim_space() != '' { + elements << &MarkdownElement{ + typ: .text + content: text + line_number: 0 + column: 0 + } + } + + // TODO: Implement parsing of inline elements like bold, italic, links, etc. + // This would involve scanning the text for markers like *, _, **, __, [, !, etc. + // and creating appropriate elements for each. + + return elements +} diff --git a/examples/webtools/vmarkdown/mlib2/parser_list.v b/examples/webtools/vmarkdown/mlib2/parser_list.v new file mode 100644 index 00000000..219dd79b --- /dev/null +++ b/examples/webtools/vmarkdown/mlib2/parser_list.v @@ -0,0 +1,96 @@ +module mlib2 + +// Parse a list element +fn (mut p Parser) parse_list() ?&MarkdownElement { + start_pos := p.pos + start_line := p.line + start_column := p.column + + // Determine list type (ordered or unordered) + mut is_ordered := false + mut start_number := 1 + mut marker := '' + + if p.text[p.pos].is_digit() { + // Ordered list + is_ordered = true + + // Parse start number + mut num_str := '' + for p.pos < p.text.len && p.text[p.pos].is_digit() { + num_str += p.text[p.pos].ascii_str() + p.pos++ + p.column++ + } + + start_number = num_str.int() + + // Must be followed by a period + if p.pos >= p.text.len || p.text[p.pos] != `.` { + p.pos = start_pos + p.line = start_line + p.column = start_column + return p.parse_paragraph() + } + + marker = '.' + p.pos++ + p.column++ + } else { + // Unordered list + marker = p.text[p.pos].ascii_str() + p.pos++ + p.column++ + } + + // Must be followed by whitespace + if p.pos >= p.text.len || (p.text[p.pos] != ` ` && p.text[p.pos] != `\t`) { + p.pos = start_pos + p.line = start_line + p.column = start_column + return p.parse_paragraph() + } + + // Create the list element + mut list := &MarkdownElement{ + typ: .list + content: '' + line_number: start_line + column: start_column + attributes: { + 'ordered': is_ordered.str() + 'start': start_number.str() + 'marker': marker + } + } + + // Parse list items + for { + // Parse list item + if item := p.parse_list_item(is_ordered, marker) { + list.children << item + } else { + break + } + + // Check if we're at the end of the list + p.skip_whitespace() + + if p.pos >= p.text.len { + break + } + + // Check for next list item + if is_ordered { + if !p.text[p.pos].is_digit() { + break + } + } else { + if p.text[p.pos] != marker[0] { + break + } + } + } + + return list +} diff --git a/examples/webtools/vmarkdown/mlib2/parser_list_item.v b/examples/webtools/vmarkdown/mlib2/parser_list_item.v new file mode 100644 index 00000000..adc39518 --- /dev/null +++ b/examples/webtools/vmarkdown/mlib2/parser_list_item.v @@ -0,0 +1,117 @@ +module mlib2 + +// Parse a list item +fn (mut p Parser) parse_list_item(is_ordered bool, marker string) ?&MarkdownElement { + start_pos := p.pos + start_line := p.line + start_column := p.column + + // Skip whitespace + p.skip_whitespace() + + // Check for task list item + mut is_task := false + mut is_completed := false + + if p.pos + 3 < p.text.len && p.text[p.pos] == `[` && + (p.text[p.pos + 1] == ` ` || p.text[p.pos + 1] == `x` || p.text[p.pos + 1] == `X`) && + p.text[p.pos + 2] == `]` && (p.text[p.pos + 3] == ` ` || p.text[p.pos + 3] == `\t`) { + is_task = true + is_completed = p.text[p.pos + 1] == `x` || p.text[p.pos + 1] == `X` + p.pos += 3 + p.column += 3 + p.skip_whitespace() + } + + // Read item content until end of line or next list item + mut content := '' + mut lines := []string{} + + // Read the first line + for p.pos < p.text.len && p.text[p.pos] != `\n` { + content += p.text[p.pos].ascii_str() + p.pos++ + p.column++ + } + lines << content + + // Skip the newline + if p.pos < p.text.len && p.text[p.pos] == `\n` { + p.pos++ + p.line++ + p.column = 1 + } + + // Read additional lines of the list item + for p.pos < p.text.len { + // Check if the line is indented (part of the current item) + if p.text[p.pos] == ` ` || p.text[p.pos] == `\t` { + // Count indentation + mut indent := 0 + for p.pos < p.text.len && (p.text[p.pos] == ` ` || p.text[p.pos] == `\t`) { + indent++ + p.pos++ + p.column++ + } + + // If indented enough, it's part of the current item + if indent >= 2 { + mut line := '' + for p.pos < p.text.len && p.text[p.pos] != `\n` { + line += p.text[p.pos].ascii_str() + p.pos++ + p.column++ + } + lines << line + + // Skip the newline + if p.pos < p.text.len && p.text[p.pos] == `\n` { + p.pos++ + p.line++ + p.column = 1 + } + } else { + // Not indented enough, end of list item + break + } + } else if p.text[p.pos] == `\n` { + // Empty line - could be a continuation or the end of the list item + p.pos++ + p.line++ + p.column = 1 + + // Check if the next line is indented + if p.pos < p.text.len && (p.text[p.pos] == ` ` || p.text[p.pos] == `\t`) { + lines << '' + } else { + break + } + } else { + // Not an indented line, end of list item + break + } + } + + // Join the lines with newlines + content = lines.join('\n') + + // Create the list item element + mut item := &MarkdownElement{ + typ: if is_task { .task_list_item } else { .list_item } + content: content + line_number: start_line + column: start_column + attributes: if is_task { + { + 'completed': is_completed.str() + } + } else { + map[string]string{} + } + } + + // Parse inline elements within the list item + item.children = p.parse_inline(content) + + return item +} diff --git a/examples/webtools/vmarkdown/mlib2/parser_main.v b/examples/webtools/vmarkdown/mlib2/parser_main.v new file mode 100644 index 00000000..13f82899 --- /dev/null +++ b/examples/webtools/vmarkdown/mlib2/parser_main.v @@ -0,0 +1,49 @@ +module mlib2 + +// Parser is responsible for parsing markdown text +struct Parser { +mut: + text string + pos int + line int + column int + doc MarkdownDocument +} + +// Main parsing function +fn (mut p Parser) parse() MarkdownDocument { + p.doc = new_document() + + // Parse blocks until end of input + for p.pos < p.text.len { + element := p.parse_block() or { break } + p.doc.root.children << element + } + + // Process footnotes + p.process_footnotes() + + return p.doc +} + +// Process footnotes and add them to the document +fn (mut p Parser) process_footnotes() { + // Nothing to do if no footnotes + if p.doc.footnotes.len == 0 { + return + } + + // Add a horizontal rule before footnotes + hr := &MarkdownElement{ + typ: .horizontal_rule + content: '' + line_number: p.line + column: p.column + } + p.doc.root.children << hr + + // Add footnotes section + for key, footnote in p.doc.footnotes { + p.doc.root.children << footnote + } +} diff --git a/examples/webtools/vmarkdown/mlib2/parser_paragraph.v b/examples/webtools/vmarkdown/mlib2/parser_paragraph.v new file mode 100644 index 00000000..79957547 --- /dev/null +++ b/examples/webtools/vmarkdown/mlib2/parser_paragraph.v @@ -0,0 +1,77 @@ +module mlib2 + +// Parse a paragraph element +fn (mut p Parser) parse_paragraph() ?&MarkdownElement { + start_pos := p.pos + start_line := p.line + start_column := p.column + + mut content := '' + mut lines := []string{} + + // Read the first line + for p.pos < p.text.len && p.text[p.pos] != `\n` { + content += p.text[p.pos].ascii_str() + p.pos++ + p.column++ + } + lines << content + + // Skip the newline + if p.pos < p.text.len && p.text[p.pos] == `\n` { + p.pos++ + p.line++ + p.column = 1 + } + + // Read additional lines of the paragraph + for p.pos < p.text.len { + // Check if the line is empty (end of paragraph) + if p.text[p.pos] == `\n` { + p.pos++ + p.line++ + p.column = 1 + break + } + + // Check if the line starts with a block element + if p.text[p.pos] == `#` || p.text[p.pos] == `>` || + (p.text[p.pos] == `-` && p.peek(1) == `-` && p.peek(2) == `-`) || + (p.text[p.pos] == `\`` && p.peek(1) == `\`` && p.peek(2) == `\``) || + p.is_list_start() || p.is_table_start() || p.is_footnote_definition() { + break + } + + // Read the line + mut line := '' + for p.pos < p.text.len && p.text[p.pos] != `\n` { + line += p.text[p.pos].ascii_str() + p.pos++ + p.column++ + } + lines << line + + // Skip the newline + if p.pos < p.text.len && p.text[p.pos] == `\n` { + p.pos++ + p.line++ + p.column = 1 + } + } + + // Join the lines with spaces + content = lines.join(' ') + + // Create the paragraph element + mut paragraph := &MarkdownElement{ + typ: .paragraph + content: content + line_number: start_line + column: start_column + } + + // Parse inline elements within the paragraph + paragraph.children = p.parse_inline(content) + + return paragraph +} diff --git a/examples/webtools/vmarkdown/mlib2/parser_table.v b/examples/webtools/vmarkdown/mlib2/parser_table.v new file mode 100644 index 00000000..89fdaba3 --- /dev/null +++ b/examples/webtools/vmarkdown/mlib2/parser_table.v @@ -0,0 +1,225 @@ +module mlib2 + +// Parse a table element +fn (mut p Parser) parse_table() ?&MarkdownElement { + start_pos := p.pos + start_line := p.line + start_column := p.column + + // Create the table element + mut table := &MarkdownElement{ + typ: .table + content: '' + line_number: start_line + column: start_column + } + + // Parse header row + mut header_row := &MarkdownElement{ + typ: .table_row + content: '' + line_number: p.line + column: p.column + attributes: { + 'is_header': 'true' + } + } + + // Skip initial pipe if present + if p.text[p.pos] == `|` { + p.pos++ + p.column++ + } + + // Parse header cells + for p.pos < p.text.len && p.text[p.pos] != `\n` { + // Parse cell content + mut cell_content := '' + for p.pos < p.text.len && p.text[p.pos] != `|` && p.text[p.pos] != `\n` { + cell_content += p.text[p.pos].ascii_str() + p.pos++ + p.column++ + } + + // Create cell element + cell := &MarkdownElement{ + typ: .table_cell + content: cell_content.trim_space() + line_number: p.line + column: p.column - cell_content.len + attributes: { + 'is_header': 'true' + } + } + + // Add cell to row + header_row.children << cell + + // Skip pipe + if p.pos < p.text.len && p.text[p.pos] == `|` { + p.pos++ + p.column++ + } else { + break + } + } + + // Skip newline + if p.pos < p.text.len && p.text[p.pos] == `\n` { + p.pos++ + p.line++ + p.column = 1 + } + + // Add header row to table + table.children << header_row + + // Parse separator row (---|---|...) + // Skip initial pipe if present + if p.pos < p.text.len && p.text[p.pos] == `|` { + p.pos++ + p.column++ + } + + // Parse alignment information + mut alignments := []string{} + + for p.pos < p.text.len && p.text[p.pos] != `\n` { + // Skip whitespace + for p.pos < p.text.len && (p.text[p.pos] == ` ` || p.text[p.pos] == `\t`) { + p.pos++ + p.column++ + } + + // Check alignment + mut left_colon := false + mut right_colon := false + + if p.pos < p.text.len && p.text[p.pos] == `:` { + left_colon = true + p.pos++ + p.column++ + } + + // Skip dashes + for p.pos < p.text.len && p.text[p.pos] == `-` { + p.pos++ + p.column++ + } + + if p.pos < p.text.len && p.text[p.pos] == `:` { + right_colon = true + p.pos++ + p.column++ + } + + // Determine alignment + mut alignment := 'left' // default + if left_colon && right_colon { + alignment = 'center' + } else if right_colon { + alignment = 'right' + } + + alignments << alignment + + // Skip whitespace + for p.pos < p.text.len && (p.text[p.pos] == ` ` || p.text[p.pos] == `\t`) { + p.pos++ + p.column++ + } + + // Skip pipe + if p.pos < p.text.len && p.text[p.pos] == `|` { + p.pos++ + p.column++ + } else { + break + } + } + + // Skip newline + if p.pos < p.text.len && p.text[p.pos] == `\n` { + p.pos++ + p.line++ + p.column = 1 + } + + // Set alignment for header cells + for i, cell in header_row.children { + if i < alignments.len { + cell.attributes['align'] = alignments[i] + } + } + + // Parse data rows + for p.pos < p.text.len && p.text[p.pos] != `\n` { + // Create row element + mut row := &MarkdownElement{ + typ: .table_row + content: '' + line_number: p.line + column: p.column + } + + // Skip initial pipe if present + if p.text[p.pos] == `|` { + p.pos++ + p.column++ + } + + // Parse cells + mut cell_index := 0 + for p.pos < p.text.len && p.text[p.pos] != `\n` { + // Parse cell content + mut cell_content := '' + for p.pos < p.text.len && p.text[p.pos] != `|` && p.text[p.pos] != `\n` { + cell_content += p.text[p.pos].ascii_str() + p.pos++ + p.column++ + } + + // Create cell element + mut cell := &MarkdownElement{ + typ: .table_cell + content: cell_content.trim_space() + line_number: p.line + column: p.column - cell_content.len + } + + // Set alignment + if cell_index < alignments.len { + cell.attributes['align'] = alignments[cell_index] + } + + // Add cell to row + row.children << cell + cell_index++ + + // Skip pipe + if p.pos < p.text.len && p.text[p.pos] == `|` { + p.pos++ + p.column++ + } else { + break + } + } + + // Add row to table + table.children << row + + // Skip newline + if p.pos < p.text.len && p.text[p.pos] == `\n` { + p.pos++ + p.line++ + p.column = 1 + } + + // Check if we're at the end of the table + if p.pos >= p.text.len || p.text[p.pos] != `|` { + break + } + } + + return table +} diff --git a/examples/webtools/vmarkdown/mlib2/renderer.v b/examples/webtools/vmarkdown/mlib2/renderer.v new file mode 100644 index 00000000..eb88243f --- /dev/null +++ b/examples/webtools/vmarkdown/mlib2/renderer.v @@ -0,0 +1,169 @@ +module mlib2 + +// Renderer is the interface for all renderers +pub interface Renderer { + render(doc MarkdownDocument) string +} + +// StructureRenderer renders a markdown document as a structure +pub struct StructureRenderer { + indent string = ' ' +} + +// Creates a new structure renderer +pub fn new_structure_renderer() StructureRenderer { + return StructureRenderer{} +} + +// Render a markdown document as a structure +pub fn (r StructureRenderer) render(doc MarkdownDocument) string { + return r.render_element(doc.root, 0) +} + +// Render an element as a structure +fn (r StructureRenderer) render_element(element &MarkdownElement, level int) string { + mut result := r.indent.repeat(level) + '${element.typ}' + + if element.content.len > 0 { + // Truncate long content + mut content := element.content + if content.len > 50 { + content = content[0..47] + '...' + } + // Escape newlines + content = content.replace('\n', '\\n') + result += ': "${content}"' + } + + if element.attributes.len > 0 { + result += ' {' + mut first := true + for key, value in element.attributes { + if !first { + result += ', ' + } + result += '${key}: "${value}"' + first = false + } + result += '}' + } + + result += '\n' + + for child in element.children { + result += r.render_element(child, level + 1) + } + + return result +} + +// PlainTextRenderer renders a markdown document as plain text +pub struct PlainTextRenderer {} + +// Creates a new plain text renderer +pub fn new_plain_text_renderer() PlainTextRenderer { + return PlainTextRenderer{} +} + +// Render a markdown document as plain text +pub fn (r PlainTextRenderer) render(doc MarkdownDocument) string { + return r.render_element(doc.root) +} + +// Render an element as plain text +fn (r PlainTextRenderer) render_element(element &MarkdownElement) string { + mut result := '' + + match element.typ { + .document { + for child in element.children { + result += r.render_element(child) + if child.typ != .horizontal_rule { + result += '\n\n' + } + } + // Trim trailing newlines + result = result.trim_right('\n') + } + .heading { + level := element.attributes['level'].int() + result += '#'.repeat(level) + ' ' + element.content + } + .paragraph { + result += element.content + } + .blockquote { + lines := element.content.split('\n') + for line in lines { + result += '> ' + line + '\n' + } + result = result.trim_right('\n') + } + .code_block { + language := element.attributes['language'] + result += '```${language}\n' + result += element.content + result += '```' + } + .list { + is_ordered := element.attributes['ordered'] == 'true' + start_number := element.attributes['start'].int() + + mut i := start_number + for child in element.children { + if is_ordered { + result += '${i}. ' + i++ + } else { + result += '- ' + } + result += r.render_element(child) + '\n' + } + result = result.trim_right('\n') + } + .list_item, .task_list_item { + if element.typ == .task_list_item { + is_completed := element.attributes['completed'] == 'true' + if is_completed { + result += '[x] ' + } else { + result += '[ ] ' + } + } + result += element.content + } + .table { + // TODO: Implement table rendering + result += '[Table with ${element.children.len} rows]' + } + .horizontal_rule { + result += '---' + } + .footnote { + identifier := element.attributes['identifier'] + result += '[^${identifier}]: ${element.content}' + } + .text { + result += element.content + } + else { + result += element.content + } + } + + return result +} + +// Convenience function to render markdown text as a structure +pub fn to_structure(text string) string { + doc := parse(text) + mut renderer := new_structure_renderer() + return renderer.render(doc) +} + +// Convenience function to render markdown text as plain text +pub fn to_plain(text string) string { + doc := parse(text) + mut renderer := new_plain_text_renderer() + return renderer.render(doc) +} diff --git a/lib/data/markdownparser/elements/base.v b/lib/data/markdownparser/elements/base.v index fd0e5495..39874029 100644 --- a/lib/data/markdownparser/elements/base.v +++ b/lib/data/markdownparser/elements/base.v @@ -37,7 +37,7 @@ fn (mut self DocBase) parent_doc() &Doc { fn (mut self DocBase) remove_empty_children() { self.children = self.children.filter(!(it.content == '' && it.children.len == 0 - && it.type_name in ['text', 'empty'])) + && it.type_name in ['paragraph','text', 'empty'])) } pub fn (mut self DocBase) process() !int { diff --git a/lib/data/markdownparser/elements/parser_paragraph.v b/lib/data/markdownparser/elements/parser_paragraph.v index 66d91591..4bd0c690 100644 --- a/lib/data/markdownparser/elements/parser_paragraph.v +++ b/lib/data/markdownparser/elements/parser_paragraph.v @@ -1,7 +1,7 @@ module elements import freeflowuniverse.herolib.core.texttools -// import freeflowuniverse.herolib.ui.console +import freeflowuniverse.herolib.ui.console // DO NOT CHANGE THE WAY HOW THIS WORKS, THIS HAS BEEN DONE AS A STATEFUL PARSER BY DESIGN // THIS ALLOWS FOR EASY ADOPTIONS TO DIFFERENT REALITIES @@ -19,7 +19,7 @@ fn (mut paragraph Paragraph) paragraph_parse() ! { mut llast := paragraph.children.last() mut char_ := parser.char_current() - // console.print_debug("[[[${char_}]]]") + //console.print_debug("[[[${char_}]]]") // char == '' means end of file if mut llast is Def { diff --git a/lib/web/docusaurus/dsite.v b/lib/web/docusaurus/dsite.v index 774c365a..f568407b 100644 --- a/lib/web/docusaurus/dsite.v +++ b/lib/web/docusaurus/dsite.v @@ -186,7 +186,7 @@ fn (mut site DocSite) process_md(mut path pathlib.Path, args MyImport)!{ mypatho_img.copy(dest:dest,rsync:false)! } - mut pathlist:=path.list()! + mut pathlist:=path.list(regex: [r'.*\.md$'],recursive:true)! for mut mypatho2 in pathlist.paths{ site.process_md(mut mypatho2,args)! } @@ -195,14 +195,16 @@ fn (mut site DocSite) process_md(mut path pathlib.Path, args MyImport)!{ mydest:='${site.path_build.path}/docs/${args.dest}/${texttools.name_fix(path.name())}' mut mydesto:=pathlib.get_file(path:mydest,create:true)! + println(path.path) mut mymd:=markdownparser.new(path:path.path)! - mut myfm:=mymd.frontmatter2()! - if ! args.visible{ - myfm.args["draft"]= 'true' - } + println(2) + // mut myfm:=mymd.frontmatter2()! + // if ! args.visible{ + // myfm.args["draft"]= 'true' + // } //println(myfm) //println(mymd.markdown()!) - mydesto.write(mymd.markdown()!)! + // mydesto.write(mymd.markdown()!)! //exit(0) }