├── .github └── workflows │ └── test.yaml ├── .gitignore ├── ChangeLog ├── LICENSE ├── META6.json ├── README.md ├── doc └── Pod │ └── To │ └── HTML.rakudoc ├── lib └── Pod │ └── To │ └── HTML.pm6 ├── resources ├── css │ └── github.css ├── examples │ ├── README.md │ ├── main.mustache │ ├── render.raku │ └── with-partials │ │ ├── css │ │ └── style.css │ │ ├── main.mustache │ │ ├── markdown-guide.pod │ │ ├── partials │ │ ├── banner.mustache │ │ ├── footer.mustache │ │ ├── head.mustache │ │ ├── heading.mustache │ │ └── meta.mustache │ │ └── render.raku └── templates │ └── main.mustache └── t ├── 010-basic.t ├── 011-external.t ├── 012-multi.t ├── 020-code.t ├── 030-comment.t ├── 040-lists.t ├── 050-format-x-index.t ├── 060-table.t ├── 070-headings.t ├── 075-defn.t ├── 080-lang.t ├── 090-css.t ├── 100-issue-37.t ├── 110-issue-41.t ├── 111-perl6-doc-issue-2270.t ├── 120-templates.t ├── 130-links.t ├── 140-config.t ├── class.pod6 ├── multi.pod6 ├── templates └── main.mustache └── test.pod6 /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: "Test in a Raku container" 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | permissions: 7 | packages: read 8 | container: 9 | image: ghcr.io/jj/raku-zef-gha 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | - name: Cache installed dists 14 | uses: actions/cache@v4 15 | id: meta6 16 | with: 17 | path: ~/.raku/ 18 | key: ${{ runner.os }}-${{ hashFiles('META6.json') }} 19 | - name: Install modules 20 | if: steps.meta6.outputs.cache-hit != 'true' 21 | run: zef install . 22 | - name: Test 23 | run: zef --debug test . 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .precomp 3 | .idea 4 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 2018-12-18 Juan J. merelo 2 | 3 | * META6.json: Bumps up version to account for minimum Pod::Load version requirements. 4 | 5 | 2018-12-14 Juan J. merelo 6 | 7 | * 011-external.t: Adds test mainly for integration with GitHub, and avoiding prepending package names to defined classes. 8 | 9 | 2018-12-11 Juan J. merelo 10 | 11 | * META6.json: Bumps up to 0.4.1 after fixing the problem of URIzing titles and subtitles. 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The Artistic License 2.0 2 | 3 | Copyright (c) 2000-2006, The Perl Foundation. 4 | 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | This license establishes the terms under which a given free software 11 | Package may be copied, modified, distributed, and/or redistributed. 12 | The intent is that the Copyright Holder maintains some artistic 13 | control over the development of that Package while still keeping the 14 | Package available as open source and free software. 15 | 16 | You are always permitted to make arrangements wholly outside of this 17 | license directly with the Copyright Holder of a given Package. If the 18 | terms of this license do not permit the full use that you propose to 19 | make of the Package, you should contact the Copyright Holder and seek 20 | a different licensing arrangement. 21 | 22 | Definitions 23 | 24 | "Copyright Holder" means the individual(s) or organization(s) 25 | named in the copyright notice for the entire Package. 26 | 27 | "Contributor" means any party that has contributed code or other 28 | material to the Package, in accordance with the Copyright Holder's 29 | procedures. 30 | 31 | "You" and "your" means any person who would like to copy, 32 | distribute, or modify the Package. 33 | 34 | "Package" means the collection of files distributed by the 35 | Copyright Holder, and derivatives of that collection and/or of 36 | those files. A given Package may consist of either the Standard 37 | Version, or a Modified Version. 38 | 39 | "Distribute" means providing a copy of the Package or making it 40 | accessible to anyone else, or in the case of a company or 41 | organization, to others outside of your company or organization. 42 | 43 | "Distributor Fee" means any fee that you charge for Distributing 44 | this Package or providing support for this Package to another 45 | party. It does not mean licensing fees. 46 | 47 | "Standard Version" refers to the Package if it has not been 48 | modified, or has been modified only in ways explicitly requested 49 | by the Copyright Holder. 50 | 51 | "Modified Version" means the Package, if it has been changed, and 52 | such changes were not explicitly requested by the Copyright 53 | Holder. 54 | 55 | "Original License" means this Artistic License as Distributed with 56 | the Standard Version of the Package, in its current version or as 57 | it may be modified by The Perl Foundation in the future. 58 | 59 | "Source" form means the source code, documentation source, and 60 | configuration files for the Package. 61 | 62 | "Compiled" form means the compiled bytecode, object code, binary, 63 | or any other form resulting from mechanical transformation or 64 | translation of the Source form. 65 | 66 | 67 | Permission for Use and Modification Without Distribution 68 | 69 | (1) You are permitted to use the Standard Version and create and use 70 | Modified Versions for any purpose without restriction, provided that 71 | you do not Distribute the Modified Version. 72 | 73 | 74 | Permissions for Redistribution of the Standard Version 75 | 76 | (2) You may Distribute verbatim copies of the Source form of the 77 | Standard Version of this Package in any medium without restriction, 78 | either gratis or for a Distributor Fee, provided that you duplicate 79 | all of the original copyright notices and associated disclaimers. At 80 | your discretion, such verbatim copies may or may not include a 81 | Compiled form of the Package. 82 | 83 | (3) You may apply any bug fixes, portability changes, and other 84 | modifications made available from the Copyright Holder. The resulting 85 | Package will still be considered the Standard Version, and as such 86 | will be subject to the Original License. 87 | 88 | 89 | Distribution of Modified Versions of the Package as Source 90 | 91 | (4) You may Distribute your Modified Version as Source (either gratis 92 | or for a Distributor Fee, and with or without a Compiled form of the 93 | Modified Version) provided that you clearly document how it differs 94 | from the Standard Version, including, but not limited to, documenting 95 | any non-standard features, executables, or modules, and provided that 96 | you do at least ONE of the following: 97 | 98 | (a) make the Modified Version available to the Copyright Holder 99 | of the Standard Version, under the Original License, so that the 100 | Copyright Holder may include your modifications in the Standard 101 | Version. 102 | 103 | (b) ensure that installation of your Modified Version does not 104 | prevent the user installing or running the Standard Version. In 105 | addition, the Modified Version must bear a name that is different 106 | from the name of the Standard Version. 107 | 108 | (c) allow anyone who receives a copy of the Modified Version to 109 | make the Source form of the Modified Version available to others 110 | under 111 | 112 | (i) the Original License or 113 | 114 | (ii) a license that permits the licensee to freely copy, 115 | modify and redistribute the Modified Version using the same 116 | licensing terms that apply to the copy that the licensee 117 | received, and requires that the Source form of the Modified 118 | Version, and of any works derived from it, be made freely 119 | available in that license fees are prohibited but Distributor 120 | Fees are allowed. 121 | 122 | 123 | Distribution of Compiled Forms of the Standard Version 124 | or Modified Versions without the Source 125 | 126 | (5) You may Distribute Compiled forms of the Standard Version without 127 | the Source, provided that you include complete instructions on how to 128 | get the Source of the Standard Version. Such instructions must be 129 | valid at the time of your distribution. If these instructions, at any 130 | time while you are carrying out such distribution, become invalid, you 131 | must provide new instructions on demand or cease further distribution. 132 | If you provide valid instructions or cease distribution within thirty 133 | days after you become aware that the instructions are invalid, then 134 | you do not forfeit any of your rights under this license. 135 | 136 | (6) You may Distribute a Modified Version in Compiled form without 137 | the Source, provided that you comply with Section 4 with respect to 138 | the Source of the Modified Version. 139 | 140 | 141 | Aggregating or Linking the Package 142 | 143 | (7) You may aggregate the Package (either the Standard Version or 144 | Modified Version) with other packages and Distribute the resulting 145 | aggregation provided that you do not charge a licensing fee for the 146 | Package. Distributor Fees are permitted, and licensing fees for other 147 | components in the aggregation are permitted. The terms of this license 148 | apply to the use and Distribution of the Standard or Modified Versions 149 | as included in the aggregation. 150 | 151 | (8) You are permitted to link Modified and Standard Versions with 152 | other works, to embed the Package in a larger work of your own, or to 153 | build stand-alone binary or bytecode versions of applications that 154 | include the Package, and Distribute the result without restriction, 155 | provided the result does not expose a direct interface to the Package. 156 | 157 | 158 | Items That are Not Considered Part of a Modified Version 159 | 160 | (9) Works (including, but not limited to, modules and scripts) that 161 | merely extend or make use of the Package, do not, by themselves, cause 162 | the Package to be a Modified Version. In addition, such works are not 163 | considered parts of the Package itself, and are not subject to the 164 | terms of this license. 165 | 166 | 167 | General Provisions 168 | 169 | (10) Any use, modification, and distribution of the Standard or 170 | Modified Versions is governed by this Artistic License. By using, 171 | modifying or distributing the Package, you accept this license. Do not 172 | use, modify, or distribute the Package, if you do not accept this 173 | license. 174 | 175 | (11) If your Modified Version has been derived from a Modified 176 | Version made by someone other than you, you are nevertheless required 177 | to ensure that your Modified Version complies with the requirements of 178 | this license. 179 | 180 | (12) This license does not grant you the right to use any trademark, 181 | service mark, tradename, or logo of the Copyright Holder. 182 | 183 | (13) This license includes the non-exclusive, worldwide, 184 | free-of-charge patent license to make, have made, use, offer to sell, 185 | sell, import and otherwise transfer the Package with respect to any 186 | patent claims licensable by the Copyright Holder that are necessarily 187 | infringed by the Package. If you institute patent litigation 188 | (including a cross-claim or counterclaim) against any party alleging 189 | that the Package constitutes direct or contributory patent 190 | infringement, then this Artistic License to you shall terminate on the 191 | date that such litigation is filed. 192 | 193 | (14) Disclaimer of Warranty: 194 | THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS 195 | IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED 196 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR 197 | NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL 198 | LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL 199 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 200 | DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF 201 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 202 | -------------------------------------------------------------------------------- /META6.json: -------------------------------------------------------------------------------- 1 | { 2 | "perl" : "6.d", 3 | "name" : "Pod::To::HTML", 4 | "version" : "v0.8.1", 5 | "auth" : "github:Raku", 6 | "authors" : [ "Raku Community modules" ], 7 | "description" : "Convert Raku Pod to HTML", 8 | "license" : "Artistic-2.0", 9 | "depends" : [ 10 | "URI", 11 | "Template::Mustache", 12 | "Pod::Load:ver<0.4.0+>", 13 | "OO::Monitors" 14 | ], 15 | "test-depends": [ 16 | "Test::Output" 17 | ], 18 | "provides" : { 19 | "Pod::To::HTML" : "lib/Pod/To/HTML.pm6" 20 | }, 21 | "resources": [ 22 | "templates/main.mustache", 23 | "css/github.css" 24 | ], 25 | "source-url" : "git://github.com/Raku/Pod-To-HTML.git" 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pod::To::HTML v0.8.1 2 | ==================== 3 | 4 | [![Test in a Raku container](https://github.com/Raku/Pod-To-HTML/actions/workflows/test.yaml/badge.svg)](https://github.com/Raku/Pod-To-HTML/actions/workflows/test.yaml) 5 | 6 | 7 | Raku module to render Pod as HTML. 8 | 9 | Synopsis 10 | -------- 11 | 12 | From the command line: 13 | 14 | raku --doc=HTML lib/FancyModule.rakumod > FancyModule.html 15 | 16 | From within Raku: 17 | 18 | use Pod::To::HTML:auth; 19 | 20 | # Pod file 21 | say render( 22 | 'your/file.pod'.IO, 23 | title => 'My Own Title', 24 | subtitle => 'On the Art of Making Titles', 25 | lang => 'en', 26 | ); 27 | 28 | Installation 29 | ------------ 30 | 31 | From the [Raku ecosystem](https://modules.raku.org): 32 | 33 | $ zef install Pod::To::HTML:auth 34 | 35 | From source: 36 | 37 | $ git clone https://github.com/Raku/Pod-To-HTML.git 38 | $ cd Pod-To-HTML/ 39 | $ zef install . 40 | 41 | **Note**: Perl 6 2018.06 introduces changes on how non-breaking whitespace was handled; this is now included in the tests. If the installation fails, please upgrade to Perl 6 >= 2018.06 or simply disregard the test and install with `--force` if that particular feature is of no use to you. 42 | 43 | **Note 2**: Perl6 2018.11 introduced handling of Definition blocks, `Defn`. Please upgrade if you are using that feature in the documentation. 44 | 45 | Description 46 | ----------- 47 | 48 | `Pod::To::HTML` takes a Pod tree and outputs correspondingly formatted HTML using a default or provided Mustache template. There are two ways of accomplishing this: 49 | 50 | * from the command line, using `raku --doc=HTML`, which extracts the Pod from the document and feeds it to `Pod::To::HTML`. 51 | 52 | * from within a Raku program via the exported `render` subroutine, which creates a complete HTML document from the Pod. This allows more customization (`title`, `subtitle`, and `lang` can override Pod's corresponding semantics, different Mustache template (possibly with partials), additional template variables for the template, etc.) than simply rendering the Pod via `raku --doc=HTML` which just use the default template. 53 | 54 | Exported subroutines 55 | -------------------- 56 | 57 | **`render`**: Render a Pod document from several sources. `title`, `subtitle`, and `lang` are supplied to the Mustache template and override the Pod document's corresponding semantic blocks. A `template` path can be passed; the Mustache template `main.mustache` must be under that path. Partials to the template must be under the same path in a directory named `partials`. 58 | 59 | * `render(Array $pod, Str :$title, Str :$subtitle, Str :$lang, Str :$template = Str, *%template-vars)` 60 | 61 | * `render(Pod::Block $pod, Str :$title, Str :$subtitle, Str :$lang, Str :$template = Str, *%template-vars)` 62 | 63 | * `render(IO::Path $file, Str :$title, Str :$subtitle, Str :$lang, Str :$template = Str, *%template-vars)` 64 | 65 | * `render(Str $pod-string, Str :$title, Str :$subtitle, Str :$lang, Str :$template = Str, *%template-vars)` 66 | 67 | Template information 68 | -------------------- 69 | 70 | `Pod::To::HTML` makes the following information available to the Mustache template: 71 | 72 | * `title`: This is picked up from the Pod's semantic block `=TITLE` (if any), although it can be overridden by supplying it via `render`. It defaults to the empty string. 73 | 74 | * `subtitle`: This is picked up from the Pod's semantic block `=SUBTITLE` (if any), although it can be overridden by supplying it via `render`. It defaults to the empty string. 75 | 76 | * `lang`: This is picked up from the Pod's semantic block `=LANG` (if any), although it can be overridden by supplying it via `render`. It defaults to the `en`. 77 | 78 | * `toc`: The Pod document's table of contents. 79 | 80 | * `footnotes`: The Pod document's [footnotes](https://docs.raku.org/language/pod#Notes). 81 | 82 | Additional information can be made available to the Mustache template by supplying to `render` as named arguments. For example, `css-url => https://design.raku.org/perl.css` will be available to the template as `css-url`. 83 | 84 | ### Semantic Blocks 85 | 86 | Semantic blocks are treated as metadata and supplied as such to a Mustache template. For example, from the Pod document: 87 | 88 | =begin pod 89 | =TITLE Classes and objects 90 | =SUBTITLE A tutorial about creating and using classes in Raku 91 | =LANG English 92 | =DATE January 01, 2020 93 | =end pod 94 | 95 | the template variables `title`, `subtitle`, `lang`, and `date` are made available to a Mustache template. Both `title` and `subtitle` can be overridden via the `render` subroutine. 96 | 97 | **Note**: Pod's semantic blocks can be overridden via `render` by using a variable of the same name. 98 | 99 | Examples 100 | -------- 101 | 102 | Check the [examples](resources/examples/README.md) directory (which should have been installed with your distribution, or is right here if you download from source) for a few illustrative examples. 103 | 104 | Debugging 105 | --------- 106 | 107 | You can set the `P6DOC_DEBUG` environmental variable to make the module produce some debugging information. 108 | 109 | P6DOC_DEBUG=1 raku --doc=HTML lib/FancyModule.rakumod > FancyModule.html 110 | 111 | License 112 | ------- 113 | 114 | You can use and distribute this module under the terms of the The Artistic License 2.0. See the LICENSE file included in this distribution for complete details. 115 | 116 | The `META6.json` file of this distribution may be distributed and modified without restrictions or attribution. 117 | -------------------------------------------------------------------------------- /doc/Pod/To/HTML.rakudoc: -------------------------------------------------------------------------------- 1 | =begin pod 2 | =head1 Pod::To::HTML 3 | 4 | [![Build Status](https://travis-ci.org/perl6/Pod-To-HTML.svg?branch=master)](https://travis-ci.org/perl6/Pod-To-HTML) 5 | 6 | Raku module to render Pod as HTML. 7 | 8 | =head2 Synopsis 9 | 10 | From the command line: 11 | 12 | =for code 13 | raku --doc=HTML lib/FancyModule.rakumod > FancyModule.html 14 | 15 | From within Raku: 16 | 17 | =begin code 18 | use Pod::To::HTML; 19 | 20 | # Pod file 21 | say render( 22 | 'your/file.pod'.IO, 23 | title => 'My Own Title', 24 | subtitle => 'On the Art of Making Titles', 25 | lang => 'en', 26 | ); 27 | =end code 28 | 29 | =head2 Installation 30 | 31 | From the L«Raku ecosystem|https://modules.raku.org»: 32 | 33 | =code $ zef install Pod::To::HTML 34 | 35 | From source: 36 | 37 | =for code 38 | $ git clone https://github.com/perl6/Pod-To-HTML.git 39 | $ cd Pod-To-HTML/ 40 | $ zef install . 41 | 42 | B: Perl 6 2018.06 introduces changes on how non-breaking 43 | whitespace was handled; this is now included in the tests. If 44 | the installation fails, please upgrade to Perl 6 >= 2018.06 or 45 | simply disregard the test and install with C<--force> if that 46 | particular feature is of no use to you. 47 | 48 | B: Perl6 2018.11 introduced handling of Definition blocks, 49 | C. Please upgrade if you are using that feature in the 50 | documentation. 51 | 52 | =head2 Description 53 | 54 | C«Pod::To::HTML» takes a Pod tree and outputs correspondingly 55 | formatted HTML using a default or provided Mustache template. 56 | There are two ways of accomplishing this: 57 | 58 | =item from the command line, using C, which extracts the Pod 59 | from the document and feeds it to C. 60 | 61 | =item from within a Raku program via the exported C subroutine, 62 | which creates a complete HTML document from the Pod. This allows more 63 | customization (C, C<subtitle>, and C<lang> can override Pod's 64 | corresponding semantics, different Mustache template (possibly with partials), 65 | additional template variables for the template, etc.) than simply 66 | rendering the Pod via C<raku --doc=HTML> which just use the default 67 | template. 68 | 69 | =head2 Exported subroutines 70 | 71 | B<C<render>>: Render a Pod document from several sources. C<title>, C<subtitle>, 72 | and C<lang> are supplied to the Mustache template and override the Pod document's 73 | corresponding semantic blocks. A C<template> path can be passed; the Mustache 74 | template C<main.mustache> must be under that path. Partials to the template 75 | must be under the same path in a directory named C<partials>. 76 | 77 | =item C<render(Array $pod, Str :$title, Str :$subtitle, Str :$lang, Str :$template = Str, *%template-vars)> 78 | =item C<render(Pod::Block $pod, Str :$title, Str :$subtitle, Str :$lang, Str :$template = Str, *%template-vars)> 79 | =item C<render(IO::Path $file, Str :$title, Str :$subtitle, Str :$lang, Str :$template = Str, *%template-vars)> 80 | =item C<render(Str $pod-string, Str :$title, Str :$subtitle, Str :$lang, Str :$template = Str, *%template-vars)> 81 | 82 | =head2 Template information 83 | 84 | C<Pod::To::HTML> makes the following information available to the Mustache 85 | template: 86 | 87 | =item C<title>: This is picked up from the Pod's semantic block C<=TITLE> 88 | (if any), although it can be overridden by supplying it via C<render>. 89 | It defaults to the empty string. 90 | =item C<subtitle>: This is picked up from the Pod's semantic block C<=SUBTITLE> 91 | (if any), although it can be overridden by supplying it via C<render>. 92 | It defaults to the empty string. 93 | =item C<lang>: This is picked up from the Pod's semantic block C<=LANG> 94 | (if any), although it can be overridden by supplying it via C<render>. 95 | It defaults to the C<en>. 96 | =item C<toc>: The Pod document's table of contents. 97 | =item C<footnotes>: The Pod document's L<footnotes|https://docs.raku.org/language/pod#Notes>. 98 | 99 | 100 | Additional information can be made available to the Mustache template 101 | by supplying to C<render> as named arguments. For example, 102 | C«css-url => https://design.raku.org/perl.css» will be available to the 103 | template as C<css-url>. 104 | 105 | =head3 Semantic Blocks 106 | 107 | Semantic blocks are treated as metadata and supplied as such to a Mustache 108 | template. For example, from the Pod document: 109 | 110 | =begin code 111 | =begin pod 112 | =TITLE Classes and objects 113 | =SUBTITLE A tutorial about creating and using classes in Raku 114 | =LANG English 115 | =DATE January 01, 2020 116 | =end pod 117 | =end code 118 | 119 | the template variables C<title>, C<subtitle>, C<lang>, and C<date> are made 120 | available to a Mustache template. Both C<title> and C<subtitle> can be 121 | overridden via the C<render> subroutine. 122 | 123 | B<Note>: Pod's semantic blocks can be overridden via C<render> by using a 124 | variable of the same name. 125 | 126 | =head2 Examples 127 | 128 | Check the L«examples|resources/examples/README.md» directory (which 129 | should have been installed with your distribution, or is right here if 130 | you download from source) for a few illustrative examples. 131 | 132 | =head2 Debugging 133 | 134 | You can set the C«P6DOC_DEBUG» environmental variable to make the 135 | module produce some debugging information. 136 | 137 | =for code 138 | P6DOC_DEBUG=1 raku --doc=HTML lib/FancyModule.rakumod > FancyModule.html 139 | 140 | =head2 License 141 | 142 | You can use and distribute this module under the terms of the 143 | The Artistic License 2.0. See the LICENSE file included in this distribution for 144 | complete details. 145 | 146 | The C«META6.json» file of this distribution may be distributed and modified 147 | without restrictions or attribution. 148 | =end pod 149 | -------------------------------------------------------------------------------- /lib/Pod/To/HTML.pm6: -------------------------------------------------------------------------------- 1 | use OO::Monitors; 2 | use URI::Escape; 3 | use Template::Mustache; 4 | use Pod::Load; 5 | 6 | class Pod::List is Pod::Block {}; 7 | class Pod::DefnList is Pod::Block {}; 8 | BEGIN { if ::('Pod::Defn') ~~ Failure { CORE::Pod::<Defn> := class {} } } 9 | class Node::To::HTML {...} 10 | class TOC::Calculator {...} 11 | 12 | my constant $MAX-COMPILE-TEMPLATE-ATTEMPTS = 5; 13 | 14 | role Pod::To::HTML::Renderer { 15 | method render(%context) { 16 | ... 17 | } 18 | } 19 | 20 | class Pod::To::HTML::Mustache does Pod::To::HTML::Renderer { 21 | has $.template; 22 | has $.main-template-path; 23 | 24 | method render(%context) { 25 | # get 'main.mustache' file (and possible its partials) under template path. 26 | my ($template-file, %partials) = retrieve-templates($!template, $!main-template-path); 27 | my $content = $template-file.IO.slurp; 28 | 29 | # Try to compile a template N times to workaround issues with race-y compilation 30 | my $counter = 0; 31 | loop { 32 | $!.throw if $counter++ == $MAX-COMPILE-TEMPLATE-ATTEMPTS; 33 | my $result = try Template::Mustache.new.render($content, %context, :from[%partials], :literal); 34 | with $! { 35 | warn "An error occurred when rendering [%context<title>], retrying, $counter times left, original message: \n" 36 | ~ $!.message; 37 | } else { 38 | return $result; 39 | } 40 | } 41 | } 42 | } 43 | 44 | # the Rakudo compiler expects there to be a render method with a Pod::To::<name> invocant 45 | ## when --doc=name is used. Then the render method is called with a pod tree. 46 | ## The following adds a Pod::To::HTML class and the method to call the subs in the module. 47 | class Pod::To::HTML { 48 | # Renderers 49 | has $.page-renderer; 50 | has $.node-renderer; 51 | # Data from creator 52 | has $.title; 53 | has $.subtitle; 54 | has &.url; 55 | has $.lang; 56 | has %.template-vars; 57 | # Page data 58 | has %.metadata; 59 | has %.crossrefs; 60 | has @.body; 61 | has @.footnotes; 62 | 63 | method new(:$title, :$subtitle, :$lang, :templates(:$template), :$main-template-path = '', 64 | :$node-renderer = Node::To::HTML, :$page-renderer = Pod::To::HTML::Mustache.new(:$template, :$main-template-path), 65 | *%template-vars) { 66 | self.bless(:$title, :$subtitle, :$lang, :$node-renderer, :$page-renderer, :%template-vars); 67 | } 68 | 69 | method url($url-text) { &!url.defined ?? &!url($url-text) !! $url-text } 70 | 71 | #| Converts a Pod tree to a HTML document using templates 72 | method render($pod, TOC::Calculator :$toc = TOC::Calculator, :&url = -> $url { $url }) { 73 | unless self { 74 | return Pod::To::HTML.new.render($pod, :$toc); 75 | } 76 | # Keep count of how many footnotes we've output. 77 | my Int $*done-notes = 0; 78 | &!url = &url; 79 | debug { note colored("About to call node2html ", "bold") ~ $pod.perl }; 80 | @!body.push: $!node-renderer.new(renderer => self).node2html($pod.map: { visit $_, :assemble(&assemble-list-items) }); 81 | 82 | # sensible css default 83 | my $default-style = %?RESOURCES<css/github.css>.IO.slurp; 84 | 85 | # title and subtitle picked from Pod document can be overridden 86 | # with provided ones via the subroutines. 87 | my $title-html = $!title // %!metadata<title> // ''; 88 | my $subtitle-html = $!subtitle // %!metadata<subtitle> // ''; 89 | my $lang-html = $!lang // %!metadata<lang> // 'en'; 90 | 91 | my %context = :@!body, 92 | # documentable template vars 93 | :title($title-html), :subtitle($subtitle-html), 94 | :toc($toc.new(:$pod).render), :lang($lang-html), 95 | :footnotes(self.do-footnotes), 96 | # probably documentable 97 | :$default-style, 98 | # user should be aware that semantic blocks are made available to the Mustache template. 99 | |%!metadata, 100 | # user can supply additional template variables for the Mustache template. 101 | |%!template-vars; 102 | $!page-renderer.render(%context); 103 | } 104 | 105 | # Flushes accumulated footnotes since last call. The idea here is that we can stick calls to this 106 | # before each C«</section>» tag (once we have those per-header) and have notes that are visually 107 | # and semantically attached to the section. 108 | method do-footnotes() { 109 | return '' unless @!footnotes; 110 | 111 | my Int $current-note = $*done-notes + 1; 112 | my $notes = @!footnotes.kv.map(-> $k, $v { 113 | my $num = $k + $current-note; 114 | qq{<li><a href="#fn-ref-$num" id="fn-$num">[↑]</a> $v </li>\n} 115 | }).join; 116 | 117 | $*done-notes += @!footnotes; 118 | qq[<aside><ol start="$current-note">\n$notes\</ol></aside>]; 119 | } 120 | } 121 | 122 | monitor Node::To::HTML { 123 | has Pod::To::HTML $.renderer; 124 | 125 | # inline level or below 126 | multi method node2inline($node --> Str) { 127 | debug { note colored("missing a node2inline multi for ", "bold") ~ $node.gist }; 128 | node2text($node); 129 | } 130 | 131 | multi method node2inline(Pod::Block::Para $node --> Str) { self.node2inline($node.contents) } 132 | 133 | multi method node2inline(Pod::FormattingCode $node --> Str) { 134 | my %basic-html = 135 | B => 'strong', #= Basis 136 | C => 'code', #= Code 137 | I => 'em', #= Important 138 | K => 'kbd', #= Keyboard 139 | R => 'var', #= Replaceable 140 | T => 'samp', #= Terminal 141 | U => 'u'; 142 | #= Unusual (css: text-decoration-line: underline) 143 | 144 | given $node.type { 145 | when any(%basic-html.keys) { 146 | return q{<} ~ %basic-html{$_} ~ q{>} 147 | ~ self.node2inline($node.contents) 148 | ~ q{</} ~ %basic-html{$_} ~ q{>}; 149 | } 150 | 151 | # Escape 152 | when 'E' { 153 | return $node.meta.map({ 154 | when Int { "&#$_;" } 155 | when Str { "&$_;" } 156 | }).join; 157 | } 158 | 159 | # Note 160 | when 'N' { 161 | $!renderer.footnotes.push(self.node2inline($node.contents)); 162 | my $id = +$!renderer.footnotes; 163 | return qq{<a href="#fn-$id" id="fn-ref-$id">[$id]</a>}; 164 | } 165 | 166 | # Links 167 | when 'L' { 168 | my $text = self.node2inline($node.contents); 169 | my $url = $node.meta[0] || node2text($node.contents); 170 | 171 | if $text ~~ /^'#'/ { 172 | # if we have an internal-only link, strip the # from the text. 173 | $text = $/.postmatch 174 | } 175 | if !$text ~~ /^\?/ { 176 | $url = unescape_html($url); 177 | } 178 | 179 | if $url ~~ /^'#'/ { 180 | $url = '#' ~ uri_escape(escape_id($/.postmatch)) 181 | } 182 | 183 | return qq[<a href="{ $!renderer.url($url) }">{ $text }</a>] 184 | } 185 | 186 | # zero-width comment 187 | when 'Z' { 188 | return ''; 189 | } 190 | 191 | when 'D' { 192 | # TODO memorise these definitions (in $node.meta) and display them properly 193 | my $text = self.node2inline($node.contents); 194 | return qq[<defn>{ $text }</defn>] 195 | } 196 | 197 | when 'X' { 198 | multi sub recurse-until-str(Str:D $s) { $s } 199 | multi sub recurse-until-str(Pod::Block $n) { $n.contents>>.&recurse-until-str().join } 200 | 201 | my $index-text = recurse-until-str($node).join; 202 | my @indices = $node.meta; 203 | my $index-name-attr = 204 | qq[index-entry{ @indices ?? '-' !! '' }{ @indices.join('-') }{ $index-text ?? '-' !! '' }$index-text] 205 | .subst('_', '__', :g).subst(' ', '_', :g); 206 | $index-name-attr = $!renderer.url($index-name-attr).subst(/^\//, ''); 207 | my $text = self.node2inline($node.contents); 208 | $!renderer.crossrefs{$_} = $text for @indices; 209 | 210 | return qq[<a name="$index-name-attr"><span class="index-entry">$text\</span></a>] if $text; 211 | return qq[<a name="$index-name-attr"></a>]; 212 | } 213 | 214 | # Stuff I haven't figured out yet 215 | default { 216 | debug { note colored("missing handling for a formatting code of type ", "red") ~ $node.type } 217 | return qq{<kbd class="pod2html-todo">$node.type()<} 218 | ~ self.node2inline($node.contents) 219 | ~ q{></kbd>}; 220 | } 221 | } 222 | } 223 | 224 | multi method node2inline(Positional $node --> Str) { 225 | return $node.map({ self.node2inline($_) }).join; 226 | } 227 | 228 | multi method node2inline(Str $node --> Str) { 229 | return escape_html($node); 230 | } 231 | 232 | # block level or below 233 | proto method node2html(| --> Str) {*} 234 | multi method node2html($node) { 235 | debug { note colored("Generic node2html called for ", "bold") ~ $node.perl }; 236 | return self.node2inline($node); 237 | } 238 | 239 | multi method node2html(Pod::Block::Declarator $node) { 240 | given $node.WHEREFORE { 241 | when Routine { 242 | "<article>\n" 243 | ~ '<code class="pod-code-inline">' 244 | ~ node2text($node.WHEREFORE.name ~ $node.WHEREFORE.signature.perl) 245 | ~ "</code>:\n" 246 | ~ self.node2html($node.contents) 247 | ~ "\n</article>\n"; 248 | } 249 | default { 250 | debug { note "I don't know what { $node.WHEREFORE.WHAT.perl } is. Assuming class..." }; 251 | "<h1>" ~ self.node2html([$node.WHEREFORE.perl, q{: }, $node.contents]) ~ "</h1>"; 252 | } 253 | } 254 | } 255 | 256 | multi method node2html(Pod::Block::Code $node) { 257 | debug { note colored("Code node2html called for ", "bold") ~ $node.gist }; 258 | if %*POD2HTML-CALLBACKS and %*POD2HTML-CALLBACKS<code> -> &cb { 259 | return cb :$node, default => sub ($node) { 260 | return '<pre class="pod-block-code">' ~ self.node2inline($node.contents) ~ "</pre>\n" 261 | } 262 | } 263 | else { 264 | return '<pre class="pod-block-code">' ~ self.node2inline($node.contents) ~ "</pre>\n" 265 | } 266 | 267 | } 268 | 269 | multi method node2html(Pod::Block::Comment $node) { 270 | debug { note colored("Comment node2html called for ", "bold") ~ $node.gist }; 271 | return ''; 272 | } 273 | 274 | multi method node2html(Pod::Block::Named $node) { 275 | debug { note colored("Named Block node2html called for ", "bold") ~ $node.gist }; 276 | given $node.name { 277 | when 'config' { return '' } 278 | when 'nested' { 279 | return qq{<div class="nested">\n} ~ self.node2html($node.contents) ~ qq{\n</div>\n}; 280 | } 281 | when 'output' { return qq[<pre class="pod-block-named-outout">\n] ~ self.node2inline($node.contents) ~ "</pre>\n"; } 282 | when 'pod' { 283 | return qq[<span class="{ $node.config<class> }">\n{ self.node2html($node.contents) }</span>\n] 284 | if $node.config<class>; 285 | return self.node2html($node.contents); 286 | } 287 | when 'para' { return self.node2html($node.contents[0], |$node.config); } 288 | when 'Image' { 289 | my $url; 290 | if $node.contents == 1 { 291 | my $n = $node.contents[0]; 292 | if $n ~~ Str { 293 | $url = $n; 294 | } 295 | elsif ($n ~~ Pod::Block::Para) && $n.contents == 1 { 296 | $url = $n.contents[0] if $n.contents[0] ~~ Str; 297 | } 298 | } 299 | without $url { 300 | die "Found an Image block, but don't know how to extract the image URL :("; 301 | } 302 | return qq[<img src="$url" />]; 303 | } 304 | when 'Xhtml' | 'Html' { 305 | unescape_html ($node.contents.map({ node2rawtext $_ }).join) 306 | } 307 | default { 308 | # A named block, specifically a semantic block. 309 | # Semantic blocks (https://docs.raku.org/language/pod#Semantic_blocks) 310 | # are collected and supplied to templates as a poor man's YAML 311 | # metadata :-). 312 | if [and] 313 | $node.name.match(/^<[A..Z]>+$/), 314 | $node.contents.elems == 1, 315 | $node.contents.head ~~ Pod::Block::Para 316 | { 317 | $!renderer.metadata{$node.name.lc} = node2text($node.contents); 318 | return ''; 319 | } 320 | # any other named block 321 | else { 322 | return '<section>' 323 | ~ "<h1>{ $node.name }</h1>\n" 324 | ~ self.node2html($node.contents) 325 | ~ "</section>\n"; 326 | } 327 | } 328 | } 329 | } 330 | 331 | multi method node2html(Pod::Block::Para $node) { 332 | debug { note colored("Para node2html called for ", "bold") ~ $node.gist }; 333 | return '<p>' ~ self.node2inline($node.contents) ~ "</p>\n"; 334 | } 335 | 336 | multi method node2html(Pod::Block::Table $node) { 337 | debug { note colored("Table node2html called for ", "bold") ~ $node.gist }; 338 | my @r = $node.config<class> ?? '<table class="pod-table ' ~ $node.config<class> ~ '">'!!'<table class="pod-table">'; 339 | 340 | if $node.caption -> $c { 341 | @r.push("<caption>{ self.node2inline($c) }</caption>"); 342 | } 343 | 344 | if $node.headers { 345 | @r.push( 346 | '<thead><tr>', 347 | $node.headers.map(-> $cell { 348 | "<th>{ self.node2html($cell) }</th>" 349 | }), 350 | '</tr></thead>' 351 | ); 352 | } 353 | 354 | @r.push( 355 | '<tbody>', 356 | $node.contents.map(-> $line { 357 | '<tr>', 358 | $line.list.map(-> $cell { 359 | "<td>{ self.node2html($cell) }</td>" 360 | }), 361 | '</tr>' 362 | }), 363 | '</tbody>', 364 | '</table>' 365 | ); 366 | 367 | return @r.join("\n"); 368 | } 369 | 370 | multi method node2html(Pod::Config $node) { 371 | debug { note colored("Config node2html called for ", "bold") ~ $node.perl }; 372 | return ''; 373 | } 374 | 375 | multi method node2html(Pod::DefnList $node) { 376 | "<dl>\n" ~ self.node2html($node.contents) ~ "\n</dl>\n"; 377 | } 378 | multi method node2html(Pod::Defn $node) { 379 | "<dt>" ~ self.node2html($node.term) ~ "</dt>\n" ~ 380 | "<dd>" ~ self.node2html($node.contents) ~ "</dd>\n"; 381 | } 382 | 383 | # TODO: would like some way to wrap these and the following content in a <section>; this might be 384 | # the same way we get lists working... 385 | multi method node2html(Pod::Heading $node) { 386 | debug { note colored("Heading node2html called for ", "bold") ~ $node.gist }; 387 | my $lvl = min($node.level, 6); 388 | #= HTML only has 6 levels of numbered headings 389 | my %escaped = 390 | id => escape_id(node2rawtext($node.contents)), 391 | html => self.node2inline($node.contents); 392 | 393 | %escaped<uri> = uri_escape %escaped<id>; 394 | 395 | my $content; 396 | if (%escaped<html> ~~ m{href .+ \<\/a\>}) { 397 | $content = %escaped<html>; 398 | } else { 399 | $content = qq[<a class="u" href="#___top" title="go to top of document">] 400 | ~ %escaped<html> 401 | ~ qq[</a>]; 402 | } 403 | 404 | return "<h$lvl id={"\"%escaped<id>\""}>$content\</h$lvl>\n"; 405 | } 406 | 407 | # FIXME 408 | multi method node2html(Pod::List $node) { 409 | return '<ul>' ~ self.node2html($node.contents) ~ "</ul>\n"; 410 | } 411 | multi method node2html(Pod::Item $node) { 412 | debug { note colored("List Item node2html called for ", "bold") ~ $node.gist }; 413 | return '<li>' ~ self.node2html($node.contents) ~ "</li>\n"; 414 | } 415 | 416 | multi method node2html(Positional $node) { 417 | debug { note colored("Positional node2html called for ", "bold") ~ $node.gist }; 418 | return $node.map({ self.node2html($_) }).join 419 | } 420 | 421 | multi method node2html(Str $node) { 422 | return escape_html($node); 423 | } 424 | } 425 | 426 | sub pod2html($pod, *%nameds --> Str) is export { 427 | Pod::To::HTML.new(|%nameds).render($pod); 428 | } 429 | 430 | sub node2html(*@pos, *%nameds --> Str) is export { 431 | Node::To::HTML.new(:renderer(Pod::To::HTML.new)).node2html(|@pos, |%nameds); 432 | } 433 | 434 | proto render(|) is export {*} 435 | multi render(Any $pod, Str :$title, Str :$subtitle, Str :$lang, 436 | Str :$template = Str, Str :$main-template-path = '', *%template-vars) { 437 | die 'Can load only Pod::Block objects or an Array of such' unless $pod ~~ Pod::Block:D | Array:D; 438 | Pod::To::HTML.new(:$title, :$subtitle, :$lang, :$template, :$main-template-path, |%template-vars).render($pod); 439 | } 440 | multi render($file where Str | IO::Path, *%nameds) { render((load($file)), |%nameds) } 441 | 442 | # FIXME: this code's a horrible mess. It'd be really helpful to have a module providing a generic 443 | # way to walk a Pod tree and invoke callbacks on each node, that would reduce the multispaghetti at 444 | # the bottom to something much more readable. 445 | 446 | # see <https://docs.perl6.org/language/traps#Constants_are_Compile_Time> 447 | my $debug := %*ENV<P6DOC_debug>; 448 | 449 | sub debug(Callable $c) { $c() if $debug; } 450 | 451 | sub escape_html(Str $str --> Str) { 452 | return $str unless ($str ~~ /<[ & < > " ' {   ]>/) or ($str ~~ / ' ' /); 453 | $str.trans([q{&}, q{<}, q{>}, q{"}, q{'}, q{ }] => 454 | [q{&}, q{<}, q{>}, q{"}, q{'}, q{ }]); 455 | } 456 | 457 | sub unescape_html(Str $str --> Str) { 458 | $str.trans([rx{'&'}, rx{'<'}, rx{'>'}, rx{'"'}, rx{'''}] => 459 | [q{&}, q{<}, q{>}, q{"}, q{'}]); 460 | } 461 | 462 | sub escape_id ($id) is export { 463 | $id.trim.subst(/\s+/, '_', :g) 464 | .subst('"', '"', :g) 465 | .subst(' ', '_', :g) 466 | .subst(''', "'", :g); 467 | } 468 | 469 | multi visit(Nil, |a) { 470 | debug { note colored("visit called for Nil", "bold") } 471 | } 472 | 473 | multi visit($root, :&pre, :&post, :&assemble = -> *% { Nil }) { 474 | debug { note colored("visit called for ", "bold") ~ $root.perl } 475 | my ($pre, $post); 476 | $pre = pre($root) if defined ⪯ 477 | 478 | my @content = $root.?contents.map: { visit $_, :&pre, :&post, :&assemble }; 479 | $post = post($root, :@content) if defined &post; 480 | 481 | return assemble(:$pre, :$post, :@content, :node($root)); 482 | } 483 | 484 | #try require Term::ANSIColor <&colored>; 485 | #if &colored.defined { 486 | # &colored = -> $t, $c { $t }; 487 | #} 488 | 489 | sub colored($text, $how) { 490 | $text 491 | } 492 | 493 | sub assemble-list-items(:@content, :$node, *%) { 494 | my @newcont; 495 | my $found-one = False; 496 | my $ever-warn = False; 497 | 498 | my $at-level = 0; 499 | my @push-alias; 500 | 501 | my sub oodwarn($got, $want) { 502 | unless $ever-warn { 503 | warn "=item$got without preceding =item$want found!"; 504 | $ever-warn = True; 505 | } 506 | } 507 | 508 | for @content { 509 | when Pod::Item { 510 | $found-one = True; 511 | 512 | # here we deal with @newcont being empty (first list), or with the 513 | # last element not being a list (new list) 514 | unless +@newcont && @newcont[*-1] ~~ Pod::List { 515 | @newcont.push(Pod::List.new()); 516 | if $_.level > 1 { 517 | oodwarn($_.level, 1); 518 | } 519 | } 520 | 521 | # only bother doing the binding business if we're at a different 522 | # level than previous items 523 | if $_.level != $at-level { 524 | # guaranteed to be bound to a Pod::List (see above 'unless') 525 | @push-alias := @newcont[*-1].contents; 526 | 527 | for 2 .. ($_.level) -> $L { 528 | unless +@push-alias && @push-alias[*-1] ~~ Pod::List { 529 | @push-alias.push(Pod::List.new()); 530 | if +@push-alias == 1 { # we had to push a sublist to a list with no =items 531 | oodwarn($OUTER::_.level, $L); 532 | } 533 | } 534 | @push-alias := @push-alias[*-1].contents; 535 | } 536 | 537 | $at-level = $_.level; 538 | } 539 | 540 | @push-alias.push($_); 541 | } 542 | # This is simpler than lists because we don't need to 543 | # list 544 | when Pod::Defn { 545 | $found-one = True; 546 | unless +@newcont && @newcont[*-1] ~~ Pod::DefnList { 547 | @newcont.push(Pod::DefnList.new()); 548 | } 549 | @newcont[*-1].contents.push($_); 550 | } 551 | 552 | default { 553 | @newcont.push($_); 554 | $at-level = 0; 555 | } 556 | } 557 | 558 | return $found-one ?? $node.clone(contents => @newcont) !! $node; 559 | } 560 | 561 | sub retrieve-templates($template-path, $main-template-path --> List) { 562 | sub get-partials($template-path --> Hash) { 563 | my $partials-dir = 'partials'; 564 | my %partials; 565 | for dir($template-path.IO.add($partials-dir)) -> $partial { 566 | %partials{$partial.basename.subst(/\.mustache/, '')} = $partial.IO.slurp; 567 | } 568 | return %partials; 569 | } 570 | 571 | my $template-file = %?RESOURCES<templates/main.mustache>; 572 | my %partials; 573 | 574 | with $template-path { 575 | if "$template-path/main.mustache".IO ~~ :f { 576 | $template-file = $template-path.IO.add('main.mustache').IO; 577 | 578 | if $template-path.IO.add('partials') ~~ :d { 579 | %partials = get-partials($template-path); 580 | } 581 | } else { 582 | note "$template-path does not contain required templates. Using default."; 583 | } 584 | } 585 | 586 | if $main-template-path { 587 | $template-file = $main-template-path.IO; 588 | } 589 | 590 | return $template-file, %partials; 591 | } 592 | 593 | monitor TOC::Calculator { 594 | has @.levels is default(0) = 0; 595 | has $.pod is required; 596 | 597 | proto method find-headings($node, :$inside-heading) {*} 598 | 599 | multi method find-headings(Str $s is raw, :$inside-heading) { 600 | $inside-heading ?? { plain => $s.&escape_html } !! () 601 | } 602 | 603 | multi method find-headings(Pod::FormattingCode $node is raw where *.type eq 'C', :$inside-heading) { 604 | my $html = $node.contents.map({ self.find-headings($_, :$inside-heading) }).Array; 605 | $inside-heading ?? { coded => $html } !! () 606 | } 607 | 608 | multi method find-headings(Pod::Heading $node is raw, :$inside-heading) { 609 | @!levels.splice($node.level) if $node.level < +@!levels; 610 | @!levels[$node.level - 1]++; 611 | my $level-hierarchy = @!levels.join('.'); 612 | # e.g. §4.2.12 613 | my $text = $node.contents.map({ self.find-headings($_, :inside-heading) }).Array; 614 | my $link = escape_id(node2text($node.contents)); 615 | { level => $node.level, :$level-hierarchy, :$text, :$link } 616 | } 617 | 618 | multi method find-headings(Positional \list, :$inside-heading) { 619 | |list.map({ self.find-headings($_, :$inside-heading) }).Array; 620 | } 621 | 622 | multi method find-headings(Pod::Block $node is raw, :$inside-heading) { 623 | |$node.contents.map({ self.find-headings($_, :$inside-heading) }).Array 624 | } 625 | 626 | multi method find-headings(Pod::Config $node, :$inside-heading) { 627 | hash; 628 | } 629 | 630 | multi method find-headings(Pod::Raw $node is raw, :$inside-heading) { 631 | |$node.contents.map({ self.find-headings($_, :$inside-heading) }).Array 632 | } 633 | 634 | method calculate() { 635 | self.find-headings($!pod).grep(so *); 636 | } 637 | 638 | method render() { 639 | my $toc = self.calculate; 640 | my $result; 641 | for $toc<> -> $item { 642 | my $text = self.render-heading($item<text>); 643 | $result ~= "<tr class=\"toc-level-{ $item<level> }\"><td class=\"toc-number\">{ $item<level-hierarchy> }\</td><td class=\"toc-text\"><a href=\"#$item<link>\">{ $text }\</a></td></tr>\n"; 644 | } 645 | $result.?trim ?? 646 | qq:to/EOH/ 647 | <nav class="indexgroup"> 648 | <table id="TOC"> 649 | <caption><h2 id="TOC_Title">Table of Contents</h2></caption> 650 | { $result } 651 | </table> 652 | </nav> 653 | EOH 654 | !! '' 655 | } 656 | 657 | method render-heading($heading) { 658 | if $heading ~~ Positional { 659 | return $heading.map({ self.render-heading($_) }).join; 660 | } elsif $heading ~~ Associative { 661 | with $heading<plain> { 662 | return $_ 663 | } 664 | with $heading<coded> { 665 | return "<code class=\"pod-code-inline\">{ self.render-heading($_) }</code>" 666 | } 667 | } 668 | } 669 | } 670 | 671 | # HTML-escaped text 672 | sub node2text($node --> Str) { 673 | debug { note colored("Generic node2text called with ", "red") ~ $node.perl }; 674 | given $node { 675 | when Pod::Block::Para { node2text($node.contents) } 676 | when Pod::Raw { $node.target.?lc eqv 'html' ?? $node.contents.join !! '' } 677 | when Positional { $node.map(&node2text).join } 678 | default { escape_html(node2rawtext($node)) } 679 | } 680 | } 681 | 682 | # plain, unescaped text 683 | sub node2rawtext($node --> Str) is export { 684 | debug { note colored("Generic node2rawtext called with ", "red") ~ $node.raku }; 685 | given $node { 686 | when Pod::Block { node2rawtext($node.contents) } 687 | when Positional { $node.map(&node2rawtext).join } 688 | default { $node.Str } 689 | } 690 | } 691 | 692 | # vim: expandtab shiftwidth=4 ft=perl6 693 | -------------------------------------------------------------------------------- /resources/css/github.css: -------------------------------------------------------------------------------- 1 | hr, 2 | img { 3 | box-sizing: content-box 4 | } 5 | body::after, 6 | body::before, 7 | hr::after, 8 | hr::before { 9 | display: table; 10 | content: "" 11 | } 12 | a, 13 | a:not([href]) { 14 | text-decoration: none 15 | } 16 | 17 | hr, 18 | svg:not(:root) { 19 | overflow: hidden 20 | } 21 | 22 | img, 23 | table tr { 24 | background-color: #fff 25 | } 26 | 27 | pre, 28 | table { 29 | overflow: auto 30 | } 31 | 32 | dl, 33 | dl dt, 34 | hr, 35 | pre code, 36 | pre>code, 37 | td, 38 | th { 39 | padding: 0 40 | } 41 | 42 | input, 43 | pre code { 44 | overflow: visible 45 | } 46 | 47 | pre, 48 | pre code { 49 | word-wrap: normal 50 | } 51 | 52 | body { 53 | -ms-text-size-adjust: 100%; 54 | -webkit-text-size-adjust: 100%; 55 | color: #333; 56 | font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 57 | font-size: 16px; 58 | line-height: 1.5; 59 | word-wrap: break-word; 60 | width: 820px; 61 | margin: 2em auto; 62 | } 63 | 64 | a { 65 | background-color: transparent; 66 | -webkit-text-decoration-skip: objects; 67 | color: #4078c0 68 | } 69 | 70 | a:active, 71 | a:hover { 72 | outline-width: 0; 73 | text-decoration: underline 74 | } 75 | 76 | h1 { 77 | margin: .67em 0 78 | } 79 | 80 | img { 81 | border-style: none; 82 | max-width: 100% 83 | } 84 | 85 | h1, 86 | h2 { 87 | padding-bottom: .3em; 88 | border-bottom: 1px solid #eee 89 | } 90 | 91 | input { 92 | font: inherit; 93 | margin: 0; 94 | font-family: inherit; 95 | font-size: inherit; 96 | line-height: inherit 97 | } 98 | 99 | * { 100 | box-sizing: border-box 101 | } 102 | 103 | strong { 104 | font-weight: 600 105 | } 106 | 107 | body::after, 108 | hr::after { 109 | clear: both 110 | } 111 | 112 | table { 113 | border-spacing: 0; 114 | border-collapse: collapse; 115 | display: block; 116 | width: 100% 117 | } 118 | 119 | blockquote { 120 | margin: 0; 121 | padding: 0 1em; 122 | color: #777; 123 | border-left: .25em solid #ddd 124 | } 125 | 126 | ol ol, 127 | ul ol { 128 | list-style-type: lower-roman 129 | } 130 | 131 | ol ol ol, 132 | ol ul ol, 133 | ul ol ol, 134 | ul ul ol { 135 | list-style-type: lower-alpha 136 | } 137 | 138 | dd { 139 | margin-left: 0 140 | } 141 | 142 | code { 143 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace 144 | } 145 | 146 | pre { 147 | font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace 148 | } 149 | 150 | input { 151 | -webkit-font-feature-settings: "liga" 0; 152 | font-feature-settings: "liga" 0 153 | } 154 | 155 | body>:first-child { 156 | margin-top: 0!important 157 | } 158 | 159 | body>:last-child { 160 | margin-bottom: 0!important 161 | } 162 | 163 | a:not([href]) { 164 | color: inherit 165 | } 166 | 167 | blockquote, 168 | dl, 169 | ol, 170 | p, 171 | pre, 172 | table, 173 | ul { 174 | margin-top: 0; 175 | margin-bottom: 16px 176 | } 177 | 178 | hr { 179 | background: #e7e7e7; 180 | height: .25em; 181 | margin: 24px 0; 182 | border: 0 183 | } 184 | 185 | blockquote>:first-child { 186 | margin-top: 0 187 | } 188 | 189 | blockquote>:last-child { 190 | margin-bottom: 0 191 | } 192 | 193 | h1, 194 | h2, 195 | h3, 196 | h4, 197 | h5, 198 | h6 { 199 | margin-top: 24px; 200 | margin-bottom: 16px; 201 | font-weight: 600; 202 | line-height: 1.25 203 | } 204 | 205 | dl dt, 206 | table th { 207 | font-weight: 700 208 | } 209 | 210 | h1 code, 211 | h1 tt, 212 | h2 code, 213 | h2 tt, 214 | h3 code, 215 | h3 tt, 216 | h4 code, 217 | h4 tt, 218 | h5 code, 219 | h5 tt, 220 | h6 code, 221 | h6 tt { 222 | font-size: inherit 223 | } 224 | 225 | h1 { 226 | font-size: 2em 227 | } 228 | 229 | h2 { 230 | font-size: 1.5em 231 | } 232 | 233 | h3 { 234 | font-size: 1.25em 235 | } 236 | 237 | h4 { 238 | font-size: 1em 239 | } 240 | 241 | h5 { 242 | font-size: .875em 243 | } 244 | 245 | h6 { 246 | font-size: .85em; 247 | color: #777 248 | } 249 | 250 | ol, 251 | ul { 252 | padding-left: 2em 253 | } 254 | 255 | ol ol, 256 | ol ul, 257 | ul ol, 258 | ul ul { 259 | margin-top: 0; 260 | margin-bottom: 0 261 | } 262 | 263 | li>p { 264 | margin-top: 16px 265 | } 266 | 267 | li+li { 268 | margin-top: .25em 269 | } 270 | 271 | dl dt { 272 | margin-top: 16px; 273 | font-size: 1em; 274 | font-style: italic 275 | } 276 | 277 | dl dd { 278 | padding: 0 16px; 279 | margin-bottom: 16px 280 | } 281 | 282 | table td, 283 | table th { 284 | padding: 6px 13px; 285 | border: 1px solid #ddd 286 | } 287 | 288 | table tr { 289 | border-top: 1px solid #ccc 290 | } 291 | 292 | table tr:nth-child(2n) { 293 | background-color: #f8f8f8 294 | } 295 | 296 | code { 297 | padding: .2em 0; 298 | margin: 0; 299 | font-size: 85%; 300 | background-color: rgba(0, 0, 0, .04); 301 | border-radius: 3px 302 | } 303 | 304 | code::after, 305 | code::before { 306 | letter-spacing: -.2em; 307 | content: "\00a0" 308 | } 309 | 310 | pre>code { 311 | margin: 0; 312 | font-size: 100%; 313 | word-break: normal; 314 | white-space: pre; 315 | background: 0 0; 316 | border: 0 317 | } 318 | 319 | pre { 320 | padding: 16px; 321 | font-size: 85%; 322 | line-height: 1.45; 323 | background-color: #f7f7f7; 324 | border-radius: 3px 325 | } 326 | 327 | pre code { 328 | display: inline; 329 | max-width: auto; 330 | margin: 0; 331 | line-height: inherit; 332 | background-color: transparent; 333 | border: 0 334 | } 335 | 336 | pre code::after, 337 | pre code::before { 338 | content: normal 339 | } 340 | 341 | kbd { 342 | display: inline-block; 343 | padding: 3px 5px; 344 | font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace; 345 | line-height: 10px; 346 | color: #555; 347 | vertical-align: middle; 348 | background-color: #fcfcfc; 349 | border: 1px solid #ccc; 350 | border-bottom-color: #bbb; 351 | border-radius: 3px; 352 | box-shadow: inset 0 -1px 0 #bbb 353 | } 354 | 355 | hr { 356 | border-bottom-color: #eee 357 | } 358 | -------------------------------------------------------------------------------- /resources/examples/README.md: -------------------------------------------------------------------------------- 1 | # Example usage 2 | 3 | ## General script 4 | 5 | From this directory: 6 | 7 | RAKUDOLIB=../../lib raku render.raku 8 | 9 | which uses the module's local documentation and the 10 | default `main.mustache` template. 11 | 12 | To use the local `main.mustache` template, run: 13 | 14 | RAKUDOLIB=../../lib raku render.raku --template=. 15 | 16 | Run `raku render.raku -h` to output the script's help message. 17 | 18 | ## Specific script 19 | 20 | In the directory `with-partials`, you can find a script using 21 | `Pod::To::HTML` to generate HTML with a template that uses partials. 22 | -------------------------------------------------------------------------------- /resources/examples/main.mustache: -------------------------------------------------------------------------------- 1 | <!DOCTYPE HTML> 2 | <html lang="{{ lang }}"> 3 | <head> 4 | <title>{{{ title }}} 5 | 6 | 14 | 15 | {{#css}}{{/css}} 16 | 17 | 18 | 19 |
20 | {{#title}}

{{{ title }}}

{{/title}} 21 | {{#subtitle}}

{{{ subtitle }}}

{{/subtitle}} 22 | {{#toc}}{{{toc}}}{{/toc}} 23 | 24 |
25 | {{#body}}{{{ . }}}{{/body}} 26 |
27 | 28 | {{#footnotes}}{{{footnotes}}}{{/footnotes}} 29 | 30 | {{#footer}}{{{footer}}}{{/footer}} 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /resources/examples/render.raku: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env raku 2 | 3 | use v6; 4 | use MONKEY-SEE-NO-EVAL; 5 | use Pod::To::HTML; 6 | 7 | my %*SUB-MAIN-OPTS = :named-anywhere; 8 | 9 | sub MAIN( 10 | $file = "../../doc/Pod/To/HTML.rakudoc", #= Pod file to convert to HTML. 11 | Str :t(:$template), #= Path to Mustache template to render Pod file. 12 | ) { 13 | my $file-content = $file.IO.slurp; 14 | die "No pod here" if not $file-content ~~ /\=begin \s+ pod/; 15 | 16 | my $pod; 17 | try $pod = EVAL($file-content ~ "\n\$=pod"); 18 | die "Pod fails: $!" if $!; 19 | 20 | put render( 21 | $pod, 22 | :$template, 23 | ); 24 | } 25 | 26 | -------------------------------------------------------------------------------- /resources/examples/with-partials/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family:-apple-system,BlinkMacSystemFont,segoe ui,Roboto,Oxygen-Sans,Ubuntu,Cantarell,helvetica neue,sans-serif; 3 | line-height:1.6; 4 | display:flex; 5 | flex-direction:column; 6 | min-height:100vh; 7 | margin:0; 8 | padding:0 9 | } 10 | main { 11 | flex-grow:1 12 | } 13 | img { 14 | max-width:100%; 15 | border-radius:.2rem 16 | } 17 | pre { 18 | overflow-x:auto; 19 | border:.1rem solid #d3d3d3; 20 | padding:1rem 21 | } 22 | code { 23 | font-family:SFMono-Regular,Consolas,liberation mono,Menlo,Courier,monospace 24 | } 25 | .Banner { 26 | list-style:none; 27 | display:flex; 28 | flex-flow:row-reverse wrap-reverse; 29 | justify-content:space-between; 30 | margin:0; 31 | padding:0 32 | } 33 | .Banner-item:nth-child(1) { 34 | order:5 35 | } 36 | .Banner-item:nth-child(2) { 37 | order:4 38 | } 39 | .Banner-item:nth-child(3) { 40 | order:3 41 | } 42 | .Banner-item:nth-child(4) { 43 | order:2 44 | } 45 | .Banner-item:nth-child(5) { 46 | order:1 47 | } 48 | .Banner-item--title { 49 | flex-grow:1 50 | } 51 | .Banner-link { 52 | font-size:1.25rem; 53 | color:#fff; 54 | padding:.5rem 1rem 55 | } 56 | .Heading { 57 | display:flex; 58 | flex-wrap:wrap; 59 | justify-content:space-between; 60 | align-items:baseline 61 | } 62 | .Heading-title { 63 | margin:1.5rem .5rem 0 0 64 | } 65 | .Heading-link { 66 | color:inherit 67 | } 68 | .Tags { 69 | list-style:none; 70 | display:flex; 71 | flex-wrap:wrap; 72 | justify-content:center; 73 | margin:1.5rem 0; 74 | padding:0 75 | } 76 | .Tags-item { 77 | border-radius:.2rem; 78 | margin:.2rem; 79 | padding:0 .3rem 80 | } 81 | .Tags-link { 82 | color:#fff 83 | } 84 | .Divider { 85 | display:flex; 86 | justify-content:center 87 | } 88 | .Divider::after { 89 | content:"\a0" 90 | } 91 | .Pagination { 92 | font-size:1.25rem; 93 | color:inherit 94 | } 95 | .Pagination--right { 96 | float:right 97 | } 98 | .Footer { 99 | text-align:center; 100 | margin:1rem 0 101 | } 102 | .u-wrapper { 103 | max-width:42rem; 104 | margin:auto 105 | } 106 | .u-padding { 107 | padding:0 1rem 108 | } 109 | .u-background { 110 | background:teal 111 | } 112 | .u-clickable { 113 | font-weight:700; 114 | text-decoration:none; 115 | display:inline-block 116 | } 117 | 118 | -------------------------------------------------------------------------------- /resources/examples/with-partials/main.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{> meta }} 6 | 7 | 8 | 9 |
10 | 11 | {{> banner }} 12 |
13 |
14 |
15 |
16 | {{> heading }} 17 |
18 | {{#body}}{{{ . }}}{{/body}} 19 |
20 |
21 |
22 |
23 |
24 | 25 | {{> footer }} 26 | 27 | 28 | -------------------------------------------------------------------------------- /resources/examples/with-partials/markdown-guide.pod: -------------------------------------------------------------------------------- 1 | =begin pod 2 | =TITLE Markdown Syntax Guide 3 | =DATE 11 March, 2019 4 | 5 | This article offers a sample of basic Markdown syntax that can be used in Hugo 6 | content files, also it shows whether basic HTML elements are decorated with CSS 7 | in a Hugo theme. 8 | Headings 9 | 10 | The following HTML

elements represent six levels of section headings. 11 |

is the highest section level while

is the lowest. 12 | 13 | =head2 Paragraph 14 | 15 | Xerum, quo qui aut unt expliquam qui dolut labo. Aque venitatiusda cum, 16 | voluptionse latur sitiae dolessi aut parist aut dollo enim qui voluptate ma 17 | dolestendit peritin re plis aut quas inctum laceat est volestemque commosa as 18 | cus endigna tectur, offic to cor sequas etum rerum idem sintibus eiur? Quianimin 19 | porecus evelectur, cum que nis nust voloribus ratem aut omnimi, sitatur? 20 | Quiatem. Nam, omnis sum am facea corem alique molestrunt et eos evelece arcillit 21 | ut aut eos eos nus, sin conecerem erum fuga. Ri oditatquam, ad quibus unda 22 | veliamenimin cusam et facea ipsamus es exerum sitate dolores editium rerore 23 | eost, temped molorro ratiae volorro te reribus dolorer sperchicium faceata 24 | tiustia prat. 25 | 26 | =head2 Other Elements — abbr, sub, sup, kbd, mark 27 | 28 | GIF is a bitmap image format. 29 | 30 | H2O 31 | 32 | Xn + Yn = Zn 33 | 34 | Press CTRL+ALT+Delete to end the session. 35 | 36 | Most salamanders are nocturnal, and hunt for insects, worms, and other small 37 | creatures. 38 | =end pod 39 | -------------------------------------------------------------------------------- /resources/examples/with-partials/partials/banner.mustache: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /resources/examples/with-partials/partials/footer.mustache: -------------------------------------------------------------------------------- 1 | {{#footer}} 2 |
3 |
4 |
5 | {{{.}}} 6 |
7 |
8 |
9 | {{/footer}} 10 | -------------------------------------------------------------------------------- /resources/examples/with-partials/partials/head.mustache: -------------------------------------------------------------------------------- 1 | 2 | {{{title}}} 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /resources/examples/with-partials/partials/heading.mustache: -------------------------------------------------------------------------------- 1 |
2 |

3 | {{ title }} 4 |

5 | 6 | {{#date}} 7 | 8 | {{/date}} 9 |
10 | -------------------------------------------------------------------------------- /resources/examples/with-partials/partials/meta.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{#description}}{{/description}} 4 | {{#css}} 5 | 6 | {{/css}} 7 | 8 | -------------------------------------------------------------------------------- /resources/examples/with-partials/render.raku: -------------------------------------------------------------------------------- 1 | use v6; 2 | use Pod::To::HTML; 3 | 4 | put render( 5 | './markdown-guide.pod'.IO, 6 | template => '.', 7 | site-title => 'A page', 8 | css => $*CWD.add('css').dir.map(*.Str), 9 | menus => ( 10 | %(name => "About"), 11 | %(name => "Posts"), 12 | ), 13 | ); 14 | 15 | -------------------------------------------------------------------------------- /resources/templates/main.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{{title}}} 5 | 6 | 7 | 8 | {{#css}}{{/css}} 9 | 10 | 11 | 12 |
13 | {{#toc}}{{{toc}}}{{/toc}} 14 | 15 |
16 | {{#body}}{{{ . }}}{{/body}} 17 |
18 | 19 | {{#footnotes}}{{{footnotes}}}{{/footnotes}} 20 | 21 | 22 | -------------------------------------------------------------------------------- /t/010-basic.t: -------------------------------------------------------------------------------- 1 | use Test; # -*- mode: perl6 -*- 2 | use Pod::To::HTML; 3 | plan 3; 4 | 5 | # XXX Need a module to walk HTML trees 6 | 7 | =begin foo 8 | =end foo 9 | 10 | my $r = node2html $=pod[0]; 11 | ok $r ~~ ms/'
' '

' foo '

' '
' /; 12 | 13 | =begin foo 14 | some text 15 | =end foo 16 | 17 | $r = node2html $=pod[1]; 18 | ok $r ~~ ms/'
' '

' foo '

' '

' some text '

' '
'/; 19 | 20 | =head1 Talking about Perl 6 21 | 22 | if $*PERL.compiler.name eq 'rakudo' 23 | and $*PERL.compiler.version before v2018.06 { 24 | skip "Your rakudo is too old for this test. Need 2018.06 or newer"; 25 | } 26 | else { 27 | $r = node2html $=pod[2]; 28 | nok $r ~~ m:s/Perl 6/, "no-break space is not converted to other space"; 29 | } 30 | -------------------------------------------------------------------------------- /t/011-external.t: -------------------------------------------------------------------------------- 1 | use Test; # -*- mode: perl6 -*- 2 | use Pod::To::HTML; 3 | plan 9; 4 | 5 | use MONKEY-SEE-NO-EVAL; 6 | 7 | for -> $base { 8 | test-files( $base ~ ".pod6" ); 9 | } 10 | 11 | sub test-files( $possible-file-path ) { 12 | 13 | my $example-path = $possible-file-path.IO.e??$possible-file-path!!"t/$possible-file-path"; 14 | 15 | my $a-pod = $example-path.IO.slurp; 16 | my $rendered= render($example-path.IO); 17 | unlike( $rendered, /Pod\:\:To/, "Is not prepending class name" ); 18 | my $pod = (EVAL ($a-pod ~ "\n\$=pod")); # use proved pod2onebigpage method 19 | my $r = node2html $pod; 20 | ok( $r, "Converting external" ); 21 | unlike( $r, /Pod\:\:To/, "Is not prepending class name" ); 22 | $r = pod2html($pod, :header(''), :footer(''), :head(''), :default-title(''), :lang('en')); 23 | unlike( $r, /Pod\:\:To/, "Is not prepending class name" ); 24 | } 25 | 26 | my $class = q:to/EOC/; 27 | #| Hello! 28 | class XYZ {} 29 | EOC 30 | my $rendered= render($class); 31 | unlike( $rendered, /Pod\:\:To/, "Is not prepending class name" ); 32 | -------------------------------------------------------------------------------- /t/012-multi.t: -------------------------------------------------------------------------------- 1 | use Test; 2 | use Pod::To::HTML; 3 | use Pod::Load; 4 | 5 | my $test-pod-path = $?FILE.IO.sibling('multi.pod6'); 6 | 7 | dies-ok { render(42) }, 'Cannot render an Int'; 8 | 9 | like render($test-pod-path), /magicians/, 'Is rendering the whole file by path Str'; 10 | 11 | like render(slurp $test-pod-path), /magicians/, 'Is rendering the whole file by text'; 12 | 13 | like render([load($test-pod-path)]), /magicians/, 'Is rendering an Array'; 14 | 15 | like render(load($test-pod-path)), /magicians/, 'Is rendering a Pod::Block'; 16 | 17 | done-testing; -------------------------------------------------------------------------------- /t/020-code.t: -------------------------------------------------------------------------------- 1 | use Test; 2 | use Pod::To::HTML; 3 | plan 3; 4 | my $r; 5 | 6 | =begin pod 7 | This ordinary paragraph introduces a code block: 8 | 9 | $this = 1 * code('block'); 10 | $which.is_specified(:by); 11 | =end pod 12 | 13 | $r = node2html $=pod[0]; 14 | ok $r ~~ ms[[ 15 | '

' 'This ordinary paragraph introduces a code block:' '

' 16 | '
$this = 1 * code('block');'
17 | '$which.is_specified(:by<indenting>);
']]; 18 | 19 | =begin pod 20 | This is an ordinary paragraph 21 | 22 | While this is not 23 | This is a code block 24 | 25 | =head1 Mumble: "mumble" 26 | 27 | Suprisingly, this is not a code block 28 | (with fancy indentation too) 29 | 30 | But this is just a text. Again 31 | 32 | =end pod 33 | 34 | $r = node2html $=pod[1]; 35 | ok $r ~~ ms[['

' 'This is an ordinary paragraph' '

' 36 | '
While this is not'
37 | 'This is a code block
' 38 | '

' '' 39 | 'Mumble: "mumble"' 40 | '' '

' 41 | '

' 'Suprisingly, this is not a code block (with fancy indentation too)' '

' 42 | '

' 'But this is just a text. Again' '

']]; 43 | 44 | my %*POD2HTML-CALLBACKS = code => sub (:$node, :&default) { 45 | ok $node.contents ~~ /:i code/, 'Callback called'; 46 | } 47 | 48 | # say $=pod[0].perl; 49 | pod2html $=pod[0]; 50 | -------------------------------------------------------------------------------- /t/030-comment.t: -------------------------------------------------------------------------------- 1 | use Test; 2 | use Pod::To::HTML; 3 | plan 1; 4 | my $r; 5 | 6 | =begin pod 7 | =for comment 8 | foo foo not rendered 9 | bla bla bla 10 | 11 | This isn't a comment 12 | =end pod 13 | 14 | $r = node2html $=pod[0]; 15 | ok $r ~~ ms/ ^ '

' 'This isn't a comment' '

' $ /; 16 | -------------------------------------------------------------------------------- /t/040-lists.t: -------------------------------------------------------------------------------- 1 | use Test; 2 | use Pod::To::HTML; 3 | plan 3; 4 | my $r; 5 | 6 | =begin pod 7 | The seven suspects are: 8 | 9 | =item Happy 10 | =item Dopey 11 | =item Sleepy 12 | =item Bashful 13 | =item Sneezy 14 | =item Grumpy 15 | =item Keyser Soze 16 | =end pod 17 | 18 | $r = pod2html $=pod[0]; 19 | ok $r ~~ ms[[ 20 | '

' 'The seven suspects are:' '

' 21 | '
    ' 22 | '
  • ' '

    ' Happy '

    ' '
  • ' 23 | '
  • ' '

    ' Dopey '

    ' '
  • ' 24 | '
  • ' '

    ' Sleepy '

    ' '
  • ' 25 | '
  • ' '

    ' Bashful '

    ' '
  • ' 26 | '
  • ' '

    ' Sneezy '

    ' '
  • ' 27 | '
  • ' '

    ' Grumpy '

    ' '
  • ' 28 | '
  • ' '

    ' Keyser Soze '

    ' '
  • ' 29 | '
' 30 | ]]; 31 | 32 | =begin pod 33 | =item1 Animal 34 | =item2 Vertebrate 35 | =item2 Invertebrate 36 | 37 | =item1 Phase 38 | =item2 Solid 39 | =item2 Liquid 40 | =item2 Gas 41 | =item2 Chocolate 42 | =end pod 43 | 44 | $r = pod2html $=pod[1]; 45 | ok $r ~~ ms[[ 46 | '
    ' 47 | '
  • ' '

    ' Animal '

    ' '
  • ' 48 | '
      ' 49 | '
    • ' '

      ' Vertebrate '

      ' '
    • ' 50 | '
    • ' '

      ' Invertebrate '

      ' '
    • ' 51 | '
    ' 52 | '
  • ' '

    ' Phase '

    ' '
  • ' 53 | '
      ' 54 | '
    • ' '

      ' Solid '

      ' '
    • ' 55 | '
    • ' '

      ' Liquid '

      ' '
    • ' 56 | '
    • ' '

      ' Gas '

      ' '
    • ' 57 | '
    • ' '

      ' Chocolate '

      ' '
    • ' 58 | '
    ' 59 | '
' 60 | ]]; 61 | 62 | =begin pod 63 | =comment CORRECT... 64 | =begin item1 65 | The choices are: 66 | =end item1 67 | =item2 Liberty 68 | =item2 Death 69 | =item2 Beer 70 | =end pod 71 | 72 | $r = pod2html $=pod[2]; 73 | ok $r ~~ ms[[ 74 | '
    ' 75 | '
  • ' '

    ' 'The choices are:' '

    ' '
  • ' 76 | '
      ' 77 | '
    • ' '

      ' Liberty '

      ' '
    • ' 78 | '
    • ' '

      ' Death '

      ' '
    • ' 79 | '
    • ' '

      ' Beer '

      ' '
    • ' 80 | '
    ' 81 | '
' 82 | ]]; 83 | -------------------------------------------------------------------------------- /t/050-format-x-index.t: -------------------------------------------------------------------------------- 1 | use Test; 2 | use Pod::To::HTML; 3 | plan 3; 4 | 5 | =begin pod 6 | X<|behavior> L 7 | =end pod 8 | 9 | my $r = pod2html $=pod, :url({ $_ }); 10 | ok $r ~~ m/'href="http://www.doesnt.get.rendered.com"'/; 11 | 12 | =begin pod 13 | 14 | When indexing X the X is used. 15 | 16 | It is possible to index X in repeated places. 17 | =end pod 18 | 19 | $r = node2html $=pod[1]; 20 | 21 | like $r, / 22 | 'When indexing' 23 | \s* '' 24 | \s* 'an item' 25 | .+ 'the' 26 | .+ 'X format' 27 | .+ 'to index' 28 | .+ 'an item' 29 | .+ 'in repeated places.' 30 | /, 'X format in text'; 31 | 32 | =begin pod 33 | 34 | When indexing X another text can be used for the index. 35 | 36 | It is possible to index Xwith hierarchical levels. 37 | 38 | And then index the X with different index entries. 39 | =end pod 40 | 41 | $r = node2html $=pod[2]; 42 | like $r, / 43 | 'When indexing ' 44 | .* 'an item' 45 | .+ 'to index ' .+ 'index-entry-defining__a_term-hierarchical_items' .+ 'hierarchical items' 46 | .+ 'index the ' .+ '>same place' 47 | /, 'Text with indexed items correct'; 48 | -------------------------------------------------------------------------------- /t/060-table.t: -------------------------------------------------------------------------------- 1 | use Test; # -*- mode: perl6 -*- 2 | use Pod::To::HTML; 3 | 4 | plan 4; 5 | 6 | my $r; 7 | 8 | =table 9 | col1 col2 10 | 11 | $r = pod2html $=pod[0]; 12 | #say $r.perl; 13 | ok $r ~~ ms[[ 14 | '' 15 | '' 16 | '' 17 | '' 18 | '' 19 | '' 20 | '' 21 | '
' col1 '' col2 '
' 22 | ]]; 23 | 24 | =table 25 | H1 H2 26 | -- -- 27 | col1 col2 28 | 29 | $r = pod2html $=pod[1]; 30 | #say $r.perl; 31 | ok $r ~~ ms[[ 32 | '' 33 | '' 34 | '' 35 | '' 36 | '' 37 | '' 38 | '' 39 | '' 40 | '' 41 | '' 42 | '' 43 | '' 44 | '' 45 | '
' H1 '' H2 '
' col1 '' col2 '
' 46 | ]]; 47 | 48 | 49 | =begin table :class 50 | 51 | H1 H2 52 | -- -- 53 | col1 col2 54 | 55 | col1 col2 56 | 57 | =end table 58 | 59 | $r = pod2html $=pod[2]; 60 | #say $r.perl; 61 | ok $r ~~ ms[[ 62 | '' 63 | '' 64 | '' 65 | '' 66 | '' 67 | '' 68 | '' 69 | '' 70 | '' 71 | '' 72 | '' 73 | '' 74 | '' 75 | '' 76 | '' 77 | '' 78 | '' 79 | '
' H1 '' H2 '
' col1 '' col2 '
' col1 '' col2 '
' 80 | ]]; 81 | 82 | =begin table :caption 83 | 84 | H1 H2 85 | -- -- 86 | col1 col2 87 | 88 | =end table 89 | 90 | $r = pod2html $=pod[3]; 91 | # say $r; 92 | ok $r ~~ ms[[ 93 | '' 94 | '' 95 | '' 96 | '' 97 | '' 98 | '' 99 | '' 100 | '' 101 | '' 102 | '' 103 | '' 104 | '' 105 | '' 106 | '' 107 | '
' 'Test Caption' '
' H1 '' H2 '
' col1 '' col2 '
' 108 | ]]; 109 | -------------------------------------------------------------------------------- /t/070-headings.t: -------------------------------------------------------------------------------- 1 | use v6; 2 | use Test; 3 | use Pod::To::HTML; 4 | 5 | plan 5; 6 | 7 | =begin pod 8 | 9 | =head1 Heading 1 10 | 11 | =head2 Heading 1.1 12 | 13 | =head2 Heading 1.2 14 | 15 | =head1 Heading 2 16 | 17 | =head2 Heading 2.1 18 | 19 | =head2 Heading 2.2 20 | 21 | =head2 L<(Exception) method message|/routine/message#class_Exception> 22 | 23 | =head3 Heading 2.2.1 24 | 25 | =head3 X 2.2.2 26 | 27 | =head1 Heading C<3> 28 | 29 | =end pod 30 | 31 | my $html = pod2html $=pod; 32 | 33 | ($html ~~ m:g/ ('2.2.2') /); 34 | 35 | ok so ($0 && $1 && $2), 'hierarchical numbering'; 36 | 37 | ($html ~~ m:g/ 'href="#Heading_3"' /); 38 | 39 | ok so $0, 'link down to heading'; 40 | 41 | ($html ~~ m:g/ ('name="index-entry-Heading"') /); 42 | 43 | ok so ($0 || $1), 'no X<> anchors in ToC'; 44 | 45 | ($html ~~ m:g/ ('
Heading 1') /); 46 | 47 | ok so $0, 'Proper rendering of heading'; 48 | 49 | ($html ~~ m:g/ ('

Heading 3

') /); 50 | 51 | ok so $0, 'Proper rendering of heading from multiple nodes'; -------------------------------------------------------------------------------- /t/075-defn.t: -------------------------------------------------------------------------------- 1 | #!perl6 2 | 3 | use Test; 4 | 5 | # do NOT move this below `Pod::To::HTML` line, the module exports a fake Pod::Defn 6 | constant no-pod-defn = ::('Pod::Defn') ~~ Failure; 7 | 8 | use Pod::To::HTML; 9 | 10 | plan :skip-all if no-pod-defn; 11 | plan 1; 12 | 13 | =begin pod 14 | 15 | =defn MAD 16 | Affected with a high degree of intellectual independence. 17 | 18 | =defn MEEKNESS 19 | Uncommon patience in planning a revenge that is worth while. 20 | 21 | =defn MORAL 22 | Conforming to a local and mutable standard of right. 23 | Having the quality of general expediency. 24 | 25 | =end pod 26 | 27 | 28 | my $html = pod2html($=pod[0]); 29 | 30 | ok $html ~~ ms[[ 31 | '
' 32 | '
MAD
' 33 | '

Affected with a high degree of intellectual independence.

' 34 | '
' 35 | '
MEEKNESS
' 36 | '

Uncommon patience in planning a revenge that is worth while.

' 37 | '
' 38 | '
MORAL
' 39 | '

Conforming to a local and mutable standard of right. Having the quality of general expediency.

' 40 | '
' 41 | 42 | '
' 43 | ]], 'generated html for =defn'; 44 | -------------------------------------------------------------------------------- /t/080-lang.t: -------------------------------------------------------------------------------- 1 | use v6; 2 | use Test; 3 | use Pod::To::HTML; 4 | 5 | plan 2; 6 | 7 | =begin pod 8 | 9 | Je suis Napoleon! 10 | 11 | =end pod 12 | 13 | like pod2html($=pod), /''/, 'default lang is English'; 14 | like pod2html($=pod, :lang), /''/, 'custom lang'; 15 | -------------------------------------------------------------------------------- /t/090-css.t: -------------------------------------------------------------------------------- 1 | use v6; 2 | use Test; 3 | use Pod::To::HTML; 4 | 5 | plan 3; 6 | 7 | =begin pod 8 | 9 | Je suis Napoleon! 10 | 11 | =end pod 12 | 13 | like pod2html($=pod, :css('https://design.raku.org/perl.css')), 14 | /', :css('')), 17 | /'), 21 | /', :meta[], :config{}, :contents["Array"] 16 | ), 'Array', 'no crash in node2html with L<>'; 17 | -------------------------------------------------------------------------------- /t/110-issue-41.t: -------------------------------------------------------------------------------- 1 | use Test; 2 | use Pod::To::HTML; 3 | plan 2; 4 | 5 | =begin pod 6 | =head2 Rendering Perl 6 with no-break space 7 | 8 | Nothing to see here 9 | 10 | =head2 What's wrong with this rendering? 11 | 12 | Nothing to see here either 13 | =end pod 14 | 15 | my $r = pod2html $=pod; 16 | 17 | ok $r ~~ m/\#What\'s_wrong/; 18 | ok $r ~~ m/\#Rendering_Perl_6/; 19 | -------------------------------------------------------------------------------- /t/111-perl6-doc-issue-2270.t: -------------------------------------------------------------------------------- 1 | use Test; 2 | use Pod::To::HTML; 3 | plan 1; 4 | 5 | my $question-mark = Pod::FormattingCode.new( 6 | type => 'L', 7 | contents => ["?"], 8 | meta => ["%3F"], 9 | ); 10 | 11 | ok pod2html($question-mark) ~~ /\%3F/; 12 | 13 | 14 | -------------------------------------------------------------------------------- /t/120-templates.t: -------------------------------------------------------------------------------- 1 | use Test; # -*- mode: perl6 -*- 2 | use Test::Output; 3 | use Pod::To::HTML; 4 | plan 6; 5 | my $r; 6 | 7 | =begin pod 8 | =TITLE The usual suspects 9 | 10 | The seven suspects are: 11 | 12 | =item Happy 13 | =item Dopey 14 | =item Sleepy 15 | =item Bashful 16 | =item Sneezy 17 | =item Grumpy 18 | =item Keyser Soze 19 | =end pod 20 | 21 | stderr-like { $r = pod2html $=pod[0], :templates }, 22 | /'does not contain required templates'/, 23 | 'Complains when required templates not found'; 24 | 25 | ok $r ~~ ms[[ 26 | '

' 'The seven suspects are:' '

' 27 | '
    ' 28 | '
  • ' '

    ' Happy '

    ' '
  • ' 29 | '
  • ' '

    ' Dopey '

    ' '
  • ' 30 | '
  • ' '

    ' Sleepy '

    ' '
  • ' 31 | '
  • ' '

    ' Bashful '

    ' '
  • ' 32 | '
  • ' '

    ' Sneezy '

    ' '
  • ' 33 | '
  • ' '

    ' Grumpy '

    ' '
  • ' 34 | '
  • ' '

    ' Keyser Soze '

    ' '
  • ' 35 | '
' 36 | ]], 'Uses default templates'; 37 | 38 | $r = pod2html $=pod[0], :templates("t/templates"); 39 | ok $r ~~ ms[[ '' ]], 'Gets text from new template'; 40 | ok $r ~~ ms[[ "

The usual suspects

" ]], 'Fills template correctly'; 41 | 42 | my $head=''; 43 | $r = pod2html $=pod[0], :templates("t/templates"), :$head ; 44 | ok $r ~~ ms[[ $head ]], 'headers are redered as is'; 45 | 46 | class Pod::To::HTML::Dummy does Pod::To::HTML::Renderer { 47 | method render(%context) { 48 | "Rendered %context"; 49 | } 50 | } 51 | 52 | $r = pod2html $=pod[0], :page-renderer(Pod::To::HTML::Dummy); 53 | 54 | is $r, 'Rendered The usual suspects', 'Custom renderer was used'; -------------------------------------------------------------------------------- /t/130-links.t: -------------------------------------------------------------------------------- 1 | use Pod::To::HTML; 2 | use URI::Escape; 3 | use Test; 4 | 5 | plan 3; 6 | 7 | my $link-html = ""; 8 | 9 | subtest 'internal-only links' => { 10 | my $link-html = node2html(create-link-pod("", "#internal-only")); 11 | is get-display-text($link-html), 12 | "internal-only", 13 | "Strip # from the text if internal-only link"; 14 | } 15 | 16 | subtest 'Do not escape special chars if not internal url' => { 17 | for <q{&} q{<} q{>} q{'}> -> $char { 18 | $link-html = node2html(create-link-pod("/routine/$char", "random text")); 19 | is get-href-content($link-html), 20 | "/routine/$char", 21 | "$char not escaped from url"; 22 | } 23 | } 24 | 25 | subtest 'Escape special chars if internal url' => { 26 | for <& < > '> -> $char { 27 | $link-html = node2html(create-link-pod("#$char", "random text")); 28 | is get-href-content($link-html), 29 | "#" ~ uri_escape($char), 30 | "$char escaped from url"; 31 | } 32 | } 33 | 34 | # helpers 35 | 36 | sub create-link-pod($url, $contents) { 37 | Pod::FormattingCode.new( 38 | type => "L", 39 | meta => [$url], 40 | contents => [$contents] 41 | ) 42 | } 43 | 44 | sub get-href-content($html) { 45 | my $content; 46 | $html ~~ /href\=\"$<cm>=<-["]>+\"/; 47 | return $<cm>.Str; 48 | } 49 | 50 | sub get-display-text($html) { 51 | my $content; 52 | $html ~~ /\>$<cm>=<-["]>+\</; 53 | return $<cm>.Str; 54 | } 55 | -------------------------------------------------------------------------------- /t/140-config.t: -------------------------------------------------------------------------------- 1 | use Pod::To::HTML; 2 | use Test; 3 | 4 | =begin pod 5 | 6 | =begin para :property<cool> 7 | Test text! 8 | =end para 9 | 10 | =end pod 11 | 12 | class Node::To::HTML::Custom is Node::To::HTML { 13 | multi method node2html(Pod::Block::Para $node, *%config --> Str) { 14 | with %config<property> { 15 | "Nice, $_ render!"; 16 | } else { 17 | "A bug appeared..."; 18 | } 19 | } 20 | } 21 | 22 | like Pod::To::HTML.new(node-renderer => Node::To::HTML::Custom).render($=pod), 23 | /'Nice, cool render!'/, 'Config in a paragraph was understood'; 24 | 25 | done-testing; 26 | -------------------------------------------------------------------------------- /t/class.pod6: -------------------------------------------------------------------------------- 1 | #| Hello! 2 | class XYZ {} -------------------------------------------------------------------------------- /t/multi.pod6: -------------------------------------------------------------------------------- 1 | =begin pod 2 | 3 | Pod 6, determined heuristically. 4 | 5 | =end pod 6 | 7 | #| Base class for magicians 8 | class Magician { 9 | has Int $.level; 10 | has Str @.spells; 11 | } 12 | 13 | #| Fight mechanics 14 | sub duel(Magician $a, Magician $b) { 15 | } 16 | #= Magicians only, no mortals. 17 | -------------------------------------------------------------------------------- /t/templates/main.mustache: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html lang="{{ lang }}"> 3 | <head> 4 | <title>{{ title }} 5 | 6 | 7 | 17 | {{# css }}{{/ css }} 18 | {{# metadata }}{{{ metadata }}}{{/ metadata }} 19 | {{{ head }}} 20 | 21 | 22 |
23 | {{{ header }}} 24 | {{# title }}

{{ title }}

{{/ title }} 25 | {{# subtitle }}

{{ subtitle }}

{{/ subtitle }} 26 | {{# toc }}{{{ toc }}}{{/ toc }} 27 |
28 | {{# body }}{{{ . }}}{{/ body }} 29 |
30 | {{# footnotes }}{{{ footnotes }}}{{/ footnotes }} 31 | {{{ footer }}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /t/test.pod6: -------------------------------------------------------------------------------- 1 | =begin pod 2 | 3 | Pod 6, indicated by extension. 4 | 5 | =end pod 6 | 7 | #| Base class for magicians 8 | class Magician { 9 | has Int $.level; 10 | has Str @.spells; 11 | } 12 | 13 | #| Fight mechanics 14 | sub duel(Magician $a, Magician $b) { 15 | } 16 | #= Magicians only, no mortals. 17 | --------------------------------------------------------------------------------