aboutsummaryrefslogtreecommitdiff
path: root/lib/propmangle.js
blob: 3c75cac959c0145aaaf48ce34cae8155fd5118a3 (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
/***********************************************************************

  A JavaScript tokenizer / parser / beautifier / compressor.
  https://github.com/mishoo/UglifyJS2

  -------------------------------- (C) ---------------------------------

                           Author: Mihai Bazon
                         <mihai.bazon@gmail.com>
                       http://mihai.bazon.net/blog

  Distributed under the BSD license:

    Copyright 2012 (c) Mihai Bazon <mihai.bazon@gmail.com>

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions
    are met:

        * Redistributions of source code must retain the above
          copyright notice, this list of conditions and the following
          disclaimer.

        * Redistributions in binary form must reproduce the above
          copyright notice, this list of conditions and the following
          disclaimer in the documentation and/or other materials
          provided with the distribution.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER “AS IS” AND ANY
    EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
    PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE
    LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
    OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
    PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
    PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
    TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
    THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
    SUCH DAMAGE.

 ***********************************************************************/

"use strict";

function find_builtins() {
    var a = [];
    [ Object, Array, Function, Number,
      String, Boolean, Error, Math,
      Date, RegExp
    ].forEach(function(ctor){
        Object.getOwnPropertyNames(ctor).map(add);
        if (ctor.prototype) {
            Object.getOwnPropertyNames(ctor.prototype).map(add);
        }
    });
    function add(name) {
        push_uniq(a, name);
    }
    return a;
}

function mangle_properties(ast, options) {
    options = defaults(options, {
        reserved : null,
        cache : null,
        only_cache : false,
        regex : null,
        ignore_quoted : false,
        debug : false
    });

    var reserved = options.reserved;
    if (reserved == null)
        reserved = find_builtins();

    var cache = options.cache;
    if (cache == null) {
        cache = {
            cname: -1,
            props: new Dictionary()
        };
    }

    var regex = options.regex;
    var ignore_quoted = options.ignore_quoted;

    // note debug is either false (disabled), or a string of the debug suffix to use (enabled).
    // note debug may be enabled as an empty string, which is falsey. Also treat passing 'true'
    // the same as passing an empty string.
    var debug = (options.debug !== false);
    var debug_name_suffix;
    if (debug) {
        debug_name_suffix = (options.debug === true ? "" : options.debug);
    }

    var names_to_mangle = [];
    var unmangleable = [];
    var ignored = {};

    // step 1: find candidates to mangle
    ast.walk(new TreeWalker(function(node){
        if (node instanceof AST_ObjectKeyVal) {
            add(node.key, ignore_quoted && node.quote);
        }
        else if (node instanceof AST_ObjectProperty) {
            // setter or getter, since KeyVal is handled above
            add(node.key.name);
        }
        else if (node instanceof AST_Dot) {
            add(node.property);
        }
        else if (node instanceof AST_Sub) {
            addStrings(node.property, ignore_quoted);
        }
    }));

    // step 2: transform the tree, renaming properties
    return ast.transform(new TreeTransformer(function(node){
        if (node instanceof AST_ObjectKeyVal) {
            if (!(ignore_quoted && node.quote))
                node.key = mangle(node.key);
        }
        else if (node instanceof AST_ObjectProperty) {
            // setter or getter
            node.key.name = mangle(node.key.name);
        }
        else if (node instanceof AST_Dot) {
            node.property = mangle(node.property);
        }
        else if (node instanceof AST_Sub) {
            if (!ignore_quoted)
                node.property = mangleStrings(node.property);
        }
        // else if (node instanceof AST_String) {
        //     if (should_mangle(node.value)) {
        //         AST_Node.warn(
        //             "Found \"{prop}\" property candidate for mangling in an arbitrary string [{file}:{line},{col}]", {
        //                 file : node.start.file,
        //                 line : node.start.line,
        //                 col  : node.start.col,
        //                 prop : node.value
        //             }
        //         );
        //     }
        // }
    }));

    // only function declarations after this line

    function can_mangle(name) {
        if (!is_identifier(name)) return false;
        if (unmangleable.indexOf(name) >= 0) return false;
        if (reserved.indexOf(name) >= 0) return false;
        if (options.only_cache) {
            return cache.props.has(name);
        }
        if (/^[0-9.]+$/.test(name)) return false;
        return true;
    }

    function should_mangle(name) {
        if (ignore_quoted && name in ignored) return false;
        if (regex && !regex.test(name)) return false;
        if (reserved.indexOf(name) >= 0) return false;
        return cache.props.has(name)
            || names_to_mangle.indexOf(name) >= 0;
    }

    function add(name, ignore) {
        if (ignore) {
            ignored[name] = true;
            return;
        }

        if (can_mangle(name))
            push_uniq(names_to_mangle, name);

        if (!should_mangle(name)) {
            push_uniq(unmangleable, name);
        }
    }

    function mangle(name) {
        if (!should_mangle(name)) {
            return name;
        }

        var mangled = cache.props.get(name);
        if (!mangled) {
            if (debug) {
                // debug mode: use a prefix and suffix to preserve readability, e.g. o.foo -> o._$foo$NNN_.
                var debug_mangled = "_$" + name + "$" + debug_name_suffix + "_";

                if (can_mangle(debug_mangled) && !(ignore_quoted && debug_mangled in ignored)) {
                    mangled = debug_mangled;
                }
            }

            // either debug mode is off, or it is on and we could not use the mangled name
            if (!mangled) {
                // note can_mangle() does not check if the name collides with the 'ignored' set
                // (filled with quoted properties when ignore_quoted set). Make sure we add this
                // check so we don't collide with a quoted name.
                do {
                    mangled = base54(++cache.cname);
                } while (!can_mangle(mangled) || (ignore_quoted && mangled in ignored));
            }

            cache.props.set(name, mangled);
        }
        return mangled;
    }

    function addStrings(node, ignore) {
        var out = {};
        try {
            (function walk(node){
                node.walk(new TreeWalker(function(node){
                    if (node instanceof AST_Seq) {
                        walk(node.cdr);
                        return true;
                    }
                    if (node instanceof AST_String) {
                        add(node.value, ignore);
                        return true;
                    }
                    if (node instanceof AST_Conditional) {
                        walk(node.consequent);
                        walk(node.alternative);
                        return true;
                    }
                    throw out;
                }));
            })(node);
        } catch(ex) {
            if (ex !== out) throw ex;
        }
    }

    function mangleStrings(node) {
        return node.transform(new TreeTransformer(function(node){
            if (node instanceof AST_Seq) {
                node.cdr = mangleStrings(node.cdr);
            }
            else if (node instanceof AST_String) {
                node.value = mangle(node.value);
            }
            else if (node instanceof AST_Conditional) {
                node.consequent = mangleStrings(node.consequent);
                node.alternative = mangleStrings(node.alternative);
            }
            return node;
        }));
    }

}
(values (reverse result) "" '()))))) (define (highlights->sxml* highlights anchors) ;; Like 'highlights->sxml', but handle nested 'paren tags. This ;; allows for paren matching highlights via appropriate CSS ;; "hover" properties. When a symbol is encountered, look it up ;; in ANCHORS, a vhash, and emit the corresponding href, if any. (define (tag->class tag) (string-append "syntax-" (symbol->string tag))) (map (match-lambda ((? string? str) str) (('paren level open (body ...) close) `(span (@ (class ,(string-append "syntax-paren" (number->string level)))) ,open (span (@ (class "syntax-symbol")) ,@(highlights->sxml* body anchors)) ,close)) (('symbol text) ;; Check whether we can emit a hyperlink for TEXT. (match (vhash-assoc text anchors) (#f `(span (@ (class ,(tag->class 'symbol))) ,text)) ((_ . target) `(a (@ (class ,(tag->class 'symbol)) (href ,target)) ,text)))) ((tag text) `(span (@ (class ,(tag->class tag))) ,text))) highlights)) (define entity->string (match-lambda ("rArr" "⇒") ("rarr" "→") ("hellip" "…") ("rsquo" "’") ("nbsp" " ") (e (pk 'unknown-entity e) (primitive-exit 2)))) (define (concatenate-snippets pieces) ;; Concatenate PIECES, which contains strings and entities, ;; replacing entities with their corresponding string. (let loop ((pieces pieces) (strings '())) (match pieces (() (string-concatenate-reverse strings)) (((? string? str) . rest) (loop rest (cons str strings))) ((('*ENTITY* "additional" entity) . rest) (loop rest (cons (entity->string entity) strings))) ((('span _ lst ...) . rest) ;for <span class="roman"> (loop (append lst rest) strings)) ((('var name) . rest) ;for @var{name} within @lisp (loop rest (cons name strings))) ;XXX: losing formatting (something (pk 'unsupported-code-snippet something) (primitive-exit 1))))) (define (highlight-definition id category symbol args) ;; Produce stylable HTML for the given definition (an @deftp, ;; @deffn, or similar). `(dt (@ (id ,id) (class "symbol-definition")) (span (@ (class "symbol-definition-category")) ,@category) (span (@ (class "symbol-definition-prototype")) ,symbol " " ,@args))) (define (space? obj) (and (string? obj) (string-every char-set:whitespace obj))) (define (syntax-highlight sxml anchors) ;; Recurse over SXML and syntax-highlight code snippets. (let loop ((sxml sxml)) (match sxml (('*TOP* decl body ...) `(*TOP* ,decl ,@(map loop body))) (('head things ...) `(head ,@things (link (@ (rel "stylesheet") (type "text/css") (href #$syntax-css-url))))) (('pre ('@ ('class "lisp")) code-snippet ...) `(pre (@ (class "lisp")) ,@(highlights->sxml* (pair-open/close (highlight lex-scheme (concatenate-snippets code-snippet))) anchors))) ;; Replace the ugly <strong> used for @deffn etc., which ;; translate to <dt>, with more stylable markup. (('dt ('@ ('id id)) ;raw Texinfo 6.8 ('span ('@ ('class "category")) category ...) ('span ('strong thing) anchor)) (highlight-definition id category thing '())) (('dt (@ ('id id)) ('span ('@ ('class "category")) category ...) ('span ('strong thing) (? space?) ('em args ...) anchor)) (highlight-definition id category thing args)) ((tag ('@ attributes ...) body ...) `(,tag (@ ,@attributes) ,@(map loop body))) ((tag body ...) `(,tag ,@(map loop body))) ((? string? str) str)))) (define (process-html file anchors) ;; Parse FILE and perform syntax highlighting for its Scheme ;; snippets. Install the result to #$output. (format (current-error-port) "processing ~a...~%" file) (let* ((shtml (call-with-input-file file html->shtml)) (highlighted (syntax-highlight shtml anchors)) (base (string-drop file (string-length #$input))) (target (string-append #$output base))) (mkdir-p (dirname target)) (call-with-output-file target (lambda (port) (write-shtml-as-html highlighted port))))) (define (copy-as-is file) ;; Copy FILE as is to #$output. (let* ((base (string-drop file (string-length #$input))) (target (string-append #$output base))) (mkdir-p (dirname target)) (catch 'system-error (lambda () (if (eq? 'symlink (stat:type (lstat file))) (symlink (readlink file) target) (link file target))) (lambda args (let ((errno (system-error-errno args))) (pk 'error-link file target (strerror errno)) (primitive-exit 3)))))) (define (html? file stat) (string-suffix? ".html" file)) (define language+node-anchors (match-lambda ((language files ...) (cons language (fold (lambda (file vhash) (let ((alist (call-with-input-file file read))) ;; Use 'fold-right' so that the first entry ;; wins (e.g., "car" from "Pairs" rather than ;; from "rnrs base" in the Guile manual). (fold-right (match-lambda* (((key . value) vhash) (vhash-cons key value vhash))) vhash alist))) vlist-null files))))) (define mono-node-anchors ;; List of language/vhash pairs, where each vhash maps an ;; identifier to the corresponding URL in a single-page manual. (map language+node-anchors '#$mono-node-indexes)) (define multi-node-anchors ;; Likewise for split-node manuals. (map language+node-anchors '#$split-node-indexes)) ;; Install a UTF-8 locale so we can process UTF-8 files. (setenv "GUIX_LOCPATH" #+(file-append glibc-utf8-locales "/lib/locale")) (setlocale LC_ALL "en_US.utf8") ;; First process the mono-node 'guix.html' files. (for-each (match-lambda ((language . anchors) (let ((files (find-files (string-append #$input "/" language) "^guix(-cookbook|)(\\.[a-zA-Z_-]+)?\\.html$"))) (n-par-for-each (parallel-job-count) (cut process-html <> anchors) files)))) mono-node-anchors) ;; Process the multi-node HTML files. (for-each (match-lambda ((language . anchors) (let ((files (find-files (string-append #$input "/" language "/html_node") "\\.html$"))) (n-par-for-each (parallel-job-count) (cut process-html <> anchors) files)))) multi-node-anchors) ;; Last, copy non-HTML files as is. (for-each copy-as-is (find-files #$input (negate html?))))))) (computed-file name build)) (define* (stylized-html source input #:key (languages %languages) (manual %manual) (manual-css-url "/themes/initial/css/manual.css")) "Process all the HTML files in INPUT; add them MANUAL-CSS-URL as a <style> link, and add a menu to choose among LANGUAGES. Use the Guix PO files found in SOURCE." (define build (with-extensions (list guile-lib) (with-imported-modules `((guix build utils) ((localization) => ,(localization-helper-module source languages))) #~(begin (use-modules (htmlprag) (localization) (guix build utils) (srfi srfi-1) (ice-9 match) (ice-9 threads)) (define* (menu-dropdown #:key (label "Item") (url "#") (items '())) ;; Return an SHTML <li> element representing a dropdown for the ;; navbar. LABEL is the text of the dropdown menu, and ITEMS is ;; the list of items in this menu. (define id "visible-dropdown") `(li (@ (class "navbar-menu-item dropdown dropdown-btn")) (input (@ (class "navbar-menu-hidden-input") (type "radio") (name "dropdown") (id ,id))) (label (@ (for ,id)) ,label) (label (@ (for "all-dropdowns-hidden")) ,label) (div (@ (class "navbar-submenu") (id "navbar-submenu")) (div (@ (class "navbar-submenu-triangle")) " ") (ul ,@items)))) (define (menu-item label url) ;; Return an SHTML <li> element for a menu item with the given ;; LABEL and URL. `(li (a (@ (class "navbar-menu-item") (href ,url)) ,label))) (define* (navigation-bar menus #:key split-node?) ;; Return the navigation bar showing all of MENUS. `(header (@ (class "navbar")) (h1 (a (@ (class "branding") (href ,(if split-node? ".." "#"))))) (nav (@ (class "navbar-menu")) (input (@ (class "navbar-menu-hidden-input") (type "radio") (name "dropdown") (id "all-dropdowns-hidden"))) (ul ,@menus)) ;; This is the button that shows up on small screen in ;; lieu of the drop-down button. (a (@ (class "navbar-menu-btn") (href ,(if split-node? "../.." "..")))))) (define* (base-language-url code manual #:key split-node?) ;; Return the base URL of MANUAL for language CODE. (if split-node? (string-append "../../" (normalize code) "/html_node") (string-append "../" (normalize code) "/" manual (if (string=? code "en") "" (string-append "." code)) ".html"))) (define (language-menu-items file) ;; Return the language menu items to be inserted in FILE. (define split-node? (string-contains file "/html_node/")) (append (map (lambda (code) (menu-item (language-code->native-name code) (base-language-url code #$manual #:split-node? split-node?))) '#$%languages) (list (menu-item "⊕" (if (string=? #$manual "guix-cookbook") "https://translate.fedoraproject.org/projects/guix/documentation-cookbook/" "https://translate.fedoraproject.org/projects/guix/documentation-manual/"))))) (define (stylized-html sxml file) ;; Return SXML, which was read from FILE, with additional ;; styling. (define split-node? (string-contains file "/html_node/")) (let loop ((sxml sxml)) (match sxml (('*TOP* decl body ...) `(*TOP* ,decl ,@(map loop body))) (('head elements ...) ;; Add reference to our own manual CSS, which provides ;; support for the language menu. `(head ,@elements (link (@ (rel "stylesheet") (type "text/css") (href #$manual-css-url))))) (('body ('@ attributes ...) elements ...) `(body (@ ,@attributes) ,(navigation-bar ;; TODO: Add "Contribute" menu, to report ;; errors, etc. (list (menu-dropdown #:label `(img (@ (alt "Language") (src "/themes/initial/img/language-picker.svg"))) #:items (language-menu-items file))) #:split-node? split-node?) ,@elements)) ((tag ('@ attributes ...) body ...) `(,tag (@ ,@attributes) ,@(map loop body))) ((tag body ...) `(,tag ,@(map loop body))) ((? string? str) str)))) (define (process-html file) ;; Parse FILE and add links to translations. Install the result ;; to #$output. (format (current-error-port) "processing ~a...~%" file) (let* ((shtml (parameterize ((%strict-tokenizer? #t)) (call-with-input-file file html->shtml))) (processed (stylized-html shtml file)) (base (string-drop file (string-length #$input))) (target (string-append #$output base))) (mkdir-p (dirname target)) (call-with-output-file target (lambda (port) (write-shtml-as-html processed port))))) ;; Install a UTF-8 locale so we can process UTF-8 files. (setenv "GUIX_LOCPATH" #+(file-append glibc-utf8-locales "/lib/locale")) (setlocale LC_ALL "en_US.utf8") (setenv "LC_ALL" "en_US.utf8") (setvbuf (current-error-port) 'line) (n-par-for-each (parallel-job-count) (lambda (file) (if (string-suffix? ".html" file) (process-html file) ;; Copy FILE as is to #$output. (let* ((base (string-drop file (string-length #$input))) (target (string-append #$output base))) (mkdir-p (dirname target)) (if (eq? 'symlink (stat:type (lstat file))) (symlink (readlink file) target) (copy-file file target))))) (find-files #$input)))))) (computed-file "stylized-html-manual" build)) (define* (html-manual source #:key (languages %languages) (version "0.0") (manual %manual) (mono-node-indexes (map list languages)) (split-node-indexes (map list languages)) (date 1) (options %makeinfo-html-options)) "Return the HTML manuals built from SOURCE for all LANGUAGES, with the given makeinfo OPTIONS." (define manual-source (texinfo-manual-source source #:version version #:languages languages #:date date)) (define images (texinfo-manual-images source)) (define build (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils) (ice-9 match)) (define (normalize language) ;; Normalize LANGUAGE. For instance, "zh_CN" becomes "zh-cn". (string-map (match-lambda (#\_ #\-) (chr chr)) (string-downcase language))) (define (language->texi-file-name language) (if (string=? language "en") (string-append #$manual-source "/" #$manual ".texi") (string-append #$manual-source "/" #$manual "." language ".texi"))) ;; Install a UTF-8 locale so that 'makeinfo' is at ease. (setenv "GUIX_LOCPATH" #+(file-append glibc-utf8-locales "/lib/locale")) (setenv "LC_ALL" "en_US.utf8") (setvbuf (current-output-port) 'line) (setvbuf (current-error-port) 'line) ;; 'makeinfo' looks for "htmlxref.cnf" in the current directory, so ;; copy it right here. (copy-file (string-append #$manual-source "/htmlxref.cnf") "htmlxref.cnf") (for-each (lambda (language) (let* ((texi (language->texi-file-name language)) (opts `("--html" "-c" ,(string-append "TOP_NODE_UP_URL=/manual/" language) #$@options ,texi))) (format #t "building HTML manual for language '~a'...~%" language) (mkdir-p (string-append #$output "/" (normalize language))) (setenv "LANGUAGE" language) (apply invoke #$(file-append texinfo "/bin/makeinfo") "-o" (string-append #$output "/" (normalize language) "/html_node") opts) (apply invoke #$(file-append texinfo "/bin/makeinfo") "--no-split" "-o" (string-append #$output "/" (normalize language) "/" #$manual (if (string=? language "en") "" (string-append "." language)) ".html") opts) ;; Make sure images are available. (symlink #$images (string-append #$output "/" (normalize language) "/images")) (symlink #$images (string-append #$output "/" (normalize language) "/html_node/images")))) (filter (compose file-exists? language->texi-file-name) '#$languages))))) (let* ((name (string-append manual "-html-manual")) (manual* (computed-file name build #:local-build? #f))) (syntax-highlighted-html (stylized-html source manual* #:languages languages #:manual manual) #:mono-node-indexes mono-node-indexes #:split-node-indexes split-node-indexes #:name (string-append name "-highlighted")))) (define* (pdf-manual source #:key (languages %languages) (version "0.0") (manual %manual) (date 1) (options '())) "Return the HTML manuals built from SOURCE for all LANGUAGES, with the given makeinfo OPTIONS." (define manual-source (texinfo-manual-source source #:version version #:languages languages #:date date)) (define texinfo-profile (profile (content (packages->manifest ;; texi2dvi requires various command line tools. (list coreutils diffutils gawk grep sed tar texinfo (texlive-updmap.cfg (list texlive-epsf texlive-texinfo))))))) (define build (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils) (srfi srfi-34) (ice-9 match)) (define (normalize language) ;XXX: deduplicate ;; Normalize LANGUAGE. For instance, "zh_CN" becomes "zh-cn". (string-map (match-lambda (#\_ #\-) (chr chr)) (string-downcase language))) ;; Install a UTF-8 locale so that 'makeinfo' is at ease. (setenv "GUIX_LOCPATH" #+(file-append glibc-utf8-locales "/lib/locale")) (setenv "LC_ALL" "en_US.utf8") (setenv "PATH" #+(file-append texinfo-profile "/bin")) (setenv "GUIX_TEXMF" #+(file-append texinfo-profile "/share/texmf-dist")) (setvbuf (current-output-port) 'line) (setvbuf (current-error-port) 'line) (setenv "HOME" (getcwd)) ;for kpathsea/mktextfm ;; 'SOURCE_DATE_EPOCH' is honored by pdftex. (setenv "SOURCE_DATE_EPOCH" "1") (for-each (lambda (language) (let ((opts `("--pdf" "-I" "." #$@options ,(if (string=? language "en") (string-append #$manual-source "/" #$manual ".texi") (string-append #$manual-source "/" #$manual "." language ".texi"))))) (format #t "building PDF manual for language '~a'...~%" language) (mkdir-p (string-append #$output "/" (normalize language))) (setenv "LANGUAGE" language) ;; FIXME: Unfortunately building PDFs for non-Latin ;; alphabets doesn't work: ;; <https://lists.gnu.org/archive/html/help-texinfo/2012-01/msg00014.html>. (guard (c ((invoke-error? c) (format (current-error-port) "~%~%Failed to produce \ PDF for language '~a'!~%~%" language))) (apply invoke #$(file-append texinfo "/bin/makeinfo") "--pdf" "-o" (string-append #$output "/" (normalize language) "/" #$manual (if (string=? language "en") "" (string-append "." language)) ".pdf") opts)))) '#$languages)))) (computed-file (string-append manual "-pdf-manual") build #:local-build? #f)) (define* (guix-manual-text-domain source #:optional (languages %manual-languages)) "Return the PO files for LANGUAGES of the 'guix-manual' text domain taken from SOURCE." (define po-directory (file-append* source "/po/doc")) (define build (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils)) (mkdir-p #$output) (for-each (lambda (language) (define directory (string-append #$output "/" language "/LC_MESSAGES")) (mkdir-p directory) (invoke #+(file-append gnu-gettext "/bin/msgfmt") "-c" "-o" (string-append directory "/guix-manual.mo") (string-append #$po-directory "/guix-manual." language ".po"))) '#$(delete "en" languages))))) (computed-file "guix-manual-po" build)) (define* (localization-helper-module source #:optional (languages %languages)) "Return a file-like object for use as the (localization) module. SOURCE must be the Guix top-level source directory, from which PO files are taken." (define content (with-extensions (list guile-json-3) #~(begin (define-module (localization) #:use-module (json) #:use-module (srfi srfi-1) #:use-module (srfi srfi-19) #:use-module (ice-9 match) #:use-module (ice-9 popen) #:export (normalize with-language translate language-code->name language-code->native-name seconds->string)) (define (normalize language) ;XXX: deduplicate ;; Normalize LANGUAGE. For instance, "zh_CN" becomes "zh-cn". (string-map (match-lambda (#\_ #\-) (chr chr)) (string-downcase language))) (define-syntax-rule (with-language language exp ...) (let ((lang (getenv "LANGUAGE"))) (dynamic-wind (lambda () (setenv "LANGUAGE" language) (setlocale LC_MESSAGES)) (lambda () exp ...) (lambda () (if lang (setenv "LANGUAGE" lang) (unsetenv "LANGUAGE")) (setlocale LC_MESSAGES))))) ;; (put 'with-language 'scheme-indent-function 1) (define* (translate str language #:key (domain "guix-manual")) (define exp `(begin (bindtextdomain "guix-manual" #+(guix-manual-text-domain source)) (bindtextdomain "iso_639-3" ;language names #+(file-append iso-codes "/share/locale")) (setenv "LANGUAGE" ,language) (write (gettext ,str ,domain)))) ;; Since the 'gettext' function caches msgid translations, ;; regardless of $LANGUAGE, we have to spawn a new process each ;; time we want to translate to a different language. Bah! (let* ((pipe (open-pipe* OPEN_READ #+(file-append guile-3.0 "/bin/guile") "-c" (object->string exp))) (str (read pipe))) (close-pipe pipe) str)) (define %iso639-languages (vector->list (assoc-ref (call-with-input-file #+(file-append iso-codes "/share/iso-codes/json/iso_639-3.json") json->scm) "639-3"))) (define (language-code->name code) "Return the full name of a language from its ISO-639-3 code." (let ((code (match (string-index code #\_) (#f code) (index (string-take code index))))) (any (lambda (language) (and (string=? (or (assoc-ref language "alpha_2") (assoc-ref language "alpha_3")) code) (assoc-ref language "name"))) %iso639-languages))) (define (language-code->native-name code) "Return the name of language CODE in that language." (translate (language-code->name code) code #:domain "iso_639-3")) (define (seconds->string seconds language) (let* ((time (make-time time-utc 0 seconds)) (date (time-utc->date time))) (with-language language (date->string date "~e ~B ~Y"))))))) (scheme-file "localization.scm" content)) (define* (html-manual-indexes source #:key (languages %languages) (version "0.0") (manual %manual) (title (if (string=? "guix" manual) "GNU Guix Reference Manual" "GNU Guix Cookbook")) (date 1)) (define build (with-imported-modules `((guix build utils) ((localization) => ,(localization-helper-module source languages))) #~(begin (use-modules (guix build utils) (localization) (sxml simple) (srfi srfi-1)) (define (guix-url path) (string-append #$%web-site-url path)) (define (sxml-index language title body) ;; FIXME: Avoid duplicating styling info from guix-artwork.git. `(html (@ (lang ,language)) (head (title ,(string-append title " — GNU Guix")) (meta (@ (charset "UTF-8"))) (meta (@ (name "viewport") (content "width=device-width, initial-scale=1.0"))) ;; Menu prefetch. (link (@ (rel "prefetch") (href ,(guix-url "menu/index.html")))) ;; Base CSS. (link (@ (rel "stylesheet") (href ,(guix-url "themes/initial/css/elements.css")))) (link (@ (rel "stylesheet") (href ,(guix-url "themes/initial/css/common.css")))) (link (@ (rel "stylesheet") (href ,(guix-url "themes/initial/css/messages.css")))) (link (@ (rel "stylesheet") (href ,(guix-url "themes/initial/css/navbar.css")))) (link (@ (rel "stylesheet") (href ,(guix-url "themes/initial/css/breadcrumbs.css")))) (link (@ (rel "stylesheet") (href ,(guix-url "themes/initial/css/buttons.css")))) (link (@ (rel "stylesheet") (href ,(guix-url "themes/initial/css/footer.css")))) (link (@ (rel "stylesheet") (href ,(guix-url "themes/initial/css/page.css")))) (link (@ (rel "stylesheet") (href ,(guix-url "themes/initial/css/post.css"))))) (body (header (@ (class "navbar")) (h1 (a (@ (class "branding") (href #$%web-site-url))) (span (@ (class "a11y-offset")) "Guix")) (nav (@ (class "menu")))) (nav (@ (class "breadcrumbs")) (a (@ (class "crumb") (href #$%web-site-url)) "Home")) ,body (footer)))) (define (language-index language) (define title (translate #$title language)) (sxml-index language title `(main (article (@ (class "page centered-block limit-width")) (h2 ,title) (p (@ (class "post-metadata centered-text")) #$version " — " ,(seconds->string #$date language)) (div (ul (li (a (@ (href "html_node")) "HTML, with a separate page per node")) (li (a (@ (href ,(string-append #$manual (if (string=? language "en") "" (string-append "." language)) ".html"))) "HTML, entirely on one page")) ,@(if (member language '("ru" "zh_CN")) '() `((li (a (@ (href ,(string-append #$manual (if (string=? language "en") "" (string-append "." language)) ".pdf")))) "PDF"))))))))) (define (top-level-index languages) (define title #$title) (sxml-index "en" title `(main (article (@ (class "page centered-block limit-width")) (h2 ,title) (div "This document is available in the following languages:\n" (ul ,@(map (lambda (language) `(li (a (@ (href ,(normalize language))) ,(language-code->native-name language)))) languages))))))) (define (write-html file sxml) (call-with-output-file file (lambda (port) (display "<!DOCTYPE html>\n" port) (sxml->xml sxml port)))) (setenv "GUIX_LOCPATH" #+(file-append glibc-utf8-locales "/lib/locale")) (setenv "LC_ALL" "en_US.utf8") (setlocale LC_ALL "en_US.utf8") (for-each (lambda (language) (define directory (string-append #$output "/" (normalize language))) (mkdir-p directory) (write-html (string-append directory "/index.html") (language-index language))) '#$languages) (write-html (string-append #$output "/index.html") (top-level-index '#$languages))))) (computed-file "html-indexes" build)) (define* (pdf+html-manual source #:key (languages %languages) (version "0.0") (date (time-second (current-time time-utc))) (mono-node-indexes (map list %languages)) (split-node-indexes (map list %languages)) (manual %manual)) "Return the union of the HTML and PDF manuals, as well as the indexes." (directory-union (string-append manual "-manual") (map (lambda (proc) (proc source #:date date #:languages languages #:version version #:manual manual)) (list html-manual-indexes (lambda (source . args) (apply html-manual source #:mono-node-indexes mono-node-indexes #:split-node-indexes split-node-indexes args)) pdf-manual)) #:copy? #t)) (define (latest-commit+date directory) "Return two values: the last commit ID (a hex string) for DIRECTORY, and its commit date (an integer)." (let* ((repository (repository-open directory)) (head (repository-head repository)) (oid (reference-target head)) (commit (commit-lookup repository oid))) ;; TODO: Use (git describe) when it's widely available. (values (oid->string oid) (commit-time commit)))) ;;; ;;; Guile manual. ;;; (define guile-manual ;; The Guile manual as HTML, including both the mono-node "guile.html" and ;; the split-node "html_node" directory. (let ((guile guile-3.0-latest)) (computed-file (string-append "guile-manual-" (package-version guile)) (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils) (ice-9 match)) (setenv "PATH" (string-append #+tar "/bin:" #+xz "/bin:" #+zstd "/bin:" #+texinfo "/bin")) (invoke "tar" "xf" #$(package-source guile)) (mkdir-p (string-append #$output "/en/html_node")) (let* ((texi (find-files "." "^guile\\.texi$")) (documentation (match texi ((file) (dirname file))))) (with-directory-excursion documentation (invoke "makeinfo" "--html" "--no-split" "-o" (string-append #$output "/en/guile.html") "guile.texi") (invoke "makeinfo" "--html" "-o" "split" "guile.texi") (copy-recursively "split" (string-append #$output "/en/html_node"))))))))) (define %guile-manual-base-url "https://www.gnu.org/software/guile/manual") (define (for-all-languages index) (map (lambda (language) (list language index)) %languages)) (define guile-mono-node-indexes ;; The Guile manual is only available in English so use the same index in ;; all languages. (for-all-languages (html-manual-identifier-index (file-append guile-manual "/en") %guile-manual-base-url #:name "guile-html-index-en"))) (define guile-split-node-indexes (for-all-languages (html-manual-identifier-index (file-append guile-manual "/en/html_node") (string-append %guile-manual-base-url "/html_node") #:name "guile-html-index-en"))) (define (merge-index-alists alist1 alist2) "Merge ALIST1 and ALIST2, both of which are list of tuples like: (LANGUAGE INDEX1 INDEX2 ...) where LANGUAGE is a string like \"en\" and INDEX1 etc. are indexes as returned by 'html-identifier-indexes'." (let ((languages (delete-duplicates (append (match alist1 (((languages . _) ...) languages)) (match alist2 (((languages . _) ...) languages)))))) (map (lambda (language) (cons language (append (or (assoc-ref alist1 language) '()) (or (assoc-ref alist2 language) '())))) languages))) (let* ((root (canonicalize-path (string-append (current-source-directory) "/.."))) (commit date (latest-commit+date root)) (version (or (getenv "GUIX_MANUAL_VERSION") (string-take commit 7))) (select? (let ((vcs? (git-predicate root))) (lambda (file stat) (and (vcs? file stat) ;; Filter out this file. (not (string=? (basename file) "build.scm")))))) (source (local-file root "guix" #:recursive? #t #:select? select?))) (define guix-manual (html-manual source #:manual "guix" #:version version #:date date)) (define guix-mono-node-indexes ;; Alist of indexes for GUIX-MANUAL, where each key is a language code and ;; each value is a file-like object containing the identifier index. (html-identifier-indexes guix-manual "" #:manual-name "guix" #:base-url (if (string=? %manual "guix") (const "") (cut string-append "/manual/devel/" <>)) #:languages %languages)) (define guix-split-node-indexes ;; Likewise for the split-node variant of GUIX-MANUAL. (html-identifier-indexes guix-manual "/html_node" #:manual-name "guix" #:base-url (if (string=? %manual "guix") (const "") (cut string-append "/manual/devel/" <> "/html_node")) #:languages %languages)) (define mono-node-indexes (merge-index-alists guix-mono-node-indexes guile-mono-node-indexes)) (define split-node-indexes (merge-index-alists guix-split-node-indexes guile-split-node-indexes)) (format (current-error-port) "building manual from work tree around commit ~a, ~a~%" commit (let* ((time (make-time time-utc 0 date)) (date (time-utc->date time))) (date->string date "~e ~B ~Y"))) (pdf+html-manual source ;; Always use the identifier indexes of GUIX-MANUAL and ;; GUILE-MANUAL. Both "guix" and "guix-cookbook" can ;; contain links to definitions that appear in either of ;; these two manuals. #:mono-node-indexes mono-node-indexes #:split-node-indexes split-node-indexes #:version version #:date date))