mirror of
https://github.com/typst/typst
synced 2025-05-16 01:55:28 +08:00
Merge branch 'main' into main
This commit is contained in:
commit
d97039c50b
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -2753,7 +2753,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-assets"
|
name = "typst-assets"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
source = "git+https://github.com/typst/typst-assets?rev=8cccef9#8cccef93b5da73a1c80389722cf2b655b624f577"
|
source = "git+https://github.com/typst/typst-assets?rev=8536748#8536748e4350198f34e519adff8593f258259cca"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-cli"
|
name = "typst-cli"
|
||||||
@ -2901,6 +2901,8 @@ dependencies = [
|
|||||||
"native-tls",
|
"native-tls",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"openssl",
|
"openssl",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"tar",
|
"tar",
|
||||||
"typst-assets",
|
"typst-assets",
|
||||||
"typst-library",
|
"typst-library",
|
||||||
|
@ -32,7 +32,7 @@ typst-svg = { path = "crates/typst-svg", version = "0.12.0" }
|
|||||||
typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" }
|
typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" }
|
||||||
typst-timing = { path = "crates/typst-timing", version = "0.12.0" }
|
typst-timing = { path = "crates/typst-timing", version = "0.12.0" }
|
||||||
typst-utils = { path = "crates/typst-utils", version = "0.12.0" }
|
typst-utils = { path = "crates/typst-utils", version = "0.12.0" }
|
||||||
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "8cccef9" }
|
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "8536748" }
|
||||||
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "7f8999d" }
|
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "7f8999d" }
|
||||||
arrayvec = "0.7.4"
|
arrayvec = "0.7.4"
|
||||||
az = "1.2"
|
az = "1.2"
|
||||||
|
@ -306,7 +306,10 @@ fn complete_math(ctx: &mut CompletionContext) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Behind existing atom or identifier: "$a|$" or "$abc|$".
|
// Behind existing atom or identifier: "$a|$" or "$abc|$".
|
||||||
if matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathIdent) {
|
if matches!(
|
||||||
|
ctx.leaf.kind(),
|
||||||
|
SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathIdent
|
||||||
|
) {
|
||||||
ctx.from = ctx.leaf.offset();
|
ctx.from = ctx.leaf.offset();
|
||||||
math_completions(ctx);
|
math_completions(ctx);
|
||||||
return true;
|
return true;
|
||||||
@ -358,7 +361,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
|
|||||||
// Behind an expression plus dot: "emoji.|".
|
// Behind an expression plus dot: "emoji.|".
|
||||||
if_chain! {
|
if_chain! {
|
||||||
if ctx.leaf.kind() == SyntaxKind::Dot
|
if ctx.leaf.kind() == SyntaxKind::Dot
|
||||||
|| (ctx.leaf.kind() == SyntaxKind::Text
|
|| (matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathText)
|
||||||
&& ctx.leaf.text() == ".");
|
&& ctx.leaf.text() == ".");
|
||||||
if ctx.leaf.range().end == ctx.cursor;
|
if ctx.leaf.range().end == ctx.cursor;
|
||||||
if let Some(prev) = ctx.leaf.prev_sibling();
|
if let Some(prev) = ctx.leaf.prev_sibling();
|
||||||
@ -1768,4 +1771,14 @@ mod tests {
|
|||||||
test("#show outline.entry: it => it.\n#outline()\n= Hi", 30)
|
test("#show outline.entry: it => it.\n#outline()\n= Hi", 30)
|
||||||
.must_include(["indented", "body", "page"]);
|
.must_include(["indented", "body", "page"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_autocomplete_symbol_variants() {
|
||||||
|
test("#sym.arrow.", -1)
|
||||||
|
.must_include(["r", "dashed"])
|
||||||
|
.must_exclude(["cases"]);
|
||||||
|
test("$ arrow. $", -3)
|
||||||
|
.must_include(["r", "dashed"])
|
||||||
|
.must_exclude(["cases"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,10 @@ pub fn jump_from_click(
|
|||||||
let Some(id) = span.id() else { continue };
|
let Some(id) = span.id() else { continue };
|
||||||
let source = world.source(id).ok()?;
|
let source = world.source(id).ok()?;
|
||||||
let node = source.find(span)?;
|
let node = source.find(span)?;
|
||||||
let pos = if node.kind() == SyntaxKind::Text {
|
let pos = if matches!(
|
||||||
|
node.kind(),
|
||||||
|
SyntaxKind::Text | SyntaxKind::MathText
|
||||||
|
) {
|
||||||
let range = node.range();
|
let range = node.range();
|
||||||
let mut offset = range.start + usize::from(span_offset);
|
let mut offset = range.start + usize::from(span_offset);
|
||||||
if (click.x - pos.x) > width / 2.0 {
|
if (click.x - pos.x) > width / 2.0 {
|
||||||
@ -115,7 +118,7 @@ pub fn jump_from_cursor(
|
|||||||
cursor: usize,
|
cursor: usize,
|
||||||
) -> Vec<Position> {
|
) -> Vec<Position> {
|
||||||
fn is_text(node: &LinkedNode) -> bool {
|
fn is_text(node: &LinkedNode) -> bool {
|
||||||
node.get().kind() == SyntaxKind::Text
|
matches!(node.kind(), SyntaxKind::Text | SyntaxKind::MathText)
|
||||||
}
|
}
|
||||||
|
|
||||||
let root = LinkedNode::new(source.root());
|
let root = LinkedNode::new(source.root());
|
||||||
@ -261,6 +264,11 @@ mod tests {
|
|||||||
test_click(s, point(21.0, 12.0), cursor(56));
|
test_click(s, point(21.0, 12.0), cursor(56));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_jump_from_click_math() {
|
||||||
|
test_click("$a + b$", point(28.0, 14.0), cursor(5));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_jump_from_cursor() {
|
fn test_jump_from_cursor() {
|
||||||
let s = "*Hello* #box[ABC] World";
|
let s = "*Hello* #box[ABC] World";
|
||||||
@ -268,6 +276,11 @@ mod tests {
|
|||||||
test_cursor(s, 14, pos(1, 37.55, 16.58));
|
test_cursor(s, 14, pos(1, 37.55, 16.58));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_jump_from_cursor_math() {
|
||||||
|
test_cursor("$a + b$", -3, pos(1, 27.51, 16.83));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_backlink() {
|
fn test_backlink() {
|
||||||
let s = "#footnote[Hi]";
|
let s = "#footnote[Hi]";
|
||||||
|
@ -23,6 +23,8 @@ flate2 = { workspace = true, optional = true }
|
|||||||
fontdb = { workspace = true, optional = true }
|
fontdb = { workspace = true, optional = true }
|
||||||
native-tls = { workspace = true, optional = true }
|
native-tls = { workspace = true, optional = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
tar = { workspace = true, optional = true }
|
tar = { workspace = true, optional = true }
|
||||||
ureq = { workspace = true, optional = true }
|
ureq = { workspace = true, optional = true }
|
||||||
|
|
||||||
|
@ -5,10 +5,9 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use ecow::eco_format;
|
use ecow::eco_format;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
use serde::Deserialize;
|
||||||
use typst_library::diag::{bail, PackageError, PackageResult, StrResult};
|
use typst_library::diag::{bail, PackageError, PackageResult, StrResult};
|
||||||
use typst_syntax::package::{
|
use typst_syntax::package::{PackageSpec, PackageVersion, VersionlessPackageSpec};
|
||||||
PackageInfo, PackageSpec, PackageVersion, VersionlessPackageSpec,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::download::{Downloader, Progress};
|
use crate::download::{Downloader, Progress};
|
||||||
|
|
||||||
@ -32,7 +31,7 @@ pub struct PackageStorage {
|
|||||||
/// The downloader used for fetching the index and packages.
|
/// The downloader used for fetching the index and packages.
|
||||||
downloader: Downloader,
|
downloader: Downloader,
|
||||||
/// The cached index of the default namespace.
|
/// The cached index of the default namespace.
|
||||||
index: OnceCell<Vec<PackageInfo>>,
|
index: OnceCell<Vec<serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PackageStorage {
|
impl PackageStorage {
|
||||||
@ -42,6 +41,18 @@ impl PackageStorage {
|
|||||||
package_cache_path: Option<PathBuf>,
|
package_cache_path: Option<PathBuf>,
|
||||||
package_path: Option<PathBuf>,
|
package_path: Option<PathBuf>,
|
||||||
downloader: Downloader,
|
downloader: Downloader,
|
||||||
|
) -> Self {
|
||||||
|
Self::with_index(package_cache_path, package_path, downloader, OnceCell::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new package storage with a pre-defined index.
|
||||||
|
///
|
||||||
|
/// Useful for testing.
|
||||||
|
fn with_index(
|
||||||
|
package_cache_path: Option<PathBuf>,
|
||||||
|
package_path: Option<PathBuf>,
|
||||||
|
downloader: Downloader,
|
||||||
|
index: OnceCell<Vec<serde_json::Value>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
package_cache_path: package_cache_path.or_else(|| {
|
package_cache_path: package_cache_path.or_else(|| {
|
||||||
@ -51,7 +62,7 @@ impl PackageStorage {
|
|||||||
dirs::data_dir().map(|data_dir| data_dir.join(DEFAULT_PACKAGES_SUBDIR))
|
dirs::data_dir().map(|data_dir| data_dir.join(DEFAULT_PACKAGES_SUBDIR))
|
||||||
}),
|
}),
|
||||||
downloader,
|
downloader,
|
||||||
index: OnceCell::new(),
|
index,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,6 +120,7 @@ impl PackageStorage {
|
|||||||
// version.
|
// version.
|
||||||
self.download_index()?
|
self.download_index()?
|
||||||
.iter()
|
.iter()
|
||||||
|
.filter_map(|value| MinimalPackageInfo::deserialize(value).ok())
|
||||||
.filter(|package| package.name == spec.name)
|
.filter(|package| package.name == spec.name)
|
||||||
.map(|package| package.version)
|
.map(|package| package.version)
|
||||||
.max()
|
.max()
|
||||||
@ -131,7 +143,7 @@ impl PackageStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Download the package index. The result of this is cached for efficiency.
|
/// Download the package index. The result of this is cached for efficiency.
|
||||||
pub fn download_index(&self) -> StrResult<&[PackageInfo]> {
|
pub fn download_index(&self) -> StrResult<&[serde_json::Value]> {
|
||||||
self.index
|
self.index
|
||||||
.get_or_try_init(|| {
|
.get_or_try_init(|| {
|
||||||
let url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/index.json");
|
let url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/index.json");
|
||||||
@ -186,3 +198,54 @@ impl PackageStorage {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Minimal information required about a package to determine its latest
|
||||||
|
/// version.
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct MinimalPackageInfo {
|
||||||
|
name: String,
|
||||||
|
version: PackageVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lazy_deser_index() {
|
||||||
|
let storage = PackageStorage::with_index(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Downloader::new("typst/test"),
|
||||||
|
OnceCell::with_value(vec![
|
||||||
|
serde_json::json!({
|
||||||
|
"name": "charged-ieee",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"entrypoint": "lib.typ",
|
||||||
|
}),
|
||||||
|
serde_json::json!({
|
||||||
|
"name": "unequivocal-ams",
|
||||||
|
// This version number is currently not valid, so this package
|
||||||
|
// can't be parsed.
|
||||||
|
"version": "0.2.0-dev",
|
||||||
|
"entrypoint": "lib.typ",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let ieee_version = storage.determine_latest_version(&VersionlessPackageSpec {
|
||||||
|
namespace: "preview".into(),
|
||||||
|
name: "charged-ieee".into(),
|
||||||
|
});
|
||||||
|
assert_eq!(ieee_version, Ok(PackageVersion { major: 0, minor: 1, patch: 0 }));
|
||||||
|
|
||||||
|
let ams_version = storage.determine_latest_version(&VersionlessPackageSpec {
|
||||||
|
namespace: "preview".into(),
|
||||||
|
name: "unequivocal-ams".into(),
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
ams_version,
|
||||||
|
Err("failed to find package @preview/unequivocal-ams".into())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -124,7 +124,6 @@ impl<'a> Collector<'a, '_, '_> {
|
|||||||
styles,
|
styles,
|
||||||
self.base,
|
self.base,
|
||||||
self.expand,
|
self.expand,
|
||||||
None,
|
|
||||||
)?
|
)?
|
||||||
.into_frames();
|
.into_frames();
|
||||||
|
|
||||||
@ -133,7 +132,8 @@ impl<'a> Collector<'a, '_, '_> {
|
|||||||
self.output.push(Child::Tag(&elem.tag));
|
self.output.push(Child::Tag(&elem.tag));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.lines(lines, styles);
|
let leading = ParElem::leading_in(styles);
|
||||||
|
self.lines(lines, leading, styles);
|
||||||
|
|
||||||
for (c, _) in &self.children[end..] {
|
for (c, _) in &self.children[end..] {
|
||||||
let elem = c.to_packed::<TagElem>().unwrap();
|
let elem = c.to_packed::<TagElem>().unwrap();
|
||||||
@ -169,10 +169,12 @@ impl<'a> Collector<'a, '_, '_> {
|
|||||||
)?
|
)?
|
||||||
.into_frames();
|
.into_frames();
|
||||||
|
|
||||||
let spacing = ParElem::spacing_in(styles);
|
let spacing = elem.spacing(styles);
|
||||||
|
let leading = elem.leading(styles);
|
||||||
|
|
||||||
self.output.push(Child::Rel(spacing.into(), 4));
|
self.output.push(Child::Rel(spacing.into(), 4));
|
||||||
|
|
||||||
self.lines(lines, styles);
|
self.lines(lines, leading, styles);
|
||||||
|
|
||||||
self.output.push(Child::Rel(spacing.into(), 4));
|
self.output.push(Child::Rel(spacing.into(), 4));
|
||||||
self.par_situation = ParSituation::Consecutive;
|
self.par_situation = ParSituation::Consecutive;
|
||||||
@ -181,9 +183,8 @@ impl<'a> Collector<'a, '_, '_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Collect laid-out lines.
|
/// Collect laid-out lines.
|
||||||
fn lines(&mut self, lines: Vec<Frame>, styles: StyleChain<'a>) {
|
fn lines(&mut self, lines: Vec<Frame>, leading: Abs, styles: StyleChain<'a>) {
|
||||||
let align = AlignElem::alignment_in(styles).resolve(styles);
|
let align = AlignElem::alignment_in(styles).resolve(styles);
|
||||||
let leading = ParElem::leading_in(styles);
|
|
||||||
let costs = TextElem::costs_in(styles);
|
let costs = TextElem::costs_in(styles);
|
||||||
|
|
||||||
// Determine whether to prevent widow and orphans.
|
// Determine whether to prevent widow and orphans.
|
||||||
|
@ -197,7 +197,50 @@ pub fn layout_flow<'a>(
|
|||||||
mode: FlowMode,
|
mode: FlowMode,
|
||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
// Prepare configuration that is shared across the whole flow.
|
// Prepare configuration that is shared across the whole flow.
|
||||||
let config = Config {
|
let config = configuration(shared, regions, columns, column_gutter, mode);
|
||||||
|
|
||||||
|
// Collect the elements into pre-processed children. These are much easier
|
||||||
|
// to handle than the raw elements.
|
||||||
|
let bump = Bump::new();
|
||||||
|
let children = collect(
|
||||||
|
engine,
|
||||||
|
&bump,
|
||||||
|
children,
|
||||||
|
locator.next(&()),
|
||||||
|
Size::new(config.columns.width, regions.full),
|
||||||
|
regions.expand.x,
|
||||||
|
mode,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut work = Work::new(&children);
|
||||||
|
let mut finished = vec![];
|
||||||
|
|
||||||
|
// This loop runs once per region produced by the flow layout.
|
||||||
|
loop {
|
||||||
|
let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?;
|
||||||
|
finished.push(frame);
|
||||||
|
|
||||||
|
// Terminate the loop when everything is processed, though draining the
|
||||||
|
// backlog if necessary.
|
||||||
|
if work.done() && (!regions.expand.y || regions.backlog.is_empty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
regions.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Fragment::frames(finished))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine the flow's configuration.
|
||||||
|
fn configuration<'x>(
|
||||||
|
shared: StyleChain<'x>,
|
||||||
|
regions: Regions,
|
||||||
|
columns: NonZeroUsize,
|
||||||
|
column_gutter: Rel<Abs>,
|
||||||
|
mode: FlowMode,
|
||||||
|
) -> Config<'x> {
|
||||||
|
Config {
|
||||||
mode,
|
mode,
|
||||||
shared,
|
shared,
|
||||||
columns: {
|
columns: {
|
||||||
@ -235,39 +278,7 @@ pub fn layout_flow<'a>(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
|
||||||
|
|
||||||
// Collect the elements into pre-processed children. These are much easier
|
|
||||||
// to handle than the raw elements.
|
|
||||||
let bump = Bump::new();
|
|
||||||
let children = collect(
|
|
||||||
engine,
|
|
||||||
&bump,
|
|
||||||
children,
|
|
||||||
locator.next(&()),
|
|
||||||
Size::new(config.columns.width, regions.full),
|
|
||||||
regions.expand.x,
|
|
||||||
mode,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut work = Work::new(&children);
|
|
||||||
let mut finished = vec![];
|
|
||||||
|
|
||||||
// This loop runs once per region produced by the flow layout.
|
|
||||||
loop {
|
|
||||||
let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?;
|
|
||||||
finished.push(frame);
|
|
||||||
|
|
||||||
// Terminate the loop when everything is processed, though draining the
|
|
||||||
// backlog if necessary.
|
|
||||||
if work.done() && (!regions.expand.y || regions.backlog.is_empty()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
regions.next();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Fragment::frames(finished))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The work that is left to do by flow layout.
|
/// The work that is left to do by flow layout.
|
||||||
|
@ -95,6 +95,8 @@ pub fn layout_image(
|
|||||||
} else {
|
} else {
|
||||||
// If neither is forced, take the natural image size at the image's
|
// If neither is forced, take the natural image size at the image's
|
||||||
// DPI bounded by the available space.
|
// DPI bounded by the available space.
|
||||||
|
//
|
||||||
|
// Division by DPI is fine since it's guaranteed to be positive.
|
||||||
let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI);
|
let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI);
|
||||||
let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi));
|
let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi));
|
||||||
Size::new(
|
Size::new(
|
||||||
|
@ -2,10 +2,8 @@ use typst_library::diag::warning;
|
|||||||
use typst_library::foundations::{Packed, Resolve};
|
use typst_library::foundations::{Packed, Resolve};
|
||||||
use typst_library::introspection::{SplitLocator, Tag, TagElem};
|
use typst_library::introspection::{SplitLocator, Tag, TagElem};
|
||||||
use typst_library::layout::{
|
use typst_library::layout::{
|
||||||
Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing,
|
Abs, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Spacing,
|
||||||
Spacing,
|
|
||||||
};
|
};
|
||||||
use typst_library::model::{EnumElem, ListElem, TermsElem};
|
|
||||||
use typst_library::routines::Pair;
|
use typst_library::routines::Pair;
|
||||||
use typst_library::text::{
|
use typst_library::text::{
|
||||||
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
|
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
|
||||||
@ -123,40 +121,20 @@ pub fn collect<'a>(
|
|||||||
children: &[Pair<'a>],
|
children: &[Pair<'a>],
|
||||||
engine: &mut Engine<'_>,
|
engine: &mut Engine<'_>,
|
||||||
locator: &mut SplitLocator<'a>,
|
locator: &mut SplitLocator<'a>,
|
||||||
styles: StyleChain<'a>,
|
config: &Config,
|
||||||
region: Size,
|
region: Size,
|
||||||
situation: Option<ParSituation>,
|
|
||||||
) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> {
|
) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> {
|
||||||
let mut collector = Collector::new(2 + children.len());
|
let mut collector = Collector::new(2 + children.len());
|
||||||
let mut quoter = SmartQuoter::new();
|
let mut quoter = SmartQuoter::new();
|
||||||
|
|
||||||
let outer_dir = TextElem::dir_in(styles);
|
if !config.first_line_indent.is_zero() {
|
||||||
|
collector.push_item(Item::Absolute(config.first_line_indent, false));
|
||||||
|
collector.spans.push(1, Span::detached());
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(situation) = situation {
|
if !config.hanging_indent.is_zero() {
|
||||||
let first_line_indent = ParElem::first_line_indent_in(styles);
|
collector.push_item(Item::Absolute(-config.hanging_indent, false));
|
||||||
if !first_line_indent.amount.is_zero()
|
collector.spans.push(1, Span::detached());
|
||||||
&& match situation {
|
|
||||||
// First-line indent for the first paragraph after a list bullet
|
|
||||||
// just looks bad.
|
|
||||||
ParSituation::First => first_line_indent.all && !in_list(styles),
|
|
||||||
ParSituation::Consecutive => true,
|
|
||||||
ParSituation::Other => first_line_indent.all,
|
|
||||||
}
|
|
||||||
&& AlignElem::alignment_in(styles).resolve(styles).x
|
|
||||||
== outer_dir.start().into()
|
|
||||||
{
|
|
||||||
collector.push_item(Item::Absolute(
|
|
||||||
first_line_indent.amount.resolve(styles),
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
collector.spans.push(1, Span::detached());
|
|
||||||
}
|
|
||||||
|
|
||||||
let hang = ParElem::hanging_indent_in(styles);
|
|
||||||
if !hang.is_zero() {
|
|
||||||
collector.push_item(Item::Absolute(-hang, false));
|
|
||||||
collector.spans.push(1, Span::detached());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for &(child, styles) in children {
|
for &(child, styles) in children {
|
||||||
@ -167,7 +145,7 @@ pub fn collect<'a>(
|
|||||||
} else if let Some(elem) = child.to_packed::<TextElem>() {
|
} else if let Some(elem) = child.to_packed::<TextElem>() {
|
||||||
collector.build_text(styles, |full| {
|
collector.build_text(styles, |full| {
|
||||||
let dir = TextElem::dir_in(styles);
|
let dir = TextElem::dir_in(styles);
|
||||||
if dir != outer_dir {
|
if dir != config.dir {
|
||||||
// Insert "Explicit Directional Embedding".
|
// Insert "Explicit Directional Embedding".
|
||||||
match dir {
|
match dir {
|
||||||
Dir::LTR => full.push_str(LTR_EMBEDDING),
|
Dir::LTR => full.push_str(LTR_EMBEDDING),
|
||||||
@ -182,7 +160,7 @@ pub fn collect<'a>(
|
|||||||
full.push_str(&elem.text);
|
full.push_str(&elem.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
if dir != outer_dir {
|
if dir != config.dir {
|
||||||
// Insert "Pop Directional Formatting".
|
// Insert "Pop Directional Formatting".
|
||||||
full.push_str(POP_EMBEDDING);
|
full.push_str(POP_EMBEDDING);
|
||||||
}
|
}
|
||||||
@ -265,16 +243,6 @@ pub fn collect<'a>(
|
|||||||
Ok((collector.full, collector.segments, collector.spans))
|
Ok((collector.full, collector.segments, collector.spans))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether we have a list ancestor.
|
|
||||||
///
|
|
||||||
/// When we support some kind of more general ancestry mechanism, this can
|
|
||||||
/// become more elegant.
|
|
||||||
fn in_list(styles: StyleChain) -> bool {
|
|
||||||
ListElem::depth_in(styles).0 > 0
|
|
||||||
|| !EnumElem::parents_in(styles).is_empty()
|
|
||||||
|| TermsElem::within_in(styles)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collects segments.
|
/// Collects segments.
|
||||||
struct Collector<'a> {
|
struct Collector<'a> {
|
||||||
full: String,
|
full: String,
|
||||||
|
@ -9,7 +9,6 @@ pub fn finalize(
|
|||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
p: &Preparation,
|
p: &Preparation,
|
||||||
lines: &[Line],
|
lines: &[Line],
|
||||||
styles: StyleChain,
|
|
||||||
region: Size,
|
region: Size,
|
||||||
expand: bool,
|
expand: bool,
|
||||||
locator: &mut SplitLocator<'_>,
|
locator: &mut SplitLocator<'_>,
|
||||||
@ -19,9 +18,10 @@ pub fn finalize(
|
|||||||
let width = if !region.x.is_finite()
|
let width = if !region.x.is_finite()
|
||||||
|| (!expand && lines.iter().all(|line| line.fr().is_zero()))
|
|| (!expand && lines.iter().all(|line| line.fr().is_zero()))
|
||||||
{
|
{
|
||||||
region
|
region.x.min(
|
||||||
.x
|
p.config.hanging_indent
|
||||||
.min(p.hang + lines.iter().map(|line| line.width).max().unwrap_or_default())
|
+ lines.iter().map(|line| line.width).max().unwrap_or_default(),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
region.x
|
region.x
|
||||||
};
|
};
|
||||||
@ -29,7 +29,7 @@ pub fn finalize(
|
|||||||
// Stack the lines into one frame per region.
|
// Stack the lines into one frame per region.
|
||||||
lines
|
lines
|
||||||
.iter()
|
.iter()
|
||||||
.map(|line| commit(engine, p, line, width, region.y, locator, styles))
|
.map(|line| commit(engine, p, line, width, region.y, locator))
|
||||||
.collect::<SourceResult<_>>()
|
.collect::<SourceResult<_>>()
|
||||||
.map(Fragment::frames)
|
.map(Fragment::frames)
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,9 @@ use std::fmt::{self, Debug, Formatter};
|
|||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
use typst_library::engine::Engine;
|
use typst_library::engine::Engine;
|
||||||
use typst_library::foundations::NativeElement;
|
|
||||||
use typst_library::introspection::{SplitLocator, Tag};
|
use typst_library::introspection::{SplitLocator, Tag};
|
||||||
use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
|
use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
|
||||||
use typst_library::model::{ParLine, ParLineMarker};
|
use typst_library::model::ParLineMarker;
|
||||||
use typst_library::text::{Lang, TextElem};
|
use typst_library::text::{Lang, TextElem};
|
||||||
use typst_utils::Numeric;
|
use typst_utils::Numeric;
|
||||||
|
|
||||||
@ -135,7 +134,7 @@ pub fn line<'a>(
|
|||||||
|
|
||||||
// Whether the line is justified.
|
// Whether the line is justified.
|
||||||
let justify = full.ends_with(LINE_SEPARATOR)
|
let justify = full.ends_with(LINE_SEPARATOR)
|
||||||
|| (p.justify && breakpoint != Breakpoint::Mandatory);
|
|| (p.config.justify && breakpoint != Breakpoint::Mandatory);
|
||||||
|
|
||||||
// Process dashes.
|
// Process dashes.
|
||||||
let dash = if breakpoint.is_hyphen() || full.ends_with(SHY) {
|
let dash = if breakpoint.is_hyphen() || full.ends_with(SHY) {
|
||||||
@ -157,14 +156,14 @@ pub fn line<'a>(
|
|||||||
// Add a hyphen at the line start, if a previous dash should be repeated.
|
// Add a hyphen at the line start, if a previous dash should be repeated.
|
||||||
if pred.map_or(false, |pred| should_repeat_hyphen(pred, full)) {
|
if pred.map_or(false, |pred| should_repeat_hyphen(pred, full)) {
|
||||||
if let Some(shaped) = items.first_text_mut() {
|
if let Some(shaped) = items.first_text_mut() {
|
||||||
shaped.prepend_hyphen(engine, p.fallback);
|
shaped.prepend_hyphen(engine, p.config.fallback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a hyphen at the line end, if we ended on a soft hyphen.
|
// Add a hyphen at the line end, if we ended on a soft hyphen.
|
||||||
if dash == Some(Dash::Soft) {
|
if dash == Some(Dash::Soft) {
|
||||||
if let Some(shaped) = items.last_text_mut() {
|
if let Some(shaped) = items.last_text_mut() {
|
||||||
shaped.push_hyphen(engine, p.fallback);
|
shaped.push_hyphen(engine, p.config.fallback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,13 +233,13 @@ where
|
|||||||
{
|
{
|
||||||
// If there is nothing bidirectional going on, skip reordering.
|
// If there is nothing bidirectional going on, skip reordering.
|
||||||
let Some(bidi) = &p.bidi else {
|
let Some(bidi) = &p.bidi else {
|
||||||
f(range, p.dir == Dir::RTL);
|
f(range, p.config.dir == Dir::RTL);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The bidi crate panics for empty lines.
|
// The bidi crate panics for empty lines.
|
||||||
if range.is_empty() {
|
if range.is_empty() {
|
||||||
f(range, p.dir == Dir::RTL);
|
f(range, p.config.dir == Dir::RTL);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,13 +307,13 @@ fn collect_range<'a>(
|
|||||||
/// punctuation marks at line start or line end.
|
/// punctuation marks at line start or line end.
|
||||||
fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) {
|
fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) {
|
||||||
if text.starts_with(BEGIN_PUNCT_PAT)
|
if text.starts_with(BEGIN_PUNCT_PAT)
|
||||||
|| (p.cjk_latin_spacing && text.starts_with(is_of_cj_script))
|
|| (p.config.cjk_latin_spacing && text.starts_with(is_of_cj_script))
|
||||||
{
|
{
|
||||||
adjust_cj_at_line_start(p, items);
|
adjust_cj_at_line_start(p, items);
|
||||||
}
|
}
|
||||||
|
|
||||||
if text.ends_with(END_PUNCT_PAT)
|
if text.ends_with(END_PUNCT_PAT)
|
||||||
|| (p.cjk_latin_spacing && text.ends_with(is_of_cj_script))
|
|| (p.config.cjk_latin_spacing && text.ends_with(is_of_cj_script))
|
||||||
{
|
{
|
||||||
adjust_cj_at_line_end(p, items);
|
adjust_cj_at_line_end(p, items);
|
||||||
}
|
}
|
||||||
@ -332,7 +331,10 @@ fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) {
|
|||||||
let shrink = glyph.shrinkability().0;
|
let shrink = glyph.shrinkability().0;
|
||||||
glyph.shrink_left(shrink);
|
glyph.shrink_left(shrink);
|
||||||
shaped.width -= shrink.at(shaped.size);
|
shaped.width -= shrink.at(shaped.size);
|
||||||
} else if p.cjk_latin_spacing && glyph.is_cj_script() && glyph.x_offset > Em::zero() {
|
} else if p.config.cjk_latin_spacing
|
||||||
|
&& glyph.is_cj_script()
|
||||||
|
&& glyph.x_offset > Em::zero()
|
||||||
|
{
|
||||||
// If the first glyph is a CJK character adjusted by
|
// If the first glyph is a CJK character adjusted by
|
||||||
// [`add_cjk_latin_spacing`], restore the original width.
|
// [`add_cjk_latin_spacing`], restore the original width.
|
||||||
let glyph = shaped.glyphs.to_mut().first_mut().unwrap();
|
let glyph = shaped.glyphs.to_mut().first_mut().unwrap();
|
||||||
@ -359,7 +361,7 @@ fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) {
|
|||||||
let punct = shaped.glyphs.to_mut().last_mut().unwrap();
|
let punct = shaped.glyphs.to_mut().last_mut().unwrap();
|
||||||
punct.shrink_right(shrink);
|
punct.shrink_right(shrink);
|
||||||
shaped.width -= shrink.at(shaped.size);
|
shaped.width -= shrink.at(shaped.size);
|
||||||
} else if p.cjk_latin_spacing
|
} else if p.config.cjk_latin_spacing
|
||||||
&& glyph.is_cj_script()
|
&& glyph.is_cj_script()
|
||||||
&& (glyph.x_advance - glyph.x_offset) > Em::one()
|
&& (glyph.x_advance - glyph.x_offset) > Em::one()
|
||||||
{
|
{
|
||||||
@ -424,16 +426,15 @@ pub fn commit(
|
|||||||
width: Abs,
|
width: Abs,
|
||||||
full: Abs,
|
full: Abs,
|
||||||
locator: &mut SplitLocator<'_>,
|
locator: &mut SplitLocator<'_>,
|
||||||
styles: StyleChain,
|
|
||||||
) -> SourceResult<Frame> {
|
) -> SourceResult<Frame> {
|
||||||
let mut remaining = width - line.width - p.hang;
|
let mut remaining = width - line.width - p.config.hanging_indent;
|
||||||
let mut offset = Abs::zero();
|
let mut offset = Abs::zero();
|
||||||
|
|
||||||
// We always build the line from left to right. In an LTR paragraph, we must
|
// We always build the line from left to right. In an LTR paragraph, we must
|
||||||
// thus add the hanging indent to the offset. In an RTL paragraph, the
|
// thus add the hanging indent to the offset. In an RTL paragraph, the
|
||||||
// hanging indent arises naturally due to the line width.
|
// hanging indent arises naturally due to the line width.
|
||||||
if p.dir == Dir::LTR {
|
if p.config.dir == Dir::LTR {
|
||||||
offset += p.hang;
|
offset += p.config.hanging_indent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle hanging punctuation to the left.
|
// Handle hanging punctuation to the left.
|
||||||
@ -554,11 +555,13 @@ pub fn commit(
|
|||||||
let mut output = Frame::soft(size);
|
let mut output = Frame::soft(size);
|
||||||
output.set_baseline(top);
|
output.set_baseline(top);
|
||||||
|
|
||||||
add_par_line_marker(&mut output, styles, engine, locator, top);
|
if let Some(marker) = &p.config.numbering_marker {
|
||||||
|
add_par_line_marker(&mut output, marker, engine, locator, top);
|
||||||
|
}
|
||||||
|
|
||||||
// Construct the line's frame.
|
// Construct the line's frame.
|
||||||
for (offset, frame) in frames {
|
for (offset, frame) in frames {
|
||||||
let x = offset + p.align.position(remaining);
|
let x = offset + p.config.align.position(remaining);
|
||||||
let y = top - frame.baseline();
|
let y = top - frame.baseline();
|
||||||
output.push_frame(Point::new(x, y), frame);
|
output.push_frame(Point::new(x, y), frame);
|
||||||
}
|
}
|
||||||
@ -575,26 +578,18 @@ pub fn commit(
|
|||||||
/// number in the margin, is aligned to the line's baseline.
|
/// number in the margin, is aligned to the line's baseline.
|
||||||
fn add_par_line_marker(
|
fn add_par_line_marker(
|
||||||
output: &mut Frame,
|
output: &mut Frame,
|
||||||
styles: StyleChain,
|
marker: &Packed<ParLineMarker>,
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
locator: &mut SplitLocator,
|
locator: &mut SplitLocator,
|
||||||
top: Abs,
|
top: Abs,
|
||||||
) {
|
) {
|
||||||
let Some(numbering) = ParLine::numbering_in(styles) else { return };
|
|
||||||
let margin = ParLine::number_margin_in(styles);
|
|
||||||
let align = ParLine::number_align_in(styles);
|
|
||||||
|
|
||||||
// Delay resolving the number clearance until line numbers are laid out to
|
|
||||||
// avoid inconsistent spacing depending on varying font size.
|
|
||||||
let clearance = ParLine::number_clearance_in(styles);
|
|
||||||
|
|
||||||
// Elements in tags must have a location for introspection to work. We do
|
// Elements in tags must have a location for introspection to work. We do
|
||||||
// the work here instead of going through all of the realization process
|
// the work here instead of going through all of the realization process
|
||||||
// just for this, given we don't need to actually place the marker as we
|
// just for this, given we don't need to actually place the marker as we
|
||||||
// manually search for it in the frame later (when building a root flow,
|
// manually search for it in the frame later (when building a root flow,
|
||||||
// where line numbers can be displayed), so we just need it to be in a tag
|
// where line numbers can be displayed), so we just need it to be in a tag
|
||||||
// and to be valid (to have a location).
|
// and to be valid (to have a location).
|
||||||
let mut marker = ParLineMarker::new(numbering, align, margin, clearance).pack();
|
let mut marker = marker.clone();
|
||||||
let key = typst_utils::hash128(&marker);
|
let key = typst_utils::hash128(&marker);
|
||||||
let loc = locator.next_location(engine.introspector, key);
|
let loc = locator.next_location(engine.introspector, key);
|
||||||
marker.set_location(loc);
|
marker.set_location(loc);
|
||||||
@ -606,7 +601,7 @@ fn add_par_line_marker(
|
|||||||
// line's general baseline. However, the line number will still need to
|
// line's general baseline. However, the line number will still need to
|
||||||
// manually adjust its own 'y' position based on its own baseline.
|
// manually adjust its own 'y' position based on its own baseline.
|
||||||
let pos = Point::with_y(top);
|
let pos = Point::with_y(top);
|
||||||
output.push(pos, FrameItem::Tag(Tag::Start(marker)));
|
output.push(pos, FrameItem::Tag(Tag::Start(marker.pack())));
|
||||||
output.push(pos, FrameItem::Tag(Tag::End(loc, key)));
|
output.push(pos, FrameItem::Tag(Tag::End(loc, key)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,15 +110,7 @@ pub fn linebreak<'a>(
|
|||||||
p: &'a Preparation<'a>,
|
p: &'a Preparation<'a>,
|
||||||
width: Abs,
|
width: Abs,
|
||||||
) -> Vec<Line<'a>> {
|
) -> Vec<Line<'a>> {
|
||||||
let linebreaks = p.linebreaks.unwrap_or_else(|| {
|
match p.config.linebreaks {
|
||||||
if p.justify {
|
|
||||||
Linebreaks::Optimized
|
|
||||||
} else {
|
|
||||||
Linebreaks::Simple
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
match linebreaks {
|
|
||||||
Linebreaks::Simple => linebreak_simple(engine, p, width),
|
Linebreaks::Simple => linebreak_simple(engine, p, width),
|
||||||
Linebreaks::Optimized => linebreak_optimized(engine, p, width),
|
Linebreaks::Optimized => linebreak_optimized(engine, p, width),
|
||||||
}
|
}
|
||||||
@ -384,7 +376,7 @@ fn linebreak_optimized_approximate(
|
|||||||
|
|
||||||
// Whether the line is justified. This is not 100% accurate w.r.t
|
// Whether the line is justified. This is not 100% accurate w.r.t
|
||||||
// to line()'s behaviour, but good enough.
|
// to line()'s behaviour, but good enough.
|
||||||
let justify = p.justify && breakpoint != Breakpoint::Mandatory;
|
let justify = p.config.justify && breakpoint != Breakpoint::Mandatory;
|
||||||
|
|
||||||
// We don't really know whether the line naturally ends with a dash
|
// We don't really know whether the line naturally ends with a dash
|
||||||
// here, so we can miss that case, but it's ok, since all of this
|
// here, so we can miss that case, but it's ok, since all of this
|
||||||
@ -573,7 +565,7 @@ fn raw_ratio(
|
|||||||
// calculate the extra amount. Also, don't divide by zero.
|
// calculate the extra amount. Also, don't divide by zero.
|
||||||
let extra_stretch = (delta - adjustability) / justifiables.max(1) as f64;
|
let extra_stretch = (delta - adjustability) / justifiables.max(1) as f64;
|
||||||
// Normalize the amount by half the em size.
|
// Normalize the amount by half the em size.
|
||||||
ratio = 1.0 + extra_stretch / (p.size / 2.0);
|
ratio = 1.0 + extra_stretch / (p.config.font_size / 2.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The min value must be < MIN_RATIO, but how much smaller doesn't matter
|
// The min value must be < MIN_RATIO, but how much smaller doesn't matter
|
||||||
@ -663,9 +655,9 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let hyphenate = p.hyphenate != Some(false);
|
let hyphenate = p.config.hyphenate != Some(false);
|
||||||
let lb = LINEBREAK_DATA.as_borrowed();
|
let lb = LINEBREAK_DATA.as_borrowed();
|
||||||
let segmenter = match p.lang {
|
let segmenter = match p.config.lang {
|
||||||
Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER,
|
Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER,
|
||||||
_ => &SEGMENTER,
|
_ => &SEGMENTER,
|
||||||
};
|
};
|
||||||
@ -830,18 +822,18 @@ fn linebreak_link(link: &str, mut f: impl FnMut(usize)) {
|
|||||||
|
|
||||||
/// Whether hyphenation is enabled at the given offset.
|
/// Whether hyphenation is enabled at the given offset.
|
||||||
fn hyphenate_at(p: &Preparation, offset: usize) -> bool {
|
fn hyphenate_at(p: &Preparation, offset: usize) -> bool {
|
||||||
p.hyphenate
|
p.config.hyphenate.unwrap_or_else(|| {
|
||||||
.or_else(|| {
|
let (_, item) = p.get(offset);
|
||||||
let (_, item) = p.get(offset);
|
match item.text() {
|
||||||
let styles = item.text()?.styles;
|
Some(text) => TextElem::hyphenate_in(text.styles).unwrap_or(p.config.justify),
|
||||||
Some(TextElem::hyphenate_in(styles))
|
None => false,
|
||||||
})
|
}
|
||||||
.unwrap_or(false)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The text language at the given offset.
|
/// The text language at the given offset.
|
||||||
fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> {
|
fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> {
|
||||||
let lang = p.lang.or_else(|| {
|
let lang = p.config.lang.or_else(|| {
|
||||||
let (_, item) = p.get(offset);
|
let (_, item) = p.get(offset);
|
||||||
let styles = item.text()?.styles;
|
let styles = item.text()?.styles;
|
||||||
Some(TextElem::lang_in(styles))
|
Some(TextElem::lang_in(styles))
|
||||||
@ -865,13 +857,13 @@ impl CostMetrics {
|
|||||||
fn compute(p: &Preparation) -> Self {
|
fn compute(p: &Preparation) -> Self {
|
||||||
Self {
|
Self {
|
||||||
// When justifying, we may stretch spaces below their natural width.
|
// When justifying, we may stretch spaces below their natural width.
|
||||||
min_ratio: if p.justify { MIN_RATIO } else { 0.0 },
|
min_ratio: if p.config.justify { MIN_RATIO } else { 0.0 },
|
||||||
min_approx_ratio: if p.justify { MIN_APPROX_RATIO } else { 0.0 },
|
min_approx_ratio: if p.config.justify { MIN_APPROX_RATIO } else { 0.0 },
|
||||||
// Approximate hyphen width for estimates.
|
// Approximate hyphen width for estimates.
|
||||||
approx_hyphen_width: Em::new(0.33).at(p.size),
|
approx_hyphen_width: Em::new(0.33).at(p.config.font_size),
|
||||||
// Costs.
|
// Costs.
|
||||||
hyph_cost: DEFAULT_HYPH_COST * p.costs.hyphenation().get(),
|
hyph_cost: DEFAULT_HYPH_COST * p.config.costs.hyphenation().get(),
|
||||||
runt_cost: DEFAULT_RUNT_COST * p.costs.runt().get(),
|
runt_cost: DEFAULT_RUNT_COST * p.config.costs.runt().get(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,12 +13,17 @@ pub use self::box_::layout_box;
|
|||||||
use comemo::{Track, Tracked, TrackedMut};
|
use comemo::{Track, Tracked, TrackedMut};
|
||||||
use typst_library::diag::SourceResult;
|
use typst_library::diag::SourceResult;
|
||||||
use typst_library::engine::{Engine, Route, Sink, Traced};
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
use typst_library::foundations::{Packed, StyleChain};
|
use typst_library::foundations::{Packed, Resolve, Smart, StyleChain};
|
||||||
use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator};
|
use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator};
|
||||||
use typst_library::layout::{Fragment, Size};
|
use typst_library::layout::{Abs, AlignElem, Dir, FixedAlignment, Fragment, Size};
|
||||||
use typst_library::model::ParElem;
|
use typst_library::model::{
|
||||||
|
EnumElem, FirstLineIndent, Linebreaks, ListElem, ParElem, ParLine, ParLineMarker,
|
||||||
|
TermsElem,
|
||||||
|
};
|
||||||
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
|
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
|
||||||
|
use typst_library::text::{Costs, Lang, TextElem};
|
||||||
use typst_library::World;
|
use typst_library::World;
|
||||||
|
use typst_utils::{Numeric, SliceExt};
|
||||||
|
|
||||||
use self::collect::{collect, Item, Segment, SpanMapper};
|
use self::collect::{collect, Item, Segment, SpanMapper};
|
||||||
use self::deco::decorate;
|
use self::deco::decorate;
|
||||||
@ -98,7 +103,7 @@ fn layout_par_impl(
|
|||||||
styles,
|
styles,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
layout_inline(
|
layout_inline_impl(
|
||||||
&mut engine,
|
&mut engine,
|
||||||
&children,
|
&children,
|
||||||
&mut locator,
|
&mut locator,
|
||||||
@ -106,33 +111,134 @@ fn layout_par_impl(
|
|||||||
region,
|
region,
|
||||||
expand,
|
expand,
|
||||||
Some(situation),
|
Some(situation),
|
||||||
|
&ConfigBase {
|
||||||
|
justify: elem.justify(styles),
|
||||||
|
linebreaks: elem.linebreaks(styles),
|
||||||
|
first_line_indent: elem.first_line_indent(styles),
|
||||||
|
hanging_indent: elem.hanging_indent(styles),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lays out realized content with inline layout.
|
/// Lays out realized content with inline layout.
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn layout_inline<'a>(
|
pub fn layout_inline<'a>(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
children: &[Pair<'a>],
|
children: &[Pair<'a>],
|
||||||
locator: &mut SplitLocator<'a>,
|
locator: &mut SplitLocator<'a>,
|
||||||
styles: StyleChain<'a>,
|
shared: StyleChain<'a>,
|
||||||
|
region: Size,
|
||||||
|
expand: bool,
|
||||||
|
) -> SourceResult<Fragment> {
|
||||||
|
layout_inline_impl(
|
||||||
|
engine,
|
||||||
|
children,
|
||||||
|
locator,
|
||||||
|
shared,
|
||||||
|
region,
|
||||||
|
expand,
|
||||||
|
None,
|
||||||
|
&ConfigBase {
|
||||||
|
justify: ParElem::justify_in(shared),
|
||||||
|
linebreaks: ParElem::linebreaks_in(shared),
|
||||||
|
first_line_indent: ParElem::first_line_indent_in(shared),
|
||||||
|
hanging_indent: ParElem::hanging_indent_in(shared),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The internal implementation of [`layout_inline`].
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn layout_inline_impl<'a>(
|
||||||
|
engine: &mut Engine,
|
||||||
|
children: &[Pair<'a>],
|
||||||
|
locator: &mut SplitLocator<'a>,
|
||||||
|
shared: StyleChain<'a>,
|
||||||
region: Size,
|
region: Size,
|
||||||
expand: bool,
|
expand: bool,
|
||||||
par: Option<ParSituation>,
|
par: Option<ParSituation>,
|
||||||
|
base: &ConfigBase,
|
||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
|
// Prepare configuration that is shared across the whole inline layout.
|
||||||
|
let config = configuration(base, children, shared, par);
|
||||||
|
|
||||||
// Collect all text into one string for BiDi analysis.
|
// Collect all text into one string for BiDi analysis.
|
||||||
let (text, segments, spans) =
|
let (text, segments, spans) = collect(children, engine, locator, &config, region)?;
|
||||||
collect(children, engine, locator, styles, region, par)?;
|
|
||||||
|
|
||||||
// Perform BiDi analysis and performs some preparation steps before we
|
// Perform BiDi analysis and performs some preparation steps before we
|
||||||
// proceed to line breaking.
|
// proceed to line breaking.
|
||||||
let p = prepare(engine, children, &text, segments, spans, styles, par)?;
|
let p = prepare(engine, &config, &text, segments, spans)?;
|
||||||
|
|
||||||
// Break the text into lines.
|
// Break the text into lines.
|
||||||
let lines = linebreak(engine, &p, region.x - p.hang);
|
let lines = linebreak(engine, &p, region.x - config.hanging_indent);
|
||||||
|
|
||||||
// Turn the selected lines into frames.
|
// Turn the selected lines into frames.
|
||||||
finalize(engine, &p, &lines, styles, region, expand, locator)
|
finalize(engine, &p, &lines, region, expand, locator)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine the inline layout's configuration.
|
||||||
|
fn configuration(
|
||||||
|
base: &ConfigBase,
|
||||||
|
children: &[Pair],
|
||||||
|
shared: StyleChain,
|
||||||
|
situation: Option<ParSituation>,
|
||||||
|
) -> Config {
|
||||||
|
let justify = base.justify;
|
||||||
|
let font_size = TextElem::size_in(shared);
|
||||||
|
let dir = TextElem::dir_in(shared);
|
||||||
|
|
||||||
|
Config {
|
||||||
|
justify,
|
||||||
|
linebreaks: base.linebreaks.unwrap_or_else(|| {
|
||||||
|
if justify {
|
||||||
|
Linebreaks::Optimized
|
||||||
|
} else {
|
||||||
|
Linebreaks::Simple
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
first_line_indent: {
|
||||||
|
let FirstLineIndent { amount, all } = base.first_line_indent;
|
||||||
|
if !amount.is_zero()
|
||||||
|
&& match situation {
|
||||||
|
// First-line indent for the first paragraph after a list
|
||||||
|
// bullet just looks bad.
|
||||||
|
Some(ParSituation::First) => all && !in_list(shared),
|
||||||
|
Some(ParSituation::Consecutive) => true,
|
||||||
|
Some(ParSituation::Other) => all,
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
&& AlignElem::alignment_in(shared).resolve(shared).x == dir.start().into()
|
||||||
|
{
|
||||||
|
amount.at(font_size)
|
||||||
|
} else {
|
||||||
|
Abs::zero()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hanging_indent: if situation.is_some() {
|
||||||
|
base.hanging_indent
|
||||||
|
} else {
|
||||||
|
Abs::zero()
|
||||||
|
},
|
||||||
|
numbering_marker: ParLine::numbering_in(shared).map(|numbering| {
|
||||||
|
Packed::new(ParLineMarker::new(
|
||||||
|
numbering,
|
||||||
|
ParLine::number_align_in(shared),
|
||||||
|
ParLine::number_margin_in(shared),
|
||||||
|
// Delay resolving the number clearance until line numbers are
|
||||||
|
// laid out to avoid inconsistent spacing depending on varying
|
||||||
|
// font size.
|
||||||
|
ParLine::number_clearance_in(shared),
|
||||||
|
))
|
||||||
|
}),
|
||||||
|
align: AlignElem::alignment_in(shared).fix(dir).x,
|
||||||
|
font_size,
|
||||||
|
dir,
|
||||||
|
hyphenate: shared_get(children, shared, TextElem::hyphenate_in)
|
||||||
|
.map(|uniform| uniform.unwrap_or(justify)),
|
||||||
|
lang: shared_get(children, shared, TextElem::lang_in),
|
||||||
|
fallback: TextElem::fallback_in(shared),
|
||||||
|
cjk_latin_spacing: TextElem::cjk_latin_spacing_in(shared).is_auto(),
|
||||||
|
costs: TextElem::costs_in(shared),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Distinguishes between a few different kinds of paragraphs.
|
/// Distinguishes between a few different kinds of paragraphs.
|
||||||
@ -148,3 +254,66 @@ pub enum ParSituation {
|
|||||||
/// Any other kind of paragraph.
|
/// Any other kind of paragraph.
|
||||||
Other,
|
Other,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Raw values from a `ParElem` or style chain. Used to initialize a [`Config`].
|
||||||
|
struct ConfigBase {
|
||||||
|
justify: bool,
|
||||||
|
linebreaks: Smart<Linebreaks>,
|
||||||
|
first_line_indent: FirstLineIndent,
|
||||||
|
hanging_indent: Abs,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared configuration for the whole inline layout.
|
||||||
|
struct Config {
|
||||||
|
/// Whether to justify text.
|
||||||
|
justify: bool,
|
||||||
|
/// How to determine line breaks.
|
||||||
|
linebreaks: Linebreaks,
|
||||||
|
/// The indent the first line of a paragraph should have.
|
||||||
|
first_line_indent: Abs,
|
||||||
|
/// The indent that all but the first line of a paragraph should have.
|
||||||
|
hanging_indent: Abs,
|
||||||
|
/// Configuration for line numbering.
|
||||||
|
numbering_marker: Option<Packed<ParLineMarker>>,
|
||||||
|
/// The resolved horizontal alignment.
|
||||||
|
align: FixedAlignment,
|
||||||
|
/// The text size.
|
||||||
|
font_size: Abs,
|
||||||
|
/// The dominant direction.
|
||||||
|
dir: Dir,
|
||||||
|
/// A uniform hyphenation setting (only `Some(_)` if it's the same for all
|
||||||
|
/// children, otherwise `None`).
|
||||||
|
hyphenate: Option<bool>,
|
||||||
|
/// The text language (only `Some(_)` if it's the same for all
|
||||||
|
/// children, otherwise `None`).
|
||||||
|
lang: Option<Lang>,
|
||||||
|
/// Whether font fallback is enabled.
|
||||||
|
fallback: bool,
|
||||||
|
/// Whether to add spacing between CJK and Latin characters.
|
||||||
|
cjk_latin_spacing: bool,
|
||||||
|
/// Costs for various layout decisions.
|
||||||
|
costs: Costs,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a style property, but only if it is the same for all of the children.
|
||||||
|
fn shared_get<T: PartialEq>(
|
||||||
|
children: &[Pair],
|
||||||
|
styles: StyleChain<'_>,
|
||||||
|
getter: fn(StyleChain) -> T,
|
||||||
|
) -> Option<T> {
|
||||||
|
let value = getter(styles);
|
||||||
|
children
|
||||||
|
.group_by_key(|&(_, s)| s)
|
||||||
|
.all(|(s, _)| getter(s) == value)
|
||||||
|
.then_some(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether we have a list ancestor.
|
||||||
|
///
|
||||||
|
/// When we support some kind of more general ancestry mechanism, this can
|
||||||
|
/// become more elegant.
|
||||||
|
fn in_list(styles: StyleChain) -> bool {
|
||||||
|
ListElem::depth_in(styles).0 > 0
|
||||||
|
|| !EnumElem::parents_in(styles).is_empty()
|
||||||
|
|| TermsElem::within_in(styles)
|
||||||
|
}
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
use typst_library::foundations::{Resolve, Smart};
|
use typst_library::layout::{Dir, Em};
|
||||||
use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment};
|
|
||||||
use typst_library::model::Linebreaks;
|
|
||||||
use typst_library::routines::Pair;
|
|
||||||
use typst_library::text::{Costs, Lang, TextElem};
|
|
||||||
use typst_utils::SliceExt;
|
|
||||||
use unicode_bidi::{BidiInfo, Level as BidiLevel};
|
use unicode_bidi::{BidiInfo, Level as BidiLevel};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -17,6 +12,8 @@ use super::*;
|
|||||||
pub struct Preparation<'a> {
|
pub struct Preparation<'a> {
|
||||||
/// The full text.
|
/// The full text.
|
||||||
pub text: &'a str,
|
pub text: &'a str,
|
||||||
|
/// Configuration for inline layout.
|
||||||
|
pub config: &'a Config,
|
||||||
/// Bidirectional text embedding levels.
|
/// Bidirectional text embedding levels.
|
||||||
///
|
///
|
||||||
/// This is `None` if all text directions are uniform (all the base
|
/// This is `None` if all text directions are uniform (all the base
|
||||||
@ -28,28 +25,6 @@ pub struct Preparation<'a> {
|
|||||||
pub indices: Vec<usize>,
|
pub indices: Vec<usize>,
|
||||||
/// The span mapper.
|
/// The span mapper.
|
||||||
pub spans: SpanMapper,
|
pub spans: SpanMapper,
|
||||||
/// Whether to hyphenate if it's the same for all children.
|
|
||||||
pub hyphenate: Option<bool>,
|
|
||||||
/// Costs for various layout decisions.
|
|
||||||
pub costs: Costs,
|
|
||||||
/// The dominant direction.
|
|
||||||
pub dir: Dir,
|
|
||||||
/// The text language if it's the same for all children.
|
|
||||||
pub lang: Option<Lang>,
|
|
||||||
/// The resolved horizontal alignment.
|
|
||||||
pub align: FixedAlignment,
|
|
||||||
/// Whether to justify text.
|
|
||||||
pub justify: bool,
|
|
||||||
/// Hanging indent to apply.
|
|
||||||
pub hang: Abs,
|
|
||||||
/// Whether to add spacing between CJK and Latin characters.
|
|
||||||
pub cjk_latin_spacing: bool,
|
|
||||||
/// Whether font fallback is enabled.
|
|
||||||
pub fallback: bool,
|
|
||||||
/// How to determine line breaks.
|
|
||||||
pub linebreaks: Smart<Linebreaks>,
|
|
||||||
/// The text size.
|
|
||||||
pub size: Abs,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Preparation<'a> {
|
impl<'a> Preparation<'a> {
|
||||||
@ -80,15 +55,12 @@ impl<'a> Preparation<'a> {
|
|||||||
#[typst_macros::time]
|
#[typst_macros::time]
|
||||||
pub fn prepare<'a>(
|
pub fn prepare<'a>(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
children: &[Pair<'a>],
|
config: &'a Config,
|
||||||
text: &'a str,
|
text: &'a str,
|
||||||
segments: Vec<Segment<'a>>,
|
segments: Vec<Segment<'a>>,
|
||||||
spans: SpanMapper,
|
spans: SpanMapper,
|
||||||
styles: StyleChain<'a>,
|
|
||||||
situation: Option<ParSituation>,
|
|
||||||
) -> SourceResult<Preparation<'a>> {
|
) -> SourceResult<Preparation<'a>> {
|
||||||
let dir = TextElem::dir_in(styles);
|
let default_level = match config.dir {
|
||||||
let default_level = match dir {
|
|
||||||
Dir::RTL => BidiLevel::rtl(),
|
Dir::RTL => BidiLevel::rtl(),
|
||||||
_ => BidiLevel::ltr(),
|
_ => BidiLevel::ltr(),
|
||||||
};
|
};
|
||||||
@ -124,51 +96,20 @@ pub fn prepare<'a>(
|
|||||||
indices.extend(range.clone().map(|_| i));
|
indices.extend(range.clone().map(|_| i));
|
||||||
}
|
}
|
||||||
|
|
||||||
let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto();
|
if config.cjk_latin_spacing {
|
||||||
if cjk_latin_spacing {
|
|
||||||
add_cjk_latin_spacing(&mut items);
|
add_cjk_latin_spacing(&mut items);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only apply hanging indent to real paragraphs.
|
|
||||||
let hang = if situation.is_some() {
|
|
||||||
ParElem::hanging_indent_in(styles)
|
|
||||||
} else {
|
|
||||||
Abs::zero()
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Preparation {
|
Ok(Preparation {
|
||||||
|
config,
|
||||||
text,
|
text,
|
||||||
bidi: is_bidi.then_some(bidi),
|
bidi: is_bidi.then_some(bidi),
|
||||||
items,
|
items,
|
||||||
indices,
|
indices,
|
||||||
spans,
|
spans,
|
||||||
hyphenate: shared_get(children, styles, TextElem::hyphenate_in),
|
|
||||||
costs: TextElem::costs_in(styles),
|
|
||||||
dir,
|
|
||||||
lang: shared_get(children, styles, TextElem::lang_in),
|
|
||||||
align: AlignElem::alignment_in(styles).resolve(styles).x,
|
|
||||||
justify: ParElem::justify_in(styles),
|
|
||||||
hang,
|
|
||||||
cjk_latin_spacing,
|
|
||||||
fallback: TextElem::fallback_in(styles),
|
|
||||||
linebreaks: ParElem::linebreaks_in(styles),
|
|
||||||
size: TextElem::size_in(styles),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a style property, but only if it is the same for all of the children.
|
|
||||||
fn shared_get<T: PartialEq>(
|
|
||||||
children: &[Pair],
|
|
||||||
styles: StyleChain<'_>,
|
|
||||||
getter: fn(StyleChain) -> T,
|
|
||||||
) -> Option<T> {
|
|
||||||
let value = getter(styles);
|
|
||||||
children
|
|
||||||
.group_by_key(|&(_, s)| s)
|
|
||||||
.all(|(s, _)| getter(s) == value)
|
|
||||||
.then_some(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add some spacing between Han characters and western characters. See
|
/// Add some spacing between Han characters and western characters. See
|
||||||
/// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition
|
/// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition
|
||||||
/// in Horizontal Written Mode
|
/// in Horizontal Written Mode
|
||||||
|
@ -107,7 +107,6 @@ fn layout_inline_text(
|
|||||||
styles,
|
styles,
|
||||||
Size::splat(Abs::inf()),
|
Size::splat(Abs::inf()),
|
||||||
false,
|
false,
|
||||||
None,
|
|
||||||
)?
|
)?
|
||||||
.into_frame();
|
.into_frame();
|
||||||
|
|
||||||
|
@ -110,7 +110,7 @@ impl f64 {
|
|||||||
f64::signum(self)
|
f64::signum(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts bytes to a float.
|
/// Interprets bytes as a float.
|
||||||
///
|
///
|
||||||
/// ```example
|
/// ```example
|
||||||
/// #float.from-bytes(bytes((0, 0, 0, 0, 0, 0, 240, 63))) \
|
/// #float.from-bytes(bytes((0, 0, 0, 0, 0, 0, 240, 63))) \
|
||||||
@ -120,8 +120,10 @@ impl f64 {
|
|||||||
pub fn from_bytes(
|
pub fn from_bytes(
|
||||||
/// The bytes that should be converted to a float.
|
/// The bytes that should be converted to a float.
|
||||||
///
|
///
|
||||||
/// Must be of length exactly 8 so that the result fits into a 64-bit
|
/// Must have a length of either 4 or 8. The bytes are then
|
||||||
/// float.
|
/// interpreted in [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754)'s
|
||||||
|
/// binary32 (single-precision) or binary64 (double-precision) format
|
||||||
|
/// depending on the length of the bytes.
|
||||||
bytes: Bytes,
|
bytes: Bytes,
|
||||||
/// The endianness of the conversion.
|
/// The endianness of the conversion.
|
||||||
#[named]
|
#[named]
|
||||||
@ -158,6 +160,13 @@ impl f64 {
|
|||||||
#[named]
|
#[named]
|
||||||
#[default(Endianness::Little)]
|
#[default(Endianness::Little)]
|
||||||
endian: Endianness,
|
endian: Endianness,
|
||||||
|
/// The size of the resulting bytes.
|
||||||
|
///
|
||||||
|
/// This must be either 4 or 8. The call will return the
|
||||||
|
/// representation of this float in either
|
||||||
|
/// [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754)'s binary32
|
||||||
|
/// (single-precision) or binary64 (double-precision) format
|
||||||
|
/// depending on the provided size.
|
||||||
#[named]
|
#[named]
|
||||||
#[default(8)]
|
#[default(8)]
|
||||||
size: u32,
|
size: u32,
|
||||||
|
@ -11,7 +11,7 @@ use crate::foundations::{
|
|||||||
use crate::html::{attr, tag, HtmlElem};
|
use crate::html::{attr, tag, HtmlElem};
|
||||||
use crate::introspection::Location;
|
use crate::introspection::Location;
|
||||||
use crate::layout::Position;
|
use crate::layout::Position;
|
||||||
use crate::text::{Hyphenate, TextElem};
|
use crate::text::TextElem;
|
||||||
|
|
||||||
/// Links to a URL or a location in the document.
|
/// Links to a URL or a location in the document.
|
||||||
///
|
///
|
||||||
@ -138,7 +138,7 @@ impl Show for Packed<LinkElem> {
|
|||||||
impl ShowSet for Packed<LinkElem> {
|
impl ShowSet for Packed<LinkElem> {
|
||||||
fn show_set(&self, _: StyleChain) -> Styles {
|
fn show_set(&self, _: StyleChain) -> Styles {
|
||||||
let mut out = Styles::new();
|
let mut out = Styles::new();
|
||||||
out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))));
|
out.set(TextElem::set_hyphenate(Smart::Custom(false)));
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
use ecow::EcoString;
|
use ecow::EcoString;
|
||||||
|
use typst_library::foundations::Target;
|
||||||
use typst_syntax::Spanned;
|
use typst_syntax::Spanned;
|
||||||
|
|
||||||
use crate::diag::{At, SourceResult};
|
use crate::diag::{warning, At, SourceResult};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain};
|
use crate::foundations::{
|
||||||
|
elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain, TargetElem,
|
||||||
|
};
|
||||||
use crate::introspection::Locatable;
|
use crate::introspection::Locatable;
|
||||||
use crate::World;
|
use crate::World;
|
||||||
|
|
||||||
@ -78,7 +81,12 @@ pub struct EmbedElem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Show for Packed<EmbedElem> {
|
impl Show for Packed<EmbedElem> {
|
||||||
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||||
|
if TargetElem::target_in(styles) == Target::Html {
|
||||||
|
engine
|
||||||
|
.sink
|
||||||
|
.warn(warning!(self.span(), "embed was ignored during HTML export"));
|
||||||
|
}
|
||||||
Ok(Content::empty())
|
Ok(Content::empty())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,6 @@ use crate::foundations::{
|
|||||||
};
|
};
|
||||||
use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel};
|
use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel};
|
||||||
use crate::math::{EquationElem, MathSize};
|
use crate::math::{EquationElem, MathSize};
|
||||||
use crate::model::ParElem;
|
|
||||||
use crate::visualize::{Color, Paint, RelativeTo, Stroke};
|
use crate::visualize::{Color, Paint, RelativeTo, Stroke};
|
||||||
use crate::World;
|
use crate::World;
|
||||||
|
|
||||||
@ -504,9 +503,8 @@ pub struct TextElem {
|
|||||||
/// enabling hyphenation can
|
/// enabling hyphenation can
|
||||||
/// improve justification.
|
/// improve justification.
|
||||||
/// ```
|
/// ```
|
||||||
#[resolve]
|
|
||||||
#[ghost]
|
#[ghost]
|
||||||
pub hyphenate: Hyphenate,
|
pub hyphenate: Smart<bool>,
|
||||||
|
|
||||||
/// The "cost" of various choices when laying out text. A higher cost means
|
/// The "cost" of various choices when laying out text. A higher cost means
|
||||||
/// the layout engine will make the choice less often. Costs are specified
|
/// the layout engine will make the choice less often. Costs are specified
|
||||||
@ -1110,27 +1108,6 @@ impl Resolve for TextDir {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether to hyphenate text.
|
|
||||||
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
|
|
||||||
pub struct Hyphenate(pub Smart<bool>);
|
|
||||||
|
|
||||||
cast! {
|
|
||||||
Hyphenate,
|
|
||||||
self => self.0.into_value(),
|
|
||||||
v: Smart<bool> => Self(v),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Resolve for Hyphenate {
|
|
||||||
type Output = bool;
|
|
||||||
|
|
||||||
fn resolve(self, styles: StyleChain) -> Self::Output {
|
|
||||||
match self.0 {
|
|
||||||
Smart::Auto => ParElem::justify_in(styles),
|
|
||||||
Smart::Custom(v) => v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A set of stylistic sets to enable.
|
/// A set of stylistic sets to enable.
|
||||||
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)]
|
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)]
|
||||||
pub struct StylisticSets(u32);
|
pub struct StylisticSets(u32);
|
||||||
|
@ -21,9 +21,7 @@ use crate::html::{tag, HtmlElem};
|
|||||||
use crate::layout::{BlockBody, BlockElem, Em, HAlignment};
|
use crate::layout::{BlockBody, BlockElem, Em, HAlignment};
|
||||||
use crate::loading::{DataSource, Load};
|
use crate::loading::{DataSource, Load};
|
||||||
use crate::model::{Figurable, ParElem};
|
use crate::model::{Figurable, ParElem};
|
||||||
use crate::text::{
|
use crate::text::{FontFamily, FontList, LinebreakElem, LocalName, TextElem, TextSize};
|
||||||
FontFamily, FontList, Hyphenate, LinebreakElem, LocalName, TextElem, TextSize,
|
|
||||||
};
|
|
||||||
use crate::visualize::Color;
|
use crate::visualize::Color;
|
||||||
use crate::World;
|
use crate::World;
|
||||||
|
|
||||||
@ -472,7 +470,7 @@ impl ShowSet for Packed<RawElem> {
|
|||||||
let mut out = Styles::new();
|
let mut out = Styles::new();
|
||||||
out.set(TextElem::set_overhang(false));
|
out.set(TextElem::set_overhang(false));
|
||||||
out.set(TextElem::set_lang(Lang::ENGLISH));
|
out.set(TextElem::set_lang(Lang::ENGLISH));
|
||||||
out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))));
|
out.set(TextElem::set_hyphenate(Smart::Custom(false)));
|
||||||
out.set(TextElem::set_size(TextSize(Em::new(0.8).into())));
|
out.set(TextElem::set_size(TextSize(Em::new(0.8).into())));
|
||||||
out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")])));
|
out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")])));
|
||||||
out.set(TextElem::set_cjk_latin_spacing(Smart::Custom(None)));
|
out.set(TextElem::set_cjk_latin_spacing(Smart::Custom(None)));
|
||||||
|
@ -251,6 +251,7 @@ impl<'s> SmartQuotes<'s> {
|
|||||||
"el" => ("‘", "’", "«", "»"),
|
"el" => ("‘", "’", "«", "»"),
|
||||||
"he" => ("’", "’", "”", "”"),
|
"he" => ("’", "’", "”", "”"),
|
||||||
"hr" => ("‘", "’", "„", "”"),
|
"hr" => ("‘", "’", "„", "”"),
|
||||||
|
"bg" => ("’", "’", "„", "“"),
|
||||||
_ if lang.dir() == Dir::RTL => ("’", "‘", "”", "“"),
|
_ if lang.dir() == Dir::RTL => ("’", "‘", "”", "“"),
|
||||||
_ => default,
|
_ => default,
|
||||||
};
|
};
|
||||||
|
@ -582,12 +582,11 @@ impl Gradient {
|
|||||||
let mut stops = stops
|
let mut stops = stops
|
||||||
.iter()
|
.iter()
|
||||||
.map(move |&(color, offset)| {
|
.map(move |&(color, offset)| {
|
||||||
let t = i as f64 / n as f64;
|
|
||||||
let r = offset.get();
|
let r = offset.get();
|
||||||
if i % 2 == 1 && mirror {
|
if i % 2 == 1 && mirror {
|
||||||
(color, Ratio::new(t + (1.0 - r) / n as f64))
|
(color, Ratio::new((i as f64 + 1.0 - r) / n as f64))
|
||||||
} else {
|
} else {
|
||||||
(color, Ratio::new(t + r / n as f64))
|
(color, Ratio::new((i as f64 + r) / n as f64))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@ -1230,7 +1229,7 @@ fn process_stops(stops: &[Spanned<GradientStop>]) -> SourceResult<Vec<(Color, Ra
|
|||||||
};
|
};
|
||||||
|
|
||||||
if stop.get() < last_stop {
|
if stop.get() < last_stop {
|
||||||
bail!(*span, "offsets must be in strictly monotonic order");
|
bail!(*span, "offsets must be in monotonic order");
|
||||||
}
|
}
|
||||||
|
|
||||||
last_stop = stop.get();
|
last_stop = stop.get();
|
||||||
|
@ -160,6 +160,8 @@ impl RasterImage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The image's pixel density in pixels per inch, if known.
|
/// The image's pixel density in pixels per inch, if known.
|
||||||
|
///
|
||||||
|
/// This is guaranteed to be positive.
|
||||||
pub fn dpi(&self) -> Option<f64> {
|
pub fn dpi(&self) -> Option<f64> {
|
||||||
self.0.dpi
|
self.0.dpi
|
||||||
}
|
}
|
||||||
@ -334,6 +336,9 @@ fn apply_rotation(image: &mut DynamicImage, rotation: u32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Try to determine the DPI (dots per inch) of the image.
|
/// Try to determine the DPI (dots per inch) of the image.
|
||||||
|
///
|
||||||
|
/// This is guaranteed to be a positive value, or `None` if invalid or
|
||||||
|
/// unspecified.
|
||||||
fn determine_dpi(data: &[u8], exif: Option<&exif::Exif>) -> Option<f64> {
|
fn determine_dpi(data: &[u8], exif: Option<&exif::Exif>) -> Option<f64> {
|
||||||
// Try to extract the DPI from the EXIF metadata. If that doesn't yield
|
// Try to extract the DPI from the EXIF metadata. If that doesn't yield
|
||||||
// anything, fall back to specialized procedures for extracting JPEG or PNG
|
// anything, fall back to specialized procedures for extracting JPEG or PNG
|
||||||
@ -341,6 +346,7 @@ fn determine_dpi(data: &[u8], exif: Option<&exif::Exif>) -> Option<f64> {
|
|||||||
exif.and_then(exif_dpi)
|
exif.and_then(exif_dpi)
|
||||||
.or_else(|| jpeg_dpi(data))
|
.or_else(|| jpeg_dpi(data))
|
||||||
.or_else(|| png_dpi(data))
|
.or_else(|| png_dpi(data))
|
||||||
|
.filter(|&dpi| dpi > 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to get the DPI from the EXIF metadata.
|
/// Try to get the DPI from the EXIF metadata.
|
||||||
|
BIN
tests/ref/issue-5831-par-constructor-args.png
Normal file
BIN
tests/ref/issue-5831-par-constructor-args.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
@ -322,6 +322,20 @@ A
|
|||||||
|
|
||||||
#context test(query(<a>).len(), 1)
|
#context test(query(<a>).len(), 1)
|
||||||
|
|
||||||
|
--- issue-5831-par-constructor-args ---
|
||||||
|
// Make sure that all arguments are also respected in the constructor.
|
||||||
|
A
|
||||||
|
#par(
|
||||||
|
leading: 2pt,
|
||||||
|
spacing: 20pt,
|
||||||
|
justify: true,
|
||||||
|
linebreaks: "simple",
|
||||||
|
first-line-indent: (amount: 1em, all: true),
|
||||||
|
hanging-indent: 5pt,
|
||||||
|
)[
|
||||||
|
The par function has a constructor and justification.
|
||||||
|
]
|
||||||
|
|
||||||
--- show-par-set-block-hint ---
|
--- show-par-set-block-hint ---
|
||||||
// Warning: 2-36 `show par: set block(spacing: ..)` has no effect anymore
|
// Warning: 2-36 `show par: set block(spacing: ..)` has no effect anymore
|
||||||
// Hint: 2-36 this is specific to paragraphs as they are not considered blocks anymore
|
// Hint: 2-36 this is specific to paragraphs as they are not considered blocks anymore
|
||||||
|
@ -658,3 +658,11 @@ $ A = mat(
|
|||||||
height: 10pt,
|
height: 10pt,
|
||||||
fill: gradient.linear(violet, blue, space: cmyk)
|
fill: gradient.linear(violet, blue, space: cmyk)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
--- issue-5819-gradient-repeat ---
|
||||||
|
// Ensure the gradient constructor generates monotonic stops which can be fed
|
||||||
|
// back into the gradient constructor itself.
|
||||||
|
#let my-gradient = gradient.linear(red, blue).repeat(5)
|
||||||
|
#let _ = gradient.linear(..my-gradient.stops())
|
||||||
|
#let my-gradient2 = gradient.linear(red, blue).repeat(5, mirror: true)
|
||||||
|
#let _ = gradient.linear(..my-gradient2.stops())
|
||||||
|
Loading…
x
Reference in New Issue
Block a user