summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--library/src/compute/data.rs132
-rw-r--r--library/src/meta/document.rs12
-rw-r--r--library/src/meta/link.rs37
-rw-r--r--library/src/meta/outline.rs61
-rw-r--r--library/src/meta/reference.rs12
-rw-r--r--macros/src/func.rs1
-rw-r--r--macros/src/lib.rs5
-rw-r--r--src/model/eval.rs11
8 files changed, 256 insertions, 15 deletions
diff --git a/library/src/compute/data.rs b/library/src/compute/data.rs
index 5de3eb61..ed87f78d 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<Value> {
let mut builder = csv::ReaderBuilder::new();
builder.has_headers(false);
+ if let Some(delimiter) = args.named::<Delimiter>("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<Value> {
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 e20821c5..c5d4e050 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<PageNode>);
#[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<EcoString> = None;
diff --git a/library/src/meta/link.rs b/library/src/meta/link.rs
index 6a2f66df..3b816083 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 6ea65391..47453526 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<Smart<Content>> = 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<NonZeroUsize> = 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<EcoString> = Some('.'.into());
+ pub const FILL: Option<Content> = Some(TextNode::packed("."));
fn construct(_: &Vm, _: &mut Args) -> SourceResult<Content> {
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 c49ff970..1a4b22e4 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 <intro>]` 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 62fbfd72..c830a32f 100644
--- a/macros/src/func.rs
+++ b/macros/src/func.rs
@@ -112,7 +112,6 @@ pub fn example(docs: &mut String) -> Option<String> {
.skip_while(|line| !line.contains("```"))
.skip(1)
.take_while(|line| !line.contains("```"))
- .map(|s| s.trim())
.collect::<Vec<_>>()
.join("\n"),
)
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index 23b03712..064e45b2 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::<Vec<_>>().join("\n")
+ text.lines()
+ .map(|s| s.strip_prefix(" ").unwrap_or(s))
+ .collect::<Vec<_>>()
+ .join("\n")
}
/// Quote an optional value.
diff --git a/src/model/eval.rs b/src/model/eval.rs
index a9fa2e14..91fe61bb 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<Self::Output> {
- 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()
+ ),
})
}
}