mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-11-24 21:19:37 +00:00
feat: reimplement typlite by html export (#1684)
* dev: init markdown file * dev: typlite element derive * feat: pass tests refactor lib.rs to separated files (#1692) feat(typlite): Docx export and export markdown in cmark-writer (#1698) * feat: docx export support * refactor: simplify DocxConverter structure and improve content handling * tests: add binary insta for docx * feat: add MathBlock style and improve frame rendering in DocxConverter * fix: enhance paragraph creation(silly method) * fix: enhance math equation rendering * use md5 instead of docx binary * feat: enhance list numbering and paragraph handling in DocxConverter * feat: add all_elements test * refactor * reimpl md export in cmark-writer * feat: add support for highlight tag in MarkdownConverter * feat: refactor LaTeXConverter to improve element processing and add new methods * fmt * Refactor DOCX converter to improve list handling and document structure - Introduced separate methods for creating ordered and unordered list numbering. - Enhanced list management by tracking next numbering IDs. - Consolidated paragraph and run management within the DocxConverter. - Improved image processing with better error handling and placeholder support. - Streamlined the handling of various HTML elements, including headings, lists, and images. - Added functionality for processing captions and preformatted blocks. - Updated methods for processing inline styles and links. * feat: update cmark-writer to version 0.2.0 * feat: refactor code block handling in DOCX converter for improved readability * refactor: refactor DOCX converter to enhance document structure * refactor docx to separated files * update instas * fmt * chore: update cmark-writer version to 0.3.0 * fix: ol custom value * feat: table and grid processing * use cmark-writer's ast node for consistency * fix: update snapshot hashes for document generation tests * fix: add preamble * update snapshot hashes * refactor DOCX conversion: Split writer functionality into separate module, enhance image processing, and clean up utility functions * update comments in LaTeX and Markdown converters for clarity and consistency * fmt * delete utils * feat: support figure node by custom node in cmark-writer * fix * fix: frame * feat: enhance table conversion logic in MarkdownConverter * refactor: simplify FigureNode implementation by removing CustomNode trait * chore: update cmark-writer to version 0.5.0 * fix: update figure and raw inline snapshots for consistency * fix: update snapshot hashes and correct caption reference in markdown.typ * refactor proj structure * feat: update CompileArgs to support multiple output files and remove debug option * docs: update README to clarify usage of multiple output formats and comment out feature section * remove DocxConverter module * feat: impl assets-path feature and add ExternalFrameNode for handling external frames and update writers to support it * feat: enhance HTML element conversion to include attributes and children handling * feat: update cmark-writer to 0.6.1 and refactor related code * fix: update snapshots for figure caption, list, outline, and docx generation * feat: refactor HTML element conversion to use create_html_element method and enhance table processing * fix * feat: add HighlightNode for highlighted text and integrate with HTML to AST parser and LaTeX writer * refactoor * update tests Co-Authored-By: Hong Jiarong <me@jrhim.com> * feat: revert latex/docx conversions * fix: warnings * bad: convert docs * build: remove other cargo deps * build: update cargo.lock * test: update snapshot * chore: remove useless parser trait * feat: annotate v1 * feat: annotate v2 * test: update snapshot * question: is it a bug? * test: update bad snapshot --------- Co-authored-by: Hong Jiarong <me@jrhim.com>
This commit is contained in:
parent
170dd7b948
commit
f35d1056ff
58 changed files with 2755 additions and 950 deletions
23
Cargo.lock
generated
23
Cargo.lock
generated
|
|
@ -579,6 +579,26 @@ dependencies = [
|
||||||
"roff",
|
"roff",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cmark-writer"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "79ac3be73d18979a75f69c6f73bae74d2ff9fa61e9c3539732040429ad2659b8"
|
||||||
|
dependencies = [
|
||||||
|
"cmark-writer-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cmark-writer-macros"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23ad8476b03e933c3ff38eefa7c96a9c155e4402621918852f27cf67cb637b99"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.100",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cobs"
|
name = "cobs"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
|
|
@ -4743,15 +4763,18 @@ version = "0.13.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"clap",
|
"clap",
|
||||||
|
"cmark-writer",
|
||||||
"comemo",
|
"comemo",
|
||||||
"ecow",
|
"ecow",
|
||||||
"insta",
|
"insta",
|
||||||
"regex",
|
"regex",
|
||||||
"tinymist-analysis",
|
"tinymist-analysis",
|
||||||
|
"tinymist-derive",
|
||||||
"tinymist-project",
|
"tinymist-project",
|
||||||
"tinymist-std",
|
"tinymist-std",
|
||||||
"tinymist-tests",
|
"tinymist-tests",
|
||||||
"typst",
|
"typst",
|
||||||
|
"typst-html",
|
||||||
"typst-svg",
|
"typst-svg",
|
||||||
"typst-syntax",
|
"typst-syntax",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,11 @@ pub fn identify_pat_docs(converted: &str) -> StrResult<TidyPatDocs> {
|
||||||
loop {
|
loop {
|
||||||
if matching_return_ty {
|
if matching_return_ty {
|
||||||
matching_return_ty = false;
|
matching_return_ty = false;
|
||||||
let Some(w) = line.trim_start().strip_prefix("->") else {
|
let line = line.trim_start();
|
||||||
|
let type_line = line
|
||||||
|
.strip_prefix("-\\>")
|
||||||
|
.or_else(|| line.strip_prefix("->"));
|
||||||
|
let Some(w) = type_line else {
|
||||||
// break_line = Some(i);
|
// break_line = Some(i);
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
@ -204,7 +208,7 @@ mod tests {
|
||||||
- <!-- typlite:begin:list-item 0 -->`types` (optional): A list of accepted argument types.<!-- typlite:end:list-item 0 -->
|
- <!-- typlite:begin:list-item 0 -->`types` (optional): A list of accepted argument types.<!-- typlite:end:list-item 0 -->
|
||||||
- <!-- typlite:begin:list-item 0 -->`default` (optional): Default value for this argument.<!-- typlite:end:list-item 0 -->
|
- <!-- typlite:begin:list-item 0 -->`default` (optional): Default value for this argument.<!-- typlite:end:list-item 0 -->
|
||||||
|
|
||||||
See @@show-module() for outputting the results of this function.
|
See show-module() for outputting the results of this function.
|
||||||
|
|
||||||
- <!-- typlite:begin:list-item 0 -->content (string): Content of `.typ` file to analyze for docstrings.<!-- typlite:end:list-item 0 -->
|
- <!-- typlite:begin:list-item 0 -->content (string): Content of `.typ` file to analyze for docstrings.<!-- typlite:end:list-item 0 -->
|
||||||
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
|
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
|
||||||
|
|
@ -223,7 +227,7 @@ See @@show-module() for outputting the results of this function.
|
||||||
- <!-- typlite:begin:list-item 0 -->`types` (optional): A list of accepted argument types.<!-- typlite:end:list-item 0 -->
|
- <!-- typlite:begin:list-item 0 -->`types` (optional): A list of accepted argument types.<!-- typlite:end:list-item 0 -->
|
||||||
- <!-- typlite:begin:list-item 0 -->`default` (optional): Default value for this argument.<!-- typlite:end:list-item 0 -->
|
- <!-- typlite:begin:list-item 0 -->`default` (optional): Default value for this argument.<!-- typlite:end:list-item 0 -->
|
||||||
|
|
||||||
See @@show-module() for outputting the results of this function.
|
See show-module() for outputting the results of this function.
|
||||||
<< docs
|
<< docs
|
||||||
>>return
|
>>return
|
||||||
string
|
string
|
||||||
|
|
@ -258,7 +262,7 @@ See @@show-module() for outputting the results of this function.
|
||||||
insta::assert_snapshot!(func(r###"These again are dictionaries with the keys
|
insta::assert_snapshot!(func(r###"These again are dictionaries with the keys
|
||||||
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
|
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
|
||||||
|
|
||||||
See @@show-module() for outputting the results of this function.
|
See show-module() for outputting the results of this function.
|
||||||
|
|
||||||
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
|
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
|
||||||
- <!-- typlite:begin:list-item 0 -->label-prefix (auto, string): The label-prefix for internal function
|
- <!-- typlite:begin:list-item 0 -->label-prefix (auto, string): The label-prefix for internal function
|
||||||
|
|
@ -270,7 +274,7 @@ See @@show-module() for outputting the results of this function.
|
||||||
These again are dictionaries with the keys
|
These again are dictionaries with the keys
|
||||||
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
|
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
|
||||||
|
|
||||||
See @@show-module() for outputting the results of this function.
|
See show-module() for outputting the results of this function.
|
||||||
<< docs
|
<< docs
|
||||||
>>return
|
>>return
|
||||||
string
|
string
|
||||||
|
|
@ -289,10 +293,10 @@ See @@show-module() for outputting the results of this function.
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_identify_tidy_docs3() {
|
fn test_identify_tidy_docs3() {
|
||||||
insta::assert_snapshot!(var(r###"See @@show-module() for outputting the results of this function.
|
insta::assert_snapshot!(var(r###"See show-module() for outputting the results of this function.
|
||||||
-> string"###), @r"
|
-> string"###), @r"
|
||||||
>> docs:
|
>> docs:
|
||||||
See @@show-module() for outputting the results of this function.
|
See show-module() for outputting the results of this function.
|
||||||
<< docs
|
<< docs
|
||||||
>>return
|
>>return
|
||||||
string
|
string
|
||||||
|
|
|
||||||
|
|
@ -96,3 +96,57 @@ pub fn gen_decl_enum(input: TokenStream) -> TokenStream {
|
||||||
|
|
||||||
TokenStream::from(expanded)
|
TokenStream::from(expanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[proc_macro_derive(TypliteAttr)]
|
||||||
|
pub fn gen_typlite_element(input: TokenStream) -> TokenStream {
|
||||||
|
// Parse the input tokens into a syntax tree
|
||||||
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
|
||||||
|
// extract the fields from the struct
|
||||||
|
let field_parsers = match &input.data {
|
||||||
|
syn::Data::Struct(data) => match &data.fields {
|
||||||
|
syn::Fields::Named(fields) => fields
|
||||||
|
.named
|
||||||
|
.iter()
|
||||||
|
.map(|f| {
|
||||||
|
let name = f.ident.as_ref().unwrap();
|
||||||
|
|
||||||
|
let ty = &f.ty;
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
md_attr::#name => {
|
||||||
|
let value = <#ty>::parse_attr(content)?;
|
||||||
|
result.#name = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
syn::Fields::Unnamed(_) => panic!("unnamed fields are not supported"),
|
||||||
|
syn::Fields::Unit => panic!("unit structs are not supported"),
|
||||||
|
},
|
||||||
|
_ => panic!("only structs are supported"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let input_name = &input.ident;
|
||||||
|
|
||||||
|
// generate parse trait
|
||||||
|
let expanded = quote! {
|
||||||
|
impl TypliteAttrsParser for #input_name {
|
||||||
|
fn parse(attrs: &HtmlAttrs) -> Result<Self> {
|
||||||
|
let mut result = Self::default();
|
||||||
|
for (name, content) in attrs.0.iter() {
|
||||||
|
match *name {
|
||||||
|
#(#field_parsers)*
|
||||||
|
_ => {
|
||||||
|
return Err(format!("unknown attribute: {name}").into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TokenStream::from(expanded)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
source: crates/tinymist-query/src/analysis.rs
|
source: crates/tinymist-query/src/analysis.rs
|
||||||
expression: "snap.join(\"\\n\")"
|
expression: "snap.join(\"\\n\")"
|
||||||
input_file: crates/tinymist-query/src/fixtures/docs/blocky2.typ
|
input_file: crates/tinymist-query/src/fixtures/docs/blocky2.typ
|
||||||
snapshot_kind: text
|
|
||||||
---
|
---
|
||||||
= docstings
|
= docstings
|
||||||
Pattern(..)@41..42 in /s0.typ -> DocString { docs: Some("This is X\nNote: This is not Y"), var_bounds: {}, vars: {}, res_ty: None }
|
Pattern(..)@41..42 in /s0.typ -> DocString { docs: Some("This is X Note: This is not Y"), var_bounds: {}, vars: {}, res_ty: None }
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
source: crates/tinymist-query/src/analysis.rs
|
source: crates/tinymist-query/src/analysis.rs
|
||||||
expression: "snap.join(\"\\n\")"
|
expression: "snap.join(\"\\n\")"
|
||||||
input_file: crates/tinymist-query/src/fixtures/docs/multiple_line.typ
|
input_file: crates/tinymist-query/src/fixtures/docs/multiple_line.typ
|
||||||
snapshot_kind: text
|
|
||||||
---
|
---
|
||||||
= docstings
|
= docstings
|
||||||
Pattern(..)@45..46 in /s0.typ -> DocString { docs: Some("This is X.\nNote: this is not Y."), var_bounds: {}, vars: {}, res_ty: None }
|
Pattern(..)@45..46 in /s0.typ -> DocString { docs: Some("This is X. Note: this is not Y."), var_bounds: {}, vars: {}, res_ty: None }
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,8 @@
|
||||||
source: crates/tinymist-query/src/hover.rs
|
source: crates/tinymist-query/src/hover.rs
|
||||||
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
|
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
|
||||||
input_file: crates/tinymist-query/src/fixtures/hover/annotate_dict_param.typ
|
input_file: crates/tinymist-query/src/fixtures/hover/annotate_dict_param.typ
|
||||||
snapshot_kind: text
|
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"contents": "```typc\nlet show-example(\n ..options: arguments,\n inherited-scope: dictionary = (:),\n) = none;\n```\n\n---\n\n- <!-- typlite:begin:list-item 0 -->inherited-scope (dictionary): Definitions that are made available to the entire parsed\n module. This parameter is only used internally.<!-- typlite:end:list-item 0 -->\n\n# Rest Parameters\n\n## options\n\n```typc\ntype: arguments\n```\n\n\n\n# Named Parameters\n\n## inherited-scope\n\n```typc\ntype: dictionary\n```\n\nDefinitions that are made available to the entire parsed\n module. This parameter is only used internally.",
|
"contents": "```typc\nlet show-example(\n ..options: arguments,\n inherited-scope: dictionary = (:),\n) = none;\n```\n\n---\n\n- <!-- typlite:begin:list-item 0 -->inherited-scope (dictionary): Definitions that are made available to the entire parsed module. This parameter is only used internally.<!-- typlite:end:list-item 0 -->\n\n# Rest Parameters\n\n## options\n\n```typc\ntype: arguments\n```\n\n\n\n# Named Parameters\n\n## inherited-scope\n\n```typc\ntype: dictionary\n```\n\nDefinitions that are made available to the entire parsed module. This parameter is only used internally.",
|
||||||
"range": "7:20:7:32"
|
"range": "7:20:7:32"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,8 @@
|
||||||
source: crates/tinymist-query/src/hover.rs
|
source: crates/tinymist-query/src/hover.rs
|
||||||
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
|
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
|
||||||
input_file: crates/tinymist-query/src/fixtures/hover/annotate_dict_param2.typ
|
input_file: crates/tinymist-query/src/fixtures/hover/annotate_dict_param2.typ
|
||||||
snapshot_kind: text
|
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"contents": "```typc\nlet inherited-scope = dictionary;\n```\n\n---\n\nDefinitions that are made available to the entire parsed\n module. This parameter is only used internally.",
|
"contents": "```typc\nlet inherited-scope = dictionary;\n```\n\n---\n\nDefinitions that are made available to the entire parsed module. This parameter is only used internally.",
|
||||||
"range": "6:21:6:36"
|
"range": "6:21:6:36"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,6 @@ expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
|
||||||
input_file: crates/tinymist-query/src/fixtures/hover/annotate_docs_error.typ
|
input_file: crates/tinymist-query/src/fixtures/hover/annotate_docs_error.typ
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"contents": "```typc\nlet speaker-note(\n note: any,\n mode: str = \"typ\",\n setting: (any) => any = Closure(..),\n) = none;\n```\n\n---\n\nSpeaker notes are a way to add additional information to your slides that is not visible to the audience. This can be useful for providing additional context or reminders to yourself.\n\n## Example\n\n```typ\n#speaker-note[This is a speaker note]\n\n```\n```\nRender Error\ncompiling node: error: unknown variable: speaker-note at \"/__render__.typ\":214..226\nHint: if you meant to use subtraction, try adding spaces around the minus sign: \\`speaker - note\\`\n\n```\n\n# Positional Parameters\n\n## note\n\n```typc\ntype: \n```\n\n\n\n# Named Parameters\n\n## mode\n\n```typc\ntype: \"typ\"\n```\n\n\n\n## setting (named)\n\n```typc\ntype: (any) => any\n```\n\n",
|
"contents": "```typc\nlet speaker-note(\n note: any,\n mode: str = \"typ\",\n setting: (any) => any = Closure(..),\n) = none;\n```\n\n---\n\n```\nfailed to parse docs: failed to convert to markdown: convert source for main file: [SourceDiagnostic { severity: Error, span: Span(..), message: \"unknown variable: example\", trace: [Import], hints: [] }]\n```\n\n````typ\nSpeaker notes are a way to add additional information to your slides that is not visible to the audience. This can be useful for providing additional context or reminders to yourself.\n\n== Example\n\n#example(```typ\n#speaker-note[This is a speaker note]\n```)\n````\n\n# Positional Parameters\n\n## note\n\n```typc\ntype: \n```\n\n\n\n# Named Parameters\n\n## mode\n\n```typc\ntype: \"typ\"\n```\n\n\n\n## setting (named)\n\n```typc\ntype: (any) => any\n```\n\n",
|
||||||
"range": "11:20:11:32"
|
"range": "11:20:11:32"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,8 @@
|
||||||
source: crates/tinymist-query/src/hover.rs
|
source: crates/tinymist-query/src/hover.rs
|
||||||
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
|
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
|
||||||
input_file: crates/tinymist-query/src/fixtures/hover/module_alias.typ
|
input_file: crates/tinymist-query/src/fixtures/hover/module_alias.typ
|
||||||
snapshot_kind: text
|
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"contents": "### Sampled Values\n```typc\n<module themod>\n```\n\n---\n\n# The Module (Alias)",
|
"contents": "### Sampled Values\n```typc\n<module themod>\n```\n\n---\n\n## The Module (Alias)\n",
|
||||||
"range": "2:24:2:31"
|
"range": "2:24:2:31"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,8 @@
|
||||||
source: crates/tinymist-query/src/hover.rs
|
source: crates/tinymist-query/src/hover.rs
|
||||||
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
|
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
|
||||||
input_file: crates/tinymist-query/src/fixtures/hover/module_path.typ
|
input_file: crates/tinymist-query/src/fixtures/hover/module_path.typ
|
||||||
snapshot_kind: text
|
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"contents": "# Some Module",
|
"contents": "## Some Module\n",
|
||||||
"range": "0:29:0:46"
|
"range": "0:29:0:46"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,8 @@
|
||||||
source: crates/tinymist-query/src/hover.rs
|
source: crates/tinymist-query/src/hover.rs
|
||||||
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
|
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
|
||||||
input_file: crates/tinymist-query/src/fixtures/hover/module_var.typ
|
input_file: crates/tinymist-query/src/fixtures/hover/module_var.typ
|
||||||
snapshot_kind: text
|
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"contents": "### Sampled Values\n```typc\n<module themod>\n```\n\n---\n\n# The Module",
|
"contents": "### Sampled Values\n```typc\n<module themod>\n```\n\n---\n\n## The Module\n",
|
||||||
"range": "2:24:2:30"
|
"range": "2:24:2:30"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,6 @@ expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
|
||||||
input_file: crates/tinymist-query/src/fixtures/hover/render_equation.typ
|
input_file: crates/tinymist-query/src/fixtures/hover/render_equation.typ
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"contents": "```typc\nlet lam(\n A: type,\n B: type,\n) = dictionary;\n```\n\n---\n\nLambda constructor.\n\nTyping Rule:\n\n<p align=\"center\"><img alt=\"typst-block\" src=\"data:image-hash/svg+xml;base64,redacted\" /></p>\n\n# Positional Parameters\n\n## A\n\n```typc\ntype: type\n```\n\nThe type of the argument.\n - <!-- typlite:begin:list-item 1 -->It can be also regarded as the condition of the proposition.<!-- typlite:end:list-item 1 -->\n\n## B (positional)\n\n```typc\ntype: type\n```\n\nThe type of the body.\n - <!-- typlite:begin:list-item 1 -->It can be also regarded as the conclusion of the proposition.<!-- typlite:end:list-item 1 -->",
|
"contents": "```typc\nlet lam(\n A: any,\n B: any,\n) = dictionary;\n```\n\n---\n\nLambda constructor.\n\nTyping Rule:\n\n<img alt=\"typst-block\" src=\"data:image-hash/svg+xml;base64,redacted\" />\n\n- <!-- typlite:begin:list-item 0 -->A (type): The type of the argument.\n \n - <!-- typlite:begin:list-item 0 -->It can be also regarded as the condition of the proposition.<!-- typlite:end:list-item 0 -->\n \n <!-- typlite:end:list-item 0 -->\n- <!-- typlite:begin:list-item 0 -->B (type): The type of the body.\n \n - <!-- typlite:begin:list-item 0 -->It can be also regarded as the conclusion of the proposition.<!-- typlite:end:list-item 0 -->\n \n <!-- typlite:end:list-item 0 -->\n\n# Positional Parameters\n\n## A\n\n```typc\ntype: \n```\n\n\n\n## B (positional)\n\n```typc\ntype: \n```\n\n",
|
||||||
"range": "12:20:12:23"
|
"range": "12:20:12:23"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,6 @@ expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
|
||||||
input_file: crates/tinymist-query/src/fixtures/hover/render_equation_no_html.typ
|
input_file: crates/tinymist-query/src/fixtures/hover/render_equation_no_html.typ
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"contents": "```typc\nlet lam(\n A: type,\n B: type,\n) = dictionary;\n```\n\n---\n\nLambda constructor.\n\nTyping Rule:\n\n```typc\n$ (Γ , x : A ⊢ M : B #h(2em) Γ ⊢ a:B)/(Γ ⊢ λ (x : A) → M : π (x : A) → B) $\n```\n\n# Positional Parameters\n\n## A\n\n```typc\ntype: type\n```\n\nThe type of the argument.\n - It can be also regarded as the condition of the proposition.\n\n## B (positional)\n\n```typc\ntype: type\n```\n\nThe type of the body.\n - It can be also regarded as the conclusion of the proposition.",
|
"contents": "```typc\nlet lam(\n A: any,\n B: any,\n) = dictionary;\n```\n\n---\n\nLambda constructor.\n\nTyping Rule:\n\n<img alt=\"typst-block\" src=\"data:image-hash/svg+xml;base64,redacted\" />\n\n- A (type): The type of the argument.\n \n - It can be also regarded as the condition of the proposition.\n \n \n- B (type): The type of the body.\n \n - It can be also regarded as the conclusion of the proposition.\n\n# Positional Parameters\n\n## A\n\n```typc\ntype: \n```\n\n\n\n## B (positional)\n\n```typc\ntype: \n```\n\n",
|
||||||
"range": "14:20:14:23"
|
"range": "14:20:14:23"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -317,7 +317,12 @@ impl fmt::Display for JsonRepr {
|
||||||
let mut ser = Serializer::with_formatter(w, PrettyFormatter::with_indent(b" "));
|
let mut ser = Serializer::with_formatter(w, PrettyFormatter::with_indent(b" "));
|
||||||
self.0.serialize(&mut ser).unwrap();
|
self.0.serialize(&mut ser).unwrap();
|
||||||
|
|
||||||
f.write_str(&String::from_utf8(ser.into_inner().into_inner().unwrap()).unwrap())
|
let res = String::from_utf8(ser.into_inner().into_inner().unwrap()).unwrap();
|
||||||
|
// replace Span(number) to Span(..)
|
||||||
|
static REG: LazyLock<regex::Regex> =
|
||||||
|
LazyLock::new(|| regex::Regex::new(r#"Span\((\d+)\)"#).unwrap());
|
||||||
|
let res = REG.replace_all(&res, "Span(..)");
|
||||||
|
f.write_str(&res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,13 @@ comemo.workspace = true
|
||||||
ecow.workspace = true
|
ecow.workspace = true
|
||||||
tinymist-analysis.workspace = true
|
tinymist-analysis.workspace = true
|
||||||
tinymist-std.workspace = true
|
tinymist-std.workspace = true
|
||||||
|
tinymist-derive.workspace = true
|
||||||
tinymist-project = { workspace = true, features = ["lsp"] }
|
tinymist-project = { workspace = true, features = ["lsp"] }
|
||||||
typst.workspace = true
|
typst.workspace = true
|
||||||
typst-svg.workspace = true
|
typst-svg.workspace = true
|
||||||
typst-syntax.workspace = true
|
typst-syntax.workspace = true
|
||||||
|
typst-html.workspace = true
|
||||||
|
cmark-writer = { version = "0.6.1", features = ["gfm"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta.workspace = true
|
insta.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,13 @@ Converts a subset of typst to markdown.
|
||||||
typlite main.typ
|
typlite main.typ
|
||||||
# specify output
|
# specify output
|
||||||
typlite main.typ output.md
|
typlite main.typ output.md
|
||||||
|
# multiple output formats
|
||||||
|
typlite main.typ output.md output.tex output.docx
|
||||||
```
|
```
|
||||||
|
|
||||||
## Feature
|
<!-- ## Feature
|
||||||
|
|
||||||
|
not implemented yet
|
||||||
|
|
||||||
- **Contexual Content Rendering**: Contents begin with `context` keyword will be rendered as svg output. The svg output will be embedded inline in the output file as **base64** by default, if the `--assets-path` parameter is not specified. Otherwise, the svg output will be saved in the specified folder and the path will be embedded in the output file. By specify the `--assets-src-path` parameter, the source code of the context will also be saved in the specified folder.
|
- **Contexual Content Rendering**: Contents begin with `context` keyword will be rendered as svg output. The svg output will be embedded inline in the output file as **base64** by default, if the `--assets-path` parameter is not specified. Otherwise, the svg output will be saved in the specified folder and the path will be embedded in the output file. By specify the `--assets-src-path` parameter, the source code of the context will also be saved in the specified folder.
|
||||||
|
|
||||||
|
|
@ -32,4 +36,4 @@ typlite main.typ output.md
|
||||||
└── main.typ # input file
|
└── main.typ # input file
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Raw Output**: Raw codes with `typlite` language will be directly output into the Markdown result.
|
- **Raw Output**: Raw codes with `typlite` language will be directly output into the Markdown result. -->
|
||||||
|
|
|
||||||
121
crates/typlite/src/attributes.rs
Normal file
121
crates/typlite/src/attributes.rs
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
//! Attributes for HTML elements and parsing
|
||||||
|
|
||||||
|
use ecow::EcoString;
|
||||||
|
use tinymist_derive::TypliteAttr;
|
||||||
|
use typst::html::HtmlAttrs;
|
||||||
|
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
/// Tag attributes defined for HTML elements.
|
||||||
|
pub mod md_attr {
|
||||||
|
use typst::html::HtmlAttr;
|
||||||
|
|
||||||
|
macro_rules! attrs {
|
||||||
|
($($attr:ident -> $name:ident)*) => {
|
||||||
|
$(#[allow(non_upper_case_globals)]
|
||||||
|
pub const $attr: HtmlAttr = HtmlAttr::constant(
|
||||||
|
stringify!($name)
|
||||||
|
);)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs! {
|
||||||
|
src -> src
|
||||||
|
alt -> alt
|
||||||
|
level -> level
|
||||||
|
dest -> dest
|
||||||
|
lang -> lang
|
||||||
|
block -> block
|
||||||
|
text -> text
|
||||||
|
value -> value
|
||||||
|
caption -> caption
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TypliteAttr, Default)]
|
||||||
|
pub struct HeadingAttr {
|
||||||
|
pub level: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TypliteAttr, Default)]
|
||||||
|
pub struct ImageAttr {
|
||||||
|
pub src: EcoString,
|
||||||
|
pub alt: EcoString,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TypliteAttr, Default)]
|
||||||
|
pub struct FigureAttr {
|
||||||
|
pub caption: EcoString,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TypliteAttr, Default)]
|
||||||
|
pub struct LinkAttr {
|
||||||
|
pub dest: EcoString,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TypliteAttr, Default)]
|
||||||
|
pub struct RawAttr {
|
||||||
|
pub lang: EcoString,
|
||||||
|
pub block: bool,
|
||||||
|
pub text: EcoString,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TypliteAttr, Default)]
|
||||||
|
pub struct ListItemAttr {
|
||||||
|
pub value: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TypliteAttrsParser {
|
||||||
|
fn parse(attrs: &HtmlAttrs) -> Result<Self>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TypliteAttrParser {
|
||||||
|
fn parse_attr(content: &EcoString) -> Result<Self>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypliteAttrParser for usize {
|
||||||
|
fn parse_attr(content: &EcoString) -> Result<Self> {
|
||||||
|
Ok(content
|
||||||
|
.parse::<usize>()
|
||||||
|
.map_err(|_| format!("cannot parse {} as usize", content))?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypliteAttrParser for u32 {
|
||||||
|
fn parse_attr(content: &EcoString) -> Result<Self> {
|
||||||
|
Ok(content
|
||||||
|
.parse::<u32>()
|
||||||
|
.map_err(|_| format!("cannot parse {} as u32", content))?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypliteAttrParser for bool {
|
||||||
|
fn parse_attr(content: &EcoString) -> Result<Self> {
|
||||||
|
Ok(content
|
||||||
|
.parse::<bool>()
|
||||||
|
.map_err(|_| format!("cannot parse {} as bool", content))?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypliteAttrParser for EcoString {
|
||||||
|
fn parse_attr(content: &EcoString) -> Result<Self> {
|
||||||
|
Ok(content.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> TypliteAttrParser for Option<T>
|
||||||
|
where
|
||||||
|
T: TypliteAttrParser,
|
||||||
|
{
|
||||||
|
fn parse_attr(content: &EcoString) -> Result<Self> {
|
||||||
|
if content.is_empty() {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
T::parse_attr(content).map(Some)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
crates/typlite/src/common.rs
Normal file
110
crates/typlite/src/common.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
//! Common types and interfaces for the conversion system
|
||||||
|
|
||||||
|
use cmark_writer::ast::{CustomNodeWriter, Node};
|
||||||
|
use cmark_writer::custom_node;
|
||||||
|
use cmark_writer::WriteResult;
|
||||||
|
use ecow::EcoString;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ListState {
|
||||||
|
Ordered,
|
||||||
|
Unordered,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Valid formats for the conversion.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Format {
|
||||||
|
Md,
|
||||||
|
LaTeX,
|
||||||
|
Docx,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Figure node implementation for all formats
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
#[custom_node]
|
||||||
|
pub struct FigureNode {
|
||||||
|
/// The main content of the figure, can be any block node
|
||||||
|
pub body: Box<Node>,
|
||||||
|
/// The caption text for the figure
|
||||||
|
pub caption: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FigureNode {
|
||||||
|
fn write_custom(&self, writer: &mut dyn CustomNodeWriter) -> WriteResult<()> {
|
||||||
|
let mut temp_writer = cmark_writer::writer::CommonMarkWriter::new();
|
||||||
|
temp_writer.write(&self.body)?;
|
||||||
|
let content = temp_writer.into_string();
|
||||||
|
writer.write_str(&content)?;
|
||||||
|
writer.write_str("\n")?;
|
||||||
|
writer.write_str(&self.caption)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_block_custom(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// External Frame node for handling frames stored as external files
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
#[custom_node]
|
||||||
|
pub struct ExternalFrameNode {
|
||||||
|
/// The path to the external file containing the frame
|
||||||
|
pub file_path: PathBuf,
|
||||||
|
/// Alternative text for the frame
|
||||||
|
pub alt_text: String,
|
||||||
|
/// Original SVG data (needed for DOCX that still embeds images)
|
||||||
|
pub svg_data: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExternalFrameNode {
|
||||||
|
fn write_custom(&self, writer: &mut dyn CustomNodeWriter) -> WriteResult<()> {
|
||||||
|
// The actual handling is implemented in format-specific writers
|
||||||
|
writer.write_str(&format!(
|
||||||
|
"",
|
||||||
|
self.alt_text,
|
||||||
|
self.file_path.display()
|
||||||
|
))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_block_custom(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Highlight node for highlighted text
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
#[custom_node]
|
||||||
|
pub struct HighlightNode {
|
||||||
|
/// The content to be highlighted
|
||||||
|
pub content: Vec<Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HighlightNode {
|
||||||
|
fn write_custom(&self, writer: &mut dyn CustomNodeWriter) -> WriteResult<()> {
|
||||||
|
let mut temp_writer = cmark_writer::writer::CommonMarkWriter::new();
|
||||||
|
for node in &self.content {
|
||||||
|
temp_writer.write(node)?;
|
||||||
|
}
|
||||||
|
let content = temp_writer.into_string();
|
||||||
|
writer.write_str(&format!("=={}==", content))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_block_custom(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Common writer interface for different formats
|
||||||
|
pub trait FormatWriter {
|
||||||
|
/// Write AST document to output format
|
||||||
|
fn write_eco(&mut self, document: &Node, output: &mut EcoString) -> Result<()>;
|
||||||
|
|
||||||
|
/// Write AST document to vector
|
||||||
|
fn write_vec(&mut self, document: &Node) -> Result<Vec<u8>>;
|
||||||
|
}
|
||||||
|
|
@ -25,11 +25,32 @@ impl fmt::Debug for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> From<T> for Error
|
impl From<std::io::Error> for Error {
|
||||||
where
|
fn from(e: std::io::Error) -> Self {
|
||||||
T: Into<Cow<'static, str>>,
|
Error(Box::new(Repr::Msg(e.to_string().into())))
|
||||||
{
|
}
|
||||||
fn from(s: T) -> Self {
|
}
|
||||||
|
|
||||||
|
impl From<fmt::Error> for Error {
|
||||||
|
fn from(e: fmt::Error) -> Self {
|
||||||
|
Error(Box::new(Repr::Msg(e.to_string().into())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&'static str> for Error {
|
||||||
|
fn from(s: &'static str) -> Self {
|
||||||
Error(Box::new(Repr::Msg(s.into())))
|
Error(Box::new(Repr::Msg(s.into())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<String> for Error {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
Error(Box::new(Repr::Msg(s.into())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Cow<'static, str>> for Error {
|
||||||
|
fn from(s: Cow<'static, str>) -> Self {
|
||||||
|
Error(Box::new(Repr::Msg(s)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
These again are dictionaries with the keys
|
These again are dictionaries with the keys
|
||||||
- `description` (optional): The description for the argument.
|
- `description` (optional): The description for the argument.
|
||||||
|
|
||||||
See @@show-module() for outputting the results of this function.
|
See show-module() for outputting the results of this function.
|
||||||
|
|
||||||
- name (string): The name for the module.
|
- name (string): The name for the module.
|
||||||
- label-prefix (auto, string): The label-prefix for internal function
|
- label-prefix (auto, string): The label-prefix for internal function
|
||||||
references. If `auto`, the label-prefix name will be the module name.
|
references. If `auto`, the label-prefix name will be the module name.
|
||||||
- nested something
|
- nested something
|
||||||
- nested something 2
|
- nested something 2
|
||||||
-> string
|
-> string
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,28 @@ source: crates/typlite/src/tests.rs
|
||||||
expression: "conv(world, true)"
|
expression: "conv(world, true)"
|
||||||
input_file: crates/typlite/src/fixtures/docs/nest_list.typ
|
input_file: crates/typlite/src/fixtures/docs/nest_list.typ
|
||||||
---
|
---
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><m1document><p>These again are dictionaries with the keys</p><ul><li><span><m1raw lang="" block="false" text="description"></m1raw></span> (optional): The description for the argument.</li></ul><m1parbreak></m1parbreak><p>See show-module() for outputting the results of this function.</p><m1parbreak></m1parbreak><ul><li>name (string): The name for the module.</li><li><p>label-prefix (auto, string): The label-prefix for internal function references. If <span><m1raw lang="" block="false" text="auto"></m1raw></span>, the label-prefix name will be the module name.</p><ul><li>nested something</li><li>nested something 2</li></ul></li></ul><p>-> string</p></m1document></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
=====
|
||||||
These again are dictionaries with the keys
|
These again are dictionaries with the keys
|
||||||
|
|
||||||
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
|
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
|
||||||
|
|
||||||
See @@show-module() for outputting the results of this function.
|
See show-module() for outputting the results of this function.
|
||||||
|
|
||||||
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
|
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
|
||||||
- <!-- typlite:begin:list-item 0 -->label-prefix (auto, string): The label-prefix for internal function
|
- <!-- typlite:begin:list-item 0 -->label-prefix (auto, string): The label-prefix for internal function references. If `auto`, the label-prefix name will be the module name.
|
||||||
references. If `auto`, the label-prefix name will be the module name.
|
|
||||||
- <!-- typlite:begin:list-item 1 -->nested something<!-- typlite:end:list-item 1 -->
|
- <!-- typlite:begin:list-item 0 -->nested something<!-- typlite:end:list-item 0 -->
|
||||||
- <!-- typlite:begin:list-item 1 -->nested something 2<!-- typlite:end:list-item 1 --><!-- typlite:end:list-item 0 -->
|
- <!-- typlite:begin:list-item 0 -->nested something 2<!-- typlite:end:list-item 0 -->
|
||||||
-> string
|
|
||||||
|
<!-- typlite:end:list-item 0 -->
|
||||||
|
|
||||||
|
-\> string
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,29 @@ source: crates/typlite/src/tests.rs
|
||||||
expression: "conv(world, true)"
|
expression: "conv(world, true)"
|
||||||
input_file: crates/typlite/src/fixtures/docs/tidy.typ
|
input_file: crates/typlite/src/fixtures/docs/tidy.typ
|
||||||
---
|
---
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><m1document><p>These again are dictionaries with the keys</p><ul><li><span><m1raw lang="" block="false" text="description"></m1raw></span> (optional): The description for the argument.</li><li><span><m1raw lang="" block="false" text="types"></m1raw></span> (optional): A list of accepted argument types.</li><li><span><m1raw lang="" block="false" text="default"></m1raw></span> (optional): Default value for this argument.</li></ul><m1parbreak></m1parbreak><p>See show-module() for outputting the results of this function.</p><m1parbreak></m1parbreak><ul><li>content (string): Content of <span><m1raw lang="" block="false" text=".typ"></m1raw></span> file to analyze for docstrings.</li><li>name (string): The name for the module.</li><li>label-prefix (auto, string): The label-prefix for internal function references. If <span><m1raw lang="" block="false" text="auto"></m1raw></span>, the label-prefix name will be the module name.</li><li>require-all-parameters (boolean): Require that all parameters of a functions are documented and fail if some are not.</li><li>scope (dictionary): A dictionary of definitions that are then available in all function and parameter descriptions.</li><li>preamble (string): Code to prepend to all code snippets shown with <span><m1raw lang="" block="false" text="#example()"></m1raw></span>. This can for instance be used to import something from the scope.</li></ul><p>-> string</p></m1document></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
=====
|
||||||
These again are dictionaries with the keys
|
These again are dictionaries with the keys
|
||||||
|
|
||||||
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
|
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
|
||||||
- <!-- typlite:begin:list-item 0 -->`types` (optional): A list of accepted argument types.<!-- typlite:end:list-item 0 -->
|
- <!-- typlite:begin:list-item 0 -->`types` (optional): A list of accepted argument types.<!-- typlite:end:list-item 0 -->
|
||||||
- <!-- typlite:begin:list-item 0 -->`default` (optional): Default value for this argument.<!-- typlite:end:list-item 0 -->
|
- <!-- typlite:begin:list-item 0 -->`default` (optional): Default value for this argument.<!-- typlite:end:list-item 0 -->
|
||||||
|
|
||||||
See @@show-module() for outputting the results of this function.
|
See show-module() for outputting the results of this function.
|
||||||
|
|
||||||
- <!-- typlite:begin:list-item 0 -->content (string): Content of `.typ` file to analyze for docstrings.<!-- typlite:end:list-item 0 -->
|
- <!-- typlite:begin:list-item 0 -->content (string): Content of `.typ` file to analyze for docstrings.<!-- typlite:end:list-item 0 -->
|
||||||
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
|
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
|
||||||
- <!-- typlite:begin:list-item 0 -->label-prefix (auto, string): The label-prefix for internal function
|
- <!-- typlite:begin:list-item 0 -->label-prefix (auto, string): The label-prefix for internal function references. If `auto`, the label-prefix name will be the module name.<!-- typlite:end:list-item 0 -->
|
||||||
references. If `auto`, the label-prefix name will be the module name.<!-- typlite:end:list-item 0 -->
|
- <!-- typlite:begin:list-item 0 -->require-all-parameters (boolean): Require that all parameters of a functions are documented and fail if some are not.<!-- typlite:end:list-item 0 -->
|
||||||
- <!-- typlite:begin:list-item 0 -->require-all-parameters (boolean): Require that all parameters of a
|
- <!-- typlite:begin:list-item 0 -->scope (dictionary): A dictionary of definitions that are then available in all function and parameter descriptions.<!-- typlite:end:list-item 0 -->
|
||||||
functions are documented and fail if some are not.<!-- typlite:end:list-item 0 -->
|
- <!-- typlite:begin:list-item 0 -->preamble (string): Code to prepend to all code snippets shown with `#example()`. This can for instance be used to import something from the scope.<!-- typlite:end:list-item 0 -->
|
||||||
- <!-- typlite:begin:list-item 0 -->scope (dictionary): A dictionary of definitions that are then available
|
|
||||||
in all function and parameter descriptions.<!-- typlite:end:list-item 0 -->
|
-\> string
|
||||||
- <!-- typlite:begin:list-item 0 -->preamble (string): Code to prepend to all code snippets shown with `#example()`.
|
|
||||||
This can for instance be used to import something from the scope.<!-- typlite:end:list-item 0 -->
|
|
||||||
-> string
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
These again are dictionaries with the keys
|
These again are dictionaries with the keys
|
||||||
- `description` (optional): The description for the argument.
|
- `description` (optional): The description for the argument.
|
||||||
- `types` (optional): A list of accepted argument types.
|
- `types` (optional): A list of accepted argument types.
|
||||||
- `default` (optional): Default value for this argument.
|
- `default` (optional): Default value for this argument.
|
||||||
|
|
||||||
See @@show-module() for outputting the results of this function.
|
See show-module() for outputting the results of this function.
|
||||||
|
|
||||||
- content (string): Content of `.typ` file to analyze for docstrings.
|
- content (string): Content of `.typ` file to analyze for docstrings.
|
||||||
- name (string): The name for the module.
|
- name (string): The name for the module.
|
||||||
- label-prefix (auto, string): The label-prefix for internal function
|
- label-prefix (auto, string): The label-prefix for internal function
|
||||||
references. If `auto`, the label-prefix name will be the module name.
|
references. If `auto`, the label-prefix name will be the module name.
|
||||||
- require-all-parameters (boolean): Require that all parameters of a
|
- require-all-parameters (boolean): Require that all parameters of a
|
||||||
functions are documented and fail if some are not.
|
functions are documented and fail if some are not.
|
||||||
- scope (dictionary): A dictionary of definitions that are then available
|
- scope (dictionary): A dictionary of definitions that are then available
|
||||||
in all function and parameter descriptions.
|
in all function and parameter descriptions.
|
||||||
- preamble (string): Code to prepend to all code snippets shown with `#example()`.
|
- preamble (string): Code to prepend to all code snippets shown with `#example()`.
|
||||||
This can for instance be used to import something from the scope.
|
This can for instance be used to import something from the scope.
|
||||||
-> string
|
-> string
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,16 @@ source: crates/typlite/src/tests.rs
|
||||||
expression: "conv(world, false)"
|
expression: "conv(world, false)"
|
||||||
input_file: crates/typlite/src/fixtures/integration/base.typ
|
input_file: crates/typlite/src/fixtures/integration/base.typ
|
||||||
---
|
---
|
||||||
# Hello, World!
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><m1document><m1heading level="1"><span style="display: inline-block;">Hello, World!</span></m1heading><p>This is a typst document.</p></m1document></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
=====
|
||||||
|
## Hello, World!
|
||||||
|
|
||||||
This is a typst document.
|
This is a typst document.
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,15 @@ source: crates/typlite/src/tests.rs
|
||||||
expression: "conv(world, false)"
|
expression: "conv(world, false)"
|
||||||
input_file: crates/typlite/src/fixtures/integration/enum.typ
|
input_file: crates/typlite/src/fixtures/integration/enum.typ
|
||||||
---
|
---
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><m1document><ol><li>A</li><li>B</li></ol></m1document></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
=====
|
||||||
1. A
|
1. A
|
||||||
1. B
|
2. B
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,15 @@ source: crates/typlite/src/tests.rs
|
||||||
expression: "conv(world, false)"
|
expression: "conv(world, false)"
|
||||||
input_file: crates/typlite/src/fixtures/integration/enum2.typ
|
input_file: crates/typlite/src/fixtures/integration/enum2.typ
|
||||||
---
|
---
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><m1document><ol><li value="2">A</li><li>B</li></ol></m1document></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
=====
|
||||||
2. A
|
2. A
|
||||||
1. B
|
3. B
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,16 @@ source: crates/typlite/src/tests.rs
|
||||||
expression: "conv(world, false)"
|
expression: "conv(world, false)"
|
||||||
input_file: crates/typlite/src/fixtures/integration/figure_caption.typ
|
input_file: crates/typlite/src/fixtures/integration/figure_caption.typ
|
||||||
---
|
---
|
||||||

|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><m1document><m1figure caption="Caption"><m1image src="./fig.svg" alt="Content"></m1image></m1figure></m1document></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
=====
|
||||||
|

|
||||||
|
|
||||||
|
Caption
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,14 @@ source: crates/typlite/src/tests.rs
|
||||||
expression: "conv(world, false)"
|
expression: "conv(world, false)"
|
||||||
input_file: crates/typlite/src/fixtures/integration/figure_image.typ
|
input_file: crates/typlite/src/fixtures/integration/figure_image.typ
|
||||||
---
|
---
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><m1document><m1figure caption=""><m1image src="./fig.svg" alt=""></m1image></m1figure></m1document></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
=====
|
||||||

|

|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,14 @@ source: crates/typlite/src/tests.rs
|
||||||
expression: "conv(world, false)"
|
expression: "conv(world, false)"
|
||||||
input_file: crates/typlite/src/fixtures/integration/figure_image_alt.typ
|
input_file: crates/typlite/src/fixtures/integration/figure_image_alt.typ
|
||||||
---
|
---
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><m1document><m1figure caption=""><m1image src="./fig.svg" alt="Content"></m1image></m1figure></m1document></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
=====
|
||||||

|

|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,14 @@ source: crates/typlite/src/tests.rs
|
||||||
expression: "conv(world, false)"
|
expression: "conv(world, false)"
|
||||||
input_file: crates/typlite/src/fixtures/integration/image.typ
|
input_file: crates/typlite/src/fixtures/integration/image.typ
|
||||||
---
|
---
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><m1document><m1image src="./fig.svg" alt=""></m1image></m1document></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
=====
|
||||||

|

|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,14 @@ source: crates/typlite/src/tests.rs
|
||||||
expression: "conv(world, false)"
|
expression: "conv(world, false)"
|
||||||
input_file: crates/typlite/src/fixtures/integration/image_alt.typ
|
input_file: crates/typlite/src/fixtures/integration/image_alt.typ
|
||||||
---
|
---
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><m1document><m1image src="./fig.svg" alt="Content"></m1image></m1document></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
=====
|
||||||

|

|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,14 @@ source: crates/typlite/src/tests.rs
|
||||||
expression: "conv(world, false)"
|
expression: "conv(world, false)"
|
||||||
input_file: crates/typlite/src/fixtures/integration/link.typ
|
input_file: crates/typlite/src/fixtures/integration/link.typ
|
||||||
---
|
---
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><m1document><span><m1link dest="https://example.com">https://example.com</m1link></span></m1document></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
=====
|
||||||
[https://example.com](https://example.com)
|
[https://example.com](https://example.com)
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,14 @@ source: crates/typlite/src/tests.rs
|
||||||
expression: "conv(world, false)"
|
expression: "conv(world, false)"
|
||||||
input_file: crates/typlite/src/fixtures/integration/link2.typ
|
input_file: crates/typlite/src/fixtures/integration/link2.typ
|
||||||
---
|
---
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><m1document><span><m1link dest="https://example.com">Content</m1link></span></m1document></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
=====
|
||||||
[Content](https://example.com)
|
[Content](https://example.com)
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,14 @@ source: crates/typlite/src/tests.rs
|
||||||
expression: "conv(world, false)"
|
expression: "conv(world, false)"
|
||||||
input_file: crates/typlite/src/fixtures/integration/link3.typ
|
input_file: crates/typlite/src/fixtures/integration/link3.typ
|
||||||
---
|
---
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><m1document><span><m1link dest="https://example.com">Reverse <span><m1strong>the World</m1strong></span></m1link></span></m1document></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
=====
|
||||||
[Reverse **the World**](https://example.com)
|
[Reverse **the World**](https://example.com)
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,15 @@ source: crates/typlite/src/tests.rs
|
||||||
expression: "conv(world, false)"
|
expression: "conv(world, false)"
|
||||||
input_file: crates/typlite/src/fixtures/integration/list.typ
|
input_file: crates/typlite/src/fixtures/integration/list.typ
|
||||||
---
|
---
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><m1document><ul><li>Some <span><m1strong>item</m1strong></span></li><li>Another <span><m1emph>item</m1emph></span></li></ul></m1document></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
=====
|
||||||
- Some **item**
|
- Some **item**
|
||||||
- Another _item_
|
- Another _item_
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,14 @@ source: crates/typlite/src/tests.rs
|
||||||
expression: "conv(world, false)"
|
expression: "conv(world, false)"
|
||||||
input_file: crates/typlite/src/fixtures/integration/math_block.typ
|
input_file: crates/typlite/src/fixtures/integration/math_block.typ
|
||||||
---
|
---
|
||||||
<p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="data:image-hash/svg+xml;base64,redacted"><img alt="typst-block" src="data:image-hash/svg+xml;base64,redacted" /></picture></p>
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><m1document><m1eqblock>redacted-frame</m1eqblock></m1document></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
=====
|
||||||
|
<img alt="typst-block" src="data:image-hash/svg+xml;base64,redacted" />
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,14 @@ source: crates/typlite/src/tests.rs
|
||||||
expression: "conv(world, false)"
|
expression: "conv(world, false)"
|
||||||
input_file: crates/typlite/src/fixtures/integration/math_block2.typ
|
input_file: crates/typlite/src/fixtures/integration/math_block2.typ
|
||||||
---
|
---
|
||||||
<p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="data:image-hash/svg+xml;base64,redacted"><img alt="typst-block" src="data:image-hash/svg+xml;base64,redacted" /></picture></p>
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><m1document><m1eqblock>redacted-frame</m1eqblock></m1document></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
=====
|
||||||
|
<img alt="typst-block" src="data:image-hash/svg+xml;base64,redacted" />
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,14 @@ source: crates/typlite/src/tests.rs
|
||||||
expression: "conv(world, false)"
|
expression: "conv(world, false)"
|
||||||
input_file: crates/typlite/src/fixtures/integration/math_inline.typ
|
input_file: crates/typlite/src/fixtures/integration/math_inline.typ
|
||||||
---
|
---
|
||||||
<picture><source media="(prefers-color-scheme: dark)" srcset="data:image-hash/svg+xml;base64,redacted"><img style="vertical-align: -0.35em" alt="typst-block" src="data:image-hash/svg+xml;base64,redacted" /></picture>
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><m1document><m1eqinline>redacted-frame</m1eqinline></m1document></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
=====
|
||||||
|
<img alt="typst-block" src="data:image-hash/svg+xml;base64,redacted" />
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,77 @@ source: crates/typlite/src/tests.rs
|
||||||
expression: "conv(world, false)"
|
expression: "conv(world, false)"
|
||||||
input_file: crates/typlite/src/fixtures/integration/outline.typ
|
input_file: crates/typlite/src/fixtures/integration/outline.typ
|
||||||
---
|
---
|
||||||
failed to convert to markdown: unknown variable: outline
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><m1document><m1outline><m1heading level="1"><span style="display: inline-block;">Contents</span></m1heading><m1outentry level="2"><m1heading level="2"><span style="display: inline-block;">Heading 1</span></m1heading></m1outentry><m1outentry level="3"><m1heading level="3"><span style="display: inline-block;">Heading 2</span></m1heading></m1outentry></m1outline><m1parbreak></m1parbreak><m1heading level="2"><span style="display: inline-block;">Heading 1</span></m1heading><m1parbreak></m1parbreak><m1heading level="3"><span style="display: inline-block;">Heading 2</span></m1heading><m1parbreak></m1parbreak><p><span><m1link dest="https://example.com">This is a link to example.com</m1link></span></p><m1parbreak></m1parbreak><p>Inline <span><m1raw lang="" block="false" text="code"></m1raw></span> has <span><m1raw lang="" block="false" text="back-ticks around"></m1raw></span> it.</p><m1parbreak></m1parbreak><m1raw lang="cs" block="true" text="using System.IO.Compression;
|
||||||
|
|
||||||
|
#pragma warning disable 414, 3021
|
||||||
|
|
||||||
|
namespace MyApplication
|
||||||
|
{
|
||||||
|
[Obsolete("...")]
|
||||||
|
class Program : IInterface
|
||||||
|
{
|
||||||
|
public static List<int> JustDoIt(int count)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Hello {Name}!");
|
||||||
|
return new List<int>(new int[] { 1, 2, 3 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"></m1raw><m1parbreak></m1parbreak><p>Math inline:</p><m1eqinline>redacted-frame</m1eqinline><p>and block:</p><m1eqblock>redacted-frame</m1eqblock><m1parbreak></m1parbreak><ul><li>First item</li><li><p>Second item</p><ol><li>First sub-item</li><li><p>Second sub-item</p><ul><li>First sub-sub-item</li></ul></li></ol></li></ul><m1parbreak></m1parbreak><dl><dt>First term</dt><dd>First definition</dd></dl><m1parbreak></m1parbreak><m1table><table><tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td><td>9</td><td>10</td><td>11</td><td>12</td><td>13</td><td>14</td><td>15</td><td>16</td><td>17</td><td>18</td><td>19</td></tr></table></m1table></m1document></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
=====
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
### Heading 1
|
||||||
|
|
||||||
|
#### Heading 2
|
||||||
|
|
||||||
|
### Heading 1
|
||||||
|
|
||||||
|
#### Heading 2
|
||||||
|
|
||||||
|
[This is a link to example.com](https://example.com)
|
||||||
|
|
||||||
|
Inline `code` has `back-ticks around` it.
|
||||||
|
|
||||||
|
```cs
|
||||||
|
using System.IO.Compression;
|
||||||
|
|
||||||
|
#pragma warning disable 414, 3021
|
||||||
|
|
||||||
|
namespace MyApplication
|
||||||
|
{
|
||||||
|
[Obsolete("...")]
|
||||||
|
class Program : IInterface
|
||||||
|
{
|
||||||
|
public static List<int> JustDoIt(int count)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Hello {Name}!");
|
||||||
|
return new List<int>(new int[] { 1, 2, 3 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Math inline:<img alt="typst-block" src="data:image-hash/svg+xml;base64,redacted" />and block:
|
||||||
|
|
||||||
|
<img alt="typst-block" src="data:image-hash/svg+xml;base64,redacted" />
|
||||||
|
|
||||||
|
- First item
|
||||||
|
- Second item
|
||||||
|
|
||||||
|
1. First sub-item
|
||||||
|
2. Second sub-item
|
||||||
|
|
||||||
|
- First sub-sub-item
|
||||||
|
|
||||||
|
<dl><dt>First term</dt><dd>First definition</dd></dl>
|
||||||
|
|
||||||
|
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,14 @@ source: crates/typlite/src/tests.rs
|
||||||
expression: "conv(world, false)"
|
expression: "conv(world, false)"
|
||||||
input_file: crates/typlite/src/fixtures/integration/raw_inline.typ
|
input_file: crates/typlite/src/fixtures/integration/raw_inline.typ
|
||||||
---
|
---
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><m1document>Some inlined raw <span><m1raw lang="" block="false" text="a"></m1raw></span>, <span><m1raw lang="c" block="false" text="b"></m1raw></span></m1document></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
=====
|
||||||
Some inlined raw `a`, `b`
|
Some inlined raw `a`, `b`
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,23 @@ source: crates/typlite/src/tests.rs
|
||||||
expression: "conv(world, false)"
|
expression: "conv(world, false)"
|
||||||
input_file: crates/typlite/src/fixtures/integration/table.typ
|
input_file: crates/typlite/src/fixtures/integration/table.typ
|
||||||
---
|
---
|
||||||
failed to convert to markdown: invalid columns argument of type Binary
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><m1document><m1table><table><tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td><td>9</td><td>10</td><td>11</td><td>12</td><td>13</td><td>14</td><td>15</td><td>16</td><td>17</td><td>18</td><td>19</td></tr></table></m1table><m1parbreak></m1parbreak><m1table><table><tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr><tr><td>6</td><td>7</td><td>8</td><td>9</td><td>10</td><td>11</td></tr><tr><td>12</td><td>13</td><td>14</td><td>15</td><td>16</td><td>17</td></tr><tr><td>18</td><td>19</td><td></td><td></td><td></td><td></td></tr></table></m1table><m1parbreak></m1parbreak><m1table><table><tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr><tr><td>6</td><td>7</td><td>8</td><td>9</td><td>10</td><td>11</td></tr><tr><td>12</td><td>13</td><td>14</td><td>15</td><td>16</td><td>17</td></tr><tr><td>18</td><td>19</td><td colspan="2">0</td><td colspan="2">1</td></tr><tr><td colspan="2">2</td><td colspan="2">3</td><td colspan="2">4</td></tr><tr><td colspan="2">5</td><td colspan="2">6</td><td colspan="2">7</td></tr><tr><td colspan="2">8</td><td colspan="2">9</td><td colspan="2">10</td></tr><tr><td colspan="2">11</td><td colspan="2">12</td><td colspan="2">13</td></tr><tr><td colspan="2">14</td><td colspan="2">15</td><td colspan="2">16</td></tr><tr><td colspan="2">17</td><td colspan="2">18</td><td colspan="2">19</td></tr></table></m1table></m1document></body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
=====
|
||||||
|
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
|
||||||
|
| 0 | 1 | 2 | 3 | 4 | 5 |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 6 | 7 | 8 | 9 | 10 | 11 |
|
||||||
|
| 12 | 13 | 14 | 15 | 16 | 17 |
|
||||||
|
| 18 | 19 |
|
||||||
|
|
||||||
|
<table><tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr><tr><td>6</td><td>7</td><td>8</td><td>9</td><td>10</td><td>11</td></tr><tr><td>12</td><td>13</td><td>14</td><td>15</td><td>16</td><td>17</td></tr><tr><td>18</td><td>19</td><td colspan="2">0</td><td colspan="2">1</td></tr><tr><td colspan="2">2</td><td colspan="2">3</td><td colspan="2">4</td></tr><tr><td colspan="2">5</td><td colspan="2">6</td><td colspan="2">7</td></tr><tr><td colspan="2">8</td><td colspan="2">9</td><td colspan="2">10</td></tr><tr><td colspan="2">11</td><td colspan="2">12</td><td colspan="2">13</td></tr><tr><td colspan="2">14</td><td colspan="2">15</td><td colspan="2">16</td></tr><tr><td colspan="2">17</td><td colspan="2">18</td><td colspan="2">19</td></tr></table>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,7 @@
|
||||||
//! # Typlite Library
|
//! # Typlite Library
|
||||||
|
|
||||||
|
use crate::{scopes::Scopes, tinymist_std::typst::diag::EcoString, worker::TypliteWorker};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use ecow::eco_format;
|
use ecow::eco_format;
|
||||||
use typst_syntax::{ast, SyntaxKind, SyntaxNode};
|
use typst_syntax::{ast, SyntaxKind, SyntaxNode};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
io::Write,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
@ -8,7 +9,7 @@ use std::{
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use tinymist_project::WorldProvider;
|
use tinymist_project::WorldProvider;
|
||||||
use typlite::{value::*, TypliteFeat};
|
use typlite::{common::Format, value::*, TypliteFeat};
|
||||||
use typlite::{CompileOnceArgs, Typlite};
|
use typlite::{CompileOnceArgs, Typlite};
|
||||||
|
|
||||||
/// Common arguments of compile, watch, and query.
|
/// Common arguments of compile, watch, and query.
|
||||||
|
|
@ -17,19 +18,13 @@ pub struct CompileArgs {
|
||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
pub compile: CompileOnceArgs,
|
pub compile: CompileOnceArgs,
|
||||||
|
|
||||||
/// Path to output file
|
/// Path to output file(s)
|
||||||
#[clap(value_name = "OUTPUT")]
|
#[clap(value_name = "OUTPUT", action = clap::ArgAction::Append)]
|
||||||
pub output: Option<String>,
|
pub outputs: Vec<String>,
|
||||||
|
|
||||||
/// Configures the path of assets directory
|
/// Configures the path of assets directory
|
||||||
#[clap(long, default_value = None, value_name = "ASSETS_PATH")]
|
#[clap(long, default_value = None, value_name = "ASSETS_PATH")]
|
||||||
pub assets_path: Option<String>,
|
pub assets_path: Option<String>,
|
||||||
|
|
||||||
/// Configure the path to the assets' corresponding source code directory.
|
|
||||||
/// When the path is specified, typlite adds a href to jump to the source
|
|
||||||
/// code in the exported asset.
|
|
||||||
#[clap(long, default_value = None, value_name = "ASSETS_SRC_PATH")]
|
|
||||||
pub assets_src_path: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> typlite::Result<()> {
|
fn main() -> typlite::Result<()> {
|
||||||
|
|
@ -41,11 +36,16 @@ fn main() -> typlite::Result<()> {
|
||||||
.input
|
.input
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or("Missing required argument: INPUT")?;
|
.ok_or("Missing required argument: INPUT")?;
|
||||||
let output = match args.output {
|
|
||||||
Some(stdout_path) if stdout_path == "-" => None,
|
let outputs = if args.outputs.is_empty() {
|
||||||
Some(output_path) => Some(PathBuf::from(output_path)),
|
vec![Path::new(input)
|
||||||
None => Some(Path::new(input).with_extension("md")),
|
.with_extension("md")
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()]
|
||||||
|
} else {
|
||||||
|
args.outputs.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let assets_path = match args.assets_path {
|
let assets_path = match args.assets_path {
|
||||||
Some(assets_path) => {
|
Some(assets_path) => {
|
||||||
let path = PathBuf::from(assets_path);
|
let path = PathBuf::from(assets_path);
|
||||||
|
|
@ -58,18 +58,6 @@ fn main() -> typlite::Result<()> {
|
||||||
}
|
}
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
let assets_src_path = match args.assets_src_path {
|
|
||||||
Some(assets_src_path) => {
|
|
||||||
let path = PathBuf::from(assets_src_path);
|
|
||||||
if !path.exists() {
|
|
||||||
if let Err(e) = std::fs::create_dir_all(&path) {
|
|
||||||
return Err(format!("failed to create assets' src directory: {}", e).into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(path)
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let universe = args.compile.resolve().map_err(|err| format!("{err:?}"))?;
|
let universe = args.compile.resolve().map_err(|err| format!("{err:?}"))?;
|
||||||
let world = universe.snapshot();
|
let world = universe.snapshot();
|
||||||
|
|
@ -77,18 +65,60 @@ fn main() -> typlite::Result<()> {
|
||||||
let converter = Typlite::new(Arc::new(world))
|
let converter = Typlite::new(Arc::new(world))
|
||||||
.with_library(lib())
|
.with_library(lib())
|
||||||
.with_feature(TypliteFeat {
|
.with_feature(TypliteFeat {
|
||||||
assets_path,
|
assets_path: assets_path.clone(),
|
||||||
assets_src_path,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
let conv = converter.convert();
|
let doc = match converter.convert_doc() {
|
||||||
|
Ok(doc) => doc,
|
||||||
|
Err(err) => return Err(format!("failed to convert document: {err}").into()),
|
||||||
|
};
|
||||||
|
|
||||||
match (conv, output) {
|
for output_path in &outputs {
|
||||||
(Ok(conv), None) => println!("{}", conv),
|
let is_stdout = output_path == "-";
|
||||||
(Ok(conv), Some(output)) => std::fs::write(output, conv.as_str()).unwrap(),
|
let output = if is_stdout {
|
||||||
(Err(err), ..) => {
|
None
|
||||||
eprintln!("{err}");
|
} else {
|
||||||
std::process::exit(1);
|
Some(PathBuf::from(output_path))
|
||||||
|
};
|
||||||
|
|
||||||
|
let format = match &output {
|
||||||
|
Some(output) if output.extension() == Some(std::ffi::OsStr::new("tex")) => {
|
||||||
|
Format::LaTeX
|
||||||
|
}
|
||||||
|
Some(output) if output.extension() == Some(std::ffi::OsStr::new("docx")) => {
|
||||||
|
Format::Docx
|
||||||
|
}
|
||||||
|
_ => Format::Md,
|
||||||
|
};
|
||||||
|
|
||||||
|
match format {
|
||||||
|
Format::Docx => todo!(),
|
||||||
|
Format::LaTeX => todo!(),
|
||||||
|
Format::Md => {
|
||||||
|
let result = doc.to_md_string();
|
||||||
|
match (result, output) {
|
||||||
|
(Ok(content), None) => {
|
||||||
|
std::io::stdout()
|
||||||
|
.write_all(content.as_str().as_bytes())
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
(Ok(content), Some(output)) => {
|
||||||
|
if let Err(err) = std::fs::write(&output, content.as_str()) {
|
||||||
|
eprintln!(
|
||||||
|
"failed to write Markdown file {}: {}",
|
||||||
|
output.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
println!("Generated Markdown file: {}", output.display());
|
||||||
|
}
|
||||||
|
(Err(err), _) => {
|
||||||
|
eprintln!("Error converting to Markdown for {}: {}", output_path, err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
5
crates/typlite/src/markdown-typst.toml
Normal file
5
crates/typlite/src/markdown-typst.toml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
[package]
|
||||||
|
name = "markdown"
|
||||||
|
version = "0.1.0"
|
||||||
|
entrypoint = "lib.typ"
|
||||||
|
description = "Markdown support for typst."
|
||||||
151
crates/typlite/src/markdown.typ
Normal file
151
crates/typlite/src/markdown.typ
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
#let bool-str(x) = {
|
||||||
|
if x {
|
||||||
|
"true"
|
||||||
|
} else {
|
||||||
|
"false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// typst doesn't allow things like `typParbreak`.
|
||||||
|
#let md-parbreak = html.elem("m1parbreak", "")
|
||||||
|
#let md-linebreak = html.elem("m1linebreak", "")
|
||||||
|
#let md-strong(body, delta: 0) = html.elem("span", html.elem("m1strong", body))
|
||||||
|
#let md-emph(body) = html.elem("span", html.elem("m1emph", body))
|
||||||
|
#let md-highlight(body) = html.elem("span", html.elem("m1highlight", body))
|
||||||
|
#let md-strike(body) = html.elem("span", html.elem("m1strike", body))
|
||||||
|
#let md-raw(lang: none, block: false, text) = {
|
||||||
|
let body = html.elem(
|
||||||
|
"m1raw",
|
||||||
|
attrs: (
|
||||||
|
lang: if lang == none {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
lang
|
||||||
|
},
|
||||||
|
block: bool-str(block),
|
||||||
|
text: text,
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
if block {
|
||||||
|
return body
|
||||||
|
} else {
|
||||||
|
html.elem("span", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#let md-link(dest: none, body) = html.elem(
|
||||||
|
"span",
|
||||||
|
html.elem(
|
||||||
|
"m1link",
|
||||||
|
attrs: (dest: dest),
|
||||||
|
body,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
#let md-label(dest: none, body) = html.elem(
|
||||||
|
"m1label",
|
||||||
|
attrs: (dest: dest),
|
||||||
|
body,
|
||||||
|
)
|
||||||
|
#let md-ref(body) = html.elem(
|
||||||
|
"span",
|
||||||
|
html.elem(
|
||||||
|
"m1ref",
|
||||||
|
body,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
#let md-heading(level: int, body) = html.elem(
|
||||||
|
"m1heading",
|
||||||
|
attrs: (level: str(level)),
|
||||||
|
box(body),
|
||||||
|
)
|
||||||
|
#let md-outline = html.elem.with("m1outline")
|
||||||
|
#let md-outline-entry(level: int, body) = html.elem(
|
||||||
|
"m1outentry",
|
||||||
|
attrs: (level: str(level)),
|
||||||
|
body,
|
||||||
|
)
|
||||||
|
#let md-quote(attribution: none, body) = html.elem(
|
||||||
|
"m1quote",
|
||||||
|
attrs: (attribution: attribution),
|
||||||
|
body,
|
||||||
|
)
|
||||||
|
#let md-table(it) = html.elem(
|
||||||
|
"m1table",
|
||||||
|
it,
|
||||||
|
)
|
||||||
|
#let md-grid(columns: auto, ..children) = html.elem(
|
||||||
|
"m1grid",
|
||||||
|
table(columns: columns, ..children.pos().map(it => table.cell(it))),
|
||||||
|
)
|
||||||
|
#let md-image(src: "", alt: none) = html.elem(
|
||||||
|
"m1image",
|
||||||
|
attrs: (
|
||||||
|
src: src,
|
||||||
|
alt: if alt == none {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
alt
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
#let md-figure(body, caption: none) = html.elem(
|
||||||
|
"m1figure",
|
||||||
|
attrs: (
|
||||||
|
caption: if caption == none {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
if caption.body.func() == text {
|
||||||
|
caption.body.text
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
body,
|
||||||
|
)
|
||||||
|
|
||||||
|
#let if-not-paged(it, act) = {
|
||||||
|
if target() == "html" {
|
||||||
|
act
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#let md-doc(body) = context {
|
||||||
|
// distinguish parbreak from <p> tag
|
||||||
|
show parbreak: it => if-not-paged(it, md-parbreak)
|
||||||
|
show strong: it => if-not-paged(it, md-strong(it.body, delta: it.delta))
|
||||||
|
show emph: it => if-not-paged(it, md-emph(it.body))
|
||||||
|
show highlight: it => if-not-paged(it, md-highlight(it))
|
||||||
|
show strike: it => if-not-paged(it, md-strike(it))
|
||||||
|
// todo: icc?
|
||||||
|
show image: it => if-not-paged(it, md-image(src: it.source, alt: it.alt))
|
||||||
|
|
||||||
|
show raw: it => if-not-paged(it, md-raw(lang: it.lang, block: it.block, it.text))
|
||||||
|
show link: it => if-not-paged(it, md-link(dest: it.dest, it.body))
|
||||||
|
show ref: it => if-not-paged(it, md-ref(it))
|
||||||
|
|
||||||
|
show heading: it => if-not-paged(it, md-heading(level: it.level, it.body))
|
||||||
|
show outline: it => if-not-paged(it, md-outline(it))
|
||||||
|
show outline.entry: it => if-not-paged(it, md-outline-entry(level: it.level, it.element))
|
||||||
|
show quote: it => if-not-paged(it, md-quote(attribution: it.attribution, it.body))
|
||||||
|
show table: it => if-not-paged(it, md-table(it))
|
||||||
|
show grid: it => if-not-paged(it, md-grid(columns: it.columns, ..it.children))
|
||||||
|
|
||||||
|
show math.equation.where(block: false): it => if-not-paged(
|
||||||
|
it,
|
||||||
|
html.elem("m1eqinline", html.frame(box(inset: 0.5em, it))),
|
||||||
|
)
|
||||||
|
show math.equation.where(block: true): it => if-not-paged(
|
||||||
|
it,
|
||||||
|
html.elem("m1eqblock", html.frame(block(inset: 0.5em, it))),
|
||||||
|
)
|
||||||
|
|
||||||
|
show linebreak: it => if-not-paged(it, md-linebreak)
|
||||||
|
show figure: it => if-not-paged(it, md-figure(it.body, caption: it.caption))
|
||||||
|
|
||||||
|
html.elem("m1document", body)
|
||||||
|
}
|
||||||
293
crates/typlite/src/parser/core.rs
Normal file
293
crates/typlite/src/parser/core.rs
Normal file
|
|
@ -0,0 +1,293 @@
|
||||||
|
//! HTML parser core, containing main structures and general parsing logic
|
||||||
|
|
||||||
|
use cmark_writer::ast::{HtmlAttribute, HtmlElement as CmarkHtmlElement, Node};
|
||||||
|
use cmark_writer::CustomNode;
|
||||||
|
use typst::html::{tag, HtmlElement, HtmlNode};
|
||||||
|
|
||||||
|
use crate::attributes::{HeadingAttr, RawAttr, TypliteAttrsParser};
|
||||||
|
use crate::common::ListState;
|
||||||
|
use crate::tags::md_tag;
|
||||||
|
use crate::Result;
|
||||||
|
use crate::TypliteFeat;
|
||||||
|
|
||||||
|
use super::{inline::InlineParser, list::ListParser, media::MediaParser, table::TableParser};
|
||||||
|
|
||||||
|
/// HTML to AST parser implementation
|
||||||
|
pub struct HtmlToAstParser {
|
||||||
|
pub feat: TypliteFeat,
|
||||||
|
pub list_state: Option<ListState>,
|
||||||
|
pub list_level: usize,
|
||||||
|
pub blocks: Vec<Node>,
|
||||||
|
pub inline_buffer: Vec<Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HtmlToAstParser {
|
||||||
|
pub fn new(feat: TypliteFeat) -> Self {
|
||||||
|
Self {
|
||||||
|
feat,
|
||||||
|
list_level: 0,
|
||||||
|
list_state: None,
|
||||||
|
blocks: Vec::new(),
|
||||||
|
inline_buffer: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn convert_element(&mut self, element: &HtmlElement) -> Result<()> {
|
||||||
|
match element.tag {
|
||||||
|
tag::head => Ok(()),
|
||||||
|
|
||||||
|
tag::html | tag::body | md_tag::doc => {
|
||||||
|
self.convert_children(element)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
md_tag::parbreak => {
|
||||||
|
self.flush_inline_buffer();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
md_tag::heading => {
|
||||||
|
self.flush_inline_buffer();
|
||||||
|
let attrs = HeadingAttr::parse(&element.attrs)?;
|
||||||
|
self.convert_children(element)?;
|
||||||
|
self.flush_inline_buffer_as_block(|content| {
|
||||||
|
Node::heading(attrs.level as u8 + 1, content)
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
tag::ol => {
|
||||||
|
self.flush_inline_buffer();
|
||||||
|
self.list_level += 1;
|
||||||
|
let items = ListParser::convert_list(self, element);
|
||||||
|
self.list_level -= 1;
|
||||||
|
self.blocks.push(Node::OrderedList {
|
||||||
|
start: 1,
|
||||||
|
items: items?,
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
tag::ul => {
|
||||||
|
self.flush_inline_buffer();
|
||||||
|
self.list_level += 1;
|
||||||
|
let items = ListParser::convert_list(self, element);
|
||||||
|
self.list_level -= 1;
|
||||||
|
self.blocks.push(Node::UnorderedList(items?));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
md_tag::raw => {
|
||||||
|
let attrs = RawAttr::parse(&element.attrs)?;
|
||||||
|
if attrs.block {
|
||||||
|
self.flush_inline_buffer();
|
||||||
|
self.blocks
|
||||||
|
.push(Node::code_block(Some(attrs.lang.into()), attrs.text.into()));
|
||||||
|
} else {
|
||||||
|
self.inline_buffer.push(Node::InlineCode(attrs.text.into()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
md_tag::quote => {
|
||||||
|
self.flush_inline_buffer();
|
||||||
|
self.convert_children(element)?;
|
||||||
|
self.flush_inline_buffer_as_block(|content| {
|
||||||
|
Node::BlockQuote(vec![Node::Paragraph(content)])
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
md_tag::figure => InlineParser::convert_figure(self, element),
|
||||||
|
|
||||||
|
tag::p | tag::span => {
|
||||||
|
self.convert_children(element)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
tag::strong | md_tag::strong => InlineParser::convert_strong(self, element),
|
||||||
|
|
||||||
|
tag::em | md_tag::emph => InlineParser::convert_emphasis(self, element),
|
||||||
|
|
||||||
|
md_tag::highlight => InlineParser::convert_highlight(self, element),
|
||||||
|
|
||||||
|
md_tag::strike => InlineParser::convert_strikethrough(self, element),
|
||||||
|
|
||||||
|
md_tag::link => InlineParser::convert_link(self, element),
|
||||||
|
|
||||||
|
md_tag::image => InlineParser::convert_image(self, element),
|
||||||
|
|
||||||
|
md_tag::linebreak => {
|
||||||
|
self.inline_buffer.push(Node::HardBreak);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
md_tag::table | md_tag::grid => {
|
||||||
|
self.flush_inline_buffer();
|
||||||
|
if let Some(table) = TableParser::convert_table(self, element)? {
|
||||||
|
self.blocks.push(table);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
md_tag::math_equation_inline | md_tag::math_equation_block => {
|
||||||
|
if element.tag == md_tag::math_equation_block {
|
||||||
|
self.flush_inline_buffer();
|
||||||
|
}
|
||||||
|
self.convert_children(element)?;
|
||||||
|
if element.tag == md_tag::math_equation_block {
|
||||||
|
self.flush_inline_buffer();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
let tag_name = element.tag.resolve().to_string();
|
||||||
|
|
||||||
|
if !tag_name.starts_with("m1") {
|
||||||
|
let html_element = self.create_html_element(element)?;
|
||||||
|
self.inline_buffer.push(html_element);
|
||||||
|
} else {
|
||||||
|
self.convert_children(element)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a CommonMark HTML element from the given HTML element
|
||||||
|
pub(crate) fn create_html_element(&mut self, element: &HtmlElement) -> Result<Node> {
|
||||||
|
let attributes = element
|
||||||
|
.attrs
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|(name, value)| HtmlAttribute {
|
||||||
|
name: name.to_string(),
|
||||||
|
value: value.to_string(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut children = Vec::new();
|
||||||
|
self.convert_children_into(&mut children, element)?;
|
||||||
|
|
||||||
|
Ok(Node::HtmlElement(CmarkHtmlElement {
|
||||||
|
tag: element.tag.resolve().to_string(),
|
||||||
|
attributes,
|
||||||
|
children,
|
||||||
|
self_closing: element.children.is_empty(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flush_inline_buffer(&mut self) {
|
||||||
|
if !self.inline_buffer.is_empty() {
|
||||||
|
self.blocks
|
||||||
|
.push(Node::Paragraph(std::mem::take(&mut self.inline_buffer)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flush_inline_buffer_as_block(&mut self, make_block: impl FnOnce(Vec<Node>) -> Node) {
|
||||||
|
if !self.inline_buffer.is_empty() {
|
||||||
|
self.blocks
|
||||||
|
.push(make_block(std::mem::take(&mut self.inline_buffer)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn convert_children(&mut self, element: &HtmlElement) -> Result<()> {
|
||||||
|
for child in &element.children {
|
||||||
|
match child {
|
||||||
|
HtmlNode::Text(text, _) => {
|
||||||
|
self.inline_buffer
|
||||||
|
.push(Node::Text(text.as_str().to_string()));
|
||||||
|
}
|
||||||
|
HtmlNode::Element(element) => {
|
||||||
|
self.convert_element(element)?;
|
||||||
|
}
|
||||||
|
HtmlNode::Frame(frame) => {
|
||||||
|
self.inline_buffer
|
||||||
|
.push(MediaParser::convert_frame(self, frame));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn convert_children_into(
|
||||||
|
&mut self,
|
||||||
|
target: &mut Vec<Node>,
|
||||||
|
element: &HtmlElement,
|
||||||
|
) -> Result<()> {
|
||||||
|
let prev_buffer = std::mem::take(&mut self.inline_buffer);
|
||||||
|
self.convert_children(element)?;
|
||||||
|
target.append(&mut self.inline_buffer);
|
||||||
|
self.inline_buffer = prev_buffer;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn begin_list(&mut self) {
|
||||||
|
if self.feat.annotate_elem {
|
||||||
|
self.inline_buffer
|
||||||
|
.push(Node::Custom(Box::new(Comment(format!(
|
||||||
|
"typlite:begin:list-item {}",
|
||||||
|
self.list_level - 1
|
||||||
|
)))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn end_list(&mut self) {
|
||||||
|
if self.feat.annotate_elem {
|
||||||
|
self.inline_buffer
|
||||||
|
.push(Node::Custom(Box::new(Comment(format!(
|
||||||
|
"typlite:end:list-item {}",
|
||||||
|
self.list_level - 1
|
||||||
|
)))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Comment(String);
|
||||||
|
|
||||||
|
impl CustomNode for Comment {
|
||||||
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(
|
||||||
|
&self,
|
||||||
|
writer: &mut dyn cmark_writer::CustomNodeWriter,
|
||||||
|
) -> cmark_writer::WriteResult<()> {
|
||||||
|
writer.write_str("<!-- ")?;
|
||||||
|
writer.write_str(&self.0)?;
|
||||||
|
writer.write_str(" -->")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_box(&self) -> Box<dyn CustomNode> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eq_box(&self, other: &dyn CustomNode) -> bool {
|
||||||
|
if let Some(other) = other.as_any().downcast_ref::<Comment>() {
|
||||||
|
self.0 == other.0
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_block(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HtmlToAstParser {
|
||||||
|
pub fn parse(mut self, root: &HtmlElement) -> Result<Node> {
|
||||||
|
self.blocks.clear();
|
||||||
|
self.inline_buffer.clear();
|
||||||
|
|
||||||
|
self.convert_element(root)?;
|
||||||
|
self.flush_inline_buffer();
|
||||||
|
|
||||||
|
Ok(Node::Document(self.blocks))
|
||||||
|
}
|
||||||
|
}
|
||||||
98
crates/typlite/src/parser/inline.rs
Normal file
98
crates/typlite/src/parser/inline.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
//! Inline element processing module, handles text and inline style elements
|
||||||
|
|
||||||
|
use cmark_writer::ast::Node;
|
||||||
|
use typst::html::HtmlElement;
|
||||||
|
|
||||||
|
use crate::attributes::{FigureAttr, ImageAttr, LinkAttr, TypliteAttrsParser};
|
||||||
|
use crate::common::{FigureNode, HighlightNode};
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
use super::core::HtmlToAstParser;
|
||||||
|
|
||||||
|
/// Inline style element parser
|
||||||
|
pub struct InlineParser;
|
||||||
|
|
||||||
|
impl InlineParser {
|
||||||
|
/// Convert strong emphasis element
|
||||||
|
pub fn convert_strong(parser: &mut HtmlToAstParser, element: &HtmlElement) -> Result<()> {
|
||||||
|
let mut content = Vec::new();
|
||||||
|
parser.convert_children_into(&mut content, element)?;
|
||||||
|
parser.inline_buffer.push(Node::Strong(content));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert emphasis element
|
||||||
|
pub fn convert_emphasis(parser: &mut HtmlToAstParser, element: &HtmlElement) -> Result<()> {
|
||||||
|
let mut content = Vec::new();
|
||||||
|
parser.convert_children_into(&mut content, element)?;
|
||||||
|
parser.inline_buffer.push(Node::Emphasis(content));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert highlight element
|
||||||
|
pub fn convert_highlight(parser: &mut HtmlToAstParser, element: &HtmlElement) -> Result<()> {
|
||||||
|
let mut content = Vec::new();
|
||||||
|
parser.convert_children_into(&mut content, element)?;
|
||||||
|
parser
|
||||||
|
.inline_buffer
|
||||||
|
.push(Node::Custom(Box::new(HighlightNode { content })));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert strikethrough element
|
||||||
|
pub fn convert_strikethrough(
|
||||||
|
parser: &mut HtmlToAstParser,
|
||||||
|
element: &HtmlElement,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut content = Vec::new();
|
||||||
|
parser.convert_children_into(&mut content, element)?;
|
||||||
|
parser.inline_buffer.push(Node::Strikethrough(content));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert link element
|
||||||
|
pub fn convert_link(parser: &mut HtmlToAstParser, element: &HtmlElement) -> Result<()> {
|
||||||
|
let attrs = LinkAttr::parse(&element.attrs)?;
|
||||||
|
let mut content = Vec::new();
|
||||||
|
parser.convert_children_into(&mut content, element)?;
|
||||||
|
parser.inline_buffer.push(Node::Link {
|
||||||
|
url: attrs.dest.into(),
|
||||||
|
title: None,
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert image element
|
||||||
|
pub fn convert_image(parser: &mut HtmlToAstParser, element: &HtmlElement) -> Result<()> {
|
||||||
|
let attrs = ImageAttr::parse(&element.attrs)?;
|
||||||
|
let src = attrs.src.as_str();
|
||||||
|
parser.inline_buffer.push(Node::Image {
|
||||||
|
url: src.to_string(),
|
||||||
|
title: None,
|
||||||
|
alt: vec![Node::Text(attrs.alt.into())],
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert figure element
|
||||||
|
pub fn convert_figure(parser: &mut HtmlToAstParser, element: &HtmlElement) -> Result<()> {
|
||||||
|
parser.flush_inline_buffer();
|
||||||
|
|
||||||
|
// Parse figure attributes to extract caption
|
||||||
|
let attrs = FigureAttr::parse(&element.attrs)?;
|
||||||
|
let caption = attrs.caption.to_string();
|
||||||
|
|
||||||
|
// Find image and body content
|
||||||
|
let mut body_content = Vec::new();
|
||||||
|
parser.convert_children_into(&mut body_content, element)?;
|
||||||
|
let body = Box::new(Node::Paragraph(body_content));
|
||||||
|
|
||||||
|
// Create figure node using generic definition
|
||||||
|
parser
|
||||||
|
.blocks
|
||||||
|
.push(Node::Custom(Box::new(FigureNode { body, caption })));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
88
crates/typlite/src/parser/list.rs
Normal file
88
crates/typlite/src/parser/list.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
//! HTML list parsing module, handling conversion of ordered and unordered lists
|
||||||
|
|
||||||
|
use cmark_writer::ast::{ListItem, Node};
|
||||||
|
use typst::html::{tag, HtmlElement, HtmlNode};
|
||||||
|
|
||||||
|
use crate::attributes::{ListItemAttr, TypliteAttrsParser};
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
use super::core::HtmlToAstParser;
|
||||||
|
|
||||||
|
/// List parser
|
||||||
|
pub struct ListParser;
|
||||||
|
|
||||||
|
impl ListParser {
|
||||||
|
/// Convert HTML list to ListItem vector
|
||||||
|
pub fn convert_list(
|
||||||
|
parser: &mut HtmlToAstParser,
|
||||||
|
element: &HtmlElement,
|
||||||
|
) -> Result<Vec<ListItem>> {
|
||||||
|
let mut all_items = Vec::new();
|
||||||
|
let prev_buffer = std::mem::take(&mut parser.inline_buffer);
|
||||||
|
let is_ordered = element.tag == tag::ol;
|
||||||
|
|
||||||
|
for child in &element.children {
|
||||||
|
if let HtmlNode::Element(li) = child {
|
||||||
|
if li.tag == tag::li {
|
||||||
|
let attrs = ListItemAttr::parse(&li.attrs)?;
|
||||||
|
let mut item_content = Vec::new();
|
||||||
|
|
||||||
|
parser.begin_list();
|
||||||
|
|
||||||
|
for li_child in &li.children {
|
||||||
|
match li_child {
|
||||||
|
HtmlNode::Text(text, _) => {
|
||||||
|
parser
|
||||||
|
.inline_buffer
|
||||||
|
.push(Node::Text(text.as_str().to_string()));
|
||||||
|
}
|
||||||
|
HtmlNode::Element(child_elem) => {
|
||||||
|
if child_elem.tag == tag::ul || child_elem.tag == tag::ol {
|
||||||
|
// Handle nested lists
|
||||||
|
if !parser.inline_buffer.is_empty() {
|
||||||
|
item_content.push(Node::Paragraph(std::mem::take(
|
||||||
|
&mut parser.inline_buffer,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let items = Self::convert_list(parser, child_elem)?;
|
||||||
|
if child_elem.tag == tag::ul {
|
||||||
|
item_content.push(Node::UnorderedList(items));
|
||||||
|
} else {
|
||||||
|
item_content.push(Node::OrderedList { start: 1, items });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parser.convert_element(child_elem)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.end_list();
|
||||||
|
|
||||||
|
if !parser.inline_buffer.is_empty() {
|
||||||
|
item_content
|
||||||
|
.push(Node::Paragraph(std::mem::take(&mut parser.inline_buffer)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !item_content.is_empty() {
|
||||||
|
if is_ordered {
|
||||||
|
all_items.push(ListItem::Ordered {
|
||||||
|
number: attrs.value,
|
||||||
|
content: item_content,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
all_items.push(ListItem::Unordered {
|
||||||
|
content: item_content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.inline_buffer = prev_buffer;
|
||||||
|
Ok(all_items)
|
||||||
|
}
|
||||||
|
}
|
||||||
74
crates/typlite/src/parser/media.rs
Normal file
74
crates/typlite/src/parser/media.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
//! Media processing module, handles images, SVG and Frame media elements
|
||||||
|
|
||||||
|
use base64::Engine;
|
||||||
|
use cmark_writer::ast::{HtmlAttribute, HtmlElement as CmarkHtmlElement, Node};
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
use typst::layout::Frame;
|
||||||
|
|
||||||
|
use crate::common::ExternalFrameNode;
|
||||||
|
|
||||||
|
use super::core::HtmlToAstParser;
|
||||||
|
|
||||||
|
/// Media content parser
|
||||||
|
pub struct MediaParser;
|
||||||
|
|
||||||
|
impl MediaParser {
|
||||||
|
/// Convert Typst frame to CommonMark node
|
||||||
|
pub fn convert_frame(parser: &HtmlToAstParser, frame: &Frame) -> Node {
|
||||||
|
let svg = typst_svg::svg_frame(frame);
|
||||||
|
let data = base64::engine::general_purpose::STANDARD.encode(svg.as_bytes());
|
||||||
|
|
||||||
|
if let Some(assets_path) = &parser.feat.assets_path {
|
||||||
|
// Use a unique static counter to generate filenames
|
||||||
|
static FRAME_COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
let file_id = FRAME_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||||
|
let file_name = format!("frame_{}.svg", file_id);
|
||||||
|
let file_path = assets_path.join(&file_name);
|
||||||
|
|
||||||
|
if let Err(e) = std::fs::write(&file_path, svg.as_bytes()) {
|
||||||
|
if parser.feat.soft_error {
|
||||||
|
return Self::create_embedded_frame(&data);
|
||||||
|
} else {
|
||||||
|
// Construct error node
|
||||||
|
return Node::HtmlElement(CmarkHtmlElement {
|
||||||
|
tag: "div".to_string(),
|
||||||
|
attributes: vec![HtmlAttribute {
|
||||||
|
name: "class".to_string(),
|
||||||
|
value: "error".to_string(),
|
||||||
|
}],
|
||||||
|
children: vec![Node::Text(format!("Error writing frame to file: {}", e))],
|
||||||
|
self_closing: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Node::Custom(Box::new(ExternalFrameNode {
|
||||||
|
file_path,
|
||||||
|
alt_text: "typst-frame".to_string(),
|
||||||
|
svg_data: data,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to embedded mode if no external asset path is specified
|
||||||
|
Self::create_embedded_frame(&data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create embedded frame node
|
||||||
|
fn create_embedded_frame(data: &str) -> Node {
|
||||||
|
Node::HtmlElement(CmarkHtmlElement {
|
||||||
|
tag: "img".to_string(),
|
||||||
|
attributes: vec![
|
||||||
|
HtmlAttribute {
|
||||||
|
name: "alt".to_string(),
|
||||||
|
value: "typst-block".to_string(),
|
||||||
|
},
|
||||||
|
HtmlAttribute {
|
||||||
|
name: "src".to_string(),
|
||||||
|
value: format!("data:image/svg+xml;base64,{data}"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
children: vec![],
|
||||||
|
self_closing: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
9
crates/typlite/src/parser/mod.rs
Normal file
9
crates/typlite/src/parser/mod.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
//! Parser implementation for Typst HTML to CommonMark AST
|
||||||
|
|
||||||
|
mod core;
|
||||||
|
mod inline;
|
||||||
|
mod list;
|
||||||
|
mod media;
|
||||||
|
mod table;
|
||||||
|
|
||||||
|
pub use core::HtmlToAstParser;
|
||||||
190
crates/typlite/src/parser/table.rs
Normal file
190
crates/typlite/src/parser/table.rs
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
//! HTML table parsing module, processes the conversion of table elements
|
||||||
|
|
||||||
|
use cmark_writer::ast::Node;
|
||||||
|
use cmark_writer::gfm::TableAlignment;
|
||||||
|
use typst::html::{tag, HtmlElement, HtmlNode};
|
||||||
|
use typst::utils::PicoStr;
|
||||||
|
|
||||||
|
use crate::tags::md_tag;
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
use super::core::HtmlToAstParser;
|
||||||
|
|
||||||
|
/// Table parser
|
||||||
|
pub struct TableParser;
|
||||||
|
|
||||||
|
impl TableParser {
|
||||||
|
/// Convert HTML table to CommonMark AST
|
||||||
|
pub fn convert_table(
|
||||||
|
parser: &mut HtmlToAstParser,
|
||||||
|
element: &HtmlElement,
|
||||||
|
) -> Result<Option<Node>> {
|
||||||
|
// Find the real table element
|
||||||
|
let real_table_elem = Self::find_real_table_element(element);
|
||||||
|
|
||||||
|
// Process the table (if found)
|
||||||
|
if let Some(table) = real_table_elem {
|
||||||
|
// Check if the table contains rowspan or colspan attributes
|
||||||
|
// If it does, fall back to using HtmlElement
|
||||||
|
if Self::table_has_complex_cells(table) {
|
||||||
|
if let Ok(html_node) = parser.create_html_element(table) {
|
||||||
|
return Ok(Some(html_node));
|
||||||
|
}
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut headers = Vec::new();
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
let mut is_header = true;
|
||||||
|
|
||||||
|
Self::extract_table_content(parser, table, &mut headers, &mut rows, &mut is_header)?;
|
||||||
|
return Self::create_table_node(headers, rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the real table element in the HTML structure
|
||||||
|
fn find_real_table_element(element: &HtmlElement) -> Option<&HtmlElement> {
|
||||||
|
if element.tag == md_tag::grid {
|
||||||
|
// For grid: grid -> table -> table
|
||||||
|
Self::find_table_in_grid(element)
|
||||||
|
} else {
|
||||||
|
// For m1table -> table
|
||||||
|
Self::find_table_direct(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_table_in_grid(grid_element: &HtmlElement) -> Option<&HtmlElement> {
|
||||||
|
for child in &grid_element.children {
|
||||||
|
if let HtmlNode::Element(table_elem) = child {
|
||||||
|
if table_elem.tag == md_tag::table {
|
||||||
|
// Find table tag within m1table
|
||||||
|
for inner_child in &table_elem.children {
|
||||||
|
if let HtmlNode::Element(inner) = inner_child {
|
||||||
|
if inner.tag == tag::table {
|
||||||
|
return Some(inner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_table_direct(element: &HtmlElement) -> Option<&HtmlElement> {
|
||||||
|
for child in &element.children {
|
||||||
|
if let HtmlNode::Element(table_elem) = child {
|
||||||
|
if table_elem.tag == tag::table {
|
||||||
|
return Some(table_elem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract table content from the table element
|
||||||
|
fn extract_table_content(
|
||||||
|
parser: &mut HtmlToAstParser,
|
||||||
|
table: &HtmlElement,
|
||||||
|
headers: &mut Vec<Vec<Node>>,
|
||||||
|
rows: &mut Vec<Vec<Vec<Node>>>,
|
||||||
|
is_header: &mut bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Process rows in the table
|
||||||
|
for row_node in &table.children {
|
||||||
|
if let HtmlNode::Element(row_elem) = row_node {
|
||||||
|
if row_elem.tag == tag::tr {
|
||||||
|
let current_row =
|
||||||
|
Self::process_table_row(parser, row_elem, *is_header, headers)?;
|
||||||
|
|
||||||
|
// After the first row, treat remaining rows as data rows
|
||||||
|
if *is_header {
|
||||||
|
*is_header = false;
|
||||||
|
} else if !current_row.is_empty() {
|
||||||
|
rows.push(current_row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_table_row(
|
||||||
|
parser: &mut HtmlToAstParser,
|
||||||
|
row_elem: &HtmlElement,
|
||||||
|
is_header: bool,
|
||||||
|
headers: &mut Vec<Vec<Node>>,
|
||||||
|
) -> Result<Vec<Vec<Node>>> {
|
||||||
|
let mut current_row = Vec::new();
|
||||||
|
|
||||||
|
// Process cells in this row
|
||||||
|
for cell_node in &row_elem.children {
|
||||||
|
if let HtmlNode::Element(cell) = cell_node {
|
||||||
|
if cell.tag == tag::td {
|
||||||
|
let mut cell_content = Vec::new();
|
||||||
|
parser.convert_children_into(&mut cell_content, cell)?;
|
||||||
|
|
||||||
|
// Add to appropriate section
|
||||||
|
if is_header {
|
||||||
|
headers.push(cell_content);
|
||||||
|
} else {
|
||||||
|
current_row.push(cell_content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(current_row)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the table has complex cells (rowspan/colspan)
|
||||||
|
fn table_has_complex_cells(table: &HtmlElement) -> bool {
|
||||||
|
for row_node in &table.children {
|
||||||
|
if let HtmlNode::Element(row_elem) = row_node {
|
||||||
|
if row_elem.tag == tag::tr {
|
||||||
|
for cell_node in &row_elem.children {
|
||||||
|
if let HtmlNode::Element(cell) = cell_node {
|
||||||
|
if (cell.tag == tag::td || cell.tag == tag::th)
|
||||||
|
&& cell.attrs.0.iter().any(|(name, _)| {
|
||||||
|
let name = name.into_inner();
|
||||||
|
name == PicoStr::constant("colspan")
|
||||||
|
|| name == PicoStr::constant("rowspan")
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_table_node(
|
||||||
|
headers: Vec<Vec<Node>>,
|
||||||
|
rows: Vec<Vec<Vec<Node>>>,
|
||||||
|
) -> Result<Option<Node>> {
|
||||||
|
// Create alignment array (default to None for all columns)
|
||||||
|
let alignments = vec![TableAlignment::None; headers.len().max(1)];
|
||||||
|
|
||||||
|
// If there is content, add the table to blocks
|
||||||
|
if !headers.is_empty() || !rows.is_empty() {
|
||||||
|
let flattened_headers = headers.into_iter().flatten().collect();
|
||||||
|
let flattened_rows: Vec<_> = rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| row.into_iter().flatten().collect())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
return Ok(Some(Node::Table {
|
||||||
|
headers: flattened_headers,
|
||||||
|
rows: flattened_rows,
|
||||||
|
alignments,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
43
crates/typlite/src/tags.rs
Normal file
43
crates/typlite/src/tags.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
//! Custom HTML tags used by Typlite
|
||||||
|
|
||||||
|
/// Tag definitions specific to markdown conversion
|
||||||
|
pub mod md_tag {
|
||||||
|
use typst::html::HtmlTag;
|
||||||
|
|
||||||
|
macro_rules! tags {
|
||||||
|
($($tag:ident -> $name:ident)*) => {
|
||||||
|
$(#[allow(non_upper_case_globals)]
|
||||||
|
pub const $tag: HtmlTag = HtmlTag::constant(
|
||||||
|
stringify!($name)
|
||||||
|
);)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tags! {
|
||||||
|
parbreak -> m1parbreak
|
||||||
|
linebreak -> m1linebreak
|
||||||
|
image -> m1image
|
||||||
|
strong -> m1strong
|
||||||
|
emph -> m1emph
|
||||||
|
highlight -> m1highlight
|
||||||
|
strike -> m1strike
|
||||||
|
raw -> m1raw
|
||||||
|
label -> m1label
|
||||||
|
reference -> m1ref
|
||||||
|
heading -> m1heading
|
||||||
|
outline -> m1outline
|
||||||
|
outline_entry -> m1outentry
|
||||||
|
quote -> m1quote
|
||||||
|
table -> m1table
|
||||||
|
// table_cell -> m1tablecell
|
||||||
|
grid -> m1grid
|
||||||
|
// grid_cell -> m1gridcell
|
||||||
|
figure -> m1figure
|
||||||
|
|
||||||
|
math_equation_inline -> m1eqinline
|
||||||
|
math_equation_block -> m1eqblock
|
||||||
|
|
||||||
|
doc -> m1document
|
||||||
|
link -> m1link
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use typst::html::{HtmlNode, HtmlTag};
|
||||||
|
use typst_syntax::Span;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|
@ -24,22 +26,49 @@ fn convert_docs() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn conv(world: LspWorld, for_docs: bool) -> EcoString {
|
fn conv(world: LspWorld, for_docs: bool) -> String {
|
||||||
let converter = Typlite::new(Arc::new(world)).with_feature(TypliteFeat {
|
let converter = Typlite::new(Arc::new(world)).with_feature(TypliteFeat {
|
||||||
annotate_elem: for_docs,
|
annotate_elem: for_docs,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
match converter.convert() {
|
let doc = match converter.convert_doc() {
|
||||||
Ok(conv) => {
|
Ok(doc) => doc,
|
||||||
static REG: OnceLock<Regex> = OnceLock::new();
|
Err(err) => return format!("failed to convert to markdown: {err}"),
|
||||||
let reg =
|
};
|
||||||
REG.get_or_init(|| Regex::new(r#"data:image/svg\+xml;base64,([^"]+)"#).unwrap());
|
|
||||||
let res = reg.replace_all(&conv, |_captures: ®ex::Captures| {
|
|
||||||
"data:image-hash/svg+xml;base64,redacted"
|
|
||||||
});
|
|
||||||
|
|
||||||
res.into()
|
let repr = typst_html::html(&redact(doc.base.clone())).unwrap();
|
||||||
|
let res = doc.to_md_string().unwrap();
|
||||||
|
static REG: OnceLock<Regex> = OnceLock::new();
|
||||||
|
let reg = REG.get_or_init(|| Regex::new(r#"data:image/svg\+xml;base64,([^"]+)"#).unwrap());
|
||||||
|
let res = reg.replace_all(&res, |_captures: ®ex::Captures| {
|
||||||
|
"data:image-hash/svg+xml;base64,redacted"
|
||||||
|
});
|
||||||
|
|
||||||
|
[repr.as_str(), res.as_ref()].join("\n=====\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn redact(doc: HtmlDocument) -> HtmlDocument {
|
||||||
|
let mut doc = doc;
|
||||||
|
for node in doc.root.children.iter_mut() {
|
||||||
|
redact_node(node);
|
||||||
|
}
|
||||||
|
doc
|
||||||
|
}
|
||||||
|
|
||||||
|
fn redact_node(node: &mut HtmlNode) {
|
||||||
|
match node {
|
||||||
|
HtmlNode::Element(elem) => {
|
||||||
|
if elem.tag == HtmlTag::constant("svg") {
|
||||||
|
elem.children = vec![];
|
||||||
|
} else {
|
||||||
|
for child in elem.children.iter_mut() {
|
||||||
|
redact_node(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(err) => format!("failed to convert to markdown: {err}").into(),
|
HtmlNode::Frame(_) => {
|
||||||
|
*node = HtmlNode::Text("redacted-frame".into(), Span::detached());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
//! # Typlite Values
|
//! # Typlite Values
|
||||||
|
|
||||||
|
use crate::tinymist_std::typst::diag::EcoString;
|
||||||
|
use crate::worker::TypliteWorker;
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
|
use typst_syntax::{
|
||||||
|
ast::{self, AstNode},
|
||||||
|
SyntaxNode,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
|
|
||||||
824
crates/typlite/src/worker.rs
Normal file
824
crates/typlite/src/worker.rs
Normal file
|
|
@ -0,0 +1,824 @@
|
||||||
|
use std::fmt::{self, Write};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
|
use base64::Engine;
|
||||||
|
use ecow::{eco_format, EcoString};
|
||||||
|
use tinymist_analysis;
|
||||||
|
use tinymist_project::base::ShadowApi;
|
||||||
|
use tinymist_project::{EntryReader, LspWorld};
|
||||||
|
use typst::foundations::{Bytes, Dict, IntoValue};
|
||||||
|
use typst::layout::Abs;
|
||||||
|
use typst::syntax::{FileId, Source, SyntaxKind, SyntaxNode};
|
||||||
|
use typst::utils::LazyHash;
|
||||||
|
use typst::World;
|
||||||
|
use typst::WorldExt;
|
||||||
|
use typst_syntax::ast::{self, AstNode};
|
||||||
|
use typst_syntax::ast::{Emph, Equation, Heading, Raw, Strong};
|
||||||
|
|
||||||
|
use crate::scopes::Scopes;
|
||||||
|
use crate::tinymist_std::path::unix_slash;
|
||||||
|
use crate::value::{Args, Value};
|
||||||
|
use crate::worker::SyntaxKind::Text;
|
||||||
|
use crate::Result;
|
||||||
|
use crate::TypliteFeat;
|
||||||
|
use crate::WorkspaceResolver;
|
||||||
|
|
||||||
|
/// Typlite worker for converting syntax nodes to markdown
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TypliteWorker {
|
||||||
|
pub current: FileId,
|
||||||
|
pub scopes: Arc<Scopes<Value>>,
|
||||||
|
pub world: Arc<LspWorld>,
|
||||||
|
pub list_depth: usize,
|
||||||
|
pub prepend_code: EcoString,
|
||||||
|
pub assets_numbering: usize,
|
||||||
|
/// Features for the conversion.
|
||||||
|
pub feat: TypliteFeat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypliteWorker {
|
||||||
|
/// Convert the content to a markdown string.
|
||||||
|
pub fn convert(&mut self, node: &SyntaxNode) -> Result<EcoString> {
|
||||||
|
Ok(Self::value(self.eval(node)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Eval the content
|
||||||
|
pub fn eval(&mut self, node: &SyntaxNode) -> Result<Value> {
|
||||||
|
use SyntaxKind::*;
|
||||||
|
let res = match node.kind() {
|
||||||
|
RawLang | RawDelim | RawTrimmed => Err("converting clause")?,
|
||||||
|
|
||||||
|
Math | MathIdent | MathAlignPoint | MathDelimited | MathAttach | MathPrimes
|
||||||
|
| MathFrac | MathRoot | MathShorthand | MathText => Err("converting math node")?,
|
||||||
|
|
||||||
|
// Error nodes
|
||||||
|
Error => Err(node.clone().into_text().to_string())?,
|
||||||
|
None | End => Ok(Value::None),
|
||||||
|
|
||||||
|
// Non-leaf nodes
|
||||||
|
Markup => self.reduce(node),
|
||||||
|
Code => self.reduce(node),
|
||||||
|
Equation => self.equation(node),
|
||||||
|
CodeBlock => {
|
||||||
|
let code_block: ast::CodeBlock = node.cast().unwrap();
|
||||||
|
self.eval(code_block.body().to_untyped())
|
||||||
|
}
|
||||||
|
ContentBlock => {
|
||||||
|
let content_block: ast::ContentBlock = node.cast().unwrap();
|
||||||
|
self.eval(content_block.body().to_untyped())
|
||||||
|
}
|
||||||
|
Parenthesized => {
|
||||||
|
let parenthesized: ast::Parenthesized = node.cast().unwrap();
|
||||||
|
self.eval(parenthesized.expr().to_untyped())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text nodes
|
||||||
|
Text | Space | Parbreak => Self::str(node),
|
||||||
|
Linebreak => Self::char('\n'),
|
||||||
|
|
||||||
|
// Semantic nodes
|
||||||
|
Escape => Self::escape(node),
|
||||||
|
Shorthand => Self::shorthand(node),
|
||||||
|
SmartQuote => Self::str(node),
|
||||||
|
Strong => self.strong(node),
|
||||||
|
Emph => self.emph(node),
|
||||||
|
Raw => Self::raw(node),
|
||||||
|
Link => self.link(node),
|
||||||
|
Label => Self::label(node),
|
||||||
|
Ref => Self::label_ref(node),
|
||||||
|
RefMarker => Self::ref_marker(node),
|
||||||
|
Heading => self.heading(node),
|
||||||
|
HeadingMarker => Self::str(node),
|
||||||
|
ListItem => self.list_item(node),
|
||||||
|
ListMarker => Self::str(node),
|
||||||
|
EnumItem => self.enum_item(node),
|
||||||
|
EnumMarker => Self::str(node),
|
||||||
|
TermItem => self.term_item(node),
|
||||||
|
TermMarker => Self::str(node),
|
||||||
|
|
||||||
|
// Punctuation
|
||||||
|
// Hash => Self::char('#'),
|
||||||
|
Hash => Ok(Value::None),
|
||||||
|
LeftBrace => Self::char('{'),
|
||||||
|
RightBrace => Self::char('}'),
|
||||||
|
LeftBracket => Self::char('['),
|
||||||
|
RightBracket => Self::char(']'),
|
||||||
|
LeftParen => Self::char('('),
|
||||||
|
RightParen => Self::char(')'),
|
||||||
|
Comma => Self::char(','),
|
||||||
|
Semicolon => Ok(Value::None),
|
||||||
|
Colon => Self::char(':'),
|
||||||
|
Star => Self::char('*'),
|
||||||
|
Underscore => Self::char('_'),
|
||||||
|
Dollar => Self::char('$'),
|
||||||
|
Plus => Self::char('+'),
|
||||||
|
Minus => Self::char('-'),
|
||||||
|
Slash => Self::char('/'),
|
||||||
|
Hat => Self::char('^'),
|
||||||
|
Prime => Self::char('\''),
|
||||||
|
Dot => Self::char('.'),
|
||||||
|
Eq => Self::char('='),
|
||||||
|
Lt => Self::char('<'),
|
||||||
|
Gt => Self::char('>'),
|
||||||
|
|
||||||
|
// Compound punctuation
|
||||||
|
EqEq => Self::str(node),
|
||||||
|
ExclEq => Self::str(node),
|
||||||
|
LtEq => Self::str(node),
|
||||||
|
GtEq => Self::str(node),
|
||||||
|
PlusEq => Self::str(node),
|
||||||
|
HyphEq => Self::str(node),
|
||||||
|
StarEq => Self::str(node),
|
||||||
|
SlashEq => Self::str(node),
|
||||||
|
Dots => Self::str(node),
|
||||||
|
Arrow => Self::str(node),
|
||||||
|
Root => Self::str(node),
|
||||||
|
|
||||||
|
// Keywords
|
||||||
|
Auto => Self::str(node),
|
||||||
|
Not => Self::str(node),
|
||||||
|
And => Self::str(node),
|
||||||
|
Or => Self::str(node),
|
||||||
|
Let => Self::str(node),
|
||||||
|
Set => Self::str(node),
|
||||||
|
Show => Self::str(node),
|
||||||
|
Context => Self::str(node),
|
||||||
|
If => Self::str(node),
|
||||||
|
Else => Self::str(node),
|
||||||
|
For => Self::str(node),
|
||||||
|
In => Self::str(node),
|
||||||
|
While => Self::str(node),
|
||||||
|
Break => Self::str(node),
|
||||||
|
Continue => Self::str(node),
|
||||||
|
Return => Self::str(node),
|
||||||
|
Import => Self::str(node),
|
||||||
|
Include => Self::str(node),
|
||||||
|
As => Self::str(node),
|
||||||
|
|
||||||
|
LetBinding => self.let_binding(node),
|
||||||
|
FieldAccess => self.field_access(node),
|
||||||
|
FuncCall => self.func_call(node),
|
||||||
|
Contextual => self.contextual(node),
|
||||||
|
|
||||||
|
// Clause nodes
|
||||||
|
Named => Ok(Value::None),
|
||||||
|
Keyed => Ok(Value::None),
|
||||||
|
Unary => Ok(Value::None),
|
||||||
|
Binary => Ok(Value::None),
|
||||||
|
Spread => Ok(Value::None),
|
||||||
|
ImportItems => Ok(Value::None),
|
||||||
|
ImportItemPath => Ok(Value::None),
|
||||||
|
RenamedImportItem => Ok(Value::None),
|
||||||
|
Closure => Ok(Value::None),
|
||||||
|
Args => Ok(Value::None),
|
||||||
|
Params => Ok(Value::None),
|
||||||
|
|
||||||
|
// Ignored code expressions
|
||||||
|
Ident => Ok(Value::None),
|
||||||
|
Bool => Ok(Value::None),
|
||||||
|
Int => Ok(Value::None),
|
||||||
|
Float => Ok(Value::None),
|
||||||
|
Numeric => Ok(Value::None),
|
||||||
|
Str => Ok(Value::Str({
|
||||||
|
let s: ast::Str = node.cast().unwrap();
|
||||||
|
s.get()
|
||||||
|
})),
|
||||||
|
Array => Ok(Value::None),
|
||||||
|
Dict => Ok(Value::None),
|
||||||
|
|
||||||
|
// Ignored code expressions
|
||||||
|
SetRule => Ok(Value::None),
|
||||||
|
ShowRule => Ok(Value::None),
|
||||||
|
Destructuring => Ok(Value::None),
|
||||||
|
DestructAssignment => Ok(Value::None),
|
||||||
|
|
||||||
|
Conditional => Ok(Value::None),
|
||||||
|
WhileLoop => Ok(Value::None),
|
||||||
|
ForLoop => Ok(Value::None),
|
||||||
|
LoopBreak => Ok(Value::None),
|
||||||
|
LoopContinue => Ok(Value::None),
|
||||||
|
FuncReturn => Ok(Value::None),
|
||||||
|
|
||||||
|
ModuleImport => Ok(Value::None),
|
||||||
|
ModuleInclude => self.include(node),
|
||||||
|
|
||||||
|
// Ignored comments
|
||||||
|
LineComment => Ok(Value::None),
|
||||||
|
BlockComment => Ok(Value::None),
|
||||||
|
Shebang => Ok(Value::None),
|
||||||
|
};
|
||||||
|
if res.clone()? == Value::None
|
||||||
|
&& !matches!(
|
||||||
|
node.kind(),
|
||||||
|
Ident | Bool | Int | Float | Numeric | Str | Array | Dict
|
||||||
|
)
|
||||||
|
{
|
||||||
|
self.prepend_code += node.clone().into_text();
|
||||||
|
if node.kind() != Hash {
|
||||||
|
self.prepend_code += "\n"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reduce(&mut self, node: &SyntaxNode) -> Result<Value> {
|
||||||
|
let mut s = EcoString::new();
|
||||||
|
|
||||||
|
for child in node.children() {
|
||||||
|
s.push_str(&Self::value(self.eval(child)?));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::Content(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_raw_block(&mut self, node: &SyntaxNode, inline: bool) -> Result<Value> {
|
||||||
|
let content = node.clone().into_text();
|
||||||
|
|
||||||
|
let s = if inline {
|
||||||
|
let mut s = EcoString::with_capacity(content.len() + 2);
|
||||||
|
s.push_str("`");
|
||||||
|
s.push_str(&content);
|
||||||
|
s.push_str("`");
|
||||||
|
s
|
||||||
|
} else {
|
||||||
|
let mut s = EcoString::with_capacity(content.len() + 15);
|
||||||
|
s.push_str("```");
|
||||||
|
let lang = match node.cast::<ast::Expr>() {
|
||||||
|
Some(ast::Expr::Text(..) | ast::Expr::Space(..)) => "typ",
|
||||||
|
Some(..) => "typc",
|
||||||
|
None => "typ",
|
||||||
|
};
|
||||||
|
s.push_str(lang);
|
||||||
|
s.push('\n');
|
||||||
|
s.push_str(&content);
|
||||||
|
s.push('\n');
|
||||||
|
s.push_str("```");
|
||||||
|
s
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Value::Content(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(
|
||||||
|
&mut self,
|
||||||
|
prepend_node: &SyntaxNode,
|
||||||
|
node: &SyntaxNode,
|
||||||
|
inline: bool,
|
||||||
|
) -> Result<Value> {
|
||||||
|
self.assets_numbering += 1;
|
||||||
|
let prepend_code = prepend_node.clone().into_text();
|
||||||
|
let code = node.clone().into_text();
|
||||||
|
// if let Some(assets_src_path) = &self.feat.assets_src_path {
|
||||||
|
// let file_name = assets_src_path
|
||||||
|
// .join(self.assets_numbering.to_string())
|
||||||
|
// .with_extension("typ");
|
||||||
|
// if let Err(e) = std::fs::write(&file_name, format!("#{{\n// render_code\n{}\n}}", code))
|
||||||
|
// {
|
||||||
|
// return Err(format!("failed to write code to file: {}", e).into());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
self.render_code(&prepend_code, &code, false, "center", "", inline)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_code(
|
||||||
|
&mut self,
|
||||||
|
prepend_code: &str,
|
||||||
|
code: &str,
|
||||||
|
is_markup: bool,
|
||||||
|
align: &str,
|
||||||
|
extra_attrs: &str,
|
||||||
|
inline: bool,
|
||||||
|
) -> Result<Value> {
|
||||||
|
let theme = self.feat.color_theme;
|
||||||
|
|
||||||
|
// let code_file_name = if let Some(assets_src_path) = &self.feat.assets_src_path {
|
||||||
|
// Some(
|
||||||
|
// assets_src_path
|
||||||
|
// .join(self.assets_numbering.to_string())
|
||||||
|
// .with_extension("typ"),
|
||||||
|
// )
|
||||||
|
// } else {
|
||||||
|
// None
|
||||||
|
// };
|
||||||
|
|
||||||
|
let code_file_name = None;
|
||||||
|
|
||||||
|
let mut render = |theme| self.render_inner(prepend_code, code, is_markup, theme);
|
||||||
|
|
||||||
|
let mut content = EcoString::new();
|
||||||
|
|
||||||
|
let inline_attrs = if inline {
|
||||||
|
r#" style="vertical-align: -0.35em""#
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
|
||||||
|
let write_error = |content: &mut EcoString, err: &str| {
|
||||||
|
let err = err.replace("`", r#"\`"#);
|
||||||
|
let _ = write!(content, "```\nRender Error\n{err}\n```");
|
||||||
|
};
|
||||||
|
|
||||||
|
let write_image = |content: &mut EcoString,
|
||||||
|
file_name: &std::path::Path,
|
||||||
|
code_file_name: Option<&PathBuf>,
|
||||||
|
inline_attrs: &str,
|
||||||
|
extra_attrs: &str| {
|
||||||
|
if let Some(code_file_name) = code_file_name {
|
||||||
|
let _ = write!(
|
||||||
|
content,
|
||||||
|
r#"<a href="{}"><img{inline_attrs} alt="typst-block" src="{}" {extra_attrs}/></a>"#,
|
||||||
|
code_file_name.display(),
|
||||||
|
file_name.display()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let _ = write!(
|
||||||
|
content,
|
||||||
|
r#"<img{inline_attrs} alt="typst-block" src="{}" {extra_attrs}/>"#,
|
||||||
|
file_name.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let write_picture = |content: &mut EcoString,
|
||||||
|
dark_file_name: &std::path::Path,
|
||||||
|
light_file_name: &std::path::Path,
|
||||||
|
code_file_name: Option<&PathBuf>,
|
||||||
|
inline_attrs: &str,
|
||||||
|
extra_attrs: &str| {
|
||||||
|
if let Some(code_file_name) = code_file_name {
|
||||||
|
let _ = write!(
|
||||||
|
content,
|
||||||
|
r#"<a href="{}"><picture><source media="(prefers-color-scheme: dark)" srcset="{}"><img{inline_attrs} alt="typst-block" src="{}" {extra_attrs}/></picture></a>"#,
|
||||||
|
code_file_name.display(),
|
||||||
|
dark_file_name.display(),
|
||||||
|
light_file_name.display()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let _ = write!(
|
||||||
|
content,
|
||||||
|
r#"<picture><source media="(prefers-color-scheme: dark)" srcset="{}"><img{inline_attrs} alt="typst-block" src="{}" {extra_attrs}/></picture>"#,
|
||||||
|
dark_file_name.display(),
|
||||||
|
light_file_name.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match theme {
|
||||||
|
Some(theme) => {
|
||||||
|
let data = match render(theme) {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(err) if self.feat.soft_error => {
|
||||||
|
write_error(&mut content, &err.to_string());
|
||||||
|
return Ok(Value::Content(content));
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !inline {
|
||||||
|
let _ = write!(content, r#"<p align="{align}">"#);
|
||||||
|
}
|
||||||
|
if let Some(assets_path) = &self.feat.assets_path {
|
||||||
|
let file_name =
|
||||||
|
assets_path.join(format!("{}_{:?}.svg", self.assets_numbering, theme));
|
||||||
|
std::fs::write(&file_name, &data)
|
||||||
|
.map_err(|e| format!("failed to write SVG to file: {}", e))?;
|
||||||
|
|
||||||
|
write_image(
|
||||||
|
&mut content,
|
||||||
|
&file_name,
|
||||||
|
code_file_name.as_ref(),
|
||||||
|
inline_attrs,
|
||||||
|
extra_attrs,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let _ = write!(
|
||||||
|
content,
|
||||||
|
r#"<img{inline_attrs} alt="typst-block" src="data:image/svg+xml;base64,{data}" {extra_attrs}/>"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !inline {
|
||||||
|
content.push_str("</p>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let dark = match render(crate::ColorTheme::Dark) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(err) if self.feat.soft_error => {
|
||||||
|
write_error(&mut content, &err.to_string());
|
||||||
|
return Ok(Value::Content(content));
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
};
|
||||||
|
let light = match render(crate::ColorTheme::Light) {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(err) if self.feat.soft_error => {
|
||||||
|
write_error(&mut content, &err.to_string());
|
||||||
|
return Ok(Value::Content(content));
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !inline {
|
||||||
|
let _ = write!(content, r#"<p align="{align}">"#);
|
||||||
|
}
|
||||||
|
if let Some(assets_path) = &self.feat.assets_path {
|
||||||
|
let dark_file_name = assets_path.join(format!(
|
||||||
|
"{}_{:?}.svg",
|
||||||
|
self.assets_numbering,
|
||||||
|
crate::ColorTheme::Dark
|
||||||
|
));
|
||||||
|
let light_file_name = assets_path.join(format!(
|
||||||
|
"{}_{:?}.svg",
|
||||||
|
self.assets_numbering,
|
||||||
|
crate::ColorTheme::Light
|
||||||
|
));
|
||||||
|
|
||||||
|
write_picture(
|
||||||
|
&mut content,
|
||||||
|
&dark_file_name,
|
||||||
|
&light_file_name,
|
||||||
|
code_file_name.as_ref(),
|
||||||
|
inline_attrs,
|
||||||
|
extra_attrs,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let _ = write!(
|
||||||
|
content,
|
||||||
|
r#"<picture><source media="(prefers-color-scheme: dark)" srcset="data:image/svg+xml;base64,{dark}"><img{inline_attrs} alt="typst-block" src="data:image/svg+xml;base64,{light}" {extra_attrs}/></picture>"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !inline {
|
||||||
|
content.push_str("</p>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::Content(content))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_inner(
|
||||||
|
&mut self,
|
||||||
|
prepend_code: &str,
|
||||||
|
code: &str,
|
||||||
|
is_markup: bool,
|
||||||
|
theme: crate::ColorTheme,
|
||||||
|
) -> Result<String> {
|
||||||
|
static DARK_THEME_INPUT: LazyLock<Arc<LazyHash<Dict>>> = LazyLock::new(|| {
|
||||||
|
Arc::new(LazyHash::new(Dict::from_iter(std::iter::once((
|
||||||
|
"x-color-theme".into(),
|
||||||
|
"dark".into_value(),
|
||||||
|
)))))
|
||||||
|
});
|
||||||
|
|
||||||
|
let code = WrapCode(code, is_markup);
|
||||||
|
let inputs = match theme {
|
||||||
|
crate::ColorTheme::Dark => Some(DARK_THEME_INPUT.clone()),
|
||||||
|
crate::ColorTheme::Light => None,
|
||||||
|
};
|
||||||
|
let code = eco_format!(
|
||||||
|
r##"{prepend_code}
|
||||||
|
#set page(width: auto, height: auto, margin: (y: 0.45em, rest: 0em), fill: none);
|
||||||
|
#set text(fill: rgb("#c0caf5")) if sys.inputs.at("x-color-theme", default: none) == "dark";
|
||||||
|
{code}"##
|
||||||
|
);
|
||||||
|
let main = Bytes::new(code.as_bytes().to_owned());
|
||||||
|
|
||||||
|
let path = Path::new("__render__.typ");
|
||||||
|
let entry = self.world.entry_state().select_in_workspace(path);
|
||||||
|
let mut world = self.world.task(tinymist_project::TaskInputs {
|
||||||
|
entry: Some(entry),
|
||||||
|
inputs,
|
||||||
|
});
|
||||||
|
world.take_db();
|
||||||
|
world.map_shadow_by_id(world.main(), main).unwrap();
|
||||||
|
|
||||||
|
let document = typst::compile(&world).output;
|
||||||
|
let document = document.map_err(|diagnostics| {
|
||||||
|
let mut err = String::new();
|
||||||
|
let _ = write!(err, "compiling node: ");
|
||||||
|
let write_span = |span: typst_syntax::Span, err: &mut String| {
|
||||||
|
let file = span.id().map(|id| match id.package() {
|
||||||
|
Some(package) if WorkspaceResolver::is_package_file(id) => {
|
||||||
|
format!("{package}:{}", unix_slash(id.vpath().as_rooted_path()))
|
||||||
|
}
|
||||||
|
Some(_) | None => unix_slash(id.vpath().as_rooted_path()),
|
||||||
|
});
|
||||||
|
let range = world.range(span);
|
||||||
|
match (file, range) {
|
||||||
|
(Some(file), Some(range)) => {
|
||||||
|
let _ = write!(err, "{file:?}:{range:?}");
|
||||||
|
}
|
||||||
|
(Some(file), None) => {
|
||||||
|
let _ = write!(err, "{file:?}");
|
||||||
|
}
|
||||||
|
(None, Some(range)) => {
|
||||||
|
let _ = write!(err, "{range:?}");
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let _ = write!(err, "unknown location");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for s in diagnostics.iter() {
|
||||||
|
match s.severity {
|
||||||
|
typst::diag::Severity::Error => {
|
||||||
|
let _ = write!(err, "error: ");
|
||||||
|
}
|
||||||
|
typst::diag::Severity::Warning => {
|
||||||
|
let _ = write!(err, "warning: ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err.push_str(&s.message);
|
||||||
|
err.push_str(" at ");
|
||||||
|
write_span(s.span, &mut err);
|
||||||
|
|
||||||
|
for hint in s.hints.iter() {
|
||||||
|
err.push_str("\nHint: ");
|
||||||
|
err.push_str(hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
for trace in &s.trace {
|
||||||
|
write!(err, "\nTrace: {} at ", trace.v).unwrap();
|
||||||
|
write_span(trace.span, &mut err);
|
||||||
|
}
|
||||||
|
|
||||||
|
err.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
err
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let svg_payload = typst_svg::svg_merged(&document, Abs::zero());
|
||||||
|
|
||||||
|
if let Some(assets_path) = &self.feat.assets_path {
|
||||||
|
let file_name = assets_path.join(format!("{}_{:?}.svg", self.assets_numbering, theme));
|
||||||
|
if let Err(e) = std::fs::write(&file_name, &svg_payload) {
|
||||||
|
return Err(format!("failed to write SVG to file: {}", e).into());
|
||||||
|
}
|
||||||
|
Ok(file_name.to_string_lossy().to_string())
|
||||||
|
} else {
|
||||||
|
Ok(base64::engine::general_purpose::STANDARD.encode(svg_payload))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn char(arg: char) -> Result<Value> {
|
||||||
|
Ok(Value::Content(arg.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn str(node: &SyntaxNode) -> Result<Value> {
|
||||||
|
Ok(Value::Content(node.clone().into_text()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(res: Value) -> EcoString {
|
||||||
|
match res {
|
||||||
|
Value::None => EcoString::new(),
|
||||||
|
Value::Content(content) => content,
|
||||||
|
Value::Str(s) => s,
|
||||||
|
Value::Image { path, alt } => eco_format!(""),
|
||||||
|
_ => eco_format!("{res:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escape(node: &SyntaxNode) -> Result<Value> {
|
||||||
|
// todo: escape characters
|
||||||
|
Self::str(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shorthand(node: &SyntaxNode) -> Result<Value> {
|
||||||
|
// todo: shorthands
|
||||||
|
Self::str(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strong(&mut self, node: &SyntaxNode) -> Result<Value> {
|
||||||
|
let mut s = EcoString::new();
|
||||||
|
|
||||||
|
let strong = node.cast::<Strong>().unwrap();
|
||||||
|
s.push_str("**");
|
||||||
|
s.push_str(&Self::value(self.eval(strong.body().to_untyped())?));
|
||||||
|
s.push_str("**");
|
||||||
|
|
||||||
|
Ok(Value::Content(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emph(&mut self, node: &SyntaxNode) -> Result<Value> {
|
||||||
|
let mut s = EcoString::new();
|
||||||
|
let emph = node.cast::<Emph>().unwrap();
|
||||||
|
s.push('_');
|
||||||
|
s.push_str(&Self::value(self.eval(emph.body().to_untyped())?));
|
||||||
|
s.push('_');
|
||||||
|
Ok(Value::Content(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn heading(&mut self, node: &SyntaxNode) -> Result<Value> {
|
||||||
|
let mut s = EcoString::new();
|
||||||
|
let heading = node.cast::<Heading>().unwrap();
|
||||||
|
let level = heading.depth();
|
||||||
|
for _ in 0..level.get() {
|
||||||
|
s.push('#');
|
||||||
|
}
|
||||||
|
s.push(' ');
|
||||||
|
s.push_str(&Self::value(self.eval(heading.body().to_untyped())?));
|
||||||
|
Ok(Value::Content(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw(node: &SyntaxNode) -> Result<Value> {
|
||||||
|
let mut s = EcoString::new();
|
||||||
|
let raw = node.cast::<Raw>().unwrap();
|
||||||
|
|
||||||
|
// Raw codes with typlite language will not be treated as a code block but
|
||||||
|
// directly output into the Markdown result.
|
||||||
|
if let Some(lang) = raw.lang() {
|
||||||
|
if &EcoString::from("typlite") == lang.get() {
|
||||||
|
for line in raw.lines() {
|
||||||
|
s.push_str(&Self::value(Self::str(line.to_untyped())?));
|
||||||
|
s.push('\n');
|
||||||
|
}
|
||||||
|
return Ok(Value::Content(s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw.block() {
|
||||||
|
s.push_str(&Self::value(Self::str(node)?));
|
||||||
|
return Ok(Value::Content(s));
|
||||||
|
}
|
||||||
|
s.push('`');
|
||||||
|
for line in raw.lines() {
|
||||||
|
s.push_str(&Self::value(Self::str(line.to_untyped())?));
|
||||||
|
}
|
||||||
|
s.push('`');
|
||||||
|
Ok(Value::Content(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn link(&mut self, node: &SyntaxNode) -> Result<Value> {
|
||||||
|
// GFM supports autolinks
|
||||||
|
if self.feat.gfm {
|
||||||
|
return Self::str(node);
|
||||||
|
}
|
||||||
|
let mut s = EcoString::new();
|
||||||
|
s.push('[');
|
||||||
|
s.push_str(&Self::value(Self::str(node)?));
|
||||||
|
s.push(']');
|
||||||
|
s.push('(');
|
||||||
|
s.push_str(&Self::value(Self::str(node)?));
|
||||||
|
s.push(')');
|
||||||
|
|
||||||
|
Ok(Value::Content(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(_node: &SyntaxNode) -> Result<Value> {
|
||||||
|
Result::Ok(Value::None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label_ref(node: &SyntaxNode) -> Result<Value> {
|
||||||
|
Self::str(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ref_marker(node: &SyntaxNode) -> Result<Value> {
|
||||||
|
Self::str(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_item(&mut self, node: &SyntaxNode) -> Result<Value> {
|
||||||
|
let mut s = EcoString::new();
|
||||||
|
|
||||||
|
let list_item = node.cast::<ast::ListItem>().unwrap();
|
||||||
|
|
||||||
|
for _ in 0..self.list_depth {
|
||||||
|
s.push_str(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
s.push_str("- ");
|
||||||
|
if self.feat.annotate_elem {
|
||||||
|
let _ = write!(s, "<!-- typlite:begin:list-item {} -->", self.list_depth);
|
||||||
|
self.list_depth += 1;
|
||||||
|
}
|
||||||
|
s.push_str(&Self::value(self.eval(list_item.body().to_untyped())?));
|
||||||
|
if self.feat.annotate_elem {
|
||||||
|
self.list_depth -= 1;
|
||||||
|
let _ = write!(s, "<!-- typlite:end:list-item {} -->", self.list_depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::Content(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enum_item(&mut self, node: &SyntaxNode) -> Result<Value> {
|
||||||
|
let enum_item = node.cast::<ast::EnumItem>().unwrap();
|
||||||
|
let mut s = EcoString::new();
|
||||||
|
|
||||||
|
for _ in 0..self.list_depth {
|
||||||
|
s.push_str(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.feat.annotate_elem {
|
||||||
|
let _ = write!(s, "<!-- typlite:begin:enum-item {} -->", self.list_depth);
|
||||||
|
self.list_depth += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(num) = enum_item.number() {
|
||||||
|
s.push_str(&format!("{}. ", num));
|
||||||
|
} else {
|
||||||
|
s.push_str("1. ");
|
||||||
|
}
|
||||||
|
|
||||||
|
s.push_str(&Self::value(self.eval(enum_item.body().to_untyped())?));
|
||||||
|
|
||||||
|
if self.feat.annotate_elem {
|
||||||
|
self.list_depth -= 1;
|
||||||
|
let _ = write!(s, "<!-- typlite:end:enum-item {} -->", self.list_depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::Content(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn term_item(&mut self, node: &SyntaxNode) -> Result<Value> {
|
||||||
|
self.reduce(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn equation(&mut self, node: &SyntaxNode) -> Result<Value> {
|
||||||
|
let equation: Equation = node.cast().unwrap();
|
||||||
|
|
||||||
|
if self.feat.remove_html {
|
||||||
|
return self.to_raw_block(node, !equation.block());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.render(&SyntaxNode::leaf(Text, ""), node, !equation.block())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn let_binding(&self, node: &SyntaxNode) -> Result<Value> {
|
||||||
|
let _ = node;
|
||||||
|
|
||||||
|
Ok(Value::None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_access(&self, node: &SyntaxNode) -> Result<Value> {
|
||||||
|
let _ = node;
|
||||||
|
|
||||||
|
Ok(Value::None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn func_call(&mut self, node: &SyntaxNode) -> Result<Value> {
|
||||||
|
let c: ast::FuncCall = node.cast().unwrap();
|
||||||
|
|
||||||
|
let callee = match c.callee() {
|
||||||
|
ast::Expr::Ident(callee) => self.scopes.get(callee.get()),
|
||||||
|
ast::Expr::FieldAccess(..) => return Ok(Value::None),
|
||||||
|
_ => return Ok(Value::None),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let Value::RawFunc(func) = callee else {
|
||||||
|
return Err("callee is not a function")?;
|
||||||
|
};
|
||||||
|
|
||||||
|
func(Args::new(self, c.args()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contextual(&mut self, node: &SyntaxNode) -> Result<Value> {
|
||||||
|
if self.feat.remove_html {
|
||||||
|
return self.to_raw_block(node, false);
|
||||||
|
}
|
||||||
|
// Trim the last `#` in the prepend code. (#context)
|
||||||
|
self.prepend_code = self.prepend_code.trim_end_matches('#').into();
|
||||||
|
self.render(
|
||||||
|
&SyntaxNode::leaf(node.kind(), self.prepend_code.clone()),
|
||||||
|
node,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn include(&self, node: &SyntaxNode) -> Result<Value> {
|
||||||
|
let include: ast::ModuleInclude = node.cast().unwrap();
|
||||||
|
|
||||||
|
let path = include.source();
|
||||||
|
let src =
|
||||||
|
tinymist_analysis::syntax::find_source_by_expr(self.world.as_ref(), self.current, path)
|
||||||
|
.ok_or_else(|| format!("failed to find source on path {path:?}"))?;
|
||||||
|
|
||||||
|
self.clone().sub_file(src).map(Value::Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sub_file(mut self, src: Source) -> Result<EcoString> {
|
||||||
|
self.current = src.id();
|
||||||
|
self.convert(src.root())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WrapCode<'a>(&'a str, bool);
|
||||||
|
|
||||||
|
impl fmt::Display for WrapCode<'_> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let is_markup = self.1;
|
||||||
|
if is_markup {
|
||||||
|
f.write_str("#[")?;
|
||||||
|
} else {
|
||||||
|
f.write_str("#{")?;
|
||||||
|
}
|
||||||
|
f.write_str(self.0)?;
|
||||||
|
if is_markup {
|
||||||
|
f.write_str("]")
|
||||||
|
} else {
|
||||||
|
f.write_str("}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
crates/typlite/src/writer/markdown.rs
Normal file
31
crates/typlite/src/writer/markdown.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
//! Markdown writer implementation
|
||||||
|
|
||||||
|
use cmark_writer::ast::Node;
|
||||||
|
use cmark_writer::writer::CommonMarkWriter;
|
||||||
|
use ecow::EcoString;
|
||||||
|
|
||||||
|
use crate::common::FormatWriter;
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
/// Markdown writer implementation
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct MarkdownWriter {}
|
||||||
|
|
||||||
|
impl MarkdownWriter {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FormatWriter for MarkdownWriter {
|
||||||
|
fn write_eco(&mut self, document: &Node, output: &mut EcoString) -> Result<()> {
|
||||||
|
let mut writer = CommonMarkWriter::new();
|
||||||
|
writer.write(document).expect("Failed to write document");
|
||||||
|
output.push_str(&writer.into_string());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_vec(&mut self, _document: &Node) -> Result<Vec<u8>> {
|
||||||
|
Err("Markdown writer does not support writing to Vec<u8>".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
25
crates/typlite/src/writer/mod.rs
Normal file
25
crates/typlite/src/writer/mod.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
//! Writer implementations for different output formats
|
||||||
|
|
||||||
|
pub mod markdown;
|
||||||
|
|
||||||
|
pub use markdown::MarkdownWriter;
|
||||||
|
|
||||||
|
use crate::common::{Format, FormatWriter};
|
||||||
|
|
||||||
|
/// Create a writer instance based on the specified format
|
||||||
|
pub fn create_writer(format: Format) -> Box<dyn FormatWriter> {
|
||||||
|
match format {
|
||||||
|
Format::Md => Box::new(markdown::MarkdownWriter::new()),
|
||||||
|
Format::LaTeX | Format::Docx => {
|
||||||
|
panic!("LaTeX and Docx writers are not implemented yet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WriterFactory;
|
||||||
|
|
||||||
|
impl WriterFactory {
|
||||||
|
pub fn create(format: Format) -> Box<dyn FormatWriter> {
|
||||||
|
create_writer(format)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue