Query-System for metadata (#1812)

Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
Beiri22 2023-08-06 23:49:04 +02:00 committed by GitHub
parent 823fc5e5c4
commit 357bce56f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 423 additions and 52 deletions

67
Cargo.lock generated
View file

@ -450,7 +450,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
dependencies = [
"cfg-if",
"hashbrown",
"hashbrown 0.12.3",
"lock_api",
"once_cell",
"parking_lot_core",
@ -531,6 +531,12 @@ dependencies = [
"log",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.1"
@ -738,6 +744,12 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
[[package]]
name = "hayagriva"
version = "0.3.0"
@ -987,8 +999,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown",
"hashbrown 0.12.3",
"rayon",
"serde",
]
[[package]]
name = "indexmap"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"equivalent",
"hashbrown 0.14.0",
]
[[package]]
@ -1003,7 +1026,7 @@ dependencies = [
"crossbeam-utils",
"dashmap",
"env_logger",
"indexmap",
"indexmap 1.9.3",
"is-terminal",
"itoa",
"log",
@ -1452,7 +1475,7 @@ dependencies = [
"crossbeam-channel",
"filetime",
"image",
"indexmap",
"indexmap 1.9.3",
"itertools",
"libdeflater",
"log",
@ -1571,7 +1594,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590"
dependencies = [
"base64",
"indexmap",
"indexmap 1.9.3",
"line-wrap",
"quick-xml 0.28.2",
"serde",
@ -2015,12 +2038,25 @@ version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b"
dependencies = [
"indexmap",
"indexmap 1.9.3",
"ryu",
"serde",
"yaml-rust",
]
[[package]]
name = "serde_yaml"
version = "0.9.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574"
dependencies = [
"indexmap 2.0.0",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "sharded-slab"
version = "0.1.4"
@ -2413,7 +2449,7 @@ version = "0.19.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d964908cec0d030b812013af25a0e57fddfadb1e066ecc6681d86253129d4f"
dependencies = [
"indexmap",
"indexmap 1.9.3",
"serde",
"serde_spanned",
"toml_datetime",
@ -2523,7 +2559,7 @@ dependencies = [
"fontdb",
"if_chain",
"image",
"indexmap",
"indexmap 1.9.3",
"log",
"miniz_oxide",
"oklab",
@ -2573,6 +2609,9 @@ dependencies = [
"once_cell",
"open",
"same-file",
"serde",
"serde_json",
"serde_yaml 0.9.25",
"siphasher",
"tar",
"tempfile",
@ -2596,7 +2635,7 @@ dependencies = [
"once_cell",
"pulldown-cmark",
"serde",
"serde_yaml",
"serde_yaml 0.8.26",
"syntect",
"typed-arena",
"typst",
@ -2629,7 +2668,7 @@ dependencies = [
"roxmltree",
"rustybuzz",
"serde_json",
"serde_yaml",
"serde_yaml 0.8.26",
"smallvec",
"syntect",
"time",
@ -2795,6 +2834,12 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "446c96c6dd42604779487f0a981060717156648c1706aa1f464677f03c6cc059"
[[package]]
name = "unsafe-libyaml"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa"
[[package]]
name = "unscanny"
version = "0.1.0"
@ -3243,7 +3288,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a94fb32d2b438e3fddf901fbfe9eb87b34d63853ca6c6da5d2ab7e27031e0bae"
dependencies = [
"serde",
"serde_yaml",
"serde_yaml 0.8.26",
]
[[package]]

View file

@ -34,6 +34,9 @@ notify = "5"
once_cell = "1"
open = "4.0.2"
same-file = "1"
serde = "1"
serde_json = "1"
serde_yaml = "0.9"
siphasher = "0.3"
tar = "0.4"
tempfile = "3.5.0"

View file

@ -1,7 +1,7 @@
use std::fmt::{self, Display, Formatter};
use std::path::PathBuf;
use clap::{ArgAction, Parser, Subcommand, ValueEnum};
use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
/// The Typst compiler.
#[derive(Debug, Clone, Parser)]
@ -29,6 +29,9 @@ pub enum Command {
#[command(visible_alias = "w")]
Watch(CompileCommand),
/// Processes an input file to extract provided metadata
Query(QueryCommand),
/// Lists all discovered fonts in system and custom font paths
Fonts(FontsCommand),
}
@ -36,12 +39,71 @@ pub enum Command {
/// Compiles the input file into a PDF file
#[derive(Debug, Clone, Parser)]
pub struct CompileCommand {
/// Path to input Typst file
pub input: PathBuf,
/// Shared arguments.
#[clap(flatten)]
pub common: SharedArgs,
/// Path to output PDF file or PNG file(s)
pub output: Option<PathBuf>,
/// Opens the output file using the default viewer after compilation
#[arg(long = "open")]
pub open: Option<Option<String>>,
/// The PPI (pixels per inch) to use for PNG export
#[arg(long = "ppi", default_value_t = 144.0)]
pub ppi: f32,
/// Produces a flamegraph of the compilation process
#[arg(long = "flamegraph", value_name = "OUTPUT_SVG")]
pub flamegraph: Option<Option<PathBuf>>,
}
impl CompileCommand {
/// The output path.
pub fn output(&self) -> PathBuf {
self.output
.clone()
.unwrap_or_else(|| self.common.input.with_extension("pdf"))
}
}
/// Processes an input file to extract provided metadata
#[derive(Debug, Clone, Parser)]
pub struct QueryCommand {
/// Shared arguments.
#[clap(flatten)]
pub common: SharedArgs,
/// Define what elements to retrieve
pub selector: String,
/// Extract just one field from all retrieved elements
#[clap(long = "field")]
pub field: Option<String>,
/// Expect and retrieve exactly one element
#[clap(long = "one", default_value = "false")]
pub one: bool,
/// The format to serialization in
#[clap(long = "format", default_value = "json")]
pub format: SerializationFormat,
}
// Output file format for query command
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
pub enum SerializationFormat {
Json,
Yaml,
}
/// Common arguments of compile, watch, and query.
#[derive(Debug, Clone, Args)]
pub struct SharedArgs {
/// Path to input Typst file
pub input: PathBuf,
/// Configures the project root
#[clap(long = "root", env = "TYPST_ROOT", value_name = "DIR")]
pub root: Option<PathBuf>,
@ -55,14 +117,6 @@ pub struct CompileCommand {
)]
pub font_paths: Vec<PathBuf>,
/// Opens the output file using the default viewer after compilation
#[arg(long = "open")]
pub open: Option<Option<String>>,
/// The PPI (pixels per inch) to use for PNG export
#[arg(long = "ppi", default_value_t = 144.0)]
pub ppi: f32,
/// In which format to emit diagnostics
#[clap(
long,
@ -70,19 +124,6 @@ pub struct CompileCommand {
value_parser = clap::value_parser!(DiagnosticFormat)
)]
pub diagnostic_format: DiagnosticFormat,
/// Produces a flamegraph of the compilation process
#[arg(long = "flamegraph", value_name = "OUTPUT_SVG")]
pub flamegraph: Option<Option<PathBuf>>,
}
impl CompileCommand {
/// The output path.
pub fn output(&self) -> PathBuf {
self.output
.clone()
.unwrap_or_else(|| self.input.with_extension("pdf"))
}
}
/// Lists all discovered fonts in system and custom font paths

View file

@ -21,7 +21,7 @@ type CodespanError = codespan_reporting::files::Error;
/// Execute a compilation command.
pub fn compile(mut command: CompileCommand) -> StrResult<()> {
let mut world = SystemWorld::new(&command)?;
let mut world = SystemWorld::new(&command.common)?;
compile_once(&mut world, &mut command, false)?;
Ok(())
}
@ -42,14 +42,12 @@ pub fn compile_once(
Status::Compiling.print(command).unwrap();
}
// Reset everything and ensure that the main file is still present.
// Reset everything and ensure that the main file is present.
world.reset();
world.source(world.main()).map_err(|err| err.to_string())?;
let mut tracer = Tracer::default();
let result = typst::compile(world, &mut tracer);
let warnings = tracer.warnings();
match result {
@ -67,7 +65,7 @@ pub fn compile_once(
}
}
print_diagnostics(world, &[], &warnings, command.diagnostic_format)
print_diagnostics(world, &[], &warnings, command.common.diagnostic_format)
.map_err(|_| "failed to print diagnostics")?;
if let Some(open) = command.open.take() {
@ -84,8 +82,13 @@ pub fn compile_once(
Status::Error.print(command).unwrap();
}
print_diagnostics(world, &errors, &warnings, command.diagnostic_format)
.map_err(|_| "failed to print diagnostics")?;
print_diagnostics(
world,
&errors,
&warnings,
command.common.diagnostic_format,
)
.map_err(|_| "failed to print diagnostics")?;
}
}
@ -152,7 +155,7 @@ fn open_file(open: Option<&str>, path: &Path) -> StrResult<()> {
}
/// Print diagnostic messages to the terminal.
fn print_diagnostics(
pub fn print_diagnostics(
world: &SystemWorld,
errors: &[SourceDiagnostic],
warnings: &[SourceDiagnostic],

View file

@ -2,6 +2,7 @@ mod args;
mod compile;
mod fonts;
mod package;
mod query;
mod tracing;
mod watch;
mod world;
@ -36,6 +37,7 @@ fn main() -> ExitCode {
let res = match arguments.command {
Command::Compile(command) => crate::compile::compile(command),
Command::Watch(command) => crate::watch::watch(command),
Command::Query(command) => crate::query::query(command),
Command::Fonts(command) => crate::fonts::fonts(command),
};

View file

@ -0,0 +1,114 @@
use comemo::Track;
use serde::Serialize;
use typst::diag::{bail, StrResult};
use typst::eval::{eval_string, EvalMode, Tracer};
use typst::model::Introspector;
use typst::World;
use typst_library::prelude::*;
use crate::args::{QueryCommand, SerializationFormat};
use crate::compile::print_diagnostics;
use crate::set_failed;
use crate::world::SystemWorld;
/// Execute a query command.
pub fn query(command: QueryCommand) -> StrResult<()> {
let mut world = SystemWorld::new(&command.common)?;
tracing::info!("Starting querying");
// Reset everything and ensure that the main file is present.
world.reset();
world.source(world.main()).map_err(|err| err.to_string())?;
let mut tracer = Tracer::default();
let result = typst::compile(&world, &mut tracer);
let warnings = tracer.warnings();
match result {
// Retrieve and print query results.
Ok(document) => {
let data = retrieve(&world, &command, &document)?;
let serialized = format(data, &command)?;
println!("{serialized}");
print_diagnostics(&world, &[], &warnings, command.common.diagnostic_format)
.map_err(|_| "failed to print diagnostics")?;
}
// Print diagnostics.
Err(errors) => {
set_failed();
print_diagnostics(
&world,
&errors,
&warnings,
command.common.diagnostic_format,
)
.map_err(|_| "failed to print diagnostics")?;
}
}
Ok(())
}
/// Retrieve the matches for the selector.
fn retrieve(
world: &dyn World,
command: &QueryCommand,
document: &Document,
) -> StrResult<Vec<Content>> {
let selector = eval_string(
world.track(),
&command.selector,
Span::detached(),
EvalMode::Code,
Scope::default(),
)
.map_err(|errors| {
let mut message = EcoString::from("failed to evaluate selector");
for (i, error) in errors.into_iter().enumerate() {
message.push_str(if i == 0 { ": " } else { ", " });
message.push_str(&error.message);
}
message
})?
.cast::<LocatableSelector>()?;
Ok(Introspector::new(&document.pages)
.query(&selector.0)
.into_iter()
.map(|x| x.into_inner())
.collect::<Vec<_>>())
}
/// Format the query result in the output format.
fn format(elements: Vec<Content>, command: &QueryCommand) -> StrResult<String> {
if command.one && elements.len() != 1 {
bail!("expected exactly one element, found {}", elements.len())
}
let mapped: Vec<_> = elements
.into_iter()
.filter_map(|c| match &command.field {
Some(field) => c.field(field),
_ => Some(c.into_value()),
})
.collect();
if command.one {
serialize(&mapped[0], command.format)
} else {
serialize(&mapped, command.format)
}
}
/// Serialize data to the output format.
fn serialize(data: &impl Serialize, format: SerializationFormat) -> StrResult<String> {
match format {
SerializationFormat::Json => {
serde_json::to_string_pretty(data).map_err(|e| eco_format!("{e}"))
}
SerializationFormat::Yaml => {
serde_yaml::to_string(&data).map_err(|e| eco_format!("{e}"))
}
}
}

View file

@ -17,7 +17,7 @@ use crate::world::SystemWorld;
/// Execute a watching compilation command.
pub fn watch(mut command: CompileCommand) -> StrResult<()> {
// Create the world that serves sources, files, and fonts.
let mut world = SystemWorld::new(&command)?;
let mut world = SystemWorld::new(&command.common)?;
// Perform initial compilation.
compile_once(&mut world, &mut command, true)?;
@ -159,7 +159,7 @@ impl Status {
w.set_color(&color)?;
write!(w, "watching")?;
w.reset()?;
writeln!(w, " {}", command.input.display())?;
writeln!(w, " {}", command.common.input.display())?;
w.set_color(&color)?;
write!(w, "writing to")?;

View file

@ -15,7 +15,7 @@ use typst::syntax::{FileId, Source};
use typst::util::PathExt;
use typst::World;
use crate::args::CompileCommand;
use crate::args::SharedArgs;
use crate::fonts::{FontSearcher, FontSlot};
use crate::package::prepare_package;
@ -44,7 +44,7 @@ pub struct SystemWorld {
impl SystemWorld {
/// Create a new system world.
pub fn new(command: &CompileCommand) -> StrResult<Self> {
pub fn new(command: &SharedArgs) -> StrResult<Self> {
let mut searcher = FontSearcher::new();
searcher.search(&command.font_paths);

View file

@ -0,0 +1,87 @@
use crate::prelude::*;
/// Exposes a value to the query system without producing visible content.
///
/// This element can be queried for with the [`query`]($func/query) function and
/// the command line `typst query` command. Its purpose is to expose an
/// arbitrary value to the introspection system. To identify a metadata value
/// among others, you can attach a [`label`]($type/label) to it and query for
/// that label.
///
/// ```typ
/// #metadata("This is a note") <note>
/// ```
///
/// ## Within Typst: `query` function { #within-typst }
/// Metadata can be retrieved from with the [`query`]($func/query) function
/// (like other elements):
///
/// ```example
/// // Put metadata somewhere.
/// #metadata("This is a note") <note>
///
/// // And find it from anywhere else.
/// #locate(loc => {
/// query(<note>, loc).first().value
/// })
/// ```
///
/// ## Outside of Typst: `typst query` command { #outside-of-typst }
/// You can also retrieve the metadata from the command line with the
/// `typst query` command. This command executes an arbitrary query on the
/// document and returns the resulting elements in serialized form.
///
/// The `metadata` element is especially useful for command line queries because
/// it allows you to expose arbitrary values to the outside world. However,
/// `typst query` also works with other elements `metadata` and complex
/// [selectors]($type/selector) like `{heading.where(level: 1)}`.
///
/// ```sh
/// $ typst query example.typ "<note>"
/// [
/// {
/// "func": "metadata",
/// "value": "This is a note",
/// "label": "<note>"
/// }
/// ]
/// ```
///
/// Frequently, you're interested in only one specific field of the resulting
/// elements. In the case of the `metadata` element, the `value` field is the
/// interesting one. You can extract just this field with the `--field`
/// argument.
///
/// ```sh
/// $ typst query example.typ "<note>" --field value
/// ["This is a note"]
/// ```
///
/// If you are interested in just a single element, you can use the `--one`
/// flag to extract just it.
///
/// ```sh
/// $ typst query example.typ "<note>" --field value --one
/// "This is a note"
/// ```
///
/// Display: Metadata
/// Category: meta
#[element(Behave, Show, Locatable)]
pub struct MetadataElem {
/// The value to embed into the document.
#[required]
pub value: Value,
}
impl Show for MetadataElem {
fn show(&self, _vt: &mut Vt, _styles: StyleChain) -> SourceResult<Content> {
Ok(Content::empty())
}
}
impl Behave for MetadataElem {
fn behaviour(&self) -> Behaviour {
Behaviour::Ignorant
}
}

View file

@ -8,6 +8,7 @@ mod figure;
mod footnote;
mod heading;
mod link;
mod metadata;
mod numbering;
mod outline;
mod query;
@ -22,6 +23,7 @@ pub use self::figure::*;
pub use self::footnote::*;
pub use self::heading::*;
pub use self::link::*;
pub use self::metadata::*;
pub use self::numbering::*;
pub use self::outline::*;
pub use self::query::*;
@ -50,6 +52,7 @@ pub(super) fn define(global: &mut Scope) {
global.define("state", state_func());
global.define("query", query_func());
global.define("selector", selector_func());
global.define("metadata", MetadataElem::func());
}
/// The named with which an element is referenced.

View file

@ -26,7 +26,7 @@ flate2 = "1"
fontdb = "0.13"
if_chain = "1"
image = { version = "0.24", default-features = false, features = ["png", "jpeg", "gif"] }
indexmap = "1.9.3"
indexmap = { version = "1.9.3", features = ["serde"] }
log = "0.4"
miniz_oxide = "0.7"
oklab = "1"

View file

@ -3,9 +3,11 @@ use std::fmt::{self, Debug, Formatter};
use std::ops::{Add, AddAssign};
use ecow::{eco_format, EcoString, EcoVec};
use serde::Serialize;
use super::{ops, Args, CastInfo, FromValue, Func, IntoValue, Reflect, Value, Vm};
use crate::diag::{At, SourceResult, StrResult};
use crate::eval::ops::{add, mul};
use crate::syntax::Span;
use crate::util::pretty_array_like;
@ -29,12 +31,11 @@ macro_rules! __array {
#[doc(inline)]
pub use crate::__array as array;
use crate::eval::ops::{add, mul};
#[doc(hidden)]
pub use ecow::eco_vec;
/// A reference counted array with value semantics.
#[derive(Default, Clone, PartialEq, Hash)]
#[derive(Default, Clone, PartialEq, Hash, Serialize)]
pub struct Array(EcoVec<Value>);
impl Array {

View file

@ -5,6 +5,7 @@ use std::sync::Arc;
use comemo::Prehashed;
use ecow::{eco_format, EcoString};
use serde::{Serialize, Serializer};
use crate::diag::StrResult;
@ -95,6 +96,19 @@ impl Debug for Bytes {
}
}
impl Serialize for Bytes {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if serializer.is_human_readable() {
serializer.serialize_str(&eco_format!("{self:?}"))
} else {
serializer.serialize_bytes(self)
}
}
}
/// The out of bounds access error message.
#[cold]
fn out_of_bounds(index: i64, len: usize) -> EcoString {

View file

@ -4,6 +4,7 @@ use std::ops::{Add, AddAssign};
use std::sync::Arc;
use ecow::{eco_format, EcoString};
use serde::{Serialize, Serializer};
use super::{array, Array, Str, Value};
use crate::diag::StrResult;
@ -188,6 +189,15 @@ impl Hash for Dict {
}
}
impl Serialize for Dict {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0.serialize(serializer)
}
}
impl Extend<(Str, Value)> for Dict {
fn extend<T: IntoIterator<Item = (Str, Value)>>(&mut self, iter: T) {
Arc::make_mut(&mut self.0).extend(iter);

View file

@ -4,6 +4,7 @@ use std::hash::{Hash, Hasher};
use std::ops::{Add, AddAssign, Deref, Range};
use ecow::EcoString;
use serde::Serialize;
use unicode_segmentation::UnicodeSegmentation;
use super::{cast, dict, Args, Array, Dict, Func, IntoValue, Value, Vm};
@ -25,7 +26,7 @@ pub use crate::__format_str as format_str;
pub use ecow::eco_format;
/// An immutable reference counted string.
#[derive(Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[derive(Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
pub struct Str(EcoString);
impl Str {

View file

@ -4,6 +4,7 @@ use std::fmt::{self, Debug, Display, Formatter, Write};
use std::sync::Arc;
use ecow::EcoString;
use serde::{Serialize, Serializer};
use crate::diag::{bail, StrResult};
@ -135,6 +136,15 @@ impl Display for Symbol {
}
}
impl Serialize for Symbol {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_char(self.get())
}
}
impl List {
/// The characters that are covered by this list.
fn variants(&self) -> Variants<'_> {

View file

@ -5,6 +5,7 @@ use std::hash::{Hash, Hasher};
use std::sync::Arc;
use ecow::eco_format;
use serde::{Serialize, Serializer};
use siphasher::sip128::{Hasher128, SipHasher13};
use super::{
@ -250,6 +251,29 @@ impl Hash for Value {
}
}
impl Serialize for Value {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::None => serializer.serialize_none(),
Self::Bool(v) => serializer.serialize_bool(*v),
Self::Int(v) => serializer.serialize_i64(*v),
Self::Float(v) => serializer.serialize_f64(*v),
Self::Str(v) => v.serialize(serializer),
Self::Bytes(v) => v.serialize(serializer),
Self::Symbol(v) => v.serialize(serializer),
Self::Content(v) => v.serialize(serializer),
Self::Array(v) => v.serialize(serializer),
Self::Dict(v) => v.serialize(serializer),
// Fall back to repr() for other things.
other => serializer.serialize_str(&other.repr()),
}
}
}
/// A dynamic value.
#[derive(Clone, Hash)]
#[allow(clippy::derived_hash_with_manual_eq)]

View file

@ -1,10 +1,11 @@
use std::any::TypeId;
use std::fmt::{self, Debug, Formatter, Write};
use std::iter::Sum;
use std::iter::{self, Sum};
use std::ops::{Add, AddAssign};
use comemo::Prehashed;
use ecow::{eco_format, EcoString, EcoVec};
use serde::{Serialize, Serializer};
use super::{
element, Behave, Behaviour, ElemFunc, Element, Guard, Label, Locatable, Location,
@ -516,6 +517,18 @@ impl Sum for Content {
}
}
impl Serialize for Content {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_map(
iter::once((&"func".into(), self.func().name().into_value()))
.chain(self.fields()),
)
}
}
impl Attr {
fn child(&self) -> Option<&Content> {
match self {