├── .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 |

Follow this link to view the reference image, which should be rendered below the text "Hello World!" on the test page in the same way that this paragraph is rendered below that text on this page.

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 | 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 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /resources_test/preserveAspectRatio.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 |
23 | 24 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
aa
aa
aa
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
aa
aa
aa
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 | 2 | 3 | 4 | 5 | 6 | 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 | 16 | Example triangle01- simple example of a 'path' 17 | Testing dashes around a square. 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 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 | 49 | Example triangle01- simple example of a 'path' 50 | Testing dashes around a square. 51 | 52 | 57 | 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 |
Icons made by Freepik from www.flaticon.com is licensed by CC 3.0 BY
4 | 5 |
Icons made by Flat Icons from www.flaticon.com is licensed by CC 3.0 BY
6 | -------------------------------------------------------------------------------- /svg/testdata/OpacityStrokeDashTest.svg: -------------------------------------------------------------------------------- 1 | 3 | Example triangle01- simple example of a 'path' 4 | Testing dashes around a square. 5 | 6 | 8 | 9 | 11 | 12 | 14 | 15 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /svg/testdata/OpacityStrokeDashTest2.svg: -------------------------------------------------------------------------------- 1 | 3 | dash lines on squares 4 | dash lines on squares 5 | 6 | Testing dashes around a square. 7 | 8 | 10 | 11 | 13 | 14 | 16 | 17 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /svg/testdata/OpacityStrokeDashTest3.svg: -------------------------------------------------------------------------------- 1 | 3 | dash lines on squares 4 | dash lines on squares 5 | 6 | Testing dashes around a square. 7 | 8 | 10 | 11 | 13 | 14 | 16 | 17 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /svg/testdata/TestPercentages.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | Example Units 5 | Illustrates various units options 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /svg/testdata/TestShapes.svg: -------------------------------------------------------------------------------- 1 | 3 | Example triangle01- simple example of a 'path' 4 | A path that draws a triangle 5 | 6 | 8 | 9 | 11 | 12 | 14 | 16 | 17 | 19 | 20 | 21 | 23 | 25 | 27 | 28 | 30 | 31 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /svg/testdata/TestShapes2.svg: -------------------------------------------------------------------------------- 1 | 3 | Example triangle01- simple example of a 'path' 4 | A path that draws a triangle 5 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /svg/testdata/TestShapes3.svg: -------------------------------------------------------------------------------- 1 | 3 | Example triangle01- simple example of a 'path' 4 | A path that draws a triangle 5 | 6 | 9 | 10 | 13 | 14 | 17 | 18 | -------------------------------------------------------------------------------- /svg/testdata/TestShapes4.svg: -------------------------------------------------------------------------------- 1 | 3 | Test 4 4 | Test line cap miter and arc clip modes, different cap and gap functions 5 | 6 | 7 | 9 | 11 | 13 | 14 | 16 | 18 | 20 | 21 | 22 | 24 | 25 | 27 | 28 | 30 | 31 | 33 | 34 | 36 | 37 | 39 | 40 | 42 | 43 | 45 | 46 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /svg/testdata/TestShapes6.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /svg/testdata/go-logo-blue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/archery.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 10 | 11 | 13 | 14 | 16 | 18 | 19 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/artistic_gymnastics.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/athletics.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 13 | 14 | 16 | 18 | 19 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/badminton.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/basketball.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 13 | 14 | 16 | 18 | 21 | 24 | 26 | 27 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/beach_volleyball.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 12 | 14 | 16 | 21 | 23 | 25 | 26 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/boxing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 11 | 13 | 14 | 16 | 20 | 22 | 24 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/canoe_slalom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 10 | 15 | 16 | 18 | 20 | 22 | 24 | 25 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/canoe_sprint.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 17 | 21 | 23 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/cycling_bmx.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/cycling_mountain_bike.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 26 | 28 | 29 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/cycling_road.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 28 | 29 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/cycling_track.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/diving.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 11 | 12 | 13 | 14 | 16 | 18 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/equestrian.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 11 | 12 | 13 | 14 | 15 | 17 | 19 | 20 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/fencing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/football.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 13 | 14 | 15 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/golf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 11 | 18 | 19 | 21 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/handball.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 14 | 15 | 16 | 19 | 21 | 22 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/hockey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 14 | 15 | 18 | 21 | 23 | 25 | 26 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/judo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 11 | 16 | 22 | 25 | 28 | 29 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/marathon_swimming.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 10 | 12 | 14 | 16 | 17 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/modern_pentathlon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/olympic_medal_bronze.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/olympic_medal_gold.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/olympic_medal_silver.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/olympic_torch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 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 | 5 | 9 | 11 | 14 | 15 | 18 | 19 | 21 | 28 | 29 | 33 | 35 | 36 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/rowing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 22 | 24 | 26 | 33 | 34 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/rugby_sevens.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 11 | 12 | 15 | 16 | 18 | 20 | 22 | 23 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/sailing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 14 | 15 | 16 | 18 | 20 | 21 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/shooting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 12 | 14 | 18 | 19 | 20 | 21 | 23 | 24 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/swimming.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 10 | 11 | 13 | 15 | 16 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/synchronised_swimming.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 10 | 12 | 14 | 15 | 16 | 18 | 20 | 21 | 23 | 25 | 26 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/table_tennis.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 17 | 19 | 20 | 21 | 23 | 24 | 26 | 27 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/taekwondo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 11 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/tennis.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 10 | 11 | 21 | 22 | 23 | 24 | 26 | 29 | 31 | 32 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/trampoline_gymnastics.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 13 | 15 | 16 | 19 | 22 | 24 | 25 | 26 | 27 | 29 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/triathlon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/trophy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/volleyball.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 13 | 15 | 16 | 17 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/water_polo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 16 | 18 | 19 | -------------------------------------------------------------------------------- /svg/testdata/sportsIcons/weightlifting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 13 | 14 | 15 | 16 | 18 | 20 | 21 | 22 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /svg/testdata/testIcons/24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /svg/testdata/testIcons/content-cut-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 27 | 29 | 50 | 55 | 56 | -------------------------------------------------------------------------------- /svg/testdata/testIcons/defs.svg: -------------------------------------------------------------------------------- 1 | 3 |   4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |   15 |   16 |   17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 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 | 2‍2 22 | % Break on both sides of ZERO-WIDTH NON JOINER (U+200C) 23 | 1‌1 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 | --------------------------------------------------------------------------------