From b8ffd3ad3dcaebddbc674e03494e0d818b21fa51 Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Tue, 20 Dec 2022 15:31:36 +0100 Subject: [PATCH] Document meta and data loading categories --- library/src/compute/data.rs | 132 +++++++++++++++++++++++++++++++++- library/src/meta/document.rs | 12 +++- library/src/meta/link.rs | 37 +++++++++- library/src/meta/outline.rs | 61 ++++++++++++++-- library/src/meta/reference.rs | 12 ++++ macros/src/func.rs | 1 - macros/src/lib.rs | 5 +- src/model/eval.rs | 11 ++- 8 files changed, 256 insertions(+), 15 deletions(-) diff --git a/library/src/compute/data.rs b/library/src/compute/data.rs index 5de3eb617..ed87f78d7 100644 --- a/library/src/compute/data.rs +++ b/library/src/compute/data.rs @@ -6,9 +6,28 @@ use crate::prelude::*; /// Read structured data from a CSV file. /// +/// The CSV file will be read and parsed into a 2-dimensional array of strings: +/// Each row in the CSV file will be represented as an array of strings, and all +/// rows will be collected into a single array. Header rows will not be +/// stripped. +/// +/// # Example +/// ``` +/// #let results = csv("/data.csv") +/// +/// #table( +/// columns: 2, +/// [*Condition*], [*Result*], +/// ..results.flatten(), +/// ) +/// ``` /// # Parameters /// - path: EcoString (positional, required) /// Path to a CSV file. +/// - delimiter: Delimiter (named) +/// The delimiter that separates columns in the CSV file. +/// Must be a single ASCII character. +/// Defaults to a comma. /// /// # Tags /// - data-loading @@ -23,6 +42,10 @@ pub fn csv(vm: &Vm, args: &mut Args) -> SourceResult { let mut builder = csv::ReaderBuilder::new(); builder.has_headers(false); + if let Some(delimiter) = args.named::("delimiter")? { + builder.delimiter(delimiter.0); + } + let mut reader = builder.from_reader(data.as_slice()); let mut vec = vec![]; @@ -35,6 +58,26 @@ pub fn csv(vm: &Vm, args: &mut Args) -> SourceResult { Ok(Value::Array(Array::from_vec(vec))) } +/// The delimiter to use when parsing CSV files. +struct Delimiter(u8); + +castable! { + Delimiter, + v: EcoString => { + let mut chars = v.chars(); + let first = chars.next().ok_or("delimiter must not be empty")?; + if chars.next().is_some() { + Err("delimiter must be a single character")? + } + + if !first.is_ascii() { + Err("delimiter must be an ASCII character")? + } + + Self(first as u8) + }, +} + /// Format the user-facing CSV error message. fn format_csv_error(error: csv::Error) -> String { match error.kind() { @@ -54,6 +97,43 @@ fn format_csv_error(error: csv::Error) -> String { /// Read structured data from a JSON file. /// +/// The file must contain a valid JSON object or array. JSON objects will be +/// converted into Typst dictionaries, and JSON arrays will be converted into +/// Typst arrays. Strings and booleans will be converted into the Typst +/// equivalents, `null` will be converted into `{none}`, and numbers will be +/// converted to floats or integers depending on whether they are whole numbers. +/// +/// The function returns a dictionary or an array, depending on the JSON file. +/// +/// The JSON files in the example contain a object with the keys `temperature`, +/// `unit`, and `weather`. +/// +/// # Example +/// ``` +/// #let forecast(day) = block[ +/// #square( +/// width: 2cm, +/// inset: 8pt, +/// fill: if day.weather == "sunny" { +/// yellow +/// } else { +/// aqua +/// }, +/// align( +/// bottom + right, +/// strong(day.weather), +/// ), +/// ) +/// #h(6pt) +/// #set text(22pt, baseline: -8pt) +/// {day.temperature} +/// °{day.unit} +/// ] +/// +/// #forecast(json("/monday.json")) +/// #forecast(json("/tuesday.json")) +/// ``` +/// /// # Parameters /// - path: EcoString (positional, required) /// Path to a JSON file. @@ -97,11 +177,61 @@ fn convert_json(value: serde_json::Value) -> Value { /// Format the user-facing JSON error message. fn format_json_error(error: serde_json::Error) -> String { assert!(error.is_syntax() || error.is_eof()); - format!("failed to parse json file: syntax error in line {}", error.line()) + format!( + "failed to parse json file: syntax error in line {}", + error.line() + ) } /// Read structured data from an XML file. /// +/// The XML file is parsed into an array of dictionaries and strings. XML nodes +/// can be elements or strings. Elements are represented as dictionaries with +/// the the following keys: +/// +/// - `tag`: The name of the element as a string. +/// - `attrs`: A dictionary of the element's attributes as strings. +/// - `children`: An array of the element's child nodes. +/// +/// The XML file in the example contains a root `news` tag with multiple +/// `article` tags. Each article has a `title`, `author`, and `content` tag. The +/// `content` tag contains one or more paragraphs, which are represented as `p` +/// tags. +/// +/// # Example +/// ``` +/// #let findChild(elem, tag) = { +/// elem.children +/// .find(e => "tag" in e and e.tag == tag) +/// } +/// +/// #let article(elem) = { +/// let title = findChild(elem, "title") +/// let author = findChild(elem, "author") +/// let pars = findChild(elem, "content") +/// +/// heading((title.children)(0)) +/// text(10pt, weight: "medium")[ +/// Published by +/// {(author.children)(0)} +/// ] +/// +/// for p in pars.children { +/// if (type(p) == "dictionary") { +/// parbreak() +/// (p.children)(0) +/// } +/// } +/// } +/// +/// #let file = xml("/example.xml") +/// #for child in file(0).children { +/// if (type(child) == "dictionary") { +/// article(child) +/// } +/// } +/// ``` +/// /// # Parameters /// - path: EcoString (positional, required) /// Path to an XML file. diff --git a/library/src/meta/document.rs b/library/src/meta/document.rs index e20821c56..c5d4e0503 100644 --- a/library/src/meta/document.rs +++ b/library/src/meta/document.rs @@ -1,7 +1,14 @@ use crate::layout::{LayoutRoot, PageNode}; use crate::prelude::*; -/// The root node that represents a full document. +/// The root element of a document and its metadata. +/// +/// All documents are automatically wrapped in a `document` element. The main +/// use of this element is to use it in `set` rules to specify document +/// metadata. +/// +/// The metadata set with this function is not rendered within the document. +/// Instead, it is embedded in the compiled PDF file. /// /// # Tags /// - meta @@ -12,7 +19,8 @@ pub struct DocumentNode(pub StyleVec); #[node] impl DocumentNode { - /// The document's title. + /// The document's title. This is often rendered as the title of the + /// PDF viewer window. #[property(referenced)] pub const TITLE: Option = None; diff --git a/library/src/meta/link.rs b/library/src/meta/link.rs index 6a2f66dfe..3b816083d 100644 --- a/library/src/meta/link.rs +++ b/library/src/meta/link.rs @@ -1,14 +1,47 @@ use crate::prelude::*; use crate::text::TextNode; -/// Link text and other elements to a destination. +/// Link to a URL or another location in the document. +/// +/// The link function makes its positional `body` argument clickable and links +/// it to the destination specified by the `dest` argument. +/// +/// # Example +/// ``` +/// #show link: underline +/// +/// #link("https://example.com") \ +/// #link("https://example.com")[ +/// See example.com +/// ] +/// ``` /// /// # Parameters /// - dest: Destination (positional, required) /// The destination the link points to. +/// +/// - To link to web pages, `dest` should be a valid URL string. If the URL is +/// in the `mailto:` or `tel:` scheme and the `body` parameter is omitted, +/// the email address or phone number will be the link's body, without the +/// scheme. +/// +/// - To link to another part of the document, `dest` must contain a +/// dictionary with a `page` key of type `integer` and `x` and `y` +/// coordinates of type `length`. Pages are counted from one, and the +/// coordinates are relative to the page's top left corner. +/// +/// # Example +/// ``` +/// #link("mailto:hello@typst.app") \ +/// #link((page: 1, x: 0pt, y: 0pt))[ +/// Go to top +/// ] +/// ``` /// /// - body: Content (positional) -/// How the link is represented. Defaults to the destination if it is a link. +/// +/// The content that should become a link. If `dest` is an URL string, the +/// parameter can be omitted. In this case, the URL will be shown as the link. /// /// # Tags /// - meta diff --git a/library/src/meta/outline.rs b/library/src/meta/outline.rs index 6ea653914..474535266 100644 --- a/library/src/meta/outline.rs +++ b/library/src/meta/outline.rs @@ -3,7 +3,22 @@ use crate::layout::{BlockNode, HNode, HideNode, RepeatNode, Spacing}; use crate::prelude::*; use crate::text::{LinebreakNode, SpaceNode, TextNode}; -/// A section outline (table of contents). +/// Generate a section outline / table of contents. +/// +/// This function generates a list of all headings in the +/// document, up to a given depth. The [@heading] numbering will be reproduced +/// within the outline. +/// +/// # Example +/// ``` +/// #outline() +/// +/// = Introduction +/// #lorem(5) +/// +/// = Prior work +/// #lorem(10) +/// ``` /// /// # Tags /// - meta @@ -15,18 +30,52 @@ pub struct OutlineNode; #[node] impl OutlineNode { /// The title of the outline. + /// + /// - When set to `{auto}`, an appropriate title for the [@text] language will + /// be used. This is the default. + /// - When set to `{none}`, the outline will not have a title. + /// - A custom title can be set by passing content. #[property(referenced)] pub const TITLE: Option> = Some(Smart::Auto); - /// The maximum depth up to which headings are included in the outline. + /// The maximum depth up to which headings are included in the outline. When + /// this arguement is `{none}`, all headings are included. pub const DEPTH: Option = None; - /// Whether to indent the subheadings to match their parents. + /// Whether to indent the subheadings to align the start of their numbering + /// with the title of their parents. This will only have an effect if a + /// [@heading] numbering is set. + /// + /// # Example + /// ``` + /// #set heading(numbering: "1.a.") + /// + /// #outline(indent: true) + /// + /// = About ACME Corp. + /// + /// == History + /// #lorem(10) + /// + /// == Products + /// #lorem(10) + /// ``` pub const INDENT: bool = false; - /// The fill symbol. + /// The symbol used to fill the space between the title and the page + /// number. Can be set to `none` to disable filling. The default is a + /// single dot. + /// + /// # Example + /// ``` + /// #outline( + /// fill: pad(x: -1.5pt)[―] + /// ) + /// + /// = A New Beginning + /// ``` #[property(referenced)] - pub const FILL: Option = Some('.'.into()); + pub const FILL: Option = Some(TextNode::packed(".")); fn construct(_: &Vm, _: &mut Args) -> SourceResult { Ok(Self.pack()) @@ -132,7 +181,7 @@ impl Show for OutlineNode { // Add filler symbols between the section name and page number. if let Some(filler) = styles.get(Self::FILL) { seq.push(SpaceNode.pack()); - seq.push(RepeatNode(TextNode::packed(filler.clone())).pack()); + seq.push(RepeatNode(filler.clone()).pack()); seq.push(SpaceNode.pack()); } else { let amount = Spacing::Fractional(Fr::one()); diff --git a/library/src/meta/reference.rs b/library/src/meta/reference.rs index c49ff9705..1a4b22e41 100644 --- a/library/src/meta/reference.rs +++ b/library/src/meta/reference.rs @@ -3,6 +3,18 @@ use crate::text::TextNode; /// A reference to a label. /// +/// *Note: This function is currently unimplemented.* +/// +/// The reference function produces a textual reference to a label. For example, +/// a reference to a heading will yield an appropriate string such as "Section +/// 1" for a reference to the first heading's label. The references are also +/// links to the respective labels. +/// +/// # Syntax +/// This function also has dedicated syntax: A reference to a label can be +/// created by typing an `@` followed by the name of the label (e.g. `[= +/// Introduction ]` can be referenced by typing `[@intro]`). +/// /// # Parameters /// - target: Label (positional, required) /// The label that should be referenced. diff --git a/macros/src/func.rs b/macros/src/func.rs index 62fbfd72e..c830a32f4 100644 --- a/macros/src/func.rs +++ b/macros/src/func.rs @@ -112,7 +112,6 @@ pub fn example(docs: &mut String) -> Option { .skip_while(|line| !line.contains("```")) .skip(1) .take_while(|line| !line.contains("```")) - .map(|s| s.trim()) .collect::>() .join("\n"), ) diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 23b03712e..064e45b2a 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -91,7 +91,10 @@ fn documentation(attrs: &[syn::Attribute]) -> String { /// Dedent documentation text. fn dedent(text: &str) -> String { - text.lines().map(str::trim).collect::>().join("\n") + text.lines() + .map(|s| s.strip_prefix(" ").unwrap_or(s)) + .collect::>() + .join("\n") } /// Quote an optional value. diff --git a/src/model/eval.rs b/src/model/eval.rs index a9fa2e145..91fe61bb8 100644 --- a/src/model/eval.rs +++ b/src/model/eval.rs @@ -476,7 +476,10 @@ impl Eval for ast::Frac { type Output = Content; fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items.math_frac)(self.num().eval(vm)?, self.denom().eval(vm)?)) + Ok((vm.items.math_frac)( + self.num().eval(vm)?, + self.denom().eval(vm)?, + )) } } @@ -778,7 +781,11 @@ impl Eval for ast::FieldAccess { .field(&field) .ok_or_else(|| format!("unknown field {field:?}")) .at(span)?, - v => bail!(self.target().span(), "cannot access field on {}", v.type_name()), + v => bail!( + self.target().span(), + "cannot access field on {}", + v.type_name() + ), }) } }