mirror of
https://github.com/typst/typst
synced 2025-08-24 19:54:14 +08:00
Compare commits
8 Commits
e9dc4bb204
...
87cb8f5094
Author | SHA1 | Date | |
---|---|---|---|
|
87cb8f5094 | ||
|
38dd6da237 | ||
|
bf8ef2a4a5 | ||
|
c2e2fd99f6 | ||
|
f8dc1ad3bd | ||
|
9050ee1639 | ||
|
c1b2aee1a9 | ||
|
fbb02f40d9 |
@ -2,7 +2,9 @@ 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::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag};
|
use typst_library::html::{
|
||||||
|
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;
|
||||||
|
|
||||||
@ -28,7 +30,7 @@ struct Writer {
|
|||||||
pretty: bool,
|
pretty: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write a newline and indent, if pretty printing is enabled.
|
/// Writes 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');
|
||||||
@ -38,7 +40,7 @@ fn write_indent(w: &mut Writer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encode an HTML node into the writer.
|
/// Encodes 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(_) => {}
|
||||||
@ -49,7 +51,7 @@ fn write_node(w: &mut Writer, node: &HtmlNode) -> SourceResult<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encode plain text into the writer.
|
/// Encodes 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) {
|
||||||
@ -61,7 +63,7 @@ fn write_text(w: &mut Writer, text: &str, span: Span) -> SourceResult<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encode one element into the write.
|
/// Encodes one element into the writer.
|
||||||
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());
|
||||||
@ -89,39 +91,17 @@ 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(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let pretty = w.pretty;
|
if tag::is_raw(element.tag) {
|
||||||
if !element.children.is_empty() {
|
write_raw(w, element)?;
|
||||||
let pretty_inside = allows_pretty_inside(element.tag)
|
} else if !element.children.is_empty() {
|
||||||
&& element.children.iter().any(|node| match node {
|
write_children(w, element)?;
|
||||||
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());
|
||||||
@ -130,6 +110,159 @@ 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.
|
||||||
///
|
///
|
||||||
@ -165,7 +298,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,9 +180,6 @@ 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, LineElem, Paint, PathElem, PathVertex, PolygonElem, RectElem,
|
FixedStroke, Geometry, LineCap, LineElem, Paint, PathElem, PathVertex, PolygonElem,
|
||||||
Shape, SquareElem, Stroke,
|
RectElem, Shape, SquareElem, Stroke,
|
||||||
};
|
};
|
||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
use typst_utils::{Get, Numeric};
|
use typst_utils::{Get, Numeric};
|
||||||
@ -889,7 +889,13 @@ 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 (shape, ontop) = segment(start, end, &corners, stroke);
|
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);
|
||||||
if ontop {
|
if ontop {
|
||||||
res.push(shape);
|
res.push(shape);
|
||||||
} else {
|
} else {
|
||||||
@ -899,7 +905,14 @@ fn segmented_rect(
|
|||||||
}
|
}
|
||||||
} else if let Some(stroke) = &strokes.top {
|
} else if let Some(stroke) = &strokes.top {
|
||||||
// single segment
|
// single segment
|
||||||
let (shape, _) = segment(Corner::TopLeft, Corner::TopLeft, &corners, stroke);
|
let (shape, _) = segment(
|
||||||
|
Corner::TopLeft,
|
||||||
|
Corner::TopLeft,
|
||||||
|
stroke.cap,
|
||||||
|
stroke.cap,
|
||||||
|
&corners,
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
res.push(shape);
|
res.push(shape);
|
||||||
}
|
}
|
||||||
res
|
res
|
||||||
@ -946,6 +959,8 @@ 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) {
|
||||||
@ -979,7 +994,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, corners, stroke)
|
fill_segment(start, end, start_cap, end_cap, corners, stroke)
|
||||||
} else {
|
} else {
|
||||||
stroke_segment(start, end, corners, stroke.clone())
|
stroke_segment(start, end, corners, stroke.clone())
|
||||||
};
|
};
|
||||||
@ -1010,6 +1025,8 @@ 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 {
|
||||||
@ -1035,8 +1052,7 @@ 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 {
|
||||||
curve.line(c.outer());
|
c.start_cap(&mut curve, start_cap);
|
||||||
curve.line(c.end_outer());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1079,7 +1095,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 {
|
||||||
curve.line(c.center_inner());
|
c.end_cap(&mut curve, end_cap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1134,6 +1150,16 @@ 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 {
|
||||||
@ -1280,6 +1306,77 @@ 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,6 +30,7 @@ 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"),
|
||||||
|
8
crates/typst-library/translations/hr.txt
Normal file
8
crates/typst-library/translations/hr.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
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;
|
use typst_library::foundations::{NativeElement, Repr};
|
||||||
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,14 +429,18 @@ 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(_, _, cp, loc) => {
|
ValidationError::InvalidCodepointMapping(_, _, c, loc) => {
|
||||||
if let Some(c) = cp.map(|c| eco_format!("{:#06x}", c as u32)) {
|
if let Some(c) = c {
|
||||||
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!(to_span(*loc), "{prefix} {msg} the disallowed codepoint {c}")
|
error!(
|
||||||
|
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.
|
||||||
@ -454,13 +458,12 @@ 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 {code_point}";
|
"{prefix} {msg} contains the codepoint `{}`", c.repr();
|
||||||
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) => {
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
<!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>
|
17
tests/ref/html/html-pre-starting-with-newline.html
Normal file
17
tests/ref/html/html-pre-starting-with-newline.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!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>
|
21
tests/ref/html/html-script.html
Normal file
21
tests/ref/html/html-script.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!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>
|
14
tests/ref/html/html-style.html
Normal file
14
tests/ref/html/html-style.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!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>
|
BIN
tests/ref/rect-stroke-caps.png
Normal file
BIN
tests/ref/rect-stroke-caps.png
Normal file
Binary file not shown.
After 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, Warned};
|
use typst::diag::{SourceDiagnostic, SourceResult, 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,17 +82,26 @@ 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, errors) = match output {
|
let (doc, mut errors) = match output {
|
||||||
Ok(doc) => (Some(doc), eco_vec![]),
|
Ok(doc) => (Some(doc), eco_vec![]),
|
||||||
Err(errors) => (None, errors),
|
Err(errors) => (None, errors),
|
||||||
};
|
};
|
||||||
|
|
||||||
if doc.is_none() && errors.is_empty() {
|
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() {
|
||||||
log!(self, "no document, but also no errors");
|
log!(self, "no document, but also no errors");
|
||||||
}
|
}
|
||||||
|
|
||||||
D::check_custom(self, doc.as_ref());
|
self.check_output(output);
|
||||||
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);
|
||||||
@ -128,12 +137,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, document: Option<&D>) {
|
fn check_output<D: OutputType>(&mut self, output: Option<(D, D::Live)>) {
|
||||||
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) = document else {
|
let Some((document, live)) = output 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());
|
||||||
@ -141,7 +150,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");
|
||||||
@ -157,7 +166,6 @@ 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.
|
||||||
@ -214,9 +222,13 @@ impl<'a> Runner<'a> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = diag.message.replace("\\", "/");
|
let message = if diag.message.contains("\\u{") {
|
||||||
|
&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 {
|
||||||
@ -359,7 +371,7 @@ trait OutputType: Document {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Produces the live output.
|
/// Produces the live output.
|
||||||
fn make_live(&self) -> Self::Live;
|
fn make_live(&self) -> SourceResult<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);
|
||||||
@ -406,8 +418,8 @@ impl OutputType for PagedDocument {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_live(&self) -> Self::Live {
|
fn make_live(&self) -> SourceResult<Self::Live> {
|
||||||
render(self, 1.0)
|
Ok(render(self, 1.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_live(&self, name: &str, live: &Self::Live) {
|
fn save_live(&self, name: &str, live: &Self::Live) {
|
||||||
@ -471,9 +483,8 @@ impl OutputType for HtmlDocument {
|
|||||||
format!("{}/html/{}.html", crate::REF_PATH, name).into()
|
format!("{}/html/{}.html", crate::REF_PATH, name).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_live(&self) -> Self::Live {
|
fn make_live(&self) -> SourceResult<Self::Live> {
|
||||||
// TODO: Do this earlier to be able to process export errors.
|
typst_html::html(self)
|
||||||
typst_html::html(self).unwrap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_live(&self, name: &str, live: &Self::Live) {
|
fn save_live(&self, name: &str, live: &Self::Live) {
|
||||||
|
63
tests/suite/html/syntax.typ
Normal file
63
tests/suite/html/syntax.typ
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
--- 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,6 +54,22 @@
|
|||||||
#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