summaryrefslogtreecommitdiff
path: root/docs/src/html.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-07-02 19:59:52 +0200
committerLaurenz <laurmaedje@gmail.com>2023-07-02 20:07:43 +0200
commitebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch)
tree2bbc24ddb4124c4bb14dec0e536129d4de37b056 /docs/src/html.rs
parent3ab19185093d7709f824b95b979060ce125389d8 (diff)
Move everything into `crates/` directory
Diffstat (limited to 'docs/src/html.rs')
-rw-r--r--docs/src/html.rs513
1 files changed, 0 insertions, 513 deletions
diff --git a/docs/src/html.rs b/docs/src/html.rs
deleted file mode 100644
index 0d40f46b..00000000
--- a/docs/src/html.rs
+++ /dev/null
@@ -1,513 +0,0 @@
-use std::ops::Range;
-
-use comemo::Prehashed;
-use pulldown_cmark as md;
-use typed_arena::Arena;
-use typst::diag::FileResult;
-use typst::eval::Datetime;
-use typst::file::FileId;
-use typst::font::{Font, FontBook};
-use typst::geom::{Point, Size};
-use typst::syntax::Source;
-use typst::util::Bytes;
-use typst::World;
-use yaml_front_matter::YamlFrontMatter;
-
-use super::*;
-
-/// HTML documentation.
-#[derive(Serialize)]
-#[serde(transparent)]
-pub struct Html {
- raw: String,
- #[serde(skip)]
- md: String,
- #[serde(skip)]
- description: Option<String>,
- #[serde(skip)]
- outline: Vec<OutlineItem>,
-}
-
-impl Html {
- /// Create HTML from a raw string.
- pub fn new(raw: String) -> Self {
- Self {
- md: String::new(),
- raw,
- description: None,
- outline: vec![],
- }
- }
-
- /// Convert markdown to HTML.
- #[track_caller]
- pub fn markdown(resolver: &dyn Resolver, md: &str) -> Self {
- Self::markdown_with_id_base(resolver, md, "")
- }
-
- /// Convert markdown to HTML, preceding all fragment identifiers with the
- /// `id_base`.
- #[track_caller]
- pub fn markdown_with_id_base(
- resolver: &dyn Resolver,
- md: &str,
- id_base: &str,
- ) -> Self {
- let mut text = md;
- let mut description = None;
- let document = YamlFrontMatter::parse::<Metadata>(md);
- if let Ok(document) = &document {
- text = &document.content;
- description = Some(document.metadata.description.clone())
- }
-
- let options = md::Options::ENABLE_TABLES | md::Options::ENABLE_HEADING_ATTRIBUTES;
-
- let ids = Arena::new();
- let mut handler = Handler::new(resolver, id_base.into(), &ids);
- let iter = md::Parser::new_ext(text, options)
- .filter_map(|mut event| handler.handle(&mut event).then_some(event));
-
- let mut raw = String::new();
- md::html::push_html(&mut raw, iter);
- raw.truncate(raw.trim_end().len());
-
- Html {
- md: text.into(),
- raw,
- description,
- outline: handler.outline,
- }
- }
-
- /// The raw HTML.
- pub fn as_str(&self) -> &str {
- &self.raw
- }
-
- /// The original Markdown, if any.
- pub fn md(&self) -> &str {
- &self.md
- }
-
- /// The title of the HTML.
- ///
- /// Returns `None` if the HTML doesn't start with an `h1` tag.
- pub fn title(&self) -> Option<&str> {
- let mut s = Scanner::new(&self.raw);
- s.eat_if("<h1>").then(|| s.eat_until("</h1>"))
- }
-
- /// The outline of the HTML.
- pub fn outline(&self) -> Vec<OutlineItem> {
- self.outline.clone()
- }
-
- /// The description from the front matter.
- pub fn description(&self) -> Option<String> {
- self.description.clone()
- }
-}
-
-impl Debug for Html {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "Html({:?})", self.title().unwrap_or(".."))
- }
-}
-
-/// Front matter metadata.
-#[derive(Deserialize)]
-struct Metadata {
- description: String,
-}
-
-struct Handler<'a> {
- resolver: &'a dyn Resolver,
- lang: Option<String>,
- code: String,
- outline: Vec<OutlineItem>,
- id_base: String,
- ids: &'a Arena<String>,
-}
-
-impl<'a> Handler<'a> {
- fn new(resolver: &'a dyn Resolver, id_base: String, ids: &'a Arena<String>) -> Self {
- Self {
- resolver,
- lang: None,
- code: String::new(),
- outline: vec![],
- id_base,
- ids,
- }
- }
-
- fn handle(&mut self, event: &mut md::Event<'a>) -> bool {
- match event {
- // Rewrite Markdown images.
- md::Event::Start(md::Tag::Image(_, path, _)) => {
- *path = self.handle_image(path).into();
- }
-
- // Rewrite HTML images.
- md::Event::Html(html) if html.starts_with("<img") => {
- let range = html_attr_range(html, "src").unwrap();
- let path = &html[range.clone()];
- let mut buf = html.to_string();
- buf.replace_range(range, &self.handle_image(path));
- *html = buf.into();
- }
-
- // Register HTML headings for the outline.
- md::Event::Start(md::Tag::Heading(level, Some(id), _)) => {
- self.handle_heading(id, level);
- }
-
- // Also handle heading closings.
- md::Event::End(md::Tag::Heading(level, Some(_), _)) => {
- if *level > md::HeadingLevel::H1 && !self.id_base.is_empty() {
- nest_heading(level);
- }
- }
-
- // Rewrite contributor sections.
- md::Event::Html(html) if html.starts_with("<contributors") => {
- let from = html_attr(html, "from").unwrap();
- let to = html_attr(html, "to").unwrap();
- let Some(output) = contributors(self.resolver, from, to) else { return false };
- *html = output.raw.into();
- }
-
- // Rewrite links.
- md::Event::Start(md::Tag::Link(ty, dest, _)) => {
- assert!(
- matches!(ty, md::LinkType::Inline | md::LinkType::Reference),
- "unsupported link type: {ty:?}",
- );
-
- *dest = self
- .handle_link(dest)
- .unwrap_or_else(|| panic!("invalid link: {dest}"))
- .into();
- }
-
- // Inline raw.
- md::Event::Code(code) => {
- let mut chars = code.chars();
- let parser = match (chars.next(), chars.next_back()) {
- (Some('['), Some(']')) => typst::syntax::parse,
- (Some('{'), Some('}')) => typst::syntax::parse_code,
- _ => return true,
- };
-
- let root = parser(&code[1..code.len() - 1]);
- let html = typst::ide::highlight_html(&root);
- *event = md::Event::Html(html.into());
- }
-
- // Code blocks.
- md::Event::Start(md::Tag::CodeBlock(md::CodeBlockKind::Fenced(lang))) => {
- self.lang = Some(lang.as_ref().into());
- self.code = String::new();
- return false;
- }
- md::Event::End(md::Tag::CodeBlock(md::CodeBlockKind::Fenced(_))) => {
- let Some(lang) = self.lang.take() else { return false };
- let html = code_block(self.resolver, &lang, &self.code);
- *event = md::Event::Html(html.raw.into());
- }
-
- // Example with preview.
- md::Event::Text(text) => {
- if self.lang.is_some() {
- self.code.push_str(text);
- return false;
- }
- }
-
- _ => {}
- }
-
- true
- }
-
- fn handle_image(&self, link: &str) -> String {
- if let Some(file) = FILES.get_file(link) {
- self.resolver.image(link, file.contents())
- } else if let Some(url) = self.resolver.link(link) {
- url
- } else {
- panic!("missing image: {link}")
- }
- }
-
- fn handle_heading(&mut self, id: &mut &'a str, level: &mut md::HeadingLevel) {
- if *level == md::HeadingLevel::H1 {
- return;
- }
-
- // Special case for things like "v0.3.0".
- let name = if id.starts_with('v') && id.contains('.') {
- id.to_string()
- } else {
- id.to_title_case()
- };
-
- let mut children = &mut self.outline;
- let mut depth = *level as usize;
- while depth > 2 {
- if !children.is_empty() {
- children = &mut children.last_mut().unwrap().children;
- }
- depth -= 1;
- }
-
- // Put base before id.
- if !self.id_base.is_empty() {
- nest_heading(level);
- *id = self.ids.alloc(format!("{}-{id}", self.id_base)).as_str();
- }
-
- children.push(OutlineItem { id: id.to_string(), name, children: vec![] });
- }
-
- fn handle_link(&self, link: &str) -> Option<String> {
- if link.starts_with('#') || link.starts_with("http") {
- return Some(link.into());
- }
-
- if !link.starts_with('$') {
- return self.resolver.link(link);
- }
-
- let root = link.split('/').next()?;
- let rest = &link[root.len()..].trim_matches('/');
- let base = match root {
- "$tutorial" => "/docs/tutorial/",
- "$reference" => "/docs/reference/",
- "$category" => "/docs/reference/",
- "$syntax" => "/docs/reference/syntax/",
- "$styling" => "/docs/reference/styling/",
- "$scripting" => "/docs/reference/scripting/",
- "$types" => "/docs/reference/types/",
- "$type" => "/docs/reference/types/",
- "$func" => "/docs/reference/",
- "$guides" => "/docs/guides/",
- "$packages" => "/docs/packages/",
- "$changelog" => "/docs/changelog/",
- "$community" => "/docs/community/",
- _ => panic!("unknown link root: {root}"),
- };
-
- let mut route = base.to_string();
- if root == "$type" && rest.contains('.') {
- let mut parts = rest.split('.');
- let ty = parts.next()?;
- let method = parts.next()?;
- route.push_str(ty);
- route.push_str("/#methods-");
- route.push_str(method);
- } else if root == "$func" {
- let mut parts = rest.split('.').peekable();
- let first = parts.peek().copied();
- let mut focus = &LIBRARY.global;
- while let Some(m) = first.and_then(|name| module(focus, name).ok()) {
- focus = m;
- parts.next();
- }
-
- let name = parts.next()?;
-
- let value = focus.get(name).ok()?;
- let Value::Func(func) = value else { return None };
- let info = func.info()?;
- route.push_str(info.category);
- route.push('/');
-
- if let Some(group) = GROUPS
- .iter()
- .filter(|_| first == Some("math"))
- .find(|group| group.functions.iter().any(|func| func == info.name))
- {
- route.push_str(&group.name);
- route.push_str("/#");
- route.push_str(info.name);
- if let Some(param) = parts.next() {
- route.push_str("-parameters-");
- route.push_str(param);
- }
- } else {
- route.push_str(name);
- route.push('/');
- if let Some(next) = parts.next() {
- if info.params.iter().any(|param| param.name == next) {
- route.push_str("#parameters-");
- route.push_str(next);
- } else if info.scope.iter().any(|(name, _)| name == next) {
- route.push('#');
- route.push_str(info.name);
- route.push('-');
- route.push_str(next);
- } else {
- return None;
- }
- }
- }
- } else {
- route.push_str(rest);
- }
-
- if !route.contains('#') && !route.ends_with('/') {
- route.push('/');
- }
-
- Some(route)
- }
-}
-
-/// Render a code block to HTML.
-fn code_block(resolver: &dyn Resolver, lang: &str, text: &str) -> Html {
- let mut display = String::new();
- let mut compile = String::new();
- for line in text.lines() {
- if let Some(suffix) = line.strip_prefix(">>>") {
- compile.push_str(suffix);
- compile.push('\n');
- } else if let Some(suffix) = line.strip_prefix("<<< ") {
- display.push_str(suffix);
- display.push('\n');
- } else {
- display.push_str(line);
- display.push('\n');
- compile.push_str(line);
- compile.push('\n');
- }
- }
-
- let mut parts = lang.split(':');
- let lang = parts.next().unwrap_or(lang);
-
- let mut zoom: Option<[Abs; 4]> = None;
- let mut single = false;
- if let Some(args) = parts.next() {
- single = true;
- if !args.contains("single") {
- zoom = args
- .split(',')
- .take(4)
- .map(|s| Abs::pt(s.parse().unwrap()))
- .collect::<Vec<_>>()
- .try_into()
- .ok();
- }
- }
-
- if lang.is_empty() {
- let mut buf = String::from("<pre>");
- md::escape::escape_html(&mut buf, &display).unwrap();
- buf.push_str("</pre>");
- return Html::new(buf);
- } else if !matches!(lang, "example" | "typ") {
- let set = &*typst_library::text::SYNTAXES;
- let buf = syntect::html::highlighted_html_for_string(
- &display,
- set,
- set.find_syntax_by_token(lang)
- .unwrap_or_else(|| panic!("unsupported highlighting language: {lang}")),
- &typst_library::text::THEME,
- )
- .expect("failed to highlight code");
- return Html::new(buf);
- }
-
- let root = typst::syntax::parse(&display);
- let highlighted = Html::new(typst::ide::highlight_html(&root));
- if lang == "typ" {
- return Html::new(format!("<pre>{}</pre>", highlighted.as_str()));
- }
-
- let id = FileId::new(None, Path::new("/main.typ"));
- let source = Source::new(id, compile);
- let world = DocWorld(source);
- let mut frames = match typst::compile(&world) {
- Ok(doc) => doc.pages,
- Err(err) => {
- let msg = &err[0].message;
- panic!("while trying to compile:\n{text}:\n\nerror: {msg}");
- }
- };
-
- if let Some([x, y, w, h]) = zoom {
- frames[0].translate(Point::new(-x, -y));
- *frames[0].size_mut() = Size::new(w, h);
- }
-
- if single {
- frames.truncate(1);
- }
-
- resolver.example(highlighted, &frames)
-}
-
-/// Extract an attribute value from an HTML element.
-fn html_attr<'a>(html: &'a str, attr: &str) -> Option<&'a str> {
- html.get(html_attr_range(html, attr)?)
-}
-
-/// Extract the range of the attribute value of an HTML element.
-fn html_attr_range(html: &str, attr: &str) -> Option<Range<usize>> {
- let needle = format!("{attr}=\"");
- let offset = html.find(&needle)? + needle.len();
- let len = html[offset..].find('"')?;
- Some(offset..offset + len)
-}
-
-/// Increase the nesting level of a Markdown heading.
-fn nest_heading(level: &mut md::HeadingLevel) {
- *level = match &level {
- md::HeadingLevel::H1 => md::HeadingLevel::H2,
- md::HeadingLevel::H2 => md::HeadingLevel::H3,
- md::HeadingLevel::H3 => md::HeadingLevel::H4,
- md::HeadingLevel::H4 => md::HeadingLevel::H5,
- md::HeadingLevel::H5 => md::HeadingLevel::H6,
- v => **v,
- };
-}
-
-/// A world for example compilations.
-struct DocWorld(Source);
-
-impl World for DocWorld {
- fn library(&self) -> &Prehashed<Library> {
- &LIBRARY
- }
-
- fn book(&self) -> &Prehashed<FontBook> {
- &FONTS.0
- }
-
- fn main(&self) -> Source {
- self.0.clone()
- }
-
- fn source(&self, _: FileId) -> FileResult<Source> {
- Ok(self.0.clone())
- }
-
- fn file(&self, id: FileId) -> FileResult<Bytes> {
- assert!(id.package().is_none());
- Ok(FILES
- .get_file(id.path().strip_prefix("/").unwrap())
- .unwrap_or_else(|| panic!("failed to load {:?}", id.path().display()))
- .contents()
- .into())
- }
-
- fn font(&self, index: usize) -> Option<Font> {
- Some(FONTS.1[index].clone())
- }
-
- fn today(&self, _: Option<i64>) -> Option<Datetime> {
- Some(Datetime::from_ymd(1970, 1, 1).unwrap())
- }
-}