pub mod attributes;
pub mod common;
mod error;
pub mod library;
pub mod parser;
pub mod tags;
pub mod writer;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
pub use error::*;
use cmark_writer::ast::Node;
use tinymist_project::base::ShadowApi;
use tinymist_project::vfs::WorkspaceResolver;
use tinymist_project::{EntryReader, LspWorld, TaskInputs};
use tinymist_std::error::prelude::*;
use typst::foundations::Bytes;
use typst::html::HtmlDocument;
use typst::World;
use typst_syntax::VirtualPath;
pub use crate::common::Format;
use crate::parser::HtmlToAstParser;
use crate::writer::WriterFactory;
use typst_syntax::FileId;
use crate::tinymist_std::typst::foundations::Value::Str;
use crate::tinymist_std::typst::{LazyHash, TypstDict};
pub type Result<T, Err = Error> = std::result::Result<T, Err>;
pub use cmark_writer::ast;
pub use tinymist_project::CompileOnceArgs;
pub use tinymist_std;
#[derive(Clone)]
pub struct MarkdownDocument {
pub base: HtmlDocument,
world: Arc<LspWorld>,
feat: TypliteFeat,
ast: Option<Node>,
}
impl MarkdownDocument {
pub fn new(base: HtmlDocument, world: Arc<LspWorld>, feat: TypliteFeat) -> Self {
Self {
base,
world,
feat,
ast: None,
}
}
pub fn with_ast(
base: HtmlDocument,
world: Arc<LspWorld>,
feat: TypliteFeat,
ast: Node,
) -> Self {
Self {
base,
world,
feat,
ast: Some(ast),
}
}
pub fn parse(&self) -> tinymist_std::Result<Node> {
if let Some(ast) = &self.ast {
return Ok(ast.clone());
}
let parser = HtmlToAstParser::new(self.feat.clone(), &self.world);
parser.parse(&self.base.root).context_ut("failed to parse")
}
pub fn to_md_string(&self) -> tinymist_std::Result<ecow::EcoString> {
let mut output = ecow::EcoString::new();
let ast = self.parse()?;
let mut writer = WriterFactory::create(Format::Md);
writer
.write_eco(&ast, &mut output)
.context_ut("failed to write")?;
Ok(output)
}
pub fn to_text_string(&self) -> tinymist_std::Result<ecow::EcoString> {
let mut output = ecow::EcoString::new();
let ast = self.parse()?;
let mut writer = WriterFactory::create(Format::Text);
writer
.write_eco(&ast, &mut output)
.context_ut("failed to write")?;
Ok(output)
}
pub fn to_tex_string(&self) -> tinymist_std::Result<ecow::EcoString> {
let mut output = ecow::EcoString::new();
let ast = self.parse()?;
let mut writer = WriterFactory::create(Format::LaTeX);
writer
.write_eco(&ast, &mut output)
.context_ut("failed to write")?;
Ok(output)
}
#[cfg(feature = "docx")]
pub fn to_docx(&self) -> tinymist_std::Result<Vec<u8>> {
let ast = self.parse()?;
let mut writer = WriterFactory::create(Format::Docx);
writer.write_vec(&ast).context_ut("failed to write")
}
}
#[derive(Debug, Default, Clone, Copy)]
pub enum ColorTheme {
#[default]
Light,
Dark,
}
#[derive(Debug, Default, Clone)]
pub struct TypliteFeat {
pub color_theme: Option<ColorTheme>,
pub assets_path: Option<PathBuf>,
pub gfm: bool,
pub annotate_elem: bool,
pub soft_error: bool,
pub remove_html: bool,
pub target: Format,
pub import_context: Option<String>,
pub processor: Option<String>,
}
pub struct Typlite {
world: Arc<LspWorld>,
feat: TypliteFeat,
format: Format,
}
impl Typlite {
pub fn new(world: Arc<LspWorld>) -> Self {
Self {
world,
feat: Default::default(),
format: Format::Md,
}
}
pub fn with_feature(mut self, feat: TypliteFeat) -> Self {
self.feat = feat;
self
}
pub fn with_format(mut self, format: Format) -> Self {
self.format = format;
self
}
pub fn convert(self) -> tinymist_std::Result<ecow::EcoString> {
match self.format {
Format::Md => self.convert_doc(Format::Md)?.to_md_string(),
Format::LaTeX => self.convert_doc(Format::LaTeX)?.to_tex_string(),
Format::Text => self.convert_doc(Format::Text)?.to_text_string(),
#[cfg(feature = "docx")]
Format::Docx => bail!("docx format is not supported"),
}
}
#[cfg(feature = "docx")]
pub fn to_docx(self) -> tinymist_std::Result<Vec<u8>> {
if self.format != Format::Docx {
bail!("format is not DOCX");
}
self.convert_doc(Format::Docx)?.to_docx()
}
pub fn convert_doc(self, format: Format) -> tinymist_std::Result<MarkdownDocument> {
let entry = self.world.entry_state();
let main = entry.main();
let current = main.context("no main file in workspace")?;
let world_origin = self.world.clone();
let world = self.world;
if WorkspaceResolver::is_package_file(current) {
bail!("package file is not supported");
}
let wrap_main_id = current.join("__wrap_md_main.typ");
let (main_id, main_content) = match self.feat.processor.as_ref() {
None => (wrap_main_id, None),
Some(processor) => {
let main_id = current.join("__md_main.typ");
let content = format!(
r#"#import {processor:?}: article
#article(include "__wrap_md_main.typ")"#
);
(main_id, Some(Bytes::from_string(content)))
}
};
let mut dict = TypstDict::new();
dict.insert("x-target".into(), Str("md".into()));
if format == Format::Text || self.feat.remove_html {
dict.insert("x-remove-html".into(), Str("true".into()));
}
let task_inputs = TaskInputs {
entry: Some(entry.select_in_workspace(main_id.vpath().as_rooted_path())),
inputs: Some(Arc::new(LazyHash::new(dict))),
};
let mut world = world.task(task_inputs).html_task().into_owned();
let markdown_id = FileId::new(
Some(
typst_syntax::package::PackageSpec::from_str("@local/markdown:0.1.0")
.context_ut("failed to import markdown package")?,
),
VirtualPath::new("lib.typ"),
);
world
.map_shadow_by_id(
markdown_id.join("typst.toml"),
Bytes::from_string(include_str!("markdown-typst.toml")),
)
.context_ut("cannot map markdown-typst.toml")?;
world
.map_shadow_by_id(
markdown_id,
Bytes::from_string(include_str!("markdown.typ")),
)
.context_ut("cannot map markdown.typ")?;
world
.map_shadow_by_id(
wrap_main_id,
Bytes::from_string(format!(
r#"#import "@local/markdown:0.1.0": md-doc, example
#show: md-doc
{}"#,
world
.source(current)
.context_ut("failed to get main file content")?
.text()
)),
)
.context_ut("cannot map source for main file")?;
if let Some(main_content) = main_content {
world
.map_shadow_by_id(main_id, main_content)
.context_ut("cannot map source for main file")?;
}
let base = typst::compile(&world).output?;
let mut feat = self.feat;
feat.target = format;
Ok(MarkdownDocument::new(base, world_origin, feat))
}
}
#[cfg(test)]
mod tests;