Chiedi a Grunt di generare index.html per diverse configurazioni


208

Sto cercando di utilizzare Grunt come strumento di creazione per la mia webapp.

Voglio avere almeno due configurazioni:

I. Impostazione dello sviluppo : carica gli script da file separati, senza concatenazione,

quindi il mio index.html sarebbe simile a:

<!DOCTYPE html>
<html>
    <head>
        <script src="js/module1.js" />
        <script src="js/module2.js" />
        <script src="js/module3.js" />
        ...
    </head>
    <body></body>
</html>

II. Setup di produzione - carica i miei script minimizzati e concatenati in un file,

con index.html di conseguenza:

<!DOCTYPE html>
<html>
    <head>
        <script src="js/MyApp-all.min.js" />
    </head>
    <body></body>
</html>

La domanda è: come posso fare grunt per rendere questi index.html a seconda della configurazione quando corro grunt devo grunt prod?

O forse sto scavando nella direzione sbagliata e sarebbe più facile da generare sempre MyApp-all.min.jsma mettere al suo interno o tutti i miei script (concatenati) o uno script caricatore che carica in modo asincrono quegli script da file separati?

Come lo fate ragazzi?


3
Prova lo strumento Yeoman, che include un'attività "usemin" che fa quello che fai tu. Inoltre, i generatori Yeamon includono molte "buone pratiche" facili da imparare che sono difficili da imparare quando si utilizza un nuovo strumento.
EricSonaron,

Risposte:


161

Di recente ho scoperto queste v0.4.0attività compatibili con Grunt :

  • grugnito-preprocess

    Grunt task attorno al modulo npm di preelaborazione.

  • grugnito-ENV

    Grunt task per automatizzare la configurazione dell'ambiente per attività future.

Di seguito sono riportati frammenti dal mio Gruntfile.js.

Configurazione ENV:

env : {

    options : {

        /* Shared Options Hash */
        //globalOption : 'foo'

    },

    dev: {

        NODE_ENV : 'DEVELOPMENT'

    },

    prod : {

        NODE_ENV : 'PRODUCTION'

    }

},

preprocess:

preprocess : {

    dev : {

        src : './src/tmpl/index.html',
        dest : './dev/index.html'

    },

    prod : {

        src : './src/tmpl/index.html',
        dest : '../<%= pkg.version %>/<%= now %>/<%= ver %>/index.html',
        options : {

            context : {
                name : '<%= pkg.name %>',
                version : '<%= pkg.version %>',
                now : '<%= now %>',
                ver : '<%= ver %>'
            }

        }

    }

}

Compiti:

grunt.registerTask('default', ['jshint']);

grunt.registerTask('dev', ['jshint', 'env:dev', 'clean:dev', 'preprocess:dev']);

grunt.registerTask('prod', ['jshint', 'env:prod', 'clean:prod', 'uglify:prod', 'cssmin:prod', 'copy:prod', 'preprocess:prod']);

E nel /src/tmpl/index.htmlfile modello (ad esempio):

<!-- @if NODE_ENV == 'DEVELOPMENT' -->

    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.js"></script>
    <script src="../src/js/foo1.js"></script>
    <script src="../src/js/foo2.js"></script>
    <script src="../src/js/jquery.blah.js"></script>
    <script src="../src/js/jquery.billy.js"></script>
    <script src="../src/js/jquery.jenkins.js"></script>

<!-- @endif -->

<!-- @if NODE_ENV == 'PRODUCTION' -->

    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>

    <script src="http://cdn.foo.com/<!-- @echo name -->/<!-- @echo version -->/<!-- @echo now -->/<!-- @echo ver -->/js/<!-- @echo name -->.min.js"></script>

<!-- @endif -->

Sono sicuro che la mia configurazione è diversa dalla maggior parte delle persone e l'utilità di quanto sopra dipenderà dalla tua situazione. Per me, sebbene sia un fantastico pezzo di codice, il grugn-usemin di Yeoman è più robusto di quello di cui ho bisogno personalmente.

NOTA: ho appena scoperto le attività sopra elencate oggi, quindi potrei mancare una funzione e / o il mio processo potrebbe cambiare lungo la strada. Per ora, adoro la semplicità e le funzionalità che grunt-preprocess e grunt-env hanno da offrire. :)


