mirror of
https://github.com/typst/typst
synced 2025-08-25 04:04:12 +08:00
Compare commits
No commits in common. "87cb8f5094b9ea45893344143ca069a521f9ba47" and "e9dc4bb20404037cf192c19f00a010ff3bb1a10b" have entirely different histories.
87cb8f5094
...
e9dc4bb204
@ -2,9 +2,7 @@ use std::fmt::Write;
|
||||
|
||||
use typst_library::diag::{bail, At, SourceResult, StrResult};
|
||||
use typst_library::foundations::Repr;
|
||||
use typst_library::html::{
|
||||
attr, charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag,
|
||||
};
|
||||
use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag};
|
||||
use typst_library::layout::Frame;
|
||||
use typst_syntax::Span;
|
||||
|
||||
@ -30,7 +28,7 @@ struct Writer {
|
||||
pretty: bool,
|
||||
}
|
||||
|
||||
/// Writes a newline and indent, if pretty printing is enabled.
|
||||
/// Write a newline and indent, if pretty printing is enabled.
|
||||
fn write_indent(w: &mut Writer) {
|
||||
if w.pretty {
|
||||
w.buf.push('\n');
|
||||
@ -40,7 +38,7 @@ fn write_indent(w: &mut Writer) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Encodes an HTML node into the writer.
|
||||
/// Encode an HTML node into the writer.
|
||||
fn write_node(w: &mut Writer, node: &HtmlNode) -> SourceResult<()> {
|
||||
match node {
|
||||
HtmlNode::Tag(_) => {}
|
||||
@ -51,7 +49,7 @@ fn write_node(w: &mut Writer, node: &HtmlNode) -> SourceResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Encodes plain text into the writer.
|
||||
/// Encode plain text into the writer.
|
||||
fn write_text(w: &mut Writer, text: &str, span: Span) -> SourceResult<()> {
|
||||
for c in text.chars() {
|
||||
if charsets::is_valid_in_normal_element_text(c) {
|
||||
@ -63,7 +61,7 @@ fn write_text(w: &mut Writer, text: &str, span: Span) -> SourceResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Encodes one element into the writer.
|
||||
/// Encode one element into the write.
|
||||
fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
|
||||
w.buf.push('<');
|
||||
w.buf.push_str(&element.tag.resolve());
|
||||
@ -91,17 +89,39 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
|
||||
w.buf.push('>');
|
||||
|
||||
if tag::is_void(element.tag) {
|
||||
if !element.children.is_empty() {
|
||||
bail!(element.span, "HTML void elements must not have children");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if tag::is_raw(element.tag) {
|
||||
write_raw(w, element)?;
|
||||
} else if !element.children.is_empty() {
|
||||
write_children(w, element)?;
|
||||
let pretty = w.pretty;
|
||||
if !element.children.is_empty() {
|
||||
let pretty_inside = allows_pretty_inside(element.tag)
|
||||
&& element.children.iter().any(|node| match node {
|
||||
HtmlNode::Element(child) => wants_pretty_around(child.tag),
|
||||
_ => false,
|
||||
});
|
||||
|
||||
w.pretty &= pretty_inside;
|
||||
let mut indent = w.pretty;
|
||||
|
||||
w.level += 1;
|
||||
for c in &element.children {
|
||||
let pretty_around = match c {
|
||||
HtmlNode::Tag(_) => continue,
|
||||
HtmlNode::Element(child) => w.pretty && wants_pretty_around(child.tag),
|
||||
HtmlNode::Text(..) | HtmlNode::Frame(_) => false,
|
||||
};
|
||||
|
||||
if core::mem::take(&mut indent) || pretty_around {
|
||||
write_indent(w);
|
||||
}
|
||||
write_node(w, c)?;
|
||||
indent = pretty_around;
|
||||
}
|
||||
w.level -= 1;
|
||||
|
||||
write_indent(w);
|
||||
}
|
||||
w.pretty = pretty;
|
||||
|
||||
w.buf.push_str("</");
|
||||
w.buf.push_str(&element.tag.resolve());
|
||||
@ -110,159 +130,6 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Encodes the children of an element.
|
||||
fn write_children(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
|
||||
// See HTML spec § 13.1.2.5.
|
||||
if element.tag == tag::pre && starts_with_newline(element) {
|
||||
w.buf.push('\n');
|
||||
}
|
||||
|
||||
let pretty = w.pretty;
|
||||
let pretty_inside = allows_pretty_inside(element.tag)
|
||||
&& element.children.iter().any(|node| match node {
|
||||
HtmlNode::Element(child) => wants_pretty_around(child.tag),
|
||||
_ => false,
|
||||
});
|
||||
|
||||
w.pretty &= pretty_inside;
|
||||
let mut indent = w.pretty;
|
||||
|
||||
w.level += 1;
|
||||
for c in &element.children {
|
||||
let pretty_around = match c {
|
||||
HtmlNode::Tag(_) => continue,
|
||||
HtmlNode::Element(child) => w.pretty && wants_pretty_around(child.tag),
|
||||
HtmlNode::Text(..) | HtmlNode::Frame(_) => false,
|
||||
};
|
||||
|
||||
if core::mem::take(&mut indent) || pretty_around {
|
||||
write_indent(w);
|
||||
}
|
||||
write_node(w, c)?;
|
||||
indent = pretty_around;
|
||||
}
|
||||
w.level -= 1;
|
||||
|
||||
write_indent(w);
|
||||
w.pretty = pretty;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether the first character in the element is a newline.
|
||||
fn starts_with_newline(element: &HtmlElement) -> bool {
|
||||
for child in &element.children {
|
||||
match child {
|
||||
HtmlNode::Tag(_) => {}
|
||||
HtmlNode::Text(text, _) => return text.starts_with(['\n', '\r']),
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Encodes the contents of a raw text element.
|
||||
fn write_raw(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
|
||||
let text = collect_raw_text(element)?;
|
||||
|
||||
if let Some(closing) = find_closing_tag(&text, element.tag) {
|
||||
bail!(
|
||||
element.span,
|
||||
"HTML raw text element cannot contain its own closing tag";
|
||||
hint: "the sequence `{closing}` appears in the raw text",
|
||||
)
|
||||
}
|
||||
|
||||
let mode = if w.pretty { RawMode::of(element, &text) } else { RawMode::Keep };
|
||||
match mode {
|
||||
RawMode::Keep => {
|
||||
w.buf.push_str(&text);
|
||||
}
|
||||
RawMode::Wrap => {
|
||||
w.buf.push('\n');
|
||||
w.buf.push_str(&text);
|
||||
write_indent(w);
|
||||
}
|
||||
RawMode::Indent => {
|
||||
w.level += 1;
|
||||
for line in text.lines() {
|
||||
write_indent(w);
|
||||
w.buf.push_str(line);
|
||||
}
|
||||
w.level -= 1;
|
||||
write_indent(w);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Collects the textual contents of a raw text element.
|
||||
fn collect_raw_text(element: &HtmlElement) -> SourceResult<String> {
|
||||
let mut output = String::new();
|
||||
for c in &element.children {
|
||||
match c {
|
||||
HtmlNode::Tag(_) => continue,
|
||||
HtmlNode::Text(text, _) => output.push_str(text),
|
||||
HtmlNode::Element(_) | HtmlNode::Frame(_) => {
|
||||
let span = match c {
|
||||
HtmlNode::Element(child) => child.span,
|
||||
_ => element.span,
|
||||
};
|
||||
bail!(span, "HTML raw text element cannot have non-text children")
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Finds a closing sequence for the given tag in the text, if it exists.
|
||||
///
|
||||
/// See HTML spec § 13.1.2.6.
|
||||
fn find_closing_tag(text: &str, tag: HtmlTag) -> Option<&str> {
|
||||
let s = tag.resolve();
|
||||
let len = s.len();
|
||||
text.match_indices("</").find_map(|(i, _)| {
|
||||
let rest = &text[i + 2..];
|
||||
let disallowed = rest.len() >= len
|
||||
&& rest[..len].eq_ignore_ascii_case(&s)
|
||||
&& rest[len..].starts_with(['\t', '\n', '\u{c}', '\r', ' ', '>', '/']);
|
||||
disallowed.then(|| &text[i..i + 2 + len])
|
||||
})
|
||||
}
|
||||
|
||||
/// How to format the contents of a raw text element.
|
||||
enum RawMode {
|
||||
/// Just don't touch it.
|
||||
Keep,
|
||||
/// Newline after the opening and newline + indent before the closing tag.
|
||||
Wrap,
|
||||
/// Newlines after opening and before closing tag and each line indented.
|
||||
Indent,
|
||||
}
|
||||
|
||||
impl RawMode {
|
||||
fn of(element: &HtmlElement, text: &str) -> Self {
|
||||
match element.tag {
|
||||
tag::script
|
||||
if !element.attrs.0.iter().any(|(attr, value)| {
|
||||
*attr == attr::r#type && value != "text/javascript"
|
||||
}) =>
|
||||
{
|
||||
// Template literals can be multi-line, so indent may change
|
||||
// the semantics of the JavaScript.
|
||||
if text.contains('`') {
|
||||
Self::Wrap
|
||||
} else {
|
||||
Self::Indent
|
||||
}
|
||||
}
|
||||
tag::style => Self::Indent,
|
||||
_ => Self::Keep,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether we are allowed to add an extra newline at the start and end of the
|
||||
/// element's contents.
|
||||
///
|
||||
@ -298,7 +165,7 @@ fn write_escape(w: &mut Writer, c: char) -> StrResult<()> {
|
||||
c if charsets::is_w3c_text_char(c) && c != '\r' => {
|
||||
write!(w.buf, "&#x{:x};", c as u32).unwrap()
|
||||
}
|
||||
_ => bail!("the character `{}` cannot be encoded in HTML", c.repr()),
|
||||
_ => bail!("the character {} cannot be encoded in HTML", c.repr()),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -180,6 +180,9 @@ fn handle(
|
||||
if let Some(body) = elem.body(styles) {
|
||||
children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
|
||||
}
|
||||
if tag::is_void(elem.tag) && !children.is_empty() {
|
||||
bail!(elem.span(), "HTML void elements may not have children");
|
||||
}
|
||||
let element = HtmlElement {
|
||||
tag: elem.tag,
|
||||
attrs: elem.attrs(styles).clone(),
|
||||
|
@ -11,8 +11,8 @@ use typst_library::layout::{
|
||||
};
|
||||
use typst_library::visualize::{
|
||||
CircleElem, CloseMode, Curve, CurveComponent, CurveElem, EllipseElem, FillRule,
|
||||
FixedStroke, Geometry, LineCap, LineElem, Paint, PathElem, PathVertex, PolygonElem,
|
||||
RectElem, Shape, SquareElem, Stroke,
|
||||
FixedStroke, Geometry, LineElem, Paint, PathElem, PathVertex, PolygonElem, RectElem,
|
||||
Shape, SquareElem, Stroke,
|
||||
};
|
||||
use typst_syntax::Span;
|
||||
use typst_utils::{Get, Numeric};
|
||||
@ -889,13 +889,7 @@ fn segmented_rect(
|
||||
let end = current;
|
||||
last = current;
|
||||
let Some(stroke) = strokes.get_ref(start.side_cw()) else { continue };
|
||||
let start_cap = stroke.cap;
|
||||
let end_cap = match strokes.get_ref(end.side_ccw()) {
|
||||
Some(stroke) => stroke.cap,
|
||||
None => start_cap,
|
||||
};
|
||||
let (shape, ontop) =
|
||||
segment(start, end, start_cap, end_cap, &corners, stroke);
|
||||
let (shape, ontop) = segment(start, end, &corners, stroke);
|
||||
if ontop {
|
||||
res.push(shape);
|
||||
} else {
|
||||
@ -905,14 +899,7 @@ fn segmented_rect(
|
||||
}
|
||||
} else if let Some(stroke) = &strokes.top {
|
||||
// single segment
|
||||
let (shape, _) = segment(
|
||||
Corner::TopLeft,
|
||||
Corner::TopLeft,
|
||||
stroke.cap,
|
||||
stroke.cap,
|
||||
&corners,
|
||||
stroke,
|
||||
);
|
||||
let (shape, _) = segment(Corner::TopLeft, Corner::TopLeft, &corners, stroke);
|
||||
res.push(shape);
|
||||
}
|
||||
res
|
||||
@ -959,8 +946,6 @@ fn curve_segment(
|
||||
fn segment(
|
||||
start: Corner,
|
||||
end: Corner,
|
||||
start_cap: LineCap,
|
||||
end_cap: LineCap,
|
||||
corners: &Corners<ControlPoints>,
|
||||
stroke: &FixedStroke,
|
||||
) -> (Shape, bool) {
|
||||
@ -994,7 +979,7 @@ fn segment(
|
||||
|
||||
let use_fill = solid && fill_corners(start, end, corners);
|
||||
let shape = if use_fill {
|
||||
fill_segment(start, end, start_cap, end_cap, corners, stroke)
|
||||
fill_segment(start, end, corners, stroke)
|
||||
} else {
|
||||
stroke_segment(start, end, corners, stroke.clone())
|
||||
};
|
||||
@ -1025,8 +1010,6 @@ fn stroke_segment(
|
||||
fn fill_segment(
|
||||
start: Corner,
|
||||
end: Corner,
|
||||
start_cap: LineCap,
|
||||
end_cap: LineCap,
|
||||
corners: &Corners<ControlPoints>,
|
||||
stroke: &FixedStroke,
|
||||
) -> Shape {
|
||||
@ -1052,7 +1035,8 @@ fn fill_segment(
|
||||
if c.arc_outer() {
|
||||
curve.arc_line(c.mid_outer(), c.center_outer(), c.end_outer());
|
||||
} else {
|
||||
c.start_cap(&mut curve, start_cap);
|
||||
curve.line(c.outer());
|
||||
curve.line(c.end_outer());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1095,7 +1079,7 @@ fn fill_segment(
|
||||
if c.arc_inner() {
|
||||
curve.arc_line(c.mid_inner(), c.center_inner(), c.start_inner());
|
||||
} else {
|
||||
c.end_cap(&mut curve, end_cap);
|
||||
curve.line(c.center_inner());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1150,16 +1134,6 @@ struct ControlPoints {
|
||||
}
|
||||
|
||||
impl ControlPoints {
|
||||
/// Rotate point around the origin, relative to the top-left.
|
||||
fn rotate_centered(&self, point: Point) -> Point {
|
||||
match self.corner {
|
||||
Corner::TopLeft => point,
|
||||
Corner::TopRight => Point { x: -point.y, y: point.x },
|
||||
Corner::BottomRight => Point { x: -point.x, y: -point.y },
|
||||
Corner::BottomLeft => Point { x: point.y, y: -point.x },
|
||||
}
|
||||
}
|
||||
|
||||
/// Move and rotate the point from top-left to the required corner.
|
||||
fn rotate(&self, point: Point) -> Point {
|
||||
match self.corner {
|
||||
@ -1306,77 +1280,6 @@ impl ControlPoints {
|
||||
y: self.stroke_after,
|
||||
})
|
||||
}
|
||||
|
||||
/// Draw the cap at the beginning of the segment.
|
||||
///
|
||||
/// If this corner has a stroke before it,
|
||||
/// a default "butt" cap is used.
|
||||
///
|
||||
/// NOTE: doesn't support the case where the corner has a radius.
|
||||
pub fn start_cap(&self, curve: &mut Curve, cap_type: LineCap) {
|
||||
if self.stroke_before != Abs::zero()
|
||||
|| self.radius != Abs::zero()
|
||||
|| cap_type == LineCap::Butt
|
||||
{
|
||||
// Just the default cap.
|
||||
curve.line(self.outer());
|
||||
} else if cap_type == LineCap::Square {
|
||||
// Extend by the stroke width.
|
||||
let offset =
|
||||
self.rotate_centered(Point { x: -self.stroke_after, y: Abs::zero() });
|
||||
curve.line(self.end_inner() + offset);
|
||||
curve.line(self.outer() + offset);
|
||||
} else if cap_type == LineCap::Round {
|
||||
// We push the center by a little bit to ensure the correct
|
||||
// half of the circle gets drawn. If it is perfectly centered
|
||||
// the `arc` function just degenerates into a line, which we
|
||||
// do not want in this case.
|
||||
curve.arc(
|
||||
self.end_inner(),
|
||||
(self.end_inner()
|
||||
+ self.rotate_centered(Point { x: Abs::raw(1.0), y: Abs::zero() })
|
||||
+ self.outer())
|
||||
/ 2.,
|
||||
self.outer(),
|
||||
);
|
||||
}
|
||||
curve.line(self.end_outer());
|
||||
}
|
||||
|
||||
/// Draw the cap at the end of the segment.
|
||||
///
|
||||
/// If this corner has a stroke before it,
|
||||
/// a default "butt" cap is used.
|
||||
///
|
||||
/// NOTE: doesn't support the case where the corner has a radius.
|
||||
pub fn end_cap(&self, curve: &mut Curve, cap_type: LineCap) {
|
||||
if self.stroke_after != Abs::zero()
|
||||
|| self.radius != Abs::zero()
|
||||
|| cap_type == LineCap::Butt
|
||||
{
|
||||
// Just the default cap.
|
||||
curve.line(self.center_inner());
|
||||
} else if cap_type == LineCap::Square {
|
||||
// Extend by the stroke width.
|
||||
let offset =
|
||||
self.rotate_centered(Point { x: Abs::zero(), y: -self.stroke_before });
|
||||
curve.line(self.outer() + offset);
|
||||
curve.line(self.center_inner() + offset);
|
||||
} else if cap_type == LineCap::Round {
|
||||
// We push the center by a little bit to ensure the correct
|
||||
// half of the circle gets drawn. If it is perfectly centered
|
||||
// the `arc` function just degenerates into a line, which we
|
||||
// do not want in this case.
|
||||
curve.arc(
|
||||
self.outer(),
|
||||
(self.outer()
|
||||
+ self.rotate_centered(Point { x: Abs::zero(), y: Abs::raw(1.0) })
|
||||
+ self.center_inner())
|
||||
/ 2.,
|
||||
self.center_inner(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to draw arcs with Bézier curves.
|
||||
|
@ -30,7 +30,6 @@ const TRANSLATIONS: &[(&str, &str)] = &[
|
||||
translation!("fr"),
|
||||
translation!("gl"),
|
||||
translation!("he"),
|
||||
translation!("hr"),
|
||||
translation!("hu"),
|
||||
translation!("id"),
|
||||
translation!("is"),
|
||||
|
@ -1,8 +0,0 @@
|
||||
figure = Slika
|
||||
table = Tablica
|
||||
equation = Jednadžba
|
||||
bibliography = Literatura
|
||||
heading = Odjeljak
|
||||
outline = Sadržaj
|
||||
raw = Kôd
|
||||
page = str.
|
@ -13,7 +13,7 @@ use krilla::surface::Surface;
|
||||
use krilla::{Document, SerializeSettings};
|
||||
use krilla_svg::render_svg_glyph;
|
||||
use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult};
|
||||
use typst_library::foundations::{NativeElement, Repr};
|
||||
use typst_library::foundations::NativeElement;
|
||||
use typst_library::introspection::Location;
|
||||
use typst_library::layout::{
|
||||
Abs, Frame, FrameItem, GroupItem, PagedDocument, Size, Transform,
|
||||
@ -429,18 +429,14 @@ fn convert_error(
|
||||
display_font(gc.fonts_backward.get(f).unwrap());
|
||||
hint: "try using a different font"
|
||||
),
|
||||
ValidationError::InvalidCodepointMapping(_, _, c, loc) => {
|
||||
if let Some(c) = c {
|
||||
ValidationError::InvalidCodepointMapping(_, _, cp, loc) => {
|
||||
if let Some(c) = cp.map(|c| eco_format!("{:#06x}", c as u32)) {
|
||||
let msg = if loc.is_some() {
|
||||
"the PDF contains text with"
|
||||
} else {
|
||||
"the text contains"
|
||||
};
|
||||
error!(
|
||||
to_span(*loc),
|
||||
"{prefix} {msg} the disallowed codepoint `{}`",
|
||||
c.repr()
|
||||
)
|
||||
error!(to_span(*loc), "{prefix} {msg} the disallowed codepoint {c}")
|
||||
} else {
|
||||
// I think this code path is in theory unreachable,
|
||||
// but just to be safe.
|
||||
@ -458,12 +454,13 @@ fn convert_error(
|
||||
}
|
||||
}
|
||||
ValidationError::UnicodePrivateArea(_, _, c, loc) => {
|
||||
let code_point = eco_format!("{:#06x}", *c as u32);
|
||||
let msg = if loc.is_some() { "the PDF" } else { "the text" };
|
||||
error!(
|
||||
to_span(*loc),
|
||||
"{prefix} {msg} contains the codepoint `{}`", c.repr();
|
||||
"{prefix} {msg} contains the codepoint {code_point}";
|
||||
hint: "codepoints from the Unicode private area are \
|
||||
forbidden in this export mode",
|
||||
forbidden in this export mode"
|
||||
)
|
||||
}
|
||||
ValidationError::Transparency(loc) => {
|
||||
|
@ -1,8 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body><textarea>hello </textarea></textarea></body>
|
||||
</html>
|
@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<pre>hello</pre>
|
||||
<pre>
|
||||
|
||||
hello</pre>
|
||||
<pre>
|
||||
|
||||
|
||||
hello</pre>
|
||||
</body>
|
||||
</html>
|
@ -1,21 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
const x = 1
|
||||
const y = 2
|
||||
console.log(x < y, Math.max(1, 2))
|
||||
</script>
|
||||
<script>
|
||||
console.log(`Hello
|
||||
World`)
|
||||
</script>
|
||||
<script type="text/python">x = 1
|
||||
y = 2
|
||||
print(x < y, max(x, y))</script>
|
||||
</body>
|
||||
</html>
|
@ -1,14 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<style>
|
||||
body {
|
||||
text: red;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
Before Width: | Height: | Size: 252 B |
@ -4,7 +4,7 @@ use std::path::PathBuf;
|
||||
|
||||
use ecow::eco_vec;
|
||||
use tiny_skia as sk;
|
||||
use typst::diag::{SourceDiagnostic, SourceResult, Warned};
|
||||
use typst::diag::{SourceDiagnostic, Warned};
|
||||
use typst::html::HtmlDocument;
|
||||
use typst::layout::{Abs, Frame, FrameItem, PagedDocument, Transform};
|
||||
use typst::visualize::Color;
|
||||
@ -82,26 +82,17 @@ impl<'a> Runner<'a> {
|
||||
/// Run test specific to document format.
|
||||
fn run_test<D: OutputType>(&mut self) {
|
||||
let Warned { output, warnings } = typst::compile(&self.world);
|
||||
let (doc, mut errors) = match output {
|
||||
let (doc, errors) = match output {
|
||||
Ok(doc) => (Some(doc), eco_vec![]),
|
||||
Err(errors) => (None, errors),
|
||||
};
|
||||
|
||||
D::check_custom(self, doc.as_ref());
|
||||
|
||||
let output = doc.and_then(|doc: D| match doc.make_live() {
|
||||
Ok(live) => Some((doc, live)),
|
||||
Err(list) => {
|
||||
errors.extend(list);
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if output.is_none() && errors.is_empty() {
|
||||
if doc.is_none() && errors.is_empty() {
|
||||
log!(self, "no document, but also no errors");
|
||||
}
|
||||
|
||||
self.check_output(output);
|
||||
D::check_custom(self, doc.as_ref());
|
||||
self.check_output(doc.as_ref());
|
||||
|
||||
for error in &errors {
|
||||
self.check_diagnostic(NoteKind::Error, error);
|
||||
@ -137,12 +128,12 @@ impl<'a> Runner<'a> {
|
||||
}
|
||||
|
||||
/// Check that the document output is correct.
|
||||
fn check_output<D: OutputType>(&mut self, output: Option<(D, D::Live)>) {
|
||||
fn check_output<D: OutputType>(&mut self, document: Option<&D>) {
|
||||
let live_path = D::live_path(&self.test.name);
|
||||
let ref_path = D::ref_path(&self.test.name);
|
||||
let ref_data = std::fs::read(&ref_path);
|
||||
|
||||
let Some((document, live)) = output else {
|
||||
let Some(document) = document else {
|
||||
if ref_data.is_ok() {
|
||||
log!(self, "missing document");
|
||||
log!(self, " ref | {}", ref_path.display());
|
||||
@ -150,7 +141,7 @@ impl<'a> Runner<'a> {
|
||||
return;
|
||||
};
|
||||
|
||||
let skippable = match D::is_skippable(&document) {
|
||||
let skippable = match D::is_skippable(document) {
|
||||
Ok(skippable) => skippable,
|
||||
Err(()) => {
|
||||
log!(self, "document has zero pages");
|
||||
@ -166,6 +157,7 @@ impl<'a> Runner<'a> {
|
||||
}
|
||||
|
||||
// Render and save live version.
|
||||
let live = document.make_live();
|
||||
document.save_live(&self.test.name, &live);
|
||||
|
||||
// Compare against reference output if available.
|
||||
@ -222,13 +214,9 @@ impl<'a> Runner<'a> {
|
||||
return;
|
||||
}
|
||||
|
||||
let message = if diag.message.contains("\\u{") {
|
||||
&diag.message
|
||||
} else {
|
||||
&diag.message.replace("\\", "/")
|
||||
};
|
||||
let message = diag.message.replace("\\", "/");
|
||||
let range = self.world.range(diag.span);
|
||||
self.validate_note(kind, diag.span.id(), range.clone(), message);
|
||||
self.validate_note(kind, diag.span.id(), range.clone(), &message);
|
||||
|
||||
// Check hints.
|
||||
for hint in &diag.hints {
|
||||
@ -371,7 +359,7 @@ trait OutputType: Document {
|
||||
}
|
||||
|
||||
/// Produces the live output.
|
||||
fn make_live(&self) -> SourceResult<Self::Live>;
|
||||
fn make_live(&self) -> Self::Live;
|
||||
|
||||
/// Saves the live output.
|
||||
fn save_live(&self, name: &str, live: &Self::Live);
|
||||
@ -418,8 +406,8 @@ impl OutputType for PagedDocument {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_live(&self) -> SourceResult<Self::Live> {
|
||||
Ok(render(self, 1.0))
|
||||
fn make_live(&self) -> Self::Live {
|
||||
render(self, 1.0)
|
||||
}
|
||||
|
||||
fn save_live(&self, name: &str, live: &Self::Live) {
|
||||
@ -483,8 +471,9 @@ impl OutputType for HtmlDocument {
|
||||
format!("{}/html/{}.html", crate::REF_PATH, name).into()
|
||||
}
|
||||
|
||||
fn make_live(&self) -> SourceResult<Self::Live> {
|
||||
typst_html::html(self)
|
||||
fn make_live(&self) -> Self::Live {
|
||||
// TODO: Do this earlier to be able to process export errors.
|
||||
typst_html::html(self).unwrap()
|
||||
}
|
||||
|
||||
fn save_live(&self, name: &str, live: &Self::Live) {
|
||||
|
@ -1,63 +0,0 @@
|
||||
--- html-non-char html ---
|
||||
// Error: 1-9 the character `"\u{fdd0}"` cannot be encoded in HTML
|
||||
\u{fdd0}
|
||||
|
||||
--- html-void-element-with-children html ---
|
||||
// Error: 2-27 HTML void elements must not have children
|
||||
#html.elem("img", [Hello])
|
||||
|
||||
--- html-pre-starting-with-newline html ---
|
||||
#html.pre("hello")
|
||||
#html.pre("\nhello")
|
||||
#html.pre("\n\nhello")
|
||||
|
||||
--- html-script html ---
|
||||
// This should be pretty and indented.
|
||||
#html.script(
|
||||
```js
|
||||
const x = 1
|
||||
const y = 2
|
||||
console.log(x < y, Math.max(1, 2))
|
||||
```.text,
|
||||
)
|
||||
|
||||
// This should have extra newlines, but no indent because of the multiline
|
||||
// string literal.
|
||||
#html.script("console.log(`Hello\nWorld`)")
|
||||
|
||||
// This should be untouched.
|
||||
#html.script(
|
||||
type: "text/python",
|
||||
```py
|
||||
x = 1
|
||||
y = 2
|
||||
print(x < y, max(x, y))
|
||||
```.text,
|
||||
)
|
||||
|
||||
--- html-style html ---
|
||||
// This should be pretty and indented.
|
||||
#html.style(
|
||||
```css
|
||||
body {
|
||||
text: red;
|
||||
}
|
||||
```.text,
|
||||
)
|
||||
|
||||
--- html-raw-text-contains-elem html ---
|
||||
// Error: 14-32 HTML raw text element cannot have non-text children
|
||||
#html.script(html.strong[Hello])
|
||||
|
||||
--- html-raw-text-contains-frame html ---
|
||||
// Error: 2-29 HTML raw text element cannot have non-text children
|
||||
#html.script(html.frame[Ok])
|
||||
|
||||
--- html-raw-text-contains-closing-tag html ---
|
||||
// Error: 2-32 HTML raw text element cannot contain its own closing tag
|
||||
// Hint: 2-32 the sequence `</SCRiPT` appears in the raw text
|
||||
#html.script("hello </SCRiPT ")
|
||||
|
||||
--- html-escapable-raw-text-contains-closing-tag html ---
|
||||
// This is okay because we escape it.
|
||||
#html.textarea("hello </textarea>")
|
@ -54,22 +54,6 @@
|
||||
#v(3pt)
|
||||
#rect(width: 20pt, height: 20pt, stroke: (thickness: 5pt, join: "round"))
|
||||
|
||||
--- rect-stroke-caps ---
|
||||
// Separated segments
|
||||
#rect(width: 20pt, height: 20pt, stroke: (
|
||||
left: (cap: "round", thickness: 5pt),
|
||||
right: (cap: "square", thickness: 7pt),
|
||||
))
|
||||
// Joined segment with different caps.
|
||||
#rect(width: 20pt, height: 20pt, stroke: (
|
||||
left: (cap: "round", thickness: 5pt),
|
||||
top: (cap: "square", thickness: 7pt),
|
||||
))
|
||||
// No caps when there is a radius for that corner.
|
||||
#rect(width: 20pt, height: 20pt, radius: (top: 3pt), stroke: (
|
||||
left: (cap: "round", thickness: 5pt),
|
||||
top: (cap: "square", thickness: 7pt),
|
||||
))
|
||||
--- red-stroke-bad-type ---
|
||||
// Error: 15-21 expected length, color, gradient, tiling, dictionary, stroke, none, or auto, found array
|
||||
#rect(stroke: (1, 2))
|
||||
|
Loading…
x
Reference in New Issue
Block a user