merman_render/svg/pipeline/
context.rs1use super::builtin::util::{
2 SvgTagScanner, checkpoint_loop, next_svg_quoted_attr_with_checkpoints, start_tag_name,
3};
4use super::preset::SvgPipelinePreset;
5use crate::environment::{RenderSession, RoutedTextMeasurer, TextMeasurementPhase};
6use crate::family::RenderFamilyKind;
7use crate::resources::{
8 RenderResourcePolicy, ResourceLimitCause, ResourceLimitExceeded, ResourceLimitOverride,
9 ResourceLimitPhase,
10};
11use merman_core::OperationPhase;
12
13#[derive(Debug, Clone, Default, PartialEq, Eq)]
14pub struct SvgPostprocessMetadata {
15 family_kind: Option<RenderFamilyKind>,
16 diagram_type: Option<String>,
17 diagram_title: Option<String>,
18 svg_id: Option<String>,
19}
20
21impl SvgPostprocessMetadata {
22 pub fn new() -> Self {
23 Self::default()
24 }
25
26 pub fn from_svg(svg: &str) -> Self {
31 let mut checkpoint = || Ok::<(), std::convert::Infallible>(());
32 match Self::from_svg_with_checkpoints(svg, &mut checkpoint) {
33 Ok(metadata) => metadata,
34 Err(error) => match error {},
35 }
36 }
37
38 pub(crate) fn from_svg_with_execution(
39 svg: &str,
40 execution: SvgPostprocessExecution<'_>,
41 ) -> crate::Result<Self> {
42 execution.checkpoint()?;
43 execution.preflight_svg_byte_count(svg.len())?;
44 Self::from_svg_with_checkpoints(svg, &mut || execution.checkpoint())
45 }
46
47 fn from_svg_with_checkpoints<E>(
48 svg: &str,
49 checkpoint: &mut impl FnMut() -> Result<(), E>,
50 ) -> Result<Self, E> {
51 checkpoint()?;
52 let mut scanner = SvgTagScanner::new(svg);
53 let mut tag_iteration = 0usize;
54 let root_tag = loop {
55 let Some(tag) = scanner.next_with_checkpoints(checkpoint)? else {
56 return Ok(Self::default());
57 };
58 checkpoint_loop(tag_iteration, checkpoint)?;
59 tag_iteration = tag_iteration.saturating_add(1);
60 let Some(root_name) = start_tag_name(tag.raw()) else {
61 continue;
62 };
63 if root_name != "svg" {
64 return Ok(Self::default());
65 }
66 break tag.raw();
67 };
68
69 let mut diagram_type = None;
70 let mut svg_id = None;
71 let mut cursor = 0usize;
72 let mut attribute_iteration = 0usize;
73 while let Some(attribute) =
74 next_svg_quoted_attr_with_checkpoints(root_tag, cursor, checkpoint)?
75 {
76 checkpoint_loop(attribute_iteration, checkpoint)?;
77 attribute_iteration = attribute_iteration.saturating_add(1);
78 let name = &root_tag[attribute.name_start..attribute.name_end];
79 if name.eq_ignore_ascii_case("aria-roledescription") && diagram_type.is_none() {
80 diagram_type = Some(copy_trimmed_with_checkpoints(
81 &root_tag[attribute.value_start..attribute.value_end],
82 checkpoint,
83 )?);
84 } else if name.eq_ignore_ascii_case("id") && svg_id.is_none() {
85 svg_id = Some(copy_trimmed_with_checkpoints(
86 &root_tag[attribute.value_start..attribute.value_end],
87 checkpoint,
88 )?);
89 }
90 cursor = attribute.full_end;
91 }
92 checkpoint()?;
93
94 Ok(Self {
95 diagram_type,
96 svg_id,
97 ..Self::default()
98 })
99 }
100
101 pub(crate) fn with_family_kind(mut self, family_kind: RenderFamilyKind) -> Self {
103 self.family_kind = Some(family_kind);
104 self
105 }
106
107 pub fn with_diagram_type(mut self, diagram_type: impl Into<String>) -> Self {
109 self.diagram_type = Some(diagram_type.into());
110 self
111 }
112
113 pub fn with_optional_diagram_type(mut self, diagram_type: Option<impl Into<String>>) -> Self {
114 self.diagram_type = diagram_type.map(Into::into);
115 self
116 }
117
118 pub fn with_diagram_title(mut self, diagram_title: impl Into<String>) -> Self {
119 self.diagram_title = Some(diagram_title.into());
120 self
121 }
122
123 pub fn with_optional_diagram_title(mut self, diagram_title: Option<impl Into<String>>) -> Self {
124 self.diagram_title = diagram_title.map(Into::into);
125 self
126 }
127
128 pub fn with_svg_id(mut self, svg_id: impl Into<String>) -> Self {
129 self.svg_id = Some(svg_id.into());
130 self
131 }
132
133 pub fn with_optional_svg_id(mut self, svg_id: Option<impl Into<String>>) -> Self {
134 if let Some(svg_id) = svg_id {
135 self.svg_id = Some(svg_id.into());
136 }
137 self
138 }
139
140 pub fn diagram_type(&self) -> Option<&str> {
141 self.diagram_type.as_deref()
142 }
143
144 pub fn family_kind(&self) -> Option<RenderFamilyKind> {
145 self.family_kind
146 }
147
148 pub fn diagram_title(&self) -> Option<&str> {
149 self.diagram_title.as_deref()
150 }
151
152 pub fn svg_id(&self) -> Option<&str> {
153 self.svg_id.as_deref()
154 }
155}
156
157fn copy_trimmed_with_checkpoints<E>(
158 value: &str,
159 checkpoint: &mut impl FnMut() -> Result<(), E>,
160) -> Result<String, E> {
161 let value = value.trim();
162 checkpoint()?;
163 let mut copied = String::with_capacity(value.len());
164 for (index, character) in value.chars().enumerate() {
165 checkpoint_loop(index, checkpoint)?;
166 copied.push(character);
167 }
168 checkpoint()?;
169 Ok(copied)
170}
171
172#[derive(Clone, Copy)]
178pub(crate) struct SvgPostprocessExecution<'a> {
179 session: &'a RenderSession,
180}
181
182impl<'a> SvgPostprocessExecution<'a> {
183 pub(crate) const fn new(session: &'a RenderSession) -> Self {
184 Self { session }
185 }
186
187 pub(crate) fn checkpoint(self) -> crate::Result<()> {
188 self.session.checkpoint(OperationPhase::Postprocess)
189 }
190
191 pub(crate) const fn resource_policy(self) -> RenderResourcePolicy {
192 self.session.resource_policy()
193 }
194
195 pub(crate) fn preflight_svg_byte_count(self, actual: usize) -> crate::Result<()> {
196 self.session
197 .work_meter()
198 .preflight_svg_byte_count(
199 actual,
200 ResourceLimitPhase::SvgPostprocess,
201 OperationPhase::Postprocess,
202 )
203 .map_err(Into::into)
204 }
205
206 pub(crate) fn preflight_svg_structure(
207 self,
208 elements: usize,
209 tree_depth: usize,
210 ) -> crate::Result<()> {
211 self.session
212 .work_meter()
213 .preflight_svg_structure(elements, tree_depth, OperationPhase::Postprocess)
214 .map_err(Into::into)
215 }
216
217 pub(crate) fn selector_index_limit(self, actual: usize, maximum: usize) -> crate::Error {
218 let policy = self.resource_policy();
219 ResourceLimitExceeded {
220 cause: ResourceLimitCause::Ceiling,
221 phase: ResourceLimitPhase::SvgPostprocess,
222 limit: "svg_fallback_selector_index",
223 actual,
224 max: maximum,
225 profile: policy.profile(),
226 explicit_overrides: policy
227 .explicit_overrides()
228 .map(|(id, value)| ResourceLimitOverride { id, value })
229 .collect(),
230 }
231 .into()
232 }
233
234 pub(crate) fn svg_byte_count_overflow(self) -> crate::Error {
235 self.session
236 .work_meter()
237 .terminate_svg_byte_count_overflow(
238 ResourceLimitPhase::SvgPostprocess,
239 OperationPhase::Postprocess,
240 )
241 .into()
242 }
243
244 pub(crate) fn terminate_resource_error(self, error: ResourceLimitExceeded) -> crate::Error {
245 self.session
246 .work_meter()
247 .terminate_absolute_resource_error(error, OperationPhase::Postprocess)
248 .into()
249 }
250
251 pub(crate) fn controlled_text_measurer(
252 self,
253 phase: TextMeasurementPhase,
254 ) -> RoutedTextMeasurer<'a> {
255 self.session
256 .controlled_text_measurer(phase, OperationPhase::Postprocess)
257 }
258}
259
260#[derive(Clone, Copy)]
261pub struct SvgPostprocessContext<'a> {
262 preset: SvgPipelinePreset,
263 pass_index: usize,
264 pass_name: &'a str,
265 metadata: &'a SvgPostprocessMetadata,
266 session: &'a RenderSession,
267}
268
269impl std::fmt::Debug for SvgPostprocessContext<'_> {
270 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271 f.debug_struct("SvgPostprocessContext")
272 .field("preset", &self.preset)
273 .field("pass_index", &self.pass_index)
274 .field("pass_name", &self.pass_name)
275 .field("metadata", &self.metadata)
276 .finish_non_exhaustive()
277 }
278}
279
280impl<'a> SvgPostprocessContext<'a> {
281 pub(crate) fn new(
282 preset: SvgPipelinePreset,
283 pass_index: usize,
284 pass_name: &'a str,
285 metadata: &'a SvgPostprocessMetadata,
286 session: &'a RenderSession,
287 ) -> Self {
288 Self {
289 preset,
290 pass_index,
291 pass_name,
292 metadata,
293 session,
294 }
295 }
296
297 pub fn preset(&self) -> SvgPipelinePreset {
298 self.preset
299 }
300
301 pub fn pass_index(&self) -> usize {
302 self.pass_index
303 }
304
305 pub fn pass_name(&self) -> &'a str {
306 self.pass_name
307 }
308
309 pub fn diagram_type(&self) -> Option<&'a str> {
310 self.metadata.diagram_type()
311 }
312
313 pub fn family_kind(&self) -> Option<RenderFamilyKind> {
314 self.metadata.family_kind()
315 }
316
317 pub fn diagram_title(&self) -> Option<&'a str> {
318 self.metadata.diagram_title()
319 }
320
321 pub fn svg_id(&self) -> Option<&'a str> {
322 self.metadata.svg_id()
323 }
324
325 pub fn text_measurer(&self, phase: TextMeasurementPhase) -> RoutedTextMeasurer<'a> {
326 self.session.text_measurer(phase)
327 }
328
329 pub(crate) fn controlled_text_measurer(
330 &self,
331 phase: TextMeasurementPhase,
332 ) -> RoutedTextMeasurer<'a> {
333 self.session
334 .controlled_text_measurer(phase, OperationPhase::Postprocess)
335 }
336
337 pub(crate) fn checkpoint(&self) -> crate::Result<()> {
339 self.execution().checkpoint()
340 }
341
342 pub(crate) const fn execution(&self) -> SvgPostprocessExecution<'a> {
343 SvgPostprocessExecution::new(self.session)
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350
351 #[test]
352 fn metadata_extracts_root_svg_id_and_diagram_type() {
353 let metadata = SvgPostprocessMetadata::from_svg(
354 r#"<svg xmlns="http://www.w3.org/2000/svg" id="diagram-1" aria-roledescription="quadrantChart"><g/></svg>"#,
355 );
356
357 assert_eq!(metadata.svg_id(), Some("diagram-1"));
358 assert_eq!(metadata.diagram_type(), Some("quadrantChart"));
359 assert_eq!(metadata.family_kind(), None);
360 }
361
362 #[test]
363 fn metadata_ignores_non_root_ids() {
364 let metadata = SvgPostprocessMetadata::from_svg(
365 r#"<g id="nested" aria-roledescription="quadrantChart"></g>"#,
366 );
367
368 assert_eq!(metadata.svg_id(), None);
369 assert_eq!(metadata.diagram_type(), None);
370 assert_eq!(metadata.family_kind(), None);
371 }
372
373 #[test]
374 fn metadata_rejects_comment_spoofs_and_nested_svg_roles() {
375 for svg in [
376 r#"<!-- <svg id="spoof" aria-roledescription="quadrantChart"> --><g/>"#,
377 r#"<g><svg id="nested" aria-roledescription="quadrantChart"/></g>"#,
378 ] {
379 let metadata = SvgPostprocessMetadata::from_svg(svg);
380
381 assert_eq!(metadata.svg_id(), None, "{svg}");
382 assert_eq!(metadata.diagram_type(), None, "{svg}");
383 assert_eq!(metadata.family_kind(), None, "{svg}");
384 }
385 }
386
387 #[test]
388 fn metadata_accepts_a_root_svg_after_xml_prolog_and_comment() {
389 let metadata = SvgPostprocessMetadata::from_svg(
390 r#"<?xml version="1.0"?><!-- generated --><svg id="root" aria-roledescription="quadrantChart"/>"#,
391 );
392
393 assert_eq!(metadata.svg_id(), Some("root"));
394 assert_eq!(metadata.diagram_type(), Some("quadrantChart"));
395 assert_eq!(metadata.family_kind(), None);
396 }
397
398 #[test]
399 fn diagram_type_string_does_not_forge_typed_family_context() {
400 let metadata = SvgPostprocessMetadata::new().with_diagram_type("quadrantChart");
401
402 assert_eq!(metadata.diagram_type(), Some("quadrantChart"));
403 assert_eq!(metadata.family_kind(), None);
404 }
405}