practicode 0.2.5

Offline-first programming language lessons and coding practice in a Rust terminal UI.
Documentation
use unicode_width::UnicodeWidthStr;

pub fn render_markdown_plain(markdown: &str) -> String {
    let mut out = Vec::new();
    let mut in_fence = false;
    for line in markdown.lines() {
        if line.trim_start().starts_with("```") {
            in_fence = !in_fence;
            continue;
        }
        if in_fence {
            out.push(format!("  {line}"));
            continue;
        }
        let trimmed = line.trim_start();
        if trimmed.starts_with('#') {
            out.push(trimmed.trim_start_matches('#').trim_start().to_string());
        } else {
            out.push(line.replace('`', ""));
        }
    }
    out.join("\n").trim_end().to_string()
}

pub fn byte_index(value: &str, char_index: usize) -> usize {
    value
        .char_indices()
        .nth(char_index)
        .map(|(index, _)| index)
        .unwrap_or(value.len())
}

pub fn char_len(value: &str) -> usize {
    value.chars().count()
}

pub fn prefix(value: &str, char_index: usize) -> String {
    value.chars().take(char_index).collect()
}

pub fn display_width(value: &str) -> usize {
    UnicodeWidthStr::width(value)
}

const CHO: &[char] = &[
    '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
    '', '', '',
];
const JUNG: &[char] = &[
    '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
    '', '', '', '', '',
];
const JONG: &[char] = &[
    '\0', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
    '', '', '', '', '', '', '', '', '', '', '', '',
];

pub fn compose_hangul_jamo(value: &str) -> String {
    let mut out = String::new();
    let mut run = Vec::new();
    for char in decompose_hangul(value).chars() {
        if is_hangul_jamo(char) {
            run.push(char);
        } else {
            if !run.is_empty() {
                out.push_str(&compose_hangul_run(&run));
                run.clear();
            }
            out.push(char);
        }
    }
    if !run.is_empty() {
        out.push_str(&compose_hangul_run(&run));
    }
    out
}

fn decompose_hangul(value: &str) -> String {
    let mut chars = String::new();
    for char in value.chars() {
        let code = char as u32;
        if (0xAC00..=0xD7A3).contains(&code) {
            let offset = code - 0xAC00;
            let lead = (offset / 588) as usize;
            let vowel = ((offset % 588) / 28) as usize;
            let tail = (offset % 28) as usize;
            chars.push(CHO[lead]);
            chars.push(JUNG[vowel]);
            if tail != 0 {
                chars.push(JONG[tail]);
            }
        } else {
            chars.push(char);
        }
    }
    chars
}

fn compose_hangul_run(chars: &[char]) -> String {
    let mut out = String::new();
    let mut lead = None;
    let mut vowel = None;
    let mut tail = None;

    fn emit(
        out: &mut String,
        lead: &mut Option<char>,
        vowel: &mut Option<char>,
        tail: &mut Option<char>,
    ) {
        match (*lead, *vowel) {
            (Some(l), Some(v)) => {
                if let (Some(l_index), Some(v_index)) = (cho_index(l), jung_index(v)) {
                    let code = 0xAC00
                        + ((l_index * 21 + v_index) * 28 + tail.and_then(jong_index).unwrap_or(0))
                            as u32;
                    if let Some(char) = char::from_u32(code) {
                        out.push(char);
                    }
                } else {
                    for part in [*lead, *vowel, *tail].into_iter().flatten() {
                        out.push(part);
                    }
                }
            }
            _ => {
                for part in [*lead, *vowel, *tail].into_iter().flatten() {
                    out.push(part);
                }
            }
        }
        *lead = None;
        *vowel = None;
        *tail = None;
    }

    for &char in chars {
        if jung_index(char).is_some() {
            if lead.is_none() {
                out.push(char);
            } else if vowel.is_none() {
                vowel = Some(char);
            } else if tail.is_none() {
                if let Some(combined) = combine_jung(vowel.unwrap(), char) {
                    vowel = Some(combined);
                } else {
                    emit(&mut out, &mut lead, &mut vowel, &mut tail);
                    out.push(char);
                }
            } else if let Some((first, second)) = split_jong(tail.unwrap()) {
                tail = Some(first);
                emit(&mut out, &mut lead, &mut vowel, &mut tail);
                lead = Some(second);
                vowel = Some(char);
            } else {
                let next_lead = tail;
                tail = None;
                emit(&mut out, &mut lead, &mut vowel, &mut tail);
                lead = next_lead;
                vowel = Some(char);
            }
        } else if lead.is_none() {
            lead = Some(char);
        } else if vowel.is_none() {
            if let Some(combined) = combine_cho(lead.unwrap(), char) {
                lead = Some(combined);
            } else {
                emit(&mut out, &mut lead, &mut vowel, &mut tail);
                lead = Some(char);
            }
        } else if tail.is_none() && jong_index(char).is_some() {
            tail = Some(char);
        } else if tail.is_some() {
            if let Some(combined) = combine_jong(tail.unwrap(), char) {
                tail = Some(combined);
            } else {
                emit(&mut out, &mut lead, &mut vowel, &mut tail);
                lead = Some(char);
            }
        } else {
            emit(&mut out, &mut lead, &mut vowel, &mut tail);
            lead = Some(char);
        }
    }
    emit(&mut out, &mut lead, &mut vowel, &mut tail);
    out
}

fn is_hangul_jamo(char: char) -> bool {
    cho_index(char).is_some() || jung_index(char).is_some() || jong_index(char).is_some()
}

fn cho_index(char: char) -> Option<usize> {
    CHO.iter().position(|value| *value == char)
}

fn jung_index(char: char) -> Option<usize> {
    JUNG.iter().position(|value| *value == char)
}

fn jong_index(char: char) -> Option<usize> {
    JONG.iter()
        .position(|value| *value == char)
        .filter(|index| *index != 0)
}

fn combine_cho(a: char, b: char) -> Option<char> {
    match (a, b) {
        ('', '') => Some(''),
        ('', '') => Some(''),
        ('', '') => Some(''),
        ('', '') => Some(''),
        ('', '') => Some(''),
        _ => None,
    }
}

fn combine_jung(a: char, b: char) -> Option<char> {
    match (a, b) {
        ('', '') => Some(''),
        ('', '') => Some(''),
        ('', '') => Some(''),
        ('', '') => Some(''),
        ('', '') => Some(''),
        ('', '') => Some(''),
        ('', '') => Some(''),
        _ => None,
    }
}

fn combine_jong(a: char, b: char) -> Option<char> {
    match (a, b) {
        ('', '') => Some(''),
        ('', '') => Some(''),
        ('', '') => Some(''),
        ('', '') => Some(''),
        ('', '') => Some(''),
        ('', '') => Some(''),
        ('', '') => Some(''),
        ('', '') => Some(''),
        ('', '') => Some(''),
        ('', '') => Some(''),
        ('', '') => Some(''),
        _ => None,
    }
}

fn split_jong(char: char) -> Option<(char, char)> {
    match char {
        '' => Some(('', '')),
        '' => Some(('', '')),
        '' => Some(('', '')),
        '' => Some(('', '')),
        '' => Some(('', '')),
        '' => Some(('', '')),
        '' => Some(('', '')),
        '' => Some(('', '')),
        '' => Some(('', '')),
        '' => Some(('', '')),
        '' => Some(('', '')),
        _ => None,
    }
}