///|
/// 切分后的一行原始内容。
priv struct RawLine {
chars : Array[Char]
newline : String
}
///|
/// 扫描结果:所有扫描行 + 未闭合代码围栏的行号(1 起始,0 表示没有)。
priv struct ScanResult {
lines : Array[SLine]
unclosed_fence : Int
}
///|
/// 扫描单元:一行文本 + 位置 + 是否位于「不检查」区域 + 逐字符保护掩码。
///
/// `mask[i]` 为 `true` 表示第 i 个字符处于受保护区间(例如行内代码、URL),
/// 内容类规则应当跳过这些位置。
priv struct SLine {
chars : Array[Char]
/// 该行首字符在全文中的字符偏移
start : Int
/// 是否属于代码块 / 前置元数据等「不检查」区域
code : Bool
/// 逐字符保护掩码,长度与 `chars` 相同
mask : Array[Bool]
}
///|
/// 判断是否为空白字符(空格或制表符)。
fn is_space_char(c : Char) -> Bool {
c == ' ' || c == '\t'
}
///|
/// 返回第一个非空白字符的下标;全为空白时返回长度。
fn first_non_space(chars : Array[Char]) -> Int {
let mut i = 0
while i < chars.length() && is_space_char(chars[i]) {
i = i + 1
}
i
}
///|
/// 判断去掉首尾空白后是否等于 `s`。
fn trimmed_equals(chars : Array[Char], s : String) -> Bool {
let start = first_non_space(chars)
let mut end = chars.length()
while end > start && is_space_char(chars[end - 1]) {
end = end - 1
}
if end - start != s.char_length() {
return false
}
let mut i = start
let mut ok = true
for c in s {
if i >= end || chars[i] != c {
ok = false
break
}
i = i + 1
}
ok
}
///|
/// 若该行是一个代码围栏(``` 或 ~~~),返回围栏使用的字符。
fn fence_marker(chars : Array[Char]) -> Char? {
let i = first_non_space(chars)
if i >= chars.length() {
return None
}
let c = chars[i]
if c != '`' && c != '~' {
return None
}
let mut j = i
while j < chars.length() && chars[j] == c {
j = j + 1
}
if j - i >= 3 {
Some(c)
} else {
None
}
}
///|
/// 逐字符切分为行,保留各行原本的换行符(`\n` 或 `\r\n`)。
fn split_raw_lines(source : String) -> Array[RawLine] {
let out : Array[RawLine] = []
let cur : Array[Char] = []
for c in source {
if c == '\n' {
let mut nl = "\n"
if cur.length() > 0 && cur[cur.length() - 1] == '\r' {
let _ = cur.pop()
nl = "\r\n"
}
out.push(RawLine::{ chars: cur.copy(), newline: nl, })
cur.clear()
} else {
cur.push(c)
}
}
if cur.length() > 0 {
out.push(RawLine::{ chars: cur, newline: "", })
}
out
}
///|
/// 在 `chars` 中从 `from` 开始查找连续 `ticks` 个反引号的结束位置(返回其后的下标),找不到返回 -1。
fn find_ticks_close(chars : Array[Char], from : Int, ticks : Int) -> Int {
let n = chars.length()
let mut k = from
while k < n {
if chars[k] == '`' {
let mut m = k
while m < n && chars[m] == '`' {
m = m + 1
}
if m - k == ticks {
return m
}
k = m
} else {
k = k + 1
}
}
-1
}
///|
/// 判断 `chars` 从 `i` 开始是否匹配 ASCII 前缀 `p`。
fn matches_at(chars : Array[Char], i : Int, p : String) -> Bool {
let plen = p.char_length()
if i + plen > chars.length() {
return false
}
let mut k = 0
let mut ok = true
for c in p {
if chars[i + k] != c {
ok = false
break
}
k = k + 1
}
ok
}
///|
/// 检测常见 URL / 邮箱前缀,返回前缀长度;不匹配返回 0。
fn url_prefix_len(chars : Array[Char], i : Int) -> Int {
let prefixes : Array[String] = [
"https://", "http://", "ftp://", "mailto:", "www.", "git@",
]
for p in prefixes {
if matches_at(chars, i, p) {
return p.char_length()
}
}
0
}
///|
/// URL 的终止字符:空白、CJK 文字、CJK 标点,以及成对的括号与引号。
fn is_url_terminator(c : Char) -> Bool {
is_space_char(c) ||
is_cjk_ideograph(c) ||
is_cjk_punct(c) ||
c == '"' ||
c == '\'' ||
c == '<' ||
c == '>' ||
c == '`'
}
///|
/// 构造一行的保护掩码:行内代码、URL、Markdown 链接目标、尖括号包裹的内容。
fn build_mask(chars : Array[Char]) -> Array[Bool] {
let n = chars.length()
let mask = Array::make(n, false)
let mut i = 0
while i < n {
let c = chars[i]
if c == '`' {
let mut j = i
while j < n && chars[j] == '`' {
j = j + 1
}
let ticks = j - i
let close = find_ticks_close(chars, j, ticks)
if close >= 0 {
for p = i; p < close; p = p + 1 {
mask[p] = true
}
i = close
} else {
i = j
}
} else if c == ']' && i + 1 < n && chars[i + 1] == '(' {
// Markdown 链接 / 图片目标
let mut k = i + 2
let mut close = -1
while k < n {
if chars[k] == ')' {
close = k
break
}
k = k + 1
}
if close >= 0 {
for p = i; p <= close; p = p + 1 {
mask[p] = true
}
i = close + 1
} else {
i = i + 1
}
} else if c == '(' && i > 0 && chars[i - 1] == ']' {
// 理论上已由上一条分支处理,这里跳过避免重复
i = i + 1
} else if c == '<' {
let mut k = i + 1
let mut close = -1
let mut has_space = false
while k < n {
if chars[k] == '>' {
close = k
break
}
if is_space_char(chars[k]) {
has_space = true
}
k = k + 1
}
if close >= 0 && !has_space {
for p = i; p <= close; p = p + 1 {
mask[p] = true
}
i = close + 1
} else {
i = i + 1
}
} else if url_prefix_len(chars, i) > 0 {
let mut k = i
while k < n && !is_url_terminator(chars[k]) {
k = k + 1
}
for p = i; p < k; p = p + 1 {
mask[p] = true
}
i = k
} else if c == '@' && i > 0 {
// 邮箱:向左吃掉单词字符,向右直到终止符
let mut start = i
while start > 0 &&
!is_space_char(chars[start - 1]) &&
!is_cjk_punct(chars[start - 1]) {
start = start - 1
}
let mut k = i + 1
while k < n && !is_url_terminator(chars[k]) {
k = k + 1
}
if k > i + 1 {
for p = start; p < k; p = p + 1 {
mask[p] = true
}
i = k
} else {
i = i + 1
}
} else {
i = i + 1
}
}
mask
}
///|
/// 把全文切分为扫描行,并标注代码区域与保护掩码。
fn scan_source(source : String, markdown : Bool) -> ScanResult {
let raws = split_raw_lines(source)
let out : Array[SLine] = []
let mut offset = 0
let mut fence : Char? = None
let mut fence_line = 0
let mut in_front_matter = false
// 上一行是否为空行(文档开头视作空行),用于识别缩进代码块
let mut prev_blank = true
let mut k = 0
while k < raws.length() {
let raw = raws[k]
let mut code = false
if markdown && k == 0 && trimmed_equals(raw.chars, "---") {
in_front_matter = true
code = true
} else if in_front_matter {
code = true
if trimmed_equals(raw.chars, "---") || trimmed_equals(raw.chars, "...") {
in_front_matter = false
}
}
if !code {
match fence {
Some(f) => {
code = true
if fence_marker(raw.chars) == Some(f) {
fence = None
fence_line = 0
}
}
None =>
match fence_marker(raw.chars) {
Some(f) => {
fence = Some(f)
fence_line = k + 1
code = true
}
None => ()
}
}
}
// 缩进代码块:紧跟在空行之后、缩进 >= 4 的行
if !code && markdown && prev_blank && is_indented_code_line(raw.chars) {
code = true
}
let mask = if code {
Array::make(raw.chars.length(), true)
} else {
build_mask(raw.chars)
}
out.push(SLine::{ chars: raw.chars, start: offset, code, mask, })
prev_blank = is_blank_line(raw.chars)
offset = offset + raw.chars.length() + raw.newline.length()
k = k + 1
}
{ lines: out, unclosed_fence: fence_line, }
}
///|
/// 规则执行上下文。
priv struct Ctx {
lines : Array[SLine]
config : Config
/// 未闭合代码围栏所在行号(1 起始,0 表示没有)
unclosed_fence : Int
}
///|
/// 位置是否落在受保护区间内(越界视为受保护)。
fn SLine::is_protected(self : SLine, i : Int) -> Bool {
if i < 0 || i >= self.mask.length() {
return true
}
self.mask[i]
}
///|
/// 取第 i 个字符;越界返回换行(便于统一处理行尾)。
fn SLine::char_at(self : SLine, i : Int) -> Char {
if i < 0 || i >= self.chars.length() {
return '\n'
}
self.chars[i]
}