├── .gitignore
├── LICENSE
├── README.md
├── backend
├── document.go
├── graphics.go
└── text.go
├── css
├── counters
│ └── counters.go
├── parser
│ ├── colors.go
│ ├── css-parsing-tests
│ │ ├── An+B.json
│ │ ├── LICENSE
│ │ ├── README.rst
│ │ ├── blocks_contents.json
│ │ ├── color3.json
│ │ ├── color3_hsl.json
│ │ ├── color3_keywords.json
│ │ ├── component_value_list.json
│ │ ├── declaration_list.json
│ │ ├── make_color3_hsl.py
│ │ ├── make_color3_keywords.py
│ │ ├── one_component_value.json
│ │ ├── one_declaration.json
│ │ ├── one_rule.json
│ │ ├── rule_list.json
│ │ ├── stylesheet.json
│ │ └── stylesheet_bytes.json
│ ├── nth.go
│ ├── parser.go
│ ├── parser_test.go
│ ├── serialize.go
│ ├── serialize_test.go
│ ├── tokenizer.go
│ └── tokenizer_test.go
├── properties
│ ├── datas.go
│ ├── float.go
│ ├── gen
│ │ ├── gen.go
│ │ └── gen_test.go
│ ├── keywords
│ │ ├── keywords.go
│ │ └── keywords_test.go
│ ├── main.go
│ ├── properties.go
│ ├── props_gen.go
│ ├── types.go
│ └── utils.go
├── selector
│ ├── README.md
│ ├── benchmark_test.go
│ ├── parser.go
│ ├── parser_test.go
│ ├── pseudo_classes.go
│ ├── selector.go
│ ├── selector_test.go
│ ├── serialize.go
│ ├── serialize_test.go
│ ├── specificity.go
│ ├── specificity_test.go
│ ├── test_resources
│ │ ├── content.xhtml
│ │ ├── invalid_selectors.json
│ │ ├── shakespeare.html
│ │ └── valid_selectors.json
│ └── w3_test.go
└── validation
│ ├── descriptors.go
│ ├── descriptors_test.go
│ ├── expanders.go
│ ├── expanders_test.go
│ ├── utils.go
│ ├── validation.go
│ └── validation_test.go
├── go.mod
├── go.sum
├── html
├── boxes
│ ├── boxes.go
│ ├── boxes_test.go
│ ├── boxes_tree.go
│ ├── build.go
│ ├── counters_test.go
│ ├── html.go
│ ├── iters.go
│ └── stubs.go
├── document
│ ├── document.go
│ ├── document_test.go
│ ├── draw.go
│ ├── draw_test.go
│ ├── stacking.go
│ ├── stacking_test.go
│ └── testdata
│ │ ├── fiche_sanitaire.html
│ │ ├── fiche_sanitaire_1.html
│ │ └── go1.17.html
├── layout
│ ├── absolute.go
│ ├── backgrounds.go
│ ├── blocks.go
│ ├── blocks_test.go
│ ├── columns.go
│ ├── columns_test.go
│ ├── flex.go
│ ├── flex_test.go
│ ├── float.go
│ ├── float_test.go
│ ├── grid.go
│ ├── grid_test.go
│ ├── inline.go
│ ├── inline_test.go
│ ├── layout.go
│ ├── layout_counters_test.go
│ ├── layout_css_test.go
│ ├── layout_css_variables_test.go
│ ├── layout_fonts_test.go
│ ├── layout_footnotes_test.go
│ ├── layout_images_test.go
│ ├── layout_inline_block_test.go
│ ├── layout_list_test.go
│ ├── layout_position_test.go
│ ├── layout_presentational_hints_test.go
│ ├── layout_shrink_to_fit_test.go
│ ├── layout_target_test.go
│ ├── layout_test.go
│ ├── layout_text_test.go
│ ├── leader.go
│ ├── min_max.go
│ ├── pages.go
│ ├── pages_test.go
│ ├── percentages.go
│ ├── preferred.go
│ ├── replaced.go
│ ├── tables.go
│ └── tables_test.go
└── tree
│ ├── accessors.go
│ ├── computed_values.go
│ ├── html5_ph.css
│ ├── html5_ua.css
│ ├── html5_ua_forms.css
│ ├── media_query.go
│ ├── style.go
│ ├── style_test.go
│ ├── target.go
│ ├── target_test.go
│ ├── tests_ua.css
│ ├── tree.go
│ └── variables_test.go
├── images
├── gradients.go
├── images.go
└── images_test.go
├── logger
└── logger.go
├── macros
├── boxes.py
├── doc_cairo.py
├── dump_box_tree.py
├── props.py
├── python_test_to_go.py
├── python_to_go.py
├── seriallized_boxes_to_go.py
└── source_box.py
├── matrix
├── matrix.go
└── matrix_test.go
├── resources_test
├── AHEM____.TTF
├── Wikipedia-Go.html
├── acid2-reference.html
├── acid2-test.html
├── blue.jpg
├── doc1.html
├── doc1_UTF-16BE.html
├── icon.png
├── latin1-test.css
├── logo_small.png
├── mini_ua.css
├── modele.html
├── pattern.gif
├── pattern.palette.png
├── pattern.png
├── pattern.svg
├── preserveAspectRatio.html
├── really-a-png.svg
├── really-a-svg.png
├── rounded_rect_ref.png
├── sheet2.css
├── sub_directory
│ └── sheet1.css
├── user.css
├── utf8-test.css
├── weasyprint.otb_fixed
├── weasyprint.otf
└── weasyprint.ttx
├── svg
├── bounding_box.go
├── bounding_box_test.go
├── css.go
├── css_test.go
├── elements.go
├── elements_path.go
├── elements_path_test.go
├── elements_text.go
├── gradients.go
├── paint.go
├── paint_test.go
├── parser.go
├── parser_test.go
├── svg.go
├── svg_draw_test.go
├── svg_test.go
├── testdata
│ ├── LICENSE
│ ├── OpacityStrokeDashTest.svg
│ ├── OpacityStrokeDashTest2.svg
│ ├── OpacityStrokeDashTest3.svg
│ ├── TestPercentages.svg
│ ├── TestShapes.svg
│ ├── TestShapes2.svg
│ ├── TestShapes3.svg
│ ├── TestShapes4.svg
│ ├── TestShapes5.svg
│ ├── TestShapes6.svg
│ ├── go-logo-blue.svg
│ ├── landscapeIcons
│ │ ├── beach.svg
│ │ ├── cape.svg
│ │ ├── iceberg.svg
│ │ ├── island.svg
│ │ ├── mountains.svg
│ │ ├── sea.svg
│ │ ├── trees.svg
│ │ └── village.svg
│ ├── sportsIcons
│ │ ├── archery.svg
│ │ ├── artistic_gymnastics.svg
│ │ ├── athletics.svg
│ │ ├── badminton.svg
│ │ ├── basketball.svg
│ │ ├── beach_volleyball.svg
│ │ ├── boxing.svg
│ │ ├── canoe_slalom.svg
│ │ ├── canoe_sprint.svg
│ │ ├── cycling_bmx.svg
│ │ ├── cycling_mountain_bike.svg
│ │ ├── cycling_road.svg
│ │ ├── cycling_track.svg
│ │ ├── diving.svg
│ │ ├── equestrian.svg
│ │ ├── fencing.svg
│ │ ├── football.svg
│ │ ├── golf.svg
│ │ ├── handball.svg
│ │ ├── hockey.svg
│ │ ├── judo.svg
│ │ ├── marathon_swimming.svg
│ │ ├── modern_pentathlon.svg
│ │ ├── olympic_medal_bronze.svg
│ │ ├── olympic_medal_gold.svg
│ │ ├── olympic_medal_silver.svg
│ │ ├── olympic_torch.svg
│ │ ├── readme.txt
│ │ ├── rhythmic_gymnastics.svg
│ │ ├── rowing.svg
│ │ ├── rugby_sevens.svg
│ │ ├── sailing.svg
│ │ ├── shooting.svg
│ │ ├── swimming.svg
│ │ ├── synchronised_swimming.svg
│ │ ├── table_tennis.svg
│ │ ├── taekwondo.svg
│ │ ├── tennis.svg
│ │ ├── trampoline_gymnastics.svg
│ │ ├── triathlon.svg
│ │ ├── trophy.svg
│ │ ├── volleyball.svg
│ │ ├── water_polo.svg
│ │ ├── weightlifting.svg
│ │ └── wrestling.svg
│ └── testIcons
│ │ ├── 24px.svg
│ │ ├── astronaut.svg
│ │ ├── content-cut-light.svg
│ │ ├── defs.svg
│ │ ├── diagram.svg
│ │ ├── jupiter.svg
│ │ ├── lander.svg
│ │ ├── original.svg
│ │ ├── school-bus.svg
│ │ ├── tagsremoved.svg
│ │ └── telescope.svg
├── tree.go
└── tree_test.go
├── text
├── draw
│ ├── draw.go
│ └── draw_pango.go
├── engine_gotext.go
├── engine_pango.go
├── fonts.go
├── fonts_test.go
├── hyphen
│ ├── datas.go
│ ├── dictionaries
│ │ ├── hyph_af_ZA.dic
│ │ ├── hyph_be_BY.dic
│ │ ├── hyph_bg_BG.dic
│ │ ├── hyph_cs_CZ.dic
│ │ ├── hyph_da_DK.dic
│ │ ├── hyph_de_AT.dic
│ │ ├── hyph_de_CH.dic
│ │ ├── hyph_de_DE.dic
│ │ ├── hyph_el_GR.dic
│ │ ├── hyph_en_GB.dic
│ │ ├── hyph_en_US.dic
│ │ ├── hyph_eo.dic
│ │ ├── hyph_es.dic
│ │ ├── hyph_et_EE.dic
│ │ ├── hyph_fr.dic
│ │ ├── hyph_gl.dic
│ │ ├── hyph_hr_HR.dic
│ │ ├── hyph_hu_HU.dic
│ │ ├── hyph_id_ID.dic
│ │ ├── hyph_is.dic
│ │ ├── hyph_it_IT.dic
│ │ ├── hyph_lt.dic
│ │ ├── hyph_lv_LV.dic
│ │ ├── hyph_mn_MN.dic
│ │ ├── hyph_nb_NO.dic
│ │ ├── hyph_nl_NL.dic
│ │ ├── hyph_nn_NO.dic
│ │ ├── hyph_pl_PL.dic
│ │ ├── hyph_pt_BR.dic
│ │ ├── hyph_pt_PT.dic
│ │ ├── hyph_ro_RO.dic
│ │ ├── hyph_ru_RU.dic
│ │ ├── hyph_sk_SK.dic
│ │ ├── hyph_sl_SI.dic
│ │ ├── hyph_sq_AL.dic
│ │ ├── hyph_sr.dic
│ │ ├── hyph_sr_Latn.dic
│ │ ├── hyph_sv.dic
│ │ ├── hyph_te_IN.dic
│ │ ├── hyph_th_TH.dic
│ │ ├── hyph_uk_UA.dic
│ │ ├── hyph_zu_ZA.dic
│ │ └── update.sh
│ ├── hyphen.go
│ ├── hyphen_test.go
│ ├── parse.go
│ └── parse_test.go
├── quotes.go
├── style.go
├── style_test.go
├── testdata
│ ├── font_descriptions.json
│ └── metrics_linux.json
├── text.go
└── text_test.go
└── utils
├── html.go
├── html_test.go
├── io.go
├── math.go
├── math_test.go
├── testutils
├── logs.go
├── tracer
│ ├── drawer.go
│ └── tracer.go
└── utils.go
├── urls.go
├── urls_test.go
├── utils.go
├── version.go
└── webencodings_test.go
/.gitignore:
--------------------------------------------------------------------------------
1 | .mypy_cache/**
2 | .idea/**
3 | macros/__pycache__/**
4 | text/testdata/cache.fc
5 | text/testdata/font_index_*
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2019, Benoit KUGLER
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Web render
2 |
3 | This module implements a static renderer for the HTML, CSS and SVG formats.
4 |
5 | It consists for the main part of a Golang port of the awesome [Weasyprint](https://github.com/Kozea/WeasyPrint) python Html to Pdf library.
6 |
7 | The project is usable, but you should use it carefully in production; breaking changes may also be committed on the fly.
8 |
9 | ## Scope
10 |
11 | The main goal of this module is to process HTML or SVG inputs into laid out documents, ready to be paint, and to be compatible with various output formats (like raster images or PDF files).
12 | To do so, this module uses an abstraction of the output, whose implementation must be provided by an higher level package.
13 |
14 | ## Outline of the module
15 |
16 | From the lower level to the higher level, this module has the following structure :
17 |
18 | - the `css` package provides a CSS parser, with property validation and a CSS selector engine (`css/selector`).
19 |
20 | - the `svg` package implements a SVG parser and renderer, supporting CSS styling.
21 |
22 | - the `html` package implements an HTML renderer
23 |
24 | - the `backend` package defines the interfaces which must be implemented by output targets.
25 |
26 | The main entry points are the `html/document` package for HTML rendering and the `svg` package if you only need SVG support.
27 |
28 | ### HTML to PDF: an overview
29 |
30 | The `html` package implements a static HTML renderer, which works by :
31 |
32 | - parsing the HTML input and fetching CSS files, and cascading the styles. This is implemented in the `html/tree` package
33 |
34 | - building a tree of boxes from the HTML structure (package `html/boxes`)
35 |
36 | - laying out this tree, that is attributing position and dimensions to the boxes, and performing line, paragraph and page breaks (package `html/layout`)
37 |
38 | - drawing the laid out tree to an output. Contrary to the Python library, this step is here performed on an abstract output, which must implement the `backend.Document` interface. This means than the core layout logic could easily be reused for other purposes, such as visualizing html document on a GUI application, or targetting other output file formats.
39 |
--------------------------------------------------------------------------------
/backend/document.go:
--------------------------------------------------------------------------------
1 | package backend
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type Anchor struct {
8 | Name string
9 | // Origin at the top-left of the page
10 | X, Y Fl
11 | }
12 |
13 | type Attachment struct {
14 | Title, Description string
15 | Content []byte
16 | }
17 |
18 | // BookmarkNode exposes the outline hierarchy of the document
19 | type BookmarkNode struct {
20 | Label string
21 | Children []BookmarkNode
22 | Open bool // state of the outline item
23 | PageIndex int // page index (0-based) to link to
24 | X, Y Fl // position in the page
25 | }
26 |
27 | // Document is the main target to whole the laid out document,
28 | // consisting in pages, metadata and embedded files.
29 | type Document interface {
30 | // AddPage creates a new page with the given dimensions and returns
31 | // it to be paint on.
32 | // The y axis grows downward, meaning bottom > top
33 | AddPage(left, top, right, bottom Fl) Page
34 |
35 | // CreateAnchors register a list of anchors per page, which are named targets of internal links.
36 | // `anchors` is a 0-based list, meaning anchors in page 1 are at index 0.
37 | // The origin of internal link has been be added by `OutputPage.AddInternalLink`.
38 | // `CreateAnchors` is called after all the pages have been created and processed
39 | CreateAnchors(anchors [][]Anchor)
40 |
41 | // Add global attachments to the file
42 | SetAttachments(as []Attachment)
43 |
44 | // Embed a file. Calling this method twice with the same id
45 | // won't embed the content twice.
46 | // `fileID` will be passed to `OutputPage.AddFileAnnotation`
47 | EmbedFile(fileID string, a Attachment)
48 |
49 | // Metadatas
50 |
51 | SetTitle(title string)
52 | SetDescription(description string)
53 | SetCreator(creator string)
54 | SetAuthors(authors []string)
55 | SetKeywords(keywords []string)
56 | SetProducer(producer string)
57 | SetDateCreation(d time.Time)
58 | SetDateModification(d time.Time)
59 |
60 | // SetBookmarks setup the document outline
61 | SetBookmarks(root []BookmarkNode)
62 | }
63 |
64 | // Page is the target of one laid out page,
65 | // composed of a Canvas and link supports.
66 | type Page interface {
67 | // AddInternalLink shows a link on the page, pointing to the
68 | // named anchor, which will be registered with `Output.CreateAnchors`
69 | AddInternalLink(xMin, yMin, xMax, yMax Fl, anchorName string)
70 |
71 | // AddExternalLink shows a link on the page, pointing to
72 | // the given url
73 | AddExternalLink(xMin, yMin, xMax, yMax Fl, url string)
74 |
75 | // AddFileAnnotation adds a file annotation on the current page.
76 | // The file content has been added with `Output.EmbedFile`.
77 | AddFileAnnotation(xMin, yMin, xMax, yMax Fl, fileID string)
78 |
79 | // Adjust the media boxes
80 |
81 | SetMediaBox(left, top, right, bottom Fl)
82 | SetTrimBox(left, top, right, bottom Fl)
83 | SetBleedBox(left, top, right, bottom Fl)
84 |
85 | Canvas
86 | }
87 |
--------------------------------------------------------------------------------
/backend/text.go:
--------------------------------------------------------------------------------
1 | package backend
2 |
3 | import (
4 | "github.com/benoitkugler/webrender/matrix"
5 | "github.com/benoitkugler/webrender/text"
6 | )
7 |
8 | // TextDrawing exposes the positionned text glyphs to draw
9 | // and the associated font, in a backend independent manner
10 | type TextDrawing struct {
11 | Runs []TextRun
12 |
13 | FontSize, ScaleX Fl
14 | X, Y Fl // origin
15 | Angle Fl // (optional) rotation
16 |
17 | Text []rune
18 | }
19 |
20 | // Matrix return the transformation scaling the text by [FontSize],
21 | // translating if to (X, Y) and applying the [Angle] rotation
22 | func (td TextDrawing) Matrix() matrix.Transform {
23 | mat := matrix.New(td.ScaleX, 0, 0, -1, td.X, td.Y)
24 | if td.Angle != 0 { // avoid useless multiplication if angle == 0
25 | mat.RightMultBy(matrix.Rotation(td.Angle))
26 | }
27 | return mat
28 | }
29 |
30 | // TextRun is a serie of glyphs with constant font.
31 | type TextRun struct {
32 | Font Font
33 | Glyphs []TextGlyph
34 | }
35 |
36 | type GID = uint32
37 |
38 | // TextGlyph stores a glyph and it's position
39 | type TextGlyph struct {
40 | Kerning int // normalized by FontSize
41 | Glyph GID
42 | Offset Fl // normalized by FontSize
43 | Rise Fl
44 | XAdvance Fl // how much to move before drawing, used for emojis
45 |
46 | // TextDrawing.Text[TextOffset:TextOffset+TextLength]
47 | // gives the runes yielding the glyph.
48 | TextOffset, TextLength int
49 | }
50 |
51 | // GlyphExtents exposes glyph metrics, normalized by the font size.
52 | type GlyphExtents struct {
53 | Width int
54 | Y int
55 | Height int
56 | }
57 |
58 | // FontChars stores some metadata that may be required in the output document.
59 | type FontChars struct {
60 | Cmap map[GID][]rune
61 | Extents map[GID]GlyphExtents
62 | Bbox [4]int
63 | }
64 |
65 | // IsFixedPitch returns true if only one width is used,
66 | // that is if the font is monospaced.
67 | func (f *FontChars) IsFixedPitch() bool {
68 | seen := -1
69 | for _, w := range f.Extents {
70 | if seen == -1 {
71 | seen = w.Width
72 | continue
73 | }
74 | if w.Width != seen {
75 | return false
76 | }
77 | }
78 | return true
79 | }
80 |
81 | type FontDescription struct {
82 | Family string
83 | Style text.FontStyle
84 | Weight int
85 |
86 | Ascent Fl
87 | Descent Fl
88 |
89 | Size int // the font size used with this font
90 |
91 | IsOpentype bool
92 | // IsOpentype is true for an OpenType file containing a PostScript Type 2 font
93 | IsOpentypeOpentype bool
94 | }
95 |
96 | // Font are implemented by valid
97 | // map keys
98 | type Font interface {
99 | Origin() text.FontOrigin
100 | Description() FontDescription
101 | }
102 |
--------------------------------------------------------------------------------
/css/parser/css-parsing-tests/LICENSE:
--------------------------------------------------------------------------------
1 | Written in 2013 by Simon Sapin.
2 |
3 | To the extent possible under law, the author(s) have dedicated all copyright
4 | and related and neighboring rights to this work to the public domain worldwide.
5 | This work is distributed without any warranty.
6 |
7 | See the CC0 Public Domain Dedication:
8 | http://creativecommons.org/publicdomain/zero/1.0/
9 |
--------------------------------------------------------------------------------
/css/parser/css-parsing-tests/declaration_list.json:
--------------------------------------------------------------------------------
1 | [
2 |
3 | "", [],
4 | ";; /**/ ; ;", [],
5 | "a:b; c:d 42!important;\n", [
6 | ["declaration", "a", [["ident", "b"]], false],
7 | ["declaration", "c", [["ident", "d"], " ", ["number", "42", 42, "integer"]], true]
8 | ],
9 |
10 | "z;a:b", [
11 | ["error", "invalid"],
12 | ["declaration", "a", [["ident", "b"]], false]
13 | ],
14 |
15 | "z:x!;a:b", [
16 | ["declaration", "z", [["ident", "x"], "!"], false],
17 | ["declaration", "a", [["ident", "b"]], false]
18 | ],
19 |
20 | "a:b; c+:d", [
21 | ["declaration", "a", [["ident", "b"]], false],
22 | ["error", "invalid"]
23 | ],
24 |
25 | "@import 'foo.css'; a:b; @import 'bar.css'", [
26 | ["at-rule", "import", [" ", ["string", "foo.css"]], null],
27 | ["declaration", "a", [["ident", "b"]], false],
28 | ["at-rule", "import", [" ", ["string", "bar.css"]], null]
29 | ],
30 |
31 | "@media screen { div{;}} a:b;; @media print{div{", [
32 | ["at-rule", "media", [" ", ["ident", "screen"], " "], [" ", ["ident", "div"], ["{}", ";"]]],
33 | ["declaration", "a", [["ident", "b"]], false],
34 | ["at-rule", "media", [" ", ["ident", "print"]], [["ident", "div"], ["{}"]]]
35 | ],
36 |
37 | "@ media screen { div{;}} a:b;; @media print{div{", [
38 | ["error", "invalid"],
39 | ["at-rule", "media", [" ", ["ident", "print"]], [["ident", "div"], ["{}"]]]
40 | ],
41 |
42 | "", []
43 |
44 | ]
45 |
--------------------------------------------------------------------------------
/css/parser/css-parsing-tests/make_color3_hsl.py:
--------------------------------------------------------------------------------
1 | import colorsys # It turns out Python already does HSL -> RGB!
2 |
3 |
4 | def trim(s):
5 | return s if not s.endswith('.0') else s[:-2]
6 |
7 |
8 | print('[')
9 | print(',\n'.join(
10 | '"hsl%s(%s, %s%%, %s%%%s)", [%s, %s, %s, %s]' % (
11 | ('a' if a is not None else '', h,
12 | trim(str(s / 10.)), trim(str(l / 10.)),
13 | ', %s' % a if a is not None else '') +
14 | tuple(trim(str(round(v, 10)))
15 | for v in colorsys.hls_to_rgb(h / 360., l / 1000., s / 1000.)) +
16 | (a if a is not None else 1,)
17 | )
18 | for a in [None, 1, .2, 0]
19 | for l in range(0, 1001, 125)
20 | for s in range(0, 1001, 125)
21 | for h in range(0, 360, 30)
22 | ))
23 | print(']')
24 |
--------------------------------------------------------------------------------
/css/parser/css-parsing-tests/one_component_value.json:
--------------------------------------------------------------------------------
1 | [
2 |
3 | "", ["error", "empty"],
4 | " ", ["error", "empty"],
5 | "/**/", ["error", "empty"],
6 | " /**/\t/* a */\n\n", ["error", "empty"],
7 |
8 | ".", ".",
9 | "a", ["ident", "a"],
10 | "/**/ 4px", ["dimension", "4", 4, "integer", "px"],
11 | "rgba(100%, 0%, 50%, .5)", ["function", "rgba",
12 | ["percentage", "100", 100, "integer"], ",", " ",
13 | ["percentage", "0", 0, "integer"], ",", " ",
14 | ["percentage", "50", 50, "integer"], ",", " ",
15 | ["number", ".5", 0.5, "number"]
16 | ],
17 |
18 | " /**/ { foo: bar; @baz [)", ["{}",
19 | " ", ["ident", "foo"], ":", " ", ["ident", "bar"], ";", " ",
20 | ["at-keyword", "baz"], " ", ["[]",
21 | ["error", ")"]
22 | ]
23 | ],
24 |
25 | ".foo", ["error", "extra-input"]
26 |
27 | ]
28 |
--------------------------------------------------------------------------------
/css/parser/css-parsing-tests/one_rule.json:
--------------------------------------------------------------------------------
1 | [
2 |
3 | "", ["error", "empty"],
4 | "foo", ["error", "invalid"],
5 | "foo 4", ["error", "invalid"],
6 |
7 | "@foo", ["at-rule", "foo", [], null],
8 |
9 | "@foo bar; \t/* comment */", ["at-rule", "foo", [" ", ["ident", "bar"]], null],
10 | " /**/ @foo bar{[(4", ["at-rule", "foo",
11 | [" ", ["ident", "bar"]],
12 | [["[]", ["()", ["number", "4", 4, "integer"]]]]
13 | ],
14 |
15 | "@foo { bar", ["at-rule", "foo", [" "], [" ", ["ident", "bar"]]],
16 | "@foo [ bar", ["at-rule", "foo", [" ", ["[]", " ", ["ident", "bar"]]], null],
17 |
18 | " /**/ div > p { color: #aaa; } /**/ ", ["qualified rule",
19 | [["ident", "div"], " ", ">", " ", ["ident", "p"], " "],
20 | [" ", ["ident", "color"], ":", " ", ["hash", "aaa", "id"], ";", " "]
21 | ],
22 |
23 | " /**/ { color: #aaa ", ["qualified rule",
24 | [],
25 | [" ", ["ident", "color"], ":", " ", ["hash", "aaa", "id"], " "]
26 | ],
27 |
28 | " /* CDO/CDC are not special */ {", ["qualified rule",
29 | ["", " "], []
30 | ],
31 |
32 | "div { color: #aaa; } p{}", ["error", "extra-input"],
33 | "div {} -->", ["error", "extra-input"],
34 | "{}a", ["error", "extra-input"]
35 |
36 | ]
37 |
--------------------------------------------------------------------------------
/css/parser/css-parsing-tests/rule_list.json:
--------------------------------------------------------------------------------
1 | [
2 |
3 | "", [],
4 | "foo", [["error", "invalid"]],
5 | "foo 4", [["error", "invalid"]],
6 |
7 | "@foo", [["at-rule", "foo", [], null]],
8 |
9 | "@foo bar; \t/* comment */", [["at-rule", "foo", [" ", ["ident", "bar"]], null]],
10 |
11 | " /**/ @foo bar{[(4", [["at-rule", "foo",
12 | [" ", ["ident", "bar"]],
13 | [["[]", ["()", ["number", "4", 4, "integer"]]]]
14 | ]],
15 |
16 | "@foo { bar", [["at-rule", "foo", [" "], [" ", ["ident", "bar"]]]],
17 | "@foo [ bar", [["at-rule", "foo", [" ", ["[]", " ", ["ident", "bar"]]], null]],
18 |
19 | " /**/ div > p { color: #aaa; } /**/ ", [["qualified rule",
20 | [["ident", "div"], " ", ">", " ", ["ident", "p"], " "],
21 | [" ", ["ident", "color"], ":", " ", ["hash", "aaa", "id"], ";", " "]
22 | ]],
23 |
24 | " /**/ { color: #aaa ", [["qualified rule",
25 | [],
26 | [" ", ["ident", "color"], ":", " ", ["hash", "aaa", "id"], " "]
27 | ]],
28 |
29 | " /* CDO/CDC are not special */ {", [["qualified rule",
30 | ["", " "], []
31 | ]],
32 |
33 | "div { color: #aaa; } p{}", [
34 | ["qualified rule", [["ident", "div"], " "],
35 | [" ", ["ident", "color"], ":", " ", ["hash", "aaa", "id"], ";", " "]
36 | ],
37 | ["qualified rule", [["ident", "p"]], []]
38 | ],
39 |
40 | "div {} -->", [
41 | ["qualified rule", [["ident", "div"], " "], []],
42 | ["error", "invalid"]
43 | ],
44 |
45 | "{}a", [["qualified rule", [], []], ["error", "invalid"]],
46 | "{}@a", [["qualified rule", [], []], ["at-rule", "a", [], null]]
47 |
48 | ]
49 |
--------------------------------------------------------------------------------
/css/parser/css-parsing-tests/stylesheet.json:
--------------------------------------------------------------------------------
1 | [
2 |
3 | "", [],
4 | "foo", [["error", "invalid"]],
5 | "foo 4", [["error", "invalid"]],
6 |
7 | "@foo", [["at-rule", "foo", [], null]],
8 |
9 | "@foo bar; \t/* comment */", [["at-rule", "foo", [" ", ["ident", "bar"]], null]],
10 |
11 | " /**/ @foo bar{[(4", [["at-rule", "foo",
12 | [" ", ["ident", "bar"]],
13 | [["[]", ["()", ["number", "4", 4, "integer"]]]]
14 | ]],
15 |
16 | "@foo { bar", [["at-rule", "foo", [" "], [" ", ["ident", "bar"]]]],
17 | "@foo [ bar", [["at-rule", "foo", [" ", ["[]", " ", ["ident", "bar"]]], null]],
18 |
19 | " /**/ div > p { color: #aaa; } /**/ ", [["qualified rule",
20 | [["ident", "div"], " ", ">", " ", ["ident", "p"], " "],
21 | [" ", ["ident", "color"], ":", " ", ["hash", "aaa", "id"], ";", " "]
22 | ]],
23 |
24 | " /**/ { color: #aaa ", [["qualified rule",
25 | [],
26 | [" ", ["ident", "color"], ":", " ", ["hash", "aaa", "id"], " "]
27 | ]],
28 |
29 | " /* CDO/CDC are ignored between rules */ {", [["qualified rule", [], []]],
30 | " a{", [["qualified rule", [["ident", "a"], ""], []]],
31 |
32 | "div { color: #aaa; } p{}", [
33 | ["qualified rule", [["ident", "div"], " "],
34 | [" ", ["ident", "color"], ":", " ", ["hash", "aaa", "id"], ";", " "]
35 | ],
36 | ["qualified rule", [["ident", "p"]], []]
37 | ],
38 |
39 | "div {} -->", [["qualified rule", [["ident", "div"], " "], []]],
40 |
41 | "{}a", [["qualified rule", [], []], ["error", "invalid"]],
42 | "{}@a", [["qualified rule", [], []], ["at-rule", "a", [], null]]
43 |
44 | ]
45 |
--------------------------------------------------------------------------------
/css/properties/float.go:
--------------------------------------------------------------------------------
1 | package properties
2 |
3 | import (
4 | "fmt"
5 | "math"
6 | )
7 |
8 | // During layout, float numbers sometimes need special values like "auto" or nil (None in Python).
9 | // This file define a float64-like type handling these cases.
10 |
11 | const (
12 | // AutoF indicates a value specified as "auto", which will
13 | // be resolved during layout.
14 | AutoF special = true
15 | )
16 |
17 | type MaybeFloat interface {
18 | V() Float
19 | }
20 |
21 | func (f Float) V() Float { return f }
22 |
23 | type special bool
24 |
25 | func (f special) V() Float { return 0 }
26 |
27 | func (f special) String() string {
28 | if f {
29 | return "auto"
30 | }
31 | return "-"
32 | }
33 |
34 | // Return true except for 0 or nil
35 | func Is(m MaybeFloat) bool {
36 | if m == nil {
37 | return false
38 | }
39 | if f, ok := m.(Float); ok {
40 | return f != 0
41 | }
42 | return false
43 | }
44 |
45 | // MaybeFloatToFloat is the same as MaybeFloat.V(),
46 | // but handles nil values
47 | func MaybeFloatToFloat(mf MaybeFloat) Float {
48 | if mf == nil {
49 | return 0
50 | }
51 | return mf.V()
52 | }
53 |
54 | func MaybeFloatToValue(mf MaybeFloat) DimOrS {
55 | if mf == nil {
56 | return DimOrS{}
57 | }
58 | if mf == AutoF {
59 | return SToV("auto")
60 | }
61 | return mf.V().ToValue()
62 | }
63 |
64 | func Min(x, y Float) Float {
65 | if x < y {
66 | return x
67 | }
68 | return y
69 | }
70 |
71 | func Max(x, y Float) Float {
72 | if x > y {
73 | return x
74 | }
75 | return y
76 | }
77 |
78 | func Floor(x Float) Float {
79 | return Float(math.Floor(float64(x)))
80 | }
81 |
82 | func Maxs(values ...Float) Float {
83 | max := -Inf
84 | for _, w := range values {
85 | if w > max {
86 | max = w
87 | }
88 | }
89 | return max
90 | }
91 |
92 | func Mins(values ...Float) Float {
93 | min := Inf
94 | for _, w := range values {
95 | if w < min {
96 | min = w
97 | }
98 | }
99 | return min
100 | }
101 |
102 | func Hypot(a, b Float) Float {
103 | return Float(math.Hypot(float64(a), float64(b)))
104 | }
105 |
106 | func Abs(x Float) Float {
107 | if x < 0 {
108 | return -x
109 | }
110 | return x
111 | }
112 |
113 | // ResolvePercentage returns the percentage of the reference value, or the value unchanged.
114 | // “referTo“ is the length for 100%. If “referTo“ is not a number, it
115 | // just replaces percentages.
116 | func ResolvePercentage(value DimOrS, referTo Float) MaybeFloat {
117 | if value.IsNone() {
118 | return nil
119 | } else if value.S == "auto" {
120 | return AutoF
121 | } else if value.Unit == Px {
122 | return value.Value
123 | } else {
124 | if value.Unit != Perc {
125 | panic(fmt.Sprintf("expected percentage, got %d", value.Unit))
126 | }
127 | return referTo * value.Value / 100.
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/css/properties/gen/gen_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func TestKebabCase(t *testing.T) {
9 | fmt.Println(kebabCase("Prop1A"))
10 | }
11 |
12 | func TestConstants(t *testing.T) {
13 | fmt.Println(parseConstants("../properties.go"))
14 | }
15 |
--------------------------------------------------------------------------------
/css/properties/keywords/keywords.go:
--------------------------------------------------------------------------------
1 | package keywords
2 |
3 | // Keyword efficiently stores CSS keywords
4 | type Keyword uint8
5 |
6 | const (
7 | _ Keyword = iota
8 | Auto
9 | Baseline
10 | Center
11 | End
12 | First
13 | FlexEnd
14 | FlexStart
15 | Last
16 | Left
17 | Legacy
18 | Normal
19 | Right
20 | Safe
21 | SelfEnd
22 | SelfStart
23 | SpaceAround
24 | SpaceBetween
25 | SpaceEvenly
26 | Start
27 | Stretch
28 | Unsafe
29 | )
30 |
31 | func NewKeyword(s string) Keyword {
32 | switch s {
33 | case "auto":
34 | return Auto
35 | case "baseline":
36 | return Baseline
37 | case "center":
38 | return Center
39 | case "end":
40 | return End
41 | case "first":
42 | return First
43 | case "flex-end":
44 | return FlexEnd
45 | case "flex-start":
46 | return FlexStart
47 | case "last":
48 | return Last
49 | case "left":
50 | return Left
51 | case "legacy":
52 | return Legacy
53 | case "normal":
54 | return Normal
55 | case "right":
56 | return Right
57 | case "safe":
58 | return Safe
59 | case "self-end":
60 | return SelfEnd
61 | case "self-start":
62 | return SelfStart
63 | case "space-around":
64 | return SpaceAround
65 | case "space-between":
66 | return SpaceBetween
67 | case "space-evenly":
68 | return SpaceEvenly
69 | case "start":
70 | return Start
71 | case "stretch":
72 | return Stretch
73 | case "unsafe":
74 | return Unsafe
75 | }
76 | return 0
77 | }
78 |
--------------------------------------------------------------------------------
/css/properties/keywords/keywords_test.go:
--------------------------------------------------------------------------------
1 | package keywords
2 |
3 | import (
4 | "fmt"
5 | "sort"
6 | "testing"
7 |
8 | "github.com/benoitkugler/webrender/utils"
9 | )
10 |
11 | func TestKeywordSorted(t *testing.T) {
12 | l := [...]string{
13 | "center", "space-between", "space-around", "space-evenly",
14 | "stretch", "normal", "flex-start", "flex-end",
15 | "start", "end", "left", "right",
16 | "safe", "unsafe",
17 | "center", "start", "end", "flex-start", "flex-end", "left",
18 | "right",
19 | "normal", "stretch", "center", "start", "end", "self-start",
20 | "self-end", "flex-start", "flex-end", "left", "right",
21 | "legacy",
22 | "baseline",
23 | "center", "start", "end", "self-start", "self-end",
24 | "flex-start", "flex-end", "left", "right",
25 | "auto", "normal", "stretch", "center", "start", "end",
26 | "self-start", "self-end", "flex-start", "flex-end", "left",
27 | "right",
28 | "center", "start", "end", "self-start", "self-end",
29 | "flex-start", "flex-end", "left", "right",
30 | "normal", "stretch", "center", "start", "end", "self-start",
31 | "self-end", "flex-start", "flex-end",
32 | "baseline",
33 | "center", "start", "end", "self-start", "self-end",
34 | "flex-start", "flex-end",
35 | "auto", "normal", "stretch", "center", "start", "end",
36 | "self-start", "self-end", "flex-start", "flex-end",
37 | "center", "start", "end", "self-start", "self-end",
38 | "flex-start", "flex-end",
39 | "center", "space-between", "space-around", "space-evenly",
40 | "stretch", "normal", "flex-start", "flex-end",
41 | "start", "end",
42 | "baseline",
43 | "center", "start", "end", "flex-start", "flex-end",
44 | "first", "last",
45 | }
46 |
47 | m := utils.NewSet(l[:]...)
48 | var out []string
49 | for l := range m {
50 | out = append(out, l)
51 | }
52 | sort.Strings(out)
53 | for _, s := range out {
54 | fmt.Printf("case %q:\nreturn %s\n", s, s)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/css/selector/README.md:
--------------------------------------------------------------------------------
1 | # selector
2 |
3 | The selector package implements CSS selectors for use with the parse trees produced by the html package.
4 |
5 | It is based on andybalholm/cascadia commit 8919e381b2b9868b86bb29a833d893796a188f1f, with some API simplification.
6 |
--------------------------------------------------------------------------------
/css/selector/benchmark_test.go:
--------------------------------------------------------------------------------
1 | package selector
2 |
3 | import (
4 | "strings"
5 | "testing"
6 |
7 | "golang.org/x/net/html"
8 | )
9 |
10 | func MustParseHTML(doc string) *html.Node {
11 | dom, err := html.Parse(strings.NewReader(doc))
12 | if err != nil {
13 | panic(err)
14 | }
15 | return dom
16 | }
17 |
18 | var (
19 | selector = MustCompile(`div.matched`)
20 | doc = `
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | `
46 | )
47 | var dom = MustParseHTML(doc)
48 |
49 | func BenchmarkMatchAll(b *testing.B) {
50 | var matches []*html.Node
51 | for i := 0; i < b.N; i++ {
52 | matches = MatchAll(dom, selector)
53 | }
54 | _ = matches
55 | }
56 |
57 | func BenchmarkMatchAllW3(b *testing.B) {
58 | tests := loadValidSelectors(b)
59 | doc := parseReference("test_resources/content.xhtml")
60 | var allSelectors []Sel
61 | for _, test := range tests {
62 | if test.Xfail {
63 | continue
64 | }
65 | sels, err := ParseGroup(test.Selector)
66 | if err != nil {
67 | b.Fatalf("%s -> unable to parse valid selector : %s : %s", test.Name, test.Selector, err)
68 | }
69 | for _, sel := range sels {
70 | if sel.PseudoElement() != "" {
71 | continue // pseudo element doesn't count as a match in this test since they are not part of the document
72 | }
73 | allSelectors = append(allSelectors, sel)
74 | }
75 | }
76 |
77 | b.ResetTimer()
78 |
79 | for i := 0; i < b.N; i++ {
80 | for _, sel := range allSelectors {
81 | _ = MatchAll(doc, sel)
82 | }
83 | }
84 | }
85 |
86 | func TestMatchAllW3(t *testing.T) {
87 | tests := loadValidSelectors(t)
88 | doc := parseReference("test_resources/content.xhtml")
89 | var allSelectors []Sel
90 | for _, test := range tests {
91 | if test.Xfail {
92 | continue
93 | }
94 | sels, err := ParseGroup(test.Selector)
95 | if err != nil {
96 | t.Fatalf("%s -> unable to parse valid selector : %s : %s", test.Name, test.Selector, err)
97 | }
98 | for _, sel := range sels {
99 | if sel.PseudoElement() != "" {
100 | continue // pseudo element doesn't count as a match in this test since they are not part of the document
101 | }
102 | allSelectors = append(allSelectors, sel)
103 | }
104 | }
105 |
106 | for _, sel := range allSelectors {
107 | _ = MatchAll(doc, sel)
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/css/selector/parser_test.go:
--------------------------------------------------------------------------------
1 | package selector
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | var identifierTests = map[string]string{
8 | "x": "x",
9 | "96": "",
10 | "-x": "-x",
11 | `r\e9 sumé`: "résumé",
12 | `r\0000e9 sumé`: "résumé",
13 | `r\0000e9sumé`: "résumé",
14 | `a\"b`: `a"b`,
15 | }
16 |
17 | func TestParseIdentifier(t *testing.T) {
18 | for source, want := range identifierTests {
19 | p := &parser{s: source}
20 | got, err := p.parseIdentifier()
21 | if err != nil {
22 | if want == "" {
23 | // It was supposed to be an error.
24 | continue
25 | }
26 | t.Errorf("parsing %q: got error (%s), want %q", source, err, want)
27 | continue
28 | }
29 |
30 | if want == "" {
31 | t.Errorf("parsing %q: got %q, want error", source, got)
32 | continue
33 | }
34 |
35 | if p.i < len(source) {
36 | t.Errorf("parsing %q: %d bytes left over", source, len(source)-p.i)
37 | continue
38 | }
39 |
40 | if got != want {
41 | t.Errorf("parsing %q: got %q, want %q", source, got, want)
42 | }
43 | }
44 | }
45 |
46 | var stringTests = map[string]string{
47 | `"x"`: "x",
48 | `'x'`: "x",
49 | `'x`: "",
50 | "'x\\\r\nx'": "xx",
51 | `"r\e9 sumé"`: "résumé",
52 | `"r\0000e9 sumé"`: "résumé",
53 | `"r\0000e9sumé"`: "résumé",
54 | `"a\"b"`: `a"b`,
55 | }
56 |
57 | func TestParseString(t *testing.T) {
58 | for source, want := range stringTests {
59 | p := &parser{s: source}
60 | got, err := p.parseString()
61 | if err != nil {
62 | if want == "" {
63 | // It was supposed to be an error.
64 | continue
65 | }
66 | t.Errorf("parsing %q: got error (%s), want %q", source, err, want)
67 | continue
68 | }
69 |
70 | if want == "" {
71 | if err == nil {
72 | t.Errorf("parsing %q: got %q, want error", source, got)
73 | }
74 | continue
75 | }
76 |
77 | if p.i < len(source) {
78 | t.Errorf("parsing %q: %d bytes left over", source, len(source)-p.i)
79 | continue
80 | }
81 |
82 | if got != want {
83 | t.Errorf("parsing %q: got %q, want %q", source, got, want)
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/css/selector/serialize_test.go:
--------------------------------------------------------------------------------
1 | package selector
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestSerialize(t *testing.T) {
9 | var testSer []string
10 | for _, test := range selectorTests {
11 | testSer = append(testSer, test.selector)
12 | }
13 | for _, test := range testsPseudo {
14 | testSer = append(testSer, test.selector)
15 | }
16 | for _, test := range loadValidSelectors(t) {
17 | if test.Xfail {
18 | continue
19 | }
20 | testSer = append(testSer, test.Selector)
21 | }
22 |
23 | for _, test := range testSer {
24 | s, err := ParseGroup(test)
25 | if err != nil {
26 | t.Fatalf("error compiling %q: %s", test, err)
27 | }
28 |
29 | serialized := s.String()
30 | s2, err := ParseGroup(serialized)
31 | if err != nil {
32 | t.Errorf("error compiling %q: %s %T (original : %s)", serialized, err, s, test)
33 | }
34 | if !reflect.DeepEqual(s, s2) {
35 | t.Errorf("can't retrieve selector from serialized : %s (original : %s, sel : %#v)", serialized, test, s)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/css/selector/specificity.go:
--------------------------------------------------------------------------------
1 | package selector
2 |
3 | // Specificity is the CSS specificity as defined in
4 | // https://www.w3.org/TR/selectors/#specificity-rules
5 | // with the convention Specificity = [A,B,C].
6 | type Specificity [3]int
7 |
8 | // returns `true` if s < other (strictly), false otherwise
9 | func (s Specificity) Less(other Specificity) bool {
10 | for i := range s {
11 | if s[i] < other[i] {
12 | return true
13 | }
14 | if s[i] > other[i] {
15 | return false
16 | }
17 | }
18 | return false
19 | }
20 |
21 | func (s Specificity) Add(other Specificity) Specificity {
22 | for i, sp := range other {
23 | s[i] += sp
24 | }
25 | return s
26 | }
27 |
--------------------------------------------------------------------------------
/css/selector/test_resources/invalid_selectors.json:
--------------------------------------------------------------------------------
1 | [
2 | {"name": "Empty String", "selector": ""},
3 | {"name": "Invalid character", "selector": "["},
4 | {"name": "Invalid character", "selector": "]"},
5 | {"name": "Invalid character", "selector": "("},
6 | {"name": "Invalid character", "selector": ")"},
7 | {"name": "Invalid character", "selector": "{"},
8 | {"name": "Invalid character", "selector": "}"},
9 | {"name": "Invalid character", "selector": "<"},
10 | {"name": "Invalid character", "selector": ">"},
11 | {"name": "Invalid character", "selector": ":"},
12 | {"name": "Invalid character", "selector": "::"},
13 | {"name": "Invalid ID", "selector": "#"},
14 | {"name": "Invalid group of selectors", "selector": "div,"},
15 | {"name": "Invalid class", "selector": "."},
16 | {"name": "Invalid class", "selector": ".5cm"},
17 | {"name": "Invalid class", "selector": "..test"},
18 | {"name": "Invalid class", "selector": ".foo..quux"},
19 | {"name": "Invalid class", "selector": ".bar."},
20 | {"name": "Invalid combinator", "selector": "div & address, p"},
21 | {"name": "Invalid combinator", "selector": "div >> address, p"},
22 | {"name": "Invalid combinator", "selector": "div ++ address, p"},
23 | {"name": "Invalid combinator", "selector": "div ~~ address, p"},
24 | {"name": "Invalid [att=value] selector", "selector": "[*=test]"},
25 | {"name": "Invalid [att=value] selector", "selector": "[*|*=test]"},
26 | {"name": "Invalid [att=value] selector", "selector": "[class= space unquoted ]"},
27 | {"name": "Unknown pseudo-class", "selector": "div:example"},
28 | {"name": "Unknown pseudo-class", "selector": ":example"},
29 | {"name": "Unknown pseudo-element", "selector": "div::example", "xfail": true},
30 | {"name": "Unknown pseudo-element", "selector": "::example", "xfail": true},
31 | {"name": "Invalid pseudo-element", "selector": ":::before"},
32 | {"name": "Undeclared namespace", "selector": "ns|div"},
33 | {"name": "Undeclared namespace", "selector": ":not(ns|div)"},
34 | {"name": "Invalid namespace", "selector": "^|div"},
35 | {"name": "Invalid namespace", "selector": "$|div"},
36 | {"name": "Invalid namespace", "selector": "$|div"},
37 | {"name": "Case insensitive, no closing ]", "selector": "[a=a i"}
38 | ]
39 |
--------------------------------------------------------------------------------
/css/selector/w3_test.go:
--------------------------------------------------------------------------------
1 | package selector
2 |
3 | import (
4 | "encoding/json"
5 | "os"
6 | "reflect"
7 | "testing"
8 |
9 | "golang.org/x/net/html"
10 | )
11 |
12 | func TestInvalidSelectors(t *testing.T) {
13 | c, err := os.ReadFile("test_resources/invalid_selectors.json")
14 | if err != nil {
15 | t.Fatal(err)
16 | }
17 | var tests []invalidSelector
18 | if err = json.Unmarshal(c, &tests); err != nil {
19 | t.Fatal(err)
20 | }
21 | for _, test := range tests {
22 | _, err := ParseGroup(test.Selector)
23 | if err == nil {
24 | t.Fatalf("%s -> expected error on invalid selector : %s", test.Name, test.Selector)
25 | }
26 | }
27 | }
28 |
29 | func parseReference(filename string) *html.Node {
30 | f, err := os.Open(filename)
31 | if err != nil {
32 | panic(err)
33 | }
34 | node, err := html.Parse(f)
35 | if err != nil {
36 | panic(err)
37 | }
38 | return node
39 | }
40 |
41 | func getId(n *html.Node) string {
42 | for _, attr := range n.Attr {
43 | if attr.Key == "id" {
44 | return attr.Val
45 | }
46 | }
47 | return ""
48 | }
49 |
50 | func isEqual(m map[string]int, l []string) bool {
51 | expected := map[string]int{}
52 | for _, s := range l {
53 | expected[s]++
54 | }
55 | return reflect.DeepEqual(m, expected)
56 | }
57 |
58 | func loadValidSelectors(t testing.TB) []validSelector {
59 | c, err := os.ReadFile("test_resources/valid_selectors.json")
60 | if err != nil {
61 | t.Fatal(err)
62 | }
63 | var tests []validSelector
64 | if err = json.Unmarshal(c, &tests); err != nil {
65 | t.Fatal(err)
66 | }
67 | return tests
68 | }
69 |
70 | func TestValidSelectors(t *testing.T) {
71 | tests := loadValidSelectors(t)
72 | doc := parseReference("test_resources/content.xhtml")
73 | for i, test := range tests {
74 | if test.Xfail {
75 | t.Logf("skiped test %s", test.Name)
76 | continue
77 | }
78 | sels, err := ParseGroup(test.Selector)
79 | if err != nil {
80 | t.Fatalf("%s -> unable to parse valid selector : %s : %s", test.Name, test.Selector, err)
81 | }
82 | matchingNodes := map[*html.Node]bool{}
83 | for _, sel := range sels {
84 | if sel.PseudoElement() != "" {
85 | continue // pseudo element doesn't count as a match in this test since they are not part of the document
86 | }
87 | for _, node := range MatchAll(doc, sel) {
88 | matchingNodes[node] = true
89 | }
90 | }
91 | matchingIds := map[string]int{}
92 | for node := range matchingNodes {
93 | matchingIds[getId(node)]++
94 | }
95 | if !isEqual(matchingIds, test.Expect) {
96 | t.Fatalf("test %d %s : expected %v got %v", i, test.Name, test.Expect, matchingIds)
97 | }
98 |
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/benoitkugler/webrender
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/benoitkugler/textlayout v0.3.1
7 | github.com/benoitkugler/textprocessing v0.0.3
8 | github.com/go-text/typesetting v0.2.1
9 | golang.org/x/image v0.23.0
10 | golang.org/x/net v0.36.0
11 | golang.org/x/text v0.22.0
12 | )
13 |
14 | require github.com/benoitkugler/pstokenizer v1.0.1 // indirect
15 |
16 | // replace github.com/go-text/typesetting => ../../go-text/typesetting
17 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE=
2 | github.com/benoitkugler/pstokenizer v1.0.1 h1:3+18uif4Dg4+w84AmkWPKOujhPKbLnkgxP1eb/KtiGg=
3 | github.com/benoitkugler/pstokenizer v1.0.1/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE=
4 | github.com/benoitkugler/textlayout v0.3.0/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7sBPgEFcuSgfvi/w=
5 | github.com/benoitkugler/textlayout v0.3.1 h1:hXCAJv3/8oF2mm68jledvbq85l6dA+aOYkwnzH5v4F8=
6 | github.com/benoitkugler/textlayout v0.3.1/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7sBPgEFcuSgfvi/w=
7 | github.com/benoitkugler/textlayout-testdata v0.1.1 h1:AvFxBxpfrQd8v55qH59mZOJOQjtD6K2SFe9/HvnIbJk=
8 | github.com/benoitkugler/textlayout-testdata v0.1.1/go.mod h1:i/qZl09BbUOtd7Bu/W1CAubRwTWrEXWq6JwMkw8wYxo=
9 | github.com/benoitkugler/textprocessing v0.0.3 h1:Q2X+Z6vxuW5Bxn1R9RaNt0qcprBfpc2hEUDeTlz90Ng=
10 | github.com/benoitkugler/textprocessing v0.0.3/go.mod h1:/4bLyCf1QYywunMK3Gf89Nhb50YI/9POewqrLxWhxd4=
11 | github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
12 | github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
13 | github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
14 | golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
15 | golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
16 | golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
17 | golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
18 | golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
19 | golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
20 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
21 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
22 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
23 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
24 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
25 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
26 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
27 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
28 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
29 |
--------------------------------------------------------------------------------
/html/boxes/iters.go:
--------------------------------------------------------------------------------
1 | package boxes
2 |
3 | type boxIterator interface {
4 | Next() bool
5 | Box() Box
6 | }
7 |
8 | // implements boxIterator
9 | type boxSlice struct {
10 | data []Box
11 | pos int
12 | }
13 |
14 | func newBoxIter(boxes []Box) *boxSlice { return &boxSlice{data: boxes} }
15 |
16 | func (s boxSlice) Next() bool { return s.pos < len(s.data) }
17 |
18 | func (s *boxSlice) Box() Box {
19 | b := s.data[s.pos]
20 | s.pos++
21 | return b
22 | }
23 |
24 | func collectBoxes(iter boxIterator) []Box {
25 | var out []Box
26 | for iter.Next() {
27 | out = append(out, iter.Box())
28 | }
29 | return out
30 | }
31 |
32 | func collectTableColumnGroupBoxs(iter boxIterator) []*TableColumnGroupBox {
33 | var out []*TableColumnGroupBox
34 | for iter.Next() {
35 | out = append(out, iter.Box().(*TableColumnGroupBox))
36 | }
37 | return out
38 | }
39 |
--------------------------------------------------------------------------------
/html/document/testdata/fiche_sanitaire.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/html/document/testdata/fiche_sanitaire.html
--------------------------------------------------------------------------------
/html/layout/layout_shrink_to_fit_test.go:
--------------------------------------------------------------------------------
1 | package layout
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "testing"
7 |
8 | bo "github.com/benoitkugler/webrender/html/boxes"
9 | tu "github.com/benoitkugler/webrender/utils/testutils"
10 | )
11 |
12 | // Tests for shrink-to-fit algorithm.
13 |
14 | func TestShrinkToFitFloatingPointError1(t *testing.T) {
15 | for marginLeft := 1; marginLeft < 10; marginLeft += 2 {
16 | for fontSize := 1; fontSize < 10; fontSize += 2 {
17 | testShrinkToFitFloatingPointError1(t, marginLeft, fontSize)
18 | }
19 | }
20 | }
21 |
22 | func testShrinkToFitFloatingPointError1(t *testing.T, marginLeft, fontSize int) {
23 | defer tu.CaptureLogs().AssertNoLogs(t)
24 |
25 | // See bugs #325 && #288, see commit fac5ee9.
26 | page := renderOnePage(t, fmt.Sprintf(`
27 |
33 | this parrot is dead
34 | `, marginLeft, fontSize))
35 | html := unpack1(page)
36 | body := unpack1(html)
37 | p := unpack1(body)
38 | tu.AssertEqual(t, len(p.Box().Children), 1)
39 | }
40 |
41 | func TestShrinkToFitFloatingPointError2(t *testing.T) {
42 | defer tu.CaptureLogs().AssertNoLogs(t)
43 |
44 | for _, fontSize := range []int{1, 5, 10, 50, 100, 1000, 10000} {
45 | letters := 1
46 | for {
47 | page := renderOnePage(t, fmt.Sprintf(`
48 |
53 | mmm %s a
54 | `, fontSize, fontSize, fontSize, strings.Repeat("i", letters)))
55 | html := unpack1(page)
56 | body := unpack1(html)
57 | p := unpack1(body)
58 | tu.AssertEqual(t, len(p.Box().Children) == 1 || len(p.Box().Children) == 2, true)
59 | tu.AssertEqual(t, len(unpack1(p).Box().Children), 2)
60 | text := unpack1(p.Box().Children[0].Box().Children[1]).(*bo.TextBox).TextS()
61 | tu.AssertEqual(t, len(text) > 0, true)
62 | if strings.HasSuffix(text, "i") {
63 | break
64 | } else {
65 | letters += 1
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/html/layout/min_max.go:
--------------------------------------------------------------------------------
1 | package layout
2 |
3 | import (
4 | pr "github.com/benoitkugler/webrender/css/properties"
5 | )
6 |
7 | type block struct {
8 | X, Y, Width, Height pr.Float
9 | }
10 |
11 | func (b block) ContainingBlock() (width, height pr.MaybeFloat) { return b.Width, b.Height }
12 |
13 | type containingBlock interface {
14 | ContainingBlock() (width, height pr.MaybeFloat)
15 | }
16 |
17 | type funcMinMax = func(box Box, context *layoutContext, containingBlock containingBlock) (bool, pr.Float)
18 |
19 | // Decorate a function that sets the used width of a box to handle
20 | // {min,max}-width.
21 | func handleMinMaxWidth(function funcMinMax) funcMinMax {
22 | wrapper := func(box Box, context *layoutContext, containingBlock containingBlock) (bool, pr.Float) {
23 | computedMarginL, computedMarginR := box.Box().MarginLeft, box.Box().MarginRight
24 | res1, res2 := function(box, context, containingBlock)
25 | if box.Box().Width.V() > box.Box().MaxWidth.V() {
26 | box.Box().Width = box.Box().MaxWidth
27 | box.Box().MarginLeft, box.Box().MarginRight = computedMarginL, computedMarginR
28 | res1, res2 = function(box, context, containingBlock)
29 | }
30 | if box.Box().Width.V() < box.Box().MinWidth.V() {
31 | box.Box().Width = box.Box().MinWidth
32 | box.Box().MarginLeft, box.Box().MarginRight = computedMarginL, computedMarginR
33 | res1, res2 = function(box, context, containingBlock)
34 | }
35 | return res1, res2
36 | }
37 | // wrapper.WithoutMinMax = function
38 | return wrapper
39 | }
40 |
41 | // Decorate a function that sets the used height of a box to handle
42 | // {min,max}-height.
43 | func handleMinMaxHeight(function funcMinMax) funcMinMax {
44 | wrapper := func(box Box, context *layoutContext, containingBlock containingBlock) (bool, pr.Float) {
45 | computedMarginT, computedMarginB := box.Box().MarginTop, box.Box().MarginBottom
46 | res1, res2 := function(box, context, containingBlock)
47 | if box.Box().Height.V() > box.Box().MaxHeight.V() {
48 | box.Box().Height = box.Box().MaxHeight
49 | box.Box().MarginTop, box.Box().MarginBottom = computedMarginT, computedMarginB
50 | res1, res2 = function(box, context, containingBlock)
51 | }
52 | if box.Box().Height.V() < box.Box().MinHeight.V() {
53 | box.Box().Height = box.Box().MinHeight
54 | box.Box().MarginTop, box.Box().MarginBottom = computedMarginT, computedMarginB
55 | res1, res2 = function(box, context, containingBlock)
56 | }
57 | return res1, res2
58 | }
59 | // wrapper.WithoutMinMax = function
60 | return wrapper
61 | }
62 |
--------------------------------------------------------------------------------
/html/tree/html5_ua_forms.css:
--------------------------------------------------------------------------------
1 | /* Default stylesheet for PDF forms */
2 |
3 | button, input, select, textarea {
4 | appearance: auto;
5 | }
6 |
--------------------------------------------------------------------------------
/html/tree/media_query.go:
--------------------------------------------------------------------------------
1 | package tree
2 |
3 | import (
4 | "github.com/benoitkugler/webrender/css/parser"
5 | "github.com/benoitkugler/webrender/logger"
6 | "github.com/benoitkugler/webrender/utils"
7 | )
8 |
9 | // Return the boolean evaluation of `queryList` for the given
10 | // `deviceMediaType`.
11 | func evaluateMediaQuery(queryList []string, deviceMediaType string) bool {
12 | // TODO: actual support for media queries, not just media types
13 | for _, query := range queryList {
14 | if query == "all" || query == deviceMediaType {
15 | return true
16 | }
17 | }
18 | return false
19 | }
20 |
21 | func parseMediaQuery(tokens []Token) []string {
22 | tokens = parser.RemoveWhitespace(tokens)
23 | if len(tokens) == 0 {
24 | return []string{"all"}
25 | } else {
26 | var media []string
27 | for _, part := range parser.SplitOnComma(tokens) {
28 | if len(part) == 1 {
29 | if ident, ok := part[0].(parser.Ident); ok {
30 | media = append(media, utils.AsciiLower(ident.Value))
31 | continue
32 | }
33 | }
34 |
35 | logger.WarningLogger.Printf("Expected a media type, got %s", parser.Serialize(part))
36 | return nil
37 | }
38 | return media
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/html/tree/target_test.go:
--------------------------------------------------------------------------------
1 | package tree
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func TestResumeStackEqual(t *testing.T) {
9 | for _, data := range []struct {
10 | s1, s2 ResumeStack
11 | expected bool
12 | }{
13 | {ResumeStack{}, nil, true},
14 | {ResumeStack{2: nil}, ResumeStack{2: ResumeStack{}}, true},
15 | {ResumeStack{3: nil}, ResumeStack{2: ResumeStack{}}, false},
16 | {ResumeStack{2: nil}, nil, false},
17 | {ResumeStack{2: nil, 3: nil}, ResumeStack{3: nil, 2: nil}, true},
18 | {ResumeStack{2: nil, 3: nil}, ResumeStack{3: nil}, false},
19 | } {
20 | if data.s1.Equals(data.s2) != data.expected {
21 | t.Fatalf("unexpected comparison for %v and %v", data.s1, data.s2)
22 | }
23 | fmt.Println(data.s1, data.s2)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/html/tree/tests_ua.css:
--------------------------------------------------------------------------------
1 | /*
2 | Simplified user-agent stylesheet for HTML5 in tests.
3 | */
4 | @page { background: white; bleed: 0; @footnote { margin: 0 } }
5 | html, body, div, h1, h2, h3, h4, ol, p, ul, hr, pre, section, article
6 | { display: block; }
7 | body { orphans: 1; widows: 1 }
8 | li { display: list-item }
9 | head { display: none }
10 | pre { white-space: pre }
11 | br:before { content: '\A'; white-space: pre-line }
12 | ol { list-style-type: decimal }
13 | ol, ul { counter-reset: list-item }
14 |
15 | table, x-table { display: table;
16 | box-sizing: border-box }
17 | tr, x-tr { display: table-row }
18 | thead, x-thead { display: table-header-group }
19 | tbody, x-tbody { display: table-row-group }
20 | tfoot, x-tfoot { display: table-footer-group }
21 | col, x-col { display: table-column }
22 | colgroup, x-colgroup { display: table-column-group }
23 | td, th, x-td, x-th { display: table-cell }
24 | caption, x-caption { display: table-caption }
25 |
26 | *[lang] { -weasy-lang: attr(lang) }
27 | a[href] { -weasy-link: attr(href) }
28 | a[name] { -weasy-anchor: attr(name) }
29 | *[id] { -weasy-anchor: attr(id) }
30 | h1 { bookmark-level: 1; bookmark-label: content(text) }
31 | h2 { bookmark-level: 2; bookmark-label: content(text) }
32 | h3 { bookmark-level: 3; bookmark-label: content(text) }
33 | h4 { bookmark-level: 4; bookmark-label: content(text) }
34 | h5 { bookmark-level: 5; bookmark-label: content(text) }
35 | h6 { bookmark-level: 6; bookmark-label: content(text) }
36 |
37 | ::marker { unicode-bidi: isolate; font-variant-numeric: tabular-nums }
38 |
39 | ::footnote-call { content: counter(footnote) }
40 | ::footnote-marker { content: counter(footnote) '.' }
41 |
--------------------------------------------------------------------------------
/images/images_test.go:
--------------------------------------------------------------------------------
1 | package images
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "testing"
7 |
8 | "github.com/benoitkugler/webrender/css/properties"
9 | "github.com/benoitkugler/webrender/svg"
10 | "github.com/benoitkugler/webrender/utils"
11 | )
12 |
13 | func TestLoadLocalImages(t *testing.T) {
14 | paths := []string{
15 | "../resources_test/blue.jpg",
16 | "../resources_test/icon.png",
17 | "../resources_test/pattern.gif",
18 | "../resources_test/pattern.svg",
19 | }
20 | for _, path := range paths {
21 | url, err := utils.PathToURL(path)
22 | if err != nil {
23 | t.Fatal(err)
24 | }
25 | out, err := getImageFromUri(utils.DefaultUrlFetcher, false, url, "", properties.SBoolFloat{String: "none"})
26 | if err != nil {
27 | t.Fatal(err)
28 | }
29 | fmt.Printf("%T\n", out)
30 | }
31 | }
32 |
33 | func TestSVGDisplayedSize(t *testing.T) {
34 | f, err := os.Open("../resources_test/pattern.svg")
35 | if err != nil {
36 | t.Fatal(err)
37 | }
38 | img, err := svg.Parse(f, "", nil, nil)
39 | if err != nil {
40 | t.Fatal(err)
41 | }
42 | w, h := img.DisplayedSize()
43 | if w != (svg.Value{V: 4, U: svg.Px}) {
44 | t.Fatalf("unexpected width %v", w)
45 | }
46 | if h != (svg.Value{V: 4, U: svg.Px}) {
47 | t.Fatalf("unexpected height %v", h)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/logger/logger.go:
--------------------------------------------------------------------------------
1 | // Package logger provides two log.Logger emitting progress status and warning
2 | // information.
3 | package logger
4 |
5 | import (
6 | "log"
7 | "os"
8 | )
9 |
10 | // ProgressLogger logs the main steps of the HTLM rendering.
11 | // It is purely informative and may be turned off safely.
12 | var ProgressLogger = log.New(os.Stdout, "webrender.progress: ", log.LstdFlags)
13 |
14 | // WarningLogger emits a warning for each non fatal error, like unsupported CSS
15 | // properties, font loading or URL resolutions errors.
16 | // It can be turned off safely, but it is a good source of information if the
17 | // rendering seems wrong.
18 | var WarningLogger = log.New(os.Stdout, "webrender.warning: ", log.Lmsgprefix)
19 |
--------------------------------------------------------------------------------
/macros/doc_cairo.py:
--------------------------------------------------------------------------------
1 | """ Import the docsstring of Context for cairocffi """
2 |
3 | import inspect
4 | import re
5 |
6 | import cairocffi
7 |
8 | from style_accessor import camel_case
9 |
10 | HEADER = """package goweasyprint
11 |
12 | // autogenerated from cairocffi.py
13 |
14 | type float = pr.Float
15 |
16 | // Drawer is the backend doing the actual drawing
17 | // operations
18 | type Drawer interface {{
19 | {meths}
20 | }}
21 | """
22 |
23 |
24 | meths = inspect.getmembers(cairocffi.Context, inspect.isfunction)
25 |
26 |
27 | def format_default(argspec):
28 | if not argspec.defaults:
29 | return None
30 | N = len(argspec.defaults)
31 | out = "//"
32 | for arg, default in zip(argspec.args[-N:], argspec.defaults):
33 | out += f" {arg} = {default}"
34 | return out
35 |
36 |
37 | def format_signature(argspec, type_args: dict):
38 | names = argspec.args
39 | if names[0] == "self":
40 | names = names[1:]
41 | names = [name + " " + type_args.get(name, "interface{}") for name in names]
42 | return ", ".join(names)
43 |
44 |
45 | def add_comments(lines: str):
46 | return "\n".join("// " + l for l in lines.splitlines())
47 |
48 |
49 | RE_TYPE = re.compile(r":type (\w+): (\w+)")
50 |
51 |
52 | def parse_type(doc: str):
53 | lines = doc.splitlines(True)
54 | out = ""
55 | d = {}
56 | for line in lines:
57 | match = RE_TYPE.search(line)
58 | if match:
59 | arg, type_ = match.group(1), match.group(2)
60 | if arg == "float": # inversion in doc string
61 | arg, type_ = type_, arg
62 | d[arg] = type_
63 | else:
64 | out += line
65 | return out, d
66 |
67 |
68 | out = ""
69 | for _, f in meths:
70 | name = camel_case(f.__name__)
71 | doc = inspect.getdoc(f)
72 | args = inspect.getfullargspec(f)
73 | if doc:
74 | doc, type_args = parse_type(doc)
75 | doc = add_comments(doc)
76 | default = format_default(args)
77 | fmt_args = format_signature(args, type_args)
78 | sig = f"{name}({fmt_args})"
79 | out += "\n" + doc + "\n"
80 | if default:
81 | out += default + "\n"
82 | out += sig + "\n"
83 |
84 | print(HEADER.format(meths=out))
85 |
--------------------------------------------------------------------------------
/macros/python_test_to_go.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import re
3 |
4 | INPUT = sys.argv[1]
5 |
6 |
7 | ATTRS = [
8 | "position",
9 | "width",
10 | "height",
11 | "margin",
12 | "children",
13 | "border",
14 | "columnGroups",
15 | "content",
16 | "colspan"
17 | ]
18 |
19 |
20 | def export_attibutes(line: str) -> str:
21 | for attr in ATTRS:
22 | line = line.replace("." + attr, ".Box()." + attr[0].upper() + attr[1:])
23 | return line
24 |
25 |
26 | def unpack_children(line: str) -> str:
27 | if not ("=" in line and line.endswith(".children\n")):
28 | return line
29 |
30 | children, box = line.split("=")
31 | childrenL = [s.strip() for s in children.split(",") if s.strip()]
32 | box = box[:-10]
33 | if len(childrenL) == 1:
34 | return f"{childrenL[0]} := {box}.children[0]\n"
35 | else:
36 | return f"{children} := unpack{len(childrenL)}({box})\n"
37 |
38 |
39 | def replace_triple_quotes(line: str) -> str:
40 | return line.replace('"""', '`')
41 |
42 |
43 | def replace_render_pages(line: str) -> str:
44 | line = line.replace("page, = renderPages(", "page := renderOnePage(t,")
45 | line = line.replace("pages = renderPages(", "pages := renderPages(t,")
46 | return line
47 |
48 |
49 | def replace_assert(line: str) -> str:
50 | if not ("assert" in line):
51 | return line
52 |
53 | line = line.strip()
54 | line = line.replace("assert ", "tu.AssertEqual(t, ")
55 | line = line.replace("==", ",")
56 |
57 | comment = ""
58 | if "//" in line:
59 | i = line.index("//")
60 | comment = line[i:]
61 | line = line[:i]
62 |
63 | line = re.sub(r" ([0-9]+)(\.([0-9]+))?",
64 | lambda x: "pr.Float(" + x.group(0) + ")", line)
65 |
66 | line = line + ') ' + comment
67 | return line + "\n"
68 |
69 |
70 | def replace_xfail(line: str) -> str:
71 | if line.startswith("@pytest.mark.xfail"):
72 | return "// xfail"
73 | return line
74 |
75 |
76 | def correct_bracket_comment(line: str) -> str:
77 | return line.replace("} //", "//")
78 |
79 |
80 | lines = []
81 | with open(INPUT) as f:
82 | for line in f.readlines():
83 | line = replace_xfail(line)
84 | line = correct_bracket_comment(line)
85 | line = replace_triple_quotes(line)
86 | line = replace_render_pages(line)
87 | line = unpack_children(line)
88 | line = export_attibutes(line)
89 | line = replace_assert(line)
90 | lines.append(line)
91 |
92 |
93 | final_lines = []
94 | for (i, line) in enumerate(lines):
95 | if not line.startswith("func Test"):
96 | final_lines.append(line)
97 | continue
98 |
99 | j = i-1
100 | while j > 0:
101 | previous_line = lines[j].strip()
102 | if previous_line == "":
103 | j -= 1
104 | continue
105 | elif previous_line != "}":
106 | final_lines.append("}\n")
107 | break
108 | else:
109 | break
110 | final_lines.append(line)
111 |
112 | print("".join(final_lines))
113 |
--------------------------------------------------------------------------------
/macros/python_to_go.py:
--------------------------------------------------------------------------------
1 | import re
2 | import sys
3 |
4 | refunc = re.compile("^(def )")
5 | respace = re.compile(" *")
6 | INPUT = sys.argv[1]
7 | indent_stack = []
8 | in_comment = False
9 | with open(INPUT) as f:
10 | s = ""
11 | for line in f.readlines():
12 | parts = line.split("_")
13 | newline = parts[0]
14 | for w in parts[1:]:
15 | if len(w) == 0:
16 | newline += "_"
17 | elif w[0] in (",", " ", ";"):
18 | newline += "_" + w
19 | else:
20 | newline += w[0].upper() + w[1:]
21 | if newline[0] == "#":
22 | newline = "//" + newline[1:]
23 | newline = newline.replace("'", '"')
24 | newline = newline.replace("True", "true")
25 | newline = newline.replace("False", "false")
26 | newline = newline.replace(" and ", " && ")
27 | newline = newline.replace(" or ", " || ")
28 | newline = newline.replace("# ", "// ")
29 | newline = newline.replace(" elif ", " else if ")
30 | newline = newline.replace(" in ", " := range ")
31 | newline = newline.replace("is not None", " != nil ")
32 | newline = newline.replace("is None", " == nil ")
33 | newline = newline.replace(" not ", " ! ")
34 | newline = refunc.sub("func ", newline)
35 |
36 | indent = len(respace.match(newline).group(0))
37 | if newline.strip() == '"""' or (
38 | (newline.strip().startswith('"""') or newline.strip().startswith('r"""')) and not newline.strip().endswith('"""')):
39 | in_comment = not in_comment
40 |
41 | if (not in_comment) and indent_stack:
42 | while indent_stack and indent < indent_stack[-1]:
43 | s += " " * indent_stack[-1] + "}\n"
44 | indent_stack.pop()
45 |
46 | if indent_stack and indent == indent_stack[-1]:
47 | newline = " " * indent + "} " + newline[indent:]
48 | indent_stack.pop()
49 |
50 | if (not in_comment) and newline.endswith(":\n"):
51 | newline = newline[:-2] + " {\n"
52 | indent_stack.append(indent)
53 |
54 | s += newline
55 |
56 | lines = s.split("\n")
57 | re_comment = re.compile(' (r?)"""(.*)"""')
58 | out = []
59 | i = 0
60 | while i < len(lines):
61 | l = lines[i]
62 | if l.startswith("func "):
63 | m = re_comment.match(lines[i+1])
64 | if m:
65 | c = m.group(2)
66 | out.append("// " + c)
67 | i += 2
68 | elif lines[i+1].startswith(' """') or lines[i+1].startswith(' r"""'):
69 | out.append("// " + lines[i+1][7:])
70 | j = 2
71 | while i+j < len(lines) and not ('"""' in lines[i+j]):
72 | if lines[i+j]:
73 | out.append("// " + lines[i+j])
74 | j += 1
75 | if i+j < len(lines):
76 | out.append("// " + lines[i+j].replace('"""', ''))
77 | j += 1
78 | i = i+j
79 | else:
80 | i += 1
81 | out.append(l)
82 | else:
83 | out.append(l)
84 | i += 1
85 |
86 | with open(INPUT, "w") as f:
87 | f.write("\n".join(out))
88 |
--------------------------------------------------------------------------------
/macros/seriallized_boxes_to_go.py:
--------------------------------------------------------------------------------
1 | # This script is used to converted Python box tree to test references
2 | import pyperclip
3 | from typing import *
4 |
5 |
6 | def to_go(boxes: List[Any]) -> str:
7 | code = "[]SerBox{"
8 | for box in boxes:
9 | tag = box[0]
10 | type_ = f"{box[1]}BoxT"
11 | if isinstance(box[2], str):
12 | content = 'BC{{Text: `{0}`}}'.format(box[2])
13 | else:
14 | children = to_go(box[2])
15 | content = f"BC{{C: {children}}}"
16 | code += f"""{{"{tag}", {type_}, {content}}},\n"""
17 | code += "}"
18 | return code
19 |
20 |
21 | IN = []
22 |
23 | with open("/home/benoit/Téléchargements/WeasyPrint/tmp") as f:
24 | l = "tmp = " + f.read()
25 | loc: Dict[str, Any] = {}
26 | exec(l, globals(), loc)
27 | IN = loc["tmp"]
28 |
29 | pyperclip.copy(to_go(IN))
30 | print("Copied in clipboard.")
31 |
--------------------------------------------------------------------------------
/resources_test/AHEM____.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/resources_test/AHEM____.TTF
--------------------------------------------------------------------------------
/resources_test/acid2-reference.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The Second Acid Test (Reference Rendering)
5 |
12 |
13 |
14 | Hello World!
15 | 
16 |
17 |
--------------------------------------------------------------------------------
/resources_test/blue.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/resources_test/blue.jpg
--------------------------------------------------------------------------------
/resources_test/doc1.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
41 |
46 |
47 |
48 |
49 |
50 | WeasyPrint test document (with Ünicōde)
51 | Hello
53 |
54 | -
55 | Home
57 |
- …
58 |
59 |
60 |
61 | WeasyPrint
62 |
63 |
64 |
--------------------------------------------------------------------------------
/resources_test/doc1_UTF-16BE.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/resources_test/doc1_UTF-16BE.html
--------------------------------------------------------------------------------
/resources_test/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/resources_test/icon.png
--------------------------------------------------------------------------------
/resources_test/latin1-test.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/resources_test/latin1-test.css
--------------------------------------------------------------------------------
/resources_test/logo_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/resources_test/logo_small.png
--------------------------------------------------------------------------------
/resources_test/mini_ua.css:
--------------------------------------------------------------------------------
1 | /* Minimal user-agent stylesheet */
2 | p { margin: 1em 0px } /* 0px should be translated to 0*/
3 | a { text-decoration: underline }
4 | h1 { font-weight: bolder }
5 |
--------------------------------------------------------------------------------
/resources_test/pattern.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/resources_test/pattern.gif
--------------------------------------------------------------------------------
/resources_test/pattern.palette.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/resources_test/pattern.palette.png
--------------------------------------------------------------------------------
/resources_test/pattern.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/resources_test/pattern.png
--------------------------------------------------------------------------------
/resources_test/pattern.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/resources_test/preserveAspectRatio.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
21 |
22 |
23 |
24 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | a |
35 | a |
36 |
37 |
38 | a |
39 | a |
40 |
41 |
42 | a |
43 | a |
44 |
45 |
46 |
47 |
48 |
49 | a |
50 | a |
51 |
52 |
53 | a |
54 | a |
55 |
56 |
57 | a |
58 | a |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/resources_test/really-a-png.svg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/resources_test/really-a-png.svg
--------------------------------------------------------------------------------
/resources_test/really-a-svg.png:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/resources_test/rounded_rect_ref.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/resources_test/rounded_rect_ref.png
--------------------------------------------------------------------------------
/resources_test/sheet2.css:
--------------------------------------------------------------------------------
1 | li {
2 | margin-bottom: 3em; /* Should be masked*/
3 | margin: 2em 0;
4 | margin-left: 4em; /* Should not be masked*/
5 | }
6 |
--------------------------------------------------------------------------------
/resources_test/sub_directory/sheet1.css:
--------------------------------------------------------------------------------
1 | @import url(../sheet2.css) all;
2 | p {
3 | background: currentColor;
4 | }
5 |
6 | @media print {
7 | ul {
8 | /* 1ex == 0.8em for ahem. */
9 | margin: 2em 2.5ex;
10 | }
11 | }
12 | @media screen {
13 | ul {
14 | border-width: 1000px !important;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/resources_test/user.css:
--------------------------------------------------------------------------------
1 | html {
2 | /* Reversed contrast */
3 | color: white;
4 | background-color: black;
5 | }
6 |
--------------------------------------------------------------------------------
/resources_test/utf8-test.css:
--------------------------------------------------------------------------------
1 | h1::before {
2 | content: "I løvë Unicode";
3 | background-image: url(pattern.png)
4 | }
5 |
--------------------------------------------------------------------------------
/resources_test/weasyprint.otb_fixed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/resources_test/weasyprint.otb_fixed
--------------------------------------------------------------------------------
/resources_test/weasyprint.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/resources_test/weasyprint.otf
--------------------------------------------------------------------------------
/svg/bounding_box_test.go:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func Test_path_boundingBox(t *testing.T) {
9 | tests := []struct {
10 | p path
11 | want Rectangle
12 | want1 bool
13 | }{
14 | {
15 | // empty path
16 | want1: false,
17 | },
18 | {
19 | p: path{
20 | moveToF(10, 20),
21 | lineToF(30, 50),
22 | },
23 | want: Rectangle{10, 20, 20, 30},
24 | want1: true,
25 | },
26 | }
27 | for _, tt := range tests {
28 | got, got1 := tt.p.boundingBox(nil, drawingDims{})
29 | if !reflect.DeepEqual(got, tt.want) {
30 | t.Errorf("path.boundingBox() got = %v, want %v", got, tt.want)
31 | }
32 | if got1 != tt.want1 {
33 | t.Errorf("path.boundingBox() got1 = %v, want %v", got1, tt.want1)
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/svg/css_test.go:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | import (
4 | "reflect"
5 | "strings"
6 | "testing"
7 |
8 | "github.com/benoitkugler/webrender/utils"
9 | "golang.org/x/net/html"
10 | )
11 |
12 | func TestParseStyle(t *testing.T) {
13 | input := `
14 |
30 | `
31 | root, err := html.Parse(strings.NewReader(input))
32 | if err != nil {
33 | t.Fatal(err)
34 | }
35 | got, _ := fetchStyleAndTextRefs((*utils.HTMLNode)(root))
36 | if !reflect.DeepEqual(got, [][]byte{
37 | []byte("css1"),
38 | []byte("css2"),
39 | []byte("css4"),
40 | }) {
41 | t.Fatalf("unexpected stylesheets %v", got)
42 | }
43 | }
44 |
45 | func TestProcessStyle(t *testing.T) {
46 | input := `
47 |
58 | `
59 |
60 | root, err := html.Parse(strings.NewReader(input))
61 | if err != nil {
62 | t.Fatal(err)
63 | }
64 | got, _ := fetchStyleAndTextRefs((*utils.HTMLNode)(root))
65 | normal, important := parseStylesheets(got, "")
66 | if len(normal) != 1 {
67 | t.Fatalf("unexpected normal style: %v", normal)
68 | }
69 | if len(important) != 0 {
70 | t.Fatalf("unexpected important style: %v", important)
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/svg/paint_test.go:
--------------------------------------------------------------------------------
1 | package svg
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 |
7 | "github.com/benoitkugler/webrender/css/parser"
8 | )
9 |
10 | func Test_newPainter(t *testing.T) {
11 | tests := []struct {
12 | args string
13 | want painter
14 | wantErr bool
15 | }{
16 | {
17 | "red",
18 | painter{"", parser.ColorKeywords["red"].RGBA, true},
19 | false,
20 | },
21 | {
22 | "",
23 | painter{"", parser.RGBA{}, false},
24 | false,
25 | },
26 | {
27 | "none",
28 | painter{"", parser.RGBA{}, false},
29 | false,
30 | },
31 | {
32 | "black",
33 | painter{"", parser.RGBA{A: 1}, true},
34 | false,
35 | },
36 | {
37 | "url(ddd",
38 | painter{},
39 | true,
40 | },
41 | {
42 | "url(#myPaint)",
43 | painter{"myPaint", parser.RGBA{}, true},
44 | false,
45 | },
46 | {
47 | "url(#myPaint) green",
48 | painter{"myPaint", parser.ColorKeywords["green"].RGBA, true},
49 | false,
50 | },
51 | }
52 | for _, tt := range tests {
53 | got, err := newPainter(tt.args)
54 | if (err != nil) != tt.wantErr {
55 | t.Errorf("newPainter() error = %v, wantErr %v", err, tt.wantErr)
56 | return
57 | }
58 | if !reflect.DeepEqual(got, tt.want) {
59 | t.Errorf("newPainter() = %v, want %v", got, tt.want)
60 | }
61 | }
62 | }
63 |
64 | func Test_clampModulo(t *testing.T) {
65 | type args struct {
66 | offset Fl
67 | total Fl
68 | }
69 | tests := []struct {
70 | args args
71 | want Fl
72 | }{
73 | {args{10, 20}, 10},
74 | {args{11.2, 22.3}, 11.2},
75 | {args{-1, 22.3}, 21.3},
76 | {args{-10.5, 22.3}, 11.799999},
77 | {args{-30, 22.3}, 14.599998},
78 | }
79 | for _, tt := range tests {
80 | if got := clampModulo(tt.args.offset, tt.args.total); !reflect.DeepEqual(got, tt.want) {
81 | t.Errorf("clampModulo() = %v, want %v", got, tt.want)
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/svg/testdata/LICENSE:
--------------------------------------------------------------------------------
1 | Test icons in the testdata folder are from either www.freepik.com or www.flaticon.com. They request any wedsite using these icons place the following in thei web pages:
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/svg/testdata/OpacityStrokeDashTest.svg:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/svg/testdata/OpacityStrokeDashTest2.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/svg/testdata/OpacityStrokeDashTest3.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/svg/testdata/TestPercentages.svg:
--------------------------------------------------------------------------------
1 |
2 |
43 |
--------------------------------------------------------------------------------
/svg/testdata/TestShapes.svg:
--------------------------------------------------------------------------------
1 |
35 |
--------------------------------------------------------------------------------
/svg/testdata/TestShapes2.svg:
--------------------------------------------------------------------------------
1 |
25 |
--------------------------------------------------------------------------------
/svg/testdata/TestShapes3.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/svg/testdata/TestShapes4.svg:
--------------------------------------------------------------------------------
1 |
51 |
--------------------------------------------------------------------------------
/svg/testdata/TestShapes6.svg:
--------------------------------------------------------------------------------
1 |
2 |
45 |
--------------------------------------------------------------------------------
/svg/testdata/go-logo-blue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/archery.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/artistic_gymnastics.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
20 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/athletics.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/badminton.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
24 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/basketball.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
27 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/beach_volleyball.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
26 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/boxing.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
30 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/canoe_slalom.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
25 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/canoe_sprint.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
27 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/cycling_bmx.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
23 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/cycling_mountain_bike.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
29 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/cycling_road.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
29 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/cycling_track.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
30 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/diving.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
27 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/equestrian.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
24 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/fencing.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
22 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/football.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
24 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/golf.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
25 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/handball.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
22 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/hockey.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
26 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/judo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
29 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/marathon_swimming.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/modern_pentathlon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
31 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/olympic_medal_bronze.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
21 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/olympic_medal_gold.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/olympic_medal_silver.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
20 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/olympic_torch.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/readme.txt:
--------------------------------------------------------------------------------
1 | Freebie: Olympics Sports Icon Set (45 Icons, EPS, PDF, PNG, SVG)
2 |
3 | This freebie has been brought to you by SmashingMagazine.com.
4 |
5 | The icon set was created and designed by the Icons8 team (https://icons8.com/) exclusively for Smashing Magazine and its readers, and cannot be sold, redistributed or modified and reposted. However, icons can be used in both private and commercial projects.
6 |
7 |
8 | - - - - - - - - - - - - - - - - - -
9 |
10 | Dearest Smashing reader,
11 |
12 | Thank you for downloading this icon set. All icons are royalty-free. You can use them in your commercial as well as your personal works. You may modify the size, color or shape of the icons. No attribution is required. However, reselling of bundles or individual pictograms is prohibited.
13 |
14 | You may make one copy of the assets solely for backup or archival purposes or transfer the assets to a single hard drive, provided that you keep the original and accompanying documentation in your possession. You may enter projects into contests, film festivals, publications and or exhibitions that use the assets in the permitted listed methods.
15 |
16 | The icons may not be resold, sub-licensed, rented, transferred or otherwise made available for use. The icons may not be offered for free downloading from websites other than SmashingMagazine.com. Please link to the article in which this freebie was released if you would like to spread the word: https://www.smashingmagazine.com/2016/07/freebie-olympics-sports-icon-set-45-icons-eps-pdf-png-svg
17 |
18 |
19 |
20 | Sincerely yours,
21 | The Smashing Magazine Team
22 | www.smashingmagazine.com
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/rhythmic_gymnastics.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
36 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/rowing.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
34 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/rugby_sevens.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
23 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/sailing.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
21 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/shooting.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
24 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/swimming.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/synchronised_swimming.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
26 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/table_tennis.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
27 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/taekwondo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/tennis.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
32 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/trampoline_gymnastics.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
39 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/triathlon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
37 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/trophy.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
27 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/volleyball.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
22 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/water_polo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
--------------------------------------------------------------------------------
/svg/testdata/sportsIcons/weightlifting.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
37 |
--------------------------------------------------------------------------------
/svg/testdata/testIcons/24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/svg/testdata/testIcons/content-cut-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
56 |
--------------------------------------------------------------------------------
/svg/testdata/testIcons/defs.svg:
--------------------------------------------------------------------------------
1 |
29 |
--------------------------------------------------------------------------------
/text/draw/draw.go:
--------------------------------------------------------------------------------
1 | // Package draw use a backend and a layout object to
2 | // draw glyphs on the ouput.
3 | package draw
4 |
5 | import (
6 | "github.com/benoitkugler/webrender/backend"
7 | pr "github.com/benoitkugler/webrender/css/properties"
8 | "github.com/benoitkugler/webrender/text"
9 | "github.com/benoitkugler/webrender/utils"
10 | )
11 |
12 | type Context struct {
13 | Output backend.Canvas // where to draw the text
14 | Fonts text.FontConfiguration // used to find fonts
15 | }
16 |
17 | // CreateFirstLine create the text for the first line of [layout], starting at position `(x,y)`.
18 | // It also register the fonts used with [backend.Canvas.AddFont].
19 | func (ctx Context) CreateFirstLine(layout text.EngineLayout, textOverflow string, blockEllipsis pr.TaggedString, scaleX, x, y, angle pr.Fl,
20 | ) backend.TextDrawing {
21 | if layout, ok := layout.(*text.TextLayoutPango); ok {
22 | return ctx.createFirstLinePango(layout, textOverflow, blockEllipsis, scaleX, x, y, angle)
23 | }
24 | return backend.TextDrawing{}
25 | }
26 |
27 | // DrawEmoji loads and draws `glyph` onto `dst`.
28 | // It may be used by backend implementations to render emojis.
29 | func DrawEmoji(font backend.Font, glyph backend.GID, extents backend.GlyphExtents,
30 | fontSize, x, y, xAdvance utils.Fl, dst backend.Canvas,
31 | ) {
32 | if pFont, ok := font.(*pangoFont); ok {
33 | drawEmojiPango(pFont, glyph, extents, fontSize, x, y, xAdvance, dst)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/text/fonts.go:
--------------------------------------------------------------------------------
1 | package text
2 |
3 | import (
4 | pr "github.com/benoitkugler/webrender/css/properties"
5 | "github.com/benoitkugler/webrender/css/validation"
6 | "github.com/benoitkugler/webrender/text/hyphen"
7 | "github.com/benoitkugler/webrender/utils"
8 | )
9 |
10 | // FontOrigin is a reference to a binary font file, either
11 | // on disk or stored in memory.
12 | type FontOrigin struct {
13 | File string // The filename or identifier of the font file.
14 |
15 | // The index of the face in a collection. It is always 0 for
16 | // single font files.
17 | Index uint16
18 |
19 | // For variable fonts, stores 1 + the instance index.
20 | // (0 to ignore variations).
21 | Instance uint16
22 | }
23 |
24 | // FontConfiguration holds information about the
25 | // available fonts on the system.
26 | // It is used for text layout at various steps of the rendering process.
27 | //
28 | // It is implemented by and totaly tighted to text engines, either pango or go-text.
29 | type FontConfiguration interface {
30 | // FontContent returns the content of the given font, which may be needed
31 | // in the final output.
32 | FontContent(font FontOrigin) []byte
33 |
34 | // AddFontFace load a font file from an external source, using
35 | // the given [urlFetcher], which must be valid.
36 | //
37 | // It returns the file name of the loaded file.
38 | AddFontFace(ruleDescriptors validation.FontFaceDescriptors, urlFetcher utils.UrlFetcher) string
39 |
40 | // CanBreakText returns True if there is a line break strictly inside [t], False otherwise.
41 | // It should return nil if t has length < 2.
42 | CanBreakText(t []rune) pr.MaybeBool
43 |
44 | // returns the advance of the '0' char, using the font described by the given [style]
45 | width0(style *TextStyle) pr.Fl
46 | // returns the height of the 'x' char, using the font described by the given [style]
47 | heightx(style *TextStyle) pr.Fl
48 | // returns the height and baseline of a line containing a single space (" ")
49 | spaceHeight(style *TextStyle) (height, baseline pr.Float)
50 |
51 | splitFirstLine(hyphenCache map[HyphenDictKey]hyphen.Hyphener, text []rune, style *TextStyle,
52 | maxWidth pr.MaybeFloat, minimum, isLineStart bool) FirstLine
53 |
54 | // compute the unicode propery of the given runes,
55 | // returning a slice of length L + 1
56 | // the returned slice is readonly, and valid only until the
57 | // next call to runeProps
58 | // runeProps([]rune) []runeProp
59 | }
60 |
--------------------------------------------------------------------------------
/text/hyphen/datas.go:
--------------------------------------------------------------------------------
1 | package hyphen
2 |
3 | import (
4 | "embed"
5 | "fmt"
6 |
7 | "github.com/benoitkugler/textlayout/language"
8 | )
9 |
10 | //go:embed dictionaries
11 | var dictionaries embed.FS
12 |
13 | var languages map[language.Language]string
14 |
15 | func init() {
16 | var err error
17 | languages, err = getLanguages(dictionaries)
18 | if err != nil {
19 | panic(fmt.Errorf("hyphen: invalid embedded dict: %s", err))
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_af_ZA.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_af_ZA.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_cs_CZ.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_cs_CZ.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_da_DK.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_da_DK.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_de_AT.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_de_AT.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_de_CH.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_de_CH.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_de_DE.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_de_DE.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_el_GR.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_el_GR.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_et_EE.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_et_EE.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_gl.dic:
--------------------------------------------------------------------------------
1 | ISO8859-1
2 | LEFTHYPHENMIN 2
3 | RIGHTHYPHENMIN 2
4 | .odi1
5 | .o3v
6 | .g2
7 | .p2
8 | .ri1a
9 | .ru1
10 | .si1o
11 | .vi1a
12 | \'a1x
13 | \'{\i}1a
14 | \'{\i}1c
15 | \'o1d
16 | \'u1a
17 | a1\'{\i}
18 | a1a
19 | a1e1
20 | a1ia
21 | a1io
22 | a1ib
23 | a1o
24 | a1b
25 | a1c
26 | a1d
27 | a1f
28 | a1g
29 | a1h
30 | a1l
31 | a1m
32 | a2n1am
33 | 2ani
34 | a1p
35 | a1q
36 | a1r
37 | ar2l
38 | a1t
39 | a1v
40 | a1x
41 | a1z
42 | e1\'~n
43 | e1a
44 | e1e
45 | e1inc
46 | e1o
47 | e1un
48 | e1b
49 | e2bac
50 | e1c
51 | e1d
52 | e1f
53 | e1g
54 | e1h
55 | e1l
56 | e1m
57 | e1p
58 | e1q
59 | e1ra
60 | er1am
61 | e1re
62 | e1ri
63 | e1ro
64 | e1ru
65 | erce2
66 | e1t
67 | e1v
68 | e1x
69 | e1z
70 | i1\'~n
71 | i1ax
72 | i1ei
73 | i1oce
74 | i1or.
75 | i1osf
76 | i1ox
77 | 1iu
78 | i1b
79 | i1c
80 | i1d
81 | i1f
82 | i1g
83 | i1h
84 | i1k
85 | i1l
86 | i1m
87 | i1p
88 | ipe2
89 | i1q
90 | i1r
91 | i1t
92 | i1v
93 | i1x
94 | i1z
95 | o1a
96 | o1e
97 | o1ia
98 | o1io
99 | o1o
100 | o1b
101 | o1c
102 | oco2m
103 | o1d
104 | ode2s
105 | odi1o
106 | o1f
107 | o1g
108 | o1h
109 | o1k
110 | o1l
111 | o2lag
112 | o1m
113 | o1p
114 | o1q
115 | o1ra
116 | o1re
117 | o1ri
118 | o1ro
119 | o1t
120 | o1v
121 | o2vo
122 | o1x
123 | o1z
124 | u1ar.
125 | u1enz
126 | u1or
127 | u1b
128 | u2bad
129 | u1c
130 | u1d
131 | u1f
132 | u1g
133 | u1l
134 | u1m
135 | u1p
136 | uque2
137 | u1r
138 | u1t
139 | u1v
140 | u1x
141 | u1z
142 | 2b.
143 | bi2e
144 | bi1om
145 | 2b1of
146 | bu2b
147 | bu1q
148 | 2b1h
149 | 2b1s
150 | bser2
151 | 2b1x
152 | 2c.
153 | co1in
154 | co2be
155 | co2v
156 | co2x
157 | 2c1c
158 | 2c1d
159 | 2c1n
160 | cre2b
161 | 2c1s
162 | 2c1t
163 | di2q
164 | 2d1d
165 | 2d1v
166 | 2f.
167 | fa1i
168 | fi1a
169 | fi2a.
170 | fi2e
171 | fo2x
172 | 2f1t
173 | 2g.
174 | glo2b
175 | 2g1m
176 | 2g1n
177 | 2l.
178 | la2i1o
179 | le2o.
180 | li1an
181 | lo2i
182 | lo2ba
183 | lo2z
184 | 2l1b
185 | 2l1c
186 | 2l1d
187 | 2l1f
188 | 2l1g
189 | 2l1m
190 | 2l1n
191 | 2l1p
192 | 2l1q
193 | 2l1s
194 | 2l1t
195 | 2l1v
196 | 2l1x
197 | 2l1z
198 | 2m.
199 | ma2i1
200 | mo2mo
201 | 2m1b
202 | mbi2q
203 | mbo2l
204 | 2m1m
205 | 2m1n
206 | 2m1p
207 | 1na
208 | 1ne
209 | 1ni
210 | 1no
211 | no2pi
212 | 1nu
213 | n1c
214 | n1d
215 | n1f
216 | n1g
217 | n1l
218 | n1m
219 | n1n
220 | n1q
221 | n1r
222 | n1s
223 | n1t
224 | n1v
225 | n1x
226 | n1z
227 | 2p.
228 | per1r
229 | pes2q
230 | podi2
231 | 2p1n
232 | pri1o
233 | 2p1s
234 | 2p1t
235 | 2r.
236 | ra1ir
237 | 2rapt
238 | r2i
239 | ru1e
240 | 2r1b
241 | 2r1c
242 | 2r1d
243 | 2r1f
244 | 2r1g
245 | 2r1l
246 | 2r1m
247 | 2r1n
248 | 2r1p
249 | 2r1q
250 | 1rr
251 | 2r1s
252 | 2r1t
253 | 2r1v
254 | 2r1x
255 | 2r1z
256 | 2s.
257 | 1sa
258 | 1se
259 | 1si
260 | 1so
261 | 1su
262 | su1e
263 | s1b
264 | 2s1c
265 | s1d
266 | 2s1f
267 | s1g
268 | s1ho
269 | s1l
270 | s1m
271 | s1n
272 | 2s1p
273 | s1q
274 | 2s1t
275 | s1v
276 | 2t.
277 | tedi1
278 | 2t1ing
279 | to2pa
280 | tudi1
281 | 2t1m
282 | 2t1n
283 | tru2e
284 | vado1
285 | vi1ad
286 | 2x.
287 | 2x1c
288 | 2x1p
289 | 2x1t
290 | 2z.
291 |
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_hr_HR.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_hr_HR.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_lt.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_lt.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_lv_LV.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_lv_LV.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_nb_NO.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_nb_NO.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_nl_NL.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_nl_NL.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_nn_NO.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_nn_NO.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_pl_PL.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_pl_PL.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_pt_PT.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_pt_PT.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_sk_SK.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_sk_SK.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_sl_SI.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_sl_SI.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_sr.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_sr.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_sr_Latn.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_sr_Latn.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_sv.dic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitkugler/webrender/f697f6c187a4095a95dad3ddf05897fd2424398f/text/hyphen/dictionaries/hyph_sv.dic
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_te_IN.dic:
--------------------------------------------------------------------------------
1 | UTF-8
2 | % Hyphenation for Telugu
3 | % Copyright (C) 2008-2009 Santhosh Thottingal
4 | %
5 | % This library is free software; you can redistribute it and/or
6 | % modify it under the terms of the GNU General Public
7 | % License as published by the Free Software Foundation;
8 | % version 3 or later version of the License.
9 | %
10 | % This library is distributed in the hope that it will be useful,
11 | % but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 | % Lesser General Public License for more details.
14 | %
15 | % You should have received a copy of the GNU General Public
16 | % License along with this library; if not, write to the Free Software
17 | % Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 | %
19 | % GENERAL RULE
20 | % Do not break either side of ZERO-WIDTH JOINER (U+200D)
21 | 22
22 | % Break on both sides of ZERO-WIDTH NON JOINER (U+200C)
23 | 11
24 | % Break before or after any independent vowel.
25 | అ1
26 | ఆ1
27 | ఇ1
28 | ఈ1
29 | ఉ1
30 | ఊ1
31 | ఋ1
32 | ౠ1
33 | ఌ1
34 | ౡ1
35 | ఎ1
36 | ఏ1
37 | ఐ1
38 | ఒ1
39 | ఓ1
40 | ఔ1
41 | % Break after any dependent vowel, but not before.
42 | ా1
43 | ి1
44 | ీ1
45 | ు1
46 | ూ1
47 | ృ1
48 | ౄ1
49 | ె1
50 | ే1
51 | ై1
52 | ొ1
53 | ో1
54 | ౌ1
55 | % Break before or after any consonant.
56 | 1క
57 | 1ఖ
58 | 1గ
59 | 1ఘ
60 | 1ఙ
61 | 1చ
62 | 1ఛ
63 | 1జ
64 | 1ఝ
65 | 1ఞ
66 | 1ట
67 | 1ఠ
68 | 1డ
69 | 1ఢ
70 | 1ణ
71 | 1త
72 | 1థ
73 | 1ద
74 | 1ధ
75 | 1న
76 | 1ప
77 | 1ఫ
78 | 1బ
79 | 1భ
80 | 1మ
81 | 1య
82 | 1ర
83 | 1ఱ
84 | 1ల
85 | 1ళ
86 | 1వ
87 | 1శ
88 | 1ష
89 | 1స
90 | 1హ
91 | % Do not break before chandrabindu, anusvara, visarga,
92 | % length mark and ai length mark.
93 | 2ఁ1
94 | 2ం1
95 | 2ః1
96 | 2ౕ1
97 | 2ౖ1
98 | % Do not break either side of virama (may be within conjunct).
99 | 2్2
100 |
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/hyph_zu_ZA.dic:
--------------------------------------------------------------------------------
1 | ISO8859-1
2 | % Ukwahlukanisela ngekhonco isiZulu: Ukulandisa kwokusebenza ne-OpenOffice.org
3 | % Hyphenation for Zulu: Version for OpenOffice.org
4 | % Copyright (C) 2005, 2007 Friedel Wolff
5 | %
6 | % This library is free software; you can redistribute it and/or
7 | % modify it under the terms of the GNU Lesser General Public
8 | % License as published by the Free Software Foundation;
9 | % version 2.1 of the License.
10 | %
11 | % This library is distributed in the hope that it will be useful,
12 | % but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 | % Lesser General Public License for more details.
15 | %
16 | % You should have received a copy of the GNU Lesser General Public
17 | % License along with this library; if not, write to the Free Software
18 | % Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 | %
20 |
21 | a1
22 | e1
23 | i1
24 | o1
25 | u1
26 | %is'thandwa njalonjalo
27 | '1
28 |
29 | %iziphambuko ngenxa yamagama esiBhunu
30 | 1be2rg.
31 | be1
32 | 1bu2rg.
33 | bu1
34 | 1da2l.
35 | da1
36 | 1do2rp.
37 | do1
38 | %angazi ngale: Modder-fo-ntein?
39 | 1fonte2i2n.
40 | fo1
41 | 1ho2e2k.
42 | 1ho2f.
43 | ho1
44 | 1klo2o2f.
45 | klo1
46 | 1ko2p.
47 | ko1
48 | 1kra2ns.
49 | kra1
50 | 1kro2o2n.
51 | kro1
52 | 1kru2i2n.
53 | kru1
54 | 1la2nd.
55 | la1
56 | 1pa2rk.
57 | pa1
58 | 1ple2i2n.
59 | ple1
60 | 1po2o2rt.
61 | po1
62 | 1ra2nd.
63 | ra1
64 | 1rivi2er.
65 | ri1
66 | 1spru2i2t.
67 | spru1
68 | 1sta2d.
69 | sta1
70 | 1stra2nd.
71 | stra1
72 |
73 | %ukukhombisa
74 | 1no2o2rd.
75 | no1
76 | 1o2o2s.
77 | 1su2i2d.
78 | su1
79 | 1we2s.
80 | we1
81 |
82 | %iziphambuko ngenxa yamagama esiNgisi
83 | 1ba2y.
84 | ba1
85 | be2a2ch
86 | e2a2ch.
87 | cli2ffe.
88 | 1da2le.
89 | 1fi2e2ld.
90 | fi1
91 | %... Hill
92 | i2ll.
93 | 1me2a2d.
94 | %1pa2rk. - bona isiBhunu
95 | 1ri2dge.
96 | %kodwa
97 | b2ri2dge.
98 | bri1
99 | 1to2n.
100 | 1to2wn.
101 | to1
102 | 1vi2e2w.
103 | 1vi2lle.
104 | vi1
105 | 1wo2o2d.
106 | wo1
107 |
108 | %ukukhombisa
109 | no2rth.
110 | e2a2st.
111 | so2u2th.
112 | so1
113 | we2st.
114 |
115 | %iziphambuko ngenxa yamagama esiSuthu
116 | a2ng.
117 | e2ng.
118 | i2ng.
119 | o2ng.
120 | u2ng.
121 |
122 | %iziphambuko ezinhlobonhlobo
123 | %mhlawumbe amaphutha okupela angazohlupa
124 | a2a1
125 | a2e1
126 | a2i1
127 | a2o1
128 | a2u1
129 | e2a1
130 | e2e1
131 | e2i1
132 | e2o1
133 | e2u1
134 | i2a1
135 | i2e1
136 | i2i1
137 | i2o1
138 | i2u1
139 | o2a1
140 | o2e1
141 | o2i1
142 | o2o1
143 | o2u1
144 | u2a1
145 | u2e1
146 | u2i1
147 | u2o1
148 | u2u1
149 |
150 | 2b.
151 | 2c.
152 | 2d.
153 | 2f.
154 | 2g.
155 | 2h.
156 | 2j.
157 | 2k.
158 | 2l.
159 | 2m.
160 | 2n.
161 | 2p.
162 | 2q.
163 | 2r.
164 | 2s.
165 | 2t.
166 | 2v.
167 | 2w.
168 | 2x.
169 | 2z.
170 |
171 |
172 |
--------------------------------------------------------------------------------
/text/hyphen/dictionaries/update.sh:
--------------------------------------------------------------------------------
1 | host='https://cgit.freedesktop.org/'
2 |
3 | for folder in `curl -s $host/libreoffice/dictionaries/tree/ | grep 'ls-dir' | cut -d "'" -f 6`; do
4 | for file in `curl -s $host$folder | grep 'ls-blob' | grep 'hyph_.*\.dic' | cut -d "'" -f 6`; do
5 | wget -N `echo $host$file | sed 's/tree/plain/'` &
6 | done
7 | done
8 |
9 | rename -- -Latn _Latn *-Latn.dic
10 | rename _ANY "" *_ANY.dic
11 |
--------------------------------------------------------------------------------
/text/style_test.go:
--------------------------------------------------------------------------------
1 | package text
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os"
7 | "sort"
8 | "testing"
9 |
10 | pr "github.com/benoitkugler/webrender/css/properties"
11 | tu "github.com/benoitkugler/webrender/utils/testutils"
12 | )
13 |
14 | func TestDefaultValues(t *testing.T) {
15 | ts := NewTextStyle(pr.InitialValues, false)
16 | tu.AssertEqual(t, ts.FontDescription.Family, []string{"serif"})
17 | tu.AssertEqual(t, ts.FontDescription.Style, FSyNormal)
18 | tu.AssertEqual(t, ts.FontDescription.Stretch, FSeNormal)
19 | tu.AssertEqual(t, ts.FontDescription.Weight, uint16(400))
20 | tu.AssertEqual(t, ts.FontDescription.Size, pr.Fl(16))
21 | tu.AssertEqual(t, ts.FontDescription.VariationSettings, []Variation(nil))
22 |
23 | tu.AssertEqual(t, ts.FontLanguageOverride, fontLanguageOverride{})
24 | tu.AssertEqual(t, ts.Lang, "")
25 | tu.AssertEqual(t, ts.TextDecorationLine, pr.Decorations(0))
26 | tu.AssertEqual(t, ts.WhiteSpace, WNormal)
27 | tu.AssertEqual(t, ts.LetterSpacing, pr.Fl(0))
28 | tu.AssertEqual(t, ts.FontFeatures, []Feature(nil))
29 | }
30 |
31 | func TestCollectStyles(t *testing.T) {
32 | t.Skip()
33 |
34 | f, err := os.Open("../html/document/styles.json")
35 | if err != nil {
36 | t.Fatal(err)
37 | }
38 | var styles []TextStyle
39 | err = json.NewDecoder(f).Decode(&styles)
40 | if err != nil {
41 | t.Fatal(err)
42 | }
43 | f.Close()
44 |
45 | fmt.Println(len(styles))
46 | m := map[string]FontDescription{}
47 | for _, sty := range styles {
48 | m[string(sty.FontDescription.binary(nil, true))] = sty.FontDescription
49 | }
50 | var desc []FontDescription
51 | for _, fd := range m {
52 | desc = append(desc, fd)
53 | }
54 | sort.Slice(desc, func(i, j int) bool { return string(desc[i].binary(nil, true)) < string(desc[j].binary(nil, true)) })
55 | f, err = os.Create("testdata/font_descriptions.json")
56 | if err != nil {
57 | t.Fatal(err)
58 | }
59 | enc := json.NewEncoder(f)
60 | enc.SetIndent(" ", " ")
61 | err = enc.Encode(desc)
62 | if err != nil {
63 | t.Fatal(err)
64 | }
65 | f.Close()
66 | }
67 |
--------------------------------------------------------------------------------
/utils/math.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "math"
5 | )
6 |
7 | func MinInt(x, y int) int {
8 | if x < y {
9 | return x
10 | }
11 | return y
12 | }
13 |
14 | func MaxInt(x, y int) int {
15 | if x > y {
16 | return x
17 | }
18 | return y
19 | }
20 |
21 | func MinF(x, y Fl) Fl {
22 | if x < y {
23 | return x
24 | }
25 | return y
26 | }
27 |
28 | func MaxF(x, y Fl) Fl {
29 | if x > y {
30 | return x
31 | }
32 | return y
33 | }
34 |
35 | type Fl = float32
36 |
37 | func Maxs(values ...Fl) Fl {
38 | max := values[0]
39 | for _, w := range values {
40 | if w > max {
41 | max = w
42 | }
43 | }
44 | return max
45 | }
46 |
47 | func Mins(values ...Fl) Fl {
48 | min := values[0]
49 | for _, w := range values {
50 | if w < min {
51 | min = w
52 | }
53 | }
54 | return min
55 | }
56 |
57 | func modLikePython(d, m int) int {
58 | var res int = d % m
59 | if (res < 0 && m > 0) || (res > 0 && m < 0) {
60 | return res + m
61 | }
62 | return res
63 | }
64 |
65 | func Round(x Fl) Fl { return Fl(math.Round(float64(x))) }
66 |
67 | func Floor(x Fl) Fl { return Fl(math.Floor(float64(x))) }
68 |
69 | func Ceil(x Fl) Fl { return Fl(math.Ceil(float64(x))) }
70 |
71 | // FloatModulo implements Python modulo for float numbers, like
72 | //
73 | // 4.456 % 3
74 | func FloatModulo(x Fl, i int) Fl {
75 | x2 := Floor(x)
76 | diff := x - x2
77 | return Fl(modLikePython(int(x2), i)) + diff
78 | }
79 |
80 | // RoundPrec rounds f with n digits precision
81 | func RoundPrec(f Fl, n int) Fl {
82 | n10 := math.Pow10(n)
83 | return Fl(math.Round(float64(f)*n10) / n10)
84 | }
85 |
86 | // Round6 rounds f with 6 digits precision
87 | func Round6(f Fl) Fl { return RoundPrec(f, 6) }
88 |
89 | // Hypot returns SQRT(a^2 + b^2)
90 | func Hypot(a, b Fl) Fl { return Fl(math.Hypot(float64(a), float64(b))) }
91 |
92 | func Abs(v int) int {
93 | if v < 0 {
94 | return -v
95 | }
96 | return v
97 | }
98 |
--------------------------------------------------------------------------------
/utils/math_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "math"
5 | "testing"
6 | )
7 |
8 | func equals(x, y Fl) bool {
9 | return math.Abs(float64(x-y)) < 1e-6
10 | }
11 |
12 | func TestModulo(t *testing.T) {
13 | if v := FloatModulo(4.456, 3); !equals(v, 1.456) {
14 | t.Errorf("expected 1.456, got %f", v)
15 | }
16 | if v := FloatModulo(-2.456, 3); !equals(v, 0.544) {
17 | t.Errorf("expected 0.544, got %f", v)
18 | }
19 | if v := FloatModulo(-8, 5); !equals(v, 2) {
20 | t.Errorf("expected 2, got %f", v)
21 | }
22 | if v := FloatModulo(45, 7); !equals(v, 3) {
23 | t.Errorf("expected 3, got %f", v)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/utils/testutils/logs.go:
--------------------------------------------------------------------------------
1 | package testutils
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "regexp"
7 | "strings"
8 | "testing"
9 |
10 | "github.com/benoitkugler/webrender/logger"
11 | )
12 |
13 | type capturedLogs struct {
14 | stack []string
15 | }
16 |
17 | func (c *capturedLogs) Write(p []byte) (int, error) {
18 | b := new(bytes.Buffer)
19 | i, err := b.Write(p)
20 | if err != nil {
21 | return i, err
22 | }
23 | c.stack = append(c.stack, strings.TrimSuffix(b.String(), "\n"))
24 | return i, nil
25 | }
26 |
27 | func CaptureLogs() *capturedLogs {
28 | out := capturedLogs{}
29 | logger.WarningLogger.SetOutput(&out)
30 | return &out
31 | }
32 |
33 | func (c capturedLogs) Logs() []string {
34 | return c.stack
35 | }
36 |
37 | // CheckEqual compares logs ignoring date time in logged.
38 | func (c capturedLogs) CheckEqual(refs []string, t *testing.T) {
39 | t.Helper()
40 |
41 | const prefixLength = len("webrender.warning: ")
42 | gots := c.Logs()
43 | if len(gots) != len(refs) {
44 | t.Fatalf("expected %d logs, got %d", len(refs), len(gots))
45 | }
46 | for i, ref := range refs {
47 | g := gots[i][prefixLength:]
48 | if g != ref {
49 | t.Fatalf("expected \n%s\n got \n%s", ref, g)
50 | }
51 | }
52 | }
53 |
54 | func (c *capturedLogs) AssertNoLogs(t *testing.T) {
55 | t.Helper()
56 |
57 | l := c.Logs()
58 | if len(l) > 0 {
59 | t.Fatalf("expected no logs, got (%d): \n %s", len(l), strings.Join(l, "\n"))
60 | }
61 | }
62 |
63 | // IndentLogger enable to write debug message with a tree structure.
64 | type IndentLogger struct {
65 | Color bool
66 | level int
67 | }
68 |
69 | // LineWithIndent prints the message with the given indent level, then increases it.
70 | func (il *IndentLogger) LineWithIndent(format string, args ...interface{}) {
71 | il.Line(format, args...)
72 | il.level++
73 | }
74 |
75 | // LineWithDedent decreases the level, then write the message.
76 | func (il *IndentLogger) LineWithDedent(format string, args ...interface{}) {
77 | il.level--
78 | il.Line(format, args...)
79 | }
80 |
81 | var reTag = regexp.MustCompile(`<(\S+)>`)
82 |
83 | func colorTag(s string) string {
84 | return reTag.ReplaceAllString(s, "\033[1;34m<$1>\033[0m")
85 | }
86 |
87 | // Line simply writes the message without changing the indentation.
88 | func (il *IndentLogger) Line(format string, args ...interface{}) {
89 | s := fmt.Sprintf(format, args...)
90 | if il.Color {
91 | s = colorTag(s)
92 | }
93 | fmt.Println(strings.Repeat(" ", il.level) + s)
94 | }
95 |
--------------------------------------------------------------------------------
/utils/testutils/tracer/tracer.go:
--------------------------------------------------------------------------------
1 | // Package tracer provides a function to dump the current layout tree,
2 | // which may be used in debug mode.
3 | package tracer
4 |
5 | import (
6 | "fmt"
7 | "os"
8 | "strconv"
9 | "strings"
10 |
11 | "github.com/benoitkugler/webrender/css/properties"
12 | "github.com/benoitkugler/webrender/html/boxes"
13 | "github.com/benoitkugler/webrender/utils"
14 | )
15 |
16 | type Tracer struct {
17 | out *os.File
18 | }
19 |
20 | // NewTracer panics if an error occurs.
21 | func NewTracer(outFile string) Tracer {
22 | f, err := os.Create(outFile)
23 | if err != nil {
24 | panic(err)
25 | }
26 |
27 | return Tracer{out: f}
28 | }
29 |
30 | func FormatMaybeFloat(v properties.MaybeFloat) string {
31 | if v, ok := v.(properties.Float); ok {
32 | return strconv.FormatFloat(float64(utils.RoundPrec(float32(v), 1)), 'g', -1, 32)
33 | }
34 | return fmt.Sprintf("%v", v)
35 | }
36 |
37 | func (t Tracer) Dump(line string) {
38 | fmt.Fprintln(t.out, line)
39 | }
40 |
41 | func (t Tracer) DumpTree(box boxes.Box, context string) {
42 | fmt.Fprintln(t.out, context)
43 |
44 | var printer func(box boxes.Box, indent int)
45 | printer = func(box boxes.Box, indent int) {
46 | fmt.Fprint(t.out, strings.Repeat(" ", indent))
47 | fmt.Fprintf(t.out, "%s: %s %s %s %s ; %s %s %s %s ; %s %s %s %s\n", box.Type(),
48 | FormatMaybeFloat(box.Box().PositionX),
49 | FormatMaybeFloat(box.Box().PositionY),
50 | FormatMaybeFloat(box.Box().Width),
51 | FormatMaybeFloat(box.Box().Height),
52 |
53 | FormatMaybeFloat(box.Box().MarginBottom),
54 | FormatMaybeFloat(box.Box().MarginTop),
55 | FormatMaybeFloat(box.Box().MarginRight),
56 | FormatMaybeFloat(box.Box().MarginLeft),
57 |
58 | FormatMaybeFloat(box.Box().BorderBottomWidth),
59 | FormatMaybeFloat(box.Box().BorderTopWidth),
60 | FormatMaybeFloat(box.Box().BorderRightWidth),
61 | FormatMaybeFloat(box.Box().BorderLeftWidth),
62 | )
63 | if tb, ok := box.(*boxes.TextBox); ok {
64 | fmt.Fprintln(t.out, tb.TextS())
65 | }
66 |
67 | for _, child := range box.Box().Children {
68 | printer(child, indent+1)
69 | }
70 | }
71 |
72 | printer(box, 0)
73 |
74 | fmt.Fprintln(t.out)
75 | }
76 |
--------------------------------------------------------------------------------
/utils/testutils/utils.go:
--------------------------------------------------------------------------------
1 | package testutils
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func AssertEqual(t *testing.T, got, exp interface{}) {
9 | t.Helper()
10 | if !reflect.DeepEqual(exp, got) {
11 | t.Fatalf("expected\n%v\n got \n%v", exp, got)
12 | }
13 | }
14 |
15 | func AssertNoErr(t *testing.T, err error) {
16 | t.Helper()
17 | if err != nil {
18 | t.Fatalf("unexpected error %s", err)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "hash/fnv"
4 |
5 | var Has = struct{}{}
6 |
7 | type Set map[string]struct{}
8 |
9 | func (s Set) Add(key string) {
10 | s[key] = Has
11 | }
12 |
13 | func (s Set) Extend(keys []string) {
14 | for _, key := range keys {
15 | s[key] = Has
16 | }
17 | }
18 |
19 | func (s Set) Has(key string) bool {
20 | _, in := s[key]
21 | return in
22 | }
23 |
24 | // Copy returns a deepcopy.
25 | func (s Set) Copy() Set {
26 | out := make(Set, len(s))
27 | for k, v := range s {
28 | out[k] = v
29 | }
30 | return out
31 | }
32 |
33 | func (s Set) IsNone() bool { return s == nil }
34 |
35 | func (s Set) Equal(other Set) bool {
36 | if len(s) != len(other) {
37 | return false
38 | }
39 | for i := range s {
40 | if _, in := other[i]; !in {
41 | return false
42 | }
43 | }
44 | return true
45 | }
46 |
47 | func NewSet(values ...string) Set {
48 | s := make(Set, len(values))
49 | for _, v := range values {
50 | s.Add(v)
51 | }
52 | return s
53 | }
54 |
55 | // Hash creates an ID from a string.
56 | func Hash(s string) int {
57 | h := fnv.New32()
58 | h.Write([]byte(s))
59 | return int(h.Sum32())
60 | }
61 |
62 | func IsIn(l []string, s string) bool {
63 | for _, v := range l {
64 | if v == s {
65 | return true
66 | }
67 | }
68 | return false
69 | }
70 |
--------------------------------------------------------------------------------
/utils/version.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | const (
8 | Version = "0.62"
9 | )
10 |
11 | // Used for "User-Agent" in HTTP
12 | var VersionString = fmt.Sprintf("Go-WebRender %s", Version)
13 |
14 | // commit of the Python reference implementation d5d7ce369aef035712cf73446f9085a32105846f
15 |
--------------------------------------------------------------------------------