///|
pub fn MarkdownEditorSnapshot::parse(source : String) -> MarkdownEditorSnapshot {
MarkdownEditorSnapshot::parse_with_font(source, @core.FontSpec::new())
}
///|
pub fn MarkdownEditorSnapshot::parse_with_font(
source : String,
base : @core.FontSpec,
base_dir? : String? = None,
) -> MarkdownEditorSnapshot {
let blocks = parse_markdown_editor_blocks(source)
let word_count = markdown_editor_word_count(source)
let metadata = markdown_editor_front_matter_metadata(blocks)
{
source,
blocks,
rich_text: markdown_editor_blocks_to_rich_text(
source,
blocks,
base,
base_dir~,
),
line_count: markdown_editor_line_count(source),
word_count,
reading_minutes: markdown_editor_reading_minutes(word_count),
active_heading: markdown_editor_active_heading(blocks),
metadata_title: metadata.title,
metadata_tags: metadata.tags,
outline: markdown_editor_outline(blocks),
}
}
///|
pub fn markdown_editor_format(
source : String,
base? : @core.FontSpec = @core.FontSpec::new(),
base_dir? : String? = None,
) -> RichTextDocument {
MarkdownEditorSnapshot::parse_with_font(source, base, base_dir~).rich_text
}
///|
pub fn markdown_editor_format_for_selection(
source : String,
selection : MarkdownEditorSelection,
base? : @core.FontSpec = @core.FontSpec::new(),
base_dir? : String? = None,
) -> RichTextDocument {
let document = markdown_editor_format(source, base~, base_dir~)
let blocks : Array[RichTextBlock] = []
for block in document.blocks {
let runs : Array[RichTextRun] = []
for run in block.runs {
runs.push(
markdown_editor_reveal_active_inline_run(source, selection, run),
)
}
blocks.push(
markdown_editor_reveal_active_block_marker(source, selection, {
..block,
runs,
}),
)
}
RichTextDocument::new(blocks~)
}
///|
pub fn markdown_editor_format_for_writing_mode(
source : String,
selection : MarkdownEditorSelection,
base? : @core.FontSpec = @core.FontSpec::new(),
focus_mode~ : Bool,
base_dir? : String? = None,
) -> RichTextDocument {
let document = markdown_editor_format_for_selection(
source,
selection,
base~,
base_dir~,
)
if focus_mode {
markdown_editor_dim_inactive_blocks(document, selection)
} else {
document
}
}
///|
fn markdown_editor_format_source_mode(
source : String,
base : @core.FontSpec,
) -> RichTextDocument {
let length = markdown_editor_text_length(source)
let font = {
..base,
families: @core.FontFamilyStack::monospace(),
size: base.size * 0.92,
}
RichTextDocument::new(blocks=[
RichTextBlock::new(
runs=[
RichTextRun::new(
text=source,
font=Some(font),
source_range=Some({ start: 0, end: length }),
),
],
font=Some(font),
source_range=Some({ start: 0, end: length }),
content_range=Some({ start: 0, end: length }),
line_height=Some(base.size * 1.42),
),
])
}
///|
fn markdown_editor_dim_inactive_blocks(
document : RichTextDocument,
selection : MarkdownEditorSelection,
) -> RichTextDocument {
let blocks : Array[RichTextBlock] = []
for block in document.blocks {
if markdown_editor_focus_block_active(block, selection) {
blocks.push(block)
} else {
blocks.push(markdown_editor_dim_rich_text_block(block))
}
}
RichTextDocument::new(blocks~)
}
///|
fn markdown_editor_focus_block_active(
block : RichTextBlock,
selection : MarkdownEditorSelection,
) -> Bool {
match block.source_range {
Some(range) => markdown_editor_selection_touches_range(selection, range)
None => true
}
}
///|
fn markdown_editor_dim_rich_text_block(block : RichTextBlock) -> RichTextBlock {
let runs : Array[RichTextRun] = []
for run in block.runs {
runs.push(markdown_editor_dim_rich_text_run(run))
}
{
..block,
runs,
color: Some(
block.color
.unwrap_or(markdown_editor_default_text_color())
.multiply_alpha(0.38),
),
border_left: markdown_editor_dim_border(block.border_left),
}
}
///|
fn markdown_editor_dim_rich_text_run(run : RichTextRun) -> RichTextRun {
{
..run,
color: match run.color {
Some(color) => Some(color.multiply_alpha(0.38))
None => None
},
}
}
///|
fn markdown_editor_dim_border(
border : @core.BorderStyle?,
) -> @core.BorderStyle? {
match border {
Some(border) => Some({ ..border, brush: border.brush.multiply_alpha(0.38) })
None => None
}
}
///|
fn markdown_editor_default_text_color() -> @core.Color {
@core.Color::rgba(r=0.12, g=0.12, b=0.12)
}
///|
fn markdown_editor_reveal_active_block_marker(
source : String,
selection : MarkdownEditorSelection,
block : RichTextBlock,
) -> RichTextBlock {
match (block.source_range, block.content_range) {
(Some(source_range), Some(content_range)) => {
if markdown_editor_active_code_block(source, selection, block) {
return markdown_editor_reveal_active_code_block(source, block)
}
if source_range.start >= content_range.start ||
block.runs.length() == 0 ||
block.runs[0].source_range is None ||
!markdown_editor_selection_touches_range(selection, content_range) {
return block
}
let prefix = markdown_editor_substring(
source,
source_range.start,
content_range.start,
)
if block.prefix != "" {
return { ..block, prefix, }
}
let runs = block.runs.copy()
let first = runs[0]
runs[0] = {
..first,
text: prefix + first.text,
visual_text: None,
source_range: match first.source_range {
Some(run_range) =>
Some({ start: source_range.start, end: run_range.end })
None => Some({ start: source_range.start, end: content_range.start })
},
}
{ ..block, prefix: "", runs }
}
_ => block
}
}
///|
fn markdown_editor_active_code_block(
source : String,
selection : MarkdownEditorSelection,
block : RichTextBlock,
) -> Bool {
match (block.source_range, block.content_range) {
(Some(source_range), Some(_content_range)) => {
let block_source = markdown_editor_substring(
source,
source_range.start,
source_range.end,
)
markdown_editor_source_starts_with_code_fence(block_source) &&
markdown_editor_selection_touches_range(selection, source_range)
}
_ => false
}
}
///|
fn markdown_editor_reveal_active_code_block(
source : String,
block : RichTextBlock,
) -> RichTextBlock {
match block.source_range {
Some(source_range) => {
let source_text = markdown_editor_substring(
source,
source_range.start,
source_range.end,
)
let font = markdown_editor_first_run_font(block)
{
..block,
runs: [
RichTextRun::new(
text=source_text,
font~,
source_range=Some(source_range),
),
],
content_range: Some(source_range),
}
}
None => block
}
}
///|
fn markdown_editor_first_run_font(block : RichTextBlock) -> @core.FontSpec? {
if block.runs.length() == 0 {
None
} else {
block.runs[0].font
}
}
///|
fn markdown_editor_source_starts_with_code_fence(source : String) -> Bool {
let chars = source.trim().to_owned().to_array()
if chars.length() < 3 {
return false
}
(chars[0] == '`' && chars[1] == '`' && chars[2] == '`') ||
(chars[0] == '~' && chars[1] == '~' && chars[2] == '~')
}
///|
fn markdown_editor_reveal_active_inline_run(
source : String,
selection : MarkdownEditorSelection,
run : RichTextRun,
) -> RichTextRun {
match run.source_range {
Some(range) =>
if markdown_editor_selection_touches_range(selection, range) {
match markdown_editor_inline_marker_source_range(source, range) {
Some(full_range) =>
{
..run,
text: markdown_editor_substring(
source,
full_range.start,
full_range.end,
),
visual_text: None,
source_range: Some(full_range),
}
None => run
}
} else {
run
}
None => run
}
}
///|
fn markdown_editor_selection_touches_range(
selection : MarkdownEditorSelection,
range : RichTextSourceRange,
) -> Bool {
match markdown_editor_active_selection(selection.selection) {
Some(active) => {
let active = active.normalized()
active.start < range.end && active.end > range.start
}
None => selection.caret >= range.start && selection.caret <= range.end
}
}
///|
fn markdown_editor_inline_marker_source_range(
source : String,
range : RichTextSourceRange,
) -> RichTextSourceRange? {
let chars = source.to_array()
if range.start < 0 || range.end < range.start || range.end > chars.length() {
return None
}
if markdown_editor_source_has_marker(chars, range.start - 2, "**") &&
markdown_editor_source_has_marker(chars, range.end, "**") {
return Some({ start: range.start - 2, end: range.end + 2 })
}
if markdown_editor_source_has_marker(chars, range.start - 2, "__") &&
markdown_editor_source_has_marker(chars, range.end, "__") {
return Some({ start: range.start - 2, end: range.end + 2 })
}
if markdown_editor_source_has_marker(chars, range.start - 2, "~~") &&
markdown_editor_source_has_marker(chars, range.end, "~~") {
return Some({ start: range.start - 2, end: range.end + 2 })
}
if markdown_editor_source_has_marker(chars, range.start - 1, "`") &&
markdown_editor_source_has_marker(chars, range.end, "`") {
return Some({ start: range.start - 1, end: range.end + 1 })
}
if markdown_editor_single_inline_marker_at(chars, range.start - 1, '*') &&
markdown_editor_single_inline_marker_at(chars, range.end, '*') {
return Some({ start: range.start - 1, end: range.end + 1 })
}
if markdown_editor_single_inline_marker_at(chars, range.start - 1, '_') &&
markdown_editor_single_inline_marker_at(chars, range.end, '_') {
return Some({ start: range.start - 1, end: range.end + 1 })
}
match markdown_editor_reference_source_range(source, range) {
Some(reference_range) => return Some(reference_range)
None => ()
}
None
}
///|
fn markdown_editor_reference_source_range(
source : String,
range : RichTextSourceRange,
) -> RichTextSourceRange? {
let text_range = @core.TextRange::new(start=range.start, end=range.end)
match markdown_editor_range_link_bounds(source, text_range) {
Some(bounds) =>
return Some({ start: bounds.open_start, end: bounds.target_end + 1 })
None => ()
}
match markdown_editor_range_image_bounds(source, text_range) {
Some(bounds) =>
Some({ start: bounds.open_start, end: bounds.target_end + 1 })
None => None
}
}
///|
fn markdown_editor_single_inline_marker_at(
chars : Array[Char],
index : Int,
marker : Char,
) -> Bool {
index >= 0 &&
index < chars.length() &&
chars[index] == marker &&
(index == 0 || chars[index - 1] != marker) &&
(index + 1 >= chars.length() || chars[index + 1] != marker)
}
///|
fn parse_markdown_editor_blocks(source : String) -> Array[MarkdownEditorBlock] {
let blocks : Array[MarkdownEditorBlock] = []
if source == "" {
return blocks
}
let definitions = markdown_reference_definitions(source)
let lines = markdown_editor_lines(source)
match markdown_editor_front_matter_block_at(lines) {
Some(front_matter) => {
blocks.push(front_matter.block)
let mut index = front_matter.next_index
while index < lines.length() {
let line = lines[index]
if line.text.trim().to_owned() == "" {
blocks.push(markdown_editor_blank_line_block(line))
index = index + 1
} else {
break
}
}
if index < lines.length() {
let rest_start = lines[index].start
let rest = markdown_editor_substring(
source,
rest_start,
markdown_editor_text_length(source),
)
for block in parse_markdown_editor_blocks(rest) {
blocks.push({
..block,
source_range: {
start: block.source_range.start + rest_start,
end: block.source_range.end + rest_start,
},
content_range: {
start: block.content_range.start + rest_start,
end: block.content_range.end + rest_start,
},
})
}
}
return blocks
}
None => ()
}
let code_lines : Array[String] = []
let mut code_start = 0
let mut code_content_start = 0
let mut code_fence = ""
let mut code_language : String? = None
let mut in_code = false
let mut index = 0
while index < lines.length() {
let line = lines[index]
if in_code {
if markdown_editor_closing_code_fence(line.text, code_fence) {
let text = markdown_join_lines(code_lines)
blocks.push({
block: markdown_editor_code_block(text, code_language),
source_range: { start: code_start, end: line.end },
content_range: { start: code_content_start, end: line.start },
})
code_lines.clear()
code_fence = ""
code_language = None
in_code = false
} else {
code_lines.push(line.text)
}
} else if line.text.trim().to_owned() == "" {
blocks.push(markdown_editor_blank_line_block(line))
} else {
match markdown_editor_opening_code_fence_marker(line.text) {
Some(fence) => {
in_code = true
code_fence = fence.marker
code_language = fence.info
code_start = line.start
code_content_start = line.end + 1
}
None =>
match markdown_editor_html_block_at(lines, index) {
Some(html) => {
blocks.push(html.block)
index = html.end_index
}
None =>
match markdown_editor_table_block_at(lines, index) {
Some(table) => {
blocks.push(table.block)
index = table.end_index
}
None =>
match
markdown_editor_setext_heading_block_at(
lines, index, definitions,
) {
Some(heading) => {
blocks.push(heading.block)
index = heading.end_index
}
None =>
match
markdown_editor_indented_code_block_at(lines, index) {
Some(code) => {
blocks.push(code.block)
index = code.end_index
}
None =>
match
markdown_editor_soft_wrapped_paragraph_block_at(
lines, index, definitions,
) {
Some(paragraph) => {
blocks.push(paragraph.block)
index = paragraph.end_index
}
None => {
let parsed = parse_markdown(line.text)
for block in parsed {
let block = markdown_editor_block_with_references(
block, definitions,
)
blocks.push({
block,
source_range: {
start: line.start,
end: line.end,
},
content_range: markdown_editor_content_range(
line,
block.kind,
),
})
}
}
}
}
}
}
}
}
}
index = index + 1
}
if in_code {
let text = markdown_join_lines(code_lines)
let end = markdown_editor_text_length(source)
blocks.push({
block: markdown_editor_code_block(text, code_language),
source_range: { start: code_start, end },
content_range: { start: code_content_start, end },
})
}
blocks
}
///|
fn markdown_editor_blank_line_block(line : MarkdownLine) -> MarkdownEditorBlock {
{
block: {
kind: MarkdownBlockKind::Paragraph,
text: "",
inlines: [],
table_alignments: [],
code_language: None,
},
source_range: { start: line.start, end: line.end },
content_range: { start: line.start, end: line.end },
}
}
///|
pub(all) struct MarkdownEditorFrontMatterParseResult {
block : MarkdownEditorBlock
next_index : Int
} derive(Eq, Debug)
///|
fn markdown_editor_front_matter_block_at(
lines : Array[MarkdownLine],
) -> MarkdownEditorFrontMatterParseResult? {
if lines.length() < 2 || lines[0].text.trim().to_owned() != "---" {
return None
}
let content : Array[String] = []
let mut index = 1
while index < lines.length() {
if lines[index].text.trim().to_owned() == "---" {
return Some({
block: {
block: markdown_block(FrontMatter, markdown_join_lines(content)),
source_range: { start: lines[0].start, end: lines[index].end },
content_range: { start: lines[0].end + 1, end: lines[index].start },
},
next_index: index + 1,
})
}
content.push(lines[index].text)
index = index + 1
}
None
}
///|
struct MarkdownEditorHtmlBlockParseResult {
block : MarkdownEditorBlock
end_index : Int
} derive(Eq, Debug)
///|
fn markdown_editor_html_block_at(
lines : Array[MarkdownLine],
index : Int,
) -> MarkdownEditorHtmlBlockParseResult? {
if index < 0 || index >= lines.length() {
return None
}
if markdown_html_block_comment_start(lines[index].text) {
return markdown_editor_html_comment_block_at(lines, index)
}
if markdown_html_block_processing_instruction_start(lines[index].text) {
return markdown_editor_html_processing_instruction_block_at(lines, index)
}
if markdown_html_block_cdata_start(lines[index].text) {
return markdown_editor_html_cdata_block_at(lines, index)
}
match markdown_html_block_start_tag(lines[index].text) {
Some(tag) => Some(markdown_editor_html_tag_block_at(lines, index, tag))
None => None
}
}
///|
fn markdown_editor_html_comment_block_at(
lines : Array[MarkdownLine],
index : Int,
) -> MarkdownEditorHtmlBlockParseResult? {
let html_lines : Array[String] = []
let mut next = index
while next < lines.length() {
html_lines.push(lines[next].text)
if markdown_html_block_comment_end(lines[next].text) {
return Some(
markdown_editor_html_block_result(lines, html_lines, index, next),
)
}
next = next + 1
}
Some(markdown_editor_html_block_result(lines, html_lines, index, next - 1))
}
///|
fn markdown_editor_html_processing_instruction_block_at(
lines : Array[MarkdownLine],
index : Int,
) -> MarkdownEditorHtmlBlockParseResult? {
let html_lines : Array[String] = []
let mut next = index
while next < lines.length() {
html_lines.push(lines[next].text)
if markdown_html_block_processing_instruction_end(lines[next].text) {
return Some(
markdown_editor_html_block_result(lines, html_lines, index, next),
)
}
next = next + 1
}
Some(markdown_editor_html_block_result(lines, html_lines, index, next - 1))
}
///|
fn markdown_editor_html_cdata_block_at(
lines : Array[MarkdownLine],
index : Int,
) -> MarkdownEditorHtmlBlockParseResult? {
let html_lines : Array[String] = []
let mut next = index
while next < lines.length() {
html_lines.push(lines[next].text)
if markdown_html_block_cdata_end(lines[next].text) {
return Some(
markdown_editor_html_block_result(lines, html_lines, index, next),
)
}
next = next + 1
}
Some(markdown_editor_html_block_result(lines, html_lines, index, next - 1))
}
///|
fn markdown_editor_html_tag_block_at(
lines : Array[MarkdownLine],
index : Int,
tag : String,
) -> MarkdownEditorHtmlBlockParseResult {
let html_lines : Array[String] = []
let mut next = index
while next < lines.length() {
if next > index && markdown_editor_blank_line(lines[next]) {
break
}
html_lines.push(lines[next].text)
if markdown_html_block_line_closes_tag(lines[next].text, tag) ||
markdown_html_block_line_self_closes(lines[next].text) {
return markdown_editor_html_block_result(lines, html_lines, index, next)
}
next = next + 1
}
markdown_editor_html_block_result(lines, html_lines, index, next - 1)
}
///|
fn markdown_editor_html_block_result(
lines : Array[MarkdownLine],
html_lines : Array[String],
start_index : Int,
end_index : Int,
) -> MarkdownEditorHtmlBlockParseResult {
let first = lines[start_index]
let last = lines[end_index]
{
block: {
block: markdown_block(HtmlBlock, markdown_join_lines(html_lines)),
source_range: { start: first.start, end: last.end },
content_range: { start: first.start, end: last.end },
},
end_index,
}
}
///|
struct MarkdownEditorSetextHeadingParseResult {
block : MarkdownEditorBlock
end_index : Int
} derive(Eq, Debug)
///|
fn markdown_editor_setext_heading_block_at(
lines : Array[MarkdownLine],
index : Int,
definitions : Array[MarkdownReferenceDefinition],
) -> MarkdownEditorSetextHeadingParseResult? {
let heading_lines : Array[MarkdownLine] = []
let text_lines : Array[String] = []
let mut next = index
while next < lines.length() {
if heading_lines.length() > 0 {
match markdown_editor_setext_underline_level(lines[next].text) {
Some(level) => {
for heading_line in heading_lines {
text_lines.push(heading_line.text.trim().to_owned())
}
let text = markdown_join_lines(text_lines)
let first = heading_lines[0]
let last_content = heading_lines[heading_lines.length() - 1]
return Some({
block: {
block: {
kind: MarkdownBlockKind::Heading(level),
text,
inlines: parse_markdown_inlines_with_definitions(
text, definitions,
),
table_alignments: [],
code_language: None,
},
source_range: { start: first.start, end: lines[next].end },
content_range: { start: first.start, end: last_content.end },
},
end_index: next,
})
}
None => ()
}
}
if markdown_editor_setext_heading_content_line(lines, next) {
heading_lines.push(lines[next])
next = next + 1
} else {
return None
}
}
None
}
///|
fn markdown_editor_setext_heading_content_line(
lines : Array[MarkdownLine],
index : Int,
) -> Bool {
if index < 0 || index >= lines.length() {
return false
}
let line = lines[index]
if line.text.trim().to_owned() == "" ||
markdown_editor_opening_code_fence_marker(line.text) is Some(_) {
return false
}
markdown_editor_is_setext_paragraph_candidate(line.text)
}
///|
fn markdown_editor_block_with_references(
block : MarkdownBlock,
definitions : Array[MarkdownReferenceDefinition],
) -> MarkdownBlock {
match block.kind {
CodeBlock | HorizontalRule | Table | HtmlBlock | FrontMatter => block
_ =>
{
..block,
inlines: markdown_resolve_reference_inlines(block.inlines, definitions),
}
}
}
///|
struct MarkdownEditorIndentedCodeParseResult {
block : MarkdownEditorBlock
end_index : Int
} derive(Eq, Debug)
///|
fn markdown_editor_indented_code_block_at(
lines : Array[MarkdownLine],
index : Int,
) -> MarkdownEditorIndentedCodeParseResult? {
if !markdown_editor_indented_code_line(lines, index) {
return None
}
let code_lines : Array[String] = []
let mut next = index
while next < lines.length() {
if markdown_editor_indented_code_line(lines, next) {
code_lines.push(markdown_indented_code_line_content(lines[next].text))
next = next + 1
} else if markdown_editor_blank_line(lines[next]) {
match markdown_editor_next_indented_code_line_index(lines, next + 1) {
Some(code_index) =>
while next < code_index {
code_lines.push("")
next = next + 1
}
None => break
}
} else {
break
}
}
let first = lines[index]
let last = lines[next - 1]
let content_start = first.start +
markdown_indented_code_content_offset(first.text)
Some({
block: {
block: markdown_editor_code_block(markdown_join_lines(code_lines), None),
source_range: { start: first.start, end: last.end },
content_range: {
start: markdown_editor_min_int(content_start, last.end),
end: last.end,
},
},
end_index: next - 1,
})
}
///|
fn markdown_editor_next_indented_code_line_index(
lines : Array[MarkdownLine],
start : Int,
) -> Int? {
let mut index = start
while index < lines.length() && markdown_editor_blank_line(lines[index]) {
index = index + 1
}
if index < lines.length() && markdown_editor_indented_code_line(lines, index) {
Some(index)
} else {
None
}
}
///|
fn markdown_editor_blank_line(line : MarkdownLine) -> Bool {
line.text.trim().to_owned() == ""
}
///|
fn markdown_editor_indented_code_line(
lines : Array[MarkdownLine],
index : Int,
) -> Bool {
if index < 0 || index >= lines.length() {
return false
}
markdown_indented_code_content_offset(lines[index].text) > 0
}
///|
struct MarkdownEditorParagraphParseResult {
block : MarkdownEditorBlock
end_index : Int
} derive(Eq, Debug)
///|
fn markdown_editor_soft_wrapped_paragraph_block_at(
lines : Array[MarkdownLine],
index : Int,
definitions : Array[MarkdownReferenceDefinition],
) -> MarkdownEditorParagraphParseResult? {
if !markdown_editor_soft_wrapped_paragraph_line(lines, index) {
return None
}
let paragraph_lines : Array[MarkdownLine] = [lines[index]]
let mut next = index + 1
while next < lines.length() &&
markdown_editor_setext_heading_level_at(lines, next) is None &&
markdown_editor_soft_wrapped_paragraph_line(lines, next) {
paragraph_lines.push(lines[next])
next = next + 1
}
if paragraph_lines.length() < 2 {
return None
}
let text_lines : Array[String] = []
for line in paragraph_lines {
text_lines.push(line.text)
}
let text = markdown_join_lines(text_lines)
Some({
block: {
block: {
kind: MarkdownBlockKind::Paragraph,
text,
inlines: parse_markdown_inlines_with_definitions(text, definitions),
table_alignments: [],
code_language: None,
},
source_range: {
start: paragraph_lines[0].start,
end: paragraph_lines[paragraph_lines.length() - 1].end,
},
content_range: {
start: paragraph_lines[0].start,
end: paragraph_lines[paragraph_lines.length() - 1].end,
},
},
end_index: next - 1,
})
}
///|
fn markdown_editor_soft_wrapped_paragraph_line(
lines : Array[MarkdownLine],
index : Int,
) -> Bool {
if index < 0 || index >= lines.length() {
return false
}
let line = lines[index]
if line.text.trim().to_owned() == "" ||
markdown_editor_opening_code_fence_marker(line.text) is Some(_) ||
markdown_editor_table_block_at(lines, index) is Some(_) ||
markdown_editor_setext_heading_level_at(lines, index) is Some(_) {
return false
}
match parse_markdown(line.text) {
[{ kind: MarkdownBlockKind::Paragraph, .. }] => true
_ => false
}
}
///|
pub(all) struct MarkdownEditorTableParseResult {
block : MarkdownEditorBlock
end_index : Int
} derive(Eq, Debug)
///|
fn markdown_editor_table_block_at(
lines : Array[MarkdownLine],
index : Int,
) -> MarkdownEditorTableParseResult? {
if index + 1 >= lines.length() ||
!markdown_editor_table_separator_line(lines[index + 1].text) {
return None
}
let table_lines : Array[MarkdownLine] = [lines[index], lines[index + 1]]
let mut next = index + 2
while next < lines.length() &&
markdown_editor_table_body_line(lines[next].text) {
table_lines.push(lines[next])
next = next + 1
}
let segment_lines : Array[String] = []
for line in table_lines {
segment_lines.push(line.text)
}
let segment = markdown_join_lines(segment_lines)
match parse_markdown(segment) {
[{ kind: MarkdownBlockKind::Table, .. } as block] => {
let source_start = table_lines[0].start
let source_end = table_lines[table_lines.length() - 1].end
Some({
block: {
block,
source_range: { start: source_start, end: source_end },
content_range: { start: source_start, end: source_end },
},
end_index: next - 1,
})
}
_ => None
}
}
///|
fn markdown_editor_table_separator_line(line : String) -> Bool {
let trimmed = line.trim().to_owned()
if !trimmed.contains("|") {
return false
}
let cells = markdown_editor_split_table_cells(trimmed)
if cells.length() == 0 {
return false
}
for cell in cells {
if !markdown_editor_table_separator_cell(cell.trim().to_owned()) {
return false
}
}
true
}
///|
fn markdown_editor_table_separator_cell(cell : String) -> Bool {
let chars = cell.to_array()
let mut dash_count = 0
for ch in chars {
if ch == '-' {
dash_count = dash_count + 1
} else if ch != ':' && ch != ' ' && ch != '\t' {
return false
}
}
dash_count >= 3
}
///|
fn markdown_editor_table_body_line(line : String) -> Bool {
let trimmed = line.trim().to_owned()
trimmed != "" && trimmed.contains("|")
}
///|
fn markdown_editor_split_table_cells(line : String) -> Array[String] {
let cells : Array[String] = []
let chars = line.to_array()
let mut start = 0
let mut end = chars.length()
if start < end && chars[start] == '|' {
start = start + 1
}
if end > start &&
chars[end - 1] == '|' &&
markdown_editor_table_pipe_is_separator(chars, end - 1) {
end = end - 1
}
let current : Array[Char] = []
let mut index = start
while index < end {
if chars[index] == '|' &&
markdown_editor_table_pipe_is_separator(chars, index) {
cells.push(String::from_array(current))
current.clear()
} else {
current.push(chars[index])
}
index = index + 1
}
cells.push(String::from_array(current))
cells
}
///|
fn markdown_editor_setext_heading_level_at(
lines : Array[MarkdownLine],
index : Int,
) -> Int? {
if index + 1 >= lines.length() {
return None
}
let text = lines[index].text.trim().to_owned()
if text == "" ||
markdown_editor_opening_code_fence_marker(lines[index].text) is Some(_) ||
!markdown_editor_is_setext_paragraph_candidate(lines[index].text) {
return None
}
markdown_editor_setext_underline_level(lines[index + 1].text)
}
///|
fn markdown_editor_is_setext_paragraph_candidate(line : String) -> Bool {
match parse_markdown(line) {
[{ kind: MarkdownBlockKind::Paragraph, .. }] => true
_ => false
}
}
///|
fn markdown_editor_setext_underline_level(line : String) -> Int? {
let text = line.trim().to_owned()
if text == "" {
return None
}
let chars = text.to_array()
let marker = chars[0]
if marker != '=' && marker != '-' {
return None
}
for ch in chars {
if ch != marker {
return None
}
}
Some(if marker == '=' { 1 } else { 2 })
}
///|
fn markdown_editor_closing_code_fence(line : String, fence : String) -> Bool {
let chars = line.to_array()
let fence_chars = fence.to_array()
let indent = markdown_editor_leading_indent_length(chars)
if !markdown_editor_code_fence_indent_allowed(chars, indent) ||
fence_chars.length() < 3 ||
chars.length() < indent + fence_chars.length() {
return false
}
for index in 0.. RichTextDocument {
let rich_blocks : Array[RichTextBlock] = []
for block in blocks {
rich_blocks.push(
markdown_editor_block_to_rich_text(source, block, base, base_dir~),
)
}
RichTextDocument::new(blocks=rich_blocks)
}
///|
fn markdown_editor_block_to_rich_text(
source : String,
editor_block : MarkdownEditorBlock,
base : @core.FontSpec,
base_dir? : String? = None,
) -> RichTextBlock {
let block = markdown_block_to_rich_text(editor_block.block, base, base_dir~)
{
..block,
runs: markdown_editor_runs_with_source_ranges(
block.runs,
source,
editor_block,
),
table: markdown_editor_table_with_source_ranges(
block.table,
source,
editor_block,
),
source_range: Some(markdown_core_range(editor_block.source_range)),
content_range: Some(markdown_core_range(editor_block.content_range)),
}
}
///|
fn markdown_editor_table_with_source_ranges(
table : RichTextTable?,
source : String,
editor_block : MarkdownEditorBlock,
) -> RichTextTable? {
match table {
Some(table) => {
let block_source_text = markdown_editor_substring(
source,
editor_block.source_range.start,
editor_block.source_range.end,
)
let source_cells = markdown_editor_table_cells_for_block(
block_source_text,
{
block: editor_block.block,
source_range: {
start: 0,
end: markdown_editor_text_length(block_source_text),
},
content_range: {
start: 0,
end: markdown_editor_text_length(block_source_text),
},
},
)
let rows : Array[Array[RichTextTableCell]] = []
for row_index in 0..
Some({
start: editor_block.source_range.start + range.content_start,
end: editor_block.source_range.start + range.content_end,
})
None => cell.source_range
},
})
}
rows.push(row)
}
Some({ ..table, rows, })
}
None => None
}
}
///|
fn markdown_editor_find_table_cell(
cells : Array[MarkdownEditorTableCellRange],
row : Int,
column : Int,
) -> MarkdownEditorTableCellRange? {
for cell in cells {
if cell.row == row && cell.column == column {
return Some(cell)
}
}
None
}
///|
fn markdown_editor_runs_with_source_ranges(
runs : Array[RichTextRun],
source : String,
editor_block : MarkdownEditorBlock,
) -> Array[RichTextRun] {
let mapped : Array[RichTextRun] = []
let mut search = 0
let mut index = 0
let content = markdown_editor_run_source_content(source, editor_block)
for run in runs {
if editor_block.block.kind is CodeBlock &&
editor_block.block.code_language is Some(_) &&
index < 2 {
mapped.push(run)
} else if index == 0 && editor_block.block.kind is FootnoteDefinition(_) {
mapped.push({
..run,
source_range: Some({
start: editor_block.source_range.start,
end: editor_block.content_range.start,
}),
})
} else {
let range = markdown_editor_find_run_source_range(
content,
run.text,
search,
)
mapped.push({
..run,
source_range: Some({
start: editor_block.content_range.start + range.start,
end: editor_block.content_range.start + range.end,
}),
})
search = range.end
}
index = index + 1
}
mapped
}
///|
fn markdown_editor_run_source_content(
source : String,
editor_block : MarkdownEditorBlock,
) -> String {
match editor_block.block.kind {
Heading(_)
| UnorderedListItem
| TaskListItem(_)
| OrderedListItem(_)
| Blockquote
| Paragraph
| FootnoteDefinition(_) =>
markdown_editor_substring(
source,
editor_block.content_range.start,
editor_block.content_range.end,
)
CodeBlock | HorizontalRule | Table | HtmlBlock | FrontMatter =>
markdown_editor_block_source_content(editor_block)
}
}
///|
fn markdown_editor_block_source_content(
editor_block : MarkdownEditorBlock,
) -> String {
let source = editor_block.source_range
let content = editor_block.content_range
let chars = markdown_editor_block_source_text(editor_block).to_array()
let start = markdown_editor_clamp_int(
content.start - source.start,
0,
chars.length(),
)
let end = markdown_editor_clamp_int(
content.end - source.start,
start,
chars.length(),
)
String::from_array(chars[start:end])
}
///|
fn markdown_editor_block_source_text(
editor_block : MarkdownEditorBlock,
) -> String {
let prefix_padding = markdown_editor_block_source_prefix_padding(editor_block)
match editor_block.block.kind {
Heading(level) =>
prefix_padding +
markdown_editor_heading_source(level, editor_block.block.text)
UnorderedListItem =>
prefix_padding +
"- " +
markdown_editor_inline_source(editor_block.block.inlines)
TaskListItem(checked) =>
if checked {
prefix_padding +
"- [x] " +
markdown_editor_inline_source(editor_block.block.inlines)
} else {
prefix_padding +
"- [ ] " +
markdown_editor_inline_source(editor_block.block.inlines)
}
OrderedListItem(number) =>
prefix_padding +
number.to_string() +
". " +
markdown_editor_inline_source(editor_block.block.inlines)
Blockquote =>
prefix_padding +
"> " +
markdown_editor_inline_source(editor_block.block.inlines)
Paragraph =>
prefix_padding + markdown_editor_inline_source(editor_block.block.inlines)
FootnoteDefinition(label) =>
prefix_padding +
"[^" +
label +
"]: " +
markdown_editor_inline_source(editor_block.block.inlines)
FrontMatter => "---\n" + editor_block.block.text + "\n---"
CodeBlock | HorizontalRule | Table | HtmlBlock => editor_block.block.text
}
}
///|
fn markdown_editor_block_source_prefix_padding(
editor_block : MarkdownEditorBlock,
) -> String {
let marker_length = match editor_block.block.kind {
Heading(level) => level + 1
UnorderedListItem => 2
TaskListItem(_) => 6
OrderedListItem(number) => markdown_editor_digit_count(number) + 2
Blockquote => 2
FootnoteDefinition(label) =>
markdown_editor_text_length("[^" + label + "]: ")
Paragraph | CodeBlock | HorizontalRule | Table | HtmlBlock | FrontMatter =>
0
}
let padding_length = markdown_editor_max_int(
0,
editor_block.content_range.start -
editor_block.source_range.start -
marker_length,
)
let chars : Array[Char] = []
for _ in 0.. String {
let text = ""
let parts : Array[String] = []
for inline in inlines {
match inline.kind {
Plain => parts.push(inline.text)
Bold => parts.push("**" + inline.text + "**")
Italic => parts.push("*" + inline.text + "*")
Code => parts.push("`" + inline.text + "`")
Strikethrough => parts.push("~~" + inline.text + "~~")
Link =>
parts.push(
"[" + inline.text + "](" + markdown_editor_inline_target(inline) + ")",
)
Autolink => parts.push("<" + markdown_editor_inline_target(inline) + ">")
Image =>
parts.push(
" +
")",
)
FootnoteReference => parts.push("[^" + inline.text + "]")
HtmlInline => parts.push(inline.text)
}
}
ignore(text)
markdown_join_lines([markdown_join_strings(parts)])
}
///|
fn markdown_editor_inline_target(inline : MarkdownInline) -> String {
match inline.target {
Some(target) => target
None => inline.text
}
}
///|
fn markdown_join_strings(parts : Array[String]) -> String {
let chars : Array[Char] = []
for part in parts {
chars.append(part.to_array())
}
String::from_array(chars)
}
///|
fn markdown_editor_heading_source(level : Int, text : String) -> String {
let marks : Array[Char] = []
for _ in 0.. MarkdownEditorRunSourceRange {
let content_chars = content.to_array()
let run_chars = run_text.to_array()
if run_chars.length() == 0 {
let offset = markdown_editor_clamp_int(start, 0, content_chars.length())
return { start: offset, end: offset }
}
let mut index = markdown_editor_clamp_int(start, 0, content_chars.length())
while index < content_chars.length() {
match
markdown_editor_match_run_source_end(content_chars, index, run_chars) {
Some(end) => return { start: index, end }
None => index = index + 1
}
}
let offset = markdown_editor_clamp_int(start, 0, content_chars.length())
{ start: offset, end: offset + run_chars.length() }
}
///|
fn markdown_editor_match_run_source_end(
content : Array[Char],
start : Int,
target : Array[Char],
) -> Int? {
let mut source_index = start
let mut target_index = 0
while target_index < target.length() {
if source_index >= content.length() {
return None
}
match markdown_html_entity_decode_at(content, source_index) {
Some(entity) if entity.ch == target[target_index] => {
source_index = entity.end
target_index = target_index + 1
continue
}
_ => ()
}
if content[source_index] == target[target_index] {
source_index = source_index + 1
target_index = target_index + 1
} else if content[source_index] == '\\' &&
source_index + 1 < content.length() &&
content[source_index + 1] == target[target_index] {
source_index = source_index + 2
target_index = target_index + 1
} else {
return None
}
}
Some(source_index)
}
///|
fn markdown_core_range(range : MarkdownSourceRange) -> RichTextSourceRange {
{ start: range.start, end: range.end }
}
///|
fn markdown_editor_block_kind_label(kind : MarkdownBlockKind) -> String {
match kind {
Heading(level) => "Heading " + level.to_string()
UnorderedListItem => "Bullet list"
TaskListItem(_) => "Task list"
OrderedListItem(_) => "Ordered list"
Blockquote => "Quote"
CodeBlock => "Code block"
HorizontalRule => "Rule"
Table => "Table"
FootnoteDefinition(label) => "Footnote " + label
HtmlBlock => "HTML"
FrontMatter => "Front matter"
Paragraph => "Paragraph"
}
}
///|
fn markdown_editor_content_range(
line : MarkdownLine,
kind : MarkdownBlockKind,
) -> MarkdownSourceRange {
let offset = match kind {
Heading(_)
| UnorderedListItem
| TaskListItem(_)
| OrderedListItem(_)
| Blockquote => markdown_editor_marker_prefix_length(line.text)
FootnoteDefinition(_) =>
markdown_editor_footnote_marker_prefix_length(line.text)
Paragraph | CodeBlock | HorizontalRule | Table | HtmlBlock | FrontMatter =>
0
}
let start = markdown_editor_min_int(line.start + offset, line.end)
let end = match kind {
Heading(_) =>
markdown_editor_min_int(
line.start +
markdown_heading_source_content_end_offset(line.text, offset),
line.end,
)
_ => line.end
}
{ start, end: markdown_editor_max_int(start, end) }
}
///|
fn markdown_editor_footnote_marker_prefix_length(line : String) -> Int {
let chars = line.to_array()
let indent = markdown_editor_leading_indent_length(chars)
if chars.length() < indent + 5 ||
chars[indent] != '[' ||
chars[indent + 1] != '^' {
return 0
}
let mut close = indent + 2
while close < chars.length() && chars[close] != ']' {
close = close + 1
}
if close <= indent + 2 ||
close + 2 >= chars.length() ||
chars[close + 1] != ':' ||
chars[close + 2] != ' ' {
return 0
}
close + 3
}
///|