///|
pub fn markdown_editor_selected_link_target(
source : String,
selection : MarkdownEditorSelection,
) -> String? {
match markdown_editor_link_context(source, selection) {
Some(context) => markdown_editor_reference_target(source, context.bounds)
None => None
}
}
///|
pub fn markdown_editor_selected_image_target(
source : String,
selection : MarkdownEditorSelection,
) -> String? {
match markdown_editor_image_context(source, selection) {
Some(context) => markdown_editor_reference_target(source, context.bounds)
None => None
}
}
///|
pub(all) struct MarkdownReferenceContext {
range : @core.TextRange
bounds : MarkdownLinkBounds
} derive(Eq, Debug)
///|
pub extend MarkdownReferenceContext with Debug::{to_repr}
///|
pub extend MarkdownReferenceContext with Eq::{not_equal, equal}
///|
pub fn markdown_editor_link_context(
source : String,
selection : MarkdownEditorSelection,
) -> MarkdownReferenceContext? {
match markdown_editor_active_selection(selection.selection) {
Some(range) => {
let range = range.normalized()
match markdown_editor_range_link_bounds(source, range) {
Some(bounds) => Some({ range, bounds, })
None => None
}
}
None => markdown_editor_link_context_at_caret(source, selection.caret)
}
}
///|
pub fn markdown_editor_image_context(
source : String,
selection : MarkdownEditorSelection,
) -> MarkdownReferenceContext? {
match markdown_editor_active_selection(selection.selection) {
Some(range) => {
let range = range.normalized()
match markdown_editor_range_image_bounds(source, range) {
Some(bounds) => Some({ range, bounds, })
None => None
}
}
None => markdown_editor_image_context_at_caret(source, selection.caret)
}
}
///|
fn markdown_editor_link_context_at_caret(
source : String,
caret : Int,
) -> MarkdownReferenceContext? {
let chars = source.to_array()
let caret = markdown_editor_clamp_int(caret, 0, chars.length())
match markdown_editor_autolink_context_at_caret(source, chars, caret) {
Some(context) => return Some(context)
None => ()
}
match
markdown_editor_bracketed_reference_range_at_caret(
chars,
caret,
image=false,
) {
Some(range) =>
match markdown_editor_range_link_bounds(source, range) {
Some(bounds) => Some({ range, bounds, })
None => None
}
None => None
}
}
///|
fn markdown_editor_image_context_at_caret(
source : String,
caret : Int,
) -> MarkdownReferenceContext? {
let chars = source.to_array()
let caret = markdown_editor_clamp_int(caret, 0, chars.length())
match
markdown_editor_bracketed_reference_range_at_caret(chars, caret, image=true) {
Some(range) =>
match markdown_editor_range_image_bounds(source, range) {
Some(bounds) => Some({ range, bounds, })
None => None
}
None => None
}
}
///|
fn markdown_editor_autolink_context_at_caret(
source : String,
chars : Array[Char],
caret : Int,
) -> MarkdownReferenceContext? {
let mut open = caret
while open > 0 && chars[open - 1] != '<' && chars[open - 1] != '\n' {
open = open - 1
}
if open <= 0 || chars[open - 1] != '<' {
return None
}
let mut close = caret
while close < chars.length() && chars[close] != '>' && chars[close] != '\n' {
close = close + 1
}
if close >= chars.length() || chars[close] != '>' {
return None
}
let range = @core.TextRange::new(start=open, end=close)
match markdown_editor_range_link_bounds(source, range) {
Some(bounds) => Some({ range, bounds, })
None => None
}
}
///|
fn markdown_editor_bracketed_reference_range_at_caret(
chars : Array[Char],
caret : Int,
image~ : Bool,
) -> @core.TextRange? {
let mut open = caret
while open > 0 && chars[open - 1] != '[' && chars[open - 1] != '\n' {
open = open - 1
}
if open <= 0 || chars[open - 1] != '[' {
return None
}
let open_start = open - 1
if image {
if open_start <= 0 || chars[open_start - 1] != '!' {
return None
}
} else if open_start > 0 && chars[open_start - 1] == '!' {
return None
}
let mut close = open
while close < chars.length() && chars[close] != ']' && chars[close] != '\n' {
close = close + 1
}
if close >= chars.length() || chars[close] != ']' || caret > close {
return None
}
Some(@core.TextRange::new(start=open, end=close))
}
///|
pub fn markdown_editor_selected_footnote_text(
source : String,
selection : MarkdownEditorSelection,
) -> String? {
match markdown_editor_footnote_label_at_selection(source, selection) {
Some(label) => markdown_editor_footnote_definition_text(source, label)
None => None
}
}
///|
pub fn markdown_editor_selected_html_block_text(
source : String,
selection : MarkdownEditorSelection,
) -> String? {
match markdown_editor_html_block_at_selection(source, selection) {
Some(block) => Some(block.block.text)
None => None
}
}
///|
pub fn markdown_editor_active_block_label(
source : String,
selection : MarkdownEditorSelection,
) -> String {
let anchor = match markdown_editor_active_selection(selection.selection) {
Some(range) => range.start
None => selection.caret
}
let anchor = markdown_editor_clamp_int(
anchor,
0,
markdown_editor_text_length(source),
)
for block in parse_markdown_editor_blocks(source) {
if anchor >= block.source_range.start && anchor <= block.source_range.end {
return markdown_editor_block_kind_label(block.block.kind)
}
}
"Paragraph"
}
///|
fn markdown_editor_code_block_at_content_offset(
source : String,
caret : Int,
selection : @core.TextRange?,
context? : MarkdownEditorInputContext? = None,
) -> MarkdownEditorBlock? {
if markdown_editor_active_selection(selection) is Some(_) {
return None
}
// With the session context the char length is the text store's O(1) tail
// offset; without one, fall back to the cached whole-source length.
let source_length = match context {
Some(context) => context.store.length()
None => markdown_editor_text_length(source)
}
let caret = markdown_editor_clamp_int(caret, 0, source_length)
// With the session's blocks at hand the ranges are already exact; only
// callers without a session pay for the whole-document parse. Block
// ranges are ordered and non-overlapping, so the caret sits inside at
// most one block and a binary search replaces the per-keystroke scan.
let blocks = match context {
Some(context) => context.blocks
None => parse_markdown_editor_blocks(source)
}
match markdown_editor_block_index_at_offset(blocks, caret) {
Some(index) => {
let block = blocks[index]
if block.block.kind is CodeBlock &&
caret >= block.content_range.start &&
caret <= block.content_range.end {
Some(block)
} else {
None
}
}
None => None
}
}
///|
fn markdown_editor_code_block_at_selection(
source : String,
selection : MarkdownEditorSelection,
) -> MarkdownEditorBlock? {
if markdown_editor_active_selection(selection.selection) is Some(_) {
return None
}
let caret = markdown_editor_clamp_int(
selection.caret,
0,
markdown_editor_text_length(source),
)
for block in parse_markdown_editor_blocks(source) {
if block.block.kind is CodeBlock &&
caret >= block.source_range.start &&
caret <= block.source_range.end {
return Some(block)
}
}
None
}
///|
/// Raw HTML blocks and front matter are edited as verbatim text: a blank
/// line inside them would close the construct, so Enter keeps the plain
/// newline there instead of splitting with a blank writing line.
fn markdown_editor_verbatim_block_at_content_offset(
source : String,
caret : Int,
selection : @core.TextRange?,
context? : MarkdownEditorInputContext? = None,
) -> MarkdownEditorBlock? {
if markdown_editor_active_selection(selection) is Some(_) {
return None
}
let caret = markdown_editor_clamp_int(
caret,
0,
markdown_editor_text_length(source),
)
let blocks = match context {
Some(context) => context.blocks
None => parse_markdown_editor_blocks(source)
}
for block in blocks {
if (block.block.kind is HtmlBlock || block.block.kind is FrontMatter) &&
caret >= block.source_range.start &&
caret <= block.source_range.end {
return Some(block)
}
}
None
}
///|
fn markdown_editor_html_block_at_selection(
source : String,
selection : MarkdownEditorSelection,
) -> MarkdownEditorBlock? {
if markdown_editor_active_selection(selection.selection) is Some(_) {
return None
}
let caret = markdown_editor_clamp_int(
selection.caret,
0,
markdown_editor_text_length(source),
)
for block in parse_markdown_editor_blocks(source) {
if block.block.kind is HtmlBlock &&
caret >= block.source_range.start &&
caret <= block.source_range.end {
return Some(block)
}
}
None
}
///|
fn markdown_editor_code_block_enter(
source : String,
caret : Int,
) -> RichTextInputTransform {
let chars = source.to_array()
let caret = markdown_editor_clamp_int(caret, 0, chars.length())
let line_start = markdown_editor_line_start(chars, caret)
let indent = markdown_editor_line_indent(chars, line_start, caret)
let insert = "\n" + indent
markdown_editor_transform_result(
text=String::from_array(chars[:caret]) +
insert +
String::from_array(chars[caret:]),
caret=caret + markdown_editor_text_length(insert),
edit_range=Some({ start: caret, end: caret, }),
)
}
///|
pub(all) struct MarkdownLinkBounds {
open_start : Int
close_start : Int
target_end : Int
target_start : Int
definition_target_start : Int
definition_target_end : Int
reference_label : String
} derive(Eq, Debug)
///|
pub extend MarkdownLinkBounds with Debug::{to_repr}
///|
pub extend MarkdownLinkBounds with Eq::{not_equal, equal}
///|
/// Whole-source entry for the command and hit-test paths, which run once
/// per interaction; the reveal path calls the windowed core directly with
/// a block-sized window instead of materializing the whole document.
fn markdown_editor_range_link_bounds(
source : String,
range : @core.TextRange,
) -> MarkdownLinkBounds? {
markdown_editor_range_link_bounds_in(
source,
chars=markdown_editor_source_chars_all(source),
range,
)
}
///|
fn markdown_editor_range_link_bounds_in(
source : String,
chars~ : MarkdownSourceChars,
range : @core.TextRange,
) -> MarkdownLinkBounds? {
match markdown_editor_range_autolink_bounds_in(chars~, range) {
Some(bounds) => return Some(bounds)
None => ()
}
if range.start < 1 || range.end + 2 > chars.length() {
return None
}
let open_start = range.start - 1
// `open_start - 1 == range.start - 2` is inside the window whenever it
// is >= 0: the near window begins at `range.start - 2`, the all window
// at 0.
let bang_before = match chars.get(open_start - 1) {
Some(ch) => ch == '!'
None => false
}
if open_start > 0 && bang_before {
return None
}
let open_char = match chars.get(open_start) {
Some(ch) => ch
None => return None
}
let close_char = match chars.get(range.end) {
Some(ch) => ch
None => return None
}
if open_char != '[' || close_char != ']' {
return None
}
match chars.get(range.end + 1) {
Some('[') =>
return markdown_editor_reference_link_bounds_in(
source,
chars~,
range,
open_start,
bang=false,
)
Some('(') => ()
// The window tail ends on blank lines, so an out-of-window read plays
// the role the '\n' terminator played in the whole-source scan.
_ =>
return markdown_editor_shortcut_reference_link_bounds_in(
source,
chars~,
range,
open_start,
)
}
let mut index = range.end + 2
let mut found = false
while index < chars.length() {
match chars.get(index) {
Some(')') => {
found = true
break
}
Some('\n') => break
Some(_) => index = index + 1
None => break
}
}
if found {
Some({
open_start,
close_start: range.end,
target_start: range.end + 2,
target_end: index,
definition_target_start: -1,
definition_target_end: -1,
reference_label: "",
})
} else {
None
}
}
///|
fn markdown_editor_range_autolink_bounds_in(
chars~ : MarkdownSourceChars,
range : @core.TextRange,
) -> MarkdownLinkBounds? {
if range.start < 1 || range.end >= chars.length() {
return None
}
let open_char = match chars.get(range.start - 1) {
Some(ch) => ch
None => return None
}
let close_char = match chars.get(range.end) {
Some(ch) => ch
None => return None
}
if open_char != '<' || close_char != '>' {
return None
}
let target = match chars.slice(range.start, range.end) {
Some(slice) => String::from_array(slice)
None => return None
}
if !markdown_editor_is_autolink_target(target) {
return None
}
Some({
open_start: range.start - 1,
close_start: range.end,
target_start: range.start,
target_end: range.end,
definition_target_start: -1,
definition_target_end: -1,
reference_label: "",
})
}
///|
fn markdown_editor_shortcut_reference_link_bounds_in(
source : String,
chars~ : MarkdownSourceChars,
range : @core.TextRange,
open_start : Int,
) -> MarkdownLinkBounds? {
let label = match chars.slice(range.start, range.end) {
Some(slice) => String::from_array(slice)
None => return None
}
if label == "" {
return None
}
match markdown_editor_reference_definition_target_range(source, label) {
Some(target) =>
Some({
open_start,
close_start: range.end,
target_start: range.end,
target_end: range.end,
definition_target_start: target.start,
definition_target_end: target.end,
reference_label: label,
})
None =>
Some({
open_start,
close_start: range.end,
target_start: range.end,
target_end: range.end,
definition_target_start: -1,
definition_target_end: -1,
reference_label: label,
})
}
}
///|
/// Whole-source entry; see `markdown_editor_range_link_bounds`.
fn markdown_editor_range_image_bounds(
source : String,
range : @core.TextRange,
) -> MarkdownLinkBounds? {
markdown_editor_range_image_bounds_in(
source,
chars=markdown_editor_source_chars_all(source),
range,
)
}
///|
fn markdown_editor_range_image_bounds_in(
source : String,
chars~ : MarkdownSourceChars,
range : @core.TextRange,
) -> MarkdownLinkBounds? {
if range.start < 2 || range.end + 2 > chars.length() {
return None
}
let bang_start = range.start - 2
let open_start = range.start - 1
let bang_char = match chars.get(bang_start) {
Some(ch) => ch
None => return None
}
let open_char = match chars.get(open_start) {
Some(ch) => ch
None => return None
}
let close_char = match chars.get(range.end) {
Some(ch) => ch
None => return None
}
if bang_char != '!' || open_char != '[' || close_char != ']' {
return None
}
match chars.get(range.end + 1) {
Some('[') =>
return markdown_editor_reference_link_bounds_in(
source,
chars~,
range,
bang_start,
bang=true,
)
Some('(') => ()
_ => return None
}
let mut index = range.end + 2
let mut found = false
while index < chars.length() {
match chars.get(index) {
Some(')') => {
found = true
break
}
Some('\n') => break
Some(_) => index = index + 1
None => break
}
}
if found {
Some({
open_start: bang_start,
close_start: range.end,
target_start: range.end + 2,
target_end: index,
definition_target_start: -1,
definition_target_end: -1,
reference_label: "",
})
} else {
None
}
}
///|
fn markdown_editor_reference_link_bounds_in(
source : String,
chars~ : MarkdownSourceChars,
range : @core.TextRange,
open_start : Int,
bang~ : Bool,
) -> MarkdownLinkBounds? {
let label_start = range.end + 2
let mut label_end = label_start
let mut closed = false
while label_end < chars.length() {
match chars.get(label_end) {
Some(']') => {
closed = true
break
}
Some('\n') => break
Some(_) => label_end = label_end + 1
None => break
}
}
if !closed {
return None
}
let fallback_start = if bang { open_start + 2 } else { open_start + 1 }
let label = if label_end == label_start {
match chars.slice(fallback_start, range.end) {
Some(slice) => String::from_array(slice)
None => return None
}
} else {
match chars.slice(label_start, label_end) {
Some(slice) => String::from_array(slice)
None => return None
}
}
match markdown_editor_reference_definition_target_range(source, label) {
Some(target) =>
Some({
open_start,
close_start: range.end,
target_start: label_start,
target_end: label_end,
definition_target_start: target.start,
definition_target_end: target.end,
reference_label: label,
})
None =>
Some({
open_start,
close_start: range.end,
target_start: label_start,
target_end: label_end,
definition_target_start: -1,
definition_target_end: -1,
reference_label: label,
})
}
}
///|
fn markdown_editor_reference_definition_target_range(
source : String,
label : String,
) -> MarkdownSourceRange? {
let key = markdown_reference_label_key(label)
for line in markdown_editor_lines(source) {
match markdown_editor_reference_definition_line_target_range(line) {
Some((definition_label, target)) =>
if markdown_reference_label_key(definition_label) == key {
return Some(target)
}
None => ()
}
}
None
}
///|
fn markdown_editor_reference_definition_line_target_range(
line : MarkdownLine,
) -> (String, MarkdownSourceRange)? {
let chars = line.text.to_array()
let start = markdown_editor_leading_indent_length(chars)
if start >= chars.length() || chars[start] != '[' {
return None
}
let mut close = start + 1
while close < chars.length() && chars[close] != ']' {
close = close + 1
}
if close <= start + 1 ||
close + 1 >= chars.length() ||
chars[close + 1] != ':' {
return None
}
let mut target_start = close + 2
while target_start < chars.length() &&
(chars[target_start] == ' ' || chars[target_start] == '\t') {
target_start = target_start + 1
}
if target_start >= chars.length() {
return None
}
let mut target_end = target_start
if chars[target_start] == '<' {
target_start = target_start + 1
target_end = target_start
while target_end < chars.length() && chars[target_end] != '>' {
target_end = target_end + 1
}
} else {
while target_end < chars.length() &&
chars[target_end] != ' ' &&
chars[target_end] != '\t' {
target_end = target_end + 1
}
}
if target_end <= target_start {
return None
}
Some(
(
String::from_array(chars[start + 1:close]),
{ start: line.start + target_start, end: line.start + target_end, },
),
)
}
///|
fn markdown_editor_wrap_link_range(
source : String,
range : @core.TextRange,
) -> MarkdownEditorEditResult {
markdown_editor_wrap_link_range_with_target(
source, range, "https://example.com",
)
}
///|
fn markdown_editor_wrap_link_range_with_target(
source : String,
range : @core.TextRange,
target : String,
) -> MarkdownEditorEditResult {
let source_length = markdown_editor_text_length(source)
let start = markdown_editor_clamp_int(range.start, 0, source_length)
let end = markdown_editor_clamp_int(range.end, start, source_length)
let selected = markdown_editor_substring(source, start, end)
let text = markdown_editor_substring(source, 0, start) +
"[" +
selected +
"](" +
target +
")" +
markdown_editor_substring(source, end, source_length)
let next_selection = @core.TextRange::new(
start=start + 1,
end=start + 1 + end - start,
)
{
source: text,
caret: next_selection.end,
selection: Some(next_selection),
edit_range: Some({ start, end, }),
}
}
///|
fn markdown_editor_wrap_image_range(
source : String,
range : @core.TextRange,
) -> MarkdownEditorEditResult {
markdown_editor_wrap_image_range_with_target(source, range, "image.png")
}
///|
fn markdown_editor_wrap_image_range_with_target(
source : String,
range : @core.TextRange,
target : String,
) -> MarkdownEditorEditResult {
let source_length = markdown_editor_text_length(source)
let start = markdown_editor_clamp_int(range.start, 0, source_length)
let end = markdown_editor_clamp_int(range.end, start, source_length)
let selected = markdown_editor_substring(source, start, end)
let text = markdown_editor_substring(source, 0, start) +
"" +
markdown_editor_substring(source, end, source_length)
let next_selection = @core.TextRange::new(
start=start + 2,
end=start + 2 + end - start,
)
{
source: text,
caret: next_selection.end,
selection: Some(next_selection),
edit_range: Some({ start, end, }),
}
}
///|
fn markdown_editor_replace_reference_target(
source : String,
range : @core.TextRange,
bounds : MarkdownLinkBounds,
target : String,
) -> MarkdownEditorEditResult {
if bounds.definition_target_start < 0 && bounds.reference_label != "" {
let text = source +
markdown_editor_reference_definition_append_prefix(source) +
"[" +
bounds.reference_label +
"]: " +
target
let next_selection = @core.TextRange::new(start=range.start, end=range.end)
return {
source: text,
caret: next_selection.end,
selection: Some(next_selection),
// Definition append: pure insertion at the end of the previous source.
edit_range: Some({
start: markdown_editor_text_length(source),
end: markdown_editor_text_length(source),
}),
}
}
let target_start = if bounds.definition_target_start >= 0 {
bounds.definition_target_start
} else {
bounds.target_start
}
let target_end = if bounds.definition_target_end >= 0 {
bounds.definition_target_end
} else {
bounds.target_end
}
let source_length = markdown_editor_text_length(source)
let text = markdown_editor_substring(source, 0, target_start) +
target +
markdown_editor_substring(source, target_end, source_length)
let next_selection = @core.TextRange::new(start=range.start, end=range.end)
{
source: text,
caret: next_selection.end,
selection: Some(next_selection),
edit_range: Some({ start: target_start, end: target_end, }),
}
}
///|
fn markdown_editor_reference_definition_append_prefix(
source : String,
) -> String {
if source == "" {
""
} else if source.has_suffix("\n\n") {
""
} else if source.has_suffix("\n") {
"\n"
} else {
"\n\n"
}
}
///|
fn markdown_editor_footnote_label_at_selection(
source : String,
selection : MarkdownEditorSelection,
) -> String? {
let anchor = match markdown_editor_active_selection(selection.selection) {
Some(range) => range.start
None => selection.caret
}
let anchor = markdown_editor_clamp_int(
anchor,
0,
markdown_editor_text_length(source),
)
match markdown_editor_footnote_reference_label_at(source, anchor) {
Some(label) => Some(label)
None => markdown_editor_footnote_definition_label_at(source, anchor)
}
}
///|
fn markdown_editor_footnote_reference_label_at(
source : String,
anchor : Int,
) -> String? {
let chars = source.to_array()
if chars.length() == 0 {
return None
}
let mut open = markdown_editor_clamp_int(anchor, 0, chars.length() - 1)
while open > 0 && chars[open] != '[' {
open = open - 1
}
if open + 3 >= chars.length() || chars[open] != '[' || chars[open + 1] != '^' {
return None
}
let mut close = open + 2
while close < chars.length() && chars[close] != ']' && chars[close] != '\n' {
close = close + 1
}
if close >= chars.length() || chars[close] != ']' {
return None
}
if anchor < open + 2 || anchor > close {
return None
}
Some(String::from_array(chars[open + 2:close]))
}
///|
fn markdown_editor_footnote_definition_label_at(
source : String,
anchor : Int,
) -> String? {
for line in markdown_editor_lines(source) {
if anchor >= line.start && anchor <= line.end {
match markdown_editor_footnote_definition_line_text_range(line) {
Some((label, _)) => return Some(label)
None => return None
}
}
}
None
}
///|
fn markdown_editor_footnote_definition_text(
source : String,
label : String,
) -> String? {
let key = markdown_reference_label_key(label)
let chars = source.to_array()
for line in markdown_editor_lines(source) {
match markdown_editor_footnote_definition_line_text_range(line) {
Some((definition_label, range)) =>
if markdown_reference_label_key(definition_label) == key {
return Some(String::from_array(chars[range.start:range.end]))
}
None => ()
}
}
None
}
///|
fn markdown_editor_footnote_definition_line_text_range(
line : MarkdownLine,
) -> (String, MarkdownSourceRange)? {
let chars = line.text.to_array()
let start = markdown_editor_leading_indent_length(chars)
if start + 4 >= chars.length() ||
chars[start] != '[' ||
chars[start + 1] != '^' {
return None
}
let mut close = start + 2
while close < chars.length() && chars[close] != ']' {
close = close + 1
}
if close <= start + 2 ||
close + 1 >= chars.length() ||
chars[close + 1] != ':' {
return None
}
let mut text_start = close + 2
while text_start < chars.length() &&
(chars[text_start] == ' ' || chars[text_start] == '\t') {
text_start = text_start + 1
}
Some(
(
String::from_array(chars[start + 2:close]),
{ start: line.start + text_start, end: line.end, },
),
)
}
///|
fn markdown_editor_set_footnote_text(
source : String,
selection : MarkdownEditorSelection,
text : String,
) -> MarkdownEditorEditResult {
match markdown_editor_footnote_label_at_selection(source, selection) {
Some(label) =>
match
markdown_editor_replace_footnote_definition_text(source, label, text) {
Some(result) => result
None =>
markdown_editor_append_footnote_definition(
source, label, text, selection,
)
}
None => markdown_editor_append_snippet(source, SetFootnoteText(text))
}
}
///|
fn markdown_editor_replace_footnote_definition_text(
source : String,
label : String,
text : String,
) -> MarkdownEditorEditResult? {
let key = markdown_reference_label_key(label)
let chars = source.to_array()
for line in markdown_editor_lines(source) {
match markdown_editor_footnote_definition_line_text_range(line) {
Some((definition_label, range)) =>
if markdown_reference_label_key(definition_label) == key {
let next_source = String::from_array(chars[:range.start]) +
text +
String::from_array(chars[range.end:])
return Some({
source: next_source,
caret: range.start + markdown_editor_text_length(text),
selection: None,
edit_range: Some({ start: range.start, end: range.end, }),
})
}
None => ()
}
}
None
}
///|
fn markdown_editor_append_footnote_definition(
source : String,
label : String,
text : String,
selection : MarkdownEditorSelection,
) -> MarkdownEditorEditResult {
let next_source = source +
markdown_editor_reference_definition_append_prefix(source) +
"[^" +
label +
"]: " +
text
{
source: next_source,
caret: selection.caret,
selection: selection.selection,
// Definition append: pure insertion at the end of the previous source.
edit_range: Some({
start: markdown_editor_text_length(source),
end: markdown_editor_text_length(source),
}),
}
}
///|
fn markdown_editor_reference_target(
source : String,
bounds : MarkdownLinkBounds,
) -> String? {
let start = if bounds.definition_target_start >= 0 {
bounds.definition_target_start
} else {
bounds.target_start
}
let end = if bounds.definition_target_end >= 0 {
bounds.definition_target_end
} else {
bounds.target_end
}
let source_length = markdown_editor_text_length(source)
if start > end || end > source_length {
None
} else {
Some(markdown_editor_substring(source, start, end))
}
}
///|
fn markdown_editor_unwrap_link_range(
source : String,
range : @core.TextRange,
bounds : MarkdownLinkBounds,
) -> MarkdownEditorEditResult {
let source_length = markdown_editor_text_length(source)
let text = markdown_editor_substring(source, 0, bounds.open_start) +
markdown_editor_substring(source, range.start, range.end) +
markdown_editor_substring(source, bounds.target_end + 1, source_length)
let next_selection = @core.TextRange::new(
start=bounds.open_start,
end=bounds.open_start + range.end - range.start,
)
{
source: text,
caret: next_selection.end,
selection: Some(next_selection),
edit_range: Some({ start: bounds.open_start, end: bounds.target_end + 1, }),
}
}
///|
fn markdown_editor_unwrap_image_range(
source : String,
range : @core.TextRange,
bounds : MarkdownLinkBounds,
) -> MarkdownEditorEditResult {
let source_length = markdown_editor_text_length(source)
let text = markdown_editor_substring(source, 0, bounds.open_start) +
markdown_editor_substring(source, range.start, range.end) +
markdown_editor_substring(source, bounds.target_end + 1, source_length)
let next_selection = @core.TextRange::new(
start=bounds.open_start,
end=bounds.open_start + range.end - range.start,
)
{
source: text,
caret: next_selection.end,
selection: Some(next_selection),
edit_range: Some({ start: bounds.open_start, end: bounds.target_end + 1, }),
}
}
///|
fn markdown_editor_selected_line_range(
source : String,
selection : MarkdownEditorSelection,
) -> MarkdownSourceRange {
let chars = source.to_array()
match markdown_editor_active_selection(selection.selection) {
Some(active) => {
let start_anchor = markdown_editor_clamp_int(
active.start,
0,
chars.length(),
)
let mut end_anchor = markdown_editor_clamp_int(
active.end,
start_anchor,
chars.length(),
)
if end_anchor > start_anchor && chars[end_anchor - 1] == '\n' {
end_anchor = end_anchor - 1
}
markdown_editor_line_range_around(chars, start_anchor, end_anchor)
}
None => markdown_editor_current_line_range(source, selection)
}
}
///|
fn markdown_editor_line_range_around(
chars : Array[Char],
start_anchor : Int,
end_anchor : Int,
) -> MarkdownSourceRange {
let mut start = start_anchor
while start > 0 && chars[start - 1] != '\n' {
start = start - 1
}
let mut end = end_anchor
while end < chars.length() && chars[end] != '\n' {
end = end + 1
}
{ start, end, }
}
///|
fn markdown_editor_current_line_range(
source : String,
selection : MarkdownEditorSelection,
) -> MarkdownSourceRange {
let chars = source.to_array()
markdown_editor_current_line_range_from_chars(chars, selection)
}
///|
fn markdown_editor_current_line_range_from_chars(
chars : Array[Char],
selection : MarkdownEditorSelection,
) -> MarkdownSourceRange {
let active = markdown_editor_active_selection(selection.selection)
let caret = match active {
Some(range) => range.start
None => selection.caret
}
let caret = markdown_editor_clamp_int(caret, 0, chars.length())
let mut start = caret
while start > 0 && chars[start - 1] != '\n' {
start = start - 1
}
let mut end = caret
while end < chars.length() && chars[end] != '\n' {
end = end + 1
}
{ start, end, }
}
///|
pub fn markdown_editor_active_selection(
selection : @core.TextRange?,
) -> @core.TextRange? {
match selection {
Some(selection) => {
let normalized = selection.normalized()
if normalized.is_collapsed() {
None
} else {
Some(normalized)
}
}
None => None
}
}
///|