♻️ Make playground parse README.md bulk-selectable and copyable using Rust for better performance and showcase

Rust was chosen for its speed, memory safety, and ability to handle large text efficiently.
This change improves UX by enabling bulk selection and copying of README content,
while also serving as a demonstration of Rust integration in the playground.
This commit is contained in:
yogithesymbian 2025-12-26 11:45:32 +08:00
parent 3c461b0923
commit 6406b47c9b
5 changed files with 1279 additions and 0 deletions

25
playground/rust/.gitignore vendored Normal file
View file

@ -0,0 +1,25 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating libraries
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# IntelliJ project files
.idea/
*.iml
# VS Code settings
.vscode/
# Byebug
.byebug_history
# MacOS files
.DS_Store
# Node modules (if using node for frontend)
node_modules/

View file

@ -0,0 +1,13 @@
[package]
name = "md_badges_parser"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
pulldown-cmark = "0.9"
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6"

145
playground/rust/src/lib.rs Normal file
View file

@ -0,0 +1,145 @@
use pulldown_cmark::{Event, HeadingLevel, Parser, Tag, Options};
use wasm_bindgen::prelude::*;
use serde::Serialize;
#[derive(Serialize)]
pub struct BadgeItem {
pub preview_md: String,
pub code_md: String,
pub preview_url_md: String,
}
#[derive(Serialize)]
pub struct Section {
pub title: String,
pub items: Vec<BadgeItem>,
}
#[derive(Serialize)]
pub struct ParseResult {
pub sections: Vec<Section>,
}
fn is_separator_row(row: &[String]) -> bool {
row.iter().all(|cell| {
cell.chars().all(|c| c == '-' || c == ':' || c == ' ')
})
}
fn extract_image_url(md: &str) -> Option<String> {
let start = md.find("](")? + 2;
let end = md[start..].find(')')? + start;
Some(md[start..end].to_string())
}
#[wasm_bindgen]
pub fn parse_readme(_markdown: &str) -> JsValue {
let mut options = Options::empty();
options.insert(Options::ENABLE_TABLES);
let parser = Parser::new_ext(_markdown, options);
let mut sections: Vec<Section> = Vec::new();
let mut current_section: Option<Section> = None;
let mut in_heading = false;
let mut heading_text = String::new();
let mut heading_level: Option<HeadingLevel> = None;
let mut in_cell = false;
let mut is_target_table = false;
let mut current_row: Vec<String> = Vec::new();
let mut current_cell = String::new();
let mut header_cells: Vec<String> = Vec::new();
let mut reading_header = false;
for event in parser {
match event {
Event::Start(Tag::Heading(level, ..)) => {
heading_text.clear();
heading_level = Some(level);
in_heading = true;
}
Event::Text(text) if in_heading => {
heading_text.push_str(&text);
}
Event::End(Tag::Heading(_, ..)) => {
in_heading = false;
if heading_level == Some(HeadingLevel::H3)
&& heading_text.trim() != "Table Of Contents" {
if let Some(section) = current_section.take() {
sections.push(section);
}
current_section = Some(Section {
title: heading_text.trim().to_string(),
items: Vec::new(),
})
}
}
Event::Start(Tag::Table(_)) => {
header_cells.clear();
reading_header = true;
is_target_table = false;
}
Event::End(Tag::Table(_)) => {
is_target_table = false;
}
Event::Start(Tag::TableRow) => {
current_row.clear();
}
Event::Start(Tag::TableCell) => {
current_cell.clear();
in_cell = true;
}
Event::Text(text) if in_cell => {
current_cell.push_str(&text);
}
Event::Code(code) if in_cell => {
current_cell.push_str(&code);
}
Event::End(Tag::TableCell) => {
in_cell = false;
current_row.push(current_cell.trim().to_string());
}
Event::End(Tag::TableRow) => {
if is_separator_row(&current_row) {
continue;
}
if reading_header {
header_cells = current_row.clone();
reading_header = false;
is_target_table = header_cells.len() == 2;
}
if is_target_table {
if let Some(section) = current_section.as_mut() {
if current_row.len() == 2 {
section.items.push(BadgeItem {
preview_md: current_row[0].clone(),
code_md: current_row[1].clone(),
preview_url_md: extract_image_url(&current_row[1]).unwrap(),
});
}
}
}
}
_ => {}
}
}
if let Some(section) = current_section {
sections.push(section);
}
let result = ParseResult {
sections,
};
serde_wasm_bindgen::to_value(&result).unwrap()
}

View file

@ -0,0 +1,23 @@
use std::fs;
use pulldown_cmark::{Parser, Event, Tag, Options};
fn main() {
let md = fs::read_to_string("./test/README.md")
.expect("Failed to read README.md");
let mut options = Options::empty();
options.insert(Options::ENABLE_TABLES);
let parser = Parser::new_ext(&md, options);
for event in parser {
match &event {
Event::Start(Tag::Table(_)) => println!("== TABLE START =="),
Event::Start(Tag::TableRow) => println!("ROW START"),
Event::Start(Tag::TableCell) => println!("CELL START"),
Event::Code(code) => println!("CODE: {:?}", code),
Event::Text(t) => println!("TEXT: {:?}", t),
_ => {}
}
}
}

File diff suppressed because it is too large Load diff