Aggiornamento gennaio 2014:

Motivato da un voto negativo ...

Quando ho pubblicato questa risposta non c'erano molte opzioni per Grunt 0.4.xche offrivano una soluzione adatta alle mie esigenze. Ora, mesi dopo, immagino che ci siano più opzioni là fuori che potrebbero essere migliori di quelle che ho pubblicato qui. Mentre uso ancora personalmente e mi diverto a usare questa tecnica per le mie build , chiedo ai futuri lettori di leggere le altre risposte fornite e di ricercare tutte le opzioni. Se trovi una soluzione migliore, pubblica qui la tua risposta.

Aggiornamento febbraio 2014:

Non sono sicuro se sarà di alcun aiuto per nessuno, ma ho creato questo repository demo su GitHub che mostra una configurazione completa (e più complessa) usando le tecniche che ho descritto sopra.


Grazie, lo controllerò!
Dmitry Pashkevich,

3
La tua soluzione mi ha risparmiato ore di battere la testa contro un muro. Grazie.
Sthomps,

1
@sthomps Sono contento che abbia aiutato! Da quando ho scoperto quei compiti, ho amato il flusso di lavoro. Cordiali saluti, ho apportato una leggera modifica al processo ... Invece di passare diverse variabili di contesto ai miei modelli html, ho scelto di passare una var path : '/<%= pkg.name %>/dist/<%= pkg.version %>/<%= now %>/<%= ver %>'che concede tutti i var (questo è il mio percorso di compilazione). Sul mio modello avrò: <script src="http://cdn.foo.com<!-- @echo path -->/js/bulldog.min.js"></script>. Ad ogni modo, sono felice di essere riuscito a farti risparmiare un po 'di tempo! : D
mhulse,

4
Puoi fare la stessa cosa usando solo grunt-template , semplicemente passando un dataoggetto diverso per dev / prod.
Mathias Bynens,

2
Amico, adoro questa soluzione. È pulita, leggibile e non troppo ingegnerizzata.
Gaurang Patel,

34

Ho trovato la mia soluzione. Non ancora lucido, ma penso che mi sposterò in quella direzione.

In sostanza, sto usando grunt.template.process () per generare il mio index.htmlda un modello che analizza la configurazione corrente e produce un elenco dei miei file sorgente originali o collegamenti a un singolo file con codice minimizzato. L'esempio seguente è per i file js ma lo stesso approccio può essere esteso a css e qualsiasi altro possibile file di testo.

grunt.js:

/*global module:false*/
module.exports = function(grunt) {
    var   // js files
        jsFiles = [
              'src/module1.js',
              'src/module2.js',
              'src/module3.js',
              'src/awesome.js'
            ];

    // Import custom tasks (see index task below)
    grunt.loadTasks( "build/tasks" );

    // Project configuration.
    grunt.initConfig({
      pkg: '<json:package.json>',
      meta: {
        banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - ' +
          '<%= grunt.template.today("yyyy-mm-dd") %> */'
      },

      jsFiles: jsFiles,

      // file name for concatenated js
      concatJsFile: '<%= pkg.name %>-all.js',

      // file name for concatenated & minified js
      concatJsMinFile: '<%= pkg.name %>-all.min.js',

      concat: {
        dist: {
            src: ['<banner:meta.banner>'].concat(jsFiles),
            dest: 'dist/<%= concatJsFile %>'
        }
      },
      min: {
        dist: {
        src: ['<banner:meta.banner>', '<config:concat.dist.dest>'],
        dest: 'dist/<%= concatJsMinFile %>'
        }
      },
      lint: {
        files: ['grunt.js'].concat(jsFiles)
      },
      // options for index.html builder task
      index: {
        src: 'index.tmpl',  // source template file
        dest: 'index.html'  // destination file (usually index.html)
      }
    });


    // Development setup
    grunt.registerTask('dev', 'Development build', function() {
        // set some global flags that all tasks can access
        grunt.config('isDebug', true);
        grunt.config('isConcat', false);
        grunt.config('isMin', false);

        // run tasks
        grunt.task.run('lint index');
    });

    // Production setup
    grunt.registerTask('prod', 'Production build', function() {
        // set some global flags that all tasks can access
        grunt.config('isDebug', false);
        grunt.config('isConcat', true);
        grunt.config('isMin', true);

        // run tasks
        grunt.task.run('lint concat min index');
    });

    // Default task
    grunt.registerTask('default', 'dev');
};

