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::layout::grid::resolve::{Cell, CellGrid};
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 crate::grid::GridLayouter;
@ -22,8 +22,9 @@ pub fn layout_list(
) -> SourceResult<Fragment> {
let indent = elem.indent(styles);
let body_indent = elem.body_indent(styles);
let tight = elem.tight(styles);
let gutter = elem.spacing(styles).unwrap_or_else(|| {
if elem.tight(styles) {
if tight {
ParElem::leading_in(styles).into()
} else {
ParElem::spacing_in(styles).into()
@ -41,11 +42,17 @@ pub fn layout_list(
let mut locator = locator.split();
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(marker.clone(), locator.next(&marker.span())));
cells.push(Cell::new(Content::empty(), locator.next(&())));
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()),
));
}
@ -78,8 +85,9 @@ pub fn layout_enum(
let reversed = elem.reversed(styles);
let indent = elem.indent(styles);
let body_indent = elem.body_indent(styles);
let tight = elem.tight(styles);
let gutter = elem.spacing(styles).unwrap_or_else(|| {
if elem.tight(styles) {
if tight {
ParElem::leading_in(styles).into()
} else {
ParElem::spacing_in(styles).into()
@ -124,11 +132,17 @@ pub fn layout_enum(
let resolved =
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(resolved, locator.next(&())));
cells.push(Cell::new(Content::empty(), locator.next(&())));
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()),
));
number =

View File

@ -11,7 +11,9 @@ use crate::foundations::{
};
use crate::html::{attr, tag, HtmlElem};
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.
///
@ -226,6 +228,8 @@ impl EnumElem {
impl Show for Packed<EnumElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let tight = self.tight(styles);
if TargetElem::target_in(styles).is_html() {
let mut elem = HtmlElem::new(tag::ol);
if self.reversed(styles) {
@ -239,7 +243,12 @@ impl Show for Packed<EnumElem> {
if let Some(nr) = item.number(styles) {
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()));
}
@ -249,7 +258,7 @@ impl Show for Packed<EnumElem> {
.pack()
.spanned(self.span());
if self.tight(styles) {
if tight {
let leading = ParElem::leading_in(styles);
let spacing =
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::layout::{BlockElem, Em, Length, VElem};
use crate::model::ParElem;
use crate::model::{ParElem, ParbreakElem};
use crate::text::TextElem;
/// A bullet list.
@ -141,11 +141,18 @@ impl ListElem {
impl Show for Packed<ListElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let tight = self.tight(styles);
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::ul)
.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)
.with_body(Some(item.body.clone()))
.with_body(Some(body))
.pack()
.spanned(item.span())
}))))
@ -158,7 +165,7 @@ impl Show for Packed<ListElem> {
.pack()
.spanned(self.span());
if self.tight(styles) {
if tight {
let leading = ParElem::leading_in(styles);
let spacing =
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::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;
/// A list of terms and their descriptions.
@ -116,17 +116,25 @@ impl TermsElem {
impl Show for Packed<TermsElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
let tight = self.tight(styles);
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::dl)
.with_body(Some(Content::sequence(self.children.iter().flat_map(
|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)
.with_body(Some(item.term.clone()))
.pack()
.spanned(item.term.span()),
HtmlElem::new(tag::dd)
.with_body(Some(item.description.clone()))
.with_body(Some(description))
.pack()
.spanned(item.description.span()),
]
@ -139,7 +147,7 @@ impl Show for Packed<TermsElem> {
let indent = self.indent(styles);
let hanging_indent = self.hanging_indent(styles);
let gutter = self.spacing(styles).unwrap_or_else(|| {
if self.tight(styles) {
if tight {
ParElem::leading_in(styles).into()
} else {
ParElem::spacing_in(styles).into()
@ -157,6 +165,12 @@ impl Show for Packed<TermsElem> {
seq.push(child.term.clone().strong());
seq.push((*separator).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)));
}
@ -168,7 +182,7 @@ impl Show for Packed<TermsElem> {
.spanned(span)
.padded(padding);
if self.tight(styles) {
if tight {
let leading = ParElem::leading_in(styles);
let spacing = VElem::new(leading.into())
.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: 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 ---
// Enum item (pre-emptive)
#enum.item(none)[Hello]
#enum.item(17)[Hello]
--- issue-5503-enum-interrupted-by-par-align ---
// `align` is block-level and should interrupt an enum
// but not a `par`
--- issue-5503-enum-in-align ---
// `align` is block-level and should interrupt an enum.
+ a
+ b
#par(leading: 5em)[+ par]
#align(right)[+ c]
+ d
#par[+ par]
+ f
#align(right)[+ align]
+ h
--- issue-5719-enum-nested ---
// Enums can be immediately nested.

View File

@ -238,6 +238,33 @@ World
#text(red)[- World]
#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 ---
// List item (pre-emptive)
#list.item[Hello]
@ -262,18 +289,11 @@ World
part($ x $ + parbreak() + parbreak() + list[A])
}
--- issue-5503-list-interrupted-by-par-align ---
// `align` is block-level and should interrupt a list
// but not a `par`
--- issue-5503-list-in-align ---
// `align` is block-level and should interrupt a list.
#show list: [List]
- a
- b
#par(leading: 5em)[- c]
- d
- e
#par[- f]
- g
- h
#align(right)[- i]
- j

View File

@ -59,6 +59,34 @@ Not in list
// Error: 8 expected colon
/ 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 ---
#set page(width: 110pt)
#set par(first-line-indent: 0.5cm)
@ -76,18 +104,10 @@ Not in list
// Term item (pre-emptive)
#terms.item[Hello][World!]
--- issue-5503-terms-interrupted-by-par-align ---
// `align` is block-level and should interrupt a `terms`
// but not a `par`
--- issue-5503-terms-in-align ---
// `align` is block-level and should interrupt a `terms`.
#show terms: [Terms]
/ a: a
/ b: b
#par(leading: 5em)[/ c: c]
/ d: d
/ e: e
#par[/ f: f]
/ g: g
/ h: h
#align(right)[/ i: i]
/ j: j