[librsvg: 1/9] Move adding_a_property.rst to the devel docs book




commit 19b107597dd966cf269a67441b44cfbea45ceaf2
Author: Federico Mena Quintero <federico gnome org>
Date:   Wed Aug 24 17:19:53 2022 -0500

    Move adding_a_property.rst to the devel docs book
    
    Part-of: <https://gitlab.gnome.org/GNOME/librsvg/-/merge_requests/735>

 devel-docs/README.md             |  11 -
 devel-docs/adding-a-property.md  | 587 ---------------------------------
 devel-docs/adding_a_property.rst | 679 +++++++++++++++++++++++++++++++++++++++
 devel-docs/index.rst             |   2 +
 4 files changed, 681 insertions(+), 598 deletions(-)
---
diff --git a/devel-docs/adding_a_property.rst b/devel-docs/adding_a_property.rst
new file mode 100644
index 000000000..b8ade9855
--- /dev/null
+++ b/devel-docs/adding_a_property.rst
@@ -0,0 +1,679 @@
+Adding a new CSS property to librsvg
+====================================
+
+This document is a little tour on how to add support for a CSS property
+to librsvg. We will implement the ```mask-type``
+property <https://www.w3.org/TR/css-masking-1/#the-mask-type>`__ from
+the **CSS Masking Module Level 1** specification.
+
+What is ``mask-type``?
+----------------------
+
+`The spec says about
+``mask-type`` <https://www.w3.org/TR/css-masking-1/#the-mask-type>`__:
+
+   The mask-type property defines whether the content of the mask
+   element is treated as as luminance mask or alpha mask, as described
+   in Calculating mask values.
+
+A **luminance mask** takes the RGB values of each pixel, converts them
+to a single luminance value, and uses that as a mask.
+
+An **alpha mask** just takes the alpha value of each pixel and uses it
+as a mask.
+
+The only mask type that SVG1.1 supported was luminance masks; there
+wasn’t even a ``mask-type`` property back then. The SVG2 spec removed
+descriptions of masking, and offloaded them to the `CSS Masking Module
+Level 1 <https://www.w3.org/TR/css-masking-1/>`__ specification, which
+it adds the ``mask-type`` property and others as well.
+
+Let’s start by figuring out how to read the spec.
+
+What the specification says
+---------------------------
+
+The specification for ``mask-type`` is in
+https://www.w3.org/TR/css-masking-1/#the-mask-type
+
+In the specs, most of the descriptions for properties start with a table
+that summarizes the property. For example, if you visit that link, you
+will find a table that starts with these items:
+
+-  **Name:** ``mask-type``
+-  **Value:** ``luminance | alpha``
+-  **Initial:** ``luminance``
+-  **Applies to:** mask elements
+-  **Inherited:** no
+-  **Computed value:** as specified
+
+Let’s go through each of these:
+
+**Name:** We have the name of the property (``mask-type``). Properties
+are case-insensitive, and librsvg already has machinery to handle that.
+
+**Value:** The possible values for the property can be ``luminance`` or
+``alpha``. In the spec’s web page, even the little ``|`` between those
+two values is a hyperlink; clicking it will take you to the
+specification for CSS Values and Units, where it describes the grammar
+that the CSS specs use to describe their values. Here you just need to
+know that ``|`` means that exactly one of the two alternatives must
+occur.
+
+As you may imagine, librsvg already parses a lot of similar properties
+that are just symbolic values. For example, the ``stroke-linecap``
+property can have values ``butt | round | square``. We’ll see how to
+write a parser for this kind of property with a minimal amount of code.
+
+**Initial:** Then there is the initial or default value, which is
+``luminance``. This means that if the ``mask-type`` property is not
+specified on an element, it takes ``luminance`` as its default. This is
+a sensible choice, since an SVG1.1 file that is processed by SVG2
+software should retain the same semantics. It also means that if there
+is a parse error, for example if you typed ``ahlpha``, the property will
+silently revert back to the default ``luminance`` value.
+
+**Applies to:** Librsvg doesn’t pay much attention to “applies to” — it
+just carries property values for all elements, and the elements that
+don’t handle a property just ignore it.
+
+**Inherited:** This property is not inherited, which means that by
+default, its value does not cascade. So if you have this:
+
+.. code:: xml
+
+   <mask style="mask-type: alpha;">
+     <other>
+       <elements>
+         <here/>
+       </elements>
+     </other>
+   </mask>
+
+Then the ``other``, ``elements``, ``here`` will not inherit the
+``mask-type`` value from their ancestor.
+
+**Computed value:** Finally, the computed value is “as specified”, which
+means that librsvg does not need to modify it in any way when resolving
+the CSS cascade. Other properties, like ``width: 1em;`` may need to be
+resolved against the ``font-size`` to obtain the computed value.
+
+The W3C specifications can get pretty verbose and it takes some practice
+to read them, but fortunately this property is short and sweet.
+
+Let’s go on.
+
+How librsvg represents properties
+---------------------------------
+
+Each property has a Rust type that can hold its values. Remember the
+part of the masking spec from above, that says the ``mask-type``
+property can have values ``luminance`` or ``alpha``, and the
+initial/default is ``luminance``? This translates easily to Rust types:
+
+.. code:: rust
+
+   #[derive(Debug, Copy, Clone, PartialEq)]
+   pub enum MaskType {
+       Luminance,
+       Alpha,
+   }
+
+   impl Default for MaskType {
+       fn default() -> MaskType {
+           MaskType::Luminance
+       }
+   }
+
+Additionally, we need to be able to say that the property does not
+inherit by default, and that its computed value is the same as the
+specified value (e.g. we can just copy the original value without
+changing it). Librsvg defines a ``Property`` trait for those actions:
+
+.. code:: rust
+
+   pub trait Property {
+       fn inherits_automatically() -> bool;
+
+       fn compute(&self, _: &ComputedValues) -> Self;
+   }
+
+For the ``mask-type`` property, we want ``inherits_automatically`` to
+return ``false``, and ``compute`` to return the value unchanged. So,
+like this:
+
+.. code:: rust
+
+   impl Property for MaskType {
+       fn inherits_automatically() -> bool {
+           false
+       }
+
+       fn compute(&self, _: &ComputedValues) -> Self {
+           self.clone()
+       }
+   }
+
+Ignore the ``ComputedValues`` argument for now — it is how librsvg
+represents an element’s complete set of property values.
+
+As you can imagine, there are a lot of properties like ``mask-type``,
+whose values are just symbolic names that map well to a data-less enum.
+For all of them, it would be a lot of repetitive code to define their
+default value, return whether they inherit or not, and clone them for
+the computed value. Additionally, we have not even written the parser
+for this property’s values yet.
+
+Fortunately, librsvg has a ``make_property!`` macro that lets you do
+this instead:
+
+.. code:: rust
+
+   make_property!(
+       /// `mask-type` property.                                          // (1)
+       ///
+       /// https://www.w3.org/TR/css-masking-1/#the-mask-type
+       MaskType,                                                          // (2)
+       default: Luminance,                                                // (3)
+       inherits_automatically: false,                                     // (4)
+
+       identifiers:                                                       // (5)
+       "luminance" => Luminance,
+       "alpha" => Alpha,
+   );
+
+-  
+
+   (1) is a documentation comment for the ``MaskType`` enum being
+       defined.
+
+-  
+
+   (2) is ``MaskType``, the name we will use for the ``mask-type``
+       property.
+
+-  
+
+   (3) indicates the “initial value”, or default, for the property.
+
+-  
+
+   (4) … whether the spec says the property should inherit or not.
+
+-  
+
+   (5) Finally, ``identifiers:`` is what makes the ``make_property!``
+       macro know that it should generate a parser for the symbolic
+       names ``luminance`` and ``alpha``, and that they should
+       correspond to the values ``MaskType::Luminance`` and
+       ``MaskType::Alpha``, respectively.
+
+This saves a lot of typing! Also, it makes it easier to gradually change
+the way properties are represented, as librsvg evolves.
+
+Properties that use the same data type
+--------------------------------------
+
+Consider the ``stroke`` and ``fill`` properties; both store a
+```<paint>`` <https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint>`__
+value, which librsvg represents with a type called ``PaintServer``. The
+``make_property!`` macro has a case for properties like that, so in the
+librsvg source code you will find both of thsese:
+
+.. code:: rust
+
+   make_property!(
+       /// `fill` property.
+       ///
+       /// https://www.w3.org/TR/SVG/painting.html#FillProperty
+       ///
+       /// https://www.w3.org/TR/SVG2/painting.html#FillProperty
+       Fill,
+       default: PaintServer::parse_str("#000").unwrap(),
+       inherits_automatically: true,
+       newtype_parse: PaintServer,
+   );
+
+   make_property!(
+       /// `stroke` property.
+       ///
+       /// https://www.w3.org/TR/SVG2/painting.html#SpecifyingStrokePaint
+       Stroke,
+       default: PaintServer::None,
+       inherits_automatically: true,
+       newtype_parse: PaintServer,
+   );
+
+The ``newtype_parse:`` is what tells the macro that it should generate a
+newtype like ``struct Stroke(PaintServer)``, and that it should just use
+the parser that ``PaintServer`` already has.
+
+Which parser is that? Read on.
+
+Custom parsers
+--------------
+
+Librsvg has a ``Parse`` trait for property values which looks rather
+scary:
+
+.. code:: rust
+
+   pub trait Parse: Sized {
+       fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>>;
+   }
+
+Don’t let the lifetimes scare you. They are required because of
+``cssparser::Parser``, from the ``cssparser`` crate, tries really hard
+to let you implement zero-copy parsers, which give you string tokens as
+slices from the original string being parsed, instead of allocating lots
+of little ``String`` values. What this ``Parse`` trait means is, you get
+tokens out of the ``Parser``, and return what is basically a
+``Result<Self, Error>``.
+
+In this tutorial we will just show you the parser for simple numeric
+types, for example, for properties that can just be represented with an
+``f64``. There is the ``stroke-miterlimit`` property defined like this:
+
+.. code:: rust
+
+   make_property!(
+       /// `stroke-miterlimit` property.
+       ///
+       /// https://www.w3.org/TR/SVG2/painting.html#StrokeMiterlimitProperty
+       StrokeMiterlimit,
+       default: 4f64,
+       inherits_automatically: true,
+       newtype_parse: f64,
+   );
+
+And the ``impl Parse for f64`` looks like this:
+
+.. code:: rust
+
+   impl Parse for f64 {
+       fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
+           let loc = parser.current_source_location();                                          // (1)
+           let n = parser.expect_number()?;                                                     // (2)
+           if n.is_finite() {                                                                   // (3)
+               Ok(f64::from(n))                                                                 // (4)
+           } else {
+               Err(loc.new_custom_error(ValueErrorKind::value_error("expected finite number"))) // (5)
+           }
+       }
+   }
+
+-  
+
+   (1) Store the current location in the parser.
+
+-  
+
+   (2) Ask the parser for a number. If a non-numeric token comes out
+       (e.g. if the user put ``stroke-miterlimit: foo`` instead of
+       ``stroke-miterlimit: 5``), ``expect_number`` will return an
+       ``Err``, which we propagate upwards with the ``?``.
+
+-  
+
+   (3) Check the number for being non-infinite or NaN….
+
+-  
+
+   (4) … and return the number converted to f64 (``cssparser`` returns
+       f32, but we promote them so that subsequent calculations can use
+       the extra precision)…
+
+-  
+
+   (5) … or return an error based on the location from (1).
+
+My advice: implement new parsers by doing cut&paste from existing ones,
+and you’ll be okay.
+
+Registering the property
+------------------------
+
+Okay! We defined ``MaskType`` and its symbolic identifiers with the
+``make_property!`` macro, and the macro took care of writing a parser
+for it and implementing the traits that the property needs.
+
+Now we need to modify the code in a few places to process the property.
+
+Register the property
+---------------------
+
+-  First, look for ``longhands:`` in ``properties.rs``. You will find
+   that it is part of a long macro invocation:
+
+.. code:: rust
+
+   make_properties! {
+       // ... stuff omitted here
+
+       longhands: {
+          // ... stuff omitted here
+
+           "marker-end"                  => (PresentationAttr::Yes, marker_end                  : MarkerEnd),
+           "marker-mid"                  => (PresentationAttr::Yes, marker_mid                  : MarkerMid),
+           "marker-start"                => (PresentationAttr::Yes, marker_start                : 
MarkerStart),
+           "mask"                        => (PresentationAttr::Yes, mask                        : Mask),
+           // "mask-type"                => (PresentationAttr::Yes, unimplemented),
+           "opacity"                     => (PresentationAttr::Yes, opacity                     : Opacity),
+           "overflow"                    => (PresentationAttr::Yes, overflow                    : Overflow),
+
+           // ... stuff omitted here
+       }
+   }
+
+In there, there is an entry for ``mask-type`` commented out. Let’s
+uncomment it and turn it into this:
+
+.. code:: rust
+
+           "mask-type"                   => (PresentationAttr::Yes, mask_type                   : MaskType),
+
+``PresentationAttr::Yes`` indicates whether the property has a
+corresponding presentation attribute. This means that you can do
+``<mask style="mask-type: alpha;">`` which is property, as well as
+``<mask mask-type="alpha">``, which is a presentation attribute.
+
+How did we find out that ``mask-type`` also exists as a presentation
+attribute? Well, `the
+spec <https://www.w3.org/TR/css-masking-1/#the-mask-type>`__ says:
+
+   The mask-type property is a presentation attribute for SVG elements.
+
+But wait! If we compile, we get this:
+
+::
+
+   error: no rules expected the token `"mask-type"`
+      --> src/properties.rs:450:9
+       |
+   450 |         "mask-type"                   => (PresentationAttr::Yes, mask_type                   : 
MaskType),
+       |         ^^^^^^^^^^^ no rules expected this token in macro call
+
+When you see that error in exactly that macro invocation, it means this:
+librsvg uses a crate called ``markup5ever`` to have a compact
+representation of the names of properties/attributes/elements. It uses
+string interning so that, for example, there is a single definition of
+``rect`` in the program’s heap instead of there being a thousands of
+duplicated ``rect`` strings when you load a big document. The thing is,
+``markup5ever`` only has ready-made definitions of the most common
+HTML/SVG/CSS names, but unfortunately ``mask-type`` is not one of them.
+
+So, we scroll down in ``properties.rs`` and move the ``mask-type``
+registration there:
+
+.. code:: rust
+
+       longhands_not_supported_by_markup5ever: {
+           "line-height"                 => (PresentationAttr::No,  line_height                 : 
LineHeight),
+           "mask-type"                   => (PresentationAttr::Yes, mask_type                   : MaskType), 
    // <- right here
+           "mix-blend-mode"              => (PresentationAttr::No,  mix_blend_mode              : 
MixBlendMode),
+           "paint-order"                 => (PresentationAttr::Yes, paint_order                 : 
PaintOrder),
+       }
+
+That block named ``longhands_not_supported_by_markup5ever`` is, well,
+exactly what it says — a separate section with property names that are
+not built into ``markup5ever``, so they must be dealt with specially.
+Just put the property there and that’s it.
+
+Next, we have to calculate the computed value for the property.
+
+Calculate the computed value
+----------------------------
+
+In ``properties.rs``, look for ``compute!``. You will find many
+invocations of this macro:
+
+.. code:: rust
+
+           compute!(MarkerEnd, marker_end);
+           compute!(MarkerMid, marker_mid);
+           compute!(MarkerStart, marker_start);
+           compute!(Mask, mask);
+           compute!(MixBlendMode, mix_blend_mode);
+           compute!(Opacity, opacity);
+           compute!(Overflow, overflow);
+
+Add a call for ``MaskType``:
+
+.. code:: rust
+
+           compute!(MarkerEnd, marker_end);
+           compute!(MarkerMid, marker_mid);
+           compute!(MarkerStart, marker_start);
+           compute!(Mask, mask);
+           compute!(MaskType, mask_type);          // this is new
+           compute!(MixBlendMode, mix_blend_mode);
+           compute!(Opacity, opacity);
+           compute!(Overflow, overflow);
+
+You will see that all those calls to ``compute!`` are inside a method
+called ``SpecifiedValues::to_computed_values()``. This method is run as
+part of the CSS cascade: it takes the ``SpecifiedValues`` from an
+element and composes them onto the ``ComputedValues`` from its parent
+element. For example, if you have a document with this bit:
+
+.. code:: xml
+
+   <g stroke="red" fill="blue">     // ComputedValues with stroke:red, fill:blue
+     <rect fill="green"/>           // SpecifiedValues with fill:green
+   </g>
+
+The ``ComputedValues`` that results from the ``<g>`` will have
+properties ``stroke:red`` and ``fill:blue`` in it. The
+``SpecifiedValues`` from the ``<rect>`` just has ``fill:green``.
+Composing them together for the ``<rect>`` gives us ``ComputedValues``
+with ``stroke:red`` and ``fill:green``.
+
+Now that the property is registered, we can actually handle it in the
+drawing code!
+
+Handling the property
+---------------------
+
+First, a digression: let’s change the name of a few methods to better
+reflect what the new structure of the code will be like.
+
+There are a few methods called ``to_mask`` in the code, that take an
+RGBA surface and turn it into an Alpha-only surface with the luminance
+of the original surface; and also the corresponding method to do this
+for a single pixel. Let’s do this kind of renaming:
+
+::
+
+   -    pub fn to_mask(&self, opacity: UnitInterval) -> Result<SharedImageSurface, cairo::Error> {
+   +    pub fn to_luminance_mask(&self, opacity: UnitInterval) -> Result<SharedImageSurface, cairo::Error> {
+
+Librsvg only effectively supported ``mask-type: luminance`` since that
+is what was in SVG1.1, but now for SVG2 we want to add behavior for
+``mask-type: alpha`` as well. So, it makes sense to rename ``to_mask``
+as ``to_luminance_mask``.
+
+``SharedImageSurface`` is the type that librsvg uses to represent images
+in memory. They can be RGBA or Alpha-only. There is already a method
+called ``extract_alpha`` that we can use to create an Alpha-only mask:
+
+.. code:: rust
+
+   // there's a type alias SharedImageSurface for this
+   impl ImageSurface<Shared> {
+       pub fn extract_alpha(&self, bounds: IRect) -> Result<SharedImageSurface, cairo::Error> { ... }
+   }
+
+Now let’s look at where ``drawing_ctx.rs`` has this:
+
+.. code:: rust
+
+           let mask = SharedImageSurface::wrap(mask_content_surface, SurfaceType::SRgb)?    // (1)
+               .to_luminance_mask()?                                                        // (2)
+               .into_image_surface()?;                                                      // (3)
+
+-  
+
+   (1) Wraps a ``SharedImageSurface`` around the Cairo surface that was
+       just rendered with the mask contents.
+
+-  
+
+   (2) Converts it to a luminance mask. We will need to change this!
+
+-  
+
+   (3) Extracts the Cairo image surface from the ``SharedImageSurface``,
+       for further processing.
+
+Remember the ``ComputedValues`` where we had the ``mask_type``? We can
+extract it with ``values.mask_type()``. Now let’s change the lines above
+to this:
+
+.. code:: rust
+
+           let tmp = SharedImageSurface::wrap(mask_content_surface, SurfaceType::SRgb)?;
+
+           let mask_result = match values.mask_type() {
+               MaskType::Luminance => tmp.to_luminance_mask()?,
+               MaskType::Alpha => tmp.extract_alpha(IRect::from_size(tmp.width(), tmp.height()))?,
+           };
+
+           let mask = mask_result.into_image_surface()?;
+
+But wait! We don’t have a test for this yet! Aaaaaargh, we are doing
+test-driven development backwards!
+
+No biggie. Let’s write the tests.
+
+Adding tests
+------------
+
+Testing graphical output is really annoying if you compare PNG files,
+because any time Cairo changes something and antialiasing changes
+juuuuuust a bit, the tests break. So, librsvg tries to do “reftests”, or
+reference tests, by comparing the rendered results of two things:
+
+-  The SVG you actually want to test.
+-  An equivalent SVG that works only with known-good features.
+
+For ``mask-type``, we need an SVG document that actually uses that
+property with both of its values, and another document that produces the
+same results but with simpler primitives.
+
+Librsvg already has tests for luminance masks, as they were the only
+available kind in SVG1.1. So we can be confident that they already work
+- we just need to test that the presence of ``mask-type="luminance"``
+actually does the same thing.
+
+First, let’s dissect the SVG that we want to test:
+
+.. code:: xml
+
+   <?xml version="1.0" encoding="UTF-8"?>
+   <svg xmlns="http://www.w3.org/2000/svg"; width="200" height="100">
+     <mask id="luminance" mask-type="luminance" maskContentUnits="objectBoundingBox">
+       <rect x="0.1" y="0.1" width="0.8" height="0.8" fill="white"/>
+     </mask>
+     <mask id="alpha" mask-type="alpha" maskContentUnits="objectBoundingBox">
+       <rect x="0.1" y="0.1" width="0.8" height="0.8" fill="black"/>
+     </mask>
+
+     <rect x="0" y="0" width="100" height="100" fill="green" mask="url(#luminance)"/>
+
+     <rect x="100" y="0" width="100" height="100" fill="green" mask="url(#alpha)"/>
+   </svg>
+
+The image has two 100x100 ``green`` squares side by side. The one on the
+left gets masked with the ``luminance`` mask, which reduces it to an
+80x80 rectangle. That mask is a **white** square, so its has full
+luminance at every pixel.
+
+The square on the right gets masked with the ``alpha`` mask. That mask
+is a **black** square, but with alpha=1.0, so it should produce the same
+result as the first one.
+
+Note that to make things easy, we use **white** for the luminance mask.
+White pixels have full luminance (1.0), which gets used as the mask.
+Conversely, we use **black** for the alpha mask. Those black pixels are
+fully opaque, and since ``mask-type="alpha"`` only considers the alpha
+channel, it will be using the full opacity of each pixel (1.0), which
+also gets used as the mask. So, the masks should be equivalent.
+
+Okay! Now let’s write the reference SVG, the one built out of simpler
+elements but that should produce the same rendering:
+
+.. code:: xml
+
+   <?xml version="1.0" encoding="UTF-8"?>
+   <svg xmlns="http://www.w3.org/2000/svg"; width="200" height="100">
+     <rect x="10" y="10" width="80" height="80" fill="green"/>
+
+     <rect x="110" y="10" width="80" height="80" fill="green"/>
+   </svg>
+
+This is just the two original squares, but already clipped or masked to
+the final result.
+
+Now, where do we put those SVG documents for the tests?
+
+Near the end of ``tests/src/filters.rs`` we can include this:
+
+.. code:: rust
+
+   test_compare_render_output!(
+       mask_type,
+       200,
+       100,
+       br##"<?xml version="1.0" encoding="UTF-8"?>
+   <svg xmlns="http://www.w3.org/2000/svg"; width="200" height="100">
+     <mask id="luminance" mask-type="luminance" maskContentUnits="objectBoundingBox">
+       <rect x="0.1" y="0.1" width="0.8" height="0.8" fill="white"/>
+     </mask>
+     <mask id="alpha" mask-type="alpha" maskContentUnits="objectBoundingBox">
+       <rect x="0.1" y="0.1" width="0.8" height="0.8" fill="black"/>
+     </mask>
+
+     <rect x="0" y="0" width="100" height="100" fill="green" mask="url(#luminance)"/>
+
+     <rect x="100" y="0" width="100" height="100" fill="green" mask="url(#alpha)"/>
+   </svg>
+   "##,
+       br##"<?xml version="1.0" encoding="UTF-8"?>
+   <svg xmlns="http://www.w3.org/2000/svg"; width="200" height="100">
+     <rect x="10" y="10" width="80" height="80" fill="green"/>
+
+     <rect x="110" y="10" width="80" height="80" fill="green"/>
+   </svg>
+   "##,
+   );
+
+Here, ``test_compare_render_output!`` is a macro that takes two SVG
+documents, the test and the reference, and compares their rendered
+results. It also takes a test name (``mask_type`` in this case), and the
+pixel size of the image to generate for testing (200x100).
+
+Final steps: documentation
+--------------------------
+
+To help people who are wondering what SVG features are supported in
+librsvg, there is a ``FEATURES.md`` file. It has a section called “CSS
+properties” with a big list of property names and notes about them.
+
+We’ll patch it like this:
+
+::
+
+    | marker-mid                  |                                                        |
+    | marker-start                |                                                        |
+    | mask                        |                                                        |
+   +| mask-type                   |                                                        |
+    | mix-blend-mode              | Not available as a presentation attribute.             |
+    | opacity                     |                                                        |
+    | overflow                    |                                                        |
+
+There is nothing remarkable about ``mask-type``, it is a plain old
+property that also has a presentation attribute (remember the
+``PresentationAttr::Yes`` from above?), so we don’t need to list any
+extra information.
+
+And with that, we are done implementing ``mask-type``. Have fun!
diff --git a/devel-docs/index.rst b/devel-docs/index.rst
index 3d8f28a95..b053fcce8 100644
--- a/devel-docs/index.rst
+++ b/devel-docs/index.rst
@@ -5,6 +5,7 @@ Development guide for librsvg
    product
    roadmap
    devel_environment
+   adding_a_property
    memory_leaks
    contributing
    ci
@@ -54,6 +55,7 @@ Test suite - move tests/readme here?
 
 Link to the internals documentation.
 
+- :doc:`adding_a_property`
 - :doc:`memory_leaks`
 
 Design documents


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]