index.js (the index task):

module.exports = function( grunt ) {
    grunt.registerTask( "index", "Generate index.html depending on configuration", function() {
        var conf = grunt.config('index'),
            tmpl = grunt.file.read(conf.src);

        grunt.file.write(conf.dest, grunt.template.process(tmpl));

        grunt.log.writeln('Generated \'' + conf.dest + '\' from \'' + conf.src + '\'');
    });
}

Infine, index.tmplcon la logica di generazione integrata in:

<doctype html>
<head>
<%
    var jsFiles = grunt.config('jsFiles'),
        isConcat = grunt.config('isConcat');

    if(isConcat) {
        print('<script type="text/javascript" src="' + grunt.config('concat.dist.dest') + '"></script>\n');
    } else {
        for(var i = 0, len = jsFiles.length; i < len; i++) {
            print('<script type="text/javascript" src="' + jsFiles[i] + '"></script>\n');
        }
    }
%>
</head>
<html>
</html>

UPD. Scoperto che Yeoman , che si basa su grugnito, ha un built-in usemin compito che si integra con il sistema di build di Yeoman. Genera una versione di produzione di index.html dalle informazioni nella versione di sviluppo di index.html e altre impostazioni dell'ambiente. Un po 'sofisticato ma interessante da vedere.


5
grunt-template è un involucro molto leggero intornogrunt.template.process()(che è quello che stai usando qui) che lo renderebbe ancora più semplice. Puoi fare la stessa cosa usando grunt-template semplicemente passando undataoggettodiversoper dev / prod.
Mathias Bynens,

15

Non mi piacciono le soluzioni qui (inclusa quella che ho precedentemente fornito ) ed ecco perché:

  • Il problema con la risposta più votata è che è necessario sincronizzare manualmente l'elenco dei tag di script quando si aggiunge / rinomina / elimina un file JS.
  • Il problema con la risposta accettata è che il tuo elenco di file JS non può avere una corrispondenza del modello. Questo significa che devi aggiornarlo manualmente nel Gruntfile.

Ho capito come risolvere entrambi questi problemi. Ho impostato il mio compito grunt in modo che ogni volta che un file viene aggiunto o eliminato, i tag di script vengono generati automaticamente per riflettere quello. In questo modo, non è necessario modificare il file html o il file grunt quando si aggiungono / rimuovono / rinominano i file JS.

Per riassumere come funziona, ho un modello html con una variabile per i tag di script. Uso https://github.com/alanshaw/grunt-include-replace per popolare quella variabile. In modalità dev, quella variabile proviene da un modello sconvolgente di tutti i miei file JS. L'attività di controllo ricalcola questo valore quando viene aggiunto o rimosso un file JS.

Ora, per ottenere risultati diversi in modalità dev o prod, devi semplicemente popolare quella variabile con un valore diverso. Ecco un po 'di codice:

var jsSrcFileArray = [
    'src/main/scripts/app/js/Constants.js',
    'src/main/scripts/app/js/Random.js',
    'src/main/scripts/app/js/Vector.js',
    'src/main/scripts/app/js/scripts.js',
    'src/main/scripts/app/js/StatsData.js',
    'src/main/scripts/app/js/Dialog.js',
    'src/main/scripts/app/**/*.js',
    '!src/main/scripts/app/js/AuditingReport.js'
];

var jsScriptTags = function (srcPattern, destPath) {
    if (srcPattern === undefined) {
        throw new Error("srcPattern undefined");
    }
    if (destPath === undefined) {
        throw new Error("destPath undefined");
    }
    return grunt.util._.reduce(
        grunt.file.expandMapping(srcPattern, destPath, {
            filter: 'isFile',
            flatten: true,
            expand: true,
            cwd: '.'
        }),
        function (sum, file) {
            return sum + '\n<script src="' + file.dest + '" type="text/javascript"></script>';
        },
        ''
    );
};

...

