mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
New interaction model
This commit is contained in:
parent
d9ce194fe7
commit
bf59c08a0a
128
library/src/core/behave.rs
Normal file
128
library/src/core/behave.rs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
//! Node interaction.
|
||||||
|
|
||||||
|
use typst::model::{capability, Content, StyleChain, StyleVec, StyleVecBuilder};
|
||||||
|
|
||||||
|
/// How a node interacts with other nodes.
|
||||||
|
#[capability]
|
||||||
|
pub trait Behave: 'static + Send + Sync {
|
||||||
|
/// The node's interaction behaviour.
|
||||||
|
fn behaviour(&self) -> Behaviour;
|
||||||
|
|
||||||
|
/// Whether this weak node is larger than a previous one and thus picked as
|
||||||
|
/// the maximum when the levels are the same.
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn larger(&self, prev: &Content) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How a node interacts with other nodes in a stream.
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub enum Behaviour {
|
||||||
|
/// A weak node which only survives when a supportive node is before and
|
||||||
|
/// after it. Furthermore, per consecutive run of weak nodes, only one
|
||||||
|
/// survives: The one with the lowest weakness level (or the larger one if
|
||||||
|
/// there is a tie).
|
||||||
|
Weak(u8),
|
||||||
|
/// A node that enables adjacent weak nodes to exist. The default.
|
||||||
|
Supportive,
|
||||||
|
/// A node that destroys adjacent weak nodes.
|
||||||
|
Destructive,
|
||||||
|
/// A node that does not interact at all with other node, having the
|
||||||
|
/// same effect as if it didn't exist.
|
||||||
|
Ignorant,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper around a [`StyleVecBuilder`] that allows items to interact.
|
||||||
|
pub struct BehavedBuilder<'a> {
|
||||||
|
/// The internal builder.
|
||||||
|
builder: StyleVecBuilder<'a, Content>,
|
||||||
|
/// Staged weak and ignorant items that we can't yet commit to the builder.
|
||||||
|
/// The option is `Some(_)` for weak items and `None` for ignorant items.
|
||||||
|
staged: Vec<(Content, Behaviour, StyleChain<'a>)>,
|
||||||
|
/// What the last non-ignorant item was.
|
||||||
|
last: Behaviour,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> BehavedBuilder<'a> {
|
||||||
|
/// Create a new style-vec builder.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
builder: StyleVecBuilder::new(),
|
||||||
|
staged: vec![],
|
||||||
|
last: Behaviour::Destructive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the builder is empty.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.builder.is_empty() && self.staged.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push an item into the sequence.
|
||||||
|
pub fn push(&mut self, item: Content, styles: StyleChain<'a>) {
|
||||||
|
let interaction = item
|
||||||
|
.to::<dyn Behave>()
|
||||||
|
.map_or(Behaviour::Supportive, Behave::behaviour);
|
||||||
|
|
||||||
|
match interaction {
|
||||||
|
Behaviour::Weak(level) => {
|
||||||
|
if matches!(self.last, Behaviour::Weak(_)) {
|
||||||
|
let item = item.to::<dyn Behave>().unwrap();
|
||||||
|
let i = self.staged.iter().position(|prev| {
|
||||||
|
let Behaviour::Weak(prev_level) = prev.1 else { return false };
|
||||||
|
level < prev_level
|
||||||
|
|| (level == prev_level && item.larger(&prev.0))
|
||||||
|
});
|
||||||
|
let Some(i) = i else { return };
|
||||||
|
self.staged.remove(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.last != Behaviour::Destructive {
|
||||||
|
self.staged.push((item, interaction, styles));
|
||||||
|
self.last = interaction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behaviour::Supportive => {
|
||||||
|
self.flush(true);
|
||||||
|
self.builder.push(item, styles);
|
||||||
|
self.last = interaction;
|
||||||
|
}
|
||||||
|
Behaviour::Destructive => {
|
||||||
|
self.flush(false);
|
||||||
|
self.builder.push(item, styles);
|
||||||
|
self.last = interaction;
|
||||||
|
}
|
||||||
|
Behaviour::Ignorant => {
|
||||||
|
self.staged.push((item, interaction, styles));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over the contained items.
|
||||||
|
pub fn items(&self) -> impl DoubleEndedIterator<Item = &Content> {
|
||||||
|
self.builder.items().chain(self.staged.iter().map(|(item, ..)| item))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the finish style vec and the common prefix chain.
|
||||||
|
pub fn finish(mut self) -> (StyleVec<Content>, StyleChain<'a>) {
|
||||||
|
self.flush(false);
|
||||||
|
self.builder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push the staged items, filtering out weak items if `supportive` is
|
||||||
|
/// false.
|
||||||
|
fn flush(&mut self, supportive: bool) {
|
||||||
|
for (item, interaction, styles) in self.staged.drain(..) {
|
||||||
|
if supportive || interaction == Behaviour::Ignorant {
|
||||||
|
self.builder.push(item, styles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Default for BehavedBuilder<'a> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
use super::*;
|
//! Extension traits.
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
/// Additional methods on content.
|
/// Additional methods on content.
|
||||||
@ -33,31 +34,31 @@ pub trait ContentExt {
|
|||||||
|
|
||||||
impl ContentExt for Content {
|
impl ContentExt for Content {
|
||||||
fn strong(self) -> Self {
|
fn strong(self) -> Self {
|
||||||
text::StrongNode(self).pack()
|
crate::text::StrongNode(self).pack()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn emph(self) -> Self {
|
fn emph(self) -> Self {
|
||||||
text::EmphNode(self).pack()
|
crate::text::EmphNode(self).pack()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn underlined(self) -> Self {
|
fn underlined(self) -> Self {
|
||||||
text::DecoNode::<{ text::UNDERLINE }>(self).pack()
|
crate::text::DecoNode::<{ crate::text::UNDERLINE }>(self).pack()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn boxed(self, sizing: Axes<Option<Rel<Length>>>) -> Self {
|
fn boxed(self, sizing: Axes<Option<Rel<Length>>>) -> Self {
|
||||||
layout::BoxNode { sizing, child: self }.pack()
|
crate::layout::BoxNode { sizing, child: self }.pack()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self {
|
fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self {
|
||||||
layout::AlignNode { aligns, child: self }.pack()
|
crate::layout::AlignNode { aligns, child: self }.pack()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn padded(self, padding: Sides<Rel<Length>>) -> Self {
|
fn padded(self, padding: Sides<Rel<Length>>) -> Self {
|
||||||
layout::PadNode { padding, child: self }.pack()
|
crate::layout::PadNode { padding, child: self }.pack()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn moved(self, delta: Axes<Rel<Length>>) -> Self {
|
fn moved(self, delta: Axes<Rel<Length>>) -> Self {
|
||||||
layout::MoveNode { delta, child: self }.pack()
|
crate::layout::MoveNode { delta, child: self }.pack()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn filled(self, fill: Paint) -> Self {
|
fn filled(self, fill: Paint) -> Self {
|
||||||
@ -73,16 +74,16 @@ impl ContentExt for Content {
|
|||||||
pub trait StyleMapExt {
|
pub trait StyleMapExt {
|
||||||
/// Set a font family composed of a preferred family and existing families
|
/// Set a font family composed of a preferred family and existing families
|
||||||
/// from a style chain.
|
/// from a style chain.
|
||||||
fn set_family(&mut self, preferred: text::FontFamily, existing: StyleChain);
|
fn set_family(&mut self, preferred: crate::text::FontFamily, existing: StyleChain);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StyleMapExt for StyleMap {
|
impl StyleMapExt for StyleMap {
|
||||||
fn set_family(&mut self, preferred: text::FontFamily, existing: StyleChain) {
|
fn set_family(&mut self, preferred: crate::text::FontFamily, existing: StyleChain) {
|
||||||
self.set(
|
self.set(
|
||||||
text::TextNode::FAMILY,
|
crate::text::TextNode::FAMILY,
|
||||||
text::FallbackList(
|
crate::text::FallbackList(
|
||||||
std::iter::once(preferred)
|
std::iter::once(preferred)
|
||||||
.chain(existing.get(text::TextNode::FAMILY).0.iter().cloned())
|
.chain(existing.get(crate::text::TextNode::FAMILY).0.iter().cloned())
|
||||||
.collect(),
|
.collect(),
|
||||||
),
|
),
|
||||||
);
|
);
|
7
library/src/core/mod.rs
Normal file
7
library/src/core/mod.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
//! Central definitions for the standard library.
|
||||||
|
|
||||||
|
mod behave;
|
||||||
|
mod ext;
|
||||||
|
|
||||||
|
pub use behave::*;
|
||||||
|
pub use ext::*;
|
@ -104,10 +104,20 @@ pub struct ColbreakNode {
|
|||||||
pub weak: bool,
|
pub weak: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node]
|
#[node(Behave)]
|
||||||
impl ColbreakNode {
|
impl ColbreakNode {
|
||||||
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
|
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
|
||||||
let weak = args.named("weak")?.unwrap_or(false);
|
let weak = args.named("weak")?.unwrap_or(false);
|
||||||
Ok(Self { weak }.pack())
|
Ok(Self { weak }.pack())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Behave for ColbreakNode {
|
||||||
|
fn behaviour(&self) -> Behaviour {
|
||||||
|
if self.weak {
|
||||||
|
Behaviour::Weak(1)
|
||||||
|
} else {
|
||||||
|
Behaviour::Destructive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -66,19 +66,25 @@ pub struct BlockNode(pub Content);
|
|||||||
impl BlockNode {
|
impl BlockNode {
|
||||||
/// The spacing between the previous and this block.
|
/// The spacing between the previous and this block.
|
||||||
#[property(skip)]
|
#[property(skip)]
|
||||||
pub const ABOVE: VNode = VNode::weak(Em::new(1.2).into());
|
pub const ABOVE: VNode = VNode::block_spacing(Em::new(1.2).into());
|
||||||
/// The spacing between this and the following block.
|
/// The spacing between this and the following block.
|
||||||
#[property(skip)]
|
#[property(skip)]
|
||||||
pub const BELOW: VNode = VNode::weak(Em::new(1.2).into());
|
pub const BELOW: VNode = VNode::block_spacing(Em::new(1.2).into());
|
||||||
|
|
||||||
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
|
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
|
||||||
Ok(Self(args.eat()?.unwrap_or_default()).pack())
|
Ok(Self(args.eat()?.unwrap_or_default()).pack())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set(...) {
|
fn set(...) {
|
||||||
let spacing = args.named("spacing")?.map(VNode::weak);
|
let spacing = args.named("spacing")?.map(VNode::block_spacing);
|
||||||
styles.set_opt(Self::ABOVE, args.named("above")?.map(VNode::strong).or(spacing));
|
styles.set_opt(
|
||||||
styles.set_opt(Self::BELOW, args.named("below")?.map(VNode::strong).or(spacing));
|
Self::ABOVE,
|
||||||
|
args.named("above")?.map(VNode::block_around).or(spacing),
|
||||||
|
);
|
||||||
|
styles.set_opt(
|
||||||
|
Self::BELOW,
|
||||||
|
args.named("below")?.map(VNode::block_around).or(spacing),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
use std::cmp::Ordering;
|
use super::{AlignNode, ColbreakNode, PlaceNode, Spacing, VNode};
|
||||||
|
|
||||||
use super::{AlignNode, PlaceNode, Spacing, VNode};
|
|
||||||
use crate::layout::BlockNode;
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::text::ParNode;
|
use crate::text::ParNode;
|
||||||
|
|
||||||
@ -10,18 +7,7 @@ use crate::text::ParNode;
|
|||||||
/// This node is reponsible for layouting both the top-level content flow and
|
/// This node is reponsible for layouting both the top-level content flow and
|
||||||
/// the contents of boxes.
|
/// the contents of boxes.
|
||||||
#[derive(Hash)]
|
#[derive(Hash)]
|
||||||
pub struct FlowNode(pub StyleVec<FlowChild>);
|
pub struct FlowNode(pub StyleVec<Content>);
|
||||||
|
|
||||||
/// A child of a flow node.
|
|
||||||
#[derive(Hash, PartialEq)]
|
|
||||||
pub enum FlowChild {
|
|
||||||
/// Vertical spacing between other children.
|
|
||||||
Spacing(VNode),
|
|
||||||
/// Arbitrary block-level content.
|
|
||||||
Block(Content),
|
|
||||||
/// A column / region break.
|
|
||||||
Colbreak,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[node(LayoutBlock)]
|
#[node(LayoutBlock)]
|
||||||
impl FlowNode {}
|
impl FlowNode {}
|
||||||
@ -33,20 +19,18 @@ impl LayoutBlock for FlowNode {
|
|||||||
regions: &Regions,
|
regions: &Regions,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
) -> SourceResult<Vec<Frame>> {
|
) -> SourceResult<Vec<Frame>> {
|
||||||
let mut layouter = FlowLayouter::new(regions, styles);
|
let mut layouter = FlowLayouter::new(regions);
|
||||||
|
|
||||||
for (child, map) in self.0.iter() {
|
for (child, map) in self.0.iter() {
|
||||||
let styles = map.chain(&styles);
|
let styles = map.chain(&styles);
|
||||||
match child {
|
if let Some(&node) = child.downcast::<VNode>() {
|
||||||
FlowChild::Spacing(node) => {
|
layouter.layout_spacing(node.amount, styles);
|
||||||
layouter.layout_spacing(node, styles);
|
} else if child.has::<dyn LayoutBlock>() {
|
||||||
}
|
layouter.layout_block(world, child, styles)?;
|
||||||
FlowChild::Block(block) => {
|
} else if child.is::<ColbreakNode>() {
|
||||||
layouter.layout_block(world, block, styles)?;
|
|
||||||
}
|
|
||||||
FlowChild::Colbreak => {
|
|
||||||
layouter.finish_region();
|
layouter.finish_region();
|
||||||
}
|
} else {
|
||||||
|
panic!("unexpected flow child: {child:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,31 +45,10 @@ impl Debug for FlowNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for FlowChild {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Spacing(kind) => write!(f, "{:?}", kind),
|
|
||||||
Self::Block(block) => block.fmt(f),
|
|
||||||
Self::Colbreak => f.pad("Colbreak"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialOrd for FlowChild {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
||||||
match (self, other) {
|
|
||||||
(Self::Spacing(a), Self::Spacing(b)) => a.partial_cmp(b),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Performs flow layout.
|
/// Performs flow layout.
|
||||||
struct FlowLayouter<'a> {
|
struct FlowLayouter {
|
||||||
/// The regions to layout children into.
|
/// The regions to layout children into.
|
||||||
regions: Regions,
|
regions: Regions,
|
||||||
/// The shared styles.
|
|
||||||
shared: StyleChain<'a>,
|
|
||||||
/// Whether the flow should expand to fill the region.
|
/// Whether the flow should expand to fill the region.
|
||||||
expand: Axes<bool>,
|
expand: Axes<bool>,
|
||||||
/// The full size of `regions.size` that was available before we started
|
/// The full size of `regions.size` that was available before we started
|
||||||
@ -95,8 +58,6 @@ struct FlowLayouter<'a> {
|
|||||||
used: Size,
|
used: Size,
|
||||||
/// The sum of fractions in the current region.
|
/// The sum of fractions in the current region.
|
||||||
fr: Fr,
|
fr: Fr,
|
||||||
/// The spacing below the last block.
|
|
||||||
below: Option<VNode>,
|
|
||||||
/// Spacing and layouted blocks.
|
/// Spacing and layouted blocks.
|
||||||
items: Vec<FlowItem>,
|
items: Vec<FlowItem>,
|
||||||
/// Finished frames for previous regions.
|
/// Finished frames for previous regions.
|
||||||
@ -115,9 +76,9 @@ enum FlowItem {
|
|||||||
Placed(Frame),
|
Placed(Frame),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> FlowLayouter<'a> {
|
impl FlowLayouter {
|
||||||
/// Create a new flow layouter.
|
/// Create a new flow layouter.
|
||||||
fn new(regions: &Regions, shared: StyleChain<'a>) -> Self {
|
fn new(regions: &Regions) -> Self {
|
||||||
let expand = regions.expand;
|
let expand = regions.expand;
|
||||||
let full = regions.first;
|
let full = regions.first;
|
||||||
|
|
||||||
@ -127,20 +88,18 @@ impl<'a> FlowLayouter<'a> {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
regions,
|
regions,
|
||||||
shared,
|
|
||||||
expand,
|
expand,
|
||||||
full,
|
full,
|
||||||
used: Size::zero(),
|
used: Size::zero(),
|
||||||
fr: Fr::zero(),
|
fr: Fr::zero(),
|
||||||
below: None,
|
|
||||||
items: vec![],
|
items: vec![],
|
||||||
finished: vec![],
|
finished: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Layout spacing.
|
/// Actually layout the spacing.
|
||||||
fn layout_spacing(&mut self, node: &VNode, styles: StyleChain) {
|
fn layout_spacing(&mut self, spacing: Spacing, styles: StyleChain) {
|
||||||
match node.amount {
|
match spacing {
|
||||||
Spacing::Relative(v) => {
|
Spacing::Relative(v) => {
|
||||||
// Resolve the spacing and limit it to the remaining space.
|
// Resolve the spacing and limit it to the remaining space.
|
||||||
let resolved = v.resolve(styles).relative_to(self.full.y);
|
let resolved = v.resolve(styles).relative_to(self.full.y);
|
||||||
@ -154,10 +113,6 @@ impl<'a> FlowLayouter<'a> {
|
|||||||
self.fr += v;
|
self.fr += v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if node.weak || node.amount.is_fractional() {
|
|
||||||
self.below = None;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Layout a block.
|
/// Layout a block.
|
||||||
@ -172,19 +127,9 @@ impl<'a> FlowLayouter<'a> {
|
|||||||
self.finish_region();
|
self.finish_region();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add spacing between the last block and this one.
|
|
||||||
if let Some(below) = self.below.take() {
|
|
||||||
let above = styles.get(BlockNode::ABOVE);
|
|
||||||
let pick_below = (above.weak && !below.weak) || (below.amount > above.amount);
|
|
||||||
let spacing = if pick_below { below } else { above };
|
|
||||||
self.layout_spacing(&spacing, self.shared);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Placed nodes that are out of flow produce placed items which aren't
|
// Placed nodes that are out of flow produce placed items which aren't
|
||||||
// aligned later.
|
// aligned later.
|
||||||
let mut is_placed = false;
|
|
||||||
if let Some(placed) = block.downcast::<PlaceNode>() {
|
if let Some(placed) = block.downcast::<PlaceNode>() {
|
||||||
is_placed = true;
|
|
||||||
if placed.out_of_flow() {
|
if placed.out_of_flow() {
|
||||||
let frame = block.layout_block(world, &self.regions, styles)?.remove(0);
|
let frame = block.layout_block(world, &self.regions, styles)?.remove(0);
|
||||||
self.items.push(FlowItem::Placed(frame));
|
self.items.push(FlowItem::Placed(frame));
|
||||||
@ -205,6 +150,7 @@ impl<'a> FlowLayouter<'a> {
|
|||||||
.unwrap_or(Align::Top),
|
.unwrap_or(Align::Top),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Layout the block itself.
|
||||||
let frames = block.layout_block(world, &self.regions, styles)?;
|
let frames = block.layout_block(world, &self.regions, styles)?;
|
||||||
let len = frames.len();
|
let len = frames.len();
|
||||||
for (i, frame) in frames.into_iter().enumerate() {
|
for (i, frame) in frames.into_iter().enumerate() {
|
||||||
@ -220,10 +166,6 @@ impl<'a> FlowLayouter<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !is_placed {
|
|
||||||
self.below = Some(styles.get(BlockNode::BELOW));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,7 +214,6 @@ impl<'a> FlowLayouter<'a> {
|
|||||||
self.full = self.regions.first;
|
self.full = self.regions.first;
|
||||||
self.used = Size::zero();
|
self.used = Size::zero();
|
||||||
self.fr = Fr::zero();
|
self.fr = Fr::zero();
|
||||||
self.below = None;
|
|
||||||
self.finished.push(output);
|
self.finished.push(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,16 +32,18 @@ use typst::diag::SourceResult;
|
|||||||
use typst::frame::Frame;
|
use typst::frame::Frame;
|
||||||
use typst::geom::*;
|
use typst::geom::*;
|
||||||
use typst::model::{
|
use typst::model::{
|
||||||
capability, Content, Node, SequenceNode, Show, StyleChain, StyleEntry, StyleVec,
|
capability, Content, Node, SequenceNode, Show, StyleChain, StyleEntry,
|
||||||
StyleVecBuilder, StyledNode, Target,
|
StyleVecBuilder, StyledNode, Target,
|
||||||
};
|
};
|
||||||
use typst::World;
|
use typst::World;
|
||||||
|
|
||||||
|
use crate::core::BehavedBuilder;
|
||||||
|
use crate::prelude::*;
|
||||||
use crate::structure::{
|
use crate::structure::{
|
||||||
DescNode, DocNode, EnumNode, ListItem, ListNode, DESC, ENUM, LIST,
|
DescNode, DocNode, EnumNode, ListItem, ListNode, DESC, ENUM, LIST,
|
||||||
};
|
};
|
||||||
use crate::text::{
|
use crate::text::{
|
||||||
LinebreakNode, ParChild, ParNode, ParbreakNode, SmartQuoteNode, SpaceNode, TextNode,
|
LinebreakNode, ParNode, ParbreakNode, SmartQuoteNode, SpaceNode, TextNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Root-level layout.
|
/// Root-level layout.
|
||||||
@ -468,41 +470,17 @@ impl Default for DocBuilder<'_> {
|
|||||||
|
|
||||||
/// Accepts flow content.
|
/// Accepts flow content.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct FlowBuilder<'a>(CollapsingBuilder<'a, FlowChild>, bool);
|
struct FlowBuilder<'a>(BehavedBuilder<'a>, bool);
|
||||||
|
|
||||||
impl<'a> FlowBuilder<'a> {
|
impl<'a> FlowBuilder<'a> {
|
||||||
fn accept(&mut self, content: &Content, styles: StyleChain<'a>) -> bool {
|
fn accept(&mut self, content: &Content, styles: StyleChain<'a>) -> bool {
|
||||||
// Weak flow elements:
|
|
||||||
// Weakness | Element
|
|
||||||
// 0 | weak colbreak
|
|
||||||
// 1 | weak fractional spacing
|
|
||||||
// 2 | weak spacing
|
|
||||||
// 3 | generated weak spacing
|
|
||||||
// 4 | generated weak fractional spacing
|
|
||||||
// 5 | par spacing
|
|
||||||
|
|
||||||
let last_was_parbreak = self.1;
|
let last_was_parbreak = self.1;
|
||||||
self.1 = false;
|
self.1 = false;
|
||||||
|
|
||||||
if content.is::<ParbreakNode>() {
|
if content.is::<ParbreakNode>() {
|
||||||
self.1 = true;
|
self.1 = true;
|
||||||
} else if let Some(colbreak) = content.downcast::<ColbreakNode>() {
|
} else if content.is::<VNode>() || content.is::<ColbreakNode>() {
|
||||||
if colbreak.weak {
|
self.0.push(content.clone(), styles);
|
||||||
self.0.weak(FlowChild::Colbreak, styles, 0);
|
|
||||||
} else {
|
|
||||||
self.0.destructive(FlowChild::Colbreak, styles);
|
|
||||||
}
|
|
||||||
} else if let Some(vertical) = content.downcast::<VNode>() {
|
|
||||||
let child = FlowChild::Spacing(*vertical);
|
|
||||||
let frac = vertical.amount.is_fractional();
|
|
||||||
if vertical.weak {
|
|
||||||
let weakness = 1 + u8::from(frac);
|
|
||||||
self.0.weak(child, styles, weakness);
|
|
||||||
} else if frac {
|
|
||||||
self.0.destructive(child, styles);
|
|
||||||
} else {
|
|
||||||
self.0.ignorant(child, styles);
|
|
||||||
}
|
|
||||||
} else if content.has::<dyn LayoutBlock>() {
|
} else if content.has::<dyn LayoutBlock>() {
|
||||||
if !last_was_parbreak {
|
if !last_was_parbreak {
|
||||||
let tight = if let Some(node) = content.downcast::<ListNode>() {
|
let tight = if let Some(node) = content.downcast::<ListNode>() {
|
||||||
@ -517,17 +495,16 @@ impl<'a> FlowBuilder<'a> {
|
|||||||
|
|
||||||
if tight {
|
if tight {
|
||||||
let leading = styles.get(ParNode::LEADING);
|
let leading = styles.get(ParNode::LEADING);
|
||||||
let spacing = VNode::weak(leading.into());
|
let spacing = VNode::list_attach(leading.into());
|
||||||
self.0.weak(FlowChild::Spacing(spacing), styles, 1);
|
self.0.push(spacing.pack(), styles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let child = FlowChild::Block(content.clone());
|
let above = styles.get(BlockNode::ABOVE);
|
||||||
if content.is::<PlaceNode>() {
|
let below = styles.get(BlockNode::BELOW);
|
||||||
self.0.ignorant(child, styles);
|
self.0.push(above.pack(), styles);
|
||||||
} else {
|
self.0.push(content.clone(), styles);
|
||||||
self.0.supportive(child, styles);
|
self.0.push(below.pack(), styles);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -549,43 +526,22 @@ impl<'a> FlowBuilder<'a> {
|
|||||||
|
|
||||||
/// Accepts paragraph content.
|
/// Accepts paragraph content.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct ParBuilder<'a>(CollapsingBuilder<'a, ParChild>);
|
struct ParBuilder<'a>(BehavedBuilder<'a>);
|
||||||
|
|
||||||
impl<'a> ParBuilder<'a> {
|
impl<'a> ParBuilder<'a> {
|
||||||
fn accept(&mut self, content: &Content, styles: StyleChain<'a>) -> bool {
|
fn accept(&mut self, content: &Content, styles: StyleChain<'a>) -> bool {
|
||||||
// Weak par elements:
|
if content.is::<SpaceNode>()
|
||||||
// Weakness | Element
|
|| content.is::<LinebreakNode>()
|
||||||
// 0 | weak fractional spacing
|
|| content.is::<HNode>()
|
||||||
// 1 | weak spacing
|
|| content.is::<SmartQuoteNode>()
|
||||||
// 2 | space
|
|| content.is::<TextNode>()
|
||||||
|
|| content.has::<dyn LayoutInline>()
|
||||||
if content.is::<SpaceNode>() {
|
{
|
||||||
self.0.weak(ParChild::Text(' '.into()), styles, 2);
|
self.0.push(content.clone(), styles);
|
||||||
} else if let Some(linebreak) = content.downcast::<LinebreakNode>() {
|
return true;
|
||||||
let c = if linebreak.justify { '\u{2028}' } else { '\n' };
|
|
||||||
self.0.destructive(ParChild::Text(c.into()), styles);
|
|
||||||
} else if let Some(horizontal) = content.downcast::<HNode>() {
|
|
||||||
let child = ParChild::Spacing(horizontal.amount);
|
|
||||||
let frac = horizontal.amount.is_fractional();
|
|
||||||
if horizontal.weak {
|
|
||||||
let weakness = u8::from(!frac);
|
|
||||||
self.0.weak(child, styles, weakness);
|
|
||||||
} else if frac {
|
|
||||||
self.0.destructive(child, styles);
|
|
||||||
} else {
|
|
||||||
self.0.ignorant(child, styles);
|
|
||||||
}
|
|
||||||
} else if let Some(quote) = content.downcast::<SmartQuoteNode>() {
|
|
||||||
self.0.supportive(ParChild::Quote { double: quote.double }, styles);
|
|
||||||
} else if let Some(text) = content.downcast::<TextNode>() {
|
|
||||||
self.0.supportive(ParChild::Text(text.0.clone()), styles);
|
|
||||||
} else if content.has::<dyn LayoutInline>() {
|
|
||||||
self.0.supportive(ParChild::Inline(content.clone()), styles);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(self, parent: &mut Builder<'a>) {
|
fn finish(self, parent: &mut Builder<'a>) {
|
||||||
@ -600,10 +556,14 @@ impl<'a> ParBuilder<'a> {
|
|||||||
if !indent.is_zero()
|
if !indent.is_zero()
|
||||||
&& children
|
&& children
|
||||||
.items()
|
.items()
|
||||||
.find_map(|child| match child {
|
.find_map(|child| {
|
||||||
ParChild::Spacing(_) => None,
|
if child.is::<TextNode>() || child.is::<SmartQuoteNode>() {
|
||||||
ParChild::Text(_) | ParChild::Quote { .. } => Some(true),
|
Some(true)
|
||||||
ParChild::Inline(_) => Some(false),
|
} else if child.has::<dyn LayoutInline>() {
|
||||||
|
Some(false)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
&& parent
|
&& parent
|
||||||
@ -611,14 +571,10 @@ impl<'a> ParBuilder<'a> {
|
|||||||
.0
|
.0
|
||||||
.items()
|
.items()
|
||||||
.rev()
|
.rev()
|
||||||
.find_map(|child| match child {
|
.find(|child| child.has::<dyn LayoutBlock>())
|
||||||
FlowChild::Spacing(_) => None,
|
.map_or(false, |child| child.is::<ParNode>())
|
||||||
FlowChild::Block(content) => Some(content.is::<ParNode>()),
|
|
||||||
FlowChild::Colbreak => Some(false),
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
{
|
{
|
||||||
children.push_front(ParChild::Spacing(indent.into()));
|
children.push_front(HNode::strong(indent.into()).pack());
|
||||||
}
|
}
|
||||||
|
|
||||||
parent.flow.accept(&ParNode(children).pack(), shared);
|
parent.flow.accept(&ParNode(children).pack(), shared);
|
||||||
@ -701,115 +657,3 @@ impl Default for ListBuilder<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A wrapper around a [`StyleVecBuilder`] that allows to collapse items.
|
|
||||||
struct CollapsingBuilder<'a, T> {
|
|
||||||
/// The internal builder.
|
|
||||||
builder: StyleVecBuilder<'a, T>,
|
|
||||||
/// Staged weak and ignorant items that we can't yet commit to the builder.
|
|
||||||
/// The option is `Some(_)` for weak items and `None` for ignorant items.
|
|
||||||
staged: Vec<(T, StyleChain<'a>, Option<u8>)>,
|
|
||||||
/// What the last non-ignorant item was.
|
|
||||||
last: Last,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What the last non-ignorant item was.
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
|
||||||
enum Last {
|
|
||||||
Weak,
|
|
||||||
Destructive,
|
|
||||||
Supportive,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T> CollapsingBuilder<'a, T> {
|
|
||||||
/// Create a new style-vec builder.
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
builder: StyleVecBuilder::new(),
|
|
||||||
staged: vec![],
|
|
||||||
last: Last::Destructive,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether the builder is empty.
|
|
||||||
fn is_empty(&self) -> bool {
|
|
||||||
self.builder.is_empty() && self.staged.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Can only exist when there is at least one supportive item to its left
|
|
||||||
/// and to its right, with no destructive items in between. There may be
|
|
||||||
/// ignorant items in between in both directions.
|
|
||||||
///
|
|
||||||
/// Between weak items, there may be at least one per layer and among the
|
|
||||||
/// candidates the strongest one (smallest `weakness`) wins. When tied,
|
|
||||||
/// the one that compares larger through `PartialOrd` wins.
|
|
||||||
fn weak(&mut self, item: T, styles: StyleChain<'a>, weakness: u8)
|
|
||||||
where
|
|
||||||
T: PartialOrd,
|
|
||||||
{
|
|
||||||
if self.last == Last::Destructive {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.last == Last::Weak {
|
|
||||||
let weak = self.staged.iter().position(|(prev_item, _, prev_weakness)| {
|
|
||||||
prev_weakness.map_or(false, |prev_weakness| {
|
|
||||||
weakness < prev_weakness
|
|
||||||
|| (weakness == prev_weakness && item > *prev_item)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let Some(weak) = weak else { return };
|
|
||||||
self.staged.remove(weak);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.staged.push((item, styles, Some(weakness)));
|
|
||||||
self.last = Last::Weak;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Forces nearby weak items to collapse.
|
|
||||||
fn destructive(&mut self, item: T, styles: StyleChain<'a>) {
|
|
||||||
self.flush(false);
|
|
||||||
self.builder.push(item, styles);
|
|
||||||
self.last = Last::Destructive;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Allows nearby weak items to exist.
|
|
||||||
fn supportive(&mut self, item: T, styles: StyleChain<'a>) {
|
|
||||||
self.flush(true);
|
|
||||||
self.builder.push(item, styles);
|
|
||||||
self.last = Last::Supportive;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Has no influence on other items.
|
|
||||||
fn ignorant(&mut self, item: T, styles: StyleChain<'a>) {
|
|
||||||
self.staged.push((item, styles, None));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterate over the contained items.
|
|
||||||
fn items(&self) -> impl DoubleEndedIterator<Item = &T> {
|
|
||||||
self.builder.items().chain(self.staged.iter().map(|(item, ..)| item))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the finish style vec and the common prefix chain.
|
|
||||||
fn finish(mut self) -> (StyleVec<T>, StyleChain<'a>) {
|
|
||||||
self.flush(false);
|
|
||||||
self.builder.finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push the staged items, filtering out weak items if `supportive` is
|
|
||||||
/// false.
|
|
||||||
fn flush(&mut self, supportive: bool) {
|
|
||||||
for (item, styles, meta) in self.staged.drain(..) {
|
|
||||||
if supportive || meta.is_none() {
|
|
||||||
self.builder.push(item, styles);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T> Default for CollapsingBuilder<'a, T> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -5,7 +5,7 @@ use crate::prelude::*;
|
|||||||
#[derive(Debug, Hash)]
|
#[derive(Debug, Hash)]
|
||||||
pub struct PlaceNode(pub Content);
|
pub struct PlaceNode(pub Content);
|
||||||
|
|
||||||
#[node(LayoutBlock)]
|
#[node(LayoutBlock, Behave)]
|
||||||
impl PlaceNode {
|
impl PlaceNode {
|
||||||
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
|
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
|
||||||
let aligns = args.find()?.unwrap_or(Axes::with_x(Some(GenAlign::Start)));
|
let aligns = args.find()?.unwrap_or(Axes::with_x(Some(GenAlign::Start)));
|
||||||
@ -54,3 +54,9 @@ impl PlaceNode {
|
|||||||
.map_or(false, |node| node.aligns.y.is_some())
|
.map_or(false, |node| node.aligns.y.is_some())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Behave for PlaceNode {
|
||||||
|
fn behaviour(&self) -> Behaviour {
|
||||||
|
Behaviour::Ignorant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,11 +5,13 @@ use crate::prelude::*;
|
|||||||
/// Horizontal spacing.
|
/// Horizontal spacing.
|
||||||
#[derive(Debug, Copy, Clone, Hash)]
|
#[derive(Debug, Copy, Clone, Hash)]
|
||||||
pub struct HNode {
|
pub struct HNode {
|
||||||
|
/// The amount of horizontal spacing.
|
||||||
pub amount: Spacing,
|
pub amount: Spacing,
|
||||||
|
/// Whether the node is weak, see also [`Behaviour`].
|
||||||
pub weak: bool,
|
pub weak: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node]
|
#[node(Behave)]
|
||||||
impl HNode {
|
impl HNode {
|
||||||
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
|
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
|
||||||
let amount = args.expect("spacing")?;
|
let amount = args.expect("spacing")?;
|
||||||
@ -18,31 +20,98 @@ impl HNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vertical spacing.
|
impl HNode {
|
||||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd)]
|
/// Normal strong spacing.
|
||||||
pub struct VNode {
|
|
||||||
pub amount: Spacing,
|
|
||||||
pub weak: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VNode {
|
|
||||||
/// Create weak vertical spacing.
|
|
||||||
pub fn weak(amount: Spacing) -> Self {
|
|
||||||
Self { amount, weak: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create strong vertical spacing.
|
|
||||||
pub fn strong(amount: Spacing) -> Self {
|
pub fn strong(amount: Spacing) -> Self {
|
||||||
Self { amount, weak: false }
|
Self { amount, weak: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// User-created weak spacing.
|
||||||
|
pub fn weak(amount: Spacing) -> Self {
|
||||||
|
Self { amount, weak: true }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node]
|
impl Behave for HNode {
|
||||||
|
fn behaviour(&self) -> Behaviour {
|
||||||
|
if self.amount.is_fractional() {
|
||||||
|
Behaviour::Destructive
|
||||||
|
} else if self.weak {
|
||||||
|
Behaviour::Weak(1)
|
||||||
|
} else {
|
||||||
|
Behaviour::Ignorant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn larger(&self, prev: &Content) -> bool {
|
||||||
|
let Some(prev) = prev.downcast::<Self>() else { return false };
|
||||||
|
self.amount > prev.amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vertical spacing.
|
||||||
|
#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd)]
|
||||||
|
pub struct VNode {
|
||||||
|
/// The amount of vertical spacing.
|
||||||
|
pub amount: Spacing,
|
||||||
|
/// The node's weakness level, see also [`Behaviour`].
|
||||||
|
pub weakness: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[node(Behave)]
|
||||||
impl VNode {
|
impl VNode {
|
||||||
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
|
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
|
||||||
let amount = args.expect("spacing")?;
|
let amount = args.expect("spacing")?;
|
||||||
let weak = args.named("weak")?.unwrap_or(false);
|
let node = if args.named("weak")?.unwrap_or(false) {
|
||||||
Ok(Self { amount, weak }.pack())
|
Self::weak(amount)
|
||||||
|
} else {
|
||||||
|
Self::strong(amount)
|
||||||
|
};
|
||||||
|
Ok(node.pack())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VNode {
|
||||||
|
/// Normal strong spacing.
|
||||||
|
pub fn strong(amount: Spacing) -> Self {
|
||||||
|
Self { amount, weakness: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// User-created weak spacing.
|
||||||
|
pub fn weak(amount: Spacing) -> Self {
|
||||||
|
Self { amount, weakness: 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Weak spacing with list attach weakness.
|
||||||
|
pub fn list_attach(amount: Spacing) -> Self {
|
||||||
|
Self { amount, weakness: 2 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Weak spacing with BlockNode::ABOVE/BELOW weakness.
|
||||||
|
pub fn block_around(amount: Spacing) -> Self {
|
||||||
|
Self { amount, weakness: 3 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Weak spacing with BlockNode::SPACING weakness.
|
||||||
|
pub fn block_spacing(amount: Spacing) -> Self {
|
||||||
|
Self { amount, weakness: 4 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Behave for VNode {
|
||||||
|
fn behaviour(&self) -> Behaviour {
|
||||||
|
if self.amount.is_fractional() {
|
||||||
|
Behaviour::Destructive
|
||||||
|
} else if self.weakness > 0 {
|
||||||
|
Behaviour::Weak(self.weakness)
|
||||||
|
} else {
|
||||||
|
Behaviour::Ignorant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn larger(&self, prev: &Content) -> bool {
|
||||||
|
let Some(prev) = prev.downcast::<Self>() else { return false };
|
||||||
|
self.amount > prev.amount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
//! Typst's standard library.
|
//! Typst's standard library.
|
||||||
|
|
||||||
pub mod base;
|
pub mod base;
|
||||||
|
pub mod core;
|
||||||
pub mod graphics;
|
pub mod graphics;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
pub mod math;
|
pub mod math;
|
||||||
@ -8,8 +9,6 @@ pub mod prelude;
|
|||||||
pub mod structure;
|
pub mod structure;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
|
|
||||||
mod ext;
|
|
||||||
|
|
||||||
use typst::geom::{Align, Color, Dir, GenAlign};
|
use typst::geom::{Align, Color, Dir, GenAlign};
|
||||||
use typst::model::{LangItems, Node, Scope, StyleMap};
|
use typst::model::{LangItems, Node, Scope, StyleMap};
|
||||||
|
|
||||||
|
@ -1,20 +1,32 @@
|
|||||||
//! Helpful imports for creating library functionality.
|
//! Helpful imports for creating library functionality.
|
||||||
|
|
||||||
|
#[doc(no_inline)]
|
||||||
pub use std::fmt::{self, Debug, Formatter};
|
pub use std::fmt::{self, Debug, Formatter};
|
||||||
|
#[doc(no_inline)]
|
||||||
pub use std::num::NonZeroUsize;
|
pub use std::num::NonZeroUsize;
|
||||||
|
|
||||||
|
#[doc(no_inline)]
|
||||||
pub use comemo::Tracked;
|
pub use comemo::Tracked;
|
||||||
|
#[doc(no_inline)]
|
||||||
pub use typst::diag::{bail, error, with_alternative, At, SourceResult, StrResult};
|
pub use typst::diag::{bail, error, with_alternative, At, SourceResult, StrResult};
|
||||||
|
#[doc(no_inline)]
|
||||||
pub use typst::frame::*;
|
pub use typst::frame::*;
|
||||||
|
#[doc(no_inline)]
|
||||||
pub use typst::geom::*;
|
pub use typst::geom::*;
|
||||||
|
#[doc(no_inline)]
|
||||||
pub use typst::model::{
|
pub use typst::model::{
|
||||||
array, capability, castable, dict, dynamic, format_str, node, Args, Array, Cast,
|
array, capability, castable, dict, dynamic, format_str, node, Args, Array, Cast,
|
||||||
Content, Dict, Finalize, Fold, Func, Key, Node, RecipeId, Resolve, Scope, Show,
|
Content, Dict, Finalize, Fold, Func, Node, RecipeId, Resolve, Show, Smart, Str,
|
||||||
Smart, Str, StyleChain, StyleMap, StyleVec, Value, Vm,
|
StyleChain, StyleMap, StyleVec, Value, Vm,
|
||||||
};
|
};
|
||||||
|
#[doc(no_inline)]
|
||||||
pub use typst::syntax::{Span, Spanned};
|
pub use typst::syntax::{Span, Spanned};
|
||||||
|
#[doc(no_inline)]
|
||||||
pub use typst::util::{format_eco, EcoString};
|
pub use typst::util::{format_eco, EcoString};
|
||||||
|
#[doc(no_inline)]
|
||||||
pub use typst::World;
|
pub use typst::World;
|
||||||
|
|
||||||
pub use super::ext::{ContentExt, StyleMapExt};
|
#[doc(no_inline)]
|
||||||
pub use super::layout::{LayoutBlock, LayoutInline, Regions};
|
pub use crate::core::{Behave, Behaviour, ContentExt, StyleMapExt};
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use crate::layout::{LayoutBlock, LayoutInline, Regions};
|
||||||
|
@ -50,20 +50,21 @@ impl Finalize for HeadingNode {
|
|||||||
_: StyleChain,
|
_: StyleChain,
|
||||||
realized: Content,
|
realized: Content,
|
||||||
) -> SourceResult<Content> {
|
) -> SourceResult<Content> {
|
||||||
let size = Em::new(match self.level.get() {
|
let scale = match self.level.get() {
|
||||||
1 => 1.4,
|
1 => 1.4,
|
||||||
2 => 1.2,
|
2 => 1.2,
|
||||||
_ => 1.0,
|
_ => 1.0,
|
||||||
});
|
};
|
||||||
|
|
||||||
let above = Em::new(if self.level.get() == 1 { 1.8 } else { 1.44 });
|
let size = Em::new(scale);
|
||||||
let below = Em::new(0.66);
|
let above = Em::new(if self.level.get() == 1 { 1.8 } else { 1.44 }) / scale;
|
||||||
|
let below = Em::new(0.66) / scale;
|
||||||
|
|
||||||
let mut map = StyleMap::new();
|
let mut map = StyleMap::new();
|
||||||
map.set(TextNode::SIZE, TextSize(size.into()));
|
map.set(TextNode::SIZE, TextSize(size.into()));
|
||||||
map.set(TextNode::WEIGHT, FontWeight::BOLD);
|
map.set(TextNode::WEIGHT, FontWeight::BOLD);
|
||||||
map.set(BlockNode::ABOVE, VNode::strong(above.into()));
|
map.set(BlockNode::ABOVE, VNode::block_around(above.into()));
|
||||||
map.set(BlockNode::BELOW, VNode::strong(below.into()));
|
map.set(BlockNode::BELOW, VNode::block_around(below.into()));
|
||||||
|
|
||||||
Ok(realized.styled_with_map(map))
|
Ok(realized.styled_with_map(map))
|
||||||
}
|
}
|
||||||
|
@ -410,20 +410,26 @@ impl Fold for FontFeatures {
|
|||||||
#[derive(Debug, Clone, Hash)]
|
#[derive(Debug, Clone, Hash)]
|
||||||
pub struct SpaceNode;
|
pub struct SpaceNode;
|
||||||
|
|
||||||
#[node]
|
#[node(Behave)]
|
||||||
impl SpaceNode {
|
impl SpaceNode {
|
||||||
fn construct(_: &mut Vm, _: &mut Args) -> SourceResult<Content> {
|
fn construct(_: &mut Vm, _: &mut Args) -> SourceResult<Content> {
|
||||||
Ok(Self.pack())
|
Ok(Self.pack())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Behave for SpaceNode {
|
||||||
|
fn behaviour(&self) -> Behaviour {
|
||||||
|
Behaviour::Weak(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A line break.
|
/// A line break.
|
||||||
#[derive(Debug, Clone, Hash)]
|
#[derive(Debug, Clone, Hash)]
|
||||||
pub struct LinebreakNode {
|
pub struct LinebreakNode {
|
||||||
pub justify: bool,
|
pub justify: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node]
|
#[node(Behave)]
|
||||||
impl LinebreakNode {
|
impl LinebreakNode {
|
||||||
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
|
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
|
||||||
let justify = args.named("justify")?.unwrap_or(false);
|
let justify = args.named("justify")?.unwrap_or(false);
|
||||||
@ -431,6 +437,12 @@ impl LinebreakNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Behave for LinebreakNode {
|
||||||
|
fn behaviour(&self) -> Behaviour {
|
||||||
|
Behaviour::Destructive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A smart quote.
|
/// A smart quote.
|
||||||
#[derive(Debug, Clone, Hash)]
|
#[derive(Debug, Clone, Hash)]
|
||||||
pub struct SmartQuoteNode {
|
pub struct SmartQuoteNode {
|
||||||
|
@ -1,30 +1,19 @@
|
|||||||
use std::cmp::Ordering;
|
|
||||||
|
|
||||||
use typst::util::EcoString;
|
|
||||||
use unicode_bidi::{BidiInfo, Level as BidiLevel};
|
use unicode_bidi::{BidiInfo, Level as BidiLevel};
|
||||||
use unicode_script::{Script, UnicodeScript};
|
use unicode_script::{Script, UnicodeScript};
|
||||||
use xi_unicode::LineBreakIterator;
|
use xi_unicode::LineBreakIterator;
|
||||||
|
|
||||||
use super::{shape, Lang, Quoter, Quotes, ShapedText, TextNode};
|
use typst::model::Key;
|
||||||
use crate::layout::Spacing;
|
|
||||||
|
use super::{
|
||||||
|
shape, Lang, LinebreakNode, Quoter, Quotes, ShapedText, SmartQuoteNode, SpaceNode,
|
||||||
|
TextNode,
|
||||||
|
};
|
||||||
|
use crate::layout::{HNode, Spacing};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
/// Arrange text, spacing and inline-level nodes into a paragraph.
|
/// Arrange text, spacing and inline-level nodes into a paragraph.
|
||||||
#[derive(Hash)]
|
#[derive(Hash)]
|
||||||
pub struct ParNode(pub StyleVec<ParChild>);
|
pub struct ParNode(pub StyleVec<Content>);
|
||||||
|
|
||||||
/// A uniformly styled atomic piece of a paragraph.
|
|
||||||
#[derive(Hash, PartialEq)]
|
|
||||||
pub enum ParChild {
|
|
||||||
/// A chunk of text.
|
|
||||||
Text(EcoString),
|
|
||||||
/// A single or double smart quote.
|
|
||||||
Quote { double: bool },
|
|
||||||
/// Horizontal spacing between other children.
|
|
||||||
Spacing(Spacing),
|
|
||||||
/// Arbitrary inline-level content.
|
|
||||||
Inline(Content),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[node(LayoutBlock)]
|
#[node(LayoutBlock)]
|
||||||
impl ParNode {
|
impl ParNode {
|
||||||
@ -84,26 +73,6 @@ impl Debug for ParNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for ParChild {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Text(text) => write!(f, "Text({:?})", text),
|
|
||||||
Self::Quote { double } => write!(f, "Quote({double})"),
|
|
||||||
Self::Spacing(kind) => write!(f, "{:?}", kind),
|
|
||||||
Self::Inline(inline) => inline.fmt(f),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialOrd for ParChild {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
||||||
match (self, other) {
|
|
||||||
(Self::Spacing(a), Self::Spacing(b)) => a.partial_cmp(b),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A horizontal alignment.
|
/// A horizontal alignment.
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
pub struct HorizontalAlign(pub GenAlign);
|
pub struct HorizontalAlign(pub GenAlign);
|
||||||
@ -426,43 +395,52 @@ fn collect<'a>(
|
|||||||
|
|
||||||
while let Some((child, map)) = iter.next() {
|
while let Some((child, map)) = iter.next() {
|
||||||
let styles = map.chain(styles);
|
let styles = map.chain(styles);
|
||||||
let segment = match child {
|
let segment = if child.is::<SpaceNode>() {
|
||||||
ParChild::Text(text) => {
|
full.push(' ');
|
||||||
|
Segment::Text(1)
|
||||||
|
} else if let Some(node) = child.downcast::<TextNode>() {
|
||||||
let prev = full.len();
|
let prev = full.len();
|
||||||
if let Some(case) = styles.get(TextNode::CASE) {
|
if let Some(case) = styles.get(TextNode::CASE) {
|
||||||
full.push_str(&case.apply(text));
|
full.push_str(&case.apply(&node.0));
|
||||||
} else {
|
} else {
|
||||||
full.push_str(text);
|
full.push_str(&node.0);
|
||||||
}
|
}
|
||||||
Segment::Text(full.len() - prev)
|
Segment::Text(full.len() - prev)
|
||||||
}
|
} else if let Some(node) = child.downcast::<LinebreakNode>() {
|
||||||
&ParChild::Quote { double } => {
|
let c = if node.justify { '\u{2028}' } else { '\n' };
|
||||||
|
full.push(c);
|
||||||
|
Segment::Text(c.len_utf8())
|
||||||
|
} else if let Some(node) = child.downcast::<SmartQuoteNode>() {
|
||||||
let prev = full.len();
|
let prev = full.len();
|
||||||
if styles.get(TextNode::SMART_QUOTES) {
|
if styles.get(TextNode::SMART_QUOTES) {
|
||||||
let lang = styles.get(TextNode::LANG);
|
let lang = styles.get(TextNode::LANG);
|
||||||
let region = styles.get(TextNode::REGION);
|
let region = styles.get(TextNode::REGION);
|
||||||
let quotes = Quotes::from_lang(lang, region);
|
let quotes = Quotes::from_lang(lang, region);
|
||||||
let peeked = iter.peek().and_then(|(child, _)| match child {
|
let peeked = iter.peek().and_then(|(child, _)| {
|
||||||
ParChild::Text(text) => text.chars().next(),
|
if let Some(node) = child.downcast::<TextNode>() {
|
||||||
ParChild::Quote { .. } => Some('"'),
|
node.0.chars().next()
|
||||||
ParChild::Spacing(_) => Some(SPACING_REPLACE),
|
} else if child.is::<SmartQuoteNode>() {
|
||||||
ParChild::Inline(_) => Some(NODE_REPLACE),
|
Some('"')
|
||||||
|
} else if child.is::<SpaceNode>() || child.is::<HNode>() {
|
||||||
|
Some(SPACING_REPLACE)
|
||||||
|
} else {
|
||||||
|
Some(NODE_REPLACE)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
full.push_str(quoter.quote("es, double, peeked));
|
full.push_str(quoter.quote("es, node.double, peeked));
|
||||||
} else {
|
} else {
|
||||||
full.push(if double { '"' } else { '\'' });
|
full.push(if node.double { '"' } else { '\'' });
|
||||||
}
|
}
|
||||||
Segment::Text(full.len() - prev)
|
Segment::Text(full.len() - prev)
|
||||||
}
|
} else if let Some(&node) = child.downcast::<HNode>() {
|
||||||
&ParChild::Spacing(spacing) => {
|
|
||||||
full.push(SPACING_REPLACE);
|
full.push(SPACING_REPLACE);
|
||||||
Segment::Spacing(spacing)
|
Segment::Spacing(node.amount)
|
||||||
}
|
} else if child.has::<dyn LayoutInline>() {
|
||||||
ParChild::Inline(inline) => {
|
|
||||||
full.push(NODE_REPLACE);
|
full.push(NODE_REPLACE);
|
||||||
Segment::Inline(inline)
|
Segment::Inline(child)
|
||||||
}
|
} else {
|
||||||
|
panic!("unexpected par child: {child:?}");
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(last) = full.chars().last() {
|
if let Some(last) = full.chars().last() {
|
||||||
@ -608,7 +586,7 @@ fn is_compatible(a: Script, b: Script) -> bool {
|
|||||||
/// paragraph.
|
/// paragraph.
|
||||||
fn shared_get<'a, K: Key<'a>>(
|
fn shared_get<'a, K: Key<'a>>(
|
||||||
styles: StyleChain<'a>,
|
styles: StyleChain<'a>,
|
||||||
children: &StyleVec<ParChild>,
|
children: &StyleVec<Content>,
|
||||||
key: K,
|
key: K,
|
||||||
) -> Option<K::Output> {
|
) -> Option<K::Output> {
|
||||||
children
|
children
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
Loading…
x
Reference in New Issue
Block a user