mirror of
https://github.com/servo/servo.git
synced 2025-08-06 06:00:15 +01:00
canvas: Use wrapped kurbo::BezPath
for path everywhere (#37967)
This PR removes existing path(segment) abstractions in favor of `kurbo::BezPath`, well actually wrapped `kurbo::BezPath`, to ensure building of valid paths. This allows us better Path2D building in script and doing all validation and segmentation there and also allows us remove blocking is_point_in_path on Path2D as we can now do this in script. Current path is still done on canvas thread side as it will be harder to move to script (will be done as a follow up), but it now uses this new path abstraction. Using kurbo also allows us to ditch our manual svgpath parser with the one provided by kurbo. Same code is stolen from: https://github.com/servo/servo/pull/36821. Testing: Existing WPT tests Fixes: #37904 wpt run: https://github.com/sagudev/servo/actions/runs/16172191716 --------- Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>
This commit is contained in:
parent
d4528e84b9
commit
9b5b26386c
23 changed files with 571 additions and 1502 deletions
|
@ -81,6 +81,7 @@ itertools = { workspace = true }
|
|||
js = { workspace = true }
|
||||
jstraceable_derive = { path = "../jstraceable_derive" }
|
||||
keyboard-types = { workspace = true }
|
||||
kurbo = { workspace = true }
|
||||
layout_api = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
|
|
@ -9,7 +9,7 @@ use std::sync::Arc;
|
|||
|
||||
use canvas_traits::canvas::{
|
||||
Canvas2dMsg, CanvasId, CanvasMsg, CompositionOrBlending, Direction, FillOrStrokeStyle,
|
||||
FillRule, LineCapStyle, LineJoinStyle, LinearGradientStyle, PathSegment, RadialGradientStyle,
|
||||
FillRule, LineCapStyle, LineJoinStyle, LinearGradientStyle, Path, RadialGradientStyle,
|
||||
RepetitionStyle, TextAlign, TextBaseline, TextMetrics as CanvasTextMetrics,
|
||||
};
|
||||
use constellation_traits::ScriptToConstellationMessage;
|
||||
|
@ -1799,7 +1799,7 @@ impl CanvasState {
|
|||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-context-2d-fill
|
||||
pub(crate) fn fill_(&self, path: Vec<PathSegment>, _fill_rule: CanvasFillRule) {
|
||||
pub(crate) fn fill_(&self, path: Path, _fill_rule: CanvasFillRule) {
|
||||
// TODO: Process fill rule
|
||||
let style = self.state.borrow().fill_style.to_fill_or_stroke_style();
|
||||
self.send_canvas_2d_msg(Canvas2dMsg::FillPath(style, path));
|
||||
|
@ -1811,7 +1811,7 @@ impl CanvasState {
|
|||
self.send_canvas_2d_msg(Canvas2dMsg::Stroke(style));
|
||||
}
|
||||
|
||||
pub(crate) fn stroke_(&self, path: Vec<PathSegment>) {
|
||||
pub(crate) fn stroke_(&self, path: Path) {
|
||||
let style = self.state.borrow().stroke_style.to_fill_or_stroke_style();
|
||||
self.send_canvas_2d_msg(Canvas2dMsg::StrokePath(style, path));
|
||||
}
|
||||
|
@ -1823,7 +1823,7 @@ impl CanvasState {
|
|||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-context-2d-clip
|
||||
pub(crate) fn clip_(&self, path: Vec<PathSegment>, _fill_rule: CanvasFillRule) {
|
||||
pub(crate) fn clip_(&self, path: Path, _fill_rule: CanvasFillRule) {
|
||||
// TODO: Process fill rule
|
||||
self.send_canvas_2d_msg(Canvas2dMsg::ClipPath(path));
|
||||
}
|
||||
|
@ -1853,24 +1853,17 @@ impl CanvasState {
|
|||
// https://html.spec.whatwg.org/multipage/#dom-context-2d-ispointinpath
|
||||
pub(crate) fn is_point_in_path_(
|
||||
&self,
|
||||
global: &GlobalScope,
|
||||
path: Vec<PathSegment>,
|
||||
_global: &GlobalScope,
|
||||
path: Path,
|
||||
x: f64,
|
||||
y: f64,
|
||||
fill_rule: CanvasFillRule,
|
||||
) -> bool {
|
||||
if !(x.is_finite() && y.is_finite()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let fill_rule = match fill_rule {
|
||||
CanvasFillRule::Nonzero => FillRule::Nonzero,
|
||||
CanvasFillRule::Evenodd => FillRule::Evenodd,
|
||||
};
|
||||
let (sender, receiver) =
|
||||
profiled_ipc::channel::<bool>(global.time_profiler_chan().clone()).unwrap();
|
||||
self.send_canvas_2d_msg(Canvas2dMsg::IsPointInPath(path, x, y, fill_rule, sender));
|
||||
receiver.recv().unwrap()
|
||||
path.is_point_in_path(x, y, fill_rule)
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-context-2d-scale
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
use std::cell::RefCell;
|
||||
|
||||
use canvas_traits::canvas::PathSegment;
|
||||
use canvas_traits::canvas::Path;
|
||||
use dom_struct::dom_struct;
|
||||
use js::rust::HandleObject;
|
||||
use script_bindings::str::DOMString;
|
||||
|
@ -15,20 +15,20 @@ use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto};
|
|||
use crate::dom::bindings::root::DomRoot;
|
||||
use crate::dom::globalscope::GlobalScope;
|
||||
use crate::script_runtime::CanGc;
|
||||
use crate::svgpath::PathParser;
|
||||
|
||||
#[dom_struct]
|
||||
pub(crate) struct Path2D {
|
||||
reflector_: Reflector,
|
||||
#[ignore_malloc_size_of = "Defined in kurbo."]
|
||||
#[no_trace]
|
||||
path: RefCell<Vec<PathSegment>>,
|
||||
path: RefCell<Path>,
|
||||
}
|
||||
|
||||
impl Path2D {
|
||||
pub(crate) fn new() -> Path2D {
|
||||
Self {
|
||||
reflector_: Reflector::new(),
|
||||
path: RefCell::new(vec![]),
|
||||
path: RefCell::new(Path::new()),
|
||||
}
|
||||
}
|
||||
pub(crate) fn new_with_path(other: &Path2D) -> Path2D {
|
||||
|
@ -37,26 +37,15 @@ impl Path2D {
|
|||
path: other.path.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_with_str(path: &str) -> Path2D {
|
||||
let mut path_segments = Vec::new();
|
||||
|
||||
for segment in PathParser::new(path) {
|
||||
if let Ok(segment) = segment {
|
||||
path_segments.push(segment);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
reflector_: Reflector::new(),
|
||||
path: RefCell::new(path_segments),
|
||||
path: RefCell::new(Path::from_svg(path)),
|
||||
}
|
||||
}
|
||||
pub(crate) fn push(&self, seg: PathSegment) {
|
||||
self.path.borrow_mut().push(seg);
|
||||
}
|
||||
pub(crate) fn segments(&self) -> Vec<PathSegment> {
|
||||
|
||||
pub(crate) fn segments(&self) -> Path {
|
||||
self.path.borrow().clone()
|
||||
}
|
||||
}
|
||||
|
@ -64,143 +53,49 @@ impl Path2D {
|
|||
impl Path2DMethods<crate::DomTypeHolder> for Path2D {
|
||||
/// <https://html.spec.whatwg.org/multipage/#dom-path2d-addpath>
|
||||
fn AddPath(&self, other: &Path2D) {
|
||||
let other = other.segments();
|
||||
// Step 7. Add all the subpaths in c to a.
|
||||
if std::ptr::eq(&self.path, &other.path) {
|
||||
// Note: this is not part of the spec, but it is a workaround to
|
||||
// avoids borrow conflict when path is same as other.path
|
||||
self.path.borrow_mut().extend_from_within(..);
|
||||
} else {
|
||||
let mut dest = self.path.borrow_mut();
|
||||
dest.extend(other.path.borrow().iter().copied());
|
||||
}
|
||||
self.path.borrow_mut().0.extend(other.0);
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-closepath>
|
||||
fn ClosePath(&self) {
|
||||
self.push(PathSegment::ClosePath);
|
||||
self.path.borrow_mut().close_path();
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-moveto>
|
||||
fn MoveTo(&self, x: f64, y: f64) {
|
||||
// Step 1. If either of the arguments are infinite or NaN, then return.
|
||||
if !(x.is_finite() && y.is_finite()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2. Create a new subpath with the specified point as its first (and only) point.
|
||||
self.push(PathSegment::MoveTo {
|
||||
x: x as f32,
|
||||
y: y as f32,
|
||||
});
|
||||
self.path.borrow_mut().move_to(x, y);
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-lineto>
|
||||
fn LineTo(&self, x: f64, y: f64) {
|
||||
// Step 1. If either of the arguments are infinite or NaN, then return.
|
||||
if !(x.is_finite() && y.is_finite()) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.push(PathSegment::LineTo {
|
||||
x: x as f32,
|
||||
y: y as f32,
|
||||
});
|
||||
self.path.borrow_mut().line_to(x, y);
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-quadraticcurveto>
|
||||
fn QuadraticCurveTo(&self, cpx: f64, cpy: f64, x: f64, y: f64) {
|
||||
// Step 1. If any of the arguments are infinite or NaN, then return.
|
||||
if !(cpx.is_finite() && cpy.is_finite() && x.is_finite() && y.is_finite()) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.push(PathSegment::Quadratic {
|
||||
cpx: cpx as f32,
|
||||
cpy: cpy as f32,
|
||||
x: x as f32,
|
||||
y: y as f32,
|
||||
});
|
||||
self.path.borrow_mut().quadratic_curve_to(cpx, cpy, x, y);
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-beziercurveto>
|
||||
fn BezierCurveTo(&self, cp1x: f64, cp1y: f64, cp2x: f64, cp2y: f64, x: f64, y: f64) {
|
||||
// Step 1. If any of the arguments are infinite or NaN, then return.
|
||||
if !(cp1x.is_finite() &&
|
||||
cp1y.is_finite() &&
|
||||
cp2x.is_finite() &&
|
||||
cp2y.is_finite() &&
|
||||
x.is_finite() &&
|
||||
y.is_finite())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.push(PathSegment::Bezier {
|
||||
cp1x: cp1x as f32,
|
||||
cp1y: cp1y as f32,
|
||||
cp2x: cp2x as f32,
|
||||
cp2y: cp2y as f32,
|
||||
x: x as f32,
|
||||
y: y as f32,
|
||||
});
|
||||
self.path
|
||||
.borrow_mut()
|
||||
.bezier_curve_to(cp1x, cp1y, cp2x, cp2y, x, y);
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-arcto>
|
||||
fn ArcTo(&self, x1: f64, y1: f64, x2: f64, y2: f64, r: f64) -> Fallible<()> {
|
||||
// Step 1. If any of the arguments are infinite or NaN, then return.
|
||||
if !(x1.is_finite() && y1.is_finite() && x2.is_finite() && y2.is_finite() && r.is_finite())
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Step 3. If radius is negative, then throw an "IndexSizeError" DOMException.
|
||||
if r < 0.0 {
|
||||
return Err(Error::IndexSize);
|
||||
}
|
||||
|
||||
self.push(PathSegment::ArcTo {
|
||||
cp1x: x1 as f32,
|
||||
cp1y: y1 as f32,
|
||||
cp2x: x2 as f32,
|
||||
cp2y: y2 as f32,
|
||||
radius: r as f32,
|
||||
});
|
||||
Ok(())
|
||||
fn ArcTo(&self, x1: f64, y1: f64, x2: f64, y2: f64, radius: f64) -> Fallible<()> {
|
||||
self.path
|
||||
.borrow_mut()
|
||||
.arc_to(x1, y1, x2, y2, radius)
|
||||
.map_err(|_| Error::IndexSize)
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-rect>
|
||||
fn Rect(&self, x: f64, y: f64, w: f64, h: f64) {
|
||||
// Step 1. If any of the arguments are infinite or NaN, then return.
|
||||
if !(x.is_finite() && y.is_finite() && w.is_finite() && h.is_finite()) {
|
||||
return;
|
||||
}
|
||||
// Step 2. Create a new subpath containing just the four points
|
||||
// (x, y), (x+w, y), (x+w, y+h), (x, y+h), in that order,
|
||||
// with those four points connected by straight lines.
|
||||
self.push(PathSegment::MoveTo {
|
||||
x: x as f32,
|
||||
y: y as f32,
|
||||
});
|
||||
self.push(PathSegment::LineTo {
|
||||
x: (x + w) as f32,
|
||||
y: y as f32,
|
||||
});
|
||||
self.push(PathSegment::LineTo {
|
||||
x: (x + w) as f32,
|
||||
y: (y + h) as f32,
|
||||
});
|
||||
self.push(PathSegment::LineTo {
|
||||
x: x as f32,
|
||||
y: (y + h) as f32,
|
||||
});
|
||||
// Step 3. Mark the subpath as closed.
|
||||
self.push(PathSegment::ClosePath);
|
||||
|
||||
// Step 4. Create a new subpath with the point (x, y) as the only point in the subpath.
|
||||
self.push(PathSegment::MoveTo {
|
||||
x: x as f32,
|
||||
y: y as f32,
|
||||
});
|
||||
self.path.borrow_mut().rect(x, y, w, h);
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-arc>
|
||||
|
@ -208,37 +103,15 @@ impl Path2DMethods<crate::DomTypeHolder> for Path2D {
|
|||
&self,
|
||||
x: f64,
|
||||
y: f64,
|
||||
r: f64,
|
||||
start: f64,
|
||||
end: f64,
|
||||
anticlockwise: bool,
|
||||
radius: f64,
|
||||
start_angle: f64,
|
||||
end_angle: f64,
|
||||
counterclockwise: bool,
|
||||
) -> Fallible<()> {
|
||||
// Step 1. If any of the arguments are infinite or NaN, then return.
|
||||
if !(x.is_finite() &&
|
||||
y.is_finite() &&
|
||||
r.is_finite() &&
|
||||
start.is_finite() &&
|
||||
end.is_finite())
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Step 2. If either radiusX or radiusY are negative, then throw an "IndexSizeError" DOMException.
|
||||
if r < 0.0 {
|
||||
return Err(Error::IndexSize);
|
||||
}
|
||||
|
||||
self.push(PathSegment::Ellipse {
|
||||
x: x as f32,
|
||||
y: y as f32,
|
||||
radius_x: r as f32,
|
||||
radius_y: r as f32,
|
||||
rotation: 0.,
|
||||
start_angle: start as f32,
|
||||
end_angle: end as f32,
|
||||
anticlockwise,
|
||||
});
|
||||
Ok(())
|
||||
self.path
|
||||
.borrow_mut()
|
||||
.arc(x, y, radius, start_angle, end_angle, counterclockwise)
|
||||
.map_err(|_| Error::IndexSize)
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-ellipse>
|
||||
|
@ -246,41 +119,26 @@ impl Path2DMethods<crate::DomTypeHolder> for Path2D {
|
|||
&self,
|
||||
x: f64,
|
||||
y: f64,
|
||||
rx: f64,
|
||||
ry: f64,
|
||||
rotation: f64,
|
||||
start: f64,
|
||||
end: f64,
|
||||
anticlockwise: bool,
|
||||
radius_x: f64,
|
||||
radius_y: f64,
|
||||
rotation_angle: f64,
|
||||
start_angle: f64,
|
||||
end_angle: f64,
|
||||
counterclockwise: bool,
|
||||
) -> Fallible<()> {
|
||||
// Step 1. If any of the arguments are infinite or NaN, then return.
|
||||
if !(x.is_finite() &&
|
||||
y.is_finite() &&
|
||||
rx.is_finite() &&
|
||||
ry.is_finite() &&
|
||||
rotation.is_finite() &&
|
||||
start.is_finite() &&
|
||||
end.is_finite())
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Step 2. If either radiusX or radiusY are negative, then throw an "IndexSizeError" DOMException.
|
||||
if rx < 0.0 || ry < 0.0 {
|
||||
return Err(Error::IndexSize);
|
||||
}
|
||||
|
||||
self.push(PathSegment::Ellipse {
|
||||
x: x as f32,
|
||||
y: y as f32,
|
||||
radius_x: rx as f32,
|
||||
radius_y: ry as f32,
|
||||
rotation: rotation as f32,
|
||||
start_angle: start as f32,
|
||||
end_angle: end as f32,
|
||||
anticlockwise,
|
||||
});
|
||||
Ok(())
|
||||
self.path
|
||||
.borrow_mut()
|
||||
.ellipse(
|
||||
x,
|
||||
y,
|
||||
radius_x,
|
||||
radius_y,
|
||||
rotation_angle,
|
||||
start_angle,
|
||||
end_angle,
|
||||
counterclockwise,
|
||||
)
|
||||
.map_err(|_| Error::IndexSize)
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#dom-path2d-dev>
|
||||
|
|
|
@ -74,8 +74,6 @@ mod drag_data_store;
|
|||
mod links;
|
||||
mod xpath;
|
||||
|
||||
mod svgpath;
|
||||
|
||||
pub use init::init;
|
||||
pub(crate) use script_bindings::DomTypes;
|
||||
pub use script_runtime::JSEngineSetup;
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
mod number;
|
||||
mod path;
|
||||
mod stream;
|
||||
|
||||
#[derive(Debug, Default, Eq, PartialEq)]
|
||||
pub struct Error;
|
||||
|
||||
pub(crate) use path::PathParser;
|
||||
pub(crate) use stream::Stream;
|
|
@ -1,198 +0,0 @@
|
|||
// Copyright 2018 the SVG Types Authors
|
||||
// Copyright 2025 the Servo Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::svgpath::{Error, Stream};
|
||||
|
||||
/// An [SVG number](https://www.w3.org/TR/SVG2/types.html#InterfaceSVGNumber).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Number(pub f32);
|
||||
|
||||
impl std::str::FromStr for Number {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(text: &str) -> Result<Self, Self::Err> {
|
||||
let mut s = Stream::from(text);
|
||||
let n = s.parse_number()?;
|
||||
s.skip_spaces();
|
||||
if !s.at_end() {
|
||||
return Err(Error);
|
||||
}
|
||||
|
||||
Ok(Self(n))
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream<'_> {
|
||||
/// Parses number from the stream.
|
||||
///
|
||||
/// This method will detect a number length and then
|
||||
/// will pass a substring to the `f64::from_str` method.
|
||||
///
|
||||
/// <https://www.w3.org/TR/SVG2/types.html#InterfaceSVGNumber>
|
||||
pub fn parse_number(&mut self) -> Result<f32, Error> {
|
||||
// Strip off leading whitespaces.
|
||||
self.skip_spaces();
|
||||
|
||||
if self.at_end() {
|
||||
return Err(Error);
|
||||
}
|
||||
|
||||
self.parse_number_impl().map_err(|_| Error)
|
||||
}
|
||||
|
||||
fn parse_number_impl(&mut self) -> Result<f32, Error> {
|
||||
let start = self.pos();
|
||||
|
||||
let mut c = self.curr_byte()?;
|
||||
|
||||
// Consume sign.
|
||||
if matches!(c, b'+' | b'-') {
|
||||
self.advance(1);
|
||||
c = self.curr_byte()?;
|
||||
}
|
||||
|
||||
// Consume integer.
|
||||
match c {
|
||||
b'0'..=b'9' => self.skip_digits(),
|
||||
b'.' => {},
|
||||
_ => return Err(Error),
|
||||
}
|
||||
|
||||
// Consume fraction.
|
||||
if let Ok(b'.') = self.curr_byte() {
|
||||
self.advance(1);
|
||||
self.skip_digits();
|
||||
}
|
||||
|
||||
if let Ok(c) = self.curr_byte() {
|
||||
if matches!(c, b'e' | b'E') {
|
||||
let c2 = self.next_byte()?;
|
||||
// Check for `em`/`ex`.
|
||||
if c2 != b'm' && c2 != b'x' {
|
||||
self.advance(1);
|
||||
|
||||
match self.curr_byte()? {
|
||||
b'+' | b'-' => {
|
||||
self.advance(1);
|
||||
self.skip_digits();
|
||||
},
|
||||
b'0'..=b'9' => self.skip_digits(),
|
||||
_ => {
|
||||
return Err(Error);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let s = self.slice_back(start);
|
||||
|
||||
// Use the default f32 parser now.
|
||||
if let Ok(n) = f32::from_str(s) {
|
||||
// inf, nan, etc. are an error.
|
||||
if n.is_finite() {
|
||||
return Ok(n);
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error)
|
||||
}
|
||||
|
||||
/// Parses number from a list of numbers.
|
||||
pub fn parse_list_number(&mut self) -> Result<f32, Error> {
|
||||
if self.at_end() {
|
||||
return Err(Error);
|
||||
}
|
||||
|
||||
let n = self.parse_number()?;
|
||||
self.skip_spaces();
|
||||
self.parse_list_separator();
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
/// A pull-based [`<list-of-numbers>`] parser.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use svgtypes::NumberListParser;
|
||||
///
|
||||
/// let mut p = NumberListParser::from("10, 20 -50");
|
||||
/// assert_eq!(p.next().unwrap().unwrap(), 10.0);
|
||||
/// assert_eq!(p.next().unwrap().unwrap(), 20.0);
|
||||
/// assert_eq!(p.next().unwrap().unwrap(), -50.0);
|
||||
/// assert_eq!(p.next().is_none(), true);
|
||||
/// ```
|
||||
///
|
||||
/// [`<list-of-numbers>`]: https://www.w3.org/TR/SVG2/types.html#InterfaceSVGNumberList
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct NumberListParser<'a>(Stream<'a>);
|
||||
|
||||
impl<'a> From<&'a str> for NumberListParser<'a> {
|
||||
#[inline]
|
||||
fn from(v: &'a str) -> Self {
|
||||
NumberListParser(Stream::from(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for NumberListParser<'_> {
|
||||
type Item = Result<f32, Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.0.at_end() {
|
||||
None
|
||||
} else {
|
||||
let v = self.0.parse_list_number();
|
||||
if v.is_err() {
|
||||
self.0.jump_to_end();
|
||||
}
|
||||
|
||||
Some(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::svgpath::Stream;
|
||||
|
||||
macro_rules! test_p {
|
||||
($name:ident, $text:expr, $result:expr) => (
|
||||
#[test]
|
||||
fn $name() {
|
||||
let mut s = Stream::from($text);
|
||||
assert_eq!(s.parse_number().unwrap(), $result);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
test_p!(parse_1, "0", 0.0);
|
||||
test_p!(parse_2, "1", 1.0);
|
||||
test_p!(parse_3, "-1", -1.0);
|
||||
test_p!(parse_4, " -1 ", -1.0);
|
||||
test_p!(parse_5, " 1 ", 1.0);
|
||||
test_p!(parse_6, ".4", 0.4);
|
||||
test_p!(parse_7, "-.4", -0.4);
|
||||
test_p!(parse_8, "-.4text", -0.4);
|
||||
test_p!(parse_9, "-.01 text", -0.01);
|
||||
test_p!(parse_10, "-.01 4", -0.01);
|
||||
test_p!(parse_11, ".0000000000008", 0.0000000000008);
|
||||
test_p!(parse_12, "1000000000000", 1000000000000.0);
|
||||
test_p!(parse_13, "123456.123456", 123456.123456);
|
||||
test_p!(parse_14, "+10", 10.0);
|
||||
test_p!(parse_15, "1e2", 100.0);
|
||||
test_p!(parse_16, "1e+2", 100.0);
|
||||
test_p!(parse_17, "1E2", 100.0);
|
||||
test_p!(parse_18, "1e-2", 0.01);
|
||||
test_p!(parse_19, "1ex", 1.0);
|
||||
test_p!(parse_20, "1em", 1.0);
|
||||
test_p!(parse_21, "12345678901234567890", 12345678901234567000.0);
|
||||
test_p!(parse_22, "0.", 0.0);
|
||||
test_p!(parse_23, "1.3e-2", 0.013);
|
||||
// test_number!(parse_24, "1e", 1.0); // TODO: this
|
||||
}
|
|
@ -1,393 +0,0 @@
|
|||
// Copyright 2021 the SVG Types Authors
|
||||
// Copyright 2025 the Servo Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
use canvas_traits::canvas::PathSegment;
|
||||
|
||||
use crate::svgpath::{Error, Stream};
|
||||
|
||||
pub struct PathParser<'a> {
|
||||
stream: Stream<'a>,
|
||||
state: State,
|
||||
last_cmd: u8,
|
||||
}
|
||||
|
||||
impl<'a> PathParser<'a> {
|
||||
pub fn new(string: &'a str) -> Self {
|
||||
Self {
|
||||
stream: Stream::from(string),
|
||||
state: State::default(),
|
||||
last_cmd: b' ',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for PathParser<'_> {
|
||||
type Item = Result<PathSegment, Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.stream.skip_spaces();
|
||||
|
||||
let Ok(curr_byte) = self.stream.curr_byte() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let cmd = if self.last_cmd == b' ' {
|
||||
if let move_to @ (b'm' | b'M') = curr_byte {
|
||||
self.stream.advance(1);
|
||||
move_to
|
||||
} else {
|
||||
return Some(Err(Error));
|
||||
}
|
||||
} else if curr_byte.is_ascii_alphabetic() {
|
||||
self.stream.advance(1);
|
||||
curr_byte
|
||||
} else {
|
||||
match self.last_cmd {
|
||||
b'm' => b'l',
|
||||
b'M' => b'L',
|
||||
b'z' | b'Z' => return Some(Err(Error)),
|
||||
cmd => cmd,
|
||||
}
|
||||
};
|
||||
|
||||
self.last_cmd = cmd;
|
||||
Some(to_point(&mut self.stream, cmd, &mut self.state))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct State {
|
||||
start: (f32, f32),
|
||||
pos: (f32, f32),
|
||||
quad: (f32, f32),
|
||||
cubic: (f32, f32),
|
||||
}
|
||||
|
||||
pub fn to_point(s: &mut Stream<'_>, cmd: u8, state: &mut State) -> Result<PathSegment, Error> {
|
||||
let abs = cmd.is_ascii_uppercase();
|
||||
let cmd = cmd.to_ascii_lowercase();
|
||||
let (dx, dy) = if abs { (0., 0.) } else { state.pos };
|
||||
let seg = match cmd {
|
||||
b'm' => PathSegment::MoveTo {
|
||||
x: s.parse_list_number()? + dx,
|
||||
y: s.parse_list_number()? + dy,
|
||||
},
|
||||
b'l' => PathSegment::LineTo {
|
||||
x: s.parse_list_number()? + dx,
|
||||
y: s.parse_list_number()? + dy,
|
||||
},
|
||||
b'h' => PathSegment::LineTo {
|
||||
x: s.parse_list_number()? + dx,
|
||||
y: state.pos.1,
|
||||
},
|
||||
b'v' => PathSegment::LineTo {
|
||||
x: state.pos.0,
|
||||
y: s.parse_list_number()? + dy,
|
||||
},
|
||||
b'c' => PathSegment::Bezier {
|
||||
cp1x: s.parse_list_number()? + dx,
|
||||
cp1y: s.parse_list_number()? + dy,
|
||||
cp2x: s.parse_list_number()? + dx,
|
||||
cp2y: s.parse_list_number()? + dy,
|
||||
x: s.parse_list_number()? + dx,
|
||||
y: s.parse_list_number()? + dy,
|
||||
},
|
||||
b's' => PathSegment::Bezier {
|
||||
cp1x: state.cubic.0,
|
||||
cp1y: state.cubic.1,
|
||||
cp2x: s.parse_list_number()? + dx,
|
||||
cp2y: s.parse_list_number()? + dy,
|
||||
x: s.parse_list_number()? + dx,
|
||||
y: s.parse_list_number()? + dy,
|
||||
},
|
||||
b'q' => PathSegment::Quadratic {
|
||||
cpx: s.parse_list_number()? + dx,
|
||||
cpy: s.parse_list_number()? + dy,
|
||||
x: s.parse_list_number()? + dx,
|
||||
y: s.parse_list_number()? + dy,
|
||||
},
|
||||
b't' => PathSegment::Quadratic {
|
||||
cpx: state.quad.0,
|
||||
cpy: state.quad.1,
|
||||
x: s.parse_list_number()? + dx,
|
||||
y: s.parse_list_number()? + dy,
|
||||
},
|
||||
b'a' => PathSegment::SvgArc {
|
||||
radius_x: s.parse_list_number()?,
|
||||
radius_y: s.parse_list_number()?,
|
||||
rotation: s.parse_list_number()?,
|
||||
large_arc: s.parse_flag()?,
|
||||
sweep: s.parse_flag()?,
|
||||
x: s.parse_list_number()? + dx,
|
||||
y: s.parse_list_number()? + dy,
|
||||
},
|
||||
b'z' => PathSegment::ClosePath,
|
||||
_ => return Err(crate::svgpath::Error),
|
||||
};
|
||||
|
||||
match seg {
|
||||
PathSegment::MoveTo { x, y } => {
|
||||
state.start = (x, y);
|
||||
state.pos = (x, y);
|
||||
state.quad = (x, y);
|
||||
state.cubic = (x, y);
|
||||
},
|
||||
PathSegment::LineTo { x, y } | PathSegment::SvgArc { x, y, .. } => {
|
||||
state.pos = (x, y);
|
||||
state.quad = (x, y);
|
||||
state.cubic = (x, y);
|
||||
},
|
||||
PathSegment::Bezier {
|
||||
cp2x, cp2y, x, y, ..
|
||||
} => {
|
||||
state.pos = (x, y);
|
||||
state.quad = (x, y);
|
||||
state.cubic = (x * 2.0 - cp2x, y * 2.0 - cp2y);
|
||||
},
|
||||
PathSegment::Quadratic { cpx, cpy, x, y, .. } => {
|
||||
state.pos = (x, y);
|
||||
state.quad = (x * 2.0 - cpx, y * 2.0 - cpy);
|
||||
state.cubic = (x, y);
|
||||
},
|
||||
PathSegment::ClosePath => {
|
||||
state.pos = state.start;
|
||||
state.quad = state.start;
|
||||
state.cubic = state.start;
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
||||
Ok(seg)
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
macro_rules! test {
|
||||
($name:ident, $text:expr, $( $seg:expr ),*) => (
|
||||
#[test]
|
||||
fn $name() {
|
||||
let mut s = PathParser::new($text);
|
||||
$(
|
||||
assert_eq!(s.next().unwrap().unwrap(), $seg);
|
||||
)*
|
||||
|
||||
if let Some(res) = s.next() {
|
||||
assert!(res.is_err());
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
test!(null, "", );
|
||||
test!(not_a_path, "q", );
|
||||
test!(not_a_move_to, "L 20 30", );
|
||||
test!(stop_on_err_1, "M 10 20 L 30 40 L 50",
|
||||
PathSegment::MoveTo { x: 10.0, y: 20.0 },
|
||||
PathSegment::LineTo { x: 30.0, y: 40.0 }
|
||||
);
|
||||
|
||||
test!(move_to_1, "M 10 20", PathSegment::MoveTo { x: 10.0, y: 20.0 });
|
||||
test!(move_to_2, "m 10 20", PathSegment::MoveTo { x: 10.0, y: 20.0 });
|
||||
test!(move_to_3, "M 10 20 30 40 50 60",
|
||||
PathSegment::MoveTo { x: 10.0, y: 20.0 },
|
||||
PathSegment::LineTo { x: 30.0, y: 40.0 },
|
||||
PathSegment::LineTo { x: 50.0, y: 60.0 }
|
||||
);
|
||||
test!(move_to_4, "M 10 20 30 40 50 60 M 70 80 90 100 110 120",
|
||||
PathSegment::MoveTo { x: 10.0, y: 20.0 },
|
||||
PathSegment::LineTo { x: 30.0, y: 40.0 },
|
||||
PathSegment::LineTo { x: 50.0, y: 60.0 },
|
||||
PathSegment::MoveTo { x: 70.0, y: 80.0 },
|
||||
PathSegment::LineTo { x: 90.0, y: 100.0 },
|
||||
PathSegment::LineTo { x: 110.0, y: 120.0 }
|
||||
);
|
||||
|
||||
test!(arc_to_1, "M 10 20 A 5 5 30 1 1 20 20",
|
||||
PathSegment::MoveTo { x: 10.0, y: 20.0 },
|
||||
PathSegment::SvgArc {
|
||||
radius_x: 5.0, radius_y: 5.0,
|
||||
rotation: 30.0,
|
||||
large_arc: true, sweep: true,
|
||||
x: 20.0, y: 20.0
|
||||
}
|
||||
);
|
||||
|
||||
test!(arc_to_2, "M 10 20 a 5 5 30 0 0 20 20",
|
||||
PathSegment::MoveTo { x: 10.0, y: 20.0 },
|
||||
PathSegment::SvgArc {
|
||||
radius_x: 5.0, radius_y: 5.0,
|
||||
rotation: 30.0,
|
||||
large_arc: false, sweep: false,
|
||||
x: 30.0, y: 40.0
|
||||
}
|
||||
);
|
||||
|
||||
test!(arc_to_10, "M10-20A5.5.3-4 010-.1",
|
||||
PathSegment::MoveTo { x: 10.0, y: -20.0 },
|
||||
PathSegment::SvgArc {
|
||||
radius_x: 5.5, radius_y: 0.3,
|
||||
rotation: -4.0,
|
||||
large_arc: false, sweep: true,
|
||||
x: 0.0, y: -0.1
|
||||
}
|
||||
);
|
||||
|
||||
test!(separator_1, "M 10 20 L 5 15 C 10 20 30 40 50 60",
|
||||
PathSegment::MoveTo { x: 10.0, y: 20.0 },
|
||||
PathSegment::LineTo { x: 5.0, y: 15.0 },
|
||||
PathSegment::Bezier {
|
||||
cp1x: 10.0, cp1y: 20.0,
|
||||
cp2x: 30.0, cp2y: 40.0,
|
||||
x: 50.0, y: 60.0,
|
||||
}
|
||||
);
|
||||
|
||||
test!(separator_2, "M 10, 20 L 5, 15 C 10, 20 30, 40 50, 60",
|
||||
PathSegment::MoveTo { x: 10.0, y: 20.0 },
|
||||
PathSegment::LineTo { x: 5.0, y: 15.0 },
|
||||
PathSegment::Bezier {
|
||||
cp1x: 10.0, cp1y: 20.0,
|
||||
cp2x: 30.0, cp2y: 40.0,
|
||||
x: 50.0, y: 60.0,
|
||||
}
|
||||
);
|
||||
|
||||
test!(separator_3, "M 10,20 L 5,15 C 10,20 30,40 50,60",
|
||||
PathSegment::MoveTo { x: 10.0, y: 20.0 },
|
||||
PathSegment::LineTo { x: 5.0, y: 15.0 },
|
||||
PathSegment::Bezier {
|
||||
cp1x: 10.0, cp1y: 20.0,
|
||||
cp2x: 30.0, cp2y: 40.0,
|
||||
x: 50.0, y: 60.0,
|
||||
}
|
||||
);
|
||||
|
||||
test!(separator_4, "M10, 20 L5, 15 C10, 20 30 40 50 60",
|
||||
PathSegment::MoveTo { x: 10.0, y: 20.0 },
|
||||
PathSegment::LineTo { x: 5.0, y: 15.0 },
|
||||
PathSegment::Bezier {
|
||||
cp1x: 10.0, cp1y: 20.0,
|
||||
cp2x: 30.0, cp2y: 40.0,
|
||||
x: 50.0, y: 60.0,
|
||||
}
|
||||
);
|
||||
|
||||
test!(separator_5, "M10 20V30H40V50H60Z",
|
||||
PathSegment::MoveTo { x: 10.0, y: 20.0 },
|
||||
PathSegment::LineTo { x: 10.0, y: 30.0 },
|
||||
PathSegment::LineTo { x: 40.0, y: 30.0 },
|
||||
PathSegment::LineTo { x: 40.0, y: 50.0 },
|
||||
PathSegment::LineTo { x: 60.0, y: 50.0 },
|
||||
PathSegment::ClosePath
|
||||
);
|
||||
|
||||
test!(all_segments_1, "M 10 20 L 30 40 H 50 V 60 C 70 80 90 100 110 120 S 130 140 150 160
|
||||
Q 170 180 190 200 T 210 220 A 50 50 30 1 1 230 240 Z",
|
||||
PathSegment::MoveTo { x: 10.0, y: 20.0 },
|
||||
PathSegment::LineTo { x: 30.0, y: 40.0 },
|
||||
PathSegment::LineTo { x: 50.0, y: 40.0 },
|
||||
PathSegment::LineTo { x: 50.0, y: 60.0 },
|
||||
PathSegment::Bezier {
|
||||
cp1x: 70.0, cp1y: 80.0,
|
||||
cp2x: 90.0, cp2y: 100.0,
|
||||
x: 110.0, y: 120.0,
|
||||
},
|
||||
PathSegment::Bezier {
|
||||
cp1x: 130.0, cp1y: 140.0,
|
||||
cp2x: 130.0, cp2y: 140.0,
|
||||
x: 150.0, y: 160.0,
|
||||
},
|
||||
PathSegment::Quadratic {
|
||||
cpx: 170.0, cpy: 180.0,
|
||||
x: 190.0, y: 200.0,
|
||||
},
|
||||
PathSegment::Quadratic {
|
||||
cpx: 210.0, cpy: 220.0,
|
||||
x: 210.0, y: 220.0,
|
||||
},
|
||||
PathSegment::SvgArc {
|
||||
radius_x: 50.0, radius_y: 50.0,
|
||||
rotation: 30.0,
|
||||
large_arc: true, sweep: true,
|
||||
x: 230.0, y: 240.0
|
||||
},
|
||||
PathSegment::ClosePath
|
||||
);
|
||||
|
||||
test!(all_segments_2, "m 10 20 l 30 40 h 50 v 60 c 70 80 90 100 110 120 s 130 140 150 160
|
||||
q 170 180 190 200 t 210 220 a 50 50 30 1 1 230 240 z",
|
||||
PathSegment::MoveTo { x: 10.0, y: 20.0 },
|
||||
PathSegment::LineTo { x: 40.0, y: 60.0 },
|
||||
PathSegment::LineTo { x: 90.0, y: 60.0 },
|
||||
PathSegment::LineTo { x: 90.0, y: 120.0 },
|
||||
PathSegment::Bezier {
|
||||
cp1x: 160.0, cp1y: 200.0,
|
||||
cp2x: 180.0, cp2y: 220.0,
|
||||
x: 200.0, y: 240.0,
|
||||
},
|
||||
PathSegment::Bezier {
|
||||
cp1x: 220.0, cp1y: 260.0, //?
|
||||
cp2x: 330.0, cp2y: 380.0,
|
||||
x: 350.0, y: 400.0,
|
||||
},
|
||||
PathSegment::Quadratic {
|
||||
cpx: 520.0, cpy: 580.0,
|
||||
x: 540.0, y: 600.0,
|
||||
},
|
||||
PathSegment::Quadratic {
|
||||
cpx: 560.0, cpy: 620.0, //?
|
||||
x: 750.0, y: 820.0
|
||||
},
|
||||
PathSegment::SvgArc {
|
||||
radius_x: 50.0, radius_y: 50.0,
|
||||
rotation: 30.0,
|
||||
large_arc: true, sweep: true,
|
||||
x: 980.0, y: 1060.0
|
||||
},
|
||||
PathSegment::ClosePath
|
||||
);
|
||||
|
||||
test!(close_path_1, "M10 20 L 30 40 ZM 100 200 L 300 400",
|
||||
PathSegment::MoveTo { x: 10.0, y: 20.0 },
|
||||
PathSegment::LineTo { x: 30.0, y: 40.0 },
|
||||
PathSegment::ClosePath,
|
||||
PathSegment::MoveTo { x: 100.0, y: 200.0 },
|
||||
PathSegment::LineTo { x: 300.0, y: 400.0 }
|
||||
);
|
||||
|
||||
test!(close_path_2, "M10 20 L 30 40 zM 100 200 L 300 400",
|
||||
PathSegment::MoveTo { x: 10.0, y: 20.0 },
|
||||
PathSegment::LineTo { x: 30.0, y: 40.0 },
|
||||
PathSegment::ClosePath,
|
||||
PathSegment::MoveTo { x: 100.0, y: 200.0 },
|
||||
PathSegment::LineTo { x: 300.0, y: 400.0 }
|
||||
);
|
||||
|
||||
test!(close_path_3, "M10 20 L 30 40 Z Z Z",
|
||||
PathSegment::MoveTo { x: 10.0, y: 20.0 },
|
||||
PathSegment::LineTo { x: 30.0, y: 40.0 },
|
||||
PathSegment::ClosePath,
|
||||
PathSegment::ClosePath,
|
||||
PathSegment::ClosePath
|
||||
);
|
||||
|
||||
// first token should be EndOfStream
|
||||
test!(invalid_1, "M\t.", );
|
||||
|
||||
// ClosePath can't be followed by a number
|
||||
test!(invalid_2, "M 0 0 Z 2",
|
||||
PathSegment::MoveTo { x: 0.0, y: 0.0 },
|
||||
PathSegment::ClosePath
|
||||
);
|
||||
|
||||
// ClosePath can be followed by any command
|
||||
test!(invalid_3, "M 0 0 Z H 10",
|
||||
PathSegment::MoveTo { x: 0.0, y: 0.0 },
|
||||
PathSegment::ClosePath,
|
||||
PathSegment::LineTo { x: 10.0, y: 0.0 }
|
||||
);
|
||||
}
|
|
@ -1,162 +0,0 @@
|
|||
// Copyright 2018 the SVG Types Authors
|
||||
// Copyright 2025 the Servo Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
use crate::svgpath::Error;
|
||||
|
||||
/// A streaming text parsing interface.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Stream<'a> {
|
||||
text: &'a str,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Stream<'a> {
|
||||
#[inline]
|
||||
fn from(text: &'a str) -> Self {
|
||||
Stream { text, pos: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Stream<'a> {
|
||||
/// Returns the current position in bytes.
|
||||
#[inline]
|
||||
pub fn pos(&self) -> usize {
|
||||
self.pos
|
||||
}
|
||||
|
||||
/// Sets current position equal to the end.
|
||||
///
|
||||
/// Used to indicate end of parsing on error.
|
||||
#[inline]
|
||||
pub fn jump_to_end(&mut self) {
|
||||
self.pos = self.text.len();
|
||||
}
|
||||
|
||||
/// Checks if the stream is reached the end.
|
||||
///
|
||||
/// Any [`pos()`] value larger than original text length indicates stream end.
|
||||
///
|
||||
/// Accessing stream after reaching end via safe methods will produce
|
||||
/// an error.
|
||||
///
|
||||
/// Accessing stream after reaching end via *_unchecked methods will produce
|
||||
/// a Rust's bound checking error.
|
||||
///
|
||||
/// [`pos()`]: #method.pos
|
||||
#[inline]
|
||||
pub fn at_end(&self) -> bool {
|
||||
self.pos >= self.text.len()
|
||||
}
|
||||
|
||||
/// Returns a byte from a current stream position.
|
||||
#[inline]
|
||||
pub fn curr_byte(&self) -> Result<u8, Error> {
|
||||
if self.at_end() {
|
||||
return Err(Error);
|
||||
}
|
||||
|
||||
Ok(self.curr_byte_unchecked())
|
||||
}
|
||||
|
||||
/// Returns a byte from a current stream position.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - if the current position is after the end of the data
|
||||
#[inline]
|
||||
pub fn curr_byte_unchecked(&self) -> u8 {
|
||||
self.text.as_bytes()[self.pos]
|
||||
}
|
||||
|
||||
/// Checks that current byte is equal to provided.
|
||||
///
|
||||
/// Returns `false` if no bytes left.
|
||||
#[inline]
|
||||
pub fn is_curr_byte_eq(&self, c: u8) -> bool {
|
||||
if !self.at_end() {
|
||||
self.curr_byte_unchecked() == c
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a next byte from a current stream position.
|
||||
#[inline]
|
||||
pub fn next_byte(&self) -> Result<u8, Error> {
|
||||
if self.pos + 1 >= self.text.len() {
|
||||
return Err(Error);
|
||||
}
|
||||
|
||||
Ok(self.text.as_bytes()[self.pos + 1])
|
||||
}
|
||||
|
||||
/// Advances by `n` bytes.
|
||||
#[inline]
|
||||
pub fn advance(&mut self, n: usize) {
|
||||
debug_assert!(self.pos + n <= self.text.len());
|
||||
self.pos += n;
|
||||
}
|
||||
|
||||
/// Skips whitespaces.
|
||||
///
|
||||
/// Accepted values: `' ' \n \r \t`.
|
||||
pub fn skip_spaces(&mut self) {
|
||||
while !self.at_end() && matches!(self.curr_byte_unchecked(), b' ' | b'\t' | b'\n' | b'\r') {
|
||||
self.advance(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes bytes by the predicate.
|
||||
pub fn skip_bytes<F>(&mut self, f: F)
|
||||
where
|
||||
F: Fn(&Stream<'_>, u8) -> bool,
|
||||
{
|
||||
while !self.at_end() {
|
||||
let c = self.curr_byte_unchecked();
|
||||
if f(self, c) {
|
||||
self.advance(1);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Slices data from `pos` to the current position.
|
||||
#[inline]
|
||||
pub fn slice_back(&self, pos: usize) -> &'a str {
|
||||
&self.text[pos..self.pos]
|
||||
}
|
||||
|
||||
/// Skips digits.
|
||||
pub fn skip_digits(&mut self) {
|
||||
self.skip_bytes(|_, c| c.is_ascii_digit());
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn parse_list_separator(&mut self) {
|
||||
if self.is_curr_byte_eq(b',') {
|
||||
self.advance(1);
|
||||
}
|
||||
}
|
||||
|
||||
// By the SVG spec 'large-arc' and 'sweep' must contain only one char
|
||||
// and can be written without any separators, e.g.: 10 20 30 01 10 20.
|
||||
pub(crate) fn parse_flag(&mut self) -> Result<bool, Error> {
|
||||
self.skip_spaces();
|
||||
|
||||
let c = self.curr_byte()?;
|
||||
match c {
|
||||
b'0' | b'1' => {
|
||||
self.advance(1);
|
||||
if self.is_curr_byte_eq(b',') {
|
||||
self.advance(1);
|
||||
}
|
||||
self.skip_spaces();
|
||||
|
||||
Ok(c == b'1')
|
||||
},
|
||||
_ => Err(Error),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue