///|
#external
priv type JsRegex
///|
/// extract basic information from a regex match result
priv struct MatchResult {
matched : String
// start : UInt
selects : Array[String]
} derive(Show)
///|
extern "js" fn JsRegex::js_regex_create(
pattern : String,
flags : String,
) -> JsRegex =
#| (pattern, flags) => new RegExp(pattern, flags)
///|
extern "js" fn JsRegex::match_str(regex : JsRegex, input : String) -> JsValue =
#| (regex, input) => {
#| let ret = input.match(regex);
#| console.log("match result: ", ret);
#| return ret;
#| }
///|
#external
priv type JsValue
///|
extern "js" fn JsValue::is_null(obj : JsValue) -> Bool =
#| (obj) => obj === null
///|
extern "js" fn JsValue::get_length(obj : JsValue) -> UInt =
#| (obj) => obj.length
///|
extern "js" fn JsValue::reinterpret_as_string(obj : JsValue) -> String =
#| (obj) => obj
///|
extern "js" fn JsValue::get_index(obj : JsValue, index : UInt) -> JsValue =
#| (obj, index) => obj[index]
///|
fn match_string_pattern(input : String, reg : JsRegex) -> MatchResult? {
let ret = JsRegex::match_str(reg, input)
if JsValue::is_null(ret) {
None
} else {
let matched = JsValue::get_index(ret, 0)
// let start = JsValue::get_prop(matched, "index")
let selects : Array[String] = Array::new()
let len = JsValue::get_length(ret).reinterpret_as_int()
for i = 0; i < len; i = i + 1 {
if i == 0 {
continue
}
let item = JsValue::get_index(ret, i.reinterpret_as_uint())
selects.push(item.reinterpret_as_string())
}
Some(MatchResult::{
matched: matched.reinterpret_as_string(),
// start: start.reinterpret_as_int().reinterpret_as_uint(),
selects,
})
}
}
///|
let peek_emphasis : JsRegex = JsRegex::js_regex_create("^([^\\*]*)\\*\\*", "")
// def peek-image $ new js/RegExp "\"^\\!\\[[^\\]]*\\]\\([^\\)]+\\)" "\"g"
///|
let peek_image : JsRegex = JsRegex::js_regex_create(
"^\\!\\[[^\\]]*\\]\\([^\\)]+\\)", "g",
)
///|
let peek_italic : JsRegex = JsRegex::js_regex_create("^([^*/]+)\\*", "")
///|
let peek_link : JsRegex = JsRegex::js_regex_create(
"^\\[[^\\]]+\\]\\([^\\)]+\\)", "",
)
///| Check if line contains a code fence (```) with optional leading whitespace
///|
/// Returns Some((indent, lang)) if found, None otherwise
fn parse_code_fence(line : String) -> (Int, String)? {
let trimmed = line.trim_start(chars=" ").to_string()
if trimmed.has_prefix("```") {
let indent = line.length() - trimmed.length()
let lang = trimmed.unsafe_substring(start=3, end=trimmed.length())
Some((indent, lang))
} else {
None
}
}
///|
/// Remove leading spaces from a line up to the specified indent level
fn remove_indent(line : String, indent : Int) -> String {
let mut count = 0
let mut start_idx = 0
for i = 0; i < line.length() && count < indent; i = i + 1 {
if line.get_char(i) == Some(' ') {
count = count + 1
start_idx = i + 1
} else {
break
}
}
line.unsafe_substring(start=start_idx, end=line.length())
}
///|
pub enum Block {
Empty
Text(@immut/array.T[String])
Code(Int, @immut/array.T[String]) // (indent, lines)
Table(@immut/array.T[Array[String]])
}
///|
pub fn split_block(text : String) -> Array[Block] {
split_block_iter(
text.split("\n").map(fn(x) -> String { x.to_string() }).to_array(),
[],
Empty,
0, // code_indent: track the indentation level of code block
)
}
///|
fn split_block_iter(
lines : Array[String],
acc : Array[Block],
buffer : Block,
code_indent : Int,
) -> Array[Block] {
if lines.is_empty() {
if buffer is Empty {
//
} else {
acc.push(buffer)
}
return acc
}
let cursor = lines[0]
let left = lines[1:].to_array()
match buffer {
Empty =>
if cursor == "" {
split_block_iter(left, acc, Empty, 0)
} else if parse_code_fence(cursor) is Some((indent, lang)) {
split_block_iter(
left,
acc,
Code(indent, @immut/array.from_array([lang])),
indent,
)
} else if is_table_line(cursor) {
split_block_iter(
left,
acc,
Table(@immut/array.from_array([split_table_content(cursor)])),
0,
)
} else {
split_block_iter(left, acc, Text(@immut/array.from_array([cursor])), 0)
}
Text(buffer) =>
if cursor == "" {
if not(buffer.is_empty()) {
acc.push(Text(buffer))
}
split_block_iter(left, acc, Empty, 0)
} else if parse_code_fence(cursor) is Some((indent, lang)) {
if not(buffer.is_empty()) {
acc.push(Text(buffer))
}
split_block_iter(
left,
acc,
Code(indent, @immut/array.from_array([lang])),
indent,
)
} else if is_table_line(cursor) {
if not(buffer.is_empty()) {
acc.push(Text(buffer))
}
split_block_iter(
left,
acc,
Table(@immut/array.from_array([split_table_content(cursor)])),
0,
)
} else {
split_block_iter(left, acc, Text(buffer.push(cursor)), 0)
}
Code(block_indent, buffer) =>
// Check for closing fence with same or less indentation
if parse_code_fence(cursor) is Some((indent, _)) && indent <= code_indent {
acc.push(Code(block_indent, buffer))
split_block_iter(left, acc, Empty, 0)
} else {
// Remove the code block's indentation from content lines
let content = remove_indent(cursor, code_indent)
split_block_iter(
left,
acc,
Code(block_indent, buffer.push(content)),
code_indent,
)
}
Table(buffer) =>
if is_table_line(cursor) {
split_block_iter(
left,
acc,
Table(buffer.push(split_table_content(cursor))),
0,
)
} else {
acc.push(Table(buffer))
split_block_iter(left, acc, Empty, 0)
}
}
}
///|
pub enum LineChunk {
Text(String)
Code(String)
Url(String)
Link(String)
Image(String)
Emphasis(String)
Italic(String)
}
///|
pub fn LineChunk::is_empty(self : LineChunk) -> Bool {
match self {
Text("") => true
Code("") => true
Url("") => true
Link("") => true
Image("") => true
Emphasis("") => true
Italic("") => true
_ => false
}
}
///|
pub fn split_line(line : String) -> Array[LineChunk] {
split_line_iter([], line, Text(""))
}
///|
fn split_line_iter(
acc : Array[LineChunk],
line : String,
mode : LineChunk,
) -> Array[LineChunk] {
if line == "" {
if mode.is_empty() {
return acc
} else {
acc.push(mode)
return acc
}
}
let cursor = line.get_char(0).unwrap()
let left = line.unsafe_substring(start=1, end=line.length())
// println("cursor: \{cursor} .. mode: \{mode} .. line: \{line}")
match mode {
Text(buffer) =>
match cursor {
'`' => {
if not(buffer.is_empty()) {
acc.push(mode)
}
split_line_iter(acc, left, Code(""))
}
'h' =>
if line.has_prefix("http://") || line.has_prefix("https://") {
let pieces = line
.split(" ")
.map(fn(x) -> String { x.to_string() })
.to_array()
if not(mode.is_empty()) {
acc.push(mode)
}
acc.push(Url(pieces[0].to_string()))
split_line_iter(acc, pieces[1:].to_array().join(" "), Text(""))
} else {
split_line_iter(acc, left, Text(buffer + cursor.to_string()))
}
'[' =>
if match_string_pattern(line, peek_link) is Some(guess) {
let guess = guess.matched
if not(mode.is_empty()) {
acc.push(mode)
}
acc.push(Link(guess.to_string()))
split_line_iter(acc, line.replace(old=guess, new=""), Text(""))
} else {
split_line_iter(acc, left, Text(buffer + cursor.to_string()))
}
'!' =>
if match_string_pattern(line, peek_image) is Some(guess) {
let guess = guess.matched
if not(mode.is_empty()) {
acc.push(mode)
}
acc.push(Image(guess.to_string()))
split_line_iter(acc, line.replace(old=guess, new=""), Text(""))
} else {
split_line_iter(acc, left, Text(buffer + cursor.to_string()))
}
'*' =>
if left == "" {
split_line_iter(acc, left, Text(buffer + "*"))
} else {
let next_left = left.unsafe_substring(start=1, end=left.length())
if left.has_prefix("*") {
if match_string_pattern(next_left, peek_emphasis) is Some(matched) {
println("emphasis: \{matched}")
let emphasis = matched.selects[0]
let rest_line = next_left.unsafe_substring(
start=emphasis.length() + 2,
end=next_left.length(),
)
acc.push(mode)
acc.push(Emphasis(emphasis))
split_line_iter(acc, rest_line, Text(""))
} else {
split_line_iter(acc, left, Text(buffer + "*"))
}
} else if match_string_pattern(left, peek_italic) is Some(matched) {
let italic = matched.selects[0]
let rest_line = next_left.unsafe_substring(
start=italic.length(),
end=next_left.length(),
)
acc.push(mode)
acc.push(Italic(italic))
split_line_iter(acc, rest_line, Text(""))
} else {
split_line_iter(acc, left, Text(buffer + "*"))
}
}
_ => split_line_iter(acc, left, Text(buffer + cursor.to_string()))
}
Code(buffer) =>
if cursor == '`' {
acc.push(Code(buffer))
split_line_iter(acc, left, Text(""))
} else {
split_line_iter(acc, left, Code(buffer + cursor.to_string()))
}
_ => panic() // unknown
}
}
///|
pub fn split_table_content(cursor : String) -> Array[String] {
cursor
.unsafe_substring(start=1, end=cursor.length() - 1)
.split("|")
.map(fn(x) { x.trim(chars=" ").to_string() })
.to_array()
}
///|
pub fn is_table_line(cursor : String) -> Bool {
cursor.has_prefix("|") && cursor.has_suffix("|")
}
///|
/// Check if a table row is a separator line (e.g., | --- | :--- | ---: |)
pub fn is_separator_line(cells : Array[String]) -> Bool {
if cells.length() == 0 {
return false
}
for cell in cells {
let trimmed = cell.trim(chars=" :")
if trimmed.length() == 0 {
return false
}
for c in trimmed {
if c != '-' {
return false
}
}
}
true
}