grunt.initConfig({

    includereplace: {
        dev: {
            options: {
                globals: {
                    scriptsTags: '<%= jsScriptTags(jsSrcFileArray, "../../main/scripts/app/js")%>'
                }
            },
            src: [
                'src/**/html-template.html'
            ],
            dest: 'src/main/generated/',
            flatten: true,
            cwd: '.',
            expand: true
        },
        prod: {
            options: {
                globals: {
                    scriptsTags: '<script src="app.min.js" type="text/javascript"></script>'
                }
            },
            src: [
                'src/**/html-template.html'
            ],
            dest: 'src/main/generatedprod/',
            flatten: true,
            cwd: '.',
            expand: true
        }

...

    jsScriptTags: jsScriptTags

jsSrcFileArrayè il tipico schema grugnito di file. jsScriptTagsprende il jsSrcFileArraye li concatena insieme ai scripttag su entrambi i lati. destPathè il prefisso che desidero su ogni file.

Ed ecco come appare l'HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>Example</title>

</head>

<body>    
@@scriptsTags
</body>
</html>

Ora, come puoi vedere nella configurazione, genera il valore di quella variabile come scripttag hard coded quando viene eseguito in prodmodalità. In modalità dev, questa variabile si espanderà a un valore come questo:

<script src="../../main/scripts/app/js/Constants.js" type="text/javascript"></script>
<script src="../../main/scripts/app/js/Random.js" type="text/javascript"></script>
<script src="../../main/scripts/app/js/Vector.js" type="text/javascript"></script>
<script src="../../main/scripts/app/js/StatsData.js" type="text/javascript"></script>
<script src="../../main/scripts/app/js/Dialog.js" type="text/javascript"></script>

Fatemi sapere se avete domande.

PS: Questa è una quantità folle di codice per qualcosa che vorrei fare in ogni app JS sul lato client. Spero che qualcuno possa trasformarlo in un plugin riutilizzabile. Forse un giorno lo farò.


1
Sembra promettente. Hai qualche possibilità di condividere alcuni frammenti?
Adam Marshall

I've set up my grunt task so that every time a file is added or deleted, the script tags automatically get generated to reflect thatCome hai fatto?
CodyBugstein,

2
Un'altra domanda: conosci un modo per eliminare solo blocchi di <script>tag HTML ?
CodyBugstein,

@Imray non dalla parte superiore della mia testa. Intendi senza alcuna forma di modello (ad esempio, grunt-include-replace)? Il primo pensiero che mi viene in mente sarebbe xslt. Probabilmente non è una buona soluzione, comunque.
Daniel Kaplan,

1
Questa risposta è a posto, anche se io personalmente rimosso destPathda jsScriptTagse scambiato grunt.file.expandMappingcon grunt.file.expanddei file che volevo erano già al posto giusto. Questo ha semplificato molto le cose. Grazie @DanielKaplan, mi hai fatto risparmiare un sacco di tempo :)
DanielM

13

Mi sto ponendo la stessa domanda da un po 'di tempo e penso che questo plugin grunt possa essere configurato per fare quello che vuoi: https://npmjs.org/package/grunt-targethtml . Implementa tag HTML condizionali, che dipendono dal bersaglio grugnito.


2
Ho visto questo plugin ma non mi piace l'idea di specificare manualmente tutti i file (e in realtà avere qualsiasi logica) nel mio index.html perché ho già un elenco di file js / css di origine nella mia configurazione grunt e don ' non voglio ripetermi. La linea di fondo è - non è in index.html dove dovresti decidere quali file includere
Dmitry Pashkevich

+1 per grunt-targethtml. Anche se è un po 'brutto aggiungere se le istruzioni per "decidere" nel tuo index.html quali risorse caricare. Tuttavia, ha senso. Questo è il luogo in cui cercherete normalmente di includere risorse nel vostro progetto. Inoltre, dare seguito a questo mi ha portato a dare un'occhiata a grunt-contrib. Ha delle cose fantastiche.
carbontax,

8

Stavo cercando una soluzione più semplice e diretta, quindi ho combinato la risposta da questa domanda:

Come posizionare se altro bloccare in gruntfile.js

e ho escogitato i seguenti semplici passaggi:

  1. Mantieni due versioni dei tuoi file indice come elencato e chiamali index-development.html e index-prodoction.html.
  2. Usa la seguente logica nel blocco concat / copy del tuo Gruntfile.js per il tuo file index.html:

    concat: {
        index: {
            src : [ (function() {
                if (grunt.option('Release')) {
                  return 'views/index-production.html';
                } else {
                  return 'views/index-development.html';
                }
              }()) ],
           dest: '<%= distdir %>/index.html',
           ...
        },
        ...
    },
  3. eseguire 'grunt - Rilascio' per scegliere il file index-production.html e lasciare il flag per avere la versione di sviluppo.

Nessun nuovo plug-in da aggiungere o configurare e nessuna nuova attività di grunt.


3
L'unico aspetto negativo qui è che ci sono due file index.html da mantenere.
Adam Marshall

5

Questa grugnita attività chiamata scriptlinker sembra un modo semplice per aggiungere gli script in modalità dev. Probabilmente potresti prima eseguire un'attività concat e quindi puntarla al tuo file concatenato in modalità prod.


+1. La documentazione è confusa e alcune cose (appRoot, relative) non sempre funzionano come previsto, ma comunque: strumento utile.
Scambia il

1
@hashchange Non utilizzo questo strumento. Ho finito per usare github.com/alanshaw/grunt-include-replace invece. Ho una variabile nel mio file html che rappresenta i tag di script. Quindi popolo quella variabile con una stringa dell'html che voglio. In modalità dev, questa variabile è un elenco di script. In modalità prod questa variabile è la versione concatenata e minimizzata.
Daniel Kaplan,

Grazie per il puntatore a grunt-include-replace. (In realtà avevo bisogno di uno strumento per aggiungere tutti i file delle specifiche in una directory a un file index.html di Mocha. Scriptlinker va bene per quello.)
hashchange

@hashchange hai ragione sul succhiare la documentazione. Come si dice dove mettere i riquadri degli script nel file HTML?
Daniel Kaplan,

1
Si definisce un commento HTML. Dai un'occhiata a questo file . Le inserzioni avvengono in <!--SINON COMPONENT SCRIPTS-->e <!--SPEC SCRIPTS-->. Ed ecco il compito di Grunt che lo fa (un vero e proprio lavoro, al contrario delle cose nei documenti). Spero che sia d'aiuto;)
Scambia l'hash

5

grunt-dom-munger legge e manipola HTML con selettori CSS. Ex. leggi i tag dal tuo html. Rimuovi nodi, aggiungi nodi e altro.

Puoi usare grunt-dom-munger per leggere tutti i tuoi file JS che sono collegati dal tuo index.html, ugualizzarli e poi usare di nuovo grunt-dom-munger per modificare il tuo index.html per collegare solo il JS minimizzato


5

Ho trovato un plugin grunt chiamato grunt-dev-prod-switch. Tutto ciò che fa è commentare alcuni blocchi che cerca in base a un'opzione --env che passi a grugnito (anche se ti limita a dev, prod e test).

Una volta impostato come spiegato qui , è possibile eseguire ad esempio:

grunt serve --env=dev, e tutto ciò che fa è commentare i blocchi che sono racchiusi da

    <!-- env:test/prod -->
    your code here
    <!-- env:test/prod:end -->

e decommenterà i blocchi che sono racchiusi da

    <!-- env:dev -->
    your code here
    <!-- env:dev:end -->

Funziona anche su JavaScript, lo uso per impostare l'indirizzo IP corretto a cui connettersi per la mia API back-end. I blocchi cambiano in

    /* env:dev */
    your code here
    /* env:dev:end */

Nel tuo caso, sarebbe semplice come questo:

<!DOCTYPE html>
<html>
    <head>
        <!-- env:dev -->
        <script src="js/module1.js" />
        <script src="js/module2.js" />
        <script src="js/module3.js" />
        ...
        <!-- env:dev:end -->
        <!-- env:prod -->
        <script src="js/MyApp-all.min.js" />
        ...
        <!-- env:prod:end -->
    </head>
    <body></body>
</html>

4

grunt-bake è un fantastico copione di grugniti che funzionerebbe benissimo qui. Lo uso nel mio script di compilazione automatica JQM.

https://github.com/imaginethepoet/autojqmphonegap

Dai un'occhiata al mio file grunt.coffee:

bake:
    resources: 
      files: "index.html":"resources/custom/components/base.html"

Questo esamina tutti i file in base.html e li succhia per creare index.html funziona alla grande per le app multipagina (phonegap). Ciò consente uno sviluppo più semplice poiché tutti gli sviluppatori non stanno lavorando su un'unica app lunga pagina (impedendo molti checkin di conflitto). Invece puoi dividere le pagine e lavorare su blocchi di codice più piccoli e compilare l'intera pagina usando un comando watch.

Bake legge il modello da base.html e inietta le pagine html del componente su watch.

<!DOCTYPE html>

Demo mobile di jQuery

app.initialize ();

<body>
    <!--(bake /resources/custom/components/page1.html)-->
    <!--(bake /resources/custom/components/page2.html)-->
    <!--(bake /resources/custom/components/page3.html)-->
</body>

Puoi fare un ulteriore passo avanti e aggiungere iniezioni nelle tue pagine per "menu", "popup" ecc. In modo da poter veramente dividere le pagine in componenti più piccoli gestibili.


Forse puoi migliorare la tua risposta con una demo del codice che utilizza grunt-bake?
Dmitry Pashkevich,

4

Utilizzare una combinazione di cablato https://github.com/taptapship/wiredep e usemin https://github.com/yeoman/grunt-usemin per consentire a grunt di occuparsi di questi compiti. Wiredep aggiungerà le tue dipendenze un file di script alla volta e usemin le concatenerà in un unico file per la produzione. Questo può quindi essere realizzato con solo alcuni commenti HTML. Ad esempio, i miei pacchetti bower sono automaticamente inclusi e aggiunti all'html quando eseguo bower install && grunt bowerInstall:

<!-- build:js /scripts/vendor.js -->
<!-- bower:js -->
<!-- endbower -->
<!-- endbuild -->

2

Questa risposta non è per nessuno!

Usa il modello Jade ... il passaggio di variabili a un modello Jade è un caso d'uso standard

Sto usando grunt (grunt-contrib-jade) ma non devi usare grunt. Basta usare il modulo standard di giada npm.

Se usi grunt, il tuo gruntfile vorrebbe qualcosa come ...

jade: {
    options: {
      // TODO - Define options here
    },
    dev: {
      options: {
        data: {
          pageTitle: '<%= grunt.file.name %>',
          homePage: '/app',
          liveReloadServer: liveReloadServer,
          cssGruntClassesForHtmlHead: 'grunt-' + '<%= grunt.task.current.target %>'
        },
        pretty: true
      },
      files: [
        {
          expand: true,
          cwd: "src/app",
          src: ["index.jade", "404.jade"],
          dest: "lib/app",
          ext: ".html"
        },
        {
          expand: true,
          flatten: true,
          cwd: "src/app",
          src: ["directives/partials/*.jade"],
          dest: "lib/app/directives/partials",
          ext: ".html"
        }
      ]
    }
  },

Ora possiamo accedere facilmente ai dati passati da grunt nel modello Jade.

Proprio come l'approccio utilizzato da Modernizr, ho impostato una classe CSS sul tag HTML in base al valore della variabile passata e da lì posso usare la logica JavaScript in base alla presenza o meno della classe CSS.

Questo è ottimo se usi Angular poiché puoi fare ng-if per includere elementi nella pagina in base alla presenza della classe.

Ad esempio, potrei includere uno script se la classe è presente ...

(Ad esempio, potrei includere lo script di ricarica live in sviluppo ma non in produzione)

<script ng-if="controller.isClassPresent()" src="//localhost:35729/livereload.js"></script> 

2

Prendi in considerazione processhtml . Permette la definizione di più "target" per build. I commenti vengono utilizzati per includere o escludere in modo condizionale materiale dall'HTML:

<!-- build:js:production js/app.js -->
...
<!-- /build -->

diventa

<script src="js/app.js"></script>

Pretende anche di fare cose belle come questa (vedi il README ):

<!-- build:[class]:dist production -->
<html class="debug_mode">
<!-- /build -->

<!-- class is changed to 'production' only when the 'dist' build is executed -->
<html class="production">
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.