mirror of
https://github.com/typst/typst
synced 2025-08-25 20:24:14 +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::diag::{bail, At, SourceResult, StrResult};
|
||||||
use typst_library::foundations::Repr;
|
use typst_library::foundations::Repr;
|
||||||
use typst_library::html::{
|
use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag};
|
||||||
attr, charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag,
|
|
||||||
};
|
|
||||||
use typst_library::layout::Frame;
|
use typst_library::layout::Frame;
|
||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
|
|
||||||
@ -30,7 +28,7 @@ struct Writer {
|
|||||||
pretty: bool,
|
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) {
|
fn write_indent(w: &mut Writer) {
|
||||||
if w.pretty {
|
if w.pretty {
|
||||||
w.buf.push('\n');
|
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<()> {
|
fn write_node(w: &mut Writer, node: &HtmlNode) -> SourceResult<()> {
|
||||||
match node {
|
match node {
|
||||||
HtmlNode::Tag(_) => {}
|
HtmlNode::Tag(_) => {}
|
||||||
@ -51,7 +49,7 @@ fn write_node(w: &mut Writer, node: &HtmlNode) -> SourceResult<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encodes plain text into the writer.
|
/// Encode plain text into the writer.
|
||||||
fn write_text(w: &mut Writer, text: &str, span: Span) -> SourceResult<()> {
|
fn write_text(w: &mut Writer, text: &str, span: Span) -> SourceResult<()> {
|
||||||
for c in text.chars() {
|
for c in text.chars() {
|
||||||
if charsets::is_valid_in_normal_element_text(c) {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encodes one element into the writer.
|
/// Encode one element into the write.
|
||||||
fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
|
fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
|
||||||
w.buf.push('<');
|
w.buf.push('<');
|
||||||
w.buf.push_str(&element.tag.resolve());
|
w.buf.push_str(&element.tag.resolve());
|
||||||
@ -91,17 +89,39 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
|
|||||||
w.buf.push('>');
|
w.buf.push('>');
|
||||||
|
|
||||||
if tag::is_void(element.tag) {
|
if tag::is_void(element.tag) {
|
||||||
if !element.children.is_empty() {
|
|
||||||
bail!(element.span, "HTML void elements must not have children");
|
|
||||||
}
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if tag::is_raw(element.tag) {
|
let pretty = w.pretty;
|
||||||
write_raw(w, element)?;
|
if !element.children.is_empty() {
|
||||||
} else if !element.children.is_empty() {
|
let pretty_inside = allows_pretty_inside(element.tag)
|
||||||
write_children(w, element)?;
|
&& 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("</");
|
||||||
w.buf.push_str(&element.tag.resolve());
|
w.buf.push_str(&element.tag.resolve());
|
||||||
@ -110,159 +130,6 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
|
|||||||
Ok(())
|
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
|
/// Whether we are allowed to add an extra newline at the start and end of the
|
||||||
/// element's contents.
|
/// 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' => {
|
c if charsets::is_w3c_text_char(c) && c != '\r' => {
|
||||||
write!(w.buf, "&#x{:x};", c as u32).unwrap()
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -180,6 +180,9 @@ fn handle(
|
|||||||
if let Some(body) = elem.body(styles) {
|
if let Some(body) = elem.body(styles) {
|
||||||
children = html_fragment(engine, body, locator.next(&elem.span()), 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 {
|
let element = HtmlElement {
|
||||||
tag: elem.tag,
|
tag: elem.tag,
|
||||||
attrs: elem.attrs(styles).clone(),
|
attrs: elem.attrs(styles).clone(),
|
||||||
|
@ -11,8 +11,8 @@ use typst_library::layout::{
|
|||||||
};
|
};
|
||||||
use typst_library::visualize::{
|
use typst_library::visualize::{
|
||||||
CircleElem, CloseMode, Curve, CurveComponent, CurveElem, EllipseElem, FillRule,
|
CircleElem, CloseMode, Curve, CurveComponent, CurveElem, EllipseElem, FillRule,
|
||||||
FixedStroke, Geometry, LineCap, LineElem, Paint, PathElem, PathVertex, PolygonElem,
|
FixedStroke, Geometry, LineElem, Paint, PathElem, PathVertex, PolygonElem, RectElem,
|
||||||
RectElem, Shape, SquareElem, Stroke,
|
Shape, SquareElem, Stroke,
|
||||||
};
|
};
|
||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
use typst_utils::{Get, Numeric};
|
use typst_utils::{Get, Numeric};
|
||||||
@ -889,13 +889,7 @@ fn segmented_rect(
|
|||||||
let end = current;
|
let end = current;
|
||||||
last = current;
|
last = current;
|
||||||
let Some(stroke) = strokes.get_ref(start.side_cw()) else { continue };
|
let Some(stroke) = strokes.get_ref(start.side_cw()) else { continue };
|
||||||
let start_cap = stroke.cap;
|
let (shape, ontop) = segment(start, end, &corners, stroke);
|
||||||
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);
|
|
||||||
if ontop {
|
if ontop {
|
||||||
res.push(shape);
|
res.push(shape);
|
||||||
} else {
|
} else {
|
||||||
@ -905,14 +899,7 @@ fn segmented_rect(
|
|||||||
}
|
}
|
||||||
} else if let Some(stroke) = &strokes.top {
|
} else if let Some(stroke) = &strokes.top {
|
||||||
// single segment
|
// single segment
|
||||||
let (shape, _) = segment(
|
let (shape, _) = segment(Corner::TopLeft, Corner::TopLeft, &corners, stroke);
|
||||||
Corner::TopLeft,
|
|
||||||
Corner::TopLeft,
|
|
||||||
stroke.cap,
|
|
||||||
stroke.cap,
|
|
||||||
&corners,
|
|
||||||
stroke,
|
|
||||||
);
|
|
||||||
res.push(shape);
|
res.push(shape);
|
||||||
}
|
}
|
||||||
res
|
res
|
||||||
@ -959,8 +946,6 @@ fn curve_segment(
|
|||||||
fn segment(
|
fn segment(
|
||||||
start: Corner,
|
start: Corner,
|
||||||
end: Corner,
|
end: Corner,
|
||||||
start_cap: LineCap,
|
|
||||||
end_cap: LineCap,
|
|
||||||
corners: &Corners<ControlPoints>,
|
corners: &Corners<ControlPoints>,
|
||||||
stroke: &FixedStroke,
|
stroke: &FixedStroke,
|
||||||
) -> (Shape, bool) {
|
) -> (Shape, bool) {
|
||||||
@ -994,7 +979,7 @@ fn segment(
|
|||||||
|
|
||||||
let use_fill = solid && fill_corners(start, end, corners);
|
let use_fill = solid && fill_corners(start, end, corners);
|
||||||
let shape = if use_fill {
|
let shape = if use_fill {
|
||||||
fill_segment(start, end, start_cap, end_cap, corners, stroke)
|
fill_segment(start, end, corners, stroke)
|
||||||
} else {
|
} else {
|
||||||
stroke_segment(start, end, corners, stroke.clone())
|
stroke_segment(start, end, corners, stroke.clone())
|
||||||
};
|
};
|
||||||
@ -1025,8 +1010,6 @@ fn stroke_segment(
|
|||||||
fn fill_segment(
|
fn fill_segment(
|
||||||
start: Corner,
|
start: Corner,
|
||||||
end: Corner,
|
end: Corner,
|
||||||
start_cap: LineCap,
|
|
||||||
end_cap: LineCap,
|
|
||||||
corners: &Corners<ControlPoints>,
|
corners: &Corners<ControlPoints>,
|
||||||
stroke: &FixedStroke,
|
stroke: &FixedStroke,
|
||||||
) -> Shape {
|
) -> Shape {
|
||||||
@ -1052,7 +1035,8 @@ fn fill_segment(
|
|||||||
if c.arc_outer() {
|
if c.arc_outer() {
|
||||||
curve.arc_line(c.mid_outer(), c.center_outer(), c.end_outer());
|
curve.arc_line(c.mid_outer(), c.center_outer(), c.end_outer());
|
||||||
} else {
|
} 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() {
|
if c.arc_inner() {
|
||||||
curve.arc_line(c.mid_inner(), c.center_inner(), c.start_inner());
|
curve.arc_line(c.mid_inner(), c.center_inner(), c.start_inner());
|
||||||
} else {
|
} else {
|
||||||
c.end_cap(&mut curve, end_cap);
|
curve.line(c.center_inner());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1150,16 +1134,6 @@ struct ControlPoints {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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.
|
/// Move and rotate the point from top-left to the required corner.
|
||||||
fn rotate(&self, point: Point) -> Point {
|
fn rotate(&self, point: Point) -> Point {
|
||||||
match self.corner {
|
match self.corner {
|
||||||
@ -1306,77 +1280,6 @@ impl ControlPoints {
|
|||||||
y: self.stroke_after,
|
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.
|
/// Helper to draw arcs with Bézier curves.
|
||||||
|
@ -30,7 +30,6 @@ const TRANSLATIONS: &[(&str, &str)] = &[
|
|||||||
translation!("fr"),
|
translation!("fr"),
|
||||||
translation!("gl"),
|
translation!("gl"),
|
||||||
translation!("he"),
|
translation!("he"),
|
||||||
translation!("hr"),
|
|
||||||
translation!("hu"),
|
translation!("hu"),
|
||||||
translation!("id"),
|
translation!("id"),
|
||||||
translation!("is"),
|
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::{Document, SerializeSettings};
|
||||||
use krilla_svg::render_svg_glyph;
|
use krilla_svg::render_svg_glyph;
|
||||||
use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult};
|
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::introspection::Location;
|
||||||
use typst_library::layout::{
|
use typst_library::layout::{
|
||||||
Abs, Frame, FrameItem, GroupItem, PagedDocument, Size, Transform,
|
Abs, Frame, FrameItem, GroupItem, PagedDocument, Size, Transform,
|
||||||
@ -429,18 +429,14 @@ fn convert_error(
|
|||||||
display_font(gc.fonts_backward.get(f).unwrap());
|
display_font(gc.fonts_backward.get(f).unwrap());
|
||||||
hint: "try using a different font"
|
hint: "try using a different font"
|
||||||
),
|
),
|
||||||
ValidationError::InvalidCodepointMapping(_, _, c, loc) => {
|
ValidationError::InvalidCodepointMapping(_, _, cp, loc) => {
|
||||||
if let Some(c) = c {
|
if let Some(c) = cp.map(|c| eco_format!("{:#06x}", c as u32)) {
|
||||||
let msg = if loc.is_some() {
|
let msg = if loc.is_some() {
|
||||||
"the PDF contains text with"
|
"the PDF contains text with"
|
||||||
} else {
|
} else {
|
||||||
"the text contains"
|
"the text contains"
|
||||||
};
|
};
|
||||||
error!(
|
error!(to_span(*loc), "{prefix} {msg} the disallowed codepoint {c}")
|
||||||
to_span(*loc),
|
|
||||||
"{prefix} {msg} the disallowed codepoint `{}`",
|
|
||||||
c.repr()
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
// I think this code path is in theory unreachable,
|
// I think this code path is in theory unreachable,
|
||||||
// but just to be safe.
|
// but just to be safe.
|
||||||
@ -458,12 +454,13 @@ fn convert_error(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ValidationError::UnicodePrivateArea(_, _, c, loc) => {
|
ValidationError::UnicodePrivateArea(_, _, c, loc) => {
|
||||||
|
let code_point = eco_format!("{:#06x}", *c as u32);
|
||||||
let msg = if loc.is_some() { "the PDF" } else { "the text" };
|
let msg = if loc.is_some() { "the PDF" } else { "the text" };
|
||||||
error!(
|
error!(
|
||||||
to_span(*loc),
|
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 \
|
hint: "codepoints from the Unicode private area are \
|
||||||
forbidden in this export mode",
|
forbidden in this export mode"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ValidationError::Transparency(loc) => {
|
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 ecow::eco_vec;
|
||||||
use tiny_skia as sk;
|
use tiny_skia as sk;
|
||||||
use typst::diag::{SourceDiagnostic, SourceResult, Warned};
|
use typst::diag::{SourceDiagnostic, Warned};
|
||||||
use typst::html::HtmlDocument;
|
use typst::html::HtmlDocument;
|
||||||
use typst::layout::{Abs, Frame, FrameItem, PagedDocument, Transform};
|
use typst::layout::{Abs, Frame, FrameItem, PagedDocument, Transform};
|
||||||
use typst::visualize::Color;
|
use typst::visualize::Color;
|
||||||
@ -82,26 +82,17 @@ impl<'a> Runner<'a> {
|
|||||||
/// Run test specific to document format.
|
/// Run test specific to document format.
|
||||||
fn run_test<D: OutputType>(&mut self) {
|
fn run_test<D: OutputType>(&mut self) {
|
||||||
let Warned { output, warnings } = typst::compile(&self.world);
|
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![]),
|
Ok(doc) => (Some(doc), eco_vec![]),
|
||||||
Err(errors) => (None, errors),
|
Err(errors) => (None, errors),
|
||||||
};
|
};
|
||||||
|
|
||||||
D::check_custom(self, doc.as_ref());
|
if doc.is_none() && errors.is_empty() {
|
||||||
|
|
||||||
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() {
|
|
||||||
log!(self, "no document, but also no errors");
|
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 {
|
for error in &errors {
|
||||||
self.check_diagnostic(NoteKind::Error, error);
|
self.check_diagnostic(NoteKind::Error, error);
|
||||||
@ -137,12 +128,12 @@ impl<'a> Runner<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check that the document output is correct.
|
/// 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 live_path = D::live_path(&self.test.name);
|
||||||
let ref_path = D::ref_path(&self.test.name);
|
let ref_path = D::ref_path(&self.test.name);
|
||||||
let ref_data = std::fs::read(&ref_path);
|
let ref_data = std::fs::read(&ref_path);
|
||||||
|
|
||||||
let Some((document, live)) = output else {
|
let Some(document) = document else {
|
||||||
if ref_data.is_ok() {
|
if ref_data.is_ok() {
|
||||||
log!(self, "missing document");
|
log!(self, "missing document");
|
||||||
log!(self, " ref | {}", ref_path.display());
|
log!(self, " ref | {}", ref_path.display());
|
||||||
@ -150,7 +141,7 @@ impl<'a> Runner<'a> {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let skippable = match D::is_skippable(&document) {
|
let skippable = match D::is_skippable(document) {
|
||||||
Ok(skippable) => skippable,
|
Ok(skippable) => skippable,
|
||||||
Err(()) => {
|
Err(()) => {
|
||||||
log!(self, "document has zero pages");
|
log!(self, "document has zero pages");
|
||||||
@ -166,6 +157,7 @@ impl<'a> Runner<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render and save live version.
|
// Render and save live version.
|
||||||
|
let live = document.make_live();
|
||||||
document.save_live(&self.test.name, &live);
|
document.save_live(&self.test.name, &live);
|
||||||
|
|
||||||
// Compare against reference output if available.
|
// Compare against reference output if available.
|
||||||
@ -222,13 +214,9 @@ impl<'a> Runner<'a> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = if diag.message.contains("\\u{") {
|
let message = diag.message.replace("\\", "/");
|
||||||
&diag.message
|
|
||||||
} else {
|
|
||||||
&diag.message.replace("\\", "/")
|
|
||||||
};
|
|
||||||
let range = self.world.range(diag.span);
|
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.
|
// Check hints.
|
||||||
for hint in &diag.hints {
|
for hint in &diag.hints {
|
||||||
@ -371,7 +359,7 @@ trait OutputType: Document {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Produces the live output.
|
/// Produces the live output.
|
||||||
fn make_live(&self) -> SourceResult<Self::Live>;
|
fn make_live(&self) -> Self::Live;
|
||||||
|
|
||||||
/// Saves the live output.
|
/// Saves the live output.
|
||||||
fn save_live(&self, name: &str, live: &Self::Live);
|
fn save_live(&self, name: &str, live: &Self::Live);
|
||||||
@ -418,8 +406,8 @@ impl OutputType for PagedDocument {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_live(&self) -> SourceResult<Self::Live> {
|
fn make_live(&self) -> Self::Live {
|
||||||
Ok(render(self, 1.0))
|
render(self, 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_live(&self, name: &str, live: &Self::Live) {
|
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()
|
format!("{}/html/{}.html", crate::REF_PATH, name).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_live(&self) -> SourceResult<Self::Live> {
|
fn make_live(&self) -> Self::Live {
|
||||||
typst_html::html(self)
|
// 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) {
|
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)
|
#v(3pt)
|
||||||
#rect(width: 20pt, height: 20pt, stroke: (thickness: 5pt, join: "round"))
|
#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 ---
|
--- red-stroke-bad-type ---
|
||||||
// Error: 15-21 expected length, color, gradient, tiling, dictionary, stroke, none, or auto, found array
|
// Error: 15-21 expected length, color, gradient, tiling, dictionary, stroke, none, or auto, found array
|
||||||
#rect(stroke: (1, 2))
|
#rect(stroke: (1, 2))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user