├── .drone.yml ├── .gitattributes ├── .gitignore ├── .jvmopts ├── .travis.yml ├── LICENSE ├── README.md ├── build.sbt ├── build.toml ├── build211.toml ├── build212.toml ├── build213.toml ├── cache ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLAreaElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLBRElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLBaseElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLBodyElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLButtonElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLContentElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLDListElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLDataListElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLDetailsElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLDialogElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLDivElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLEmbedElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLFormElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLHRElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLHeadElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLHeadingElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLHtmlElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLIFrameElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLImageElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLInputElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLLIElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLLabelElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLLinkElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLMenuItemElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLMetaElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLMeterElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLModElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLOListElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLObjectElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLOptGroupElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLOutputElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLParagraphElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLParamElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLPreElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLProgressElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLShadowElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLSpanElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLStyleElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLTableCaptionElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLTableColElement.html ├── https___developer.mozilla.org_en-US_docs_Web_API_HTMLTemplateElement.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_Heading_Elements.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_a.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_acronym.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_address.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_applet.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_area.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_article.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_audio.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_b.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_base.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_basefont.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_big.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_blink.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_body.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_br.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_button.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_canvas.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_caption.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_center.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_code.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_col.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_colgroup.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_content.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_datalist.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_dd.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_decorator.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_del.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_details.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_dialog.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_dir.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_div.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_dl.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_dt.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_element.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_em.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_embed.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_fieldset.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_figcaption.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_figure.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_footer.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_form.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_frame.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_frameset.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_h1.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_h2.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_h3.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_h4.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_h5.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_h6.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_head.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_header.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_hgroup.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_hr.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_html.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_i.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_iframe.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_img.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_input.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_ins.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_isindex.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_keygen.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_label.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_legend.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_li.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_link.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_listing.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_main.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_map.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_menu.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_menuitem.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_meta.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_meter.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_nav.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_noembed.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_noscript.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_object.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_ol.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_optgroup.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_option.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_output.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_p.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_param.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_plaintext.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_pre.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_progress.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_script.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_section.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_select.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_shadow.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_small.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_source.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_spacer.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_span.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_strike.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_strong.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_style.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_summary.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_table.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_tbody.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_td.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_template.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_textarea.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_tfoot.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_th.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_thead.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_title.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_tr.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_track.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_tt.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_ul.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_video.html ├── https___developer.mozilla.org_en-US_docs_Web_HTML_Element_xmp.html └── https___developer.mozilla.org_en-US_docs_Web_HTML_Global_attributes.html ├── docs ├── 1-tree-construction.md ├── 2-tree-updates.md ├── 3-web-development.md ├── 4-development.md └── 5-manual.md ├── manual.toml ├── project ├── MDNParser.scala ├── build.properties ├── build.sbt └── plugins.sbt ├── src ├── bench │ ├── js │ │ └── pine │ │ │ └── bench │ │ │ ├── PlatformBench.scala │ │ │ └── PlatformUtil.scala │ ├── jvm │ │ └── pine │ │ │ └── bench │ │ │ ├── PlatformBench.scala │ │ │ └── PlatformUtil.scala │ └── shared │ │ └── pine │ │ └── bench │ │ ├── Bench.scala │ │ ├── BenchUtil.scala │ │ ├── SharedBench.scala │ │ └── TreeBench.scala ├── main │ ├── scala-js │ │ └── pine │ │ │ └── dom │ │ │ ├── DOM.scala │ │ │ ├── DiffRender.scala │ │ │ ├── Document.scala │ │ │ ├── Event.scala │ │ │ ├── Implicits.scala │ │ │ ├── Js.scala │ │ │ ├── NodeRender.scala │ │ │ ├── Window.scala │ │ │ └── package.scala │ ├── scala-jvm │ │ └── pine │ │ │ └── GenerateEntities.scala │ └── scala │ │ └── pine │ │ ├── Attribute.scala │ │ ├── Diff.scala │ │ ├── DiffRender.scala │ │ ├── HtmlEntities.scala │ │ ├── HtmlHelpers.scala │ │ ├── Node.scala │ │ ├── ParseError.scala │ │ ├── Parser.scala │ │ ├── Reader.scala │ │ ├── RenderContext.scala │ │ ├── TagRef.scala │ │ ├── TagRender.scala │ │ ├── XmlEntities.scala │ │ ├── dsl │ │ ├── Display.scala │ │ └── Imports.scala │ │ ├── macros │ │ ├── ExternalHtml.scala │ │ ├── Helpers.scala │ │ └── InlineHtml.scala │ │ ├── package.scala │ │ └── tag │ │ ├── Attributes.scala │ │ └── package.scala └── test │ ├── html │ ├── list.html │ ├── test.html │ └── test2.html │ ├── scala-js │ └── pine │ │ └── dom │ │ ├── DOMSpec.scala │ │ └── TagRefSpec.scala │ └── scala │ └── pine │ ├── BindingsSpec.scala │ ├── DiffSpec.scala │ ├── ExternalHtmlSpec.scala │ ├── HtmlHelpersSpec.scala │ ├── HtmlParserSpec.scala │ ├── ImplicitsSpec.scala │ ├── InlineHtmlSpec.scala │ ├── NodePropSpec.scala │ ├── NodeSpec.scala │ ├── TagRefSpec.scala │ ├── TextSpec.scala │ └── XmlParserSpec.scala └── version.sbt /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | name: default 3 | steps: 4 | - name: build 5 | image: tindzk/seed:0.1.7 6 | commands: 7 | - apk add --no-cache yarn 8 | - yarn add jsdom object-sizeof 9 | # Patch Bloop version to support Scala.js 1.0 10 | - apk add --no-cache curl && curl -L https://github.com/scalacenter/bloop/releases/download/v1.4.0-RC1/install.py | python - -d /usr/bin/ -v 1.4.0-RC1-229-b7c15aa9 || true 11 | - blp-server & 12 | - seed --build=build211.toml bloop 13 | - bloop compile pine-native 14 | - bloop test pine-jvm 15 | - sleep 5 # Synchronise analysis.bin files, otherwise rm might fail 16 | - rm -rf .bloop build 17 | - seed --build=build212.toml bloop 18 | - bloop test pine-jvm pine-js 19 | - sleep 5 20 | - rm -rf .bloop build 21 | - seed --build=build213.toml bloop 22 | - bloop test pine-jvm pine-js 23 | - bloop run pine-bench-jvm -- fast 24 | - bloop run pine-bench-js -- fast 25 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | cache/* -diff 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache/ 6 | .history/ 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # Scala-IDE specific 16 | .scala_dependencies 17 | .worksheet 18 | .idea 19 | 20 | node_modules/ 21 | .bloop/ 22 | .metals/ 23 | *.DS_Store 24 | /package-lock.json 25 | /build/ 26 | /seed 27 | -------------------------------------------------------------------------------- /.jvmopts: -------------------------------------------------------------------------------- 1 | -Xms768m 2 | -Xmx1536m 3 | -Xss2m 4 | -XX:+UseG1GC 5 | -XX:+CMSClassUnloadingEnabled -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 3 | - 2.11.11-bin-typelevel-4 4 | jdk: 5 | - openjdk8 6 | 7 | # See https://github.com/scala-js/scala-js/issues/2642 8 | install: 9 | - . $HOME/.nvm/nvm.sh 10 | - nvm install stable 11 | - nvm use stable 12 | - npm install 13 | - npm install jsdom 14 | - curl https://raw.githubusercontent.com/scala-native/scala-native/master/scripts/travis_setup.sh | bash -x 15 | 16 | # See https://github.com/scala-js/scala-js/issues/2642 17 | script: 18 | sbt 'set parallelExecution in ThisBuild := false' pineJVM2_11/test pineNative2_11/test pineJS2_12/test pineJVM2_12/test pineJS2_13/test pineJVM2_13/test 19 | 20 | # From http://www.scala-sbt.org/0.13/docs/Travis-CI-with-sbt.html 21 | # Use container-based infrastructure 22 | sudo: false 23 | # These directories are cached to S3 at the end of the build 24 | cache: 25 | directories: 26 | - $HOME/.ivy2/cache 27 | - $HOME/.sbt 28 | before_cache: 29 | # Cleanup the cached directories to avoid unnecessary cache updates 30 | - find $HOME/.ivy2/cache -name "ivydata-*.properties" -print -delete 31 | - find $HOME/.sbt -name "*.lock" -print -delete 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2019 sparse.tech OÜ 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2 | [![Build Status](https://travis-ci.org/sparsetech/pine.svg?branch=master)](https://travis-ci.org/sparsetech/pine) 3 | [![Build Status](http://ci.sparse.tech/api/badges/sparsetech/pine/status.svg)](http://ci.sparse.tech/sparsetech/pine) 4 | [![Maven Central](https://img.shields.io/maven-central/v/tech.sparse/pine_2.12.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22tech.sparse%22%20AND%20a%3A%22pine_2.12%22) 5 | 6 | Pine is a functional HTML5 and XML library for the Scala platform. It supports parsing, manipulating and rendering of HTML. Pine provides type-safe bindings for HTML5 generated from [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element). Tree nodes are immutable and can be constructed from compile-time or runtime HTML/XML content. The tree may be manipulated and rendered back as HTML or as a browser node. 7 | 8 | ## Features 9 | * Immutable and typed trees 10 | * Type-safe bindings, generated from MDN 11 | * Support for custom elements and attributes 12 | * HTML5 and XML parser based on recursive descent 13 | * Macros for compile-time HTML string/file parsing 14 | * Tree manipulation 15 | * Rendering as HTML strings 16 | * DSL for common operations 17 | * Unit and property-based tests 18 | * Few dependencies 19 | 20 | ### JavaScript back end 21 | * Rendering as DOM nodes 22 | * Updating nodes in DOM 23 | * DSL for attaching/detaching events 24 | 25 | ## Compatibility 26 | | Platform | Platform version | Scala versions | 27 | |:-------------|:-----------------|:-----------------------------| 28 | | JVM | | 2.11 (T), 2.12 (T), 2.13 (L) | 29 | | Scala.js | 1.0 | 2.12 (T), 2.13 (L) | 30 | | Scala Native | 0.4.0-M2 | 2.11 (T) | 31 | 32 | * (T): Typelevel Scala 33 | * (L): Lightbend Scala 34 | 35 | ## Examples 36 | ```scala 37 | import pine._ 38 | 39 | val url = "http://github.com/" 40 | val root = html"GitHub" 41 | 42 | println(root.toHtml) // GitHub 43 | ``` 44 | 45 | ### JavaScript 46 | ```scala 47 | import pine.dom._ 48 | println(root.toDom) // [object HTMLAnchorElement] 49 | ``` 50 | 51 | ## sbt 52 | Pine makes use of a language extension called *literal types*, see [SIP-23](http://docs.scala-lang.org/sips/pending/42.type.html). For Scala 2.11 and 2.12, only [Typelevel Scala](https://github.com/typelevel/scala) implements this feature. However, it is available in Lightbend Scala from 2.13 onwards. 53 | 54 | ### 2.13 onwards 55 | ```scala 56 | scalaVersion := "2." 57 | libraryDependencies += scalaOrganization.value % "scala-reflect" % scalaVersion.value 58 | ``` 59 | 60 | ### < 2.13 61 | ```scala 62 | scalaVersion := "2.12.4-bin-typelevel-4" // or "2.11.11-bin-typelevel-4" 63 | scalaOrganization := "org.typelevel" 64 | scalacOptions += "-Yliteral-types" 65 | 66 | libraryDependencies += scalaOrganization.value % "scala-reflect" % scalaVersion.value 67 | ``` 68 | 69 | #### Scala.js settings 70 | ```scala 71 | libraryDependencies := libraryDependencies.value.filterNot(_.name == "scalajs-compiler") 72 | addCompilerPlugin("org.scala-js" % "scalajs-compiler" % scalaJSVersion cross CrossVersion.patch) 73 | ``` 74 | 75 | #### Scala Native settings 76 | ```scala 77 | libraryDependencies := libraryDependencies.value.filterNot(_.name == "nscplugin") 78 | addCompilerPlugin("org.scala-native" % "nscplugin" % nativeVersion cross CrossVersion.patch) 79 | ``` 80 | 81 | ### Dependencies 82 | ```scala 83 | libraryDependencies += "tech.sparse" %% "pine" % "" // JVM 84 | libraryDependencies += "tech.sparse" %%% "pine" % "" // JavaScript, Native 85 | ``` 86 | 87 | ## Links 88 | * [Documentation](http://sparse.tech/docs/pine.html) 89 | * [ScalaDoc](https://www.javadoc.io/doc/tech.sparse/pine_2.12/) 90 | 91 | ## Licence 92 | Pine is licensed under the terms of the Apache v2.0 licence. 93 | 94 | ## Contributors 95 | * Tim Nieradzik 96 | * Matt Hicks 97 | * Anatoliy Kmetyuk 98 | * Keven Wright 99 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | 2 | val convertMDN = taskKey[Unit]("Generate MDN bindings") 3 | 4 | val V = new { 5 | val paradise = "2.1.1" 6 | val scala2_11 = "2.11.11-bin-typelevel-4" 7 | val scala2_12 = "2.12.4-bin-typelevel-4" 8 | val scala2_13 = "2.13.3" 9 | val scalaTest = "3.2.2" 10 | val scalaCheck = "1.14.3" 11 | val scalaJsDom = "1.1.0" 12 | } 13 | 14 | ThisBuild / scalaVersion := V.scala2_13 15 | 16 | val commonSettings = nocomma { 17 | name := "pine" 18 | convertMDN := MDNParser.createFiles(new File("src/main/scala")) 19 | 20 | // See https://github.com/sbt/sbt/pull/2659 21 | incOptions := incOptions.value.withLogRecompileOnMacro(false) 22 | 23 | libraryDependencies ++= Seq( 24 | scalaOrganization.value % "scala-reflect" % scalaVersion.value % "provided", 25 | scalaOrganization.value % "scala-compiler" % scalaVersion.value % "provided", 26 | 27 | "org.scalatest" %%% "scalatest" % V.scalaTest % "test", 28 | "org.scalacheck" %%% "scalacheck" % V.scalaCheck % "test" 29 | ) 30 | 31 | scalacOptions ++= Seq( 32 | "-deprecation", // Emit warning and location for usages of deprecated APIs. 33 | "-encoding", "utf-8", // Specify character encoding used by source files. 34 | "-explaintypes", // Explain type errors in more detail. 35 | "-feature", // Emit warning and location for usages of features that should be imported explicitly. 36 | "-language:existentials", // Existential types (besides wildcard types) can be written and inferred 37 | "-language:experimental.macros", // Allow macro definition (besides implementation and application) 38 | "-language:higherKinds", // Allow higher-kinded types 39 | "-unchecked", // Enable additional warnings where generated code depends on assumptions. 40 | "-Xcheckinit", // Wrap field accessors to throw an exception on uninitialized access. 41 | "-Xfatal-warnings", // Fail the compilation if there are any warnings. 42 | "-Xlint:adapted-args", // Warn if an argument list is modified to match the receiver. 43 | "-Xlint:delayedinit-select", // Selecting member of DelayedInit. 44 | "-Xlint:doc-detached", // A Scaladoc comment appears to be detached from its element. 45 | "-Xlint:inaccessible", // Warn about inaccessible types in method signatures. 46 | "-Xlint:infer-any", // Warn when a type argument is inferred to be `Any`. 47 | "-Xlint:missing-interpolator", // A string literal appears to be missing an interpolator id. 48 | "-Xlint:nullary-unit", // Warn when nullary methods return Unit. 49 | "-Xlint:option-implicit", // Option.apply used implicit view. 50 | "-Xlint:package-object-classes", // Class or object defined in package object. 51 | "-Xlint:poly-implicit-overload", // Parameterized overloaded implicit methods are not visible as view bounds. 52 | "-Xlint:private-shadow", // A private field (or class parameter) shadows a superclass field. 53 | "-Xlint:stars-align", // Pattern sequence wildcard must align with sequence component. 54 | "-Xlint:type-parameter-shadow", // A local type parameter shadows a type already in scope. 55 | // "-Ywarn-dead-code", // Warn when dead code is identified. 56 | "-Ywarn-numeric-widen", // Warn when numerics are widened. 57 | ) 58 | } 59 | 60 | val Pre13Settings = nocomma { 61 | scalaOrganization := "org.typelevel" 62 | 63 | scalacOptions ++= Seq( 64 | "-Xfuture", // Turn on future language features. 65 | "-Xlint:by-name-right-associative", // By-name parameter of right associative operator. 66 | "-Xlint:unsound-match", // Pattern match may not be typesafe. 67 | "-Yliteral-types", // ... this is why we need the typelevel org 68 | "-Yno-adapted-args", // Do not adapt an argument list (either by inserting () or creating a tuple) to match the receiver. 69 | "-Ypartial-unification", // Enable partial unification in type constructor inference 70 | "-Ywarn-inaccessible", // Warn about inaccessible types in method signatures. 71 | "-Ywarn-infer-any", // Warn when a type argument is inferred to be `Any`. 72 | "-Ywarn-nullary-override", // Warn when non-nullary `def f()' overrides nullary `def f'. 73 | "-Ywarn-nullary-unit", // Warn when nullary methods return Unit. 74 | ) 75 | 76 | addCompilerPlugin("org.scalamacros" %% "paradise" % V.paradise cross CrossVersion.patch) 77 | } 78 | 79 | val Post11Settings = nocomma { 80 | scalacOptions ++= Seq( 81 | "-Xlint:constant", // Evaluation of a constant arithmetic expression results in an error. 82 | "-Ywarn-extra-implicit", // Warn when more than one implicit parameter section is defined. 83 | "-Ywarn-unused:imports", // Warn if an import selector is not referenced. 84 | "-Ywarn-unused:locals", // Warn if a local definition is unused. 85 | "-Ywarn-unused:patvars", // Warn if a variable bound in a pattern is unused. 86 | "-Ywarn-unused:privates", // Warn if a private member is unused. 87 | // "-Ywarn-unused:implicits", // Warn if an implicit parameter is unused. 88 | // "-Ywarn-unused:params", // Warn if a value parameter is unused. 89 | // "-Ywarn-value-discard" // Warn when non-Unit expression results are unused. 90 | ) 91 | } 92 | 93 | val Scala11Settings = Pre13Settings 94 | 95 | val Scala12Settings = Pre13Settings ++ Post11Settings 96 | 97 | val Scala13Settings = Post11Settings ++ nocomma { 98 | scalacOptions += "-Ymacro-annotations" 99 | } 100 | 101 | val JvmSettings = Seq() 102 | 103 | val JsSettings = nocomma { 104 | libraryDependencies ++= Seq( 105 | "org.scala-js" %%% "scalajs-dom" % V.scalaJsDom 106 | ) 107 | 108 | // We need to remove and re-add this if working under the typelevel compiler 109 | // under the lighbend compiler it causes no harm 110 | // See https://github.com/scala-js/scala-js/pull/2954 111 | libraryDependencies ~= (_.filterNot(_.name == "scalajs-compiler")) 112 | addCompilerPlugin("org.scala-js" % "scalajs-compiler" % scalaJSVersion cross CrossVersion.patch) 113 | 114 | Test / jsEnv := new org.scalajs.jsenv.jsdomnodejs.JSDOMNodeJSEnv 115 | Global / scalaJSStage := FastOptStage 116 | } 117 | 118 | val NativeSettings = nocomma { 119 | libraryDependencies ~= (_.filterNot(_.name == "nscplugin")) 120 | addCompilerPlugin("org.scala-native" % "nscplugin" % nativeVersion cross CrossVersion.patch) 121 | 122 | // See https://github.com/scalalandio/chimney/issues/78#issuecomment-419705142 123 | nativeLinkStubs := true 124 | 125 | // Workaround for "No documentation generated with unsuccessful compiler run" 126 | sources in (Compile, doc) := Seq() 127 | } 128 | 129 | lazy val pine = (projectMatrix in file(".")) 130 | .settings(commonSettings) 131 | .jvmPlatform( Seq(V.scala2_13), JvmSettings ++ Scala13Settings) 132 | .jvmPlatform( Seq(V.scala2_12), JvmSettings ++ Scala12Settings) 133 | .jvmPlatform( Seq(V.scala2_11), JvmSettings ++ Scala11Settings) 134 | .jsPlatform( Seq(V.scala2_13), JsSettings ++ Scala13Settings) 135 | .jsPlatform( Seq(V.scala2_12), JsSettings ++ Scala12Settings) 136 | .nativePlatform( Seq(V.scala2_11), NativeSettings ++ Pre13Settings) 137 | 138 | // root settings. src/ is handled by sbt-projectmatrix 139 | publish / skip := false 140 | Compile / sources := List() 141 | Test / sources := List() 142 | 143 | // publish settings 144 | ThisBuild / organization := "tech.sparse" 145 | ThisBuild / homepage := Some(url("https://github.com/sparsetech/pine")) 146 | ThisBuild / licenses := List("Apache 2" -> url("https://www.apache.org/licenses/LICENSE-2.0.html")) 147 | ThisBuild / scmInfo := Some( 148 | ScmInfo( 149 | url("https://github.com/sparsetech/pine"), 150 | "scm:git@github.com:sparsetech/pine.git" 151 | ) 152 | ) 153 | ThisBuild / developers := List( 154 | Developer( 155 | id = "tindzk", 156 | name = "Tim Nieradzik", 157 | email = "@tindzk", 158 | url = url("http://github.com/tindzk") 159 | ) 160 | ) 161 | -------------------------------------------------------------------------------- /build.toml: -------------------------------------------------------------------------------- 1 | build213.toml -------------------------------------------------------------------------------- /build211.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | scalaVersion = "2.11.11-bin-typelevel-4" 3 | scalaNativeVersion = "0.4.0-M2" 4 | scalaOptions = [ 5 | "-deprecation", # Emit warning and location for usages of deprecated APIs. 6 | "-encoding", "utf-8", # Specify character encoding used by source files. 7 | "-explaintypes", # Explain type errors in more detail. 8 | "-feature", # Emit warning and location for usages of features that should be imported explicitly. 9 | "-language:existentials", # Existential types (besides wildcard types) can be written and inferred 10 | "-language:experimental.macros", # Allow macro definition (besides implementation and application) 11 | "-language:higherKinds", # Allow higher-kinded types 12 | "-unchecked", # Enable additional warnings where generated code depends on assumptions. 13 | "-Xcheckinit", # Wrap field accessors to throw an exception on uninitialized access. 14 | "-Xfatal-warnings", # Fail the compilation if there are any warnings. 15 | "-Xlint:adapted-args", # Warn if an argument list is modified to match the receiver. 16 | "-Xlint:delayedinit-select", # Selecting member of DelayedInit. 17 | "-Xlint:doc-detached", # A Scaladoc comment appears to be detached from its element. 18 | "-Xlint:inaccessible", # Warn about inaccessible types in method signatures. 19 | "-Xlint:infer-any", # Warn when a type argument is inferred to be `Any`. 20 | "-Xlint:missing-interpolator", # A string literal appears to be missing an interpolator id. 21 | "-Xlint:nullary-override", # Warn when non-nullary `def f()' overrides nullary `def f'. 22 | "-Xlint:nullary-unit", # Warn when nullary methods return Unit. 23 | "-Xlint:option-implicit", # Option.apply used implicit view. 24 | "-Xlint:package-object-classes", # Class or object defined in package object. 25 | "-Xlint:poly-implicit-overload", # Parameterized overloaded implicit methods are not visible as view bounds. 26 | "-Xlint:private-shadow", # A private field (or class parameter) shadows a superclass field. 27 | "-Xlint:stars-align", # Pattern sequence wildcard must align with sequence component. 28 | "-Xlint:type-parameter-shadow", # A local type parameter shadows a type already in scope. 29 | # "-Ywarn-dead-code", # Warn when dead code is identified. 30 | "-Ywarn-numeric-widen", # Warn when numerics are widened. 31 | 32 | ### pre-13 options ### 33 | 34 | "-Xfuture", # Turn on future language features. 35 | "-Xlint:by-name-right-associative", # By-name parameter of right associative operator. 36 | "-Xlint:unsound-match", # Pattern match may not be typesafe. 37 | "-Yliteral-types", # ... this is why we need the typelevel org 38 | "-Yno-adapted-args", # Do not adapt an argument list (either by inserting () or creating a tuple) to match the receiver. 39 | "-Ypartial-unification", # Enable partial unification in type constructor inference 40 | "-Ywarn-inaccessible", # Warn about inaccessible types in method signatures. 41 | "-Ywarn-infer-any", # Warn when a type argument is inferred to be `Any`. 42 | "-Ywarn-nullary-override", # Warn when non-nullary `def f()' overrides nullary `def f'. 43 | "-Ywarn-nullary-unit", # Warn when nullary methods return Unit. 44 | ] 45 | scalaOrganisation = "org.typelevel" 46 | testFrameworks = [ 47 | "org.scalatest.tools.Framework", 48 | "org.scalacheck.ScalaCheckFramework" 49 | ] 50 | 51 | [module.pine] 52 | root = "src/main/scala" 53 | sources = ["src/main/scala"] 54 | targets = ["jvm", "native"] 55 | 56 | [module.pine.test] 57 | sources = ["src/test/scala"] 58 | targets = ["jvm"] 59 | scalaDeps = [ 60 | ["org.scalatest" , "scalatest" , "3.2.2" ], 61 | ["org.scalacheck", "scalacheck", "1.14.3"] 62 | ] 63 | 64 | [module.pine.jvm] 65 | root = "src/main/scala-jvm" 66 | sources = ["src/main/scala-jvm"] 67 | 68 | [module.pine-bench] 69 | moduleDeps = ["pine"] 70 | root = "src/bench" 71 | sources = ["src/bench/shared"] 72 | 73 | [module.pine-bench.jvm] 74 | root = "src/bench/jvm" 75 | sources = ["src/bench/jvm"] 76 | javaDeps = [ 77 | ["org.openjdk.jol", "jol-core", "0.13"] 78 | ] 79 | -------------------------------------------------------------------------------- /build212.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | scalaVersion = "2.12.4-bin-typelevel-4" 3 | scalaJsVersion = "1.0.1" 4 | scalaOptions = [ 5 | "-deprecation", # Emit warning and location for usages of deprecated APIs. 6 | "-encoding", "utf-8", # Specify character encoding used by source files. 7 | "-explaintypes", # Explain type errors in more detail. 8 | "-feature", # Emit warning and location for usages of features that should be imported explicitly. 9 | "-language:existentials", # Existential types (besides wildcard types) can be written and inferred 10 | "-language:experimental.macros", # Allow macro definition (besides implementation and application) 11 | "-language:higherKinds", # Allow higher-kinded types 12 | "-unchecked", # Enable additional warnings where generated code depends on assumptions. 13 | "-Xcheckinit", # Wrap field accessors to throw an exception on uninitialized access. 14 | "-Xfatal-warnings", # Fail the compilation if there are any warnings. 15 | "-Xlint:adapted-args", # Warn if an argument list is modified to match the receiver. 16 | "-Xlint:delayedinit-select", # Selecting member of DelayedInit. 17 | "-Xlint:doc-detached", # A Scaladoc comment appears to be detached from its element. 18 | "-Xlint:inaccessible", # Warn about inaccessible types in method signatures. 19 | "-Xlint:infer-any", # Warn when a type argument is inferred to be `Any`. 20 | "-Xlint:missing-interpolator", # A string literal appears to be missing an interpolator id. 21 | "-Xlint:nullary-override", # Warn when non-nullary `def f()' overrides nullary `def f'. 22 | "-Xlint:nullary-unit", # Warn when nullary methods return Unit. 23 | "-Xlint:option-implicit", # Option.apply used implicit view. 24 | "-Xlint:package-object-classes", # Class or object defined in package object. 25 | "-Xlint:poly-implicit-overload", # Parameterized overloaded implicit methods are not visible as view bounds. 26 | "-Xlint:private-shadow", # A private field (or class parameter) shadows a superclass field. 27 | "-Xlint:stars-align", # Pattern sequence wildcard must align with sequence component. 28 | "-Xlint:type-parameter-shadow", # A local type parameter shadows a type already in scope. 29 | # "-Ywarn-dead-code", # Warn when dead code is identified. 30 | "-Ywarn-numeric-widen", # Warn when numerics are widened. 31 | 32 | ### post-11 options ### 33 | 34 | "-Xlint:constant", # Evaluation of a constant arithmetic expression results in an error. 35 | "-Ywarn-extra-implicit", # Warn when more than one implicit parameter section is defined. 36 | "-Ywarn-unused:imports", # Warn if an import selector is not referenced. 37 | "-Ywarn-unused:locals", # Warn if a local definition is unused. 38 | "-Ywarn-unused:patvars", # Warn if a variable bound in a pattern is unused. 39 | "-Ywarn-unused:privates", # Warn if a private member is unused. 40 | # "-Ywarn-unused:implicits", # Warn if an implicit parameter is unused. 41 | # "-Ywarn-unused:params", # Warn if a value parameter is unused. 42 | # "-Ywarn-value-discard" # Warn when non-Unit expression results are unused. 43 | 44 | ### pre-13 options ### 45 | 46 | "-Xfuture", # Turn on future language features. 47 | "-Xlint:by-name-right-associative", # By-name parameter of right associative operator. 48 | "-Xlint:unsound-match", # Pattern match may not be typesafe. 49 | "-Yliteral-types", # ... this is why we need the typelevel org 50 | "-Yno-adapted-args", # Do not adapt an argument list (either by inserting () or creating a tuple) to match the receiver. 51 | "-Ypartial-unification", # Enable partial unification in type constructor inference 52 | "-Ywarn-inaccessible", # Warn about inaccessible types in method signatures. 53 | "-Ywarn-infer-any", # Warn when a type argument is inferred to be `Any`. 54 | "-Ywarn-nullary-override", # Warn when non-nullary `def f()' overrides nullary `def f'. 55 | "-Ywarn-nullary-unit", # Warn when nullary methods return Unit. 56 | ] 57 | scalaOrganisation = "org.typelevel" 58 | testFrameworks = [ 59 | "org.scalatest.tools.Framework", 60 | "org.scalacheck.ScalaCheckFramework" 61 | ] 62 | 63 | [module.pine] 64 | root = "src/main/scala" 65 | sources = ["src/main/scala"] 66 | targets = ["js", "jvm"] 67 | 68 | [module.pine.test] 69 | sources = ["src/test/scala"] 70 | targets = ["js", "jvm"] 71 | scalaDeps = [ 72 | ["org.scalatest" , "scalatest" , "3.1.1" ], 73 | ["org.scalacheck", "scalacheck", "1.14.3"] 74 | ] 75 | 76 | [module.pine.jvm] 77 | root = "src/main/scala-jvm" 78 | sources = ["src/main/scala-jvm"] 79 | 80 | [module.pine.js] 81 | root = "src/main/scala-js" 82 | sources = ["src/main/scala-js"] 83 | scalaDeps = [ 84 | ["org.scala-js", "scalajs-dom", "1.1.0"] 85 | ] 86 | 87 | [module.pine.test.js] 88 | jsdom = true 89 | sources = ["src/test/scala-js"] 90 | 91 | [module.pine-bench] 92 | moduleDeps = ["pine"] 93 | root = "src/bench" 94 | sources = ["src/bench/shared"] 95 | targets = ["js", "jvm"] 96 | 97 | [module.pine-bench.jvm] 98 | root = "src/bench/jvm" 99 | sources = ["src/bench/jvm"] 100 | javaDeps = [ 101 | ["org.openjdk.jol", "jol-core", "0.13"] 102 | ] 103 | 104 | [module.pine-bench.js] 105 | root = "src/bench/js" 106 | sources = ["src/bench/js"] 107 | moduleKind = "commonjs" 108 | -------------------------------------------------------------------------------- /build213.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | scalaVersion = "2.13.2" 3 | scalaJsVersion = "1.0.1" 4 | scalaOptions = [ 5 | "-deprecation", # Emit warning and location for usages of deprecated APIs. 6 | "-encoding", "utf-8", # Specify character encoding used by source files. 7 | "-explaintypes", # Explain type errors in more detail. 8 | "-feature", # Emit warning and location for usages of features that should be imported explicitly. 9 | "-language:existentials", # Existential types (besides wildcard types) can be written and inferred 10 | "-language:experimental.macros", # Allow macro definition (besides implementation and application) 11 | "-language:higherKinds", # Allow higher-kinded types 12 | "-unchecked", # Enable additional warnings where generated code depends on assumptions. 13 | "-Xcheckinit", # Wrap field accessors to throw an exception on uninitialized access. 14 | "-Xfatal-warnings", # Fail the compilation if there are any warnings. 15 | "-Xlint:adapted-args", # Warn if an argument list is modified to match the receiver. 16 | "-Xlint:delayedinit-select", # Selecting member of DelayedInit. 17 | "-Xlint:doc-detached", # A Scaladoc comment appears to be detached from its element. 18 | "-Xlint:inaccessible", # Warn about inaccessible types in method signatures. 19 | "-Xlint:infer-any", # Warn when a type argument is inferred to be `Any`. 20 | "-Xlint:missing-interpolator", # A string literal appears to be missing an interpolator id. 21 | "-Xlint:nullary-unit", # Warn when nullary methods return Unit. 22 | "-Xlint:option-implicit", # Option.apply used implicit view. 23 | "-Xlint:package-object-classes", # Class or object defined in package object. 24 | "-Xlint:poly-implicit-overload", # Parameterized overloaded implicit methods are not visible as view bounds. 25 | "-Xlint:private-shadow", # A private field (or class parameter) shadows a superclass field. 26 | "-Xlint:stars-align", # Pattern sequence wildcard must align with sequence component. 27 | "-Xlint:type-parameter-shadow", # A local type parameter shadows a type already in scope. 28 | # "-Ywarn-dead-code", # Warn when dead code is identified. 29 | "-Ywarn-numeric-widen", # Warn when numerics are widened. 30 | 31 | ### post-11 options ### 32 | 33 | "-Xlint:constant", # Evaluation of a constant arithmetic expression results in an error. 34 | "-Ywarn-extra-implicit", # Warn when more than one implicit parameter section is defined. 35 | "-Ywarn-unused:imports", # Warn if an import selector is not referenced. 36 | "-Ywarn-unused:locals", # Warn if a local definition is unused. 37 | "-Ywarn-unused:patvars", # Warn if a variable bound in a pattern is unused. 38 | "-Ywarn-unused:privates", # Warn if a private member is unused. 39 | # "-Ywarn-unused:implicits", # Warn if an implicit parameter is unused. 40 | # "-Ywarn-unused:params", # Warn if a value parameter is unused. 41 | # "-Ywarn-value-discard" # Warn when non-Unit expression results are unused. 42 | ] 43 | testFrameworks = [ 44 | "org.scalatest.tools.Framework", 45 | "org.scalacheck.ScalaCheckFramework" 46 | ] 47 | 48 | [module.pine] 49 | root = "src/main/scala" 50 | sources = ["src/main/scala"] 51 | targets = ["js", "jvm"] 52 | 53 | [module.pine.test] 54 | sources = ["src/test/scala"] 55 | targets = ["js", "jvm"] 56 | scalaDeps = [ 57 | ["org.scalatest" , "scalatest" , "3.1.1" ], 58 | ["org.scalacheck", "scalacheck", "1.14.3"] 59 | ] 60 | 61 | [module.pine.jvm] 62 | root = "src/main/scala-jvm" 63 | sources = ["src/main/scala-jvm"] 64 | 65 | [module.pine.js] 66 | root = "src/main/scala-js" 67 | sources = ["src/main/scala-js"] 68 | scalaDeps = [ 69 | ["org.scala-js", "scalajs-dom", "1.1.0"] 70 | ] 71 | 72 | [module.pine.test.js] 73 | jsdom = true 74 | sources = ["src/test/scala-js"] 75 | 76 | [module.pine-bench] 77 | moduleDeps = ["pine"] 78 | root = "src/bench" 79 | sources = ["src/bench/shared"] 80 | targets = ["js", "jvm"] 81 | 82 | [module.pine-bench.jvm] 83 | root = "src/bench/jvm" 84 | sources = ["src/bench/jvm"] 85 | javaDeps = [ 86 | ["org.openjdk.jol", "jol-core", "0.13"] 87 | ] 88 | 89 | [module.pine-bench.js] 90 | root = "src/bench/js" 91 | sources = ["src/bench/js"] 92 | moduleKind = "commonjs" 93 | -------------------------------------------------------------------------------- /cache/https___developer.mozilla.org_en-US_docs_Web_HTML_Element_h1.html: -------------------------------------------------------------------------------- 1 | https___developer.mozilla.org_en-US_docs_Web_HTML_Element_Heading_Elements.html -------------------------------------------------------------------------------- /cache/https___developer.mozilla.org_en-US_docs_Web_HTML_Element_h2.html: -------------------------------------------------------------------------------- 1 | https___developer.mozilla.org_en-US_docs_Web_HTML_Element_Heading_Elements.html -------------------------------------------------------------------------------- /cache/https___developer.mozilla.org_en-US_docs_Web_HTML_Element_h3.html: -------------------------------------------------------------------------------- 1 | https___developer.mozilla.org_en-US_docs_Web_HTML_Element_Heading_Elements.html -------------------------------------------------------------------------------- /cache/https___developer.mozilla.org_en-US_docs_Web_HTML_Element_h4.html: -------------------------------------------------------------------------------- 1 | https___developer.mozilla.org_en-US_docs_Web_HTML_Element_Heading_Elements.html -------------------------------------------------------------------------------- /cache/https___developer.mozilla.org_en-US_docs_Web_HTML_Element_h5.html: -------------------------------------------------------------------------------- 1 | https___developer.mozilla.org_en-US_docs_Web_HTML_Element_Heading_Elements.html -------------------------------------------------------------------------------- /cache/https___developer.mozilla.org_en-US_docs_Web_HTML_Element_h6.html: -------------------------------------------------------------------------------- 1 | https___developer.mozilla.org_en-US_docs_Web_HTML_Element_Heading_Elements.html -------------------------------------------------------------------------------- /docs/1-tree-construction.md: -------------------------------------------------------------------------------- 1 | # Tree construction 2 | Unless otherwise stated, all code samples require a prior `import pine._`. 3 | 4 | ## DSL 5 | Pine offers a DSL that allows to create trees in terms of immutable objects: 6 | 7 | ```scala 8 | val a = tag.A 9 | .href("http://github.com/") 10 | .set(Text("GitHub")) 11 | 12 | a.toHtml // GitHub 13 | ``` 14 | 15 | The bindings are derived from the MDN documentation. For all attributes, we provide getters and setters. `set()` replaces the children of a node. 16 | 17 | ## Macros 18 | Pine defines a couple of compile-time macros for increased comfort or performance. 19 | 20 | ### Inline HTML 21 | Inline HTML can include placeholders referring to Scala values: 22 | 23 | ```scala 24 | val url = "http://github.com/" 25 | val title = "GitHub" 26 | val root = html"""$title""" 27 | 28 | root.toHtml // GitHub 29 | ``` 30 | 31 | A placeholder may also reference `Seq[Node]` values: 32 | 33 | ```scala 34 | val spans = Seq( 35 | html"test", 36 | html"test2" 37 | ) 38 | 39 | val div = html"
$spans
" 40 | div.toHtml //
testtest2
41 | ``` 42 | 43 | ### External HTML 44 | For loading external HTML files during compile-time, a constant file name must be passed: 45 | 46 | ```scala 47 | val tpl = html("test.html") 48 | tpl.toHtml //
...
49 | ``` 50 | 51 | ## Runtime HTML parser 52 | Pine provides an HTML parser with the same semantics on all backends: 53 | 54 | ```scala 55 | val html = """
42
""" 56 | val node = HtmlParser.fromString(html) 57 | node.toHtml == html // true 58 | ``` 59 | 60 | HTML code is parsed during compile-time and then translated to an immutable tree. This reduces any runtime overhead. HTML can be specified inline or loaded from external files. 61 | 62 | The parser has the following limitations: 63 | 64 | - The `DOCTYPE` tag is ignored 65 | - The input is expected to be valid HTML5 66 | 67 | The parser supports the complete set of more than 2100 HTML entities such as `"` as well as numeric ones (`"`). These entities can be decoded using the function `HtmlHelpers.decodeEntity()`. If you would like to decode a text that may contain such entities, you can call `decodeText()` instead. 68 | 69 | ## XML 70 | XML has slightly different semantics with regards to self-closing tags. The following example is valid XML, but would yield a parse error when parsed as HTML: 71 | 72 | ```xml 73 | 74 | ``` 75 | 76 | Also, the typical XML header `""") 80 | ``` 81 | 82 | ```scala 83 | xml""" 84 | 85 | 88 | 89 | 90 | 91 | 92 | """ 93 | ``` 94 | 95 | As per the XML specification, Pine supports only the following four entities: 96 | 97 | * `'` (`'`) 98 | * `<` (`<`) 99 | * `>` (`>`) 100 | * `&` (`&`) 101 | 102 | The underlying data structures are the same for HTML and XML trees. Pine strives for simplicity and performance at the cost of implementing only a subset of XML's features. Please refer to [scala-xml](https://github.com/scala/scala-xml) for a more complete implementation. 103 | 104 | At the moment, we are aware of the following parser limitations: 105 | 106 | - The XML header is optional and its attributes are ignored. The input is expected to be in UTF-8 regardless of the specified character set. 107 | - [DTDs](https://docstore.mik.ua/orelly/web2/xhtml/ch15_03.htm) are not supported. Therefore, additional entity or element declarations cannot be defined. 108 | - Processing instructions (other than ``) are not supported 109 | 110 | ## Conversion 111 | Some functions return `Tag[_]` when the tag type cannot be statically determined. A more concrete type is useful if you want to access element-specific attributes, like `href` on anchor nodes. You can use `as` to convert a tag to its correct type: 112 | 113 | ```scala 114 | val tag = html"
" 115 | val div = tag.as[tag.Div] 116 | ``` 117 | 118 | Unlike `asInstanceOf`, this function ensures that the conversion is well-defined. 119 | 120 | ## Custom tags 121 | So far, we have used elements from the `tag` namespace. For each HTML element, Pine defines a type and an empty instance, i.e. without attributes and children. If you want to support a new element such as ``, you could define it as follows: 122 | 123 | ```scala 124 | type CustomType = "custom-type" 125 | val CustomType = Tag("CustomType") 126 | ``` 127 | 128 | The compiler feature we use here are literal types. Originally developed within Typelevel Scala, it is now part of Lightbend Scala 2.13 onwards. 129 | 130 | Additionally, you can define methods to access attributes conveniently: 131 | 132 | ```scala 133 | implicit class TagAttributesCustomType(tag: Tag[CustomType]) { 134 | val myValue = TagAttribute[CustomType, String](tag, "my-value") 135 | } 136 | ``` 137 | 138 | Now, you can access and modify your custom HTML element while preserving type-safety: 139 | 140 | ```scala 141 | val tag = html"""""" 142 | val ct = tag.as[CustomType] 143 | ct.myValue() // value 144 | ct.myValue("value2").toHtml // 145 | ``` 146 | 147 | Note that the type definition above is optional and you could also write the literal type directly: 148 | 149 | ```scala 150 | val ct2 = tag.as["custom-type"] 151 | ``` 152 | 153 | `TagAttribute` takes an implicit `AttributeCodec`. If you would like to enforce more type-safety in attributes, you could define an enumeration and create an `AttributeCodec` instance for it: 154 | 155 | ```scala 156 | sealed abstract class Language(val id: String) 157 | object Language { 158 | case object French extends Language("french") 159 | case object Spanish extends Language("spanish") 160 | case object Unknown extends Language("unknown") 161 | val All = Set(French, Spanish) 162 | } 163 | 164 | implicit case object LanguageAttributeCodec extends AttributeCodec[Language] { 165 | override def encode(value: Language): Option[String] = Some(value.id) 166 | override def decode(value: Option[String]): Language = 167 | value.flatMap(id => Language.All.find(_.id == id)) 168 | .getOrElse(Language.Unknown) 169 | } 170 | 171 | implicit class TagAttributesCustomDiv(tag: Tag[pine.tag.Div]) { 172 | val dataLanguage = TagAttribute[pine.tag.Div, Language](tag, "data-language") 173 | } 174 | 175 | tag.Div.dataLanguage(Language.Spanish) 176 | ``` 177 | 178 | ## Rendering 179 | A node has several rendering methods: 180 | 181 | - **HTML:** `toHtml` is defined on every node and will return the tree as an HTML5 string. If the root node is an `` tag, the `DOCTYPE` will be included as well. 182 | - **XML:** `toXml` returns the tree as an XML 1.0 string. It always includes the XML header, specifying the encoding as UTF-8. 183 | - **DOM:** `toDom` is only available in JavaScript. It renders the tree as a browser node, which can be inserted into the DOM. 184 | -------------------------------------------------------------------------------- /docs/2-tree-updates.md: -------------------------------------------------------------------------------- 1 | # Tree updates 2 | ## Operations 3 | A `Node` is equipped with a variety of functions to easily manipulate trees such as `prepend`, `append`, `remove`, `clearAll`, `filter`, `flatMap`, `map` and others. See the [source code](https://github.com/sparsetech/pine/blob/master/shared/src/main/scala/pine/Node.scala) for an overview. 4 | 5 | ## Referencing nodes 6 | While the operations from the previous section allow you to modify the tree, they operate on either the root node or are applied recursively to all children. 7 | 8 | If you would like to update a specific child further down in the hierarchy, Pine introduces the concept of tag references (`TagRef`s). These have the advantage that changes can be batched and applied efficiently. 9 | 10 | In order to do so, you need to make the nodes you would like to reference identifiable, for example by setting the `id` attributes: 11 | 12 | ```scala 13 | val node = html""" 14 |
15 | 16 | 17 |
18 | """ 19 | ``` 20 | 21 | Now you can reference these nodes using `TagRef`s. A `TagRef` takes the referenced tag's ID and HTML type: 22 | 23 | ```scala 24 | val spanAge = TagRef[tag.Span]("age") 25 | val spanName = TagRef[tag.Span]("name") 26 | ``` 27 | 28 | There are more ways to reference nodes such as by class name or tag type. See section "Tag references". 29 | 30 | ## Updating nodes 31 | You can use the `update()` method to change the node: 32 | 33 | ```scala 34 | val result = node.update { implicit ctx => 35 | spanAge := 42 36 | spanName := "Joe" 37 | } 38 | ``` 39 | 40 | The changes (_diffs_) take an implicit rendering context. When you call `update()`, the changes will be queued up in the rendering context and processed in a batch. 41 | 42 | `result` will be equivalent to: 43 | 44 | ```html 45 |
46 | 42 47 | Joe 48 |
49 | ``` 50 | 51 | ## Replacing nodes 52 | If you would like to replace the node itself, you can use `replace()`: 53 | 54 | ```scala 55 | val result = node.update { implicit ctx => 56 | spanAge .replace(42) 57 | spanName.replace("Joe") 58 | } 59 | ``` 60 | 61 | `result` will be equivalent to: 62 | 63 | ```html 64 |
65 | 42 66 | Joe 67 |
68 | ``` 69 | 70 | ## Updating children 71 | ```scala 72 | val node = html"""
""" 73 | val root = TagRef[tag.Div]("page") 74 | ``` 75 | 76 | In order to render a list, you can use the `:=` function (alias for `set`): 77 | 78 | ```scala 79 | root.update { implicit ctx => 80 | root := List( 81 | html"
Hello,
", 82 | html"
world!
" 83 | ) 84 | } 85 | ``` 86 | 87 | But if you would like to later access those child nodes they need unique IDs. This is particularly useful when you render your HTML on the server and want to access it in JavaScript, e.g. in order to attach event handlers. 88 | 89 | First, we define a data type we would like to render: 90 | 91 | ```scala 92 | case class Item(id: Int, name: String) 93 | ``` 94 | 95 | Next, we define a function that returns a child node given an item. 96 | 97 | ```scala 98 | val itemView = html"""
""" 99 | def idOf(item: Item): String = item.id.toString 100 | def renderItem(item: Item): Tag[_] = { 101 | val id = idOf(item) 102 | val node = itemView.suffixIds(id) 103 | val spanName = TagRef[tag.Span]("name", id) 104 | node.update(implicit ctx => spanName := item.name) 105 | } 106 | ``` 107 | 108 | Finally, we render a list of items using the `set` method. 109 | 110 | ```scala 111 | val items = List(Item(0, "Joe"), Item(1, "Jeff")) 112 | val result = node.update(implicit ctx => root.set(items.map(renderItem))) 113 | ``` 114 | 115 | `result` will be equivalent to: 116 | 117 | ```html 118 |
119 |
120 | Joe 121 |
122 |
123 | Jeff 124 |
125 |
126 | ``` 127 | 128 | Now, we can reference child nodes using a `TagRef`: 129 | 130 | ```scala 131 | TagRef[tag.Div]("child", idOf(items.head)) // TagRef[tag.Child]("child0") 132 | ``` 133 | 134 | ## Updating attributes 135 | As our `TagRef` objects are typed, we can provide implicits for supported attributes. 136 | 137 | ```scala 138 | val node = html"""GitHub""" 139 | node.update(implicit ctx => 140 | NodeRef[tag.A]("lnk").href := "https://github.com/" 141 | ) 142 | ``` 143 | 144 | ## Tag references 145 | Tags can be referenced using: 146 | 147 | * ID attribute: `TagRef[tag.A]("id")` 148 | * Tag type: `TagRef[tag.A]` 149 | * Class name: `TagRef.byClass[tag.A]("class-name")` 150 | 151 | A `TagRef` exposes methods for manipulating nodes and their attributes. See its [source code](https://github.com/sparsetech/pine/tree/master/src/main/scala/pine/TagRef.scala) for a full list of operations. 152 | 153 | ## Diffs 154 | A `Diff` is an immutable object which describes tree changes. It is instantiated for example by the `TagRef` operations you have seen before such as `:=` (`set`), `replace` etc. 155 | 156 | So far, these changes were performed directly on the tree. However, for the JavaScript back end, we have an additional rendering context that can apply those changes to the DOM. This will be explained in the next chapter. 157 | 158 | The full list of supported diffs can be found [here](https://github.com/sparsetech/pine/tree/master/src/main/scala/pine/Diff.scala). 159 | 160 | ### Multiple occurrences 161 | If you would like to perform a change on all occurrences of a `TagRef`, use the `each` function: 162 | 163 | ```scala 164 | val div = html"""
""" 165 | div.update(implicit ctx => 166 | TagRef["span"].each += html"Hello").toHtml 167 | //
HelloHello
168 | ``` 169 | 170 | `each` can also be used in conjunction with any other diff type, such as attribute updates: 171 | 172 | ```scala 173 | val div = html"""
AB
""" 174 | val html = div.update(implicit ctx => 175 | TagRef["a"].each.href.update(_.map(url => s"$url/test"))).toHtml 176 | //
AB
177 | ``` 178 | 179 | ## HTML/CSS extensions 180 | Pine's DSL provides extensions to facilitate interaction with HTML/CSS. For toggling the visibility of a node, you can use `hide()`: 181 | 182 | ```scala 183 | div.hide(true) // Sets `style` attribute to hide the element in the browser 184 | ``` 185 | 186 | ## Token list attributes 187 | There are certain HTML attributes whose values are encoded as space-separated tokens. `class` and `rel` are the most prominent examples. 188 | 189 | These attributes have a special mapping in Pine that models their underlying sequential nature: 190 | 191 | ```scala 192 | tag.Div.`class`("a", "b") // Sets the classes "a" and "b" 193 | tag.Div.`class`.set(Seq("a", "b")) // Same as before 194 | tag.Div.`class`.get // Returns the list of classes 195 | tag.Div.`class`.add("a") // Adds the class "a" 196 | tag.Div.`class`.remove("a") // Removes the class "a" 197 | tag.Div.`class`.clear() // Removes all classes 198 | tag.Div.`class`.toggle("a") // Toggles the class "a" 199 | tag.Div.`class`.state(value, "a") // Adds the class "a" if value is true, remove otherwise 200 | tag.Div.`class`.update(_ :+ "a") // Updates the classes 201 | ``` 202 | 203 | The same functionality is available on `TagRef`s. 204 | 205 | ## Custom attributes 206 | If you would like to support custom attributes, you can extend the functionality of any tag by defining an implicit class. This is the same approach which Pine uses internally to define attributes for HTML elements. 207 | 208 | For example, to define attributes on anchor nodes, you would write: 209 | 210 | ```scala 211 | implicit class TagRefAttributesA(tagRef: TagRef[tag.A]) { 212 | val dataTooltip = TagRefAttribute[tag.A, String](tagRef, "data-tooltip") 213 | val dataShow = TagRefAttribute[tag.A, Boolean](tagRef, "data-show") 214 | } 215 | ``` 216 | -------------------------------------------------------------------------------- /docs/3-web-development.md: -------------------------------------------------------------------------------- 1 | # Web development 2 | Pine may be used for web development. You can use it in various architectures, for example: 3 | 4 | * Client-side rendered pages (SPA, single-page applications) 5 | * Server-side rendered pages without client logic 6 | * Server-side rendered pages with client logic 7 | * Server-side rendered pages with client logic and client-side rendering 8 | 9 | Please refer to our [sample project](https://github.com/sparsetech/pine-example) which implements the last architecture. 10 | 11 | _Server_ refers to either the JVM or LLVM back end, whereas _client_ refers to JavaScript. 12 | 13 | All examples require a prior `import pine.dom._`. 14 | 15 | ## Architectures 16 | Pine advocates web development in the FP style. You are advised to split your HTML rendering into composable functions and share the code across platforms. Pine does not provide any abstractions for pages or components to maximise its use cases. 17 | 18 | The fourth architecture is the most sophisticated and allows for the best user experience. For this, you have to define a shared protocol for the data layer as well as shared code for populating the templates. On the client, you evaluate which page the server rendered and then attach the event handlers. Also, when the user clicks an internal page link, instead of redirecting to it, you can use the shared template layer to perform the rendering directly in the browser. 19 | 20 | This architecture has the following life cycle for a page `p`, which you could define in terms of four functions: 21 | 22 | 1. `node(p)`: Creates an immutable tree node (`shared` project) 23 | 2. `populate(p)`: Populates the tree with content (`shared` project) 24 | 3. `attach(p)`: Attach event handlers, only called in JavaScript (`js` project) 25 | 4. `detach(p)`: Detach event handlers, only called in JavaScript (`js` project) 26 | 27 | ## Render JavaScript node 28 | To render a Pine node as a JavaScript node, use the function `toDom`: 29 | 30 | ```scala 31 | val div = html"""
""".as[tag.Div] 32 | val jsNode = div.toDom // dom.html.Div 33 | ``` 34 | 35 | `toDom` returns the correct JavaScript type depending on your node type: 36 | 37 | ```scala 38 | Text("test").toDom // dom.raw.Text 39 | ``` 40 | 41 | You need to add the JavaScript node manually to the DOM to be able to access it via a `TagRef`: 42 | 43 | ```scala 44 | dom.document.body.appendChild(jsNode) 45 | ``` 46 | 47 | ## Access DOM node 48 | Use `dom` on a `TagRef` to access the underlying DOM node: 49 | 50 | ```scala 51 | val text = TagRef[tag.Div]("text") 52 | text.dom // Returns browser node, of type org.scalajs.dom.html.Div 53 | ``` 54 | 55 | If you would like to retrieve all matching nodes, use `each` and `domAll` instead: 56 | 57 | ```scala 58 | val input = TagRef[tag.Input].each 59 | input.domAll // List[org.scalajs.dom.html.Input] 60 | ``` 61 | 62 | ## Access DOM attribute 63 | ```scala 64 | val text = TagRef[tag.Div]("text") 65 | text.`class`.get // Retrieves 'class' attribute from DOM node, of type Option[String] 66 | ``` 67 | 68 | Note that in JavaScript, DOM attributes may not represent the current state of a node. If this is the case, you can retrieve the value via `dom`: 69 | 70 | ```scala 71 | val name = TagRef[tag.Input]("name") 72 | name.value.get // Returns value the DOM node was initialised with 73 | name.dom.value // Returns current value 74 | ``` 75 | 76 | ## Converting JavaScript nodes 77 | It is also possible to convert regular DOM nodes to Pine: 78 | 79 | ```scala 80 | val node = dom.document.createElement("span") 81 | node.setAttribute("id", "test") 82 | node.appendChild(dom.document.createTextNode("Hello world")) 83 | 84 | DOM.toTree(node) // Tag(span,Map(id -> test),List(Text(Hello world))) 85 | ``` 86 | 87 | ## Diffs 88 | Previously, we used `update` to perform the changes on the nodes. To carry out the changes in the DOM, we have to use `DOM.render`: 89 | 90 | ```scala 91 | DOM.render(implicit ctx => text := "Hello, world!") 92 | ``` 93 | 94 | ### Events 95 | As an extension to content updates, you can set event handlers. In JavaScript projects, a `TagRef` exposes all event handlers which the underlying DOM element supports. These changes are side-effecting and therefore do not require a rendering context. The motivation is that event handlers do not change the visual page content. Therefore, instantiating `Diff`s and performing a batch execution would be redundant. 96 | 97 | ```scala 98 | val btnRemove = TagRef[tag.Button]("remove") 99 | btnRemove.click := println("Remove click") 100 | ``` 101 | 102 | It is possible to attach an event to all matching elements using `each`: 103 | 104 | ```scala 105 | val input = TagRef[tag.Div].each 106 | input.click := println("Any div was clicked") 107 | ``` 108 | 109 | See also `dom.Window` and `dom.Document` for global events. 110 | 111 | ## Troubleshooting 112 | ### Dangling rendering context 113 | If you encounter a *Dangling rendering context* exception, this may be reminiscent of a dangling pointer in C. The underlying problem is the same: You set up a rendering context passing it a function, which adds diffs to the context. After this function returns, the diffs are processed. Now, the rendering context should not be used anymore. 114 | 115 | Most likely an asynchronous event took place which re-used the implicit context from the scope and added a diff to it. One such example is: 116 | 117 | ```scala 118 | DOM.render { implicit ctx => 119 | button.click := box.hide(true) 120 | } 121 | ``` 122 | 123 | When the button was clicked, `hide` will re-use `ctx`. The following fixes the situation: 124 | 125 | ```scala 126 | button.click := DOM.render(implicit ctx => box.hide(true)) 127 | ``` 128 | 129 | You can safely nest multiple `DOM.render` blocks. The inner-most block will always use the context from the immediate scope. It is advisable to limit the rendering context only to functions that change the DOM and take an implicit context. This could prevent problems as above since an implicit context would not have been found in the first place. 130 | 131 | ### IntelliJ support 132 | When loading the sample projects in IntelliJ, some references in the `shared` module may not be resolved properly. This happens because IntelliJ doesn't add a dependency from platform-specific modules (i.e. `jvm` and `js`) to the `shared` module. 133 | 134 | To fix this, please go to `File -> Project Structure... -> Modules -> project-Sources -> Dependencies`. Then, add a module dependency to `projectJVM`/`projectNative` and `projectJS`. 135 | -------------------------------------------------------------------------------- /docs/4-development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | ## Benchmarking 3 | The benchmark suite measures the performance of Pine's core functionality. It is available for the JVM and JavaScript back ends. 4 | 5 | The benchmarks can be run on the JVM as follows: 6 | 7 | ```shell 8 | bloop run pine-bench-jvm -- fast # Fast profile 9 | bloop run pine-bench-jvm -- slow # Slow profile 10 | ``` 11 | 12 | The JavaScript suite requires two dependencies to be installed: 13 | 14 | ```shell 15 | yarn add jsdom object-sizeof 16 | ``` 17 | 18 | If you are compiling to tmpfs, link the `node_modules` folder: 19 | 20 | ```shell 21 | ln -s $(pwd)/node_modules /tmp/build-pine/node_modules 22 | ``` 23 | 24 | Then, run the suite with Node.js: 25 | 26 | ``` 27 | bloop run pine-bench-js -- fast # Fast profile 28 | bloop run pine-bench-js -- slow # Slow profile 29 | ``` 30 | 31 | Since Node.js is lacking a DOM implementation, several benchmarks will be omitted. However, you can run the full benchmark suite in the browser. This requires the webpack dependency: 32 | 33 | ```shell 34 | yarn add webpack webpack-cli 35 | ``` 36 | 37 | Next, create an HTML file in the build folder (e.g. `/tmp/build-pine/`): 38 | 39 | ```html 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ``` 50 | 51 | And the webpack configuration `webpack.config.js`: 52 | 53 | ```javascript 54 | const path = require('path'); 55 | 56 | module.exports = { 57 | entry: './pine-bench.js', 58 | output: { 59 | filename: 'main.js', 60 | path: __dirname 61 | } 62 | }; 63 | ``` 64 | 65 | Finally, link the benchmark suite and bundle all external dependencies into a single JavaScript file: 66 | 67 | ```shell 68 | bloop link pine-bench-js 69 | yarn exec webpack --config webpack.config.js 70 | ``` 71 | 72 | You can now open the HTML file in the browser. The results will be printed to the browser console. 73 | 74 | To detect performance regressions, the benchmarks are run with JVM and Node.js as part of every CI build. Since the benchmark suite uses the fast profile to speed up CI runs, it is advisable to also run the benchmarks locally in the `slow` profile. Similarly, the DOM benchmarks are not run as part of CI and should be tested manually in a browser. 75 | -------------------------------------------------------------------------------- /docs/5-manual.md: -------------------------------------------------------------------------------- 1 | # Development 2 | [![Build Status](https://travis-ci.org/sparsetech/pine.svg?branch=master)](https://travis-ci.org/sparsetech/pine) 3 | 4 | ## Manual 5 | The manual was generated using [Instructor](https://github.com/sparsetech/instructor). Follow its installation instructions, then run the following command: 6 | 7 | ```shell 8 | instructor manual.toml 9 | ``` 10 | -------------------------------------------------------------------------------- /manual.toml: -------------------------------------------------------------------------------- 1 | [meta] 2 | title = "Pine User Manual" 3 | author = "Tim Nieradzik" 4 | affiliation = "sparse.tech" 5 | abstract = "Functional HTML5 and XML library for the Scala platform" 6 | language = "en-GB" 7 | editSourceUrl = "https://github.com/sparsetech/pine/edit/master/" 8 | 9 | [input] 10 | paths = ["docs/*.md"] 11 | 12 | [output] 13 | highlightJsStyle = "tomorrow" 14 | 15 | [constants] 16 | inherit = "version.sbt" 17 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.3.8 2 | -------------------------------------------------------------------------------- /project/build.sbt: -------------------------------------------------------------------------------- 1 | libraryDependencies += "org.jsoup" % "jsoup" % "1.8.3" 2 | 3 | libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.3.0" 4 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | 2 | logLevel := Level.Warn 3 | 4 | // see http://eed3si9n.com/removing-commas-with-sbt-nocomma 5 | addSbtPlugin("com.eed3si9n" % "sbt-nocomma" % "0.1.0") 6 | addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.3.0") 7 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.2.0") 8 | addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.0-M2") 9 | 10 | libraryDependencies += "org.scala-js" %% "scalajs-env-jsdom-nodejs" % "1.1.0" 11 | -------------------------------------------------------------------------------- /src/bench/js/pine/bench/PlatformBench.scala: -------------------------------------------------------------------------------- 1 | package pine.bench 2 | 3 | import scala.scalajs.js 4 | 5 | import pine._ 6 | import pine.dom._ 7 | import pine.bench.BenchUtil._ 8 | 9 | class PlatformBench extends BenchmarkSuite with TreeBench { 10 | val isNodeJs = js.typeOf(js.Dynamic.global.window) == "undefined" 11 | 12 | if (isNodeJs) println("[warn] Ignoring all tests requiring DOM access") 13 | else { 14 | bench("Render and update node in DOM", depths, measureMemory = false) { depth => 15 | val ref = TagRef.ByTag("span").each 16 | val rendered = treesWoAttributes(depth).toDom 17 | org.scalajs.dom.document.body.appendChild(rendered) 18 | DOM.render { implicit ctx => ref := "test" } 19 | org.scalajs.dom.document.body.removeChild(rendered) 20 | numberOfNodes(depth) -> (()) 21 | } 22 | 23 | bench("Render as DOM node", depths, measureMemory = false) { depth => 24 | numberOfNodes(depth) -> treesWithAttributes(depth).toDom 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/bench/js/pine/bench/PlatformUtil.scala: -------------------------------------------------------------------------------- 1 | package pine.bench 2 | 3 | import scala.scalajs.js 4 | import scala.scalajs.js.JSNumberOps._ 5 | import scala.scalajs.js.annotation.JSImport 6 | 7 | @js.native 8 | @JSImport("object-sizeof", JSImport.Namespace) 9 | object SizeOf extends js.Object { 10 | def apply(obj: js.Any): Any = js.native 11 | } 12 | 13 | object PlatformUtil { 14 | def cliArgs(): List[String] = 15 | if (js.typeOf(js.Dynamic.global.process) == "undefined") List() 16 | else js.Dynamic.global.process.argv.asInstanceOf[js.Array[String]].toList.drop(2) 17 | def format(value: Double): String = value.toFixed(1) 18 | def measure(obj: Any): Long = 19 | SizeOf(obj.asInstanceOf[js.Any]) match { 20 | case i: Int => i.toLong 21 | case i: Float => i.toLong // TODO Should be Long instead of Float 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/bench/jvm/pine/bench/PlatformBench.scala: -------------------------------------------------------------------------------- 1 | package pine.bench 2 | 3 | class PlatformBench extends BenchmarkSuite 4 | -------------------------------------------------------------------------------- /src/bench/jvm/pine/bench/PlatformUtil.scala: -------------------------------------------------------------------------------- 1 | package pine.bench 2 | 3 | import java.text.DecimalFormat 4 | 5 | import org.openjdk.jol.info.GraphLayout 6 | 7 | object PlatformUtil { 8 | private val decimalFormat = new DecimalFormat("0.0") 9 | 10 | def cliArgs(): List[String] = List() 11 | def format(value: Double): String = decimalFormat.format(value) 12 | def measure(obj: Any): Long = 13 | GraphLayout.parseInstance(obj.asInstanceOf[Object]).totalSize() 14 | } 15 | -------------------------------------------------------------------------------- /src/bench/shared/pine/bench/Bench.scala: -------------------------------------------------------------------------------- 1 | package pine.bench 2 | 3 | import scala.collection.mutable.ListBuffer 4 | 5 | case class Benchmark[T, U]( 6 | description: String, 7 | values: List[T], 8 | f: T => (Long, U), 9 | measureMemory: Boolean 10 | ) 11 | 12 | trait BenchmarkSuite { 13 | private[bench] val benchmarks = ListBuffer[Benchmark[_, _]]() 14 | 15 | def bench[T, U]( 16 | description: String, values: List[T], measureMemory: Boolean 17 | )(f: T => (Long, U)): Unit = 18 | benchmarks += Benchmark[T, U](description, values, f, measureMemory) 19 | } 20 | 21 | /** 22 | * @param minimumDuration Milliseconds for which each benchmark must be run at 23 | * least. This applies to warmup and measurement 24 | * iterations. 25 | */ 26 | case class Profile( 27 | warmUpIterations: Int, 28 | measurementIterations: Int, 29 | minimumDuration: Int, 30 | garbageCollection: Boolean 31 | ) 32 | 33 | object BenchmarkRunner { 34 | val slowProfile = Profile( 35 | warmUpIterations = 1, 36 | measurementIterations = 3, 37 | minimumDuration = 2000, 38 | garbageCollection = true 39 | ) 40 | 41 | val fastProfile = Profile( 42 | warmUpIterations = 0, 43 | measurementIterations = 2, 44 | minimumDuration = 500, 45 | garbageCollection = false 46 | ) 47 | 48 | val benchmarkSuites = List(new SharedBench, new PlatformBench) 49 | 50 | case class MeasureResult( 51 | iterations: Int, 52 | units: Long, 53 | unitSize: Long, 54 | totalSize: Long, 55 | totalRunTime: Long 56 | ) 57 | 58 | def measure( 59 | f: () => (Long, Any), profile: Profile, measureSize: Boolean 60 | ): MeasureResult = { 61 | if (profile.garbageCollection) System.gc() 62 | 63 | var runTime = 0L 64 | var iterations = 0 65 | var unitsCumulative = 0L 66 | var last: (Long, Any) = (0L, null) // (units, result) 67 | 68 | while (runTime < profile.minimumDuration) { 69 | val start = System.currentTimeMillis() 70 | last = f() 71 | val end = System.currentTimeMillis() 72 | 73 | runTime += end - start 74 | unitsCumulative += last._1 75 | iterations += 1 76 | } 77 | 78 | val (unitSize, totalSize) = 79 | if (!measureSize) (0L, 0L) 80 | else { 81 | val (units, result) = last 82 | val totalSize = PlatformUtil.measure(result) 83 | (totalSize / units, totalSize) 84 | } 85 | 86 | val units = unitsCumulative / iterations 87 | MeasureResult(iterations, units, unitSize, totalSize, runTime) 88 | } 89 | 90 | def main(jvmArgs: Array[String]): Unit = { 91 | val args = if (jvmArgs.isEmpty) PlatformUtil.cliArgs() else jvmArgs.toList 92 | val profile = 93 | if (args.headOption.contains("slow")) slowProfile else fastProfile 94 | 95 | import profile._ 96 | println(f"Warm up iterations: $warmUpIterations") 97 | println(f"Measurement iterations: $measurementIterations") 98 | println(f"Minimum duration: $minimumDuration ms") 99 | println(f"Garbage collection: $garbageCollection") 100 | 101 | println() 102 | 103 | benchmarkSuites.foreach { b => 104 | b.benchmarks.foreach { b_ => 105 | val b = b_.asInstanceOf[Benchmark[Any,Any]] 106 | println(s"Benchmark: ${b.description}") 107 | val depthResults = b.values.map { v => 108 | println(s"- depth=$v:") 109 | (0 until warmUpIterations) 110 | .foreach(_ => measure(() => b.f(v), profile, false)) 111 | 112 | val results = (0 until measurementIterations) 113 | .map(_ => measure(() => b.f(v), profile, b.measureMemory)) 114 | 115 | val units = results.last.units 116 | val iterations = results.last.iterations 117 | println(f" units: $units") 118 | println(f" iterations: $iterations") 119 | 120 | val memoryTotal = results.last.totalSize 121 | val memoryUnit = results.last.unitSize 122 | 123 | val runTimes = results.map(r => 124 | r.totalRunTime.toDouble / r.iterations * 1000000) 125 | val unitTimeMean = runTimes.sum / results.length 126 | val unitTimeStdDev = math.sqrt(runTimes.map(x => 127 | (x - unitTimeMean) * (x - unitTimeMean)).sum / results.length) 128 | 129 | println(s" run time: ${unitTimeMean.toLong} μs/it ± ${unitTimeStdDev.toLong}") 130 | 131 | if (b.measureMemory) 132 | println(s" memory: $memoryTotal bytes (≃ $memoryUnit bytes/unit)") 133 | 134 | (units, unitTimeMean, memoryTotal) 135 | } 136 | 137 | val unitGrowth = depthResults.zip(depthResults.tail) 138 | .map { case ((units1, _, _), (units2, _, _)) => 139 | units2.toDouble / units1 } 140 | 141 | val timeGrowth = depthResults.zip(depthResults.tail) 142 | .map { case ((_, time1, _), (_, time2, _)) => 143 | time2.toDouble / time1 } 144 | 145 | println() 146 | println("Summary:") 147 | println(s" Unit growth: " + 148 | unitGrowth 149 | .map(x => f"${PlatformUtil.format(x)}x") 150 | .mkString(", ")) 151 | println(s" Run time growth: " + 152 | timeGrowth 153 | .map(x => f"${PlatformUtil.format(x)}x") 154 | .mkString(", ")) 155 | 156 | if (b.measureMemory) { 157 | val memoryGrowth = depthResults 158 | .zip(depthResults.tail) 159 | .map { case ((_, _, memory1), (_, _, memory2)) => 160 | memory2.toDouble / memory1 } 161 | 162 | println(s" Memory growth: " + 163 | memoryGrowth 164 | .map(x => f"${PlatformUtil.format(x)}x") 165 | .mkString(", ")) 166 | } 167 | 168 | println() 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/bench/shared/pine/bench/BenchUtil.scala: -------------------------------------------------------------------------------- 1 | package pine.bench 2 | 3 | import pine._ 4 | 5 | object BenchUtil { 6 | def binaryTree(depth: Int): Node = 7 | if (depth == 0) tag.Span // empty leaf 8 | else tag.Div 9 | .append(binaryTree(depth - 1)) 10 | .append(binaryTree(depth - 1)) 11 | 12 | def binaryTreeMacro(depth: Int): Node = 13 | if (depth == 0) html"""""" // empty leaf 14 | else html"""
${binaryTree(depth - 1)}${binaryTree(depth - 1)}
""" 15 | 16 | def binaryTreeWithAttribute(depth: Int): Node = 17 | if (depth == 0) tag.Span.setAttr("attr", "value") 18 | else tag.Div 19 | .append(binaryTree(depth - 1)) 20 | .append(binaryTree(depth - 1)) 21 | 22 | def binaryTreeWithAttributesMacro(depth: Int): Node = 23 | if (depth == 0) html"""""" // empty leaf 24 | else html"""
${binaryTree(depth - 1)}${binaryTree(depth - 1)}
""" 25 | 26 | /** @return total number of nodes in binary tree, including leaves */ 27 | @inline def numberOfNodes(depth: Int): Long = (1L << (depth + 1)) - 1 28 | } 29 | -------------------------------------------------------------------------------- /src/bench/shared/pine/bench/SharedBench.scala: -------------------------------------------------------------------------------- 1 | package pine.bench 2 | 3 | import pine._ 4 | import pine.bench.BenchUtil._ 5 | 6 | class SharedBench extends BenchmarkSuite with TreeBench { 7 | bench("Build binary tree via combinators", depths, measureMemory = true) { depth => 8 | val tree = binaryTree(depth).asInstanceOf[Tag[Singleton]] 9 | numberOfNodes(depth) -> tree 10 | } 11 | 12 | bench("Build binary tree via macros", depths, measureMemory = true) { depth => 13 | val tree = binaryTreeMacro(depth).asInstanceOf[Tag[Singleton]] 14 | numberOfNodes(depth) -> tree 15 | } 16 | 17 | bench("Build binary tree with attributes via combinators", depths, measureMemory = true) { depth => 18 | val tree = binaryTreeWithAttribute(depth).asInstanceOf[Tag[Singleton]] 19 | numberOfNodes(depth) -> tree 20 | } 21 | 22 | bench("Build binary tree with attributes via macros", depths, measureMemory = true) { depth => 23 | val tree = binaryTreeWithAttributesMacro(depth).asInstanceOf[Tag[Singleton]] 24 | numberOfNodes(depth) -> tree 25 | } 26 | 27 | bench("Modify tree via map()", depths, measureMemory = false) { depth => 28 | numberOfNodes(depth) -> treesWoAttributes(depth).map { 29 | case t @ Tag("span", _, _) => t.set("test") 30 | case n => n 31 | } 32 | } 33 | 34 | bench("Modify tree via TagRef", depths, measureMemory = false) { depth => 35 | val ref = TagRef.ByTag("span").each 36 | numberOfNodes(depth) -> treesWoAttributes(depth).update { implicit ctx => ref := "test" } 37 | } 38 | 39 | bench("Generate HTML w/o attributes", depths, measureMemory = false) { depth => 40 | numberOfNodes(depth) -> treesWoAttributes(depth).toHtml 41 | } 42 | 43 | bench("Generate HTML w/ attributes", depths, measureMemory = false) { depth => 44 | numberOfNodes(depth) -> treesWithAttributes(depth).toHtml 45 | } 46 | 47 | bench("Parse HTML w/o attributes", depths, measureMemory = false) { depth => 48 | numberOfNodes(depth) -> HtmlParser.fromString(htmlTreesWoAttributes(depth)) 49 | } 50 | 51 | bench("Parse HTML w/ attributes", depths, measureMemory = false) { depth => 52 | numberOfNodes(depth) -> HtmlParser.fromString(htmlTreesWithAttributes(depth)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/bench/shared/pine/bench/TreeBench.scala: -------------------------------------------------------------------------------- 1 | package pine.bench 2 | 3 | import pine._ 4 | import pine.bench.BenchUtil._ 5 | 6 | /** 7 | * Specifies parameters for tree benchmarks and caches trees to reduce run time 8 | */ 9 | trait TreeBench { 10 | val depths = (2 to 14 by 4).toList 11 | 12 | val treesWoAttributes = depths.map(d => 13 | d -> binaryTree(d).asInstanceOf[Tag[Singleton]] 14 | ).toMap 15 | 16 | val htmlTreesWoAttributes = treesWoAttributes.map { case (k, v) => (k, v.toHtml) } 17 | 18 | val treesWithAttributes = depths.map(d => 19 | d -> binaryTreeWithAttribute(d).asInstanceOf[Tag[Singleton]] 20 | ).toMap 21 | 22 | val htmlTreesWithAttributes = treesWithAttributes.map { case (k, v) => (k, v.toHtml) } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scala-js/pine/dom/DOM.scala: -------------------------------------------------------------------------------- 1 | package pine.dom 2 | 3 | import org.scalajs.dom 4 | import org.scalajs.dom.Element 5 | import org.scalajs.dom.document 6 | 7 | import pine._ 8 | 9 | object DOM { 10 | trait Extensions { 11 | implicit class DomNodeExtensions(parent: dom.Node) { 12 | def removeChildren(): Unit = 13 | while (parent.lastChild != null) 14 | parent.removeChild(parent.lastChild) 15 | 16 | def replaceFirstChild(node: dom.Node): Unit = 17 | if (parent.firstChild == null) parent.appendChild(node) 18 | else parent.replaceChild(node, parent.firstChild) 19 | 20 | def prependChild(node: dom.Node): Unit = 21 | if (parent.firstChild == null) parent.appendChild(node) 22 | else parent.insertBefore(node, parent.firstChild) 23 | 24 | def insertAfter(node: dom.Node, refChild: dom.Node): Unit = 25 | if (refChild.nextSibling == null) parent.appendChild(node) 26 | else parent.insertBefore(node, refChild.nextSibling) 27 | 28 | def insertChildAt(position: Int, node: dom.Node): Unit = 29 | if (parent.firstChild == null) parent.appendChild(node) 30 | else parent.insertBefore(node, parent.childNodes(position)) 31 | } 32 | 33 | implicit class DomElementExtensions(parent: dom.Element) { 34 | def toTree: Tag[_] = DOM.toTree(parent) 35 | } 36 | } 37 | 38 | /** Resolve relative tag reference */ 39 | def resolve[T <: Singleton](element: Element, tagRef: TagRef[T]) 40 | (implicit js: Js[T]): js.X = 41 | tagRef match { 42 | case TagRef.ById(id) => 43 | document.getElementById(id).asInstanceOf[js.X] 44 | case TagRef.ByTag(tag) => 45 | element.getElementsByTagName(tag)(0).asInstanceOf[js.X] 46 | case TagRef.ByClass(cls) => 47 | element.getElementsByClassName(cls)(0).asInstanceOf[js.X] 48 | case TagRef.Each(tr) => resolve(element, tr)(js) 49 | case TagRef.Opt(tr) => resolve(element, tr)(js) 50 | } 51 | 52 | def collectNodes(node: dom.Node): Map[String, dom.Element] = 53 | node match { 54 | case e: dom.Element => 55 | val map = 56 | if (e.id != "") Map(e.id -> e) 57 | else Map.empty[String, dom.Element] 58 | 59 | import dom.ext._ 60 | e.getElementsByTagName("*").collect { 61 | case e: dom.Element if e.id.nonEmpty => e.id -> e 62 | }.toMap ++ map 63 | 64 | case _ => Map.empty 65 | } 66 | 67 | private def toTreeChild(node: dom.Node): Node = 68 | node match { 69 | case t: dom.raw.Text => Text(t.textContent) 70 | case e: dom.Element => toTree(e) 71 | } 72 | 73 | def toTree(e: dom.Element): Tag[Singleton] = { 74 | val attributes = (0 until e.attributes.length) 75 | .map(e.attributes(_)) 76 | .map(attr => attr.name -> attr.value).toMap 77 | 78 | val children = (0 until e.childNodes.length) 79 | .toList 80 | .map(e.childNodes(_)) 81 | .filter { 82 | case _: dom.Comment => false 83 | case _ => true 84 | }.map(toTreeChild) 85 | 86 | Tag( 87 | // TODO See https://github.com/typelevel/scala/issues/154 88 | tagName = e.tagName.toLowerCase.asInstanceOf[String with Singleton], 89 | attributes = attributes, 90 | children = children 91 | ) 92 | } 93 | 94 | def toTree(id: String): Tag[Singleton] = 95 | toTree(dom.document.getElementById(id)) 96 | 97 | def render(f: DomRenderContext => Unit): Unit = { 98 | val ctx = new DomRenderContext 99 | f(ctx) 100 | ctx.commit() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/scala-js/pine/dom/DiffRender.scala: -------------------------------------------------------------------------------- 1 | package pine.dom 2 | 3 | import scala.collection.mutable 4 | 5 | import org.scalajs.dom.Element 6 | 7 | import pine._ 8 | 9 | object DiffRender { 10 | def render(dom: Element, diff: Diff): Unit = 11 | diff match { 12 | case Diff.SetAttribute(name, value) => dom.setAttribute(name, value) 13 | case Diff.RemoveAttribute(name) => dom.removeAttribute(name) 14 | case Diff.UpdateAttribute(name, f) => 15 | f(Option(dom.getAttribute(name))) match { 16 | case None => dom.removeAttribute(name) 17 | case Some(v) => dom.setAttribute(name, v) 18 | } 19 | 20 | case Diff.InsertBefore(childRef, nodes) => 21 | val ref = DOM.resolve(dom, childRef) 22 | nodes.reverse.foreach(child => 23 | dom.insertBefore(NodeRender.renderChild(child), ref)) 24 | 25 | case Diff.InsertAfter(childRef, nodes) => 26 | val ref = DOM.resolve(dom, childRef) 27 | nodes.reverse.foreach(child => 28 | dom.insertAfter(NodeRender.renderChild(child), ref)) 29 | 30 | case Diff.SetChildren(children) => 31 | dom.removeChildren() 32 | children.foreach(child => 33 | dom.appendChild(NodeRender.renderChild(child))) 34 | 35 | case Diff.Replace(nodes) => 36 | nodes.foreach(node => 37 | dom.parentNode.insertBefore(NodeRender.renderChild(node), dom)) 38 | dom.parentNode.removeChild(dom) 39 | 40 | case Diff.PrependChildren(children) => 41 | children.reverse.foreach(child => 42 | dom.prependChild(NodeRender.renderChild(child))) 43 | 44 | case Diff.AppendChildren(children) => 45 | children.foreach(child => 46 | dom.appendChild(NodeRender.renderChild(child))) 47 | 48 | case Diff.InsertAt(position, children) => 49 | children.reverse.foreach(child => 50 | dom.insertChildAt(position, NodeRender.renderChild(child))) 51 | 52 | case Diff.RemoveNode => dom.parentNode.removeChild(dom) 53 | } 54 | } 55 | 56 | class DomRenderContext extends RenderContext { 57 | var committed = false 58 | val diffs = mutable.Queue.empty[(TagRef[Singleton], Diff)] 59 | 60 | override def render[T <: Singleton](tagRef: TagRef[T], diff: Diff): Unit = { 61 | if (committed) throw new Exception("Dangling rendering context") 62 | diffs.enqueue((tagRef.asInstanceOf[TagRef[Singleton]], diff)) 63 | } 64 | 65 | def commit(): Unit = { 66 | while (diffs.nonEmpty) { 67 | val (ref, diff) = diffs.dequeue() 68 | ref.resolve.foreach(node => DiffRender.render(node, diff)) 69 | } 70 | 71 | committed = true 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/scala-js/pine/dom/Document.scala: -------------------------------------------------------------------------------- 1 | package pine.dom 2 | 3 | import org.scalajs.dom 4 | 5 | object Document { 6 | def dragEnd: Event[dom.DragEvent] = 7 | new Event(dom.document.ondragend = _) 8 | def keyDown: Event[dom.KeyboardEvent] = 9 | new Event(dom.document.onkeydown = _) 10 | def dragOver: Event[dom.DragEvent] = 11 | new Event(dom.document.ondragover = _) 12 | def keyUp: Event[dom.KeyboardEvent] = 13 | new Event(dom.document.onkeyup = _) 14 | def reset: Event[dom.Event] = 15 | new Event(dom.document.onreset = _) 16 | def mouseUp: Event[dom.MouseEvent] = 17 | new Event(dom.document.onmouseup = _) 18 | def dragStart: Event[dom.DragEvent] = 19 | new Event(dom.document.ondragstart = _) 20 | def drag: Event[dom.DragEvent] = 21 | new Event(dom.document.ondrag = _) 22 | def mouseOver: Event[dom.MouseEvent] = 23 | new Event(dom.document.onmouseover = _) 24 | def dragLeave: Event[dom.DragEvent] = 25 | new Event(dom.document.ondragleave = _) 26 | def pause: Event[dom.Event] = 27 | new Event(dom.document.onpause = _) 28 | def mouseDown: Event[dom.MouseEvent] = 29 | new Event(dom.document.onmousedown = _) 30 | def seeked: Event[dom.Event] = 31 | new Event(dom.document.onseeked = _) 32 | def click: Event[dom.MouseEvent] = 33 | new Event(dom.document.onclick = _) 34 | def waiting: Event[dom.Event] = 35 | new Event(dom.document.onwaiting = _) 36 | def durationChange: Event[dom.Event] = 37 | new Event(dom.document.ondurationchange = _) 38 | def blur: Event[dom.FocusEvent] = 39 | new Event(dom.document.onblur = _) 40 | def emptied: Event[dom.Event] = 41 | new Event(dom.document.onemptied = _) 42 | def seeking: Event[dom.Event] = 43 | new Event(dom.document.onseeking = _) 44 | def canPlay: Event[dom.Event] = 45 | new Event(dom.document.oncanplay = _) 46 | def stalled: Event[dom.Event] = 47 | new Event(dom.document.onstalled = _) 48 | def mouseMove: Event[dom.MouseEvent] = 49 | new Event(dom.document.onmousemove = _) 50 | def rateChange: Event[dom.Event] = 51 | new Event(dom.document.onratechange = _) 52 | def loadStart: Event[dom.Event] = 53 | new Event(dom.document.onloadstart = _) 54 | def dragEnter: Event[dom.DragEvent] = 55 | new Event(dom.document.ondragenter = _) 56 | def submit: Event[dom.Event] = 57 | new Event(dom.document.onsubmit = _) 58 | // def progress: DomEvent[js.Any] = 59 | // new DomEvent(dom.document.onprogress = _) 60 | def dblClick: Event[dom.MouseEvent] = 61 | new Event(dom.document.ondblclick = _) 62 | def contextMenu: Event[dom.MouseEvent] = 63 | new Event(dom.document.oncontextmenu = _) 64 | def change: Event[dom.Event] = 65 | new Event(dom.document.onchange = _) 66 | def loadedMetadata: Event[dom.Event] = 67 | new Event(dom.document.onloadedmetadata = _) 68 | def play: Event[dom.Event] = 69 | new Event(dom.document.onplay = _) 70 | def error: Event[dom.Event] = 71 | new Event(dom.document.onerror = _) 72 | def playing: Event[dom.Event] = 73 | new Event(dom.document.onplaying = _) 74 | def canPlayThrough: Event[dom.Event] = 75 | new Event(dom.document.oncanplaythrough = _) 76 | def abort: Event[dom.UIEvent] = 77 | new Event(dom.document.onabort = _) 78 | def readyStateChange: Event[dom.Event] = 79 | new Event(dom.document.onreadystatechange = _) 80 | def keyPress: Event[dom.KeyboardEvent] = 81 | new Event(dom.document.onkeypress = _) 82 | def loadedData: Event[dom.Event] = 83 | new Event(dom.document.onloadeddata = _) 84 | def suspend: Event[dom.Event] = 85 | new Event(dom.document.onsuspend = _) 86 | def focus: Event[dom.FocusEvent] = 87 | new Event(dom.document.onfocus = _) 88 | def timeUpdate: Event[dom.Event] = 89 | new Event(dom.document.ontimeupdate = _) 90 | def select: Event[dom.UIEvent] = 91 | new Event(dom.document.onselect = _) 92 | def drop: Event[dom.DragEvent] = 93 | new Event(dom.document.ondrop = _) 94 | def mouseOut: Event[dom.MouseEvent] = 95 | new Event(dom.document.onmouseout = _) 96 | def ended: Event[dom.Event] = 97 | new Event(dom.document.onended = _) 98 | def scroll: Event[dom.UIEvent] = 99 | new Event(dom.document.onscroll = _) 100 | def mouseWheel: Event[dom.WheelEvent] = 101 | new Event(dom.document.onmousewheel = _) 102 | def load: Event[dom.Event] = 103 | new Event(dom.document.onload = _) 104 | def volumeChange: Event[dom.Event] = 105 | new Event(dom.document.onvolumechange = _) 106 | def input: Event[dom.Event] = 107 | new Event(dom.document.oninput = _) 108 | } 109 | -------------------------------------------------------------------------------- /src/main/scala-js/pine/dom/Event.scala: -------------------------------------------------------------------------------- 1 | package pine.dom 2 | 3 | import org.scalajs.dom 4 | 5 | import scala.scalajs.js 6 | 7 | class Event[T <: dom.Event](setF: js.Function1[T, _] => Unit) { 8 | def set(f: => Unit): Unit = set((_: T) => f) 9 | def set(f: T => Unit): Unit = setF(f) 10 | 11 | def detach(): Unit = setF(null) 12 | 13 | def :=(diff: => Unit): Unit = set(diff) 14 | def :=(diff: T => Unit): Unit = set(diff) 15 | } 16 | 17 | class EventN[T <: dom.Event](nodes: List[dom.Element], setF: (dom.Element, js.Function1[T, _]) => Unit) { 18 | def set(f: => Unit): Unit = set((_: T) => f) 19 | def set(f: T => Unit): Unit = nodes.foreach(setF(_, f)) 20 | 21 | def detach(): Unit = nodes.foreach(setF(_, null)) 22 | 23 | def :=(diff: => Unit): Unit = set(diff) 24 | def :=(diff: T => Unit): Unit = set(diff) 25 | } 26 | 27 | class EventHtml[T <: dom.Event](nodes: List[dom.html.Element], setF: (dom.html.Element, js.Function1[T, _]) => Unit) { 28 | def set(f: => Unit): Unit = set((_: T) => f) 29 | def set(f: T => Unit): Unit = nodes.foreach(setF(_, f)) 30 | 31 | def detach(): Unit = nodes.foreach(setF(_, null)) 32 | 33 | def :=(diff: => Unit): Unit = set(diff) 34 | def :=(diff: T => Unit): Unit = set(diff) 35 | } 36 | -------------------------------------------------------------------------------- /src/main/scala-js/pine/dom/Implicits.scala: -------------------------------------------------------------------------------- 1 | package pine.dom 2 | 3 | import org.scalajs.dom._ 4 | import org.scalajs.dom.ext._ 5 | 6 | import pine.{HtmlHelpers, TagRef, TagRefAttribute, TagRefTokenSetAttribute, tag} 7 | 8 | trait Implicits { 9 | /** Resolve DOM node */ 10 | private final def domOpt[T <: Singleton](tagRef: TagRef[T]) 11 | (implicit js: Js[T]): Option[js.X] = 12 | tagRef match { 13 | case TagRef.ById(id) => 14 | Option(document.getElementById(id).asInstanceOf[js.X]) 15 | case TagRef.ByTag(tag) => 16 | val nodes = document.getElementsByTagName(tag) 17 | if (nodes.length == 0) None else Some(nodes.item(0).asInstanceOf[js.X]) 18 | case TagRef.ByClass(cls) => 19 | val nodes = document.getElementsByClassName(cls) 20 | if (nodes.length == 0) None else Some(nodes.item(0).asInstanceOf[js.X]) 21 | case t => throw new Exception(s"`dom` must be called on sub-type $t") 22 | } 23 | 24 | /** Resolve all DOM nodes */ 25 | private def domEach[T <: Singleton](tagRef: TagRef[T]) 26 | (implicit js: Js[T]): List[js.X] = 27 | tagRef match { 28 | case TagRef.ByTag(tag) => 29 | document.getElementsByTagName(tag).toList.asInstanceOf[List[js.X]] 30 | case TagRef.ByClass(cls) => 31 | document.getElementsByClassName(cls).toList.asInstanceOf[List[js.X]] 32 | case t => throw new Exception(s"Unexpected type $t") 33 | } 34 | 35 | implicit class TagRefOptExtensions[T <: Singleton](opt: TagRef.Opt[T]) { 36 | def dom(implicit js: Js[T]): Option[js.X] = domOpt(opt.tagRef)(js) 37 | } 38 | 39 | implicit class TagRefEachExtensions[T <: Singleton](each: TagRef.Each[T]) { 40 | def dom(implicit js: Js[T]): List[js.X] = domEach(each.tagRef)(js) 41 | } 42 | 43 | implicit class TagRefExtensions[T <: Singleton](tagRef: TagRef[T]) { 44 | def dom(implicit js: Js[T]): js.X = 45 | domOpt(tagRef)(js).getOrElse( 46 | throw new Exception(s"Invalid node reference $tagRef")) 47 | 48 | private[dom] def resolve(implicit js: Js[T]): List[js.X] = 49 | tagRef match { 50 | case TagRef.Each(tr) => domEach(tr)(js) 51 | case TagRef.Opt (tr) => domOpt(tr)(js).toList 52 | case _ => List(dom(js)) 53 | } 54 | 55 | def cut(implicit js: Js[T]): EventN[ClipboardEvent] = 56 | new EventN(tagRef.resolve, _.oncut = _) 57 | def copy(implicit js: Js[T]): EventN[ClipboardEvent] = 58 | new EventN(tagRef.resolve, _.oncopy = _) 59 | def paste(implicit js: Js[T]): EventN[ClipboardEvent] = 60 | new EventN(tagRef.resolve, _.onpaste = _) 61 | } 62 | 63 | implicit class TagRefHtmlExtensions[T <: Singleton](tagRef: TagRef[T]) { 64 | def onEnter(f: String => Unit)(implicit js: JsHtml[T], ev: T <:< tag.Input): Unit = 65 | tagRef.keyPress := { e => 66 | if (e.keyCode == KeyCode.Enter) 67 | f(tagRef.dom.asInstanceOf[org.scalajs.dom.html.Input].value) 68 | } 69 | 70 | def dragEnd(implicit js: JsHtml[T]): EventHtml[DragEvent] = 71 | new EventHtml(tagRef.resolve(js), _.ondragend = _) 72 | def keyDown(implicit js: JsHtml[T]): EventHtml[KeyboardEvent] = 73 | new EventHtml(tagRef.resolve, _.onkeydown = _) 74 | def dragOver(implicit js: JsHtml[T]): EventHtml[DragEvent] = 75 | new EventHtml(tagRef.resolve, _.ondragover = _) 76 | def keyUp(implicit js: JsHtml[T]): EventHtml[KeyboardEvent] = 77 | new EventHtml(tagRef.resolve, _.onkeyup = _) 78 | def reset(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 79 | new EventHtml(tagRef.resolve, _.onreset = _) 80 | def mouseUp(implicit js: JsHtml[T]): EventHtml[MouseEvent] = 81 | new EventHtml(tagRef.resolve, _.onmouseup = _) 82 | def dragStart(implicit js: JsHtml[T]): EventHtml[DragEvent] = 83 | new EventHtml(tagRef.resolve, _.ondragstart = _) 84 | def drag(implicit js: JsHtml[T]): EventHtml[DragEvent] = 85 | new EventHtml(tagRef.resolve, _.ondrag = _) 86 | def mouseOver(implicit js: JsHtml[T]): EventHtml[MouseEvent] = 87 | new EventHtml(tagRef.resolve, _.onmouseover = _) 88 | def dragLeave(implicit js: JsHtml[T]): EventHtml[DragEvent] = 89 | new EventHtml(tagRef.resolve, _.ondragleave = _) 90 | def pause(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 91 | new EventHtml(tagRef.resolve, _.onpause = _) 92 | def mouseDown(implicit js: JsHtml[T]): EventHtml[MouseEvent] = 93 | new EventHtml(tagRef.resolve, _.onmousedown = _) 94 | def seeked(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 95 | new EventHtml(tagRef.resolve, _.onseeked = _) 96 | def click(implicit js: JsHtml[T]): EventHtml[MouseEvent] = 97 | new EventHtml(tagRef.resolve, _.onclick = _) 98 | def waiting(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 99 | new EventHtml(tagRef.resolve, _.onwaiting = _) 100 | def durationChange(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 101 | new EventHtml(tagRef.resolve, _.ondurationchange = _) 102 | def blur(implicit js: JsHtml[T]): EventHtml[FocusEvent] = 103 | new EventHtml(tagRef.resolve, _.onblur = _) 104 | def emptied(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 105 | new EventHtml(tagRef.resolve, _.onemptied = _) 106 | def seeking(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 107 | new EventHtml(tagRef.resolve, _.onseeking = _) 108 | def canPlay(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 109 | new EventHtml(tagRef.resolve, _.oncanplay = _) 110 | def stalled(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 111 | new EventHtml(tagRef.resolve, _.onstalled = _) 112 | def mouseMove(implicit js: JsHtml[T]): EventHtml[MouseEvent] = 113 | new EventHtml(tagRef.resolve, _.onmousemove = _) 114 | def rateChange(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 115 | new EventHtml(tagRef.resolve, _.onratechange = _) 116 | def loadStart(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 117 | new EventHtml(tagRef.resolve, _.onloadstart = _) 118 | def dragEnter(implicit js: JsHtml[T]): EventHtml[DragEvent] = 119 | new EventHtml(tagRef.resolve, _.ondragenter = _) 120 | def submit(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 121 | new EventHtml(tagRef.resolve, _.onsubmit = _) 122 | // def progress(implicit js: JsHtml[T]): DomEventHtml[scalajs.js.Any] = 123 | // new DomEventHtml(tagRef.resolve, _.onprogress = _) 124 | def dblClick(implicit js: JsHtml[T]): EventHtml[MouseEvent] = 125 | new EventHtml(tagRef.resolve, _.ondblclick = _) 126 | def contextMenu(implicit js: JsHtml[T]): EventHtml[MouseEvent] = 127 | new EventHtml(tagRef.resolve, _.oncontextmenu = _) 128 | def change(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 129 | new EventHtml(tagRef.resolve, _.onchange = _) 130 | def loadedMetadata(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 131 | new EventHtml(tagRef.resolve, _.onloadedmetadata = _) 132 | def play(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 133 | new EventHtml(tagRef.resolve, _.onplay = _) 134 | def playing(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 135 | new EventHtml(tagRef.resolve, _.onplaying = _) 136 | def canPlayThrough(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 137 | new EventHtml(tagRef.resolve, _.oncanplaythrough = _) 138 | def abort(implicit js: JsHtml[T]): EventHtml[UIEvent] = 139 | new EventHtml(tagRef.resolve, _.onabort = _) 140 | def readyStateChange(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 141 | new EventHtml(tagRef.resolve, _.onreadystatechange = _) 142 | def keyPress(implicit js: JsHtml[T]): EventHtml[KeyboardEvent] = 143 | new EventHtml(tagRef.resolve, _.onkeypress = _) 144 | def loadedData(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 145 | new EventHtml(tagRef.resolve, _.onloadeddata = _) 146 | def suspend(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 147 | new EventHtml(tagRef.resolve, _.onsuspend = _) 148 | def focus(implicit js: JsHtml[T]): EventHtml[FocusEvent] = 149 | new EventHtml(tagRef.resolve, _.onfocus = _) 150 | def timeUpdate(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 151 | new EventHtml(tagRef.resolve, _.ontimeupdate = _) 152 | def select(implicit js: JsHtml[T]): EventHtml[UIEvent] = 153 | new EventHtml(tagRef.resolve, _.onselect = _) 154 | def drop(implicit js: JsHtml[T]): EventHtml[DragEvent] = 155 | new EventHtml(tagRef.resolve, _.ondrop = _) 156 | def mouseOut(implicit js: JsHtml[T]): EventHtml[MouseEvent] = 157 | new EventHtml(tagRef.resolve, _.onmouseout = _) 158 | def ended(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 159 | new EventHtml(tagRef.resolve, _.onended = _) 160 | def scroll(implicit js: JsHtml[T]): EventHtml[UIEvent] = 161 | new EventHtml(tagRef.resolve, _.onscroll = _) 162 | def mouseWheel(implicit js: JsHtml[T]): EventHtml[WheelEvent] = 163 | new EventHtml(tagRef.resolve, _.onmousewheel = _) 164 | def volumeChange(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 165 | new EventHtml(tagRef.resolve, _.onvolumechange = _) 166 | def input(implicit js: JsHtml[T]): EventHtml[org.scalajs.dom.Event] = 167 | new EventHtml(tagRef.resolve, _.oninput = _) 168 | } 169 | 170 | implicit class TagRefAttributeExtensions[T <: Singleton, U]( 171 | attribute: TagRefAttribute[T, U] 172 | ) { 173 | def get(implicit js: Js[T]): U = { 174 | val node = attribute.parent.dom 175 | attribute.codec.decode(Option(node.getAttribute(attribute.name))) 176 | } 177 | } 178 | 179 | implicit class TagRefTokenSetAttributeExtensions[T <: Singleton, U]( 180 | attribute: TagRefTokenSetAttribute[T] 181 | ) { 182 | def get(implicit js: Js[T]): List[String] = { 183 | val node = attribute.parent.dom 184 | Option(node.getAttribute(attribute.name)) 185 | .map(HtmlHelpers.parseTokenSet).getOrElse(List.empty) 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/main/scala-js/pine/dom/Js.scala: -------------------------------------------------------------------------------- 1 | package pine.dom 2 | 3 | import org.scalajs.dom 4 | import pine._ 5 | 6 | trait Js[T <: Singleton] { type X <: dom.Element } 7 | 8 | trait JsSvg [T <: Singleton] extends Js[T] { override type X <: dom.svg.Element } 9 | trait JsHtml[T <: Singleton] extends Js[T] { override type X <: dom.html.Element } 10 | 11 | trait JsLowPrio { 12 | implicit object JsTag extends Js[Singleton] { override type X = dom.Element } 13 | } 14 | 15 | object Js extends JsLowPrio { 16 | implicit object JsA extends JsHtml[tag.A] { override type X = dom.html.Anchor } 17 | implicit object JsB extends JsHtml[tag.B] { override type X = dom.html.Span } 18 | implicit object JsArea extends JsHtml[tag.Area] { override type X = dom.html.Area } 19 | implicit object JsAudio extends JsHtml[tag.Audio] { override type X = dom.html.Audio } 20 | implicit object JsBR extends JsHtml[tag.Br] { override type X = dom.html.BR } 21 | implicit object JsBase extends JsHtml[tag.Base] { override type X = dom.html.Base } 22 | implicit object JsBody extends JsHtml[tag.Body] { override type X = dom.html.Body } 23 | implicit object JsButton extends JsHtml[tag.Button] { override type X = dom.html.Button } 24 | implicit object JsCanvas extends JsHtml[tag.Canvas] { override type X = dom.html.Canvas } 25 | // implicit object JsCollection extends JsHtml[tag.Col] { override type X = dom.html.Collection } 26 | implicit object JsDList extends JsHtml[tag.Dl] { override type X = dom.html.DList } 27 | implicit object JsDataList extends JsHtml[tag.Datalist] { override type X = dom.html.DataList } 28 | implicit object JsDiv extends JsHtml[tag.Div] { override type X = dom.html.Div } 29 | implicit object JsEmbed extends JsHtml[tag.Embed] { override type X = dom.html.Embed } 30 | implicit object JsFieldSet extends JsHtml[tag.Fieldset] { override type X = dom.html.FieldSet } 31 | implicit object JsForm extends JsHtml[tag.Form] { override type X = dom.html.Form } 32 | implicit object JsH1 extends JsHtml[tag.H1] { override type X = dom.html.Heading } 33 | implicit object JsH2 extends JsHtml[tag.H2] { override type X = dom.html.Heading } 34 | implicit object JsH3 extends JsHtml[tag.H3] { override type X = dom.html.Heading } 35 | implicit object JsH4 extends JsHtml[tag.H4] { override type X = dom.html.Heading } 36 | implicit object JsH5 extends JsHtml[tag.H5] { override type X = dom.html.Heading } 37 | implicit object JsH6 extends JsHtml[tag.H6] { override type X = dom.html.Heading } 38 | implicit object JsHR extends JsHtml[tag.Hr] { override type X = dom.html.HR } 39 | implicit object JsHead extends JsHtml[tag.Head] { override type X = dom.html.Head } 40 | implicit object JsHtml extends JsHtml[tag.Html] { override type X = dom.html.Html } 41 | implicit object JsI extends JsHtml[tag.I] { override type X = dom.html.Span } 42 | implicit object JsIFrame extends JsHtml[tag.Iframe] { override type X = dom.html.IFrame } 43 | implicit object JsImage extends JsHtml[tag.Img] { override type X = dom.html.Image } 44 | implicit object JsInput extends JsHtml[tag.Input] { override type X = dom.html.Input } 45 | implicit object JsLabel extends JsHtml[tag.Label] { override type X = dom.html.Label } 46 | implicit object JsLegend extends JsHtml[tag.Legend] { override type X = dom.html.Legend } 47 | implicit object JsLi extends JsHtml[tag.Li] { override type X = dom.html.LI } 48 | implicit object JsLink extends JsHtml[tag.Link] { override type X = dom.html.Link } 49 | implicit object JsMapJS extends JsHtml[tag.Map] { override type X = dom.html.Map } 50 | implicit object JsMenu extends JsHtml[tag.Menu] { override type X = dom.html.Menu } 51 | implicit object JsMeta extends JsHtml[tag.Meta] { override type X = dom.html.Meta } 52 | implicit object JsOList extends JsHtml[tag.Ol] { override type X = dom.html.OList } 53 | implicit object JsObject extends JsHtml[tag.Object] { override type X = dom.html.Object } 54 | implicit object JsOptGroup extends JsHtml[tag.Optgroup] { override type X = dom.html.OptGroup } 55 | implicit object JsOpt extends JsHtml[tag.Option] { override type X = dom.html.Option } 56 | implicit object JsParagraph extends JsHtml[tag.P] { override type X = dom.html.Paragraph } 57 | implicit object JsParam extends JsHtml[tag.Param] { override type X = dom.html.Param } 58 | implicit object JsPre extends JsHtml[tag.Pre] { override type X = dom.html.Pre } 59 | implicit object JsProgress extends JsHtml[tag.Progress] { override type X = dom.html.Progress } 60 | implicit object JsScript extends JsHtml[tag.Script] { override type X = dom.html.Script } 61 | implicit object JsSelect extends JsHtml[tag.Select] { override type X = dom.html.Select } 62 | implicit object JsSource extends JsHtml[tag.Source] { override type X = dom.html.Source } 63 | implicit object JsSpan extends JsHtml[tag.Span] { override type X = dom.html.Span } 64 | implicit object JsStrong extends JsHtml[tag.Strong] { override type X = dom.html.Span } 65 | implicit object JsStrike extends JsHtml[tag.Strike] { override type X = dom.html.Span } 66 | implicit object JsStyle extends JsHtml[tag.Style] { override type X = dom.html.Style } 67 | implicit object JsTable extends JsHtml[tag.Table] { override type X = dom.html.Table } 68 | implicit object JsTableRow extends JsHtml[tag.Tr] { override type X = dom.html.TableRow } 69 | implicit object JsTableDataCell extends JsHtml[tag.Td] { override type X = dom.html.TableCell } 70 | implicit object JsTableHeadCell extends JsHtml[tag.Th] { override type X = dom.html.TableCell } 71 | implicit object JsTextArea extends JsHtml[tag.Textarea] { override type X = dom.html.TextArea } 72 | implicit object JsTitle extends JsHtml[tag.Title] { override type X = dom.html.Title } 73 | implicit object JsTrack extends JsHtml[tag.Track] { override type X = dom.html.Track } 74 | implicit object JsUl extends JsHtml[tag.Ul] { override type X = dom.html.UList } 75 | implicit object JsVideo extends JsHtml[tag.Video] { override type X = dom.html.Video } 76 | 77 | implicit object JsSvg extends JsSvg[tag.Svg] { override type X = dom.svg.SVG } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/scala-js/pine/dom/NodeRender.scala: -------------------------------------------------------------------------------- 1 | package pine.dom 2 | 3 | import org.scalajs.dom 4 | 5 | import pine._ 6 | 7 | object NodeRender extends TagRender[Tag[_], dom.Element] { 8 | trait Implicit { 9 | implicit class TextToDom(node: Text) { 10 | def toDom: dom.raw.Text = renderText(node) 11 | } 12 | 13 | implicit class TagToDom[T <: Singleton](node: Tag[T]) { 14 | def toDom(implicit js: Js[T]): js.X = renderTag(node).asInstanceOf[js.X] 15 | } 16 | } 17 | 18 | override def render(tag: Tag[_]): dom.Element = renderTag(tag) 19 | 20 | @inline def renderChild(node: Node): dom.Node = 21 | node match { 22 | case n @ Tag(_, _, _) => renderTag(n) 23 | case n @ Text(_) => renderText(n) 24 | } 25 | 26 | @inline def renderChildWithNamespace(node: Node, xmlns: String): dom.Node = 27 | node match { 28 | case n @ Tag(_, _, _) => renderTagWithNamespace(n, xmlns) 29 | case n @ Text(_) => renderText(n) 30 | } 31 | 32 | def renderTagWithNamespace(node: Tag[_], xmlns: String): dom.Element = { 33 | val element = dom.document.createElementNS(xmlns, node.tagName) 34 | node.attributes.foreach { case (k, v) => 35 | if (k == "xmlns" || k.contains(":")) element.setAttribute(k, v.toString) 36 | else element.setAttributeNS(null, k, v.toString) 37 | } 38 | 39 | node 40 | .children.map(renderChildWithNamespace(_, xmlns)) 41 | .foreach(element.appendChild) 42 | element 43 | } 44 | 45 | def renderTag(node: Tag[_]): dom.Element = { 46 | val element = 47 | node.attr("xmlns") match { 48 | case Some(xmlns) => renderTagWithNamespace(node, xmlns) 49 | case None => 50 | val element = dom.document.createElement(node.tagName) 51 | node.attributes.foreach { case (k, v) => 52 | element.setAttribute(k, v.toString) 53 | } 54 | element 55 | } 56 | 57 | node.children.map(renderChild).foreach(element.appendChild) 58 | element 59 | } 60 | 61 | @inline def renderText(node: Text): dom.raw.Text = 62 | dom.document.createTextNode(node.text) 63 | } 64 | -------------------------------------------------------------------------------- /src/main/scala-js/pine/dom/Window.scala: -------------------------------------------------------------------------------- 1 | package pine.dom 2 | 3 | import org.scalajs.dom 4 | 5 | object Window { 6 | def dragEnd: Event[dom.DragEvent] = 7 | new Event(dom.window.ondragend = _) 8 | def keyDown: Event[dom.KeyboardEvent] = 9 | new Event(dom.window.onkeydown = _) 10 | def dragOver: Event[dom.DragEvent] = 11 | new Event(dom.window.ondragover = _) 12 | def keyUp: Event[dom.KeyboardEvent] = 13 | new Event(dom.window.onkeyup = _) 14 | def reset: Event[dom.Event] = 15 | new Event(dom.window.onreset = _) 16 | def mouseUp: Event[dom.MouseEvent] = 17 | new Event(dom.window.onmouseup = _) 18 | def dragStart: Event[dom.DragEvent] = 19 | new Event(dom.window.ondragstart = _) 20 | def drag: Event[dom.DragEvent] = 21 | new Event(dom.window.ondrag = _) 22 | def mouseOver: Event[dom.MouseEvent] = 23 | new Event(dom.window.onmouseover = _) 24 | def dragLeave: Event[dom.DragEvent] = 25 | new Event(dom.window.ondragleave = _) 26 | def afterPrint: Event[dom.Event] = 27 | new Event(dom.window.onafterprint = _) 28 | def pause: Event[dom.Event] = 29 | new Event(dom.window.onpause = _) 30 | def beforePrint: Event[dom.Event] = 31 | new Event(dom.window.onbeforeprint = _) 32 | def mouseDown: Event[dom.MouseEvent] = 33 | new Event(dom.window.onmousedown = _) 34 | def seeked: Event[dom.Event] = 35 | new Event(dom.window.onseeked = _) 36 | def click: Event[dom.MouseEvent] = 37 | new Event(dom.window.onclick = _) 38 | def waiting: Event[dom.Event] = 39 | new Event(dom.window.onwaiting = _) 40 | def online: Event[dom.Event] = 41 | new Event(dom.window.ononline = _) 42 | def durationChange: Event[dom.Event] = 43 | new Event(dom.window.ondurationchange = _) 44 | def blur: Event[dom.FocusEvent] = 45 | new Event(dom.window.onblur = _) 46 | def emptied: Event[dom.Event] = 47 | new Event(dom.window.onemptied = _) 48 | def seeking: Event[dom.Event] = 49 | new Event(dom.window.onseeking = _) 50 | def canPlay: Event[dom.Event] = 51 | new Event(dom.window.oncanplay = _) 52 | def stalled: Event[dom.Event] = 53 | new Event(dom.window.onstalled = _) 54 | def mouseMove: Event[dom.MouseEvent] = 55 | new Event(dom.window.onmousemove = _) 56 | def offline: Event[dom.Event] = 57 | new Event(dom.window.onoffline = _) 58 | def beforeUnload: Event[dom.BeforeUnloadEvent] = 59 | new Event(dom.window.onbeforeunload = _) 60 | def rateChange: Event[dom.Event] = 61 | new Event(dom.window.onratechange = _) 62 | def storage: Event[dom.StorageEvent] = 63 | new Event(dom.window.onstorage = _) 64 | def loadStart: Event[dom.Event] = 65 | new Event(dom.window.onloadstart = _) 66 | def dragEnter: Event[dom.DragEvent] = 67 | new Event(dom.window.ondragenter = _) 68 | def submit: Event[dom.Event] = 69 | new Event(dom.window.onsubmit = _) 70 | // def progress: DomEvent[js.Any] = 71 | // new DomEvent(dom.window.onprogress = _) 72 | def dblClick: Event[dom.MouseEvent] = 73 | new Event(dom.window.ondblclick = _) 74 | def contextMenu: Event[dom.MouseEvent] = 75 | new Event(dom.window.oncontextmenu = _) 76 | def change: Event[dom.Event] = 77 | new Event(dom.window.onchange = _) 78 | def loadedMetadata: Event[dom.Event] = 79 | new Event(dom.window.onloadedmetadata = _) 80 | def play: Event[dom.Event] = 81 | new Event(dom.window.onplay = _) 82 | // def error: DomEvent[dom.Event] = 83 | // new DomEvent(dom.window.onerror = _) 84 | def playing: Event[dom.Event] = 85 | new Event(dom.window.onplaying = _) 86 | def canPlayThrough: Event[dom.Event] = 87 | new Event(dom.window.oncanplaythrough = _) 88 | def abort: Event[dom.UIEvent] = 89 | new Event(dom.window.onabort = _) 90 | def readyStateChange: Event[dom.Event] = 91 | new Event(dom.window.onreadystatechange = _) 92 | def keyPress: Event[dom.KeyboardEvent] = 93 | new Event(dom.window.onkeypress = _) 94 | def loadedData: Event[dom.Event] = 95 | new Event(dom.window.onloadeddata = _) 96 | def suspend: Event[dom.Event] = 97 | new Event(dom.window.onsuspend = _) 98 | def focus: Event[dom.FocusEvent] = 99 | new Event(dom.window.onfocus = _) 100 | def message: Event[dom.MessageEvent] = 101 | new Event(dom.window.onmessage = _) 102 | def timeUpdate: Event[dom.Event] = 103 | new Event(dom.window.ontimeupdate = _) 104 | def resize: Event[dom.UIEvent] = 105 | new Event(dom.window.onresize = _) 106 | def select: Event[dom.UIEvent] = 107 | new Event(dom.window.onselect = _) 108 | def drop: Event[dom.DragEvent] = 109 | new Event(dom.window.ondrop = _) 110 | def mouseOut: Event[dom.MouseEvent] = 111 | new Event(dom.window.onmouseout = _) 112 | def ended: Event[dom.Event] = 113 | new Event(dom.window.onended = _) 114 | def hashChange: Event[dom.HashChangeEvent] = 115 | new Event(dom.window.onhashchange = _) 116 | def unload: Event[dom.Event] = 117 | new Event(dom.window.onunload = _) 118 | def scroll: Event[dom.UIEvent] = 119 | new Event(dom.window.onscroll = _) 120 | def mouseWheel: Event[dom.WheelEvent] = 121 | new Event(dom.window.onmousewheel = _) 122 | def load: Event[dom.Event] = 123 | new Event(dom.window.onload = _) 124 | def volumeChange: Event[dom.Event] = 125 | new Event(dom.window.onvolumechange = _) 126 | def input: Event[dom.Event] = 127 | new Event(dom.window.oninput = _) 128 | def popState: Event[dom.PopStateEvent] = 129 | new Event(dom.window.onpopstate = _) 130 | } 131 | -------------------------------------------------------------------------------- /src/main/scala-js/pine/dom/package.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | package object dom 4 | extends Implicits 5 | with DOM.Extensions 6 | with dom.NodeRender.Implicit 7 | -------------------------------------------------------------------------------- /src/main/scala-jvm/pine/GenerateEntities.scala: -------------------------------------------------------------------------------- 1 | //package pine 2 | 3 | /*import java.io.{File, FileWriter} 4 | 5 | object GenerateEntities extends App { 6 | // File can be obtained from https://hackage.haskell.org/package/html5-entity-0.2.0.3/src/generation/entities.json 7 | val entities = io.Source.fromFile(new java.io.File("entities.json")).getLines().toList.flatMap { line => 8 | val entity = "&(\\w*);\"".r.findFirstMatchIn(line) 9 | 10 | entity.map { e => 11 | val characters = "characters\": \"(.*)\"".r.findFirstMatchIn(line) 12 | 13 | val entityStr = e.group(1) 14 | val charactersStr = 15 | if (characters.get.group(1) == "\\u005C") "\\u005C\\u005C" 16 | else if (characters.get.group(1) == "\\u0022") "\\u005C\\u0022" 17 | else characters.get.group(1) 18 | 19 | s""""$entityStr" -> \"$charactersStr\"""" 20 | } 21 | } 22 | 23 | val fw = new FileWriter(new File("shared/src/main/scala/pine/HtmlEntities.scala")) 24 | fw.append("package pine\n\n") 25 | fw.append("object HtmlEntities {\n") 26 | fw.append(" val entities: Map[String, String] = Map(\n") 27 | entities.zipWithIndex.foreach { case (entity, i) => 28 | if (i + 1 == entities.length) fw.append(" " + entity + "\n") 29 | else fw.append(" " + entity + ",\n") 30 | } 31 | fw.append(" )\n") 32 | fw.append("}") 33 | fw.flush() 34 | fw.close() 35 | }*/ 36 | -------------------------------------------------------------------------------- /src/main/scala/pine/Attribute.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | trait AttributeCodec[T] { 4 | def encode(value: T): Option[String] 5 | def decode(value: Option[String]): T 6 | } 7 | 8 | case class TagAttribute[T <: Singleton, U](parent: Tag[T], name: String) 9 | (implicit val codec: AttributeCodec[U]) { 10 | def get: U = codec.decode(parent.attr(name)) 11 | def set(value: U): Tag[T] = 12 | codec.encode(value) match { 13 | case None => parent.remAttr(name) 14 | case Some(v) => parent.setAttr(name, v) 15 | } 16 | 17 | def apply(): U = get 18 | def apply(value: U): Tag[T] = set(value) 19 | } 20 | 21 | case class TagRefAttribute[T <: Singleton, U](parent: TagRef[T], name: String) 22 | (implicit val codec: AttributeCodec[U]) { 23 | def set(value: U)(implicit renderCtx: RenderContext): Unit = { 24 | val diff = codec.encode(value) match { 25 | case None => Diff.RemoveAttribute(name) 26 | case Some(v) => Diff.SetAttribute(name, v) 27 | } 28 | 29 | renderCtx.render(parent, diff) 30 | } 31 | 32 | def update(f: U => U)(implicit renderCtx: RenderContext): Unit = { 33 | val diff = Diff.UpdateAttribute(name, { curValue => 34 | val newValue = f(codec.decode(curValue)) 35 | codec.encode(newValue) 36 | }) 37 | 38 | renderCtx.render(parent, diff) 39 | } 40 | 41 | def remove()(implicit renderCtx: RenderContext): Unit = 42 | renderCtx.render(parent, Diff.RemoveAttribute(name)) 43 | 44 | def :=(value: U)(implicit renderCtx: RenderContext): Unit = set(value) 45 | } 46 | 47 | /** 48 | * Insertion order preserving set of space-separated tokens 49 | * 50 | * @see https://html.spec.whatwg.org/#ordered-set-of-unique-space-separated-tokens 51 | * @see https://dom.spec.whatwg.org/#interface-domtokenlist 52 | */ 53 | case class TagTokenSetAttribute[T <: Singleton](parent: Tag[T], name: String) { 54 | def get: List[String] = 55 | parent.attr(name).map(HtmlHelpers.parseTokenSet).getOrElse(List.empty) 56 | def set(value: String): Tag[T] = parent.setAttr(name, value) 57 | def set(values: Seq[String]): Tag[T] = 58 | if (values.isEmpty) parent.remAttr(name) 59 | else parent.setAttr(name, values.mkString(" ")) 60 | def has(value: String): Boolean = get.contains(value) 61 | def add(value: String): Tag[T] = { 62 | val current = get 63 | if (current.contains(value)) parent else set(current :+ value) 64 | } 65 | def remove(value: String): Tag[T] = set(get.diff(List(value))) 66 | def clear(): Tag[T] = parent.remAttr(name) 67 | def toggle(value: String): Tag[T] = { 68 | val current = get 69 | if (current.contains(value)) set(current.diff(List(value))) 70 | else set(current :+ value) 71 | } 72 | def state(state: Boolean, value: String): Tag[T] = 73 | if (state) add(value) else remove(value) 74 | def update(f: List[String] => List[String]): Tag[T] = set(f(get)) 75 | 76 | def apply(): List[String] = get 77 | def apply(values: String*): Tag[T] = set(values) 78 | } 79 | 80 | case class TagRefTokenSetAttribute[T <: Singleton]( 81 | parent: TagRef[T], name: String 82 | ) { 83 | def set(value: String)(implicit renderCtx: RenderContext): Unit = 84 | renderCtx.render(parent, Diff.SetAttribute(name, value)) 85 | 86 | def set(values: Seq[String])(implicit renderCtx: RenderContext): Unit = { 87 | val diff = 88 | if (values.isEmpty) Diff.RemoveAttribute(name) 89 | else Diff.SetAttribute(name, values.mkString(" ")) 90 | 91 | renderCtx.render(parent, diff) 92 | } 93 | 94 | def add(value: String)(implicit renderCtx: RenderContext): Unit = 95 | update { current => 96 | if (current.contains(value)) current 97 | else current :+ value 98 | } 99 | 100 | def remove(value: String)(implicit renderCtx: RenderContext): Unit = 101 | update(_.diff(List(value))) 102 | 103 | def clear()(implicit renderCtx: RenderContext): Unit = 104 | renderCtx.render(parent, Diff.RemoveAttribute(name)) 105 | 106 | def toggle(value: String)(implicit renderCtx: RenderContext): Unit = 107 | update(values => 108 | if (!values.contains(value)) values :+ value 109 | else values.diff(List(value))) 110 | 111 | def state(state: Boolean, value: String) 112 | (implicit renderCtx: RenderContext): Unit = 113 | update(values => 114 | if (!state) values.diff(List(value)) 115 | else if (values.contains(value)) values 116 | else values :+ value) 117 | 118 | def update(f: List[String] => List[String]) 119 | (implicit renderCtx: RenderContext): Unit = 120 | renderCtx.render(parent, Diff.UpdateAttribute(name, { oldValue => 121 | val current = 122 | oldValue.map(HtmlHelpers.parseTokenSet).getOrElse(List.empty) 123 | val updated = f(current) 124 | if (updated.isEmpty) None else Some(updated.mkString(" ")) 125 | })) 126 | 127 | def :=(value: String)(implicit renderCtx: RenderContext): Unit = set(value) 128 | } 129 | 130 | object AttributeCodec { 131 | implicit case object StringAttributeCodec extends AttributeCodec[String] { 132 | override def encode(value: String): Option[String] = Some(value) 133 | override def decode(value: Option[String]): String = value.getOrElse("") 134 | } 135 | 136 | implicit case object IntAttributeCodec extends AttributeCodec[Int] { 137 | override def encode(value: Int): Option[String] = 138 | Some(value.toString) 139 | 140 | override def decode(value: Option[String]): Int = 141 | value.map(_.toInt).getOrElse(0) 142 | } 143 | 144 | implicit case object LongAttributeCodec extends AttributeCodec[Long] { 145 | override def encode(value: Long): Option[String] = 146 | Some(value.toString) 147 | 148 | override def decode(value: Option[String]): Long = 149 | value.map(_.toLong).getOrElse(0L) 150 | } 151 | 152 | implicit case object DoubleAttributeCodec extends AttributeCodec[Double] { 153 | override def encode(value: Double): Option[String] = 154 | Some(value.toString) 155 | 156 | override def decode(value: Option[String]): Double = 157 | value.map(_.toDouble).getOrElse(0) 158 | } 159 | 160 | implicit case object BooleanAttributeCodec extends AttributeCodec[Boolean] { 161 | override def encode(value: Boolean): Option[String] = 162 | if (!value) None else Some("") 163 | 164 | override def decode(value: Option[String]): Boolean = value.nonEmpty 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/main/scala/pine/Diff.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | /** A Diff defines a tree modification */ 4 | sealed trait Diff 5 | object Diff { 6 | case class SetAttribute(name: String, value: String) extends Diff 7 | case class RemoveAttribute(name: String) extends Diff 8 | case class UpdateAttribute(name: String, 9 | f: Option[String] => Option[String]) extends Diff 10 | case class Replace(nodes: List[Node]) extends Diff 11 | case class SetChildren(children: List[Node]) extends Diff 12 | case class PrependChildren(children: List[Node]) extends Diff 13 | case class AppendChildren(children: List[Node]) extends Diff 14 | case class InsertBefore[T <: Singleton](childRef: TagRef[T], 15 | nodes: List[Node]) extends Diff 16 | case class InsertAfter[T <: Singleton](childRef: TagRef[T], 17 | nodes: List[Node]) extends Diff 18 | case class InsertAt(position: Int, children: List[Node]) extends Diff 19 | case object RemoveNode extends Diff 20 | } 21 | -------------------------------------------------------------------------------- /src/main/scala/pine/DiffRender.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | object DiffRender { 4 | def render[T <: Singleton](tag: Tag[T]): Diff => List[Node] = { 5 | case Diff.SetAttribute(name, value) => List(tag.setAttr(name, value)) 6 | case Diff.RemoveAttribute(name) => List(tag.remAttr(name)) 7 | case Diff.UpdateAttribute(name, f) => 8 | List(f(tag.attr(name)) match { 9 | case None => tag.remAttr(name) 10 | case Some(v) => tag.setAttr(name, v) 11 | }) 12 | case Diff.InsertBefore(childRef, nodes) => 13 | List(tag.children.zipWithIndex.collectFirst { 14 | case (t: Tag[_], i) if t.matches(childRef) => i 15 | }.map(tag.insertAt(_, nodes)).getOrElse( 16 | throw new Exception(s"Invalid child reference $childRef"))) 17 | case Diff.InsertAfter(childRef, nodes) => 18 | List(tag.children.zipWithIndex.collectFirst { 19 | case (t: Tag[_], i) if t.matches(childRef) => i + 1 20 | }.map(tag.insertAt(_, nodes)).getOrElse( 21 | throw new Exception(s"Invalid child reference $childRef"))) 22 | case Diff.PrependChildren(children) => List(tag.prependAll(children)) 23 | case Diff.AppendChildren(children) => List(tag.appendAll(children)) 24 | case Diff.Replace(nodes) => nodes 25 | case Diff.SetChildren(children) => List(tag.set(children)) 26 | case Diff.InsertAt(position, children) => 27 | List(tag.insertAt(position, children)) 28 | case Diff.RemoveNode => List.empty 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/pine/HtmlHelpers.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import scala.collection.immutable.StringOps 4 | 5 | object HtmlHelpers { 6 | /** An end tag must not specified for void elements. List of elements taken 7 | * from http://www.w3.org/TR/html-markup/syntax.html#syntax-elements 8 | */ 9 | val VoidElements = Set("area", "base", "br", "col", "command", "embed", "hr", 10 | "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr") 11 | 12 | /** Content of these tags is treated as raw text */ 13 | val CdataElements = Set("script", "style") 14 | 15 | /** @note < and > do not need to be escaped */ 16 | def encodeAttributeValue(value: Any): String = 17 | "\"" + 18 | value.toString 19 | .replaceAll("&", "&") 20 | .replaceAll("\"", """) + 21 | "\"" 22 | 23 | def decodeAttributeValue(value: String): String = 24 | value 25 | .replaceAll("<", "<") 26 | .replaceAll(">", ">") 27 | .replaceAll("&", "&") 28 | .replaceAll(""", "\"") 29 | 30 | /** 31 | * @note " and ' may be represented by " or ', but this is not 32 | * compulsory 33 | * @see scala.xml.Utility.escape() 34 | */ 35 | def encodeText(text: String, xml: Boolean): String = 36 | (text: StringOps).flatMap { c => 37 | c match { 38 | case '<' => "<" 39 | case '>' => ">" 40 | 41 | // TODO In HTML5, we only have to replace ampersands if they are 42 | // ambiguous. As per: 43 | // https://www.w3.org/TR/html5/syntax.html#syntax-ambiguous-ampersand 44 | // 45 | // > An ambiguous ampersand is a U+0026 AMPERSAND character (&) that is 46 | // > followed by one or more alphanumeric ASCII characters, followed by a 47 | // > ";" (U+003B) character, where these characters do not match any of 48 | // > the names given in the named character references section. 49 | case '&' => "&" 50 | 51 | case '\r' | '\n' | '\t' => c.toString 52 | case _ if c >= ' ' => c.toString 53 | case _ => "" 54 | } 55 | } 56 | 57 | def parseTokenSet(value: String): List[String] = 58 | if (value.isEmpty) List.empty else value.split(' ').toList 59 | 60 | /** From http://hohonuuli.blogspot.com/2012/10/simple-hex-string-to-ascii-function-for.html */ 61 | def parseHexBinary(hex: String): String = { 62 | val sb = new StringBuilder 63 | 64 | for (i <- 0 until hex.length by 2) { 65 | val str = hex.substring(i, i + 2) 66 | sb.append(Integer.parseInt(str, 16).toChar) 67 | } 68 | 69 | sb.toString 70 | } 71 | 72 | private def stripLeading(str: String, c: Char): String = 73 | if (str.isEmpty) str 74 | else { 75 | var i = 0 76 | while (str(i) == c) i += 1 77 | str.substring(i) 78 | } 79 | 80 | def decodeEntity(e: String, xml: Boolean): Option[String] = 81 | if (e.startsWith("#x0")) 82 | Some(new String(parseHexBinary(stripLeading(e.substring(3), '0')))) 83 | else if (e.startsWith("#")) Some(e.substring(1).toInt.toChar.toString) 84 | else if (e.isEmpty) None 85 | else if (xml) XmlEntities.entities.get(e) 86 | else HtmlEntities.entities.get(e) 87 | 88 | def decodeText(text: String, xml: Boolean): String = { 89 | val reader = new Reader(text) 90 | 91 | def f(): String = 92 | reader.collect('&') match { 93 | case None => reader.rest() 94 | case Some(prefix) => 95 | reader.collectUntil(c => !c.isLetterOrDigit && c != '#') match { 96 | case None => throw new ParseError("Ambiguous entity") 97 | case Some(e) => 98 | if (!reader.prefix(';')) prefix + "&" + f() 99 | else { 100 | val ent = decodeEntity(e, xml).getOrElse( 101 | throw new ParseError(s"Invalid entity '$e'")) 102 | prefix + ent + f() 103 | } 104 | } 105 | } 106 | 107 | f() 108 | } 109 | 110 | def encodeAttributes(attributes: Map[String, Any]): String = 111 | attributes.map { case (key, value) => 112 | s"$key=" + encodeAttributeValue(value) 113 | }.mkString(" ") 114 | 115 | def node(tagName: String, 116 | attributes: Map[String, Any], 117 | contents: List[String], 118 | xml: Boolean): String = { 119 | val attrs = 120 | if (attributes.isEmpty) "" 121 | else s" ${encodeAttributes(attributes)}" 122 | 123 | // As Pine does not support DTDs, we do not know which elements were 124 | // declared EMPTY. 125 | // See also https://www.w3.org/TR/REC-xml/#NT-EmptyElemTag 126 | if (!xml && HtmlHelpers.VoidElements.contains(tagName) && contents.isEmpty) 127 | s"<$tagName$attrs/>" 128 | else { 129 | val children = contents.mkString 130 | s"<$tagName$attrs>$children" 131 | } 132 | } 133 | 134 | def identifierCharacter(c: Char): Boolean = 135 | c.isLetterOrDigit || c == '-' || c == '_' || c == ':' 136 | } 137 | -------------------------------------------------------------------------------- /src/main/scala/pine/Node.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import scala.annotation.tailrec 4 | import scala.language.implicitConversions 5 | 6 | object Node { 7 | trait Implicits { 8 | implicit def StringToNode(value: String): Node = Text(value) 9 | implicit def BooleanToNode(value: Boolean): Node = Text(value.toString) 10 | implicit def NumericToNode[T](value: T)(implicit num: Numeric[T]): Node = 11 | Text(value.toString) 12 | } 13 | } 14 | 15 | sealed trait Node { 16 | type T <: Node 17 | 18 | def +:[U <: Singleton](node: Tag[U]): Tag[U] = node.prepend(this) 19 | def map(f: Node => Node): T 20 | def flatMap(f: Node => List[Node]): T 21 | def mapFirst(f: PartialFunction[Node, Node]): T 22 | 23 | /** Recursively traverses tree and returns content of first text node */ 24 | def textContent: Option[String] 25 | } 26 | 27 | case class Text(text: String) extends Node { 28 | override type T = Text 29 | 30 | override def map(f: Node => Node): T = this 31 | override def flatMap(f: Node => List[Node]): T = this 32 | override def mapFirst(f: PartialFunction[Node, Node]): T = this 33 | override def textContent: Option[String] = Some(text) 34 | } 35 | 36 | case class Tag[TagName <: Singleton](tagName : String with TagName, 37 | attributes: Map[String, String] = Map.empty, 38 | children : List[Node] = List.empty 39 | ) extends Node { 40 | override type T = Tag[TagName] 41 | 42 | // TODO Rewrite in a more functional style 43 | def find(f: Node => Boolean): Option[Node] = { 44 | if (f(this)) Some(this) 45 | else { 46 | for (child <- children) { 47 | if (f(child)) return Some(child) 48 | else child match { 49 | case t: Tag[_] => 50 | val result = t.find(f) 51 | if (result.nonEmpty) return result 52 | 53 | case _ => 54 | } 55 | } 56 | 57 | None 58 | } 59 | } 60 | 61 | def prepend(node: Node): Tag[TagName] = set(node +: children) 62 | def prependAll(node: List[Node]): Tag[TagName] = set(node ++ children) 63 | 64 | def append(node: Node): Tag[TagName] = set(children :+ node) 65 | def :+(node: Node): Tag[TagName] = append(node) 66 | 67 | def appendAll(nodes: List[Node]): Tag[TagName] = set(children ++ nodes) 68 | def ++(nodes: List[Node]): Tag[TagName] = appendAll(nodes) 69 | 70 | def insertAt(position: Int, nodes: List[Node]): Tag[TagName] = { 71 | assert(position <= children.length) 72 | copy(children = children.patch(position, nodes, 0)) 73 | } 74 | 75 | def insertAt(position: Int, node: Node): Tag[TagName] = 76 | insertAt(position, List(node)) 77 | 78 | def set(node: Node): Tag[TagName] = copy(children = List(node)) 79 | def set(nodes: List[Node]): Tag[TagName] = copy(children = nodes) 80 | 81 | def clearAll: Tag[TagName] = copy(children = List.empty) 82 | 83 | def remove(node: Node): Tag[TagName] = set(children.diff(List(node))) 84 | def -(node: Node): Tag[TagName] = remove(node) 85 | 86 | def removeAll(nodes: List[Node]): Tag[TagName] = set(children.diff(nodes)) 87 | def --(node: List[Node]): Tag[TagName] = removeAll(node) 88 | 89 | def replace(reference: Node, node: Node): Tag[TagName] = 90 | copy(children = children.map(n => if (n == reference) node else n)) 91 | 92 | def tag[S <: Singleton with String](implicit vo: ValueOf[S]): Tag[S] = 93 | copy(tagName = vo.value) 94 | 95 | def attr(attribute: String): Option[String] = attributes.get(attribute) 96 | def setAttr(attribute: String, value: String): Tag[TagName] = 97 | copy(attributes = attributes + (attribute -> value)) 98 | def hasAttr(attribute: String): Boolean = 99 | attributes.contains(attribute) 100 | def remAttr(attribute: String): Tag[TagName] = 101 | copy(attributes = attributes - attribute) 102 | def clearAttr: Tag[TagName] = copy(attributes = Map.empty) 103 | 104 | private def filterChildren(f: Node => Boolean): List[Node] = { 105 | val seq = if (f(this)) List(this) else List.empty 106 | seq ++ children.flatMap { 107 | case t: Tag[_] => t.filterChildren(f) 108 | case n => if (f(n)) List(n) else List.empty 109 | } 110 | } 111 | 112 | def filter(f: Node => Boolean): List[Node] = 113 | children.flatMap { 114 | case tag: Tag[_] => tag.filterChildren(f) 115 | case node => if (f(node)) List(node) else List.empty 116 | } 117 | 118 | def filterTags(f: Tag[_] => Boolean): List[Tag[_]] = 119 | filter { 120 | case t: Tag[_] if f(t) => true 121 | case _ => false 122 | }.map(_.asInstanceOf[Tag[_]]) 123 | 124 | def as[S <: Singleton with String](implicit vo: ValueOf[S]): Tag[S] = { 125 | assert(tagName == vo.value) 126 | this.asInstanceOf[Tag[S]] 127 | } 128 | 129 | def update(f: NodeRenderContext => Unit): Tag[TagName] = { 130 | val ctx = new NodeRenderContext() 131 | f(ctx) 132 | ctx.commit(this) 133 | } 134 | 135 | /** Recursively map children, excluding root node */ 136 | override def map(f: Node => Node): Tag[TagName] = set(children.map(f(_).map(f))) 137 | 138 | /** Recursively map tag children, including root node */ 139 | def mapRoot(f: Tag[_] => Tag[_]): Tag[TagName] = { 140 | def iter(node: Node): Node = 141 | node match { 142 | case tag: Tag[_] => f(tag.copy(children = tag.children.map(iter))) 143 | case n => n 144 | } 145 | 146 | iter(this).asInstanceOf[T] 147 | } 148 | 149 | override def flatMap(f: Node => List[Node]): Tag[TagName] = 150 | copy(children = children.flatMap(n => f(n.flatMap(f)))) 151 | 152 | override def mapFirst(f: PartialFunction[Node, Node]): Tag[TagName] = { 153 | var done = false 154 | 155 | def m(n: Node): Node = 156 | if (done) n 157 | else f.lift(n) match { 158 | case Some(mapped) => 159 | done = true 160 | mapped 161 | 162 | case None => 163 | n match { 164 | case t: Tag[_] => t.copy(children = t.children.map(m)) 165 | case _ => n 166 | } 167 | } 168 | 169 | copy(children = children.map(m)) 170 | } 171 | 172 | def partialMap(f: PartialFunction[Node, Node]): Tag[TagName] = 173 | map(node => f.lift(node).getOrElse(node)) 174 | 175 | override def textContent: Option[String] = { 176 | for (c <- children) { 177 | c.textContent match { 178 | case Some(c) => return Some(c) 179 | case _ => 180 | } 181 | } 182 | 183 | None 184 | } 185 | 186 | /** 187 | * Recursively adds `suffix` to every given attribute. 188 | * 189 | * @param suffix the text to add to the attribute value 190 | * @param attributes which attributes to add the suffix to, by default this is just `Set("id")` 191 | * for backward compatibility reasons. Use [[Tag.IdAttributeNames]] for 192 | * a more comprehensive set. 193 | */ 194 | def suffixIds(suffix: String, attributes: Set[String] = Set("id")): Tag[TagName] = 195 | if (suffix.isEmpty || attributes.isEmpty) copy() 196 | else mapRoot { 197 | case t @ Tag(_, _, _) => 198 | attributes.foldLeft(t) { case (acc, attr) => 199 | acc.attr(attr).filter(_.nonEmpty).fold(acc)(value => acc.setAttr(attr, value + suffix)) 200 | } 201 | case n => n 202 | } 203 | 204 | @tailrec final def matches[S <: Singleton](tagRef: TagRef[S]): Boolean = 205 | tagRef match { 206 | case TagRef.Opt(tr) => matches(tr) 207 | case TagRef.Each(tr) => matches(tr) 208 | case TagRef.ById(id) => this.id.get == id 209 | case TagRef.ByTag(tn) => tagName == tn 210 | case TagRef.ByClass(cls) => this.`class`.has(cls) 211 | } 212 | 213 | def byIdOpt(id: String): Option[Tag[_]] = 214 | find { 215 | case t @ Tag(_, _, _) => t.id.get == id 216 | case _ => false 217 | }.map(_.asInstanceOf[Tag[_]]) 218 | 219 | def byId(id: String): Tag[_] = 220 | byIdOpt(id) 221 | .getOrElse(throw new IllegalArgumentException(s"Invalid node ID '$id'")) 222 | 223 | def byTagAll[U <: Singleton](implicit vu: ValueOf[U]): List[Tag[U]] = 224 | filter { 225 | case Tag(vu.value, _, _) => true 226 | case _ => false 227 | }.map(_.asInstanceOf[Tag[U]]) 228 | 229 | def byTagOpt[U <: Singleton](implicit vu: ValueOf[U]): Option[Tag[U]] = 230 | find { 231 | case Tag(vu.value, _, _) => true 232 | case _ => false 233 | }.map(_.asInstanceOf[Tag[U]]) 234 | 235 | def byTag[U <: Singleton](implicit ct: ValueOf[U]): Tag[U] = 236 | byTagOpt[U].getOrElse( 237 | throw new IllegalArgumentException(s"Invalid tag name '$tagName'")) 238 | 239 | def byClassAll(`class`: String): List[Tag[_]] = 240 | filter { 241 | case t: Tag[_] => t.`class`.has(`class`) 242 | case _ => false 243 | }.map(_.asInstanceOf[Tag[_]]) 244 | 245 | def byClassOpt(`class`: String): Option[Tag[_]] = 246 | find { 247 | case t: Tag[_] => t.`class`.has(`class`) 248 | case _ => false 249 | }.map(_.asInstanceOf[Tag[_]]) 250 | 251 | def byClass(`class`: String): Tag[_] = byClassOpt(`class`).get 252 | } 253 | 254 | object Tag { 255 | /** Attributes that contain IDs. Can be used in [[Tag.suffixIds]]. 256 | * The set can be extended in future Pine versions. 257 | */ 258 | val IdAttributeNames = Set("id", "for", "list", "form", "headers") 259 | } 260 | -------------------------------------------------------------------------------- /src/main/scala/pine/ParseError.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | class ParseError(e: String) extends Exception(e) 4 | -------------------------------------------------------------------------------- /src/main/scala/pine/Parser.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import scala.annotation.tailrec 4 | 5 | object Parser { 6 | def parseAttr(reader: Reader): (String, String) = { 7 | val name = identifier(reader) 8 | reader.skip(_.isWhitespace) 9 | val value = if (reader.prefix('=')) { 10 | reader.skip(_.isWhitespace) 11 | parseAttrValue(reader) 12 | } else "" 13 | 14 | name -> value 15 | } 16 | 17 | def parseAttrs(reader: Reader): Map[String, String] = { 18 | @tailrec def f(acc: Map[String, String]): Map[String, String] = 19 | if (reader.lookahead("/>") || reader.lookahead(">")) acc 20 | else { 21 | val attr = parseAttr(reader) 22 | reader.skip(_.isWhitespace) 23 | f(acc + attr) 24 | } 25 | 26 | f(Map.empty) 27 | } 28 | 29 | def rest(reader: Reader): String = reader.rest().take(20) + "[...]" 30 | def expected(reader: Reader, expected: String) = 31 | throw new ParseError(s"""Expected '$expected', found '${rest(reader)}'""") 32 | 33 | def parseAttrValue(reader: Reader): String = { 34 | if (!reader.prefix('"')) expected(reader, "\"") 35 | val str = reader.collect('"').getOrElse(expected(reader, "\"")) 36 | HtmlHelpers.decodeAttributeValue(str) 37 | } 38 | 39 | def identifier(reader: Reader): String = 40 | reader.collectUntil(!HtmlHelpers.identifierCharacter(_)) match { 41 | case None | Some("") => 42 | throw new ParseError(s"Identifier expected, found '${rest(reader)}'") 43 | case Some(value) => value 44 | } 45 | 46 | def parseChildren(reader: Reader, tagName: String, xml: Boolean): List[Node] = 47 | if (!xml && HtmlHelpers.VoidElements.contains(tagName)) List.empty 48 | else if (!xml && HtmlHelpers.CdataElements.contains(tagName)) { 49 | val result = reader.collect(s"").getOrElse( 50 | expected(reader, s"")) 51 | List(Text(result)) 52 | } else { 53 | @tailrec def f(nodes: List[Node]): List[Node] = 54 | if (reader.prefix(s"")) nodes 55 | else if (skipComment(reader)) f(nodes) 56 | else parseNode(reader, xml) match { 57 | case None => nodes 58 | case Some(t) => f(nodes :+ t) 59 | } 60 | 61 | f(List.empty) 62 | } 63 | 64 | def skipComment(reader: Reader): Boolean = 65 | if (!reader.prefix("").orElse(expected(reader, "-->")) 68 | true 69 | } 70 | 71 | def skipDocType(reader: Reader): Unit = 72 | if (reader.prefixIgnoreCase("').orElse(expected(reader, ">")) 74 | 75 | def skipXml(reader: Reader): Unit = 76 | if (reader.prefix("').orElse(expected(reader, ">")) 78 | 79 | def parseCdata(reader: Reader): Option[Text] = 80 | if (!reader.prefix("").map(Text).orElse(expected(reader, "]]>")) 82 | 83 | def parseTag(reader: Reader, xml: Boolean): Option[Tag[_]] = 84 | if (!reader.prefix("<")) None 85 | else { 86 | val tagName = identifier(reader) 87 | reader.skip(_.isWhitespace) 88 | val tagAttrs = parseAttrs(reader) 89 | val tagChildren = 90 | if (reader.prefix("/>")) List.empty 91 | else if (reader.prefix(">")) parseChildren(reader, tagName, xml) 92 | else expected(reader, "/>") 93 | 94 | Some(Tag(tagName, tagAttrs, tagChildren)) 95 | } 96 | 97 | def parseText(reader: Reader, xml: Boolean): Option[Text] = { 98 | val text = reader.collectUntil('<').getOrElse(reader.restAdvance()) 99 | if (text.isEmpty) None 100 | else Some(Text(HtmlHelpers.decodeText(text, xml))) 101 | } 102 | 103 | def parseNode(reader: Reader, xml: Boolean): Option[Node] = 104 | (if (!xml) None else parseCdata(reader)) 105 | .orElse(parseTag(reader, xml)) 106 | .orElse(parseText(reader, xml)) 107 | 108 | def parseRootNode(reader: Reader, xml: Boolean): Option[Tag[_]] = { 109 | while (skipComment(reader)) {} 110 | parseTag(reader, xml) 111 | } 112 | 113 | def fromString(html: String, xml: Boolean): Tag[Singleton] = { 114 | val reader = new Reader(html) 115 | reader.skip(_.isWhitespace) 116 | skipDocType(reader) 117 | reader.skip(_.isWhitespace) 118 | if (xml) skipXml(reader) 119 | reader.skip(_.isWhitespace) 120 | parseRootNode(reader, xml) 121 | .getOrElse(throw new ParseError("Invalid input")) 122 | .asInstanceOf[Tag[Singleton]] 123 | } 124 | } 125 | 126 | object HtmlParser { 127 | def fromString(html: String): Tag[Singleton] = 128 | Parser.fromString(html, xml = false) 129 | } 130 | 131 | object XmlParser { 132 | def fromString(html: String): Tag[Singleton] = 133 | Parser.fromString(html, xml = true) 134 | } 135 | -------------------------------------------------------------------------------- /src/main/scala/pine/Reader.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import scala.annotation.tailrec 4 | 5 | private[pine] class Reader(data: String) { 6 | private var offset = 0 7 | 8 | def rest(): String = data.drop(offset) 9 | 10 | def restAdvance(): String = { 11 | val result = data.drop(offset) 12 | offset = data.length 13 | result 14 | } 15 | 16 | /** Return next `n` characters (or less) without advancing pointer */ 17 | def take(n: Int): String = data.slice(offset, offset + n) 18 | 19 | /** Returns true if `value` matches */ 20 | def lookahead(value: Char): Boolean = data(offset) == value 21 | 22 | /** Returns true if `value` matches */ 23 | def lookahead(value: String): Boolean = { 24 | if (value.length > data.length - offset) false 25 | else { 26 | var i = 0 27 | while (i < value.length) { 28 | if (value(i) != data(offset + i)) return false 29 | i += 1 30 | } 31 | 32 | true 33 | } 34 | } 35 | 36 | /** Returns true if `value` matches and places pointer afterwards */ 37 | def prefix(value: Char): Boolean = 38 | data(offset) == value && { 39 | offset += 1 40 | true 41 | } 42 | 43 | /** Returns true if `value` matches and places pointer afterwards */ 44 | def prefix(value: String): Boolean = 45 | lookahead(value) && { 46 | offset += value.length 47 | true 48 | } 49 | 50 | /** Returns true if `value` matches regardless of case and places pointer 51 | * afterwards 52 | */ 53 | def prefixIgnoreCase(value: String): Boolean = 54 | take(value.length).equalsIgnoreCase(value) && { 55 | offset += value.length 56 | true 57 | } 58 | 59 | /** Aggregates all characters until f(c) returns true */ 60 | def collectUntil(f: Char => Boolean): Option[String] = { 61 | @tailrec def iter(ofs: Int): Option[Int] = 62 | if (ofs == data.length) None 63 | else if (f(data(ofs))) Some(ofs) 64 | else iter(ofs + 1) 65 | 66 | iter(offset).map { ofs => 67 | val result = data.slice(offset, ofs) 68 | offset = ofs 69 | result 70 | } 71 | } 72 | 73 | /** Aggregates all characters until `value` */ 74 | def collectUntil(value: Char): Option[String] = collectUntil(_ == value) 75 | 76 | /** Aggregates all characters until `value` and places pointer afterwards */ 77 | def collect(value: Char): Option[String] = 78 | collectUntil(_ == value).map { result => 79 | offset += 1 80 | result 81 | } 82 | 83 | /** Aggregates all characters until `value` and places pointer afterwards */ 84 | def collect(value: String): Option[String] = { 85 | val sub = data.drop(offset) 86 | sub.indexOf(value) match { 87 | case -1 => None 88 | case len => 89 | offset += len + value.length 90 | Some(sub.take(len)) 91 | } 92 | } 93 | 94 | /** Advances pointer while f(c) returns true */ 95 | @tailrec final def skip(f: Char => Boolean): Unit = 96 | if (offset < data.length && f(data(offset))) { 97 | offset += 1 98 | skip(f) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/scala/pine/RenderContext.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import scala.collection.mutable 4 | 5 | trait RenderContext { 6 | def render[T <: Singleton](tagRef: TagRef[T], diff: Diff): Unit 7 | } 8 | 9 | class NodeRenderContext extends RenderContext { 10 | val diffs = mutable.Queue.empty[(TagRef[Singleton], Diff)] 11 | 12 | override def render[T <: Singleton](tagRef: TagRef[T], diff: Diff): Unit = 13 | diffs.enqueue((tagRef.asInstanceOf[TagRef[Singleton]], diff)) 14 | 15 | /** Recursively iterates over `node` and applies changes in place while 16 | * `diffs.nonEmpty` */ 17 | def render(tag: Node): List[Node] = 18 | tag match { 19 | case acc @ Tag(_, _, _) if diffs.nonEmpty => 20 | diffs.find { case (ref, _) => acc.matches(ref) } match { 21 | case None => List(acc.set(acc.children.flatMap(render))) 22 | case Some(rd @ (ref, diff)) => 23 | val result = DiffRender.render(acc)(diff) 24 | if (ref.isInstanceOf[TagRef.Each[_]]) result 25 | else { 26 | diffs.dequeueFirst(_ == rd) 27 | result.flatMap(render) 28 | } 29 | } 30 | 31 | case result => List(result) 32 | } 33 | 34 | def commit[T <: Singleton](tag: Tag[T]): Tag[T] = { 35 | val r = render(tag) 36 | if (r.length != 1) throw new Exception("The root must consist of exactly one node") 37 | else { 38 | if (diffs.exists { 39 | case (TagRef.Each(_), _) => false 40 | case (TagRef.Opt(_), _) => false 41 | case _ => true 42 | }) throw new Exception(s"Some diffs could not be applied: $diffs") 43 | 44 | r.head.asInstanceOf[Tag[T]] 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/pine/TagRef.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | sealed trait TagRef[T <: Singleton] { 4 | def set(nodes: List[Node])(implicit renderCtx: RenderContext): Unit = 5 | renderCtx.render(this, Diff.SetChildren(nodes)) 6 | 7 | def set(node: Node)(implicit renderCtx: RenderContext): Unit = 8 | renderCtx.render(this, Diff.SetChildren(List(node))) 9 | 10 | def replace(nodes: List[Node])(implicit renderCtx: RenderContext): Unit = 11 | renderCtx.render(this, Diff.Replace(nodes)) 12 | 13 | def replace(node: Node)(implicit renderCtx: RenderContext): Unit = 14 | renderCtx.render(this, Diff.Replace(List(node))) 15 | 16 | def remove()(implicit renderCtx: RenderContext): Unit = 17 | renderCtx.render(this, Diff.RemoveNode) 18 | 19 | def prepend(nodes: List[Node])(implicit renderCtx: RenderContext): Unit = 20 | renderCtx.render(this, Diff.PrependChildren(nodes)) 21 | 22 | def prepend(node: Node)(implicit renderCtx: RenderContext): Unit = 23 | renderCtx.render(this, Diff.PrependChildren(List(node))) 24 | 25 | def append(nodes: List[Node])(implicit renderCtx: RenderContext): Unit = 26 | renderCtx.render(this, Diff.AppendChildren(nodes)) 27 | 28 | def append(node: Node)(implicit renderCtx: RenderContext): Unit = 29 | renderCtx.render(this, Diff.AppendChildren(List(node))) 30 | 31 | def insertBefore[U <: Singleton](childRef: TagRef[U], nodes: List[Node]) 32 | (implicit renderCtx: RenderContext): Unit = 33 | renderCtx.render(this, Diff.InsertBefore(childRef, nodes)) 34 | 35 | def insertBefore[U <: Singleton](childRef: TagRef[U], node: Node) 36 | (implicit renderCtx: RenderContext): Unit = 37 | renderCtx.render(this, Diff.InsertBefore(childRef, List(node))) 38 | 39 | def insertAfter[U <: Singleton](childRef: TagRef[U], nodes: List[Node]) 40 | (implicit renderCtx: RenderContext): Unit = 41 | renderCtx.render(this, Diff.InsertAfter(childRef, nodes)) 42 | 43 | def insertAfter[U <: Singleton](childRef: TagRef[U], node: Node) 44 | (implicit renderCtx: RenderContext): Unit = 45 | renderCtx.render(this, Diff.InsertAfter(childRef, List(node))) 46 | 47 | def insertAt(position: Int, nodes: List[Node]) 48 | (implicit renderCtx: RenderContext): Unit = 49 | renderCtx.render(this, Diff.InsertAt(position, nodes)) 50 | 51 | def insertAt(position: Int, node: Node) 52 | (implicit renderCtx: RenderContext): Unit = 53 | renderCtx.render(this, Diff.InsertAt(position, List(node))) 54 | 55 | def clearAll()(implicit renderCtx: RenderContext): Unit = 56 | renderCtx.render(this, Diff.SetChildren(List.empty)) 57 | 58 | def :=(nodes: List[Node])(implicit renderCtx: RenderContext): Unit = 59 | set(nodes) 60 | def :=(node: Node)(implicit renderCtx: RenderContext): Unit = set(node) 61 | def +=(node: Node)(implicit renderCtx: RenderContext): Unit = append(node) 62 | def ++=(nodes: List[Node])(implicit renderCtx: RenderContext): Unit = 63 | append(nodes) 64 | } 65 | 66 | object TagRef { 67 | case class Opt [T <: Singleton](tagRef: TagRef[T]) extends TagRef[T] 68 | case class Each[T <: Singleton](tagRef: TagRef[T]) extends TagRef[T] 69 | 70 | // Do not rename `tagRefId` to `id`, otherwise it shadows 71 | // `TagRefAttributes.id`. 72 | case class ById[T <: Singleton](tagRefId: String) extends TagRef[T] { 73 | def opt = TagRef.Opt(this) 74 | } 75 | 76 | case class ByTag[T <: Singleton](tagName: String with T) extends TagRef[T] { 77 | def opt = TagRef.Opt (this) 78 | def each = TagRef.Each(this) 79 | } 80 | 81 | case class ByClass[T <: Singleton](_class: String) extends TagRef[T] { 82 | def opt = TagRef.Opt (this) 83 | def each = TagRef.Each(this) 84 | } 85 | 86 | def apply[T <: Singleton](id: String): ById[T] = ById[T](id) 87 | def apply[T <: Singleton](id: String, child: String): ById[T] = 88 | ById[T](id + child) 89 | def apply[T <: String with Singleton](implicit vt: ValueOf[T]): ByTag[T] = 90 | ByTag[T](vt.value) 91 | def byClass[T <: Singleton](`class`: String): ByClass[T] = ByClass(`class`) 92 | } 93 | -------------------------------------------------------------------------------- /src/main/scala/pine/TagRender.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | trait TagRender[N <: Tag[_], T] { 4 | def render(s: N): T 5 | } 6 | 7 | object TagRender { 8 | trait Implicits { 9 | implicit class TagToHtml(tag: Tag[_]) { 10 | def toXml : String = XML.render(tag) 11 | def toHtml: String = HTML.render(tag) 12 | def toText: String = Text.render(tag) 13 | } 14 | } 15 | 16 | object HTML extends TagRender[Tag[_], String] { 17 | override def render(tag: Tag[_]): String = 18 | tag match { 19 | case t @ Tag("html", _, _) => "" + renderChild(t) 20 | case n => renderChild(n) 21 | } 22 | 23 | def renderChild(node: Node): String = 24 | node match { 25 | case Tag(tagName, attributes, children) 26 | if HtmlHelpers.CdataElements.contains(tagName) => 27 | HtmlHelpers.node( 28 | tagName, attributes, children.collect { case c: Text => c.text }, 29 | xml = false) 30 | 31 | case Tag(tagName, attributes, children) => 32 | HtmlHelpers.node( 33 | tagName, attributes, children.map(renderChild), xml = false) 34 | 35 | case pine.Text(text) => HtmlHelpers.encodeText(text, xml = false) 36 | } 37 | } 38 | 39 | object XML extends TagRender[Tag[_], String] { 40 | override def render(tag: Tag[_]): String = 41 | """""" + renderChild(tag) 42 | 43 | def renderChild(node: Node): String = 44 | node match { 45 | case Tag(tagName, attributes, children) => 46 | HtmlHelpers.node( 47 | tagName, attributes, children.map(renderChild), xml = true) 48 | 49 | case pine.Text(text) => HtmlHelpers.encodeText(text, xml = true) 50 | } 51 | } 52 | 53 | object Text extends TagRender[Tag[_], String] { 54 | // TODO Don't mutate global state 55 | private[pine] var lineBreak = false 56 | 57 | override def render(tag: Tag[_]): String = { 58 | lineBreak = false 59 | renderChild(tag).trim 60 | } 61 | 62 | def renderChild(node: Node): String = 63 | node match { 64 | case node @ Tag(_, _, _) => 65 | def children(): String = 66 | node.children.map { child => 67 | if (!lineBreak) Text.renderChild(child) 68 | else { 69 | val text = Text.renderChild(child).span(_.isSpaceChar)._2 70 | lineBreak = false 71 | text 72 | } 73 | }.mkString 74 | 75 | node.tagName match { 76 | case tag.Script.tagName => "" 77 | case tag.Br.tagName => 78 | lineBreak = true 79 | "\n" 80 | case tag.Ul.tagName => 81 | node.children.map { 82 | case li @ Tag("li", _, _) => "- " + renderChild(li).trim + "\n" 83 | case _ => "" 84 | }.mkString + "\n" + { lineBreak = true; "" } 85 | case tag.Div.tagName => 86 | children() + "\n" + { lineBreak = true; "" } 87 | case tag.H1.tagName | tag.H2.tagName | tag.H3.tagName | tag.H4.tagName | tag.H5.tagName | tag.H6.tagName => 88 | children() + "\n\n" + { lineBreak = true; "" } 89 | case tag.P.tagName => 90 | children() + "\n\n" + { lineBreak = true; "" } 91 | case _ => children() 92 | } 93 | 94 | case pine.Text(text) => text.replaceAll("\\s+", " ") 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/scala/pine/XmlEntities.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | object XmlEntities { 4 | /** @see https://www.w3.org/TR/REC-xml/#sec-predefined-ent */ 5 | val entities: Map[String, String] = Map( 6 | "lt" -> "\u003C", 7 | "gt" -> "\u003E", 8 | "amp" -> "\u0026", 9 | "apos" -> "\u0027", 10 | "quot" -> "\u005C\u0022" 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /src/main/scala/pine/dsl/Display.scala: -------------------------------------------------------------------------------- 1 | package pine.dsl 2 | 3 | sealed abstract class Display(val css: String) { 4 | def property: String = "display: " + css 5 | } 6 | 7 | object Display { 8 | case object Block extends Display("block") 9 | case object None extends Display("none") 10 | } 11 | -------------------------------------------------------------------------------- /src/main/scala/pine/dsl/Imports.scala: -------------------------------------------------------------------------------- 1 | package pine.dsl 2 | 3 | import pine._ 4 | 5 | trait Imports { 6 | implicit class TagRefDSLExtensions[T <: Singleton](tagRef: TagRef[T]) { 7 | /** Toggle `cssTags` depending on `state` */ 8 | @deprecated("Use `class`.state() instead", "forever") 9 | def css(state: Boolean, cssTags: String*) 10 | (implicit renderCtx: RenderContext): Unit = 11 | cssTags.foreach(tagRef.`class`.state(state, _)) 12 | 13 | /** Sets `style` to `display: none` if `state` is true, otherwise 14 | * sets `style` to `showDisplay` (empty string if None). 15 | */ 16 | def hide(state: Boolean, showDisplay: Option[Display] = None) 17 | (implicit renderCtx: RenderContext): Unit = 18 | tagRef.style := ( 19 | if (state) Display.None.property 20 | else showDisplay.map(_.property).getOrElse("")) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/scala/pine/macros/ExternalHtml.scala: -------------------------------------------------------------------------------- 1 | package pine.macros 2 | 3 | import java.io.File 4 | 5 | import scala.reflect.macros.blackbox.Context 6 | 7 | import pine._ 8 | 9 | object ExternalHtml { 10 | trait Method { 11 | /** Expose `html` as a global method */ 12 | def xml (fileName: String): Tag[Singleton] = macro XmlImpl 13 | def html(fileName: String): Tag[Singleton] = macro HtmlImpl 14 | } 15 | 16 | def convert(c: Context)(node: Node, root: Boolean): c.Expr[Node] = { 17 | import c.universe._ 18 | 19 | node match { 20 | case Text(text) => c.Expr(q"pine.Text($text)") 21 | case tag @ Tag(_, _, _) => 22 | val tagAttrs = tag.attributes 23 | val tagChildren = tag.children.map(convert(c)(_, root = false)) 24 | 25 | c.Expr(q"pine.Tag(${tag.tagName}, $tagAttrs, List(..$tagChildren))") 26 | } 27 | } 28 | 29 | def parse(c: Context, xml: Boolean) 30 | (fileName: c.Expr[String]): c.Expr[Tag[Singleton]] = { 31 | val path = new File(c.enclosingPosition.source.path).getParentFile 32 | val fileNameValue = Helpers.literalValueExpr(c)(fileName) 33 | val html = Helpers.readFile(new File(path, fileNameValue)) 34 | val node = Parser.fromString(html, xml) 35 | convert(c)(node, root = true) 36 | .asInstanceOf[c.Expr[Tag[Singleton]]] 37 | } 38 | 39 | def XmlImpl(c: Context)(fileName: c.Expr[String]): c.Expr[Tag[Singleton]] = 40 | parse(c, xml = true)(fileName) 41 | 42 | def HtmlImpl(c: Context)(fileName: c.Expr[String]): c.Expr[Tag[Singleton]] = 43 | parse(c, xml = false)(fileName) 44 | } 45 | -------------------------------------------------------------------------------- /src/main/scala/pine/macros/Helpers.scala: -------------------------------------------------------------------------------- 1 | package pine.macros 2 | 3 | import java.io.File 4 | 5 | import scala.reflect.macros.blackbox.Context 6 | 7 | object Helpers { 8 | def literalValueTree[T](c: Context)(tree: c.Tree): T = { 9 | import c.universe._ 10 | tree match { 11 | case Literal(Constant(value)) => value.asInstanceOf[T] 12 | case _ => 13 | c.error(c.enclosingPosition, "Literal expected") 14 | throw new RuntimeException() 15 | } 16 | } 17 | 18 | def literalValueExpr[T](c: Context)(expr: c.Expr[T]): T = 19 | literalValueTree[T](c)(expr.tree) 20 | 21 | def readFile(file: File): String = { 22 | val source = io.Source.fromFile(file) 23 | try source.mkString finally source.close() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/pine/macros/InlineHtml.scala: -------------------------------------------------------------------------------- 1 | package pine.macros 2 | 3 | import scala.reflect.macros.blackbox.Context 4 | 5 | import pine._ 6 | 7 | object InlineHtml { 8 | trait Implicit { 9 | implicit class HtmlString(sc: StringContext) { 10 | def xml (args: Any*): Tag[Singleton] = macro XmlImpl 11 | def html(args: Any*): Tag[Singleton] = macro HtmlImpl 12 | } 13 | } 14 | 15 | def leftOrErr[A, B](either: Either[A,B]): A = 16 | either.fold( 17 | left => left, 18 | _ => throw new NoSuchElementException("Either.left.get on Right") 19 | ) 20 | 21 | /** @return Left if string value, Right if optional value */ 22 | def convertAttribute(c: Context)( 23 | value: String, args: Seq[c.Expr[Any]] 24 | ): Either[c.Expr[Any], c.Expr[Any]] = { 25 | import c.universe._ 26 | 27 | val stringType = definitions.StringClass.toType 28 | val optionStringType = 29 | appliedType(definitions.OptionClass, List(stringType)) 30 | 31 | value.indexOf("${") match { 32 | case -1 => Left(c.Expr(q"$value")) 33 | case start => 34 | val end = value.indexOf("}") 35 | val index = value.substring(start + 2, end).toInt 36 | val prefix = value.substring(0, start) 37 | val suffix = value.substring(end + 1) 38 | 39 | args(index) match { 40 | case a if a.tree.tpe =:= stringType => 41 | Left( 42 | (prefix, suffix) match { 43 | case ("", "") => a 44 | case (p, "") => c.Expr(q"$p + $a") 45 | case ("", s) => 46 | c.Expr(q"$a + ${leftOrErr(convertAttribute(c)(s, args))}") 47 | case (p, s) => 48 | c.Expr(q"$p + $a + ${leftOrErr(convertAttribute(c)(s, args))}") 49 | } 50 | ) 51 | 52 | case a if a.tree.tpe <:< optionStringType => 53 | if (prefix.nonEmpty || suffix.nonEmpty) 54 | c.error(c.enclosingPosition, 55 | s"${a.tree.symbol} must not have a prefix or suffix") 56 | 57 | Right(a) 58 | 59 | case a => 60 | c.error(c.enclosingPosition, 61 | s"Type ${a.tree.tpe} (${a.tree.symbol}) not supported") 62 | null 63 | } 64 | } 65 | } 66 | 67 | def convertTag[T <: Singleton](c: Context)(tag: Tag[T], 68 | args: Seq[c.Expr[Any]] 69 | ): c.Expr[Tag[T]] = { 70 | import c.universe._ 71 | 72 | val integerType = definitions.IntTpe 73 | val booleanType = definitions.BooleanTpe 74 | val stringType = definitions.StringClass.toType 75 | val nodeType = c.mirror.staticClass("pine.Node").toType 76 | val listType = c.mirror.staticClass("scala.collection.immutable.List") 77 | val listNodeType = appliedType(listType, List(nodeType)) 78 | 79 | val tagAttrs = tag.attributes.map { case (k, v) => 80 | convertAttribute(c)(v, args) match { 81 | case Left (a) => c.Expr(q"List($k -> $a)") 82 | case Right(a) => c.Expr(q"$a.map($k -> _).toList") 83 | } 84 | } 85 | 86 | val tagChildren = tag.children.flatMap { 87 | case t @ Tag(_, _, _) => 88 | val tag = convertTag(c)(t, args) 89 | List(c.Expr(q"List($tag)")) 90 | 91 | case Text(text) => 92 | // TODO Find a better solution 93 | val parts = text.replaceAll("""\$\{\d+\}""", "_$0_").split("_") 94 | .toList.filter(_.nonEmpty) 95 | 96 | parts.map { v => 97 | if (!v.startsWith("${") || !v.endsWith("}")) 98 | c.Expr(q"List(pine.Text($v))") 99 | else { 100 | val index = v.drop(2).init.toInt 101 | 102 | args(index) match { 103 | case n if n.tree.tpe <:< nodeType => c.Expr(q"List($n)") 104 | case n if n.tree.tpe <:< listNodeType => 105 | n.asInstanceOf[c.Expr[List[Node]]] 106 | case n if n.tree.tpe =:= integerType || 107 | n.tree.tpe =:= booleanType => 108 | c.Expr(q"List(pine.Text($n.toString))") 109 | case n if n.tree.tpe =:= stringType => 110 | c.Expr(q"List(pine.Text($n))") 111 | case n => 112 | c.error(c.enclosingPosition, 113 | s"Type ${n.tree.tpe} (${n.tree.symbol}) not supported") 114 | null 115 | } 116 | } 117 | } 118 | } 119 | 120 | c.Expr(q""" 121 | pine.Tag( 122 | ${tag.tagName}, 123 | List(..$tagAttrs).flatten.toMap, 124 | List(..$tagChildren).flatten 125 | ) 126 | """) 127 | } 128 | 129 | def insertPlaceholders(c: Context)(parts: List[c.universe.Tree]): String = 130 | parts.zipWithIndex.map { case (tree, i) => 131 | val p = Helpers.literalValueTree[String](c)(tree) 132 | 133 | if (i == parts.length - 1) p 134 | else if (p.lastOption.contains('=')) p + "\"${" + i + "}\"" 135 | else p + "${" + i + "}" 136 | }.mkString 137 | 138 | def convert(c: Context, xml: Boolean) 139 | (parts: List[c.universe.Tree], 140 | args: Seq[c.Expr[Any]] 141 | ): c.Expr[Tag[Singleton]] = { 142 | val html = insertPlaceholders(c)(parts) 143 | val node = Parser.fromString(html, xml) 144 | convertTag(c)(node, args) 145 | } 146 | 147 | def XmlImpl(c: Context)(args: c.Expr[Any]*): c.Expr[Tag[Singleton]] = { 148 | import c.universe._ 149 | 150 | c.prefix.tree match { 151 | case Apply(_, List(Apply(_, parts))) => 152 | convert(c, xml = true)(parts, args) 153 | } 154 | } 155 | 156 | def HtmlImpl(c: Context)(args: c.Expr[Any]*): c.Expr[Tag[Singleton]] = { 157 | import c.universe._ 158 | 159 | c.prefix.tree match { 160 | case Apply(_, List(Apply(_, parts))) => 161 | convert(c, xml = false)(parts, args) 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/main/scala/pine/package.scala: -------------------------------------------------------------------------------- 1 | package object pine 2 | extends Node.Implicits 3 | with TagRender.Implicits 4 | with macros.InlineHtml.Implicit 5 | with macros.ExternalHtml.Method 6 | with tag.Attributes 7 | with dsl.Imports 8 | -------------------------------------------------------------------------------- /src/main/scala/pine/tag/package.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | package object tag { 4 | type A = "a" 5 | val A = Tag("a") 6 | type Acronym = "acronym" 7 | val Acronym = Tag("acronym") 8 | type Address = "address" 9 | val Address = Tag("address") 10 | type Applet = "applet" 11 | val Applet = Tag("applet") 12 | type Area = "area" 13 | val Area = Tag("area") 14 | type Article = "article" 15 | val Article = Tag("article") 16 | type Audio = "audio" 17 | val Audio = Tag("audio") 18 | type B = "b" 19 | val B = Tag("b") 20 | type Base = "base" 21 | val Base = Tag("base") 22 | type Basefont = "basefont" 23 | val Basefont = Tag("basefont") 24 | type Big = "big" 25 | val Big = Tag("big") 26 | type Body = "body" 27 | val Body = Tag("body") 28 | type Br = "br" 29 | val Br = Tag("br") 30 | type Button = "button" 31 | val Button = Tag("button") 32 | type Canvas = "canvas" 33 | val Canvas = Tag("canvas") 34 | type Caption = "caption" 35 | val Caption = Tag("caption") 36 | type Center = "center" 37 | val Center = Tag("center") 38 | type Code = "code" 39 | val Code = Tag("code") 40 | type Col = "col" 41 | val Col = Tag("col") 42 | type Colgroup = "colgroup" 43 | val Colgroup = Tag("colgroup") 44 | type Content = "content" 45 | val Content = Tag("content") 46 | type Datalist = "datalist" 47 | val Datalist = Tag("datalist") 48 | type Dd = "dd" 49 | val Dd = Tag("dd") 50 | type Del = "del" 51 | val Del = Tag("del") 52 | type Details = "details" 53 | val Details = Tag("details") 54 | type Dialog = "dialog" 55 | val Dialog = Tag("dialog") 56 | type Dir = "dir" 57 | val Dir = Tag("dir") 58 | type Div = "div" 59 | val Div = Tag("div") 60 | type Dl = "dl" 61 | val Dl = Tag("dl") 62 | type Dt = "dt" 63 | val Dt = Tag("dt") 64 | type Em = "em" 65 | val Em = Tag("em") 66 | type Embed = "embed" 67 | val Embed = Tag("embed") 68 | type Fieldset = "fieldset" 69 | val Fieldset = Tag("fieldset") 70 | type Figcaption = "figcaption" 71 | val Figcaption = Tag("figcaption") 72 | type Figure = "figure" 73 | val Figure = Tag("figure") 74 | type Footer = "footer" 75 | val Footer = Tag("footer") 76 | type Form = "form" 77 | val Form = Tag("form") 78 | type Frame = "frame" 79 | val Frame = Tag("frame") 80 | type Frameset = "frameset" 81 | val Frameset = Tag("frameset") 82 | type H1 = "h1" 83 | val H1 = Tag("h1") 84 | type H2 = "h2" 85 | val H2 = Tag("h2") 86 | type H3 = "h3" 87 | val H3 = Tag("h3") 88 | type H4 = "h4" 89 | val H4 = Tag("h4") 90 | type H5 = "h5" 91 | val H5 = Tag("h5") 92 | type H6 = "h6" 93 | val H6 = Tag("h6") 94 | type Head = "head" 95 | val Head = Tag("head") 96 | type Header = "header" 97 | val Header = Tag("header") 98 | type Hgroup = "hgroup" 99 | val Hgroup = Tag("hgroup") 100 | type Hr = "hr" 101 | val Hr = Tag("hr") 102 | type Html = "html" 103 | val Html = Tag("html") 104 | type I = "i" 105 | val I = Tag("i") 106 | type Iframe = "iframe" 107 | val Iframe = Tag("iframe") 108 | type Img = "img" 109 | val Img = Tag("img") 110 | type Input = "input" 111 | val Input = Tag("input") 112 | type Ins = "ins" 113 | val Ins = Tag("ins") 114 | type Isindex = "isindex" 115 | val Isindex = Tag("isindex") 116 | type Keygen = "keygen" 117 | val Keygen = Tag("keygen") 118 | type Label = "label" 119 | val Label = Tag("label") 120 | type Legend = "legend" 121 | val Legend = Tag("legend") 122 | type Li = "li" 123 | val Li = Tag("li") 124 | type Link = "link" 125 | val Link = Tag("link") 126 | type Listing = "listing" 127 | val Listing = Tag("listing") 128 | type Main = "main" 129 | val Main = Tag("main") 130 | type Map = "map" 131 | val Map = Tag("map") 132 | type Menu = "menu" 133 | val Menu = Tag("menu") 134 | type Menuitem = "menuitem" 135 | val Menuitem = Tag("menuitem") 136 | type Meta = "meta" 137 | val Meta = Tag("meta") 138 | type Meter = "meter" 139 | val Meter = Tag("meter") 140 | type Nav = "nav" 141 | val Nav = Tag("nav") 142 | type Noscript = "noscript" 143 | val Noscript = Tag("noscript") 144 | type Object = "object" 145 | val Object = Tag("object") 146 | type Ol = "ol" 147 | val Ol = Tag("ol") 148 | type Optgroup = "optgroup" 149 | val Optgroup = Tag("optgroup") 150 | type Option = "option" 151 | val Option = Tag("option") 152 | type Output = "output" 153 | val Output = Tag("output") 154 | type P = "p" 155 | val P = Tag("p") 156 | type Param = "param" 157 | val Param = Tag("param") 158 | type Plaintext = "plaintext" 159 | val Plaintext = Tag("plaintext") 160 | type Pre = "pre" 161 | val Pre = Tag("pre") 162 | type Progress = "progress" 163 | val Progress = Tag("progress") 164 | type Script = "script" 165 | val Script = Tag("script") 166 | type Section = "section" 167 | val Section = Tag("section") 168 | type Select = "select" 169 | val Select = Tag("select") 170 | type Shadow = "shadow" 171 | val Shadow = Tag("shadow") 172 | type Small = "small" 173 | val Small = Tag("small") 174 | type Source = "source" 175 | val Source = Tag("source") 176 | type Spacer = "spacer" 177 | val Spacer = Tag("spacer") 178 | type Span = "span" 179 | val Span = Tag("span") 180 | type Strike = "strike" 181 | val Strike = Tag("strike") 182 | type Strong = "strong" 183 | val Strong = Tag("strong") 184 | type Style = "style" 185 | val Style = Tag("style") 186 | type Summary = "summary" 187 | val Summary = Tag("summary") 188 | type Svg = "svg" 189 | val Svg = Tag("svg") 190 | type Table = "table" 191 | val Table = Tag("table") 192 | type Tbody = "tbody" 193 | val Tbody = Tag("tbody") 194 | type Td = "td" 195 | val Td = Tag("td") 196 | type Template = "template" 197 | val Template = Tag("template") 198 | type Textarea = "textarea" 199 | val Textarea = Tag("textarea") 200 | type Tfoot = "tfoot" 201 | val Tfoot = Tag("tfoot") 202 | type Th = "th" 203 | val Th = Tag("th") 204 | type Thead = "thead" 205 | val Thead = Tag("thead") 206 | type Title = "title" 207 | val Title = Tag("title") 208 | type Tr = "tr" 209 | val Tr = Tag("tr") 210 | type Track = "track" 211 | val Track = Tag("track") 212 | type Tt = "tt" 213 | val Tt = Tag("tt") 214 | type Ul = "ul" 215 | val Ul = Tag("ul") 216 | type Video = "video" 217 | val Video = Tag("video") 218 | type Xmp = "xmp" 219 | val Xmp = Tag("xmp") 220 | } 221 | -------------------------------------------------------------------------------- /src/test/html/list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

List

4 |
5 |
Title
Subtitle
6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /src/test/html/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello World 4 |
Div 1 contents
5 |
Div 2 contents
6 | 7 | 8 | -------------------------------------------------------------------------------- /src/test/html/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Div 1 contents
4 |
Div 2 contents
5 | 6 | 7 | -------------------------------------------------------------------------------- /src/test/scala-js/pine/dom/TagRefSpec.scala: -------------------------------------------------------------------------------- 1 | package pine.dom 2 | 3 | import org.scalajs.dom.document 4 | import org.scalatest.funsuite.AnyFunSuite 5 | 6 | import pine._ 7 | 8 | class TagRefSpec extends AnyFunSuite { 9 | test("Get value of String attribute") { 10 | val node = document.createElement("a") 11 | node.setAttribute("id", "test") 12 | node.setAttribute("href", "http://google.com/") 13 | node.appendChild(document.createTextNode("Google")) 14 | 15 | document.body.appendChild(node) 16 | 17 | val tagRef = TagRef[tag.A]("test") 18 | assert(tagRef.href.get.contains("http://google.com/")) 19 | 20 | document.body.removeChild(node) 21 | } 22 | 23 | test("Get value of Boolean attribute") { 24 | val node = document.createElement("input") 25 | node.setAttribute("id", "test") 26 | node.setAttribute("type", "checkbox") 27 | 28 | document.body.appendChild(node) 29 | 30 | val tagRef = TagRef[tag.Input]("test") 31 | assert(!tagRef.checked.get) 32 | 33 | document.body.removeChild(node) 34 | } 35 | 36 | test("Get value of Boolean attribute (2)") { 37 | val node = document.createElement("input") 38 | node.setAttribute("id", "test") 39 | node.setAttribute("type", "checkbox") 40 | node.setAttribute("checked", "") 41 | 42 | document.body.appendChild(node) 43 | 44 | val tagRef = TagRef[tag.Input]("test") 45 | assert(tagRef.checked.get) 46 | 47 | document.body.removeChild(node) 48 | } 49 | 50 | test("Set value of Boolean attribute") { 51 | val node = document.createElement("input") 52 | node.setAttribute("id", "test") 53 | node.setAttribute("type", "checkbox") 54 | 55 | val tagRef = TagRef[tag.Input]("test") 56 | 57 | document.body.appendChild(node) 58 | 59 | DOM.render(implicit ctx => tagRef.checked := true) 60 | assert(tagRef.checked.get) 61 | 62 | DOM.render(implicit ctx => tagRef.checked := false) 63 | assert(!tagRef.checked.get) 64 | 65 | document.body.removeChild(node) 66 | } 67 | 68 | test("Set value of String attribute") { 69 | val node = document.createElement("a") 70 | node.setAttribute("id", "test") 71 | node.setAttribute("href", "http://google.com/") 72 | node.appendChild(document.createTextNode("Google")) 73 | 74 | val tagRef = TagRef[tag.A]("test") 75 | 76 | document.body.appendChild(node) 77 | 78 | DOM.render(implicit ctx => tagRef.href := "http://github.com/") 79 | assert(tagRef.href.get.contains("http://github.com/")) 80 | 81 | DOM.render(implicit ctx => tagRef.href := "") 82 | assert(tagRef.href.get.contains("")) 83 | 84 | DOM.render(implicit ctx => tagRef.href.remove()) 85 | assert(tagRef.href.get.isEmpty) 86 | 87 | document.body.removeChild(node) 88 | } 89 | 90 | test("Update value of String attribute") { 91 | val node = document.createElement("a") 92 | node.setAttribute("id", "test") 93 | node.setAttribute("href", "http://google.com/") 94 | node.appendChild(document.createTextNode("Google")) 95 | 96 | val tagRef = TagRef[tag.A]("test") 97 | document.body.appendChild(node) 98 | 99 | DOM.render(implicit ctx => tagRef.href.update(_ => "")) 100 | assert(tagRef.href.get.isEmpty) 101 | 102 | DOM.render(implicit ctx => tagRef.href.update(_ => "test")) 103 | assert(tagRef.href.get.contains("test")) 104 | 105 | document.body.removeChild(node) 106 | } 107 | 108 | test("Update value of Boolean attribute") { 109 | val node = document.createElement("input") 110 | node.setAttribute("id", "test") 111 | node.setAttribute("type", "checkbox") 112 | 113 | val tagRef = TagRef[tag.Input]("test") 114 | document.body.appendChild(node) 115 | 116 | DOM.render(implicit ctx => tagRef.checked.update(!_)) 117 | assert(tagRef.checked.get) 118 | 119 | DOM.render(implicit ctx => tagRef.checked.update(!_)) 120 | assert(!tagRef.checked.get) 121 | 122 | document.body.removeChild(node) 123 | } 124 | 125 | test("Use DSL to update CSS tag") { 126 | val node = document.createElement("input") 127 | node.setAttribute("id", "test") 128 | node.setAttribute("type", "checkbox") 129 | node.setAttribute("class", "a b c") 130 | 131 | val tagRef = TagRef[tag.Input]("test") 132 | document.body.appendChild(node) 133 | 134 | DOM.render(implicit ctx => tagRef.`class`.state(false, "b")) 135 | assert(tagRef.`class`.get == List("a", "c")) 136 | assert(tagRef.dom.className == "a c") 137 | 138 | document.body.removeChild(node) 139 | } 140 | 141 | test("Match by tag") { 142 | val node = document.createElement("input") 143 | val div = document.createElement("div") 144 | 145 | document.body.appendChild(node) 146 | document.body.appendChild(div) 147 | 148 | val tagRef1 = TagRef[tag.Input] 149 | val tagRef2 = TagRef[tag.Div] 150 | 151 | assert(tagRef1.dom == node) 152 | assert(tagRef2.dom == div) 153 | 154 | document.body.removeChild(div) 155 | document.body.removeChild(node) 156 | } 157 | 158 | test("Prepend nodes") { 159 | val div = html"""

""".toDom 160 | document.body.appendChild(div) 161 | 162 | DOM.render(implicit ctx => 163 | TagRef["span"].prepend(List( 164 | html"Hello", 165 | html"World" 166 | )) 167 | ) 168 | 169 | val html = DOM.toTree(div).toHtml 170 | document.body.removeChild(div) 171 | 172 | assert(html == "
HelloWorld
") 173 | } 174 | 175 | test("Insert before") { 176 | val div = html"""


""".toDom 177 | document.body.appendChild(div) 178 | 179 | DOM.render(implicit ctx => 180 | TagRef["span"].insertBefore(TagRef["hr"], tag.Div)) 181 | 182 | val html = DOM.toTree(div).toHtml 183 | document.body.removeChild(div) 184 | 185 | assert(html == "


") 186 | } 187 | 188 | test("Insert after") { 189 | val div = html"""

""".toDom 190 | document.body.appendChild(div) 191 | 192 | DOM.render(implicit ctx => 193 | TagRef["span"].insertAfter(TagRef["hr"], tag.Div)) 194 | 195 | val html = DOM.toTree(div).toHtml 196 | document.body.removeChild(div) 197 | 198 | assert(html == "

") 199 | } 200 | 201 | test("Insert after (2)") { 202 | val div = html"""


""".toDom 203 | document.body.appendChild(div) 204 | 205 | DOM.render(implicit ctx => 206 | TagRef["span"].insertAfter(TagRef["hr"], tag.Div)) 207 | 208 | val html = DOM.toTree(div).toHtml 209 | document.body.removeChild(div) 210 | 211 | assert(html == "


") 212 | } 213 | 214 | test("Insert at position") { 215 | val div = html"""
""".toDom 216 | document.body.appendChild(div) 217 | 218 | DOM.render { implicit ctx => 219 | TagRef("nodes").insertAt(0, tag.Span.set("a")) 220 | } 221 | 222 | val html = DOM.toTree(div).toHtml 223 | document.body.removeChild(div) 224 | 225 | assert(html == """
a
""") 226 | } 227 | 228 | test("Insert at position (2)") { 229 | val div = html"""
abc
""".toDom 230 | document.body.appendChild(div) 231 | 232 | DOM.render { implicit ctx => 233 | TagRef("nodes").insertAt(1, List(tag.Span.set("d"), tag.Span.set("e"))) 234 | } 235 | 236 | val html = DOM.toTree(div).toHtml 237 | document.body.removeChild(div) 238 | 239 | assert(html == """
adebc
""") 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/test/scala/pine/BindingsSpec.scala: -------------------------------------------------------------------------------- 1 | //package pine 2 | 3 | /* 4 | import minitest._ 5 | 6 | object BindingsSpec extends SimpleTestSuite { 7 | test("Binding `href` on `a` tags") { 8 | val a = tag.A() 9 | a.href := "http://example.com/" 10 | 11 | assertEquals(a.toHtml, """""") 12 | } 13 | 14 | test("Don't escape """) 19 | } 20 | 21 | test("one-way binding tree") { 22 | val url = Var("http://github.com/") 23 | val title = Var("GitHub") 24 | 25 | val root = html"""""" 26 | root.attribute("href").asInstanceOf[Var[String]].subscribe(url) 27 | root.subscribeChildren(Buffer({ 28 | val s = new state.Text 29 | s.listen(title) 30 | s 31 | })) 32 | 33 | assertEquals(root.toHtml, """GitHub""") 34 | 35 | url := "http://google.com/" 36 | assertEquals(root.attribute("href").get, "http://google.com/") 37 | assertEquals(root.toHtml, """GitHub""") 38 | 39 | root.clearChildren() 40 | assertEquals(root.toHtml, """""") 41 | } 42 | 43 | test("Check attribute and content updates") { 44 | val url = Var("http://github.com/") 45 | val title = Var("GitHub") 46 | 47 | val root = html"""""" 48 | assertEquals(root.toHtml, """""") 49 | 50 | val href = root.attribute("href").asInstanceOf[StateChannel[String]] 51 | assertEquals(root.toHtml, """""") 52 | 53 | href.subscribe(url) 54 | root := { 55 | val s = new state.Text 56 | s.listen(title) 57 | s 58 | } 59 | 60 | assertEquals(root.toHtml, """GitHub""") 61 | 62 | url := "http://google.com/" 63 | title := "Google" 64 | assertEquals(root.toHtml, """Google""") 65 | } 66 | 67 | test("Bind list") { 68 | val list = html"""
""" 69 | 70 | list.subscribeChildren(Buffer("a", "b", "c").map { i => 71 | val title = s"Title $i" 72 | val subtitle = s"Subtitle $i" 73 | html"""
$title
$subtitle
""" 74 | }) 75 | 76 | assertEquals(list.children.size, 3) 77 | assertEquals(list.children.last.toHtml, 78 | """
Title c
Subtitle c
""") 79 | } 80 | 81 | test("Inline event handler") { 82 | var clicked = 0 83 | val btn = html"""""" 84 | 85 | btn.click() 86 | assertEquals(clicked, 1) 87 | } 88 | 89 | test("Inline event handler (2)") { 90 | var eventTriggered = 0 91 | val input = html"""""" 92 | 93 | input.triggerAction("focus") 94 | assertEquals(eventTriggered, 1) 95 | } 96 | 97 | test("Function as inline event handler") { 98 | var clicked = 0 99 | def click(event: Any): Unit = clicked += 1 100 | val btn = html"""""" 101 | btn.click() 102 | assertEquals(clicked, 1) 103 | } 104 | 105 | test("Function as inline event handler (2)") { 106 | var clicked = 0 107 | def click(event: Any): Unit = clicked += 1 108 | val btn = html"""""" 109 | btn.click() 110 | assertEquals(clicked, 1) 111 | } 112 | 113 | test("Function as inline event handler (3)") { 114 | var clicked = 0 115 | def f(): Unit = clicked += 1 116 | val btn = html"""""" 117 | 118 | btn.click() 119 | assertEquals(clicked, 1) 120 | } 121 | } 122 | */ 123 | -------------------------------------------------------------------------------- /src/test/scala/pine/DiffSpec.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | class DiffSpec extends AnyFunSuite { 6 | test("Replace nodes") { 7 | val spanAge = TagRef["span"]("age") 8 | val spanName = TagRef["span"]("name") 9 | 10 | val node = html"""
""" 11 | val result = node.update { implicit ctx => 12 | spanAge := 42 13 | spanName := "Joe" 14 | } 15 | 16 | assert(result == html"""
42Joe
""") 17 | } 18 | 19 | test("Render lists") { 20 | case class Item(id: Int, name: String) 21 | val itemView = html"""
""" 22 | 23 | def renderItem(item: Item): Tag[_] = { 24 | val id = item.id.toString 25 | val node = itemView.suffixIds(id) 26 | val spanName = TagRef["span"]("name", id) 27 | node.update { implicit ctx => 28 | spanName := item.name 29 | } 30 | } 31 | 32 | val node = html"""
""" 33 | val root = TagRef["div"]("page") 34 | val items = List(Item(0, "Joe"), Item(1, "Jeff")) 35 | val result = node.update(implicit ctx => root.set(items.map(renderItem))) 36 | assert(result == html"""
Joe
Jeff
""") 37 | } 38 | 39 | test("insertAt()") { 40 | val node = html"""
""" 41 | val result = node.update { implicit ctx => 42 | TagRef("nodes").insertAt(0, tag.Span.set("a")) 43 | } 44 | 45 | assert(result == html"""
a
""") 46 | } 47 | 48 | test("insertAt() (2)") { 49 | val node = html"""
abc
""" 50 | val result = node.update { implicit ctx => 51 | TagRef("nodes").insertAt(1, List(tag.Span.set("d"), tag.Span.set("e"))) 52 | } 53 | 54 | assert(result == html"""
adebc
""") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/scala/pine/ExternalHtmlSpec.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | class ExternalHtmlSpec extends AnyFunSuite { 6 | def checkTestHtml(value: String): Unit = 7 | assert(value == 8 | """ 9 | 10 | Hello World 11 |
Div 1 contents
12 |
Div 2 contents
13 | 14 | """) 15 | 16 | test("Load immutable template") { 17 | val tpl = html("../../html/test.html") 18 | checkTestHtml(tpl.toHtml) 19 | 20 | val tplOneWay = html("../../html/test.html") 21 | checkTestHtml(tplOneWay.toHtml) 22 | } 23 | 24 | test("Load mutable template") { 25 | val tpl = html("../../html/test.html") 26 | checkTestHtml(tpl.toHtml) 27 | 28 | val tplOneWay = html("../../html/test.html") 29 | checkTestHtml(tplOneWay.toHtml) 30 | } 31 | 32 | test("Replacing nodes") { 33 | val tpl = html("../../html/test2.html") 34 | 35 | val div2 = tpl.byId("div2") 36 | assert(div2.toHtml == """
Div 2 contents
""") 37 | 38 | val updated = div2.set(Text("42")) 39 | assert(updated.toHtml == """
42
""") 40 | } 41 | 42 | test("Instantiate template") { 43 | val tpl = html("../../html/list.html") 44 | val listItem = tpl.byId("list-item") 45 | 46 | val inst = listItem.update { implicit ctx => 47 | TagRef("list-item-title") := "Title" 48 | TagRef("list-item-subtitle") := "Subtitle" 49 | }.toHtml 50 | 51 | assert(inst == """
Title
Subtitle
""") 52 | } 53 | 54 | test("Bind list item from template") { 55 | // Obtain tree without creating a state object 56 | val tpl = html("../../html/list.html") 57 | 58 | // When embedding list items, we need to drop the ID attribute 59 | val listItem = tpl.byId("list-item").remAttr("id") 60 | 61 | val items = List("a", "b", "c").map { i => 62 | listItem.update { implicit ctx => 63 | TagRef("list-item-title") := s"Title $i" 64 | TagRef("list-item-title").id.remove() 65 | 66 | TagRef("list-item-subtitle") := s"Subtitle $i" 67 | TagRef("list-item-subtitle").id.remove() 68 | } 69 | } 70 | 71 | // Instantiate template and replace list 72 | val replaced = tpl.update(implicit ctx => TagRef("list").set(items)) 73 | 74 | val list = replaced.byId("list") 75 | assert(list.children.size == 3) 76 | assert(list.children.last.asInstanceOf[Tag[_]].toHtml == 77 | """
Title c
Subtitle c
""") 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/scala/pine/HtmlHelpersSpec.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | class HtmlHelpersSpec extends AnyFunSuite { 6 | test("Reader") { 7 | val reader = new Reader("test;bc") 8 | assert(reader.collect(';').contains("test")) 9 | assert(reader.rest() == "bc") 10 | 11 | val reader2 = new Reader(";bc") 12 | assert(reader2.collect(';').contains("")) 13 | assert(reader2.rest() == "bc") 14 | 15 | val reader3 = new Reader(";bc") 16 | assert(reader3.collect('!').isEmpty) 17 | assert(reader3.rest() == ";bc") 18 | } 19 | 20 | test("Decode text") { 21 | assert(HtmlHelpers.decodeText("Hello", xml = false) == "Hello") 22 | } 23 | 24 | test("Decode text (2)") { 25 | assert(HtmlHelpers.decodeText("test`42", xml = false) == "test`42") 26 | } 27 | 28 | test("Decode text (3)") { 29 | assert(HtmlHelpers.decodeText("`", xml = false) == "`") 30 | } 31 | 32 | test("Decode text (4)") { 33 | assert(HtmlHelpers.decodeText("`", xml = false) == "`") 34 | } 35 | 36 | test("Decode text (5)") { 37 | assert(HtmlHelpers.decodeText("+=", xml = false) == "+=") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/scala/pine/HtmlParserSpec.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | class HtmlParserSpec extends AnyFunSuite { 6 | test("Empty text node") { 7 | assertThrows[ParseError] { 8 | HtmlParser.fromString("") 9 | } 10 | } 11 | 12 | test("Parse text") { 13 | assertThrows[ParseError] { 14 | HtmlParser.fromString("Hello world") 15 | } 16 | } 17 | 18 | test("Parse text (2)") { 19 | assertThrows[ParseError] { 20 | HtmlParser.fromString("'\"") 21 | } 22 | } 23 | 24 | test("Parse attributes") { 25 | val node = tag.Span.setAttr("ä-ö_ü", "äöü") 26 | assert(HtmlParser.fromString(node.toHtml) == node) 27 | } 28 | 29 | test("Parse HTML node") { 30 | val node = tag.Html.set(List(tag.Head, tag.Body)) 31 | assert(HtmlParser.fromString(node.toHtml) == node) 32 | } 33 | 34 | test("Parse head node") { 35 | val node = tag.Head 36 | assert(HtmlParser.fromString(node.toHtml) == node) 37 | } 38 | 39 | test("Parse title node") { 40 | val node = tag.Title 41 | assert(HtmlParser.fromString(node.toHtml) == node) 42 | } 43 | 44 | test("Parse meta node") { 45 | val node = tag.Meta 46 | assert(HtmlParser.fromString(node.toHtml) == node) 47 | } 48 | 49 | test("Parse link node") { 50 | val node = tag.Link 51 | assert(HtmlParser.fromString(node.toHtml) == node) 52 | } 53 | 54 | test("Parse body node") { 55 | val node = tag.Body 56 | assert(HtmlParser.fromString(node.toHtml) == node) 57 | } 58 | 59 | test("Parse text with entities") { 60 | val html = "(Hello world)" 61 | val node = HtmlParser.fromString(html) 62 | assert(node == tag.Span.set(Text("(Hello world)"))) 63 | } 64 | 65 | test("Parse text with entities (2)") { 66 | val html = "````" 67 | val node = HtmlParser.fromString(html) 68 | assert(node == tag.Span.set(Text("````"))) 69 | } 70 | 71 | test("Parse text with hex entities") { 72 | val html = "Hello world!" 73 | val node = HtmlParser.fromString(html) 74 | assert(node == tag.Span.set(Text("Hello world!"))) 75 | } 76 | 77 | test("Parse invalid entities") { 78 | assertThrows[ParseError] { 79 | val text = "&abcd;" 80 | HtmlParser.fromString(text) 81 | } 82 | } 83 | 84 | test("Parse invalid entities (2)") { 85 | assertThrows[ParseError] { 86 | val text = "&;" 87 | HtmlParser.fromString(text) 88 | } 89 | } 90 | 91 | test("Parse simple tag") { 92 | val html = "
" 93 | val node = HtmlParser.fromString(html) 94 | assert(node == tag.Br) 95 | } 96 | 97 | test("Parse tag") { 98 | val html = """Google""" 99 | val node = HtmlParser.fromString(html) 100 | assert(node == (tag.A.href("http://google.com/") :+ Text("Google"))) 101 | } 102 | 103 | test("Parse HTML") { 104 | val html = """
42
""" 105 | val node = HtmlParser.fromString(html) 106 | assert(node.toHtml == html) 107 | } 108 | 109 | test("Decode arguments") { 110 | val html = """""" 111 | val node = HtmlParser.fromString(html) 112 | assert(node == tag.A.href("a&b")) 113 | 114 | val html2 = """""" 115 | val node2 = HtmlParser.fromString(html2) 116 | assert(node2 == tag.A.href("a&>b")) 117 | } 118 | 119 | test("Parse node with boolean attribute") { 120 | val html = """""" 121 | val htmlParsed = """""" 122 | val node = HtmlParser.fromString(html) 123 | assert(node.toHtml == htmlParsed) 124 | } 125 | 126 | test("Parse node with custom data attribute") { 127 | val html = """""" 128 | val htmlParsed = """""" 129 | val node = HtmlParser.fromString(html) 130 | assert(node.toHtml == htmlParsed) 131 | } 132 | 133 | test("Parse node with attribute which contains spaces before and after equal sign") { 134 | val html = """""" 135 | val htmlParsed = """""" 136 | val node = HtmlParser.fromString(html) 137 | assert(node.toHtml == htmlParsed) 138 | } 139 | 140 | test("Ignore comments") { 141 | val html = """
test !
""" 142 | val node = HtmlParser.fromString(html) 143 | assert(node == (tag.Div :+ Text("test ") :+ Text("!"))) 144 | } 145 | 146 | test("Ignore comments (2)") { 147 | val html = """
test !
""" 148 | val node = HtmlParser.fromString(html) 149 | assert(node == (tag.Div :+ Text("test ") :+ Text("!"))) 150 | } 151 | 152 | test("Resolve node") { 153 | val html = """
test
""" 154 | val div = HtmlParser.fromString(html).asInstanceOf[Tag[_]].byId("b") 155 | assert(div == html"""""") 156 | } 157 | 158 | test("Parse DOCTYPE") { 159 | val html = """42""" 160 | val node = HtmlParser.fromString(html) 161 | assert(node.toHtml == html) 162 | } 163 | 164 | test("Parse DOCTYPE (2)") { 165 | // Parser must match DOCTYPE case-insensitively 166 | // See https://html.spec.whatwg.org/multipage/syntax.html#the-doctype 167 | val html = """42""" 168 | val node = HtmlParser.fromString(html) 169 | assert(node.toHtml == html.replace("// """ 176 | 177 | val node = HtmlParser.fromString(html) 178 | assert(node == tag.Script.set( 179 | """// """)) 182 | } 183 | 184 | test("Parse script nodes") { 185 | val html = """""" 186 | val node = HtmlParser.fromString(html) 187 | assert(node.toHtml == html) 188 | } 189 | 190 | test("Parse style nodes") { 191 | val html = """""" 192 | val node = HtmlParser.fromString(html) 193 | assert(node.toHtml == html) 194 | 195 | val html2 = """""" 196 | val node2 = HtmlParser.fromString(html2) 197 | assert(node2.toHtml == html2) 198 | } 199 | 200 | test("Parse multiple root comments (prefix)") { 201 | val html = """""" 202 | assert(HtmlParser.fromString(html) == tag.A) 203 | } 204 | 205 | test("Parse multiple root comments (suffix)") { 206 | val html = """""" 207 | assert(HtmlParser.fromString(html) == tag.A) 208 | } 209 | 210 | test("Parse multiple child comments") { 211 | val html = """""" 212 | assert(HtmlParser.fromString(html) == tag.A) 213 | } 214 | 215 | test("Cannot parse XML tags") { 216 | assertThrows[ParseError] { 217 | HtmlParser.fromString("""""") 218 | } 219 | } 220 | 221 | test("Parse XML tags") { 222 | XmlParser.fromString("""""") 223 | xml"""""" 224 | } 225 | 226 | test("Read unambiguous ampersand") { 227 | assert(HtmlParser.fromString("
editable && copy
") == 228 | tag.Div.set("editable && copy")) 229 | } 230 | 231 | test("Detect ambiguous ampersand") { 232 | assertThrows[ParseError] { 233 | HtmlParser.fromString("
editable&©
") 234 | } 235 | } 236 | 237 | test("Ignore unclosed tags") { 238 | assert(HtmlParser.fromString("") == tag.Html) 239 | assert(HtmlParser.fromString("\n") == tag.Html.set("\n")) 240 | 241 | assert(XmlParser.fromString("") == tag.Html) 242 | assert(XmlParser.fromString("\n") == tag.Html.set("\n")) 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/test/scala/pine/ImplicitsSpec.scala: -------------------------------------------------------------------------------- 1 | //package pine 2 | 3 | /* 4 | import minitest._ 5 | 6 | object ImplicitsSpec extends SimpleTestSuite { 7 | test("Var[String] to node") { 8 | val title = Var("test") 9 | 10 | val span = html"""""" 11 | span.setChildren(List(title)) 12 | 13 | assertEquals(span.toHtml, "test") 14 | 15 | title := "test2" 16 | assertEquals(span.toHtml, "test2") 17 | } 18 | 19 | test("Convert values") { 20 | val span = html"""""" 21 | 22 | span := "Hello world" 23 | assertEquals(span.toHtml, "Hello world") 24 | 25 | span := 42 26 | assertEquals(span.toHtml, "42") 27 | 28 | span := true 29 | assertEquals(span.toHtml, "true") 30 | } 31 | 32 | test("Convert value channels") { 33 | val span = html"""""" 34 | 35 | val v = Var("GitHub") 36 | span := v 37 | assertEquals(span.toHtml, "GitHub") 38 | 39 | v := "Google" 40 | assertEquals(span.toHtml, "Google") 41 | 42 | val v2 = Var(42) 43 | span := v2 44 | assertEquals(span.toHtml, "42") 45 | 46 | v2 := 23 47 | assertEquals(span.toHtml, "23") 48 | } 49 | 50 | test("Convert node channels") { 51 | val span = html"""""" 52 | 53 | val v = Var(html"""
42
""") 54 | span := v 55 | assertEquals(span.toHtml, "
42
") 56 | 57 | v := html"""
23
""" 58 | assertEquals(span.toHtml, "
23
") 59 | } 60 | 61 | test("Convert string buffers") { 62 | val span = html"""""" 63 | 64 | val buffer = Buffer("Hello") 65 | span := buffer 66 | assertEquals(span.toHtml, "Hello") 67 | 68 | buffer += " world" 69 | assertEquals(span.toHtml, "Hello world") 70 | } 71 | 72 | test("Convert string channels") { 73 | val v = Var("") 74 | 75 | val span = html"""""" 76 | span.subscribe(v) 77 | 78 | assertEquals(span.toHtml, "") 79 | 80 | v := "Hello world" 81 | assertEquals(span.toHtml, "Hello world") 82 | } 83 | } 84 | */ 85 | -------------------------------------------------------------------------------- /src/test/scala/pine/InlineHtmlSpec.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | class InlineHtmlSpec extends AnyFunSuite { 6 | test("toHtml on immutable tree") { 7 | val url = "http://github.com/" 8 | val title = "GitHub" 9 | val root = html"""$title""" 10 | 11 | assert(root.toHtml == """GitHub""") 12 | 13 | val updated = root.setAttr("href", "http://google.com/") 14 | assert(updated.attr("href").get == "http://google.com/") 15 | assert(updated.toHtml == """GitHub""") 16 | 17 | val updated2 = updated.clearAll 18 | assert(updated2.toHtml == """""") 19 | } 20 | 21 | test("Look up by ID") { 22 | val root = html"""
""" 23 | val appended = root ++ List( 24 | html"""
""", 25 | html"""
""") 26 | 27 | assert(appended.byIdOpt("b").nonEmpty) 28 | } 29 | 30 | test("String attribute placeholder") { 31 | val id = "test" 32 | val div = html"
" 33 | assert(div.toHtml == """
""") 34 | } 35 | 36 | test("String (type alias) attribute placeholder") { 37 | type Str = String 38 | val id: Str = "test" 39 | val div = html"
" 40 | assert(div.toHtml == """
""") 41 | } 42 | 43 | test("Option[String] attribute placeholder") { 44 | val id = Some("test") 45 | val div = html"
" 46 | assert(div.toHtml == """
""") 47 | 48 | val id2 = None 49 | val div2 = html"
" 50 | assert(div2.toHtml == """
""") 51 | 52 | val id3 = Option.empty[String] 53 | val div3 = html"
" 54 | assert(div3.toHtml == """
""") 55 | 56 | assertDoesNotCompile { 57 | """ 58 | val id4 = Option.empty[Int] 59 | val div4 = html"
" 60 | """ 61 | } 62 | } 63 | 64 | test("Concatenated string attribute placeholder (suffix)") { 65 | val id = "test" 66 | val node = html"""
    """ 67 | assert(node.toHtml == """
      """) 68 | } 69 | 70 | test("Concatenated string attribute placeholder (prefix)") { 71 | val id = "test" 72 | val node = html"""
        """ 73 | assert(node.toHtml == """
          """) 74 | } 75 | 76 | test("Concatenated string attribute placeholder (infix)") { 77 | val id = "test" 78 | val id2 = "test2" 79 | val node = html"""
            """ 80 | assert(node.toHtml == """
              """) 81 | } 82 | 83 | test("String content placeholder") { 84 | val text = "Hello world" 85 | val div = html"
              $text
              " 86 | assert(div.toHtml == "
              Hello world
              ") 87 | } 88 | 89 | test("String (type alias) content placeholder") { 90 | type Str = String 91 | val text: Str = "Hello world" 92 | val div = html"
              $text
              " 93 | assert(div.toHtml == "
              Hello world
              ") 94 | } 95 | 96 | test("Integer content placeholder") { 97 | val text = 42 98 | val div = html"
              $text
              " 99 | assert(div.toHtml == "
              42
              ") 100 | } 101 | 102 | test("Boolean content placeholder") { 103 | val text = true 104 | val div = html"
              $text
              " 105 | assert(div.toHtml == "
              true
              ") 106 | } 107 | 108 | test("Node content placeholder") { 109 | val span = html"test" 110 | val div = html"
              $span
              " 111 | assert(div.toHtml == "
              test
              ") 112 | } 113 | 114 | test("List[Node] content placeholders") { 115 | val spans = List( 116 | html"test", 117 | html"test2" 118 | ) 119 | 120 | val div = html"
              $spans
              " 121 | assert(div.toHtml == "
              testtest2
              ") 122 | } 123 | 124 | test("List[Node] content placeholders (2)") { 125 | val children = List(Text("hello")) 126 | assert(html"$children" == tag.A.set("hello")) 127 | } 128 | 129 | test("Keep DOCTYPE") { 130 | val doctype = html"" 131 | assert(doctype.toHtml == """""") 132 | } 133 | 134 | test("Escape strings") { 135 | val id = "a < b" 136 | val div = html"
              $id
              " 137 | assert(div.toHtml == """
              a < b
              """) 138 | } 139 | 140 | test("Escape attributes") { 141 | val id = "a\"b" 142 | val div = html"
              " 143 | assert(div.toHtml == """
              """) 144 | } 145 | 146 | test("Void element") { 147 | val div = html"" 148 | assert(div.toHtml == "") 149 | } 150 | 151 | test("XML parsing") { 152 | val xml = 153 | xml"""""" 154 | 155 | assert(xml.attributes == Predef.Map( 156 | "version" -> "2.0", 157 | "xmlns:atom" -> "http://www.w3.org/2005/Atom", 158 | "xmlns:dc" -> "http://purl.org/dc/elements/1.1/" 159 | )) 160 | 161 | val atomLink = xml 162 | .children.head.asInstanceOf[Tag[_]] 163 | .children.head.asInstanceOf[Tag[_]] 164 | assert(atomLink.tagName == "atom:link") 165 | } 166 | 167 | test("Resolve node") { 168 | val div = html"""
              test
              """ 169 | val resolved = div.byId("b") 170 | assert(resolved == html"""""") 171 | } 172 | 173 | test("Resolve node (2)") { 174 | val div = html""" 175 | 176 | 177 | 178 | 179 | Example 180 | 181 | 182 |
              183 |

              Index

              184 | 185 | 186 |
              187 | 188 | 189 | """ 190 | assert(div.byIdOpt("page").nonEmpty) 191 | } 192 | 193 | test("Getting value of Boolean attributes") { 194 | val input = html"""""".as["input"] 195 | assert(!input.checked()) 196 | 197 | val input2 = html"""""".as["input"] 198 | assert(input2.checked()) 199 | } 200 | 201 | test("Setting value of Boolean attributes") { 202 | val input = html"""""".as["input"] 203 | val input2 = input.checked(true) 204 | assert(input2.checked()) 205 | assert(input2.toHtml == """""") 206 | assert(input == input2.checked(false)) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/test/scala/pine/NodePropSpec.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import org.scalacheck.{Gen, Properties} 4 | import org.scalacheck.Prop.forAll 5 | 6 | import scala.collection.mutable.ListBuffer 7 | 8 | class NodePropSpec extends Properties("Node") { 9 | val attributeKeyChar = Gen.oneOf('a', '1', '-', '_', 'ä') 10 | val attributeKeyGen = Gen.listOf(attributeKeyChar).map(_.mkString) 11 | .filter(_.nonEmpty) 12 | .filter(x => x.head.isLetter || x.head == '_') 13 | val attributeValueChar = Gen.oneOf('a', '1', 'ö', '&', ';', '\'', '"', '-', ' ') 14 | val attributeValueGen = Gen.listOf(attributeValueChar).map(_.mkString) 15 | 16 | val attribute = for { 17 | k <- attributeKeyGen 18 | v <- attributeValueGen 19 | } yield (k, v) 20 | 21 | val textGen = for { 22 | s <- attributeValueGen 23 | } yield Text(s) 24 | 25 | def tagGen(sz: Int): Gen[Tag[_]] = tagGen(sz, List.empty) 26 | 27 | def tagGen(sz: Int, parentTags: List[String]): Gen[Tag[_]] = 28 | for { 29 | // TODO Consider nesting rules (tag.Input cannot have children) 30 | tag <- Gen.oneOf("a", "b", "div", "span").filter { t => 31 | if (Set("a", "b").contains(t)) !parentTags.contains(t) 32 | else true 33 | } 34 | attributes <- Gen.mapOfN(sz / 3, attribute) 35 | 36 | n <- Gen.choose(sz / 5, sz / 2) 37 | children <- Gen.listOfN(n, sizedTree(sz / 2, parentTags ++ List(tag))).filter { l => 38 | l.length <= 1 || !l.zip(l.tail).exists { 39 | // Two adjacent nodes cannot be text nodes 40 | case (left, right) => 41 | left.isInstanceOf[Text] && 42 | right.isInstanceOf[Text] 43 | } 44 | }.filter { l => 45 | // Ignore empty text nodes 46 | !l.exists { 47 | case t: Text => t.text.isEmpty 48 | case _ => false 49 | } 50 | } 51 | } yield Tag(tag.asInstanceOf[String with Singleton], attributes, children) 52 | 53 | def sizedTree(sz: Int, parentTags: List[String]): Gen[Node] = 54 | if (sz <= 0) textGen 55 | else Gen.frequency((1, textGen), (3, tagGen(sz, parentTags))) 56 | 57 | val sized = Gen.choose(0, 20) 58 | 59 | def rootTagGen: Gen[Tag[_]] = sized.flatMap(tagGen) 60 | 61 | def fun1: Node => Boolean = { 62 | case Tag(_, _, _) => true 63 | case _ => false 64 | } 65 | 66 | def fun2: Node => Boolean = { 67 | case _: Text => true 68 | case _ => false 69 | } 70 | 71 | def filterFunGen: Gen[Node => Boolean] = Gen.oneOf(fun1, fun2) 72 | 73 | property("toHtml") = forAll(rootTagGen) { tag: Tag[_] => 74 | HtmlParser.fromString(tag.toHtml) == tag 75 | } 76 | 77 | def myFilter(node: Tag[_], f: Node => Boolean): List[Node] = { 78 | val collected = ListBuffer.empty[Node] 79 | def iter(node: Node): Unit = { 80 | if (f(node)) collected += node 81 | node match { 82 | case t @ Tag(_, _, _) => t.children.foreach(iter) 83 | case _ => 84 | } 85 | } 86 | node.children.foreach(iter) 87 | collected.toList 88 | } 89 | 90 | property("filter (forall)") = forAll(sized.flatMap(tagGen), filterFunGen) { (tag, f) => 91 | tag.filter(f).forall(f) 92 | } 93 | 94 | property("filter (reference implementation)") = forAll(sized.flatMap(tagGen), filterFunGen) { (tag, f) => 95 | tag.filter(f) == myFilter(tag, f) 96 | } 97 | 98 | property("toText") = forAll(sized.flatMap(tagGen)) { tag: Tag[_] => 99 | val text = tag.toText 100 | tag 101 | .filter(_.isInstanceOf[Text]) 102 | .forall(x => text.contains(x.asInstanceOf[Text].text.trim.replaceAll("\\s+", " "))) 103 | } 104 | 105 | /* 106 | property("toText (DOM)") = forAll(nodeGen) { node: Node => 107 | node.toText == node.toDom.textContent 108 | } 109 | }*/ 110 | } 111 | -------------------------------------------------------------------------------- /src/test/scala/pine/TagRefSpec.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | class TagRefSpec extends AnyFunSuite { 6 | test("Use DSL to set boolean attribute to true") { 7 | val node = tag.Input.id("test") 8 | 9 | val tagRef = TagRef["input"]("test") 10 | 11 | assert(!node.checked()) 12 | 13 | val updated = node.update(implicit ctx => tagRef.checked.update(!_)) 14 | assert(updated == node.checked(true)) 15 | } 16 | 17 | test("Use DSL to set boolean attribute to false") { 18 | val node = tag.Input 19 | .id("test") 20 | .checked(true) 21 | 22 | val tagRef = TagRef[tag.Input]("test") 23 | 24 | assert(node.checked()) 25 | 26 | val updated = node.update(implicit ctx => tagRef.checked.update(!_)) 27 | assert(updated == node.checked(false)) 28 | } 29 | 30 | test("Use DSL to set CSS tag") { 31 | val node = tag.Input 32 | .id("test") 33 | .`type`("checkbox") 34 | 35 | val tagRef = TagRef["input"]("test") 36 | 37 | val updated = node.update(implicit ctx => tagRef.`class`.state(true, "a")) 38 | assert(updated == node.`class`("a")) 39 | } 40 | 41 | test("Use DSL to remove CSS tag") { 42 | val node = tag.Input 43 | .id("test") 44 | .`type`("checkbox") 45 | .`class`("a", "b", "c") 46 | 47 | val tagRef = TagRef["input"]("test") 48 | 49 | val updated = node.update(implicit ctx => tagRef.`class`.state(false, "b")) 50 | assert(updated == node.`class`("a c")) 51 | } 52 | 53 | test("Match by tag") { 54 | val node = tag.Div.set(tag.Span) 55 | val ref = TagRef["span"] 56 | val updated = node.update(implicit ctx => ref := tag.B) 57 | 58 | assert(updated == tag.Div.set(tag.Span.set(tag.B))) 59 | } 60 | 61 | test("Prepend nodes") { 62 | val div = html"""

              """ 63 | val html = div.update(implicit ctx => 64 | TagRef["span"].prepend(List( 65 | html"Hello", 66 | html"World" 67 | )) 68 | ).toHtml 69 | assert(html == "
              HelloWorld
              ") 70 | } 71 | 72 | test("Append nodes") { 73 | val div = html"""

              """ 74 | val html = div.update(implicit ctx => 75 | TagRef["span"] ++= List( 76 | html"Hello", 77 | html"World" 78 | ) 79 | ).toHtml 80 | assert(html == "

              HelloWorld
              ") 81 | } 82 | 83 | test("Insert before") { 84 | val div = html"""


              """ 85 | val html = div.update(implicit ctx => 86 | TagRef["span"].insertBefore(TagRef["hr"], tag.Div) 87 | ).toHtml 88 | assert(html == "


              ") 89 | } 90 | 91 | test("Insert after") { 92 | val div = html"""

              """ 93 | val html = div.update(implicit ctx => 94 | TagRef["span"].insertAfter(TagRef["hr"], tag.Div) 95 | ).toHtml 96 | assert(html == "

              ") 97 | } 98 | 99 | test("Insert after (2)") { 100 | val div = html"""


              """ 101 | val html = div.update(implicit ctx => 102 | TagRef["span"].insertAfter(TagRef["hr"], tag.Div) 103 | ).toHtml 104 | assert(html == "


              ") 105 | } 106 | 107 | test("Replace node") { 108 | val node = tag.Div.set(tag.Span) 109 | val ref = TagRef["span"] 110 | val updated = node.update(implicit ctx => ref.replace(tag.B)) 111 | 112 | assert(updated == tag.Div.set(tag.B)) 113 | } 114 | 115 | test("Clear nodes") { 116 | val div = html"""

              """ 117 | val html = div.update(implicit ctx => TagRef["span"].clearAll()).toHtml 118 | assert(html == "
              ") 119 | } 120 | 121 | test("Add class") { 122 | val div = html"""
              """ 123 | val html = div.update(implicit ctx => 124 | TagRef["div"].`class`.add("a")).toHtml 125 | assert(html == """
              """) 126 | } 127 | 128 | test("Add existing class") { 129 | val div = html"""
              """ 130 | val html = div.update(implicit ctx => 131 | TagRef["div"].`class`.add("a")).toHtml 132 | assert(html == """
              """) 133 | } 134 | 135 | test("Add existing class (2)") { 136 | // Retain order 137 | val div = html"""
              """ 138 | val html = div.update(implicit ctx => 139 | TagRef["div"].`class`.add("a")).toHtml 140 | assert(html == """
              """) 141 | } 142 | 143 | test("Add existing class (3)") { 144 | val div = html"""
              """ 145 | val html = div.update(implicit ctx => 146 | TagRef["div"].`class`.state(true, "a")).toHtml 147 | assert(html == """
              """) 148 | } 149 | 150 | test("Remove non-existing class") { 151 | val div = html"""
              """ 152 | val html = div.update(implicit ctx => 153 | TagRef["div"].`class`.remove("c")).toHtml 154 | assert(html == """
              """) 155 | } 156 | 157 | test("Remove non-existing class (2)") { 158 | val div = html"""
              """ 159 | val html = div.update(implicit ctx => 160 | TagRef["div"].`class`.remove("c")).toHtml 161 | assert(html == """
              """) 162 | } 163 | 164 | test("Toggle class") { 165 | val div = html"""
              """ 166 | val html = div.update(implicit ctx => 167 | TagRef["div"].`class`.toggle("b")).toHtml 168 | assert(html == """
              """) 169 | } 170 | 171 | test("Toggle class (2)") { 172 | val div = html"""
              """ 173 | val html = div.update(implicit ctx => 174 | TagRef["div"].`class`.toggle("b")).toHtml 175 | assert(html == """
              """) 176 | } 177 | 178 | test("Update class on ByClass reference") { 179 | val node = tag.Div.`class`("test") 180 | val ref = TagRef.ByClass[tag.Div]("test") 181 | 182 | val updated = node.update(implicit ctx => ref.`class` := "test2") 183 | assert(updated == tag.Div.`class`("test2")) 184 | } 185 | 186 | test("Resolve child references") { 187 | val itemRef = TagRef["div"]("item", "0") 188 | assert(itemRef == TagRef["div"]("item0")) 189 | } 190 | 191 | test("Invalid reference") { 192 | val node = tag.Div 193 | val ref = TagRef["span"] 194 | 195 | assertThrows[Exception] { 196 | node.update(implicit ctx => ref.replace(tag.B)) 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/test/scala/pine/TextSpec.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | class TextSpec extends AnyFunSuite { 6 | test("Text node") { 7 | val node = tag.Span.set(Text("Hello\nworld")) 8 | assert(node.toText == "Hello world") 9 | } 10 | 11 | test("Text node (2)") { 12 | val node = tag.Span.set(Text("Hello\tworld")) 13 | assert(node.toText == "Hello world") 14 | } 15 | 16 | test("Text node (3)") { 17 | val node = tag.Span.set(Text("Hello \t\n world")) 18 | assert(node.toText == "Hello world") 19 | } 20 | 21 | test("Script node") { 22 | val node = html"""""" 23 | assert(node.toText == "") 24 | } 25 | 26 | test("List node") { 27 | val node = html"""
              • Hello
              • World
              """ 28 | assert(node.toText == "- Hello\n- World") 29 | } 30 | 31 | test("List node (2)") { 32 | val node = html"""
              • Hello
              • World
              """ 33 | assert(node.toText == "- Hello\n- World") 34 | } 35 | 36 | test("List node (3)") { 37 | val node = html"""
              • Hello
              • World
              """ 38 | assert(node.toText == "- Hello\n- World") 39 | } 40 | 41 | test("Div node") { 42 | val node = html"""
              Hello
              """ 43 | assert(node.toText == "Hello") 44 | } 45 | 46 | test("Div nodes") { 47 | val node = html"""
              Hello
              World
              """ 48 | assert(node.toText == "Hello\nWorld") 49 | } 50 | 51 | test("Heading nodes") { 52 | val node = html"""

              Heading

              Text
              """ 53 | assert(node.toText == "Heading\n\nText") 54 | } 55 | 56 | test("Heading nodes (2)") { 57 | val node = html"""

              Heading

              Text

              """ 58 | assert(node.toText == "Heading\n\nText") 59 | } 60 | 61 | test("Heading nodes (3)") { 62 | val node = html"""

              Heading

              Text

              Heading

              Text

              """ 63 | assert(node.toText == "Heading\n\nText\n\nHeading\n\nText") 64 | } 65 | 66 | test("Paragraph nodes") { 67 | val node = html"""

              First

              Second

              """ 68 | assert(node.toText == "First\n\nSecond") 69 | } 70 | 71 | test("Line break node") { 72 | val node = html"""
              Hello
              world
              """ 73 | assert(node.toText == "Hello\nworld") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/scala/pine/XmlParserSpec.scala: -------------------------------------------------------------------------------- 1 | package pine 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | class XmlParserSpec extends AnyFunSuite { 6 | test("Simple node") { 7 | val xml = "test" 8 | assert(XmlParser.fromString(xml) == tag.B.set("test")) 9 | } 10 | 11 | test("Empty entity") { 12 | val xml = "&;" 13 | assertThrows[ParseError] { 14 | XmlParser.fromString(xml) 15 | } 16 | } 17 | 18 | test("Invalid entity") { 19 | val xml = "λ" 20 | assertThrows[ParseError] { 21 | XmlParser.fromString(xml) 22 | } 23 | } 24 | 25 | test("Invalid entity (2)") { 26 | val xml = "&abcd;" 27 | assertThrows[ParseError] { 28 | XmlParser.fromString(xml) 29 | } 30 | } 31 | 32 | test("Valid entity") { 33 | val xml = "&" 34 | assert(XmlParser.fromString(xml) == tag.B.set("&")) 35 | } 36 | 37 | test("Parse CDATA") { 38 | val html = "" 39 | val node = XmlParser.fromString(html) 40 | assert(node == Tag("node").set(Text("hello"))) 41 | } 42 | 43 | // From https://www.soapui.org/docs/functional-testing/working-with-cdata.html 44 | test("Parse CDATA (2)") { 45 | val html = "embedded XML]]>" 46 | val node = XmlParser.fromString(html) 47 | assert(node == Tag("message").set(Text("embedded XML"))) 48 | } 49 | 50 | test("Parse CDATA (3)") { 51 | val html = "embedded XML with XML]]]]>>]]>" 52 | val node = XmlParser.fromString(html) 53 | assert(node == Tag("message").set(List( 54 | Text("embedded XML with XML]]"), 55 | Text(">"), 56 | Text("")) 57 | )) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := { 2 | import sys.process._ 3 | val version = Seq("git", "describe", "--tags").!!.trim.tail 4 | println("[info] Setting version to: " + version) 5 | version 6 | } 7 | --------------------------------------------------------------------------------