List items are only paragraphs in wide lists

This commit is contained in:
Laurenz 2025-01-23 17:53:49 +01:00
parent ab9ba8c387
commit e40bf22c23
17 changed files with 262 additions and 42 deletions

View File

@ -6,7 +6,7 @@ use typst_library::foundations::{Content, Context, Depth, Packed, StyleChain};
use typst_library::introspection::Locator; use typst_library::introspection::Locator;
use typst_library::layout::grid::resolve::{Cell, CellGrid}; use typst_library::layout::grid::resolve::{Cell, CellGrid};
use typst_library::layout::{Axes, Fragment, HAlignment, Regions, Sizing, VAlignment}; use typst_library::layout::{Axes, Fragment, HAlignment, Regions, Sizing, VAlignment};
use typst_library::model::{EnumElem, ListElem, Numbering, ParElem}; use typst_library::model::{EnumElem, ListElem, Numbering, ParElem, ParbreakElem};
use typst_library::text::TextElem; use typst_library::text::TextElem;
use crate::grid::GridLayouter; use crate::grid::GridLayouter;
@ -22,8 +22,9 @@ pub fn layout_list(
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let indent = elem.indent(styles); let indent = elem.indent(styles);
let body_indent = elem.body_indent(styles); let body_indent = elem.body_indent(styles);
let tight = elem.tight(styles);
let gutter = elem.spacing(styles).unwrap_or_else(|| { let gutter = elem.spacing(styles).unwrap_or_else(|| {
if elem.tight(styles) { if tight {
ParElem::leading_in(styles).into() ParElem::leading_in(styles).into()
} else { } else {
ParElem::spacing_in(styles).into() ParElem::spacing_in(styles).into()
@ -41,11 +42,17 @@ pub fn layout_list(
let mut locator = locator.split(); let mut locator = locator.split();
for item in &elem.children { for item in &elem.children {
// Text in wide lists shall always turn into paragraphs.
let mut body = item.body.clone();
if !tight {
body += ParbreakElem::shared();
}
cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new(marker.clone(), locator.next(&marker.span()))); cells.push(Cell::new(marker.clone(), locator.next(&marker.span())));
cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new( cells.push(Cell::new(
item.body.clone().styled(ListElem::set_depth(Depth(1))), body.styled(ListElem::set_depth(Depth(1))),
locator.next(&item.body.span()), locator.next(&item.body.span()),
)); ));
} }
@ -78,8 +85,9 @@ pub fn layout_enum(
let reversed = elem.reversed(styles); let reversed = elem.reversed(styles);
let indent = elem.indent(styles); let indent = elem.indent(styles);
let body_indent = elem.body_indent(styles); let body_indent = elem.body_indent(styles);
let tight = elem.tight(styles);
let gutter = elem.spacing(styles).unwrap_or_else(|| { let gutter = elem.spacing(styles).unwrap_or_else(|| {
if elem.tight(styles) { if tight {
ParElem::leading_in(styles).into() ParElem::leading_in(styles).into()
} else { } else {
ParElem::spacing_in(styles).into() ParElem::spacing_in(styles).into()
@ -124,11 +132,17 @@ pub fn layout_enum(
let resolved = let resolved =
resolved.aligned(number_align).styled(TextElem::set_overhang(false)); resolved.aligned(number_align).styled(TextElem::set_overhang(false));
// Text in wide enums shall always turn into paragraphs.
let mut body = item.body.clone();
if !tight {
body += ParbreakElem::shared();
}
cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new(resolved, locator.next(&()))); cells.push(Cell::new(resolved, locator.next(&())));
cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new( cells.push(Cell::new(
item.body.clone().styled(EnumElem::set_parents(smallvec![number])), body.styled(EnumElem::set_parents(smallvec![number])),
locator.next(&item.body.span()), locator.next(&item.body.span()),
)); ));
number = number =

View File

@ -11,7 +11,9 @@ use crate::foundations::{
}; };
use crate::html::{attr, tag, HtmlElem}; use crate::html::{attr, tag, HtmlElem};
use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem}; use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem};
use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern, ParElem}; use crate::model::{
ListItemLike, ListLike, Numbering, NumberingPattern, ParElem, ParbreakElem,
};
/// A numbered list. /// A numbered list.
/// ///
@ -226,6 +228,8 @@ impl EnumElem {
impl Show for Packed<EnumElem> { impl Show for Packed<EnumElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let tight = self.tight(styles);
if TargetElem::target_in(styles).is_html() { if TargetElem::target_in(styles).is_html() {
let mut elem = HtmlElem::new(tag::ol); let mut elem = HtmlElem::new(tag::ol);
if self.reversed(styles) { if self.reversed(styles) {
@ -239,7 +243,12 @@ impl Show for Packed<EnumElem> {
if let Some(nr) = item.number(styles) { if let Some(nr) = item.number(styles) {
li = li.with_attr(attr::value, eco_format!("{nr}")); li = li.with_attr(attr::value, eco_format!("{nr}"));
} }
li.with_body(Some(item.body.clone())).pack().spanned(item.span()) // Text in wide enums shall always turn into paragraphs.
let mut body = item.body.clone();
if !tight {
body += ParbreakElem::shared();
}
li.with_body(Some(body)).pack().spanned(item.span())
})); }));
return Ok(elem.with_body(Some(body)).pack().spanned(self.span())); return Ok(elem.with_body(Some(body)).pack().spanned(self.span()));
} }
@ -249,7 +258,7 @@ impl Show for Packed<EnumElem> {
.pack() .pack()
.spanned(self.span()); .spanned(self.span());
if self.tight(styles) { if tight {
let leading = ParElem::leading_in(styles); let leading = ParElem::leading_in(styles);
let spacing = let spacing =
VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); VElem::new(leading.into()).with_weak(true).with_attach(true).pack();

View File

@ -8,7 +8,7 @@ use crate::foundations::{
}; };
use crate::html::{tag, HtmlElem}; use crate::html::{tag, HtmlElem};
use crate::layout::{BlockElem, Em, Length, VElem}; use crate::layout::{BlockElem, Em, Length, VElem};
use crate::model::ParElem; use crate::model::{ParElem, ParbreakElem};
use crate::text::TextElem; use crate::text::TextElem;
/// A bullet list. /// A bullet list.
@ -141,11 +141,18 @@ impl ListElem {
impl Show for Packed<ListElem> { impl Show for Packed<ListElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let tight = self.tight(styles);
if TargetElem::target_in(styles).is_html() { if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::ul) return Ok(HtmlElem::new(tag::ul)
.with_body(Some(Content::sequence(self.children.iter().map(|item| { .with_body(Some(Content::sequence(self.children.iter().map(|item| {
// Text in wide lists shall always turn into paragraphs.
let mut body = item.body.clone();
if !tight {
body += ParbreakElem::shared();
}
HtmlElem::new(tag::li) HtmlElem::new(tag::li)
.with_body(Some(item.body.clone())) .with_body(Some(body))
.pack() .pack()
.spanned(item.span()) .spanned(item.span())
})))) }))))
@ -158,7 +165,7 @@ impl Show for Packed<ListElem> {
.pack() .pack()
.spanned(self.span()); .spanned(self.span());
if self.tight(styles) { if tight {
let leading = ParElem::leading_in(styles); let leading = ParElem::leading_in(styles);
let spacing = let spacing =
VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); VElem::new(leading.into()).with_weak(true).with_attach(true).pack();

View File

@ -8,7 +8,7 @@ use crate::foundations::{
}; };
use crate::html::{tag, HtmlElem}; use crate::html::{tag, HtmlElem};
use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem}; use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem};
use crate::model::{ListItemLike, ListLike, ParElem}; use crate::model::{ListItemLike, ListLike, ParElem, ParbreakElem};
use crate::text::TextElem; use crate::text::TextElem;
/// A list of terms and their descriptions. /// A list of terms and their descriptions.
@ -116,17 +116,25 @@ impl TermsElem {
impl Show for Packed<TermsElem> { impl Show for Packed<TermsElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span(); let span = self.span();
let tight = self.tight(styles);
if TargetElem::target_in(styles).is_html() { if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::dl) return Ok(HtmlElem::new(tag::dl)
.with_body(Some(Content::sequence(self.children.iter().flat_map( .with_body(Some(Content::sequence(self.children.iter().flat_map(
|item| { |item| {
// Text in wide term lists shall always turn into paragraphs.
let mut description = item.description.clone();
if !tight {
description += ParbreakElem::shared();
}
[ [
HtmlElem::new(tag::dt) HtmlElem::new(tag::dt)
.with_body(Some(item.term.clone())) .with_body(Some(item.term.clone()))
.pack() .pack()
.spanned(item.term.span()), .spanned(item.term.span()),
HtmlElem::new(tag::dd) HtmlElem::new(tag::dd)
.with_body(Some(item.description.clone())) .with_body(Some(description))
.pack() .pack()
.spanned(item.description.span()), .spanned(item.description.span()),
] ]
@ -139,7 +147,7 @@ impl Show for Packed<TermsElem> {
let indent = self.indent(styles); let indent = self.indent(styles);
let hanging_indent = self.hanging_indent(styles); let hanging_indent = self.hanging_indent(styles);
let gutter = self.spacing(styles).unwrap_or_else(|| { let gutter = self.spacing(styles).unwrap_or_else(|| {
if self.tight(styles) { if tight {
ParElem::leading_in(styles).into() ParElem::leading_in(styles).into()
} else { } else {
ParElem::spacing_in(styles).into() ParElem::spacing_in(styles).into()
@ -157,6 +165,12 @@ impl Show for Packed<TermsElem> {
seq.push(child.term.clone().strong()); seq.push(child.term.clone().strong());
seq.push((*separator).clone()); seq.push((*separator).clone());
seq.push(child.description.clone()); seq.push(child.description.clone());
// Text in wide term lists shall always turn into paragraphs.
if !tight {
seq.push(ParbreakElem::shared().clone());
}
children.push(StackChild::Block(Content::sequence(seq))); children.push(StackChild::Block(Content::sequence(seq)));
} }
@ -168,7 +182,7 @@ impl Show for Packed<TermsElem> {
.spanned(span) .spanned(span)
.padded(padding); .padded(padding);
if self.tight(styles) { if tight {
let leading = ParElem::leading_in(styles); let leading = ParElem::leading_in(styles);
let spacing = VElem::new(leading.into()) let spacing = VElem::new(leading.into())
.with_weak(true) .with_weak(true)

BIN
tests/ref/enum-par.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div>
<ol>
<li>Hello</li>
<li>World</li>
</ol>
</div>
<div>
<ol>
<li>
<p>Hello</p>
<p>From</p>
</li>
<li>World</li>
</ol>
</div>
<div>
<ol>
<li>
<p>Hello</p>
<p>From</p>
<p>The</p>
</li>
<li>
<p>World</p>
</li>
</ol>
</div>
</body>
</html>

View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div>
<ul>
<li>Hello</li>
<li>World</li>
</ul>
</div>
<div>
<ul>
<li>
<p>Hello</p>
<p>From</p>
</li>
<li>World</li>
</ul>
</div>
<div>
<ul>
<li>
<p>Hello</p>
<p>From</p>
<p>The</p>
</li>
<li>
<p>World</p>
</li>
</ul>
</div>
</body>
</html>

View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div>
<dl>
<dt>Hello</dt>
<dd>A</dd>
<dt>World</dt>
<dd>B</dd>
</dl>
</div>
<div>
<dl>
<dt>Hello</dt>
<dd>
<p>A</p>
<p>From</p>
</dd>
<dt>World</dt>
<dd>B</dd>
</dl>
</div>
<div>
<dl>
<dt>Hello</dt>
<dd>
<p>A</p>
<p>From</p>
<p>The</p>
</dd>
<dt>World</dt>
<dd>
<p>B</p>
</dd>
</dl>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1004 B

View File

Before

Width:  |  Height:  |  Size: 415 B

After

Width:  |  Height:  |  Size: 415 B

View File

Before

Width:  |  Height:  |  Size: 569 B

After

Width:  |  Height:  |  Size: 569 B

BIN
tests/ref/list-par.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
tests/ref/terms-par.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -183,22 +183,44 @@ a + 0.
#set enum(number-align: horizon) #set enum(number-align: horizon)
#set enum(number-align: bottom) #set enum(number-align: bottom)
--- enum-par render html ---
// Check whether the contents of enum items become paragraphs.
#show par: it => if target() != "html" { highlight(it) } else { it }
// No paragraphs.
#block[
+ Hello
+ World
]
#block[
+ Hello // Paragraphs
From
+ World // No paragraph because it's a tight enum
]
#block[
+ Hello // Paragraphs
From
The
+ World // Paragraph because it's a wide enum
]
--- issue-2530-enum-item-panic --- --- issue-2530-enum-item-panic ---
// Enum item (pre-emptive) // Enum item (pre-emptive)
#enum.item(none)[Hello] #enum.item(none)[Hello]
#enum.item(17)[Hello] #enum.item(17)[Hello]
--- issue-5503-enum-interrupted-by-par-align --- --- issue-5503-enum-in-align ---
// `align` is block-level and should interrupt an enum // `align` is block-level and should interrupt an enum.
// but not a `par`
+ a + a
+ b + b
#par(leading: 5em)[+ par] #align(right)[+ c]
+ d + d
#par[+ par]
+ f
#align(right)[+ align]
+ h
--- issue-5719-enum-nested --- --- issue-5719-enum-nested ---
// Enums can be immediately nested. // Enums can be immediately nested.

View File

@ -238,6 +238,33 @@ World
#text(red)[- World] #text(red)[- World]
#text(green)[- What up?] #text(green)[- What up?]
--- list-par render html ---
// Check whether the contents of list items become paragraphs.
#show par: it => if target() != "html" { highlight(it) } else { it }
#block[
// No paragraphs.
- Hello
- World
]
#block[
- Hello // Paragraphs
From
- World // No paragraph because it's a tight list.
]
#block[
- Hello // Paragraphs either way
From
The
- World // Paragraph because it's a wide list.
]
--- issue-2530-list-item-panic --- --- issue-2530-list-item-panic ---
// List item (pre-emptive) // List item (pre-emptive)
#list.item[Hello] #list.item[Hello]
@ -262,18 +289,11 @@ World
part($ x $ + parbreak() + parbreak() + list[A]) part($ x $ + parbreak() + parbreak() + list[A])
} }
--- issue-5503-list-interrupted-by-par-align --- --- issue-5503-list-in-align ---
// `align` is block-level and should interrupt a list // `align` is block-level and should interrupt a list.
// but not a `par`
#show list: [List] #show list: [List]
- a - a
- b - b
#par(leading: 5em)[- c]
- d
- e
#par[- f]
- g
- h
#align(right)[- i] #align(right)[- i]
- j - j

View File

@ -59,6 +59,34 @@ Not in list
// Error: 8 expected colon // Error: 8 expected colon
/ Hello / Hello
--- terms-par render html ---
// Check whether the contents of term list items become paragraphs.
#show par: it => if target() != "html" { highlight(it) } else { it }
// No paragraphs.
#block[
/ Hello: A
/ World: B
]
#block[
/ Hello: A // Paragraphs
From
/ World: B // No paragraphs because it's a tight term list.
]
#block[
/ Hello: A // Paragraphs
From
The
/ World: B // Paragraph because it's a wide term list.
]
--- issue-1050-terms-indent --- --- issue-1050-terms-indent ---
#set page(width: 110pt) #set page(width: 110pt)
#set par(first-line-indent: 0.5cm) #set par(first-line-indent: 0.5cm)
@ -76,18 +104,10 @@ Not in list
// Term item (pre-emptive) // Term item (pre-emptive)
#terms.item[Hello][World!] #terms.item[Hello][World!]
--- issue-5503-terms-interrupted-by-par-align --- --- issue-5503-terms-in-align ---
// `align` is block-level and should interrupt a `terms` // `align` is block-level and should interrupt a `terms`.
// but not a `par`
#show terms: [Terms] #show terms: [Terms]
/ a: a / a: a
/ b: b
#par(leading: 5em)[/ c: c]
/ d: d
/ e: e
#par[/ f: f]
/ g: g
/ h: h
#align(right)[/ i: i] #align(right)[/ i: i]
/ j: j / j: j