///|
priv struct MarkdownTableParseResult {
block : MarkdownBlock
end_index : Int
}
///|
fn markdown_table_block_at(
lines : Array[String],
index : Int,
) -> MarkdownTableParseResult? {
if index + 1 >= lines.length() ||
!markdown_table_separator_line(lines[index + 1]) {
return None
}
let segment_lines : Array[String] = [lines[index], lines[index + 1]]
let mut next = index + 2
while next < lines.length() && markdown_table_body_line(lines[next]) {
segment_lines.push(lines[next])
next = next + 1
}
Some({
block: markdown_table_block(
markdown_table_text_from_source_lines(segment_lines),
markdown_table_alignments_for_separator(lines[index + 1]),
),
end_index: next - 1,
})
}
///|
fn markdown_table_separator_line(line : String) -> Bool {
let trimmed = line.trim().to_owned()
if !trimmed.contains("|") {
return false
}
let cells = markdown_split_table_cells(trimmed)
if cells.length() == 0 {
return false
}
for cell in cells {
if !markdown_table_separator_cell(cell.trim().to_owned()) {
return false
}
}
true
}
///|
fn markdown_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_table_body_line(line : String) -> Bool {
let trimmed = line.trim().to_owned()
trimmed != "" && trimmed.contains("|")
}
///|
pub fn markdown_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_table_block(
text : String,
table_alignments : Array[@core.TextAlign?],
) -> MarkdownBlock {
{ kind: Table, text, inlines: [], table_alignments, code_language: None, }
}
///|
fn markdown_table_text(
header : Array[@md.TableCell],
rows : Array[Array[@md.TableCell]],
) -> String {
let lines : Array[String] = []
lines.push(markdown_table_row_text(header))
for row in rows {
lines.push(markdown_table_row_text(row))
}
markdown_join_lines(lines)
}
///|
fn markdown_table_text_from_source_lines(lines : Array[String]) -> String {
let rows : Array[String] = []
for index in 0.. String {
let cells : Array[String] = []
for cell in row {
cells.push(markdown_inline_text(cell.children).trim().to_owned())
}
cells.join(" | ")
}
///|
fn markdown_table_preview(
block : MarkdownBlock,
base : @core.FontSpec,
) -> RichTextTable? {
match block.kind {
Table => {
let lines = markdown_split_lines(block.text)
if lines.length() == 0 {
return None
}
let rows : Array[Array[RichTextTableCell]] = []
let aligns = block.table_alignments
let mut line_offset = 0
for index in 0.. None
}
}
///|
/// Styled runs for one table cell: parse the cell's raw source as inline
/// markdown so `` ` ``/`**`/... render WYSIWYG inside the cell, and hide the
/// markers via `visual_text` (the raw text stays in `text`/source ranges, so
/// caret/hit geometry keeps mapping to the source). A cell whose parse
/// yields a single plain span keeps empty runs — the plain-text path is
/// then exactly what it always painted.
fn markdown_table_cell_runs(
cell_text : String,
base : @core.FontSpec,
) -> Array[RichTextRun] {
if cell_text == "" {
return []
}
let inlines = parse_markdown_inlines(cell_text)
let plain = inlines.length() == 1 && inlines[0].kind is Plain
if plain {
return []
}
// Multi-span parse: locate each run's raw source (markers included,
// re-serialized from the inline) inside the cell by sequential search,
// mirroring how block runs map to block content. A span that cannot be
// located (entity decoding changed the text) falls back to its visible
// text with no source range.
let runs : Array[RichTextRun] = []
let chars = cell_text.to_array()
let mut search = 0
for inline in inlines {
let raw = markdown_editor_inline_source([inline])
let range = markdown_table_find_run_range(chars, raw, search)
let run = RichTextRun::new(
text=match range {
Some(_) => raw
None => inline.text
},
visual_text=if inline.kind is Plain { None } else { Some(inline.text) },
font=Some(markdown_inline_font(inline.kind, base)),
color=markdown_inline_color(inline.kind),
background=markdown_inline_background(inline.kind),
decoration=markdown_inline_decoration(inline.kind),
source_range=range,
)
runs.push(run)
match range {
Some(range) => search = range.end
None => ()
}
}
runs
}
///|
/// Locate `raw` inside the cell chars at/after `search`, returning the
/// cell-relative range including markers. Finds the closing marker beyond
/// the visible text so the range covers the whole construct.
fn markdown_table_find_run_range(
chars : Array[Char],
raw : String,
search : Int,
) -> RichTextSourceRange? {
if raw == "" {
return None
}
let raw_chars = raw.to_array()
let mut index = search
while index + raw_chars.length() <= chars.length() {
let mut matched = true
for offset in 0.. Array[MarkdownTableCellPreviewRange] {
let cells : Array[MarkdownTableCellPreviewRange] = []
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 mut segment_start = start
let mut index = start
while index <= end {
if index == end ||
(
chars[index] == '|' &&
markdown_editor_table_pipe_is_separator(chars, index)
) {
let range = markdown_table_trimmed_range(chars, segment_start, index)
cells.push({
text: String::from_array(chars[range.start:range.end]),
start: range.start,
end: range.end,
})
segment_start = index + 1
}
index = index + 1
}
cells
}
///|
fn markdown_table_alignments_for_separator(
line : String,
) -> Array[@core.TextAlign?] {
let aligns : Array[@core.TextAlign?] = []
for cell in markdown_split_table_cells(line) {
aligns.push(markdown_table_alignment_for_separator_cell(cell))
}
aligns
}
///|
fn markdown_table_alignment_for_separator_cell(
cell : String,
) -> @core.TextAlign? {
let trimmed = cell.trim().to_owned()
let chars = trimmed.to_array()
if chars.length() == 0 {
return None
}
let leading = chars[0] == ':'
let trailing = chars[chars.length() - 1] == ':'
if leading && trailing {
Some(TextCenter)
} else if trailing {
Some(TextEnd)
} else {
None
}
}
///|
fn markdown_table_alignment_at(
aligns : Array[@core.TextAlign?],
index : Int,
) -> @core.TextAlign? {
if index >= 0 && index < aligns.length() {
aligns[index]
} else {
None
}
}
///|
fn markdown_table_trimmed_range(
chars : Array[Char],
start : Int,
end : Int,
) -> RichTextSourceRange {
let mut content_start = start
let mut content_end = end
while content_start < content_end &&
(chars[content_start] == ' ' || chars[content_start] == '\t') {
content_start = content_start + 1
}
while content_end > content_start &&
(chars[content_end - 1] == ' ' || chars[content_end - 1] == '\t') {
content_end = content_end - 1
}
{ start: content_start, end: content_end, }
}