diff options
-rw-r--r-- | .gitignore | 20 | ||||
-rw-r--r-- | .gitmodules | 0 | ||||
-rw-r--r-- | .goxc.json | 6 | ||||
-rw-r--r-- | .mailmap | 3 | ||||
-rw-r--r-- | .travis.yml | 21 | ||||
-rw-r--r-- | CONTRIBUTING.md | 161 | ||||
-rw-r--r-- | Dockerfile | 15 | ||||
-rw-r--r-- | LICENSE.md | 194 | ||||
-rw-r--r-- | Makefile | 83 | ||||
-rw-r--r-- | README.md | 107 | ||||
-rw-r--r-- | appveyor.yml | 15 | ||||
-rwxr-xr-x | bench.sh | 35 | ||||
-rwxr-xr-x | benchSite.sh | 9 | ||||
-rw-r--r-- | bufferpool/bufpool.go | 38 | ||||
-rw-r--r-- | bufferpool/bufpool_test.go | 27 | ||||
-rw-r--r-- | cache/partitioned_lazy_cache.go | 80 | ||||
-rw-r--r-- | cache/partitioned_lazy_cache_test.go | 92 | ||||
-rw-r--r-- | commands/benchmark.go | 112 | ||||
-rw-r--r-- | commands/check.go | 23 | ||||
-rw-r--r-- | commands/commandeer.go | 62 | ||||
-rw-r--r-- | commands/convert.go | 158 | ||||
-rw-r--r-- | commands/env.go | 35 | ||||
-rw-r--r-- | commands/gen.go | 23 | ||||
-rw-r--r-- | commands/genautocomplete.go | 70 | ||||
-rw-r--r-- | commands/gendoc.go | 86 | ||||
-rw-r--r-- | commands/gendocshelper.go | 72 | ||||
-rw-r--r-- | commands/genman.go | 66 | ||||
-rw-r--r-- | commands/hugo.go | 1046 | ||||
-rw-r--r-- | commands/hugo_windows.go | 27 | ||||
-rw-r--r-- | commands/import_jekyll.go | 577 | ||||
-rw-r--r-- | commands/import_jekyll_test.go | 126 | ||||
-rw-r--r-- | commands/limit_darwin.go | 85 | ||||
-rw-r--r-- | commands/limit_others.go | 32 | ||||
-rw-r--r-- | commands/list.go | 161 | ||||
-rw-r--r-- | commands/list_config.go | 66 | ||||
-rw-r--r-- | commands/new.go | 399 | ||||
-rw-r--r-- | commands/new_test.go | 122 | ||||
-rw-r--r-- | commands/release.go | 62 | ||||
-rw-r--r-- | commands/server.go | 309 | ||||
-rw-r--r-- | commands/server_test.go | 58 | ||||
-rw-r--r-- | commands/undraft.go | 157 | ||||
-rw-r--r-- | commands/undraft_test.go | 85 | ||||
-rw-r--r-- | commands/version.go | 80 | ||||
-rw-r--r-- | config/configProvider.go | 26 | ||||
-rw-r--r-- | create/content.go | 129 | ||||
-rw-r--r-- | create/content_template_handler.go | 135 | ||||
-rw-r--r-- | create/content_test.go | 198 | ||||
-rw-r--r-- | deps/deps.go | 176 | ||||
-rw-r--r-- | docs/.gitignore | 2 | ||||
-rw-r--r-- | docs/README.md | 3 | ||||
-rw-r--r-- | docs/archetypes/default.md (renamed from archetypes/default.md) | 0 | ||||
-rw-r--r-- | docs/archetypes/showcase.md (renamed from archetypes/showcase.md) | 0 | ||||
-rw-r--r-- | docs/config.toml (renamed from config.toml) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo.md (renamed from content/commands/hugo.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_benchmark.md (renamed from content/commands/hugo_benchmark.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_check.md (renamed from content/commands/hugo_check.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_check_ulimit.md (renamed from content/commands/hugo_check_ulimit.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_config.md (renamed from content/commands/hugo_config.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_convert.md (renamed from content/commands/hugo_convert.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_convert_toJSON.md (renamed from content/commands/hugo_convert_toJSON.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_convert_toTOML.md (renamed from content/commands/hugo_convert_toTOML.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_convert_toYAML.md (renamed from content/commands/hugo_convert_toYAML.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_env.md (renamed from content/commands/hugo_env.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_gen.md (renamed from content/commands/hugo_gen.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_gen_autocomplete.md (renamed from content/commands/hugo_gen_autocomplete.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_gen_doc.md (renamed from content/commands/hugo_gen_doc.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_gen_man.md (renamed from content/commands/hugo_gen_man.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_import.md (renamed from content/commands/hugo_import.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_import_jekyll.md (renamed from content/commands/hugo_import_jekyll.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_list.md (renamed from content/commands/hugo_list.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_list_drafts.md (renamed from content/commands/hugo_list_drafts.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_list_expired.md (renamed from content/commands/hugo_list_expired.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_list_future.md (renamed from content/commands/hugo_list_future.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_new.md (renamed from content/commands/hugo_new.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_new_site.md (renamed from content/commands/hugo_new_site.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_new_theme.md (renamed from content/commands/hugo_new_theme.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_server.md (renamed from content/commands/hugo_server.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_undraft.md (renamed from content/commands/hugo_undraft.md) | 0 | ||||
-rw-r--r-- | docs/content/commands/hugo_version.md (renamed from content/commands/hugo_version.md) | 0 | ||||
-rw-r--r-- | docs/content/community/contributing.md (renamed from content/community/contributing.md) | 0 | ||||
-rw-r--r-- | docs/content/community/mailing-list.md (renamed from content/community/mailing-list.md) | 0 | ||||
-rw-r--r-- | docs/content/community/press.md (renamed from content/community/press.md) | 0 | ||||
-rw-r--r-- | docs/content/content/archetypes.md (renamed from content/content/archetypes.md) | 0 | ||||
-rw-r--r-- | docs/content/content/example.md (renamed from content/content/example.md) | 0 | ||||
-rw-r--r-- | docs/content/content/front-matter.md (renamed from content/content/front-matter.md) | 0 | ||||
-rw-r--r-- | docs/content/content/markdown-extras.md (renamed from content/content/markdown-extras.md) | 0 | ||||
-rw-r--r-- | docs/content/content/multilingual.md (renamed from content/content/multilingual.md) | 0 | ||||
-rw-r--r-- | docs/content/content/ordering.md (renamed from content/content/ordering.md) | 0 | ||||
-rw-r--r-- | docs/content/content/organization.md (renamed from content/content/organization.md) | 0 | ||||
-rw-r--r-- | docs/content/content/sections.md (renamed from content/content/sections.md) | 0 | ||||
-rw-r--r-- | docs/content/content/summaries.md (renamed from content/content/summaries.md) | 0 | ||||
-rw-r--r-- | docs/content/content/supported-formats.md (renamed from content/content/supported-formats.md) | 0 | ||||
-rw-r--r-- | docs/content/content/types.md (renamed from content/content/types.md) | 0 | ||||
-rw-r--r-- | docs/content/content/using-index-md.md (renamed from content/content/using-index-md.md) | 0 | ||||
-rw-r--r-- | docs/content/extras/aliases.md (renamed from content/extras/aliases.md) | 0 | ||||
-rw-r--r-- | docs/content/extras/analytics.md (renamed from content/extras/analytics.md) | 0 | ||||
-rw-r--r-- | docs/content/extras/builders.md (renamed from content/extras/builders.md) | 0 | ||||
-rw-r--r-- | docs/content/extras/comments.md (renamed from content/extras/comments.md) | 0 | ||||
-rw-r--r-- | docs/content/extras/crossreferences.md (renamed from content/extras/crossreferences.md) | 0 | ||||
-rw-r--r-- | docs/content/extras/datadrivencontent.md (renamed from content/extras/datadrivencontent.md) | 0 | ||||
-rw-r--r-- | docs/content/extras/datafiles.md (renamed from content/extras/datafiles.md) | 0 | ||||
-rw-r--r-- | docs/content/extras/gitinfo.md (renamed from content/extras/gitinfo.md) | 0 | ||||
-rw-r--r-- | docs/content/extras/highlighting.md (renamed from content/extras/highlighting.md) | 0 | ||||
-rw-r--r-- | docs/content/extras/livereload.md (renamed from content/extras/livereload.md) | 0 | ||||
-rw-r--r-- | docs/content/extras/localfiles.md (renamed from content/extras/localfiles.md) | 0 | ||||
-rw-r--r-- | docs/content/extras/menus.md (renamed from content/extras/menus.md) | 0 | ||||
-rw-r--r-- | docs/content/extras/output-formats.md (renamed from content/extras/output-formats.md) | 0 | ||||
-rw-r--r-- | docs/content/extras/pagination.md (renamed from content/extras/pagination.md) | 0 | ||||
-rw-r--r-- | docs/content/extras/permalinks.md (renamed from content/extras/permalinks.md) | 0 | ||||
-rw-r--r-- | docs/content/extras/robots-txt.md (renamed from content/extras/robots-txt.md) | 0 | ||||
-rw-r--r-- | docs/content/extras/scratch.md (renamed from content/extras/scratch.md) | 0 | ||||
-rw-r--r-- | docs/content/extras/shortcodes.md (renamed from content/extras/shortcodes.md) | 0 | ||||
-rw-r--r-- | docs/content/extras/toc.md (renamed from content/extras/toc.md) | 0 | ||||
-rw-r--r-- | docs/content/extras/urls.md (renamed from content/extras/urls.md) | 0 | ||||
-rw-r--r-- | docs/content/meta/license.md (renamed from content/meta/license.md) | 0 | ||||
-rw-r--r-- | docs/content/meta/roadmap.md (renamed from content/meta/roadmap.md) | 0 | ||||
-rw-r--r-- | docs/content/overview/configuration.md (renamed from content/overview/configuration.md) | 0 | ||||
-rw-r--r-- | docs/content/overview/installing.md (renamed from content/overview/installing.md) | 0 | ||||
-rw-r--r-- | docs/content/overview/introduction.md (renamed from content/overview/introduction.md) | 0 | ||||
-rw-r--r-- | docs/content/overview/quickstart.md (renamed from content/overview/quickstart.md) | 0 | ||||
-rw-r--r-- | docs/content/overview/source-directory.md (renamed from content/overview/source-directory.md) | 0 | ||||
-rw-r--r-- | docs/content/overview/usage.md (renamed from content/overview/usage.md) | 0 | ||||
-rw-r--r-- | docs/content/release-notes/0.20.3-relnotes.md (renamed from content/release-notes/0.20.3-relnotes.md) | 0 | ||||
-rw-r--r-- | docs/content/release-notes/0.20.4-relnotes.md (renamed from content/release-notes/0.20.4-relnotes.md) | 0 | ||||
-rw-r--r-- | docs/content/release-notes/0.20.5-relnotes.md (renamed from content/release-notes/0.20.5-relnotes.md) | 0 | ||||
-rw-r--r-- | docs/content/release-notes/0.20.6-relnotes.md (renamed from content/release-notes/0.20.6-relnotes.md) | 0 | ||||
-rw-r--r-- | docs/content/release-notes/0.21-relnotes.md (renamed from content/release-notes/0.21-relnotes.md) | 0 | ||||
-rw-r--r-- | docs/content/release-notes/0.22-relnotes.md (renamed from content/release-notes/0.22-relnotes.md) | 0 | ||||
-rw-r--r-- | docs/content/release-notes/0.22.1-relnotes.md (renamed from content/release-notes/0.22.1-relnotes.md) | 0 | ||||
-rw-r--r-- | docs/content/release-notes/0.23-relnotes.md (renamed from content/release-notes/0.23-relnotes.md) | 0 | ||||
-rw-r--r-- | docs/content/release-notes/0.24-relnotes.md (renamed from content/release-notes/0.24-relnotes.md) | 0 | ||||
-rw-r--r-- | docs/content/release-notes/0.24.1-relnotes.md (renamed from content/release-notes/0.24.1-relnotes.md) | 0 | ||||
-rw-r--r-- | docs/content/release-notes/_index.md (renamed from content/release-notes/_index.md) | 0 | ||||
-rw-r--r-- | docs/content/release-notes/release-notes.md (renamed from content/release-notes/release-notes.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/2626info.md (renamed from content/showcase/2626info.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/antzucaro.md (renamed from content/showcase/antzucaro.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/appernetic.md (renamed from content/showcase/appernetic.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/arresteddevops.md (renamed from content/showcase/arresteddevops.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/asc.md (renamed from content/showcase/asc.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/astrochili.md (renamed from content/showcase/astrochili.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/aydoscom.md (renamed from content/showcase/aydoscom.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/balaramadurai.net.md (renamed from content/showcase/balaramadurai.net.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/barricade.md (renamed from content/showcase/barricade.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/bepsays.md (renamed from content/showcase/bepsays.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/bharathpalavalli.com.md (renamed from content/showcase/bharathpalavalli.com.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/bugtrackers.io.md (renamed from content/showcase/bugtrackers.io.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/bullion-investor.md (renamed from content/showcase/bullion-investor.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/camunda-blog.md (renamed from content/showcase/camunda-blog.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/camunda-docs.md (renamed from content/showcase/camunda-docs.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/carnivorousplants.md (renamed from content/showcase/carnivorousplants.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/cdnoverview.md (renamed from content/showcase/cdnoverview.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/chinese-grammar.md (renamed from content/showcase/chinese-grammar.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/chingli.md (renamed from content/showcase/chingli.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/chipsncookies.md (renamed from content/showcase/chipsncookies.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/christianmendoza.md (renamed from content/showcase/christianmendoza.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/cinegyopen.md (renamed from content/showcase/cinegyopen.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/clearhaus.md (renamed from content/showcase/clearhaus.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/cloudshark.md (renamed from content/showcase/cloudshark.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/coding-journal.md (renamed from content/showcase/coding-journal.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/consequently.md (renamed from content/showcase/consequently.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/ctlcompiled.md (renamed from content/showcase/ctlcompiled.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/danmux.md (renamed from content/showcase/danmux.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/datapipelinearchitect.md (renamed from content/showcase/datapipelinearchitect.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/davidepetilli.md (renamed from content/showcase/davidepetilli.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/davidrallen.md (renamed from content/showcase/davidrallen.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/davidyates.md (renamed from content/showcase/davidyates.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/dbzman-online.md (renamed from content/showcase/dbzman-online.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/devmonk.md (renamed from content/showcase/devmonk.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/dmitriid.com.md (renamed from content/showcase/dmitriid.com.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/emilyhorsman.com.md (renamed from content/showcase/emilyhorsman.com.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/enjoyablerecipes.md (renamed from content/showcase/enjoyablerecipes.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/esaezgil.md (renamed from content/showcase/esaezgil.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/esolia-com.md (renamed from content/showcase/esolia-com.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/esolia-pro.md (renamed from content/showcase/esolia-pro.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/eurie.md (renamed from content/showcase/eurie.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/fale.md (renamed from content/showcase/fale.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/firstnameclub.md (renamed from content/showcase/firstnameclub.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/fixatom.md (renamed from content/showcase/fixatom.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/furqansoftware.md (renamed from content/showcase/furqansoftware.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/fxsitecompat.md (renamed from content/showcase/fxsitecompat.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/gntech.md (renamed from content/showcase/gntech.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/gogb.md (renamed from content/showcase/gogb.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/goin5minutes.md (renamed from content/showcase/goin5minutes.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/h10n.me.md (renamed from content/showcase/h10n.me.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/heimatverein-niederjosbach.md (renamed from content/showcase/heimatverein-niederjosbach.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/horeaporutiu.md (renamed from content/showcase/horeaporutiu.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/hugo.md (renamed from content/showcase/hugo.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/invision.md (renamed from content/showcase/invision.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/jamescampbell.md (renamed from content/showcase/jamescampbell.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/jorgennilsson.md (renamed from content/showcase/jorgennilsson.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/kieranhealy.md (renamed from content/showcase/kieranhealy.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/klingt-net.md (renamed from content/showcase/klingt-net.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/launchcode5.md (renamed from content/showcase/launchcode5.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/leepenney.md (renamed from content/showcase/leepenney.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/leowkahman.md (renamed from content/showcase/leowkahman.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/lk4d4.darth.io.md (renamed from content/showcase/lk4d4.darth.io.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/losslesslife.md (renamed from content/showcase/losslesslife.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/lucumt.info.md (renamed from content/showcase/lucumt.info.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/mariosanchez.md (renamed from content/showcase/mariosanchez.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/mayan-edms.md (renamed from content/showcase/mayan-edms.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/michaelwhatcott.md (renamed from content/showcase/michaelwhatcott.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/mongodb-eng-journal.md (renamed from content/showcase/mongodb-eng-journal.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/mtbhomer.md (renamed from content/showcase/mtbhomer.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/myearworms.md (renamed from content/showcase/myearworms.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/neavey.net.md (renamed from content/showcase/neavey.net.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/nickoneill.md (renamed from content/showcase/nickoneill.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/ninjaducks.in.md (renamed from content/showcase/ninjaducks.in.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/ninya.io.md (renamed from content/showcase/ninya.io.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/nodesk.md (renamed from content/showcase/nodesk.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/novelist-xyz.md (renamed from content/showcase/novelist-xyz.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/npf.md (renamed from content/showcase/npf.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/nutspubcrawl.md (renamed from content/showcase/nutspubcrawl.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/ocul-maps.md (renamed from content/showcase/ocul-maps.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/petanikode.md (renamed from content/showcase/petanikode.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/peteraba.md (renamed from content/showcase/peteraba.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/picturingjordan.md (renamed from content/showcase/picturingjordan.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/promotive.md (renamed from content/showcase/promotive.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/rahulrai.md (renamed from content/showcase/rahulrai.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/rakutentech.md (renamed from content/showcase/rakutentech.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/rdegges.md (renamed from content/showcase/rdegges.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/readtext.md (renamed from content/showcase/readtext.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/richardsumilang.md (renamed from content/showcase/richardsumilang.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/rick-cogley-info.md (renamed from content/showcase/rick-cogley-info.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/ridingbytes.md (renamed from content/showcase/ridingbytes.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/robertbasic.md (renamed from content/showcase/robertbasic.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/sanjay-saxena.md (renamed from content/showcase/sanjay-saxena.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/scottcwilson.md (renamed from content/showcase/scottcwilson.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/shapeshed.md (renamed from content/showcase/shapeshed.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/shelan.md (renamed from content/showcase/shelan.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/siba.md (renamed from content/showcase/siba.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/silvergeko.md (renamed from content/showcase/silvergeko.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/softinio.md (renamed from content/showcase/softinio.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/spf13.md (renamed from content/showcase/spf13.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/steambap.md (renamed from content/showcase/steambap.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/stefano.chiodino.md (renamed from content/showcase/stefano.chiodino.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/stou.md (renamed from content/showcase/stou.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/szymonkatra.md (renamed from content/showcase/szymonkatra.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/techmadeplain.md (renamed from content/showcase/techmadeplain.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/tendermint.md (renamed from content/showcase/tendermint.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/thecodeking.md (renamed from content/showcase/thecodeking.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/thehome.md (renamed from content/showcase/thehome.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/thislittleduck.md (renamed from content/showcase/thislittleduck.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/tibobeijen.nl.md (renamed from content/showcase/tibobeijen.nl.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/ttsreader.md (renamed from content/showcase/ttsreader.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/tutorialonfly.md (renamed from content/showcase/tutorialonfly.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/tutswiki.md (renamed from content/showcase/tutswiki.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/ucsb.md (renamed from content/showcase/ucsb.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/upbeat.md (renamed from content/showcase/upbeat.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/vamp.md (renamed from content/showcase/vamp.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/viglug.org.md (renamed from content/showcase/viglug.org.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/vurt.co.md (renamed from content/showcase/vurt.co.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/worldtowriters.md (renamed from content/showcase/worldtowriters.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/yslow-rules.md (renamed from content/showcase/yslow-rules.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/ysqi.md (renamed from content/showcase/ysqi.md) | 0 | ||||
-rw-r--r-- | docs/content/showcase/yulinling.net.md (renamed from content/showcase/yulinling.net.md) | 0 | ||||
-rw-r--r-- | docs/content/taxonomies/displaying.md (renamed from content/taxonomies/displaying.md) | 0 | ||||
-rw-r--r-- | docs/content/taxonomies/methods.md (renamed from content/taxonomies/methods.md) | 0 | ||||
-rw-r--r-- | docs/content/taxonomies/ordering.md (renamed from content/taxonomies/ordering.md) | 0 | ||||
-rw-r--r-- | docs/content/taxonomies/overview.md (renamed from content/taxonomies/overview.md) | 0 | ||||
-rw-r--r-- | docs/content/taxonomies/templates.md (renamed from content/taxonomies/templates.md) | 0 | ||||
-rw-r--r-- | docs/content/taxonomies/usage.md (renamed from content/taxonomies/usage.md) | 0 | ||||
-rw-r--r-- | docs/content/templates/404.md (renamed from content/templates/404.md) | 0 | ||||
-rw-r--r-- | docs/content/templates/ace.md (renamed from content/templates/ace.md) | 0 | ||||
-rw-r--r-- | docs/content/templates/amber.md (renamed from content/templates/amber.md) | 0 | ||||
-rw-r--r-- | docs/content/templates/blocks.md (renamed from content/templates/blocks.md) | 0 | ||||
-rw-r--r-- | docs/content/templates/content.md (renamed from content/templates/content.md) | 0 | ||||
-rw-r--r-- | docs/content/templates/debugging.md (renamed from content/templates/debugging.md) | 0 | ||||
-rw-r--r-- | docs/content/templates/functions.md (renamed from content/templates/functions.md) | 0 | ||||
-rw-r--r-- | docs/content/templates/go-templates.md (renamed from content/templates/go-templates.md) | 0 | ||||
-rw-r--r-- | docs/content/templates/homepage.md (renamed from content/templates/homepage.md) | 0 | ||||
-rw-r--r-- | docs/content/templates/list.md (renamed from content/templates/list.md) | 0 | ||||
-rw-r--r-- | docs/content/templates/overview.md (renamed from content/templates/overview.md) | 0 | ||||
-rw-r--r-- | docs/content/templates/partials.md (renamed from content/templates/partials.md) | 0 | ||||
-rw-r--r-- | docs/content/templates/rss.md (renamed from content/templates/rss.md) | 0 | ||||
-rw-r--r-- | docs/content/templates/sitemap.md (renamed from content/templates/sitemap.md) | 0 | ||||
-rw-r--r-- | docs/content/templates/terms.md (renamed from content/templates/terms.md) | 0 | ||||
-rw-r--r-- | docs/content/templates/variables.md (renamed from content/templates/variables.md) | 0 | ||||
-rw-r--r-- | docs/content/templates/views.md (renamed from content/templates/views.md) | 0 | ||||
-rw-r--r-- | docs/content/themes/creation.md (renamed from content/themes/creation.md) | 0 | ||||
-rw-r--r-- | docs/content/themes/customizing.md (renamed from content/themes/customizing.md) | 0 | ||||
-rw-r--r-- | docs/content/themes/installing.md (renamed from content/themes/installing.md) | 0 | ||||
-rw-r--r-- | docs/content/themes/overview.md (renamed from content/themes/overview.md) | 0 | ||||
-rw-r--r-- | docs/content/themes/usage.md (renamed from content/themes/usage.md) | 0 | ||||
-rw-r--r-- | docs/content/tools/_index.md (renamed from content/tools/_index.md) | 0 | ||||
-rw-r--r-- | docs/content/troubleshooting/categories-with-accented-characters.md (renamed from content/troubleshooting/categories-with-accented-characters.md) | 0 | ||||
-rw-r--r-- | docs/content/troubleshooting/overview.md (renamed from content/troubleshooting/overview.md) | 0 | ||||
-rw-r--r-- | docs/content/troubleshooting/strange-eof-error.md (renamed from content/troubleshooting/strange-eof-error.md) | 0 | ||||
-rw-r--r-- | docs/content/tutorials/automated-deployments.md (renamed from content/tutorials/automated-deployments.md) | 0 | ||||
-rw-r--r-- | docs/content/tutorials/create-a-multilingual-site.md (renamed from content/tutorials/create-a-multilingual-site.md) | 0 | ||||
-rw-r--r-- | docs/content/tutorials/creating-a-new-theme.md (renamed from content/tutorials/creating-a-new-theme.md) | 0 | ||||
-rw-r--r-- | docs/content/tutorials/deployment-with-rsync.md (renamed from content/tutorials/deployment-with-rsync.md) | 0 | ||||
-rw-r--r-- | docs/content/tutorials/github-pages-blog.md (renamed from content/tutorials/github-pages-blog.md) | 0 | ||||
-rw-r--r-- | docs/content/tutorials/hosting-on-bitbucket.md (renamed from content/tutorials/hosting-on-bitbucket.md) | 0 | ||||
-rw-r--r-- | docs/content/tutorials/hosting-on-gitlab.md (renamed from content/tutorials/hosting-on-gitlab.md) | 0 | ||||
-rw-r--r-- | docs/content/tutorials/how-to-contribute-to-hugo.md (renamed from content/tutorials/how-to-contribute-to-hugo.md) | 0 | ||||
-rw-r--r-- | docs/content/tutorials/installing-on-mac.md (renamed from content/tutorials/installing-on-mac.md) | 0 | ||||
-rw-r--r-- | docs/content/tutorials/installing-on-windows.md (renamed from content/tutorials/installing-on-windows.md) | 0 | ||||
-rw-r--r-- | docs/content/tutorials/mathjax.md (renamed from content/tutorials/mathjax.md) | 0 | ||||
-rw-r--r-- | docs/content/tutorials/migrate-from-jekyll.md (renamed from content/tutorials/migrate-from-jekyll.md) | 0 | ||||
-rw-r--r-- | docs/data/docs.json (renamed from data/docs.json) | 0 | ||||
-rw-r--r-- | docs/data/references.toml (renamed from data/references.toml) | 0 | ||||
-rw-r--r-- | docs/data/titles.toml (renamed from data/titles.toml) | 0 | ||||
-rw-r--r-- | docs/layouts/404.html (renamed from layouts/404.html) | 0 | ||||
-rw-r--r-- | docs/layouts/_default/baseof.html (renamed from layouts/_default/baseof.html) | 0 | ||||
-rw-r--r-- | docs/layouts/_default/list.html (renamed from layouts/_default/list.html) | 0 | ||||
-rw-r--r-- | docs/layouts/_default/single.html (renamed from layouts/_default/single.html) | 0 | ||||
-rw-r--r-- | docs/layouts/index.html (renamed from layouts/index.html) | 0 | ||||
-rw-r--r-- | docs/layouts/index.redir (renamed from layouts/index.redir) | 0 | ||||
-rw-r--r-- | docs/layouts/partials/analytics.html (renamed from layouts/partials/analytics.html) | 0 | ||||
-rw-r--r-- | docs/layouts/partials/footer.html (renamed from layouts/partials/footer.html) | 0 | ||||
-rw-r--r-- | docs/layouts/partials/header.html (renamed from layouts/partials/header.html) | 0 | ||||
-rw-r--r-- | docs/layouts/partials/menu.html (renamed from layouts/partials/menu.html) | 0 | ||||
-rw-r--r-- | docs/layouts/partials/quotes.html (renamed from layouts/partials/quotes.html) | 0 | ||||
-rw-r--r-- | docs/layouts/partials/search.html (renamed from layouts/partials/search.html) | 0 | ||||
-rw-r--r-- | docs/layouts/section/commands.html (renamed from layouts/section/commands.html) | 0 | ||||
-rw-r--r-- | docs/layouts/section/release-notes.html (renamed from layouts/section/release-notes.html) | 0 | ||||
-rw-r--r-- | docs/layouts/section/showcase.html (renamed from layouts/section/showcase.html) | 0 | ||||
-rw-r--r-- | docs/layouts/shortcodes/datatable-vertical.html (renamed from layouts/shortcodes/datatable-vertical.html) | 0 | ||||
-rw-r--r-- | docs/layouts/shortcodes/datatable.html (renamed from layouts/shortcodes/datatable.html) | 0 | ||||
-rw-r--r-- | docs/layouts/shortcodes/directoryindex.html (renamed from layouts/shortcodes/directoryindex.html) | 0 | ||||
-rw-r--r-- | docs/layouts/shortcodes/gh.html (renamed from layouts/shortcodes/gh.html) | 0 | ||||
-rw-r--r-- | docs/layouts/shortcodes/nohighlight.html (renamed from layouts/shortcodes/nohighlight.html) | 0 | ||||
-rw-r--r-- | docs/layouts/shortcodes/readfile.html (renamed from layouts/shortcodes/readfile.html) | 0 | ||||
-rw-r--r-- | docs/layouts/shortcodes/youtube.html (renamed from layouts/shortcodes/youtube.html) | 0 | ||||
-rw-r--r-- | docs/layouts/showcase/thumbnail.html (renamed from layouts/showcase/thumbnail.html) | 0 | ||||
-rw-r--r-- | docs/static/_headers (renamed from static/_headers) | 0 | ||||
-rw-r--r-- | docs/static/apple-touch-icon.png (renamed from static/apple-touch-icon.png) | bin | 7993 -> 7993 bytes | |||
-rw-r--r-- | docs/static/css/bootstrap-additions-gohugo.css (renamed from static/css/bootstrap-additions-gohugo.css) | 0 | ||||
-rw-r--r-- | docs/static/css/bootstrap-changes-gohugo.css (renamed from static/css/bootstrap-changes-gohugo.css) | 0 | ||||
-rw-r--r-- | docs/static/css/bootstrap-stripped-gohugo.css (renamed from static/css/bootstrap-stripped-gohugo.css) | 0 | ||||
-rw-r--r-- | docs/static/css/content-style.css (renamed from static/css/content-style.css) | 0 | ||||
-rw-r--r-- | docs/static/css/home-page-style-responsive.css (renamed from static/css/home-page-style-responsive.css) | 0 | ||||
-rw-r--r-- | docs/static/css/home-page-style.css (renamed from static/css/home-page-style.css) | 0 | ||||
-rw-r--r-- | docs/static/css/hugofont.css (renamed from static/css/hugofont.css) | 0 | ||||
-rw-r--r-- | docs/static/css/style-responsive.css (renamed from static/css/style-responsive.css) | 0 | ||||
-rw-r--r-- | docs/static/css/style.css (renamed from static/css/style.css) | 0 | ||||
-rw-r--r-- | docs/static/favicon.ico (renamed from static/favicon.ico) | bin | 15086 -> 15086 bytes | |||
-rw-r--r-- | docs/static/fonts/glyphicons-halflings-regular.eot (renamed from static/fonts/glyphicons-halflings-regular.eot) | bin | 20127 -> 20127 bytes | |||
-rw-r--r-- | docs/static/fonts/glyphicons-halflings-regular.svg (renamed from static/fonts/glyphicons-halflings-regular.svg) | 0 | ||||
-rw-r--r-- | docs/static/fonts/glyphicons-halflings-regular.ttf (renamed from static/fonts/glyphicons-halflings-regular.ttf) | bin | 45404 -> 45404 bytes | |||
-rw-r--r-- | docs/static/fonts/glyphicons-halflings-regular.woff (renamed from static/fonts/glyphicons-halflings-regular.woff) | bin | 23424 -> 23424 bytes | |||
-rw-r--r-- | docs/static/fonts/glyphicons-halflings-regular.woff2 (renamed from static/fonts/glyphicons-halflings-regular.woff2) | bin | 18028 -> 18028 bytes | |||
-rw-r--r-- | docs/static/fonts/hugo.eot (renamed from static/fonts/hugo.eot) | bin | 16380 -> 16380 bytes | |||
-rw-r--r-- | docs/static/fonts/hugo.svg (renamed from static/fonts/hugo.svg) | 0 | ||||
-rw-r--r-- | docs/static/fonts/hugo.ttf (renamed from static/fonts/hugo.ttf) | bin | 16228 -> 16228 bytes | |||
-rw-r--r-- | docs/static/fonts/hugo.woff (renamed from static/fonts/hugo.woff) | bin | 11728 -> 11728 bytes | |||
-rw-r--r-- | docs/static/img/2626info-tn.png (renamed from static/img/2626info-tn.png) | bin | 9428 -> 9428 bytes | |||
-rw-r--r-- | docs/static/img/antzucaro-tn.jpg (renamed from static/img/antzucaro-tn.jpg) | bin | 26326 -> 26326 bytes | |||
-rw-r--r-- | docs/static/img/apperneticioblog.png (renamed from static/img/apperneticioblog.png) | bin | 28644 -> 28644 bytes | |||
-rw-r--r-- | docs/static/img/arresteddevops-tn.png (renamed from static/img/arresteddevops-tn.png) | bin | 47184 -> 47184 bytes | |||
-rw-r--r-- | docs/static/img/asc-tn.jpg (renamed from static/img/asc-tn.jpg) | bin | 31491 -> 31491 bytes | |||
-rw-r--r-- | docs/static/img/astrochili-tn.png (renamed from static/img/astrochili-tn.png) | bin | 55302 -> 55302 bytes | |||
-rw-r--r-- | docs/static/img/aydoscom.png (renamed from static/img/aydoscom.png) | bin | 20544 -> 20544 bytes | |||
-rw-r--r-- | docs/static/img/balaramadurai-net-tn.jpg (renamed from static/img/balaramadurai-net-tn.jpg) | bin | 60891 -> 60891 bytes | |||
-rw-r--r-- | docs/static/img/barricade-tn.png (renamed from static/img/barricade-tn.png) | bin | 42099 -> 42099 bytes | |||
-rw-r--r-- | docs/static/img/bepsays-tn.png (renamed from static/img/bepsays-tn.png) | bin | 17762 -> 17762 bytes | |||
-rw-r--r-- | docs/static/img/bharathpalavalli-tn.png (renamed from static/img/bharathpalavalli-tn.png) | bin | 115959 -> 115959 bytes | |||
-rw-r--r-- | docs/static/img/bugtrackersio-tn.jpg (renamed from static/img/bugtrackersio-tn.jpg) | bin | 44038 -> 44038 bytes | |||
-rw-r--r-- | docs/static/img/bullion-investor-com.png (renamed from static/img/bullion-investor-com.png) | bin | 17335 -> 17335 bytes | |||
-rw-r--r-- | docs/static/img/camunda-blog.png (renamed from static/img/camunda-blog.png) | bin | 15944 -> 15944 bytes | |||
-rw-r--r-- | docs/static/img/camunda-docs.png (renamed from static/img/camunda-docs.png) | bin | 17636 -> 17636 bytes | |||
-rw-r--r-- | docs/static/img/carnivorousplants-tn.png (renamed from static/img/carnivorousplants-tn.png) | bin | 60597 -> 60597 bytes | |||
-rw-r--r-- | docs/static/img/cdnoverview-tn.png (renamed from static/img/cdnoverview-tn.png) | bin | 20772 -> 20772 bytes | |||
-rw-r--r-- | docs/static/img/chinese-grammar-tn.png (renamed from static/img/chinese-grammar-tn.png) | bin | 22363 -> 22363 bytes | |||
-rw-r--r-- | docs/static/img/chingli-tn.jpg (renamed from static/img/chingli-tn.jpg) | bin | 37598 -> 37598 bytes | |||
-rw-r--r-- | docs/static/img/chipsncookies-tn.png (renamed from static/img/chipsncookies-tn.png) | bin | 23365 -> 23365 bytes | |||
-rw-r--r-- | docs/static/img/christianmendoza-tn.jpg (renamed from static/img/christianmendoza-tn.jpg) | bin | 55934 -> 55934 bytes | |||
-rw-r--r-- | docs/static/img/cinegyopen-tn.png (renamed from static/img/cinegyopen-tn.png) | bin | 34628 -> 34628 bytes | |||
-rw-r--r-- | docs/static/img/clearhaus-tn.png (renamed from static/img/clearhaus-tn.png) | bin | 44627 -> 44627 bytes | |||
-rw-r--r-- | docs/static/img/cloudshark-tn.jpg (renamed from static/img/cloudshark-tn.jpg) | bin | 32670 -> 32670 bytes | |||
-rw-r--r-- | docs/static/img/codingjournal-tn.png (renamed from static/img/codingjournal-tn.png) | bin | 42764 -> 42764 bytes | |||
-rw-r--r-- | docs/static/img/consequently.jpg (renamed from static/img/consequently.jpg) | bin | 77232 -> 77232 bytes | |||
-rw-r--r-- | docs/static/img/content/archetypes/archetype-hierarchy.png (renamed from static/img/content/archetypes/archetype-hierarchy.png) | bin | 38799 -> 38799 bytes | |||
-rw-r--r-- | docs/static/img/ctlcompiled-tn.png (renamed from static/img/ctlcompiled-tn.png) | bin | 45924 -> 45924 bytes | |||
-rw-r--r-- | docs/static/img/danmux-tn.jpg (renamed from static/img/danmux-tn.jpg) | bin | 22847 -> 22847 bytes | |||
-rw-r--r-- | docs/static/img/datapipelinearchitect-tn.jpg (renamed from static/img/datapipelinearchitect-tn.jpg) | bin | 49401 -> 49401 bytes | |||
-rw-r--r-- | docs/static/img/davidepetilli-tn.jpg (renamed from static/img/davidepetilli-tn.jpg) | bin | 66606 -> 66606 bytes | |||
-rw-r--r-- | docs/static/img/davidrallen-tn.png (renamed from static/img/davidrallen-tn.png) | bin | 9016 -> 9016 bytes | |||
-rw-r--r-- | docs/static/img/davidyates-tn.png (renamed from static/img/davidyates-tn.png) | bin | 28368 -> 28368 bytes | |||
-rw-r--r-- | docs/static/img/dbzman-online-tn.png (renamed from static/img/dbzman-online-tn.png) | bin | 26773 -> 26773 bytes | |||
-rw-r--r-- | docs/static/img/desk-mini.jpg (renamed from static/img/desk-mini.jpg) | bin | 61635 -> 61635 bytes | |||
-rw-r--r-- | docs/static/img/desk-sm.jpg (renamed from static/img/desk-sm.jpg) | bin | 139803 -> 139803 bytes | |||
-rw-r--r-- | docs/static/img/desk-wide.jpg (renamed from static/img/desk-wide.jpg) | bin | 198917 -> 198917 bytes | |||
-rw-r--r-- | docs/static/img/desk.jpg (renamed from static/img/desk.jpg) | bin | 383437 -> 383437 bytes | |||
-rw-r--r-- | docs/static/img/devmonk-tn.jpg (renamed from static/img/devmonk-tn.jpg) | bin | 25360 -> 25360 bytes | |||
-rw-r--r-- | docs/static/img/dmitriid.com.png (renamed from static/img/dmitriid.com.png) | bin | 82160 -> 82160 bytes | |||
-rw-r--r-- | docs/static/img/docs.eurie.io-tn.png (renamed from static/img/docs.eurie.io-tn.png) | bin | 16757 -> 16757 bytes | |||
-rw-r--r-- | docs/static/img/emilyhorsman.com-tn.jpg (renamed from static/img/emilyhorsman.com-tn.jpg) | bin | 58731 -> 58731 bytes | |||
-rw-r--r-- | docs/static/img/enjoyablerecipes-tn.png (renamed from static/img/enjoyablerecipes-tn.png) | bin | 68005 -> 68005 bytes | |||
-rw-r--r-- | docs/static/img/esaezgil_com-tn.png (renamed from static/img/esaezgil_com-tn.png) | bin | 83183 -> 83183 bytes | |||
-rw-r--r-- | docs/static/img/esolia_com-tn.png (renamed from static/img/esolia_com-tn.png) | bin | 45769 -> 45769 bytes | |||
-rw-r--r-- | docs/static/img/esolia_pro-tn.png (renamed from static/img/esolia_pro-tn.png) | bin | 59162 -> 59162 bytes | |||
-rw-r--r-- | docs/static/img/fale-tn.png (renamed from static/img/fale-tn.png) | bin | 15252 -> 15252 bytes | |||
-rw-r--r-- | docs/static/img/firstnameclub.png (renamed from static/img/firstnameclub.png) | bin | 58599 -> 58599 bytes | |||
-rw-r--r-- | docs/static/img/fixatom-tn.png (renamed from static/img/fixatom-tn.png) | bin | 6311 -> 6311 bytes | |||
-rw-r--r-- | docs/static/img/freebsd-19px.svg (renamed from static/img/freebsd-19px.svg) | 0 | ||||
-rw-r--r-- | docs/static/img/furqansoftware-tn.png (renamed from static/img/furqansoftware-tn.png) | bin | 63612 -> 63612 bytes | |||
-rw-r--r-- | docs/static/img/fxsitecompat-tn.png (renamed from static/img/fxsitecompat-tn.png) | bin | 42094 -> 42094 bytes | |||
-rw-r--r-- | docs/static/img/gntech-tn.png (renamed from static/img/gntech-tn.png) | bin | 15379 -> 15379 bytes | |||
-rw-r--r-- | docs/static/img/gogb-tn.jpg (renamed from static/img/gogb-tn.jpg) | bin | 35464 -> 35464 bytes | |||
-rw-r--r-- | docs/static/img/goin5minutes-tn.png (renamed from static/img/goin5minutes-tn.png) | bin | 9297 -> 9297 bytes | |||
-rw-r--r-- | docs/static/img/gray.png (renamed from static/img/gray.png) | bin | 19856 -> 19856 bytes | |||
-rw-r--r-- | docs/static/img/h10n.me-tn.png (renamed from static/img/h10n.me-tn.png) | bin | 26543 -> 26543 bytes | |||
-rw-r--r-- | docs/static/img/heimatverein-niederjosbach-tn.png (renamed from static/img/heimatverein-niederjosbach-tn.png) | bin | 67682 -> 67682 bytes | |||
-rw-r--r-- | docs/static/img/horeaporutiu-tn.jpg (renamed from static/img/horeaporutiu-tn.jpg) | bin | 13700 -> 13700 bytes | |||
-rw-r--r-- | docs/static/img/hugo-logo-med.png (renamed from static/img/hugo-logo-med.png) | bin | 23265 -> 23265 bytes | |||
-rw-r--r-- | docs/static/img/hugo-logo.png (renamed from static/img/hugo-logo.png) | bin | 13782 -> 13782 bytes | |||
-rw-r--r-- | docs/static/img/hugo-tn.jpg (renamed from static/img/hugo-tn.jpg) | bin | 70522 -> 70522 bytes | |||
-rw-r--r-- | docs/static/img/hugo.png (renamed from static/img/hugo.png) | bin | 18210 -> 18210 bytes | |||
-rw-r--r-- | docs/static/img/hugoSM.png (renamed from static/img/hugoSM.png) | bin | 1869 -> 1869 bytes | |||
-rw-r--r-- | docs/static/img/invision-tn.png (renamed from static/img/invision-tn.png) | bin | 22903 -> 22903 bytes | |||
-rw-r--r-- | docs/static/img/jamescampbell-tn.png (renamed from static/img/jamescampbell-tn.png) | bin | 47449 -> 47449 bytes | |||
-rw-r--r-- | docs/static/img/jorgennilsson-tn.png (renamed from static/img/jorgennilsson-tn.png) | bin | 24915 -> 24915 bytes | |||
-rw-r--r-- | docs/static/img/kjhealy-tn.jpg (renamed from static/img/kjhealy-tn.jpg) | bin | 37932 -> 37932 bytes | |||
-rw-r--r-- | docs/static/img/klingt-net-tn.png (renamed from static/img/klingt-net-tn.png) | bin | 23171 -> 23171 bytes | |||
-rw-r--r-- | docs/static/img/launchcode-tn.jpg (renamed from static/img/launchcode-tn.jpg) | bin | 51867 -> 51867 bytes | |||
-rw-r--r-- | docs/static/img/leepenney-tn.jpg (renamed from static/img/leepenney-tn.jpg) | bin | 78656 -> 78656 bytes | |||
-rw-r--r-- | docs/static/img/leowkahman-tn.png (renamed from static/img/leowkahman-tn.png) | bin | 12801 -> 12801 bytes | |||
-rw-r--r-- | docs/static/img/lk4d4-tn.jpg (renamed from static/img/lk4d4-tn.jpg) | bin | 39146 -> 39146 bytes | |||
-rw-r--r-- | docs/static/img/losslesslife-tn.png (renamed from static/img/losslesslife-tn.png) | bin | 32104 -> 32104 bytes | |||
-rw-r--r-- | docs/static/img/lucumt.info.png (renamed from static/img/lucumt.info.png) | bin | 23316 -> 23316 bytes | |||
-rw-r--r-- | docs/static/img/mariosanchez-tn.jpg (renamed from static/img/mariosanchez-tn.jpg) | bin | 46099 -> 46099 bytes | |||
-rw-r--r-- | docs/static/img/mayan-edms-tn.png (renamed from static/img/mayan-edms-tn.png) | bin | 66683 -> 66683 bytes | |||
-rw-r--r-- | docs/static/img/michaelwhatcott-tn.jpg (renamed from static/img/michaelwhatcott-tn.jpg) | bin | 12223 -> 12223 bytes | |||
-rw-r--r-- | docs/static/img/mongodb-eng-tn.png (renamed from static/img/mongodb-eng-tn.png) | bin | 28118 -> 28118 bytes | |||
-rw-r--r-- | docs/static/img/mtbhomer-tn.png (renamed from static/img/mtbhomer-tn.png) | bin | 45192 -> 45192 bytes | |||
-rw-r--r-- | docs/static/img/myearworms-tn.jpg (renamed from static/img/myearworms-tn.jpg) | bin | 46701 -> 46701 bytes | |||
-rw-r--r-- | docs/static/img/neavey-tn.jpg (renamed from static/img/neavey-tn.jpg) | bin | 61843 -> 61843 bytes | |||
-rw-r--r-- | docs/static/img/nickoneill-tn.jpg (renamed from static/img/nickoneill-tn.jpg) | bin | 36535 -> 36535 bytes | |||
-rw-r--r-- | docs/static/img/ninjaducks-tn.png (renamed from static/img/ninjaducks-tn.png) | bin | 56680 -> 56680 bytes | |||
-rw-r--r-- | docs/static/img/ninya-tn.jpg (renamed from static/img/ninya-tn.jpg) | bin | 33215 -> 33215 bytes | |||
-rw-r--r-- | docs/static/img/nodesk-tn.png (renamed from static/img/nodesk-tn.png) | bin | 34688 -> 34688 bytes | |||
-rw-r--r-- | docs/static/img/novelist-xyz.png (renamed from static/img/novelist-xyz.png) | bin | 17130 -> 17130 bytes | |||
-rw-r--r-- | docs/static/img/npf-tn.jpg (renamed from static/img/npf-tn.jpg) | bin | 34938 -> 34938 bytes | |||
-rw-r--r-- | docs/static/img/nutspubcrawl.jpg (renamed from static/img/nutspubcrawl.jpg) | bin | 61648 -> 61648 bytes | |||
-rw-r--r-- | docs/static/img/ocul-maps.png (renamed from static/img/ocul-maps.png) | bin | 354202 -> 354202 bytes | |||
-rw-r--r-- | docs/static/img/petanikode.png (renamed from static/img/petanikode.png) | bin | 39711 -> 39711 bytes | |||
-rw-r--r-- | docs/static/img/peteraba-tn.jpg (renamed from static/img/peteraba-tn.jpg) | bin | 43715 -> 43715 bytes | |||
-rw-r--r-- | docs/static/img/picturingjordan-tn.png (renamed from static/img/picturingjordan-tn.png) | bin | 55661 -> 55661 bytes | |||
-rw-r--r-- | docs/static/img/promotive.png (renamed from static/img/promotive.png) | bin | 69433 -> 69433 bytes | |||
-rw-r--r-- | docs/static/img/quickstart/bookshelf-bleak-theme.png (renamed from static/img/quickstart/bookshelf-bleak-theme.png) | bin | 9737 -> 9737 bytes | |||
-rw-r--r-- | docs/static/img/quickstart/bookshelf-disqus.png (renamed from static/img/quickstart/bookshelf-disqus.png) | bin | 27503 -> 27503 bytes | |||
-rw-r--r-- | docs/static/img/quickstart/bookshelf-new-default-image.png (renamed from static/img/quickstart/bookshelf-new-default-image.png) | bin | 175389 -> 175389 bytes | |||
-rw-r--r-- | docs/static/img/quickstart/bookshelf-only-picture.png (renamed from static/img/quickstart/bookshelf-only-picture.png) | bin | 168101 -> 168101 bytes | |||
-rw-r--r-- | docs/static/img/quickstart/bookshelf-robust-theme.png (renamed from static/img/quickstart/bookshelf-robust-theme.png) | bin | 51542 -> 51542 bytes | |||
-rw-r--r-- | docs/static/img/quickstart/bookshelf-updated-config.png (renamed from static/img/quickstart/bookshelf-updated-config.png) | bin | 54264 -> 54264 bytes | |||
-rw-r--r-- | docs/static/img/quickstart/bookshelf.png (renamed from static/img/quickstart/bookshelf.png) | bin | 249181 -> 249181 bytes | |||
-rw-r--r-- | docs/static/img/quickstart/default.jpg (renamed from static/img/quickstart/default.jpg) | bin | 307075 -> 307075 bytes | |||
-rw-r--r-- | docs/static/img/rahulrai_in-tn.png (renamed from static/img/rahulrai_in-tn.png) | bin | 26526 -> 26526 bytes | |||
-rw-r--r-- | docs/static/img/rakutentech-tn.png (renamed from static/img/rakutentech-tn.png) | bin | 35257 -> 35257 bytes | |||
-rw-r--r-- | docs/static/img/rdegges-tn.png (renamed from static/img/rdegges-tn.png) | bin | 11456 -> 11456 bytes | |||
-rw-r--r-- | docs/static/img/readtext-tn.png (renamed from static/img/readtext-tn.png) | bin | 70494 -> 70494 bytes | |||
-rw-r--r-- | docs/static/img/richardsumilang-tn.png (renamed from static/img/richardsumilang-tn.png) | bin | 26669 -> 26669 bytes | |||
-rw-r--r-- | docs/static/img/rick_cogley_info-tn.jpg (renamed from static/img/rick_cogley_info-tn.jpg) | bin | 73886 -> 73886 bytes | |||
-rw-r--r-- | docs/static/img/ridingbytes-tn.png (renamed from static/img/ridingbytes-tn.png) | bin | 66491 -> 66491 bytes | |||
-rw-r--r-- | docs/static/img/robertbasic-tn.png (renamed from static/img/robertbasic-tn.png) | bin | 28466 -> 28466 bytes | |||
-rw-r--r-- | docs/static/img/sanjay-saxena-tn.png (renamed from static/img/sanjay-saxena-tn.png) | bin | 227467 -> 227467 bytes | |||
-rw-r--r-- | docs/static/img/scottcwilson-tn.png (renamed from static/img/scottcwilson-tn.png) | bin | 15772 -> 15772 bytes | |||
-rw-r--r-- | docs/static/img/shapeshed-tn.png (renamed from static/img/shapeshed-tn.png) | bin | 17711 -> 17711 bytes | |||
-rw-r--r-- | docs/static/img/shelan-tn.png (renamed from static/img/shelan-tn.png) | bin | 26929 -> 26929 bytes | |||
-rw-r--r-- | docs/static/img/siba-tn.png (renamed from static/img/siba-tn.png) | bin | 185479 -> 185479 bytes | |||
-rw-r--r-- | docs/static/img/silvergeko.jpg (renamed from static/img/silvergeko.jpg) | bin | 50233 -> 50233 bytes | |||
-rw-r--r-- | docs/static/img/softinio-tn.png (renamed from static/img/softinio-tn.png) | bin | 45917 -> 45917 bytes | |||
-rw-r--r-- | docs/static/img/spf13-tn.jpg (renamed from static/img/spf13-tn.jpg) | bin | 33008 -> 33008 bytes | |||
-rw-r--r-- | docs/static/img/steambap.png (renamed from static/img/steambap.png) | bin | 19834 -> 19834 bytes | |||
-rw-r--r-- | docs/static/img/stefano.chiodino-tn.jpg (renamed from static/img/stefano.chiodino-tn.jpg) | bin | 56236 -> 56236 bytes | |||
-rw-r--r-- | docs/static/img/stou-tn.png (renamed from static/img/stou-tn.png) | bin | 50903 -> 50903 bytes | |||
-rw-r--r-- | docs/static/img/szymonkatra-tn.png (renamed from static/img/szymonkatra-tn.png) | bin | 30284 -> 30284 bytes | |||
-rw-r--r-- | docs/static/img/techmadeplain-tn.jpg (renamed from static/img/techmadeplain-tn.jpg) | bin | 50148 -> 50148 bytes | |||
-rw-r--r-- | docs/static/img/tendermint-tn.jpg (renamed from static/img/tendermint-tn.jpg) | bin | 28086 -> 28086 bytes | |||
-rw-r--r-- | docs/static/img/thecodeking-tn.jpg (renamed from static/img/thecodeking-tn.jpg) | bin | 58170 -> 58170 bytes | |||
-rw-r--r-- | docs/static/img/thehome-tn.png (renamed from static/img/thehome-tn.png) | bin | 51678 -> 51678 bytes | |||
-rw-r--r-- | docs/static/img/thislittleduck-tn.png (renamed from static/img/thislittleduck-tn.png) | bin | 84150 -> 84150 bytes | |||
-rw-r--r-- | docs/static/img/tibobeijen-nl-tn.png (renamed from static/img/tibobeijen-nl-tn.png) | bin | 72081 -> 72081 bytes | |||
-rw-r--r-- | docs/static/img/ttsreader-tn.png (renamed from static/img/ttsreader-tn.png) | bin | 139135 -> 139135 bytes | |||
-rw-r--r-- | docs/static/img/tutorialonfly-tn.jpg (renamed from static/img/tutorialonfly-tn.jpg) | bin | 27467 -> 27467 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/automated-deployments/adding-a-deploy-pipeline.png (renamed from static/img/tutorials/automated-deployments/adding-a-deploy-pipeline.png) | bin | 69238 -> 69238 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/automated-deployments/adding-a-deploy-step.png (renamed from static/img/tutorials/automated-deployments/adding-a-deploy-step.png) | bin | 50990 -> 50990 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/automated-deployments/adding-the-project-to-github.png (renamed from static/img/tutorials/automated-deployments/adding-the-project-to-github.png) | bin | 67637 -> 67637 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/automated-deployments/creating-a-basic-hugo-site.png (renamed from static/img/tutorials/automated-deployments/creating-a-basic-hugo-site.png) | bin | 34409 -> 34409 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/automated-deployments/public-or-not.png (renamed from static/img/tutorials/automated-deployments/public-or-not.png) | bin | 16659 -> 16659 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/automated-deployments/using-hugo-build.png (renamed from static/img/tutorials/automated-deployments/using-hugo-build.png) | bin | 14897 -> 14897 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/automated-deployments/wercker-access.png (renamed from static/img/tutorials/automated-deployments/wercker-access.png) | bin | 60815 -> 60815 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/automated-deployments/wercker-add-app.png (renamed from static/img/tutorials/automated-deployments/wercker-add-app.png) | bin | 46966 -> 46966 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/automated-deployments/wercker-git-connections.png (renamed from static/img/tutorials/automated-deployments/wercker-git-connections.png) | bin | 27003 -> 27003 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/automated-deployments/wercker-search.png (renamed from static/img/tutorials/automated-deployments/wercker-search.png) | bin | 31555 -> 31555 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/automated-deployments/wercker-select-repository.png (renamed from static/img/tutorials/automated-deployments/wercker-select-repository.png) | bin | 92355 -> 92355 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/automated-deployments/wercker-sign-up-page.png (renamed from static/img/tutorials/automated-deployments/wercker-sign-up-page.png) | bin | 14867 -> 14867 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/automated-deployments/wercker-sign-up.png (renamed from static/img/tutorials/automated-deployments/wercker-sign-up.png) | bin | 57317 -> 57317 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/automated-deployments/werckeryml.png (renamed from static/img/tutorials/automated-deployments/werckeryml.png) | bin | 126015 -> 126015 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/hosting-on-bitbucket/bitbucket-blog-post.png (renamed from static/img/tutorials/hosting-on-bitbucket/bitbucket-blog-post.png) | bin | 37585 -> 37585 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/hosting-on-bitbucket/bitbucket-create-repo.png (renamed from static/img/tutorials/hosting-on-bitbucket/bitbucket-create-repo.png) | bin | 24689 -> 24689 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/how-to-contribute-to-hugo/accept-cla.png (renamed from static/img/tutorials/how-to-contribute-to-hugo/accept-cla.png) | bin | 33286 -> 33286 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/how-to-contribute-to-hugo/ci-errors.png (renamed from static/img/tutorials/how-to-contribute-to-hugo/ci-errors.png) | bin | 124801 -> 124801 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/how-to-contribute-to-hugo/copy-remote-url.png (renamed from static/img/tutorials/how-to-contribute-to-hugo/copy-remote-url.png) | bin | 10570 -> 10570 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/how-to-contribute-to-hugo/forking-a-repository.png (renamed from static/img/tutorials/how-to-contribute-to-hugo/forking-a-repository.png) | bin | 6759 -> 6759 bytes | |||
-rw-r--r-- | docs/static/img/tutorials/how-to-contribute-to-hugo/open-pull-request.png (renamed from static/img/tutorials/how-to-contribute-to-hugo/open-pull-request.png) | bin | 59990 -> 59990 bytes | |||
-rw-r--r-- | docs/static/img/tutswiki-tn.jpg (renamed from static/img/tutswiki-tn.jpg) | bin | 227784 -> 227784 bytes | |||
-rw-r--r-- | docs/static/img/ucsb-tn.jpg (renamed from static/img/ucsb-tn.jpg) | bin | 80710 -> 80710 bytes | |||
-rw-r--r-- | docs/static/img/upbeat.png (renamed from static/img/upbeat.png) | bin | 32126 -> 32126 bytes | |||
-rw-r--r-- | docs/static/img/vamp_landingpage-tn.png (renamed from static/img/vamp_landingpage-tn.png) | bin | 21596 -> 21596 bytes | |||
-rw-r--r-- | docs/static/img/viglug-tn.png (renamed from static/img/viglug-tn.png) | bin | 14675 -> 14675 bytes | |||
-rw-r--r-- | docs/static/img/vurt.co-tn.jpg (renamed from static/img/vurt.co-tn.jpg) | bin | 36001 -> 36001 bytes | |||
-rw-r--r-- | docs/static/img/worldtowriters-com.jpg (renamed from static/img/worldtowriters-com.jpg) | bin | 27730 -> 27730 bytes | |||
-rw-r--r-- | docs/static/img/yslow-rules-tn.png (renamed from static/img/yslow-rules-tn.png) | bin | 21054 -> 21054 bytes | |||
-rw-r--r-- | docs/static/img/ysqi-blog.png (renamed from static/img/ysqi-blog.png) | bin | 29341 -> 29341 bytes | |||
-rw-r--r-- | docs/static/img/yulinling-tn.jpg (renamed from static/img/yulinling-tn.jpg) | bin | 78819 -> 78819 bytes | |||
-rw-r--r-- | docs/static/js/livereload.js (renamed from static/js/livereload.js) | 0 | ||||
-rw-r--r-- | docs/static/js/owl.carousel-custom.js (renamed from static/js/owl.carousel-custom.js) | 0 | ||||
-rw-r--r-- | docs/static/js/scripts.js (renamed from static/js/scripts.js) | 0 | ||||
-rw-r--r-- | docs/static/share/hugo-tall.png (renamed from static/share/hugo-tall.png) | bin | 9971 -> 9971 bytes | |||
-rw-r--r-- | docs/static/share/made-with-hugo-dark.png (renamed from static/share/made-with-hugo-dark.png) | bin | 8764 -> 8764 bytes | |||
-rw-r--r-- | docs/static/share/made-with-hugo-long-dark.png (renamed from static/share/made-with-hugo-long-dark.png) | bin | 9116 -> 9116 bytes | |||
-rw-r--r-- | docs/static/share/made-with-hugo-long.png (renamed from static/share/made-with-hugo-long.png) | bin | 9318 -> 9318 bytes | |||
-rw-r--r-- | docs/static/share/made-with-hugo.png (renamed from static/share/made-with-hugo.png) | bin | 8900 -> 8900 bytes | |||
-rw-r--r-- | docs/static/share/powered-by-hugo-dark.png (renamed from static/share/powered-by-hugo-dark.png) | bin | 3545 -> 3545 bytes | |||
-rw-r--r-- | docs/static/share/powered-by-hugo-long-dark.png (renamed from static/share/powered-by-hugo-long-dark.png) | bin | 3857 -> 3857 bytes | |||
-rw-r--r-- | docs/static/share/powered-by-hugo-long.png (renamed from static/share/powered-by-hugo-long.png) | bin | 3773 -> 3773 bytes | |||
-rw-r--r-- | docs/static/share/powered-by-hugo.png (renamed from static/share/powered-by-hugo.png) | bin | 3527 -> 3527 bytes | |||
-rw-r--r-- | docs/static/vendor/OwlCarousel2/LICENSE (renamed from static/vendor/OwlCarousel2/LICENSE) | 0 | ||||
-rw-r--r-- | docs/static/vendor/OwlCarousel2/css/owl.carousel.css (renamed from static/vendor/OwlCarousel2/css/owl.carousel.css) | 0 | ||||
-rw-r--r-- | docs/static/vendor/OwlCarousel2/css/owl.theme.default.css (renamed from static/vendor/OwlCarousel2/css/owl.theme.default.css) | 0 | ||||
-rw-r--r-- | docs/static/vendor/OwlCarousel2/js/owl.carousel.min.js (renamed from static/vendor/OwlCarousel2/js/owl.carousel.min.js) | 0 | ||||
-rw-r--r-- | docs/static/vendor/OwlCarousel2/notes.txt (renamed from static/vendor/OwlCarousel2/notes.txt) | 0 | ||||
-rw-r--r-- | docs/static/vendor/dieulot/js/instantclick.min.js (renamed from static/vendor/dieulot/js/instantclick.min.js) | 0 | ||||
-rw-r--r-- | docs/static/vendor/flesler/js/jquery.scrollTo.min.js (renamed from static/vendor/flesler/js/jquery.scrollTo.min.js) | 0 | ||||
-rw-r--r-- | docs/static/vendor/font-awesome/css/font-awesome.min.css (renamed from static/vendor/font-awesome/css/font-awesome.min.css) | 0 | ||||
-rw-r--r-- | docs/static/vendor/font-awesome/fonts/FontAwesome.otf (renamed from static/vendor/font-awesome/fonts/FontAwesome.otf) | bin | 109688 -> 109688 bytes | |||
-rw-r--r-- | docs/static/vendor/font-awesome/fonts/fontawesome-webfont.eot (renamed from static/vendor/font-awesome/fonts/fontawesome-webfont.eot) | bin | 70807 -> 70807 bytes | |||
-rw-r--r-- | docs/static/vendor/font-awesome/fonts/fontawesome-webfont.svg (renamed from static/vendor/font-awesome/fonts/fontawesome-webfont.svg) | 0 | ||||
-rw-r--r-- | docs/static/vendor/font-awesome/fonts/fontawesome-webfont.ttf (renamed from static/vendor/font-awesome/fonts/fontawesome-webfont.ttf) | bin | 142072 -> 142072 bytes | |||
-rw-r--r-- | docs/static/vendor/font-awesome/fonts/fontawesome-webfont.woff (renamed from static/vendor/font-awesome/fonts/fontawesome-webfont.woff) | bin | 83588 -> 83588 bytes | |||
-rw-r--r-- | docs/static/vendor/font-awesome/fonts/fontawesome-webfont.woff2 (renamed from static/vendor/font-awesome/fonts/fontawesome-webfont.woff2) | bin | 66624 -> 66624 bytes | |||
-rw-r--r-- | docs/static/vendor/highlightjs/css/monokai-sublime.css (renamed from static/vendor/highlightjs/css/monokai-sublime.css) | 0 | ||||
-rw-r--r-- | docs/static/vendor/highlightjs/js/highlight.pack.js (renamed from static/vendor/highlightjs/js/highlight.pack.js) | 0 | ||||
-rw-r--r-- | docs/static/vendor/highlightjs/notes.txt (renamed from static/vendor/highlightjs/notes.txt) | 0 | ||||
-rw-r--r-- | docs/static/vendor/jquery/js/jquery-2.1.4.min.js (renamed from static/vendor/jquery/js/jquery-2.1.4.min.js) | 0 | ||||
-rw-r--r-- | docs/static/vendor/twitter/js/bootstrap.min.js (renamed from static/vendor/twitter/js/bootstrap.min.js) | 0 | ||||
-rw-r--r-- | docs/temp/0.22.1-relnotes.md (renamed from temp/0.22.1-relnotes.md) | 0 | ||||
-rw-r--r-- | docshelper/docs.go | 32 | ||||
-rw-r--r-- | examples/blog/.gitignore | 12 | ||||
-rw-r--r-- | examples/blog/README.md | 42 | ||||
-rw-r--r-- | examples/blog/config.toml | 4 | ||||
-rw-r--r-- | examples/blog/content/post/another-post.md | 57 | ||||
-rw-r--r-- | examples/blog/content/post/hello-hugo.md | 61 | ||||
-rw-r--r-- | examples/blog/layouts/_default/single.html | 21 | ||||
-rw-r--r-- | examples/blog/layouts/index.html | 19 | ||||
-rw-r--r-- | examples/blog/layouts/indexes/category.html | 24 | ||||
-rw-r--r-- | examples/blog/layouts/indexes/post.html | 24 | ||||
-rw-r--r-- | examples/blog/layouts/indexes/tag.html | 24 | ||||
-rw-r--r-- | examples/blog/layouts/partials/footer.copyright.html | 9 | ||||
-rw-r--r-- | examples/blog/layouts/partials/footer.html | 5 | ||||
-rw-r--r-- | examples/blog/layouts/partials/header.html | 10 | ||||
-rw-r--r-- | examples/blog/layouts/partials/header.includes.html | 4 | ||||
-rw-r--r-- | examples/blog/layouts/partials/menu.html | 15 | ||||
-rw-r--r-- | examples/blog/layouts/partials/meta.html | 6 | ||||
-rw-r--r-- | examples/blog/layouts/partials/navbar.html | 22 | ||||
-rw-r--r-- | examples/blog/layouts/post/li.html | 4 | ||||
-rw-r--r-- | examples/blog/layouts/post/single.html | 35 | ||||
-rw-r--r-- | examples/blog/layouts/post/summary.html | 9 | ||||
-rw-r--r-- | examples/blog/static/css/bootstrap.min.css | 11 | ||||
-rw-r--r-- | examples/blog/static/css/custom.css | 7 | ||||
-rw-r--r-- | examples/blog/static/css/font-awesome.css | 2086 | ||||
-rw-r--r-- | examples/blog/static/fonts/FontAwesome.otf | bin | 0 -> 109688 bytes | |||
-rw-r--r-- | examples/blog/static/fonts/fontawesome-webfont.eot | bin | 0 -> 70807 bytes | |||
-rw-r--r-- | examples/blog/static/fonts/fontawesome-webfont.svg | 655 | ||||
-rw-r--r-- | examples/blog/static/fonts/fontawesome-webfont.ttf | bin | 0 -> 142072 bytes | |||
-rw-r--r-- | examples/blog/static/fonts/fontawesome-webfont.woff | bin | 0 -> 83588 bytes | |||
-rw-r--r-- | examples/blog/static/fonts/fontawesome-webfont.woff2 | bin | 0 -> 66624 bytes | |||
-rw-r--r-- | examples/blog/static/fonts/glyphicons-halflings-regular.eot | bin | 0 -> 20127 bytes | |||
-rw-r--r-- | examples/blog/static/fonts/glyphicons-halflings-regular.svg | 288 | ||||
-rw-r--r-- | examples/blog/static/fonts/glyphicons-halflings-regular.ttf | bin | 0 -> 45404 bytes | |||
-rw-r--r-- | examples/blog/static/fonts/glyphicons-halflings-regular.woff | bin | 0 -> 23424 bytes | |||
-rw-r--r-- | examples/blog/static/fonts/glyphicons-halflings-regular.woff2 | bin | 0 -> 18028 bytes | |||
-rw-r--r-- | examples/blog/static/js/bootstrap.js | 2363 | ||||
-rw-r--r-- | examples/blog/static/js/jquery-1.11.3.min.js | 5 | ||||
-rw-r--r-- | examples/multilingual/.gitignore | 1 | ||||
-rw-r--r-- | examples/multilingual/README.md | 15 | ||||
-rw-r--r-- | examples/multilingual/config.toml | 39 | ||||
-rw-r--r-- | examples/multilingual/content/about.en.md | 12 | ||||
-rw-r--r-- | examples/multilingual/content/about.et.md | 12 | ||||
-rw-r--r-- | examples/multilingual/content/index.en.md | 10 | ||||
-rw-r--r-- | examples/multilingual/content/index.et.md | 10 | ||||
-rw-r--r-- | examples/multilingual/content/story/alpha.en.md | 14 | ||||
-rw-r--r-- | examples/multilingual/content/story/beta.en.md | 14 | ||||
-rw-r--r-- | examples/multilingual/content/story/index.en.md | 5 | ||||
-rw-r--r-- | examples/multilingual/content/uudis/alfa.et.md | 15 | ||||
-rw-r--r-- | examples/multilingual/content/uudis/beeta.et.md | 15 | ||||
-rw-r--r-- | examples/multilingual/content/uudis/index.et.md | 5 | ||||
-rw-r--r-- | examples/multilingual/i18n/en.toml | 2 | ||||
-rw-r--r-- | examples/multilingual/i18n/et.toml | 2 | ||||
-rw-r--r-- | examples/multilingual/layouts/_default/single.html | 4 | ||||
-rw-r--r-- | examples/multilingual/layouts/index.html | 1 | ||||
-rw-r--r-- | examples/multilingual/layouts/partials/footer.html | 3 | ||||
-rw-r--r-- | examples/multilingual/layouts/partials/head.html | 11 | ||||
-rw-r--r-- | examples/multilingual/layouts/partials/header.html | 17 | ||||
-rw-r--r-- | examples/multilingual/layouts/story/single.html | 17 | ||||
-rw-r--r-- | examples/multilingual/layouts/uudis/single.html | 17 | ||||
-rw-r--r-- | examples/multilingual/static/main.css | 90 | ||||
-rw-r--r-- | goreleaser.yml | 48 | ||||
-rw-r--r-- | helpers/baseURL.go | 73 | ||||
-rw-r--r-- | helpers/baseURL_test.go | 61 | ||||
-rw-r--r-- | helpers/content.go | 682 | ||||
-rw-r--r-- | helpers/content_renderer.go | 127 | ||||
-rw-r--r-- | helpers/content_renderer_test.go | 133 | ||||
-rw-r--r-- | helpers/content_test.go | 462 | ||||
-rw-r--r-- | helpers/emoji.go | 91 | ||||
-rw-r--r-- | helpers/emoji_test.go | 147 | ||||
-rw-r--r-- | helpers/general.go | 357 | ||||
-rw-r--r-- | helpers/general_test.go | 216 | ||||
-rw-r--r-- | helpers/hugo.go | 136 | ||||
-rw-r--r-- | helpers/hugo_test.go | 51 | ||||
-rw-r--r-- | helpers/language.go | 160 | ||||
-rw-r--r-- | helpers/language_test.go | 33 | ||||
-rw-r--r-- | helpers/path.go | 612 | ||||
-rw-r--r-- | helpers/path_test.go | 828 | ||||
-rw-r--r-- | helpers/pathspec.go | 119 | ||||
-rw-r--r-- | helpers/pathspec_test.go | 62 | ||||
-rw-r--r-- | helpers/pygments.go | 237 | ||||
-rw-r--r-- | helpers/pygments_test.go | 95 | ||||
-rw-r--r-- | helpers/testhelpers_test.go | 38 | ||||
-rw-r--r-- | helpers/url.go | 392 | ||||
-rw-r--r-- | helpers/url_test.go | 321 | ||||
-rw-r--r-- | hugofs/fs.go | 79 | ||||
-rw-r--r-- | hugofs/fs_test.go | 60 | ||||
-rw-r--r-- | hugolib/404_test.go | 43 | ||||
-rw-r--r-- | hugolib/alias.go | 183 | ||||
-rw-r--r-- | hugolib/alias_test.go | 152 | ||||
-rw-r--r-- | hugolib/author.go | 45 | ||||
-rw-r--r-- | hugolib/case_insensitive_test.go | 306 | ||||
-rw-r--r-- | hugolib/config.go | 135 | ||||
-rw-r--r-- | hugolib/config_test.go | 43 | ||||
-rw-r--r-- | hugolib/datafiles_test.go | 154 | ||||
-rw-r--r-- | hugolib/disableKinds_test.go | 221 | ||||
-rw-r--r-- | hugolib/embedded_shortcodes_test.go | 409 | ||||
-rw-r--r-- | hugolib/gitinfo.go | 69 | ||||
-rw-r--r-- | hugolib/handler_base.go | 60 | ||||
-rw-r--r-- | hugolib/handler_file.go | 59 | ||||
-rw-r--r-- | hugolib/handler_meta.go | 117 | ||||
-rw-r--r-- | hugolib/handler_page.go | 147 | ||||
-rw-r--r-- | hugolib/handler_test.go | 77 | ||||
-rw-r--r-- | hugolib/hugo_info.go | 49 | ||||
-rw-r--r-- | hugolib/hugo_sites.go | 633 | ||||
-rw-r--r-- | hugolib/hugo_sites_build.go | 237 | ||||
-rw-r--r-- | hugolib/hugo_sites_build_test.go | 1320 | ||||
-rw-r--r-- | hugolib/media.go | 60 | ||||
-rw-r--r-- | hugolib/menu.go | 215 | ||||
-rw-r--r-- | hugolib/menu_old_test.go | 642 | ||||
-rw-r--r-- | hugolib/menu_test.go | 96 | ||||
-rw-r--r-- | hugolib/multilingual.go | 117 | ||||
-rw-r--r-- | hugolib/node_as_page_test.go | 831 | ||||
-rw-r--r-- | hugolib/page.go | 1776 | ||||
-rw-r--r-- | hugolib/pageCache.go | 108 | ||||
-rw-r--r-- | hugolib/pageCache_test.go | 73 | ||||
-rw-r--r-- | hugolib/pageGroup.go | 297 | ||||
-rw-r--r-- | hugolib/pageGroup_test.go | 457 | ||||
-rw-r--r-- | hugolib/pageSort.go | 303 | ||||
-rw-r--r-- | hugolib/pageSort_test.go | 202 | ||||
-rw-r--r-- | hugolib/page_collections.go | 181 | ||||
-rw-r--r-- | hugolib/page_collections_test.go | 140 | ||||
-rw-r--r-- | hugolib/page_output.go | 273 | ||||
-rw-r--r-- | hugolib/page_paths.go | 256 | ||||
-rw-r--r-- | hugolib/page_paths_test.go | 190 | ||||
-rw-r--r-- | hugolib/page_permalink_test.go | 100 | ||||
-rw-r--r-- | hugolib/page_taxonomy_test.go | 96 | ||||
-rw-r--r-- | hugolib/page_test.go | 1502 | ||||
-rw-r--r-- | hugolib/page_time_integration_test.go | 183 | ||||
-rw-r--r-- | hugolib/pagesPrevNext.go | 40 | ||||
-rw-r--r-- | hugolib/pagesPrevNext_test.go | 86 | ||||
-rw-r--r-- | hugolib/pagination.go | 535 | ||||
-rw-r--r-- | hugolib/pagination_test.go | 579 | ||||
-rw-r--r-- | hugolib/path_separators_test.go | 38 | ||||
-rw-r--r-- | hugolib/permalinker.go | 25 | ||||
-rw-r--r-- | hugolib/permalinks.go | 209 | ||||
-rw-r--r-- | hugolib/permalinks_test.go | 94 | ||||
-rw-r--r-- | hugolib/robotstxt_test.go | 46 | ||||
-rw-r--r-- | hugolib/rss_test.go | 59 | ||||
-rw-r--r-- | hugolib/scratch.go | 127 | ||||
-rw-r--r-- | hugolib/scratch_test.go | 161 | ||||
-rw-r--r-- | hugolib/shortcode.go | 716 | ||||
-rw-r--r-- | hugolib/shortcode_test.go | 845 | ||||
-rw-r--r-- | hugolib/shortcodeparser.go | 588 | ||||
-rw-r--r-- | hugolib/shortcodeparser_test.go | 202 | ||||
-rw-r--r-- | hugolib/site.go | 2127 | ||||
-rw-r--r-- | hugolib/siteJSONEncode_test.go | 51 | ||||
-rw-r--r-- | hugolib/site_benchmark_test.go | 254 | ||||
-rw-r--r-- | hugolib/site_output.go | 99 | ||||
-rw-r--r-- | hugolib/site_output_test.go | 365 | ||||
-rw-r--r-- | hugolib/site_render.go | 400 | ||||
-rw-r--r-- | hugolib/site_sections.go | 306 | ||||
-rw-r--r-- | hugolib/site_sections_test.go | 266 | ||||
-rw-r--r-- | hugolib/site_test.go | 1100 | ||||
-rw-r--r-- | hugolib/site_url_test.go | 90 | ||||
-rw-r--r-- | hugolib/sitemap.go | 45 | ||||
-rw-r--r-- | hugolib/sitemap_test.go | 102 | ||||
-rw-r--r-- | hugolib/taxonomy.go | 224 | ||||
-rw-r--r-- | hugolib/taxonomy_test.go | 187 | ||||
-rw-r--r-- | hugolib/template_engines_test.go | 95 | ||||
-rw-r--r-- | hugolib/template_test.go | 215 | ||||
-rw-r--r-- | hugolib/testdata/redis.cn.md | 697 | ||||
-rw-r--r-- | hugolib/testhelpers_test.go | 214 | ||||
-rw-r--r-- | hugolib/translations.go | 75 | ||||
-rw-r--r-- | i18n/i18n.go | 113 | ||||
-rw-r--r-- | i18n/i18n_test.go | 177 | ||||
-rw-r--r-- | i18n/translationProvider.go | 73 | ||||
-rw-r--r-- | livereload/connection.go | 66 | ||||
-rw-r--r-- | livereload/hub.go | 56 | ||||
-rw-r--r-- | livereload/livereload.go | 89 | ||||
-rw-r--r-- | main.go | 38 | ||||
-rw-r--r-- | media/docshelper.go | 17 | ||||
-rw-r--r-- | media/mediaType.go | 203 | ||||
-rw-r--r-- | media/mediaType_test.go | 139 | ||||
-rw-r--r-- | output/docshelper.go | 86 | ||||
-rw-r--r-- | output/layout.go | 268 | ||||
-rw-r--r-- | output/layout_base.go | 213 | ||||
-rw-r--r-- | output/layout_base_test.go | 168 | ||||
-rw-r--r-- | output/layout_test.go | 132 | ||||
-rw-r--r-- | output/outputFormat.go | 328 | ||||
-rw-r--r-- | output/outputFormat_test.go | 226 | ||||
-rw-r--r-- | parser/frontmatter.go | 226 | ||||
-rw-r--r-- | parser/frontmatter_test.go | 398 | ||||
-rw-r--r-- | parser/long_text_test.md | 263 | ||||
-rw-r--r-- | parser/page.go | 408 | ||||
-rw-r--r-- | parser/page_test.go | 130 | ||||
-rw-r--r-- | parser/parse_frontmatter_test.go | 316 | ||||
-rw-r--r-- | releaser/git.go | 296 | ||||
-rw-r--r-- | releaser/git_test.go | 76 | ||||
-rw-r--r-- | releaser/github.go | 129 | ||||
-rw-r--r-- | releaser/github_test.go | 42 | ||||
-rw-r--r-- | releaser/releasenotes_writer.go | 248 | ||||
-rw-r--r-- | releaser/releasenotes_writer_test.go | 44 | ||||
-rw-r--r-- | releaser/releaser.go | 331 | ||||
-rw-r--r-- | releaser/releaser_test.go | 79 | ||||
-rw-r--r-- | requirements.txt | 1 | ||||
-rw-r--r-- | snapcraft.yaml | 37 | ||||
-rw-r--r-- | source/content_directory_test.go | 61 | ||||
-rw-r--r-- | source/file.go | 170 | ||||
-rw-r--r-- | source/file_test.go | 57 | ||||
-rw-r--r-- | source/filesystem.go | 181 | ||||
-rw-r--r-- | source/filesystem_test.go | 113 | ||||
-rw-r--r-- | source/filesystem_unix_test.go | 28 | ||||
-rw-r--r-- | source/filesystem_windows_test.go | 28 | ||||
-rw-r--r-- | source/inmemory.go | 23 | ||||
-rw-r--r-- | source/lazy_file_reader.go | 170 | ||||
-rw-r--r-- | source/lazy_file_reader_test.go | 236 | ||||
-rw-r--r-- | tpl/cast/cast.go | 51 | ||||
-rw-r--r-- | tpl/cast/cast_test.go | 83 | ||||
-rw-r--r-- | tpl/cast/docshelper.go | 41 | ||||
-rw-r--r-- | tpl/cast/init.go | 51 | ||||
-rw-r--r-- | tpl/cast/init_test.go | 38 | ||||
-rw-r--r-- | tpl/collections/apply.go | 150 | ||||
-rw-r--r-- | tpl/collections/apply_test.go | 64 | ||||
-rw-r--r-- | tpl/collections/collections.go | 708 | ||||
-rw-r--r-- | tpl/collections/collections_test.go | 716 | ||||
-rw-r--r-- | tpl/collections/index.go | 107 | ||||
-rw-r--r-- | tpl/collections/index_test.go | 60 | ||||
-rw-r--r-- | tpl/collections/init.go | 152 | ||||
-rw-r--r-- | tpl/collections/init_test.go | 38 | ||||
-rw-r--r-- | tpl/collections/sort.go | 161 | ||||
-rw-r--r-- | tpl/collections/sort_test.go | 237 | ||||
-rw-r--r-- | tpl/collections/where.go | 431 | ||||
-rw-r--r-- | tpl/collections/where_test.go | 606 | ||||
-rw-r--r-- | tpl/compare/compare.go | 207 | ||||
-rw-r--r-- | tpl/compare/compare_test.go | 200 | ||||
-rw-r--r-- | tpl/compare/init.go | 77 | ||||
-rw-r--r-- | tpl/compare/init_test.go | 38 | ||||
-rw-r--r-- | tpl/crypto/crypto.go | 64 | ||||
-rw-r--r-- | tpl/crypto/crypto_test.go | 103 | ||||
-rw-r--r-- | tpl/crypto/init.go | 59 | ||||
-rw-r--r-- | tpl/crypto/init_test.go | 38 | ||||
-rw-r--r-- | tpl/data/cache.go | 83 | ||||
-rw-r--r-- | tpl/data/cache_test.go | 63 | ||||
-rw-r--r-- | tpl/data/data.go | 138 | ||||
-rw-r--r-- | tpl/data/data_test.go | 251 | ||||
-rw-r--r-- | tpl/data/init.go | 45 | ||||
-rw-r--r-- | tpl/data/init_test.go | 38 | ||||
-rw-r--r-- | tpl/data/resources.go | 134 | ||||
-rw-r--r-- | tpl/data/resources_test.go | 174 | ||||
-rw-r--r-- | tpl/encoding/encoding.go | 61 | ||||
-rw-r--r-- | tpl/encoding/encoding_test.go | 109 | ||||
-rw-r--r-- | tpl/encoding/init.go | 59 | ||||
-rw-r--r-- | tpl/encoding/init_test.go | 38 | ||||
-rw-r--r-- | tpl/fmt/fmt.go | 40 | ||||
-rw-r--r-- | tpl/fmt/init.go | 58 | ||||
-rw-r--r-- | tpl/fmt/init_test.go | 38 | ||||
-rw-r--r-- | tpl/images/images.go | 82 | ||||
-rw-r--r-- | tpl/images/images_test.go | 121 | ||||
-rw-r--r-- | tpl/images/init.go | 42 | ||||
-rw-r--r-- | tpl/images/init_test.go | 38 | ||||
-rw-r--r-- | tpl/inflect/inflect.go | 76 | ||||
-rw-r--r-- | tpl/inflect/inflect_test.go | 48 | ||||
-rw-r--r-- | tpl/inflect/init.go | 61 | ||||
-rw-r--r-- | tpl/inflect/init_test.go | 38 | ||||
-rw-r--r-- | tpl/internal/templatefuncRegistry_test.go | 33 | ||||
-rw-r--r-- | tpl/internal/templatefuncsRegistry.go | 276 | ||||
-rw-r--r-- | tpl/lang/init.go | 52 | ||||
-rw-r--r-- | tpl/lang/init_test.go | 38 | ||||
-rw-r--r-- | tpl/lang/lang.go | 136 | ||||
-rw-r--r-- | tpl/lang/lang_test.go | 54 | ||||
-rw-r--r-- | tpl/math/init.go | 79 | ||||
-rw-r--r-- | tpl/math/init_test.go | 38 | ||||
-rw-r--r-- | tpl/math/math.go | 196 | ||||
-rw-r--r-- | tpl/math/math_test.go | 220 | ||||
-rw-r--r-- | tpl/os/init.go | 56 | ||||
-rw-r--r-- | tpl/os/init_test.go | 38 | ||||
-rw-r--r-- | tpl/os/os.go | 98 | ||||
-rw-r--r-- | tpl/os/os_test.go | 65 | ||||
-rw-r--r-- | tpl/partials/init.go | 49 | ||||
-rw-r--r-- | tpl/partials/init_test.go | 38 | ||||
-rw-r--r-- | tpl/partials/partials.go | 126 | ||||
-rw-r--r-- | tpl/safe/init.go | 81 | ||||
-rw-r--r-- | tpl/safe/init_test.go | 38 | ||||
-rw-r--r-- | tpl/safe/safe.go | 71 | ||||
-rw-r--r-- | tpl/safe/safe_test.go | 214 | ||||
-rw-r--r-- | tpl/strings/init.go | 142 | ||||
-rw-r--r-- | tpl/strings/init_test.go | 38 | ||||
-rw-r--r-- | tpl/strings/regexp.go | 109 | ||||
-rw-r--r-- | tpl/strings/regexp_test.go | 86 | ||||
-rw-r--r-- | tpl/strings/strings.go | 377 | ||||
-rw-r--r-- | tpl/strings/strings_test.go | 634 | ||||
-rw-r--r-- | tpl/strings/truncate.go | 156 | ||||
-rw-r--r-- | tpl/strings/truncate_test.go | 84 | ||||
-rw-r--r-- | tpl/template.go | 112 | ||||
-rw-r--r-- | tpl/time/init.go | 73 | ||||
-rw-r--r-- | tpl/time/init_test.go | 38 | ||||
-rw-r--r-- | tpl/time/time.go | 56 | ||||
-rw-r--r-- | tpl/time/time_test.go | 57 | ||||
-rw-r--r-- | tpl/tplimpl/ace.go | 51 | ||||
-rw-r--r-- | tpl/tplimpl/amber_compiler.go | 42 | ||||
-rw-r--r-- | tpl/tplimpl/template.go | 706 | ||||
-rw-r--r-- | tpl/tplimpl/templateFuncster.go | 77 | ||||
-rw-r--r-- | tpl/tplimpl/templateProvider.go | 59 | ||||
-rw-r--r-- | tpl/tplimpl/template_ast_transformers.go | 293 | ||||
-rw-r--r-- | tpl/tplimpl/template_ast_transformers_test.go | 290 | ||||
-rw-r--r-- | tpl/tplimpl/template_embedded.go | 289 | ||||
-rw-r--r-- | tpl/tplimpl/template_funcs.go | 67 | ||||
-rw-r--r-- | tpl/tplimpl/template_funcs_test.go | 266 | ||||
-rw-r--r-- | tpl/transform/init.go | 96 | ||||
-rw-r--r-- | tpl/transform/init_test.go | 38 | ||||
-rw-r--r-- | tpl/transform/transform.go | 114 | ||||
-rw-r--r-- | tpl/transform/transform_test.go | 206 | ||||
-rw-r--r-- | tpl/urls/init.go | 67 | ||||
-rw-r--r-- | tpl/urls/init_test.go | 38 | ||||
-rw-r--r-- | tpl/urls/urls.go | 111 | ||||
-rw-r--r-- | transform/absurl.go | 28 | ||||
-rw-r--r-- | transform/absurlreplacer.go | 312 | ||||
-rw-r--r-- | transform/chain.go | 104 | ||||
-rw-r--r-- | transform/chain_test.go | 258 | ||||
-rw-r--r-- | transform/hugogeneratorinject.go | 50 | ||||
-rw-r--r-- | transform/hugogeneratorinject_test.go | 59 | ||||
-rw-r--r-- | transform/livereloadinject.go | 38 | ||||
-rw-r--r-- | transform/livereloadinject_test.go | 39 | ||||
-rw-r--r-- | utils/utils.go | 59 | ||||
-rw-r--r-- | vendor/vendor.json | 453 | ||||
-rw-r--r-- | watcher/batcher.go | 70 |
855 files changed, 62330 insertions, 4 deletions
diff --git a/.gitignore b/.gitignore index 665360d49..47721b7cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,18 @@ -/.idea -/public +hugo
+docs/public*
+/.idea
+hugo.exe
+*.test
+*.prof
+nohup.out
+cover.out
+*.swp
+*.swo
+.DS_Store
+*~
+vendor/*/ +*.bench +coverage*.out + +GoBuilds +dist diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/.gitmodules diff --git a/.goxc.json b/.goxc.json new file mode 100644 index 000000000..10f19d226 --- /dev/null +++ b/.goxc.json @@ -0,0 +1,6 @@ +{ + "ArtifactsDest": "GoBuilds/", + "OutPath": "{{.Dest}}{{.PS}}{{.AppName}}{{.PS}}{{.Version}}{{.PS}}{{.AppName}}_{{.Version}}_{{.Os}}_{{.Arch}}{{.Ext}}", + "BuildConstraints": "linux windows darwin freebsd netbsd openbsd dragonfly", + "ConfigVersion": "0.9" +} diff --git a/.mailmap b/.mailmap new file mode 100644 index 000000000..e93adabc1 --- /dev/null +++ b/.mailmap @@ -0,0 +1,3 @@ +spf13 <[email protected]> Steve Francia <[email protected]> +bep <[email protected]> Bjørn Erik Pedersen <[email protected]> + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..bb47a93d6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: go +sudo: required +go: + - 1.7.6 + - 1.8.3 + - tip +os: + - linux + - osx +matrix: + allow_failures: + - go: tip + fast_finish: true +install: + - make vendor +script: + - make hugo-race check +before_install: + # gem install must be run with sudo on OSX + - sudo gem install asciidoctor | gem install asciidoctor + - sudo pip install docutils diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..0368e6666 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,161 @@ +# Contributing to Hugo + +We welcome contributions to Hugo of any kind including documentation, themes, +organization, tutorials, blog posts, bug reports, issues, feature requests, +feature implementations, pull requests, answering questions on the forum, +helping to manage issues, etc. + +The Hugo community and maintainers are [very active](https://github.com/gohugoio/hugo/pulse/monthly) and helpful, and the project benefits greatly from this activity. We created a [step by step guide](https://gohugo.io/tutorials/how-to-contribute-to-hugo/) if you're unfamiliar with GitHub or contributing to open source projects in general. + +## Table of Contents + +* [Asking Support Questions](#asking-support-questions) +* [Reporting Issues](#reporting-issues) +* [Submitting Patches](#submitting-patches) + * [Code Contribution Guidelines](#code-contribution-guidelines) + * [Git Commit Message Guidelines](#git-commit-message-guidelines) + * [Vendored Dependencies](#vendored-dependencies) + * [Fetching the Sources From GitHub](#fetching-the-sources-from-github) + * [Using Git Remotes](#using-git-remotes) + * [Build Hugo with Your Changes](#build-hugo-with-your-changes) + * [Updating the Hugo Sources](#updating-the-hugo-sources) + +## Asking Support Questions + +We have an active [discussion forum](https://discourse.gohugo.io) where users and developers can ask questions. +Please don't use the GitHub issue tracker to ask questions. + +## Reporting Issues + +If you believe you have found a defect in Hugo or its documentation, use +the GitHub [issue tracker](https://github.com/gohugoio/hugo/issues) to report the problem to the Hugo maintainers. +If you're not sure if it's a bug or not, start by asking in the [discussion forum](https://discourse.gohugo.io). +When reporting the issue, please provide the version of Hugo in use (`hugo version`) and your operating system. + +## Submitting Patches + +The Hugo project welcomes all contributors and contributions regardless of skill or experience level. +If you are interested in helping with the project, we will help you with your contribution. +Hugo is a very active project with many contributions happening daily. +Because we want to create the best possible product for our users and the best contribution experience for our developers, +we have a set of guidelines which ensure that all contributions are acceptable. +The guidelines are not intended as a filter or barrier to participation. +If you are unfamiliar with the contribution process, the Hugo team will help you and teach you how to bring your contribution in accordance with the guidelines. + +### Code Contribution Guidelines + +To make the contribution process as seamless as possible, we ask for the following: + +* Go ahead and fork the project and make your changes. We encourage pull requests to allow for review and discussion of code changes. +* When you’re ready to create a pull request, be sure to: + * Sign the [CLA](https://cla-assistant.io/gohugoio/hugo). + * Have test cases for the new code. If you have questions about how to do this, please ask in your pull request. + * Run `go fmt`. + * Add documentation if you are adding new features or changing functionality. The docs site lives in `/docs`. + * Squash your commits into a single commit. `git rebase -i`. It’s okay to force update your pull request with `git push -f`. + * Ensure that `make check` succeeds. [Travis CI](https://travis-ci.org/gohugoio/hugo) (Linux and macOS) and [AppVeyor](https://ci.appveyor.com/project/gohugoio/hugo/branch/master) (Windows) will fail the build if `make check` fails. + * Follow the **Git Commit Message Guidelines** below. + +### Git Commit Message Guidelines + +This [blog article](http://chris.beams.io/posts/git-commit/) is a good resource for learning how to write good commit messages, +the most important part being that each commit message should have a title/subject in imperative mood starting with a capital letter and no trailing period: +*"Return error on wrong use of the Paginator"*, **NOT** *"returning some error."* + +Also, if your commit references one or more GitHub issues, always end your commit message body with *See #1234* or *Fixes #1234*. +Replace *1234* with the GitHub issue ID. The last example will close the issue when the commit is merged into *master*. + +Sometimes it makes sense to prefix the commit message with the packagename (or docs folder) all lowercased ending with a colon. +That is fine, but the rest of the rules above apply. +So it is "tpl: Add emojify template func", not "tpl: add emojify template func.", and "docs: Document emoji", not "doc: document emoji." + +Please consider to use a short and descriptive branch name, e.g. **NOT** "patch-1". It's very common but creates a naming conflict each time when a submission is pulled for a review. + +An example: + +```text +tpl: Add custom index function + +Add a custom index template function that deviates from the stdlib simply by not +returning an "index out of range" error if an array, slice or string index is +out of range. Instead, we just return nil values. This should help make the +new default function more useful for Hugo users. + +Fixes #1949 +``` + +### Vendored Dependencies + +Hugo uses [govendor](https://github.com/kardianos/govendor) to vendor dependencies, but we don't commit the vendored packages themselves to the Hugo git repository. +Therefore, a simple `go get` is not supported since `go get` is not vendor-aware. +You **must use govendor** to fetch and manage Hugo's dependencies. + +### Fetch the Sources From GitHub + +``` +go get github.com/kardianos/govendor +govendor get github.com/gohugoio/hugo +``` + +### Using Git Remotes + +Due to the way Go handles package imports, the best approach for working on a +Hugo fork is to use Git Remotes. Here's a simple walk-through for getting +started: + +1. Fetch the Hugo sources as described above. + +1. Change to the Hugo source directory: + + ``` + cd $HOME/go/src/github.com/gohugoio/hugo + ``` + +1. Create a new branch for your changes (the branch name is arbitrary): + + ``` + git checkout -b iss1234 + ``` + +1. After making your changes, commit them to your new branch: + + ``` + git commit -a -v + ``` + +1. Fork Hugo in GitHub. + +1. Add your fork as a new remote (the remote name, "fork" in this example, is arbitrary): + + ``` + git remote add fork git://github.com/USERNAME/hugo.git + ``` + +1. Push the changes to your new remote: + + ``` + git push --set-upstream fork iss1234 + ``` + +1. You're now ready to submit a PR based upon the new branch in your forked repository. + +### Build Hugo with Your Changes + +```bash +cd $HOME/go/src/github.com/gohugoio/hugo +make hugo +# or to install in $HOME/go/bin: +make install +``` + +### Updating the Hugo Sources + +If you want to stay in sync with the Hugo repository, you can easily pull down +the source changes, but you'll need to keep the vendored packages up-to-date as +well. + +``` +git pull +make vendor +``` + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..67dd91209 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.8-alpine + +ENV GOPATH /go +ENV USER root + +RUN apk update && apk add git make + +# pre-install known dependencies before the source, so we don't redownload them whenever the source changes +RUN go get github.com/kardianos/govendor \ + && govendor get github.com/gohugoio/hugo + +COPY . $GOPATH/src/github.com/gohugoio/hugo + +RUN cd $GOPATH/src/github.com/gohugoio/hugo \ + && make install test diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..b62a9b5ff --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,194 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<<http://www.apache.org/licenses/>>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..4508ab7cf --- /dev/null +++ b/Makefile @@ -0,0 +1,83 @@ +# A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html + +PACKAGE = github.com/gohugoio/hugo +COMMIT_HASH = `git rev-parse --short HEAD 2>/dev/null` +BUILD_DATE = `date +%FT%T%z` +LDFLAGS = -ldflags "-X ${PACKAGE}/hugolib.CommitHash=${COMMIT_HASH} -X ${PACKAGE}/hugolib.BuildDate=${BUILD_DATE}" +NOGI_LDFLAGS = -ldflags "-X ${PACKAGE}/hugolib.BuildDate=${BUILD_DATE}" + +.PHONY: vendor docker check fmt lint test test-race vet test-cover-html help +.DEFAULT_GOAL := help + +vendor: ## Install govendor and sync Hugo's vendored dependencies + go get github.com/kardianos/govendor + govendor sync ${PACKAGE} + +hugo: vendor ## Build hugo binary + go build ${LDFLAGS} ${PACKAGE} + +hugo-race: vendor ## Build hugo binary with race detector enabled + go build -race ${LDFLAGS} ${PACKAGE} + +install: vendor ## Install hugo binary + go install ${LDFLAGS} ${PACKAGE} + +hugo-no-gitinfo: LDFLAGS = ${NOGI_LDFLAGS} +hugo-no-gitinfo: vendor hugo ## Build hugo without git info + +docker: ## Build hugo Docker container + docker build -t hugo . + docker rm -f hugo-build || true + docker run --name hugo-build hugo ls /go/bin + docker cp hugo-build:/go/bin/hugo . + docker rm hugo-build + +govendor: vendor # Deprecated: use "vendor" target +get: vendor # Deprecated: use "vendor" +gitinfo: hugo # Deprecated: use "hugo" target +install-gitinfo: install # Deprecated: use "install" target +no-git-info: hugo-no-gitinfo # Deprecated: use "hugo-no-gitinfo" target + +check: test-race test386 fmt vet ## Run tests and linters + +test386: ## Run tests in 32-bit mode + GOARCH=386 govendor test +local + +test: ## Run tests + govendor test +local + +test-race: ## Run tests with race detector + govendor test -race +local + +fmt: ## Run gofmt linter + @for d in `govendor list -no-status +local | sed 's/github.com.gohugoio.hugo/./'` ; do \ + if [ "`gofmt -l $$d/*.go | tee /dev/stderr`" ]; then \ + echo "^ improperly formatted go files" && echo && exit 1; \ + fi \ + done + +lint: ## Run golint linter + @for d in `govendor list -no-status +local | sed 's/github.com.gohugoio.hugo/./'` ; do \ + if [ "`golint $$d | tee /dev/stderr`" ]; then \ + echo "^ golint errors!" && echo && exit 1; \ + fi \ + done + +vet: ## Run go vet linter + @if [ "`govendor vet +local | tee /dev/stderr`" ]; then \ + echo "^ go vet errors!" && echo && exit 1; \ + fi + +test-cover-html: PACKAGES = $(shell govendor list -no-status +local | sed 's/github.com.gohugoio.hugo/./') +test-cover-html: ## Generate test coverage report + echo "mode: count" > coverage-all.out + $(foreach pkg,$(PACKAGES),\ + govendor test -coverprofile=coverage.out -covermode=count $(pkg);\ + tail -n +2 coverage.out >> coverage-all.out;) + go tool cover -html=coverage-all.out + +check-vendor: ## Verify that vendored packages match git HEAD + @git diff-index --quiet HEAD vendor/ || (echo "check-vendor target failed: vendored packages out of sync" && echo && git diff vendor/ && exit 1) + +help: + @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' @@ -1,3 +1,106 @@ -# Hugo Docs +![Hugo](https://raw.githubusercontent.com/gohugoio/hugoDocs/master/static/img/hugo-logo.png) -Documentation site for [Hugo](https://github.com/gohugoio/hugo), the very fast and flexible static site generator built with love in GoLang. +A Fast and Flexible Static Site Generator built with love by [spf13](http://spf13.com/) and [friends](https://github.com/gohugoio/hugo/graphs/contributors) in [Go][]. + +[Website](https://gohugo.io) | +[Forum](https://discourse.gohugo.io) | +[Developer Chat (no support)](https://gitter.im/gohugoio/hugo) | +[Documentation](https://gohugo.io/overview/introduction/) | +[Installation Guide](https://gohugo.io/overview/installing/) | +[Contribution Guide](CONTRIBUTING.md) | +[Twitter](http://twitter.com/gohugoio) + +[![GoDoc](https://godoc.org/github.com/gohugoio/hugo?status.svg)](https://godoc.org/github.com/gohugoio/hugo) +[![Linux and macOS Build Status](https://api.travis-ci.org/gohugoio/hugo.svg?branch=master&label=Linux+and+macOS+build "Linux and macOS Build Status")](https://travis-ci.org/gohugoio/hugo) +[![Windows Build Status](https://ci.appveyor.com/api/projects/status/a5mr220vsd091kua?svg=true&label=Windows+build "Windows Build Status")](https://ci.appveyor.com/project/bep/hugo/branch/master) +[![Dev chat at https://gitter.im/gohugoio/hugo](https://img.shields.io/badge/gitter-developer_chat-46bc99.svg)](https://gitter.im/spf13/hugo?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Go Report Card](https://goreportcard.com/badge/github.com/gohugoio/hugo)](https://goreportcard.com/report/github.com/gohugoio/hugo) + +## Overview + +Hugo is a static HTML and CSS website generator written in [Go][]. +It is optimized for speed, easy use and configurability. +Hugo takes a directory with content and templates and renders them into a full HTML website. + +Hugo relies on Markdown files with front matter for meta data. +And you can run Hugo from any directory. +This works well for shared hosts and other systems where you don’t have a privileged account. + +Hugo renders a typical website of moderate size in a fraction of a second. +A good rule of thumb is that each piece of content renders in around 1 millisecond. + +Hugo is designed to work well for any kind of website including blogs, tumbles and docs. + +#### Supported Architectures + +Currently, we provide pre-built Hugo binaries for Windows, Linux, FreeBSD, NetBSD and macOS (Darwin) and [Android](https://gist.github.com/bep/a0d8a26cf6b4f8bc992729b8e50b480b) for x64, i386 and ARM architectures. + +Hugo may also be compiled from source wherever the Go compiler tool chain can run, e.g. for other operating systems including DragonFly BSD, OpenBSD, Plan 9 and Solaris. + +**Complete documentation is available at [Hugo Documentation][].** + +## Choose How to Install + +If you want to use Hugo as your site generator, simply install the Hugo binaries. +The Hugo binaries have no external dependencies. + +To contribute to the Hugo source code or documentation, you should [fork the Hugo GitHub project](https://github.com/gohugoio/hugo#fork-destination-box) and clone it to your local machine. + +Finally, you can install the Hugo source code with `go`, build the binaries yourself, and run Hugo that way. +Building the binaries is an easy task for an experienced `go` getter. + +### Install Hugo as Your Site Generator (Binary Install) + +Use the [installation instructions in the Hugo documentation](https://gohugo.io/overview/installing/). + +### Build and Install the Binaries from Source (Advanced Install) + +Add Hugo and its package dependencies to your go `src` directory. + + go get -v github.com/gohugoio/hugo + +Once the `get` completes, you should find your new `hugo` (or `hugo.exe`) executable sitting inside `$GOPATH/bin/`. + +To update Hugo’s dependencies, use `go get` with the `-u` option. + + go get -u -v github.com/gohugoio/hugo + +## Contributing to Hugo + +For a complete guide to contributing to Hugo, see the [Contribution Guide](CONTRIBUTING.md). + +We welcome contributions to Hugo of any kind including documentation, themes, +organization, tutorials, blog posts, bug reports, issues, feature requests, +feature implementations, pull requests, answering questions on the forum, +helping to manage issues, etc. + +The Hugo community and maintainers are [very active](https://github.com/gohugoio/hugo/pulse/monthly) and helpful, and the project benefits greatly from this activity. + +### Asking Support Questions + +We have an active [discussion forum](https://discourse.gohugo.io) where users and developers can ask questions. +Please don't use the GitHub issue tracker to ask questions. + +### Reporting Issues + +If you believe you have found a defect in Hugo or its documentation, use +the GitHub issue tracker to report the problem to the Hugo maintainers. +If you're not sure if it's a bug or not, start by asking in the [discussion forum](https://discourse.gohugo.io). +When reporting the issue, please provide the version of Hugo in use (`hugo version`). + +### Submitting Patches + +The Hugo project welcomes all contributors and contributions regardless of skill or experience level. +If you are interested in helping with the project, we will help you with your contribution. +Hugo is a very active project with many contributions happening daily. +Because we want to create the best possible product for our users and the best contribution experience for our developers, +we have a set of guidelines which ensure that all contributions are acceptable. +The guidelines are not intended as a filter or barrier to participation. +If you are unfamiliar with the contribution process, the Hugo team will help you and teach you how to bring your contribution in accordance with the guidelines. + +For a complete guide to contributing code to Hugo, see the [Contribution Guide](CONTRIBUTING.md). + +[![Analytics](https://ga-beacon.appspot.com/UA-7131036-6/hugo/readme)](https://github.com/igrigorik/ga-beacon) + +[Go]: https://golang.org/ +[Hugo Documentation]: https://gohugo.io/overview/introduction/ diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..4785a441d --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,15 @@ +init: + - copy c:\MinGW\bin\mingw32-make.exe c:\MinGW\bin\make.exe + - set PATH=%PATH%;C:\MinGW\bin;%GOPATH%\bin + - go version + - go env + +# clones and cd's to path +clone_folder: C:\GOPATH\src\github.com\gohugoio\hugo + +install: + - gem install asciidoctor + - pip install docutils + +build_script: + - make hugo-race check diff --git a/bench.sh b/bench.sh new file mode 100755 index 000000000..367a74403 --- /dev/null +++ b/bench.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + + +# Convenience script to +# - For a given branch +# - Run benchmark tests for a given package +# - Do the same for master +# - then compare the two runs with benchcmp + +benchFilter=".*" + +if (( $# < 2 )); + then + echo "USAGE: ./bench.sh <git-branch> <package-to-bench> (and <benchmark filter> (regexp, optional))" + exit 1 +fi + + + +if [ $# -eq 3 ]; then + benchFilter=$3 +fi + + +BRANCH=$1 +PACKAGE=$2 + +git checkout $BRANCH +go test -test.run=NONE -bench="$benchFilter" -test.benchmem=true ./$PACKAGE > /tmp/bench-$PACKAGE-$BRANCH.txt + +git checkout master +go test -test.run=NONE -bench="$benchFilter" -test.benchmem=true ./$PACKAGE > /tmp/bench-$PACKAGE-master.txt + + +benchcmp /tmp/bench-$PACKAGE-master.txt /tmp/bench-$PACKAGE-$BRANCH.txt
\ No newline at end of file diff --git a/benchSite.sh b/benchSite.sh new file mode 100755 index 000000000..8130559f5 --- /dev/null +++ b/benchSite.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Send in a regexp mathing the benchmarks you want to run, i.e. './benchSite.sh "YAML"'. +# Note the quotes, which will be needed for more complex expressions. +# The above will run all variations, but only for front matter YAML. + +echo "Running with BenchmarkSiteBuilding/${1}" + +go test -run="NONE" -bench="BenchmarkSiteBuilding/${1}$" -test.benchmem=true ./hugolib -memprofile mem.prof -cpuprofile cpu.prof
\ No newline at end of file diff --git a/bufferpool/bufpool.go b/bufferpool/bufpool.go new file mode 100644 index 000000000..c1e4105d0 --- /dev/null +++ b/bufferpool/bufpool.go @@ -0,0 +1,38 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package bufferpool provides a pool of bytes buffers. +package bufferpool + +import ( + "bytes" + "sync" +) + +var bufferPool = &sync.Pool{ + New: func() interface{} { + return &bytes.Buffer{} + }, +} + +// GetBuffer returns a buffer from the pool. +func GetBuffer() (buf *bytes.Buffer) { + return bufferPool.Get().(*bytes.Buffer) +} + +// PutBuffer returns a buffer to the pool. +// The buffer is reset before it is put back into circulation. +func PutBuffer(buf *bytes.Buffer) { + buf.Reset() + bufferPool.Put(buf) +} diff --git a/bufferpool/bufpool_test.go b/bufferpool/bufpool_test.go new file mode 100644 index 000000000..cfa247f62 --- /dev/null +++ b/bufferpool/bufpool_test.go @@ -0,0 +1,27 @@ +// Copyright 2016-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bufferpool + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestBufferPool(t *testing.T) { + buff := GetBuffer() + buff.WriteString("do be do be do") + assert.Equal(t, "do be do be do", buff.String()) + PutBuffer(buff) + assert.Equal(t, 0, buff.Len()) +} diff --git a/cache/partitioned_lazy_cache.go b/cache/partitioned_lazy_cache.go new file mode 100644 index 000000000..9baf0377d --- /dev/null +++ b/cache/partitioned_lazy_cache.go @@ -0,0 +1,80 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cache + +import ( + "sync" +) + +// Partition represents a cache partition where Load is the callback +// for when the partition is needed. +type Partition struct { + Key string + Load func() (map[string]interface{}, error) +} + +type lazyPartition struct { + initSync sync.Once + cache map[string]interface{} + load func() (map[string]interface{}, error) +} + +func (l *lazyPartition) init() error { + var err error + l.initSync.Do(func() { + var c map[string]interface{} + c, err = l.load() + l.cache = c + }) + + return err +} + +// PartitionedLazyCache is a lazily loaded cache paritioned by a supplied string key. +type PartitionedLazyCache struct { + partitions map[string]*lazyPartition +} + +// NewPartitionedLazyCache creates a new NewPartitionedLazyCache with the supplied +// partitions. +func NewPartitionedLazyCache(partitions ...Partition) *PartitionedLazyCache { + lazyPartitions := make(map[string]*lazyPartition, len(partitions)) + for _, partition := range partitions { + lazyPartitions[partition.Key] = &lazyPartition{load: partition.Load} + } + cache := &PartitionedLazyCache{partitions: lazyPartitions} + + return cache +} + +// Get initializes the partition if not already done so, then looks up the given +// key in the given partition, returns nil if no value found. +func (c *PartitionedLazyCache) Get(partition, key string) (interface{}, error) { + p, found := c.partitions[partition] + + if !found { + return nil, nil + } + + if err := p.init(); err != nil { + return nil, err + } + + if v, found := p.cache[key]; found { + return v, nil + } + + return nil, nil + +} diff --git a/cache/partitioned_lazy_cache_test.go b/cache/partitioned_lazy_cache_test.go new file mode 100644 index 000000000..73f75fe17 --- /dev/null +++ b/cache/partitioned_lazy_cache_test.go @@ -0,0 +1,92 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cache + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewPartitionedLazyCache(t *testing.T) { + t.Parallel() + + assert := require.New(t) + + p1 := Partition{ + Key: "p1", + Load: func() (map[string]interface{}, error) { + return map[string]interface{}{ + "p1_1": "p1v1", + "p1_2": "p1v2", + "p1_nil": nil, + }, nil + }, + } + + p2 := Partition{ + Key: "p2", + Load: func() (map[string]interface{}, error) { + return map[string]interface{}{ + "p2_1": "p2v1", + "p2_2": "p2v2", + "p2_3": "p2v3", + }, nil + }, + } + + cache := NewPartitionedLazyCache(p1, p2) + + v, err := cache.Get("p1", "p1_1") + assert.NoError(err) + assert.Equal("p1v1", v) + + v, err = cache.Get("p1", "p2_1") + assert.NoError(err) + assert.Nil(v) + + v, err = cache.Get("p1", "p1_nil") + assert.NoError(err) + assert.Nil(v) + + v, err = cache.Get("p2", "p2_3") + assert.NoError(err) + assert.Equal("p2v3", v) + + v, err = cache.Get("doesnotexist", "p1_1") + assert.NoError(err) + assert.Nil(v) + + v, err = cache.Get("p1", "doesnotexist") + assert.NoError(err) + assert.Nil(v) + + errorP := Partition{ + Key: "p3", + Load: func() (map[string]interface{}, error) { + return nil, errors.New("Failed") + }, + } + + cache = NewPartitionedLazyCache(errorP) + + v, err = cache.Get("p1", "doesnotexist") + assert.NoError(err) + assert.Nil(v) + + _, err = cache.Get("p3", "doesnotexist") + assert.Error(err) + +} diff --git a/commands/benchmark.go b/commands/benchmark.go new file mode 100644 index 000000000..51f2be876 --- /dev/null +++ b/commands/benchmark.go @@ -0,0 +1,112 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "os" + "runtime" + "runtime/pprof" + "time" + + "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" +) + +var ( + benchmarkTimes int + cpuProfileFile string + memProfileFile string +) + +var benchmarkCmd = &cobra.Command{ + Use: "benchmark", + Short: "Benchmark Hugo by building a site a number of times.", + Long: `Hugo can build a site many times over and analyze the running process +creating a benchmark.`, +} + +func init() { + initHugoBuilderFlags(benchmarkCmd) + initBenchmarkBuildingFlags(benchmarkCmd) + + benchmarkCmd.Flags().StringVar(&cpuProfileFile, "cpuprofile", "", "path/filename for the CPU profile file") + benchmarkCmd.Flags().StringVar(&memProfileFile, "memprofile", "", "path/filename for the memory profile file") + benchmarkCmd.Flags().IntVarP(&benchmarkTimes, "count", "n", 13, "number of times to build the site") + + benchmarkCmd.RunE = benchmark +} + +func benchmark(cmd *cobra.Command, args []string) error { + cfg, err := InitializeConfig(benchmarkCmd) + if err != nil { + return err + } + + c, err := newCommandeer(cfg) + if err != nil { + return err + } + + var memProf *os.File + if memProfileFile != "" { + memProf, err = os.Create(memProfileFile) + if err != nil { + return err + } + } + + var cpuProf *os.File + if cpuProfileFile != "" { + cpuProf, err = os.Create(cpuProfileFile) + if err != nil { + return err + } + } + + var memStats runtime.MemStats + runtime.ReadMemStats(&memStats) + memAllocated := memStats.TotalAlloc + mallocs := memStats.Mallocs + if cpuProf != nil { + pprof.StartCPUProfile(cpuProf) + } + + t := time.Now() + for i := 0; i < benchmarkTimes; i++ { + if err = c.resetAndBuildSites(false); err != nil { + return err + } + } + totalTime := time.Since(t) + + if memProf != nil { + pprof.WriteHeapProfile(memProf) + memProf.Close() + } + if cpuProf != nil { + pprof.StopCPUProfile() + cpuProf.Close() + } + + runtime.ReadMemStats(&memStats) + totalMemAllocated := memStats.TotalAlloc - memAllocated + totalMallocs := memStats.Mallocs - mallocs + + jww.FEEDBACK.Println() + jww.FEEDBACK.Printf("Average time per operation: %vms\n", int(1000*totalTime.Seconds()/float64(benchmarkTimes))) + jww.FEEDBACK.Printf("Average memory allocated per operation: %vkB\n", totalMemAllocated/uint64(benchmarkTimes)/1024) + jww.FEEDBACK.Printf("Average allocations per operation: %v\n", totalMallocs/uint64(benchmarkTimes)) + + return nil +} diff --git a/commands/check.go b/commands/check.go new file mode 100644 index 000000000..e5dbc1ffa --- /dev/null +++ b/commands/check.go @@ -0,0 +1,23 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "github.com/spf13/cobra" +) + +var checkCmd = &cobra.Command{ + Use: "check", + Short: "Contains some verification checks", +} diff --git a/commands/commandeer.go b/commands/commandeer.go new file mode 100644 index 000000000..7de185d2f --- /dev/null +++ b/commands/commandeer.go @@ -0,0 +1,62 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" +) + +type commandeer struct { + *deps.DepsCfg + pathSpec *helpers.PathSpec + configured bool +} + +func (c *commandeer) Set(key string, value interface{}) { + if c.configured { + panic("commandeer cannot be changed") + } + c.Cfg.Set(key, value) +} + +// PathSpec lazily creates a new PathSpec, as all the paths must +// be configured before it is created. +func (c *commandeer) PathSpec() *helpers.PathSpec { + c.configured = true + return c.pathSpec +} + +func (c *commandeer) initFs(fs *hugofs.Fs) error { + c.DepsCfg.Fs = fs + ps, err := helpers.NewPathSpec(fs, c.Cfg) + if err != nil { + return err + } + c.pathSpec = ps + return nil +} + +func newCommandeer(cfg *deps.DepsCfg) (*commandeer, error) { + l := cfg.Language + if l == nil { + l = helpers.NewDefaultLanguage(cfg.Cfg) + } + ps, err := helpers.NewPathSpec(cfg.Fs, l) + if err != nil { + return nil, err + } + return &commandeer{DepsCfg: cfg, pathSpec: ps}, nil +} diff --git a/commands/convert.go b/commands/convert.go new file mode 100644 index 000000000..298ff6019 --- /dev/null +++ b/commands/convert.go @@ -0,0 +1,158 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "errors" + "fmt" + "path/filepath" + "time" + + "github.com/gohugoio/hugo/hugolib" + "github.com/gohugoio/hugo/parser" + "github.com/spf13/cast" + "github.com/spf13/cobra" +) + +var outputDir string +var unsafe bool + +var convertCmd = &cobra.Command{ + Use: "convert", + Short: "Convert your content to different formats", + Long: `Convert your content (e.g. front matter) to different formats. + +See convert's subcommands toJSON, toTOML and toYAML for more information.`, + RunE: nil, +} + +var toJSONCmd = &cobra.Command{ + Use: "toJSON", + Short: "Convert front matter to JSON", + Long: `toJSON converts all front matter in the content directory +to use JSON for the front matter.`, + RunE: func(cmd *cobra.Command, args []string) error { + return convertContents(rune([]byte(parser.JSONLead)[0])) + }, +} + +var toTOMLCmd = &cobra.Command{ + Use: "toTOML", + Short: "Convert front matter to TOML", + Long: `toTOML converts all front matter in the content directory +to use TOML for the front matter.`, + RunE: func(cmd *cobra.Command, args []string) error { + return convertContents(rune([]byte(parser.TOMLLead)[0])) + }, +} + +var toYAMLCmd = &cobra.Command{ + Use: "toYAML", + Short: "Convert front matter to YAML", + Long: `toYAML converts all front matter in the content directory +to use YAML for the front matter.`, + RunE: func(cmd *cobra.Command, args []string) error { + return convertContents(rune([]byte(parser.YAMLLead)[0])) + }, +} + +func init() { + convertCmd.AddCommand(toJSONCmd) + convertCmd.AddCommand(toTOMLCmd) + convertCmd.AddCommand(toYAMLCmd) + convertCmd.PersistentFlags().StringVarP(&outputDir, "output", "o", "", "filesystem path to write files to") + convertCmd.PersistentFlags().StringVarP(&source, "source", "s", "", "filesystem path to read files relative from") + convertCmd.PersistentFlags().BoolVar(&unsafe, "unsafe", false, "enable less safe operations, please backup first") + convertCmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{}) +} + +func convertContents(mark rune) error { + cfg, err := InitializeConfig() + if err != nil { + return err + } + + h, err := hugolib.NewHugoSites(*cfg) + if err != nil { + return err + } + + site := h.Sites[0] + + if err = site.Initialise(); err != nil { + return err + } + + if site.Source == nil { + panic("site.Source not set") + } + if len(site.Source.Files()) < 1 { + return errors.New("No source files found") + } + + contentDir := site.PathSpec.AbsPathify(site.Cfg.GetString("contentDir")) + site.Log.FEEDBACK.Println("processing", len(site.Source.Files()), "content files") + for _, file := range site.Source.Files() { + site.Log.INFO.Println("Attempting to convert", file.LogicalName()) + page, err := site.NewPage(file.LogicalName()) + if err != nil { + return err + } + + psr, err := parser.ReadFrom(file.Contents) + if err != nil { + site.Log.ERROR.Println("Error processing file:", file.Path()) + return err + } + metadata, err := psr.Metadata() + if err != nil { + site.Log.ERROR.Println("Error processing file:", file.Path()) + return err + } + + // better handling of dates in formats that don't have support for them + if mark == parser.FormatToLeadRune("json") || mark == parser.FormatToLeadRune("yaml") || mark == parser.FormatToLeadRune("toml") { + newMetadata := cast.ToStringMap(metadata) + for k, v := range newMetadata { + switch vv := v.(type) { + case time.Time: + newMetadata[k] = vv.Format(time.RFC3339) + } + } + metadata = newMetadata + } + + page.SetDir(filepath.Join(contentDir, file.Dir())) + page.SetSourceContent(psr.Content()) + if err = page.SetSourceMetaData(metadata, mark); err != nil { + site.Log.ERROR.Printf("Failed to set source metadata for file %q: %s. For more info see For more info see https://github.com/gohugoio/hugo/issues/2458", page.FullFilePath(), err) + continue + } + + if outputDir != "" { + if err = page.SaveSourceAs(filepath.Join(outputDir, page.FullFilePath())); err != nil { + return fmt.Errorf("Failed to save file %q: %s", page.FullFilePath(), err) + } + } else { + if unsafe { + if err = page.SaveSource(); err != nil { + return fmt.Errorf("Failed to save file %q: %s", page.FullFilePath(), err) + } + } else { + site.Log.FEEDBACK.Println("Unsafe operation not allowed, use --unsafe or set a different output path") + } + } + } + return nil +} diff --git a/commands/env.go b/commands/env.go new file mode 100644 index 000000000..54c98d527 --- /dev/null +++ b/commands/env.go @@ -0,0 +1,35 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "runtime" + + "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" +) + +var envCmd = &cobra.Command{ + Use: "env", + Short: "Print Hugo version and environment info", + Long: `Print Hugo version and environment info. This is useful in Hugo bug reports.`, + RunE: func(cmd *cobra.Command, args []string) error { + printHugoVersion() + jww.FEEDBACK.Printf("GOOS=%q\n", runtime.GOOS) + jww.FEEDBACK.Printf("GOARCH=%q\n", runtime.GOARCH) + jww.FEEDBACK.Printf("GOVERSION=%q\n", runtime.Version()) + + return nil + }, +} diff --git a/commands/gen.go b/commands/gen.go new file mode 100644 index 000000000..62a84b0d0 --- /dev/null +++ b/commands/gen.go @@ -0,0 +1,23 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "github.com/spf13/cobra" +) + +var genCmd = &cobra.Command{ + Use: "gen", + Short: "A collection of several useful generators.", +} diff --git a/commands/genautocomplete.go b/commands/genautocomplete.go new file mode 100644 index 000000000..c2004ab22 --- /dev/null +++ b/commands/genautocomplete.go @@ -0,0 +1,70 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" +) + +var autocompleteTarget string + +// bash for now (zsh and others will come) +var autocompleteType string + +var genautocompleteCmd = &cobra.Command{ + Use: "autocomplete", + Short: "Generate shell autocompletion script for Hugo", + Long: `Generates a shell autocompletion script for Hugo. + +NOTE: The current version supports Bash only. + This should work for *nix systems with Bash installed. + +By default, the file is written directly to /etc/bash_completion.d +for convenience, and the command may need superuser rights, e.g.: + + $ sudo hugo gen autocomplete + +Add ` + "`--completionfile=/path/to/file`" + ` flag to set alternative +file-path and name. + +Logout and in again to reload the completion scripts, +or just source them in directly: + + $ . /etc/bash_completion`, + + RunE: func(cmd *cobra.Command, args []string) error { + if autocompleteType != "bash" { + return newUserError("Only Bash is supported for now") + } + + err := cmd.Root().GenBashCompletionFile(autocompleteTarget) + + if err != nil { + return err + } + + jww.FEEDBACK.Println("Bash completion file for Hugo saved to", autocompleteTarget) + + return nil + }, +} + +func init() { + genautocompleteCmd.PersistentFlags().StringVarP(&autocompleteTarget, "completionfile", "", "/etc/bash_completion.d/hugo.sh", "autocompletion file") + genautocompleteCmd.PersistentFlags().StringVarP(&autocompleteType, "type", "", "bash", "autocompletion type (currently only bash supported)") + + // For bash-completion + genautocompleteCmd.PersistentFlags().SetAnnotation("completionfile", cobra.BashCompFilenameExt, []string{}) +} diff --git a/commands/gendoc.go b/commands/gendoc.go new file mode 100644 index 000000000..c4840050b --- /dev/null +++ b/commands/gendoc.go @@ -0,0 +1,86 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "fmt" + "path" + "path/filepath" + "strings" + "time" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" + jww "github.com/spf13/jwalterweatherman" +) + +const gendocFrontmatterTemplate = `--- +date: %s +title: "%s" +slug: %s +url: %s +--- +` + +var gendocdir string +var gendocCmd = &cobra.Command{ + Use: "doc", + Short: "Generate Markdown documentation for the Hugo CLI.", + Long: `Generate Markdown documentation for the Hugo CLI. + +This command is, mostly, used to create up-to-date documentation +of Hugo's command-line interface for http://gohugo.io/. + +It creates one Markdown file per command with front matter suitable +for rendering in Hugo.`, + + RunE: func(cmd *cobra.Command, args []string) error { + if !strings.HasSuffix(gendocdir, helpers.FilePathSeparator) { + gendocdir += helpers.FilePathSeparator + } + if found, _ := helpers.Exists(gendocdir, hugofs.Os); !found { + jww.FEEDBACK.Println("Directory", gendocdir, "does not exist, creating...") + if err := hugofs.Os.MkdirAll(gendocdir, 0777); err != nil { + return err + } + } + now := time.Now().Format(time.RFC3339) + prepender := func(filename string) string { + name := filepath.Base(filename) + base := strings.TrimSuffix(name, path.Ext(name)) + url := "/commands/" + strings.ToLower(base) + "/" + return fmt.Sprintf(gendocFrontmatterTemplate, now, strings.Replace(base, "_", " ", -1), base, url) + } + + linkHandler := func(name string) string { + base := strings.TrimSuffix(name, path.Ext(name)) + return "/commands/" + strings.ToLower(base) + "/" + } + + jww.FEEDBACK.Println("Generating Hugo command-line documentation in", gendocdir, "...") + doc.GenMarkdownTreeCustom(cmd.Root(), gendocdir, prepender, linkHandler) + jww.FEEDBACK.Println("Done.") + + return nil + }, +} + +func init() { + gendocCmd.PersistentFlags().StringVar(&gendocdir, "dir", "/tmp/hugodoc/", "the directory to write the doc.") + + // For bash-completion + gendocCmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{}) +} diff --git a/commands/gendocshelper.go b/commands/gendocshelper.go new file mode 100644 index 000000000..6e59d99eb --- /dev/null +++ b/commands/gendocshelper.go @@ -0,0 +1,72 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/gohugoio/hugo/docshelper" + "github.com/spf13/cobra" +) + +type genDocsHelper struct { + target string + cmd *cobra.Command +} + +func createGenDocsHelper() *genDocsHelper { + g := &genDocsHelper{ + cmd: &cobra.Command{ + Use: "docshelper", + Short: "Generate some data files for the Hugo docs.", + Hidden: true, + }, + } + + g.cmd.RunE = func(cmd *cobra.Command, args []string) error { + return g.generate() + } + + // Note that ./docs is a submodule, and writing to that would be suboptimal. + // Let us assume that the default is a sibling project. + g.cmd.PersistentFlags().StringVarP(&g.target, "dir", "", "../hugoDocs/data", "data dir") + + return g +} + +func (g *genDocsHelper) generate() error { + fmt.Println("Generate docs data to", g.target) + + targetFile := filepath.Join(g.target, "docs.json") + + f, err := os.Create(targetFile) + if err != nil { + return err + } + defer f.Close() + + enc := json.NewEncoder(f) + enc.SetIndent("", " ") + + if err := enc.Encode(docshelper.DocProviders); err != nil { + return err + } + + fmt.Println("Done!") + return nil + +} diff --git a/commands/genman.go b/commands/genman.go new file mode 100644 index 000000000..004e669e7 --- /dev/null +++ b/commands/genman.go @@ -0,0 +1,66 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "fmt" + "strings" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" + jww "github.com/spf13/jwalterweatherman" +) + +var genmandir string +var genmanCmd = &cobra.Command{ + Use: "man", + Short: "Generate man pages for the Hugo CLI", + Long: `This command automatically generates up-to-date man pages of Hugo's +command-line interface. By default, it creates the man page files +in the "man" directory under the current directory.`, + + RunE: func(cmd *cobra.Command, args []string) error { + header := &doc.GenManHeader{ + Section: "1", + Manual: "Hugo Manual", + Source: fmt.Sprintf("Hugo %s", helpers.CurrentHugoVersion), + } + if !strings.HasSuffix(genmandir, helpers.FilePathSeparator) { + genmandir += helpers.FilePathSeparator + } + if found, _ := helpers.Exists(genmandir, hugofs.Os); !found { + jww.FEEDBACK.Println("Directory", genmandir, "does not exist, creating...") + if err := hugofs.Os.MkdirAll(genmandir, 0777); err != nil { + return err + } + } + cmd.Root().DisableAutoGenTag = true + + jww.FEEDBACK.Println("Generating Hugo man pages in", genmandir, "...") + doc.GenManTree(cmd.Root(), header, genmandir) + + jww.FEEDBACK.Println("Done.") + + return nil + }, +} + +func init() { + genmanCmd.PersistentFlags().StringVar(&genmandir, "dir", "man/", "the directory to write the man pages.") + + // For bash-completion + genmanCmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{}) +} diff --git a/commands/hugo.go b/commands/hugo.go new file mode 100644 index 000000000..b965ba167 --- /dev/null +++ b/commands/hugo.go @@ -0,0 +1,1046 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package commands defines and implements command-line commands and flags +// used by Hugo. Commands and flags are implemented using Cobra. +package commands + +import ( + "fmt" + "io/ioutil" + + "github.com/gohugoio/hugo/hugofs" + + "log" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + "time" + + "github.com/gohugoio/hugo/config" + + "github.com/gohugoio/hugo/parser" + flag "github.com/spf13/pflag" + + "regexp" + + "github.com/fsnotify/fsnotify" + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugolib" + "github.com/gohugoio/hugo/livereload" + "github.com/gohugoio/hugo/utils" + "github.com/gohugoio/hugo/watcher" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/spf13/fsync" + jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/nitro" + "github.com/spf13/viper" +) + +// Hugo represents the Hugo sites to build. This variable is exported as it +// is used by at least one external library (the Hugo caddy plugin). We should +// provide a cleaner external API, but until then, this is it. +var Hugo *hugolib.HugoSites + +// Reset resets Hugo ready for a new full build. This is mainly only useful +// for benchmark testing etc. via the CLI commands. +func Reset() error { + Hugo = nil + return nil +} + +// commandError is an error used to signal different error situations in command handling. +type commandError struct { + s string + userError bool +} + +func (c commandError) Error() string { + return c.s +} + +func (c commandError) isUserError() bool { + return c.userError +} + +func newUserError(a ...interface{}) commandError { + return commandError{s: fmt.Sprintln(a...), userError: true} +} + +func newSystemError(a ...interface{}) commandError { + return commandError{s: fmt.Sprintln(a...), userError: false} +} + +func newSystemErrorF(format string, a ...interface{}) commandError { + return commandError{s: fmt.Sprintf(format, a...), userError: false} +} + +// Catch some of the obvious user errors from Cobra. +// We don't want to show the usage message for every error. +// The below may be to generic. Time will show. +var userErrorRegexp = regexp.MustCompile("argument|flag|shorthand") + +func isUserError(err error) bool { + if cErr, ok := err.(commandError); ok && cErr.isUserError() { + return true + } + + return userErrorRegexp.MatchString(err.Error()) +} + +// HugoCmd is Hugo's root command. +// Every other command attached to HugoCmd is a child command to it. +var HugoCmd = &cobra.Command{ + Use: "hugo", + Short: "hugo builds your site", + Long: `hugo is the main command, used to build your Hugo site. + +Hugo is a Fast and Flexible Static Site Generator +built with love by spf13 and friends in Go. + +Complete documentation is available at http://gohugo.io/.`, + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := InitializeConfig() + if err != nil { + return err + } + + c, err := newCommandeer(cfg) + if err != nil { + return err + } + + if buildWatch { + cfg.Cfg.Set("disableLiveReload", true) + c.watchConfig() + } + + return c.build() + }, +} + +var hugoCmdV *cobra.Command + +// Flags that are to be added to commands. +var ( + buildWatch bool + logging bool + renderToMemory bool // for benchmark testing + verbose bool + verboseLog bool + quiet bool +) + +var ( + baseURL string + cacheDir string + contentDir string + layoutDir string + cfgFile string + destination string + logFile string + theme string + themesDir string + source string + logI18nWarnings bool + disableKinds []string +) + +// Execute adds all child commands to the root command HugoCmd and sets flags appropriately. +func Execute() { + HugoCmd.SetGlobalNormalizationFunc(helpers.NormalizeHugoFlags) + + HugoCmd.SilenceUsage = true + + AddCommands() + + if c, err := HugoCmd.ExecuteC(); err != nil { + if isUserError(err) { + c.Println("") + c.Println(c.UsageString()) + } + + os.Exit(-1) + } +} + +// AddCommands adds child commands to the root command HugoCmd. +func AddCommands() { + HugoCmd.AddCommand(serverCmd) + HugoCmd.AddCommand(versionCmd) + HugoCmd.AddCommand(envCmd) + HugoCmd.AddCommand(configCmd) + HugoCmd.AddCommand(checkCmd) + HugoCmd.AddCommand(benchmarkCmd) + HugoCmd.AddCommand(convertCmd) + HugoCmd.AddCommand(newCmd) + HugoCmd.AddCommand(listCmd) + HugoCmd.AddCommand(undraftCmd) + HugoCmd.AddCommand(importCmd) + + HugoCmd.AddCommand(genCmd) + genCmd.AddCommand(genautocompleteCmd) + genCmd.AddCommand(gendocCmd) + genCmd.AddCommand(genmanCmd) + genCmd.AddCommand(createGenDocsHelper().cmd) +} + +// initHugoBuilderFlags initializes all common flags, typically used by the +// core build commands, namely hugo itself, server, check and benchmark. +func initHugoBuilderFlags(cmd *cobra.Command) { + initHugoBuildCommonFlags(cmd) +} + +func initRootPersistentFlags() { + HugoCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is path/config.yaml|json|toml)") + HugoCmd.PersistentFlags().BoolVar(&quiet, "quiet", false, "build in quiet mode") + + // Set bash-completion + validConfigFilenames := []string{"json", "js", "yaml", "yml", "toml", "tml"} + _ = HugoCmd.PersistentFlags().SetAnnotation("config", cobra.BashCompFilenameExt, validConfigFilenames) +} + +// initHugoBuildCommonFlags initialize common flags related to the Hugo build. +// Called by initHugoBuilderFlags. +func initHugoBuildCommonFlags(cmd *cobra.Command) { + cmd.Flags().Bool("cleanDestinationDir", false, "remove files from destination not found in static directories") + cmd.Flags().BoolP("buildDrafts", "D", false, "include content marked as draft") + cmd.Flags().BoolP("buildFuture", "F", false, "include content with publishdate in the future") + cmd.Flags().BoolP("buildExpired", "E", false, "include expired content") + cmd.Flags().Bool("disable404", false, "do not render 404 page") + cmd.Flags().Bool("disableRSS", false, "do not build RSS files") + cmd.Flags().Bool("disableSitemap", false, "do not build Sitemap file") + cmd.Flags().StringVarP(&source, "source", "s", "", "filesystem path to read files relative from") + cmd.Flags().StringVarP(&contentDir, "contentDir", "c", "", "filesystem path to content directory") + cmd.Flags().StringVarP(&layoutDir, "layoutDir", "l", "", "filesystem path to layout directory") + cmd.Flags().StringVarP(&cacheDir, "cacheDir", "", "", "filesystem path to cache directory. Defaults: $TMPDIR/hugo_cache/") + cmd.Flags().BoolP("ignoreCache", "", false, "ignores the cache directory") + cmd.Flags().StringVarP(&destination, "destination", "d", "", "filesystem path to write files to") + cmd.Flags().StringVarP(&theme, "theme", "t", "", "theme to use (located in /themes/THEMENAME/)") + cmd.Flags().StringVarP(&themesDir, "themesDir", "", "", "filesystem path to themes directory") + cmd.Flags().Bool("uglyURLs", false, "if true, use /filename.html instead of /filename/") + cmd.Flags().Bool("canonifyURLs", false, "if true, all relative URLs will be canonicalized using baseURL") + cmd.Flags().StringVarP(&baseURL, "baseURL", "b", "", "hostname (and path) to the root, e.g. http://spf13.com/") + cmd.Flags().Bool("enableGitInfo", false, "add Git revision, date and author info to the pages") + + cmd.Flags().BoolVar(&nitro.AnalysisOn, "stepAnalysis", false, "display memory and timing of different steps of the program") + cmd.Flags().Bool("pluralizeListTitles", true, "pluralize titles in lists using inflect") + cmd.Flags().Bool("preserveTaxonomyNames", false, `preserve taxonomy names as written ("Gérard Depardieu" vs "gerard-depardieu")`) + cmd.Flags().BoolP("forceSyncStatic", "", false, "copy all files when static is changed.") + cmd.Flags().BoolP("noTimes", "", false, "don't sync modification time of files") + cmd.Flags().BoolP("noChmod", "", false, "don't sync permission mode of files") + cmd.Flags().BoolVarP(&logI18nWarnings, "i18n-warnings", "", false, "print missing translations") + + cmd.Flags().StringSliceVar(&disableKinds, "disableKinds", []string{}, "disable different kind of pages (home, RSS etc.)") + + // Set bash-completion. + // Each flag must first be defined before using the SetAnnotation() call. + _ = cmd.Flags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{}) + _ = cmd.Flags().SetAnnotation("cacheDir", cobra.BashCompSubdirsInDir, []string{}) + _ = cmd.Flags().SetAnnotation("destination", cobra.BashCompSubdirsInDir, []string{}) + _ = cmd.Flags().SetAnnotation("theme", cobra.BashCompSubdirsInDir, []string{"themes"}) +} + +func initBenchmarkBuildingFlags(cmd *cobra.Command) { + cmd.Flags().BoolVar(&renderToMemory, "renderToMemory", false, "render to memory (only useful for benchmark testing)") +} + +// init initializes flags. +func init() { + HugoCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output") + HugoCmd.PersistentFlags().BoolVar(&logging, "log", false, "enable Logging") + HugoCmd.PersistentFlags().StringVar(&logFile, "logFile", "", "log File path (if set, logging enabled automatically)") + HugoCmd.PersistentFlags().BoolVar(&verboseLog, "verboseLog", false, "verbose logging") + + initRootPersistentFlags() + initHugoBuilderFlags(HugoCmd) + initBenchmarkBuildingFlags(HugoCmd) + + HugoCmd.Flags().BoolVarP(&buildWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed") + hugoCmdV = HugoCmd + + // Set bash-completion + _ = HugoCmd.PersistentFlags().SetAnnotation("logFile", cobra.BashCompFilenameExt, []string{}) +} + +// InitializeConfig initializes a config file with sensible default configuration flags. +func InitializeConfig(subCmdVs ...*cobra.Command) (*deps.DepsCfg, error) { + + var cfg *deps.DepsCfg = &deps.DepsCfg{} + + // Init file systems. This may be changed at a later point. + osFs := hugofs.Os + + config, err := hugolib.LoadConfig(osFs, source, cfgFile) + if err != nil { + return cfg, err + } + + // Init file systems. This may be changed at a later point. + cfg.Cfg = config + + c, err := newCommandeer(cfg) + if err != nil { + return nil, err + } + + for _, cmdV := range append([]*cobra.Command{hugoCmdV}, subCmdVs...) { + c.initializeFlags(cmdV) + } + + if len(disableKinds) > 0 { + c.Set("disableKinds", disableKinds) + } + + logger, err := createLogger(cfg.Cfg) + if err != nil { + return cfg, err + } + + cfg.Logger = logger + + config.Set("logI18nWarnings", logI18nWarnings) + + if baseURL != "" { + config.Set("baseURL", baseURL) + } + + if !config.GetBool("relativeURLs") && config.GetString("baseURL") == "" { + cfg.Logger.ERROR.Println("No 'baseURL' set in configuration or as a flag. Features like page menus will not work without one.") + } + + if theme != "" { + config.Set("theme", theme) + } + + if themesDir != "" { + config.Set("themesDir", themesDir) + } + + if destination != "" { + config.Set("publishDir", destination) + } + + var dir string + if source != "" { + dir, _ = filepath.Abs(source) + } else { + dir, _ = os.Getwd() + } + config.Set("workingDir", dir) + + fs := hugofs.NewFrom(osFs, config) + + // Hugo writes the output to memory instead of the disk. + // This is only used for benchmark testing. Cause the content is only visible + // in memory. + if renderToMemory { + fs.Destination = new(afero.MemMapFs) + // Rendering to memoryFS, publish to Root regardless of publishDir. + c.Set("publishDir", "/") + } + + if contentDir != "" { + config.Set("contentDir", contentDir) + } + + if layoutDir != "" { + config.Set("layoutDir", layoutDir) + } + + if cacheDir != "" { + config.Set("cacheDir", cacheDir) + } + + cacheDir = config.GetString("cacheDir") + if cacheDir != "" { + if helpers.FilePathSeparator != cacheDir[len(cacheDir)-1:] { + cacheDir = cacheDir + helpers.FilePathSeparator + } + isDir, err := helpers.DirExists(cacheDir, fs.Source) + utils.CheckErr(cfg.Logger, err) + if !isDir { + mkdir(cacheDir) + } + config.Set("cacheDir", cacheDir) + } else { + config.Set("cacheDir", helpers.GetTempDir("hugo_cache", fs.Source)) + } + + if err := c.initFs(fs); err != nil { + return nil, err + } + + cfg.Logger.INFO.Println("Using config file:", viper.ConfigFileUsed()) + + themeDir := c.PathSpec().GetThemeDir() + if themeDir != "" { + if _, err := cfg.Fs.Source.Stat(themeDir); os.IsNotExist(err) { + return cfg, newSystemError("Unable to find theme Directory:", themeDir) + } + } + + themeVersionMismatch, minVersion := c.isThemeVsHugoVersionMismatch() + + if themeVersionMismatch { + cfg.Logger.ERROR.Printf("Current theme does not support Hugo version %s. Minimum version required is %s\n", + helpers.CurrentHugoVersion.ReleaseVersion(), minVersion) + } + + return cfg, nil + +} + +func createLogger(cfg config.Provider) (*jww.Notepad, error) { + var ( + logHandle = ioutil.Discard + logThreshold = jww.LevelWarn + logFile = cfg.GetString("logFile") + outHandle = os.Stdout + stdoutThreshold = jww.LevelError + ) + + if verboseLog || logging || (logFile != "") { + var err error + if logFile != "" { + logHandle, err = os.OpenFile(logFile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666) + if err != nil { + return nil, newSystemError("Failed to open log file:", logFile, err) + } + } else { + logHandle, err = ioutil.TempFile("", "hugo") + if err != nil { + return nil, newSystemError(err) + } + } + } else if !quiet && cfg.GetBool("verbose") { + stdoutThreshold = jww.LevelInfo + } + + if verboseLog { + logThreshold = jww.LevelInfo + } + + // The global logger is used in some few cases. + jww.SetLogOutput(logHandle) + jww.SetLogThreshold(logThreshold) + jww.SetStdoutThreshold(stdoutThreshold) + helpers.InitLoggers() + + return jww.NewNotepad(stdoutThreshold, logThreshold, outHandle, logHandle, "", log.Ldate|log.Ltime), nil +} + +func (c *commandeer) initializeFlags(cmd *cobra.Command) { + persFlagKeys := []string{"verbose", "logFile"} + flagKeys := []string{ + "cleanDestinationDir", + "buildDrafts", + "buildFuture", + "buildExpired", + "uglyURLs", + "canonifyURLs", + "disable404", + "disableRSS", + "disableSitemap", + "enableRobotsTXT", + "enableGitInfo", + "pluralizeListTitles", + "preserveTaxonomyNames", + "ignoreCache", + "forceSyncStatic", + "noTimes", + "noChmod", + } + + // Remove these in Hugo 0.23. + if cmd.Flags().Changed("disable404") { + helpers.Deprecated("command line", "--disable404", "Use --disableKinds=404", false) + } + + if cmd.Flags().Changed("disableRSS") { + helpers.Deprecated("command line", "--disableRSS", "Use --disableKinds=RSS", false) + } + + if cmd.Flags().Changed("disableSitemap") { + helpers.Deprecated("command line", "--disableSitemap", "Use --disableKinds=sitemap", false) + } + + for _, key := range persFlagKeys { + c.setValueFromFlag(cmd.PersistentFlags(), key) + } + for _, key := range flagKeys { + c.setValueFromFlag(cmd.Flags(), key) + } + +} + +func (c *commandeer) setValueFromFlag(flags *flag.FlagSet, key string) { + if flags.Changed(key) { + f := flags.Lookup(key) + c.Set(key, f.Value.String()) + } +} + +func (c *commandeer) watchConfig() { + v := c.Cfg.(*viper.Viper) + v.WatchConfig() + v.OnConfigChange(func(e fsnotify.Event) { + c.Logger.FEEDBACK.Println("Config file changed:", e.Name) + // Force a full rebuild + utils.CheckErr(c.Logger, c.recreateAndBuildSites(true)) + if !c.Cfg.GetBool("disableLiveReload") { + // Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized + livereload.ForceRefresh() + } + }) +} + +func (c *commandeer) build(watches ...bool) error { + if err := c.copyStatic(); err != nil { + return fmt.Errorf("Error copying static files to %s: %s", c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")), err) + } + watch := false + if len(watches) > 0 && watches[0] { + watch = true + } + if err := c.buildSites(buildWatch || watch); err != nil { + return fmt.Errorf("Error building site: %s", err) + } + + if buildWatch { + c.Logger.FEEDBACK.Println("Watching for changes in", c.PathSpec().AbsPathify(c.Cfg.GetString("contentDir"))) + c.Logger.FEEDBACK.Println("Press Ctrl+C to stop") + utils.CheckErr(c.Logger, c.newWatcher(0)) + } + + return nil +} + +func (c *commandeer) getStaticSourceFs() afero.Fs { + source := c.Fs.Source + themeDir, err := c.PathSpec().GetThemeStaticDirPath() + staticDir := c.PathSpec().GetStaticDirPath() + helpers.FilePathSeparator + useTheme := true + useStatic := true + + if err != nil { + if err != helpers.ErrThemeUndefined { + c.Logger.WARN.Println(err) + } + useTheme = false + } else { + if _, err := source.Stat(themeDir); os.IsNotExist(err) { + c.Logger.WARN.Println("Unable to find Theme Static Directory:", themeDir) + useTheme = false + } + } + + if _, err := source.Stat(staticDir); os.IsNotExist(err) { + c.Logger.WARN.Println("Unable to find Static Directory:", staticDir) + useStatic = false + } + + if !useStatic && !useTheme { + return nil + } + + if !useStatic { + c.Logger.INFO.Println(themeDir, "is the only static directory available to sync from") + return afero.NewReadOnlyFs(afero.NewBasePathFs(source, themeDir)) + } + + if !useTheme { + c.Logger.INFO.Println(staticDir, "is the only static directory available to sync from") + return afero.NewReadOnlyFs(afero.NewBasePathFs(source, staticDir)) + } + + c.Logger.INFO.Println("using a UnionFS for static directory comprised of:") + c.Logger.INFO.Println("Base:", themeDir) + c.Logger.INFO.Println("Overlay:", staticDir) + base := afero.NewReadOnlyFs(afero.NewBasePathFs(source, themeDir)) + overlay := afero.NewReadOnlyFs(afero.NewBasePathFs(source, staticDir)) + return afero.NewCopyOnWriteFs(base, overlay) +} + +func (c *commandeer) copyStatic() error { + publishDir := c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")) + helpers.FilePathSeparator + + // If root, remove the second '/' + if publishDir == "//" { + publishDir = helpers.FilePathSeparator + } + + // Includes both theme/static & /static + staticSourceFs := c.getStaticSourceFs() + + if staticSourceFs == nil { + c.Logger.WARN.Println("No static directories found to sync") + return nil + } + + syncer := fsync.NewSyncer() + syncer.NoTimes = c.Cfg.GetBool("noTimes") + syncer.NoChmod = c.Cfg.GetBool("noChmod") + syncer.SrcFs = staticSourceFs + syncer.DestFs = c.Fs.Destination + // Now that we are using a unionFs for the static directories + // We can effectively clean the publishDir on initial sync + syncer.Delete = c.Cfg.GetBool("cleanDestinationDir") + + if syncer.Delete { + c.Logger.INFO.Println("removing all files from destination that don't exist in static dirs") + + syncer.DeleteFilter = func(f os.FileInfo) bool { + return f.IsDir() && strings.HasPrefix(f.Name(), ".") + } + } + c.Logger.INFO.Println("syncing static files to", publishDir) + + // because we are using a baseFs (to get the union right). + // set sync src to root + return syncer.Sync(publishDir, helpers.FilePathSeparator) +} + +// getDirList provides NewWatcher() with a list of directories to watch for changes. +func (c *commandeer) getDirList() []string { + var a []string + dataDir := c.PathSpec().AbsPathify(c.Cfg.GetString("dataDir")) + i18nDir := c.PathSpec().AbsPathify(c.Cfg.GetString("i18nDir")) + layoutDir := c.PathSpec().GetLayoutDirPath() + staticDir := c.PathSpec().GetStaticDirPath() + + walker := func(path string, fi os.FileInfo, err error) error { + if err != nil { + if path == dataDir && os.IsNotExist(err) { + c.Logger.WARN.Println("Skip dataDir:", err) + return nil + } + + if path == i18nDir && os.IsNotExist(err) { + c.Logger.WARN.Println("Skip i18nDir:", err) + return nil + } + + if path == layoutDir && os.IsNotExist(err) { + c.Logger.WARN.Println("Skip layoutDir:", err) + return nil + } + + if path == staticDir && os.IsNotExist(err) { + c.Logger.WARN.Println("Skip staticDir:", err) + return nil + } + + if os.IsNotExist(err) { + // Ignore. + return nil + } + + c.Logger.ERROR.Println("Walker: ", err) + return nil + } + + // Skip .git directories. + // Related to https://github.com/gohugoio/hugo/issues/3468. + if fi.Name() == ".git" { + return nil + } + + if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + link, err := filepath.EvalSymlinks(path) + if err != nil { + c.Logger.ERROR.Printf("Cannot read symbolic link '%s', error was: %s", path, err) + return nil + } + linkfi, err := c.Fs.Source.Stat(link) + if err != nil { + c.Logger.ERROR.Printf("Cannot stat '%s', error was: %s", link, err) + return nil + } + if !linkfi.Mode().IsRegular() { + c.Logger.ERROR.Printf("Symbolic links for directories not supported, skipping '%s'", path) + } + return nil + } + + if fi.IsDir() { + if fi.Name() == ".git" || + fi.Name() == "node_modules" || fi.Name() == "bower_components" { + return filepath.SkipDir + } + a = append(a, path) + } + return nil + } + + // SymbolicWalk will log anny ERRORs + _ = helpers.SymbolicWalk(c.Fs.Source, dataDir, walker) + _ = helpers.SymbolicWalk(c.Fs.Source, c.PathSpec().AbsPathify(c.Cfg.GetString("contentDir")), walker) + _ = helpers.SymbolicWalk(c.Fs.Source, i18nDir, walker) + _ = helpers.SymbolicWalk(c.Fs.Source, layoutDir, walker) + _ = helpers.SymbolicWalk(c.Fs.Source, staticDir, walker) + + if c.PathSpec().ThemeSet() { + themesDir := c.PathSpec().GetThemeDir() + _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "layouts"), walker) + _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "static"), walker) + _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "i18n"), walker) + _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "data"), walker) + } + + return a +} + +func (c *commandeer) recreateAndBuildSites(watching bool) (err error) { + if err := c.initSites(); err != nil { + return err + } + if !quiet { + c.Logger.FEEDBACK.Println("Started building sites ...") + } + return Hugo.Build(hugolib.BuildCfg{CreateSitesFromConfig: true, Watching: watching, PrintStats: !quiet}) +} + +func (c *commandeer) resetAndBuildSites(watching bool) (err error) { + if err = c.initSites(); err != nil { + return + } + if !quiet { + c.Logger.FEEDBACK.Println("Started building sites ...") + } + return Hugo.Build(hugolib.BuildCfg{ResetState: true, Watching: watching, PrintStats: !quiet}) +} + +func (c *commandeer) initSites() error { + if Hugo != nil { + return nil + } + h, err := hugolib.NewHugoSites(*c.DepsCfg) + + if err != nil { + return err + } + Hugo = h + + return nil +} + +func (c *commandeer) buildSites(watching bool) (err error) { + if err := c.initSites(); err != nil { + return err + } + if !quiet { + c.Logger.FEEDBACK.Println("Started building sites ...") + } + return Hugo.Build(hugolib.BuildCfg{Watching: watching, PrintStats: !quiet}) +} + +func (c *commandeer) rebuildSites(events []fsnotify.Event) error { + if err := c.initSites(); err != nil { + return err + } + return Hugo.Build(hugolib.BuildCfg{PrintStats: !quiet, Watching: true}, events...) +} + +// newWatcher creates a new watcher to watch filesystem events. +func (c *commandeer) newWatcher(port int) error { + if runtime.GOOS == "darwin" { + tweakLimit() + } + + watcher, err := watcher.New(1 * time.Second) + var wg sync.WaitGroup + + if err != nil { + return err + } + + defer watcher.Close() + + wg.Add(1) + + for _, d := range c.getDirList() { + if d != "" { + _ = watcher.Add(d) + } + } + + go func() { + for { + select { + case evs := <-watcher.Events: + c.Logger.INFO.Println("Received System Events:", evs) + + staticEvents := []fsnotify.Event{} + dynamicEvents := []fsnotify.Event{} + + for _, ev := range evs { + ext := filepath.Ext(ev.Name) + baseName := filepath.Base(ev.Name) + istemp := strings.HasSuffix(ext, "~") || + (ext == ".swp") || // vim + (ext == ".swx") || // vim + (ext == ".tmp") || // generic temp file + (ext == ".DS_Store") || // OSX Thumbnail + baseName == "4913" || // vim + strings.HasPrefix(ext, ".goutputstream") || // gnome + strings.HasSuffix(ext, "jb_old___") || // intelliJ + strings.HasSuffix(ext, "jb_tmp___") || // intelliJ + strings.HasSuffix(ext, "jb_bak___") || // intelliJ + strings.HasPrefix(ext, ".sb-") || // byword + strings.HasPrefix(baseName, ".#") || // emacs + strings.HasPrefix(baseName, "#") // emacs + if istemp { + continue + } + // Sometimes during rm -rf operations a '"": REMOVE' is triggered. Just ignore these + if ev.Name == "" { + continue + } + + // Write and rename operations are often followed by CHMOD. + // There may be valid use cases for rebuilding the site on CHMOD, + // but that will require more complex logic than this simple conditional. + // On OS X this seems to be related to Spotlight, see: + // https://github.com/go-fsnotify/fsnotify/issues/15 + // A workaround is to put your site(s) on the Spotlight exception list, + // but that may be a little mysterious for most end users. + // So, for now, we skip reload on CHMOD. + // We do have to check for WRITE though. On slower laptops a Chmod + // could be aggregated with other important events, and we still want + // to rebuild on those + if ev.Op&(fsnotify.Chmod|fsnotify.Write|fsnotify.Create) == fsnotify.Chmod { + continue + } + + walkAdder := func(path string, f os.FileInfo, err error) error { + if f.IsDir() { + c.Logger.FEEDBACK.Println("adding created directory to watchlist", path) + if err := watcher.Add(path); err != nil { + return err + } + } else if !c.isStatic(path) { + // Hugo's rebuilding logic is entirely file based. When you drop a new folder into + // /content on OSX, the above logic will handle future watching of those files, + // but the initial CREATE is lost. + dynamicEvents = append(dynamicEvents, fsnotify.Event{Name: path, Op: fsnotify.Create}) + } + return nil + } + + // recursively add new directories to watch list + // When mkdir -p is used, only the top directory triggers an event (at least on OSX) + if ev.Op&fsnotify.Create == fsnotify.Create { + if s, err := c.Fs.Source.Stat(ev.Name); err == nil && s.Mode().IsDir() { + _ = helpers.SymbolicWalk(c.Fs.Source, ev.Name, walkAdder) + } + } + + if c.isStatic(ev.Name) { + staticEvents = append(staticEvents, ev) + } else { + dynamicEvents = append(dynamicEvents, ev) + } + } + + if len(staticEvents) > 0 { + publishDir := c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")) + helpers.FilePathSeparator + + // If root, remove the second '/' + if publishDir == "//" { + publishDir = helpers.FilePathSeparator + } + + c.Logger.FEEDBACK.Println("\nStatic file changes detected") + const layout = "2006-01-02 15:04 -0700" + c.Logger.FEEDBACK.Println(time.Now().Format(layout)) + + if c.Cfg.GetBool("forceSyncStatic") { + c.Logger.FEEDBACK.Printf("Syncing all static files\n") + err := c.copyStatic() + if err != nil { + utils.StopOnErr(c.Logger, err, fmt.Sprintf("Error copying static files to %s", publishDir)) + } + } else { + staticSourceFs := c.getStaticSourceFs() + + if staticSourceFs == nil { + c.Logger.WARN.Println("No static directories found to sync") + return + } + + syncer := fsync.NewSyncer() + syncer.NoTimes = c.Cfg.GetBool("noTimes") + syncer.NoChmod = c.Cfg.GetBool("noChmod") + syncer.SrcFs = staticSourceFs + syncer.DestFs = c.Fs.Destination + + // prevent spamming the log on changes + logger := helpers.NewDistinctFeedbackLogger() + + for _, ev := range staticEvents { + // Due to our approach of layering both directories and the content's rendered output + // into one we can't accurately remove a file not in one of the source directories. + // If a file is in the local static dir and also in the theme static dir and we remove + // it from one of those locations we expect it to still exist in the destination + // + // If Hugo generates a file (from the content dir) over a static file + // the content generated file should take precedence. + // + // Because we are now watching and handling individual events it is possible that a static + // event that occupies the same path as a content generated file will take precedence + // until a regeneration of the content takes places. + // + // Hugo assumes that these cases are very rare and will permit this bad behavior + // The alternative is to track every single file and which pipeline rendered it + // and then to handle conflict resolution on every event. + + fromPath := ev.Name + + // If we are here we already know the event took place in a static dir + relPath, err := c.PathSpec().MakeStaticPathRelative(fromPath) + if err != nil { + c.Logger.ERROR.Println(err) + continue + } + + // Remove || rename is harder and will require an assumption. + // Hugo takes the following approach: + // If the static file exists in any of the static source directories after this event + // Hugo will re-sync it. + // If it does not exist in all of the static directories Hugo will remove it. + // + // This assumes that Hugo has not generated content on top of a static file and then removed + // the source of that static file. In this case Hugo will incorrectly remove that file + // from the published directory. + if ev.Op&fsnotify.Rename == fsnotify.Rename || ev.Op&fsnotify.Remove == fsnotify.Remove { + if _, err := staticSourceFs.Stat(relPath); os.IsNotExist(err) { + // If file doesn't exist in any static dir, remove it + toRemove := filepath.Join(publishDir, relPath) + logger.Println("File no longer exists in static dir, removing", toRemove) + _ = c.Fs.Destination.RemoveAll(toRemove) + } else if err == nil { + // If file still exists, sync it + logger.Println("Syncing", relPath, "to", publishDir) + if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil { + c.Logger.ERROR.Println(err) + } + } else { + c.Logger.ERROR.Println(err) + } + + continue + } + + // For all other event operations Hugo will sync static. + logger.Println("Syncing", relPath, "to", publishDir) + if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil { + c.Logger.ERROR.Println(err) + } + } + } + + if !buildWatch && !c.Cfg.GetBool("disableLiveReload") { + // Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized + + // force refresh when more than one file + if len(staticEvents) > 0 { + for _, ev := range staticEvents { + path, _ := c.PathSpec().MakeStaticPathRelative(ev.Name) + livereload.RefreshPath(path) + } + + } else { + livereload.ForceRefresh() + } + } + } + + if len(dynamicEvents) > 0 { + c.Logger.FEEDBACK.Println("\nChange detected, rebuilding site") + const layout = "2006-01-02 15:04 -0700" + c.Logger.FEEDBACK.Println(time.Now().Format(layout)) + + if err := c.rebuildSites(dynamicEvents); err != nil { + c.Logger.ERROR.Println("Failed to rebuild site:", err) + } + + if !buildWatch && !c.Cfg.GetBool("disableLiveReload") { + // Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized + livereload.ForceRefresh() + } + } + case err := <-watcher.Errors: + if err != nil { + c.Logger.ERROR.Println(err) + } + } + } + }() + + if port > 0 { + if !c.Cfg.GetBool("disableLiveReload") { + livereload.Initialize() + http.HandleFunc("/livereload.js", livereload.ServeJS) + http.HandleFunc("/livereload", livereload.Handler) + } + + go c.serve(port) + } + + wg.Wait() + return nil +} + +func (c *commandeer) isStatic(path string) bool { + return strings.HasPrefix(path, c.PathSpec().GetStaticDirPath()) || (len(c.PathSpec().GetThemesDirPath()) > 0 && strings.HasPrefix(path, c.PathSpec().GetThemesDirPath())) +} + +// isThemeVsHugoVersionMismatch returns whether the current Hugo version is +// less than the theme's min_version. +func (c *commandeer) isThemeVsHugoVersionMismatch() (mismatch bool, requiredMinVersion string) { + if !c.PathSpec().ThemeSet() { + return + } + + themeDir := c.PathSpec().GetThemeDir() + + path := filepath.Join(themeDir, "theme.toml") + + exists, err := helpers.Exists(path, c.Fs.Source) + + if err != nil || !exists { + return + } + + b, err := afero.ReadFile(c.Fs.Source, path) + + tomlMeta, err := parser.HandleTOMLMetaData(b) + + if err != nil { + return + } + + config := tomlMeta.(map[string]interface{}) + + if minVersion, ok := config["min_version"]; ok { + return helpers.CompareVersion(minVersion) > 0, fmt.Sprint(minVersion) + } + + return +} diff --git a/commands/hugo_windows.go b/commands/hugo_windows.go new file mode 100644 index 000000000..7342f21a0 --- /dev/null +++ b/commands/hugo_windows.go @@ -0,0 +1,27 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import "github.com/spf13/cobra" + +func init() { + // This message to show to Windows users if Hugo is opened from explorer.exe + cobra.MousetrapHelpText = ` + + Hugo is a command-line tool for generating static website. + + You need to open cmd.exe and run Hugo from there. + + Visit http://gohugo.io/ for more information.` +} diff --git a/commands/import_jekyll.go b/commands/import_jekyll.go new file mode 100644 index 000000000..26a41c82a --- /dev/null +++ b/commands/import_jekyll.go @@ -0,0 +1,577 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "bytes" + "errors" + "io" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/hugolib" + "github.com/gohugoio/hugo/parser" + "github.com/spf13/afero" + "github.com/spf13/cast" + "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" +) + +func init() { + importCmd.AddCommand(importJekyllCmd) +} + +var importCmd = &cobra.Command{ + Use: "import", + Short: "Import your site from others.", + Long: `Import your site from other web site generators like Jekyll. + +Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.", + RunE: nil, +} + +var importJekyllCmd = &cobra.Command{ + Use: "jekyll", + Short: "hugo import from Jekyll", + Long: `hugo import from Jekyll. + +Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.", + RunE: importFromJekyll, +} + +func init() { + importJekyllCmd.Flags().Bool("force", false, "allow import into non-empty target directory") +} + +func importFromJekyll(cmd *cobra.Command, args []string) error { + + if len(args) < 2 { + return newUserError(`Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.") + } + + jekyllRoot, err := filepath.Abs(filepath.Clean(args[0])) + if err != nil { + return newUserError("Path error:", args[0]) + } + + targetDir, err := filepath.Abs(filepath.Clean(args[1])) + if err != nil { + return newUserError("Path error:", args[1]) + } + + jww.INFO.Println("Import Jekyll from:", jekyllRoot, "to:", targetDir) + + if strings.HasPrefix(filepath.Dir(targetDir), jekyllRoot) { + return newUserError("Target path should not be inside the Jekyll root, aborting.") + } + + forceImport, _ := cmd.Flags().GetBool("force") + site, err := createSiteFromJekyll(jekyllRoot, targetDir, forceImport) + if err != nil { + return err + } + + jww.FEEDBACK.Println("Importing...") + + fileCount := 0 + callback := func(path string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + + if fi.IsDir() { + return nil + } + + relPath, err := filepath.Rel(jekyllRoot, path) + if err != nil { + return newUserError("Get rel path error:", path) + } + + relPath = filepath.ToSlash(relPath) + draft := false + + switch { + case strings.HasPrefix(relPath, "_posts/"): + relPath = "content/post" + relPath[len("_posts"):] + case strings.HasPrefix(relPath, "_drafts/"): + relPath = "content/draft" + relPath[len("_drafts"):] + draft = true + default: + return nil + } + + fileCount++ + return convertJekyllPost(site, path, relPath, targetDir, draft) + } + + err = helpers.SymbolicWalk(hugofs.Os, jekyllRoot, callback) + + if err != nil { + return err + } + jww.FEEDBACK.Println("Congratulations!", fileCount, "post(s) imported!") + jww.FEEDBACK.Println("Now, start Hugo by yourself:\n" + + "$ git clone https://github.com/spf13/herring-cove.git " + args[1] + "/themes/herring-cove") + jww.FEEDBACK.Println("$ cd " + args[1] + "\n$ hugo server --theme=herring-cove") + + return nil +} + +// TODO: Consider calling doNewSite() instead? +func createSiteFromJekyll(jekyllRoot, targetDir string, force bool) (*hugolib.Site, error) { + s, err := hugolib.NewSiteDefaultLang() + if err != nil { + return nil, err + } + + fs := s.Fs.Source + + if exists, _ := helpers.Exists(targetDir, fs); exists { + if isDir, _ := helpers.IsDir(targetDir, fs); !isDir { + return nil, errors.New("Target path \"" + targetDir + "\" already exists but not a directory") + } + + isEmpty, _ := helpers.IsEmpty(targetDir, fs) + + if !isEmpty && !force { + return nil, errors.New("Target path \"" + targetDir + "\" already exists and is not empty") + } + } + + jekyllConfig := loadJekyllConfig(fs, jekyllRoot) + + // Crude test to make sure at least one of _drafts/ and _posts/ exists + // and is not empty. + hasPostsOrDrafts := false + postsDir := filepath.Join(jekyllRoot, "_posts") + draftsDir := filepath.Join(jekyllRoot, "_drafts") + for _, d := range []string{postsDir, draftsDir} { + if exists, _ := helpers.Exists(d, fs); exists { + if isDir, _ := helpers.IsDir(d, fs); isDir { + if isEmpty, _ := helpers.IsEmpty(d, fs); !isEmpty { + hasPostsOrDrafts = true + } + } + } + } + if !hasPostsOrDrafts { + return nil, errors.New("Your Jekyll root contains neither posts nor drafts, aborting.") + } + + mkdir(targetDir, "layouts") + mkdir(targetDir, "content") + mkdir(targetDir, "archetypes") + mkdir(targetDir, "static") + mkdir(targetDir, "data") + mkdir(targetDir, "themes") + + createConfigFromJekyll(fs, targetDir, "yaml", jekyllConfig) + + copyJekyllFilesAndFolders(jekyllRoot, filepath.Join(targetDir, "static")) + + return s, nil +} + +func loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]interface{} { + path := filepath.Join(jekyllRoot, "_config.yml") + + exists, err := helpers.Exists(path, fs) + + if err != nil || !exists { + jww.WARN.Println("_config.yaml not found: Is the specified Jekyll root correct?") + return nil + } + + f, err := fs.Open(path) + if err != nil { + return nil + } + + defer f.Close() + + b, err := ioutil.ReadAll(f) + + if err != nil { + return nil + } + + c, err := parser.HandleYAMLMetaData(b) + + if err != nil { + return nil + } + + return c.(map[string]interface{}) +} + +func createConfigFromJekyll(fs afero.Fs, inpath string, kind string, jekyllConfig map[string]interface{}) (err error) { + title := "My New Hugo Site" + baseURL := "http://example.org/" + + for key, value := range jekyllConfig { + lowerKey := strings.ToLower(key) + + switch lowerKey { + case "title": + if str, ok := value.(string); ok { + title = str + } + + case "url": + if str, ok := value.(string); ok { + baseURL = str + } + } + } + + in := map[string]interface{}{ + "baseURL": baseURL, + "title": title, + "languageCode": "en-us", + "disablePathToLower": true, + } + kind = parser.FormatSanitize(kind) + + var buf bytes.Buffer + err = parser.InterfaceToConfig(in, parser.FormatToLeadRune(kind), &buf) + if err != nil { + return err + } + + return helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), &buf, fs) +} + +func copyFile(source string, dest string) error { + sf, err := os.Open(source) + if err != nil { + return err + } + defer sf.Close() + df, err := os.Create(dest) + if err != nil { + return err + } + defer df.Close() + _, err = io.Copy(df, sf) + if err == nil { + si, err := os.Stat(source) + if err != nil { + err = os.Chmod(dest, si.Mode()) + + if err != nil { + return err + } + } + + } + return nil +} + +func copyDir(source string, dest string) error { + fi, err := os.Stat(source) + if err != nil { + return err + } + if !fi.IsDir() { + return errors.New(source + " is not a directory") + } + err = os.MkdirAll(dest, fi.Mode()) + if err != nil { + return err + } + entries, err := ioutil.ReadDir(source) + for _, entry := range entries { + sfp := filepath.Join(source, entry.Name()) + dfp := filepath.Join(dest, entry.Name()) + if entry.IsDir() { + err = copyDir(sfp, dfp) + if err != nil { + jww.ERROR.Println(err) + } + } else { + err = copyFile(sfp, dfp) + if err != nil { + jww.ERROR.Println(err) + } + } + + } + return nil +} + +func copyJekyllFilesAndFolders(jekyllRoot string, dest string) error { + fi, err := os.Stat(jekyllRoot) + if err != nil { + return err + } + if !fi.IsDir() { + return errors.New(jekyllRoot + " is not a directory") + } + err = os.MkdirAll(dest, fi.Mode()) + if err != nil { + return err + } + entries, err := ioutil.ReadDir(jekyllRoot) + for _, entry := range entries { + sfp := filepath.Join(jekyllRoot, entry.Name()) + dfp := filepath.Join(dest, entry.Name()) + if entry.IsDir() { + if entry.Name()[0] != '_' && entry.Name()[0] != '.' { + err = copyDir(sfp, dfp) + if err != nil { + jww.ERROR.Println(err) + } + } + } else { + lowerEntryName := strings.ToLower(entry.Name()) + exceptSuffix := []string{".md", ".markdown", ".html", ".htm", + ".xml", ".textile", "rakefile", "gemfile", ".lock"} + isExcept := false + for _, suffix := range exceptSuffix { + if strings.HasSuffix(lowerEntryName, suffix) { + isExcept = true + break + } + } + + if !isExcept && entry.Name()[0] != '.' && entry.Name()[0] != '_' { + err = copyFile(sfp, dfp) + if err != nil { + jww.ERROR.Println(err) + } + } + } + + } + return nil +} + +func parseJekyllFilename(filename string) (time.Time, string, error) { + re := regexp.MustCompile(`(\d+-\d+-\d+)-(.+)\..*`) + r := re.FindAllStringSubmatch(filename, -1) + if len(r) == 0 { + return time.Now(), "", errors.New("filename not match") + } + + postDate, err := time.Parse("2006-1-2", r[0][1]) + if err != nil { + return time.Now(), "", err + } + + postName := r[0][2] + + return postDate, postName, nil +} + +func convertJekyllPost(s *hugolib.Site, path, relPath, targetDir string, draft bool) error { + jww.TRACE.Println("Converting", path) + + filename := filepath.Base(path) + postDate, postName, err := parseJekyllFilename(filename) + if err != nil { + jww.WARN.Printf("Failed to parse filename '%s': %s. Skipping.", filename, err) + return nil + } + + jww.TRACE.Println(filename, postDate, postName) + + targetFile := filepath.Join(targetDir, relPath) + targetParentDir := filepath.Dir(targetFile) + os.MkdirAll(targetParentDir, 0777) + + contentBytes, err := ioutil.ReadFile(path) + if err != nil { + jww.ERROR.Println("Read file error:", path) + return err + } + + psr, err := parser.ReadFrom(bytes.NewReader(contentBytes)) + if err != nil { + jww.ERROR.Println("Parse file error:", path) + return err + } + + metadata, err := psr.Metadata() + if err != nil { + jww.ERROR.Println("Processing file error:", path) + return err + } + + newmetadata, err := convertJekyllMetaData(metadata, postName, postDate, draft) + if err != nil { + jww.ERROR.Println("Convert metadata error:", path) + return err + } + + jww.TRACE.Println(newmetadata) + content := convertJekyllContent(newmetadata, string(psr.Content())) + + page, err := s.NewPage(filename) + if err != nil { + jww.ERROR.Println("New page error", filename) + return err + } + + page.SetDir(targetParentDir) + page.SetSourceContent([]byte(content)) + page.SetSourceMetaData(newmetadata, parser.FormatToLeadRune("yaml")) + page.SaveSourceAs(targetFile) + + jww.TRACE.Println("Target file:", targetFile) + + return nil +} + +func convertJekyllMetaData(m interface{}, postName string, postDate time.Time, draft bool) (interface{}, error) { + url := postDate.Format("/2006/01/02/") + postName + "/" + + metadata, err := cast.ToStringMapE(m) + if err != nil { + return nil, err + } + + if draft { + metadata["draft"] = true + } + + for key, value := range metadata { + lowerKey := strings.ToLower(key) + + switch lowerKey { + case "layout": + delete(metadata, key) + case "permalink": + if str, ok := value.(string); ok { + url = str + } + delete(metadata, key) + case "category": + if str, ok := value.(string); ok { + metadata["categories"] = []string{str} + } + delete(metadata, key) + case "excerpt_separator": + if key != lowerKey { + delete(metadata, key) + metadata[lowerKey] = value + } + case "date": + if str, ok := value.(string); ok { + re := regexp.MustCompile(`(\d+):(\d+):(\d+)`) + r := re.FindAllStringSubmatch(str, -1) + if len(r) > 0 { + hour, _ := strconv.Atoi(r[0][1]) + minute, _ := strconv.Atoi(r[0][2]) + second, _ := strconv.Atoi(r[0][3]) + postDate = time.Date(postDate.Year(), postDate.Month(), postDate.Day(), hour, minute, second, 0, time.UTC) + } + } + delete(metadata, key) + } + + } + + metadata["url"] = url + metadata["date"] = postDate.Format(time.RFC3339) + + return metadata, nil +} + +func convertJekyllContent(m interface{}, content string) string { + metadata, _ := cast.ToStringMapE(m) + + lines := strings.Split(content, "\n") + var resultLines []string + for _, line := range lines { + resultLines = append(resultLines, strings.Trim(line, "\r\n")) + } + + content = strings.Join(resultLines, "\n") + + excerptSep := "<!--more-->" + if value, ok := metadata["excerpt_separator"]; ok { + if str, strOk := value.(string); strOk { + content = strings.Replace(content, strings.TrimSpace(str), excerptSep, -1) + } + } + + replaceList := []struct { + re *regexp.Regexp + replace string + }{ + {regexp.MustCompile("(?i)<!-- more -->"), "<!--more-->"}, + {regexp.MustCompile(`\{%\s*raw\s*%\}\s*(.*?)\s*\{%\s*endraw\s*%\}`), "$1"}, + {regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`), "{{< highlight $1 >}}"}, + {regexp.MustCompile(`{%\s*endhighlight\s*%}`), "{{< / highlight >}}"}, + } + + for _, replace := range replaceList { + content = replace.re.ReplaceAllString(content, replace.replace) + } + + replaceListFunc := []struct { + re *regexp.Regexp + replace func(string) string + }{ + // Octopress image tag: http://octopress.org/docs/plugins/image-tag/ + {regexp.MustCompile(`{%\s+img\s*(.*?)\s*%}`), replaceImageTag}, + } + + for _, replace := range replaceListFunc { + content = replace.re.ReplaceAllStringFunc(content, replace.replace) + } + + return content +} + +func replaceImageTag(match string) string { + r := regexp.MustCompile(`{%\s+img\s*(\p{L}*)\s+([\S]*/[\S]+)\s+(\d*)\s*(\d*)\s*(.*?)\s*%}`) + result := bytes.NewBufferString("{{< figure ") + parts := r.FindStringSubmatch(match) + // Index 0 is the entire string, ignore + replaceOptionalPart(result, "class", parts[1]) + replaceOptionalPart(result, "src", parts[2]) + replaceOptionalPart(result, "width", parts[3]) + replaceOptionalPart(result, "height", parts[4]) + // title + alt + part := parts[5] + if len(part) > 0 { + splits := strings.Split(part, "'") + lenSplits := len(splits) + if lenSplits == 1 { + replaceOptionalPart(result, "title", splits[0]) + } else if lenSplits == 3 { + replaceOptionalPart(result, "title", splits[1]) + } else if lenSplits == 5 { + replaceOptionalPart(result, "title", splits[1]) + replaceOptionalPart(result, "alt", splits[3]) + } + } + result.WriteString(">}}") + return result.String() + +} +func replaceOptionalPart(buffer *bytes.Buffer, partName string, part string) { + if len(part) > 0 { + buffer.WriteString(partName + "=\"" + part + "\" ") + } +} diff --git a/commands/import_jekyll_test.go b/commands/import_jekyll_test.go new file mode 100644 index 000000000..90a05c01c --- /dev/null +++ b/commands/import_jekyll_test.go @@ -0,0 +1,126 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "encoding/json" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestParseJekyllFilename(t *testing.T) { + filenameArray := []string{ + "2015-01-02-test.md", + "2012-03-15-中文.markup", + } + + expectResult := []struct { + postDate time.Time + postName string + }{ + {time.Date(2015, time.January, 2, 0, 0, 0, 0, time.UTC), "test"}, + {time.Date(2012, time.March, 15, 0, 0, 0, 0, time.UTC), "中文"}, + } + + for i, filename := range filenameArray { + postDate, postName, err := parseJekyllFilename(filename) + assert.Equal(t, err, nil) + assert.Equal(t, expectResult[i].postDate.Format("2006-01-02"), postDate.Format("2006-01-02")) + assert.Equal(t, expectResult[i].postName, postName) + } +} + +func TestConvertJekyllMetadata(t *testing.T) { + testDataList := []struct { + metadata interface{} + postName string + postDate time.Time + draft bool + expect string + }{ + {map[interface{}]interface{}{}, "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false, + `{"date":"2015-10-01T00:00:00Z","url":"/2015/10/01/testPost/"}`}, + {map[interface{}]interface{}{}, "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), true, + `{"date":"2015-10-01T00:00:00Z","draft":true,"url":"/2015/10/01/testPost/"}`}, + {map[interface{}]interface{}{"Permalink": "/permalink.html", "layout": "post"}, + "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false, + `{"date":"2015-10-01T00:00:00Z","url":"/permalink.html"}`}, + {map[interface{}]interface{}{"permalink": "/permalink.html"}, + "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false, + `{"date":"2015-10-01T00:00:00Z","url":"/permalink.html"}`}, + {map[interface{}]interface{}{"category": nil, "permalink": 123}, + "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false, + `{"date":"2015-10-01T00:00:00Z","url":"/2015/10/01/testPost/"}`}, + {map[interface{}]interface{}{"Excerpt_Separator": "sep"}, + "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false, + `{"date":"2015-10-01T00:00:00Z","excerpt_separator":"sep","url":"/2015/10/01/testPost/"}`}, + {map[interface{}]interface{}{"category": "book", "layout": "post", "Others": "Goods", "Date": "2015-10-01 12:13:11"}, + "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false, + `{"Others":"Goods","categories":["book"],"date":"2015-10-01T12:13:11Z","url":"/2015/10/01/testPost/"}`}, + } + + for _, data := range testDataList { + result, err := convertJekyllMetaData(data.metadata, data.postName, data.postDate, data.draft) + assert.Equal(t, nil, err) + jsonResult, err := json.Marshal(result) + assert.Equal(t, nil, err) + assert.Equal(t, data.expect, string(jsonResult)) + } +} + +func TestConvertJekyllContent(t *testing.T) { + testDataList := []struct { + metadata interface{} + content string + expect string + }{ + {map[interface{}]interface{}{}, + `Test content\n<!-- more -->\npart2 content`, `Test content\n<!--more-->\npart2 content`}, + {map[interface{}]interface{}{}, + `Test content\n<!-- More -->\npart2 content`, `Test content\n<!--more-->\npart2 content`}, + {map[interface{}]interface{}{"excerpt_separator": "<!--sep-->"}, + `Test content\n<!--sep-->\npart2 content`, `Test content\n<!--more-->\npart2 content`}, + {map[interface{}]interface{}{}, "{% raw %}text{% endraw %}", "text"}, + {map[interface{}]interface{}{}, "{%raw%} text2 {%endraw %}", "text2"}, + {map[interface{}]interface{}{}, + "{% highlight go %}\nvar s int\n{% endhighlight %}", + "{{< highlight go >}}\nvar s int\n{{< / highlight >}}"}, + + // Octopress image tag + {map[interface{}]interface{}{}, + "{% img http://placekitten.com/890/280 %}", + "{{< figure src=\"http://placekitten.com/890/280\" >}}"}, + {map[interface{}]interface{}{}, + "{% img left http://placekitten.com/320/250 Place Kitten #2 %}", + "{{< figure class=\"left\" src=\"http://placekitten.com/320/250\" title=\"Place Kitten #2\" >}}"}, + {map[interface{}]interface{}{}, + "{% img right http://placekitten.com/300/500 150 250 'Place Kitten #3' %}", + "{{< figure class=\"right\" src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #3\" >}}"}, + {map[interface{}]interface{}{}, + "{% img right http://placekitten.com/300/500 150 250 'Place Kitten #4' 'An image of a very cute kitten' %}", + "{{< figure class=\"right\" src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}"}, + {map[interface{}]interface{}{}, + "{% img http://placekitten.com/300/500 150 250 'Place Kitten #4' 'An image of a very cute kitten' %}", + "{{< figure src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}"}, + {map[interface{}]interface{}{}, + "{% img right /placekitten/300/500 'Place Kitten #4' 'An image of a very cute kitten' %}", + "{{< figure class=\"right\" src=\"/placekitten/300/500\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}"}, + } + + for _, data := range testDataList { + result := convertJekyllContent(data.metadata, data.content) + assert.Equal(t, data.expect, result) + } +} diff --git a/commands/limit_darwin.go b/commands/limit_darwin.go new file mode 100644 index 000000000..9246f4497 --- /dev/null +++ b/commands/limit_darwin.go @@ -0,0 +1,85 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "syscall" + + "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" +) + +func init() { + checkCmd.AddCommand(limit) +} + +var limit = &cobra.Command{ + Use: "ulimit", + Short: "Check system ulimit settings", + Long: `Hugo will inspect the current ulimit settings on the system. +This is primarily to ensure that Hugo can watch enough files on some OSs`, + RunE: func(cmd *cobra.Command, args []string) error { + var rLimit syscall.Rlimit + err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) + if err != nil { + return newSystemError("Error Getting Rlimit ", err) + } + + jww.FEEDBACK.Println("Current rLimit:", rLimit) + + jww.FEEDBACK.Println("Attempting to increase limit") + rLimit.Max = 999999 + rLimit.Cur = 999999 + err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit) + if err != nil { + return newSystemError("Error Setting rLimit ", err) + } + err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) + if err != nil { + return newSystemError("Error Getting rLimit ", err) + } + jww.FEEDBACK.Println("rLimit after change:", rLimit) + + return nil + }, +} + +func tweakLimit() { + var rLimit syscall.Rlimit + err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) + if err != nil { + jww.ERROR.Println("Unable to obtain rLimit", err) + } + if rLimit.Cur < rLimit.Max { + rLimit.Max = 64000 + rLimit.Cur = 64000 + err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit) + if err != nil { + jww.WARN.Println("Unable to increase number of open files limit", err) + } + } +} diff --git a/commands/limit_others.go b/commands/limit_others.go new file mode 100644 index 000000000..c757f174e --- /dev/null +++ b/commands/limit_others.go @@ -0,0 +1,32 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build !darwin +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +func tweakLimit() { + // nothing to do +} diff --git a/commands/list.go b/commands/list.go new file mode 100644 index 000000000..b2a6b5395 --- /dev/null +++ b/commands/list.go @@ -0,0 +1,161 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "path/filepath" + + "github.com/gohugoio/hugo/hugolib" + "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" +) + +func init() { + listCmd.AddCommand(listDraftsCmd) + listCmd.AddCommand(listFutureCmd) + listCmd.AddCommand(listExpiredCmd) + listCmd.PersistentFlags().StringVarP(&source, "source", "s", "", "filesystem path to read files relative from") + listCmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{}) +} + +var listCmd = &cobra.Command{ + Use: "list", + Short: "Listing out various types of content", + Long: `Listing out various types of content. + +List requires a subcommand, e.g. ` + "`hugo list drafts`.", + RunE: nil, +} + +var listDraftsCmd = &cobra.Command{ + Use: "drafts", + Short: "List all drafts", + Long: `List all of the drafts in your content directory.`, + RunE: func(cmd *cobra.Command, args []string) error { + + cfg, err := InitializeConfig() + if err != nil { + return err + } + + c, err := newCommandeer(cfg) + if err != nil { + return err + } + + c.Set("buildDrafts", true) + + sites, err := hugolib.NewHugoSites(*cfg) + + if err != nil { + return newSystemError("Error creating sites", err) + } + + if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil { + return newSystemError("Error Processing Source Content", err) + } + + for _, p := range sites.Pages() { + if p.IsDraft() { + jww.FEEDBACK.Println(filepath.Join(p.File.Dir(), p.File.LogicalName())) + } + + } + + return nil + + }, +} + +var listFutureCmd = &cobra.Command{ + Use: "future", + Short: "List all posts dated in the future", + Long: `List all of the posts in your content directory which will be +posted in the future.`, + RunE: func(cmd *cobra.Command, args []string) error { + + cfg, err := InitializeConfig() + if err != nil { + return err + } + + c, err := newCommandeer(cfg) + if err != nil { + return err + } + + c.Set("buildFuture", true) + + sites, err := hugolib.NewHugoSites(*cfg) + + if err != nil { + return newSystemError("Error creating sites", err) + } + + if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil { + return newSystemError("Error Processing Source Content", err) + } + + for _, p := range sites.Pages() { + if p.IsFuture() { + jww.FEEDBACK.Println(filepath.Join(p.File.Dir(), p.File.LogicalName())) + } + + } + + return nil + + }, +} + +var listExpiredCmd = &cobra.Command{ + Use: "expired", + Short: "List all posts already expired", + Long: `List all of the posts in your content directory which has already +expired.`, + RunE: func(cmd *cobra.Command, args []string) error { + + cfg, err := InitializeConfig() + if err != nil { + return err + } + + c, err := newCommandeer(cfg) + if err != nil { + return err + } + + c.Set("buildExpired", true) + + sites, err := hugolib.NewHugoSites(*cfg) + + if err != nil { + return newSystemError("Error creating sites", err) + } + + if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil { + return newSystemError("Error Processing Source Content", err) + } + + for _, p := range sites.Pages() { + if p.IsExpired() { + jww.FEEDBACK.Println(filepath.Join(p.File.Dir(), p.File.LogicalName())) + } + + } + + return nil + + }, +} diff --git a/commands/list_config.go b/commands/list_config.go new file mode 100644 index 000000000..f47f6e144 --- /dev/null +++ b/commands/list_config.go @@ -0,0 +1,66 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.Print the version number of Hug + +package commands + +import ( + "reflect" + "sort" + + "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/viper" +) + +var configCmd = &cobra.Command{ + Use: "config", + Short: "Print the site configuration", + Long: `Print the site configuration, both default and custom settings.`, +} + +func init() { + configCmd.RunE = printConfig +} + +func printConfig(cmd *cobra.Command, args []string) error { + cfg, err := InitializeConfig(configCmd) + + if err != nil { + return err + } + + allSettings := cfg.Cfg.(*viper.Viper).AllSettings() + + var separator string + if allSettings["metadataformat"] == "toml" { + separator = " = " + } else { + separator = ": " + } + + var keys []string + for k := range allSettings { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + kv := reflect.ValueOf(allSettings[k]) + if kv.Kind() == reflect.String { + jww.FEEDBACK.Printf("%s%s\"%+v\"\n", k, separator, allSettings[k]) + } else { + jww.FEEDBACK.Printf("%s%s%+v\n", k, separator, allSettings[k]) + } + } + + return nil +} diff --git a/commands/new.go b/commands/new.go new file mode 100644 index 000000000..6a6e0615f --- /dev/null +++ b/commands/new.go @@ -0,0 +1,399 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "bytes" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/gohugoio/hugo/create" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/hugolib" + "github.com/gohugoio/hugo/parser" + "github.com/spf13/afero" + "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/viper" +) + +var ( + configFormat string + contentEditor string + contentType string +) + +func init() { + newSiteCmd.Flags().StringVarP(&configFormat, "format", "f", "toml", "config & frontmatter format") + newSiteCmd.Flags().Bool("force", false, "init inside non-empty directory") + newCmd.Flags().StringVarP(&contentType, "kind", "k", "", "content type to create") + newCmd.PersistentFlags().StringVarP(&source, "source", "s", "", "filesystem path to read files relative from") + newCmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{}) + newCmd.Flags().StringVar(&contentEditor, "editor", "", "edit new content with this editor, if provided") + + newCmd.AddCommand(newSiteCmd) + newCmd.AddCommand(newThemeCmd) + +} + +var newCmd = &cobra.Command{ + Use: "new [path]", + Short: "Create new content for your site", + Long: `Create a new content file and automatically set the date and title. +It will guess which kind of file to create based on the path provided. + +You can also specify the kind with ` + "`-k KIND`" + `. + +If archetypes are provided in your theme or site, they will be used.`, + + RunE: NewContent, +} + +var newSiteCmd = &cobra.Command{ + Use: "site [path]", + Short: "Create a new site (skeleton)", + Long: `Create a new site in the provided directory. +The new site will have the correct structure, but no content or theme yet. +Use ` + "`hugo new [contentPath]`" + ` to create new content.`, + RunE: NewSite, +} + +var newThemeCmd = &cobra.Command{ + Use: "theme [name]", + Short: "Create a new theme", + Long: `Create a new theme (skeleton) called [name] in the current directory. +New theme is a skeleton. Please add content to the touched files. Add your +name to the copyright line in the license and adjust the theme.toml file +as you see fit.`, + RunE: NewTheme, +} + +// NewContent adds new content to a Hugo site. +func NewContent(cmd *cobra.Command, args []string) error { + cfg, err := InitializeConfig() + + if err != nil { + return err + } + + c, err := newCommandeer(cfg) + if err != nil { + return err + } + + if cmd.Flags().Changed("editor") { + c.Set("newContentEditor", contentEditor) + } + + if len(args) < 1 { + return newUserError("path needs to be provided") + } + + createPath := args[0] + + var kind string + + createPath, kind = newContentPathSection(createPath) + + if contentType != "" { + kind = contentType + } + + ps, err := helpers.NewPathSpec(cfg.Fs, cfg.Cfg) + if err != nil { + return err + } + + // If a site isn't in use in the archetype template, we can skip the build. + siteFactory := func(filename string, siteUsed bool) (*hugolib.Site, error) { + if !siteUsed { + return hugolib.NewSite(*cfg) + } + var s *hugolib.Site + if err := c.initSites(); err != nil { + return nil, err + } + + if err := Hugo.Build(hugolib.BuildCfg{SkipRender: true, PrintStats: false}); err != nil { + return nil, err + } + + s = Hugo.Sites[0] + + if len(Hugo.Sites) > 1 { + // Find the best match. + for _, ss := range Hugo.Sites { + if strings.Contains(createPath, "."+ss.Language.Lang) { + s = ss + break + } + } + } + return s, nil + } + + return create.NewContent(ps, siteFactory, kind, createPath) +} + +func doNewSite(fs *hugofs.Fs, basepath string, force bool) error { + archeTypePath := filepath.Join(basepath, "archetypes") + dirs := []string{ + filepath.Join(basepath, "layouts"), + filepath.Join(basepath, "content"), + archeTypePath, + filepath.Join(basepath, "static"), + filepath.Join(basepath, "data"), + filepath.Join(basepath, "themes"), + } + + if exists, _ := helpers.Exists(basepath, fs.Source); exists { + if isDir, _ := helpers.IsDir(basepath, fs.Source); !isDir { + return errors.New(basepath + " already exists but not a directory") + } + + isEmpty, _ := helpers.IsEmpty(basepath, fs.Source) + + switch { + case !isEmpty && !force: + return errors.New(basepath + " already exists and is not empty") + + case !isEmpty && force: + all := append(dirs, filepath.Join(basepath, "config."+configFormat)) + for _, path := range all { + if exists, _ := helpers.Exists(path, fs.Source); exists { + return errors.New(path + " already exists") + } + } + } + } + + for _, dir := range dirs { + if err := fs.Source.MkdirAll(dir, 0777); err != nil { + return fmt.Errorf("Failed to create dir: %s", err) + } + } + + createConfig(fs, basepath, configFormat) + + // Create a defaul archetype file. + helpers.SafeWriteToDisk(filepath.Join(archeTypePath, "default.md"), + strings.NewReader(create.ArchetypeTemplateTemplate), fs.Source) + + jww.FEEDBACK.Printf("Congratulations! Your new Hugo site is created in %s.\n\n", basepath) + jww.FEEDBACK.Println(nextStepsText()) + + return nil +} + +func nextStepsText() string { + var nextStepsText bytes.Buffer + + nextStepsText.WriteString(`Just a few more steps and you're ready to go: + +1. Download a theme into the same-named folder. + Choose a theme from https://themes.gohugo.io/, or + create your own with the "hugo new theme <THEMENAME>" command. +2. Perhaps you want to add some content. You can add single files + with "hugo new `) + + nextStepsText.WriteString(filepath.Join("<SECTIONNAME>", "<FILENAME>.<FORMAT>")) + + nextStepsText.WriteString(`". +3. Start the built-in live server via "hugo server". + +Visit https://gohugo.io/ for quickstart guide and full documentation.`) + + return nextStepsText.String() +} + +// NewSite creates a new Hugo site and initializes a structured Hugo directory. +func NewSite(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return newUserError("path needs to be provided") + } + + createpath, err := filepath.Abs(filepath.Clean(args[0])) + if err != nil { + return newUserError(err) + } + + forceNew, _ := cmd.Flags().GetBool("force") + + return doNewSite(hugofs.NewDefault(viper.New()), createpath, forceNew) +} + +// NewTheme creates a new Hugo theme. +func NewTheme(cmd *cobra.Command, args []string) error { + cfg, err := InitializeConfig() + + if err != nil { + return err + } + + if len(args) < 1 { + return newUserError("theme name needs to be provided") + } + + c, err := newCommandeer(cfg) + if err != nil { + return err + } + + createpath := c.PathSpec().AbsPathify(filepath.Join(c.Cfg.GetString("themesDir"), args[0])) + jww.INFO.Println("creating theme at", createpath) + + if x, _ := helpers.Exists(createpath, cfg.Fs.Source); x { + return newUserError(createpath, "already exists") + } + + mkdir(createpath, "layouts", "_default") + mkdir(createpath, "layouts", "partials") + + touchFile(cfg.Fs.Source, createpath, "layouts", "index.html") + touchFile(cfg.Fs.Source, createpath, "layouts", "404.html") + touchFile(cfg.Fs.Source, createpath, "layouts", "_default", "list.html") + touchFile(cfg.Fs.Source, createpath, "layouts", "_default", "single.html") + + touchFile(cfg.Fs.Source, createpath, "layouts", "partials", "header.html") + touchFile(cfg.Fs.Source, createpath, "layouts", "partials", "footer.html") + + mkdir(createpath, "archetypes") + + archDefault := []byte("+++\n+++\n") + + err = helpers.WriteToDisk(filepath.Join(createpath, "archetypes", "default.md"), bytes.NewReader(archDefault), cfg.Fs.Source) + if err != nil { + return err + } + + mkdir(createpath, "static", "js") + mkdir(createpath, "static", "css") + + by := []byte(`The MIT License (MIT) + +Copyright (c) ` + time.Now().Format("2006") + ` YOUR_NAME_HERE + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +`) + + err = helpers.WriteToDisk(filepath.Join(createpath, "LICENSE.md"), bytes.NewReader(by), cfg.Fs.Source) + if err != nil { + return err + } + + createThemeMD(cfg.Fs, createpath) + + return nil +} + +func mkdir(x ...string) { + p := filepath.Join(x...) + + err := os.MkdirAll(p, 0777) // before umask + if err != nil { + jww.FATAL.Fatalln(err) + } +} + +func touchFile(fs afero.Fs, x ...string) { + inpath := filepath.Join(x...) + mkdir(filepath.Dir(inpath)) + err := helpers.WriteToDisk(inpath, bytes.NewReader([]byte{}), fs) + if err != nil { + jww.FATAL.Fatalln(err) + } +} + +func createThemeMD(fs *hugofs.Fs, inpath string) (err error) { + + by := []byte(`# theme.toml template for a Hugo theme +# See https://github.com/gohugoio/hugoThemes#themetoml for an example + +name = "` + strings.Title(helpers.MakeTitle(filepath.Base(inpath))) + `" +license = "MIT" +licenselink = "https://github.com/yourname/yourtheme/blob/master/LICENSE.md" +description = "" +homepage = "http://example.com/" +tags = [] +features = [] +min_version = "0.24" + +[author] + name = "" + homepage = "" + +# If porting an existing theme +[original] + name = "" + homepage = "" + repo = "" +`) + + err = helpers.WriteToDisk(filepath.Join(inpath, "theme.toml"), bytes.NewReader(by), fs.Source) + if err != nil { + return + } + + return nil +} + +func newContentPathSection(path string) (string, string) { + // Forward slashes is used in all examples. Convert if needed. + // Issue #1133 + createpath := filepath.FromSlash(path) + var section string + // assume the first directory is the section (kind) + if strings.Contains(createpath[1:], helpers.FilePathSeparator) { + section = helpers.GuessSection(createpath) + } + + return createpath, section +} + +func createConfig(fs *hugofs.Fs, inpath string, kind string) (err error) { + in := map[string]string{ + "baseURL": "http://example.org/", + "title": "My New Hugo Site", + "languageCode": "en-us", + } + kind = parser.FormatSanitize(kind) + + var buf bytes.Buffer + err = parser.InterfaceToConfig(in, parser.FormatToLeadRune(kind), &buf) + if err != nil { + return err + } + + return helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), &buf, fs.Source) +} diff --git a/commands/new_test.go b/commands/new_test.go new file mode 100644 index 000000000..c9adb83d4 --- /dev/null +++ b/commands/new_test.go @@ -0,0 +1,122 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "path/filepath" + "testing" + + "github.com/gohugoio/hugo/hugofs" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Issue #1133 +func TestNewContentPathSectionWithForwardSlashes(t *testing.T) { + p, s := newContentPathSection("/post/new.md") + assert.Equal(t, filepath.FromSlash("/post/new.md"), p) + assert.Equal(t, "post", s) +} + +func checkNewSiteInited(fs *hugofs.Fs, basepath string, t *testing.T) { + + paths := []string{ + filepath.Join(basepath, "layouts"), + filepath.Join(basepath, "content"), + filepath.Join(basepath, "archetypes"), + filepath.Join(basepath, "static"), + filepath.Join(basepath, "data"), + filepath.Join(basepath, "config.toml"), + } + + for _, path := range paths { + _, err := fs.Source.Stat(path) + require.NoError(t, err) + } +} + +func TestDoNewSite(t *testing.T) { + basepath := filepath.Join("base", "blog") + _, fs := newTestCfg() + + require.NoError(t, doNewSite(fs, basepath, false)) + + checkNewSiteInited(fs, basepath, t) +} + +func TestDoNewSite_noerror_base_exists_but_empty(t *testing.T) { + basepath := filepath.Join("base", "blog") + _, fs := newTestCfg() + + require.NoError(t, fs.Source.MkdirAll(basepath, 777)) + + require.NoError(t, doNewSite(fs, basepath, false)) +} + +func TestDoNewSite_error_base_exists(t *testing.T) { + basepath := filepath.Join("base", "blog") + _, fs := newTestCfg() + + require.NoError(t, fs.Source.MkdirAll(basepath, 777)) + _, err := fs.Source.Create(filepath.Join(basepath, "foo")) + require.NoError(t, err) + // Since the directory already exists and isn't empty, expect an error + require.Error(t, doNewSite(fs, basepath, false)) + +} + +func TestDoNewSite_force_empty_dir(t *testing.T) { + basepath := filepath.Join("base", "blog") + _, fs := newTestCfg() + + require.NoError(t, fs.Source.MkdirAll(basepath, 777)) + + require.NoError(t, doNewSite(fs, basepath, true)) + + checkNewSiteInited(fs, basepath, t) +} + +func TestDoNewSite_error_force_dir_inside_exists(t *testing.T) { + basepath := filepath.Join("base", "blog") + _, fs := newTestCfg() + + contentPath := filepath.Join(basepath, "content") + + require.NoError(t, fs.Source.MkdirAll(contentPath, 777)) + require.Error(t, doNewSite(fs, basepath, true)) +} + +func TestDoNewSite_error_force_config_inside_exists(t *testing.T) { + basepath := filepath.Join("base", "blog") + _, fs := newTestCfg() + + configPath := filepath.Join(basepath, "config.toml") + require.NoError(t, fs.Source.MkdirAll(basepath, 777)) + _, err := fs.Source.Create(configPath) + require.NoError(t, err) + + require.Error(t, doNewSite(fs, basepath, true)) +} + +func newTestCfg() (*viper.Viper, *hugofs.Fs) { + + v := viper.New() + fs := hugofs.NewMem(v) + + v.SetFs(fs.Source) + + return v, fs + +} diff --git a/commands/release.go b/commands/release.go new file mode 100644 index 000000000..c9275a0f0 --- /dev/null +++ b/commands/release.go @@ -0,0 +1,62 @@ +// +build release + +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "github.com/gohugoio/hugo/releaser" + "github.com/spf13/cobra" +) + +func init() { + HugoCmd.AddCommand(createReleaser().cmd) +} + +type releaseCommandeer struct { + cmd *cobra.Command + + // Will be zero for main releases. + patchLevel int + + skipPublish bool + + step int +} + +func createReleaser() *releaseCommandeer { + // Note: This is a command only meant for internal use and must be run + // via "go run -tags release main.go release" on the actual code base that is in the release. + r := &releaseCommandeer{ + cmd: &cobra.Command{ + Use: "release", + Short: "Release a new version of Hugo.", + Hidden: true, + }, + } + + r.cmd.RunE = func(cmd *cobra.Command, args []string) error { + return r.release() + } + + r.cmd.PersistentFlags().IntVarP(&r.patchLevel, "patch", "p", 0, "patch level, defaults to 0 for main releases") + r.cmd.PersistentFlags().IntVarP(&r.step, "step", "s", -1, "release step, defaults to -1 for all steps.") + r.cmd.PersistentFlags().BoolVarP(&r.skipPublish, "skip-publish", "", false, "skip all publishing pipes of the release") + + return r +} + +func (r *releaseCommandeer) release() error { + return releaser.New(r.patchLevel, r.step, r.skipPublish).Run() +} diff --git a/commands/server.go b/commands/server.go new file mode 100644 index 000000000..32a94f9c8 --- /dev/null +++ b/commands/server.go @@ -0,0 +1,309 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "fmt" + "net" + "net/http" + "net/url" + "os" + "runtime" + "strconv" + "strings" + "time" + + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/helpers" + "github.com/spf13/afero" + "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" +) + +var ( + disableLiveReload bool + renderToDisk bool + serverAppend bool + serverInterface string + serverPort int + serverWatch bool +) + +var serverCmd = &cobra.Command{ + Use: "server", + Aliases: []string{"serve"}, + Short: "A high performance webserver", + Long: `Hugo provides its own webserver which builds and serves the site. +While hugo server is high performance, it is a webserver with limited options. +Many run it in production, but the standard behavior is for people to use it +in development and use a more full featured server such as Nginx or Caddy. + +'hugo server' will avoid writing the rendered and served content to disk, +preferring to store it in memory. + +By default hugo will also watch your files for any changes you make and +automatically rebuild the site. It will then live reload any open browser pages +and push the latest content to them. As most Hugo sites are built in a fraction +of a second, you will be able to save and see your changes nearly instantly.`, + //RunE: server, +} + +type filesOnlyFs struct { + fs http.FileSystem +} + +type noDirFile struct { + http.File +} + +func (fs filesOnlyFs) Open(name string) (http.File, error) { + f, err := fs.fs.Open(name) + if err != nil { + return nil, err + } + return noDirFile{f}, nil +} + +func (f noDirFile) Readdir(count int) ([]os.FileInfo, error) { + return nil, nil +} + +func init() { + initHugoBuilderFlags(serverCmd) + + serverCmd.Flags().IntVarP(&serverPort, "port", "p", 1313, "port on which the server will listen") + serverCmd.Flags().StringVarP(&serverInterface, "bind", "", "127.0.0.1", "interface to which the server will bind") + serverCmd.Flags().BoolVarP(&serverWatch, "watch", "w", true, "watch filesystem for changes and recreate as needed") + serverCmd.Flags().BoolVarP(&serverAppend, "appendPort", "", true, "append port to baseURL") + serverCmd.Flags().BoolVar(&disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild") + serverCmd.Flags().BoolVar(&renderToDisk, "renderToDisk", false, "render to Destination path (default is render to memory & serve from there)") + serverCmd.Flags().String("memstats", "", "log memory usage to this file") + serverCmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".") + + serverCmd.RunE = server + +} + +func server(cmd *cobra.Command, args []string) error { + cfg, err := InitializeConfig(serverCmd) + if err != nil { + return err + } + + c, err := newCommandeer(cfg) + if err != nil { + return err + } + + if cmd.Flags().Changed("disableLiveReload") { + c.Set("disableLiveReload", disableLiveReload) + } + + if serverWatch { + c.Set("watch", true) + } + + if c.Cfg.GetBool("watch") { + serverWatch = true + c.watchConfig() + } + + l, err := net.Listen("tcp", net.JoinHostPort(serverInterface, strconv.Itoa(serverPort))) + if err == nil { + l.Close() + } else { + if serverCmd.Flags().Changed("port") { + // port set explicitly by user -- he/she probably meant it! + return newSystemErrorF("Server startup failed: %s", err) + } + jww.ERROR.Println("port", serverPort, "already in use, attempting to use an available port") + sp, err := helpers.FindAvailablePort() + if err != nil { + return newSystemError("Unable to find alternative port to use:", err) + } + serverPort = sp.Port + } + + c.Set("port", serverPort) + + baseURL, err = fixURL(c.Cfg, baseURL) + if err != nil { + return err + } + c.Set("baseURL", baseURL) + + if err := memStats(); err != nil { + jww.ERROR.Println("memstats error:", err) + } + + // If a Destination is provided via flag write to disk + if destination != "" { + renderToDisk = true + } + + // Hugo writes the output to memory instead of the disk + if !renderToDisk { + cfg.Fs.Destination = new(afero.MemMapFs) + // Rendering to memoryFS, publish to Root regardless of publishDir. + c.Set("publishDir", "/") + } + + if err := c.build(serverWatch); err != nil { + return err + } + + for _, s := range Hugo.Sites { + s.RegisterMediaTypes() + } + + // Watch runs its own server as part of the routine + if serverWatch { + watchDirs := c.getDirList() + baseWatchDir := c.Cfg.GetString("workingDir") + for i, dir := range watchDirs { + watchDirs[i], _ = helpers.GetRelativePath(dir, baseWatchDir) + } + + rootWatchDirs := strings.Join(helpers.UniqueStrings(helpers.ExtractRootPaths(watchDirs)), ",") + + jww.FEEDBACK.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs) + err := c.newWatcher(serverPort) + + if err != nil { + return err + } + } + + c.serve(serverPort) + + return nil +} + +func (c *commandeer) serve(port int) { + if renderToDisk { + jww.FEEDBACK.Println("Serving pages from " + c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir"))) + } else { + jww.FEEDBACK.Println("Serving pages from memory") + } + + httpFs := afero.NewHttpFs(c.Fs.Destination) + fs := filesOnlyFs{httpFs.Dir(c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")))} + fileserver := http.FileServer(fs) + + // We're only interested in the path + u, err := url.Parse(c.Cfg.GetString("baseURL")) + if err != nil { + jww.ERROR.Fatalf("Invalid baseURL: %s", err) + } + if u.Path == "" || u.Path == "/" { + http.Handle("/", fileserver) + } else { + http.Handle(u.Path, http.StripPrefix(u.Path, fileserver)) + } + + jww.FEEDBACK.Printf("Web Server is available at %s (bind address %s)\n", u.String(), serverInterface) + jww.FEEDBACK.Println("Press Ctrl+C to stop") + + endpoint := net.JoinHostPort(serverInterface, strconv.Itoa(port)) + err = http.ListenAndServe(endpoint, nil) + if err != nil { + jww.ERROR.Printf("Error: %s\n", err.Error()) + os.Exit(1) + } +} + +// fixURL massages the baseURL into a form needed for serving +// all pages correctly. +func fixURL(cfg config.Provider, s string) (string, error) { + useLocalhost := false + if s == "" { + s = cfg.GetString("baseURL") + useLocalhost = true + } + + if !strings.HasSuffix(s, "/") { + s = s + "/" + } + + // do an initial parse of the input string + u, err := url.Parse(s) + if err != nil { + return "", err + } + + // if no Host is defined, then assume that no schema or double-slash were + // present in the url. Add a double-slash and make a best effort attempt. + if u.Host == "" && s != "/" { + s = "//" + s + + u, err = url.Parse(s) + if err != nil { + return "", err + } + } + + if useLocalhost { + if u.Scheme == "https" { + u.Scheme = "http" + } + u.Host = "localhost" + } + + if serverAppend { + if strings.Contains(u.Host, ":") { + u.Host, _, err = net.SplitHostPort(u.Host) + if err != nil { + return "", fmt.Errorf("Failed to split baseURL hostpost: %s", err) + } + } + u.Host += fmt.Sprintf(":%d", serverPort) + } + + return u.String(), nil +} + +func memStats() error { + memstats := serverCmd.Flags().Lookup("memstats").Value.String() + if memstats != "" { + interval, err := time.ParseDuration(serverCmd.Flags().Lookup("meminterval").Value.String()) + if err != nil { + interval, _ = time.ParseDuration("100ms") + } + + fileMemStats, err := os.Create(memstats) + if err != nil { + return err + } + + fileMemStats.WriteString("# Time\tHeapSys\tHeapAlloc\tHeapIdle\tHeapReleased\n") + + go func() { + var stats runtime.MemStats + + start := time.Now().UnixNano() + + for { + runtime.ReadMemStats(&stats) + if fileMemStats != nil { + fileMemStats.WriteString(fmt.Sprintf("%d\t%d\t%d\t%d\t%d\n", + (time.Now().UnixNano()-start)/1000000, stats.HeapSys, stats.HeapAlloc, stats.HeapIdle, stats.HeapReleased)) + time.Sleep(interval) + } else { + break + } + } + }() + } + return nil +} diff --git a/commands/server_test.go b/commands/server_test.go new file mode 100644 index 000000000..3f1518aaa --- /dev/null +++ b/commands/server_test.go @@ -0,0 +1,58 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "testing" + + "github.com/spf13/viper" +) + +func TestFixURL(t *testing.T) { + type data struct { + TestName string + CLIBaseURL string + CfgBaseURL string + AppendPort bool + Port int + Result string + } + tests := []data{ + {"Basic http localhost", "", "http://foo.com", true, 1313, "http://localhost:1313/"}, + {"Basic https production, http localhost", "", "https://foo.com", true, 1313, "http://localhost:1313/"}, + {"Basic subdir", "", "http://foo.com/bar", true, 1313, "http://localhost:1313/bar/"}, + {"Basic production", "http://foo.com", "http://foo.com", false, 80, "http://foo.com/"}, + {"Production subdir", "http://foo.com/bar", "http://foo.com/bar", false, 80, "http://foo.com/bar/"}, + {"No http", "", "foo.com", true, 1313, "//localhost:1313/"}, + {"Override configured port", "", "foo.com:2020", true, 1313, "//localhost:1313/"}, + {"No http production", "foo.com", "foo.com", false, 80, "//foo.com/"}, + {"No http production with port", "foo.com", "foo.com", true, 2020, "//foo.com:2020/"}, + {"No config", "", "", true, 1313, "//localhost:1313/"}, + } + + for i, test := range tests { + v := viper.New() + baseURL = test.CLIBaseURL + v.Set("baseURL", test.CfgBaseURL) + serverAppend = test.AppendPort + serverPort = test.Port + result, err := fixURL(v, baseURL) + if err != nil { + t.Errorf("Test #%d %s: unexpected error %s", i, test.TestName, err) + } + if result != test.Result { + t.Errorf("Test #%d %s: expected %q, got %q", i, test.TestName, test.Result, result) + } + } +} diff --git a/commands/undraft.go b/commands/undraft.go new file mode 100644 index 000000000..2a3b85360 --- /dev/null +++ b/commands/undraft.go @@ -0,0 +1,157 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "bytes" + "errors" + "os" + "time" + + "github.com/gohugoio/hugo/parser" + "github.com/spf13/cobra" +) + +var undraftCmd = &cobra.Command{ + Use: "undraft path/to/content", + Short: "Undraft changes the content's draft status from 'True' to 'False'", + Long: `Undraft changes the content's draft status from 'True' to 'False' +and updates the date to the current date and time. +If the content's draft status is 'False', nothing is done.`, + RunE: Undraft, +} + +// Undraft publishes the specified content by setting its draft status +// to false and setting its publish date to now. If the specified content is +// not a draft, it will log an error. +func Undraft(cmd *cobra.Command, args []string) error { + cfg, err := InitializeConfig() + + if err != nil { + return err + } + + if len(args) < 1 { + return newUserError("a piece of content needs to be specified") + } + + location := args[0] + // open the file + f, err := cfg.Fs.Source.Open(location) + if err != nil { + return err + } + + // get the page from file + p, err := parser.ReadFrom(f) + f.Close() + if err != nil { + return err + } + + w, err := undraftContent(p) + if err != nil { + return newSystemErrorF("an error occurred while undrafting %q: %s", location, err) + } + + f, err = cfg.Fs.Source.OpenFile(location, os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return newSystemErrorF("%q not be undrafted due to error opening file to save changes: %q\n", location, err) + } + defer f.Close() + _, err = w.WriteTo(f) + if err != nil { + return newSystemErrorF("%q not be undrafted due to save error: %q\n", location, err) + } + return nil +} + +// undraftContent: if the content is a draft, change its draft status to +// 'false' and set the date to time.Now(). If the draft status is already +// 'false', don't do anything. +func undraftContent(p parser.Page) (bytes.Buffer, error) { + var buff bytes.Buffer + // get the metadata; easiest way to see if it's a draft + meta, err := p.Metadata() + if err != nil { + return buff, err + } + // since the metadata was obtainable, we can also get the key/value separator for + // Front Matter + fm := p.FrontMatter() + if fm == nil { + return buff, errors.New("Front Matter was found, nothing was finalized") + } + + var isDraft, gotDate bool + var date string +L: + for k, v := range meta.(map[string]interface{}) { + switch k { + case "draft": + if !v.(bool) { + return buff, errors.New("not a Draft: nothing was done") + } + isDraft = true + if gotDate { + break L + } + case "date": + date = v.(string) // capture the value to make replacement easier + gotDate = true + if isDraft { + break L + } + } + } + + // if draft wasn't found in FrontMatter, it isn't a draft. + if !isDraft { + return buff, errors.New("not a Draft: nothing was done") + } + + // get the front matter as bytes and split it into lines + var lineEnding []byte + fmLines := bytes.Split(fm, []byte("\n")) + if len(fmLines) == 1 { // if the result is only 1 element, try to split on dos line endings + fmLines = bytes.Split(fm, []byte("\r\n")) + if len(fmLines) == 1 { + return buff, errors.New("unable to split FrontMatter into lines") + } + lineEnding = append(lineEnding, []byte("\r\n")...) + } else { + lineEnding = append(lineEnding, []byte("\n")...) + } + + // Write the front matter lines to the buffer, replacing as necessary + for _, v := range fmLines { + pos := bytes.Index(v, []byte("draft")) + if pos != -1 { + v = bytes.Replace(v, []byte("true"), []byte("false"), 1) + goto write + } + pos = bytes.Index(v, []byte("date")) + if pos != -1 { // if date field wasn't found, add it + v = bytes.Replace(v, []byte(date), []byte(time.Now().Format(time.RFC3339)), 1) + } + write: + buff.Write(v) + buff.Write(lineEnding) + } + + // append the actual content + buff.Write(p.Content()) + + return buff, nil +} diff --git a/commands/undraft_test.go b/commands/undraft_test.go new file mode 100644 index 000000000..7f32c7e20 --- /dev/null +++ b/commands/undraft_test.go @@ -0,0 +1,85 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +// TODO Support Mac Encoding (\r) + +import ( + "bytes" + "strings" + "testing" + "time" + + "github.com/gohugoio/hugo/parser" +) + +var ( + jsonFM = "{\n \"date\": \"12-04-06\",\n \"title\": \"test json\"\n}" + jsonDraftFM = "{\n \"draft\": true,\n \"date\": \"12-04-06\",\n \"title\":\"test json\"\n}" + tomlFM = "+++\n date= \"12-04-06\"\n title= \"test toml\"\n+++" + tomlDraftFM = "+++\n draft= true\n date= \"12-04-06\"\n title=\"test toml\"\n+++" + yamlFM = "---\n date: \"12-04-06\"\n title: \"test yaml\"\n---" + yamlDraftFM = "---\n draft: true\n date: \"12-04-06\"\n title: \"test yaml\"\n---" +) + +func TestUndraftContent(t *testing.T) { + tests := []struct { + fm string + expectedErr string + }{ + {jsonFM, "not a Draft: nothing was done"}, + {jsonDraftFM, ""}, + {tomlFM, "not a Draft: nothing was done"}, + {tomlDraftFM, ""}, + {yamlFM, "not a Draft: nothing was done"}, + {yamlDraftFM, ""}, + } + + for i, test := range tests { + r := bytes.NewReader([]byte(test.fm)) + p, _ := parser.ReadFrom(r) + res, err := undraftContent(p) + if test.expectedErr != "" { + if err == nil { + t.Errorf("[%d] Expected error, got none", i) + continue + } + if err.Error() != test.expectedErr { + t.Errorf("[%d] Expected %q, got %q", i, test.expectedErr, err) + continue + } + } else { + r = bytes.NewReader(res.Bytes()) + p, _ = parser.ReadFrom(r) + meta, err := p.Metadata() + if err != nil { + t.Errorf("[%d] unexpected error %q", i, err) + continue + } + for k, v := range meta.(map[string]interface{}) { + if k == "draft" { + if v.(bool) { + t.Errorf("[%d] Expected %q to be \"false\", got \"true\"", i, k) + continue + } + } + if k == "date" { + if !strings.HasPrefix(v.(string), time.Now().Format("2006-01-02")) { + t.Errorf("[%d] Expected %v to start with %v", i, v.(string), time.Now().Format("2006-01-02")) + } + } + } + } + } +} diff --git a/commands/version.go b/commands/version.go new file mode 100644 index 000000000..5cd398b2b --- /dev/null +++ b/commands/version.go @@ -0,0 +1,80 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugolib" + "github.com/kardianos/osext" + "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" +) + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print the version number of Hugo", + Long: `All software has versions. This is Hugo's.`, + RunE: func(cmd *cobra.Command, args []string) error { + printHugoVersion() + return nil + }, +} + +func printHugoVersion() { + if hugolib.BuildDate == "" { + setBuildDate() // set the build date from executable's mdate + } else { + formatBuildDate() // format the compile time + } + if hugolib.CommitHash == "" { + jww.FEEDBACK.Printf("Hugo Static Site Generator v%s %s/%s BuildDate: %s\n", helpers.CurrentHugoVersion, runtime.GOOS, runtime.GOARCH, hugolib.BuildDate) + } else { + jww.FEEDBACK.Printf("Hugo Static Site Generator v%s-%s %s/%s BuildDate: %s\n", helpers.CurrentHugoVersion, strings.ToUpper(hugolib.CommitHash), runtime.GOOS, runtime.GOARCH, hugolib.BuildDate) + } +} + +// setBuildDate checks the ModTime of the Hugo executable and returns it as a +// formatted string. This assumes that the executable name is Hugo, if it does +// not exist, an empty string will be returned. This is only called if the +// hugolib.BuildDate wasn't set during compile time. +// +// osext is used for cross-platform. +func setBuildDate() { + fname, _ := osext.Executable() + dir, err := filepath.Abs(filepath.Dir(fname)) + if err != nil { + jww.ERROR.Println(err) + return + } + fi, err := os.Lstat(filepath.Join(dir, filepath.Base(fname))) + if err != nil { + jww.ERROR.Println(err) + return + } + t := fi.ModTime() + hugolib.BuildDate = t.Format(time.RFC3339) +} + +// formatBuildDate formats the hugolib.BuildDate according to the value in +// .Params.DateFormat, if it's set. +func formatBuildDate() { + t, _ := time.Parse("2006-01-02T15:04:05-0700", hugolib.BuildDate) + hugolib.BuildDate = t.Format(time.RFC3339) +} diff --git a/config/configProvider.go b/config/configProvider.go new file mode 100644 index 000000000..870341f7f --- /dev/null +++ b/config/configProvider.go @@ -0,0 +1,26 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +// Provider provides the configuration settings for Hugo. +type Provider interface { + GetString(key string) string + GetInt(key string) int + GetBool(key string) bool + GetStringMap(key string) map[string]interface{} + GetStringMapString(key string) map[string]string + Get(key string) interface{} + Set(key string, value interface{}) + IsSet(key string) bool +} diff --git a/create/content.go b/create/content.go new file mode 100644 index 000000000..8af417294 --- /dev/null +++ b/create/content.go @@ -0,0 +1,129 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package create provides functions to create new content. +package create + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugolib" + jww "github.com/spf13/jwalterweatherman" +) + +// NewContent creates a new content file in the content directory based upon the +// given kind, which is used to lookup an archetype. +func NewContent( + ps *helpers.PathSpec, + siteFactory func(filename string, siteUsed bool) (*hugolib.Site, error), kind, targetPath string) error { + ext := helpers.Ext(targetPath) + + jww.INFO.Printf("attempting to create %q of %q of ext %q", targetPath, kind, ext) + + archetypeFilename := findArchetype(ps, kind, ext) + + // Building the sites can be expensive, so only do it if really needed. + siteUsed := false + + if archetypeFilename != "" { + f, err := ps.Fs.Source.Open(archetypeFilename) + if err != nil { + return err + } + defer f.Close() + + if helpers.ReaderContains(f, []byte(".Site")) { + siteUsed = true + } + } + + s, err := siteFactory(targetPath, siteUsed) + if err != nil { + return err + } + + var content []byte + + content, err = executeArcheTypeAsTemplate(s, kind, targetPath, archetypeFilename) + if err != nil { + return err + } + + contentPath := s.PathSpec.AbsPathify(filepath.Join(s.Cfg.GetString("contentDir"), targetPath)) + + if err := helpers.SafeWriteToDisk(contentPath, bytes.NewReader(content), s.Fs.Source); err != nil { + return err + } + + jww.FEEDBACK.Println(contentPath, "created") + + editor := s.Cfg.GetString("newContentEditor") + if editor != "" { + jww.FEEDBACK.Printf("Editing %s with %q ...\n", targetPath, editor) + + cmd := exec.Command(editor, contentPath) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() + } + + return nil +} + +// FindArchetype takes a given kind/archetype of content and returns an output +// path for that archetype. If no archetype is found, an empty string is +// returned. +func findArchetype(ps *helpers.PathSpec, kind, ext string) (outpath string) { + search := []string{ps.AbsPathify(ps.Cfg.GetString("archetypeDir"))} + + if ps.Cfg.GetString("theme") != "" { + themeDir := filepath.Join(ps.AbsPathify(ps.Cfg.GetString("themesDir")+"/"+ps.Cfg.GetString("theme")), "/archetypes/") + if _, err := ps.Fs.Source.Stat(themeDir); os.IsNotExist(err) { + jww.ERROR.Printf("Unable to find archetypes directory for theme %q at %q", ps.Cfg.GetString("theme"), themeDir) + } else { + search = append(search, themeDir) + } + } + + for _, x := range search { + // If the new content isn't in a subdirectory, kind == "". + // Therefore it should be excluded otherwise `is a directory` + // error will occur. github.com/gohugoio/hugo/issues/411 + var pathsToCheck = []string{"default"} + + if ext != "" { + if kind != "" { + pathsToCheck = append([]string{kind + ext, "default" + ext}, pathsToCheck...) + } else { + pathsToCheck = append([]string{"default" + ext}, pathsToCheck...) + } + } + + for _, p := range pathsToCheck { + curpath := filepath.Join(x, p) + jww.DEBUG.Println("checking", curpath, "for archetypes") + if exists, _ := helpers.Exists(curpath, ps.Fs.Source); exists { + jww.INFO.Println("curpath: " + curpath) + return curpath + } + } + } + + return "" +} diff --git a/create/content_template_handler.go b/create/content_template_handler.go new file mode 100644 index 000000000..0be495d15 --- /dev/null +++ b/create/content_template_handler.go @@ -0,0 +1,135 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package create + +import ( + "bytes" + "fmt" + "strings" + "time" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/source" + + "github.com/gohugoio/hugo/hugolib" + "github.com/gohugoio/hugo/tpl" + "github.com/spf13/afero" +) + +// ArchetypeFileData represents the data available to an archetype template. +type ArchetypeFileData struct { + // The archetype content type, either given as --kind option or extracted + // from the target path's section, i.e. "blog/mypost.md" will resolve to + // "blog". + Type string + + // The current date and time as a RFC3339 formatted string, suitable for use in front matter. + Date string + + // The Site, fully equipped with all the pages etc. Note: This will only be set if it is actually + // used in the archetype template. Also, if this is a multilingual setup, + // this site is the site that best matches the target content file, based + // on the presence of language code in the filename. + Site *hugolib.Site + + // The target content file. Note that the .Content will be empty, as that + // has not been created yet. + *source.File +} + +const ( + ArchetypeTemplateTemplate = `--- +title: "{{ replace .TranslationBaseName "-" " " | title }}" +date: {{ .Date }} +draft: true +--- + +` +) + +var ( + archetypeShortcodeReplacementsPre = strings.NewReplacer( + "{{<", "{x{<", + "{{%", "{x{%", + ">}}", ">}x}", + "%}}", "%}x}") + + archetypeShortcodeReplacementsPost = strings.NewReplacer( + "{x{<", "{{<", + "{x{%", "{{%", + ">}x}", ">}}", + "%}x}", "%}}") +) + +func executeArcheTypeAsTemplate(s *hugolib.Site, kind, targetPath, archetypeFilename string) ([]byte, error) { + + var ( + archetypeContent []byte + archetypeTemplate []byte + err error + ) + + sp := source.NewSourceSpec(s.Deps.Cfg, s.Deps.Fs) + f := sp.NewFile(targetPath) + + data := ArchetypeFileData{ + Type: kind, + Date: time.Now().Format(time.RFC3339), + File: f, + Site: s, + } + + if archetypeFilename == "" { + // TODO(bep) archetype revive the issue about wrong tpl funcs arg order + archetypeTemplate = []byte(ArchetypeTemplateTemplate) + } else { + archetypeTemplate, err = afero.ReadFile(s.Fs.Source, archetypeFilename) + if err != nil { + return nil, fmt.Errorf("Failed to read archetype file %q: %s", archetypeFilename, err) + } + + } + + // The archetype template may contain shortcodes, and these does not play well + // with the Go templates. Need to set some temporary delimiters. + archetypeTemplate = []byte(archetypeShortcodeReplacementsPre.Replace(string(archetypeTemplate))) + + // Reuse the Hugo template setup to get the template funcs properly set up. + templateHandler := s.Deps.Tmpl.(tpl.TemplateHandler) + templateName := "_text/" + helpers.Filename(archetypeFilename) + if err := templateHandler.AddTemplate(templateName, string(archetypeTemplate)); err != nil { + return nil, fmt.Errorf("Failed to parse archetype file %q: %s", archetypeFilename, err) + } + + templ := templateHandler.Lookup(templateName) + + var buff bytes.Buffer + if err := templ.Execute(&buff, data); err != nil { + return nil, fmt.Errorf("Failed to process archetype file %q: %s", archetypeFilename, err) + } + + archetypeContent = []byte(archetypeShortcodeReplacementsPost.Replace(buff.String())) + + if !bytes.Contains(archetypeContent, []byte("date")) || !bytes.Contains(archetypeContent, []byte("title")) { + // TODO(bep) remove some time in the future. + s.Log.FEEDBACK.Println(fmt.Sprintf(`WARNING: date and/or title missing from archetype file %q. +From Hugo 0.24 this must be provided in the archetype file itself, if needed. Example: +%s +`, archetypeFilename, ArchetypeTemplateTemplate)) + + } + + return archetypeContent, nil + +} diff --git a/create/content_test.go b/create/content_test.go new file mode 100644 index 000000000..914759164 --- /dev/null +++ b/create/content_test.go @@ -0,0 +1,198 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package create_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/gohugoio/hugo/deps" + + "github.com/gohugoio/hugo/hugolib" + + "fmt" + + "github.com/gohugoio/hugo/hugofs" + + "github.com/gohugoio/hugo/create" + "github.com/gohugoio/hugo/helpers" + "github.com/spf13/afero" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" +) + +func TestNewContent(t *testing.T) { + v := viper.New() + initViper(v) + + cases := []struct { + kind string + path string + expected []string + }{ + {"post", "post/sample-1.md", []string{`title = "Post Arch title"`, `test = "test1"`, "date = \"2015-01-12T19:20:04-07:00\""}}, + {"post", "post/org-1.org", []string{`#+title: ORG-1`}}, + {"emptydate", "post/sample-ed.md", []string{`title = "Empty Date Arch title"`, `test = "test1"`}}, + {"stump", "stump/sample-2.md", []string{`title: "Sample 2"`}}, // no archetype file + {"", "sample-3.md", []string{`title: "Sample 3"`}}, // no archetype + {"product", "product/sample-4.md", []string{`title = "SAMPLE-4"`}}, // empty archetype front matter + {"shortcodes", "shortcodes/go.md", []string{ + `title = "GO"`, + "{{< myshortcode >}}", + "{{% myshortcode %}}", + "{{</* comment */>}}\n{{%/* comment */%}}"}}, // shortcodes + } + + for _, c := range cases { + cfg, fs := newTestCfg() + ps, err := helpers.NewPathSpec(fs, cfg) + require.NoError(t, err) + h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs}) + require.NoError(t, err) + require.NoError(t, initFs(fs)) + + siteFactory := func(filename string, siteUsed bool) (*hugolib.Site, error) { + return h.Sites[0], nil + } + + require.NoError(t, create.NewContent(ps, siteFactory, c.kind, c.path)) + + fname := filepath.Join("content", filepath.FromSlash(c.path)) + content := readFileFromFs(t, fs.Source, fname) + for i, v := range c.expected { + found := strings.Contains(content, v) + if !found { + t.Errorf("[%d] %q missing from output:\n%q", i, v, content) + } + } + } +} + +func initViper(v *viper.Viper) { + v.Set("metaDataFormat", "toml") + v.Set("archetypeDir", "archetypes") + v.Set("contentDir", "content") + v.Set("themesDir", "themes") + v.Set("layoutDir", "layouts") + v.Set("i18nDir", "i18n") + v.Set("theme", "sample") +} + +func initFs(fs *hugofs.Fs) error { + perm := os.FileMode(0755) + var err error + + // create directories + dirs := []string{ + "archetypes", + "content", + filepath.Join("themes", "sample", "archetypes"), + } + for _, dir := range dirs { + err = fs.Source.Mkdir(dir, perm) + if err != nil { + return err + } + } + + // create files + for _, v := range []struct { + path string + content string + }{ + { + path: filepath.Join("archetypes", "post.md"), + content: "+++\ndate = \"2015-01-12T19:20:04-07:00\"\ntitle = \"Post Arch title\"\ntest = \"test1\"\n+++\n", + }, + { + path: filepath.Join("archetypes", "post.org"), + content: "#+title: {{ .BaseFileName | upper }}", + }, + { + path: filepath.Join("archetypes", "product.md"), + content: `+++ +title = "{{ .BaseFileName | upper }}" ++++`, + }, + { + path: filepath.Join("archetypes", "emptydate.md"), + content: "+++\ndate =\"\"\ntitle = \"Empty Date Arch title\"\ntest = \"test1\"\n+++\n", + }, + // #3623x + { + path: filepath.Join("archetypes", "shortcodes.md"), + content: `+++ +title = "{{ .BaseFileName | upper }}" ++++ + +{{< myshortcode >}} + +Some text. + +{{% myshortcode %}} +{{</* comment */>}} +{{%/* comment */%}} + + +`, + }, + } { + f, err := fs.Source.Create(v.path) + if err != nil { + return err + } + defer f.Close() + + _, err = f.Write([]byte(v.content)) + if err != nil { + return err + } + } + + return nil +} + +// TODO(bep) extract common testing package with this and some others +func readFileFromFs(t *testing.T, fs afero.Fs, filename string) string { + filename = filepath.FromSlash(filename) + b, err := afero.ReadFile(fs, filename) + if err != nil { + // Print some debug info + root := strings.Split(filename, helpers.FilePathSeparator)[0] + afero.Walk(fs, root, func(path string, info os.FileInfo, err error) error { + if info != nil && !info.IsDir() { + fmt.Println(" ", path) + } + + return nil + }) + t.Fatalf("Failed to read file: %s", err) + } + return string(b) +} + +func newTestCfg() (*viper.Viper, *hugofs.Fs) { + + v := viper.New() + fs := hugofs.NewMem(v) + + v.SetFs(fs.Source) + + initViper(v) + + return v, fs + +} diff --git a/deps/deps.go b/deps/deps.go new file mode 100644 index 000000000..b5f935c09 --- /dev/null +++ b/deps/deps.go @@ -0,0 +1,176 @@ +package deps + +import ( + "io/ioutil" + "log" + "os" + + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/output" + "github.com/gohugoio/hugo/tpl" + jww "github.com/spf13/jwalterweatherman" +) + +// Deps holds dependencies used by many. +// There will be normally be only one instance of deps in play +// at a given time, i.e. one per Site built. +type Deps struct { + // The logger to use. + Log *jww.Notepad `json:"-"` + + // The templates to use. This will usually implement the full tpl.TemplateHandler. + Tmpl tpl.TemplateFinder `json:"-"` + + // The file systems to use. + Fs *hugofs.Fs `json:"-"` + + // The PathSpec to use + *helpers.PathSpec `json:"-"` + + // The ContentSpec to use + *helpers.ContentSpec `json:"-"` + + // The configuration to use + Cfg config.Provider `json:"-"` + + // The translation func to use + Translate func(translationID string, args ...interface{}) string `json:"-"` + + Language *helpers.Language + + // All the output formats available for the current site. + OutputFormatsConfig output.Formats + + templateProvider ResourceProvider + WithTemplate func(templ tpl.TemplateHandler) error `json:"-"` + + translationProvider ResourceProvider +} + +// ResourceProvider is used to create and refresh, and clone resources needed. +type ResourceProvider interface { + Update(deps *Deps) error + Clone(deps *Deps) error +} + +func (d *Deps) TemplateHandler() tpl.TemplateHandler { + return d.Tmpl.(tpl.TemplateHandler) +} + +func (d *Deps) LoadResources() error { + // Note that the translations need to be loaded before the templates. + if err := d.translationProvider.Update(d); err != nil { + return err + } + + if err := d.templateProvider.Update(d); err != nil { + return err + } + + if th, ok := d.Tmpl.(tpl.TemplateHandler); ok { + th.PrintErrors() + } + + return nil +} + +func New(cfg DepsCfg) (*Deps, error) { + var ( + logger = cfg.Logger + fs = cfg.Fs + ) + + if cfg.TemplateProvider == nil { + panic("Must have a TemplateProvider") + } + + if cfg.TranslationProvider == nil { + panic("Must have a TranslationProvider") + } + + if cfg.Language == nil { + panic("Must have a Language") + } + + if logger == nil { + logger = jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) + } + + if fs == nil { + // Default to the production file system. + fs = hugofs.NewDefault(cfg.Language) + } + + ps, err := helpers.NewPathSpec(fs, cfg.Language) + + if err != nil { + return nil, err + } + + d := &Deps{ + Fs: fs, + Log: logger, + templateProvider: cfg.TemplateProvider, + translationProvider: cfg.TranslationProvider, + WithTemplate: cfg.WithTemplate, + PathSpec: ps, + ContentSpec: helpers.NewContentSpec(cfg.Language), + Cfg: cfg.Language, + Language: cfg.Language, + } + + return d, nil +} + +// ForLanguage creates a copy of the Deps with the language dependent +// parts switched out. +func (d Deps) ForLanguage(l *helpers.Language) (*Deps, error) { + var err error + + d.PathSpec, err = helpers.NewPathSpec(d.Fs, l) + if err != nil { + return nil, err + } + + d.ContentSpec = helpers.NewContentSpec(l) + d.Cfg = l + d.Language = l + + if err := d.translationProvider.Clone(&d); err != nil { + return nil, err + } + + if err := d.templateProvider.Clone(&d); err != nil { + return nil, err + } + + return &d, nil + +} + +// DepsCfg contains configuration options that can be used to configure Hugo +// on a global level, i.e. logging etc. +// Nil values will be given default values. +type DepsCfg struct { + + // The Logger to use. + Logger *jww.Notepad + + // The file systems to use + Fs *hugofs.Fs + + // The language to use. + Language *helpers.Language + + // The configuration to use. + Cfg config.Provider + + // Template handling. + TemplateProvider ResourceProvider + WithTemplate func(templ tpl.TemplateHandler) error + + // i18n handling. + TranslationProvider ResourceProvider +} diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 000000000..665360d49 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +/.idea +/public diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..9e89a0383 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,3 @@ +# Hugo Docs + +Documentation site for [Hugo](https://github.com/gohugoio/hugo), the very fast and flexible static site generator built with love in GoLang. diff --git a/archetypes/default.md b/docs/archetypes/default.md index 6d6497c4d..6d6497c4d 100644 --- a/archetypes/default.md +++ b/docs/archetypes/default.md diff --git a/archetypes/showcase.md b/docs/archetypes/showcase.md index ebe87035a..ebe87035a 100644 --- a/archetypes/showcase.md +++ b/docs/archetypes/showcase.md diff --git a/config.toml b/docs/config.toml index 31f5e0099..31f5e0099 100644 --- a/config.toml +++ b/docs/config.toml diff --git a/content/commands/hugo.md b/docs/content/commands/hugo.md index 90862cf21..90862cf21 100644 --- a/content/commands/hugo.md +++ b/docs/content/commands/hugo.md diff --git a/content/commands/hugo_benchmark.md b/docs/content/commands/hugo_benchmark.md index 2bfb1c8d3..2bfb1c8d3 100644 --- a/content/commands/hugo_benchmark.md +++ b/docs/content/commands/hugo_benchmark.md diff --git a/content/commands/hugo_check.md b/docs/content/commands/hugo_check.md index ae686559a..ae686559a 100644 --- a/content/commands/hugo_check.md +++ b/docs/content/commands/hugo_check.md diff --git a/content/commands/hugo_check_ulimit.md b/docs/content/commands/hugo_check_ulimit.md index 90ef8b030..90ef8b030 100644 --- a/content/commands/hugo_check_ulimit.md +++ b/docs/content/commands/hugo_check_ulimit.md diff --git a/content/commands/hugo_config.md b/docs/content/commands/hugo_config.md index 1504c0788..1504c0788 100644 --- a/content/commands/hugo_config.md +++ b/docs/content/commands/hugo_config.md diff --git a/content/commands/hugo_convert.md b/docs/content/commands/hugo_convert.md index 67b9f870b..67b9f870b 100644 --- a/content/commands/hugo_convert.md +++ b/docs/content/commands/hugo_convert.md diff --git a/content/commands/hugo_convert_toJSON.md b/docs/content/commands/hugo_convert_toJSON.md index a2da44ec4..a2da44ec4 100644 --- a/content/commands/hugo_convert_toJSON.md +++ b/docs/content/commands/hugo_convert_toJSON.md diff --git a/content/commands/hugo_convert_toTOML.md b/docs/content/commands/hugo_convert_toTOML.md index 8a48f52f7..8a48f52f7 100644 --- a/content/commands/hugo_convert_toTOML.md +++ b/docs/content/commands/hugo_convert_toTOML.md diff --git a/content/commands/hugo_convert_toYAML.md b/docs/content/commands/hugo_convert_toYAML.md index 5d85825fe..5d85825fe 100644 --- a/content/commands/hugo_convert_toYAML.md +++ b/docs/content/commands/hugo_convert_toYAML.md diff --git a/content/commands/hugo_env.md b/docs/content/commands/hugo_env.md index 7e268546f..7e268546f 100644 --- a/content/commands/hugo_env.md +++ b/docs/content/commands/hugo_env.md diff --git a/content/commands/hugo_gen.md b/docs/content/commands/hugo_gen.md index 7e753c4e1..7e753c4e1 100644 --- a/content/commands/hugo_gen.md +++ b/docs/content/commands/hugo_gen.md diff --git a/content/commands/hugo_gen_autocomplete.md b/docs/content/commands/hugo_gen_autocomplete.md index 455322def..455322def 100644 --- a/content/commands/hugo_gen_autocomplete.md +++ b/docs/content/commands/hugo_gen_autocomplete.md diff --git a/content/commands/hugo_gen_doc.md b/docs/content/commands/hugo_gen_doc.md index d7b2f719b..d7b2f719b 100644 --- a/content/commands/hugo_gen_doc.md +++ b/docs/content/commands/hugo_gen_doc.md diff --git a/content/commands/hugo_gen_man.md b/docs/content/commands/hugo_gen_man.md index 14c403d9a..14c403d9a 100644 --- a/content/commands/hugo_gen_man.md +++ b/docs/content/commands/hugo_gen_man.md diff --git a/content/commands/hugo_import.md b/docs/content/commands/hugo_import.md index ce58bb3af..ce58bb3af 100644 --- a/content/commands/hugo_import.md +++ b/docs/content/commands/hugo_import.md diff --git a/content/commands/hugo_import_jekyll.md b/docs/content/commands/hugo_import_jekyll.md index 89c4c47c5..89c4c47c5 100644 --- a/content/commands/hugo_import_jekyll.md +++ b/docs/content/commands/hugo_import_jekyll.md diff --git a/content/commands/hugo_list.md b/docs/content/commands/hugo_list.md index d9c609596..d9c609596 100644 --- a/content/commands/hugo_list.md +++ b/docs/content/commands/hugo_list.md diff --git a/content/commands/hugo_list_drafts.md b/docs/content/commands/hugo_list_drafts.md index d84dd56f5..d84dd56f5 100644 --- a/content/commands/hugo_list_drafts.md +++ b/docs/content/commands/hugo_list_drafts.md diff --git a/content/commands/hugo_list_expired.md b/docs/content/commands/hugo_list_expired.md index e4304a40c..e4304a40c 100644 --- a/content/commands/hugo_list_expired.md +++ b/docs/content/commands/hugo_list_expired.md diff --git a/content/commands/hugo_list_future.md b/docs/content/commands/hugo_list_future.md index 530b4a68f..530b4a68f 100644 --- a/content/commands/hugo_list_future.md +++ b/docs/content/commands/hugo_list_future.md diff --git a/content/commands/hugo_new.md b/docs/content/commands/hugo_new.md index bf230a25b..bf230a25b 100644 --- a/content/commands/hugo_new.md +++ b/docs/content/commands/hugo_new.md diff --git a/content/commands/hugo_new_site.md b/docs/content/commands/hugo_new_site.md index 8c097e727..8c097e727 100644 --- a/content/commands/hugo_new_site.md +++ b/docs/content/commands/hugo_new_site.md diff --git a/content/commands/hugo_new_theme.md b/docs/content/commands/hugo_new_theme.md index 317de98b7..317de98b7 100644 --- a/content/commands/hugo_new_theme.md +++ b/docs/content/commands/hugo_new_theme.md diff --git a/content/commands/hugo_server.md b/docs/content/commands/hugo_server.md index 4c23c93fc..4c23c93fc 100644 --- a/content/commands/hugo_server.md +++ b/docs/content/commands/hugo_server.md diff --git a/content/commands/hugo_undraft.md b/docs/content/commands/hugo_undraft.md index e082d0a15..e082d0a15 100644 --- a/content/commands/hugo_undraft.md +++ b/docs/content/commands/hugo_undraft.md diff --git a/content/commands/hugo_version.md b/docs/content/commands/hugo_version.md index a4bdbce01..a4bdbce01 100644 --- a/content/commands/hugo_version.md +++ b/docs/content/commands/hugo_version.md diff --git a/content/community/contributing.md b/docs/content/community/contributing.md index 98530f4b5..98530f4b5 100644 --- a/content/community/contributing.md +++ b/docs/content/community/contributing.md diff --git a/content/community/mailing-list.md b/docs/content/community/mailing-list.md index 3bd9f58bf..3bd9f58bf 100644 --- a/content/community/mailing-list.md +++ b/docs/content/community/mailing-list.md diff --git a/content/community/press.md b/docs/content/community/press.md index c5523d66e..c5523d66e 100644 --- a/content/community/press.md +++ b/docs/content/community/press.md diff --git a/content/content/archetypes.md b/docs/content/content/archetypes.md index 88efdbd0f..88efdbd0f 100644 --- a/content/content/archetypes.md +++ b/docs/content/content/archetypes.md diff --git a/content/content/example.md b/docs/content/content/example.md index 022764994..022764994 100644 --- a/content/content/example.md +++ b/docs/content/content/example.md diff --git a/content/content/front-matter.md b/docs/content/content/front-matter.md index e61a48f55..e61a48f55 100644 --- a/content/content/front-matter.md +++ b/docs/content/content/front-matter.md diff --git a/content/content/markdown-extras.md b/docs/content/content/markdown-extras.md index 7673c53da..7673c53da 100644 --- a/content/content/markdown-extras.md +++ b/docs/content/content/markdown-extras.md diff --git a/content/content/multilingual.md b/docs/content/content/multilingual.md index 5e09bc539..5e09bc539 100644 --- a/content/content/multilingual.md +++ b/docs/content/content/multilingual.md diff --git a/content/content/ordering.md b/docs/content/content/ordering.md index d0933a608..d0933a608 100644 --- a/content/content/ordering.md +++ b/docs/content/content/ordering.md diff --git a/content/content/organization.md b/docs/content/content/organization.md index bd83fbf04..bd83fbf04 100644 --- a/content/content/organization.md +++ b/docs/content/content/organization.md diff --git a/content/content/sections.md b/docs/content/content/sections.md index c603c0872..c603c0872 100644 --- a/content/content/sections.md +++ b/docs/content/content/sections.md diff --git a/content/content/summaries.md b/docs/content/content/summaries.md index 1f65d431d..1f65d431d 100644 --- a/content/content/summaries.md +++ b/docs/content/content/summaries.md diff --git a/content/content/supported-formats.md b/docs/content/content/supported-formats.md index 3fc905d6d..3fc905d6d 100644 --- a/content/content/supported-formats.md +++ b/docs/content/content/supported-formats.md diff --git a/content/content/types.md b/docs/content/content/types.md index 277294881..277294881 100644 --- a/content/content/types.md +++ b/docs/content/content/types.md diff --git a/content/content/using-index-md.md b/docs/content/content/using-index-md.md index 1f1298f67..1f1298f67 100644 --- a/content/content/using-index-md.md +++ b/docs/content/content/using-index-md.md diff --git a/content/extras/aliases.md b/docs/content/extras/aliases.md index 00f014ca0..00f014ca0 100644 --- a/content/extras/aliases.md +++ b/docs/content/extras/aliases.md diff --git a/content/extras/analytics.md b/docs/content/extras/analytics.md index 31f44c7bf..31f44c7bf 100644 --- a/content/extras/analytics.md +++ b/docs/content/extras/analytics.md diff --git a/content/extras/builders.md b/docs/content/extras/builders.md index 707eff306..707eff306 100644 --- a/content/extras/builders.md +++ b/docs/content/extras/builders.md diff --git a/content/extras/comments.md b/docs/content/extras/comments.md index 5ecb6ba20..5ecb6ba20 100644 --- a/content/extras/comments.md +++ b/docs/content/extras/comments.md diff --git a/content/extras/crossreferences.md b/docs/content/extras/crossreferences.md index 9f3e7ef23..9f3e7ef23 100644 --- a/content/extras/crossreferences.md +++ b/docs/content/extras/crossreferences.md diff --git a/content/extras/datadrivencontent.md b/docs/content/extras/datadrivencontent.md index e5c6c9130..e5c6c9130 100644 --- a/content/extras/datadrivencontent.md +++ b/docs/content/extras/datadrivencontent.md diff --git a/content/extras/datafiles.md b/docs/content/extras/datafiles.md index dc7e8059c..dc7e8059c 100644 --- a/content/extras/datafiles.md +++ b/docs/content/extras/datafiles.md diff --git a/content/extras/gitinfo.md b/docs/content/extras/gitinfo.md index d29641bcb..d29641bcb 100644 --- a/content/extras/gitinfo.md +++ b/docs/content/extras/gitinfo.md diff --git a/content/extras/highlighting.md b/docs/content/extras/highlighting.md index 601373deb..601373deb 100644 --- a/content/extras/highlighting.md +++ b/docs/content/extras/highlighting.md diff --git a/content/extras/livereload.md b/docs/content/extras/livereload.md index cb4047636..cb4047636 100644 --- a/content/extras/livereload.md +++ b/docs/content/extras/livereload.md diff --git a/content/extras/localfiles.md b/docs/content/extras/localfiles.md index 28b611870..28b611870 100644 --- a/content/extras/localfiles.md +++ b/docs/content/extras/localfiles.md diff --git a/content/extras/menus.md b/docs/content/extras/menus.md index 84b59e831..84b59e831 100644 --- a/content/extras/menus.md +++ b/docs/content/extras/menus.md diff --git a/content/extras/output-formats.md b/docs/content/extras/output-formats.md index 933e7f6e0..933e7f6e0 100644 --- a/content/extras/output-formats.md +++ b/docs/content/extras/output-formats.md diff --git a/content/extras/pagination.md b/docs/content/extras/pagination.md index 1f20e7e5a..1f20e7e5a 100644 --- a/content/extras/pagination.md +++ b/docs/content/extras/pagination.md diff --git a/content/extras/permalinks.md b/docs/content/extras/permalinks.md index 4249f23a4..4249f23a4 100644 --- a/content/extras/permalinks.md +++ b/docs/content/extras/permalinks.md diff --git a/content/extras/robots-txt.md b/docs/content/extras/robots-txt.md index d22607088..d22607088 100644 --- a/content/extras/robots-txt.md +++ b/docs/content/extras/robots-txt.md diff --git a/content/extras/scratch.md b/docs/content/extras/scratch.md index ae6ea95b6..ae6ea95b6 100644 --- a/content/extras/scratch.md +++ b/docs/content/extras/scratch.md diff --git a/content/extras/shortcodes.md b/docs/content/extras/shortcodes.md index 4019d870f..4019d870f 100644 --- a/content/extras/shortcodes.md +++ b/docs/content/extras/shortcodes.md diff --git a/content/extras/toc.md b/docs/content/extras/toc.md index 56e093ba7..56e093ba7 100644 --- a/content/extras/toc.md +++ b/docs/content/extras/toc.md diff --git a/content/extras/urls.md b/docs/content/extras/urls.md index 487e685f3..487e685f3 100644 --- a/content/extras/urls.md +++ b/docs/content/extras/urls.md diff --git a/content/meta/license.md b/docs/content/meta/license.md index a16923bfb..a16923bfb 100644 --- a/content/meta/license.md +++ b/docs/content/meta/license.md diff --git a/content/meta/roadmap.md b/docs/content/meta/roadmap.md index 8a0a914be..8a0a914be 100644 --- a/content/meta/roadmap.md +++ b/docs/content/meta/roadmap.md diff --git a/content/overview/configuration.md b/docs/content/overview/configuration.md index 7d05570b7..7d05570b7 100644 --- a/content/overview/configuration.md +++ b/docs/content/overview/configuration.md diff --git a/content/overview/installing.md b/docs/content/overview/installing.md index 895fae2af..895fae2af 100644 --- a/content/overview/installing.md +++ b/docs/content/overview/installing.md diff --git a/content/overview/introduction.md b/docs/content/overview/introduction.md index 4ae8ccaa7..4ae8ccaa7 100644 --- a/content/overview/introduction.md +++ b/docs/content/overview/introduction.md diff --git a/content/overview/quickstart.md b/docs/content/overview/quickstart.md index 7340a4957..7340a4957 100644 --- a/content/overview/quickstart.md +++ b/docs/content/overview/quickstart.md diff --git a/content/overview/source-directory.md b/docs/content/overview/source-directory.md index 2d4ce10f4..2d4ce10f4 100644 --- a/content/overview/source-directory.md +++ b/docs/content/overview/source-directory.md diff --git a/content/overview/usage.md b/docs/content/overview/usage.md index d7c7f7772..d7c7f7772 100644 --- a/content/overview/usage.md +++ b/docs/content/overview/usage.md diff --git a/content/release-notes/0.20.3-relnotes.md b/docs/content/release-notes/0.20.3-relnotes.md index d62740abc..d62740abc 100644 --- a/content/release-notes/0.20.3-relnotes.md +++ b/docs/content/release-notes/0.20.3-relnotes.md diff --git a/content/release-notes/0.20.4-relnotes.md b/docs/content/release-notes/0.20.4-relnotes.md index 0065945fb..0065945fb 100644 --- a/content/release-notes/0.20.4-relnotes.md +++ b/docs/content/release-notes/0.20.4-relnotes.md diff --git a/content/release-notes/0.20.5-relnotes.md b/docs/content/release-notes/0.20.5-relnotes.md index 15ecbc232..15ecbc232 100644 --- a/content/release-notes/0.20.5-relnotes.md +++ b/docs/content/release-notes/0.20.5-relnotes.md diff --git a/content/release-notes/0.20.6-relnotes.md b/docs/content/release-notes/0.20.6-relnotes.md index b3c94b3dd..b3c94b3dd 100644 --- a/content/release-notes/0.20.6-relnotes.md +++ b/docs/content/release-notes/0.20.6-relnotes.md diff --git a/content/release-notes/0.21-relnotes.md b/docs/content/release-notes/0.21-relnotes.md index 2f393d20f..2f393d20f 100644 --- a/content/release-notes/0.21-relnotes.md +++ b/docs/content/release-notes/0.21-relnotes.md diff --git a/content/release-notes/0.22-relnotes.md b/docs/content/release-notes/0.22-relnotes.md index f5250062e..f5250062e 100644 --- a/content/release-notes/0.22-relnotes.md +++ b/docs/content/release-notes/0.22-relnotes.md diff --git a/content/release-notes/0.22.1-relnotes.md b/docs/content/release-notes/0.22.1-relnotes.md index c0a7dd453..c0a7dd453 100644 --- a/content/release-notes/0.22.1-relnotes.md +++ b/docs/content/release-notes/0.22.1-relnotes.md diff --git a/content/release-notes/0.23-relnotes.md b/docs/content/release-notes/0.23-relnotes.md index aa940d4b9..aa940d4b9 100644 --- a/content/release-notes/0.23-relnotes.md +++ b/docs/content/release-notes/0.23-relnotes.md diff --git a/content/release-notes/0.24-relnotes.md b/docs/content/release-notes/0.24-relnotes.md index 2f4c87912..2f4c87912 100644 --- a/content/release-notes/0.24-relnotes.md +++ b/docs/content/release-notes/0.24-relnotes.md diff --git a/content/release-notes/0.24.1-relnotes.md b/docs/content/release-notes/0.24.1-relnotes.md index 1c7e57c70..1c7e57c70 100644 --- a/content/release-notes/0.24.1-relnotes.md +++ b/docs/content/release-notes/0.24.1-relnotes.md diff --git a/content/release-notes/_index.md b/docs/content/release-notes/_index.md index 3b934c69d..3b934c69d 100644 --- a/content/release-notes/_index.md +++ b/docs/content/release-notes/_index.md diff --git a/content/release-notes/release-notes.md b/docs/content/release-notes/release-notes.md index 7464de2ef..7464de2ef 100644 --- a/content/release-notes/release-notes.md +++ b/docs/content/release-notes/release-notes.md diff --git a/content/showcase/2626info.md b/docs/content/showcase/2626info.md index 3797860c0..3797860c0 100644 --- a/content/showcase/2626info.md +++ b/docs/content/showcase/2626info.md diff --git a/content/showcase/antzucaro.md b/docs/content/showcase/antzucaro.md index 36ec43245..36ec43245 100644 --- a/content/showcase/antzucaro.md +++ b/docs/content/showcase/antzucaro.md diff --git a/content/showcase/appernetic.md b/docs/content/showcase/appernetic.md index 53d2d8f77..53d2d8f77 100644 --- a/content/showcase/appernetic.md +++ b/docs/content/showcase/appernetic.md diff --git a/content/showcase/arresteddevops.md b/docs/content/showcase/arresteddevops.md index b520b5667..b520b5667 100644 --- a/content/showcase/arresteddevops.md +++ b/docs/content/showcase/arresteddevops.md diff --git a/content/showcase/asc.md b/docs/content/showcase/asc.md index bc9424820..bc9424820 100644 --- a/content/showcase/asc.md +++ b/docs/content/showcase/asc.md diff --git a/content/showcase/astrochili.md b/docs/content/showcase/astrochili.md index fcf1a4217..fcf1a4217 100644 --- a/content/showcase/astrochili.md +++ b/docs/content/showcase/astrochili.md diff --git a/content/showcase/aydoscom.md b/docs/content/showcase/aydoscom.md index 3239d4467..3239d4467 100644 --- a/content/showcase/aydoscom.md +++ b/docs/content/showcase/aydoscom.md diff --git a/content/showcase/balaramadurai.net.md b/docs/content/showcase/balaramadurai.net.md index ca4a7c71e..ca4a7c71e 100644 --- a/content/showcase/balaramadurai.net.md +++ b/docs/content/showcase/balaramadurai.net.md diff --git a/content/showcase/barricade.md b/docs/content/showcase/barricade.md index 6316e085a..6316e085a 100644 --- a/content/showcase/barricade.md +++ b/docs/content/showcase/barricade.md diff --git a/content/showcase/bepsays.md b/docs/content/showcase/bepsays.md index c6c749f11..c6c749f11 100644 --- a/content/showcase/bepsays.md +++ b/docs/content/showcase/bepsays.md diff --git a/content/showcase/bharathpalavalli.com.md b/docs/content/showcase/bharathpalavalli.com.md index 79d60dcf3..79d60dcf3 100644 --- a/content/showcase/bharathpalavalli.com.md +++ b/docs/content/showcase/bharathpalavalli.com.md diff --git a/content/showcase/bugtrackers.io.md b/docs/content/showcase/bugtrackers.io.md index d3a61489c..d3a61489c 100644 --- a/content/showcase/bugtrackers.io.md +++ b/docs/content/showcase/bugtrackers.io.md diff --git a/content/showcase/bullion-investor.md b/docs/content/showcase/bullion-investor.md index 262dcfb53..262dcfb53 100644 --- a/content/showcase/bullion-investor.md +++ b/docs/content/showcase/bullion-investor.md diff --git a/content/showcase/camunda-blog.md b/docs/content/showcase/camunda-blog.md index 7e7f76427..7e7f76427 100644 --- a/content/showcase/camunda-blog.md +++ b/docs/content/showcase/camunda-blog.md diff --git a/content/showcase/camunda-docs.md b/docs/content/showcase/camunda-docs.md index 8d97d1492..8d97d1492 100644 --- a/content/showcase/camunda-docs.md +++ b/docs/content/showcase/camunda-docs.md diff --git a/content/showcase/carnivorousplants.md b/docs/content/showcase/carnivorousplants.md index 30f3b3756..30f3b3756 100644 --- a/content/showcase/carnivorousplants.md +++ b/docs/content/showcase/carnivorousplants.md diff --git a/content/showcase/cdnoverview.md b/docs/content/showcase/cdnoverview.md index d48788ec6..d48788ec6 100644 --- a/content/showcase/cdnoverview.md +++ b/docs/content/showcase/cdnoverview.md diff --git a/content/showcase/chinese-grammar.md b/docs/content/showcase/chinese-grammar.md index 3e8bc3563..3e8bc3563 100644 --- a/content/showcase/chinese-grammar.md +++ b/docs/content/showcase/chinese-grammar.md diff --git a/content/showcase/chingli.md b/docs/content/showcase/chingli.md index ae2d4a4db..ae2d4a4db 100644 --- a/content/showcase/chingli.md +++ b/docs/content/showcase/chingli.md diff --git a/content/showcase/chipsncookies.md b/docs/content/showcase/chipsncookies.md index ff3128d81..ff3128d81 100644 --- a/content/showcase/chipsncookies.md +++ b/docs/content/showcase/chipsncookies.md diff --git a/content/showcase/christianmendoza.md b/docs/content/showcase/christianmendoza.md index 6d18735d9..6d18735d9 100644 --- a/content/showcase/christianmendoza.md +++ b/docs/content/showcase/christianmendoza.md diff --git a/content/showcase/cinegyopen.md b/docs/content/showcase/cinegyopen.md index 592df2612..592df2612 100644 --- a/content/showcase/cinegyopen.md +++ b/docs/content/showcase/cinegyopen.md diff --git a/content/showcase/clearhaus.md b/docs/content/showcase/clearhaus.md index 9408ec8e6..9408ec8e6 100644 --- a/content/showcase/clearhaus.md +++ b/docs/content/showcase/clearhaus.md diff --git a/content/showcase/cloudshark.md b/docs/content/showcase/cloudshark.md index 6a8b3fa4f..6a8b3fa4f 100644 --- a/content/showcase/cloudshark.md +++ b/docs/content/showcase/cloudshark.md diff --git a/content/showcase/coding-journal.md b/docs/content/showcase/coding-journal.md index 78e4b38d2..78e4b38d2 100644 --- a/content/showcase/coding-journal.md +++ b/docs/content/showcase/coding-journal.md diff --git a/content/showcase/consequently.md b/docs/content/showcase/consequently.md index 3469129f9..3469129f9 100644 --- a/content/showcase/consequently.md +++ b/docs/content/showcase/consequently.md diff --git a/content/showcase/ctlcompiled.md b/docs/content/showcase/ctlcompiled.md index 18179cf5b..18179cf5b 100644 --- a/content/showcase/ctlcompiled.md +++ b/docs/content/showcase/ctlcompiled.md diff --git a/content/showcase/danmux.md b/docs/content/showcase/danmux.md index 1ae4589d1..1ae4589d1 100644 --- a/content/showcase/danmux.md +++ b/docs/content/showcase/danmux.md diff --git a/content/showcase/datapipelinearchitect.md b/docs/content/showcase/datapipelinearchitect.md index a74eb835e..a74eb835e 100644 --- a/content/showcase/datapipelinearchitect.md +++ b/docs/content/showcase/datapipelinearchitect.md diff --git a/content/showcase/davidepetilli.md b/docs/content/showcase/davidepetilli.md index 6b69eee0f..6b69eee0f 100644 --- a/content/showcase/davidepetilli.md +++ b/docs/content/showcase/davidepetilli.md diff --git a/content/showcase/davidrallen.md b/docs/content/showcase/davidrallen.md index 06e802fd5..06e802fd5 100644 --- a/content/showcase/davidrallen.md +++ b/docs/content/showcase/davidrallen.md diff --git a/content/showcase/davidyates.md b/docs/content/showcase/davidyates.md index 0d6063ad4..0d6063ad4 100644 --- a/content/showcase/davidyates.md +++ b/docs/content/showcase/davidyates.md diff --git a/content/showcase/dbzman-online.md b/docs/content/showcase/dbzman-online.md index f30d71491..f30d71491 100644 --- a/content/showcase/dbzman-online.md +++ b/docs/content/showcase/dbzman-online.md diff --git a/content/showcase/devmonk.md b/docs/content/showcase/devmonk.md index 00a1a7572..00a1a7572 100644 --- a/content/showcase/devmonk.md +++ b/docs/content/showcase/devmonk.md diff --git a/content/showcase/dmitriid.com.md b/docs/content/showcase/dmitriid.com.md index 2a5e308ba..2a5e308ba 100644 --- a/content/showcase/dmitriid.com.md +++ b/docs/content/showcase/dmitriid.com.md diff --git a/content/showcase/emilyhorsman.com.md b/docs/content/showcase/emilyhorsman.com.md index 8e792f716..8e792f716 100644 --- a/content/showcase/emilyhorsman.com.md +++ b/docs/content/showcase/emilyhorsman.com.md diff --git a/content/showcase/enjoyablerecipes.md b/docs/content/showcase/enjoyablerecipes.md index 7b97e6293..7b97e6293 100644 --- a/content/showcase/enjoyablerecipes.md +++ b/docs/content/showcase/enjoyablerecipes.md diff --git a/content/showcase/esaezgil.md b/docs/content/showcase/esaezgil.md index f1ae53b20..f1ae53b20 100644 --- a/content/showcase/esaezgil.md +++ b/docs/content/showcase/esaezgil.md diff --git a/content/showcase/esolia-com.md b/docs/content/showcase/esolia-com.md index 58a482f51..58a482f51 100644 --- a/content/showcase/esolia-com.md +++ b/docs/content/showcase/esolia-com.md diff --git a/content/showcase/esolia-pro.md b/docs/content/showcase/esolia-pro.md index 24b06b1f8..24b06b1f8 100644 --- a/content/showcase/esolia-pro.md +++ b/docs/content/showcase/esolia-pro.md diff --git a/content/showcase/eurie.md b/docs/content/showcase/eurie.md index 0ab5966f7..0ab5966f7 100644 --- a/content/showcase/eurie.md +++ b/docs/content/showcase/eurie.md diff --git a/content/showcase/fale.md b/docs/content/showcase/fale.md index 449c1cc22..449c1cc22 100644 --- a/content/showcase/fale.md +++ b/docs/content/showcase/fale.md diff --git a/content/showcase/firstnameclub.md b/docs/content/showcase/firstnameclub.md index 573cdda0a..573cdda0a 100644 --- a/content/showcase/firstnameclub.md +++ b/docs/content/showcase/firstnameclub.md diff --git a/content/showcase/fixatom.md b/docs/content/showcase/fixatom.md index 8ba1978d1..8ba1978d1 100644 --- a/content/showcase/fixatom.md +++ b/docs/content/showcase/fixatom.md diff --git a/content/showcase/furqansoftware.md b/docs/content/showcase/furqansoftware.md index 4d377e642..4d377e642 100644 --- a/content/showcase/furqansoftware.md +++ b/docs/content/showcase/furqansoftware.md diff --git a/content/showcase/fxsitecompat.md b/docs/content/showcase/fxsitecompat.md index 393b097d9..393b097d9 100644 --- a/content/showcase/fxsitecompat.md +++ b/docs/content/showcase/fxsitecompat.md diff --git a/content/showcase/gntech.md b/docs/content/showcase/gntech.md index 3d199e931..3d199e931 100644 --- a/content/showcase/gntech.md +++ b/docs/content/showcase/gntech.md diff --git a/content/showcase/gogb.md b/docs/content/showcase/gogb.md index d104741ba..d104741ba 100644 --- a/content/showcase/gogb.md +++ b/docs/content/showcase/gogb.md diff --git a/content/showcase/goin5minutes.md b/docs/content/showcase/goin5minutes.md index ae0c61da3..ae0c61da3 100644 --- a/content/showcase/goin5minutes.md +++ b/docs/content/showcase/goin5minutes.md diff --git a/content/showcase/h10n.me.md b/docs/content/showcase/h10n.me.md index 0576edb1f..0576edb1f 100644 --- a/content/showcase/h10n.me.md +++ b/docs/content/showcase/h10n.me.md diff --git a/content/showcase/heimatverein-niederjosbach.md b/docs/content/showcase/heimatverein-niederjosbach.md index d063e9c50..d063e9c50 100644 --- a/content/showcase/heimatverein-niederjosbach.md +++ b/docs/content/showcase/heimatverein-niederjosbach.md diff --git a/content/showcase/horeaporutiu.md b/docs/content/showcase/horeaporutiu.md index e48d3de0b..e48d3de0b 100644 --- a/content/showcase/horeaporutiu.md +++ b/docs/content/showcase/horeaporutiu.md diff --git a/content/showcase/hugo.md b/docs/content/showcase/hugo.md index 54c9ce09a..54c9ce09a 100644 --- a/content/showcase/hugo.md +++ b/docs/content/showcase/hugo.md diff --git a/content/showcase/invision.md b/docs/content/showcase/invision.md index baf1fc463..baf1fc463 100644 --- a/content/showcase/invision.md +++ b/docs/content/showcase/invision.md diff --git a/content/showcase/jamescampbell.md b/docs/content/showcase/jamescampbell.md index de7fa62a2..de7fa62a2 100644 --- a/content/showcase/jamescampbell.md +++ b/docs/content/showcase/jamescampbell.md diff --git a/content/showcase/jorgennilsson.md b/docs/content/showcase/jorgennilsson.md index 7821eea7d..7821eea7d 100644 --- a/content/showcase/jorgennilsson.md +++ b/docs/content/showcase/jorgennilsson.md diff --git a/content/showcase/kieranhealy.md b/docs/content/showcase/kieranhealy.md index f4f32989c..f4f32989c 100644 --- a/content/showcase/kieranhealy.md +++ b/docs/content/showcase/kieranhealy.md diff --git a/content/showcase/klingt-net.md b/docs/content/showcase/klingt-net.md index 369e44cdb..369e44cdb 100644 --- a/content/showcase/klingt-net.md +++ b/docs/content/showcase/klingt-net.md diff --git a/content/showcase/launchcode5.md b/docs/content/showcase/launchcode5.md index 99bea2cfb..99bea2cfb 100644 --- a/content/showcase/launchcode5.md +++ b/docs/content/showcase/launchcode5.md diff --git a/content/showcase/leepenney.md b/docs/content/showcase/leepenney.md index db8bfb45b..db8bfb45b 100644 --- a/content/showcase/leepenney.md +++ b/docs/content/showcase/leepenney.md diff --git a/content/showcase/leowkahman.md b/docs/content/showcase/leowkahman.md index a7ce50418..a7ce50418 100644 --- a/content/showcase/leowkahman.md +++ b/docs/content/showcase/leowkahman.md diff --git a/content/showcase/lk4d4.darth.io.md b/docs/content/showcase/lk4d4.darth.io.md index a35773380..a35773380 100644 --- a/content/showcase/lk4d4.darth.io.md +++ b/docs/content/showcase/lk4d4.darth.io.md diff --git a/content/showcase/losslesslife.md b/docs/content/showcase/losslesslife.md index 5e7158daa..5e7158daa 100644 --- a/content/showcase/losslesslife.md +++ b/docs/content/showcase/losslesslife.md diff --git a/content/showcase/lucumt.info.md b/docs/content/showcase/lucumt.info.md index 0f66b294f..0f66b294f 100644 --- a/content/showcase/lucumt.info.md +++ b/docs/content/showcase/lucumt.info.md diff --git a/content/showcase/mariosanchez.md b/docs/content/showcase/mariosanchez.md index 99285f871..99285f871 100644 --- a/content/showcase/mariosanchez.md +++ b/docs/content/showcase/mariosanchez.md diff --git a/content/showcase/mayan-edms.md b/docs/content/showcase/mayan-edms.md index 656a6790c..656a6790c 100644 --- a/content/showcase/mayan-edms.md +++ b/docs/content/showcase/mayan-edms.md diff --git a/content/showcase/michaelwhatcott.md b/docs/content/showcase/michaelwhatcott.md index 9c5bcd07f..9c5bcd07f 100644 --- a/content/showcase/michaelwhatcott.md +++ b/docs/content/showcase/michaelwhatcott.md diff --git a/content/showcase/mongodb-eng-journal.md b/docs/content/showcase/mongodb-eng-journal.md index 9dc5ea0ad..9dc5ea0ad 100644 --- a/content/showcase/mongodb-eng-journal.md +++ b/docs/content/showcase/mongodb-eng-journal.md diff --git a/content/showcase/mtbhomer.md b/docs/content/showcase/mtbhomer.md index a98c59621..a98c59621 100644 --- a/content/showcase/mtbhomer.md +++ b/docs/content/showcase/mtbhomer.md diff --git a/content/showcase/myearworms.md b/docs/content/showcase/myearworms.md index 17d0549f3..17d0549f3 100644 --- a/content/showcase/myearworms.md +++ b/docs/content/showcase/myearworms.md diff --git a/content/showcase/neavey.net.md b/docs/content/showcase/neavey.net.md index 443d2b670..443d2b670 100644 --- a/content/showcase/neavey.net.md +++ b/docs/content/showcase/neavey.net.md diff --git a/content/showcase/nickoneill.md b/docs/content/showcase/nickoneill.md index 405a9f945..405a9f945 100644 --- a/content/showcase/nickoneill.md +++ b/docs/content/showcase/nickoneill.md diff --git a/content/showcase/ninjaducks.in.md b/docs/content/showcase/ninjaducks.in.md index 46be436fc..46be436fc 100644 --- a/content/showcase/ninjaducks.in.md +++ b/docs/content/showcase/ninjaducks.in.md diff --git a/content/showcase/ninya.io.md b/docs/content/showcase/ninya.io.md index a7ac4fe99..a7ac4fe99 100644 --- a/content/showcase/ninya.io.md +++ b/docs/content/showcase/ninya.io.md diff --git a/content/showcase/nodesk.md b/docs/content/showcase/nodesk.md index 8656c7ec0..8656c7ec0 100644 --- a/content/showcase/nodesk.md +++ b/docs/content/showcase/nodesk.md diff --git a/content/showcase/novelist-xyz.md b/docs/content/showcase/novelist-xyz.md index e0b59fb41..e0b59fb41 100644 --- a/content/showcase/novelist-xyz.md +++ b/docs/content/showcase/novelist-xyz.md diff --git a/content/showcase/npf.md b/docs/content/showcase/npf.md index b11f6cbc4..b11f6cbc4 100644 --- a/content/showcase/npf.md +++ b/docs/content/showcase/npf.md diff --git a/content/showcase/nutspubcrawl.md b/docs/content/showcase/nutspubcrawl.md index bb5ddf6d8..bb5ddf6d8 100644 --- a/content/showcase/nutspubcrawl.md +++ b/docs/content/showcase/nutspubcrawl.md diff --git a/content/showcase/ocul-maps.md b/docs/content/showcase/ocul-maps.md index 516bda399..516bda399 100644 --- a/content/showcase/ocul-maps.md +++ b/docs/content/showcase/ocul-maps.md diff --git a/content/showcase/petanikode.md b/docs/content/showcase/petanikode.md index 7ebd51b9f..7ebd51b9f 100644 --- a/content/showcase/petanikode.md +++ b/docs/content/showcase/petanikode.md diff --git a/content/showcase/peteraba.md b/docs/content/showcase/peteraba.md index c00373e54..c00373e54 100644 --- a/content/showcase/peteraba.md +++ b/docs/content/showcase/peteraba.md diff --git a/content/showcase/picturingjordan.md b/docs/content/showcase/picturingjordan.md index c0005312a..c0005312a 100644 --- a/content/showcase/picturingjordan.md +++ b/docs/content/showcase/picturingjordan.md diff --git a/content/showcase/promotive.md b/docs/content/showcase/promotive.md index 45e99d1a1..45e99d1a1 100644 --- a/content/showcase/promotive.md +++ b/docs/content/showcase/promotive.md diff --git a/content/showcase/rahulrai.md b/docs/content/showcase/rahulrai.md index 1970ad683..1970ad683 100644 --- a/content/showcase/rahulrai.md +++ b/docs/content/showcase/rahulrai.md diff --git a/content/showcase/rakutentech.md b/docs/content/showcase/rakutentech.md index b78b3e8b1..b78b3e8b1 100644 --- a/content/showcase/rakutentech.md +++ b/docs/content/showcase/rakutentech.md diff --git a/content/showcase/rdegges.md b/docs/content/showcase/rdegges.md index 2d7589b60..2d7589b60 100644 --- a/content/showcase/rdegges.md +++ b/docs/content/showcase/rdegges.md diff --git a/content/showcase/readtext.md b/docs/content/showcase/readtext.md index 330d89ca2..330d89ca2 100644 --- a/content/showcase/readtext.md +++ b/docs/content/showcase/readtext.md diff --git a/content/showcase/richardsumilang.md b/docs/content/showcase/richardsumilang.md index 8d4e97d09..8d4e97d09 100644 --- a/content/showcase/richardsumilang.md +++ b/docs/content/showcase/richardsumilang.md diff --git a/content/showcase/rick-cogley-info.md b/docs/content/showcase/rick-cogley-info.md index 997a45757..997a45757 100644 --- a/content/showcase/rick-cogley-info.md +++ b/docs/content/showcase/rick-cogley-info.md diff --git a/content/showcase/ridingbytes.md b/docs/content/showcase/ridingbytes.md index 29fbb2ecf..29fbb2ecf 100644 --- a/content/showcase/ridingbytes.md +++ b/docs/content/showcase/ridingbytes.md diff --git a/content/showcase/robertbasic.md b/docs/content/showcase/robertbasic.md index 237fce837..237fce837 100644 --- a/content/showcase/robertbasic.md +++ b/docs/content/showcase/robertbasic.md diff --git a/content/showcase/sanjay-saxena.md b/docs/content/showcase/sanjay-saxena.md index df869414d..df869414d 100644 --- a/content/showcase/sanjay-saxena.md +++ b/docs/content/showcase/sanjay-saxena.md diff --git a/content/showcase/scottcwilson.md b/docs/content/showcase/scottcwilson.md index 9d07cf123..9d07cf123 100644 --- a/content/showcase/scottcwilson.md +++ b/docs/content/showcase/scottcwilson.md diff --git a/content/showcase/shapeshed.md b/docs/content/showcase/shapeshed.md index 8e8908d00..8e8908d00 100644 --- a/content/showcase/shapeshed.md +++ b/docs/content/showcase/shapeshed.md diff --git a/content/showcase/shelan.md b/docs/content/showcase/shelan.md index 230b7b04f..230b7b04f 100644 --- a/content/showcase/shelan.md +++ b/docs/content/showcase/shelan.md diff --git a/content/showcase/siba.md b/docs/content/showcase/siba.md index 21f6f2a5f..21f6f2a5f 100644 --- a/content/showcase/siba.md +++ b/docs/content/showcase/siba.md diff --git a/content/showcase/silvergeko.md b/docs/content/showcase/silvergeko.md index 3e0105f30..3e0105f30 100644 --- a/content/showcase/silvergeko.md +++ b/docs/content/showcase/silvergeko.md diff --git a/content/showcase/softinio.md b/docs/content/showcase/softinio.md index fdbd5f364..fdbd5f364 100644 --- a/content/showcase/softinio.md +++ b/docs/content/showcase/softinio.md diff --git a/content/showcase/spf13.md b/docs/content/showcase/spf13.md index e0b524255..e0b524255 100644 --- a/content/showcase/spf13.md +++ b/docs/content/showcase/spf13.md diff --git a/content/showcase/steambap.md b/docs/content/showcase/steambap.md index 439c12e8b..439c12e8b 100644 --- a/content/showcase/steambap.md +++ b/docs/content/showcase/steambap.md diff --git a/content/showcase/stefano.chiodino.md b/docs/content/showcase/stefano.chiodino.md index 5880eb9ed..5880eb9ed 100644 --- a/content/showcase/stefano.chiodino.md +++ b/docs/content/showcase/stefano.chiodino.md diff --git a/content/showcase/stou.md b/docs/content/showcase/stou.md index ddd4b313b..ddd4b313b 100644 --- a/content/showcase/stou.md +++ b/docs/content/showcase/stou.md diff --git a/content/showcase/szymonkatra.md b/docs/content/showcase/szymonkatra.md index 1be52b94c..1be52b94c 100644 --- a/content/showcase/szymonkatra.md +++ b/docs/content/showcase/szymonkatra.md diff --git a/content/showcase/techmadeplain.md b/docs/content/showcase/techmadeplain.md index 25712751d..25712751d 100644 --- a/content/showcase/techmadeplain.md +++ b/docs/content/showcase/techmadeplain.md diff --git a/content/showcase/tendermint.md b/docs/content/showcase/tendermint.md index d95e3c090..d95e3c090 100644 --- a/content/showcase/tendermint.md +++ b/docs/content/showcase/tendermint.md diff --git a/content/showcase/thecodeking.md b/docs/content/showcase/thecodeking.md index 86786d736..86786d736 100644 --- a/content/showcase/thecodeking.md +++ b/docs/content/showcase/thecodeking.md diff --git a/content/showcase/thehome.md b/docs/content/showcase/thehome.md index 1db5706f3..1db5706f3 100644 --- a/content/showcase/thehome.md +++ b/docs/content/showcase/thehome.md diff --git a/content/showcase/thislittleduck.md b/docs/content/showcase/thislittleduck.md index 295b9cd32..295b9cd32 100644 --- a/content/showcase/thislittleduck.md +++ b/docs/content/showcase/thislittleduck.md diff --git a/content/showcase/tibobeijen.nl.md b/docs/content/showcase/tibobeijen.nl.md index 37db6491b..37db6491b 100644 --- a/content/showcase/tibobeijen.nl.md +++ b/docs/content/showcase/tibobeijen.nl.md diff --git a/content/showcase/ttsreader.md b/docs/content/showcase/ttsreader.md index 90f02763b..90f02763b 100644 --- a/content/showcase/ttsreader.md +++ b/docs/content/showcase/ttsreader.md diff --git a/content/showcase/tutorialonfly.md b/docs/content/showcase/tutorialonfly.md index 7c975aaa9..7c975aaa9 100644 --- a/content/showcase/tutorialonfly.md +++ b/docs/content/showcase/tutorialonfly.md diff --git a/content/showcase/tutswiki.md b/docs/content/showcase/tutswiki.md index 72bd65844..72bd65844 100644 --- a/content/showcase/tutswiki.md +++ b/docs/content/showcase/tutswiki.md diff --git a/content/showcase/ucsb.md b/docs/content/showcase/ucsb.md index 9f9ff77e7..9f9ff77e7 100644 --- a/content/showcase/ucsb.md +++ b/docs/content/showcase/ucsb.md diff --git a/content/showcase/upbeat.md b/docs/content/showcase/upbeat.md index 7c5b0fe57..7c5b0fe57 100644 --- a/content/showcase/upbeat.md +++ b/docs/content/showcase/upbeat.md diff --git a/content/showcase/vamp.md b/docs/content/showcase/vamp.md index a2591dbb8..a2591dbb8 100644 --- a/content/showcase/vamp.md +++ b/docs/content/showcase/vamp.md diff --git a/content/showcase/viglug.org.md b/docs/content/showcase/viglug.org.md index a9e78290a..a9e78290a 100644 --- a/content/showcase/viglug.org.md +++ b/docs/content/showcase/viglug.org.md diff --git a/content/showcase/vurt.co.md b/docs/content/showcase/vurt.co.md index 6da6aa09d..6da6aa09d 100644 --- a/content/showcase/vurt.co.md +++ b/docs/content/showcase/vurt.co.md diff --git a/content/showcase/worldtowriters.md b/docs/content/showcase/worldtowriters.md index d28fc3e43..d28fc3e43 100644 --- a/content/showcase/worldtowriters.md +++ b/docs/content/showcase/worldtowriters.md diff --git a/content/showcase/yslow-rules.md b/docs/content/showcase/yslow-rules.md index ccc84bfbb..ccc84bfbb 100644 --- a/content/showcase/yslow-rules.md +++ b/docs/content/showcase/yslow-rules.md diff --git a/content/showcase/ysqi.md b/docs/content/showcase/ysqi.md index 56bf06bd1..56bf06bd1 100644 --- a/content/showcase/ysqi.md +++ b/docs/content/showcase/ysqi.md diff --git a/content/showcase/yulinling.net.md b/docs/content/showcase/yulinling.net.md index b5a6559f1..b5a6559f1 100644 --- a/content/showcase/yulinling.net.md +++ b/docs/content/showcase/yulinling.net.md diff --git a/content/taxonomies/displaying.md b/docs/content/taxonomies/displaying.md index 43dd48ba8..43dd48ba8 100644 --- a/content/taxonomies/displaying.md +++ b/docs/content/taxonomies/displaying.md diff --git a/content/taxonomies/methods.md b/docs/content/taxonomies/methods.md index c5b9e755b..c5b9e755b 100644 --- a/content/taxonomies/methods.md +++ b/docs/content/taxonomies/methods.md diff --git a/content/taxonomies/ordering.md b/docs/content/taxonomies/ordering.md index ac86bc69d..ac86bc69d 100644 --- a/content/taxonomies/ordering.md +++ b/docs/content/taxonomies/ordering.md diff --git a/content/taxonomies/overview.md b/docs/content/taxonomies/overview.md index a01e63980..a01e63980 100644 --- a/content/taxonomies/overview.md +++ b/docs/content/taxonomies/overview.md diff --git a/content/taxonomies/templates.md b/docs/content/taxonomies/templates.md index 0a44d5b19..0a44d5b19 100644 --- a/content/taxonomies/templates.md +++ b/docs/content/taxonomies/templates.md diff --git a/content/taxonomies/usage.md b/docs/content/taxonomies/usage.md index 9c5dc2189..9c5dc2189 100644 --- a/content/taxonomies/usage.md +++ b/docs/content/taxonomies/usage.md diff --git a/content/templates/404.md b/docs/content/templates/404.md index 0dc2e2c9c..0dc2e2c9c 100644 --- a/content/templates/404.md +++ b/docs/content/templates/404.md diff --git a/content/templates/ace.md b/docs/content/templates/ace.md index 1bd85796a..1bd85796a 100644 --- a/content/templates/ace.md +++ b/docs/content/templates/ace.md diff --git a/content/templates/amber.md b/docs/content/templates/amber.md index 2ba84353b..2ba84353b 100644 --- a/content/templates/amber.md +++ b/docs/content/templates/amber.md diff --git a/content/templates/blocks.md b/docs/content/templates/blocks.md index f80a3d908..f80a3d908 100644 --- a/content/templates/blocks.md +++ b/docs/content/templates/blocks.md diff --git a/content/templates/content.md b/docs/content/templates/content.md index 24d1782ad..24d1782ad 100644 --- a/content/templates/content.md +++ b/docs/content/templates/content.md diff --git a/content/templates/debugging.md b/docs/content/templates/debugging.md index ae348f533..ae348f533 100644 --- a/content/templates/debugging.md +++ b/docs/content/templates/debugging.md diff --git a/content/templates/functions.md b/docs/content/templates/functions.md index 0c6968bd2..0c6968bd2 100644 --- a/content/templates/functions.md +++ b/docs/content/templates/functions.md diff --git a/content/templates/go-templates.md b/docs/content/templates/go-templates.md index bb7c71606..bb7c71606 100644 --- a/content/templates/go-templates.md +++ b/docs/content/templates/go-templates.md diff --git a/content/templates/homepage.md b/docs/content/templates/homepage.md index c5cebd219..c5cebd219 100644 --- a/content/templates/homepage.md +++ b/docs/content/templates/homepage.md diff --git a/content/templates/list.md b/docs/content/templates/list.md index 22d3123ac..22d3123ac 100644 --- a/content/templates/list.md +++ b/docs/content/templates/list.md diff --git a/content/templates/overview.md b/docs/content/templates/overview.md index 3b41a6641..3b41a6641 100644 --- a/content/templates/overview.md +++ b/docs/content/templates/overview.md diff --git a/content/templates/partials.md b/docs/content/templates/partials.md index 46c2c2400..46c2c2400 100644 --- a/content/templates/partials.md +++ b/docs/content/templates/partials.md diff --git a/content/templates/rss.md b/docs/content/templates/rss.md index 10f2a8535..10f2a8535 100644 --- a/content/templates/rss.md +++ b/docs/content/templates/rss.md diff --git a/content/templates/sitemap.md b/docs/content/templates/sitemap.md index c893f3155..c893f3155 100644 --- a/content/templates/sitemap.md +++ b/docs/content/templates/sitemap.md diff --git a/content/templates/terms.md b/docs/content/templates/terms.md index 83414ceed..83414ceed 100644 --- a/content/templates/terms.md +++ b/docs/content/templates/terms.md diff --git a/content/templates/variables.md b/docs/content/templates/variables.md index 962297156..962297156 100644 --- a/content/templates/variables.md +++ b/docs/content/templates/variables.md diff --git a/content/templates/views.md b/docs/content/templates/views.md index c8426ec11..c8426ec11 100644 --- a/content/templates/views.md +++ b/docs/content/templates/views.md diff --git a/content/themes/creation.md b/docs/content/themes/creation.md index 5f7a2e53b..5f7a2e53b 100644 --- a/content/themes/creation.md +++ b/docs/content/themes/creation.md diff --git a/content/themes/customizing.md b/docs/content/themes/customizing.md index f381106db..f381106db 100644 --- a/content/themes/customizing.md +++ b/docs/content/themes/customizing.md diff --git a/content/themes/installing.md b/docs/content/themes/installing.md index a70f50287..a70f50287 100644 --- a/content/themes/installing.md +++ b/docs/content/themes/installing.md diff --git a/content/themes/overview.md b/docs/content/themes/overview.md index 94e7259fd..94e7259fd 100644 --- a/content/themes/overview.md +++ b/docs/content/themes/overview.md diff --git a/content/themes/usage.md b/docs/content/themes/usage.md index 5b1d11c02..5b1d11c02 100644 --- a/content/themes/usage.md +++ b/docs/content/themes/usage.md diff --git a/content/tools/_index.md b/docs/content/tools/_index.md index d5543f045..d5543f045 100644 --- a/content/tools/_index.md +++ b/docs/content/tools/_index.md diff --git a/content/troubleshooting/categories-with-accented-characters.md b/docs/content/troubleshooting/categories-with-accented-characters.md index 3a6dba82d..3a6dba82d 100644 --- a/content/troubleshooting/categories-with-accented-characters.md +++ b/docs/content/troubleshooting/categories-with-accented-characters.md diff --git a/content/troubleshooting/overview.md b/docs/content/troubleshooting/overview.md index 5f5ec19dc..5f5ec19dc 100644 --- a/content/troubleshooting/overview.md +++ b/docs/content/troubleshooting/overview.md diff --git a/content/troubleshooting/strange-eof-error.md b/docs/content/troubleshooting/strange-eof-error.md index 559b1e7d2..559b1e7d2 100644 --- a/content/troubleshooting/strange-eof-error.md +++ b/docs/content/troubleshooting/strange-eof-error.md diff --git a/content/tutorials/automated-deployments.md b/docs/content/tutorials/automated-deployments.md index 9f6d5c628..9f6d5c628 100644 --- a/content/tutorials/automated-deployments.md +++ b/docs/content/tutorials/automated-deployments.md diff --git a/content/tutorials/create-a-multilingual-site.md b/docs/content/tutorials/create-a-multilingual-site.md index 8a2dd960e..8a2dd960e 100644 --- a/content/tutorials/create-a-multilingual-site.md +++ b/docs/content/tutorials/create-a-multilingual-site.md diff --git a/content/tutorials/creating-a-new-theme.md b/docs/content/tutorials/creating-a-new-theme.md index a0b95b5e6..a0b95b5e6 100644 --- a/content/tutorials/creating-a-new-theme.md +++ b/docs/content/tutorials/creating-a-new-theme.md diff --git a/content/tutorials/deployment-with-rsync.md b/docs/content/tutorials/deployment-with-rsync.md index 7bc516650..7bc516650 100644 --- a/content/tutorials/deployment-with-rsync.md +++ b/docs/content/tutorials/deployment-with-rsync.md diff --git a/content/tutorials/github-pages-blog.md b/docs/content/tutorials/github-pages-blog.md index 013642ded..013642ded 100644 --- a/content/tutorials/github-pages-blog.md +++ b/docs/content/tutorials/github-pages-blog.md diff --git a/content/tutorials/hosting-on-bitbucket.md b/docs/content/tutorials/hosting-on-bitbucket.md index 027618fa5..027618fa5 100644 --- a/content/tutorials/hosting-on-bitbucket.md +++ b/docs/content/tutorials/hosting-on-bitbucket.md diff --git a/content/tutorials/hosting-on-gitlab.md b/docs/content/tutorials/hosting-on-gitlab.md index 0c213d1ee..0c213d1ee 100644 --- a/content/tutorials/hosting-on-gitlab.md +++ b/docs/content/tutorials/hosting-on-gitlab.md diff --git a/content/tutorials/how-to-contribute-to-hugo.md b/docs/content/tutorials/how-to-contribute-to-hugo.md index 5f3795f20..5f3795f20 100644 --- a/content/tutorials/how-to-contribute-to-hugo.md +++ b/docs/content/tutorials/how-to-contribute-to-hugo.md diff --git a/content/tutorials/installing-on-mac.md b/docs/content/tutorials/installing-on-mac.md index 12bf01e2b..12bf01e2b 100644 --- a/content/tutorials/installing-on-mac.md +++ b/docs/content/tutorials/installing-on-mac.md diff --git a/content/tutorials/installing-on-windows.md b/docs/content/tutorials/installing-on-windows.md index d249571f6..d249571f6 100644 --- a/content/tutorials/installing-on-windows.md +++ b/docs/content/tutorials/installing-on-windows.md diff --git a/content/tutorials/mathjax.md b/docs/content/tutorials/mathjax.md index e8d896354..e8d896354 100644 --- a/content/tutorials/mathjax.md +++ b/docs/content/tutorials/mathjax.md diff --git a/content/tutorials/migrate-from-jekyll.md b/docs/content/tutorials/migrate-from-jekyll.md index 0fd5d4af7..0fd5d4af7 100644 --- a/content/tutorials/migrate-from-jekyll.md +++ b/docs/content/tutorials/migrate-from-jekyll.md diff --git a/data/docs.json b/docs/data/docs.json index 83d614afa..83d614afa 100644 --- a/data/docs.json +++ b/docs/data/docs.json diff --git a/data/references.toml b/docs/data/references.toml index 84d8c5935..84d8c5935 100644 --- a/data/references.toml +++ b/docs/data/references.toml diff --git a/data/titles.toml b/docs/data/titles.toml index 2348c8561..2348c8561 100644 --- a/data/titles.toml +++ b/docs/data/titles.toml diff --git a/layouts/404.html b/docs/layouts/404.html index 4b140fbdb..4b140fbdb 100644 --- a/layouts/404.html +++ b/docs/layouts/404.html diff --git a/layouts/_default/baseof.html b/docs/layouts/_default/baseof.html index 076f46dda..076f46dda 100644 --- a/layouts/_default/baseof.html +++ b/docs/layouts/_default/baseof.html diff --git a/layouts/_default/list.html b/docs/layouts/_default/list.html index f2b122c40..f2b122c40 100644 --- a/layouts/_default/list.html +++ b/docs/layouts/_default/list.html diff --git a/layouts/_default/single.html b/docs/layouts/_default/single.html index b8bcfa70c..b8bcfa70c 100644 --- a/layouts/_default/single.html +++ b/docs/layouts/_default/single.html diff --git a/layouts/index.html b/docs/layouts/index.html index a212e1229..a212e1229 100644 --- a/layouts/index.html +++ b/docs/layouts/index.html diff --git a/layouts/index.redir b/docs/layouts/index.redir index 2dfd2bc0f..2dfd2bc0f 100644 --- a/layouts/index.redir +++ b/docs/layouts/index.redir diff --git a/layouts/partials/analytics.html b/docs/layouts/partials/analytics.html index a30754c0a..a30754c0a 100644 --- a/layouts/partials/analytics.html +++ b/docs/layouts/partials/analytics.html diff --git a/layouts/partials/footer.html b/docs/layouts/partials/footer.html index 9734aa983..9734aa983 100644 --- a/layouts/partials/footer.html +++ b/docs/layouts/partials/footer.html diff --git a/layouts/partials/header.html b/docs/layouts/partials/header.html index 8149a7bb6..8149a7bb6 100644 --- a/layouts/partials/header.html +++ b/docs/layouts/partials/header.html diff --git a/layouts/partials/menu.html b/docs/layouts/partials/menu.html index d9667291d..d9667291d 100644 --- a/layouts/partials/menu.html +++ b/docs/layouts/partials/menu.html diff --git a/layouts/partials/quotes.html b/docs/layouts/partials/quotes.html index 3ba0db953..3ba0db953 100644 --- a/layouts/partials/quotes.html +++ b/docs/layouts/partials/quotes.html diff --git a/layouts/partials/search.html b/docs/layouts/partials/search.html index 6a58160cc..6a58160cc 100644 --- a/layouts/partials/search.html +++ b/docs/layouts/partials/search.html diff --git a/layouts/section/commands.html b/docs/layouts/section/commands.html index afa045452..afa045452 100644 --- a/layouts/section/commands.html +++ b/docs/layouts/section/commands.html diff --git a/layouts/section/release-notes.html b/docs/layouts/section/release-notes.html index 6af512603..6af512603 100644 --- a/layouts/section/release-notes.html +++ b/docs/layouts/section/release-notes.html diff --git a/layouts/section/showcase.html b/docs/layouts/section/showcase.html index b21e98359..b21e98359 100644 --- a/layouts/section/showcase.html +++ b/docs/layouts/section/showcase.html diff --git a/layouts/shortcodes/datatable-vertical.html b/docs/layouts/shortcodes/datatable-vertical.html index 1d2629eca..1d2629eca 100644 --- a/layouts/shortcodes/datatable-vertical.html +++ b/docs/layouts/shortcodes/datatable-vertical.html diff --git a/layouts/shortcodes/datatable.html b/docs/layouts/shortcodes/datatable.html index f40605404..f40605404 100644 --- a/layouts/shortcodes/datatable.html +++ b/docs/layouts/shortcodes/datatable.html diff --git a/layouts/shortcodes/directoryindex.html b/docs/layouts/shortcodes/directoryindex.html index 02a4efadc..02a4efadc 100644 --- a/layouts/shortcodes/directoryindex.html +++ b/docs/layouts/shortcodes/directoryindex.html diff --git a/layouts/shortcodes/gh.html b/docs/layouts/shortcodes/gh.html index 0a28bf121..0a28bf121 100644 --- a/layouts/shortcodes/gh.html +++ b/docs/layouts/shortcodes/gh.html diff --git a/layouts/shortcodes/nohighlight.html b/docs/layouts/shortcodes/nohighlight.html index d9cb5f302..d9cb5f302 100644 --- a/layouts/shortcodes/nohighlight.html +++ b/docs/layouts/shortcodes/nohighlight.html diff --git a/layouts/shortcodes/readfile.html b/docs/layouts/shortcodes/readfile.html index f5b3459bf..f5b3459bf 100644 --- a/layouts/shortcodes/readfile.html +++ b/docs/layouts/shortcodes/readfile.html diff --git a/layouts/shortcodes/youtube.html b/docs/layouts/shortcodes/youtube.html index ce7dd0508..ce7dd0508 100644 --- a/layouts/shortcodes/youtube.html +++ b/docs/layouts/shortcodes/youtube.html diff --git a/layouts/showcase/thumbnail.html b/docs/layouts/showcase/thumbnail.html index 5c36e5d2f..5c36e5d2f 100644 --- a/layouts/showcase/thumbnail.html +++ b/docs/layouts/showcase/thumbnail.html diff --git a/static/_headers b/docs/static/_headers index 53cc866dc..53cc866dc 100644 --- a/static/_headers +++ b/docs/static/_headers diff --git a/static/apple-touch-icon.png b/docs/static/apple-touch-icon.png Binary files differindex 50e23ce1d..50e23ce1d 100644 --- a/static/apple-touch-icon.png +++ b/docs/static/apple-touch-icon.png diff --git a/static/css/bootstrap-additions-gohugo.css b/docs/static/css/bootstrap-additions-gohugo.css index 4128f3b8c..4128f3b8c 100644 --- a/static/css/bootstrap-additions-gohugo.css +++ b/docs/static/css/bootstrap-additions-gohugo.css diff --git a/static/css/bootstrap-changes-gohugo.css b/docs/static/css/bootstrap-changes-gohugo.css index d4ef99bcc..d4ef99bcc 100644 --- a/static/css/bootstrap-changes-gohugo.css +++ b/docs/static/css/bootstrap-changes-gohugo.css diff --git a/static/css/bootstrap-stripped-gohugo.css b/docs/static/css/bootstrap-stripped-gohugo.css index aafb0df53..aafb0df53 100644 --- a/static/css/bootstrap-stripped-gohugo.css +++ b/docs/static/css/bootstrap-stripped-gohugo.css diff --git a/static/css/content-style.css b/docs/static/css/content-style.css index 1356b0495..1356b0495 100644 --- a/static/css/content-style.css +++ b/docs/static/css/content-style.css diff --git a/static/css/home-page-style-responsive.css b/docs/static/css/home-page-style-responsive.css index 46ce3eb43..46ce3eb43 100644 --- a/static/css/home-page-style-responsive.css +++ b/docs/static/css/home-page-style-responsive.css diff --git a/static/css/home-page-style.css b/docs/static/css/home-page-style.css index 4ff3bc815..4ff3bc815 100644 --- a/static/css/home-page-style.css +++ b/docs/static/css/home-page-style.css diff --git a/static/css/hugofont.css b/docs/static/css/hugofont.css index 09d6ce070..09d6ce070 100644 --- a/static/css/hugofont.css +++ b/docs/static/css/hugofont.css diff --git a/static/css/style-responsive.css b/docs/static/css/style-responsive.css index 58f614bb4..58f614bb4 100644 --- a/static/css/style-responsive.css +++ b/docs/static/css/style-responsive.css diff --git a/static/css/style.css b/docs/static/css/style.css index 312c247c9..312c247c9 100644 --- a/static/css/style.css +++ b/docs/static/css/style.css diff --git a/static/favicon.ico b/docs/static/favicon.ico Binary files differindex 36693330b..36693330b 100644 --- a/static/favicon.ico +++ b/docs/static/favicon.ico diff --git a/static/fonts/glyphicons-halflings-regular.eot b/docs/static/fonts/glyphicons-halflings-regular.eot Binary files differindex b93a4953f..b93a4953f 100644 --- a/static/fonts/glyphicons-halflings-regular.eot +++ b/docs/static/fonts/glyphicons-halflings-regular.eot diff --git a/static/fonts/glyphicons-halflings-regular.svg b/docs/static/fonts/glyphicons-halflings-regular.svg index 94fb5490a..94fb5490a 100644 --- a/static/fonts/glyphicons-halflings-regular.svg +++ b/docs/static/fonts/glyphicons-halflings-regular.svg diff --git a/static/fonts/glyphicons-halflings-regular.ttf b/docs/static/fonts/glyphicons-halflings-regular.ttf Binary files differindex 1413fc609..1413fc609 100644 --- a/static/fonts/glyphicons-halflings-regular.ttf +++ b/docs/static/fonts/glyphicons-halflings-regular.ttf diff --git a/static/fonts/glyphicons-halflings-regular.woff b/docs/static/fonts/glyphicons-halflings-regular.woff Binary files differindex 9e612858f..9e612858f 100644 --- a/static/fonts/glyphicons-halflings-regular.woff +++ b/docs/static/fonts/glyphicons-halflings-regular.woff diff --git a/static/fonts/glyphicons-halflings-regular.woff2 b/docs/static/fonts/glyphicons-halflings-regular.woff2 Binary files differindex 64539b54c..64539b54c 100644 --- a/static/fonts/glyphicons-halflings-regular.woff2 +++ b/docs/static/fonts/glyphicons-halflings-regular.woff2 diff --git a/static/fonts/hugo.eot b/docs/static/fonts/hugo.eot Binary files differindex b92f00f93..b92f00f93 100644 --- a/static/fonts/hugo.eot +++ b/docs/static/fonts/hugo.eot diff --git a/static/fonts/hugo.svg b/docs/static/fonts/hugo.svg index 7913f7c1f..7913f7c1f 100644 --- a/static/fonts/hugo.svg +++ b/docs/static/fonts/hugo.svg diff --git a/static/fonts/hugo.ttf b/docs/static/fonts/hugo.ttf Binary files differindex 962914d33..962914d33 100644 --- a/static/fonts/hugo.ttf +++ b/docs/static/fonts/hugo.ttf diff --git a/static/fonts/hugo.woff b/docs/static/fonts/hugo.woff Binary files differindex 4693fbe7f..4693fbe7f 100644 --- a/static/fonts/hugo.woff +++ b/docs/static/fonts/hugo.woff diff --git a/static/img/2626info-tn.png b/docs/static/img/2626info-tn.png Binary files differindex fb6b11757..fb6b11757 100644 --- a/static/img/2626info-tn.png +++ b/docs/static/img/2626info-tn.png diff --git a/static/img/antzucaro-tn.jpg b/docs/static/img/antzucaro-tn.jpg Binary files differindex 188769c0f..188769c0f 100644 --- a/static/img/antzucaro-tn.jpg +++ b/docs/static/img/antzucaro-tn.jpg diff --git a/static/img/apperneticioblog.png b/docs/static/img/apperneticioblog.png Binary files differindex f2fcf6d96..f2fcf6d96 100644 --- a/static/img/apperneticioblog.png +++ b/docs/static/img/apperneticioblog.png diff --git a/static/img/arresteddevops-tn.png b/docs/static/img/arresteddevops-tn.png Binary files differindex 868df1d77..868df1d77 100644 --- a/static/img/arresteddevops-tn.png +++ b/docs/static/img/arresteddevops-tn.png diff --git a/static/img/asc-tn.jpg b/docs/static/img/asc-tn.jpg Binary files differindex a5148e236..a5148e236 100644 --- a/static/img/asc-tn.jpg +++ b/docs/static/img/asc-tn.jpg diff --git a/static/img/astrochili-tn.png b/docs/static/img/astrochili-tn.png Binary files differindex ec11741ee..ec11741ee 100644 --- a/static/img/astrochili-tn.png +++ b/docs/static/img/astrochili-tn.png diff --git a/static/img/aydoscom.png b/docs/static/img/aydoscom.png Binary files differindex f2cfc3982..f2cfc3982 100644 --- a/static/img/aydoscom.png +++ b/docs/static/img/aydoscom.png diff --git a/static/img/balaramadurai-net-tn.jpg b/docs/static/img/balaramadurai-net-tn.jpg Binary files differindex 207a4a840..207a4a840 100644 --- a/static/img/balaramadurai-net-tn.jpg +++ b/docs/static/img/balaramadurai-net-tn.jpg diff --git a/static/img/barricade-tn.png b/docs/static/img/barricade-tn.png Binary files differindex 96eed0fbe..96eed0fbe 100644 --- a/static/img/barricade-tn.png +++ b/docs/static/img/barricade-tn.png diff --git a/static/img/bepsays-tn.png b/docs/static/img/bepsays-tn.png Binary files differindex ca9119cc5..ca9119cc5 100644 --- a/static/img/bepsays-tn.png +++ b/docs/static/img/bepsays-tn.png diff --git a/static/img/bharathpalavalli-tn.png b/docs/static/img/bharathpalavalli-tn.png Binary files differindex bcf15ed0a..bcf15ed0a 100644 --- a/static/img/bharathpalavalli-tn.png +++ b/docs/static/img/bharathpalavalli-tn.png diff --git a/static/img/bugtrackersio-tn.jpg b/docs/static/img/bugtrackersio-tn.jpg Binary files differindex a56e94009..a56e94009 100644 --- a/static/img/bugtrackersio-tn.jpg +++ b/docs/static/img/bugtrackersio-tn.jpg diff --git a/static/img/bullion-investor-com.png b/docs/static/img/bullion-investor-com.png Binary files differindex 3cd78b97d..3cd78b97d 100644 --- a/static/img/bullion-investor-com.png +++ b/docs/static/img/bullion-investor-com.png diff --git a/static/img/camunda-blog.png b/docs/static/img/camunda-blog.png Binary files differindex 95a004ff7..95a004ff7 100644 --- a/static/img/camunda-blog.png +++ b/docs/static/img/camunda-blog.png diff --git a/static/img/camunda-docs.png b/docs/static/img/camunda-docs.png Binary files differindex e008fdabb..e008fdabb 100644 --- a/static/img/camunda-docs.png +++ b/docs/static/img/camunda-docs.png diff --git a/static/img/carnivorousplants-tn.png b/docs/static/img/carnivorousplants-tn.png Binary files differindex 2e45bc013..2e45bc013 100644 --- a/static/img/carnivorousplants-tn.png +++ b/docs/static/img/carnivorousplants-tn.png diff --git a/static/img/cdnoverview-tn.png b/docs/static/img/cdnoverview-tn.png Binary files differindex a95852c45..a95852c45 100644 --- a/static/img/cdnoverview-tn.png +++ b/docs/static/img/cdnoverview-tn.png diff --git a/static/img/chinese-grammar-tn.png b/docs/static/img/chinese-grammar-tn.png Binary files differindex 3d84184cf..3d84184cf 100644 --- a/static/img/chinese-grammar-tn.png +++ b/docs/static/img/chinese-grammar-tn.png diff --git a/static/img/chingli-tn.jpg b/docs/static/img/chingli-tn.jpg Binary files differindex 61ee53e04..61ee53e04 100644 --- a/static/img/chingli-tn.jpg +++ b/docs/static/img/chingli-tn.jpg diff --git a/static/img/chipsncookies-tn.png b/docs/static/img/chipsncookies-tn.png Binary files differindex f355cb5a4..f355cb5a4 100644 --- a/static/img/chipsncookies-tn.png +++ b/docs/static/img/chipsncookies-tn.png diff --git a/static/img/christianmendoza-tn.jpg b/docs/static/img/christianmendoza-tn.jpg Binary files differindex 434d9dc54..434d9dc54 100644 --- a/static/img/christianmendoza-tn.jpg +++ b/docs/static/img/christianmendoza-tn.jpg diff --git a/static/img/cinegyopen-tn.png b/docs/static/img/cinegyopen-tn.png Binary files differindex 3216259fc..3216259fc 100644 --- a/static/img/cinegyopen-tn.png +++ b/docs/static/img/cinegyopen-tn.png diff --git a/static/img/clearhaus-tn.png b/docs/static/img/clearhaus-tn.png Binary files differindex 4785019a6..4785019a6 100644 --- a/static/img/clearhaus-tn.png +++ b/docs/static/img/clearhaus-tn.png diff --git a/static/img/cloudshark-tn.jpg b/docs/static/img/cloudshark-tn.jpg Binary files differindex 68f8018ef..68f8018ef 100644 --- a/static/img/cloudshark-tn.jpg +++ b/docs/static/img/cloudshark-tn.jpg diff --git a/static/img/codingjournal-tn.png b/docs/static/img/codingjournal-tn.png Binary files differindex e2bfde580..e2bfde580 100644 --- a/static/img/codingjournal-tn.png +++ b/docs/static/img/codingjournal-tn.png diff --git a/static/img/consequently.jpg b/docs/static/img/consequently.jpg Binary files differindex fdb1ebd7b..fdb1ebd7b 100644 --- a/static/img/consequently.jpg +++ b/docs/static/img/consequently.jpg diff --git a/static/img/content/archetypes/archetype-hierarchy.png b/docs/static/img/content/archetypes/archetype-hierarchy.png Binary files differindex cb0d0bcf4..cb0d0bcf4 100644 --- a/static/img/content/archetypes/archetype-hierarchy.png +++ b/docs/static/img/content/archetypes/archetype-hierarchy.png diff --git a/static/img/ctlcompiled-tn.png b/docs/static/img/ctlcompiled-tn.png Binary files differindex e5137da94..e5137da94 100644 --- a/static/img/ctlcompiled-tn.png +++ b/docs/static/img/ctlcompiled-tn.png diff --git a/static/img/danmux-tn.jpg b/docs/static/img/danmux-tn.jpg Binary files differindex e6c82a2ef..e6c82a2ef 100644 --- a/static/img/danmux-tn.jpg +++ b/docs/static/img/danmux-tn.jpg diff --git a/static/img/datapipelinearchitect-tn.jpg b/docs/static/img/datapipelinearchitect-tn.jpg Binary files differindex 597ecdbba..597ecdbba 100644 --- a/static/img/datapipelinearchitect-tn.jpg +++ b/docs/static/img/datapipelinearchitect-tn.jpg diff --git a/static/img/davidepetilli-tn.jpg b/docs/static/img/davidepetilli-tn.jpg Binary files differindex a46fb9149..a46fb9149 100644 --- a/static/img/davidepetilli-tn.jpg +++ b/docs/static/img/davidepetilli-tn.jpg diff --git a/static/img/davidrallen-tn.png b/docs/static/img/davidrallen-tn.png Binary files differindex 937147dee..937147dee 100644 --- a/static/img/davidrallen-tn.png +++ b/docs/static/img/davidrallen-tn.png diff --git a/static/img/davidyates-tn.png b/docs/static/img/davidyates-tn.png Binary files differindex a6a0a6143..a6a0a6143 100644 --- a/static/img/davidyates-tn.png +++ b/docs/static/img/davidyates-tn.png diff --git a/static/img/dbzman-online-tn.png b/docs/static/img/dbzman-online-tn.png Binary files differindex eb9d4ea0c..eb9d4ea0c 100644 --- a/static/img/dbzman-online-tn.png +++ b/docs/static/img/dbzman-online-tn.png diff --git a/static/img/desk-mini.jpg b/docs/static/img/desk-mini.jpg Binary files differindex ff296c8f7..ff296c8f7 100644 --- a/static/img/desk-mini.jpg +++ b/docs/static/img/desk-mini.jpg diff --git a/static/img/desk-sm.jpg b/docs/static/img/desk-sm.jpg Binary files differindex d4a21ad55..d4a21ad55 100644 --- a/static/img/desk-sm.jpg +++ b/docs/static/img/desk-sm.jpg diff --git a/static/img/desk-wide.jpg b/docs/static/img/desk-wide.jpg Binary files differindex 8ff17bc4d..8ff17bc4d 100644 --- a/static/img/desk-wide.jpg +++ b/docs/static/img/desk-wide.jpg diff --git a/static/img/desk.jpg b/docs/static/img/desk.jpg Binary files differindex 43ecc6694..43ecc6694 100644 --- a/static/img/desk.jpg +++ b/docs/static/img/desk.jpg diff --git a/static/img/devmonk-tn.jpg b/docs/static/img/devmonk-tn.jpg Binary files differindex 05331d119..05331d119 100644 --- a/static/img/devmonk-tn.jpg +++ b/docs/static/img/devmonk-tn.jpg diff --git a/static/img/dmitriid.com.png b/docs/static/img/dmitriid.com.png Binary files differindex 9c7217f6e..9c7217f6e 100644 --- a/static/img/dmitriid.com.png +++ b/docs/static/img/dmitriid.com.png diff --git a/static/img/docs.eurie.io-tn.png b/docs/static/img/docs.eurie.io-tn.png Binary files differindex 43443167b..43443167b 100644 --- a/static/img/docs.eurie.io-tn.png +++ b/docs/static/img/docs.eurie.io-tn.png diff --git a/static/img/emilyhorsman.com-tn.jpg b/docs/static/img/emilyhorsman.com-tn.jpg Binary files differindex 99d2f9559..99d2f9559 100644 --- a/static/img/emilyhorsman.com-tn.jpg +++ b/docs/static/img/emilyhorsman.com-tn.jpg diff --git a/static/img/enjoyablerecipes-tn.png b/docs/static/img/enjoyablerecipes-tn.png Binary files differindex 460c804bc..460c804bc 100644 --- a/static/img/enjoyablerecipes-tn.png +++ b/docs/static/img/enjoyablerecipes-tn.png diff --git a/static/img/esaezgil_com-tn.png b/docs/static/img/esaezgil_com-tn.png Binary files differindex f9b4087b6..f9b4087b6 100644 --- a/static/img/esaezgil_com-tn.png +++ b/docs/static/img/esaezgil_com-tn.png diff --git a/static/img/esolia_com-tn.png b/docs/static/img/esolia_com-tn.png Binary files differindex 4574b085f..4574b085f 100644 --- a/static/img/esolia_com-tn.png +++ b/docs/static/img/esolia_com-tn.png diff --git a/static/img/esolia_pro-tn.png b/docs/static/img/esolia_pro-tn.png Binary files differindex 021911456..021911456 100644 --- a/static/img/esolia_pro-tn.png +++ b/docs/static/img/esolia_pro-tn.png diff --git a/static/img/fale-tn.png b/docs/static/img/fale-tn.png Binary files differindex 2c5f53f84..2c5f53f84 100644 --- a/static/img/fale-tn.png +++ b/docs/static/img/fale-tn.png diff --git a/static/img/firstnameclub.png b/docs/static/img/firstnameclub.png Binary files differindex b5bf80847..b5bf80847 100644 --- a/static/img/firstnameclub.png +++ b/docs/static/img/firstnameclub.png diff --git a/static/img/fixatom-tn.png b/docs/static/img/fixatom-tn.png Binary files differindex 39ded2463..39ded2463 100644 --- a/static/img/fixatom-tn.png +++ b/docs/static/img/fixatom-tn.png diff --git a/static/img/freebsd-19px.svg b/docs/static/img/freebsd-19px.svg index 4215b83a9..4215b83a9 100644 --- a/static/img/freebsd-19px.svg +++ b/docs/static/img/freebsd-19px.svg diff --git a/static/img/furqansoftware-tn.png b/docs/static/img/furqansoftware-tn.png Binary files differindex e1d0e964f..e1d0e964f 100644 --- a/static/img/furqansoftware-tn.png +++ b/docs/static/img/furqansoftware-tn.png diff --git a/static/img/fxsitecompat-tn.png b/docs/static/img/fxsitecompat-tn.png Binary files differindex 3df542f59..3df542f59 100644 --- a/static/img/fxsitecompat-tn.png +++ b/docs/static/img/fxsitecompat-tn.png diff --git a/static/img/gntech-tn.png b/docs/static/img/gntech-tn.png Binary files differindex 0a3fad9ba..0a3fad9ba 100644 --- a/static/img/gntech-tn.png +++ b/docs/static/img/gntech-tn.png diff --git a/static/img/gogb-tn.jpg b/docs/static/img/gogb-tn.jpg Binary files differindex caed5cfbb..caed5cfbb 100644 --- a/static/img/gogb-tn.jpg +++ b/docs/static/img/gogb-tn.jpg diff --git a/static/img/goin5minutes-tn.png b/docs/static/img/goin5minutes-tn.png Binary files differindex eef26f110..eef26f110 100644 --- a/static/img/goin5minutes-tn.png +++ b/docs/static/img/goin5minutes-tn.png diff --git a/static/img/gray.png b/docs/static/img/gray.png Binary files differindex 3807691d3..3807691d3 100644 --- a/static/img/gray.png +++ b/docs/static/img/gray.png diff --git a/static/img/h10n.me-tn.png b/docs/static/img/h10n.me-tn.png Binary files differindex 74bfee21b..74bfee21b 100644 --- a/static/img/h10n.me-tn.png +++ b/docs/static/img/h10n.me-tn.png diff --git a/static/img/heimatverein-niederjosbach-tn.png b/docs/static/img/heimatverein-niederjosbach-tn.png Binary files differindex f47425b7e..f47425b7e 100644 --- a/static/img/heimatverein-niederjosbach-tn.png +++ b/docs/static/img/heimatverein-niederjosbach-tn.png diff --git a/static/img/horeaporutiu-tn.jpg b/docs/static/img/horeaporutiu-tn.jpg Binary files differindex 7e0d0fc80..7e0d0fc80 100644 --- a/static/img/horeaporutiu-tn.jpg +++ b/docs/static/img/horeaporutiu-tn.jpg diff --git a/static/img/hugo-logo-med.png b/docs/static/img/hugo-logo-med.png Binary files differindex dcc141690..dcc141690 100644 --- a/static/img/hugo-logo-med.png +++ b/docs/static/img/hugo-logo-med.png diff --git a/static/img/hugo-logo.png b/docs/static/img/hugo-logo.png Binary files differindex a4f1321b0..a4f1321b0 100644 --- a/static/img/hugo-logo.png +++ b/docs/static/img/hugo-logo.png diff --git a/static/img/hugo-tn.jpg b/docs/static/img/hugo-tn.jpg Binary files differindex 9ac04a38a..9ac04a38a 100644 --- a/static/img/hugo-tn.jpg +++ b/docs/static/img/hugo-tn.jpg diff --git a/static/img/hugo.png b/docs/static/img/hugo.png Binary files differindex 48acf346c..48acf346c 100644 --- a/static/img/hugo.png +++ b/docs/static/img/hugo.png diff --git a/static/img/hugoSM.png b/docs/static/img/hugoSM.png Binary files differindex f64f43088..f64f43088 100644 --- a/static/img/hugoSM.png +++ b/docs/static/img/hugoSM.png diff --git a/static/img/invision-tn.png b/docs/static/img/invision-tn.png Binary files differindex 097f71bb3..097f71bb3 100644 --- a/static/img/invision-tn.png +++ b/docs/static/img/invision-tn.png diff --git a/static/img/jamescampbell-tn.png b/docs/static/img/jamescampbell-tn.png Binary files differindex 31fb7dc28..31fb7dc28 100644 --- a/static/img/jamescampbell-tn.png +++ b/docs/static/img/jamescampbell-tn.png diff --git a/static/img/jorgennilsson-tn.png b/docs/static/img/jorgennilsson-tn.png Binary files differindex abcc54ab3..abcc54ab3 100644 --- a/static/img/jorgennilsson-tn.png +++ b/docs/static/img/jorgennilsson-tn.png diff --git a/static/img/kjhealy-tn.jpg b/docs/static/img/kjhealy-tn.jpg Binary files differindex dd2561ae3..dd2561ae3 100644 --- a/static/img/kjhealy-tn.jpg +++ b/docs/static/img/kjhealy-tn.jpg diff --git a/static/img/klingt-net-tn.png b/docs/static/img/klingt-net-tn.png Binary files differindex 5ffd3d6a7..5ffd3d6a7 100644 --- a/static/img/klingt-net-tn.png +++ b/docs/static/img/klingt-net-tn.png diff --git a/static/img/launchcode-tn.jpg b/docs/static/img/launchcode-tn.jpg Binary files differindex c422450a1..c422450a1 100644 --- a/static/img/launchcode-tn.jpg +++ b/docs/static/img/launchcode-tn.jpg diff --git a/static/img/leepenney-tn.jpg b/docs/static/img/leepenney-tn.jpg Binary files differindex c1085d779..c1085d779 100644 --- a/static/img/leepenney-tn.jpg +++ b/docs/static/img/leepenney-tn.jpg diff --git a/static/img/leowkahman-tn.png b/docs/static/img/leowkahman-tn.png Binary files differindex ae7803f57..ae7803f57 100644 --- a/static/img/leowkahman-tn.png +++ b/docs/static/img/leowkahman-tn.png diff --git a/static/img/lk4d4-tn.jpg b/docs/static/img/lk4d4-tn.jpg Binary files differindex 687606814..687606814 100644 --- a/static/img/lk4d4-tn.jpg +++ b/docs/static/img/lk4d4-tn.jpg diff --git a/static/img/losslesslife-tn.png b/docs/static/img/losslesslife-tn.png Binary files differindex cc9e286aa..cc9e286aa 100644 --- a/static/img/losslesslife-tn.png +++ b/docs/static/img/losslesslife-tn.png diff --git a/static/img/lucumt.info.png b/docs/static/img/lucumt.info.png Binary files differindex 15a3a213d..15a3a213d 100644 --- a/static/img/lucumt.info.png +++ b/docs/static/img/lucumt.info.png diff --git a/static/img/mariosanchez-tn.jpg b/docs/static/img/mariosanchez-tn.jpg Binary files differindex 75d116c22..75d116c22 100644 --- a/static/img/mariosanchez-tn.jpg +++ b/docs/static/img/mariosanchez-tn.jpg diff --git a/static/img/mayan-edms-tn.png b/docs/static/img/mayan-edms-tn.png Binary files differindex 8feca78e4..8feca78e4 100644 --- a/static/img/mayan-edms-tn.png +++ b/docs/static/img/mayan-edms-tn.png diff --git a/static/img/michaelwhatcott-tn.jpg b/docs/static/img/michaelwhatcott-tn.jpg Binary files differindex 4cb1b5e80..4cb1b5e80 100644 --- a/static/img/michaelwhatcott-tn.jpg +++ b/docs/static/img/michaelwhatcott-tn.jpg diff --git a/static/img/mongodb-eng-tn.png b/docs/static/img/mongodb-eng-tn.png Binary files differindex 6b223e745..6b223e745 100644 --- a/static/img/mongodb-eng-tn.png +++ b/docs/static/img/mongodb-eng-tn.png diff --git a/static/img/mtbhomer-tn.png b/docs/static/img/mtbhomer-tn.png Binary files differindex c53f68792..c53f68792 100644 --- a/static/img/mtbhomer-tn.png +++ b/docs/static/img/mtbhomer-tn.png diff --git a/static/img/myearworms-tn.jpg b/docs/static/img/myearworms-tn.jpg Binary files differindex 49992889b..49992889b 100644 --- a/static/img/myearworms-tn.jpg +++ b/docs/static/img/myearworms-tn.jpg diff --git a/static/img/neavey-tn.jpg b/docs/static/img/neavey-tn.jpg Binary files differindex 6f81fcb69..6f81fcb69 100644 --- a/static/img/neavey-tn.jpg +++ b/docs/static/img/neavey-tn.jpg diff --git a/static/img/nickoneill-tn.jpg b/docs/static/img/nickoneill-tn.jpg Binary files differindex b8f1d1ae5..b8f1d1ae5 100644 --- a/static/img/nickoneill-tn.jpg +++ b/docs/static/img/nickoneill-tn.jpg diff --git a/static/img/ninjaducks-tn.png b/docs/static/img/ninjaducks-tn.png Binary files differindex dd70268be..dd70268be 100644 --- a/static/img/ninjaducks-tn.png +++ b/docs/static/img/ninjaducks-tn.png diff --git a/static/img/ninya-tn.jpg b/docs/static/img/ninya-tn.jpg Binary files differindex 06bba8083..06bba8083 100644 --- a/static/img/ninya-tn.jpg +++ b/docs/static/img/ninya-tn.jpg diff --git a/static/img/nodesk-tn.png b/docs/static/img/nodesk-tn.png Binary files differindex 76457d994..76457d994 100644 --- a/static/img/nodesk-tn.png +++ b/docs/static/img/nodesk-tn.png diff --git a/static/img/novelist-xyz.png b/docs/static/img/novelist-xyz.png Binary files differindex c2ebed74e..c2ebed74e 100644 --- a/static/img/novelist-xyz.png +++ b/docs/static/img/novelist-xyz.png diff --git a/static/img/npf-tn.jpg b/docs/static/img/npf-tn.jpg Binary files differindex d3eba9c8d..d3eba9c8d 100644 --- a/static/img/npf-tn.jpg +++ b/docs/static/img/npf-tn.jpg diff --git a/static/img/nutspubcrawl.jpg b/docs/static/img/nutspubcrawl.jpg Binary files differindex 34862b5a2..34862b5a2 100644 --- a/static/img/nutspubcrawl.jpg +++ b/docs/static/img/nutspubcrawl.jpg diff --git a/static/img/ocul-maps.png b/docs/static/img/ocul-maps.png Binary files differindex 298d55ecd..298d55ecd 100644 --- a/static/img/ocul-maps.png +++ b/docs/static/img/ocul-maps.png diff --git a/static/img/petanikode.png b/docs/static/img/petanikode.png Binary files differindex 3935a5c96..3935a5c96 100644 --- a/static/img/petanikode.png +++ b/docs/static/img/petanikode.png diff --git a/static/img/peteraba-tn.jpg b/docs/static/img/peteraba-tn.jpg Binary files differindex f30d3f042..f30d3f042 100644 --- a/static/img/peteraba-tn.jpg +++ b/docs/static/img/peteraba-tn.jpg diff --git a/static/img/picturingjordan-tn.png b/docs/static/img/picturingjordan-tn.png Binary files differindex 75e6ea115..75e6ea115 100644 --- a/static/img/picturingjordan-tn.png +++ b/docs/static/img/picturingjordan-tn.png diff --git a/static/img/promotive.png b/docs/static/img/promotive.png Binary files differindex 9f3f6209f..9f3f6209f 100644 --- a/static/img/promotive.png +++ b/docs/static/img/promotive.png diff --git a/static/img/quickstart/bookshelf-bleak-theme.png b/docs/static/img/quickstart/bookshelf-bleak-theme.png Binary files differindex ccd18c42d..ccd18c42d 100644 --- a/static/img/quickstart/bookshelf-bleak-theme.png +++ b/docs/static/img/quickstart/bookshelf-bleak-theme.png diff --git a/static/img/quickstart/bookshelf-disqus.png b/docs/static/img/quickstart/bookshelf-disqus.png Binary files differindex 3ce645a0c..3ce645a0c 100644 --- a/static/img/quickstart/bookshelf-disqus.png +++ b/docs/static/img/quickstart/bookshelf-disqus.png diff --git a/static/img/quickstart/bookshelf-new-default-image.png b/docs/static/img/quickstart/bookshelf-new-default-image.png Binary files differindex d7274c7a6..d7274c7a6 100644 --- a/static/img/quickstart/bookshelf-new-default-image.png +++ b/docs/static/img/quickstart/bookshelf-new-default-image.png diff --git a/static/img/quickstart/bookshelf-only-picture.png b/docs/static/img/quickstart/bookshelf-only-picture.png Binary files differindex a363383bc..a363383bc 100644 --- a/static/img/quickstart/bookshelf-only-picture.png +++ b/docs/static/img/quickstart/bookshelf-only-picture.png diff --git a/static/img/quickstart/bookshelf-robust-theme.png b/docs/static/img/quickstart/bookshelf-robust-theme.png Binary files differindex 7c5e6b8d2..7c5e6b8d2 100644 --- a/static/img/quickstart/bookshelf-robust-theme.png +++ b/docs/static/img/quickstart/bookshelf-robust-theme.png diff --git a/static/img/quickstart/bookshelf-updated-config.png b/docs/static/img/quickstart/bookshelf-updated-config.png Binary files differindex bbda606c7..bbda606c7 100644 --- a/static/img/quickstart/bookshelf-updated-config.png +++ b/docs/static/img/quickstart/bookshelf-updated-config.png diff --git a/static/img/quickstart/bookshelf.png b/docs/static/img/quickstart/bookshelf.png Binary files differindex 3b572adbb..3b572adbb 100644 --- a/static/img/quickstart/bookshelf.png +++ b/docs/static/img/quickstart/bookshelf.png diff --git a/static/img/quickstart/default.jpg b/docs/static/img/quickstart/default.jpg Binary files differindex 78d7bd28e..78d7bd28e 100644 --- a/static/img/quickstart/default.jpg +++ b/docs/static/img/quickstart/default.jpg diff --git a/static/img/rahulrai_in-tn.png b/docs/static/img/rahulrai_in-tn.png Binary files differindex cd146dce5..cd146dce5 100644 --- a/static/img/rahulrai_in-tn.png +++ b/docs/static/img/rahulrai_in-tn.png diff --git a/static/img/rakutentech-tn.png b/docs/static/img/rakutentech-tn.png Binary files differindex 04f56e314..04f56e314 100644 --- a/static/img/rakutentech-tn.png +++ b/docs/static/img/rakutentech-tn.png diff --git a/static/img/rdegges-tn.png b/docs/static/img/rdegges-tn.png Binary files differindex a2e4b6c86..a2e4b6c86 100644 --- a/static/img/rdegges-tn.png +++ b/docs/static/img/rdegges-tn.png diff --git a/static/img/readtext-tn.png b/docs/static/img/readtext-tn.png Binary files differindex 9e71627b0..9e71627b0 100644 --- a/static/img/readtext-tn.png +++ b/docs/static/img/readtext-tn.png diff --git a/static/img/richardsumilang-tn.png b/docs/static/img/richardsumilang-tn.png Binary files differindex 68815495f..68815495f 100644 --- a/static/img/richardsumilang-tn.png +++ b/docs/static/img/richardsumilang-tn.png diff --git a/static/img/rick_cogley_info-tn.jpg b/docs/static/img/rick_cogley_info-tn.jpg Binary files differindex 414e0108c..414e0108c 100644 --- a/static/img/rick_cogley_info-tn.jpg +++ b/docs/static/img/rick_cogley_info-tn.jpg diff --git a/static/img/ridingbytes-tn.png b/docs/static/img/ridingbytes-tn.png Binary files differindex 624cab96d..624cab96d 100644 --- a/static/img/ridingbytes-tn.png +++ b/docs/static/img/ridingbytes-tn.png diff --git a/static/img/robertbasic-tn.png b/docs/static/img/robertbasic-tn.png Binary files differindex 5ceecfead..5ceecfead 100644 --- a/static/img/robertbasic-tn.png +++ b/docs/static/img/robertbasic-tn.png diff --git a/static/img/sanjay-saxena-tn.png b/docs/static/img/sanjay-saxena-tn.png Binary files differindex 85bc5cc58..85bc5cc58 100644 --- a/static/img/sanjay-saxena-tn.png +++ b/docs/static/img/sanjay-saxena-tn.png diff --git a/static/img/scottcwilson-tn.png b/docs/static/img/scottcwilson-tn.png Binary files differindex 5517edf22..5517edf22 100644 --- a/static/img/scottcwilson-tn.png +++ b/docs/static/img/scottcwilson-tn.png diff --git a/static/img/shapeshed-tn.png b/docs/static/img/shapeshed-tn.png Binary files differindex 218b96c5e..218b96c5e 100644 --- a/static/img/shapeshed-tn.png +++ b/docs/static/img/shapeshed-tn.png diff --git a/static/img/shelan-tn.png b/docs/static/img/shelan-tn.png Binary files differindex 0f7634041..0f7634041 100644 --- a/static/img/shelan-tn.png +++ b/docs/static/img/shelan-tn.png diff --git a/static/img/siba-tn.png b/docs/static/img/siba-tn.png Binary files differindex 52373df20..52373df20 100644 --- a/static/img/siba-tn.png +++ b/docs/static/img/siba-tn.png diff --git a/static/img/silvergeko.jpg b/docs/static/img/silvergeko.jpg Binary files differindex 19b8f98cb..19b8f98cb 100644 --- a/static/img/silvergeko.jpg +++ b/docs/static/img/silvergeko.jpg diff --git a/static/img/softinio-tn.png b/docs/static/img/softinio-tn.png Binary files differindex ad94ae876..ad94ae876 100644 --- a/static/img/softinio-tn.png +++ b/docs/static/img/softinio-tn.png diff --git a/static/img/spf13-tn.jpg b/docs/static/img/spf13-tn.jpg Binary files differindex a987c71f9..a987c71f9 100644 --- a/static/img/spf13-tn.jpg +++ b/docs/static/img/spf13-tn.jpg diff --git a/static/img/steambap.png b/docs/static/img/steambap.png Binary files differindex bad21f438..bad21f438 100644 --- a/static/img/steambap.png +++ b/docs/static/img/steambap.png diff --git a/static/img/stefano.chiodino-tn.jpg b/docs/static/img/stefano.chiodino-tn.jpg Binary files differindex 7747798d5..7747798d5 100644 --- a/static/img/stefano.chiodino-tn.jpg +++ b/docs/static/img/stefano.chiodino-tn.jpg diff --git a/static/img/stou-tn.png b/docs/static/img/stou-tn.png Binary files differindex fe449b797..fe449b797 100644 --- a/static/img/stou-tn.png +++ b/docs/static/img/stou-tn.png diff --git a/static/img/szymonkatra-tn.png b/docs/static/img/szymonkatra-tn.png Binary files differindex e64f2b33d..e64f2b33d 100644 --- a/static/img/szymonkatra-tn.png +++ b/docs/static/img/szymonkatra-tn.png diff --git a/static/img/techmadeplain-tn.jpg b/docs/static/img/techmadeplain-tn.jpg Binary files differindex cae544861..cae544861 100644 --- a/static/img/techmadeplain-tn.jpg +++ b/docs/static/img/techmadeplain-tn.jpg diff --git a/static/img/tendermint-tn.jpg b/docs/static/img/tendermint-tn.jpg Binary files differindex 807b42d74..807b42d74 100644 --- a/static/img/tendermint-tn.jpg +++ b/docs/static/img/tendermint-tn.jpg diff --git a/static/img/thecodeking-tn.jpg b/docs/static/img/thecodeking-tn.jpg Binary files differindex 158384d4e..158384d4e 100644 --- a/static/img/thecodeking-tn.jpg +++ b/docs/static/img/thecodeking-tn.jpg diff --git a/static/img/thehome-tn.png b/docs/static/img/thehome-tn.png Binary files differindex 7b66e6215..7b66e6215 100644 --- a/static/img/thehome-tn.png +++ b/docs/static/img/thehome-tn.png diff --git a/static/img/thislittleduck-tn.png b/docs/static/img/thislittleduck-tn.png Binary files differindex 0d7407d62..0d7407d62 100644 --- a/static/img/thislittleduck-tn.png +++ b/docs/static/img/thislittleduck-tn.png diff --git a/static/img/tibobeijen-nl-tn.png b/docs/static/img/tibobeijen-nl-tn.png Binary files differindex 801e34a12..801e34a12 100644 --- a/static/img/tibobeijen-nl-tn.png +++ b/docs/static/img/tibobeijen-nl-tn.png diff --git a/static/img/ttsreader-tn.png b/docs/static/img/ttsreader-tn.png Binary files differindex db33322b4..db33322b4 100644 --- a/static/img/ttsreader-tn.png +++ b/docs/static/img/ttsreader-tn.png diff --git a/static/img/tutorialonfly-tn.jpg b/docs/static/img/tutorialonfly-tn.jpg Binary files differindex 5ff99fff6..5ff99fff6 100644 --- a/static/img/tutorialonfly-tn.jpg +++ b/docs/static/img/tutorialonfly-tn.jpg diff --git a/static/img/tutorials/automated-deployments/adding-a-deploy-pipeline.png b/docs/static/img/tutorials/automated-deployments/adding-a-deploy-pipeline.png Binary files differindex 29f637b06..29f637b06 100644 --- a/static/img/tutorials/automated-deployments/adding-a-deploy-pipeline.png +++ b/docs/static/img/tutorials/automated-deployments/adding-a-deploy-pipeline.png diff --git a/static/img/tutorials/automated-deployments/adding-a-deploy-step.png b/docs/static/img/tutorials/automated-deployments/adding-a-deploy-step.png Binary files differindex 346187927..346187927 100644 --- a/static/img/tutorials/automated-deployments/adding-a-deploy-step.png +++ b/docs/static/img/tutorials/automated-deployments/adding-a-deploy-step.png diff --git a/static/img/tutorials/automated-deployments/adding-the-project-to-github.png b/docs/static/img/tutorials/automated-deployments/adding-the-project-to-github.png Binary files differindex e1065bb00..e1065bb00 100644 --- a/static/img/tutorials/automated-deployments/adding-the-project-to-github.png +++ b/docs/static/img/tutorials/automated-deployments/adding-the-project-to-github.png diff --git a/static/img/tutorials/automated-deployments/creating-a-basic-hugo-site.png b/docs/static/img/tutorials/automated-deployments/creating-a-basic-hugo-site.png Binary files differindex 78d238f88..78d238f88 100644 --- a/static/img/tutorials/automated-deployments/creating-a-basic-hugo-site.png +++ b/docs/static/img/tutorials/automated-deployments/creating-a-basic-hugo-site.png diff --git a/static/img/tutorials/automated-deployments/public-or-not.png b/docs/static/img/tutorials/automated-deployments/public-or-not.png Binary files differindex 9d81a8ba4..9d81a8ba4 100644 --- a/static/img/tutorials/automated-deployments/public-or-not.png +++ b/docs/static/img/tutorials/automated-deployments/public-or-not.png diff --git a/static/img/tutorials/automated-deployments/using-hugo-build.png b/docs/static/img/tutorials/automated-deployments/using-hugo-build.png Binary files differindex b0dbec94c..b0dbec94c 100644 --- a/static/img/tutorials/automated-deployments/using-hugo-build.png +++ b/docs/static/img/tutorials/automated-deployments/using-hugo-build.png diff --git a/static/img/tutorials/automated-deployments/wercker-access.png b/docs/static/img/tutorials/automated-deployments/wercker-access.png Binary files differindex 6e89c0ef3..6e89c0ef3 100644 --- a/static/img/tutorials/automated-deployments/wercker-access.png +++ b/docs/static/img/tutorials/automated-deployments/wercker-access.png diff --git a/static/img/tutorials/automated-deployments/wercker-add-app.png b/docs/static/img/tutorials/automated-deployments/wercker-add-app.png Binary files differindex 94ccef518..94ccef518 100644 --- a/static/img/tutorials/automated-deployments/wercker-add-app.png +++ b/docs/static/img/tutorials/automated-deployments/wercker-add-app.png diff --git a/static/img/tutorials/automated-deployments/wercker-git-connections.png b/docs/static/img/tutorials/automated-deployments/wercker-git-connections.png Binary files differindex d89c0cd8b..d89c0cd8b 100644 --- a/static/img/tutorials/automated-deployments/wercker-git-connections.png +++ b/docs/static/img/tutorials/automated-deployments/wercker-git-connections.png diff --git a/static/img/tutorials/automated-deployments/wercker-search.png b/docs/static/img/tutorials/automated-deployments/wercker-search.png Binary files differindex d099cfd5c..d099cfd5c 100644 --- a/static/img/tutorials/automated-deployments/wercker-search.png +++ b/docs/static/img/tutorials/automated-deployments/wercker-search.png diff --git a/static/img/tutorials/automated-deployments/wercker-select-repository.png b/docs/static/img/tutorials/automated-deployments/wercker-select-repository.png Binary files differindex 0f7d63d98..0f7d63d98 100644 --- a/static/img/tutorials/automated-deployments/wercker-select-repository.png +++ b/docs/static/img/tutorials/automated-deployments/wercker-select-repository.png diff --git a/static/img/tutorials/automated-deployments/wercker-sign-up-page.png b/docs/static/img/tutorials/automated-deployments/wercker-sign-up-page.png Binary files differindex 55b0ebd52..55b0ebd52 100644 --- a/static/img/tutorials/automated-deployments/wercker-sign-up-page.png +++ b/docs/static/img/tutorials/automated-deployments/wercker-sign-up-page.png diff --git a/static/img/tutorials/automated-deployments/wercker-sign-up.png b/docs/static/img/tutorials/automated-deployments/wercker-sign-up.png Binary files differindex 9c6270061..9c6270061 100644 --- a/static/img/tutorials/automated-deployments/wercker-sign-up.png +++ b/docs/static/img/tutorials/automated-deployments/wercker-sign-up.png diff --git a/static/img/tutorials/automated-deployments/werckeryml.png b/docs/static/img/tutorials/automated-deployments/werckeryml.png Binary files differindex daa392b4a..daa392b4a 100644 --- a/static/img/tutorials/automated-deployments/werckeryml.png +++ b/docs/static/img/tutorials/automated-deployments/werckeryml.png diff --git a/static/img/tutorials/hosting-on-bitbucket/bitbucket-blog-post.png b/docs/static/img/tutorials/hosting-on-bitbucket/bitbucket-blog-post.png Binary files differindex b78f6fd15..b78f6fd15 100644 --- a/static/img/tutorials/hosting-on-bitbucket/bitbucket-blog-post.png +++ b/docs/static/img/tutorials/hosting-on-bitbucket/bitbucket-blog-post.png diff --git a/static/img/tutorials/hosting-on-bitbucket/bitbucket-create-repo.png b/docs/static/img/tutorials/hosting-on-bitbucket/bitbucket-create-repo.png Binary files differindex e97f13465..e97f13465 100644 --- a/static/img/tutorials/hosting-on-bitbucket/bitbucket-create-repo.png +++ b/docs/static/img/tutorials/hosting-on-bitbucket/bitbucket-create-repo.png diff --git a/static/img/tutorials/how-to-contribute-to-hugo/accept-cla.png b/docs/static/img/tutorials/how-to-contribute-to-hugo/accept-cla.png Binary files differindex 929fda6ab..929fda6ab 100644 --- a/static/img/tutorials/how-to-contribute-to-hugo/accept-cla.png +++ b/docs/static/img/tutorials/how-to-contribute-to-hugo/accept-cla.png diff --git a/static/img/tutorials/how-to-contribute-to-hugo/ci-errors.png b/docs/static/img/tutorials/how-to-contribute-to-hugo/ci-errors.png Binary files differindex 95cd290b6..95cd290b6 100644 --- a/static/img/tutorials/how-to-contribute-to-hugo/ci-errors.png +++ b/docs/static/img/tutorials/how-to-contribute-to-hugo/ci-errors.png diff --git a/static/img/tutorials/how-to-contribute-to-hugo/copy-remote-url.png b/docs/static/img/tutorials/how-to-contribute-to-hugo/copy-remote-url.png Binary files differindex 9006f4a48..9006f4a48 100644 --- a/static/img/tutorials/how-to-contribute-to-hugo/copy-remote-url.png +++ b/docs/static/img/tutorials/how-to-contribute-to-hugo/copy-remote-url.png diff --git a/static/img/tutorials/how-to-contribute-to-hugo/forking-a-repository.png b/docs/static/img/tutorials/how-to-contribute-to-hugo/forking-a-repository.png Binary files differindex ea132cab3..ea132cab3 100644 --- a/static/img/tutorials/how-to-contribute-to-hugo/forking-a-repository.png +++ b/docs/static/img/tutorials/how-to-contribute-to-hugo/forking-a-repository.png diff --git a/static/img/tutorials/how-to-contribute-to-hugo/open-pull-request.png b/docs/static/img/tutorials/how-to-contribute-to-hugo/open-pull-request.png Binary files differindex 63b504fb2..63b504fb2 100644 --- a/static/img/tutorials/how-to-contribute-to-hugo/open-pull-request.png +++ b/docs/static/img/tutorials/how-to-contribute-to-hugo/open-pull-request.png diff --git a/static/img/tutswiki-tn.jpg b/docs/static/img/tutswiki-tn.jpg Binary files differindex 11efb2a5b..11efb2a5b 100644 --- a/static/img/tutswiki-tn.jpg +++ b/docs/static/img/tutswiki-tn.jpg diff --git a/static/img/ucsb-tn.jpg b/docs/static/img/ucsb-tn.jpg Binary files differindex 45962027d..45962027d 100644 --- a/static/img/ucsb-tn.jpg +++ b/docs/static/img/ucsb-tn.jpg diff --git a/static/img/upbeat.png b/docs/static/img/upbeat.png Binary files differindex e7a6a694c..e7a6a694c 100644 --- a/static/img/upbeat.png +++ b/docs/static/img/upbeat.png diff --git a/static/img/vamp_landingpage-tn.png b/docs/static/img/vamp_landingpage-tn.png Binary files differindex 474261e0e..474261e0e 100644 --- a/static/img/vamp_landingpage-tn.png +++ b/docs/static/img/vamp_landingpage-tn.png diff --git a/static/img/viglug-tn.png b/docs/static/img/viglug-tn.png Binary files differindex d18ab4023..d18ab4023 100644 --- a/static/img/viglug-tn.png +++ b/docs/static/img/viglug-tn.png diff --git a/static/img/vurt.co-tn.jpg b/docs/static/img/vurt.co-tn.jpg Binary files differindex 5e7f131a0..5e7f131a0 100644 --- a/static/img/vurt.co-tn.jpg +++ b/docs/static/img/vurt.co-tn.jpg diff --git a/static/img/worldtowriters-com.jpg b/docs/static/img/worldtowriters-com.jpg Binary files differindex 570d06fa9..570d06fa9 100644 --- a/static/img/worldtowriters-com.jpg +++ b/docs/static/img/worldtowriters-com.jpg diff --git a/static/img/yslow-rules-tn.png b/docs/static/img/yslow-rules-tn.png Binary files differindex 5c75a6943..5c75a6943 100644 --- a/static/img/yslow-rules-tn.png +++ b/docs/static/img/yslow-rules-tn.png diff --git a/static/img/ysqi-blog.png b/docs/static/img/ysqi-blog.png Binary files differindex 6dd234109..6dd234109 100644 --- a/static/img/ysqi-blog.png +++ b/docs/static/img/ysqi-blog.png diff --git a/static/img/yulinling-tn.jpg b/docs/static/img/yulinling-tn.jpg Binary files differindex bdb12f0e7..bdb12f0e7 100644 --- a/static/img/yulinling-tn.jpg +++ b/docs/static/img/yulinling-tn.jpg diff --git a/static/js/livereload.js b/docs/static/js/livereload.js index f6c3b7f90..f6c3b7f90 100644 --- a/static/js/livereload.js +++ b/docs/static/js/livereload.js diff --git a/static/js/owl.carousel-custom.js b/docs/static/js/owl.carousel-custom.js index 685c8e361..685c8e361 100644 --- a/static/js/owl.carousel-custom.js +++ b/docs/static/js/owl.carousel-custom.js diff --git a/static/js/scripts.js b/docs/static/js/scripts.js index ef6074f55..ef6074f55 100644 --- a/static/js/scripts.js +++ b/docs/static/js/scripts.js diff --git a/static/share/hugo-tall.png b/docs/static/share/hugo-tall.png Binary files differindex 001ce5eb3..001ce5eb3 100644 --- a/static/share/hugo-tall.png +++ b/docs/static/share/hugo-tall.png diff --git a/static/share/made-with-hugo-dark.png b/docs/static/share/made-with-hugo-dark.png Binary files differindex c6cadf283..c6cadf283 100644 --- a/static/share/made-with-hugo-dark.png +++ b/docs/static/share/made-with-hugo-dark.png diff --git a/static/share/made-with-hugo-long-dark.png b/docs/static/share/made-with-hugo-long-dark.png Binary files differindex 1e49995fb..1e49995fb 100644 --- a/static/share/made-with-hugo-long-dark.png +++ b/docs/static/share/made-with-hugo-long-dark.png diff --git a/static/share/made-with-hugo-long.png b/docs/static/share/made-with-hugo-long.png Binary files differindex c5df534cf..c5df534cf 100644 --- a/static/share/made-with-hugo-long.png +++ b/docs/static/share/made-with-hugo-long.png diff --git a/static/share/made-with-hugo.png b/docs/static/share/made-with-hugo.png Binary files differindex 52dfd19e5..52dfd19e5 100644 --- a/static/share/made-with-hugo.png +++ b/docs/static/share/made-with-hugo.png diff --git a/static/share/powered-by-hugo-dark.png b/docs/static/share/powered-by-hugo-dark.png Binary files differindex a8e2ebc80..a8e2ebc80 100644 --- a/static/share/powered-by-hugo-dark.png +++ b/docs/static/share/powered-by-hugo-dark.png diff --git a/static/share/powered-by-hugo-long-dark.png b/docs/static/share/powered-by-hugo-long-dark.png Binary files differindex 1b760b1bf..1b760b1bf 100644 --- a/static/share/powered-by-hugo-long-dark.png +++ b/docs/static/share/powered-by-hugo-long-dark.png diff --git a/static/share/powered-by-hugo-long.png b/docs/static/share/powered-by-hugo-long.png Binary files differindex 37131359d..37131359d 100644 --- a/static/share/powered-by-hugo-long.png +++ b/docs/static/share/powered-by-hugo-long.png diff --git a/static/share/powered-by-hugo.png b/docs/static/share/powered-by-hugo.png Binary files differindex 27ff099d5..27ff099d5 100644 --- a/static/share/powered-by-hugo.png +++ b/docs/static/share/powered-by-hugo.png diff --git a/static/vendor/OwlCarousel2/LICENSE b/docs/static/vendor/OwlCarousel2/LICENSE index 7162d578b..7162d578b 100644 --- a/static/vendor/OwlCarousel2/LICENSE +++ b/docs/static/vendor/OwlCarousel2/LICENSE diff --git a/static/vendor/OwlCarousel2/css/owl.carousel.css b/docs/static/vendor/OwlCarousel2/css/owl.carousel.css index aaf80dd13..aaf80dd13 100644 --- a/static/vendor/OwlCarousel2/css/owl.carousel.css +++ b/docs/static/vendor/OwlCarousel2/css/owl.carousel.css diff --git a/static/vendor/OwlCarousel2/css/owl.theme.default.css b/docs/static/vendor/OwlCarousel2/css/owl.theme.default.css index dcd4c82ae..dcd4c82ae 100644 --- a/static/vendor/OwlCarousel2/css/owl.theme.default.css +++ b/docs/static/vendor/OwlCarousel2/css/owl.theme.default.css diff --git a/static/vendor/OwlCarousel2/js/owl.carousel.min.js b/docs/static/vendor/OwlCarousel2/js/owl.carousel.min.js index cd327896d..cd327896d 100644 --- a/static/vendor/OwlCarousel2/js/owl.carousel.min.js +++ b/docs/static/vendor/OwlCarousel2/js/owl.carousel.min.js diff --git a/static/vendor/OwlCarousel2/notes.txt b/docs/static/vendor/OwlCarousel2/notes.txt index eeb8f7a89..eeb8f7a89 100644 --- a/static/vendor/OwlCarousel2/notes.txt +++ b/docs/static/vendor/OwlCarousel2/notes.txt diff --git a/static/vendor/dieulot/js/instantclick.min.js b/docs/static/vendor/dieulot/js/instantclick.min.js index a2539d884..a2539d884 100644 --- a/static/vendor/dieulot/js/instantclick.min.js +++ b/docs/static/vendor/dieulot/js/instantclick.min.js diff --git a/static/vendor/flesler/js/jquery.scrollTo.min.js b/docs/static/vendor/flesler/js/jquery.scrollTo.min.js index 65a020d92..65a020d92 100644 --- a/static/vendor/flesler/js/jquery.scrollTo.min.js +++ b/docs/static/vendor/flesler/js/jquery.scrollTo.min.js diff --git a/static/vendor/font-awesome/css/font-awesome.min.css b/docs/static/vendor/font-awesome/css/font-awesome.min.css index d0603cb4b..d0603cb4b 100644 --- a/static/vendor/font-awesome/css/font-awesome.min.css +++ b/docs/static/vendor/font-awesome/css/font-awesome.min.css diff --git a/static/vendor/font-awesome/fonts/FontAwesome.otf b/docs/static/vendor/font-awesome/fonts/FontAwesome.otf Binary files differindex 3ed7f8b48..3ed7f8b48 100644 --- a/static/vendor/font-awesome/fonts/FontAwesome.otf +++ b/docs/static/vendor/font-awesome/fonts/FontAwesome.otf diff --git a/static/vendor/font-awesome/fonts/fontawesome-webfont.eot b/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.eot Binary files differindex 9b6afaedc..9b6afaedc 100644 --- a/static/vendor/font-awesome/fonts/fontawesome-webfont.eot +++ b/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.eot diff --git a/static/vendor/font-awesome/fonts/fontawesome-webfont.svg b/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.svg index d05688e9e..d05688e9e 100644 --- a/static/vendor/font-awesome/fonts/fontawesome-webfont.svg +++ b/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.svg diff --git a/static/vendor/font-awesome/fonts/fontawesome-webfont.ttf b/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.ttf Binary files differindex 26dea7951..26dea7951 100644 --- a/static/vendor/font-awesome/fonts/fontawesome-webfont.ttf +++ b/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.ttf diff --git a/static/vendor/font-awesome/fonts/fontawesome-webfont.woff b/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.woff Binary files differindex dc35ce3c2..dc35ce3c2 100644 --- a/static/vendor/font-awesome/fonts/fontawesome-webfont.woff +++ b/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.woff diff --git a/static/vendor/font-awesome/fonts/fontawesome-webfont.woff2 b/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.woff2 Binary files differindex 500e51725..500e51725 100644 --- a/static/vendor/font-awesome/fonts/fontawesome-webfont.woff2 +++ b/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.woff2 diff --git a/static/vendor/highlightjs/css/monokai-sublime.css b/docs/static/vendor/highlightjs/css/monokai-sublime.css index 2864170da..2864170da 100644 --- a/static/vendor/highlightjs/css/monokai-sublime.css +++ b/docs/static/vendor/highlightjs/css/monokai-sublime.css diff --git a/static/vendor/highlightjs/js/highlight.pack.js b/docs/static/vendor/highlightjs/js/highlight.pack.js index ab4712ecf..ab4712ecf 100644 --- a/static/vendor/highlightjs/js/highlight.pack.js +++ b/docs/static/vendor/highlightjs/js/highlight.pack.js diff --git a/static/vendor/highlightjs/notes.txt b/docs/static/vendor/highlightjs/notes.txt index 43475e5f9..43475e5f9 100644 --- a/static/vendor/highlightjs/notes.txt +++ b/docs/static/vendor/highlightjs/notes.txt diff --git a/static/vendor/jquery/js/jquery-2.1.4.min.js b/docs/static/vendor/jquery/js/jquery-2.1.4.min.js index 49990d6e1..49990d6e1 100644 --- a/static/vendor/jquery/js/jquery-2.1.4.min.js +++ b/docs/static/vendor/jquery/js/jquery-2.1.4.min.js diff --git a/static/vendor/twitter/js/bootstrap.min.js b/docs/static/vendor/twitter/js/bootstrap.min.js index e79c06513..e79c06513 100644 --- a/static/vendor/twitter/js/bootstrap.min.js +++ b/docs/static/vendor/twitter/js/bootstrap.min.js diff --git a/temp/0.22.1-relnotes.md b/docs/temp/0.22.1-relnotes.md index 994e6c4ac..994e6c4ac 100644 --- a/temp/0.22.1-relnotes.md +++ b/docs/temp/0.22.1-relnotes.md diff --git a/docshelper/docs.go b/docshelper/docs.go new file mode 100644 index 000000000..3de350f61 --- /dev/null +++ b/docshelper/docs.go @@ -0,0 +1,32 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package docshelper provides some helpers for the Hugo documentation, and +// is of limited interest for the general Hugo user. +package docshelper + +import ( + "encoding/json" +) + +var DocProviders = make(map[string]DocProvider) + +func AddDocProvider(name string, provider DocProvider) { + DocProviders[name] = provider +} + +type DocProvider func() map[string]interface{} + +func (d DocProvider) MarshalJSON() ([]byte, error) { + return json.MarshalIndent(d(), "", " ") +} diff --git a/examples/blog/.gitignore b/examples/blog/.gitignore new file mode 100644 index 000000000..958340e12 --- /dev/null +++ b/examples/blog/.gitignore @@ -0,0 +1,12 @@ +# Hugo default output directory +/public + +## OS Files +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ + +# OSX +.DS_Store diff --git a/examples/blog/README.md b/examples/blog/README.md new file mode 100644 index 000000000..cfdff3c91 --- /dev/null +++ b/examples/blog/README.md @@ -0,0 +1,42 @@ +Hugo Example Blog +================= + +This repository provides a fully-working example of a [Hugo](https://github.com/gohugoio/hugo)-powered blog. Many +Hugo-specific features are used as a way to see them in action, and hopefully ease the learning curve for creating your +very own site with Hugo. + +Features +-------- + +- Recent Posts at main index +- Indexes for `tags` and `categories` +- Post information block, with links for all `tags` and `categories` post belongs to +- [Bootstrap 3](http://getbootstrap.com/) ready + - Currently using the [Yeti](http://bootswatch.com/yeti/) theme from http://bootswatch.com/ + +Common things that should be added in the near future *(pull requests are welcome!)*: + +- Disqus integration +- More content types to demonstrate different layout methods + - About Me + - Contact + +Getting Started +--------------- + +To get started, you should simply fork or clone this repository! That's definitely an important first step. + +[Install Hugo](http://gohugo.io/overview/installing) in a way that best suits your environment and comfort level. + +Edit `config.toml` and change the default properties to suit your own information. This is not required to run the +example, but this is the global configuration file and you're going to need to use it eventually. Start here! + +In a command prompt or terminal, navigate to the path that contains your `config.toml` file and run `hugo`. That's it! +You should now have a `public` directory with a complete blog! Open `public/index.html` in your browser and bask. + +If that wasn't amazing enough, from the same terminal, run `hugo server`. This will watch your directories for changes +and rebuild the site immediately, *and* it will make these changes available at http://localhost:1313/ so you can view +your finished site in your browser. Go on, try it. This is one of the best ways to preview your site while working on it. + +To further learn Hugo and learn more, read through the Hugo [documentation](http://gohugo.io/overview/introduction) +or browse around the files in this repository. Have fun! diff --git a/examples/blog/config.toml b/examples/blog/config.toml new file mode 100644 index 000000000..b402f2c7f --- /dev/null +++ b/examples/blog/config.toml @@ -0,0 +1,4 @@ +baseURL = "http://blog.hugoexample.com/" +languageCode = "en-us" +title = "Hugo Example Blog" +canonifyURLs = true diff --git a/examples/blog/content/post/another-post.md b/examples/blog/content/post/another-post.md new file mode 100644 index 000000000..057c2d27b --- /dev/null +++ b/examples/blog/content/post/another-post.md @@ -0,0 +1,57 @@ ++++ +title = "Another Hugo Post" +description = "Nothing special, but one post is boring." +date = "2014-09-02" +categories = [ "example", "configuration" ] +tags = [ + "example", + "hugo", + "toml" +] ++++ + +TOML, YAML, JSON --- Oh my! +------------------------- + +One of the nifty Hugo features we should cover: flexible configuration and front matter formats! This entry has front +matter in `toml`, unlike the last one which used `yaml`, and `json` is also available if that's your preference. + +<!--more--> + +The `toml` front matter used on this entry: + +``` ++++ +title = "Another Hugo Post" +description = "Nothing special, but one post is boring." +date = "2014-09-02" +categories = [ "example", "configuration" ] +tags = [ + "example", + "hugo", + "toml" +] ++++ +``` + +This flexibility also extends to your site's global configuration file. You're free to use any format you prefer::simply +name the file `config.yaml`, `config.toml` or `config.json`, and go on your merry way. + +JSON Example +------------ + +How would this entry's front matter look in `json`? That's easy enough to demonstrate: + +``` +{ + "title": "Another Hugo Post", + "description": "Nothing special, but one post is boring.", + "date": "2014-09-02", + "categories": [ "example", "configuration" ], + "tags": [ + "example", + "hugo", + "toml" + ], +} +``` diff --git a/examples/blog/content/post/hello-hugo.md b/examples/blog/content/post/hello-hugo.md new file mode 100644 index 000000000..f58886ee8 --- /dev/null +++ b/examples/blog/content/post/hello-hugo.md @@ -0,0 +1,61 @@ +--- +title: "Hello Hugo!" +description: "Saying 'Hello' from Hugo" +date: "2014-09-01" +categories: + - "example" + - "hello" +tags: + - "example" + - "hugo" + - "blog" +--- + +Hello from Hugo! If you're reading this in your browser, good job! The file `content/post/hello-hugo.md` has been +converted into a complete HTML document by Hugo. Isn't that pretty nifty? + +A Section +--------- + +Here's a simple titled section where you can place whatever information you want. + +You can use inline HTML if you want, but really there's not much that Markdown can't do. + +Showing off with Markdown +------------------------- + +A full cheat sheet can be found [here](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) +or through [Google](https://google.com/). + +There are some *easy* examples for styling, though. I can't **emphasize** that enough. +Creating [links](https://google.com/) or `inline code` blocks are very straightforward. + +``` +There are some *easy* examples for styling, though. I can't **emphasize** that enough. +Creating [links](https://google.com/) or `inline code` blocks are very straightforward. +``` + +Front Matter for Fun +-------------------- + +This is the meta data for this post. It is located at the top of the `content/post/hello-hugo.md` markdown file. + +``` +--- +title: "Hello Hugo!" +description: "Saying 'Hello' from Hugo" +date: "2014-09-01" +categories: + - "example" + - "hello" +tags: + - "example" + - "hugo" + - "blog" +--- +``` + +This section, called 'Front Matter', is what tells Hugo about the content in this file: the `title` of the item, the +`description`, and the `date` it was posted. In our example, we've added two custom bits of data too. The `categories` and +`tags` sections are used in this example for indexing/grouping content. You will learn more about what that means by +examining the code in this example and through reading the Hugo [documentation](http://gohugo.io/overview/introduction). diff --git a/examples/blog/layouts/_default/single.html b/examples/blog/layouts/_default/single.html new file mode 100644 index 000000000..13a53f666 --- /dev/null +++ b/examples/blog/layouts/_default/single.html @@ -0,0 +1,21 @@ +{{ partial "header.html" . }} +<body> +{{ partial "navbar.html" . }} +<div class="container"> + <div class="row"> + <div class="col-md-9"> + <div class="well well-sm"> + <h3>{{ .Title }}<br> <small>{{ .Description }}</small></h3> + <hr> + {{ .Content }} + </div> + </div> + + <!-- Sidebar --> + <div class="col-md-3"> + {{ partial "menu.html" . }} + </div> + </div> +{{ partial "footer.copyright.html" . }} +</div> +{{ partial "footer.html" . }} diff --git a/examples/blog/layouts/index.html b/examples/blog/layouts/index.html new file mode 100644 index 000000000..a69100409 --- /dev/null +++ b/examples/blog/layouts/index.html @@ -0,0 +1,19 @@ +{{ partial "header.html" . }} +<body> +{{ partial "navbar.html" . }} +<div class="container"> + <div class="row"> + <div class="col-md-9"> + {{ range first 10 .Data.Pages }} + {{ .Render "summary" }} + {{ end }} + </div> + + <!-- Sidebar --> + <div class="col-md-3"> + {{ partial "menu.html" . }} + </div> + </div> +{{ partial "footer.copyright.html" . }} +</div> +{{ partial "footer.html" . }} diff --git a/examples/blog/layouts/indexes/category.html b/examples/blog/layouts/indexes/category.html new file mode 100644 index 000000000..653d81964 --- /dev/null +++ b/examples/blog/layouts/indexes/category.html @@ -0,0 +1,24 @@ +{{ partial "header.html" . }} +<body> +{{ partial "navbar.html" . }} +<div class="container"> + <div class="row"> + <div class="col-md-9"> + <div class="well well-sm"> + <strong>Items in category <code>{{ .Title | lower }}</code></strong> + <ul class="list-unstyled"> + {{ range .Data.Pages }} + {{ .Render "li" }} + {{ end}} + </ul> + </div> + </div> + + <!-- Sidebar --> + <div class="col-md-3"> + {{ partial "menu.html" . }} + </div> + </div> +{{ partial "footer.copyright.html" . }} +</div> +{{ partial "footer.html" . }} diff --git a/examples/blog/layouts/indexes/post.html b/examples/blog/layouts/indexes/post.html new file mode 100644 index 000000000..b3a835ccd --- /dev/null +++ b/examples/blog/layouts/indexes/post.html @@ -0,0 +1,24 @@ +{{ partial "header.html" . }} +<body> +{{ partial "navbar.html" . }} +<div class="container"> + <div class="row"> + <div class="col-md-9"> + <div class="well well-sm"> + <strong>Blog Post Archive</strong> + <ul class="list-unstyled"> + {{ range .Data.Pages }} + {{ .Render "li" }} + {{ end}} + </ul> + </div> + </div> + + <!-- Sidebar --> + <div class="col-md-3"> + {{ partial "menu.html" . }} + </div> + </div> +{{ partial "footer.copyright.html" . }} +</div> +{{ partial "footer.html" . }} diff --git a/examples/blog/layouts/indexes/tag.html b/examples/blog/layouts/indexes/tag.html new file mode 100644 index 000000000..f59b76715 --- /dev/null +++ b/examples/blog/layouts/indexes/tag.html @@ -0,0 +1,24 @@ +{{ partial "header.html" . }} +<body> +{{ partial "navbar.html" . }} +<div class="container"> + <div class="row"> + <div class="col-md-9"> + <div class="well well-sm"> + <strong>Items with tag <code>{{ .Title | lower }}</code></strong> + <ul class="list-unstyled"> + {{ range .Data.Pages }} + {{ .Render "li" }} + {{ end}} + </ul> + </div> + </div> + + <!-- Sidebar --> + <div class="col-md-3"> + {{ partial "menu.html" . }} + </div> + </div> +{{ partial "footer.copyright.html" . }} +</div> +{{ partial "footer.html" . }} diff --git a/examples/blog/layouts/partials/footer.copyright.html b/examples/blog/layouts/partials/footer.copyright.html new file mode 100644 index 000000000..64c9353ea --- /dev/null +++ b/examples/blog/layouts/partials/footer.copyright.html @@ -0,0 +1,9 @@ + <footer> + <div class="row"> + <hr> + <div class="col-sm-12"> + <p>© Enthusiastic Hugo User {{ .Now.Format "2006" }} · + Built with <a href="https://github.com/gohugoio/hugo">Hugo</a></p> + </div> + </div> + </footer>
\ No newline at end of file diff --git a/examples/blog/layouts/partials/footer.html b/examples/blog/layouts/partials/footer.html new file mode 100644 index 000000000..8945fa4ed --- /dev/null +++ b/examples/blog/layouts/partials/footer.html @@ -0,0 +1,5 @@ + + <script src="/js/jquery-1.11.3.min.js"></script> + <script src="/js/bootstrap.js"></script> +</body> +</html> diff --git a/examples/blog/layouts/partials/header.html b/examples/blog/layouts/partials/header.html new file mode 100644 index 000000000..24500a483 --- /dev/null +++ b/examples/blog/layouts/partials/header.html @@ -0,0 +1,10 @@ +<!doctype html> +<html lang="en"> +<head> + {{ partial "meta.html" . }} + + <title>{{ .Title }} - {{ .Site.BaseURL }}</title> + <link rel="canonical" href="{{ .Permalink }}"> + {{ partial "header.includes.html" . }} + {{ if .RSSLink }}<link href="{{ .RSSLink }}" rel="alternate" type="application/rss+xml" title="{{ .Site.Title }}" />{{ end }} +</head> diff --git a/examples/blog/layouts/partials/header.includes.html b/examples/blog/layouts/partials/header.includes.html new file mode 100644 index 000000000..767e3eee1 --- /dev/null +++ b/examples/blog/layouts/partials/header.includes.html @@ -0,0 +1,4 @@ + + <link href="/css/bootstrap.min.css" rel="stylesheet"> + <link href="/css/font-awesome.css" rel="stylesheet"> + <link href="/css/custom.css" rel="stylesheet"> diff --git a/examples/blog/layouts/partials/menu.html b/examples/blog/layouts/partials/menu.html new file mode 100644 index 000000000..61ce0c6b5 --- /dev/null +++ b/examples/blog/layouts/partials/menu.html @@ -0,0 +1,15 @@ + <div class="panel panel-default"> + <div class="panel-heading" style="padding: 2px 15px;"> + <h4>Connect. Socialize.</h4> + </div> + <div class="panel-body"> + <a href="https://github.com/SomeSillyUserNameHere/" class="btn btn-primary btn-xs"><i class="fa fa-github-square fa-2x"></i></a> + <a href="https://www.facebook.com/SomeSillyUserNameHere" class="btn btn-info btn-xs"><i class="fa fa-facebook-square fa-2x"></i></a> + + <div class="alert alert-info alert-dismissable" style="margin-top:25px;margin-bottom:5px;"> + <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> + <strong>Hey, listen!</strong><br> + You should modify the <code>layouts/partials/menu.html</code> template and include your own profile links. + </div> + </div> + </div> diff --git a/examples/blog/layouts/partials/meta.html b/examples/blog/layouts/partials/meta.html new file mode 100644 index 000000000..95fd2a711 --- /dev/null +++ b/examples/blog/layouts/partials/meta.html @@ -0,0 +1,6 @@ + + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="{{ .Description }}"> + <meta name="author" content="A Hugo User"> <!-- This should be modified to be your name, if you want to include this information -->
\ No newline at end of file diff --git a/examples/blog/layouts/partials/navbar.html b/examples/blog/layouts/partials/navbar.html new file mode 100644 index 000000000..b15c24630 --- /dev/null +++ b/examples/blog/layouts/partials/navbar.html @@ -0,0 +1,22 @@ + <nav class="navbar navbar-default navbar-fixed-top" role="navigation"> + <div class="container"> + <div class="navbar-header"> + <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse"> + <span class="sr-only">Toggle Navigation</span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + </button> + <a class="navbar-brand" href="{{ .Site.BaseURL }}">{{ .Site.Title }}</a> + </div> + <div class="collapse navbar-collapse navbar-ex1-collapse"> + <ul class="nav navbar-nav"> + <li><a href="/post/">Post Index</a></li> + <!-- + And here is where you'd add more links to sections, or anywhere you like. + <li><a href="#">About Me</a></li> + --> + </ul> + </div> + </div> + </nav> diff --git a/examples/blog/layouts/post/li.html b/examples/blog/layouts/post/li.html new file mode 100644 index 000000000..d57be9c80 --- /dev/null +++ b/examples/blog/layouts/post/li.html @@ -0,0 +1,4 @@ + <li> + <h5><a href="{{ .Permalink }}">{{ .Title}}</a><br> + <small>posted on {{ .Date.Format "January 2, 2006" }}</small></h5> + </li>
\ No newline at end of file diff --git a/examples/blog/layouts/post/single.html b/examples/blog/layouts/post/single.html new file mode 100644 index 000000000..4b792ebe2 --- /dev/null +++ b/examples/blog/layouts/post/single.html @@ -0,0 +1,35 @@ +{{ partial "header.html" . }} +<body> +{{ partial "navbar.html" . }} +<div class="container"> + <div class="row"> + <div class="col-md-9"> + <div class="well well-sm"> + <h3>{{ .Title }}<br> <small>{{ .Description }}</small></h3> + <hr> + {{ .Content }} + </div> + </div> + + <!-- Sidebar --> + <div class="col-md-3"> + <div class="well well-sm"> <!-- Post-specific stats --> + <h4>{{ .Date.Format "January 2, 2006" }}<br> + <small>{{ .WordCount }} words</small></h4> + <hr> + <strong>Categories</strong> + <ul class="list-unstyled"> + {{ range .Params.categories }} + <li><a href="/categories/{{ . | urlize }}">{{ . }}</a></li> + {{ end }} + </ul> + <hr> + <strong>Tags</strong><br> + {{ range .Params.tags }}<a class="label label-default" href="/tags/{{ . | urlize }}">{{ . }}</a> {{ end }} + </div> + {{ partial "menu.html" . }} + </div> + </div> +{{ partial "footer.copyright.html" . }} +</div> +{{ partial "footer.html" . }} diff --git a/examples/blog/layouts/post/summary.html b/examples/blog/layouts/post/summary.html new file mode 100644 index 000000000..f70b6827a --- /dev/null +++ b/examples/blog/layouts/post/summary.html @@ -0,0 +1,9 @@ +<div class="well well-sm"> + <h4> + <a href="{{ .Permalink }}">{{ .Title }}</a> <small class="pull-right">Posted on {{ .Date.Format "Jan 2, 2006" }}</small><br> + <small>{{ .Description }}</small> + </h4> + <hr> + <p>{{ .Summary }}</p> + <a class="btn btn-primary btn-xs" href="{{ .Permalink }}">Read More <span class="fa fa-angle-double-right"></span></a> +</div>
\ No newline at end of file diff --git a/examples/blog/static/css/bootstrap.min.css b/examples/blog/static/css/bootstrap.min.css new file mode 100644 index 000000000..70829d0d3 --- /dev/null +++ b/examples/blog/static/css/bootstrap.min.css @@ -0,0 +1,11 @@ +@import url("https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,700italic,400,300,700");/*! + * bootswatch v3.3.6 + * Homepage: http://bootswatch.com + * Copyright 2012-2015 Thomas Park + * Licensed under MIT + * Based on Bootstrap +*//*! + * Bootstrap v3.3.6 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,*:before,*:after{background:transparent !important;color:#000 !important;-webkit-box-shadow:none !important;box-shadow:none !important;text-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}@font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons-halflings-regular.eot');src:url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'),url('../fonts/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-euro:before,.glyphicon-eur:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#222222;background-color:#ffffff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#008cba;text-decoration:none}a:hover,a:focus{color:#008cba;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:0}.img-thumbnail{padding:4px;line-height:1.4;background-color:#ffffff;border:1px solid #dddddd;border-radius:0;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:21px;margin-bottom:21px;border:0;border-top:1px solid #dddddd}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#999999}h1,.h1,h2,.h2,h3,.h3{margin-top:21px;margin-bottom:10.5px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10.5px;margin-bottom:10.5px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:39px}h2,.h2{font-size:32px}h3,.h3{font-size:26px}h4,.h4{font-size:19px}h5,.h5{font-size:15px}h6,.h6{font-size:13px}p{margin:0 0 10.5px}.lead{margin-bottom:21px;font-size:17px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:22.5px}}small,.small{font-size:80%}mark,.mark{background-color:#fcf8e3;padding:.2em}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#999999}.text-primary{color:#008cba}a.text-primary:hover,a.text-primary:focus{color:#006687}.text-success{color:#43ac6a}a.text-success:hover,a.text-success:focus{color:#358753}.text-info{color:#5bc0de}a.text-info:hover,a.text-info:focus{color:#31b0d5}.text-warning{color:#e99002}a.text-warning:hover,a.text-warning:focus{color:#b67102}.text-danger{color:#f04124}a.text-danger:hover,a.text-danger:focus{color:#d32a0e}.bg-primary{color:#fff;background-color:#008cba}a.bg-primary:hover,a.bg-primary:focus{background-color:#006687}.bg-success{background-color:#dff0d8}a.bg-success:hover,a.bg-success:focus{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover,a.bg-info:focus{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover,a.bg-warning:focus{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover,a.bg-danger:focus{background-color:#e4b9b9}.page-header{padding-bottom:9.5px;margin:42px 0 21px;border-bottom:1px solid #dddddd}ul,ol{margin-top:0;margin-bottom:10.5px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:21px}dt,dd{line-height:1.4}dt{font-weight:bold}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10.5px 21px;margin:0 0 21px;font-size:18.75px;border-left:5px solid #dddddd}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.4;color:#6f6f6f}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #dddddd;border-left:0;text-align:right}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}address{margin-bottom:21px;font-style:normal;line-height:1.4}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:0}kbd{padding:2px 4px;font-size:90%;color:#ffffff;background-color:#333333;border-radius:0;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}kbd kbd{padding:0;font-size:100%;font-weight:bold;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:10px;margin:0 0 10.5px;font-size:14px;line-height:1.4;word-break:break-all;word-wrap:break-word;color:#333333;background-color:#f5f5f5;border:1px solid #cccccc;border-radius:0}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0%}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0%}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0%}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0%}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#999999;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:21px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.4;vertical-align:top;border-top:1px solid #dddddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #dddddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #dddddd}.table .table{background-color:#ffffff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #dddddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #dddddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*="col-"]{position:static;float:none;display:table-column}table td[class*="col-"],table th[class*="col-"]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}.table-responsive{overflow-x:auto;min-height:0.01%}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15.75px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #dddddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:21px;font-size:22.5px;line-height:inherit;color:#333333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:9px;font-size:15px;line-height:1.4;color:#6f6f6f}.form-control{display:block;width:100%;height:39px;padding:8px 12px;font-size:15px;line-height:1.4;color:#6f6f6f;background-color:#ffffff;background-image:none;border:1px solid #cccccc;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6)}.form-control::-moz-placeholder{color:#999999;opacity:1}.form-control:-ms-input-placeholder{color:#999999}.form-control::-webkit-input-placeholder{color:#999999}.form-control::-ms-expand{border:0;background-color:transparent}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eeeeee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type="search"]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{line-height:39px}input[type="date"].input-sm,input[type="time"].input-sm,input[type="datetime-local"].input-sm,input[type="month"].input-sm,.input-group-sm input[type="date"],.input-group-sm input[type="time"],.input-group-sm input[type="datetime-local"],.input-group-sm input[type="month"]{line-height:36px}input[type="date"].input-lg,input[type="time"].input-lg,input[type="datetime-local"].input-lg,input[type="month"].input-lg,.input-group-lg input[type="date"],.input-group-lg input[type="time"],.input-group-lg input[type="datetime-local"],.input-group-lg input[type="month"]{line-height:60px}}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{min-height:21px;padding-left:20px;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{position:absolute;margin-left:-20px;margin-top:4px \9}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:normal;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"].disabled,input[type="checkbox"].disabled,fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"]{cursor:not-allowed}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:9px;padding-bottom:9px;margin-bottom:0;min-height:36px}.form-control-static.input-lg,.form-control-static.input-sm{padding-left:0;padding-right:0}.input-sm{height:36px;padding:8px 12px;font-size:12px;line-height:1.5;border-radius:0}select.input-sm{height:36px;line-height:36px}textarea.input-sm,select[multiple].input-sm{height:auto}.form-group-sm .form-control{height:36px;padding:8px 12px;font-size:12px;line-height:1.5;border-radius:0}.form-group-sm select.form-control{height:36px;line-height:36px}.form-group-sm textarea.form-control,.form-group-sm select[multiple].form-control{height:auto}.form-group-sm .form-control-static{height:36px;min-height:33px;padding:9px 12px;font-size:12px;line-height:1.5}.input-lg{height:60px;padding:16px 20px;font-size:19px;line-height:1.3333333;border-radius:0}select.input-lg{height:60px;line-height:60px}textarea.input-lg,select[multiple].input-lg{height:auto}.form-group-lg .form-control{height:60px;padding:16px 20px;font-size:19px;line-height:1.3333333;border-radius:0}.form-group-lg select.form-control{height:60px;line-height:60px}.form-group-lg textarea.form-control,.form-group-lg select[multiple].form-control{height:auto}.form-group-lg .form-control-static{height:60px;min-height:40px;padding:17px 20px;font-size:19px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:48.75px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:39px;height:39px;line-height:39px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback,.input-group-lg+.form-control-feedback,.form-group-lg .form-control+.form-control-feedback{width:60px;height:60px;line-height:60px}.input-sm+.form-control-feedback,.input-group-sm+.form-control-feedback,.form-group-sm .form-control+.form-control-feedback{width:36px;height:36px;line-height:36px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label{color:#43ac6a}.has-success .form-control{border-color:#43ac6a;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#358753;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #85d0a1;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #85d0a1}.has-success .input-group-addon{color:#43ac6a;border-color:#43ac6a;background-color:#dff0d8}.has-success .form-control-feedback{color:#43ac6a}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label{color:#e99002}.has-warning .form-control{border-color:#e99002;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#b67102;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #febc53;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #febc53}.has-warning .input-group-addon{color:#e99002;border-color:#e99002;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#e99002}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label{color:#f04124}.has-error .form-control{border-color:#f04124;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#d32a0e;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #f79483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #f79483}.has-error .input-group-addon{color:#f04124;border-color:#f04124;background-color:#f2dede}.has-error .form-control-feedback{color:#f04124}.has-feedback label~.form-control-feedback{top:26px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#626262}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:9px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:30px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}@media (min-width:768px){.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:9px}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:17px;font-size:19px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:9px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:8px 12px;font-size:15px;line-height:1.4;border-radius:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#333333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333333;background-color:#e7e7e7;border-color:#cccccc}.btn-default:focus,.btn-default.focus{color:#333333;background-color:#cecece;border-color:#8c8c8c}.btn-default:hover{color:#333333;background-color:#cecece;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333333;background-color:#cecece;border-color:#adadad}.btn-default:active:hover,.btn-default.active:hover,.open>.dropdown-toggle.btn-default:hover,.btn-default:active:focus,.btn-default.active:focus,.open>.dropdown-toggle.btn-default:focus,.btn-default:active.focus,.btn-default.active.focus,.open>.dropdown-toggle.btn-default.focus{color:#333333;background-color:#bcbcbc;border-color:#8c8c8c}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus{background-color:#e7e7e7;border-color:#cccccc}.btn-default .badge{color:#e7e7e7;background-color:#333333}.btn-primary{color:#ffffff;background-color:#008cba;border-color:#0079a1}.btn-primary:focus,.btn-primary.focus{color:#ffffff;background-color:#006687;border-color:#001921}.btn-primary:hover{color:#ffffff;background-color:#006687;border-color:#004b63}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#ffffff;background-color:#006687;border-color:#004b63}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#ffffff;background-color:#004b63;border-color:#001921}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus{background-color:#008cba;border-color:#0079a1}.btn-primary .badge{color:#008cba;background-color:#ffffff}.btn-success{color:#ffffff;background-color:#43ac6a;border-color:#3c9a5f}.btn-success:focus,.btn-success.focus{color:#ffffff;background-color:#358753;border-color:#183e26}.btn-success:hover{color:#ffffff;background-color:#358753;border-color:#2b6e44}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#ffffff;background-color:#358753;border-color:#2b6e44}.btn-success:active:hover,.btn-success.active:hover,.open>.dropdown-toggle.btn-success:hover,.btn-success:active:focus,.btn-success.active:focus,.open>.dropdown-toggle.btn-success:focus,.btn-success:active.focus,.btn-success.active.focus,.open>.dropdown-toggle.btn-success.focus{color:#ffffff;background-color:#2b6e44;border-color:#183e26}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus{background-color:#43ac6a;border-color:#3c9a5f}.btn-success .badge{color:#43ac6a;background-color:#ffffff}.btn-info{color:#ffffff;background-color:#5bc0de;border-color:#46b8da}.btn-info:focus,.btn-info.focus{color:#ffffff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#ffffff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#ffffff;background-color:#31b0d5;border-color:#269abc}.btn-info:active:hover,.btn-info.active:hover,.open>.dropdown-toggle.btn-info:hover,.btn-info:active:focus,.btn-info.active:focus,.open>.dropdown-toggle.btn-info:focus,.btn-info:active.focus,.btn-info.active.focus,.open>.dropdown-toggle.btn-info.focus{color:#ffffff;background-color:#269abc;border-color:#1b6d85}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#ffffff}.btn-warning{color:#ffffff;background-color:#e99002;border-color:#d08002}.btn-warning:focus,.btn-warning.focus{color:#ffffff;background-color:#b67102;border-color:#513201}.btn-warning:hover{color:#ffffff;background-color:#b67102;border-color:#935b01}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#ffffff;background-color:#b67102;border-color:#935b01}.btn-warning:active:hover,.btn-warning.active:hover,.open>.dropdown-toggle.btn-warning:hover,.btn-warning:active:focus,.btn-warning.active:focus,.open>.dropdown-toggle.btn-warning:focus,.btn-warning:active.focus,.btn-warning.active.focus,.open>.dropdown-toggle.btn-warning.focus{color:#ffffff;background-color:#935b01;border-color:#513201}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus{background-color:#e99002;border-color:#d08002}.btn-warning .badge{color:#e99002;background-color:#ffffff}.btn-danger{color:#ffffff;background-color:#f04124;border-color:#ea2f10}.btn-danger:focus,.btn-danger.focus{color:#ffffff;background-color:#d32a0e;border-color:#731708}.btn-danger:hover{color:#ffffff;background-color:#d32a0e;border-color:#b1240c}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#ffffff;background-color:#d32a0e;border-color:#b1240c}.btn-danger:active:hover,.btn-danger.active:hover,.open>.dropdown-toggle.btn-danger:hover,.btn-danger:active:focus,.btn-danger.active:focus,.open>.dropdown-toggle.btn-danger:focus,.btn-danger:active.focus,.btn-danger.active.focus,.open>.dropdown-toggle.btn-danger.focus{color:#ffffff;background-color:#b1240c;border-color:#731708}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus{background-color:#f04124;border-color:#ea2f10}.btn-danger .badge{color:#f04124;background-color:#ffffff}.btn-link{color:#008cba;font-weight:normal;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#008cba;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999999;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:16px 20px;font-size:19px;line-height:1.3333333;border-radius:0}.btn-sm,.btn-group-sm>.btn{padding:8px 12px;font-size:12px;line-height:1.5;border-radius:0}.btn-xs,.btn-group-xs>.btn{padding:4px 6px;font-size:12px;line-height:1.5;border-radius:0}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height, visibility;-o-transition-property:height, visibility;transition-property:height, visibility;-webkit-transition-duration:0.35s;-o-transition-duration:0.35s;transition-duration:0.35s;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid \9;border-right:4px solid transparent;border-left:4px solid transparent}.dropup,.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:15px;text-align:left;background-color:#ffffff;border:1px solid #cccccc;border:1px solid rgba(0,0,0,0.15);border-radius:0;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);-webkit-background-clip:padding-box;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9.5px 0;overflow:hidden;background-color:rgba(0,0,0,0.2)}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.4;color:#555555;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#262626;background-color:#eeeeee}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#ffffff;text-decoration:none;outline:0;background-color:#008cba}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.4;color:#999999;white-space:nowrap}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px dashed;border-bottom:4px solid \9;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:0;border-top-left-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-top-left-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle="buttons"]>.btn input[type="radio"],[data-toggle="buttons"]>.btn-group>.btn input[type="radio"],[data-toggle="buttons"]>.btn input[type="checkbox"],[data-toggle="buttons"]>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:60px;padding:16px 20px;font-size:19px;line-height:1.3333333;border-radius:0}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:60px;line-height:60px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:36px;padding:8px 12px;font-size:12px;line-height:1.5;border-radius:0}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:36px;line-height:36px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:8px 12px;font-size:15px;font-weight:normal;line-height:1;color:#6f6f6f;text-align:center;background-color:#eeeeee;border:1px solid #cccccc;border-radius:0}.input-group-addon.input-sm{padding:8px 12px;font-size:12px;border-radius:0}.input-group-addon.input-lg{padding:16px 20px;font-size:19px;border-radius:0}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eeeeee}.nav>li.disabled>a{color:#999999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999999;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eeeeee;border-color:#008cba}.nav .nav-divider{height:1px;margin:9.5px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #dddddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.4;border:1px solid transparent;border-radius:0 0 0 0}.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #dddddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#6f6f6f;background-color:#ffffff;border:1px solid #dddddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #dddddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #dddddd;border-radius:0 0 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#ffffff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:0}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#ffffff;background-color:#008cba}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #dddddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #dddddd;border-radius:0 0 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#ffffff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:45px;margin-bottom:21px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:0}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block !important;height:auto !important;padding-bottom:0;overflow:visible !important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:12px 15px;font-size:19px;line-height:21px;height:45px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:5.5px;margin-bottom:5.5px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:0}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:6px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:21px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:21px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:12px;padding-bottom:12px}}.navbar-form{margin-left:-15px;margin-right:-15px;padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);margin-top:3px;margin-bottom:3px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-right-radius:0;border-top-left-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:3px;margin-bottom:3px}.navbar-btn.btn-sm{margin-top:4.5px;margin-bottom:4.5px}.navbar-btn.btn-xs{margin-top:11.5px;margin-bottom:11.5px}.navbar-text{margin-top:12px;margin-bottom:12px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}}@media (min-width:768px){.navbar-left{float:left !important}.navbar-right{float:right !important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#333333;border-color:#222222}.navbar-default .navbar-brand{color:#ffffff}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#ffffff;background-color:transparent}.navbar-default .navbar-text{color:#ffffff}.navbar-default .navbar-nav>li>a{color:#ffffff}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#ffffff;background-color:#272727}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#ffffff;background-color:#272727}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#cccccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:transparent}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:transparent}.navbar-default .navbar-toggle .icon-bar{background-color:#ffffff}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#222222}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#272727;color:#ffffff}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#ffffff}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#ffffff;background-color:#272727}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#ffffff;background-color:#272727}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#cccccc;background-color:transparent}}.navbar-default .navbar-link{color:#ffffff}.navbar-default .navbar-link:hover{color:#ffffff}.navbar-default .btn-link{color:#ffffff}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#ffffff}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#cccccc}.navbar-inverse{background-color:#008cba;border-color:#006687}.navbar-inverse .navbar-brand{color:#ffffff}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#ffffff;background-color:transparent}.navbar-inverse .navbar-text{color:#ffffff}.navbar-inverse .navbar-nav>li>a{color:#ffffff}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#ffffff;background-color:#006687}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#ffffff;background-color:#006687}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:transparent}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:transparent}.navbar-inverse .navbar-toggle .icon-bar{background-color:#ffffff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#007196}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:#006687;color:#ffffff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#006687}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#006687}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#ffffff}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#ffffff;background-color:#006687}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#ffffff;background-color:#006687}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444444;background-color:transparent}}.navbar-inverse .navbar-link{color:#ffffff}.navbar-inverse .navbar-link:hover{color:#ffffff}.navbar-inverse .btn-link{color:#ffffff}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#ffffff}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#444444}.breadcrumb{padding:8px 15px;margin-bottom:21px;list-style:none;background-color:#f5f5f5;border-radius:0}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#999999}.breadcrumb>.active{color:#333333}.pagination{display:inline-block;padding-left:0;margin:21px 0;border-radius:0}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:8px 12px;line-height:1.4;text-decoration:none;color:#008cba;background-color:transparent;border:1px solid transparent;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:0;border-top-left-radius:0}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:0;border-top-right-radius:0}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{z-index:2;color:#008cba;background-color:#eeeeee;border-color:transparent}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:3;color:#ffffff;background-color:#008cba;border-color:transparent;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999999;background-color:#ffffff;border-color:transparent;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:16px 20px;font-size:19px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:0;border-top-left-radius:0}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:0;border-top-right-radius:0}.pagination-sm>li>a,.pagination-sm>li>span{padding:8px 12px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:0;border-top-left-radius:0}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:0;border-top-right-radius:0}.pager{padding-left:0;margin:21px 0;list-style:none;text-align:center}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:transparent;border:1px solid transparent;border-radius:3px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eeeeee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999999;background-color:transparent;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#ffffff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#ffffff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999999}.label-default[href]:hover,.label-default[href]:focus{background-color:#808080}.label-primary{background-color:#008cba}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#006687}.label-success{background-color:#43ac6a}.label-success[href]:hover,.label-success[href]:focus{background-color:#358753}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#e99002}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#b67102}.label-danger{background-color:#f04124}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#d32a0e}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;color:#ffffff;line-height:1;vertical-align:middle;white-space:nowrap;text-align:center;background-color:#008cba;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge,.btn-group-xs>.btn .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#ffffff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#008cba;background-color:#ffffff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#fafafa}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:23px;font-weight:200}.jumbotron>hr{border-top-color:#e1e1e1}.container .jumbotron,.container-fluid .jumbotron{border-radius:0;padding-left:15px;padding-right:15px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-left:60px;padding-right:60px}.jumbotron h1,.jumbotron .h1{font-size:68px}}.thumbnail{display:block;padding:4px;margin-bottom:21px;line-height:1.4;background-color:#ffffff;border:1px solid #dddddd;border-radius:0;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-left:auto;margin-right:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#008cba}.thumbnail .caption{padding:9px;color:#222222}.alert{padding:15px;margin-bottom:21px;border:1px solid transparent;border-radius:0}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#43ac6a;border-color:#3c9a5f;color:#ffffff}.alert-success hr{border-top-color:#358753}.alert-success .alert-link{color:#e6e6e6}.alert-info{background-color:#5bc0de;border-color:#3db5d8;color:#ffffff}.alert-info hr{border-top-color:#2aabd2}.alert-info .alert-link{color:#e6e6e6}.alert-warning{background-color:#e99002;border-color:#d08002;color:#ffffff}.alert-warning hr{border-top-color:#b67102}.alert-warning .alert-link{color:#e6e6e6}.alert-danger{background-color:#f04124;border-color:#ea2f10;color:#ffffff}.alert-danger hr{border-top-color:#d32a0e}.alert-danger .alert-link{color:#e6e6e6}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{overflow:hidden;height:21px;margin-bottom:21px;background-color:#f5f5f5;border-radius:0;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0%;height:100%;font-size:12px;line-height:21px;color:#ffffff;text-align:center;background-color:#008cba;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width 0.6s ease;-o-transition:width 0.6s ease;transition:width 0.6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#43ac6a}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-warning{background-color:#e99002}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-danger{background-color:#f04124}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{zoom:1;overflow:hidden}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-left,.media-right,.media-body{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#ffffff;border:1px solid #dddddd}.list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}a.list-group-item,button.list-group-item{color:#555555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333333}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{text-decoration:none;color:#555555;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{background-color:#eeeeee;color:#999999;cursor:not-allowed}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#999999}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#ffffff;background-color:#008cba;border-color:#008cba}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#87e1ff}.list-group-item-success{color:#43ac6a;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#43ac6a}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,button.list-group-item-success:hover,a.list-group-item-success:focus,button.list-group-item-success:focus{color:#43ac6a;background-color:#d0e9c6}a.list-group-item-success.active,button.list-group-item-success.active,a.list-group-item-success.active:hover,button.list-group-item-success.active:hover,a.list-group-item-success.active:focus,button.list-group-item-success.active:focus{color:#fff;background-color:#43ac6a;border-color:#43ac6a}.list-group-item-info{color:#5bc0de;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#5bc0de}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,button.list-group-item-info:hover,a.list-group-item-info:focus,button.list-group-item-info:focus{color:#5bc0de;background-color:#c4e3f3}a.list-group-item-info.active,button.list-group-item-info.active,a.list-group-item-info.active:hover,button.list-group-item-info.active:hover,a.list-group-item-info.active:focus,button.list-group-item-info.active:focus{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.list-group-item-warning{color:#e99002;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#e99002}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,button.list-group-item-warning:hover,a.list-group-item-warning:focus,button.list-group-item-warning:focus{color:#e99002;background-color:#faf2cc}a.list-group-item-warning.active,button.list-group-item-warning.active,a.list-group-item-warning.active:hover,button.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus,button.list-group-item-warning.active:focus{color:#fff;background-color:#e99002;border-color:#e99002}.list-group-item-danger{color:#f04124;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#f04124}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,button.list-group-item-danger:hover,a.list-group-item-danger:focus,button.list-group-item-danger:focus{color:#f04124;background-color:#ebcccc}a.list-group-item-danger.active,button.list-group-item-danger.active,a.list-group-item-danger.active:hover,button.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus,button.list-group-item-danger.active:focus{color:#fff;background-color:#f04124;border-color:#f04124}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:21px;background-color:#ffffff;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:-1;border-top-left-radius:-1}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:17px;color:inherit}.panel-title>a,.panel-title>small,.panel-title>.small,.panel-title>small>a,.panel-title>.small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #dddddd;border-bottom-right-radius:-1;border-bottom-left-radius:-1}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:-1;border-top-left-radius:-1}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:-1;border-bottom-left-radius:-1}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table caption,.panel>.table-responsive>.table caption,.panel>.panel-collapse>.table caption{padding-left:15px;padding-right:15px}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:-1;border-top-left-radius:-1}.panel>.table:first-child>thead:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child{border-top-left-radius:-1;border-top-right-radius:-1}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:-1}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:-1}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:-1;border-bottom-left-radius:-1}.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-left-radius:-1;border-bottom-right-radius:-1}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:-1}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:-1}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #dddddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:21px}.panel-group .panel{margin-bottom:0;border-radius:0}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body,.panel-group .panel-heading+.panel-collapse>.list-group{border-top:1px solid #dddddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #dddddd}.panel-default{border-color:#dddddd}.panel-default>.panel-heading{color:#333333;background-color:#f5f5f5;border-color:#dddddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#dddddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#dddddd}.panel-primary{border-color:#008cba}.panel-primary>.panel-heading{color:#ffffff;background-color:#008cba;border-color:#008cba}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#008cba}.panel-primary>.panel-heading .badge{color:#008cba;background-color:#ffffff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#008cba}.panel-success{border-color:#3c9a5f}.panel-success>.panel-heading{color:#ffffff;background-color:#43ac6a;border-color:#3c9a5f}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#3c9a5f}.panel-success>.panel-heading .badge{color:#43ac6a;background-color:#ffffff}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#3c9a5f}.panel-info{border-color:#3db5d8}.panel-info>.panel-heading{color:#ffffff;background-color:#5bc0de;border-color:#3db5d8}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#3db5d8}.panel-info>.panel-heading .badge{color:#5bc0de;background-color:#ffffff}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#3db5d8}.panel-warning{border-color:#d08002}.panel-warning>.panel-heading{color:#ffffff;background-color:#e99002;border-color:#d08002}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d08002}.panel-warning>.panel-heading .badge{color:#e99002;background-color:#ffffff}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d08002}.panel-danger{border-color:#ea2f10}.panel-danger>.panel-heading{color:#ffffff;background-color:#f04124;border-color:#ea2f10}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ea2f10}.panel-danger>.panel-heading .badge{color:#f04124;background-color:#ffffff}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ea2f10}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;left:0;bottom:0;height:100%;width:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#fafafa;border:1px solid #e8e8e8;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:0}.well-sm{padding:9px;border-radius:0}.close{float:right;font-size:22.5px;font-weight:bold;line-height:1;color:#ffffff;text-shadow:0 1px 0 #ffffff;opacity:0.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#ffffff;text-decoration:none;cursor:pointer;opacity:0.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:hidden;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0, -25%);-ms-transform:translate(0, -25%);-o-transform:translate(0, -25%);transform:translate(0, -25%);-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#ffffff;border:1px solid #999999;border:1px solid rgba(0,0,0,0.2);border-radius:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);-webkit-background-clip:padding-box;background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:0.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.4}.modal-body{position:relative;padding:20px}.modal-footer{padding:20px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:normal;letter-spacing:normal;line-break:auto;line-height:1.4;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;font-size:12px;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:0.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;background-color:#333333;border-radius:0}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#333333}.tooltip.top-left .tooltip-arrow{bottom:0;right:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#333333}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#333333}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#333333}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#333333}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#333333}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#333333}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#333333}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:normal;letter-spacing:normal;line-break:auto;line-height:1.4;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;font-size:15px;background-color:#333333;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #333333;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:15px;background-color:#333333;border-bottom:1px solid #262626;border-radius:-1 -1 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#000000;border-top-color:rgba(0,0,0,0.05);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#333333}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#000000;border-right-color:rgba(0,0,0,0.05)}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#333333}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#000000;border-bottom-color:rgba(0,0,0,0.05);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#333333}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#000000;border-left-color:rgba(0,0,0,0.05)}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#333333;bottom:-10px}.carousel{position:relative}.carousel-inner{position:relative;overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.next,.carousel-inner>.item.active.right{-webkit-transform:translate3d(100%, 0, 0);transform:translate3d(100%, 0, 0);left:0}.carousel-inner>.item.prev,.carousel-inner>.item.active.left{-webkit-transform:translate3d(-100%, 0, 0);transform:translate3d(-100%, 0, 0);left:0}.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right,.carousel-inner>.item.active{-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;left:0;bottom:0;width:15%;opacity:0.5;filter:alpha(opacity=50);font-size:20px;color:#ffffff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6);background-color:rgba(0,0,0,0)}.carousel-control.left{background-image:-webkit-linear-gradient(left, rgba(0,0,0,0.5) 0, rgba(0,0,0,0.0001) 100%);background-image:-o-linear-gradient(left, rgba(0,0,0,0.5) 0, rgba(0,0,0,0.0001) 100%);background-image:-webkit-gradient(linear, left top, right top, from(rgba(0,0,0,0.5)), to(rgba(0,0,0,0.0001)));background-image:linear-gradient(to right, rgba(0,0,0,0.5) 0, rgba(0,0,0,0.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left, rgba(0,0,0,0.0001) 0, rgba(0,0,0,0.5) 100%);background-image:-o-linear-gradient(left, rgba(0,0,0,0.0001) 0, rgba(0,0,0,0.5) 100%);background-image:-webkit-gradient(linear, left top, right top, from(rgba(0,0,0,0.0001)), to(rgba(0,0,0,0.5)));background-image:linear-gradient(to right, rgba(0,0,0,0.0001) 0, rgba(0,0,0,0.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control:hover,.carousel-control:focus{outline:0;color:#ffffff;text-decoration:none;opacity:0.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;margin-top:-10px;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;margin-left:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;margin-right:-10px}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;line-height:1;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #ffffff;border-radius:10px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0)}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#ffffff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px;color:#ffffff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-header:before,.modal-header:after,.modal-footer:before,.modal-footer:after{content:" ";display:table}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-header:after,.modal-footer:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none !important}@media (max-width:767px){.visible-xs{display:block !important}table.visible-xs{display:table !important}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media (max-width:767px){.visible-xs-block{display:block !important}}@media (max-width:767px){.visible-xs-inline{display:inline !important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block !important}table.visible-sm{display:table !important}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block !important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block !important}table.visible-md{display:table !important}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block !important}}@media (min-width:1200px){.visible-lg{display:block !important}table.visible-lg{display:table !important}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media (min-width:1200px){.visible-lg-block{display:block !important}}@media (min-width:1200px){.visible-lg-inline{display:inline !important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block !important}}@media (max-width:767px){.hidden-xs{display:none !important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none !important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none !important}}@media (min-width:1200px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table !important}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}.visible-print-block{display:none !important}@media print{.visible-print-block{display:block !important}}.visible-print-inline{display:none !important}@media print{.visible-print-inline{display:inline !important}}.visible-print-inline-block{display:none !important}@media print{.visible-print-inline-block{display:inline-block !important}}@media print{.hidden-print{display:none !important}}.navbar{border:none;font-size:13px;font-weight:300}.navbar .navbar-toggle:hover .icon-bar{background-color:#b3b3b3}.navbar-collapse{border-top-color:rgba(0,0,0,0.2);-webkit-box-shadow:none;box-shadow:none}.navbar .btn{padding-top:6px;padding-bottom:6px}.navbar-form{margin-top:7px;margin-bottom:5px}.navbar-form .form-control{height:auto;padding:4px 6px}.navbar .dropdown-menu{border:none}.navbar .dropdown-menu>li>a,.navbar .dropdown-menu>li>a:focus{background-color:transparent;font-size:13px;font-weight:300}.navbar .dropdown-header{color:rgba(255,255,255,0.5)}.navbar-default .dropdown-menu{background-color:#333333}.navbar-default .dropdown-menu>li>a,.navbar-default .dropdown-menu>li>a:focus{color:#ffffff}.navbar-default .dropdown-menu>li>a:hover,.navbar-default .dropdown-menu>.active>a,.navbar-default .dropdown-menu>.active>a:hover{background-color:#272727}.navbar-inverse .dropdown-menu{background-color:#008cba}.navbar-inverse .dropdown-menu>li>a,.navbar-inverse .dropdown-menu>li>a:focus{color:#ffffff}.navbar-inverse .dropdown-menu>li>a:hover,.navbar-inverse .dropdown-menu>.active>a,.navbar-inverse .dropdown-menu>.active>a:hover{background-color:#006687}.btn{padding:8px 12px}.btn-lg{padding:16px 20px}.btn-sm{padding:8px 12px}.btn-xs{padding:4px 6px}.btn-group .btn~.dropdown-toggle{padding-left:16px;padding-right:16px}.btn-group .dropdown-menu{border-top-width:0}.btn-group.dropup .dropdown-menu{border-top-width:1px;border-bottom-width:0;margin-bottom:0}.btn-group .dropdown-toggle.btn-default~.dropdown-menu{background-color:#e7e7e7;border-color:#cccccc}.btn-group .dropdown-toggle.btn-default~.dropdown-menu>li>a{color:#333333}.btn-group .dropdown-toggle.btn-default~.dropdown-menu>li>a:hover{background-color:#d3d3d3}.btn-group .dropdown-toggle.btn-primary~.dropdown-menu{background-color:#008cba;border-color:#0079a1}.btn-group .dropdown-toggle.btn-primary~.dropdown-menu>li>a{color:#ffffff}.btn-group .dropdown-toggle.btn-primary~.dropdown-menu>li>a:hover{background-color:#006d91}.btn-group .dropdown-toggle.btn-success~.dropdown-menu{background-color:#43ac6a;border-color:#3c9a5f}.btn-group .dropdown-toggle.btn-success~.dropdown-menu>li>a{color:#ffffff}.btn-group .dropdown-toggle.btn-success~.dropdown-menu>li>a:hover{background-color:#388f58}.btn-group .dropdown-toggle.btn-info~.dropdown-menu{background-color:#5bc0de;border-color:#46b8da}.btn-group .dropdown-toggle.btn-info~.dropdown-menu>li>a{color:#ffffff}.btn-group .dropdown-toggle.btn-info~.dropdown-menu>li>a:hover{background-color:#39b3d7}.btn-group .dropdown-toggle.btn-warning~.dropdown-menu{background-color:#e99002;border-color:#d08002}.btn-group .dropdown-toggle.btn-warning~.dropdown-menu>li>a{color:#ffffff}.btn-group .dropdown-toggle.btn-warning~.dropdown-menu>li>a:hover{background-color:#c17702}.btn-group .dropdown-toggle.btn-danger~.dropdown-menu{background-color:#f04124;border-color:#ea2f10}.btn-group .dropdown-toggle.btn-danger~.dropdown-menu>li>a{color:#ffffff}.btn-group .dropdown-toggle.btn-danger~.dropdown-menu>li>a:hover{background-color:#dc2c0f}.lead{color:#6f6f6f}cite{font-style:italic}blockquote{border-left-width:1px;color:#6f6f6f}blockquote.pull-right{border-right-width:1px}blockquote small{font-size:12px;font-weight:300}table{font-size:12px}label,.control-label,.help-block,.checkbox,.radio{font-size:12px;font-weight:normal}input[type="radio"],input[type="checkbox"]{margin-top:1px}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{border-color:transparent}.nav-tabs>li>a{background-color:#e7e7e7;color:#222222}.nav-tabs .caret{border-top-color:#222222;border-bottom-color:#222222}.nav-pills{font-weight:300}.breadcrumb{border:1px solid #dddddd;border-radius:3px;font-size:10px;font-weight:300;text-transform:uppercase}.pagination{font-size:12px;font-weight:300;color:#999999}.pagination>li>a,.pagination>li>span{margin-left:4px;color:#999999}.pagination>.active>a,.pagination>.active>span{color:#fff}.pagination>li>a,.pagination>li:first-child>a,.pagination>li:last-child>a,.pagination>li>span,.pagination>li:first-child>span,.pagination>li:last-child>span{border-radius:3px}.pagination-lg>li>a,.pagination-lg>li>span{padding-left:22px;padding-right:22px}.pagination-sm>li>a,.pagination-sm>li>span{padding:0 5px}.pager{font-size:12px;font-weight:300;color:#999999}.list-group{font-size:12px;font-weight:300}.close{opacity:0.4;text-decoration:none;text-shadow:none}.close:hover,.close:focus{opacity:1}.alert{font-size:12px;font-weight:300}.alert .alert-link{font-weight:normal;color:#fff;text-decoration:underline}.label{padding-left:1em;padding-right:1em;border-radius:0;font-weight:300}.label-default{background-color:#e7e7e7;color:#333333}.badge{font-weight:300}.progress{height:22px;padding:2px;background-color:#f6f6f6;border:1px solid #ccc;-webkit-box-shadow:none;box-shadow:none}.dropdown-menu{padding:0;margin-top:0;font-size:12px}.dropdown-menu>li>a{padding:12px 15px}.dropdown-header{padding-left:15px;padding-right:15px;font-size:9px;text-transform:uppercase}.popover{color:#fff;font-size:12px;font-weight:300}.panel-heading,.panel-footer{border-top-right-radius:0;border-top-left-radius:0}.panel-default .close{color:#222222}.modal .close{color:#222222}
\ No newline at end of file diff --git a/examples/blog/static/css/custom.css b/examples/blog/static/css/custom.css new file mode 100644 index 000000000..a9bb3c03b --- /dev/null +++ b/examples/blog/static/css/custom.css @@ -0,0 +1,7 @@ +body { + margin-top: 75px; /* 100px is double the height of the navbar - I made it a big larger for some more space - keep it at 50px at least if you want to use the fixed top nav */ +} + +footer { + margin: 50px 0; +}
\ No newline at end of file diff --git a/examples/blog/static/css/font-awesome.css b/examples/blog/static/css/font-awesome.css new file mode 100644 index 000000000..b2a5fe2f2 --- /dev/null +++ b/examples/blog/static/css/font-awesome.css @@ -0,0 +1,2086 @@ +/*! + * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ +/* FONT PATH + * -------------------------- */ +@font-face { + font-family: 'FontAwesome'; + src: url('../fonts/fontawesome-webfont.eot?v=4.5.0'); + src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.5.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.5.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.5.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.5.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.5.0#fontawesomeregular') format('svg'); + font-weight: normal; + font-style: normal; +} +.fa { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +/* makes the font 33% larger relative to the icon container */ +.fa-lg { + font-size: 1.33333333em; + line-height: 0.75em; + vertical-align: -15%; +} +.fa-2x { + font-size: 2em; +} +.fa-3x { + font-size: 3em; +} +.fa-4x { + font-size: 4em; +} +.fa-5x { + font-size: 5em; +} +.fa-fw { + width: 1.28571429em; + text-align: center; +} +.fa-ul { + padding-left: 0; + margin-left: 2.14285714em; + list-style-type: none; +} +.fa-ul > li { + position: relative; +} +.fa-li { + position: absolute; + left: -2.14285714em; + width: 2.14285714em; + top: 0.14285714em; + text-align: center; +} +.fa-li.fa-lg { + left: -1.85714286em; +} +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eeeeee; + border-radius: .1em; +} +.fa-pull-left { + float: left; +} +.fa-pull-right { + float: right; +} +.fa.fa-pull-left { + margin-right: .3em; +} +.fa.fa-pull-right { + margin-left: .3em; +} +/* Deprecated as of 4.4.0 */ +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.fa.pull-left { + margin-right: .3em; +} +.fa.pull-right { + margin-left: .3em; +} +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); +} +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +.fa-rotate-90 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} +.fa-rotate-180 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} +.fa-rotate-270 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); +} +.fa-flip-horizontal { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.fa-flip-vertical { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); +} +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + filter: none; +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.fa-stack-1x { + line-height: inherit; +} +.fa-stack-2x { + font-size: 2em; +} +.fa-inverse { + color: #ffffff; +} +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: "\f000"; +} +.fa-music:before { + content: "\f001"; +} +.fa-search:before { + content: "\f002"; +} +.fa-envelope-o:before { + content: "\f003"; +} +.fa-heart:before { + content: "\f004"; +} +.fa-star:before { + content: "\f005"; +} +.fa-star-o:before { + content: "\f006"; +} +.fa-user:before { + content: "\f007"; +} +.fa-film:before { + content: "\f008"; +} +.fa-th-large:before { + content: "\f009"; +} +.fa-th:before { + content: "\f00a"; +} +.fa-th-list:before { + content: "\f00b"; +} +.fa-check:before { + content: "\f00c"; +} +.fa-remove:before, +.fa-close:before, +.fa-times:before { + content: "\f00d"; +} +.fa-search-plus:before { + content: "\f00e"; +} +.fa-search-minus:before { + content: "\f010"; +} +.fa-power-off:before { + content: "\f011"; +} +.fa-signal:before { + content: "\f012"; +} +.fa-gear:before, +.fa-cog:before { + content: "\f013"; +} +.fa-trash-o:before { + content: "\f014"; +} +.fa-home:before { + content: "\f015"; +} +.fa-file-o:before { + content: "\f016"; +} +.fa-clock-o:before { + content: "\f017"; +} +.fa-road:before { + content: "\f018"; +} +.fa-download:before { + content: "\f019"; +} +.fa-arrow-circle-o-down:before { + content: "\f01a"; +} +.fa-arrow-circle-o-up:before { + content: "\f01b"; +} +.fa-inbox:before { + content: "\f01c"; +} +.fa-play-circle-o:before { + content: "\f01d"; +} +.fa-rotate-right:before, +.fa-repeat:before { + content: "\f01e"; +} +.fa-refresh:before { + content: "\f021"; +} +.fa-list-alt:before { + content: "\f022"; +} +.fa-lock:before { + content: "\f023"; +} +.fa-flag:before { + content: "\f024"; +} +.fa-headphones:before { + content: "\f025"; +} +.fa-volume-off:before { + content: "\f026"; +} +.fa-volume-down:before { + content: "\f027"; +} +.fa-volume-up:before { + content: "\f028"; +} +.fa-qrcode:before { + content: "\f029"; +} +.fa-barcode:before { + content: "\f02a"; +} +.fa-tag:before { + content: "\f02b"; +} +.fa-tags:before { + content: "\f02c"; +} +.fa-book:before { + content: "\f02d"; +} +.fa-bookmark:before { + content: "\f02e"; +} +.fa-print:before { + content: "\f02f"; +} +.fa-camera:before { + content: "\f030"; +} +.fa-font:before { + content: "\f031"; +} +.fa-bold:before { + content: "\f032"; +} +.fa-italic:before { + content: "\f033"; +} +.fa-text-height:before { + content: "\f034"; +} +.fa-text-width:before { + content: "\f035"; +} +.fa-align-left:before { + content: "\f036"; +} +.fa-align-center:before { + content: "\f037"; +} +.fa-align-right:before { + content: "\f038"; +} +.fa-align-justify:before { + content: "\f039"; +} +.fa-list:before { + content: "\f03a"; +} +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} +.fa-indent:before { + content: "\f03c"; +} +.fa-video-camera:before { + content: "\f03d"; +} +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: "\f03e"; +} +.fa-pencil:before { + content: "\f040"; +} +.fa-map-marker:before { + content: "\f041"; +} +.fa-adjust:before { + content: "\f042"; +} +.fa-tint:before { + content: "\f043"; +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: "\f044"; +} +.fa-share-square-o:before { + content: "\f045"; +} +.fa-check-square-o:before { + content: "\f046"; +} +.fa-arrows:before { + content: "\f047"; +} +.fa-step-backward:before { + content: "\f048"; +} +.fa-fast-backward:before { + content: "\f049"; +} +.fa-backward:before { + content: "\f04a"; +} +.fa-play:before { + content: "\f04b"; +} +.fa-pause:before { + content: "\f04c"; +} +.fa-stop:before { + content: "\f04d"; +} +.fa-forward:before { + content: "\f04e"; +} +.fa-fast-forward:before { + content: "\f050"; +} +.fa-step-forward:before { + content: "\f051"; +} +.fa-eject:before { + content: "\f052"; +} +.fa-chevron-left:before { + content: "\f053"; +} +.fa-chevron-right:before { + content: "\f054"; +} +.fa-plus-circle:before { + content: "\f055"; +} +.fa-minus-circle:before { + content: "\f056"; +} +.fa-times-circle:before { + content: "\f057"; +} +.fa-check-circle:before { + content: "\f058"; +} +.fa-question-circle:before { + content: "\f059"; +} +.fa-info-circle:before { + content: "\f05a"; +} +.fa-crosshairs:before { + content: "\f05b"; +} +.fa-times-circle-o:before { + content: "\f05c"; +} +.fa-check-circle-o:before { + content: "\f05d"; +} +.fa-ban:before { + content: "\f05e"; +} +.fa-arrow-left:before { + content: "\f060"; +} +.fa-arrow-right:before { + content: "\f061"; +} +.fa-arrow-up:before { + content: "\f062"; +} +.fa-arrow-down:before { + content: "\f063"; +} +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} +.fa-expand:before { + content: "\f065"; +} +.fa-compress:before { + content: "\f066"; +} +.fa-plus:before { + content: "\f067"; +} +.fa-minus:before { + content: "\f068"; +} +.fa-asterisk:before { + content: "\f069"; +} +.fa-exclamation-circle:before { + content: "\f06a"; +} +.fa-gift:before { + content: "\f06b"; +} +.fa-leaf:before { + content: "\f06c"; +} +.fa-fire:before { + content: "\f06d"; +} +.fa-eye:before { + content: "\f06e"; +} +.fa-eye-slash:before { + content: "\f070"; +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "\f071"; +} +.fa-plane:before { + content: "\f072"; +} +.fa-calendar:before { + content: "\f073"; +} +.fa-random:before { + content: "\f074"; +} +.fa-comment:before { + content: "\f075"; +} +.fa-magnet:before { + content: "\f076"; +} +.fa-chevron-up:before { + content: "\f077"; +} +.fa-chevron-down:before { + content: "\f078"; +} +.fa-retweet:before { + content: "\f079"; +} +.fa-shopping-cart:before { + content: "\f07a"; +} +.fa-folder:before { + content: "\f07b"; +} +.fa-folder-open:before { + content: "\f07c"; +} +.fa-arrows-v:before { + content: "\f07d"; +} +.fa-arrows-h:before { + content: "\f07e"; +} +.fa-bar-chart-o:before, +.fa-bar-chart:before { + content: "\f080"; +} +.fa-twitter-square:before { + content: "\f081"; +} +.fa-facebook-square:before { + content: "\f082"; +} +.fa-camera-retro:before { + content: "\f083"; +} +.fa-key:before { + content: "\f084"; +} +.fa-gears:before, +.fa-cogs:before { + content: "\f085"; +} +.fa-comments:before { + content: "\f086"; +} +.fa-thumbs-o-up:before { + content: "\f087"; +} +.fa-thumbs-o-down:before { + content: "\f088"; +} +.fa-star-half:before { + content: "\f089"; +} +.fa-heart-o:before { + content: "\f08a"; +} +.fa-sign-out:before { + content: "\f08b"; +} +.fa-linkedin-square:before { + content: "\f08c"; +} +.fa-thumb-tack:before { + content: "\f08d"; +} +.fa-external-link:before { + content: "\f08e"; +} +.fa-sign-in:before { + content: "\f090"; +} +.fa-trophy:before { + content: "\f091"; +} +.fa-github-square:before { + content: "\f092"; +} +.fa-upload:before { + content: "\f093"; +} +.fa-lemon-o:before { + content: "\f094"; +} +.fa-phone:before { + content: "\f095"; +} +.fa-square-o:before { + content: "\f096"; +} +.fa-bookmark-o:before { + content: "\f097"; +} +.fa-phone-square:before { + content: "\f098"; +} +.fa-twitter:before { + content: "\f099"; +} +.fa-facebook-f:before, +.fa-facebook:before { + content: "\f09a"; +} +.fa-github:before { + content: "\f09b"; +} +.fa-unlock:before { + content: "\f09c"; +} +.fa-credit-card:before { + content: "\f09d"; +} +.fa-feed:before, +.fa-rss:before { + content: "\f09e"; +} +.fa-hdd-o:before { + content: "\f0a0"; +} +.fa-bullhorn:before { + content: "\f0a1"; +} +.fa-bell:before { + content: "\f0f3"; +} +.fa-certificate:before { + content: "\f0a3"; +} +.fa-hand-o-right:before { + content: "\f0a4"; +} +.fa-hand-o-left:before { + content: "\f0a5"; +} +.fa-hand-o-up:before { + content: "\f0a6"; +} +.fa-hand-o-down:before { + content: "\f0a7"; +} +.fa-arrow-circle-left:before { + content: "\f0a8"; +} +.fa-arrow-circle-right:before { + content: "\f0a9"; +} +.fa-arrow-circle-up:before { + content: "\f0aa"; +} +.fa-arrow-circle-down:before { + content: "\f0ab"; +} +.fa-globe:before { + content: "\f0ac"; +} +.fa-wrench:before { + content: "\f0ad"; +} +.fa-tasks:before { + content: "\f0ae"; +} +.fa-filter:before { + content: "\f0b0"; +} +.fa-briefcase:before { + content: "\f0b1"; +} +.fa-arrows-alt:before { + content: "\f0b2"; +} +.fa-group:before, +.fa-users:before { + content: "\f0c0"; +} +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} +.fa-cloud:before { + content: "\f0c2"; +} +.fa-flask:before { + content: "\f0c3"; +} +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} +.fa-copy:before, +.fa-files-o:before { + content: "\f0c5"; +} +.fa-paperclip:before { + content: "\f0c6"; +} +.fa-save:before, +.fa-floppy-o:before { + content: "\f0c7"; +} +.fa-square:before { + content: "\f0c8"; +} +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: "\f0c9"; +} +.fa-list-ul:before { + content: "\f0ca"; +} +.fa-list-ol:before { + content: "\f0cb"; +} +.fa-strikethrough:before { + content: "\f0cc"; +} +.fa-underline:before { + content: "\f0cd"; +} +.fa-table:before { + content: "\f0ce"; +} +.fa-magic:before { + content: "\f0d0"; +} +.fa-truck:before { + content: "\f0d1"; +} +.fa-pinterest:before { + content: "\f0d2"; +} +.fa-pinterest-square:before { + content: "\f0d3"; +} +.fa-google-plus-square:before { + content: "\f0d4"; +} +.fa-google-plus:before { + content: "\f0d5"; +} +.fa-money:before { + content: "\f0d6"; +} +.fa-caret-down:before { + content: "\f0d7"; +} +.fa-caret-up:before { + content: "\f0d8"; +} +.fa-caret-left:before { + content: "\f0d9"; +} +.fa-caret-right:before { + content: "\f0da"; +} +.fa-columns:before { + content: "\f0db"; +} +.fa-unsorted:before, +.fa-sort:before { + content: "\f0dc"; +} +.fa-sort-down:before, +.fa-sort-desc:before { + content: "\f0dd"; +} +.fa-sort-up:before, +.fa-sort-asc:before { + content: "\f0de"; +} +.fa-envelope:before { + content: "\f0e0"; +} +.fa-linkedin:before { + content: "\f0e1"; +} +.fa-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} +.fa-legal:before, +.fa-gavel:before { + content: "\f0e3"; +} +.fa-dashboard:before, +.fa-tachometer:before { + content: "\f0e4"; +} +.fa-comment-o:before { + content: "\f0e5"; +} +.fa-comments-o:before { + content: "\f0e6"; +} +.fa-flash:before, +.fa-bolt:before { + content: "\f0e7"; +} +.fa-sitemap:before { + content: "\f0e8"; +} +.fa-umbrella:before { + content: "\f0e9"; +} +.fa-paste:before, +.fa-clipboard:before { + content: "\f0ea"; +} +.fa-lightbulb-o:before { + content: "\f0eb"; +} +.fa-exchange:before { + content: "\f0ec"; +} +.fa-cloud-download:before { + content: "\f0ed"; +} +.fa-cloud-upload:before { + content: "\f0ee"; +} +.fa-user-md:before { + content: "\f0f0"; +} +.fa-stethoscope:before { + content: "\f0f1"; +} +.fa-suitcase:before { + content: "\f0f2"; +} +.fa-bell-o:before { + content: "\f0a2"; +} +.fa-coffee:before { + content: "\f0f4"; +} +.fa-cutlery:before { + content: "\f0f5"; +} +.fa-file-text-o:before { + content: "\f0f6"; +} +.fa-building-o:before { + content: "\f0f7"; +} +.fa-hospital-o:before { + content: "\f0f8"; +} +.fa-ambulance:before { + content: "\f0f9"; +} +.fa-medkit:before { + content: "\f0fa"; +} +.fa-fighter-jet:before { + content: "\f0fb"; +} +.fa-beer:before { + content: "\f0fc"; +} +.fa-h-square:before { + content: "\f0fd"; +} +.fa-plus-square:before { + content: "\f0fe"; +} +.fa-angle-double-left:before { + content: "\f100"; +} +.fa-angle-double-right:before { + content: "\f101"; +} +.fa-angle-double-up:before { + content: "\f102"; +} +.fa-angle-double-down:before { + content: "\f103"; +} +.fa-angle-left:before { + content: "\f104"; +} +.fa-angle-right:before { + content: "\f105"; +} +.fa-angle-up:before { + content: "\f106"; +} +.fa-angle-down:before { + content: "\f107"; +} +.fa-desktop:before { + content: "\f108"; +} +.fa-laptop:before { + content: "\f109"; +} +.fa-tablet:before { + content: "\f10a"; +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f10b"; +} +.fa-circle-o:before { + content: "\f10c"; +} +.fa-quote-left:before { + content: "\f10d"; +} +.fa-quote-right:before { + content: "\f10e"; +} +.fa-spinner:before { + content: "\f110"; +} +.fa-circle:before { + content: "\f111"; +} +.fa-mail-reply:before, +.fa-reply:before { + content: "\f112"; +} +.fa-github-alt:before { + content: "\f113"; +} +.fa-folder-o:before { + content: "\f114"; +} +.fa-folder-open-o:before { + content: "\f115"; +} +.fa-smile-o:before { + content: "\f118"; +} +.fa-frown-o:before { + content: "\f119"; +} +.fa-meh-o:before { + content: "\f11a"; +} +.fa-gamepad:before { + content: "\f11b"; +} +.fa-keyboard-o:before { + content: "\f11c"; +} +.fa-flag-o:before { + content: "\f11d"; +} +.fa-flag-checkered:before { + content: "\f11e"; +} +.fa-terminal:before { + content: "\f120"; +} +.fa-code:before { + content: "\f121"; +} +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "\f122"; +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "\f123"; +} +.fa-location-arrow:before { + content: "\f124"; +} +.fa-crop:before { + content: "\f125"; +} +.fa-code-fork:before { + content: "\f126"; +} +.fa-unlink:before, +.fa-chain-broken:before { + content: "\f127"; +} +.fa-question:before { + content: "\f128"; +} +.fa-info:before { + content: "\f129"; +} +.fa-exclamation:before { + content: "\f12a"; +} +.fa-superscript:before { + content: "\f12b"; +} +.fa-subscript:before { + content: "\f12c"; +} +.fa-eraser:before { + content: "\f12d"; +} +.fa-puzzle-piece:before { + content: "\f12e"; +} +.fa-microphone:before { + content: "\f130"; +} +.fa-microphone-slash:before { + content: "\f131"; +} +.fa-shield:before { + content: "\f132"; +} +.fa-calendar-o:before { + content: "\f133"; +} +.fa-fire-extinguisher:before { + content: "\f134"; +} +.fa-rocket:before { + content: "\f135"; +} +.fa-maxcdn:before { + content: "\f136"; +} +.fa-chevron-circle-left:before { + content: "\f137"; +} +.fa-chevron-circle-right:before { + content: "\f138"; +} +.fa-chevron-circle-up:before { + content: "\f139"; +} +.fa-chevron-circle-down:before { + content: "\f13a"; +} +.fa-html5:before { + content: "\f13b"; +} +.fa-css3:before { + content: "\f13c"; +} +.fa-anchor:before { + content: "\f13d"; +} +.fa-unlock-alt:before { + content: "\f13e"; +} +.fa-bullseye:before { + content: "\f140"; +} +.fa-ellipsis-h:before { + content: "\f141"; +} +.fa-ellipsis-v:before { + content: "\f142"; +} +.fa-rss-square:before { + content: "\f143"; +} +.fa-play-circle:before { + content: "\f144"; +} +.fa-ticket:before { + content: "\f145"; +} +.fa-minus-square:before { + content: "\f146"; +} +.fa-minus-square-o:before { + content: "\f147"; +} +.fa-level-up:before { + content: "\f148"; +} +.fa-level-down:before { + content: "\f149"; +} +.fa-check-square:before { + content: "\f14a"; +} +.fa-pencil-square:before { + content: "\f14b"; +} +.fa-external-link-square:before { + content: "\f14c"; +} +.fa-share-square:before { + content: "\f14d"; +} +.fa-compass:before { + content: "\f14e"; +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "\f150"; +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "\f151"; +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "\f152"; +} +.fa-euro:before, +.fa-eur:before { + content: "\f153"; +} +.fa-gbp:before { + content: "\f154"; +} +.fa-dollar:before, +.fa-usd:before { + content: "\f155"; +} +.fa-rupee:before, +.fa-inr:before { + content: "\f156"; +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "\f157"; +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "\f158"; +} +.fa-won:before, +.fa-krw:before { + content: "\f159"; +} +.fa-bitcoin:before, +.fa-btc:before { + content: "\f15a"; +} +.fa-file:before { + content: "\f15b"; +} +.fa-file-text:before { + content: "\f15c"; +} +.fa-sort-alpha-asc:before { + content: "\f15d"; +} +.fa-sort-alpha-desc:before { + content: "\f15e"; +} +.fa-sort-amount-asc:before { + content: "\f160"; +} +.fa-sort-amount-desc:before { + content: "\f161"; +} +.fa-sort-numeric-asc:before { + content: "\f162"; +} +.fa-sort-numeric-desc:before { + content: "\f163"; +} +.fa-thumbs-up:before { + content: "\f164"; +} +.fa-thumbs-down:before { + content: "\f165"; +} +.fa-youtube-square:before { + content: "\f166"; +} +.fa-youtube:before { + content: "\f167"; +} +.fa-xing:before { + content: "\f168"; +} +.fa-xing-square:before { + content: "\f169"; +} +.fa-youtube-play:before { + content: "\f16a"; +} +.fa-dropbox:before { + content: "\f16b"; +} +.fa-stack-overflow:before { + content: "\f16c"; +} +.fa-instagram:before { + content: "\f16d"; +} +.fa-flickr:before { + content: "\f16e"; +} +.fa-adn:before { + content: "\f170"; +} +.fa-bitbucket:before { + content: "\f171"; +} +.fa-bitbucket-square:before { + content: "\f172"; +} +.fa-tumblr:before { + content: "\f173"; +} +.fa-tumblr-square:before { + content: "\f174"; +} +.fa-long-arrow-down:before { + content: "\f175"; +} +.fa-long-arrow-up:before { + content: "\f176"; +} +.fa-long-arrow-left:before { + content: "\f177"; +} +.fa-long-arrow-right:before { + content: "\f178"; +} +.fa-apple:before { + content: "\f179"; +} +.fa-windows:before { + content: "\f17a"; +} +.fa-android:before { + content: "\f17b"; +} +.fa-linux:before { + content: "\f17c"; +} +.fa-dribbble:before { + content: "\f17d"; +} +.fa-skype:before { + content: "\f17e"; +} +.fa-foursquare:before { + content: "\f180"; +} +.fa-trello:before { + content: "\f181"; +} +.fa-female:before { + content: "\f182"; +} +.fa-male:before { + content: "\f183"; +} +.fa-gittip:before, +.fa-gratipay:before { + content: "\f184"; +} +.fa-sun-o:before { + content: "\f185"; +} +.fa-moon-o:before { + content: "\f186"; +} +.fa-archive:before { + content: "\f187"; +} +.fa-bug:before { + content: "\f188"; +} +.fa-vk:before { + content: "\f189"; +} +.fa-weibo:before { + content: "\f18a"; +} +.fa-renren:before { + content: "\f18b"; +} +.fa-pagelines:before { + content: "\f18c"; +} +.fa-stack-exchange:before { + content: "\f18d"; +} +.fa-arrow-circle-o-right:before { + content: "\f18e"; +} +.fa-arrow-circle-o-left:before { + content: "\f190"; +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "\f191"; +} +.fa-dot-circle-o:before { + content: "\f192"; +} +.fa-wheelchair:before { + content: "\f193"; +} +.fa-vimeo-square:before { + content: "\f194"; +} +.fa-turkish-lira:before, +.fa-try:before { + content: "\f195"; +} +.fa-plus-square-o:before { + content: "\f196"; +} +.fa-space-shuttle:before { + content: "\f197"; +} +.fa-slack:before { + content: "\f198"; +} +.fa-envelope-square:before { + content: "\f199"; +} +.fa-wordpress:before { + content: "\f19a"; +} +.fa-openid:before { + content: "\f19b"; +} +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: "\f19c"; +} +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: "\f19d"; +} +.fa-yahoo:before { + content: "\f19e"; +} +.fa-google:before { + content: "\f1a0"; +} +.fa-reddit:before { + content: "\f1a1"; +} +.fa-reddit-square:before { + content: "\f1a2"; +} +.fa-stumbleupon-circle:before { + content: "\f1a3"; +} +.fa-stumbleupon:before { + content: "\f1a4"; +} +.fa-delicious:before { + content: "\f1a5"; +} +.fa-digg:before { + content: "\f1a6"; +} +.fa-pied-piper:before { + content: "\f1a7"; +} +.fa-pied-piper-alt:before { + content: "\f1a8"; +} +.fa-drupal:before { + content: "\f1a9"; +} +.fa-joomla:before { + content: "\f1aa"; +} +.fa-language:before { + content: "\f1ab"; +} +.fa-fax:before { + content: "\f1ac"; +} +.fa-building:before { + content: "\f1ad"; +} +.fa-child:before { + content: "\f1ae"; +} +.fa-paw:before { + content: "\f1b0"; +} +.fa-spoon:before { + content: "\f1b1"; +} +.fa-cube:before { + content: "\f1b2"; +} +.fa-cubes:before { + content: "\f1b3"; +} +.fa-behance:before { + content: "\f1b4"; +} +.fa-behance-square:before { + content: "\f1b5"; +} +.fa-steam:before { + content: "\f1b6"; +} +.fa-steam-square:before { + content: "\f1b7"; +} +.fa-recycle:before { + content: "\f1b8"; +} +.fa-automobile:before, +.fa-car:before { + content: "\f1b9"; +} +.fa-cab:before, +.fa-taxi:before { + content: "\f1ba"; +} +.fa-tree:before { + content: "\f1bb"; +} +.fa-spotify:before { + content: "\f1bc"; +} +.fa-deviantart:before { + content: "\f1bd"; +} +.fa-soundcloud:before { + content: "\f1be"; +} +.fa-database:before { + content: "\f1c0"; +} +.fa-file-pdf-o:before { + content: "\f1c1"; +} +.fa-file-word-o:before { + content: "\f1c2"; +} +.fa-file-excel-o:before { + content: "\f1c3"; +} +.fa-file-powerpoint-o:before { + content: "\f1c4"; +} +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: "\f1c5"; +} +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: "\f1c6"; +} +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: "\f1c7"; +} +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: "\f1c8"; +} +.fa-file-code-o:before { + content: "\f1c9"; +} +.fa-vine:before { + content: "\f1ca"; +} +.fa-codepen:before { + content: "\f1cb"; +} +.fa-jsfiddle:before { + content: "\f1cc"; +} +.fa-life-bouy:before, +.fa-life-buoy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: "\f1cd"; +} +.fa-circle-o-notch:before { + content: "\f1ce"; +} +.fa-ra:before, +.fa-rebel:before { + content: "\f1d0"; +} +.fa-ge:before, +.fa-empire:before { + content: "\f1d1"; +} +.fa-git-square:before { + content: "\f1d2"; +} +.fa-git:before { + content: "\f1d3"; +} +.fa-y-combinator-square:before, +.fa-yc-square:before, +.fa-hacker-news:before { + content: "\f1d4"; +} +.fa-tencent-weibo:before { + content: "\f1d5"; +} +.fa-qq:before { + content: "\f1d6"; +} +.fa-wechat:before, +.fa-weixin:before { + content: "\f1d7"; +} +.fa-send:before, +.fa-paper-plane:before { + content: "\f1d8"; +} +.fa-send-o:before, +.fa-paper-plane-o:before { + content: "\f1d9"; +} +.fa-history:before { + content: "\f1da"; +} +.fa-circle-thin:before { + content: "\f1db"; +} +.fa-header:before { + content: "\f1dc"; +} +.fa-paragraph:before { + content: "\f1dd"; +} +.fa-sliders:before { + content: "\f1de"; +} +.fa-share-alt:before { + content: "\f1e0"; +} +.fa-share-alt-square:before { + content: "\f1e1"; +} +.fa-bomb:before { + content: "\f1e2"; +} +.fa-soccer-ball-o:before, +.fa-futbol-o:before { + content: "\f1e3"; +} +.fa-tty:before { + content: "\f1e4"; +} +.fa-binoculars:before { + content: "\f1e5"; +} +.fa-plug:before { + content: "\f1e6"; +} +.fa-slideshare:before { + content: "\f1e7"; +} +.fa-twitch:before { + content: "\f1e8"; +} +.fa-yelp:before { + content: "\f1e9"; +} +.fa-newspaper-o:before { + content: "\f1ea"; +} +.fa-wifi:before { + content: "\f1eb"; +} +.fa-calculator:before { + content: "\f1ec"; +} +.fa-paypal:before { + content: "\f1ed"; +} +.fa-google-wallet:before { + content: "\f1ee"; +} +.fa-cc-visa:before { + content: "\f1f0"; +} +.fa-cc-mastercard:before { + content: "\f1f1"; +} +.fa-cc-discover:before { + content: "\f1f2"; +} +.fa-cc-amex:before { + content: "\f1f3"; +} +.fa-cc-paypal:before { + content: "\f1f4"; +} +.fa-cc-stripe:before { + content: "\f1f5"; +} +.fa-bell-slash:before { + content: "\f1f6"; +} +.fa-bell-slash-o:before { + content: "\f1f7"; +} +.fa-trash:before { + content: "\f1f8"; +} +.fa-copyright:before { + content: "\f1f9"; +} +.fa-at:before { + content: "\f1fa"; +} +.fa-eyedropper:before { + content: "\f1fb"; +} +.fa-paint-brush:before { + content: "\f1fc"; +} +.fa-birthday-cake:before { + content: "\f1fd"; +} +.fa-area-chart:before { + content: "\f1fe"; +} +.fa-pie-chart:before { + content: "\f200"; +} +.fa-line-chart:before { + content: "\f201"; +} +.fa-lastfm:before { + content: "\f202"; +} +.fa-lastfm-square:before { + content: "\f203"; +} +.fa-toggle-off:before { + content: "\f204"; +} +.fa-toggle-on:before { + content: "\f205"; +} +.fa-bicycle:before { + content: "\f206"; +} +.fa-bus:before { + content: "\f207"; +} +.fa-ioxhost:before { + content: "\f208"; +} +.fa-angellist:before { + content: "\f209"; +} +.fa-cc:before { + content: "\f20a"; +} +.fa-shekel:before, +.fa-sheqel:before, +.fa-ils:before { + content: "\f20b"; +} +.fa-meanpath:before { + content: "\f20c"; +} +.fa-buysellads:before { + content: "\f20d"; +} +.fa-connectdevelop:before { + content: "\f20e"; +} +.fa-dashcube:before { + content: "\f210"; +} +.fa-forumbee:before { + content: "\f211"; +} +.fa-leanpub:before { + content: "\f212"; +} +.fa-sellsy:before { + content: "\f213"; +} +.fa-shirtsinbulk:before { + content: "\f214"; +} +.fa-simplybuilt:before { + content: "\f215"; +} +.fa-skyatlas:before { + content: "\f216"; +} +.fa-cart-plus:before { + content: "\f217"; +} +.fa-cart-arrow-down:before { + content: "\f218"; +} +.fa-diamond:before { + content: "\f219"; +} +.fa-ship:before { + content: "\f21a"; +} +.fa-user-secret:before { + content: "\f21b"; +} +.fa-motorcycle:before { + content: "\f21c"; +} +.fa-street-view:before { + content: "\f21d"; +} +.fa-heartbeat:before { + content: "\f21e"; +} +.fa-venus:before { + content: "\f221"; +} +.fa-mars:before { + content: "\f222"; +} +.fa-mercury:before { + content: "\f223"; +} +.fa-intersex:before, +.fa-transgender:before { + content: "\f224"; +} +.fa-transgender-alt:before { + content: "\f225"; +} +.fa-venus-double:before { + content: "\f226"; +} +.fa-mars-double:before { + content: "\f227"; +} +.fa-venus-mars:before { + content: "\f228"; +} +.fa-mars-stroke:before { + content: "\f229"; +} +.fa-mars-stroke-v:before { + content: "\f22a"; +} +.fa-mars-stroke-h:before { + content: "\f22b"; +} +.fa-neuter:before { + content: "\f22c"; +} +.fa-genderless:before { + content: "\f22d"; +} +.fa-facebook-official:before { + content: "\f230"; +} +.fa-pinterest-p:before { + content: "\f231"; +} +.fa-whatsapp:before { + content: "\f232"; +} +.fa-server:before { + content: "\f233"; +} +.fa-user-plus:before { + content: "\f234"; +} +.fa-user-times:before { + content: "\f235"; +} +.fa-hotel:before, +.fa-bed:before { + content: "\f236"; +} +.fa-viacoin:before { + content: "\f237"; +} +.fa-train:before { + content: "\f238"; +} +.fa-subway:before { + content: "\f239"; +} +.fa-medium:before { + content: "\f23a"; +} +.fa-yc:before, +.fa-y-combinator:before { + content: "\f23b"; +} +.fa-optin-monster:before { + content: "\f23c"; +} +.fa-opencart:before { + content: "\f23d"; +} +.fa-expeditedssl:before { + content: "\f23e"; +} +.fa-battery-4:before, +.fa-battery-full:before { + content: "\f240"; +} +.fa-battery-3:before, +.fa-battery-three-quarters:before { + content: "\f241"; +} +.fa-battery-2:before, +.fa-battery-half:before { + content: "\f242"; +} +.fa-battery-1:before, +.fa-battery-quarter:before { + content: "\f243"; +} +.fa-battery-0:before, +.fa-battery-empty:before { + content: "\f244"; +} +.fa-mouse-pointer:before { + content: "\f245"; +} +.fa-i-cursor:before { + content: "\f246"; +} +.fa-object-group:before { + content: "\f247"; +} +.fa-object-ungroup:before { + content: "\f248"; +} +.fa-sticky-note:before { + content: "\f249"; +} +.fa-sticky-note-o:before { + content: "\f24a"; +} +.fa-cc-jcb:before { + content: "\f24b"; +} +.fa-cc-diners-club:before { + content: "\f24c"; +} +.fa-clone:before { + content: "\f24d"; +} +.fa-balance-scale:before { + content: "\f24e"; +} +.fa-hourglass-o:before { + content: "\f250"; +} +.fa-hourglass-1:before, +.fa-hourglass-start:before { + content: "\f251"; +} +.fa-hourglass-2:before, +.fa-hourglass-half:before { + content: "\f252"; +} +.fa-hourglass-3:before, +.fa-hourglass-end:before { + content: "\f253"; +} +.fa-hourglass:before { + content: "\f254"; +} +.fa-hand-grab-o:before, +.fa-hand-rock-o:before { + content: "\f255"; +} +.fa-hand-stop-o:before, +.fa-hand-paper-o:before { + content: "\f256"; +} +.fa-hand-scissors-o:before { + content: "\f257"; +} +.fa-hand-lizard-o:before { + content: "\f258"; +} +.fa-hand-spock-o:before { + content: "\f259"; +} +.fa-hand-pointer-o:before { + content: "\f25a"; +} +.fa-hand-peace-o:before { + content: "\f25b"; +} +.fa-trademark:before { + content: "\f25c"; +} +.fa-registered:before { + content: "\f25d"; +} +.fa-creative-commons:before { + content: "\f25e"; +} +.fa-gg:before { + content: "\f260"; +} +.fa-gg-circle:before { + content: "\f261"; +} +.fa-tripadvisor:before { + content: "\f262"; +} +.fa-odnoklassniki:before { + content: "\f263"; +} +.fa-odnoklassniki-square:before { + content: "\f264"; +} +.fa-get-pocket:before { + content: "\f265"; +} +.fa-wikipedia-w:before { + content: "\f266"; +} +.fa-safari:before { + content: "\f267"; +} +.fa-chrome:before { + content: "\f268"; +} +.fa-firefox:before { + content: "\f269"; +} +.fa-opera:before { + content: "\f26a"; +} +.fa-internet-explorer:before { + content: "\f26b"; +} +.fa-tv:before, +.fa-television:before { + content: "\f26c"; +} +.fa-contao:before { + content: "\f26d"; +} +.fa-500px:before { + content: "\f26e"; +} +.fa-amazon:before { + content: "\f270"; +} +.fa-calendar-plus-o:before { + content: "\f271"; +} +.fa-calendar-minus-o:before { + content: "\f272"; +} +.fa-calendar-times-o:before { + content: "\f273"; +} +.fa-calendar-check-o:before { + content: "\f274"; +} +.fa-industry:before { + content: "\f275"; +} +.fa-map-pin:before { + content: "\f276"; +} +.fa-map-signs:before { + content: "\f277"; +} +.fa-map-o:before { + content: "\f278"; +} +.fa-map:before { + content: "\f279"; +} +.fa-commenting:before { + content: "\f27a"; +} +.fa-commenting-o:before { + content: "\f27b"; +} +.fa-houzz:before { + content: "\f27c"; +} +.fa-vimeo:before { + content: "\f27d"; +} +.fa-black-tie:before { + content: "\f27e"; +} +.fa-fonticons:before { + content: "\f280"; +} +.fa-reddit-alien:before { + content: "\f281"; +} +.fa-edge:before { + content: "\f282"; +} +.fa-credit-card-alt:before { + content: "\f283"; +} +.fa-codiepie:before { + content: "\f284"; +} +.fa-modx:before { + content: "\f285"; +} +.fa-fort-awesome:before { + content: "\f286"; +} +.fa-usb:before { + content: "\f287"; +} +.fa-product-hunt:before { + content: "\f288"; +} +.fa-mixcloud:before { + content: "\f289"; +} +.fa-scribd:before { + content: "\f28a"; +} +.fa-pause-circle:before { + content: "\f28b"; +} +.fa-pause-circle-o:before { + content: "\f28c"; +} +.fa-stop-circle:before { + content: "\f28d"; +} +.fa-stop-circle-o:before { + content: "\f28e"; +} +.fa-shopping-bag:before { + content: "\f290"; +} +.fa-shopping-basket:before { + content: "\f291"; +} +.fa-hashtag:before { + content: "\f292"; +} +.fa-bluetooth:before { + content: "\f293"; +} +.fa-bluetooth-b:before { + content: "\f294"; +} +.fa-percent:before { + content: "\f295"; +} diff --git a/examples/blog/static/fonts/FontAwesome.otf b/examples/blog/static/fonts/FontAwesome.otf Binary files differnew file mode 100644 index 000000000..3ed7f8b48 --- /dev/null +++ b/examples/blog/static/fonts/FontAwesome.otf diff --git a/examples/blog/static/fonts/fontawesome-webfont.eot b/examples/blog/static/fonts/fontawesome-webfont.eot Binary files differnew file mode 100644 index 000000000..9b6afaedc --- /dev/null +++ b/examples/blog/static/fonts/fontawesome-webfont.eot diff --git a/examples/blog/static/fonts/fontawesome-webfont.svg b/examples/blog/static/fonts/fontawesome-webfont.svg new file mode 100644 index 000000000..d05688e9e --- /dev/null +++ b/examples/blog/static/fonts/fontawesome-webfont.svg @@ -0,0 +1,655 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" > +<svg xmlns="http://www.w3.org/2000/svg"> +<metadata></metadata> +<defs> +<font id="fontawesomeregular" horiz-adv-x="1536" > +<font-face units-per-em="1792" ascent="1536" descent="-256" /> +<missing-glyph horiz-adv-x="448" /> +<glyph unicode=" " horiz-adv-x="448" /> +<glyph unicode="	" horiz-adv-x="448" /> +<glyph unicode=" " horiz-adv-x="448" /> +<glyph unicode="¨" horiz-adv-x="1792" /> +<glyph unicode="©" horiz-adv-x="1792" /> +<glyph unicode="®" horiz-adv-x="1792" /> +<glyph unicode="´" horiz-adv-x="1792" /> +<glyph unicode="Æ" horiz-adv-x="1792" /> +<glyph unicode="Ø" horiz-adv-x="1792" /> +<glyph unicode=" " horiz-adv-x="768" /> +<glyph unicode=" " horiz-adv-x="1537" /> +<glyph unicode=" " horiz-adv-x="768" /> +<glyph unicode=" " horiz-adv-x="1537" /> +<glyph unicode=" " horiz-adv-x="512" /> +<glyph unicode=" " horiz-adv-x="384" /> +<glyph unicode=" " horiz-adv-x="256" /> +<glyph unicode=" " horiz-adv-x="256" /> +<glyph unicode=" " horiz-adv-x="192" /> +<glyph unicode=" " horiz-adv-x="307" /> +<glyph unicode=" " horiz-adv-x="85" /> +<glyph unicode=" " horiz-adv-x="307" /> +<glyph unicode=" " horiz-adv-x="384" /> +<glyph unicode="™" horiz-adv-x="1792" /> +<glyph unicode="∞" horiz-adv-x="1792" /> +<glyph unicode="≠" horiz-adv-x="1792" /> +<glyph unicode="◼" horiz-adv-x="500" d="M0 0z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1699 1350q0 -35 -43 -78l-632 -632v-768h320q26 0 45 -19t19 -45t-19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45t45 19h320v768l-632 632q-43 43 -43 78q0 23 18 36.5t38 17.5t43 4h1408q23 0 43 -4t38 -17.5t18 -36.5z" /> +<glyph unicode="" d="M1536 1312v-1120q0 -50 -34 -89t-86 -60.5t-103.5 -32t-96.5 -10.5t-96.5 10.5t-103.5 32t-86 60.5t-34 89t34 89t86 60.5t103.5 32t96.5 10.5q105 0 192 -39v537l-768 -237v-709q0 -50 -34 -89t-86 -60.5t-103.5 -32t-96.5 -10.5t-96.5 10.5t-103.5 32t-86 60.5t-34 89 t34 89t86 60.5t103.5 32t96.5 10.5q105 0 192 -39v967q0 31 19 56.5t49 35.5l832 256q12 4 28 4q40 0 68 -28t28 -68z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1152 704q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5zM1664 -128q0 -52 -38 -90t-90 -38q-54 0 -90 38l-343 342q-179 -124 -399 -124q-143 0 -273.5 55.5t-225 150t-150 225t-55.5 273.5 t55.5 273.5t150 225t225 150t273.5 55.5t273.5 -55.5t225 -150t150 -225t55.5 -273.5q0 -220 -124 -399l343 -343q37 -37 37 -90z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1664 32v768q-32 -36 -69 -66q-268 -206 -426 -338q-51 -43 -83 -67t-86.5 -48.5t-102.5 -24.5h-1h-1q-48 0 -102.5 24.5t-86.5 48.5t-83 67q-158 132 -426 338q-37 30 -69 66v-768q0 -13 9.5 -22.5t22.5 -9.5h1472q13 0 22.5 9.5t9.5 22.5zM1664 1083v11v13.5t-0.5 13 t-3 12.5t-5.5 9t-9 7.5t-14 2.5h-1472q-13 0 -22.5 -9.5t-9.5 -22.5q0 -168 147 -284q193 -152 401 -317q6 -5 35 -29.5t46 -37.5t44.5 -31.5t50.5 -27.5t43 -9h1h1q20 0 43 9t50.5 27.5t44.5 31.5t46 37.5t35 29.5q208 165 401 317q54 43 100.5 115.5t46.5 131.5z M1792 1120v-1088q0 -66 -47 -113t-113 -47h-1472q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1472q66 0 113 -47t47 -113z" /> +<glyph unicode="" horiz-adv-x="1792" d="M896 -128q-26 0 -44 18l-624 602q-10 8 -27.5 26t-55.5 65.5t-68 97.5t-53.5 121t-23.5 138q0 220 127 344t351 124q62 0 126.5 -21.5t120 -58t95.5 -68.5t76 -68q36 36 76 68t95.5 68.5t120 58t126.5 21.5q224 0 351 -124t127 -344q0 -221 -229 -450l-623 -600 q-18 -18 -44 -18z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1664 889q0 -22 -26 -48l-363 -354l86 -500q1 -7 1 -20q0 -21 -10.5 -35.5t-30.5 -14.5q-19 0 -40 12l-449 236l-449 -236q-22 -12 -40 -12q-21 0 -31.5 14.5t-10.5 35.5q0 6 2 20l86 500l-364 354q-25 27 -25 48q0 37 56 46l502 73l225 455q19 41 49 41t49 -41l225 -455 l502 -73q56 -9 56 -46z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1137 532l306 297l-422 62l-189 382l-189 -382l-422 -62l306 -297l-73 -421l378 199l377 -199zM1664 889q0 -22 -26 -48l-363 -354l86 -500q1 -7 1 -20q0 -50 -41 -50q-19 0 -40 12l-449 236l-449 -236q-22 -12 -40 -12q-21 0 -31.5 14.5t-10.5 35.5q0 6 2 20l86 500 l-364 354q-25 27 -25 48q0 37 56 46l502 73l225 455q19 41 49 41t49 -41l225 -455l502 -73q56 -9 56 -46z" /> +<glyph unicode="" horiz-adv-x="1408" d="M1408 131q0 -120 -73 -189.5t-194 -69.5h-874q-121 0 -194 69.5t-73 189.5q0 53 3.5 103.5t14 109t26.5 108.5t43 97.5t62 81t85.5 53.5t111.5 20q9 0 42 -21.5t74.5 -48t108 -48t133.5 -21.5t133.5 21.5t108 48t74.5 48t42 21.5q61 0 111.5 -20t85.5 -53.5t62 -81 t43 -97.5t26.5 -108.5t14 -109t3.5 -103.5zM1088 1024q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5z" /> +<glyph unicode="" horiz-adv-x="1920" d="M384 -64v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM384 320v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM384 704v128q0 26 -19 45t-45 19h-128 q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1408 -64v512q0 26 -19 45t-45 19h-768q-26 0 -45 -19t-19 -45v-512q0 -26 19 -45t45 -19h768q26 0 45 19t19 45zM384 1088v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45 t45 -19h128q26 0 45 19t19 45zM1792 -64v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1408 704v512q0 26 -19 45t-45 19h-768q-26 0 -45 -19t-19 -45v-512q0 -26 19 -45t45 -19h768q26 0 45 19t19 45zM1792 320v128 q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1792 704v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1792 1088v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19 t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1920 1248v-1344q0 -66 -47 -113t-113 -47h-1600q-66 0 -113 47t-47 113v1344q0 66 47 113t113 47h1600q66 0 113 -47t47 -113z" /> +<glyph unicode="" horiz-adv-x="1664" d="M768 512v-384q0 -52 -38 -90t-90 -38h-512q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h512q52 0 90 -38t38 -90zM768 1280v-384q0 -52 -38 -90t-90 -38h-512q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h512q52 0 90 -38t38 -90zM1664 512v-384q0 -52 -38 -90t-90 -38 h-512q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h512q52 0 90 -38t38 -90zM1664 1280v-384q0 -52 -38 -90t-90 -38h-512q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h512q52 0 90 -38t38 -90z" /> +<glyph unicode="" horiz-adv-x="1792" d="M512 288v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM512 800v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1152 288v-192q0 -40 -28 -68t-68 -28h-320 q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM512 1312v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1152 800v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28 h320q40 0 68 -28t28 -68zM1792 288v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1152 1312v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1792 800v-192 q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1792 1312v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68z" /> +<glyph unicode="" horiz-adv-x="1792" d="M512 288v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM512 800v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1792 288v-192q0 -40 -28 -68t-68 -28h-960 q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h960q40 0 68 -28t28 -68zM512 1312v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1792 800v-192q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v192q0 40 28 68t68 28 h960q40 0 68 -28t28 -68zM1792 1312v-192q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h960q40 0 68 -28t28 -68z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1671 970q0 -40 -28 -68l-724 -724l-136 -136q-28 -28 -68 -28t-68 28l-136 136l-362 362q-28 28 -28 68t28 68l136 136q28 28 68 28t68 -28l294 -295l656 657q28 28 68 28t68 -28l136 -136q28 -28 28 -68z" /> +<glyph unicode="" horiz-adv-x="1408" d="M1298 214q0 -40 -28 -68l-136 -136q-28 -28 -68 -28t-68 28l-294 294l-294 -294q-28 -28 -68 -28t-68 28l-136 136q-28 28 -28 68t28 68l294 294l-294 294q-28 28 -28 68t28 68l136 136q28 28 68 28t68 -28l294 -294l294 294q28 28 68 28t68 -28l136 -136q28 -28 28 -68 t-28 -68l-294 -294l294 -294q28 -28 28 -68z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1024 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-224v-224q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v224h-224q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h224v224q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5v-224h224 q13 0 22.5 -9.5t9.5 -22.5zM1152 704q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5zM1664 -128q0 -53 -37.5 -90.5t-90.5 -37.5q-54 0 -90 38l-343 342q-179 -124 -399 -124q-143 0 -273.5 55.5 t-225 150t-150 225t-55.5 273.5t55.5 273.5t150 225t225 150t273.5 55.5t273.5 -55.5t225 -150t150 -225t55.5 -273.5q0 -220 -124 -399l343 -343q37 -37 37 -90z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1024 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-576q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h576q13 0 22.5 -9.5t9.5 -22.5zM1152 704q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5z M1664 -128q0 -53 -37.5 -90.5t-90.5 -37.5q-54 0 -90 38l-343 342q-179 -124 -399 -124q-143 0 -273.5 55.5t-225 150t-150 225t-55.5 273.5t55.5 273.5t150 225t225 150t273.5 55.5t273.5 -55.5t225 -150t150 -225t55.5 -273.5q0 -220 -124 -399l343 -343q37 -37 37 -90z " /> +<glyph unicode="" d="M1536 640q0 -156 -61 -298t-164 -245t-245 -164t-298 -61t-298 61t-245 164t-164 245t-61 298q0 182 80.5 343t226.5 270q43 32 95.5 25t83.5 -50q32 -42 24.5 -94.5t-49.5 -84.5q-98 -74 -151.5 -181t-53.5 -228q0 -104 40.5 -198.5t109.5 -163.5t163.5 -109.5 t198.5 -40.5t198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5q0 121 -53.5 228t-151.5 181q-42 32 -49.5 84.5t24.5 94.5q31 43 84 50t95 -25q146 -109 226.5 -270t80.5 -343zM896 1408v-640q0 -52 -38 -90t-90 -38t-90 38t-38 90v640q0 52 38 90t90 38t90 -38t38 -90z" /> +<glyph unicode="" horiz-adv-x="1792" d="M256 96v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM640 224v-320q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v320q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1024 480v-576q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23 v576q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1408 864v-960q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v960q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1792 1376v-1472q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v1472q0 14 9 23t23 9h192q14 0 23 -9t9 -23z" /> +<glyph unicode="" d="M1024 640q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM1536 749v-222q0 -12 -8 -23t-20 -13l-185 -28q-19 -54 -39 -91q35 -50 107 -138q10 -12 10 -25t-9 -23q-27 -37 -99 -108t-94 -71q-12 0 -26 9l-138 108q-44 -23 -91 -38 q-16 -136 -29 -186q-7 -28 -36 -28h-222q-14 0 -24.5 8.5t-11.5 21.5l-28 184q-49 16 -90 37l-141 -107q-10 -9 -25 -9q-14 0 -25 11q-126 114 -165 168q-7 10 -7 23q0 12 8 23q15 21 51 66.5t54 70.5q-27 50 -41 99l-183 27q-13 2 -21 12.5t-8 23.5v222q0 12 8 23t19 13 l186 28q14 46 39 92q-40 57 -107 138q-10 12 -10 24q0 10 9 23q26 36 98.5 107.5t94.5 71.5q13 0 26 -10l138 -107q44 23 91 38q16 136 29 186q7 28 36 28h222q14 0 24.5 -8.5t11.5 -21.5l28 -184q49 -16 90 -37l142 107q9 9 24 9q13 0 25 -10q129 -119 165 -170q7 -8 7 -22 q0 -12 -8 -23q-15 -21 -51 -66.5t-54 -70.5q26 -50 41 -98l183 -28q13 -2 21 -12.5t8 -23.5z" /> +<glyph unicode="" horiz-adv-x="1408" d="M512 800v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM768 800v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1024 800v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576 q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1152 76v948h-896v-948q0 -22 7 -40.5t14.5 -27t10.5 -8.5h832q3 0 10.5 8.5t14.5 27t7 40.5zM480 1152h448l-48 117q-7 9 -17 11h-317q-10 -2 -17 -11zM1408 1120v-64q0 -14 -9 -23t-23 -9h-96v-948q0 -83 -47 -143.5t-113 -60.5h-832 q-66 0 -113 58.5t-47 141.5v952h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h309l70 167q15 37 54 63t79 26h320q40 0 79 -26t54 -63l70 -167h309q14 0 23 -9t9 -23z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1408 544v-480q0 -26 -19 -45t-45 -19h-384v384h-256v-384h-384q-26 0 -45 19t-19 45v480q0 1 0.5 3t0.5 3l575 474l575 -474q1 -2 1 -6zM1631 613l-62 -74q-8 -9 -21 -11h-3q-13 0 -21 7l-692 577l-692 -577q-12 -8 -24 -7q-13 2 -21 11l-62 74q-8 10 -7 23.5t11 21.5 l719 599q32 26 76 26t76 -26l244 -204v195q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-408l219 -182q10 -8 11 -21.5t-7 -23.5z" /> +<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z " /> +<glyph unicode="" d="M896 992v-448q0 -14 -9 -23t-23 -9h-320q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h224v352q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" horiz-adv-x="1920" d="M1111 540v4l-24 320q-1 13 -11 22.5t-23 9.5h-186q-13 0 -23 -9.5t-11 -22.5l-24 -320v-4q-1 -12 8 -20t21 -8h244q12 0 21 8t8 20zM1870 73q0 -73 -46 -73h-704q13 0 22 9.5t8 22.5l-20 256q-1 13 -11 22.5t-23 9.5h-272q-13 0 -23 -9.5t-11 -22.5l-20 -256 q-1 -13 8 -22.5t22 -9.5h-704q-46 0 -46 73q0 54 26 116l417 1044q8 19 26 33t38 14h339q-13 0 -23 -9.5t-11 -22.5l-15 -192q-1 -14 8 -23t22 -9h166q13 0 22 9t8 23l-15 192q-1 13 -11 22.5t-23 9.5h339q20 0 38 -14t26 -33l417 -1044q26 -62 26 -116z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1280 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1536 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 416v-320q0 -40 -28 -68t-68 -28h-1472q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h465l135 -136 q58 -56 136 -56t136 56l136 136h464q40 0 68 -28t28 -68zM1339 985q17 -41 -14 -70l-448 -448q-18 -19 -45 -19t-45 19l-448 448q-31 29 -14 70q17 39 59 39h256v448q0 26 19 45t45 19h256q26 0 45 -19t19 -45v-448h256q42 0 59 -39z" /> +<glyph unicode="" d="M1120 608q0 -12 -10 -24l-319 -319q-11 -9 -23 -9t-23 9l-320 320q-15 16 -7 35q8 20 30 20h192v352q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-352h192q14 0 23 -9t9 -23zM768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273 t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1118 660q-8 -20 -30 -20h-192v-352q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v352h-192q-14 0 -23 9t-9 23q0 12 10 24l319 319q11 9 23 9t23 -9l320 -320q15 -16 7 -35zM768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198 t73 273t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1023 576h316q-1 3 -2.5 8t-2.5 8l-212 496h-708l-212 -496q-1 -2 -2.5 -8t-2.5 -8h316l95 -192h320zM1536 546v-482q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v482q0 62 25 123l238 552q10 25 36.5 42t52.5 17h832q26 0 52.5 -17t36.5 -42l238 -552 q25 -61 25 -123z" /> +<glyph unicode="" d="M1184 640q0 -37 -32 -55l-544 -320q-15 -9 -32 -9q-16 0 -32 8q-32 19 -32 56v640q0 37 32 56q33 18 64 -1l544 -320q32 -18 32 -55zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1536 1280v-448q0 -26 -19 -45t-45 -19h-448q-42 0 -59 40q-17 39 14 69l138 138q-148 137 -349 137q-104 0 -198.5 -40.5t-163.5 -109.5t-109.5 -163.5t-40.5 -198.5t40.5 -198.5t109.5 -163.5t163.5 -109.5t198.5 -40.5q119 0 225 52t179 147q7 10 23 12q14 0 25 -9 l137 -138q9 -8 9.5 -20.5t-7.5 -22.5q-109 -132 -264 -204.5t-327 -72.5q-156 0 -298 61t-245 164t-164 245t-61 298t61 298t164 245t245 164t298 61q147 0 284.5 -55.5t244.5 -156.5l130 129q29 31 70 14q39 -17 39 -59z" /> +<glyph unicode="" d="M1511 480q0 -5 -1 -7q-64 -268 -268 -434.5t-478 -166.5q-146 0 -282.5 55t-243.5 157l-129 -129q-19 -19 -45 -19t-45 19t-19 45v448q0 26 19 45t45 19h448q26 0 45 -19t19 -45t-19 -45l-137 -137q71 -66 161 -102t187 -36q134 0 250 65t186 179q11 17 53 117 q8 23 30 23h192q13 0 22.5 -9.5t9.5 -22.5zM1536 1280v-448q0 -26 -19 -45t-45 -19h-448q-26 0 -45 19t-19 45t19 45l138 138q-148 137 -349 137q-134 0 -250 -65t-186 -179q-11 -17 -53 -117q-8 -23 -30 -23h-199q-13 0 -22.5 9.5t-9.5 22.5v7q65 268 270 434.5t480 166.5 q146 0 284 -55.5t245 -156.5l130 129q19 19 45 19t45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1792" d="M384 352v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 608v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M384 864v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1536 352v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-960q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h960q13 0 22.5 -9.5t9.5 -22.5z M1536 608v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-960q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h960q13 0 22.5 -9.5t9.5 -22.5zM1536 864v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-960q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h960q13 0 22.5 -9.5 t9.5 -22.5zM1664 160v832q0 13 -9.5 22.5t-22.5 9.5h-1472q-13 0 -22.5 -9.5t-9.5 -22.5v-832q0 -13 9.5 -22.5t22.5 -9.5h1472q13 0 22.5 9.5t9.5 22.5zM1792 1248v-1088q0 -66 -47 -113t-113 -47h-1472q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1472q66 0 113 -47 t47 -113z" /> +<glyph unicode="" horiz-adv-x="1152" d="M320 768h512v192q0 106 -75 181t-181 75t-181 -75t-75 -181v-192zM1152 672v-576q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v576q0 40 28 68t68 28h32v192q0 184 132 316t316 132t316 -132t132 -316v-192h32q40 0 68 -28t28 -68z" /> +<glyph unicode="" horiz-adv-x="1792" d="M320 1280q0 -72 -64 -110v-1266q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v1266q-64 38 -64 110q0 53 37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1792 1216v-763q0 -25 -12.5 -38.5t-39.5 -27.5q-215 -116 -369 -116q-61 0 -123.5 22t-108.5 48 t-115.5 48t-142.5 22q-192 0 -464 -146q-17 -9 -33 -9q-26 0 -45 19t-19 45v742q0 32 31 55q21 14 79 43q236 120 421 120q107 0 200 -29t219 -88q38 -19 88 -19q54 0 117.5 21t110 47t88 47t54.5 21q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1664 650q0 -166 -60 -314l-20 -49l-185 -33q-22 -83 -90.5 -136.5t-156.5 -53.5v-32q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-32q71 0 130 -35.5t93 -95.5l68 12q29 95 29 193q0 148 -88 279t-236.5 209t-315.5 78 t-315.5 -78t-236.5 -209t-88 -279q0 -98 29 -193l68 -12q34 60 93 95.5t130 35.5v32q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v32q-88 0 -156.5 53.5t-90.5 136.5l-185 33l-20 49q-60 148 -60 314q0 151 67 291t179 242.5 t266 163.5t320 61t320 -61t266 -163.5t179 -242.5t67 -291z" /> +<glyph unicode="" horiz-adv-x="768" d="M768 1184v-1088q0 -26 -19 -45t-45 -19t-45 19l-333 333h-262q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h262l333 333q19 19 45 19t45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1152" d="M768 1184v-1088q0 -26 -19 -45t-45 -19t-45 19l-333 333h-262q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h262l333 333q19 19 45 19t45 -19t19 -45zM1152 640q0 -76 -42.5 -141.5t-112.5 -93.5q-10 -5 -25 -5q-26 0 -45 18.5t-19 45.5q0 21 12 35.5t29 25t34 23t29 35.5 t12 57t-12 57t-29 35.5t-34 23t-29 25t-12 35.5q0 27 19 45.5t45 18.5q15 0 25 -5q70 -27 112.5 -93t42.5 -142z" /> +<glyph unicode="" horiz-adv-x="1664" d="M768 1184v-1088q0 -26 -19 -45t-45 -19t-45 19l-333 333h-262q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h262l333 333q19 19 45 19t45 -19t19 -45zM1152 640q0 -76 -42.5 -141.5t-112.5 -93.5q-10 -5 -25 -5q-26 0 -45 18.5t-19 45.5q0 21 12 35.5t29 25t34 23t29 35.5 t12 57t-12 57t-29 35.5t-34 23t-29 25t-12 35.5q0 27 19 45.5t45 18.5q15 0 25 -5q70 -27 112.5 -93t42.5 -142zM1408 640q0 -153 -85 -282.5t-225 -188.5q-13 -5 -25 -5q-27 0 -46 19t-19 45q0 39 39 59q56 29 76 44q74 54 115.5 135.5t41.5 173.5t-41.5 173.5 t-115.5 135.5q-20 15 -76 44q-39 20 -39 59q0 26 19 45t45 19q13 0 26 -5q140 -59 225 -188.5t85 -282.5zM1664 640q0 -230 -127 -422.5t-338 -283.5q-13 -5 -26 -5q-26 0 -45 19t-19 45q0 36 39 59q7 4 22.5 10.5t22.5 10.5q46 25 82 51q123 91 192 227t69 289t-69 289 t-192 227q-36 26 -82 51q-7 4 -22.5 10.5t-22.5 10.5q-39 23 -39 59q0 26 19 45t45 19q13 0 26 -5q211 -91 338 -283.5t127 -422.5z" /> +<glyph unicode="" horiz-adv-x="1408" d="M384 384v-128h-128v128h128zM384 1152v-128h-128v128h128zM1152 1152v-128h-128v128h128zM128 129h384v383h-384v-383zM128 896h384v384h-384v-384zM896 896h384v384h-384v-384zM640 640v-640h-640v640h640zM1152 128v-128h-128v128h128zM1408 128v-128h-128v128h128z M1408 640v-384h-384v128h-128v-384h-128v640h384v-128h128v128h128zM640 1408v-640h-640v640h640zM1408 1408v-640h-640v640h640z" /> +<glyph unicode="" horiz-adv-x="1792" d="M63 0h-63v1408h63v-1408zM126 1h-32v1407h32v-1407zM220 1h-31v1407h31v-1407zM377 1h-31v1407h31v-1407zM534 1h-62v1407h62v-1407zM660 1h-31v1407h31v-1407zM723 1h-31v1407h31v-1407zM786 1h-31v1407h31v-1407zM943 1h-63v1407h63v-1407zM1100 1h-63v1407h63v-1407z M1226 1h-63v1407h63v-1407zM1352 1h-63v1407h63v-1407zM1446 1h-63v1407h63v-1407zM1635 1h-94v1407h94v-1407zM1698 1h-32v1407h32v-1407zM1792 0h-63v1408h63v-1408z" /> +<glyph unicode="" d="M448 1088q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1515 512q0 -53 -37 -90l-491 -492q-39 -37 -91 -37q-53 0 -90 37l-715 716q-38 37 -64.5 101t-26.5 117v416q0 52 38 90t90 38h416q53 0 117 -26.5t102 -64.5 l715 -714q37 -39 37 -91z" /> +<glyph unicode="" horiz-adv-x="1920" d="M448 1088q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1515 512q0 -53 -37 -90l-491 -492q-39 -37 -91 -37q-53 0 -90 37l-715 716q-38 37 -64.5 101t-26.5 117v416q0 52 38 90t90 38h416q53 0 117 -26.5t102 -64.5 l715 -714q37 -39 37 -91zM1899 512q0 -53 -37 -90l-491 -492q-39 -37 -91 -37q-36 0 -59 14t-53 45l470 470q37 37 37 90q0 52 -37 91l-715 714q-38 38 -102 64.5t-117 26.5h224q53 0 117 -26.5t102 -64.5l715 -714q37 -39 37 -91z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1639 1058q40 -57 18 -129l-275 -906q-19 -64 -76.5 -107.5t-122.5 -43.5h-923q-77 0 -148.5 53.5t-99.5 131.5q-24 67 -2 127q0 4 3 27t4 37q1 8 -3 21.5t-3 19.5q2 11 8 21t16.5 23.5t16.5 23.5q23 38 45 91.5t30 91.5q3 10 0.5 30t-0.5 28q3 11 17 28t17 23 q21 36 42 92t25 90q1 9 -2.5 32t0.5 28q4 13 22 30.5t22 22.5q19 26 42.5 84.5t27.5 96.5q1 8 -3 25.5t-2 26.5q2 8 9 18t18 23t17 21q8 12 16.5 30.5t15 35t16 36t19.5 32t26.5 23.5t36 11.5t47.5 -5.5l-1 -3q38 9 51 9h761q74 0 114 -56t18 -130l-274 -906 q-36 -119 -71.5 -153.5t-128.5 -34.5h-869q-27 0 -38 -15q-11 -16 -1 -43q24 -70 144 -70h923q29 0 56 15.5t35 41.5l300 987q7 22 5 57q38 -15 59 -43zM575 1056q-4 -13 2 -22.5t20 -9.5h608q13 0 25.5 9.5t16.5 22.5l21 64q4 13 -2 22.5t-20 9.5h-608q-13 0 -25.5 -9.5 t-16.5 -22.5zM492 800q-4 -13 2 -22.5t20 -9.5h608q13 0 25.5 9.5t16.5 22.5l21 64q4 13 -2 22.5t-20 9.5h-608q-13 0 -25.5 -9.5t-16.5 -22.5z" /> +<glyph unicode="" horiz-adv-x="1280" d="M1164 1408q23 0 44 -9q33 -13 52.5 -41t19.5 -62v-1289q0 -34 -19.5 -62t-52.5 -41q-19 -8 -44 -8q-48 0 -83 32l-441 424l-441 -424q-36 -33 -83 -33q-23 0 -44 9q-33 13 -52.5 41t-19.5 62v1289q0 34 19.5 62t52.5 41q21 9 44 9h1048z" /> +<glyph unicode="" horiz-adv-x="1664" d="M384 0h896v256h-896v-256zM384 640h896v384h-160q-40 0 -68 28t-28 68v160h-640v-640zM1536 576q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 576v-416q0 -13 -9.5 -22.5t-22.5 -9.5h-224v-160q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68 v160h-224q-13 0 -22.5 9.5t-9.5 22.5v416q0 79 56.5 135.5t135.5 56.5h64v544q0 40 28 68t68 28h672q40 0 88 -20t76 -48l152 -152q28 -28 48 -76t20 -88v-256h64q79 0 135.5 -56.5t56.5 -135.5z" /> +<glyph unicode="" horiz-adv-x="1920" d="M960 864q119 0 203.5 -84.5t84.5 -203.5t-84.5 -203.5t-203.5 -84.5t-203.5 84.5t-84.5 203.5t84.5 203.5t203.5 84.5zM1664 1280q106 0 181 -75t75 -181v-896q0 -106 -75 -181t-181 -75h-1408q-106 0 -181 75t-75 181v896q0 106 75 181t181 75h224l51 136 q19 49 69.5 84.5t103.5 35.5h512q53 0 103.5 -35.5t69.5 -84.5l51 -136h224zM960 128q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" /> +<glyph unicode="" horiz-adv-x="1664" d="M725 977l-170 -450q33 0 136.5 -2t160.5 -2q19 0 57 2q-87 253 -184 452zM0 -128l2 79q23 7 56 12.5t57 10.5t49.5 14.5t44.5 29t31 50.5l237 616l280 724h75h53q8 -14 11 -21l205 -480q33 -78 106 -257.5t114 -274.5q15 -34 58 -144.5t72 -168.5q20 -45 35 -57 q19 -15 88 -29.5t84 -20.5q6 -38 6 -57q0 -4 -0.5 -13t-0.5 -13q-63 0 -190 8t-191 8q-76 0 -215 -7t-178 -8q0 43 4 78l131 28q1 0 12.5 2.5t15.5 3.5t14.5 4.5t15 6.5t11 8t9 11t2.5 14q0 16 -31 96.5t-72 177.5t-42 100l-450 2q-26 -58 -76.5 -195.5t-50.5 -162.5 q0 -22 14 -37.5t43.5 -24.5t48.5 -13.5t57 -8.5t41 -4q1 -19 1 -58q0 -9 -2 -27q-58 0 -174.5 10t-174.5 10q-8 0 -26.5 -4t-21.5 -4q-80 -14 -188 -14z" /> +<glyph unicode="" horiz-adv-x="1408" d="M555 15q74 -32 140 -32q376 0 376 335q0 114 -41 180q-27 44 -61.5 74t-67.5 46.5t-80.5 25t-84 10.5t-94.5 2q-73 0 -101 -10q0 -53 -0.5 -159t-0.5 -158q0 -8 -1 -67.5t-0.5 -96.5t4.5 -83.5t12 -66.5zM541 761q42 -7 109 -7q82 0 143 13t110 44.5t74.5 89.5t25.5 142 q0 70 -29 122.5t-79 82t-108 43.5t-124 14q-50 0 -130 -13q0 -50 4 -151t4 -152q0 -27 -0.5 -80t-0.5 -79q0 -46 1 -69zM0 -128l2 94q15 4 85 16t106 27q7 12 12.5 27t8.5 33.5t5.5 32.5t3 37.5t0.5 34v35.5v30q0 982 -22 1025q-4 8 -22 14.5t-44.5 11t-49.5 7t-48.5 4.5 t-30.5 3l-4 83q98 2 340 11.5t373 9.5q23 0 68.5 -0.5t67.5 -0.5q70 0 136.5 -13t128.5 -42t108 -71t74 -104.5t28 -137.5q0 -52 -16.5 -95.5t-39 -72t-64.5 -57.5t-73 -45t-84 -40q154 -35 256.5 -134t102.5 -248q0 -100 -35 -179.5t-93.5 -130.5t-138 -85.5t-163.5 -48.5 t-176 -14q-44 0 -132 3t-132 3q-106 0 -307 -11t-231 -12z" /> +<glyph unicode="" horiz-adv-x="1024" d="M0 -126l17 85q6 2 81.5 21.5t111.5 37.5q28 35 41 101q1 7 62 289t114 543.5t52 296.5v25q-24 13 -54.5 18.5t-69.5 8t-58 5.5l19 103q33 -2 120 -6.5t149.5 -7t120.5 -2.5q48 0 98.5 2.5t121 7t98.5 6.5q-5 -39 -19 -89q-30 -10 -101.5 -28.5t-108.5 -33.5 q-8 -19 -14 -42.5t-9 -40t-7.5 -45.5t-6.5 -42q-27 -148 -87.5 -419.5t-77.5 -355.5q-2 -9 -13 -58t-20 -90t-16 -83.5t-6 -57.5l1 -18q17 -4 185 -31q-3 -44 -16 -99q-11 0 -32.5 -1.5t-32.5 -1.5q-29 0 -87 10t-86 10q-138 2 -206 2q-51 0 -143 -9t-121 -11z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1744 128q33 0 42 -18.5t-11 -44.5l-126 -162q-20 -26 -49 -26t-49 26l-126 162q-20 26 -11 44.5t42 18.5h80v1024h-80q-33 0 -42 18.5t11 44.5l126 162q20 26 49 26t49 -26l126 -162q20 -26 11 -44.5t-42 -18.5h-80v-1024h80zM81 1407l54 -27q12 -5 211 -5q44 0 132 2 t132 2q36 0 107.5 -0.5t107.5 -0.5h293q6 0 21 -0.5t20.5 0t16 3t17.5 9t15 17.5l42 1q4 0 14 -0.5t14 -0.5q2 -112 2 -336q0 -80 -5 -109q-39 -14 -68 -18q-25 44 -54 128q-3 9 -11 48t-14.5 73.5t-7.5 35.5q-6 8 -12 12.5t-15.5 6t-13 2.5t-18 0.5t-16.5 -0.5 q-17 0 -66.5 0.5t-74.5 0.5t-64 -2t-71 -6q-9 -81 -8 -136q0 -94 2 -388t2 -455q0 -16 -2.5 -71.5t0 -91.5t12.5 -69q40 -21 124 -42.5t120 -37.5q5 -40 5 -50q0 -14 -3 -29l-34 -1q-76 -2 -218 8t-207 10q-50 0 -151 -9t-152 -9q-3 51 -3 52v9q17 27 61.5 43t98.5 29t78 27 q19 42 19 383q0 101 -3 303t-3 303v117q0 2 0.5 15.5t0.5 25t-1 25.5t-3 24t-5 14q-11 12 -162 12q-33 0 -93 -12t-80 -26q-19 -13 -34 -72.5t-31.5 -111t-42.5 -53.5q-42 26 -56 44v383z" /> +<glyph unicode="" d="M81 1407l54 -27q12 -5 211 -5q44 0 132 2t132 2q70 0 246.5 1t304.5 0.5t247 -4.5q33 -1 56 31l42 1q4 0 14 -0.5t14 -0.5q2 -112 2 -336q0 -80 -5 -109q-39 -14 -68 -18q-25 44 -54 128q-3 9 -11 47.5t-15 73.5t-7 36q-10 13 -27 19q-5 2 -66 2q-30 0 -93 1t-103 1 t-94 -2t-96 -7q-9 -81 -8 -136l1 -152v52q0 -55 1 -154t1.5 -180t0.5 -153q0 -16 -2.5 -71.5t0 -91.5t12.5 -69q40 -21 124 -42.5t120 -37.5q5 -40 5 -50q0 -14 -3 -29l-34 -1q-76 -2 -218 8t-207 10q-50 0 -151 -9t-152 -9q-3 51 -3 52v9q17 27 61.5 43t98.5 29t78 27 q7 16 11.5 74t6 145.5t1.5 155t-0.5 153.5t-0.5 89q0 7 -2.5 21.5t-2.5 22.5q0 7 0.5 44t1 73t0 76.5t-3 67.5t-6.5 32q-11 12 -162 12q-41 0 -163 -13.5t-138 -24.5q-19 -12 -34 -71.5t-31.5 -111.5t-42.5 -54q-42 26 -56 44v383zM1310 125q12 0 42 -19.5t57.5 -41.5 t59.5 -49t36 -30q26 -21 26 -49t-26 -49q-4 -3 -36 -30t-59.5 -49t-57.5 -41.5t-42 -19.5q-13 0 -20.5 10.5t-10 28.5t-2.5 33.5t1.5 33t1.5 19.5h-1024q0 -2 1.5 -19.5t1.5 -33t-2.5 -33.5t-10 -28.5t-20.5 -10.5q-12 0 -42 19.5t-57.5 41.5t-59.5 49t-36 30q-26 21 -26 49 t26 49q4 3 36 30t59.5 49t57.5 41.5t42 19.5q13 0 20.5 -10.5t10 -28.5t2.5 -33.5t-1.5 -33t-1.5 -19.5h1024q0 2 -1.5 19.5t-1.5 33t2.5 33.5t10 28.5t20.5 10.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1408 576v-128q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1280q26 0 45 -19t19 -45zM1664 960v-128q0 -26 -19 -45 t-45 -19h-1536q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1536q26 0 45 -19t19 -45zM1280 1344v-128q0 -26 -19 -45t-45 -19h-1152q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1408 576v-128q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h896q26 0 45 -19t19 -45zM1664 960v-128q0 -26 -19 -45t-45 -19 h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1280 1344v-128q0 -26 -19 -45t-45 -19h-640q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h640q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 576v-128q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1280q26 0 45 -19t19 -45zM1792 960v-128q0 -26 -19 -45 t-45 -19h-1536q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1536q26 0 45 -19t19 -45zM1792 1344v-128q0 -26 -19 -45t-45 -19h-1152q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 576v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 960v-128q0 -26 -19 -45 t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 1344v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1792" d="M256 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5t9.5 -22.5zM256 608v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5 t9.5 -22.5zM256 992v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5t9.5 -22.5zM1792 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1344 q13 0 22.5 -9.5t9.5 -22.5zM256 1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5t9.5 -22.5zM1792 608v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5 t22.5 9.5h1344q13 0 22.5 -9.5t9.5 -22.5zM1792 992v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1344q13 0 22.5 -9.5t9.5 -22.5zM1792 1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v192 q0 13 9.5 22.5t22.5 9.5h1344q13 0 22.5 -9.5t9.5 -22.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M384 992v-576q0 -13 -9.5 -22.5t-22.5 -9.5q-14 0 -23 9l-288 288q-9 9 -9 23t9 23l288 288q9 9 23 9q13 0 22.5 -9.5t9.5 -22.5zM1792 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1728q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1728q13 0 22.5 -9.5 t9.5 -22.5zM1792 608v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1088q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1088q13 0 22.5 -9.5t9.5 -22.5zM1792 992v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1088q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1088 q13 0 22.5 -9.5t9.5 -22.5zM1792 1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1728q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1728q13 0 22.5 -9.5t9.5 -22.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M352 704q0 -14 -9 -23l-288 -288q-9 -9 -23 -9q-13 0 -22.5 9.5t-9.5 22.5v576q0 13 9.5 22.5t22.5 9.5q14 0 23 -9l288 -288q9 -9 9 -23zM1792 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1728q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1728q13 0 22.5 -9.5 t9.5 -22.5zM1792 608v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1088q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1088q13 0 22.5 -9.5t9.5 -22.5zM1792 992v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1088q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1088 q13 0 22.5 -9.5t9.5 -22.5zM1792 1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1728q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1728q13 0 22.5 -9.5t9.5 -22.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1792 1184v-1088q0 -42 -39 -59q-13 -5 -25 -5q-27 0 -45 19l-403 403v-166q0 -119 -84.5 -203.5t-203.5 -84.5h-704q-119 0 -203.5 84.5t-84.5 203.5v704q0 119 84.5 203.5t203.5 84.5h704q119 0 203.5 -84.5t84.5 -203.5v-165l403 402q18 19 45 19q12 0 25 -5 q39 -17 39 -59z" /> +<glyph unicode="" horiz-adv-x="1920" d="M640 960q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1664 576v-448h-1408v192l320 320l160 -160l512 512zM1760 1280h-1600q-13 0 -22.5 -9.5t-9.5 -22.5v-1216q0 -13 9.5 -22.5t22.5 -9.5h1600q13 0 22.5 9.5t9.5 22.5v1216 q0 13 -9.5 22.5t-22.5 9.5zM1920 1248v-1216q0 -66 -47 -113t-113 -47h-1600q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1600q66 0 113 -47t47 -113z" /> +<glyph unicode="" d="M363 0l91 91l-235 235l-91 -91v-107h128v-128h107zM886 928q0 22 -22 22q-10 0 -17 -7l-542 -542q-7 -7 -7 -17q0 -22 22 -22q10 0 17 7l542 542q7 7 7 17zM832 1120l416 -416l-832 -832h-416v416zM1515 1024q0 -53 -37 -90l-166 -166l-416 416l166 165q36 38 90 38 q53 0 91 -38l235 -234q37 -39 37 -91z" /> +<glyph unicode="" horiz-adv-x="1024" d="M768 896q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM1024 896q0 -109 -33 -179l-364 -774q-16 -33 -47.5 -52t-67.5 -19t-67.5 19t-46.5 52l-365 774q-33 70 -33 179q0 212 150 362t362 150t362 -150t150 -362z" /> +<glyph unicode="" d="M768 96v1088q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" horiz-adv-x="1024" d="M512 384q0 36 -20 69q-1 1 -15.5 22.5t-25.5 38t-25 44t-21 50.5q-4 16 -21 16t-21 -16q-7 -23 -21 -50.5t-25 -44t-25.5 -38t-15.5 -22.5q-20 -33 -20 -69q0 -53 37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1024 512q0 -212 -150 -362t-362 -150t-362 150t-150 362 q0 145 81 275q6 9 62.5 90.5t101 151t99.5 178t83 201.5q9 30 34 47t51 17t51.5 -17t33.5 -47q28 -93 83 -201.5t99.5 -178t101 -151t62.5 -90.5q81 -127 81 -275z" /> +<glyph unicode="" horiz-adv-x="1792" d="M888 352l116 116l-152 152l-116 -116v-56h96v-96h56zM1328 1072q-16 16 -33 -1l-350 -350q-17 -17 -1 -33t33 1l350 350q17 17 1 33zM1408 478v-190q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832 q63 0 117 -25q15 -7 18 -23q3 -17 -9 -29l-49 -49q-14 -14 -32 -8q-23 6 -45 6h-832q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v126q0 13 9 22l64 64q15 15 35 7t20 -29zM1312 1216l288 -288l-672 -672h-288v288zM1756 1084l-92 -92 l-288 288l92 92q28 28 68 28t68 -28l152 -152q28 -28 28 -68t-28 -68z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1408 547v-259q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h255v0q13 0 22.5 -9.5t9.5 -22.5q0 -27 -26 -32q-77 -26 -133 -60q-10 -4 -16 -4h-112q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832 q66 0 113 47t47 113v214q0 19 18 29q28 13 54 37q16 16 35 8q21 -9 21 -29zM1645 1043l-384 -384q-18 -19 -45 -19q-12 0 -25 5q-39 17 -39 59v192h-160q-323 0 -438 -131q-119 -137 -74 -473q3 -23 -20 -34q-8 -2 -12 -2q-16 0 -26 13q-10 14 -21 31t-39.5 68.5t-49.5 99.5 t-38.5 114t-17.5 122q0 49 3.5 91t14 90t28 88t47 81.5t68.5 74t94.5 61.5t124.5 48.5t159.5 30.5t196.5 11h160v192q0 42 39 59q13 5 25 5q26 0 45 -19l384 -384q19 -19 19 -45t-19 -45z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1408 606v-318q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832q63 0 117 -25q15 -7 18 -23q3 -17 -9 -29l-49 -49q-10 -10 -23 -10q-3 0 -9 2q-23 6 -45 6h-832q-66 0 -113 -47t-47 -113v-832 q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v254q0 13 9 22l64 64q10 10 23 10q6 0 12 -3q20 -8 20 -29zM1639 1095l-814 -814q-24 -24 -57 -24t-57 24l-430 430q-24 24 -24 57t24 57l110 110q24 24 57 24t57 -24l263 -263l647 647q24 24 57 24t57 -24l110 -110 q24 -24 24 -57t-24 -57z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1792 640q0 -26 -19 -45l-256 -256q-19 -19 -45 -19t-45 19t-19 45v128h-384v-384h128q26 0 45 -19t19 -45t-19 -45l-256 -256q-19 -19 -45 -19t-45 19l-256 256q-19 19 -19 45t19 45t45 19h128v384h-384v-128q0 -26 -19 -45t-45 -19t-45 19l-256 256q-19 19 -19 45 t19 45l256 256q19 19 45 19t45 -19t19 -45v-128h384v384h-128q-26 0 -45 19t-19 45t19 45l256 256q19 19 45 19t45 -19l256 -256q19 -19 19 -45t-19 -45t-45 -19h-128v-384h384v128q0 26 19 45t45 19t45 -19l256 -256q19 -19 19 -45z" /> +<glyph unicode="" horiz-adv-x="1024" d="M979 1395q19 19 32 13t13 -32v-1472q0 -26 -13 -32t-32 13l-710 710q-9 9 -13 19v-678q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-678q4 11 13 19z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1747 1395q19 19 32 13t13 -32v-1472q0 -26 -13 -32t-32 13l-710 710q-9 9 -13 19v-710q0 -26 -13 -32t-32 13l-710 710q-9 9 -13 19v-678q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-678q4 11 13 19l710 710 q19 19 32 13t13 -32v-710q4 11 13 19z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1619 1395q19 19 32 13t13 -32v-1472q0 -26 -13 -32t-32 13l-710 710q-8 9 -13 19v-710q0 -26 -13 -32t-32 13l-710 710q-19 19 -19 45t19 45l710 710q19 19 32 13t13 -32v-710q5 11 13 19z" /> +<glyph unicode="" horiz-adv-x="1408" d="M1384 609l-1328 -738q-23 -13 -39.5 -3t-16.5 36v1472q0 26 16.5 36t39.5 -3l1328 -738q23 -13 23 -31t-23 -31z" /> +<glyph unicode="" d="M1536 1344v-1408q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h512q26 0 45 -19t19 -45zM640 1344v-1408q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h512q26 0 45 -19t19 -45z" /> +<glyph unicode="" d="M1536 1344v-1408q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h1408q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1664" d="M45 -115q-19 -19 -32 -13t-13 32v1472q0 26 13 32t32 -13l710 -710q8 -8 13 -19v710q0 26 13 32t32 -13l710 -710q19 -19 19 -45t-19 -45l-710 -710q-19 -19 -32 -13t-13 32v710q-5 -10 -13 -19z" /> +<glyph unicode="" horiz-adv-x="1792" d="M45 -115q-19 -19 -32 -13t-13 32v1472q0 26 13 32t32 -13l710 -710q8 -8 13 -19v710q0 26 13 32t32 -13l710 -710q8 -8 13 -19v678q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-1408q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v678q-5 -10 -13 -19l-710 -710 q-19 -19 -32 -13t-13 32v710q-5 -10 -13 -19z" /> +<glyph unicode="" horiz-adv-x="1024" d="M45 -115q-19 -19 -32 -13t-13 32v1472q0 26 13 32t32 -13l710 -710q8 -8 13 -19v678q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-1408q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v678q-5 -10 -13 -19z" /> +<glyph unicode="" horiz-adv-x="1538" d="M14 557l710 710q19 19 45 19t45 -19l710 -710q19 -19 13 -32t-32 -13h-1472q-26 0 -32 13t13 32zM1473 0h-1408q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h1408q26 0 45 -19t19 -45v-256q0 -26 -19 -45t-45 -19z" /> +<glyph unicode="" horiz-adv-x="1280" d="M1171 1235l-531 -531l531 -531q19 -19 19 -45t-19 -45l-166 -166q-19 -19 -45 -19t-45 19l-742 742q-19 19 -19 45t19 45l742 742q19 19 45 19t45 -19l166 -166q19 -19 19 -45t-19 -45z" /> +<glyph unicode="" horiz-adv-x="1280" d="M1107 659l-742 -742q-19 -19 -45 -19t-45 19l-166 166q-19 19 -19 45t19 45l531 531l-531 531q-19 19 -19 45t19 45l166 166q19 19 45 19t45 -19l742 -742q19 -19 19 -45t-19 -45z" /> +<glyph unicode="" d="M1216 576v128q0 26 -19 45t-45 19h-256v256q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-256h-256q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h256v-256q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v256h256q26 0 45 19t19 45zM1536 640q0 -209 -103 -385.5 t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1216 576v128q0 26 -19 45t-45 19h-768q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h768q26 0 45 19t19 45zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5 t103 -385.5z" /> +<glyph unicode="" d="M1149 414q0 26 -19 45l-181 181l181 181q19 19 19 45q0 27 -19 46l-90 90q-19 19 -46 19q-26 0 -45 -19l-181 -181l-181 181q-19 19 -45 19q-27 0 -46 -19l-90 -90q-19 -19 -19 -46q0 -26 19 -45l181 -181l-181 -181q-19 -19 -19 -45q0 -27 19 -46l90 -90q19 -19 46 -19 q26 0 45 19l181 181l181 -181q19 -19 45 -19q27 0 46 19l90 90q19 19 19 46zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1284 802q0 28 -18 46l-91 90q-19 19 -45 19t-45 -19l-408 -407l-226 226q-19 19 -45 19t-45 -19l-91 -90q-18 -18 -18 -46q0 -27 18 -45l362 -362q19 -19 45 -19q27 0 46 19l543 543q18 18 18 45zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103 t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M896 160v192q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h192q14 0 23 9t9 23zM1152 832q0 88 -55.5 163t-138.5 116t-170 41q-243 0 -371 -213q-15 -24 8 -42l132 -100q7 -6 19 -6q16 0 25 12q53 68 86 92q34 24 86 24q48 0 85.5 -26t37.5 -59 q0 -38 -20 -61t-68 -45q-63 -28 -115.5 -86.5t-52.5 -125.5v-36q0 -14 9 -23t23 -9h192q14 0 23 9t9 23q0 19 21.5 49.5t54.5 49.5q32 18 49 28.5t46 35t44.5 48t28 60.5t12.5 81zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1024 160v160q0 14 -9 23t-23 9h-96v512q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-160q0 -14 9 -23t23 -9h96v-320h-96q-14 0 -23 -9t-9 -23v-160q0 -14 9 -23t23 -9h448q14 0 23 9t9 23zM896 1056v160q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-160q0 -14 9 -23 t23 -9h192q14 0 23 9t9 23zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1197 512h-109q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h109q-32 108 -112.5 188.5t-188.5 112.5v-109q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v109q-108 -32 -188.5 -112.5t-112.5 -188.5h109q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-109 q32 -108 112.5 -188.5t188.5 -112.5v109q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-109q108 32 188.5 112.5t112.5 188.5zM1536 704v-128q0 -26 -19 -45t-45 -19h-143q-37 -161 -154.5 -278.5t-278.5 -154.5v-143q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v143 q-161 37 -278.5 154.5t-154.5 278.5h-143q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h143q37 161 154.5 278.5t278.5 154.5v143q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-143q161 -37 278.5 -154.5t154.5 -278.5h143q26 0 45 -19t19 -45z" /> +<glyph unicode="" d="M1097 457l-146 -146q-10 -10 -23 -10t-23 10l-137 137l-137 -137q-10 -10 -23 -10t-23 10l-146 146q-10 10 -10 23t10 23l137 137l-137 137q-10 10 -10 23t10 23l146 146q10 10 23 10t23 -10l137 -137l137 137q10 10 23 10t23 -10l146 -146q10 -10 10 -23t-10 -23 l-137 -137l137 -137q10 -10 10 -23t-10 -23zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5 t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1171 723l-422 -422q-19 -19 -45 -19t-45 19l-294 294q-19 19 -19 45t19 45l102 102q19 19 45 19t45 -19l147 -147l275 275q19 19 45 19t45 -19l102 -102q19 -19 19 -45t-19 -45zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198 t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1312 643q0 161 -87 295l-754 -753q137 -89 297 -89q111 0 211.5 43.5t173.5 116.5t116 174.5t43 212.5zM313 344l755 754q-135 91 -300 91q-148 0 -273 -73t-198 -199t-73 -274q0 -162 89 -299zM1536 643q0 -157 -61 -300t-163.5 -246t-245 -164t-298.5 -61t-298.5 61 t-245 164t-163.5 246t-61 300t61 299.5t163.5 245.5t245 164t298.5 61t298.5 -61t245 -164t163.5 -245.5t61 -299.5z" /> +<glyph unicode="" d="M1536 640v-128q0 -53 -32.5 -90.5t-84.5 -37.5h-704l293 -294q38 -36 38 -90t-38 -90l-75 -76q-37 -37 -90 -37q-52 0 -91 37l-651 652q-37 37 -37 90q0 52 37 91l651 650q38 38 91 38q52 0 90 -38l75 -74q38 -38 38 -91t-38 -91l-293 -293h704q52 0 84.5 -37.5 t32.5 -90.5z" /> +<glyph unicode="" d="M1472 576q0 -54 -37 -91l-651 -651q-39 -37 -91 -37q-51 0 -90 37l-75 75q-38 38 -38 91t38 91l293 293h-704q-52 0 -84.5 37.5t-32.5 90.5v128q0 53 32.5 90.5t84.5 37.5h704l-293 294q-38 36 -38 90t38 90l75 75q38 38 90 38q53 0 91 -38l651 -651q37 -35 37 -90z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1611 565q0 -51 -37 -90l-75 -75q-38 -38 -91 -38q-54 0 -90 38l-294 293v-704q0 -52 -37.5 -84.5t-90.5 -32.5h-128q-53 0 -90.5 32.5t-37.5 84.5v704l-294 -293q-36 -38 -90 -38t-90 38l-75 75q-38 38 -38 90q0 53 38 91l651 651q35 37 90 37q54 0 91 -37l651 -651 q37 -39 37 -91z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1611 704q0 -53 -37 -90l-651 -652q-39 -37 -91 -37q-53 0 -90 37l-651 652q-38 36 -38 90q0 53 38 91l74 75q39 37 91 37q53 0 90 -37l294 -294v704q0 52 38 90t90 38h128q52 0 90 -38t38 -90v-704l294 294q37 37 90 37q52 0 91 -37l75 -75q37 -39 37 -91z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1792 896q0 -26 -19 -45l-512 -512q-19 -19 -45 -19t-45 19t-19 45v256h-224q-98 0 -175.5 -6t-154 -21.5t-133 -42.5t-105.5 -69.5t-80 -101t-48.5 -138.5t-17.5 -181q0 -55 5 -123q0 -6 2.5 -23.5t2.5 -26.5q0 -15 -8.5 -25t-23.5 -10q-16 0 -28 17q-7 9 -13 22 t-13.5 30t-10.5 24q-127 285 -127 451q0 199 53 333q162 403 875 403h224v256q0 26 19 45t45 19t45 -19l512 -512q19 -19 19 -45z" /> +<glyph unicode="" d="M755 480q0 -13 -10 -23l-332 -332l144 -144q19 -19 19 -45t-19 -45t-45 -19h-448q-26 0 -45 19t-19 45v448q0 26 19 45t45 19t45 -19l144 -144l332 332q10 10 23 10t23 -10l114 -114q10 -10 10 -23zM1536 1344v-448q0 -26 -19 -45t-45 -19t-45 19l-144 144l-332 -332 q-10 -10 -23 -10t-23 10l-114 114q-10 10 -10 23t10 23l332 332l-144 144q-19 19 -19 45t19 45t45 19h448q26 0 45 -19t19 -45z" /> +<glyph unicode="" d="M768 576v-448q0 -26 -19 -45t-45 -19t-45 19l-144 144l-332 -332q-10 -10 -23 -10t-23 10l-114 114q-10 10 -10 23t10 23l332 332l-144 144q-19 19 -19 45t19 45t45 19h448q26 0 45 -19t19 -45zM1523 1248q0 -13 -10 -23l-332 -332l144 -144q19 -19 19 -45t-19 -45 t-45 -19h-448q-26 0 -45 19t-19 45v448q0 26 19 45t45 19t45 -19l144 -144l332 332q10 10 23 10t23 -10l114 -114q10 -10 10 -23z" /> +<glyph unicode="" horiz-adv-x="1408" d="M1408 800v-192q0 -40 -28 -68t-68 -28h-416v-416q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v416h-416q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h416v416q0 40 28 68t68 28h192q40 0 68 -28t28 -68v-416h416q40 0 68 -28t28 -68z" /> +<glyph unicode="" horiz-adv-x="1408" d="M1408 800v-192q0 -40 -28 -68t-68 -28h-1216q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h1216q40 0 68 -28t28 -68z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1482 486q46 -26 59.5 -77.5t-12.5 -97.5l-64 -110q-26 -46 -77.5 -59.5t-97.5 12.5l-266 153v-307q0 -52 -38 -90t-90 -38h-128q-52 0 -90 38t-38 90v307l-266 -153q-46 -26 -97.5 -12.5t-77.5 59.5l-64 110q-26 46 -12.5 97.5t59.5 77.5l266 154l-266 154 q-46 26 -59.5 77.5t12.5 97.5l64 110q26 46 77.5 59.5t97.5 -12.5l266 -153v307q0 52 38 90t90 38h128q52 0 90 -38t38 -90v-307l266 153q46 26 97.5 12.5t77.5 -59.5l64 -110q26 -46 12.5 -97.5t-59.5 -77.5l-266 -154z" /> +<glyph unicode="" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM896 161v190q0 14 -9 23.5t-22 9.5h-192q-13 0 -23 -10t-10 -23v-190q0 -13 10 -23t23 -10h192 q13 0 22 9.5t9 23.5zM894 505l18 621q0 12 -10 18q-10 8 -24 8h-220q-14 0 -24 -8q-10 -6 -10 -18l17 -621q0 -10 10 -17.5t24 -7.5h185q14 0 23.5 7.5t10.5 17.5z" /> +<glyph unicode="" d="M928 180v56v468v192h-320v-192v-468v-56q0 -25 18 -38.5t46 -13.5h192q28 0 46 13.5t18 38.5zM472 1024h195l-126 161q-26 31 -69 31q-40 0 -68 -28t-28 -68t28 -68t68 -28zM1160 1120q0 40 -28 68t-68 28q-43 0 -69 -31l-125 -161h194q40 0 68 28t28 68zM1536 864v-320 q0 -14 -9 -23t-23 -9h-96v-416q0 -40 -28 -68t-68 -28h-1088q-40 0 -68 28t-28 68v416h-96q-14 0 -23 9t-9 23v320q0 14 9 23t23 9h440q-93 0 -158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5q107 0 168 -77l128 -165l128 165q61 77 168 77q93 0 158.5 -65.5t65.5 -158.5 t-65.5 -158.5t-158.5 -65.5h440q14 0 23 -9t9 -23z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1280 832q0 26 -19 45t-45 19q-172 0 -318 -49.5t-259.5 -134t-235.5 -219.5q-19 -21 -19 -45q0 -26 19 -45t45 -19q24 0 45 19q27 24 74 71t67 66q137 124 268.5 176t313.5 52q26 0 45 19t19 45zM1792 1030q0 -95 -20 -193q-46 -224 -184.5 -383t-357.5 -268 q-214 -108 -438 -108q-148 0 -286 47q-15 5 -88 42t-96 37q-16 0 -39.5 -32t-45 -70t-52.5 -70t-60 -32q-30 0 -51 11t-31 24t-27 42q-2 4 -6 11t-5.5 10t-3 9.5t-1.5 13.5q0 35 31 73.5t68 65.5t68 56t31 48q0 4 -14 38t-16 44q-9 51 -9 104q0 115 43.5 220t119 184.5 t170.5 139t204 95.5q55 18 145 25.5t179.5 9t178.5 6t163.5 24t113.5 56.5l29.5 29.5t29.5 28t27 20t36.5 16t43.5 4.5q39 0 70.5 -46t47.5 -112t24 -124t8 -96z" /> +<glyph unicode="" horiz-adv-x="1408" d="M1408 -160v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h1344q13 0 22.5 -9.5t9.5 -22.5zM1152 896q0 -78 -24.5 -144t-64 -112.5t-87.5 -88t-96 -77.5t-87.5 -72t-64 -81.5t-24.5 -96.5q0 -96 67 -224l-4 1l1 -1 q-90 41 -160 83t-138.5 100t-113.5 122.5t-72.5 150.5t-27.5 184q0 78 24.5 144t64 112.5t87.5 88t96 77.5t87.5 72t64 81.5t24.5 96.5q0 94 -66 224l3 -1l-1 1q90 -41 160 -83t138.5 -100t113.5 -122.5t72.5 -150.5t27.5 -184z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1664 576q-152 236 -381 353q61 -104 61 -225q0 -185 -131.5 -316.5t-316.5 -131.5t-316.5 131.5t-131.5 316.5q0 121 61 225q-229 -117 -381 -353q133 -205 333.5 -326.5t434.5 -121.5t434.5 121.5t333.5 326.5zM944 960q0 20 -14 34t-34 14q-125 0 -214.5 -89.5 t-89.5 -214.5q0 -20 14 -34t34 -14t34 14t14 34q0 86 61 147t147 61q20 0 34 14t14 34zM1792 576q0 -34 -20 -69q-140 -230 -376.5 -368.5t-499.5 -138.5t-499.5 139t-376.5 368q-20 35 -20 69t20 69q140 229 376.5 368t499.5 139t499.5 -139t376.5 -368q20 -35 20 -69z" /> +<glyph unicode="" horiz-adv-x="1792" d="M555 201l78 141q-87 63 -136 159t-49 203q0 121 61 225q-229 -117 -381 -353q167 -258 427 -375zM944 960q0 20 -14 34t-34 14q-125 0 -214.5 -89.5t-89.5 -214.5q0 -20 14 -34t34 -14t34 14t14 34q0 86 61 147t147 61q20 0 34 14t14 34zM1307 1151q0 -7 -1 -9 q-105 -188 -315 -566t-316 -567l-49 -89q-10 -16 -28 -16q-12 0 -134 70q-16 10 -16 28q0 12 44 87q-143 65 -263.5 173t-208.5 245q-20 31 -20 69t20 69q153 235 380 371t496 136q89 0 180 -17l54 97q10 16 28 16q5 0 18 -6t31 -15.5t33 -18.5t31.5 -18.5t19.5 -11.5 q16 -10 16 -27zM1344 704q0 -139 -79 -253.5t-209 -164.5l280 502q8 -45 8 -84zM1792 576q0 -35 -20 -69q-39 -64 -109 -145q-150 -172 -347.5 -267t-419.5 -95l74 132q212 18 392.5 137t301.5 307q-115 179 -282 294l63 112q95 -64 182.5 -153t144.5 -184q20 -34 20 -69z " /> +<glyph unicode="" horiz-adv-x="1792" d="M1024 161v190q0 14 -9.5 23.5t-22.5 9.5h-192q-13 0 -22.5 -9.5t-9.5 -23.5v-190q0 -14 9.5 -23.5t22.5 -9.5h192q13 0 22.5 9.5t9.5 23.5zM1022 535l18 459q0 12 -10 19q-13 11 -24 11h-220q-11 0 -24 -11q-10 -7 -10 -21l17 -457q0 -10 10 -16.5t24 -6.5h185 q14 0 23.5 6.5t10.5 16.5zM1008 1469l768 -1408q35 -63 -2 -126q-17 -29 -46.5 -46t-63.5 -17h-1536q-34 0 -63.5 17t-46.5 46q-37 63 -2 126l768 1408q17 31 47 49t65 18t65 -18t47 -49z" /> +<glyph unicode="" horiz-adv-x="1408" d="M1376 1376q44 -52 12 -148t-108 -172l-161 -161l160 -696q5 -19 -12 -33l-128 -96q-7 -6 -19 -6q-4 0 -7 1q-15 3 -21 16l-279 508l-259 -259l53 -194q5 -17 -8 -31l-96 -96q-9 -9 -23 -9h-2q-15 2 -24 13l-189 252l-252 189q-11 7 -13 23q-1 13 9 25l96 97q9 9 23 9 q6 0 8 -1l194 -53l259 259l-508 279q-14 8 -17 24q-2 16 9 27l128 128q14 13 30 8l665 -159l160 160q76 76 172 108t148 -12z" /> +<glyph unicode="" horiz-adv-x="1664" d="M128 -128h288v288h-288v-288zM480 -128h320v288h-320v-288zM128 224h288v320h-288v-320zM480 224h320v320h-320v-320zM128 608h288v288h-288v-288zM864 -128h320v288h-320v-288zM480 608h320v288h-320v-288zM1248 -128h288v288h-288v-288zM864 224h320v320h-320v-320z M512 1088v288q0 13 -9.5 22.5t-22.5 9.5h-64q-13 0 -22.5 -9.5t-9.5 -22.5v-288q0 -13 9.5 -22.5t22.5 -9.5h64q13 0 22.5 9.5t9.5 22.5zM1248 224h288v320h-288v-320zM864 608h320v288h-320v-288zM1248 608h288v288h-288v-288zM1280 1088v288q0 13 -9.5 22.5t-22.5 9.5h-64 q-13 0 -22.5 -9.5t-9.5 -22.5v-288q0 -13 9.5 -22.5t22.5 -9.5h64q13 0 22.5 9.5t9.5 22.5zM1664 1152v-1280q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h384v96q0 66 47 113t113 47 h64q66 0 113 -47t47 -113v-96h128q52 0 90 -38t38 -90z" /> +<glyph unicode="" horiz-adv-x="1792" d="M666 1055q-60 -92 -137 -273q-22 45 -37 72.5t-40.5 63.5t-51 56.5t-63 35t-81.5 14.5h-224q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h224q250 0 410 -225zM1792 256q0 -14 -9 -23l-320 -320q-9 -9 -23 -9q-13 0 -22.5 9.5t-9.5 22.5v192q-32 0 -85 -0.5t-81 -1t-73 1 t-71 5t-64 10.5t-63 18.5t-58 28.5t-59 40t-55 53.5t-56 69.5q59 93 136 273q22 -45 37 -72.5t40.5 -63.5t51 -56.5t63 -35t81.5 -14.5h256v192q0 14 9 23t23 9q12 0 24 -10l319 -319q9 -9 9 -23zM1792 1152q0 -14 -9 -23l-320 -320q-9 -9 -23 -9q-13 0 -22.5 9.5t-9.5 22.5 v192h-256q-48 0 -87 -15t-69 -45t-51 -61.5t-45 -77.5q-32 -62 -78 -171q-29 -66 -49.5 -111t-54 -105t-64 -100t-74 -83t-90 -68.5t-106.5 -42t-128 -16.5h-224q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h224q48 0 87 15t69 45t51 61.5t45 77.5q32 62 78 171q29 66 49.5 111 t54 105t64 100t74 83t90 68.5t106.5 42t128 16.5h256v192q0 14 9 23t23 9q12 0 24 -10l319 -319q9 -9 9 -23z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1792 640q0 -174 -120 -321.5t-326 -233t-450 -85.5q-70 0 -145 8q-198 -175 -460 -242q-49 -14 -114 -22q-17 -2 -30.5 9t-17.5 29v1q-3 4 -0.5 12t2 10t4.5 9.5l6 9t7 8.5t8 9q7 8 31 34.5t34.5 38t31 39.5t32.5 51t27 59t26 76q-157 89 -247.5 220t-90.5 281 q0 130 71 248.5t191 204.5t286 136.5t348 50.5q244 0 450 -85.5t326 -233t120 -321.5z" /> +<glyph unicode="" d="M1536 704v-128q0 -201 -98.5 -362t-274 -251.5t-395.5 -90.5t-395.5 90.5t-274 251.5t-98.5 362v128q0 26 19 45t45 19h384q26 0 45 -19t19 -45v-128q0 -52 23.5 -90t53.5 -57t71 -30t64 -13t44 -2t44 2t64 13t71 30t53.5 57t23.5 90v128q0 26 19 45t45 19h384 q26 0 45 -19t19 -45zM512 1344v-384q0 -26 -19 -45t-45 -19h-384q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h384q26 0 45 -19t19 -45zM1536 1344v-384q0 -26 -19 -45t-45 -19h-384q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h384q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1683 205l-166 -165q-19 -19 -45 -19t-45 19l-531 531l-531 -531q-19 -19 -45 -19t-45 19l-166 165q-19 19 -19 45.5t19 45.5l742 741q19 19 45 19t45 -19l742 -741q19 -19 19 -45.5t-19 -45.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1683 728l-742 -741q-19 -19 -45 -19t-45 19l-742 741q-19 19 -19 45.5t19 45.5l166 165q19 19 45 19t45 -19l531 -531l531 531q19 19 45 19t45 -19l166 -165q19 -19 19 -45.5t-19 -45.5z" /> +<glyph unicode="" horiz-adv-x="1920" d="M1280 32q0 -13 -9.5 -22.5t-22.5 -9.5h-960q-8 0 -13.5 2t-9 7t-5.5 8t-3 11.5t-1 11.5v13v11v160v416h-192q-26 0 -45 19t-19 45q0 24 15 41l320 384q19 22 49 22t49 -22l320 -384q15 -17 15 -41q0 -26 -19 -45t-45 -19h-192v-384h576q16 0 25 -11l160 -192q7 -11 7 -21 zM1920 448q0 -24 -15 -41l-320 -384q-20 -23 -49 -23t-49 23l-320 384q-15 17 -15 41q0 26 19 45t45 19h192v384h-576q-16 0 -25 12l-160 192q-7 9 -7 20q0 13 9.5 22.5t22.5 9.5h960q8 0 13.5 -2t9 -7t5.5 -8t3 -11.5t1 -11.5v-13v-11v-160v-416h192q26 0 45 -19t19 -45z " /> +<glyph unicode="" horiz-adv-x="1664" d="M640 0q0 -52 -38 -90t-90 -38t-90 38t-38 90t38 90t90 38t90 -38t38 -90zM1536 0q0 -52 -38 -90t-90 -38t-90 38t-38 90t38 90t90 38t90 -38t38 -90zM1664 1088v-512q0 -24 -16.5 -42.5t-40.5 -21.5l-1044 -122q13 -60 13 -70q0 -16 -24 -64h920q26 0 45 -19t19 -45 t-19 -45t-45 -19h-1024q-26 0 -45 19t-19 45q0 11 8 31.5t16 36t21.5 40t15.5 29.5l-177 823h-204q-26 0 -45 19t-19 45t19 45t45 19h256q16 0 28.5 -6.5t19.5 -15.5t13 -24.5t8 -26t5.5 -29.5t4.5 -26h1201q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1664 928v-704q0 -92 -66 -158t-158 -66h-1216q-92 0 -158 66t-66 158v960q0 92 66 158t158 66h320q92 0 158 -66t66 -158v-32h672q92 0 158 -66t66 -158z" /> +<glyph unicode="" horiz-adv-x="1920" d="M1879 584q0 -31 -31 -66l-336 -396q-43 -51 -120.5 -86.5t-143.5 -35.5h-1088q-34 0 -60.5 13t-26.5 43q0 31 31 66l336 396q43 51 120.5 86.5t143.5 35.5h1088q34 0 60.5 -13t26.5 -43zM1536 928v-160h-832q-94 0 -197 -47.5t-164 -119.5l-337 -396l-5 -6q0 4 -0.5 12.5 t-0.5 12.5v960q0 92 66 158t158 66h320q92 0 158 -66t66 -158v-32h544q92 0 158 -66t66 -158z" /> +<glyph unicode="" horiz-adv-x="768" d="M704 1216q0 -26 -19 -45t-45 -19h-128v-1024h128q26 0 45 -19t19 -45t-19 -45l-256 -256q-19 -19 -45 -19t-45 19l-256 256q-19 19 -19 45t19 45t45 19h128v1024h-128q-26 0 -45 19t-19 45t19 45l256 256q19 19 45 19t45 -19l256 -256q19 -19 19 -45z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1792 640q0 -26 -19 -45l-256 -256q-19 -19 -45 -19t-45 19t-19 45v128h-1024v-128q0 -26 -19 -45t-45 -19t-45 19l-256 256q-19 19 -19 45t19 45l256 256q19 19 45 19t45 -19t19 -45v-128h1024v128q0 26 19 45t45 19t45 -19l256 -256q19 -19 19 -45z" /> +<glyph unicode="" horiz-adv-x="2048" d="M640 640v-512h-256v512h256zM1024 1152v-1024h-256v1024h256zM2048 0v-128h-2048v1536h128v-1408h1920zM1408 896v-768h-256v768h256zM1792 1280v-1152h-256v1152h256z" /> +<glyph unicode="" d="M1280 926q-56 -25 -121 -34q68 40 93 117q-65 -38 -134 -51q-61 66 -153 66q-87 0 -148.5 -61.5t-61.5 -148.5q0 -29 5 -48q-129 7 -242 65t-192 155q-29 -50 -29 -106q0 -114 91 -175q-47 1 -100 26v-2q0 -75 50 -133.5t123 -72.5q-29 -8 -51 -8q-13 0 -39 4 q21 -63 74.5 -104t121.5 -42q-116 -90 -261 -90q-26 0 -50 3q148 -94 322 -94q112 0 210 35.5t168 95t120.5 137t75 162t24.5 168.5q0 18 -1 27q63 45 105 109zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5 t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-188v595h199l30 232h-229v148q0 56 23.5 84t91.5 28l122 1v207q-63 9 -178 9q-136 0 -217.5 -80t-81.5 -226v-171h-200v-232h200v-595h-532q-119 0 -203.5 84.5t-84.5 203.5v960 q0 119 84.5 203.5t203.5 84.5h960z" /> +<glyph unicode="" horiz-adv-x="1792" d="M928 704q0 14 -9 23t-23 9q-66 0 -113 -47t-47 -113q0 -14 9 -23t23 -9t23 9t9 23q0 40 28 68t68 28q14 0 23 9t9 23zM1152 574q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181zM128 0h1536v128h-1536v-128zM1280 574q0 159 -112.5 271.5 t-271.5 112.5t-271.5 -112.5t-112.5 -271.5t112.5 -271.5t271.5 -112.5t271.5 112.5t112.5 271.5zM256 1216h384v128h-384v-128zM128 1024h1536v118v138h-828l-64 -128h-644v-128zM1792 1280v-1280q0 -53 -37.5 -90.5t-90.5 -37.5h-1536q-53 0 -90.5 37.5t-37.5 90.5v1280 q0 53 37.5 90.5t90.5 37.5h1536q53 0 90.5 -37.5t37.5 -90.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M832 1024q0 80 -56 136t-136 56t-136 -56t-56 -136q0 -42 19 -83q-41 19 -83 19q-80 0 -136 -56t-56 -136t56 -136t136 -56t136 56t56 136q0 42 -19 83q41 -19 83 -19q80 0 136 56t56 136zM1683 320q0 -17 -49 -66t-66 -49q-9 0 -28.5 16t-36.5 33t-38.5 40t-24.5 26 l-96 -96l220 -220q28 -28 28 -68q0 -42 -39 -81t-81 -39q-40 0 -68 28l-671 671q-176 -131 -365 -131q-163 0 -265.5 102.5t-102.5 265.5q0 160 95 313t248 248t313 95q163 0 265.5 -102.5t102.5 -265.5q0 -189 -131 -365l355 -355l96 96q-3 3 -26 24.5t-40 38.5t-33 36.5 t-16 28.5q0 17 49 66t66 49q13 0 23 -10q6 -6 46 -44.5t82 -79.5t86.5 -86t73 -78t28.5 -41z" /> +<glyph unicode="" horiz-adv-x="1920" d="M896 640q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM1664 128q0 52 -38 90t-90 38t-90 -38t-38 -90q0 -53 37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1664 1152q0 52 -38 90t-90 38t-90 -38t-38 -90q0 -53 37.5 -90.5t90.5 -37.5 t90.5 37.5t37.5 90.5zM1280 731v-185q0 -10 -7 -19.5t-16 -10.5l-155 -24q-11 -35 -32 -76q34 -48 90 -115q7 -10 7 -20q0 -12 -7 -19q-23 -30 -82.5 -89.5t-78.5 -59.5q-11 0 -21 7l-115 90q-37 -19 -77 -31q-11 -108 -23 -155q-7 -24 -30 -24h-186q-11 0 -20 7.5t-10 17.5 l-23 153q-34 10 -75 31l-118 -89q-7 -7 -20 -7q-11 0 -21 8q-144 133 -144 160q0 9 7 19q10 14 41 53t47 61q-23 44 -35 82l-152 24q-10 1 -17 9.5t-7 19.5v185q0 10 7 19.5t16 10.5l155 24q11 35 32 76q-34 48 -90 115q-7 11 -7 20q0 12 7 20q22 30 82 89t79 59q11 0 21 -7 l115 -90q34 18 77 32q11 108 23 154q7 24 30 24h186q11 0 20 -7.5t10 -17.5l23 -153q34 -10 75 -31l118 89q8 7 20 7q11 0 21 -8q144 -133 144 -160q0 -9 -7 -19q-12 -16 -42 -54t-45 -60q23 -48 34 -82l152 -23q10 -2 17 -10.5t7 -19.5zM1920 198v-140q0 -16 -149 -31 q-12 -27 -30 -52q51 -113 51 -138q0 -4 -4 -7q-122 -71 -124 -71q-8 0 -46 47t-52 68q-20 -2 -30 -2t-30 2q-14 -21 -52 -68t-46 -47q-2 0 -124 71q-4 3 -4 7q0 25 51 138q-18 25 -30 52q-149 15 -149 31v140q0 16 149 31q13 29 30 52q-51 113 -51 138q0 4 4 7q4 2 35 20 t59 34t30 16q8 0 46 -46.5t52 -67.5q20 2 30 2t30 -2q51 71 92 112l6 2q4 0 124 -70q4 -3 4 -7q0 -25 -51 -138q17 -23 30 -52q149 -15 149 -31zM1920 1222v-140q0 -16 -149 -31q-12 -27 -30 -52q51 -113 51 -138q0 -4 -4 -7q-122 -71 -124 -71q-8 0 -46 47t-52 68 q-20 -2 -30 -2t-30 2q-14 -21 -52 -68t-46 -47q-2 0 -124 71q-4 3 -4 7q0 25 51 138q-18 25 -30 52q-149 15 -149 31v140q0 16 149 31q13 29 30 52q-51 113 -51 138q0 4 4 7q4 2 35 20t59 34t30 16q8 0 46 -46.5t52 -67.5q20 2 30 2t30 -2q51 71 92 112l6 2q4 0 124 -70 q4 -3 4 -7q0 -25 -51 -138q17 -23 30 -52q149 -15 149 -31z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1408 768q0 -139 -94 -257t-256.5 -186.5t-353.5 -68.5q-86 0 -176 16q-124 -88 -278 -128q-36 -9 -86 -16h-3q-11 0 -20.5 8t-11.5 21q-1 3 -1 6.5t0.5 6.5t2 6l2.5 5t3.5 5.5t4 5t4.5 5t4 4.5q5 6 23 25t26 29.5t22.5 29t25 38.5t20.5 44q-124 72 -195 177t-71 224 q0 139 94 257t256.5 186.5t353.5 68.5t353.5 -68.5t256.5 -186.5t94 -257zM1792 512q0 -120 -71 -224.5t-195 -176.5q10 -24 20.5 -44t25 -38.5t22.5 -29t26 -29.5t23 -25q1 -1 4 -4.5t4.5 -5t4 -5t3.5 -5.5l2.5 -5t2 -6t0.5 -6.5t-1 -6.5q-3 -14 -13 -22t-22 -7 q-50 7 -86 16q-154 40 -278 128q-90 -16 -176 -16q-271 0 -472 132q58 -4 88 -4q161 0 309 45t264 129q125 92 192 212t67 254q0 77 -23 152q129 -71 204 -178t75 -230z" /> +<glyph unicode="" d="M256 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 768q0 51 -39 89.5t-89 38.5h-352q0 58 48 159.5t48 160.5q0 98 -32 145t-128 47q-26 -26 -38 -85t-30.5 -125.5t-59.5 -109.5q-22 -23 -77 -91q-4 -5 -23 -30t-31.5 -41t-34.5 -42.5 t-40 -44t-38.5 -35.5t-40 -27t-35.5 -9h-32v-640h32q13 0 31.5 -3t33 -6.5t38 -11t35 -11.5t35.5 -12.5t29 -10.5q211 -73 342 -73h121q192 0 192 167q0 26 -5 56q30 16 47.5 52.5t17.5 73.5t-18 69q53 50 53 119q0 25 -10 55.5t-25 47.5q32 1 53.5 47t21.5 81zM1536 769 q0 -89 -49 -163q9 -33 9 -69q0 -77 -38 -144q3 -21 3 -43q0 -101 -60 -178q1 -139 -85 -219.5t-227 -80.5h-36h-93q-96 0 -189.5 22.5t-216.5 65.5q-116 40 -138 40h-288q-53 0 -90.5 37.5t-37.5 90.5v640q0 53 37.5 90.5t90.5 37.5h274q36 24 137 155q58 75 107 128 q24 25 35.5 85.5t30.5 126.5t62 108q39 37 90 37q84 0 151 -32.5t102 -101.5t35 -186q0 -93 -48 -192h176q104 0 180 -76t76 -179z" /> +<glyph unicode="" d="M256 1088q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 512q0 35 -21.5 81t-53.5 47q15 17 25 47.5t10 55.5q0 69 -53 119q18 32 18 69t-17.5 73.5t-47.5 52.5q5 30 5 56q0 85 -49 126t-136 41h-128q-131 0 -342 -73q-5 -2 -29 -10.5 t-35.5 -12.5t-35 -11.5t-38 -11t-33 -6.5t-31.5 -3h-32v-640h32q16 0 35.5 -9t40 -27t38.5 -35.5t40 -44t34.5 -42.5t31.5 -41t23 -30q55 -68 77 -91q41 -43 59.5 -109.5t30.5 -125.5t38 -85q96 0 128 47t32 145q0 59 -48 160.5t-48 159.5h352q50 0 89 38.5t39 89.5z M1536 511q0 -103 -76 -179t-180 -76h-176q48 -99 48 -192q0 -118 -35 -186q-35 -69 -102 -101.5t-151 -32.5q-51 0 -90 37q-34 33 -54 82t-25.5 90.5t-17.5 84.5t-31 64q-48 50 -107 127q-101 131 -137 155h-274q-53 0 -90.5 37.5t-37.5 90.5v640q0 53 37.5 90.5t90.5 37.5 h288q22 0 138 40q128 44 223 66t200 22h112q140 0 226.5 -79t85.5 -216v-5q60 -77 60 -178q0 -22 -3 -43q38 -67 38 -144q0 -36 -9 -69q49 -74 49 -163z" /> +<glyph unicode="" horiz-adv-x="896" d="M832 1504v-1339l-449 -236q-22 -12 -40 -12q-21 0 -31.5 14.5t-10.5 35.5q0 6 2 20l86 500l-364 354q-25 27 -25 48q0 37 56 46l502 73l225 455q19 41 49 41z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1664 940q0 81 -21.5 143t-55 98.5t-81.5 59.5t-94 31t-98 8t-112 -25.5t-110.5 -64t-86.5 -72t-60 -61.5q-18 -22 -49 -22t-49 22q-24 28 -60 61.5t-86.5 72t-110.5 64t-112 25.5t-98 -8t-94 -31t-81.5 -59.5t-55 -98.5t-21.5 -143q0 -168 187 -355l581 -560l580 559 q188 188 188 356zM1792 940q0 -221 -229 -450l-623 -600q-18 -18 -44 -18t-44 18l-624 602q-10 8 -27.5 26t-55.5 65.5t-68 97.5t-53.5 121t-23.5 138q0 220 127 344t351 124q62 0 126.5 -21.5t120 -58t95.5 -68.5t76 -68q36 36 76 68t95.5 68.5t120 58t126.5 21.5 q224 0 351 -124t127 -344z" /> +<glyph unicode="" horiz-adv-x="1664" d="M640 96q0 -4 1 -20t0.5 -26.5t-3 -23.5t-10 -19.5t-20.5 -6.5h-320q-119 0 -203.5 84.5t-84.5 203.5v704q0 119 84.5 203.5t203.5 84.5h320q13 0 22.5 -9.5t9.5 -22.5q0 -4 1 -20t0.5 -26.5t-3 -23.5t-10 -19.5t-20.5 -6.5h-320q-66 0 -113 -47t-47 -113v-704 q0 -66 47 -113t113 -47h288h11h13t11.5 -1t11.5 -3t8 -5.5t7 -9t2 -13.5zM1568 640q0 -26 -19 -45l-544 -544q-19 -19 -45 -19t-45 19t-19 45v288h-448q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h448v288q0 26 19 45t45 19t45 -19l544 -544q19 -19 19 -45z" /> +<glyph unicode="" d="M237 122h231v694h-231v-694zM483 1030q-1 52 -36 86t-93 34t-94.5 -34t-36.5 -86q0 -51 35.5 -85.5t92.5 -34.5h1q59 0 95 34.5t36 85.5zM1068 122h231v398q0 154 -73 233t-193 79q-136 0 -209 -117h2v101h-231q3 -66 0 -694h231v388q0 38 7 56q15 35 45 59.5t74 24.5 q116 0 116 -157v-371zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="1152" d="M480 672v448q0 14 -9 23t-23 9t-23 -9t-9 -23v-448q0 -14 9 -23t23 -9t23 9t9 23zM1152 320q0 -26 -19 -45t-45 -19h-429l-51 -483q-2 -12 -10.5 -20.5t-20.5 -8.5h-1q-27 0 -32 27l-76 485h-404q-26 0 -45 19t-19 45q0 123 78.5 221.5t177.5 98.5v512q-52 0 -90 38 t-38 90t38 90t90 38h640q52 0 90 -38t38 -90t-38 -90t-90 -38v-512q99 0 177.5 -98.5t78.5 -221.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1408 608v-320q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h704q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v320 q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1792 1472v-512q0 -26 -19 -45t-45 -19t-45 19l-176 176l-652 -652q-10 -10 -23 -10t-23 10l-114 114q-10 10 -10 23t10 23l652 652l-176 176q-19 19 -19 45t19 45t45 19h512q26 0 45 -19t19 -45z" /> +<glyph unicode="" d="M1184 640q0 -26 -19 -45l-544 -544q-19 -19 -45 -19t-45 19t-19 45v288h-448q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h448v288q0 26 19 45t45 19t45 -19l544 -544q19 -19 19 -45zM1536 992v-704q0 -119 -84.5 -203.5t-203.5 -84.5h-320q-13 0 -22.5 9.5t-9.5 22.5 q0 4 -1 20t-0.5 26.5t3 23.5t10 19.5t20.5 6.5h320q66 0 113 47t47 113v704q0 66 -47 113t-113 47h-288h-11h-13t-11.5 1t-11.5 3t-8 5.5t-7 9t-2 13.5q0 4 -1 20t-0.5 26.5t3 23.5t10 19.5t20.5 6.5h320q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="1664" d="M458 653q-74 162 -74 371h-256v-96q0 -78 94.5 -162t235.5 -113zM1536 928v96h-256q0 -209 -74 -371q141 29 235.5 113t94.5 162zM1664 1056v-128q0 -71 -41.5 -143t-112 -130t-173 -97.5t-215.5 -44.5q-42 -54 -95 -95q-38 -34 -52.5 -72.5t-14.5 -89.5q0 -54 30.5 -91 t97.5 -37q75 0 133.5 -45.5t58.5 -114.5v-64q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23v64q0 69 58.5 114.5t133.5 45.5q67 0 97.5 37t30.5 91q0 51 -14.5 89.5t-52.5 72.5q-53 41 -95 95q-113 5 -215.5 44.5t-173 97.5t-112 130t-41.5 143v128q0 40 28 68t68 28h288v96 q0 66 47 113t113 47h576q66 0 113 -47t47 -113v-96h288q40 0 68 -28t28 -68z" /> +<glyph unicode="" d="M394 184q-8 -9 -20 3q-13 11 -4 19q8 9 20 -3q12 -11 4 -19zM352 245q9 -12 0 -19q-8 -6 -17 7t0 18q9 7 17 -6zM291 305q-5 -7 -13 -2q-10 5 -7 12q3 5 13 2q10 -5 7 -12zM322 271q-6 -7 -16 3q-9 11 -2 16q6 6 16 -3q9 -11 2 -16zM451 159q-4 -12 -19 -6q-17 4 -13 15 t19 7q16 -5 13 -16zM514 154q0 -11 -16 -11q-17 -2 -17 11q0 11 16 11q17 2 17 -11zM572 164q2 -10 -14 -14t-18 8t14 15q16 2 18 -9zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-224q-16 0 -24.5 1t-19.5 5t-16 14.5t-5 27.5v239q0 97 -52 142q57 6 102.5 18t94 39 t81 66.5t53 105t20.5 150.5q0 121 -79 206q37 91 -8 204q-28 9 -81 -11t-92 -44l-38 -24q-93 26 -192 26t-192 -26q-16 11 -42.5 27t-83.5 38.5t-86 13.5q-44 -113 -7 -204q-79 -85 -79 -206q0 -85 20.5 -150t52.5 -105t80.5 -67t94 -39t102.5 -18q-40 -36 -49 -103 q-21 -10 -45 -15t-57 -5t-65.5 21.5t-55.5 62.5q-19 32 -48.5 52t-49.5 24l-20 3q-21 0 -29 -4.5t-5 -11.5t9 -14t13 -12l7 -5q22 -10 43.5 -38t31.5 -51l10 -23q13 -38 44 -61.5t67 -30t69.5 -7t55.5 3.5l23 4q0 -38 0.5 -103t0.5 -68q0 -22 -11 -33.5t-22 -13t-33 -1.5 h-224q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1280 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1536 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 288v-320q0 -40 -28 -68t-68 -28h-1472q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h427q21 -56 70.5 -92 t110.5 -36h256q61 0 110.5 36t70.5 92h427q40 0 68 -28t28 -68zM1339 936q-17 -40 -59 -40h-256v-448q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v448h-256q-42 0 -59 40q-17 39 14 69l448 448q18 19 45 19t45 -19l448 -448q31 -30 14 -69z" /> +<glyph unicode="" d="M1407 710q0 44 -7 113.5t-18 96.5q-12 30 -17 44t-9 36.5t-4 48.5q0 23 5 68.5t5 67.5q0 37 -10 55q-4 1 -13 1q-19 0 -58 -4.5t-59 -4.5q-60 0 -176 24t-175 24q-43 0 -94.5 -11.5t-85 -23.5t-89.5 -34q-137 -54 -202 -103q-96 -73 -159.5 -189.5t-88 -236t-24.5 -248.5 q0 -40 12.5 -120t12.5 -121q0 -23 -11 -66.5t-11 -65.5t12 -36.5t34 -14.5q24 0 72.5 11t73.5 11q57 0 169.5 -15.5t169.5 -15.5q181 0 284 36q129 45 235.5 152.5t166 245.5t59.5 275zM1535 712q0 -165 -70 -327.5t-196 -288t-281 -180.5q-124 -44 -326 -44 q-57 0 -170 14.5t-169 14.5q-24 0 -72.5 -14.5t-73.5 -14.5q-73 0 -123.5 55.5t-50.5 128.5q0 24 11 68t11 67q0 40 -12.5 120.5t-12.5 121.5q0 111 18 217.5t54.5 209.5t100.5 194t150 156q78 59 232 120q194 78 316 78q60 0 175.5 -24t173.5 -24q19 0 57 5t58 5 q81 0 118 -50.5t37 -134.5q0 -23 -5 -68t-5 -68q0 -10 1 -18.5t3 -17t4 -13.5t6.5 -16t6.5 -17q16 -40 25 -118.5t9 -136.5z" /> +<glyph unicode="" horiz-adv-x="1408" d="M1408 296q0 -27 -10 -70.5t-21 -68.5q-21 -50 -122 -106q-94 -51 -186 -51q-27 0 -52.5 3.5t-57.5 12.5t-47.5 14.5t-55.5 20.5t-49 18q-98 35 -175 83q-128 79 -264.5 215.5t-215.5 264.5q-48 77 -83 175q-3 9 -18 49t-20.5 55.5t-14.5 47.5t-12.5 57.5t-3.5 52.5 q0 92 51 186q56 101 106 122q25 11 68.5 21t70.5 10q14 0 21 -3q18 -6 53 -76q11 -19 30 -54t35 -63.5t31 -53.5q3 -4 17.5 -25t21.5 -35.5t7 -28.5q0 -20 -28.5 -50t-62 -55t-62 -53t-28.5 -46q0 -9 5 -22.5t8.5 -20.5t14 -24t11.5 -19q76 -137 174 -235t235 -174 q2 -1 19 -11.5t24 -14t20.5 -8.5t22.5 -5q18 0 46 28.5t53 62t55 62t50 28.5q14 0 28.5 -7t35.5 -21.5t25 -17.5q25 -15 53.5 -31t63.5 -35t54 -30q70 -35 76 -53q3 -7 3 -21z" /> +<glyph unicode="" horiz-adv-x="1408" d="M1120 1280h-832q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v832q0 66 -47 113t-113 47zM1408 1120v-832q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832 q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="1280" d="M1152 1280h-1024v-1242l423 406l89 85l89 -85l423 -406v1242zM1164 1408q23 0 44 -9q33 -13 52.5 -41t19.5 -62v-1289q0 -34 -19.5 -62t-52.5 -41q-19 -8 -44 -8q-48 0 -83 32l-441 424l-441 -424q-36 -33 -83 -33q-23 0 -44 9q-33 13 -52.5 41t-19.5 62v1289 q0 34 19.5 62t52.5 41q21 9 44 9h1048z" /> +<glyph unicode="" d="M1280 343q0 11 -2 16q-3 8 -38.5 29.5t-88.5 49.5l-53 29q-5 3 -19 13t-25 15t-21 5q-18 0 -47 -32.5t-57 -65.5t-44 -33q-7 0 -16.5 3.5t-15.5 6.5t-17 9.5t-14 8.5q-99 55 -170.5 126.5t-126.5 170.5q-2 3 -8.5 14t-9.5 17t-6.5 15.5t-3.5 16.5q0 13 20.5 33.5t45 38.5 t45 39.5t20.5 36.5q0 10 -5 21t-15 25t-13 19q-3 6 -15 28.5t-25 45.5t-26.5 47.5t-25 40.5t-16.5 18t-16 2q-48 0 -101 -22q-46 -21 -80 -94.5t-34 -130.5q0 -16 2.5 -34t5 -30.5t9 -33t10 -29.5t12.5 -33t11 -30q60 -164 216.5 -320.5t320.5 -216.5q6 -2 30 -11t33 -12.5 t29.5 -10t33 -9t30.5 -5t34 -2.5q57 0 130.5 34t94.5 80q22 53 22 101zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1620 1128q-67 -98 -162 -167q1 -14 1 -42q0 -130 -38 -259.5t-115.5 -248.5t-184.5 -210.5t-258 -146t-323 -54.5q-271 0 -496 145q35 -4 78 -4q225 0 401 138q-105 2 -188 64.5t-114 159.5q33 -5 61 -5q43 0 85 11q-112 23 -185.5 111.5t-73.5 205.5v4q68 -38 146 -41 q-66 44 -105 115t-39 154q0 88 44 163q121 -149 294.5 -238.5t371.5 -99.5q-8 38 -8 74q0 134 94.5 228.5t228.5 94.5q140 0 236 -102q109 21 205 78q-37 -115 -142 -178q93 10 186 50z" /> +<glyph unicode="" horiz-adv-x="1024" d="M959 1524v-264h-157q-86 0 -116 -36t-30 -108v-189h293l-39 -296h-254v-759h-306v759h-255v296h255v218q0 186 104 288.5t277 102.5q147 0 228 -12z" /> +<glyph unicode="" d="M1536 640q0 -251 -146.5 -451.5t-378.5 -277.5q-27 -5 -39.5 7t-12.5 30v211q0 97 -52 142q57 6 102.5 18t94 39t81 66.5t53 105t20.5 150.5q0 121 -79 206q37 91 -8 204q-28 9 -81 -11t-92 -44l-38 -24q-93 26 -192 26t-192 -26q-16 11 -42.5 27t-83.5 38.5t-86 13.5 q-44 -113 -7 -204q-79 -85 -79 -206q0 -85 20.5 -150t52.5 -105t80.5 -67t94 -39t102.5 -18q-40 -36 -49 -103q-21 -10 -45 -15t-57 -5t-65.5 21.5t-55.5 62.5q-19 32 -48.5 52t-49.5 24l-20 3q-21 0 -29 -4.5t-5 -11.5t9 -14t13 -12l7 -5q22 -10 43.5 -38t31.5 -51l10 -23 q13 -38 44 -61.5t67 -30t69.5 -7t55.5 3.5l23 4q0 -38 0.5 -89t0.5 -54q0 -18 -13 -30t-40 -7q-232 77 -378.5 277.5t-146.5 451.5q0 209 103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1664 960v-256q0 -26 -19 -45t-45 -19h-64q-26 0 -45 19t-19 45v256q0 106 -75 181t-181 75t-181 -75t-75 -181v-192h96q40 0 68 -28t28 -68v-576q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v576q0 40 28 68t68 28h672v192q0 185 131.5 316.5t316.5 131.5 t316.5 -131.5t131.5 -316.5z" /> +<glyph unicode="" horiz-adv-x="1920" d="M1760 1408q66 0 113 -47t47 -113v-1216q0 -66 -47 -113t-113 -47h-1600q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1600zM160 1280q-13 0 -22.5 -9.5t-9.5 -22.5v-224h1664v224q0 13 -9.5 22.5t-22.5 9.5h-1600zM1760 0q13 0 22.5 9.5t9.5 22.5v608h-1664v-608 q0 -13 9.5 -22.5t22.5 -9.5h1600zM256 128v128h256v-128h-256zM640 128v128h384v-128h-384z" /> +<glyph unicode="" horiz-adv-x="1408" d="M384 192q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM896 69q2 -28 -17 -48q-18 -21 -47 -21h-135q-25 0 -43 16.5t-20 41.5q-22 229 -184.5 391.5t-391.5 184.5q-25 2 -41.5 20t-16.5 43v135q0 29 21 47q17 17 43 17h5q160 -13 306 -80.5 t259 -181.5q114 -113 181.5 -259t80.5 -306zM1408 67q2 -27 -18 -47q-18 -20 -46 -20h-143q-26 0 -44.5 17.5t-19.5 42.5q-12 215 -101 408.5t-231.5 336t-336 231.5t-408.5 102q-25 1 -42.5 19.5t-17.5 43.5v143q0 28 20 46q18 18 44 18h3q262 -13 501.5 -120t425.5 -294 q187 -186 294 -425.5t120 -501.5z" /> +<glyph unicode="" d="M1040 320q0 -33 -23.5 -56.5t-56.5 -23.5t-56.5 23.5t-23.5 56.5t23.5 56.5t56.5 23.5t56.5 -23.5t23.5 -56.5zM1296 320q0 -33 -23.5 -56.5t-56.5 -23.5t-56.5 23.5t-23.5 56.5t23.5 56.5t56.5 23.5t56.5 -23.5t23.5 -56.5zM1408 160v320q0 13 -9.5 22.5t-22.5 9.5 h-1216q-13 0 -22.5 -9.5t-9.5 -22.5v-320q0 -13 9.5 -22.5t22.5 -9.5h1216q13 0 22.5 9.5t9.5 22.5zM178 640h1180l-157 482q-4 13 -16 21.5t-26 8.5h-782q-14 0 -26 -8.5t-16 -21.5zM1536 480v-320q0 -66 -47 -113t-113 -47h-1216q-66 0 -113 47t-47 113v320q0 25 16 75 l197 606q17 53 63 86t101 33h782q55 0 101 -33t63 -86l197 -606q16 -50 16 -75z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1664 896q53 0 90.5 -37.5t37.5 -90.5t-37.5 -90.5t-90.5 -37.5v-384q0 -52 -38 -90t-90 -38q-417 347 -812 380q-58 -19 -91 -66t-31 -100.5t40 -92.5q-20 -33 -23 -65.5t6 -58t33.5 -55t48 -50t61.5 -50.5q-29 -58 -111.5 -83t-168.5 -11.5t-132 55.5q-7 23 -29.5 87.5 t-32 94.5t-23 89t-15 101t3.5 98.5t22 110.5h-122q-66 0 -113 47t-47 113v192q0 66 47 113t113 47h480q435 0 896 384q52 0 90 -38t38 -90v-384zM1536 292v954q-394 -302 -768 -343v-270q377 -42 768 -341z" /> +<glyph unicode="" horiz-adv-x="1792" d="M912 -160q0 16 -16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5q16 0 16 16zM246 128h1300q-266 300 -266 832q0 51 -24 105t-69 103t-121.5 80.5t-169.5 31.5t-169.5 -31.5t-121.5 -80.5t-69 -103t-24 -105q0 -532 -266 -832z M1728 128q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-181 75t-75 181h-448q-52 0 -90 38t-38 90q50 42 91 88t85 119.5t74.5 158.5t50 206t19.5 260q0 152 117 282.5t307 158.5q-8 19 -8 39q0 40 28 68t68 28t68 -28t28 -68q0 -20 -8 -39q190 -28 307 -158.5 t117 -282.5q0 -139 19.5 -260t50 -206t74.5 -158.5t85 -119.5t91 -88z" /> +<glyph unicode="" d="M1376 640l138 -135q30 -28 20 -70q-12 -41 -52 -51l-188 -48l53 -186q12 -41 -19 -70q-29 -31 -70 -19l-186 53l-48 -188q-10 -40 -51 -52q-12 -2 -19 -2q-31 0 -51 22l-135 138l-135 -138q-28 -30 -70 -20q-41 11 -51 52l-48 188l-186 -53q-41 -12 -70 19q-31 29 -19 70 l53 186l-188 48q-40 10 -52 51q-10 42 20 70l138 135l-138 135q-30 28 -20 70q12 41 52 51l188 48l-53 186q-12 41 19 70q29 31 70 19l186 -53l48 188q10 41 51 51q41 12 70 -19l135 -139l135 139q29 30 70 19q41 -10 51 -51l48 -188l186 53q41 12 70 -19q31 -29 19 -70 l-53 -186l188 -48q40 -10 52 -51q10 -42 -20 -70z" /> +<glyph unicode="" horiz-adv-x="1792" d="M256 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 768q0 51 -39 89.5t-89 38.5h-576q0 20 15 48.5t33 55t33 68t15 84.5q0 67 -44.5 97.5t-115.5 30.5q-24 0 -90 -139q-24 -44 -37 -65q-40 -64 -112 -145q-71 -81 -101 -106 q-69 -57 -140 -57h-32v-640h32q72 0 167 -32t193.5 -64t179.5 -32q189 0 189 167q0 26 -5 56q30 16 47.5 52.5t17.5 73.5t-18 69q53 50 53 119q0 25 -10 55.5t-25 47.5h331q52 0 90 38t38 90zM1792 769q0 -105 -75.5 -181t-180.5 -76h-169q-4 -62 -37 -119q3 -21 3 -43 q0 -101 -60 -178q1 -139 -85 -219.5t-227 -80.5q-133 0 -322 69q-164 59 -223 59h-288q-53 0 -90.5 37.5t-37.5 90.5v640q0 53 37.5 90.5t90.5 37.5h288q10 0 21.5 4.5t23.5 14t22.5 18t24 22.5t20.5 21.5t19 21.5t14 17q65 74 100 129q13 21 33 62t37 72t40.5 63t55 49.5 t69.5 17.5q125 0 206.5 -67t81.5 -189q0 -68 -22 -128h374q104 0 180 -76t76 -179z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1376 128h32v640h-32q-35 0 -67.5 12t-62.5 37t-50 46t-49 54q-2 3 -3.5 4.5t-4 4.5t-4.5 5q-72 81 -112 145q-14 22 -38 68q-1 3 -10.5 22.5t-18.5 36t-20 35.5t-21.5 30.5t-18.5 11.5q-71 0 -115.5 -30.5t-44.5 -97.5q0 -43 15 -84.5t33 -68t33 -55t15 -48.5h-576 q-50 0 -89 -38.5t-39 -89.5q0 -52 38 -90t90 -38h331q-15 -17 -25 -47.5t-10 -55.5q0 -69 53 -119q-18 -32 -18 -69t17.5 -73.5t47.5 -52.5q-4 -24 -4 -56q0 -85 48.5 -126t135.5 -41q84 0 183 32t194 64t167 32zM1664 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45 t45 -19t45 19t19 45zM1792 768v-640q0 -53 -37.5 -90.5t-90.5 -37.5h-288q-59 0 -223 -59q-190 -69 -317 -69q-142 0 -230 77.5t-87 217.5l1 5q-61 76 -61 178q0 22 3 43q-33 57 -37 119h-169q-105 0 -180.5 76t-75.5 181q0 103 76 179t180 76h374q-22 60 -22 128 q0 122 81.5 189t206.5 67q38 0 69.5 -17.5t55 -49.5t40.5 -63t37 -72t33 -62q35 -55 100 -129q2 -3 14 -17t19 -21.5t20.5 -21.5t24 -22.5t22.5 -18t23.5 -14t21.5 -4.5h288q53 0 90.5 -37.5t37.5 -90.5z" /> +<glyph unicode="" d="M1280 -64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 700q0 189 -167 189q-26 0 -56 -5q-16 30 -52.5 47.5t-73.5 17.5t-69 -18q-50 53 -119 53q-25 0 -55.5 -10t-47.5 -25v331q0 52 -38 90t-90 38q-51 0 -89.5 -39t-38.5 -89v-576 q-20 0 -48.5 15t-55 33t-68 33t-84.5 15q-67 0 -97.5 -44.5t-30.5 -115.5q0 -24 139 -90q44 -24 65 -37q64 -40 145 -112q81 -71 106 -101q57 -69 57 -140v-32h640v32q0 72 32 167t64 193.5t32 179.5zM1536 705q0 -133 -69 -322q-59 -164 -59 -223v-288q0 -53 -37.5 -90.5 t-90.5 -37.5h-640q-53 0 -90.5 37.5t-37.5 90.5v288q0 10 -4.5 21.5t-14 23.5t-18 22.5t-22.5 24t-21.5 20.5t-21.5 19t-17 14q-74 65 -129 100q-21 13 -62 33t-72 37t-63 40.5t-49.5 55t-17.5 69.5q0 125 67 206.5t189 81.5q68 0 128 -22v374q0 104 76 180t179 76 q105 0 181 -75.5t76 -180.5v-169q62 -4 119 -37q21 3 43 3q101 0 178 -60q139 1 219.5 -85t80.5 -227z" /> +<glyph unicode="" d="M1408 576q0 84 -32 183t-64 194t-32 167v32h-640v-32q0 -35 -12 -67.5t-37 -62.5t-46 -50t-54 -49q-9 -8 -14 -12q-81 -72 -145 -112q-22 -14 -68 -38q-3 -1 -22.5 -10.5t-36 -18.5t-35.5 -20t-30.5 -21.5t-11.5 -18.5q0 -71 30.5 -115.5t97.5 -44.5q43 0 84.5 15t68 33 t55 33t48.5 15v-576q0 -50 38.5 -89t89.5 -39q52 0 90 38t38 90v331q46 -35 103 -35q69 0 119 53q32 -18 69 -18t73.5 17.5t52.5 47.5q24 -4 56 -4q85 0 126 48.5t41 135.5zM1280 1344q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1536 580 q0 -142 -77.5 -230t-217.5 -87l-5 1q-76 -61 -178 -61q-22 0 -43 3q-54 -30 -119 -37v-169q0 -105 -76 -180.5t-181 -75.5q-103 0 -179 76t-76 180v374q-54 -22 -128 -22q-121 0 -188.5 81.5t-67.5 206.5q0 38 17.5 69.5t49.5 55t63 40.5t72 37t62 33q55 35 129 100 q3 2 17 14t21.5 19t21.5 20.5t22.5 24t18 22.5t14 23.5t4.5 21.5v288q0 53 37.5 90.5t90.5 37.5h640q53 0 90.5 -37.5t37.5 -90.5v-288q0 -59 59 -223q69 -190 69 -317z" /> +<glyph unicode="" d="M1280 576v128q0 26 -19 45t-45 19h-502l189 189q19 19 19 45t-19 45l-91 91q-18 18 -45 18t-45 -18l-362 -362l-91 -91q-18 -18 -18 -45t18 -45l91 -91l362 -362q18 -18 45 -18t45 18l91 91q18 18 18 45t-18 45l-189 189h502q26 0 45 19t19 45zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1285 640q0 27 -18 45l-91 91l-362 362q-18 18 -45 18t-45 -18l-91 -91q-18 -18 -18 -45t18 -45l189 -189h-502q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h502l-189 -189q-19 -19 -19 -45t19 -45l91 -91q18 -18 45 -18t45 18l362 362l91 91q18 18 18 45zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1284 641q0 27 -18 45l-362 362l-91 91q-18 18 -45 18t-45 -18l-91 -91l-362 -362q-18 -18 -18 -45t18 -45l91 -91q18 -18 45 -18t45 18l189 189v-502q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v502l189 -189q19 -19 45 -19t45 19l91 91q18 18 18 45zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1284 639q0 27 -18 45l-91 91q-18 18 -45 18t-45 -18l-189 -189v502q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-502l-189 189q-19 19 -45 19t-45 -19l-91 -91q-18 -18 -18 -45t18 -45l362 -362l91 -91q18 -18 45 -18t45 18l91 91l362 362q18 18 18 45zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM1042 887q-2 -1 -9.5 -9.5t-13.5 -9.5q2 0 4.5 5t5 11t3.5 7q6 7 22 15q14 6 52 12q34 8 51 -11 q-2 2 9.5 13t14.5 12q3 2 15 4.5t15 7.5l2 22q-12 -1 -17.5 7t-6.5 21q0 -2 -6 -8q0 7 -4.5 8t-11.5 -1t-9 -1q-10 3 -15 7.5t-8 16.5t-4 15q-2 5 -9.5 10.5t-9.5 10.5q-1 2 -2.5 5.5t-3 6.5t-4 5.5t-5.5 2.5t-7 -5t-7.5 -10t-4.5 -5q-3 2 -6 1.5t-4.5 -1t-4.5 -3t-5 -3.5 q-3 -2 -8.5 -3t-8.5 -2q15 5 -1 11q-10 4 -16 3q9 4 7.5 12t-8.5 14h5q-1 4 -8.5 8.5t-17.5 8.5t-13 6q-8 5 -34 9.5t-33 0.5q-5 -6 -4.5 -10.5t4 -14t3.5 -12.5q1 -6 -5.5 -13t-6.5 -12q0 -7 14 -15.5t10 -21.5q-3 -8 -16 -16t-16 -12q-5 -8 -1.5 -18.5t10.5 -16.5 q2 -2 1.5 -4t-3.5 -4.5t-5.5 -4t-6.5 -3.5l-3 -2q-11 -5 -20.5 6t-13.5 26q-7 25 -16 30q-23 8 -29 -1q-5 13 -41 26q-25 9 -58 4q6 1 0 15q-7 15 -19 12q3 6 4 17.5t1 13.5q3 13 12 23q1 1 7 8.5t9.5 13.5t0.5 6q35 -4 50 11q5 5 11.5 17t10.5 17q9 6 14 5.5t14.5 -5.5 t14.5 -5q14 -1 15.5 11t-7.5 20q12 -1 3 17q-5 7 -8 9q-12 4 -27 -5q-8 -4 2 -8q-1 1 -9.5 -10.5t-16.5 -17.5t-16 5q-1 1 -5.5 13.5t-9.5 13.5q-8 0 -16 -15q3 8 -11 15t-24 8q19 12 -8 27q-7 4 -20.5 5t-19.5 -4q-5 -7 -5.5 -11.5t5 -8t10.5 -5.5t11.5 -4t8.5 -3 q14 -10 8 -14q-2 -1 -8.5 -3.5t-11.5 -4.5t-6 -4q-3 -4 0 -14t-2 -14q-5 5 -9 17.5t-7 16.5q7 -9 -25 -6l-10 1q-4 0 -16 -2t-20.5 -1t-13.5 8q-4 8 0 20q1 4 4 2q-4 3 -11 9.5t-10 8.5q-46 -15 -94 -41q6 -1 12 1q5 2 13 6.5t10 5.5q34 14 42 7l5 5q14 -16 20 -25 q-7 4 -30 1q-20 -6 -22 -12q7 -12 5 -18q-4 3 -11.5 10t-14.5 11t-15 5q-16 0 -22 -1q-146 -80 -235 -222q7 -7 12 -8q4 -1 5 -9t2.5 -11t11.5 3q9 -8 3 -19q1 1 44 -27q19 -17 21 -21q3 -11 -10 -18q-1 2 -9 9t-9 4q-3 -5 0.5 -18.5t10.5 -12.5q-7 0 -9.5 -16t-2.5 -35.5 t-1 -23.5l2 -1q-3 -12 5.5 -34.5t21.5 -19.5q-13 -3 20 -43q6 -8 8 -9q3 -2 12 -7.5t15 -10t10 -10.5q4 -5 10 -22.5t14 -23.5q-2 -6 9.5 -20t10.5 -23q-1 0 -2.5 -1t-2.5 -1q3 -7 15.5 -14t15.5 -13q1 -3 2 -10t3 -11t8 -2q2 20 -24 62q-15 25 -17 29q-3 5 -5.5 15.5 t-4.5 14.5q2 0 6 -1.5t8.5 -3.5t7.5 -4t2 -3q-3 -7 2 -17.5t12 -18.5t17 -19t12 -13q6 -6 14 -19.5t0 -13.5q9 0 20 -10t17 -20q5 -8 8 -26t5 -24q2 -7 8.5 -13.5t12.5 -9.5l16 -8t13 -7q5 -2 18.5 -10.5t21.5 -11.5q10 -4 16 -4t14.5 2.5t13.5 3.5q15 2 29 -15t21 -21 q36 -19 55 -11q-2 -1 0.5 -7.5t8 -15.5t9 -14.5t5.5 -8.5q5 -6 18 -15t18 -15q6 4 7 9q-3 -8 7 -20t18 -10q14 3 14 32q-31 -15 -49 18q0 1 -2.5 5.5t-4 8.5t-2.5 8.5t0 7.5t5 3q9 0 10 3.5t-2 12.5t-4 13q-1 8 -11 20t-12 15q-5 -9 -16 -8t-16 9q0 -1 -1.5 -5.5t-1.5 -6.5 q-13 0 -15 1q1 3 2.5 17.5t3.5 22.5q1 4 5.5 12t7.5 14.5t4 12.5t-4.5 9.5t-17.5 2.5q-19 -1 -26 -20q-1 -3 -3 -10.5t-5 -11.5t-9 -7q-7 -3 -24 -2t-24 5q-13 8 -22.5 29t-9.5 37q0 10 2.5 26.5t3 25t-5.5 24.5q3 2 9 9.5t10 10.5q2 1 4.5 1.5t4.5 0t4 1.5t3 6q-1 1 -4 3 q-3 3 -4 3q7 -3 28.5 1.5t27.5 -1.5q15 -11 22 2q0 1 -2.5 9.5t-0.5 13.5q5 -27 29 -9q3 -3 15.5 -5t17.5 -5q3 -2 7 -5.5t5.5 -4.5t5 0.5t8.5 6.5q10 -14 12 -24q11 -40 19 -44q7 -3 11 -2t4.5 9.5t0 14t-1.5 12.5l-1 8v18l-1 8q-15 3 -18.5 12t1.5 18.5t15 18.5q1 1 8 3.5 t15.5 6.5t12.5 8q21 19 15 35q7 0 11 9q-1 0 -5 3t-7.5 5t-4.5 2q9 5 2 16q5 3 7.5 11t7.5 10q9 -12 21 -2q7 8 1 16q5 7 20.5 10.5t18.5 9.5q7 -2 8 2t1 12t3 12q4 5 15 9t13 5l17 11q3 4 0 4q18 -2 31 11q10 11 -6 20q3 6 -3 9.5t-15 5.5q3 1 11.5 0.5t10.5 1.5 q15 10 -7 16q-17 5 -43 -12zM879 10q206 36 351 189q-3 3 -12.5 4.5t-12.5 3.5q-18 7 -24 8q1 7 -2.5 13t-8 9t-12.5 8t-11 7q-2 2 -7 6t-7 5.5t-7.5 4.5t-8.5 2t-10 -1l-3 -1q-3 -1 -5.5 -2.5t-5.5 -3t-4 -3t0 -2.5q-21 17 -36 22q-5 1 -11 5.5t-10.5 7t-10 1.5t-11.5 -7 q-5 -5 -6 -15t-2 -13q-7 5 0 17.5t2 18.5q-3 6 -10.5 4.5t-12 -4.5t-11.5 -8.5t-9 -6.5t-8.5 -5.5t-8.5 -7.5q-3 -4 -6 -12t-5 -11q-2 4 -11.5 6.5t-9.5 5.5q2 -10 4 -35t5 -38q7 -31 -12 -48q-27 -25 -29 -40q-4 -22 12 -26q0 -7 -8 -20.5t-7 -21.5q0 -6 2 -16z" /> +<glyph unicode="" horiz-adv-x="1664" d="M384 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1028 484l-682 -682q-37 -37 -90 -37q-52 0 -91 37l-106 108q-38 36 -38 90q0 53 38 91l681 681q39 -98 114.5 -173.5t173.5 -114.5zM1662 919q0 -39 -23 -106q-47 -134 -164.5 -217.5 t-258.5 -83.5q-185 0 -316.5 131.5t-131.5 316.5t131.5 316.5t316.5 131.5q58 0 121.5 -16.5t107.5 -46.5q16 -11 16 -28t-16 -28l-293 -169v-224l193 -107q5 3 79 48.5t135.5 81t70.5 35.5q15 0 23.5 -10t8.5 -25z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1024 128h640v128h-640v-128zM640 640h1024v128h-1024v-128zM1280 1152h384v128h-384v-128zM1792 320v-256q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 832v-256q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19 t-19 45v256q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 1344v-256q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h1664q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1408" d="M1403 1241q17 -41 -14 -70l-493 -493v-742q0 -42 -39 -59q-13 -5 -25 -5q-27 0 -45 19l-256 256q-19 19 -19 45v486l-493 493q-31 29 -14 70q17 39 59 39h1280q42 0 59 -39z" /> +<glyph unicode="" horiz-adv-x="1792" d="M640 1280h512v128h-512v-128zM1792 640v-480q0 -66 -47 -113t-113 -47h-1472q-66 0 -113 47t-47 113v480h672v-160q0 -26 19 -45t45 -19h320q26 0 45 19t19 45v160h672zM1024 640v-128h-256v128h256zM1792 1120v-384h-1792v384q0 66 47 113t113 47h352v160q0 40 28 68 t68 28h576q40 0 68 -28t28 -68v-160h352q66 0 113 -47t47 -113z" /> +<glyph unicode="" d="M1283 995l-355 -355l355 -355l144 144q29 31 70 14q39 -17 39 -59v-448q0 -26 -19 -45t-45 -19h-448q-42 0 -59 40q-17 39 14 69l144 144l-355 355l-355 -355l144 -144q31 -30 14 -69q-17 -40 -59 -40h-448q-26 0 -45 19t-19 45v448q0 42 40 59q39 17 69 -14l144 -144 l355 355l-355 355l-144 -144q-19 -19 -45 -19q-12 0 -24 5q-40 17 -40 59v448q0 26 19 45t45 19h448q42 0 59 -40q17 -39 -14 -69l-144 -144l355 -355l355 355l-144 144q-31 30 -14 69q17 40 59 40h448q26 0 45 -19t19 -45v-448q0 -42 -39 -59q-13 -5 -25 -5q-26 0 -45 19z " /> +<glyph unicode="" horiz-adv-x="1920" d="M593 640q-162 -5 -265 -128h-134q-82 0 -138 40.5t-56 118.5q0 353 124 353q6 0 43.5 -21t97.5 -42.5t119 -21.5q67 0 133 23q-5 -37 -5 -66q0 -139 81 -256zM1664 3q0 -120 -73 -189.5t-194 -69.5h-874q-121 0 -194 69.5t-73 189.5q0 53 3.5 103.5t14 109t26.5 108.5 t43 97.5t62 81t85.5 53.5t111.5 20q10 0 43 -21.5t73 -48t107 -48t135 -21.5t135 21.5t107 48t73 48t43 21.5q61 0 111.5 -20t85.5 -53.5t62 -81t43 -97.5t26.5 -108.5t14 -109t3.5 -103.5zM640 1280q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75 t75 -181zM1344 896q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5zM1920 671q0 -78 -56 -118.5t-138 -40.5h-134q-103 123 -265 128q81 117 81 256q0 29 -5 66q66 -23 133 -23q59 0 119 21.5t97.5 42.5 t43.5 21q124 0 124 -353zM1792 1280q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1456 320q0 40 -28 68l-208 208q-28 28 -68 28q-42 0 -72 -32q3 -3 19 -18.5t21.5 -21.5t15 -19t13 -25.5t3.5 -27.5q0 -40 -28 -68t-68 -28q-15 0 -27.5 3.5t-25.5 13t-19 15t-21.5 21.5t-18.5 19q-33 -31 -33 -73q0 -40 28 -68l206 -207q27 -27 68 -27q40 0 68 26 l147 146q28 28 28 67zM753 1025q0 40 -28 68l-206 207q-28 28 -68 28q-39 0 -68 -27l-147 -146q-28 -28 -28 -67q0 -40 28 -68l208 -208q27 -27 68 -27q42 0 72 31q-3 3 -19 18.5t-21.5 21.5t-15 19t-13 25.5t-3.5 27.5q0 40 28 68t68 28q15 0 27.5 -3.5t25.5 -13t19 -15 t21.5 -21.5t18.5 -19q33 31 33 73zM1648 320q0 -120 -85 -203l-147 -146q-83 -83 -203 -83q-121 0 -204 85l-206 207q-83 83 -83 203q0 123 88 209l-88 88q-86 -88 -208 -88q-120 0 -204 84l-208 208q-84 84 -84 204t85 203l147 146q83 83 203 83q121 0 204 -85l206 -207 q83 -83 83 -203q0 -123 -88 -209l88 -88q86 88 208 88q120 0 204 -84l208 -208q84 -84 84 -204z" /> +<glyph unicode="" horiz-adv-x="1920" d="M1920 384q0 -159 -112.5 -271.5t-271.5 -112.5h-1088q-185 0 -316.5 131.5t-131.5 316.5q0 132 71 241.5t187 163.5q-2 28 -2 43q0 212 150 362t362 150q158 0 286.5 -88t187.5 -230q70 62 166 62q106 0 181 -75t75 -181q0 -75 -41 -138q129 -30 213 -134.5t84 -239.5z " /> +<glyph unicode="" horiz-adv-x="1664" d="M1527 88q56 -89 21.5 -152.5t-140.5 -63.5h-1152q-106 0 -140.5 63.5t21.5 152.5l503 793v399h-64q-26 0 -45 19t-19 45t19 45t45 19h512q26 0 45 -19t19 -45t-19 -45t-45 -19h-64v-399zM748 813l-272 -429h712l-272 429l-20 31v37v399h-128v-399v-37z" /> +<glyph unicode="" horiz-adv-x="1792" d="M960 640q26 0 45 -19t19 -45t-19 -45t-45 -19t-45 19t-19 45t19 45t45 19zM1260 576l507 -398q28 -20 25 -56q-5 -35 -35 -51l-128 -64q-13 -7 -29 -7q-17 0 -31 8l-690 387l-110 -66q-8 -4 -12 -5q14 -49 10 -97q-7 -77 -56 -147.5t-132 -123.5q-132 -84 -277 -84 q-136 0 -222 78q-90 84 -79 207q7 76 56 147t131 124q132 84 278 84q83 0 151 -31q9 13 22 22l122 73l-122 73q-13 9 -22 22q-68 -31 -151 -31q-146 0 -278 84q-82 53 -131 124t-56 147q-5 59 15.5 113t63.5 93q85 79 222 79q145 0 277 -84q83 -52 132 -123t56 -148 q4 -48 -10 -97q4 -1 12 -5l110 -66l690 387q14 8 31 8q16 0 29 -7l128 -64q30 -16 35 -51q3 -36 -25 -56zM579 836q46 42 21 108t-106 117q-92 59 -192 59q-74 0 -113 -36q-46 -42 -21 -108t106 -117q92 -59 192 -59q74 0 113 36zM494 91q81 51 106 117t-21 108 q-39 36 -113 36q-100 0 -192 -59q-81 -51 -106 -117t21 -108q39 -36 113 -36q100 0 192 59zM672 704l96 -58v11q0 36 33 56l14 8l-79 47l-26 -26q-3 -3 -10 -11t-12 -12q-2 -2 -4 -3.5t-3 -2.5zM896 480l96 -32l736 576l-128 64l-768 -431v-113l-160 -96l9 -8q2 -2 7 -6 q4 -4 11 -12t11 -12l26 -26zM1600 64l128 64l-520 408l-177 -138q-2 -3 -13 -7z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1696 1152q40 0 68 -28t28 -68v-1216q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v288h-544q-40 0 -68 28t-28 68v672q0 40 20 88t48 76l408 408q28 28 76 48t88 20h416q40 0 68 -28t28 -68v-328q68 40 128 40h416zM1152 939l-299 -299h299v299zM512 1323l-299 -299 h299v299zM708 676l316 316v416h-384v-416q0 -40 -28 -68t-68 -28h-416v-640h512v256q0 40 20 88t48 76zM1664 -128v1152h-384v-416q0 -40 -28 -68t-68 -28h-416v-640h896z" /> +<glyph unicode="" horiz-adv-x="1408" d="M1404 151q0 -117 -79 -196t-196 -79q-135 0 -235 100l-777 776q-113 115 -113 271q0 159 110 270t269 111q158 0 273 -113l605 -606q10 -10 10 -22q0 -16 -30.5 -46.5t-46.5 -30.5q-13 0 -23 10l-606 607q-79 77 -181 77q-106 0 -179 -75t-73 -181q0 -105 76 -181 l776 -777q63 -63 145 -63q64 0 106 42t42 106q0 82 -63 145l-581 581q-26 24 -60 24q-29 0 -48 -19t-19 -48q0 -32 25 -59l410 -410q10 -10 10 -22q0 -16 -31 -47t-47 -31q-12 0 -22 10l-410 410q-63 61 -63 149q0 82 57 139t139 57q88 0 149 -63l581 -581q100 -98 100 -235 z" /> +<glyph unicode="" d="M384 0h768v384h-768v-384zM1280 0h128v896q0 14 -10 38.5t-20 34.5l-281 281q-10 10 -34 20t-39 10v-416q0 -40 -28 -68t-68 -28h-576q-40 0 -68 28t-28 68v416h-128v-1280h128v416q0 40 28 68t68 28h832q40 0 68 -28t28 -68v-416zM896 928v320q0 13 -9.5 22.5t-22.5 9.5 h-192q-13 0 -22.5 -9.5t-9.5 -22.5v-320q0 -13 9.5 -22.5t22.5 -9.5h192q13 0 22.5 9.5t9.5 22.5zM1536 896v-928q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h928q40 0 88 -20t76 -48l280 -280q28 -28 48 -76t20 -88z" /> +<glyph unicode="" d="M1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" d="M1536 192v-128q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1536 704v-128q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1536 1216v-128q0 -26 -19 -45 t-45 -19h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1792" d="M384 128q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM384 640q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1792 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5 t22.5 9.5h1216q13 0 22.5 -9.5t9.5 -22.5zM384 1152q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1792 736v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1216q13 0 22.5 -9.5t9.5 -22.5z M1792 1248v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1216q13 0 22.5 -9.5t9.5 -22.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M381 -84q0 -80 -54.5 -126t-135.5 -46q-106 0 -172 66l57 88q49 -45 106 -45q29 0 50.5 14.5t21.5 42.5q0 64 -105 56l-26 56q8 10 32.5 43.5t42.5 54t37 38.5v1q-16 0 -48.5 -1t-48.5 -1v-53h-106v152h333v-88l-95 -115q51 -12 81 -49t30 -88zM383 543v-159h-362 q-6 36 -6 54q0 51 23.5 93t56.5 68t66 47.5t56.5 43.5t23.5 45q0 25 -14.5 38.5t-39.5 13.5q-46 0 -81 -58l-85 59q24 51 71.5 79.5t105.5 28.5q73 0 123 -41.5t50 -112.5q0 -50 -34 -91.5t-75 -64.5t-75.5 -50.5t-35.5 -52.5h127v60h105zM1792 224v-192q0 -13 -9.5 -22.5 t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 14 9 23t23 9h1216q13 0 22.5 -9.5t9.5 -22.5zM384 1123v-99h-335v99h107q0 41 0.5 122t0.5 121v12h-2q-8 -17 -50 -54l-71 76l136 127h106v-404h108zM1792 736v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5 t-9.5 22.5v192q0 14 9 23t23 9h1216q13 0 22.5 -9.5t9.5 -22.5zM1792 1248v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1216q13 0 22.5 -9.5t9.5 -22.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1760 640q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-1728q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h1728zM483 704q-28 35 -51 80q-48 97 -48 188q0 181 134 309q133 127 393 127q50 0 167 -19q66 -12 177 -48q10 -38 21 -118q14 -123 14 -183q0 -18 -5 -45l-12 -3l-84 6 l-14 2q-50 149 -103 205q-88 91 -210 91q-114 0 -182 -59q-67 -58 -67 -146q0 -73 66 -140t279 -129q69 -20 173 -66q58 -28 95 -52h-743zM990 448h411q7 -39 7 -92q0 -111 -41 -212q-23 -55 -71 -104q-37 -35 -109 -81q-80 -48 -153 -66q-80 -21 -203 -21q-114 0 -195 23 l-140 40q-57 16 -72 28q-8 8 -8 22v13q0 108 -2 156q-1 30 0 68l2 37v44l102 2q15 -34 30 -71t22.5 -56t12.5 -27q35 -57 80 -94q43 -36 105 -57q59 -22 132 -22q64 0 139 27q77 26 122 86q47 61 47 129q0 84 -81 157q-34 29 -137 71z" /> +<glyph unicode="" d="M48 1313q-37 2 -45 4l-3 88q13 1 40 1q60 0 112 -4q132 -7 166 -7q86 0 168 3q116 4 146 5q56 0 86 2l-1 -14l2 -64v-9q-60 -9 -124 -9q-60 0 -79 -25q-13 -14 -13 -132q0 -13 0.5 -32.5t0.5 -25.5l1 -229l14 -280q6 -124 51 -202q35 -59 96 -92q88 -47 177 -47 q104 0 191 28q56 18 99 51q48 36 65 64q36 56 53 114q21 73 21 229q0 79 -3.5 128t-11 122.5t-13.5 159.5l-4 59q-5 67 -24 88q-34 35 -77 34l-100 -2l-14 3l2 86h84l205 -10q76 -3 196 10l18 -2q6 -38 6 -51q0 -7 -4 -31q-45 -12 -84 -13q-73 -11 -79 -17q-15 -15 -15 -41 q0 -7 1.5 -27t1.5 -31q8 -19 22 -396q6 -195 -15 -304q-15 -76 -41 -122q-38 -65 -112 -123q-75 -57 -182 -89q-109 -33 -255 -33q-167 0 -284 46q-119 47 -179 122q-61 76 -83 195q-16 80 -16 237v333q0 188 -17 213q-25 36 -147 39zM1536 -96v64q0 14 -9 23t-23 9h-1472 q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h1472q14 0 23 9t9 23z" /> +<glyph unicode="" horiz-adv-x="1664" d="M512 160v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM512 544v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1024 160v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23 v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM512 928v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1024 544v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1536 160v192 q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1024 928v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1536 544v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192 q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1536 928v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1664 1248v-1088q0 -66 -47 -113t-113 -47h-1344q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1344q66 0 113 -47t47 -113 z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1190 955l293 293l-107 107l-293 -293zM1637 1248q0 -27 -18 -45l-1286 -1286q-18 -18 -45 -18t-45 18l-198 198q-18 18 -18 45t18 45l1286 1286q18 18 45 18t45 -18l198 -198q18 -18 18 -45zM286 1438l98 -30l-98 -30l-30 -98l-30 98l-98 30l98 30l30 98zM636 1276 l196 -60l-196 -60l-60 -196l-60 196l-196 60l196 60l60 196zM1566 798l98 -30l-98 -30l-30 -98l-30 98l-98 30l98 30l30 98zM926 1438l98 -30l-98 -30l-30 -98l-30 98l-98 30l98 30l30 98z" /> +<glyph unicode="" horiz-adv-x="1792" d="M640 128q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM256 640h384v256h-158q-13 0 -22 -9l-195 -195q-9 -9 -9 -22v-30zM1536 128q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM1792 1216v-1024q0 -15 -4 -26.5t-13.5 -18.5 t-16.5 -11.5t-23.5 -6t-22.5 -2t-25.5 0t-22.5 0.5q0 -106 -75 -181t-181 -75t-181 75t-75 181h-384q0 -106 -75 -181t-181 -75t-181 75t-75 181h-64q-3 0 -22.5 -0.5t-25.5 0t-22.5 2t-23.5 6t-16.5 11.5t-13.5 18.5t-4 26.5q0 26 19 45t45 19v320q0 8 -0.5 35t0 38 t2.5 34.5t6.5 37t14 30.5t22.5 30l198 198q19 19 50.5 32t58.5 13h160v192q0 26 19 45t45 19h1024q26 0 45 -19t19 -45z" /> +<glyph unicode="" d="M1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103q-111 0 -218 32q59 93 78 164q9 34 54 211q20 -39 73 -67.5t114 -28.5q121 0 216 68.5t147 188.5t52 270q0 114 -59.5 214t-172.5 163t-255 63q-105 0 -196 -29t-154.5 -77t-109 -110.5t-67 -129.5t-21.5 -134 q0 -104 40 -183t117 -111q30 -12 38 20q2 7 8 31t8 30q6 23 -11 43q-51 61 -51 151q0 151 104.5 259.5t273.5 108.5q151 0 235.5 -82t84.5 -213q0 -170 -68.5 -289t-175.5 -119q-61 0 -98 43.5t-23 104.5q8 35 26.5 93.5t30 103t11.5 75.5q0 50 -27 83t-77 33 q-62 0 -105 -57t-43 -142q0 -73 25 -122l-99 -418q-17 -70 -13 -177q-206 91 -333 281t-127 423q0 209 103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-725q85 122 108 210q9 34 53 209q21 -39 73.5 -67t112.5 -28q181 0 295.5 147.5t114.5 373.5q0 84 -35 162.5t-96.5 139t-152.5 97t-197 36.5q-104 0 -194.5 -28.5t-153 -76.5 t-107.5 -109.5t-66.5 -128t-21.5 -132.5q0 -102 39.5 -180t116.5 -110q13 -5 23.5 0t14.5 19q10 44 15 61q6 23 -11 42q-50 62 -50 150q0 150 103.5 256.5t270.5 106.5q149 0 232.5 -81t83.5 -210q0 -168 -67.5 -286t-173.5 -118q-60 0 -97 43.5t-23 103.5q8 34 26.5 92.5 t29.5 102t11 74.5q0 49 -26.5 81.5t-75.5 32.5q-61 0 -103.5 -56.5t-42.5 -139.5q0 -72 24 -121l-98 -414q-24 -100 -7 -254h-183q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960z" /> +<glyph unicode="" d="M917 631q0 26 -6 64h-362v-132h217q-3 -24 -16.5 -50t-37.5 -53t-66.5 -44.5t-96.5 -17.5q-99 0 -169 71t-70 171t70 171t169 71q92 0 153 -59l104 101q-108 100 -257 100q-160 0 -272 -112.5t-112 -271.5t112 -271.5t272 -112.5q165 0 266.5 105t101.5 270zM1262 585 h109v110h-109v110h-110v-110h-110v-110h110v-110h110v110zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="2304" d="M1437 623q0 -208 -87 -370.5t-248 -254t-369 -91.5q-149 0 -285 58t-234 156t-156 234t-58 285t58 285t156 234t234 156t285 58q286 0 491 -192l-199 -191q-117 113 -292 113q-123 0 -227.5 -62t-165.5 -168.5t-61 -232.5t61 -232.5t165.5 -168.5t227.5 -62 q83 0 152.5 23t114.5 57.5t78.5 78.5t49 83t21.5 74h-416v252h692q12 -63 12 -122zM2304 745v-210h-209v-209h-210v209h-209v210h209v209h210v-209h209z" /> +<glyph unicode="" horiz-adv-x="1920" d="M768 384h384v96h-128v448h-114l-148 -137l77 -80q42 37 55 57h2v-288h-128v-96zM1280 640q0 -70 -21 -142t-59.5 -134t-101.5 -101t-138 -39t-138 39t-101.5 101t-59.5 134t-21 142t21 142t59.5 134t101.5 101t138 39t138 -39t101.5 -101t59.5 -134t21 -142zM1792 384 v512q-106 0 -181 75t-75 181h-1152q0 -106 -75 -181t-181 -75v-512q106 0 181 -75t75 -181h1152q0 106 75 181t181 75zM1920 1216v-1152q0 -26 -19 -45t-45 -19h-1792q-26 0 -45 19t-19 45v1152q0 26 19 45t45 19h1792q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1024" d="M1024 832q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19l-448 448q-19 19 -19 45t19 45t45 19h896q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1024" d="M1024 320q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45l448 448q19 19 45 19t45 -19l448 -448q19 -19 19 -45z" /> +<glyph unicode="" horiz-adv-x="640" d="M640 1088v-896q0 -26 -19 -45t-45 -19t-45 19l-448 448q-19 19 -19 45t19 45l448 448q19 19 45 19t45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="640" d="M576 640q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19t-19 45v896q0 26 19 45t45 19t45 -19l448 -448q19 -19 19 -45z" /> +<glyph unicode="" horiz-adv-x="1664" d="M160 0h608v1152h-640v-1120q0 -13 9.5 -22.5t22.5 -9.5zM1536 32v1120h-640v-1152h608q13 0 22.5 9.5t9.5 22.5zM1664 1248v-1216q0 -66 -47 -113t-113 -47h-1344q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1344q66 0 113 -47t47 -113z" /> +<glyph unicode="" horiz-adv-x="1024" d="M1024 448q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19l-448 448q-19 19 -19 45t19 45t45 19h896q26 0 45 -19t19 -45zM1024 832q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45l448 448q19 19 45 19t45 -19l448 -448q19 -19 19 -45z" /> +<glyph unicode="" horiz-adv-x="1024" d="M1024 448q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19l-448 448q-19 19 -19 45t19 45t45 19h896q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1024" d="M1024 832q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45l448 448q19 19 45 19t45 -19l448 -448q19 -19 19 -45z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1792 826v-794q0 -66 -47 -113t-113 -47h-1472q-66 0 -113 47t-47 113v794q44 -49 101 -87q362 -246 497 -345q57 -42 92.5 -65.5t94.5 -48t110 -24.5h1h1q51 0 110 24.5t94.5 48t92.5 65.5q170 123 498 345q57 39 100 87zM1792 1120q0 -79 -49 -151t-122 -123 q-376 -261 -468 -325q-10 -7 -42.5 -30.5t-54 -38t-52 -32.5t-57.5 -27t-50 -9h-1h-1q-23 0 -50 9t-57.5 27t-52 32.5t-54 38t-42.5 30.5q-91 64 -262 182.5t-205 142.5q-62 42 -117 115.5t-55 136.5q0 78 41.5 130t118.5 52h1472q65 0 112.5 -47t47.5 -113z" /> +<glyph unicode="" d="M349 911v-991h-330v991h330zM370 1217q1 -73 -50.5 -122t-135.5 -49h-2q-82 0 -132 49t-50 122q0 74 51.5 122.5t134.5 48.5t133 -48.5t51 -122.5zM1536 488v-568h-329v530q0 105 -40.5 164.5t-126.5 59.5q-63 0 -105.5 -34.5t-63.5 -85.5q-11 -30 -11 -81v-553h-329 q2 399 2 647t-1 296l-1 48h329v-144h-2q20 32 41 56t56.5 52t87 43.5t114.5 15.5q171 0 275 -113.5t104 -332.5z" /> +<glyph unicode="" d="M1536 640q0 -156 -61 -298t-164 -245t-245 -164t-298 -61q-172 0 -327 72.5t-264 204.5q-7 10 -6.5 22.5t8.5 20.5l137 138q10 9 25 9q16 -2 23 -12q73 -95 179 -147t225 -52q104 0 198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5t-40.5 198.5t-109.5 163.5 t-163.5 109.5t-198.5 40.5q-98 0 -188 -35.5t-160 -101.5l137 -138q31 -30 14 -69q-17 -40 -59 -40h-448q-26 0 -45 19t-19 45v448q0 42 40 59q39 17 69 -14l130 -129q107 101 244.5 156.5t284.5 55.5q156 0 298 -61t245 -164t164 -245t61 -298z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1771 0q0 -53 -37 -90l-107 -108q-39 -37 -91 -37q-53 0 -90 37l-363 364q-38 36 -38 90q0 53 43 96l-256 256l-126 -126q-14 -14 -34 -14t-34 14q2 -2 12.5 -12t12.5 -13t10 -11.5t10 -13.5t6 -13.5t5.5 -16.5t1.5 -18q0 -38 -28 -68q-3 -3 -16.5 -18t-19 -20.5 t-18.5 -16.5t-22 -15.5t-22 -9t-26 -4.5q-40 0 -68 28l-408 408q-28 28 -28 68q0 13 4.5 26t9 22t15.5 22t16.5 18.5t20.5 19t18 16.5q30 28 68 28q10 0 18 -1.5t16.5 -5.5t13.5 -6t13.5 -10t11.5 -10t13 -12.5t12 -12.5q-14 14 -14 34t14 34l348 348q14 14 34 14t34 -14 q-2 2 -12.5 12t-12.5 13t-10 11.5t-10 13.5t-6 13.5t-5.5 16.5t-1.5 18q0 38 28 68q3 3 16.5 18t19 20.5t18.5 16.5t22 15.5t22 9t26 4.5q40 0 68 -28l408 -408q28 -28 28 -68q0 -13 -4.5 -26t-9 -22t-15.5 -22t-16.5 -18.5t-20.5 -19t-18 -16.5q-30 -28 -68 -28 q-10 0 -18 1.5t-16.5 5.5t-13.5 6t-13.5 10t-11.5 10t-13 12.5t-12 12.5q14 -14 14 -34t-14 -34l-126 -126l256 -256q43 43 96 43q52 0 91 -37l363 -363q37 -39 37 -91z" /> +<glyph unicode="" horiz-adv-x="1792" d="M384 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM576 832q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1004 351l101 382q6 26 -7.5 48.5t-38.5 29.5 t-48 -6.5t-30 -39.5l-101 -382q-60 -5 -107 -43.5t-63 -98.5q-20 -77 20 -146t117 -89t146 20t89 117q16 60 -6 117t-72 91zM1664 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1024 1024q0 53 -37.5 90.5 t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1472 832q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1792 384q0 -261 -141 -483q-19 -29 -54 -29h-1402q-35 0 -54 29 q-141 221 -141 483q0 182 71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" /> +<glyph unicode="" horiz-adv-x="1792" d="M896 1152q-204 0 -381.5 -69.5t-282 -187.5t-104.5 -255q0 -112 71.5 -213.5t201.5 -175.5l87 -50l-27 -96q-24 -91 -70 -172q152 63 275 171l43 38l57 -6q69 -8 130 -8q204 0 381.5 69.5t282 187.5t104.5 255t-104.5 255t-282 187.5t-381.5 69.5zM1792 640 q0 -174 -120 -321.5t-326 -233t-450 -85.5q-70 0 -145 8q-198 -175 -460 -242q-49 -14 -114 -22h-5q-15 0 -27 10.5t-16 27.5v1q-3 4 -0.5 12t2 10t4.5 9.5l6 9t7 8.5t8 9q7 8 31 34.5t34.5 38t31 39.5t32.5 51t27 59t26 76q-157 89 -247.5 220t-90.5 281q0 174 120 321.5 t326 233t450 85.5t450 -85.5t326 -233t120 -321.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M704 1152q-153 0 -286 -52t-211.5 -141t-78.5 -191q0 -82 53 -158t149 -132l97 -56l-35 -84q34 20 62 39l44 31l53 -10q78 -14 153 -14q153 0 286 52t211.5 141t78.5 191t-78.5 191t-211.5 141t-286 52zM704 1280q191 0 353.5 -68.5t256.5 -186.5t94 -257t-94 -257 t-256.5 -186.5t-353.5 -68.5q-86 0 -176 16q-124 -88 -278 -128q-36 -9 -86 -16h-3q-11 0 -20.5 8t-11.5 21q-1 3 -1 6.5t0.5 6.5t2 6l2.5 5t3.5 5.5t4 5t4.5 5t4 4.5q5 6 23 25t26 29.5t22.5 29t25 38.5t20.5 44q-124 72 -195 177t-71 224q0 139 94 257t256.5 186.5 t353.5 68.5zM1526 111q10 -24 20.5 -44t25 -38.5t22.5 -29t26 -29.5t23 -25q1 -1 4 -4.5t4.5 -5t4 -5t3.5 -5.5l2.5 -5t2 -6t0.5 -6.5t-1 -6.5q-3 -14 -13 -22t-22 -7q-50 7 -86 16q-154 40 -278 128q-90 -16 -176 -16q-271 0 -472 132q58 -4 88 -4q161 0 309 45t264 129 q125 92 192 212t67 254q0 77 -23 152q129 -71 204 -178t75 -230q0 -120 -71 -224.5t-195 -176.5z" /> +<glyph unicode="" horiz-adv-x="896" d="M885 970q18 -20 7 -44l-540 -1157q-13 -25 -42 -25q-4 0 -14 2q-17 5 -25.5 19t-4.5 30l197 808l-406 -101q-4 -1 -12 -1q-18 0 -31 11q-18 15 -13 39l201 825q4 14 16 23t28 9h328q19 0 32 -12.5t13 -29.5q0 -8 -5 -18l-171 -463l396 98q8 2 12 2q19 0 34 -15z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1792 288v-320q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h96v192h-512v-192h96q40 0 68 -28t28 -68v-320q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h96v192h-512v-192h96q40 0 68 -28t28 -68v-320 q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h96v192q0 52 38 90t90 38h512v192h-96q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h320q40 0 68 -28t28 -68v-320q0 -40 -28 -68t-68 -28h-96v-192h512q52 0 90 -38t38 -90v-192h96q40 0 68 -28t28 -68 z" /> +<glyph unicode="" horiz-adv-x="1664" d="M896 708v-580q0 -104 -76 -180t-180 -76t-180 76t-76 180q0 26 19 45t45 19t45 -19t19 -45q0 -50 39 -89t89 -39t89 39t39 89v580q33 11 64 11t64 -11zM1664 681q0 -13 -9.5 -22.5t-22.5 -9.5q-11 0 -23 10q-49 46 -93 69t-102 23q-68 0 -128 -37t-103 -97 q-7 -10 -17.5 -28t-14.5 -24q-11 -17 -28 -17q-18 0 -29 17q-4 6 -14.5 24t-17.5 28q-43 60 -102.5 97t-127.5 37t-127.5 -37t-102.5 -97q-7 -10 -17.5 -28t-14.5 -24q-11 -17 -29 -17q-17 0 -28 17q-4 6 -14.5 24t-17.5 28q-43 60 -103 97t-128 37q-58 0 -102 -23t-93 -69 q-12 -10 -23 -10q-13 0 -22.5 9.5t-9.5 22.5q0 5 1 7q45 183 172.5 319.5t298 204.5t360.5 68q140 0 274.5 -40t246.5 -113.5t194.5 -187t115.5 -251.5q1 -2 1 -7zM896 1408v-98q-42 2 -64 2t-64 -2v98q0 26 19 45t45 19t45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1792" d="M768 -128h896v640h-416q-40 0 -68 28t-28 68v416h-384v-1152zM1024 1312v64q0 13 -9.5 22.5t-22.5 9.5h-704q-13 0 -22.5 -9.5t-9.5 -22.5v-64q0 -13 9.5 -22.5t22.5 -9.5h704q13 0 22.5 9.5t9.5 22.5zM1280 640h299l-299 299v-299zM1792 512v-672q0 -40 -28 -68t-68 -28 h-960q-40 0 -68 28t-28 68v160h-544q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h1088q40 0 68 -28t28 -68v-328q21 -13 36 -28l408 -408q28 -28 48 -76t20 -88z" /> +<glyph unicode="" horiz-adv-x="1024" d="M736 960q0 -13 -9.5 -22.5t-22.5 -9.5t-22.5 9.5t-9.5 22.5q0 46 -54 71t-106 25q-13 0 -22.5 9.5t-9.5 22.5t9.5 22.5t22.5 9.5q50 0 99.5 -16t87 -54t37.5 -90zM896 960q0 72 -34.5 134t-90 101.5t-123 62t-136.5 22.5t-136.5 -22.5t-123 -62t-90 -101.5t-34.5 -134 q0 -101 68 -180q10 -11 30.5 -33t30.5 -33q128 -153 141 -298h228q13 145 141 298q10 11 30.5 33t30.5 33q68 79 68 180zM1024 960q0 -155 -103 -268q-45 -49 -74.5 -87t-59.5 -95.5t-34 -107.5q47 -28 47 -82q0 -37 -25 -64q25 -27 25 -64q0 -52 -45 -81q13 -23 13 -47 q0 -46 -31.5 -71t-77.5 -25q-20 -44 -60 -70t-87 -26t-87 26t-60 70q-46 0 -77.5 25t-31.5 71q0 24 13 47q-45 29 -45 81q0 37 25 64q-25 27 -25 64q0 54 47 82q-4 50 -34 107.5t-59.5 95.5t-74.5 87q-103 113 -103 268q0 99 44.5 184.5t117 142t164 89t186.5 32.5 t186.5 -32.5t164 -89t117 -142t44.5 -184.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1792 352v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5q-12 0 -24 10l-319 320q-9 9 -9 22q0 14 9 23l320 320q9 9 23 9q13 0 22.5 -9.5t9.5 -22.5v-192h1376q13 0 22.5 -9.5t9.5 -22.5zM1792 896q0 -14 -9 -23l-320 -320q-9 -9 -23 -9 q-13 0 -22.5 9.5t-9.5 22.5v192h-1376q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1376v192q0 14 9 23t23 9q12 0 24 -10l319 -319q9 -9 9 -23z" /> +<glyph unicode="" horiz-adv-x="1920" d="M1280 608q0 14 -9 23t-23 9h-224v352q0 13 -9.5 22.5t-22.5 9.5h-192q-13 0 -22.5 -9.5t-9.5 -22.5v-352h-224q-13 0 -22.5 -9.5t-9.5 -22.5q0 -14 9 -23l352 -352q9 -9 23 -9t23 9l351 351q10 12 10 24zM1920 384q0 -159 -112.5 -271.5t-271.5 -112.5h-1088 q-185 0 -316.5 131.5t-131.5 316.5q0 130 70 240t188 165q-2 30 -2 43q0 212 150 362t362 150q156 0 285.5 -87t188.5 -231q71 62 166 62q106 0 181 -75t75 -181q0 -76 -41 -138q130 -31 213.5 -135.5t83.5 -238.5z" /> +<glyph unicode="" horiz-adv-x="1920" d="M1280 672q0 14 -9 23l-352 352q-9 9 -23 9t-23 -9l-351 -351q-10 -12 -10 -24q0 -14 9 -23t23 -9h224v-352q0 -13 9.5 -22.5t22.5 -9.5h192q13 0 22.5 9.5t9.5 22.5v352h224q13 0 22.5 9.5t9.5 22.5zM1920 384q0 -159 -112.5 -271.5t-271.5 -112.5h-1088 q-185 0 -316.5 131.5t-131.5 316.5q0 130 70 240t188 165q-2 30 -2 43q0 212 150 362t362 150q156 0 285.5 -87t188.5 -231q71 62 166 62q106 0 181 -75t75 -181q0 -76 -41 -138q130 -31 213.5 -135.5t83.5 -238.5z" /> +<glyph unicode="" horiz-adv-x="1408" d="M384 192q0 -26 -19 -45t-45 -19t-45 19t-19 45t19 45t45 19t45 -19t19 -45zM1408 131q0 -121 -73 -190t-194 -69h-874q-121 0 -194 69t-73 190q0 68 5.5 131t24 138t47.5 132.5t81 103t120 60.5q-22 -52 -22 -120v-203q-58 -20 -93 -70t-35 -111q0 -80 56 -136t136 -56 t136 56t56 136q0 61 -35.5 111t-92.5 70v203q0 62 25 93q132 -104 295 -104t295 104q25 -31 25 -93v-64q-106 0 -181 -75t-75 -181v-89q-32 -29 -32 -71q0 -40 28 -68t68 -28t68 28t28 68q0 42 -32 71v89q0 52 38 90t90 38t90 -38t38 -90v-89q-32 -29 -32 -71q0 -40 28 -68 t68 -28t68 28t28 68q0 42 -32 71v89q0 68 -34.5 127.5t-93.5 93.5q0 10 0.5 42.5t0 48t-2.5 41.5t-7 47t-13 40q68 -15 120 -60.5t81 -103t47.5 -132.5t24 -138t5.5 -131zM1088 1024q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5 t271.5 -112.5t112.5 -271.5z" /> +<glyph unicode="" horiz-adv-x="1408" d="M1280 832q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 832q0 -62 -35.5 -111t-92.5 -70v-395q0 -159 -131.5 -271.5t-316.5 -112.5t-316.5 112.5t-131.5 271.5v132q-164 20 -274 128t-110 252v512q0 26 19 45t45 19q6 0 16 -2q17 30 47 48 t65 18q53 0 90.5 -37.5t37.5 -90.5t-37.5 -90.5t-90.5 -37.5q-33 0 -64 18v-402q0 -106 94 -181t226 -75t226 75t94 181v402q-31 -18 -64 -18q-53 0 -90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5q35 0 65 -18t47 -48q10 2 16 2q26 0 45 -19t19 -45v-512q0 -144 -110 -252 t-274 -128v-132q0 -106 94 -181t226 -75t226 75t94 181v395q-57 21 -92.5 70t-35.5 111q0 80 56 136t136 56t136 -56t56 -136z" /> +<glyph unicode="" horiz-adv-x="1792" d="M640 1152h512v128h-512v-128zM288 1152v-1280h-64q-92 0 -158 66t-66 158v832q0 92 66 158t158 66h64zM1408 1152v-1280h-1024v1280h128v160q0 40 28 68t68 28h576q40 0 68 -28t28 -68v-160h128zM1792 928v-832q0 -92 -66 -158t-158 -66h-64v1280h64q92 0 158 -66 t66 -158z" /> +<glyph unicode="" horiz-adv-x="1792" d="M912 -160q0 16 -16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5q16 0 16 16zM1728 128q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-181 75t-75 181h-448q-52 0 -90 38t-38 90q50 42 91 88t85 119.5t74.5 158.5 t50 206t19.5 260q0 152 117 282.5t307 158.5q-8 19 -8 39q0 40 28 68t68 28t68 -28t28 -68q0 -20 -8 -39q190 -28 307 -158.5t117 -282.5q0 -139 19.5 -260t50 -206t74.5 -158.5t85 -119.5t91 -88z" /> +<glyph unicode="" horiz-adv-x="1920" d="M1664 896q0 80 -56 136t-136 56h-64v-384h64q80 0 136 56t56 136zM0 128h1792q0 -106 -75 -181t-181 -75h-1280q-106 0 -181 75t-75 181zM1856 896q0 -159 -112.5 -271.5t-271.5 -112.5h-64v-32q0 -92 -66 -158t-158 -66h-704q-92 0 -158 66t-66 158v736q0 26 19 45 t45 19h1152q159 0 271.5 -112.5t112.5 -271.5z" /> +<glyph unicode="" horiz-adv-x="1408" d="M640 1472v-640q0 -61 -35.5 -111t-92.5 -70v-779q0 -52 -38 -90t-90 -38h-128q-52 0 -90 38t-38 90v779q-57 20 -92.5 70t-35.5 111v640q0 26 19 45t45 19t45 -19t19 -45v-416q0 -26 19 -45t45 -19t45 19t19 45v416q0 26 19 45t45 19t45 -19t19 -45v-416q0 -26 19 -45 t45 -19t45 19t19 45v416q0 26 19 45t45 19t45 -19t19 -45zM1408 1472v-1600q0 -52 -38 -90t-90 -38h-128q-52 0 -90 38t-38 90v512h-224q-13 0 -22.5 9.5t-9.5 22.5v800q0 132 94 226t226 94h256q26 0 45 -19t19 -45z" /> +<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M384 736q0 14 9 23t23 9h704q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704q-14 0 -23 9t-9 23v64zM1120 512q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h704zM1120 256q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704 q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h704z" /> +<glyph unicode="" horiz-adv-x="1408" d="M384 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M1152 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM896 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 992v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M1152 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM896 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 992v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 1248v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M1152 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM896 992v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 1248v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1152 992v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M896 1248v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1152 1248v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M896 -128h384v1536h-1152v-1536h384v224q0 13 9.5 22.5t22.5 9.5h320q13 0 22.5 -9.5t9.5 -22.5v-224zM1408 1472v-1664q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v1664q0 26 19 45t45 19h1280q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1408" d="M384 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M1152 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM896 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1152 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M896 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1152 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M896 -128h384v1152h-256v-32q0 -40 -28 -68t-68 -28h-448q-40 0 -68 28t-28 68v32h-256v-1152h384v224q0 13 9.5 22.5t22.5 9.5h320q13 0 22.5 -9.5t9.5 -22.5v-224zM896 1056v320q0 13 -9.5 22.5t-22.5 9.5h-64q-13 0 -22.5 -9.5t-9.5 -22.5v-96h-128v96q0 13 -9.5 22.5 t-22.5 9.5h-64q-13 0 -22.5 -9.5t-9.5 -22.5v-320q0 -13 9.5 -22.5t22.5 -9.5h64q13 0 22.5 9.5t9.5 22.5v96h128v-96q0 -13 9.5 -22.5t22.5 -9.5h64q13 0 22.5 9.5t9.5 22.5zM1408 1088v-1280q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v1280q0 26 19 45t45 19h320 v288q0 40 28 68t68 28h448q40 0 68 -28t28 -68v-288h320q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1920" d="M640 128q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM256 640h384v256h-158q-14 -2 -22 -9l-195 -195q-7 -12 -9 -22v-30zM1536 128q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5 t90.5 37.5t37.5 90.5zM1664 800v192q0 14 -9 23t-23 9h-224v224q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-224h-224q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h224v-224q0 -14 9 -23t23 -9h192q14 0 23 9t9 23v224h224q14 0 23 9t9 23zM1920 1344v-1152 q0 -26 -19 -45t-45 -19h-192q0 -106 -75 -181t-181 -75t-181 75t-75 181h-384q0 -106 -75 -181t-181 -75t-181 75t-75 181h-128q-26 0 -45 19t-19 45t19 45t45 19v416q0 26 13 58t32 51l198 198q19 19 51 32t58 13h160v320q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1280 416v192q0 14 -9 23t-23 9h-224v224q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-224h-224q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h224v-224q0 -14 9 -23t23 -9h192q14 0 23 9t9 23v224h224q14 0 23 9t9 23zM640 1152h512v128h-512v-128zM256 1152v-1280h-32 q-92 0 -158 66t-66 158v832q0 92 66 158t158 66h32zM1440 1152v-1280h-1088v1280h160v160q0 40 28 68t68 28h576q40 0 68 -28t28 -68v-160h160zM1792 928v-832q0 -92 -66 -158t-158 -66h-32v1280h32q92 0 158 -66t66 -158z" /> +<glyph unicode="" horiz-adv-x="1920" d="M1920 576q-1 -32 -288 -96l-352 -32l-224 -64h-64l-293 -352h69q26 0 45 -4.5t19 -11.5t-19 -11.5t-45 -4.5h-96h-160h-64v32h64v416h-160l-192 -224h-96l-32 32v192h32v32h128v8l-192 24v128l192 24v8h-128v32h-32v192l32 32h96l192 -224h160v416h-64v32h64h160h96 q26 0 45 -4.5t19 -11.5t-19 -11.5t-45 -4.5h-69l293 -352h64l224 -64l352 -32q261 -58 287 -93z" /> +<glyph unicode="" horiz-adv-x="1664" d="M640 640v384h-256v-256q0 -53 37.5 -90.5t90.5 -37.5h128zM1664 192v-192h-1152v192l128 192h-128q-159 0 -271.5 112.5t-112.5 271.5v320l-64 64l32 128h480l32 128h960l32 -192l-64 -32v-800z" /> +<glyph unicode="" d="M1280 192v896q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-320h-512v320q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-896q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v320h512v-320q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" d="M1280 576v128q0 26 -19 45t-45 19h-320v320q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-320h-320q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h320v-320q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v320h320q26 0 45 19t19 45zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="1024" d="M627 160q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l50 -50q10 -10 10 -23t-10 -23l-393 -393l393 -393q10 -10 10 -23zM1011 160q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23 t10 23l466 466q10 10 23 10t23 -10l50 -50q10 -10 10 -23t-10 -23l-393 -393l393 -393q10 -10 10 -23z" /> +<glyph unicode="" horiz-adv-x="1024" d="M595 576q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23zM979 576q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23 l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23z" /> +<glyph unicode="" horiz-adv-x="1152" d="M1075 224q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-393 393l-393 -393q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l466 -466q10 -10 10 -23zM1075 608q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-393 393l-393 -393 q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l466 -466q10 -10 10 -23z" /> +<glyph unicode="" horiz-adv-x="1152" d="M1075 672q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l393 -393l393 393q10 10 23 10t23 -10l50 -50q10 -10 10 -23zM1075 1056q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23 t10 23l50 50q10 10 23 10t23 -10l393 -393l393 393q10 10 23 10t23 -10l50 -50q10 -10 10 -23z" /> +<glyph unicode="" horiz-adv-x="640" d="M627 992q0 -13 -10 -23l-393 -393l393 -393q10 -10 10 -23t-10 -23l-50 -50q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l50 -50q10 -10 10 -23z" /> +<glyph unicode="" horiz-adv-x="640" d="M595 576q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23z" /> +<glyph unicode="" horiz-adv-x="1152" d="M1075 352q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-393 393l-393 -393q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l466 -466q10 -10 10 -23z" /> +<glyph unicode="" horiz-adv-x="1152" d="M1075 800q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l393 -393l393 393q10 10 23 10t23 -10l50 -50q10 -10 10 -23z" /> +<glyph unicode="" horiz-adv-x="1920" d="M1792 544v832q0 13 -9.5 22.5t-22.5 9.5h-1600q-13 0 -22.5 -9.5t-9.5 -22.5v-832q0 -13 9.5 -22.5t22.5 -9.5h1600q13 0 22.5 9.5t9.5 22.5zM1920 1376v-1088q0 -66 -47 -113t-113 -47h-544q0 -37 16 -77.5t32 -71t16 -43.5q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19 t-19 45q0 14 16 44t32 70t16 78h-544q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1600q66 0 113 -47t47 -113z" /> +<glyph unicode="" horiz-adv-x="1920" d="M416 256q-66 0 -113 47t-47 113v704q0 66 47 113t113 47h1088q66 0 113 -47t47 -113v-704q0 -66 -47 -113t-113 -47h-1088zM384 1120v-704q0 -13 9.5 -22.5t22.5 -9.5h1088q13 0 22.5 9.5t9.5 22.5v704q0 13 -9.5 22.5t-22.5 9.5h-1088q-13 0 -22.5 -9.5t-9.5 -22.5z M1760 192h160v-96q0 -40 -47 -68t-113 -28h-1600q-66 0 -113 28t-47 68v96h160h1600zM1040 96q16 0 16 16t-16 16h-160q-16 0 -16 -16t16 -16h160z" /> +<glyph unicode="" horiz-adv-x="1152" d="M640 128q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1024 288v960q0 13 -9.5 22.5t-22.5 9.5h-832q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h832q13 0 22.5 9.5t9.5 22.5zM1152 1248v-1088q0 -66 -47 -113t-113 -47h-832 q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h832q66 0 113 -47t47 -113z" /> +<glyph unicode="" horiz-adv-x="768" d="M464 128q0 33 -23.5 56.5t-56.5 23.5t-56.5 -23.5t-23.5 -56.5t23.5 -56.5t56.5 -23.5t56.5 23.5t23.5 56.5zM672 288v704q0 13 -9.5 22.5t-22.5 9.5h-512q-13 0 -22.5 -9.5t-9.5 -22.5v-704q0 -13 9.5 -22.5t22.5 -9.5h512q13 0 22.5 9.5t9.5 22.5zM480 1136 q0 16 -16 16h-160q-16 0 -16 -16t16 -16h160q16 0 16 16zM768 1152v-1024q0 -52 -38 -90t-90 -38h-512q-52 0 -90 38t-38 90v1024q0 52 38 90t90 38h512q52 0 90 -38t38 -90z" /> +<glyph unicode="" d="M768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103 t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" horiz-adv-x="1664" d="M768 576v-384q0 -80 -56 -136t-136 -56h-384q-80 0 -136 56t-56 136v704q0 104 40.5 198.5t109.5 163.5t163.5 109.5t198.5 40.5h64q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-64q-106 0 -181 -75t-75 -181v-32q0 -40 28 -68t68 -28h224q80 0 136 -56t56 -136z M1664 576v-384q0 -80 -56 -136t-136 -56h-384q-80 0 -136 56t-56 136v704q0 104 40.5 198.5t109.5 163.5t163.5 109.5t198.5 40.5h64q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-64q-106 0 -181 -75t-75 -181v-32q0 -40 28 -68t68 -28h224q80 0 136 -56t56 -136z" /> +<glyph unicode="" horiz-adv-x="1664" d="M768 1216v-704q0 -104 -40.5 -198.5t-109.5 -163.5t-163.5 -109.5t-198.5 -40.5h-64q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h64q106 0 181 75t75 181v32q0 40 -28 68t-68 28h-224q-80 0 -136 56t-56 136v384q0 80 56 136t136 56h384q80 0 136 -56t56 -136zM1664 1216 v-704q0 -104 -40.5 -198.5t-109.5 -163.5t-163.5 -109.5t-198.5 -40.5h-64q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h64q106 0 181 75t75 181v32q0 40 -28 68t-68 28h-224q-80 0 -136 56t-56 136v384q0 80 56 136t136 56h384q80 0 136 -56t56 -136z" /> +<glyph unicode="" horiz-adv-x="1792" d="M526 142q0 -53 -37.5 -90.5t-90.5 -37.5q-52 0 -90 38t-38 90q0 53 37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1024 -64q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM320 640q0 -53 -37.5 -90.5t-90.5 -37.5 t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1522 142q0 -52 -38 -90t-90 -38q-53 0 -90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM558 1138q0 -66 -47 -113t-113 -47t-113 47t-47 113t47 113t113 47t113 -47t47 -113z M1728 640q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1088 1344q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1618 1138q0 -93 -66 -158.5t-158 -65.5q-93 0 -158.5 65.5t-65.5 158.5 q0 92 65.5 158t158.5 66q92 0 158 -66t66 -158z" /> +<glyph unicode="" d="M1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1792 416q0 -166 -127 -451q-3 -7 -10.5 -24t-13.5 -30t-13 -22q-12 -17 -28 -17q-15 0 -23.5 10t-8.5 25q0 9 2.5 26.5t2.5 23.5q5 68 5 123q0 101 -17.5 181t-48.5 138.5t-80 101t-105.5 69.5t-133 42.5t-154 21.5t-175.5 6h-224v-256q0 -26 -19 -45t-45 -19t-45 19 l-512 512q-19 19 -19 45t19 45l512 512q19 19 45 19t45 -19t19 -45v-256h224q713 0 875 -403q53 -134 53 -333z" /> +<glyph unicode="" horiz-adv-x="1664" d="M640 320q0 -40 -12.5 -82t-43 -76t-72.5 -34t-72.5 34t-43 76t-12.5 82t12.5 82t43 76t72.5 34t72.5 -34t43 -76t12.5 -82zM1280 320q0 -40 -12.5 -82t-43 -76t-72.5 -34t-72.5 34t-43 76t-12.5 82t12.5 82t43 76t72.5 34t72.5 -34t43 -76t12.5 -82zM1440 320 q0 120 -69 204t-187 84q-41 0 -195 -21q-71 -11 -157 -11t-157 11q-152 21 -195 21q-118 0 -187 -84t-69 -204q0 -88 32 -153.5t81 -103t122 -60t140 -29.5t149 -7h168q82 0 149 7t140 29.5t122 60t81 103t32 153.5zM1664 496q0 -207 -61 -331q-38 -77 -105.5 -133t-141 -86 t-170 -47.5t-171.5 -22t-167 -4.5q-78 0 -142 3t-147.5 12.5t-152.5 30t-137 51.5t-121 81t-86 115q-62 123 -62 331q0 237 136 396q-27 82 -27 170q0 116 51 218q108 0 190 -39.5t189 -123.5q147 35 309 35q148 0 280 -32q105 82 187 121t189 39q51 -102 51 -218 q0 -87 -27 -168q136 -160 136 -398z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1536 224v704q0 40 -28 68t-68 28h-704q-40 0 -68 28t-28 68v64q0 40 -28 68t-68 28h-320q-40 0 -68 -28t-28 -68v-960q0 -40 28 -68t68 -28h1216q40 0 68 28t28 68zM1664 928v-704q0 -92 -66 -158t-158 -66h-1216q-92 0 -158 66t-66 158v960q0 92 66 158t158 66h320 q92 0 158 -66t66 -158v-32h672q92 0 158 -66t66 -158z" /> +<glyph unicode="" horiz-adv-x="1920" d="M1781 605q0 35 -53 35h-1088q-40 0 -85.5 -21.5t-71.5 -52.5l-294 -363q-18 -24 -18 -40q0 -35 53 -35h1088q40 0 86 22t71 53l294 363q18 22 18 39zM640 768h768v160q0 40 -28 68t-68 28h-576q-40 0 -68 28t-28 68v64q0 40 -28 68t-68 28h-320q-40 0 -68 -28t-28 -68 v-853l256 315q44 53 116 87.5t140 34.5zM1909 605q0 -62 -46 -120l-295 -363q-43 -53 -116 -87.5t-140 -34.5h-1088q-92 0 -158 66t-66 158v960q0 92 66 158t158 66h320q92 0 158 -66t66 -158v-32h544q92 0 158 -66t66 -158v-160h192q54 0 99 -24.5t67 -70.5q15 -32 15 -68z " /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" d="M1134 461q-37 -121 -138 -195t-228 -74t-228 74t-138 195q-8 25 4 48.5t38 31.5q25 8 48.5 -4t31.5 -38q25 -80 92.5 -129.5t151.5 -49.5t151.5 49.5t92.5 129.5q8 26 32 38t49 4t37 -31.5t4 -48.5zM640 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5 t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1152 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1408 640q0 130 -51 248.5t-136.5 204t-204 136.5t-248.5 51t-248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5 t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1134 307q8 -25 -4 -48.5t-37 -31.5t-49 4t-32 38q-25 80 -92.5 129.5t-151.5 49.5t-151.5 -49.5t-92.5 -129.5q-8 -26 -31.5 -38t-48.5 -4q-26 8 -38 31.5t-4 48.5q37 121 138 195t228 74t228 -74t138 -195zM640 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5 t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1152 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1408 640q0 130 -51 248.5t-136.5 204t-204 136.5t-248.5 51t-248.5 -51t-204 -136.5t-136.5 -204 t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1152 448q0 -26 -19 -45t-45 -19h-640q-26 0 -45 19t-19 45t19 45t45 19h640q26 0 45 -19t19 -45zM640 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1152 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5 t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1408 640q0 130 -51 248.5t-136.5 204t-204 136.5t-248.5 51t-248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" horiz-adv-x="1920" d="M832 448v128q0 14 -9 23t-23 9h-192v192q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-192h-192q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h192v-192q0 -14 9 -23t23 -9h128q14 0 23 9t9 23v192h192q14 0 23 9t9 23zM1408 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5 t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1664 640q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1920 512q0 -212 -150 -362t-362 -150q-192 0 -338 128h-220q-146 -128 -338 -128q-212 0 -362 150 t-150 362t150 362t362 150h896q212 0 362 -150t150 -362z" /> +<glyph unicode="" horiz-adv-x="1920" d="M384 368v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM512 624v-96q0 -16 -16 -16h-224q-16 0 -16 16v96q0 16 16 16h224q16 0 16 -16zM384 880v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1408 368v-96q0 -16 -16 -16 h-864q-16 0 -16 16v96q0 16 16 16h864q16 0 16 -16zM768 624v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM640 880v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1024 624v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16 h96q16 0 16 -16zM896 880v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1280 624v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1664 368v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1152 880v-96 q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1408 880v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1664 880v-352q0 -16 -16 -16h-224q-16 0 -16 16v96q0 16 16 16h112v240q0 16 16 16h96q16 0 16 -16zM1792 128v896h-1664v-896 h1664zM1920 1024v-896q0 -53 -37.5 -90.5t-90.5 -37.5h-1664q-53 0 -90.5 37.5t-37.5 90.5v896q0 53 37.5 90.5t90.5 37.5h1664q53 0 90.5 -37.5t37.5 -90.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1664 491v616q-169 -91 -306 -91q-82 0 -145 32q-100 49 -184 76.5t-178 27.5q-173 0 -403 -127v-599q245 113 433 113q55 0 103.5 -7.5t98 -26t77 -31t82.5 -39.5l28 -14q44 -22 101 -22q120 0 293 92zM320 1280q0 -35 -17.5 -64t-46.5 -46v-1266q0 -14 -9 -23t-23 -9 h-64q-14 0 -23 9t-9 23v1266q-29 17 -46.5 46t-17.5 64q0 53 37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1792 1216v-763q0 -39 -35 -57q-10 -5 -17 -9q-218 -116 -369 -116q-88 0 -158 35l-28 14q-64 33 -99 48t-91 29t-114 14q-102 0 -235.5 -44t-228.5 -102 q-15 -9 -33 -9q-16 0 -32 8q-32 19 -32 56v742q0 35 31 55q35 21 78.5 42.5t114 52t152.5 49.5t155 19q112 0 209 -31t209 -86q38 -19 89 -19q122 0 310 112q22 12 31 17q31 16 62 -2q31 -20 31 -55z" /> +<glyph unicode="" horiz-adv-x="1792" d="M832 536v192q-181 -16 -384 -117v-185q205 96 384 110zM832 954v197q-172 -8 -384 -126v-189q215 111 384 118zM1664 491v184q-235 -116 -384 -71v224q-20 6 -39 15q-5 3 -33 17t-34.5 17t-31.5 15t-34.5 15.5t-32.5 13t-36 12.5t-35 8.5t-39.5 7.5t-39.5 4t-44 2 q-23 0 -49 -3v-222h19q102 0 192.5 -29t197.5 -82q19 -9 39 -15v-188q42 -17 91 -17q120 0 293 92zM1664 918v189q-169 -91 -306 -91q-45 0 -78 8v-196q148 -42 384 90zM320 1280q0 -35 -17.5 -64t-46.5 -46v-1266q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v1266 q-29 17 -46.5 46t-17.5 64q0 53 37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1792 1216v-763q0 -39 -35 -57q-10 -5 -17 -9q-218 -116 -369 -116q-88 0 -158 35l-28 14q-64 33 -99 48t-91 29t-114 14q-102 0 -235.5 -44t-228.5 -102q-15 -9 -33 -9q-16 0 -32 8 q-32 19 -32 56v742q0 35 31 55q35 21 78.5 42.5t114 52t152.5 49.5t155 19q112 0 209 -31t209 -86q38 -19 89 -19q122 0 310 112q22 12 31 17q31 16 62 -2q31 -20 31 -55z" /> +<glyph unicode="" horiz-adv-x="1664" d="M585 553l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23t-10 -23zM1664 96v-64q0 -14 -9 -23t-23 -9h-960q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h960q14 0 23 -9 t9 -23z" /> +<glyph unicode="" horiz-adv-x="1920" d="M617 137l-50 -50q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l50 -50q10 -10 10 -23t-10 -23l-393 -393l393 -393q10 -10 10 -23t-10 -23zM1208 1204l-373 -1291q-4 -13 -15.5 -19.5t-23.5 -2.5l-62 17q-13 4 -19.5 15.5t-2.5 24.5 l373 1291q4 13 15.5 19.5t23.5 2.5l62 -17q13 -4 19.5 -15.5t2.5 -24.5zM1865 553l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23t-10 -23z" /> +<glyph unicode="" horiz-adv-x="1792" d="M640 454v-70q0 -42 -39 -59q-13 -5 -25 -5q-27 0 -45 19l-512 512q-19 19 -19 45t19 45l512 512q29 31 70 14q39 -17 39 -59v-69l-397 -398q-19 -19 -19 -45t19 -45zM1792 416q0 -58 -17 -133.5t-38.5 -138t-48 -125t-40.5 -90.5l-20 -40q-8 -17 -28 -17q-6 0 -9 1 q-25 8 -23 34q43 400 -106 565q-64 71 -170.5 110.5t-267.5 52.5v-251q0 -42 -39 -59q-13 -5 -25 -5q-27 0 -45 19l-512 512q-19 19 -19 45t19 45l512 512q29 31 70 14q39 -17 39 -59v-262q411 -28 599 -221q169 -173 169 -509z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1186 579l257 250l-356 52l-66 10l-30 60l-159 322v-963l59 -31l318 -168l-60 355l-12 66zM1638 841l-363 -354l86 -500q5 -33 -6 -51.5t-34 -18.5q-17 0 -40 12l-449 236l-449 -236q-23 -12 -40 -12q-23 0 -34 18.5t-6 51.5l86 500l-364 354q-32 32 -23 59.5t54 34.5 l502 73l225 455q20 41 49 41q28 0 49 -41l225 -455l502 -73q45 -7 54 -34.5t-24 -59.5z" /> +<glyph unicode="" horiz-adv-x="1408" d="M1401 1187l-640 -1280q-17 -35 -57 -35q-5 0 -15 2q-22 5 -35.5 22.5t-13.5 39.5v576h-576q-22 0 -39.5 13.5t-22.5 35.5t4 42t29 30l1280 640q13 7 29 7q27 0 45 -19q15 -14 18.5 -34.5t-6.5 -39.5z" /> +<glyph unicode="" horiz-adv-x="1664" d="M557 256h595v595zM512 301l595 595h-595v-595zM1664 224v-192q0 -14 -9 -23t-23 -9h-224v-224q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v224h-864q-14 0 -23 9t-9 23v864h-224q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h224v224q0 14 9 23t23 9h192q14 0 23 -9t9 -23 v-224h851l246 247q10 9 23 9t23 -9q9 -10 9 -23t-9 -23l-247 -246v-851h224q14 0 23 -9t9 -23z" /> +<glyph unicode="" horiz-adv-x="1024" d="M288 64q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM288 1216q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM928 1088q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM1024 1088q0 -52 -26 -96.5t-70 -69.5 q-2 -287 -226 -414q-68 -38 -203 -81q-128 -40 -169.5 -71t-41.5 -100v-26q44 -25 70 -69.5t26 -96.5q0 -80 -56 -136t-136 -56t-136 56t-56 136q0 52 26 96.5t70 69.5v820q-44 25 -70 69.5t-26 96.5q0 80 56 136t136 56t136 -56t56 -136q0 -52 -26 -96.5t-70 -69.5v-497 q54 26 154 57q55 17 87.5 29.5t70.5 31t59 39.5t40.5 51t28 69.5t8.5 91.5q-44 25 -70 69.5t-26 96.5q0 80 56 136t136 56t136 -56t56 -136z" /> +<glyph unicode="" horiz-adv-x="1664" d="M439 265l-256 -256q-10 -9 -23 -9q-12 0 -23 9q-9 10 -9 23t9 23l256 256q10 9 23 9t23 -9q9 -10 9 -23t-9 -23zM608 224v-320q0 -14 -9 -23t-23 -9t-23 9t-9 23v320q0 14 9 23t23 9t23 -9t9 -23zM384 448q0 -14 -9 -23t-23 -9h-320q-14 0 -23 9t-9 23t9 23t23 9h320 q14 0 23 -9t9 -23zM1648 320q0 -120 -85 -203l-147 -146q-83 -83 -203 -83q-121 0 -204 85l-334 335q-21 21 -42 56l239 18l273 -274q27 -27 68 -27.5t68 26.5l147 146q28 28 28 67q0 40 -28 68l-274 275l18 239q35 -21 56 -42l336 -336q84 -86 84 -204zM1031 1044l-239 -18 l-273 274q-28 28 -68 28q-39 0 -68 -27l-147 -146q-28 -28 -28 -67q0 -40 28 -68l274 -274l-18 -240q-35 21 -56 42l-336 336q-84 86 -84 204q0 120 85 203l147 146q83 83 203 83q121 0 204 -85l334 -335q21 -21 42 -56zM1664 960q0 -14 -9 -23t-23 -9h-320q-14 0 -23 9 t-9 23t9 23t23 9h320q14 0 23 -9t9 -23zM1120 1504v-320q0 -14 -9 -23t-23 -9t-23 9t-9 23v320q0 14 9 23t23 9t23 -9t9 -23zM1527 1353l-256 -256q-11 -9 -23 -9t-23 9q-9 10 -9 23t9 23l256 256q10 9 23 9t23 -9q9 -10 9 -23t-9 -23z" /> +<glyph unicode="" horiz-adv-x="1024" d="M704 280v-240q0 -16 -12 -28t-28 -12h-240q-16 0 -28 12t-12 28v240q0 16 12 28t28 12h240q16 0 28 -12t12 -28zM1020 880q0 -54 -15.5 -101t-35 -76.5t-55 -59.5t-57.5 -43.5t-61 -35.5q-41 -23 -68.5 -65t-27.5 -67q0 -17 -12 -32.5t-28 -15.5h-240q-15 0 -25.5 18.5 t-10.5 37.5v45q0 83 65 156.5t143 108.5q59 27 84 56t25 76q0 42 -46.5 74t-107.5 32q-65 0 -108 -29q-35 -25 -107 -115q-13 -16 -31 -16q-12 0 -25 8l-164 125q-13 10 -15.5 25t5.5 28q160 266 464 266q80 0 161 -31t146 -83t106 -127.5t41 -158.5z" /> +<glyph unicode="" horiz-adv-x="640" d="M640 192v-128q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h64v384h-64q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h384q26 0 45 -19t19 -45v-576h64q26 0 45 -19t19 -45zM512 1344v-192q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v192 q0 26 19 45t45 19h256q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="640" d="M512 288v-224q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v224q0 26 19 45t45 19h256q26 0 45 -19t19 -45zM542 1344l-28 -768q-1 -26 -20.5 -45t-45.5 -19h-256q-26 0 -45.5 19t-20.5 45l-28 768q-1 26 17.5 45t44.5 19h320q26 0 44.5 -19t17.5 -45z" /> +<glyph unicode="" d="M897 167v-167h-248l-159 252l-24 42q-8 9 -11 21h-3l-9 -21q-10 -20 -25 -44l-155 -250h-258v167h128l197 291l-185 272h-137v168h276l139 -228q2 -4 23 -42q8 -9 11 -21h3q3 9 11 21l25 42l140 228h257v-168h-125l-184 -267l204 -296h109zM1534 846v-206h-514l-3 27 q-4 28 -4 46q0 64 26 117t65 86.5t84 65t84 54.5t65 54t26 64q0 38 -29.5 62.5t-70.5 24.5q-51 0 -97 -39q-14 -11 -36 -38l-105 92q26 37 63 66q83 65 188 65q110 0 178 -59.5t68 -158.5q0 -56 -24.5 -103t-62 -76.5t-81.5 -58.5t-82 -50.5t-65.5 -51.5t-30.5 -63h232v80 h126z" /> +<glyph unicode="" d="M897 167v-167h-248l-159 252l-24 42q-8 9 -11 21h-3l-9 -21q-10 -20 -25 -44l-155 -250h-258v167h128l197 291l-185 272h-137v168h276l139 -228q2 -4 23 -42q8 -9 11 -21h3q3 9 11 21l25 42l140 228h257v-168h-125l-184 -267l204 -296h109zM1536 -50v-206h-514l-4 27 q-3 45 -3 46q0 64 26 117t65 86.5t84 65t84 54.5t65 54t26 64q0 38 -29.5 62.5t-70.5 24.5q-51 0 -97 -39q-14 -11 -36 -38l-105 92q26 37 63 66q80 65 188 65q110 0 178 -59.5t68 -158.5q0 -66 -34.5 -118.5t-84 -86t-99.5 -62.5t-87 -63t-41 -73h232v80h126z" /> +<glyph unicode="" horiz-adv-x="1920" d="M896 128l336 384h-768l-336 -384h768zM1909 1205q15 -34 9.5 -71.5t-30.5 -65.5l-896 -1024q-38 -44 -96 -44h-768q-38 0 -69.5 20.5t-47.5 54.5q-15 34 -9.5 71.5t30.5 65.5l896 1024q38 44 96 44h768q38 0 69.5 -20.5t47.5 -54.5z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1664 438q0 -81 -44.5 -135t-123.5 -54q-41 0 -77.5 17.5t-59 38t-56.5 38t-71 17.5q-110 0 -110 -124q0 -39 16 -115t15 -115v-5q-22 0 -33 -1q-34 -3 -97.5 -11.5t-115.5 -13.5t-98 -5q-61 0 -103 26.5t-42 83.5q0 37 17.5 71t38 56.5t38 59t17.5 77.5q0 79 -54 123.5 t-135 44.5q-84 0 -143 -45.5t-59 -127.5q0 -43 15 -83t33.5 -64.5t33.5 -53t15 -50.5q0 -45 -46 -89q-37 -35 -117 -35q-95 0 -245 24q-9 2 -27.5 4t-27.5 4l-13 2q-1 0 -3 1q-2 0 -2 1v1024q2 -1 17.5 -3.5t34 -5t21.5 -3.5q150 -24 245 -24q80 0 117 35q46 44 46 89 q0 22 -15 50.5t-33.5 53t-33.5 64.5t-15 83q0 82 59 127.5t144 45.5q80 0 134 -44.5t54 -123.5q0 -41 -17.5 -77.5t-38 -59t-38 -56.5t-17.5 -71q0 -57 42 -83.5t103 -26.5q64 0 180 15t163 17v-2q-1 -2 -3.5 -17.5t-5 -34t-3.5 -21.5q-24 -150 -24 -245q0 -80 35 -117 q44 -46 89 -46q22 0 50.5 15t53 33.5t64.5 33.5t83 15q82 0 127.5 -59t45.5 -143z" /> +<glyph unicode="" horiz-adv-x="1152" d="M1152 832v-128q0 -221 -147.5 -384.5t-364.5 -187.5v-132h256q26 0 45 -19t19 -45t-19 -45t-45 -19h-640q-26 0 -45 19t-19 45t19 45t45 19h256v132q-217 24 -364.5 187.5t-147.5 384.5v128q0 26 19 45t45 19t45 -19t19 -45v-128q0 -185 131.5 -316.5t316.5 -131.5 t316.5 131.5t131.5 316.5v128q0 26 19 45t45 19t45 -19t19 -45zM896 1216v-512q0 -132 -94 -226t-226 -94t-226 94t-94 226v512q0 132 94 226t226 94t226 -94t94 -226z" /> +<glyph unicode="" horiz-adv-x="1408" d="M271 591l-101 -101q-42 103 -42 214v128q0 26 19 45t45 19t45 -19t19 -45v-128q0 -53 15 -113zM1385 1193l-361 -361v-128q0 -132 -94 -226t-226 -94q-55 0 -109 19l-96 -96q97 -51 205 -51q185 0 316.5 131.5t131.5 316.5v128q0 26 19 45t45 19t45 -19t19 -45v-128 q0 -221 -147.5 -384.5t-364.5 -187.5v-132h256q26 0 45 -19t19 -45t-19 -45t-45 -19h-640q-26 0 -45 19t-19 45t19 45t45 19h256v132q-125 13 -235 81l-254 -254q-10 -10 -23 -10t-23 10l-82 82q-10 10 -10 23t10 23l1234 1234q10 10 23 10t23 -10l82 -82q10 -10 10 -23 t-10 -23zM1005 1325l-621 -621v512q0 132 94 226t226 94q102 0 184.5 -59t116.5 -152z" /> +<glyph unicode="" horiz-adv-x="1280" d="M1088 576v640h-448v-1137q119 63 213 137q235 184 235 360zM1280 1344v-768q0 -86 -33.5 -170.5t-83 -150t-118 -127.5t-126.5 -103t-121 -77.5t-89.5 -49.5t-42.5 -20q-12 -6 -26 -6t-26 6q-16 7 -42.5 20t-89.5 49.5t-121 77.5t-126.5 103t-118 127.5t-83 150 t-33.5 170.5v768q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1664" d="M128 -128h1408v1024h-1408v-1024zM512 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1280 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1664 1152v-1280 q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h384v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h128q52 0 90 -38t38 -90z" /> +<glyph unicode="" horiz-adv-x="1408" d="M512 1344q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 1376v-320q0 -16 -12 -25q-8 -7 -20 -7q-4 0 -7 1l-448 96q-11 2 -18 11t-7 20h-256v-102q111 -23 183.5 -111t72.5 -203v-800q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v800 q0 106 62.5 190.5t161.5 114.5v111h-32q-59 0 -115 -23.5t-91.5 -53t-66 -66.5t-40.5 -53.5t-14 -24.5q-17 -35 -57 -35q-16 0 -29 7q-23 12 -31.5 37t3.5 49q5 10 14.5 26t37.5 53.5t60.5 70t85 67t108.5 52.5q-25 42 -25 86q0 66 47 113t113 47t113 -47t47 -113 q0 -33 -14 -64h302q0 11 7 20t18 11l448 96q3 1 7 1q12 0 20 -7q12 -9 12 -25z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1440 1088q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM1664 1376q0 -249 -75.5 -430.5t-253.5 -360.5q-81 -80 -195 -176l-20 -379q-2 -16 -16 -26l-384 -224q-7 -4 -16 -4q-12 0 -23 9l-64 64q-13 14 -8 32l85 276l-281 281l-276 -85q-3 -1 -9 -1 q-14 0 -23 9l-64 64q-17 19 -5 39l224 384q10 14 26 16l379 20q96 114 176 195q188 187 358 258t431 71q14 0 24 -9.5t10 -22.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1745 763l-164 -763h-334l178 832q13 56 -15 88q-27 33 -83 33h-169l-204 -953h-334l204 953h-286l-204 -953h-334l204 953l-153 327h1276q101 0 189.5 -40.5t147.5 -113.5q60 -73 81 -168.5t0 -194.5z" /> +<glyph unicode="" d="M909 141l102 102q19 19 19 45t-19 45l-307 307l307 307q19 19 19 45t-19 45l-102 102q-19 19 -45 19t-45 -19l-454 -454q-19 -19 -19 -45t19 -45l454 -454q19 -19 45 -19t45 19zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M717 141l454 454q19 19 19 45t-19 45l-454 454q-19 19 -45 19t-45 -19l-102 -102q-19 -19 -19 -45t19 -45l307 -307l-307 -307q-19 -19 -19 -45t19 -45l102 -102q19 -19 45 -19t45 19zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1165 397l102 102q19 19 19 45t-19 45l-454 454q-19 19 -45 19t-45 -19l-454 -454q-19 -19 -19 -45t19 -45l102 -102q19 -19 45 -19t45 19l307 307l307 -307q19 -19 45 -19t45 19zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M813 237l454 454q19 19 19 45t-19 45l-102 102q-19 19 -45 19t-45 -19l-307 -307l-307 307q-19 19 -45 19t-45 -19l-102 -102q-19 -19 -19 -45t19 -45l454 -454q19 -19 45 -19t45 19zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" horiz-adv-x="1408" d="M1130 939l16 175h-884l47 -534h612l-22 -228l-197 -53l-196 53l-13 140h-175l22 -278l362 -100h4v1l359 99l50 544h-644l-15 181h674zM0 1408h1408l-128 -1438l-578 -162l-574 162z" /> +<glyph unicode="" horiz-adv-x="1792" d="M275 1408h1505l-266 -1333l-804 -267l-698 267l71 356h297l-29 -147l422 -161l486 161l68 339h-1208l58 297h1209l38 191h-1208z" /> +<glyph unicode="" horiz-adv-x="1792" d="M960 1280q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1792 352v-352q0 -22 -20 -30q-8 -2 -12 -2q-13 0 -23 9l-93 93q-119 -143 -318.5 -226.5t-429.5 -83.5t-429.5 83.5t-318.5 226.5l-93 -93q-9 -9 -23 -9q-4 0 -12 2q-20 8 -20 30v352 q0 14 9 23t23 9h352q22 0 30 -20q8 -19 -7 -35l-100 -100q67 -91 189.5 -153.5t271.5 -82.5v647h-192q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h192v163q-58 34 -93 92.5t-35 128.5q0 106 75 181t181 75t181 -75t75 -181q0 -70 -35 -128.5t-93 -92.5v-163h192q26 0 45 -19 t19 -45v-128q0 -26 -19 -45t-45 -19h-192v-647q149 20 271.5 82.5t189.5 153.5l-100 100q-15 16 -7 35q8 20 30 20h352q14 0 23 -9t9 -23z" /> +<glyph unicode="" horiz-adv-x="1152" d="M1056 768q40 0 68 -28t28 -68v-576q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v576q0 40 28 68t68 28h32v320q0 185 131.5 316.5t316.5 131.5t316.5 -131.5t131.5 -316.5q0 -26 -19 -45t-45 -19h-64q-26 0 -45 19t-19 45q0 106 -75 181t-181 75t-181 -75t-75 -181 v-320h736z" /> +<glyph unicode="" d="M1024 640q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181zM1152 640q0 159 -112.5 271.5t-271.5 112.5t-271.5 -112.5t-112.5 -271.5t112.5 -271.5t271.5 -112.5t271.5 112.5t112.5 271.5zM1280 640q0 -212 -150 -362t-362 -150t-362 150 t-150 362t150 362t362 150t362 -150t150 -362zM1408 640q0 130 -51 248.5t-136.5 204t-204 136.5t-248.5 51t-248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" horiz-adv-x="1408" d="M384 800v-192q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68zM896 800v-192q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68zM1408 800v-192q0 -40 -28 -68t-68 -28h-192 q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68z" /> +<glyph unicode="" horiz-adv-x="384" d="M384 288v-192q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68zM384 800v-192q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68zM384 1312v-192q0 -40 -28 -68t-68 -28h-192 q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68z" /> +<glyph unicode="" d="M512 256q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM863 162q-13 232 -177 396t-396 177q-14 1 -24 -9t-10 -23v-128q0 -13 8.5 -22t21.5 -10q154 -11 264 -121t121 -264q1 -13 10 -21.5t22 -8.5h128q13 0 23 10 t9 24zM1247 161q-5 154 -56 297.5t-139.5 260t-205 205t-260 139.5t-297.5 56q-14 1 -23 -9q-10 -10 -10 -23v-128q0 -13 9 -22t22 -10q204 -7 378 -111.5t278.5 -278.5t111.5 -378q1 -13 10 -22t22 -9h128q13 0 23 10q11 9 9 23zM1536 1120v-960q0 -119 -84.5 -203.5 t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM1152 585q32 18 32 55t-32 55l-544 320q-31 19 -64 1q-32 -19 -32 -56v-640q0 -37 32 -56 q16 -8 32 -8q17 0 32 9z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1024 1084l316 -316l-572 -572l-316 316zM813 105l618 618q19 19 19 45t-19 45l-362 362q-18 18 -45 18t-45 -18l-618 -618q-19 -19 -19 -45t19 -45l362 -362q18 -18 45 -18t45 18zM1702 742l-907 -908q-37 -37 -90.5 -37t-90.5 37l-126 126q56 56 56 136t-56 136 t-136 56t-136 -56l-125 126q-37 37 -37 90.5t37 90.5l907 906q37 37 90.5 37t90.5 -37l125 -125q-56 -56 -56 -136t56 -136t136 -56t136 56l126 -125q37 -37 37 -90.5t-37 -90.5z" /> +<glyph unicode="" d="M1280 576v128q0 26 -19 45t-45 19h-896q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h896q26 0 45 19t19 45zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5 t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="1408" d="M1152 736v-64q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h832q14 0 23 -9t9 -23zM1280 288v832q0 66 -47 113t-113 47h-832q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113zM1408 1120v-832q0 -119 -84.5 -203.5 t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="1024" d="M1018 933q-18 -37 -58 -37h-192v-864q0 -14 -9 -23t-23 -9h-704q-21 0 -29 18q-8 20 4 35l160 192q9 11 25 11h320v640h-192q-40 0 -58 37q-17 37 9 68l320 384q18 22 49 22t49 -22l320 -384q27 -32 9 -68z" /> +<glyph unicode="" horiz-adv-x="1024" d="M32 1280h704q13 0 22.5 -9.5t9.5 -23.5v-863h192q40 0 58 -37t-9 -69l-320 -384q-18 -22 -49 -22t-49 22l-320 384q-26 31 -9 69q18 37 58 37h192v640h-320q-14 0 -25 11l-160 192q-13 14 -4 34q9 19 29 19z" /> +<glyph unicode="" d="M685 237l614 614q19 19 19 45t-19 45l-102 102q-19 19 -45 19t-45 -19l-467 -467l-211 211q-19 19 -45 19t-45 -19l-102 -102q-19 -19 -19 -45t19 -45l358 -358q19 -19 45 -19t45 19zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5 t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" d="M404 428l152 -152l-52 -52h-56v96h-96v56zM818 818q14 -13 -3 -30l-291 -291q-17 -17 -30 -3q-14 13 3 30l291 291q17 17 30 3zM544 128l544 544l-288 288l-544 -544v-288h288zM1152 736l92 92q28 28 28 68t-28 68l-152 152q-28 28 -68 28t-68 -28l-92 -92zM1536 1120 v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" d="M1280 608v480q0 26 -19 45t-45 19h-480q-42 0 -59 -39q-17 -41 14 -70l144 -144l-534 -534q-19 -19 -19 -45t19 -45l102 -102q19 -19 45 -19t45 19l534 534l144 -144q18 -19 45 -19q12 0 25 5q39 17 39 59zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960 q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" d="M1005 435l352 352q19 19 19 45t-19 45l-352 352q-30 31 -69 14q-40 -17 -40 -59v-160q-119 0 -216 -19.5t-162.5 -51t-114 -79t-76.5 -95.5t-44.5 -109t-21.5 -111.5t-5 -110.5q0 -181 167 -404q10 -12 25 -12q7 0 13 3q22 9 19 33q-44 354 62 473q46 52 130 75.5 t224 23.5v-160q0 -42 40 -59q12 -5 24 -5q26 0 45 19zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" d="M640 448l256 128l-256 128v-256zM1024 1039v-542l-512 -256v542zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103 t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1145 861q18 -35 -5 -66l-320 -448q-19 -27 -52 -27t-52 27l-320 448q-23 31 -5 66q17 35 57 35h640q40 0 57 -35zM1280 160v960q0 13 -9.5 22.5t-22.5 9.5h-960q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h960q13 0 22.5 9.5t9.5 22.5zM1536 1120 v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" d="M1145 419q-17 -35 -57 -35h-640q-40 0 -57 35q-18 35 5 66l320 448q19 27 52 27t52 -27l320 -448q23 -31 5 -66zM1280 160v960q0 13 -9.5 22.5t-22.5 9.5h-960q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h960q13 0 22.5 9.5t9.5 22.5zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" d="M1088 640q0 -33 -27 -52l-448 -320q-31 -23 -66 -5q-35 17 -35 57v640q0 40 35 57q35 18 66 -5l448 -320q27 -19 27 -52zM1280 160v960q0 14 -9 23t-23 9h-960q-14 0 -23 -9t-9 -23v-960q0 -14 9 -23t23 -9h960q14 0 23 9t9 23zM1536 1120v-960q0 -119 -84.5 -203.5 t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="1024" d="M976 229l35 -159q3 -12 -3 -22.5t-17 -14.5l-5 -1q-4 -2 -10.5 -3.5t-16 -4.5t-21.5 -5.5t-25.5 -5t-30 -5t-33.5 -4.5t-36.5 -3t-38.5 -1q-234 0 -409 130.5t-238 351.5h-95q-13 0 -22.5 9.5t-9.5 22.5v113q0 13 9.5 22.5t22.5 9.5h66q-2 57 1 105h-67q-14 0 -23 9 t-9 23v114q0 14 9 23t23 9h98q67 210 243.5 338t400.5 128q102 0 194 -23q11 -3 20 -15q6 -11 3 -24l-43 -159q-3 -13 -14 -19.5t-24 -2.5l-4 1q-4 1 -11.5 2.5l-17.5 3.5t-22.5 3.5t-26 3t-29 2.5t-29.5 1q-126 0 -226 -64t-150 -176h468q16 0 25 -12q10 -12 7 -26 l-24 -114q-5 -26 -32 -26h-488q-3 -37 0 -105h459q15 0 25 -12q9 -12 6 -27l-24 -112q-2 -11 -11 -18.5t-20 -7.5h-387q48 -117 149.5 -185.5t228.5 -68.5q18 0 36 1.5t33.5 3.5t29.5 4.5t24.5 5t18.5 4.5l12 3l5 2q13 5 26 -2q12 -7 15 -21z" /> +<glyph unicode="" horiz-adv-x="1024" d="M1020 399v-367q0 -14 -9 -23t-23 -9h-956q-14 0 -23 9t-9 23v150q0 13 9.5 22.5t22.5 9.5h97v383h-95q-14 0 -23 9.5t-9 22.5v131q0 14 9 23t23 9h95v223q0 171 123.5 282t314.5 111q185 0 335 -125q9 -8 10 -20.5t-7 -22.5l-103 -127q-9 -11 -22 -12q-13 -2 -23 7 q-5 5 -26 19t-69 32t-93 18q-85 0 -137 -47t-52 -123v-215h305q13 0 22.5 -9t9.5 -23v-131q0 -13 -9.5 -22.5t-22.5 -9.5h-305v-379h414v181q0 13 9 22.5t23 9.5h162q14 0 23 -9.5t9 -22.5z" /> +<glyph unicode="" horiz-adv-x="1024" d="M978 351q0 -153 -99.5 -263.5t-258.5 -136.5v-175q0 -14 -9 -23t-23 -9h-135q-13 0 -22.5 9.5t-9.5 22.5v175q-66 9 -127.5 31t-101.5 44.5t-74 48t-46.5 37.5t-17.5 18q-17 21 -2 41l103 135q7 10 23 12q15 2 24 -9l2 -2q113 -99 243 -125q37 -8 74 -8q81 0 142.5 43 t61.5 122q0 28 -15 53t-33.5 42t-58.5 37.5t-66 32t-80 32.5q-39 16 -61.5 25t-61.5 26.5t-62.5 31t-56.5 35.5t-53.5 42.5t-43.5 49t-35.5 58t-21 66.5t-8.5 78q0 138 98 242t255 134v180q0 13 9.5 22.5t22.5 9.5h135q14 0 23 -9t9 -23v-176q57 -6 110.5 -23t87 -33.5 t63.5 -37.5t39 -29t15 -14q17 -18 5 -38l-81 -146q-8 -15 -23 -16q-14 -3 -27 7q-3 3 -14.5 12t-39 26.5t-58.5 32t-74.5 26t-85.5 11.5q-95 0 -155 -43t-60 -111q0 -26 8.5 -48t29.5 -41.5t39.5 -33t56 -31t60.5 -27t70 -27.5q53 -20 81 -31.5t76 -35t75.5 -42.5t62 -50 t53 -63.5t31.5 -76.5t13 -94z" /> +<glyph unicode="" horiz-adv-x="898" d="M898 1066v-102q0 -14 -9 -23t-23 -9h-168q-23 -144 -129 -234t-276 -110q167 -178 459 -536q14 -16 4 -34q-8 -18 -29 -18h-195q-16 0 -25 12q-306 367 -498 571q-9 9 -9 22v127q0 13 9.5 22.5t22.5 9.5h112q132 0 212.5 43t102.5 125h-427q-14 0 -23 9t-9 23v102 q0 14 9 23t23 9h413q-57 113 -268 113h-145q-13 0 -22.5 9.5t-9.5 22.5v133q0 14 9 23t23 9h832q14 0 23 -9t9 -23v-102q0 -14 -9 -23t-23 -9h-233q47 -61 64 -144h171q14 0 23 -9t9 -23z" /> +<glyph unicode="" horiz-adv-x="1027" d="M603 0h-172q-13 0 -22.5 9t-9.5 23v330h-288q-13 0 -22.5 9t-9.5 23v103q0 13 9.5 22.5t22.5 9.5h288v85h-288q-13 0 -22.5 9t-9.5 23v104q0 13 9.5 22.5t22.5 9.5h214l-321 578q-8 16 0 32q10 16 28 16h194q19 0 29 -18l215 -425q19 -38 56 -125q10 24 30.5 68t27.5 61 l191 420q8 19 29 19h191q17 0 27 -16q9 -14 1 -31l-313 -579h215q13 0 22.5 -9.5t9.5 -22.5v-104q0 -14 -9.5 -23t-22.5 -9h-290v-85h290q13 0 22.5 -9.5t9.5 -22.5v-103q0 -14 -9.5 -23t-22.5 -9h-290v-330q0 -13 -9.5 -22.5t-22.5 -9.5z" /> +<glyph unicode="" horiz-adv-x="1280" d="M1043 971q0 100 -65 162t-171 62h-320v-448h320q106 0 171 62t65 162zM1280 971q0 -193 -126.5 -315t-326.5 -122h-340v-118h505q14 0 23 -9t9 -23v-128q0 -14 -9 -23t-23 -9h-505v-192q0 -14 -9.5 -23t-22.5 -9h-167q-14 0 -23 9t-9 23v192h-224q-14 0 -23 9t-9 23v128 q0 14 9 23t23 9h224v118h-224q-14 0 -23 9t-9 23v149q0 13 9 22.5t23 9.5h224v629q0 14 9 23t23 9h539q200 0 326.5 -122t126.5 -315z" /> +<glyph unicode="" horiz-adv-x="1792" d="M514 341l81 299h-159l75 -300q1 -1 1 -3t1 -3q0 1 0.5 3.5t0.5 3.5zM630 768l35 128h-292l32 -128h225zM822 768h139l-35 128h-70zM1271 340l78 300h-162l81 -299q0 -1 0.5 -3.5t1.5 -3.5q0 1 0.5 3t0.5 3zM1382 768l33 128h-297l34 -128h230zM1792 736v-64q0 -14 -9 -23 t-23 -9h-213l-164 -616q-7 -24 -31 -24h-159q-24 0 -31 24l-166 616h-209l-167 -616q-7 -24 -31 -24h-159q-11 0 -19.5 7t-10.5 17l-160 616h-208q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h175l-33 128h-142q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h109l-89 344q-5 15 5 28 q10 12 26 12h137q26 0 31 -24l90 -360h359l97 360q7 24 31 24h126q24 0 31 -24l98 -360h365l93 360q5 24 31 24h137q16 0 26 -12q10 -13 5 -28l-91 -344h111q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-145l-34 -128h179q14 0 23 -9t9 -23z" /> +<glyph unicode="" horiz-adv-x="1280" d="M1167 896q18 -182 -131 -258q117 -28 175 -103t45 -214q-7 -71 -32.5 -125t-64.5 -89t-97 -58.5t-121.5 -34.5t-145.5 -15v-255h-154v251q-80 0 -122 1v-252h-154v255q-18 0 -54 0.5t-55 0.5h-200l31 183h111q50 0 58 51v402h16q-6 1 -16 1v287q-13 68 -89 68h-111v164 l212 -1q64 0 97 1v252h154v-247q82 2 122 2v245h154v-252q79 -7 140 -22.5t113 -45t82.5 -78t36.5 -114.5zM952 351q0 36 -15 64t-37 46t-57.5 30.5t-65.5 18.5t-74 9t-69 3t-64.5 -1t-47.5 -1v-338q8 0 37 -0.5t48 -0.5t53 1.5t58.5 4t57 8.5t55.5 14t47.5 21t39.5 30 t24.5 40t9.5 51zM881 827q0 33 -12.5 58.5t-30.5 42t-48 28t-55 16.5t-61.5 8t-58 2.5t-54 -1t-39.5 -0.5v-307q5 0 34.5 -0.5t46.5 0t50 2t55 5.5t51.5 11t48.5 18.5t37 27t27 38.5t9 51z" /> +<glyph unicode="" d="M1024 1024v472q22 -14 36 -28l408 -408q14 -14 28 -36h-472zM896 992q0 -40 28 -68t68 -28h544v-1056q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h800v-544z" /> +<glyph unicode="" d="M1468 1060q14 -14 28 -36h-472v472q22 -14 36 -28zM992 896h544v-1056q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h800v-544q0 -40 28 -68t68 -28zM1152 160v64q0 14 -9 23t-23 9h-704q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h704 q14 0 23 9t9 23zM1152 416v64q0 14 -9 23t-23 9h-704q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h704q14 0 23 9t9 23zM1152 672v64q0 14 -9 23t-23 9h-704q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h704q14 0 23 9t9 23z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1191 1128h177l-72 218l-12 47q-2 16 -2 20h-4l-3 -20q0 -1 -3.5 -18t-7.5 -29zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9t9 -23zM1572 -23 v-233h-584v90l369 529q12 18 21 27l11 9v3q-2 0 -6.5 -0.5t-7.5 -0.5q-12 -3 -30 -3h-232v-115h-120v229h567v-89l-369 -530q-6 -8 -21 -26l-11 -11v-2l14 2q9 2 30 2h248v119h121zM1661 874v-106h-288v106h75l-47 144h-243l-47 -144h75v-106h-287v106h70l230 662h162 l230 -662h70z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1191 104h177l-72 218l-12 47q-2 16 -2 20h-4l-3 -20q0 -1 -3.5 -18t-7.5 -29zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9t9 -23zM1661 -150 v-106h-288v106h75l-47 144h-243l-47 -144h75v-106h-287v106h70l230 662h162l230 -662h70zM1572 1001v-233h-584v90l369 529q12 18 21 27l11 9v3q-2 0 -6.5 -0.5t-7.5 -0.5q-12 -3 -30 -3h-232v-115h-120v229h567v-89l-369 -530q-6 -8 -21 -26l-11 -10v-3l14 3q9 1 30 1h248 v119h121z" /> +<glyph unicode="" horiz-adv-x="1792" d="M736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9t9 -23zM1792 -32v-192q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h832 q14 0 23 -9t9 -23zM1600 480v-192q0 -14 -9 -23t-23 -9h-640q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h640q14 0 23 -9t9 -23zM1408 992v-192q0 -14 -9 -23t-23 -9h-448q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h448q14 0 23 -9t9 -23zM1216 1504v-192q0 -14 -9 -23t-23 -9h-256 q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h256q14 0 23 -9t9 -23z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1216 -32v-192q0 -14 -9 -23t-23 -9h-256q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h256q14 0 23 -9t9 -23zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192 q14 0 23 -9t9 -23zM1408 480v-192q0 -14 -9 -23t-23 -9h-448q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h448q14 0 23 -9t9 -23zM1600 992v-192q0 -14 -9 -23t-23 -9h-640q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h640q14 0 23 -9t9 -23zM1792 1504v-192q0 -14 -9 -23t-23 -9h-832 q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h832q14 0 23 -9t9 -23z" /> +<glyph unicode="" d="M1346 223q0 63 -44 116t-103 53q-52 0 -83 -37t-31 -94t36.5 -95t104.5 -38q50 0 85 27t35 68zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9t9 -23 zM1486 165q0 -62 -13 -121.5t-41 -114t-68 -95.5t-98.5 -65.5t-127.5 -24.5q-62 0 -108 16q-24 8 -42 15l39 113q15 -7 31 -11q37 -13 75 -13q84 0 134.5 58.5t66.5 145.5h-2q-21 -23 -61.5 -37t-84.5 -14q-106 0 -173 71.5t-67 172.5q0 105 72 178t181 73q123 0 205 -94.5 t82 -252.5zM1456 882v-114h-469v114h167v432q0 7 0.5 19t0.5 17v16h-2l-7 -12q-8 -13 -26 -31l-62 -58l-82 86l192 185h123v-654h165z" /> +<glyph unicode="" d="M1346 1247q0 63 -44 116t-103 53q-52 0 -83 -37t-31 -94t36.5 -95t104.5 -38q50 0 85 27t35 68zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9 t9 -23zM1456 -142v-114h-469v114h167v432q0 7 0.5 19t0.5 17v16h-2l-7 -12q-8 -13 -26 -31l-62 -58l-82 86l192 185h123v-654h165zM1486 1189q0 -62 -13 -121.5t-41 -114t-68 -95.5t-98.5 -65.5t-127.5 -24.5q-62 0 -108 16q-24 8 -42 15l39 113q15 -7 31 -11q37 -13 75 -13 q84 0 134.5 58.5t66.5 145.5h-2q-21 -23 -61.5 -37t-84.5 -14q-106 0 -173 71.5t-67 172.5q0 105 72 178t181 73q123 0 205 -94.5t82 -252.5z" /> +<glyph unicode="" horiz-adv-x="1664" d="M256 192q0 26 -19 45t-45 19q-27 0 -45.5 -19t-18.5 -45q0 -27 18.5 -45.5t45.5 -18.5q26 0 45 18.5t19 45.5zM416 704v-640q0 -26 -19 -45t-45 -19h-288q-26 0 -45 19t-19 45v640q0 26 19 45t45 19h288q26 0 45 -19t19 -45zM1600 704q0 -86 -55 -149q15 -44 15 -76 q3 -76 -43 -137q17 -56 0 -117q-15 -57 -54 -94q9 -112 -49 -181q-64 -76 -197 -78h-36h-76h-17q-66 0 -144 15.5t-121.5 29t-120.5 39.5q-123 43 -158 44q-26 1 -45 19.5t-19 44.5v641q0 25 18 43.5t43 20.5q24 2 76 59t101 121q68 87 101 120q18 18 31 48t17.5 48.5 t13.5 60.5q7 39 12.5 61t19.5 52t34 50q19 19 45 19q46 0 82.5 -10.5t60 -26t40 -40.5t24 -45t12 -50t5 -45t0.5 -39q0 -38 -9.5 -76t-19 -60t-27.5 -56q-3 -6 -10 -18t-11 -22t-8 -24h277q78 0 135 -57t57 -135z" /> +<glyph unicode="" horiz-adv-x="1664" d="M256 960q0 -26 -19 -45t-45 -19q-27 0 -45.5 19t-18.5 45q0 27 18.5 45.5t45.5 18.5q26 0 45 -18.5t19 -45.5zM416 448v640q0 26 -19 45t-45 19h-288q-26 0 -45 -19t-19 -45v-640q0 -26 19 -45t45 -19h288q26 0 45 19t19 45zM1545 597q55 -61 55 -149q-1 -78 -57.5 -135 t-134.5 -57h-277q4 -14 8 -24t11 -22t10 -18q18 -37 27 -57t19 -58.5t10 -76.5q0 -24 -0.5 -39t-5 -45t-12 -50t-24 -45t-40 -40.5t-60 -26t-82.5 -10.5q-26 0 -45 19q-20 20 -34 50t-19.5 52t-12.5 61q-9 42 -13.5 60.5t-17.5 48.5t-31 48q-33 33 -101 120q-49 64 -101 121 t-76 59q-25 2 -43 20.5t-18 43.5v641q0 26 19 44.5t45 19.5q35 1 158 44q77 26 120.5 39.5t121.5 29t144 15.5h17h76h36q133 -2 197 -78q58 -69 49 -181q39 -37 54 -94q17 -61 0 -117q46 -61 43 -137q0 -32 -15 -76z" /> +<glyph unicode="" d="M919 233v157q0 50 -29 50q-17 0 -33 -16v-224q16 -16 33 -16q29 0 29 49zM1103 355h66v34q0 51 -33 51t-33 -51v-34zM532 621v-70h-80v-423h-74v423h-78v70h232zM733 495v-367h-67v40q-39 -45 -76 -45q-33 0 -42 28q-6 16 -6 54v290h66v-270q0 -24 1 -26q1 -15 15 -15 q20 0 42 31v280h67zM985 384v-146q0 -52 -7 -73q-12 -42 -53 -42q-35 0 -68 41v-36h-67v493h67v-161q32 40 68 40q41 0 53 -42q7 -21 7 -74zM1236 255v-9q0 -29 -2 -43q-3 -22 -15 -40q-27 -40 -80 -40q-52 0 -81 38q-21 27 -21 86v129q0 59 20 86q29 38 80 38t78 -38 q21 -28 21 -86v-76h-133v-65q0 -51 34 -51q24 0 30 26q0 1 0.5 7t0.5 16.5v21.5h68zM785 1079v-156q0 -51 -32 -51t-32 51v156q0 52 32 52t32 -52zM1318 366q0 177 -19 260q-10 44 -43 73.5t-76 34.5q-136 15 -412 15q-275 0 -411 -15q-44 -5 -76.5 -34.5t-42.5 -73.5 q-20 -87 -20 -260q0 -176 20 -260q10 -43 42.5 -73t75.5 -35q137 -15 412 -15t412 15q43 5 75.5 35t42.5 73q20 84 20 260zM563 1017l90 296h-75l-51 -195l-53 195h-78l24 -69t23 -69q35 -103 46 -158v-201h74v201zM852 936v130q0 58 -21 87q-29 38 -78 38q-51 0 -78 -38 q-21 -29 -21 -87v-130q0 -58 21 -87q27 -38 78 -38q49 0 78 38q21 27 21 87zM1033 816h67v370h-67v-283q-22 -31 -42 -31q-15 0 -16 16q-1 2 -1 26v272h-67v-293q0 -37 6 -55q11 -27 43 -27q36 0 77 45v-40zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960 q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" d="M971 292v-211q0 -67 -39 -67q-23 0 -45 22v301q22 22 45 22q39 0 39 -67zM1309 291v-46h-90v46q0 68 45 68t45 -68zM343 509h107v94h-312v-94h105v-569h100v569zM631 -60h89v494h-89v-378q-30 -42 -57 -42q-18 0 -21 21q-1 3 -1 35v364h-89v-391q0 -49 8 -73 q12 -37 58 -37q48 0 102 61v-54zM1060 88v197q0 73 -9 99q-17 56 -71 56q-50 0 -93 -54v217h-89v-663h89v48q45 -55 93 -55q54 0 71 55q9 27 9 100zM1398 98v13h-91q0 -51 -2 -61q-7 -36 -40 -36q-46 0 -46 69v87h179v103q0 79 -27 116q-39 51 -106 51q-68 0 -107 -51 q-28 -37 -28 -116v-173q0 -79 29 -116q39 -51 108 -51q72 0 108 53q18 27 21 54q2 9 2 58zM790 1011v210q0 69 -43 69t-43 -69v-210q0 -70 43 -70t43 70zM1509 260q0 -234 -26 -350q-14 -59 -58 -99t-102 -46q-184 -21 -555 -21t-555 21q-58 6 -102.5 46t-57.5 99 q-26 112 -26 350q0 234 26 350q14 59 58 99t103 47q183 20 554 20t555 -20q58 -7 102.5 -47t57.5 -99q26 -112 26 -350zM511 1536h102l-121 -399v-271h-100v271q-14 74 -61 212q-37 103 -65 187h106l71 -263zM881 1203v-175q0 -81 -28 -118q-37 -51 -106 -51q-67 0 -105 51 q-28 38 -28 118v175q0 80 28 117q38 51 105 51q69 0 106 -51q28 -37 28 -117zM1216 1365v-499h-91v55q-53 -62 -103 -62q-46 0 -59 37q-8 24 -8 75v394h91v-367q0 -33 1 -35q3 -22 21 -22q27 0 57 43v381h91z" /> +<glyph unicode="" horiz-adv-x="1408" d="M597 869q-10 -18 -257 -456q-27 -46 -65 -46h-239q-21 0 -31 17t0 36l253 448q1 0 0 1l-161 279q-12 22 -1 37q9 15 32 15h239q40 0 66 -45zM1403 1511q11 -16 0 -37l-528 -934v-1l336 -615q11 -20 1 -37q-10 -15 -32 -15h-239q-42 0 -66 45l-339 622q18 32 531 942 q25 45 64 45h241q22 0 31 -15z" /> +<glyph unicode="" d="M685 771q0 1 -126 222q-21 34 -52 34h-184q-18 0 -26 -11q-7 -12 1 -29l125 -216v-1l-196 -346q-9 -14 0 -28q8 -13 24 -13h185q31 0 50 36zM1309 1268q-7 12 -24 12h-187q-30 0 -49 -35l-411 -729q1 -2 262 -481q20 -35 52 -35h184q18 0 25 12q8 13 -1 28l-260 476v1 l409 723q8 16 0 28zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1280 640q0 37 -30 54l-512 320q-31 20 -65 2q-33 -18 -33 -56v-640q0 -38 33 -56q16 -8 31 -8q20 0 34 10l512 320q30 17 30 54zM1792 640q0 -96 -1 -150t-8.5 -136.5t-22.5 -147.5q-16 -73 -69 -123t-124 -58q-222 -25 -671 -25t-671 25q-71 8 -124.5 58t-69.5 123 q-14 65 -21.5 147.5t-8.5 136.5t-1 150t1 150t8.5 136.5t22.5 147.5q16 73 69 123t124 58q222 25 671 25t671 -25q71 -8 124.5 -58t69.5 -123q14 -65 21.5 -147.5t8.5 -136.5t1 -150z" /> +<glyph unicode="" horiz-adv-x="1792" d="M402 829l494 -305l-342 -285l-490 319zM1388 274v-108l-490 -293v-1l-1 1l-1 -1v1l-489 293v108l147 -96l342 284v2l1 -1l1 1v-2l343 -284zM554 1418l342 -285l-494 -304l-338 270zM1390 829l338 -271l-489 -319l-343 285zM1239 1418l489 -319l-338 -270l-494 304z" /> +<glyph unicode="" d="M1289 -96h-1118v480h-160v-640h1438v640h-160v-480zM347 428l33 157l783 -165l-33 -156zM450 802l67 146l725 -339l-67 -145zM651 1158l102 123l614 -513l-102 -123zM1048 1536l477 -641l-128 -96l-477 641zM330 65v159h800v-159h-800z" /> +<glyph unicode="" d="M1362 110v648h-135q20 -63 20 -131q0 -126 -64 -232.5t-174 -168.5t-240 -62q-197 0 -337 135.5t-140 327.5q0 68 20 131h-141v-648q0 -26 17.5 -43.5t43.5 -17.5h1069q25 0 43 17.5t18 43.5zM1078 643q0 124 -90.5 211.5t-218.5 87.5q-127 0 -217.5 -87.5t-90.5 -211.5 t90.5 -211.5t217.5 -87.5q128 0 218.5 87.5t90.5 211.5zM1362 1003v165q0 28 -20 48.5t-49 20.5h-174q-29 0 -49 -20.5t-20 -48.5v-165q0 -29 20 -49t49 -20h174q29 0 49 20t20 49zM1536 1211v-1142q0 -81 -58 -139t-139 -58h-1142q-81 0 -139 58t-58 139v1142q0 81 58 139 t139 58h1142q81 0 139 -58t58 -139z" /> +<glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960zM698 640q0 88 -62 150t-150 62t-150 -62t-62 -150t62 -150t150 -62t150 62t62 150zM1262 640q0 88 -62 150 t-150 62t-150 -62t-62 -150t62 -150t150 -62t150 62t62 150z" /> +<glyph unicode="" d="M768 914l201 -306h-402zM1133 384h94l-459 691l-459 -691h94l104 160h522zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" horiz-adv-x="1408" d="M815 677q8 -63 -50.5 -101t-111.5 -6q-39 17 -53.5 58t-0.5 82t52 58q36 18 72.5 12t64 -35.5t27.5 -67.5zM926 698q-14 107 -113 164t-197 13q-63 -28 -100.5 -88.5t-34.5 -129.5q4 -91 77.5 -155t165.5 -56q91 8 152 84t50 168zM1165 1240q-20 27 -56 44.5t-58 22 t-71 12.5q-291 47 -566 -2q-43 -7 -66 -12t-55 -22t-50 -43q30 -28 76 -45.5t73.5 -22t87.5 -11.5q228 -29 448 -1q63 8 89.5 12t72.5 21.5t75 46.5zM1222 205q-8 -26 -15.5 -76.5t-14 -84t-28.5 -70t-58 -56.5q-86 -48 -189.5 -71.5t-202 -22t-201.5 18.5q-46 8 -81.5 18 t-76.5 27t-73 43.5t-52 61.5q-25 96 -57 292l6 16l18 9q223 -148 506.5 -148t507.5 148q21 -6 24 -23t-5 -45t-8 -37zM1403 1166q-26 -167 -111 -655q-5 -30 -27 -56t-43.5 -40t-54.5 -31q-252 -126 -610 -88q-248 27 -394 139q-15 12 -25.5 26.5t-17 35t-9 34t-6 39.5 t-5.5 35q-9 50 -26.5 150t-28 161.5t-23.5 147.5t-22 158q3 26 17.5 48.5t31.5 37.5t45 30t46 22.5t48 18.5q125 46 313 64q379 37 676 -50q155 -46 215 -122q16 -20 16.5 -51t-5.5 -54z" /> +<glyph unicode="" d="M848 666q0 43 -41 66t-77 1q-43 -20 -42.5 -72.5t43.5 -70.5q39 -23 81 4t36 72zM928 682q8 -66 -36 -121t-110 -61t-119 40t-56 113q-2 49 25.5 93t72.5 64q70 31 141.5 -10t81.5 -118zM1100 1073q-20 -21 -53.5 -34t-53 -16t-63.5 -8q-155 -20 -324 0q-44 6 -63 9.5 t-52.5 16t-54.5 32.5q13 19 36 31t40 15.5t47 8.5q198 35 408 1q33 -5 51 -8.5t43 -16t39 -31.5zM1142 327q0 7 5.5 26.5t3 32t-17.5 16.5q-161 -106 -365 -106t-366 106l-12 -6l-5 -12q26 -154 41 -210q47 -81 204 -108q249 -46 428 53q34 19 49 51.5t22.5 85.5t12.5 71z M1272 1020q9 53 -8 75q-43 55 -155 88q-216 63 -487 36q-132 -12 -226 -46q-38 -15 -59.5 -25t-47 -34t-29.5 -54q8 -68 19 -138t29 -171t24 -137q1 -5 5 -31t7 -36t12 -27t22 -28q105 -80 284 -100q259 -28 440 63q24 13 39.5 23t31 29t19.5 40q48 267 80 473zM1536 1120 v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="1024" d="M944 207l80 -237q-23 -35 -111 -66t-177 -32q-104 -2 -190.5 26t-142.5 74t-95 106t-55.5 120t-16.5 118v544h-168v215q72 26 129 69.5t91 90t58 102t34 99t15 88.5q1 5 4.5 8.5t7.5 3.5h244v-424h333v-252h-334v-518q0 -30 6.5 -56t22.5 -52.5t49.5 -41.5t81.5 -14 q78 2 134 29z" /> +<glyph unicode="" d="M1136 75l-62 183q-44 -22 -103 -22q-36 -1 -62 10.5t-38.5 31.5t-17.5 40.5t-5 43.5v398h257v194h-256v326h-188q-8 0 -9 -10q-5 -44 -17.5 -87t-39 -95t-77 -95t-118.5 -68v-165h130v-418q0 -57 21.5 -115t65 -111t121 -85.5t176.5 -30.5q69 1 136.5 25t85.5 50z M1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="768" d="M765 237q8 -19 -5 -35l-350 -384q-10 -10 -23 -10q-14 0 -24 10l-355 384q-13 16 -5 35q9 19 29 19h224v1248q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1248h224q21 0 29 -19z" /> +<glyph unicode="" horiz-adv-x="768" d="M765 1043q-9 -19 -29 -19h-224v-1248q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v1248h-224q-21 0 -29 19t5 35l350 384q10 10 23 10q14 0 24 -10l355 -384q13 -16 5 -35z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1792 736v-192q0 -14 -9 -23t-23 -9h-1248v-224q0 -21 -19 -29t-35 5l-384 350q-10 10 -10 23q0 14 10 24l384 354q16 14 35 6q19 -9 19 -29v-224h1248q14 0 23 -9t9 -23z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1728 643q0 -14 -10 -24l-384 -354q-16 -14 -35 -6q-19 9 -19 29v224h-1248q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h1248v224q0 21 19 29t35 -5l384 -350q10 -10 10 -23z" /> +<glyph unicode="" horiz-adv-x="1408" d="M1393 321q-39 -125 -123 -250q-129 -196 -257 -196q-49 0 -140 32q-86 32 -151 32q-61 0 -142 -33q-81 -34 -132 -34q-152 0 -301 259q-147 261 -147 503q0 228 113 374q112 144 284 144q72 0 177 -30q104 -30 138 -30q45 0 143 34q102 34 173 34q119 0 213 -65 q52 -36 104 -100q-79 -67 -114 -118q-65 -94 -65 -207q0 -124 69 -223t158 -126zM1017 1494q0 -61 -29 -136q-30 -75 -93 -138q-54 -54 -108 -72q-37 -11 -104 -17q3 149 78 257q74 107 250 148q1 -3 2.5 -11t2.5 -11q0 -4 0.5 -10t0.5 -10z" /> +<glyph unicode="" horiz-adv-x="1664" d="M682 530v-651l-682 94v557h682zM682 1273v-659h-682v565zM1664 530v-786l-907 125v661h907zM1664 1408v-794h-907v669z" /> +<glyph unicode="" horiz-adv-x="1408" d="M493 1053q16 0 27.5 11.5t11.5 27.5t-11.5 27.5t-27.5 11.5t-27 -11.5t-11 -27.5t11 -27.5t27 -11.5zM915 1053q16 0 27 11.5t11 27.5t-11 27.5t-27 11.5t-27.5 -11.5t-11.5 -27.5t11.5 -27.5t27.5 -11.5zM103 869q42 0 72 -30t30 -72v-430q0 -43 -29.5 -73t-72.5 -30 t-73 30t-30 73v430q0 42 30 72t73 30zM1163 850v-666q0 -46 -32 -78t-77 -32h-75v-227q0 -43 -30 -73t-73 -30t-73 30t-30 73v227h-138v-227q0 -43 -30 -73t-73 -30q-42 0 -72 30t-30 73l-1 227h-74q-46 0 -78 32t-32 78v666h918zM931 1255q107 -55 171 -153.5t64 -215.5 h-925q0 117 64 215.5t172 153.5l-71 131q-7 13 5 20q13 6 20 -6l72 -132q95 42 201 42t201 -42l72 132q7 12 20 6q12 -7 5 -20zM1408 767v-430q0 -43 -30 -73t-73 -30q-42 0 -72 30t-30 73v430q0 43 30 72.5t72 29.5q43 0 73 -29.5t30 -72.5z" /> +<glyph unicode="" d="M663 1125q-11 -1 -15.5 -10.5t-8.5 -9.5q-5 -1 -5 5q0 12 19 15h10zM750 1111q-4 -1 -11.5 6.5t-17.5 4.5q24 11 32 -2q3 -6 -3 -9zM399 684q-4 1 -6 -3t-4.5 -12.5t-5.5 -13.5t-10 -13q-7 -10 -1 -12q4 -1 12.5 7t12.5 18q1 3 2 7t2 6t1.5 4.5t0.5 4v3t-1 2.5t-3 2z M1254 325q0 18 -55 42q4 15 7.5 27.5t5 26t3 21.5t0.5 22.5t-1 19.5t-3.5 22t-4 20.5t-5 25t-5.5 26.5q-10 48 -47 103t-72 75q24 -20 57 -83q87 -162 54 -278q-11 -40 -50 -42q-31 -4 -38.5 18.5t-8 83.5t-11.5 107q-9 39 -19.5 69t-19.5 45.5t-15.5 24.5t-13 15t-7.5 7 q-14 62 -31 103t-29.5 56t-23.5 33t-15 40q-4 21 6 53.5t4.5 49.5t-44.5 25q-15 3 -44.5 18t-35.5 16q-8 1 -11 26t8 51t36 27q37 3 51 -30t4 -58q-11 -19 -2 -26.5t30 -0.5q13 4 13 36v37q-5 30 -13.5 50t-21 30.5t-23.5 15t-27 7.5q-107 -8 -89 -134q0 -15 -1 -15 q-9 9 -29.5 10.5t-33 -0.5t-15.5 5q1 57 -16 90t-45 34q-27 1 -41.5 -27.5t-16.5 -59.5q-1 -15 3.5 -37t13 -37.5t15.5 -13.5q10 3 16 14q4 9 -7 8q-7 0 -15.5 14.5t-9.5 33.5q-1 22 9 37t34 14q17 0 27 -21t9.5 -39t-1.5 -22q-22 -15 -31 -29q-8 -12 -27.5 -23.5 t-20.5 -12.5q-13 -14 -15.5 -27t7.5 -18q14 -8 25 -19.5t16 -19t18.5 -13t35.5 -6.5q47 -2 102 15q2 1 23 7t34.5 10.5t29.5 13t21 17.5q9 14 20 8q5 -3 6.5 -8.5t-3 -12t-16.5 -9.5q-20 -6 -56.5 -21.5t-45.5 -19.5q-44 -19 -70 -23q-25 -5 -79 2q-10 2 -9 -2t17 -19 q25 -23 67 -22q17 1 36 7t36 14t33.5 17.5t30 17t24.5 12t17.5 2.5t8.5 -11q0 -2 -1 -4.5t-4 -5t-6 -4.5t-8.5 -5t-9 -4.5t-10 -5t-9.5 -4.5q-28 -14 -67.5 -44t-66.5 -43t-49 -1q-21 11 -63 73q-22 31 -25 22q-1 -3 -1 -10q0 -25 -15 -56.5t-29.5 -55.5t-21 -58t11.5 -63 q-23 -6 -62.5 -90t-47.5 -141q-2 -18 -1.5 -69t-5.5 -59q-8 -24 -29 -3q-32 31 -36 94q-2 28 4 56q4 19 -1 18l-4 -5q-36 -65 10 -166q5 -12 25 -28t24 -20q20 -23 104 -90.5t93 -76.5q16 -15 17.5 -38t-14 -43t-45.5 -23q8 -15 29 -44.5t28 -54t7 -70.5q46 24 7 92 q-4 8 -10.5 16t-9.5 12t-2 6q3 5 13 9.5t20 -2.5q46 -52 166 -36q133 15 177 87q23 38 34 30q12 -6 10 -52q-1 -25 -23 -92q-9 -23 -6 -37.5t24 -15.5q3 19 14.5 77t13.5 90q2 21 -6.5 73.5t-7.5 97t23 70.5q15 18 51 18q1 37 34.5 53t72.5 10.5t60 -22.5zM626 1152 q3 17 -2.5 30t-11.5 15q-9 2 -9 -7q2 -5 5 -6q10 0 7 -15q-3 -20 8 -20q3 0 3 3zM1045 955q-2 8 -6.5 11.5t-13 5t-14.5 5.5q-5 3 -9.5 8t-7 8t-5.5 6.5t-4 4t-4 -1.5q-14 -16 7 -43.5t39 -31.5q9 -1 14.5 8t3.5 20zM867 1168q0 11 -5 19.5t-11 12.5t-9 3q-14 -1 -7 -7l4 -2 q14 -4 18 -31q0 -3 8 2zM921 1401q0 2 -2.5 5t-9 7t-9.5 6q-15 15 -24 15q-9 -1 -11.5 -7.5t-1 -13t-0.5 -12.5q-1 -4 -6 -10.5t-6 -9t3 -8.5q4 -3 8 0t11 9t15 9q1 1 9 1t15 2t9 7zM1486 60q20 -12 31 -24.5t12 -24t-2.5 -22.5t-15.5 -22t-23.5 -19.5t-30 -18.5 t-31.5 -16.5t-32 -15.5t-27 -13q-38 -19 -85.5 -56t-75.5 -64q-17 -16 -68 -19.5t-89 14.5q-18 9 -29.5 23.5t-16.5 25.5t-22 19.5t-47 9.5q-44 1 -130 1q-19 0 -57 -1.5t-58 -2.5q-44 -1 -79.5 -15t-53.5 -30t-43.5 -28.5t-53.5 -11.5q-29 1 -111 31t-146 43q-19 4 -51 9.5 t-50 9t-39.5 9.5t-33.5 14.5t-17 19.5q-10 23 7 66.5t18 54.5q1 16 -4 40t-10 42.5t-4.5 36.5t10.5 27q14 12 57 14t60 12q30 18 42 35t12 51q21 -73 -32 -106q-32 -20 -83 -15q-34 3 -43 -10q-13 -15 5 -57q2 -6 8 -18t8.5 -18t4.5 -17t1 -22q0 -15 -17 -49t-14 -48 q3 -17 37 -26q20 -6 84.5 -18.5t99.5 -20.5q24 -6 74 -22t82.5 -23t55.5 -4q43 6 64.5 28t23 48t-7.5 58.5t-19 52t-20 36.5q-121 190 -169 242q-68 74 -113 40q-11 -9 -15 15q-3 16 -2 38q1 29 10 52t24 47t22 42q8 21 26.5 72t29.5 78t30 61t39 54q110 143 124 195 q-12 112 -16 310q-2 90 24 151.5t106 104.5q39 21 104 21q53 1 106 -13.5t89 -41.5q57 -42 91.5 -121.5t29.5 -147.5q-5 -95 30 -214q34 -113 133 -218q55 -59 99.5 -163t59.5 -191q8 -49 5 -84.5t-12 -55.5t-20 -22q-10 -2 -23.5 -19t-27 -35.5t-40.5 -33.5t-61 -14 q-18 1 -31.5 5t-22.5 13.5t-13.5 15.5t-11.5 20.5t-9 19.5q-22 37 -41 30t-28 -49t7 -97q20 -70 1 -195q-10 -65 18 -100.5t73 -33t85 35.5q59 49 89.5 66.5t103.5 42.5q53 18 77 36.5t18.5 34.5t-25 28.5t-51.5 23.5q-33 11 -49.5 48t-15 72.5t15.5 47.5q1 -31 8 -56.5 t14.5 -40.5t20.5 -28.5t21 -19t21.5 -13t16.5 -9.5z" /> +<glyph unicode="" d="M1024 36q-42 241 -140 498h-2l-2 -1q-16 -6 -43 -16.5t-101 -49t-137 -82t-131 -114.5t-103 -148l-15 11q184 -150 418 -150q132 0 256 52zM839 643q-21 49 -53 111q-311 -93 -673 -93q-1 -7 -1 -21q0 -124 44 -236.5t124 -201.5q50 89 123.5 166.5t142.5 124.5t130.5 81 t99.5 48l37 13q4 1 13 3.5t13 4.5zM732 855q-120 213 -244 378q-138 -65 -234 -186t-128 -272q302 0 606 80zM1416 536q-210 60 -409 29q87 -239 128 -469q111 75 185 189.5t96 250.5zM611 1277q-1 0 -2 -1q1 1 2 1zM1201 1132q-185 164 -433 164q-76 0 -155 -19 q131 -170 246 -382q69 26 130 60.5t96.5 61.5t65.5 57t37.5 40.5zM1424 647q-3 232 -149 410l-1 -1q-9 -12 -19 -24.5t-43.5 -44.5t-71 -60.5t-100 -65t-131.5 -64.5q25 -53 44 -95q2 -6 6.5 -17.5t7.5 -16.5q36 5 74.5 7t73.5 2t69 -1.5t64 -4t56.5 -5.5t48 -6.5t36.5 -6 t25 -4.5zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1173 473q0 50 -19.5 91.5t-48.5 68.5t-73 49t-82.5 34t-87.5 23l-104 24q-30 7 -44 10.5t-35 11.5t-30 16t-16.5 21t-7.5 30q0 77 144 77q43 0 77 -12t54 -28.5t38 -33.5t40 -29t48 -12q47 0 75.5 32t28.5 77q0 55 -56 99.5t-142 67.5t-182 23q-68 0 -132 -15.5 t-119.5 -47t-89 -87t-33.5 -128.5q0 -61 19 -106.5t56 -75.5t80 -48.5t103 -32.5l146 -36q90 -22 112 -36q32 -20 32 -60q0 -39 -40 -64.5t-105 -25.5q-51 0 -91.5 16t-65 38.5t-45.5 45t-46 38.5t-54 16q-50 0 -75.5 -30t-25.5 -75q0 -92 122 -157.5t291 -65.5 q73 0 140 18.5t122.5 53.5t88.5 93.5t33 131.5zM1536 256q0 -159 -112.5 -271.5t-271.5 -112.5q-130 0 -234 80q-77 -16 -150 -16q-143 0 -273.5 55.5t-225 150t-150 225t-55.5 273.5q0 73 16 150q-80 104 -80 234q0 159 112.5 271.5t271.5 112.5q130 0 234 -80 q77 16 150 16q143 0 273.5 -55.5t225 -150t150 -225t55.5 -273.5q0 -73 -16 -150q80 -104 80 -234z" /> +<glyph unicode="" horiz-adv-x="1280" d="M1000 1102l37 194q5 23 -9 40t-35 17h-712q-23 0 -38.5 -17t-15.5 -37v-1101q0 -7 6 -1l291 352q23 26 38 33.5t48 7.5h239q22 0 37 14.5t18 29.5q24 130 37 191q4 21 -11.5 40t-36.5 19h-294q-29 0 -48 19t-19 48v42q0 29 19 47.5t48 18.5h346q18 0 35 13.5t20 29.5z M1227 1324q-15 -73 -53.5 -266.5t-69.5 -350t-35 -173.5q-6 -22 -9 -32.5t-14 -32.5t-24.5 -33t-38.5 -21t-58 -10h-271q-13 0 -22 -10q-8 -9 -426 -494q-22 -25 -58.5 -28.5t-48.5 5.5q-55 22 -55 98v1410q0 55 38 102.5t120 47.5h888q95 0 127 -53t10 -159zM1227 1324 l-158 -790q4 17 35 173.5t69.5 350t53.5 266.5z" /> +<glyph unicode="" d="M704 192v1024q0 14 -9 23t-23 9h-480q-14 0 -23 -9t-9 -23v-1024q0 -14 9 -23t23 -9h480q14 0 23 9t9 23zM1376 576v640q0 14 -9 23t-23 9h-480q-14 0 -23 -9t-9 -23v-640q0 -14 9 -23t23 -9h480q14 0 23 9t9 23zM1536 1344v-1408q0 -26 -19 -45t-45 -19h-1408 q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h1408q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1280" d="M1280 480q0 -40 -28 -68t-68 -28q-51 0 -80 43l-227 341h-45v-132l247 -411q9 -15 9 -33q0 -26 -19 -45t-45 -19h-192v-272q0 -46 -33 -79t-79 -33h-160q-46 0 -79 33t-33 79v272h-192q-26 0 -45 19t-19 45q0 18 9 33l247 411v132h-45l-227 -341q-29 -43 -80 -43 q-40 0 -68 28t-28 68q0 29 16 53l256 384q73 107 176 107h384q103 0 176 -107l256 -384q16 -24 16 -53zM864 1280q0 -93 -65.5 -158.5t-158.5 -65.5t-158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5t158.5 -65.5t65.5 -158.5z" /> +<glyph unicode="" horiz-adv-x="1024" d="M1024 832v-416q0 -40 -28 -68t-68 -28t-68 28t-28 68v352h-64v-912q0 -46 -33 -79t-79 -33t-79 33t-33 79v464h-64v-464q0 -46 -33 -79t-79 -33t-79 33t-33 79v912h-64v-352q0 -40 -28 -68t-68 -28t-68 28t-28 68v416q0 80 56 136t136 56h640q80 0 136 -56t56 -136z M736 1280q0 -93 -65.5 -158.5t-158.5 -65.5t-158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5t158.5 -65.5t65.5 -158.5z" /> +<glyph unicode="" d="M773 234l350 473q16 22 24.5 59t-6 85t-61.5 79q-40 26 -83 25.5t-73.5 -17.5t-54.5 -45q-36 -40 -96 -40q-59 0 -95 40q-24 28 -54.5 45t-73.5 17.5t-84 -25.5q-46 -31 -60.5 -79t-6 -85t24.5 -59zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103 t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1472 640q0 117 -45.5 223.5t-123 184t-184 123t-223.5 45.5t-223.5 -45.5t-184 -123t-123 -184t-45.5 -223.5t45.5 -223.5t123 -184t184 -123t223.5 -45.5t223.5 45.5t184 123t123 184t45.5 223.5zM1748 363q-4 -15 -20 -20l-292 -96v-306q0 -16 -13 -26q-15 -10 -29 -4 l-292 94l-180 -248q-10 -13 -26 -13t-26 13l-180 248l-292 -94q-14 -6 -29 4q-13 10 -13 26v306l-292 96q-16 5 -20 20q-5 17 4 29l180 248l-180 248q-9 13 -4 29q4 15 20 20l292 96v306q0 16 13 26q15 10 29 4l292 -94l180 248q9 12 26 12t26 -12l180 -248l292 94 q14 6 29 -4q13 -10 13 -26v-306l292 -96q16 -5 20 -20q5 -16 -4 -29l-180 -248l180 -248q9 -12 4 -29z" /> +<glyph unicode="" d="M1262 233q-54 -9 -110 -9q-182 0 -337 90t-245 245t-90 337q0 192 104 357q-201 -60 -328.5 -229t-127.5 -384q0 -130 51 -248.5t136.5 -204t204 -136.5t248.5 -51q144 0 273.5 61.5t220.5 171.5zM1465 318q-94 -203 -283.5 -324.5t-413.5 -121.5q-156 0 -298 61 t-245 164t-164 245t-61 298q0 153 57.5 292.5t156 241.5t235.5 164.5t290 68.5q44 2 61 -39q18 -41 -15 -72q-86 -78 -131.5 -181.5t-45.5 -218.5q0 -148 73 -273t198 -198t273 -73q118 0 228 51q41 18 72 -13q14 -14 17.5 -34t-4.5 -38z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1088 704q0 26 -19 45t-45 19h-256q-26 0 -45 -19t-19 -45t19 -45t45 -19h256q26 0 45 19t19 45zM1664 896v-960q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v960q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1728 1344v-256q0 -26 -19 -45t-45 -19h-1536 q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h1536q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1632 576q0 -26 -19 -45t-45 -19h-224q0 -171 -67 -290l208 -209q19 -19 19 -45t-19 -45q-18 -19 -45 -19t-45 19l-198 197q-5 -5 -15 -13t-42 -28.5t-65 -36.5t-82 -29t-97 -13v896h-128v-896q-51 0 -101.5 13.5t-87 33t-66 39t-43.5 32.5l-15 14l-183 -207 q-20 -21 -48 -21q-24 0 -43 16q-19 18 -20.5 44.5t15.5 46.5l202 227q-58 114 -58 274h-224q-26 0 -45 19t-19 45t19 45t45 19h224v294l-173 173q-19 19 -19 45t19 45t45 19t45 -19l173 -173h844l173 173q19 19 45 19t45 -19t19 -45t-19 -45l-173 -173v-294h224q26 0 45 -19 t19 -45zM1152 1152h-640q0 133 93.5 226.5t226.5 93.5t226.5 -93.5t93.5 -226.5z" /> +<glyph unicode="" horiz-adv-x="1920" d="M1917 1016q23 -64 -150 -294q-24 -32 -65 -85q-78 -100 -90 -131q-17 -41 14 -81q17 -21 81 -82h1l1 -1l1 -1l2 -2q141 -131 191 -221q3 -5 6.5 -12.5t7 -26.5t-0.5 -34t-25 -27.5t-59 -12.5l-256 -4q-24 -5 -56 5t-52 22l-20 12q-30 21 -70 64t-68.5 77.5t-61 58 t-56.5 15.5q-3 -1 -8 -3.5t-17 -14.5t-21.5 -29.5t-17 -52t-6.5 -77.5q0 -15 -3.5 -27.5t-7.5 -18.5l-4 -5q-18 -19 -53 -22h-115q-71 -4 -146 16.5t-131.5 53t-103 66t-70.5 57.5l-25 24q-10 10 -27.5 30t-71.5 91t-106 151t-122.5 211t-130.5 272q-6 16 -6 27t3 16l4 6 q15 19 57 19l274 2q12 -2 23 -6.5t16 -8.5l5 -3q16 -11 24 -32q20 -50 46 -103.5t41 -81.5l16 -29q29 -60 56 -104t48.5 -68.5t41.5 -38.5t34 -14t27 5q2 1 5 5t12 22t13.5 47t9.5 81t0 125q-2 40 -9 73t-14 46l-6 12q-25 34 -85 43q-13 2 5 24q17 19 38 30q53 26 239 24 q82 -1 135 -13q20 -5 33.5 -13.5t20.5 -24t10.5 -32t3.5 -45.5t-1 -55t-2.5 -70.5t-1.5 -82.5q0 -11 -1 -42t-0.5 -48t3.5 -40.5t11.5 -39t22.5 -24.5q8 -2 17 -4t26 11t38 34.5t52 67t68 107.5q60 104 107 225q4 10 10 17.5t11 10.5l4 3l5 2.5t13 3t20 0.5l288 2 q39 5 64 -2.5t31 -16.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M675 252q21 34 11 69t-45 50q-34 14 -73 1t-60 -46q-22 -34 -13 -68.5t43 -50.5t74.5 -2.5t62.5 47.5zM769 373q8 13 3.5 26.5t-17.5 18.5q-14 5 -28.5 -0.5t-21.5 -18.5q-17 -31 13 -45q14 -5 29 0.5t22 18.5zM943 266q-45 -102 -158 -150t-224 -12 q-107 34 -147.5 126.5t6.5 187.5q47 93 151.5 139t210.5 19q111 -29 158.5 -119.5t2.5 -190.5zM1255 426q-9 96 -89 170t-208.5 109t-274.5 21q-223 -23 -369.5 -141.5t-132.5 -264.5q9 -96 89 -170t208.5 -109t274.5 -21q223 23 369.5 141.5t132.5 264.5zM1563 422 q0 -68 -37 -139.5t-109 -137t-168.5 -117.5t-226 -83t-270.5 -31t-275 33.5t-240.5 93t-171.5 151t-65 199.5q0 115 69.5 245t197.5 258q169 169 341.5 236t246.5 -7q65 -64 20 -209q-4 -14 -1 -20t10 -7t14.5 0.5t13.5 3.5l6 2q139 59 246 59t153 -61q45 -63 0 -178 q-2 -13 -4.5 -20t4.5 -12.5t12 -7.5t17 -6q57 -18 103 -47t80 -81.5t34 -116.5zM1489 1046q42 -47 54.5 -108.5t-6.5 -117.5q-8 -23 -29.5 -34t-44.5 -4q-23 8 -34 29.5t-4 44.5q20 63 -24 111t-107 35q-24 -5 -45 8t-25 37q-5 24 8 44.5t37 25.5q60 13 119 -5.5t101 -65.5z M1670 1209q87 -96 112.5 -222.5t-13.5 -241.5q-9 -27 -34 -40t-52 -4t-40 34t-5 52q28 82 10 172t-80 158q-62 69 -148 95.5t-173 8.5q-28 -6 -52 9.5t-30 43.5t9.5 51.5t43.5 29.5q123 26 244 -11.5t208 -134.5z" /> +<glyph unicode="" d="M1133 -34q-171 -94 -368 -94q-196 0 -367 94q138 87 235.5 211t131.5 268q35 -144 132.5 -268t235.5 -211zM638 1394v-485q0 -252 -126.5 -459.5t-330.5 -306.5q-181 215 -181 495q0 187 83.5 349.5t229.5 269.5t325 137zM1536 638q0 -280 -181 -495 q-204 99 -330.5 306.5t-126.5 459.5v485q179 -30 325 -137t229.5 -269.5t83.5 -349.5z" /> +<glyph unicode="" horiz-adv-x="1408" d="M1402 433q-32 -80 -76 -138t-91 -88.5t-99 -46.5t-101.5 -14.5t-96.5 8.5t-86.5 22t-69.5 27.5t-46 22.5l-17 10q-113 -228 -289.5 -359.5t-384.5 -132.5q-19 0 -32 13t-13 32t13 31.5t32 12.5q173 1 322.5 107.5t251.5 294.5q-36 -14 -72 -23t-83 -13t-91 2.5t-93 28.5 t-92 59t-84.5 100t-74.5 146q114 47 214 57t167.5 -7.5t124.5 -56.5t88.5 -77t56.5 -82q53 131 79 291q-7 -1 -18 -2.5t-46.5 -2.5t-69.5 0.5t-81.5 10t-88.5 23t-84 42.5t-75 65t-54.5 94.5t-28.5 127.5q70 28 133.5 36.5t112.5 -1t92 -30t73.5 -50t56 -61t42 -63t27.5 -56 t16 -39.5l4 -16q12 122 12 195q-8 6 -21.5 16t-49 44.5t-63.5 71.5t-54 93t-33 112.5t12 127t70 138.5q73 -25 127.5 -61.5t84.5 -76.5t48 -85t20.5 -89t-0.5 -85.5t-13 -76.5t-19 -62t-17 -42l-7 -15q1 -5 1 -50.5t-1 -71.5q3 7 10 18.5t30.5 43t50.5 58t71 55.5t91.5 44.5 t112 14.5t132.5 -24q-2 -78 -21.5 -141.5t-50 -104.5t-69.5 -71.5t-81.5 -45.5t-84.5 -24t-80 -9.5t-67.5 1t-46.5 4.5l-17 3q-23 -147 -73 -283q6 7 18 18.5t49.5 41t77.5 52.5t99.5 42t117.5 20t129 -23.5t137 -77.5z" /> +<glyph unicode="" horiz-adv-x="1280" d="M1259 283v-66q0 -85 -57.5 -144.5t-138.5 -59.5h-57l-260 -269v269h-529q-81 0 -138.5 59.5t-57.5 144.5v66h1238zM1259 609v-255h-1238v255h1238zM1259 937v-255h-1238v255h1238zM1259 1077v-67h-1238v67q0 84 57.5 143.5t138.5 59.5h846q81 0 138.5 -59.5t57.5 -143.5z " /> +<glyph unicode="" d="M1152 640q0 -14 -9 -23l-320 -320q-9 -9 -23 -9q-13 0 -22.5 9.5t-9.5 22.5v192h-352q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h352v192q0 14 9 23t23 9q12 0 24 -10l319 -319q9 -9 9 -23zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198 t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1152 736v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-352v-192q0 -14 -9 -23t-23 -9q-12 0 -24 10l-319 319q-9 9 -9 23t9 23l320 320q9 9 23 9q13 0 22.5 -9.5t9.5 -22.5v-192h352q13 0 22.5 -9.5t9.5 -22.5zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198 t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1024 960v-640q0 -26 -19 -45t-45 -19q-20 0 -37 12l-448 320q-27 19 -27 52t27 52l448 320q17 12 37 12q26 0 45 -19t19 -45zM1280 160v960q0 13 -9.5 22.5t-22.5 9.5h-960q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h960q13 0 22.5 9.5t9.5 22.5z M1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" d="M1024 640q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181zM768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5 t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1023 349l102 -204q-58 -179 -210 -290t-339 -111q-156 0 -288.5 77.5t-210 210t-77.5 288.5q0 181 104.5 330t274.5 211l17 -131q-122 -54 -195 -165.5t-73 -244.5q0 -185 131.5 -316.5t316.5 -131.5q126 0 232.5 65t165 175.5t49.5 236.5zM1571 249l58 -114l-256 -128 q-13 -7 -29 -7q-40 0 -57 35l-239 477h-472q-24 0 -42.5 16.5t-21.5 40.5l-96 779q-2 16 6 42q14 51 57 82.5t97 31.5q66 0 113 -47t47 -113q0 -69 -52 -117.5t-120 -41.5l37 -289h423v-128h-407l16 -128h455q40 0 57 -35l228 -455z" /> +<glyph unicode="" d="M1292 898q10 216 -161 222q-231 8 -312 -261q44 19 82 19q85 0 74 -96q-4 -57 -74 -167t-105 -110q-43 0 -82 169q-13 54 -45 255q-30 189 -160 177q-59 -7 -164 -100l-81 -72l-81 -72l52 -67q76 52 87 52q57 0 107 -179q15 -55 45 -164.5t45 -164.5q68 -179 164 -179 q157 0 383 294q220 283 226 444zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="1152" d="M1152 704q0 -191 -94.5 -353t-256.5 -256.5t-353 -94.5h-160q-14 0 -23 9t-9 23v611l-215 -66q-3 -1 -9 -1q-10 0 -19 6q-13 10 -13 26v128q0 23 23 31l233 71v93l-215 -66q-3 -1 -9 -1q-10 0 -19 6q-13 10 -13 26v128q0 23 23 31l233 71v250q0 14 9 23t23 9h160 q14 0 23 -9t9 -23v-181l375 116q15 5 28 -5t13 -26v-128q0 -23 -23 -31l-393 -121v-93l375 116q15 5 28 -5t13 -26v-128q0 -23 -23 -31l-393 -121v-487q188 13 318 151t130 328q0 14 9 23t23 9h160q14 0 23 -9t9 -23z" /> +<glyph unicode="" horiz-adv-x="1408" d="M1152 736v-64q0 -14 -9 -23t-23 -9h-352v-352q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v352h-352q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h352v352q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-352h352q14 0 23 -9t9 -23zM1280 288v832q0 66 -47 113t-113 47h-832 q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113zM1408 1120v-832q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="2176" d="M620 416q-110 -64 -268 -64h-128v64h-64q-13 0 -22.5 23.5t-9.5 56.5q0 24 7 49q-58 2 -96.5 10.5t-38.5 20.5t38.5 20.5t96.5 10.5q-7 25 -7 49q0 33 9.5 56.5t22.5 23.5h64v64h128q158 0 268 -64h1113q42 -7 106.5 -18t80.5 -14q89 -15 150 -40.5t83.5 -47.5t22.5 -40 t-22.5 -40t-83.5 -47.5t-150 -40.5q-16 -3 -80.5 -14t-106.5 -18h-1113zM1739 668q53 -36 53 -92t-53 -92l81 -30q68 48 68 122t-68 122zM625 400h1015q-217 -38 -456 -80q-57 0 -113 -24t-83 -48l-28 -24l-288 -288q-26 -26 -70.5 -45t-89.5 -19h-96l-93 464h29 q157 0 273 64zM352 816h-29l93 464h96q46 0 90 -19t70 -45l288 -288q4 -4 11 -10.5t30.5 -23t48.5 -29t61.5 -23t72.5 -10.5l456 -80h-1015q-116 64 -273 64z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1519 760q62 0 103.5 -40.5t41.5 -101.5q0 -97 -93 -130l-172 -59l56 -167q7 -21 7 -47q0 -59 -42 -102t-101 -43q-47 0 -85.5 27t-53.5 72l-55 165l-310 -106l55 -164q8 -24 8 -47q0 -59 -42 -102t-102 -43q-47 0 -85 27t-53 72l-55 163l-153 -53q-29 -9 -50 -9 q-61 0 -101.5 40t-40.5 101q0 47 27.5 85t71.5 53l156 53l-105 313l-156 -54q-26 -8 -48 -8q-60 0 -101 40.5t-41 100.5q0 47 27.5 85t71.5 53l157 53l-53 159q-8 24 -8 47q0 60 42 102.5t102 42.5q47 0 85 -27t53 -72l54 -160l310 105l-54 160q-8 24 -8 47q0 59 42.5 102 t101.5 43q47 0 85.5 -27.5t53.5 -71.5l53 -161l162 55q21 6 43 6q60 0 102.5 -39.5t42.5 -98.5q0 -45 -30 -81.5t-74 -51.5l-157 -54l105 -316l164 56q24 8 46 8zM725 498l310 105l-105 315l-310 -107z" /> +<glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960zM1280 352v436q-31 -35 -64 -55q-34 -22 -132.5 -85t-151.5 -99q-98 -69 -164 -69v0v0q-66 0 -164 69 q-46 32 -141.5 92.5t-142.5 92.5q-12 8 -33 27t-31 27v-436q0 -40 28 -68t68 -28h832q40 0 68 28t28 68zM1280 925q0 41 -27.5 70t-68.5 29h-832q-40 0 -68 -28t-28 -68q0 -37 30.5 -76.5t67.5 -64.5q47 -32 137.5 -89t129.5 -83q3 -2 17 -11.5t21 -14t21 -13t23.5 -13 t21.5 -9.5t22.5 -7.5t20.5 -2.5t20.5 2.5t22.5 7.5t21.5 9.5t23.5 13t21 13t21 14t17 11.5l267 174q35 23 66.5 62.5t31.5 73.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M127 640q0 163 67 313l367 -1005q-196 95 -315 281t-119 411zM1415 679q0 -19 -2.5 -38.5t-10 -49.5t-11.5 -44t-17.5 -59t-17.5 -58l-76 -256l-278 826q46 3 88 8q19 2 26 18.5t-2.5 31t-28.5 13.5l-205 -10q-75 1 -202 10q-12 1 -20.5 -5t-11.5 -15t-1.5 -18.5t9 -16.5 t19.5 -8l80 -8l120 -328l-168 -504l-280 832q46 3 88 8q19 2 26 18.5t-2.5 31t-28.5 13.5l-205 -10q-7 0 -23 0.5t-26 0.5q105 160 274.5 253.5t367.5 93.5q147 0 280.5 -53t238.5 -149h-10q-55 0 -92 -40.5t-37 -95.5q0 -12 2 -24t4 -21.5t8 -23t9 -21t12 -22.5t12.5 -21 t14.5 -24t14 -23q63 -107 63 -212zM909 573l237 -647q1 -6 5 -11q-126 -44 -255 -44q-112 0 -217 32zM1570 1009q95 -174 95 -369q0 -209 -104 -385.5t-279 -278.5l235 678q59 169 59 276q0 42 -6 79zM896 1536q182 0 348 -71t286 -191t191 -286t71 -348t-71 -348t-191 -286 t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71zM896 -215q173 0 331.5 68t273 182.5t182.5 273t68 331.5t-68 331.5t-182.5 273t-273 182.5t-331.5 68t-331.5 -68t-273 -182.5t-182.5 -273t-68 -331.5t68 -331.5t182.5 -273 t273 -182.5t331.5 -68z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1086 1536v-1536l-272 -128q-228 20 -414 102t-293 208.5t-107 272.5q0 140 100.5 263.5t275 205.5t391.5 108v-172q-217 -38 -356.5 -150t-139.5 -255q0 -152 154.5 -267t388.5 -145v1360zM1755 954l37 -390l-525 114l147 83q-119 70 -280 99v172q277 -33 481 -157z" /> +<glyph unicode="" horiz-adv-x="2048" d="M960 1536l960 -384v-128h-128q0 -26 -20.5 -45t-48.5 -19h-1526q-28 0 -48.5 19t-20.5 45h-128v128zM256 896h256v-768h128v768h256v-768h128v768h256v-768h128v768h256v-768h59q28 0 48.5 -19t20.5 -45v-64h-1664v64q0 26 20.5 45t48.5 19h59v768zM1851 -64 q28 0 48.5 -19t20.5 -45v-128h-1920v128q0 26 20.5 45t48.5 19h1782z" /> +<glyph unicode="" horiz-adv-x="2304" d="M1774 700l18 -316q4 -69 -82 -128t-235 -93.5t-323 -34.5t-323 34.5t-235 93.5t-82 128l18 316l574 -181q22 -7 48 -7t48 7zM2304 1024q0 -23 -22 -31l-1120 -352q-4 -1 -10 -1t-10 1l-652 206q-43 -34 -71 -111.5t-34 -178.5q63 -36 63 -109q0 -69 -58 -107l58 -433 q2 -14 -8 -25q-9 -11 -24 -11h-192q-15 0 -24 11q-10 11 -8 25l58 433q-58 38 -58 107q0 73 65 111q11 207 98 330l-333 104q-22 8 -22 31t22 31l1120 352q4 1 10 1t10 -1l1120 -352q22 -8 22 -31z" /> +<glyph unicode="" d="M859 579l13 -707q-62 11 -105 11q-41 0 -105 -11l13 707q-40 69 -168.5 295.5t-216.5 374.5t-181 287q58 -15 108 -15q43 0 111 15q63 -111 133.5 -229.5t167 -276.5t138.5 -227q37 61 109.5 177.5t117.5 190t105 176t107 189.5q54 -14 107 -14q56 0 114 14v0 q-28 -39 -60 -88.5t-49.5 -78.5t-56.5 -96t-49 -84q-146 -248 -353 -610z" /> +<glyph unicode="" d="M768 750h725q12 -67 12 -128q0 -217 -91 -387.5t-259.5 -266.5t-386.5 -96q-157 0 -299 60.5t-245 163.5t-163.5 245t-60.5 299t60.5 299t163.5 245t245 163.5t299 60.5q300 0 515 -201l-209 -201q-123 119 -306 119q-129 0 -238.5 -65t-173.5 -176.5t-64 -243.5 t64 -243.5t173.5 -176.5t238.5 -65q87 0 160 24t120 60t82 82t51.5 87t22.5 78h-436v264z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1095 369q16 -16 0 -31q-62 -62 -199 -62t-199 62q-16 15 0 31q6 6 15 6t15 -6q48 -49 169 -49q120 0 169 49q6 6 15 6t15 -6zM788 550q0 -37 -26 -63t-63 -26t-63.5 26t-26.5 63q0 38 26.5 64t63.5 26t63 -26.5t26 -63.5zM1183 550q0 -37 -26.5 -63t-63.5 -26t-63 26 t-26 63t26 63.5t63 26.5t63.5 -26t26.5 -64zM1434 670q0 49 -35 84t-85 35t-86 -36q-130 90 -311 96l63 283l200 -45q0 -37 26 -63t63 -26t63.5 26.5t26.5 63.5t-26.5 63.5t-63.5 26.5q-54 0 -80 -50l-221 49q-19 5 -25 -16l-69 -312q-180 -7 -309 -97q-35 37 -87 37 q-50 0 -85 -35t-35 -84q0 -35 18.5 -64t49.5 -44q-6 -27 -6 -56q0 -142 140 -243t337 -101q198 0 338 101t140 243q0 32 -7 57q30 15 48 43.5t18 63.5zM1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191 t348 71t348 -71t286 -191t191 -286t71 -348z" /> +<glyph unicode="" d="M939 407q13 -13 0 -26q-53 -53 -171 -53t-171 53q-13 13 0 26q5 6 13 6t13 -6q42 -42 145 -42t145 42q5 6 13 6t13 -6zM676 563q0 -31 -23 -54t-54 -23t-54 23t-23 54q0 32 22.5 54.5t54.5 22.5t54.5 -22.5t22.5 -54.5zM1014 563q0 -31 -23 -54t-54 -23t-54 23t-23 54 q0 32 22.5 54.5t54.5 22.5t54.5 -22.5t22.5 -54.5zM1229 666q0 42 -30 72t-73 30q-42 0 -73 -31q-113 78 -267 82l54 243l171 -39q1 -32 23.5 -54t53.5 -22q32 0 54.5 22.5t22.5 54.5t-22.5 54.5t-54.5 22.5q-48 0 -69 -43l-189 42q-17 5 -21 -13l-60 -268q-154 -6 -265 -83 q-30 32 -74 32q-43 0 -73 -30t-30 -72q0 -30 16 -55t42 -38q-5 -25 -5 -48q0 -122 120 -208.5t289 -86.5q170 0 290 86.5t120 208.5q0 25 -6 49q25 13 40.5 37.5t15.5 54.5zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960 q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" d="M866 697l90 27v62q0 79 -58 135t-138 56t-138 -55.5t-58 -134.5v-283q0 -20 -14 -33.5t-33 -13.5t-32.5 13.5t-13.5 33.5v120h-151v-122q0 -82 57.5 -139t139.5 -57q81 0 138.5 56.5t57.5 136.5v280q0 19 13.5 33t33.5 14q19 0 32.5 -14t13.5 -33v-54zM1199 502v122h-150 v-126q0 -20 -13.5 -33.5t-33.5 -13.5q-19 0 -32.5 14t-13.5 33v123l-90 -26l-60 28v-123q0 -80 58 -137t139 -57t138.5 57t57.5 139zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103 t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" horiz-adv-x="1920" d="M1062 824v118q0 42 -30 72t-72 30t-72 -30t-30 -72v-612q0 -175 -126 -299t-303 -124q-178 0 -303.5 125.5t-125.5 303.5v266h328v-262q0 -43 30 -72.5t72 -29.5t72 29.5t30 72.5v620q0 171 126.5 292t301.5 121q176 0 302 -122t126 -294v-136l-195 -58zM1592 602h328 v-266q0 -178 -125.5 -303.5t-303.5 -125.5q-177 0 -303 124.5t-126 300.5v268l131 -61l195 58v-270q0 -42 30 -71.5t72 -29.5t72 29.5t30 71.5v275z" /> +<glyph unicode="" d="M1472 160v480h-704v704h-480q-93 0 -158.5 -65.5t-65.5 -158.5v-480h704v-704h480q93 0 158.5 65.5t65.5 158.5zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5 t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="2048" d="M328 1254h204v-983h-532v697h328v286zM328 435v369h-123v-369h123zM614 968v-697h205v697h-205zM614 1254v-204h205v204h-205zM901 968h533v-942h-533v163h328v82h-328v697zM1229 435v369h-123v-369h123zM1516 968h532v-942h-532v163h327v82h-327v697zM1843 435v369h-123 v-369h123z" /> +<glyph unicode="" d="M1046 516q0 -64 -38 -109t-91 -45q-43 0 -70 15v277q28 17 70 17q53 0 91 -45.5t38 -109.5zM703 944q0 -64 -38 -109.5t-91 -45.5q-43 0 -70 15v277q28 17 70 17q53 0 91 -45t38 -109zM1265 513q0 134 -88 229t-213 95q-20 0 -39 -3q-23 -78 -78 -136q-87 -95 -211 -101 v-636l211 41v206q51 -19 117 -19q125 0 213 95t88 229zM922 940q0 134 -88.5 229t-213.5 95q-74 0 -141 -36h-186v-840l211 41v206q55 -19 116 -19q125 0 213.5 95t88.5 229zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960 q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="2038" d="M1222 607q75 3 143.5 -20.5t118 -58.5t101 -94.5t84 -108t75.5 -120.5q33 -56 78.5 -109t75.5 -80.5t99 -88.5q-48 -30 -108.5 -57.5t-138.5 -59t-114 -47.5q-44 37 -74 115t-43.5 164.5t-33 180.5t-42.5 168.5t-72.5 123t-122.5 48.5l-10 -2l-6 -4q4 -5 13 -14 q6 -5 28 -23.5t25.5 -22t19 -18t18 -20.5t11.5 -21t10.5 -27.5t4.5 -31t4 -40.5l1 -33q1 -26 -2.5 -57.5t-7.5 -52t-12.5 -58.5t-11.5 -53q-35 1 -101 -9.5t-98 -10.5q-39 0 -72 10q-2 16 -2 47q0 74 3 96q2 13 31.5 41.5t57 59t26.5 51.5q-24 2 -43 -24 q-36 -53 -111.5 -99.5t-136.5 -46.5q-25 0 -75.5 63t-106.5 139.5t-84 96.5q-6 4 -27 30q-482 -112 -513 -112q-16 0 -28 11t-12 27q0 15 8.5 26.5t22.5 14.5l486 106q-8 14 -8 25t5.5 17.5t16 11.5t20 7t23 4.5t18.5 4.5q4 1 15.5 7.5t17.5 6.5q15 0 28 -16t20 -33 q163 37 172 37q17 0 29.5 -11t12.5 -28q0 -15 -8.5 -26t-23.5 -14l-182 -40l-1 -16q-1 -26 81.5 -117.5t104.5 -91.5q47 0 119 80t72 129q0 36 -23.5 53t-51 18.5t-51 11.5t-23.5 34q0 16 10 34l-68 19q43 44 43 117q0 26 -5 58q82 16 144 16q44 0 71.5 -1.5t48.5 -8.5 t31 -13.5t20.5 -24.5t15.5 -33.5t17 -47.5t24 -60l50 25q-3 -40 -23 -60t-42.5 -21t-40 -6.5t-16.5 -20.5zM1282 842q-5 5 -13.5 15.5t-12 14.5t-10.5 11.5t-10 10.5l-8 8t-8.5 7.5t-8 5t-8.5 4.5q-7 3 -14.5 5t-20.5 2.5t-22 0.5h-32.5h-37.5q-126 0 -217 -43 q16 30 36 46.5t54 29.5t65.5 36t46 36.5t50 55t43.5 50.5q12 -9 28 -31.5t32 -36.5t38 -13l12 1v-76l22 -1q247 95 371 190q28 21 50 39t42.5 37.5t33 31t29.5 34t24 31t24.5 37t23 38t27 47.5t29.5 53l7 9q-2 -53 -43 -139q-79 -165 -205 -264t-306 -142q-14 -3 -42 -7.5 t-50 -9.5t-39 -14q3 -19 24.5 -46t21.5 -34q0 -11 -26 -30zM1061 -79q39 26 131.5 47.5t146.5 21.5q9 0 22.5 -15.5t28 -42.5t26 -50t24 -51t14.5 -33q-121 -45 -244 -45q-61 0 -125 11zM822 568l48 12l109 -177l-73 -48zM1323 51q3 -15 3 -16q0 -7 -17.5 -14.5t-46 -13 t-54 -9.5t-53.5 -7.5t-32 -4.5l-7 43q21 2 60.5 8.5t72 10t60.5 3.5h14zM866 679l-96 -20l-6 17q10 1 32.5 7t34.5 6q19 0 35 -10zM1061 45h31l10 -83l-41 -12v95zM1950 1535v1v-1zM1950 1535l-1 -5l-2 -2l1 3zM1950 1535l1 1z" /> +<glyph unicode="" d="M1167 -50q-5 19 -24 5q-30 -22 -87 -39t-131 -17q-129 0 -193 49q-5 4 -13 4q-11 0 -26 -12q-7 -6 -7.5 -16t7.5 -20q34 -32 87.5 -46t102.5 -12.5t99 4.5q41 4 84.5 20.5t65 30t28.5 20.5q12 12 7 29zM1128 65q-19 47 -39 61q-23 15 -76 15q-47 0 -71 -10 q-29 -12 -78 -56q-26 -24 -12 -44q9 -8 17.5 -4.5t31.5 23.5q3 2 10.5 8.5t10.5 8.5t10 7t11.5 7t12.5 5t15 4.5t16.5 2.5t20.5 1q27 0 44.5 -7.5t23 -14.5t13.5 -22q10 -17 12.5 -20t12.5 1q23 12 14 34zM1483 346q0 22 -5 44.5t-16.5 45t-34 36.5t-52.5 14 q-33 0 -97 -41.5t-129 -83.5t-101 -42q-27 -1 -63.5 19t-76 49t-83.5 58t-100 49t-111 19q-115 -1 -197 -78.5t-84 -178.5q-2 -112 74 -164q29 -20 62.5 -28.5t103.5 -8.5q57 0 132 32.5t134 71t120 70.5t93 31q26 -1 65 -31.5t71.5 -67t68 -67.5t55.5 -32q35 -3 58.5 14 t55.5 63q28 41 42.5 101t14.5 106zM1536 506q0 -164 -62 -304.5t-166 -236t-242.5 -149.5t-290.5 -54t-293 57.5t-247.5 157t-170.5 241.5t-64 302q0 89 19.5 172.5t49 145.5t70.5 118.5t78.5 94t78.5 69.5t64.5 46.5t42.5 24.5q14 8 51 26.5t54.5 28.5t48 30t60.5 44 q36 28 58 72.5t30 125.5q129 -155 186 -193q44 -29 130 -68t129 -66q21 -13 39 -25t60.5 -46.5t76 -70.5t75 -95t69 -122t47 -148.5t19.5 -177.5z" /> +<glyph unicode="" d="M1070 463l-160 -160l-151 -152l-30 -30q-65 -64 -151.5 -87t-171.5 -2q-16 -70 -72 -115t-129 -45q-85 0 -145 60.5t-60 145.5q0 72 44.5 128t113.5 72q-22 86 1 173t88 152l12 12l151 -152l-11 -11q-37 -37 -37 -89t37 -90q37 -37 89 -37t89 37l30 30l151 152l161 160z M729 1145l12 -12l-152 -152l-12 12q-37 37 -89 37t-89 -37t-37 -89.5t37 -89.5l29 -29l152 -152l160 -160l-151 -152l-161 160l-151 152l-30 30q-68 67 -90 159.5t5 179.5q-70 15 -115 71t-45 129q0 85 60 145.5t145 60.5q76 0 133.5 -49t69.5 -123q84 20 169.5 -3.5 t149.5 -87.5zM1536 78q0 -85 -60 -145.5t-145 -60.5q-74 0 -131 47t-71 118q-86 -28 -179.5 -6t-161.5 90l-11 12l151 152l12 -12q37 -37 89 -37t89 37t37 89t-37 89l-30 30l-152 152l-160 160l152 152l160 -160l152 -152l29 -30q64 -64 87.5 -150.5t2.5 -171.5 q76 -11 126.5 -68.5t50.5 -134.5zM1534 1202q0 -77 -51 -135t-127 -69q26 -85 3 -176.5t-90 -158.5l-12 -12l-151 152l12 12q37 37 37 89t-37 89t-89 37t-89 -37l-30 -30l-152 -152l-160 -160l-152 152l161 160l152 152l29 30q67 67 159 89.5t178 -3.5q11 75 68.5 126 t135.5 51q85 0 145 -60.5t60 -145.5z" /> +<glyph unicode="" d="M654 458q-1 -3 -12.5 0.5t-31.5 11.5l-20 9q-44 20 -87 49q-7 5 -41 31.5t-38 28.5q-67 -103 -134 -181q-81 -95 -105 -110q-4 -2 -19.5 -4t-18.5 0q6 4 82 92q21 24 85.5 115t78.5 118q17 30 51 98.5t36 77.5q-8 1 -110 -33q-8 -2 -27.5 -7.5t-34.5 -9.5t-17 -5 q-2 -2 -2 -10.5t-1 -9.5q-5 -10 -31 -15q-23 -7 -47 0q-18 4 -28 21q-4 6 -5 23q6 2 24.5 5t29.5 6q58 16 105 32q100 35 102 35q10 2 43 19.5t44 21.5q9 3 21.5 8t14.5 5.5t6 -0.5q2 -12 -1 -33q0 -2 -12.5 -27t-26.5 -53.5t-17 -33.5q-25 -50 -77 -131l64 -28 q12 -6 74.5 -32t67.5 -28q4 -1 10.5 -25.5t4.5 -30.5zM449 944q3 -15 -4 -28q-12 -23 -50 -38q-30 -12 -60 -12q-26 3 -49 26q-14 15 -18 41l1 3q3 -3 19.5 -5t26.5 0t58 16q36 12 55 14q17 0 21 -17zM1147 815l63 -227l-139 42zM39 15l694 232v1032l-694 -233v-1031z M1280 332l102 -31l-181 657l-100 31l-216 -536l102 -31l45 110l211 -65zM777 1294l573 -184v380zM1088 -29l158 -13l-54 -160l-40 66q-130 -83 -276 -108q-58 -12 -91 -12h-84q-79 0 -199.5 39t-183.5 85q-8 7 -8 16q0 8 5 13.5t13 5.5q4 0 18 -7.5t30.5 -16.5t20.5 -11 q73 -37 159.5 -61.5t157.5 -24.5q95 0 167 14.5t157 50.5q15 7 30.5 15.5t34 19t28.5 16.5zM1536 1050v-1079l-774 246q-14 -6 -375 -127.5t-368 -121.5q-13 0 -18 13q0 1 -1 3v1078q3 9 4 10q5 6 20 11q106 35 149 50v384l558 -198q2 0 160.5 55t316 108.5t161.5 53.5 q20 0 20 -21v-418z" /> +<glyph unicode="" horiz-adv-x="1792" d="M288 1152q66 0 113 -47t47 -113v-1088q0 -66 -47 -113t-113 -47h-128q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h128zM1664 989q58 -34 93 -93t35 -128v-768q0 -106 -75 -181t-181 -75h-864q-66 0 -113 47t-47 113v1536q0 40 28 68t68 28h672q40 0 88 -20t76 -48 l152 -152q28 -28 48 -76t20 -88v-163zM928 0v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM928 256v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM928 512v128q0 14 -9 23 t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1184 0v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1184 256v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128 q14 0 23 9t9 23zM1184 512v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1440 0v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1440 256v128q0 14 -9 23t-23 9h-128 q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1440 512v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1536 896v256h-160q-40 0 -68 28t-28 68v160h-640v-512h896z" /> +<glyph unicode="" d="M1344 1536q26 0 45 -19t19 -45v-1664q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v1664q0 26 19 45t45 19h1280zM512 1248v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM512 992v-64q0 -14 9 -23t23 -9h64q14 0 23 9 t9 23v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM512 736v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM512 480v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM384 160v64 q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM384 416v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM384 672v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64 q14 0 23 9t9 23zM384 928v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM384 1184v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM896 -96v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9 t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM896 416v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM896 672v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM896 928v64 q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM896 1184v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1152 160v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64 q14 0 23 9t9 23zM1152 416v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1152 672v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1152 928v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9 t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1152 1184v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23z" /> +<glyph unicode="" horiz-adv-x="1280" d="M1188 988l-292 -292v-824q0 -46 -33 -79t-79 -33t-79 33t-33 79v384h-64v-384q0 -46 -33 -79t-79 -33t-79 33t-33 79v824l-292 292q-28 28 -28 68t28 68t68 28t68 -28l228 -228h368l228 228q28 28 68 28t68 -28t28 -68t-28 -68zM864 1152q0 -93 -65.5 -158.5 t-158.5 -65.5t-158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5t158.5 -65.5t65.5 -158.5z" /> +<glyph unicode="" horiz-adv-x="1664" d="M780 1064q0 -60 -19 -113.5t-63 -92.5t-105 -39q-76 0 -138 57.5t-92 135.5t-30 151q0 60 19 113.5t63 92.5t105 39q77 0 138.5 -57.5t91.5 -135t30 -151.5zM438 581q0 -80 -42 -139t-119 -59q-76 0 -141.5 55.5t-100.5 133.5t-35 152q0 80 42 139.5t119 59.5 q76 0 141.5 -55.5t100.5 -134t35 -152.5zM832 608q118 0 255 -97.5t229 -237t92 -254.5q0 -46 -17 -76.5t-48.5 -45t-64.5 -20t-76 -5.5q-68 0 -187.5 45t-182.5 45q-66 0 -192.5 -44.5t-200.5 -44.5q-183 0 -183 146q0 86 56 191.5t139.5 192.5t187.5 146t193 59zM1071 819 q-61 0 -105 39t-63 92.5t-19 113.5q0 74 30 151.5t91.5 135t138.5 57.5q61 0 105 -39t63 -92.5t19 -113.5q0 -73 -30 -151t-92 -135.5t-138 -57.5zM1503 923q77 0 119 -59.5t42 -139.5q0 -74 -35 -152t-100.5 -133.5t-141.5 -55.5q-77 0 -119 59t-42 139q0 74 35 152.5 t100.5 134t141.5 55.5z" /> +<glyph unicode="" horiz-adv-x="768" d="M704 1008q0 -145 -57 -243.5t-152 -135.5l45 -821q2 -26 -16 -45t-44 -19h-192q-26 0 -44 19t-16 45l45 821q-95 37 -152 135.5t-57 243.5q0 128 42.5 249.5t117.5 200t160 78.5t160 -78.5t117.5 -200t42.5 -249.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M896 -93l640 349v636l-640 -233v-752zM832 772l698 254l-698 254l-698 -254zM1664 1024v-768q0 -35 -18 -65t-49 -47l-704 -384q-28 -16 -61 -16t-61 16l-704 384q-31 17 -49 47t-18 65v768q0 40 23 73t61 47l704 256q22 8 44 8t44 -8l704 -256q38 -14 61 -47t23 -73z " /> +<glyph unicode="" horiz-adv-x="2304" d="M640 -96l384 192v314l-384 -164v-342zM576 358l404 173l-404 173l-404 -173zM1664 -96l384 192v314l-384 -164v-342zM1600 358l404 173l-404 173l-404 -173zM1152 651l384 165v266l-384 -164v-267zM1088 1030l441 189l-441 189l-441 -189zM2176 512v-416q0 -36 -19 -67 t-52 -47l-448 -224q-25 -14 -57 -14t-57 14l-448 224q-5 2 -7 4q-2 -2 -7 -4l-448 -224q-25 -14 -57 -14t-57 14l-448 224q-33 16 -52 47t-19 67v416q0 38 21.5 70t56.5 48l434 186v400q0 38 21.5 70t56.5 48l448 192q23 10 50 10t50 -10l448 -192q35 -16 56.5 -48t21.5 -70 v-400l434 -186q36 -16 57 -48t21 -70z" /> +<glyph unicode="" horiz-adv-x="2048" d="M1848 1197h-511v-124h511v124zM1596 771q-90 0 -146 -52.5t-62 -142.5h408q-18 195 -200 195zM1612 186q63 0 122 32t76 87h221q-100 -307 -427 -307q-214 0 -340.5 132t-126.5 347q0 208 130.5 345.5t336.5 137.5q138 0 240.5 -68t153 -179t50.5 -248q0 -17 -2 -47h-658 q0 -111 57.5 -171.5t166.5 -60.5zM277 236h296q205 0 205 167q0 180 -199 180h-302v-347zM277 773h281q78 0 123.5 36.5t45.5 113.5q0 144 -190 144h-260v-294zM0 1282h594q87 0 155 -14t126.5 -47.5t90 -96.5t31.5 -154q0 -181 -172 -263q114 -32 172 -115t58 -204 q0 -75 -24.5 -136.5t-66 -103.5t-98.5 -71t-121 -42t-134 -13h-611v1260z" /> +<glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960zM499 1041h-371v-787h382q117 0 197 57.5t80 170.5q0 158 -143 200q107 52 107 164q0 57 -19.5 96.5 t-56.5 60.5t-79 29.5t-97 8.5zM477 723h-176v184h163q119 0 119 -90q0 -94 -106 -94zM486 388h-185v217h189q124 0 124 -113q0 -104 -128 -104zM1136 356q-68 0 -104 38t-36 107h411q1 10 1 30q0 132 -74.5 220.5t-203.5 88.5q-128 0 -210 -86t-82 -216q0 -135 79 -217 t213 -82q205 0 267 191h-138q-11 -34 -47.5 -54t-75.5 -20zM1126 722q113 0 124 -122h-254q4 56 39 89t91 33zM964 988h319v-77h-319v77z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1582 954q0 -101 -71.5 -172.5t-172.5 -71.5t-172.5 71.5t-71.5 172.5t71.5 172.5t172.5 71.5t172.5 -71.5t71.5 -172.5zM812 212q0 104 -73 177t-177 73q-27 0 -54 -6l104 -42q77 -31 109.5 -106.5t1.5 -151.5q-31 -77 -107 -109t-152 -1q-21 8 -62 24.5t-61 24.5 q32 -60 91 -96.5t130 -36.5q104 0 177 73t73 177zM1642 953q0 126 -89.5 215.5t-215.5 89.5q-127 0 -216.5 -89.5t-89.5 -215.5q0 -127 89.5 -216t216.5 -89q126 0 215.5 89t89.5 216zM1792 953q0 -189 -133.5 -322t-321.5 -133l-437 -319q-12 -129 -109 -218t-229 -89 q-121 0 -214 76t-118 192l-230 92v429l389 -157q79 48 173 48q13 0 35 -2l284 407q2 187 135.5 319t320.5 132q188 0 321.5 -133.5t133.5 -321.5z" /> +<glyph unicode="" d="M1242 889q0 80 -57 136.5t-137 56.5t-136.5 -57t-56.5 -136q0 -80 56.5 -136.5t136.5 -56.5t137 56.5t57 136.5zM632 301q0 -83 -58 -140.5t-140 -57.5q-56 0 -103 29t-72 77q52 -20 98 -40q60 -24 120 1.5t85 86.5q24 60 -1.5 120t-86.5 84l-82 33q22 5 42 5 q82 0 140 -57.5t58 -140.5zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v153l172 -69q20 -92 93.5 -152t168.5 -60q104 0 181 70t87 173l345 252q150 0 255.5 105.5t105.5 254.5q0 150 -105.5 255.5t-255.5 105.5 q-148 0 -253 -104.5t-107 -252.5l-225 -322q-9 1 -28 1q-75 0 -137 -37l-297 119v468q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5zM1289 887q0 -100 -71 -170.5t-171 -70.5t-170.5 70.5t-70.5 170.5t70.5 171t170.5 71q101 0 171.5 -70.5t70.5 -171.5z " /> +<glyph unicode="" horiz-adv-x="1792" d="M836 367l-15 -368l-2 -22l-420 29q-36 3 -67 31.5t-47 65.5q-11 27 -14.5 55t4 65t12 55t21.5 64t19 53q78 -12 509 -28zM449 953l180 -379l-147 92q-63 -72 -111.5 -144.5t-72.5 -125t-39.5 -94.5t-18.5 -63l-4 -21l-190 357q-17 26 -18 56t6 47l8 18q35 63 114 188 l-140 86zM1680 436l-188 -359q-12 -29 -36.5 -46.5t-43.5 -20.5l-18 -4q-71 -7 -219 -12l8 -164l-230 367l211 362l7 -173q170 -16 283 -5t170 33zM895 1360q-47 -63 -265 -435l-317 187l-19 12l225 356q20 31 60 45t80 10q24 -2 48.5 -12t42 -21t41.5 -33t36 -34.5 t36 -39.5t32 -35zM1550 1053l212 -363q18 -37 12.5 -76t-27.5 -74q-13 -20 -33 -37t-38 -28t-48.5 -22t-47 -16t-51.5 -14t-46 -12q-34 72 -265 436l313 195zM1407 1279l142 83l-220 -373l-419 20l151 86q-34 89 -75 166t-75.5 123.5t-64.5 80t-47 46.5l-17 13l405 -1 q31 3 58 -10.5t39 -28.5l11 -15q39 -61 112 -190z" /> +<glyph unicode="" horiz-adv-x="2048" d="M480 448q0 66 -47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47t113 47t47 113zM516 768h1016l-89 357q-2 8 -14 17.5t-21 9.5h-768q-9 0 -21 -9.5t-14 -17.5zM1888 448q0 66 -47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47t113 47t47 113zM2048 544v-384 q0 -14 -9 -23t-23 -9h-96v-128q0 -80 -56 -136t-136 -56t-136 56t-56 136v128h-1024v-128q0 -80 -56 -136t-136 -56t-136 56t-56 136v128h-96q-14 0 -23 9t-9 23v384q0 93 65.5 158.5t158.5 65.5h28l105 419q23 94 104 157.5t179 63.5h768q98 0 179 -63.5t104 -157.5 l105 -419h28q93 0 158.5 -65.5t65.5 -158.5z" /> +<glyph unicode="" horiz-adv-x="2048" d="M1824 640q93 0 158.5 -65.5t65.5 -158.5v-384q0 -14 -9 -23t-23 -9h-96v-64q0 -80 -56 -136t-136 -56t-136 56t-56 136v64h-1024v-64q0 -80 -56 -136t-136 -56t-136 56t-56 136v64h-96q-14 0 -23 9t-9 23v384q0 93 65.5 158.5t158.5 65.5h28l105 419q23 94 104 157.5 t179 63.5h128v224q0 14 9 23t23 9h448q14 0 23 -9t9 -23v-224h128q98 0 179 -63.5t104 -157.5l105 -419h28zM320 160q66 0 113 47t47 113t-47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47zM516 640h1016l-89 357q-2 8 -14 17.5t-21 9.5h-768q-9 0 -21 -9.5t-14 -17.5z M1728 160q66 0 113 47t47 113t-47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47z" /> +<glyph unicode="" d="M1504 64q0 -26 -19 -45t-45 -19h-462q1 -17 6 -87.5t5 -108.5q0 -25 -18 -42.5t-43 -17.5h-320q-25 0 -43 17.5t-18 42.5q0 38 5 108.5t6 87.5h-462q-26 0 -45 19t-19 45t19 45l402 403h-229q-26 0 -45 19t-19 45t19 45l402 403h-197q-26 0 -45 19t-19 45t19 45l384 384 q19 19 45 19t45 -19l384 -384q19 -19 19 -45t-19 -45t-45 -19h-197l402 -403q19 -19 19 -45t-19 -45t-45 -19h-229l402 -403q19 -19 19 -45z" /> +<glyph unicode="" d="M1127 326q0 32 -30 51q-193 115 -447 115q-133 0 -287 -34q-42 -9 -42 -52q0 -20 13.5 -34.5t35.5 -14.5q5 0 37 8q132 27 243 27q226 0 397 -103q19 -11 33 -11q19 0 33 13.5t14 34.5zM1223 541q0 40 -35 61q-237 141 -548 141q-153 0 -303 -42q-48 -13 -48 -64 q0 -25 17.5 -42.5t42.5 -17.5q7 0 37 8q122 33 251 33q279 0 488 -124q24 -13 38 -13q25 0 42.5 17.5t17.5 42.5zM1331 789q0 47 -40 70q-126 73 -293 110.5t-343 37.5q-204 0 -364 -47q-23 -7 -38.5 -25.5t-15.5 -48.5q0 -31 20.5 -52t51.5 -21q11 0 40 8q133 37 307 37 q159 0 309.5 -34t253.5 -95q21 -12 40 -12q29 0 50.5 20.5t21.5 51.5zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" horiz-adv-x="1024" d="M1024 1233l-303 -582l24 -31h279v-415h-507l-44 -30l-142 -273l-30 -30h-301v303l303 583l-24 30h-279v415h507l44 30l142 273l30 30h301v-303z" /> +<glyph unicode="" horiz-adv-x="2304" d="M784 164l16 241l-16 523q-1 10 -7.5 17t-16.5 7q-9 0 -16 -7t-7 -17l-14 -523l14 -241q1 -10 7.5 -16.5t15.5 -6.5q22 0 24 23zM1080 193l11 211l-12 586q0 16 -13 24q-8 5 -16 5t-16 -5q-13 -8 -13 -24l-1 -6l-10 -579q0 -1 11 -236v-1q0 -10 6 -17q9 -11 23 -11 q11 0 20 9q9 7 9 20zM35 533l20 -128l-20 -126q-2 -9 -9 -9t-9 9l-17 126l17 128q2 9 9 9t9 -9zM121 612l26 -207l-26 -203q-2 -9 -10 -9q-9 0 -9 10l-23 202l23 207q0 9 9 9q8 0 10 -9zM401 159zM213 650l25 -245l-25 -237q0 -11 -11 -11q-10 0 -12 11l-21 237l21 245 q2 12 12 12q11 0 11 -12zM307 657l23 -252l-23 -244q-2 -13 -14 -13q-13 0 -13 13l-21 244l21 252q0 13 13 13q12 0 14 -13zM401 639l21 -234l-21 -246q-2 -16 -16 -16q-6 0 -10.5 4.5t-4.5 11.5l-20 246l20 234q0 6 4.5 10.5t10.5 4.5q14 0 16 -15zM784 164zM495 785 l21 -380l-21 -246q0 -7 -5 -12.5t-12 -5.5q-16 0 -18 18l-18 246l18 380q2 18 18 18q7 0 12 -5.5t5 -12.5zM589 871l19 -468l-19 -244q0 -8 -5.5 -13.5t-13.5 -5.5q-18 0 -20 19l-16 244l16 468q2 19 20 19q8 0 13.5 -5.5t5.5 -13.5zM687 911l18 -506l-18 -242 q-2 -21 -22 -21q-19 0 -21 21l-16 242l16 506q0 9 6.5 15.5t14.5 6.5q9 0 15 -6.5t7 -15.5zM1079 169v0v0zM881 915l15 -510l-15 -239q0 -10 -7.5 -17.5t-17.5 -7.5t-17 7t-8 18l-14 239l14 510q0 11 7.5 18t17.5 7t17.5 -7t7.5 -18zM980 896l14 -492l-14 -236q0 -11 -8 -19 t-19 -8t-19 8t-9 19l-12 236l12 492q1 12 9 20t19 8t18.5 -8t8.5 -20zM1192 404l-14 -231v0q0 -13 -9 -22t-22 -9t-22 9t-10 22l-6 114l-6 117l12 636v3q2 15 12 24q9 7 20 7q8 0 15 -5q14 -8 16 -26zM2304 423q0 -117 -83 -199.5t-200 -82.5h-786q-13 2 -22 11t-9 22v899 q0 23 28 33q85 34 181 34q195 0 338 -131.5t160 -323.5q53 22 110 22q117 0 200 -83t83 -201z" /> +<glyph unicode="" d="M768 768q237 0 443 43t325 127v-170q0 -69 -103 -128t-280 -93.5t-385 -34.5t-385 34.5t-280 93.5t-103 128v170q119 -84 325 -127t443 -43zM768 0q237 0 443 43t325 127v-170q0 -69 -103 -128t-280 -93.5t-385 -34.5t-385 34.5t-280 93.5t-103 128v170q119 -84 325 -127 t443 -43zM768 384q237 0 443 43t325 127v-170q0 -69 -103 -128t-280 -93.5t-385 -34.5t-385 34.5t-280 93.5t-103 128v170q119 -84 325 -127t443 -43zM768 1536q208 0 385 -34.5t280 -93.5t103 -128v-128q0 -69 -103 -128t-280 -93.5t-385 -34.5t-385 34.5t-280 93.5 t-103 128v128q0 69 103 128t280 93.5t385 34.5z" /> +<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M894 465q33 -26 84 -56q59 7 117 7q147 0 177 -49q16 -22 2 -52q0 -1 -1 -2l-2 -2v-1q-6 -38 -71 -38q-48 0 -115 20t-130 53q-221 -24 -392 -83q-153 -262 -242 -262q-15 0 -28 7l-24 12q-1 1 -6 5q-10 10 -6 36q9 40 56 91.5t132 96.5q14 9 23 -6q2 -2 2 -4q52 85 107 197 q68 136 104 262q-24 82 -30.5 159.5t6.5 127.5q11 40 42 40h21h1q23 0 35 -15q18 -21 9 -68q-2 -6 -4 -8q1 -3 1 -8v-30q-2 -123 -14 -192q55 -164 146 -238zM318 54q52 24 137 158q-51 -40 -87.5 -84t-49.5 -74zM716 974q-15 -42 -2 -132q1 7 7 44q0 3 7 43q1 4 4 8 q-1 1 -1 2t-0.5 1.5t-0.5 1.5q-1 22 -13 36q0 -1 -1 -2v-2zM592 313q135 54 284 81q-2 1 -13 9.5t-16 13.5q-76 67 -127 176q-27 -86 -83 -197q-30 -56 -45 -83zM1238 329q-24 24 -140 24q76 -28 124 -28q14 0 18 1q0 1 -2 3z" /> +<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M233 768v-107h70l164 -661h159l128 485q7 20 10 46q2 16 2 24h4l3 -24q1 -3 3.5 -20t5.5 -26l128 -485h159l164 661h70v107h-300v-107h90l-99 -438q-5 -20 -7 -46l-2 -21h-4l-3 21q-1 5 -4 21t-5 25l-144 545h-114l-144 -545q-2 -9 -4.5 -24.5t-3.5 -21.5l-4 -21h-4l-2 21 q-2 26 -7 46l-99 438h90v107h-300z" /> +<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M429 106v-106h281v106h-75l103 161q5 7 10 16.5t7.5 13.5t3.5 4h2q1 -4 5 -10q2 -4 4.5 -7.5t6 -8t6.5 -8.5l107 -161h-76v-106h291v106h-68l-192 273l195 282h67v107h-279v-107h74l-103 -159q-4 -7 -10 -16.5t-9 -13.5l-2 -3h-2q-1 4 -5 10q-6 11 -17 23l-106 159h76v107 h-290v-107h68l189 -272l-194 -283h-68z" /> +<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M416 106v-106h327v106h-93v167h137q76 0 118 15q67 23 106.5 87t39.5 146q0 81 -37 141t-100 87q-48 19 -130 19h-368v-107h92v-555h-92zM769 386h-119v268h120q52 0 83 -18q56 -33 56 -115q0 -89 -62 -120q-31 -15 -78 -15z" /> +<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M1280 320v-320h-1024v192l192 192l128 -128l384 384zM448 512q-80 0 -136 56t-56 136t56 136t136 56t136 -56t56 -136t-56 -136t-136 -56z" /> +<glyph unicode="" d="M640 1152v128h-128v-128h128zM768 1024v128h-128v-128h128zM640 896v128h-128v-128h128zM768 768v128h-128v-128h128zM1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400 v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-128v-128h-128v128h-512v-1536h1280zM781 593l107 -349q8 -27 8 -52q0 -83 -72.5 -137.5t-183.5 -54.5t-183.5 54.5t-72.5 137.5q0 25 8 52q21 63 120 396v128h128v-128h79 q22 0 39 -13t23 -34zM640 128q53 0 90.5 19t37.5 45t-37.5 45t-90.5 19t-90.5 -19t-37.5 -45t37.5 -45t90.5 -19z" /> +<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M620 686q20 -8 20 -30v-544q0 -22 -20 -30q-8 -2 -12 -2q-12 0 -23 9l-166 167h-131q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h131l166 167q16 15 35 7zM1037 -3q31 0 50 24q129 159 129 363t-129 363q-16 21 -43 24t-47 -14q-21 -17 -23.5 -43.5t14.5 -47.5 q100 -123 100 -282t-100 -282q-17 -21 -14.5 -47.5t23.5 -42.5q18 -15 40 -15zM826 145q27 0 47 20q87 93 87 219t-87 219q-18 19 -45 20t-46 -17t-20 -44.5t18 -46.5q52 -57 52 -131t-52 -131q-19 -20 -18 -46.5t20 -44.5q20 -17 44 -17z" /> +<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M768 768q52 0 90 -38t38 -90v-384q0 -52 -38 -90t-90 -38h-384q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h384zM1260 766q20 -8 20 -30v-576q0 -22 -20 -30q-8 -2 -12 -2q-14 0 -23 9l-265 266v90l265 266q9 9 23 9q4 0 12 -2z" /> +<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M480 768q8 11 21 12.5t24 -6.5l51 -38q11 -8 12.5 -21t-6.5 -24l-182 -243l182 -243q8 -11 6.5 -24t-12.5 -21l-51 -38q-11 -8 -24 -6.5t-21 12.5l-226 301q-14 19 0 38zM1282 467q14 -19 0 -38l-226 -301q-8 -11 -21 -12.5t-24 6.5l-51 38q-11 8 -12.5 21t6.5 24l182 243 l-182 243q-8 11 -6.5 24t12.5 21l51 38q11 8 24 6.5t21 -12.5zM662 6q-13 2 -20.5 13t-5.5 24l138 831q2 13 13 20.5t24 5.5l63 -10q13 -2 20.5 -13t5.5 -24l-138 -831q-2 -13 -13 -20.5t-24 -5.5z" /> +<glyph unicode="" d="M1497 709v-198q-101 -23 -198 -23q-65 -136 -165.5 -271t-181.5 -215.5t-128 -106.5q-80 -45 -162 3q-28 17 -60.5 43.5t-85 83.5t-102.5 128.5t-107.5 184t-105.5 244t-91.5 314.5t-70.5 390h283q26 -218 70 -398.5t104.5 -317t121.5 -235.5t140 -195q169 169 287 406 q-142 72 -223 220t-81 333q0 192 104 314.5t284 122.5q178 0 273 -105.5t95 -297.5q0 -159 -58 -286q-7 -1 -19.5 -3t-46 -2t-63 6t-62 25.5t-50.5 51.5q31 103 31 184q0 87 -29 132t-79 45q-53 0 -85 -49.5t-32 -140.5q0 -186 105 -293.5t267 -107.5q62 0 121 14z" /> +<glyph unicode="" horiz-adv-x="1792" d="M216 367l603 -402v359l-334 223zM154 511l193 129l-193 129v-258zM973 -35l603 402l-269 180l-334 -223v-359zM896 458l272 182l-272 182l-272 -182zM485 733l334 223v359l-603 -402zM1445 640l193 -129v258zM1307 733l269 180l-603 402v-359zM1792 913v-546 q0 -41 -34 -64l-819 -546q-21 -13 -43 -13t-43 13l-819 546q-34 23 -34 64v546q0 41 34 64l819 546q21 13 43 13t43 -13l819 -546q34 -23 34 -64z" /> +<glyph unicode="" horiz-adv-x="2048" d="M1800 764q111 -46 179.5 -145.5t68.5 -221.5q0 -164 -118 -280.5t-285 -116.5q-4 0 -11.5 0.5t-10.5 0.5h-1209h-1h-2h-5q-170 10 -288 125.5t-118 280.5q0 110 55 203t147 147q-12 39 -12 82q0 115 82 196t199 81q95 0 172 -58q75 154 222.5 248t326.5 94 q166 0 306 -80.5t221.5 -218.5t81.5 -301q0 -6 -0.5 -18t-0.5 -18zM468 498q0 -122 84 -193t208 -71q137 0 240 99q-16 20 -47.5 56.5t-43.5 50.5q-67 -65 -144 -65q-55 0 -93.5 33.5t-38.5 87.5q0 53 38.5 87t91.5 34q44 0 84.5 -21t73 -55t65 -75t69 -82t77 -75t97 -55 t121.5 -21q121 0 204.5 71.5t83.5 190.5q0 121 -84 192t-207 71q-143 0 -241 -97q14 -16 29.5 -34t34.5 -40t29 -34q66 64 142 64q52 0 92 -33t40 -84q0 -57 -37 -91.5t-94 -34.5q-43 0 -82.5 21t-72 55t-65.5 75t-69.5 82t-77.5 75t-96.5 55t-118.5 21q-122 0 -207 -70.5 t-85 -189.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M896 1536q182 0 348 -71t286 -191t191 -286t71 -348t-71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71zM896 1408q-190 0 -361 -90l194 -194q82 28 167 28t167 -28l194 194q-171 90 -361 90zM218 279l194 194 q-28 82 -28 167t28 167l-194 194q-90 -171 -90 -361t90 -361zM896 -128q190 0 361 90l-194 194q-82 -28 -167 -28t-167 28l-194 -194q171 -90 361 -90zM896 256q159 0 271.5 112.5t112.5 271.5t-112.5 271.5t-271.5 112.5t-271.5 -112.5t-112.5 -271.5t112.5 -271.5 t271.5 -112.5zM1380 473l194 -194q90 171 90 361t-90 361l-194 -194q28 -82 28 -167t-28 -167z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1760 640q0 -176 -68.5 -336t-184 -275.5t-275.5 -184t-336 -68.5t-336 68.5t-275.5 184t-184 275.5t-68.5 336q0 213 97 398.5t265 305.5t374 151v-228q-221 -45 -366.5 -221t-145.5 -406q0 -130 51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5 t136.5 204t51 248.5q0 230 -145.5 406t-366.5 221v228q206 -31 374 -151t265 -305.5t97 -398.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M19 662q8 217 116 406t305 318h5q0 -1 -1 -3q-8 -8 -28 -33.5t-52 -76.5t-60 -110.5t-44.5 -135.5t-14 -150.5t39 -157.5t108.5 -154q50 -50 102 -69.5t90.5 -11.5t69.5 23.5t47 32.5l16 16q39 51 53 116.5t6.5 122.5t-21 107t-26.5 80l-14 29q-10 25 -30.5 49.5t-43 41 t-43.5 29.5t-35 19l-13 6l104 115q39 -17 78 -52t59 -61l19 -27q1 48 -18.5 103.5t-40.5 87.5l-20 31l161 183l160 -181q-33 -46 -52.5 -102.5t-22.5 -90.5l-4 -33q22 37 61.5 72.5t67.5 52.5l28 17l103 -115q-44 -14 -85 -50t-60 -65l-19 -29q-31 -56 -48 -133.5t-7 -170 t57 -156.5q33 -45 77.5 -60.5t85 -5.5t76 26.5t57.5 33.5l21 16q60 53 96.5 115t48.5 121.5t10 121.5t-18 118t-37 107.5t-45.5 93t-45 72t-34.5 47.5l-13 17q-14 13 -7 13l10 -3q40 -29 62.5 -46t62 -50t64 -58t58.5 -65t55.5 -77t45.5 -88t38 -103t23.5 -117t10.5 -136 q3 -259 -108 -465t-312 -321t-456 -115q-185 0 -351 74t-283.5 198t-184 293t-60.5 353z" /> +<glyph unicode="" horiz-adv-x="1792" d="M874 -102v-66q-208 6 -385 109.5t-283 275.5l58 34q29 -49 73 -99l65 57q148 -168 368 -212l-17 -86q65 -12 121 -13zM276 428l-83 -28q22 -60 49 -112l-57 -33q-98 180 -98 385t98 385l57 -33q-30 -56 -49 -112l82 -28q-35 -100 -35 -212q0 -109 36 -212zM1528 251 l58 -34q-106 -172 -283 -275.5t-385 -109.5v66q56 1 121 13l-17 86q220 44 368 212l65 -57q44 50 73 99zM1377 805l-233 -80q14 -42 14 -85t-14 -85l232 -80q-31 -92 -98 -169l-185 162q-57 -67 -147 -85l48 -241q-52 -10 -98 -10t-98 10l48 241q-90 18 -147 85l-185 -162 q-67 77 -98 169l232 80q-14 42 -14 85t14 85l-233 80q33 93 99 169l185 -162q59 68 147 86l-48 240q44 10 98 10t98 -10l-48 -240q88 -18 147 -86l185 162q66 -76 99 -169zM874 1448v-66q-65 -2 -121 -13l17 -86q-220 -42 -368 -211l-65 56q-38 -42 -73 -98l-57 33 q106 172 282 275.5t385 109.5zM1705 640q0 -205 -98 -385l-57 33q27 52 49 112l-83 28q36 103 36 212q0 112 -35 212l82 28q-19 56 -49 112l57 33q98 -180 98 -385zM1585 1063l-57 -33q-35 56 -73 98l-65 -56q-148 169 -368 211l17 86q-56 11 -121 13v66q209 -6 385 -109.5 t282 -275.5zM1748 640q0 173 -67.5 331t-181.5 272t-272 181.5t-331 67.5t-331 -67.5t-272 -181.5t-181.5 -272t-67.5 -331t67.5 -331t181.5 -272t272 -181.5t331 -67.5t331 67.5t272 181.5t181.5 272t67.5 331zM1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71 t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" /> +<glyph unicode="" d="M582 228q0 -66 -93 -66q-107 0 -107 63q0 64 98 64q102 0 102 -61zM546 694q0 -85 -74 -85q-77 0 -77 84q0 90 77 90q36 0 55 -25.5t19 -63.5zM712 769v125q-78 -29 -135 -29q-50 29 -110 29q-86 0 -145 -57t-59 -143q0 -50 29.5 -102t73.5 -67v-3q-38 -17 -38 -85 q0 -53 41 -77v-3q-113 -37 -113 -139q0 -45 20 -78.5t54 -51t72 -25.5t81 -8q224 0 224 188q0 67 -48 99t-126 46q-27 5 -51.5 20.5t-24.5 39.5q0 44 49 52q77 15 122 70t45 134q0 24 -10 52q37 9 49 13zM771 350h137q-2 27 -2 82v387q0 46 2 69h-137q3 -23 3 -71v-392 q0 -50 -3 -75zM1280 366v121q-30 -21 -68 -21q-53 0 -53 82v225h52q9 0 26.5 -1t26.5 -1v117h-105q0 82 3 102h-140q4 -24 4 -55v-47h-60v-117q36 3 37 3q3 0 11 -0.5t12 -0.5v-2h-2v-217q0 -37 2.5 -64t11.5 -56.5t24.5 -48.5t43.5 -31t66 -12q64 0 108 24zM924 1072 q0 36 -24 63.5t-60 27.5t-60.5 -27t-24.5 -64q0 -36 25 -62.5t60 -26.5t59.5 27t24.5 62zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M595 22q0 100 -165 100q-158 0 -158 -104q0 -101 172 -101q151 0 151 105zM536 777q0 61 -30 102t-89 41q-124 0 -124 -145q0 -135 124 -135q119 0 119 137zM805 1101v-202q-36 -12 -79 -22q16 -43 16 -84q0 -127 -73 -216.5t-197 -112.5q-40 -8 -59.5 -27t-19.5 -58 q0 -31 22.5 -51.5t58 -32t78.5 -22t86 -25.5t78.5 -37.5t58 -64t22.5 -98.5q0 -304 -363 -304q-69 0 -130 12.5t-116 41t-87.5 82t-32.5 127.5q0 165 182 225v4q-67 41 -67 126q0 109 63 137v4q-72 24 -119.5 108.5t-47.5 165.5q0 139 95 231.5t235 92.5q96 0 178 -47 q98 0 218 47zM1123 220h-222q4 45 4 134v609q0 94 -4 128h222q-4 -33 -4 -124v-613q0 -89 4 -134zM1724 442v-196q-71 -39 -174 -39q-62 0 -107 20t-70 50t-39.5 78t-18.5 92t-4 103v351h2v4q-7 0 -19 1t-18 1q-21 0 -59 -6v190h96v76q0 54 -6 89h227q-6 -41 -6 -165h171 v-190q-15 0 -43.5 2t-42.5 2h-85v-365q0 -131 87 -131q61 0 109 33zM1148 1389q0 -58 -39 -101.5t-96 -43.5q-58 0 -98 43.5t-40 101.5q0 59 39.5 103t98.5 44q58 0 96.5 -44.5t38.5 -102.5z" /> +<glyph unicode="" d="M809 532l266 499h-112l-157 -312q-24 -48 -44 -92l-42 92l-155 312h-120l263 -493v-324h101v318zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="1280" d="M842 964q0 -80 -57 -136.5t-136 -56.5q-60 0 -111 35q-62 -67 -115 -146q-247 -371 -202 -859q1 -22 -12.5 -38.5t-34.5 -18.5h-5q-20 0 -35 13.5t-17 33.5q-14 126 -3.5 247.5t29.5 217t54 186t69 155.5t74 125q61 90 132 165q-16 35 -16 77q0 80 56.5 136.5t136.5 56.5 t136.5 -56.5t56.5 -136.5zM1223 953q0 -158 -78 -292t-212.5 -212t-292.5 -78q-64 0 -131 14q-21 5 -32.5 23.5t-6.5 39.5q5 20 23 31.5t39 7.5q51 -13 108 -13q97 0 186 38t153 102t102 153t38 186t-38 186t-102 153t-153 102t-186 38t-186 -38t-153 -102t-102 -153 t-38 -186q0 -114 52 -218q10 -20 3.5 -40t-25.5 -30t-39.5 -3t-30.5 26q-64 123 -64 265q0 119 46.5 227t124.5 186t186 124t226 46q158 0 292.5 -78t212.5 -212.5t78 -292.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M270 730q-8 19 -8 52q0 20 11 49t24 45q-1 22 7.5 53t22.5 43q0 139 92.5 288.5t217.5 209.5q139 66 324 66q133 0 266 -55q49 -21 90 -48t71 -56t55 -68t42 -74t32.5 -84.5t25.5 -89.5t22 -98l1 -5q55 -83 55 -150q0 -14 -9 -40t-9 -38q0 -1 1.5 -3.5t3.5 -5t2 -3.5 q77 -114 120.5 -214.5t43.5 -208.5q0 -43 -19.5 -100t-55.5 -57q-9 0 -19.5 7.5t-19 17.5t-19 26t-16 26.5t-13.5 26t-9 17.5q-1 1 -3 1l-5 -4q-59 -154 -132 -223q20 -20 61.5 -38.5t69 -41.5t35.5 -65q-2 -4 -4 -16t-7 -18q-64 -97 -302 -97q-53 0 -110.5 9t-98 20 t-104.5 30q-15 5 -23 7q-14 4 -46 4.5t-40 1.5q-41 -45 -127.5 -65t-168.5 -20q-35 0 -69 1.5t-93 9t-101 20.5t-74.5 40t-32.5 64q0 40 10 59.5t41 48.5q11 2 40.5 13t49.5 12q4 0 14 2q2 2 2 4l-2 3q-48 11 -108 105.5t-73 156.5l-5 3q-4 0 -12 -20q-18 -41 -54.5 -74.5 t-77.5 -37.5h-1q-4 0 -6 4.5t-5 5.5q-23 54 -23 100q0 275 252 466z" /> +<glyph unicode="" horiz-adv-x="2048" d="M580 1075q0 41 -25 66t-66 25q-43 0 -76 -25.5t-33 -65.5q0 -39 33 -64.5t76 -25.5q41 0 66 24.5t25 65.5zM1323 568q0 28 -25.5 50t-65.5 22q-27 0 -49.5 -22.5t-22.5 -49.5q0 -28 22.5 -50.5t49.5 -22.5q40 0 65.5 22t25.5 51zM1087 1075q0 41 -24.5 66t-65.5 25 q-43 0 -76 -25.5t-33 -65.5q0 -39 33 -64.5t76 -25.5q41 0 65.5 24.5t24.5 65.5zM1722 568q0 28 -26 50t-65 22q-27 0 -49.5 -22.5t-22.5 -49.5q0 -28 22.5 -50.5t49.5 -22.5q39 0 65 22t26 51zM1456 965q-31 4 -70 4q-169 0 -311 -77t-223.5 -208.5t-81.5 -287.5 q0 -78 23 -152q-35 -3 -68 -3q-26 0 -50 1.5t-55 6.5t-44.5 7t-54.5 10.5t-50 10.5l-253 -127l72 218q-290 203 -290 490q0 169 97.5 311t264 223.5t363.5 81.5q176 0 332.5 -66t262 -182.5t136.5 -260.5zM2048 404q0 -117 -68.5 -223.5t-185.5 -193.5l55 -181l-199 109 q-150 -37 -218 -37q-169 0 -311 70.5t-223.5 191.5t-81.5 264t81.5 264t223.5 191.5t311 70.5q161 0 303 -70.5t227.5 -192t85.5 -263.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1764 1525q33 -24 27 -64l-256 -1536q-5 -29 -32 -45q-14 -8 -31 -8q-11 0 -24 5l-453 185l-242 -295q-18 -23 -49 -23q-13 0 -22 4q-19 7 -30.5 23.5t-11.5 36.5v349l864 1059l-1069 -925l-395 162q-37 14 -40 55q-2 40 32 59l1664 960q15 9 32 9q20 0 36 -11z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1764 1525q33 -24 27 -64l-256 -1536q-5 -29 -32 -45q-14 -8 -31 -8q-11 0 -24 5l-527 215l-298 -327q-18 -21 -47 -21q-14 0 -23 4q-19 7 -30 23.5t-11 36.5v452l-472 193q-37 14 -40 55q-3 39 32 59l1664 960q35 21 68 -2zM1422 26l221 1323l-1434 -827l336 -137 l863 639l-478 -797z" /> +<glyph unicode="" d="M1536 640q0 -156 -61 -298t-164 -245t-245 -164t-298 -61q-172 0 -327 72.5t-264 204.5q-7 10 -6.5 22.5t8.5 20.5l137 138q10 9 25 9q16 -2 23 -12q73 -95 179 -147t225 -52q104 0 198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5t-40.5 198.5t-109.5 163.5 t-163.5 109.5t-198.5 40.5q-98 0 -188 -35.5t-160 -101.5l137 -138q31 -30 14 -69q-17 -40 -59 -40h-448q-26 0 -45 19t-19 45v448q0 42 40 59q39 17 69 -14l130 -129q107 101 244.5 156.5t284.5 55.5q156 0 298 -61t245 -164t164 -245t61 -298zM896 928v-448q0 -14 -9 -23 t-23 -9h-320q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h224v352q0 14 9 23t23 9h64q14 0 23 -9t9 -23z" /> +<glyph unicode="" d="M768 1280q-130 0 -248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5t-51 248.5t-136.5 204t-204 136.5t-248.5 51zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103 t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1682 -128q-44 0 -132.5 3.5t-133.5 3.5q-44 0 -132 -3.5t-132 -3.5q-24 0 -37 20.5t-13 45.5q0 31 17 46t39 17t51 7t45 15q33 21 33 140l-1 391q0 21 -1 31q-13 4 -50 4h-675q-38 0 -51 -4q-1 -10 -1 -31l-1 -371q0 -142 37 -164q16 -10 48 -13t57 -3.5t45 -15 t20 -45.5q0 -26 -12.5 -48t-36.5 -22q-47 0 -139.5 3.5t-138.5 3.5q-43 0 -128 -3.5t-127 -3.5q-23 0 -35.5 21t-12.5 45q0 30 15.5 45t36 17.5t47.5 7.5t42 15q33 23 33 143l-1 57v813q0 3 0.5 26t0 36.5t-1.5 38.5t-3.5 42t-6.5 36.5t-11 31.5t-16 18q-15 10 -45 12t-53 2 t-41 14t-18 45q0 26 12 48t36 22q46 0 138.5 -3.5t138.5 -3.5q42 0 126.5 3.5t126.5 3.5q25 0 37.5 -22t12.5 -48q0 -30 -17 -43.5t-38.5 -14.5t-49.5 -4t-43 -13q-35 -21 -35 -160l1 -320q0 -21 1 -32q13 -3 39 -3h699q25 0 38 3q1 11 1 32l1 320q0 139 -35 160 q-18 11 -58.5 12.5t-66 13t-25.5 49.5q0 26 12.5 48t37.5 22q44 0 132 -3.5t132 -3.5q43 0 129 3.5t129 3.5q25 0 37.5 -22t12.5 -48q0 -30 -17.5 -44t-40 -14.5t-51.5 -3t-44 -12.5q-35 -23 -35 -161l1 -943q0 -119 34 -140q16 -10 46 -13.5t53.5 -4.5t41.5 -15.5t18 -44.5 q0 -26 -12 -48t-36 -22z" /> +<glyph unicode="" horiz-adv-x="1280" d="M1278 1347v-73q0 -29 -18.5 -61t-42.5 -32q-50 0 -54 -1q-26 -6 -32 -31q-3 -11 -3 -64v-1152q0 -25 -18 -43t-43 -18h-108q-25 0 -43 18t-18 43v1218h-143v-1218q0 -25 -17.5 -43t-43.5 -18h-108q-26 0 -43.5 18t-17.5 43v496q-147 12 -245 59q-126 58 -192 179 q-64 117 -64 259q0 166 88 286q88 118 209 159q111 37 417 37h479q25 0 43 -18t18 -43z" /> +<glyph unicode="" d="M352 128v-128h-352v128h352zM704 256q26 0 45 -19t19 -45v-256q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h256zM864 640v-128h-864v128h864zM224 1152v-128h-224v128h224zM1536 128v-128h-736v128h736zM576 1280q26 0 45 -19t19 -45v-256 q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h256zM1216 768q26 0 45 -19t19 -45v-256q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h256zM1536 640v-128h-224v128h224zM1536 1152v-128h-864v128h864z" /> +<glyph unicode="" d="M1216 512q133 0 226.5 -93.5t93.5 -226.5t-93.5 -226.5t-226.5 -93.5t-226.5 93.5t-93.5 226.5q0 12 2 34l-360 180q-92 -86 -218 -86q-133 0 -226.5 93.5t-93.5 226.5t93.5 226.5t226.5 93.5q126 0 218 -86l360 180q-2 22 -2 34q0 133 93.5 226.5t226.5 93.5 t226.5 -93.5t93.5 -226.5t-93.5 -226.5t-226.5 -93.5q-126 0 -218 86l-360 -180q2 -22 2 -34t-2 -34l360 -180q92 86 218 86z" /> +<glyph unicode="" d="M1280 341q0 88 -62.5 151t-150.5 63q-84 0 -145 -58l-241 120q2 16 2 23t-2 23l241 120q61 -58 145 -58q88 0 150.5 63t62.5 151t-62.5 150.5t-150.5 62.5t-151 -62.5t-63 -150.5q0 -7 2 -23l-241 -120q-62 57 -145 57q-88 0 -150.5 -62.5t-62.5 -150.5t62.5 -150.5 t150.5 -62.5q83 0 145 57l241 -120q-2 -16 -2 -23q0 -88 63 -150.5t151 -62.5t150.5 62.5t62.5 150.5zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M571 947q-10 25 -34 35t-49 0q-108 -44 -191 -127t-127 -191q-10 -25 0 -49t35 -34q13 -5 24 -5q42 0 60 40q34 84 98.5 148.5t148.5 98.5q25 11 35 35t0 49zM1513 1303l46 -46l-244 -243l68 -68q19 -19 19 -45.5t-19 -45.5l-64 -64q89 -161 89 -343q0 -143 -55.5 -273.5 t-150 -225t-225 -150t-273.5 -55.5t-273.5 55.5t-225 150t-150 225t-55.5 273.5t55.5 273.5t150 225t225 150t273.5 55.5q182 0 343 -89l64 64q19 19 45.5 19t45.5 -19l68 -68zM1521 1359q-10 -10 -22 -10q-13 0 -23 10l-91 90q-9 10 -9 23t9 23q10 9 23 9t23 -9l90 -91 q10 -9 10 -22.5t-10 -22.5zM1751 1129q-11 -9 -23 -9t-23 9l-90 91q-10 9 -10 22.5t10 22.5q9 10 22.5 10t22.5 -10l91 -90q9 -10 9 -23t-9 -23zM1792 1312q0 -14 -9 -23t-23 -9h-96q-14 0 -23 9t-9 23t9 23t23 9h96q14 0 23 -9t9 -23zM1600 1504v-96q0 -14 -9 -23t-23 -9 t-23 9t-9 23v96q0 14 9 23t23 9t23 -9t9 -23zM1751 1449l-91 -90q-10 -10 -22 -10q-13 0 -23 10q-10 9 -10 22.5t10 22.5l90 91q10 9 23 9t23 -9q9 -10 9 -23t-9 -23z" /> +<glyph unicode="" horiz-adv-x="1792" d="M609 720l287 208l287 -208l-109 -336h-355zM896 1536q182 0 348 -71t286 -191t191 -286t71 -348t-71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71zM1515 186q149 203 149 454v3l-102 -89l-240 224l63 323 l134 -12q-150 206 -389 282l53 -124l-287 -159l-287 159l53 124q-239 -76 -389 -282l135 12l62 -323l-240 -224l-102 89v-3q0 -251 149 -454l30 132l326 -40l139 -298l-116 -69q117 -39 240 -39t240 39l-116 69l139 298l326 40z" /> +<glyph unicode="" horiz-adv-x="1792" d="M448 224v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM256 608v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM832 224v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23 v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM640 608v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM66 768q-28 0 -47 19t-19 46v129h514v-129q0 -27 -19 -46t-46 -19h-383zM1216 224v-192q0 -14 -9 -23t-23 -9h-192 q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1024 608v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1600 224v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23 zM1408 608v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1792 1016v-13h-514v10q0 104 -382 102q-382 -1 -382 -102v-10h-514v13q0 17 8.5 43t34 64t65.5 75.5t110.5 76t160 67.5t224 47.5t293.5 18.5t293 -18.5t224 -47.5 t160.5 -67.5t110.5 -76t65.5 -75.5t34 -64t8.5 -43zM1792 608v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1792 962v-129q0 -27 -19 -46t-46 -19h-384q-27 0 -46 19t-19 46v129h514z" /> +<glyph unicode="" horiz-adv-x="1792" d="M704 1216v-768q0 -26 -19 -45t-45 -19v-576q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v512l249 873q7 23 31 23h424zM1024 1216v-704h-256v704h256zM1792 320v-512q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v576q-26 0 -45 19t-19 45v768h424q24 0 31 -23z M736 1504v-224h-352v224q0 14 9 23t23 9h288q14 0 23 -9t9 -23zM1408 1504v-224h-352v224q0 14 9 23t23 9h288q14 0 23 -9t9 -23z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1755 1083q37 -37 37 -90t-37 -91l-401 -400l150 -150l-160 -160q-163 -163 -389.5 -186.5t-411.5 100.5l-362 -362h-181v181l362 362q-124 185 -100.5 411.5t186.5 389.5l160 160l150 -150l400 401q38 37 91 37t90 -37t37 -90.5t-37 -90.5l-400 -401l234 -234l401 400 q38 37 91 37t90 -37z" /> +<glyph unicode="" horiz-adv-x="1792" d="M873 796q0 -83 -63.5 -142.5t-152.5 -59.5t-152.5 59.5t-63.5 142.5q0 84 63.5 143t152.5 59t152.5 -59t63.5 -143zM1375 796q0 -83 -63 -142.5t-153 -59.5q-89 0 -152.5 59.5t-63.5 142.5q0 84 63.5 143t152.5 59q90 0 153 -59t63 -143zM1600 616v667q0 87 -32 123.5 t-111 36.5h-1112q-83 0 -112.5 -34t-29.5 -126v-673q43 -23 88.5 -40t81 -28t81 -18.5t71 -11t70 -4t58.5 -0.5t56.5 2t44.5 2q68 1 95 -27q6 -6 10 -9q26 -25 61 -51q7 91 118 87q5 0 36.5 -1.5t43 -2t45.5 -1t53 1t54.5 4.5t61 8.5t62 13.5t67 19.5t67.5 27t72 34.5z M1763 621q-121 -149 -372 -252q84 -285 -23 -465q-66 -113 -183 -148q-104 -32 -182 15q-86 51 -82 164l-1 326v1q-8 2 -24.5 6t-23.5 5l-1 -338q4 -114 -83 -164q-79 -47 -183 -15q-117 36 -182 150q-105 180 -22 463q-251 103 -372 252q-25 37 -4 63t60 -1q3 -2 11 -7 t11 -8v694q0 72 47 123t114 51h1257q67 0 114 -51t47 -123v-694l21 15q39 27 60 1t-4 -63z" /> +<glyph unicode="" horiz-adv-x="1792" d="M896 1102v-434h-145v434h145zM1294 1102v-434h-145v434h145zM1294 342l253 254v795h-1194v-1049h326v-217l217 217h398zM1692 1536v-1013l-434 -434h-326l-217 -217h-217v217h-398v1158l109 289h1483z" /> +<glyph unicode="" d="M773 217v-127q-1 -292 -6 -305q-12 -32 -51 -40q-54 -9 -181.5 38t-162.5 89q-13 15 -17 36q-1 12 4 26q4 10 34 47t181 216q1 0 60 70q15 19 39.5 24.5t49.5 -3.5q24 -10 37.5 -29t12.5 -42zM624 468q-3 -55 -52 -70l-120 -39q-275 -88 -292 -88q-35 2 -54 36 q-12 25 -17 75q-8 76 1 166.5t30 124.5t56 32q13 0 202 -77q70 -29 115 -47l84 -34q23 -9 35.5 -30.5t11.5 -48.5zM1450 171q-7 -54 -91.5 -161t-135.5 -127q-37 -14 -63 7q-14 10 -184 287l-47 77q-14 21 -11.5 46t19.5 46q35 43 83 26q1 -1 119 -40q203 -66 242 -79.5 t47 -20.5q28 -22 22 -61zM778 803q5 -102 -54 -122q-58 -17 -114 71l-378 598q-8 35 19 62q41 43 207.5 89.5t224.5 31.5q40 -10 49 -45q3 -18 22 -305.5t24 -379.5zM1440 695q3 -39 -26 -59q-15 -10 -329 -86q-67 -15 -91 -23l1 2q-23 -6 -46 4t-37 32q-30 47 0 87 q1 1 75 102q125 171 150 204t34 39q28 19 65 2q48 -23 123 -133.5t81 -167.5v-3z" /> +<glyph unicode="" horiz-adv-x="2048" d="M1024 1024h-384v-384h384v384zM1152 384v-128h-640v128h640zM1152 1152v-640h-640v640h640zM1792 384v-128h-512v128h512zM1792 640v-128h-512v128h512zM1792 896v-128h-512v128h512zM1792 1152v-128h-512v128h512zM256 192v960h-128v-960q0 -26 19 -45t45 -19t45 19 t19 45zM1920 192v1088h-1536v-1088q0 -33 -11 -64h1483q26 0 45 19t19 45zM2048 1408v-1216q0 -80 -56 -136t-136 -56h-1664q-80 0 -136 56t-56 136v1088h256v128h1792z" /> +<glyph unicode="" horiz-adv-x="2048" d="M1024 13q-20 0 -93 73.5t-73 93.5q0 32 62.5 54t103.5 22t103.5 -22t62.5 -54q0 -20 -73 -93.5t-93 -73.5zM1294 284q-2 0 -40 25t-101.5 50t-128.5 25t-128.5 -25t-101 -50t-40.5 -25q-18 0 -93.5 75t-75.5 93q0 13 10 23q78 77 196 121t233 44t233 -44t196 -121 q10 -10 10 -23q0 -18 -75.5 -93t-93.5 -75zM1567 556q-11 0 -23 8q-136 105 -252 154.5t-268 49.5q-85 0 -170.5 -22t-149 -53t-113.5 -62t-79 -53t-31 -22q-17 0 -92 75t-75 93q0 12 10 22q132 132 320 205t380 73t380 -73t320 -205q10 -10 10 -22q0 -18 -75 -93t-92 -75z M1838 827q-11 0 -22 9q-179 157 -371.5 236.5t-420.5 79.5t-420.5 -79.5t-371.5 -236.5q-11 -9 -22 -9q-17 0 -92.5 75t-75.5 93q0 13 10 23q187 186 445 288t527 102t527 -102t445 -288q10 -10 10 -23q0 -18 -75.5 -93t-92.5 -75z" /> +<glyph unicode="" horiz-adv-x="1792" d="M384 0q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM768 0q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM384 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5 t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1152 0q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM768 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5 t37.5 90.5zM384 768q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1152 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM768 768q0 53 -37.5 90.5t-90.5 37.5 t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1536 0v384q0 52 -38 90t-90 38t-90 -38t-38 -90v-384q0 -52 38 -90t90 -38t90 38t38 90zM1152 768q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5z M1536 1088v256q0 26 -19 45t-45 19h-1280q-26 0 -45 -19t-19 -45v-256q0 -26 19 -45t45 -19h1280q26 0 45 19t19 45zM1536 768q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1664 1408v-1536q0 -52 -38 -90t-90 -38 h-1408q-52 0 -90 38t-38 90v1536q0 52 38 90t90 38h1408q52 0 90 -38t38 -90z" /> +<glyph unicode="" d="M1519 890q18 -84 -4 -204q-87 -444 -565 -444h-44q-25 0 -44 -16.5t-24 -42.5l-4 -19l-55 -346l-2 -15q-5 -26 -24.5 -42.5t-44.5 -16.5h-251q-21 0 -33 15t-9 36q9 56 26.5 168t26.5 168t27 167.5t27 167.5q5 37 43 37h131q133 -2 236 21q175 39 287 144q102 95 155 246 q24 70 35 133q1 6 2.5 7.5t3.5 1t6 -3.5q79 -59 98 -162zM1347 1172q0 -107 -46 -236q-80 -233 -302 -315q-113 -40 -252 -42q0 -1 -90 -1l-90 1q-100 0 -118 -96q-2 -8 -85 -530q-1 -10 -12 -10h-295q-22 0 -36.5 16.5t-11.5 38.5l232 1471q5 29 27.5 48t51.5 19h598 q34 0 97.5 -13t111.5 -32q107 -41 163.5 -123t56.5 -196z" /> +<glyph unicode="" horiz-adv-x="1792" d="M602 949q19 -61 31 -123.5t17 -141.5t-14 -159t-62 -145q-21 81 -67 157t-95.5 127t-99 90.5t-78.5 57.5t-33 19q-62 34 -81.5 100t14.5 128t101 81.5t129 -14.5q138 -83 238 -177zM927 1236q11 -25 20.5 -46t36.5 -100.5t42.5 -150.5t25.5 -179.5t0 -205.5t-47.5 -209.5 t-105.5 -208.5q-51 -72 -138 -72q-54 0 -98 31q-57 40 -69 109t28 127q60 85 81 195t13 199.5t-32 180.5t-39 128t-22 52q-31 63 -8.5 129.5t85.5 97.5q34 17 75 17q47 0 88.5 -25t63.5 -69zM1248 567q-17 -160 -72 -311q-17 131 -63 246q25 174 -5 361q-27 178 -94 342 q114 -90 212 -211q9 -37 15 -80q26 -179 7 -347zM1520 1440q9 -17 23.5 -49.5t43.5 -117.5t50.5 -178t34 -227.5t5 -269t-47 -300t-112.5 -323.5q-22 -48 -66 -75.5t-95 -27.5q-39 0 -74 16q-67 31 -92.5 100t4.5 136q58 126 90 257.5t37.5 239.5t-3.5 213.5t-26.5 180.5 t-38.5 138.5t-32.5 90t-15.5 32.5q-34 65 -11.5 135.5t87.5 104.5q37 20 81 20q49 0 91.5 -25.5t66.5 -70.5z" /> +<glyph unicode="" horiz-adv-x="2304" d="M1975 546h-138q14 37 66 179l3 9q4 10 10 26t9 26l12 -55zM531 611l-58 295q-11 54 -75 54h-268l-2 -13q311 -79 403 -336zM710 960l-162 -438l-17 89q-26 70 -85 129.5t-131 88.5l135 -510h175l261 641h-176zM849 318h166l104 642h-166zM1617 944q-69 27 -149 27 q-123 0 -201 -59t-79 -153q-1 -102 145 -174q48 -23 67 -41t19 -39q0 -30 -30 -46t-69 -16q-86 0 -156 33l-22 11l-23 -144q74 -34 185 -34q130 -1 208.5 59t80.5 160q0 106 -140 174q-49 25 -71 42t-22 38q0 22 24.5 38.5t70.5 16.5q70 1 124 -24l15 -8zM2042 960h-128 q-65 0 -87 -54l-246 -588h174l35 96h212q5 -22 20 -96h154zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" /> +<glyph unicode="" horiz-adv-x="2304" d="M671 603h-13q-47 0 -47 -32q0 -22 20 -22q17 0 28 15t12 39zM1066 639h62v3q1 4 0.5 6.5t-1 7t-2 8t-4.5 6.5t-7.5 5t-11.5 2q-28 0 -36 -38zM1606 603h-12q-48 0 -48 -32q0 -22 20 -22q17 0 28 15t12 39zM1925 629q0 41 -30 41q-19 0 -31 -20t-12 -51q0 -42 28 -42 q20 0 32.5 20t12.5 52zM480 770h87l-44 -262h-56l32 201l-71 -201h-39l-4 200l-34 -200h-53l44 262h81l2 -163zM733 663q0 -6 -4 -42q-16 -101 -17 -113h-47l1 22q-20 -26 -58 -26q-23 0 -37.5 16t-14.5 42q0 39 26 60.5t73 21.5q14 0 23 -1q0 3 0.5 5.5t1 4.5t0.5 3 q0 20 -36 20q-29 0 -59 -10q0 4 7 48q38 11 67 11q74 0 74 -62zM889 721l-8 -49q-22 3 -41 3q-27 0 -27 -17q0 -8 4.5 -12t21.5 -11q40 -19 40 -60q0 -72 -87 -71q-34 0 -58 6q0 2 7 49q29 -8 51 -8q32 0 32 19q0 7 -4.5 11.5t-21.5 12.5q-43 20 -43 59q0 72 84 72 q30 0 50 -4zM977 721h28l-7 -52h-29q-2 -17 -6.5 -40.5t-7 -38.5t-2.5 -18q0 -16 19 -16q8 0 16 2l-8 -47q-21 -7 -40 -7q-43 0 -45 47q0 12 8 56q3 20 25 146h55zM1180 648q0 -23 -7 -52h-111q-3 -22 10 -33t38 -11q30 0 58 14l-9 -54q-30 -8 -57 -8q-95 0 -95 95 q0 55 27.5 90.5t69.5 35.5q35 0 55.5 -21t20.5 -56zM1319 722q-13 -23 -22 -62q-22 2 -31 -24t-25 -128h-56l3 14q22 130 29 199h51l-3 -33q14 21 25.5 29.5t28.5 4.5zM1506 763l-9 -57q-28 14 -50 14q-31 0 -51 -27.5t-20 -70.5q0 -30 13.5 -47t38.5 -17q21 0 48 13 l-10 -59q-28 -8 -50 -8q-45 0 -71.5 30.5t-26.5 82.5q0 70 35.5 114.5t91.5 44.5q26 0 61 -13zM1668 663q0 -18 -4 -42q-13 -79 -17 -113h-46l1 22q-20 -26 -59 -26q-23 0 -37 16t-14 42q0 39 25.5 60.5t72.5 21.5q15 0 23 -1q2 7 2 13q0 20 -36 20q-29 0 -59 -10q0 4 8 48 q38 11 67 11q73 0 73 -62zM1809 722q-14 -24 -21 -62q-23 2 -31.5 -23t-25.5 -129h-56l3 14q19 104 29 199h52q0 -11 -4 -33q15 21 26.5 29.5t27.5 4.5zM1950 770h56l-43 -262h-53l3 19q-23 -23 -52 -23q-31 0 -49.5 24t-18.5 64q0 53 27.5 92t64.5 39q31 0 53 -29z M2061 640q0 148 -72.5 273t-198 198t-273.5 73q-181 0 -328 -110q127 -116 171 -284h-50q-44 150 -158 253q-114 -103 -158 -253h-50q44 168 171 284q-147 110 -328 110q-148 0 -273.5 -73t-198 -198t-72.5 -273t72.5 -273t198 -198t273.5 -73q181 0 328 110 q-120 111 -165 264h50q46 -138 152 -233q106 95 152 233h50q-45 -153 -165 -264q147 -110 328 -110q148 0 273.5 73t198 198t72.5 273zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" /> +<glyph unicode="" horiz-adv-x="2304" d="M313 759q0 -51 -36 -84q-29 -26 -89 -26h-17v220h17q61 0 89 -27q36 -31 36 -83zM2089 824q0 -52 -64 -52h-19v101h20q63 0 63 -49zM380 759q0 74 -50 120.5t-129 46.5h-95v-333h95q74 0 119 38q60 51 60 128zM410 593h65v333h-65v-333zM730 694q0 40 -20.5 62t-75.5 42 q-29 10 -39.5 19t-10.5 23q0 16 13.5 26.5t34.5 10.5q29 0 53 -27l34 44q-41 37 -98 37q-44 0 -74 -27.5t-30 -67.5q0 -35 18 -55.5t64 -36.5q37 -13 45 -19q19 -12 19 -34q0 -20 -14 -33.5t-36 -13.5q-48 0 -71 44l-42 -40q44 -64 115 -64q51 0 83 30.5t32 79.5zM1008 604 v77q-37 -37 -78 -37q-49 0 -80.5 32.5t-31.5 82.5q0 48 31.5 81.5t77.5 33.5q43 0 81 -38v77q-40 20 -80 20q-74 0 -125.5 -50.5t-51.5 -123.5t51 -123.5t125 -50.5q42 0 81 19zM2240 0v527q-65 -40 -144.5 -84t-237.5 -117t-329.5 -137.5t-417.5 -134.5t-504 -118h1569 q26 0 45 19t19 45zM1389 757q0 75 -53 128t-128 53t-128 -53t-53 -128t53 -128t128 -53t128 53t53 128zM1541 584l144 342h-71l-90 -224l-89 224h-71l142 -342h35zM1714 593h184v56h-119v90h115v56h-115v74h119v57h-184v-333zM2105 593h80l-105 140q76 16 76 94q0 47 -31 73 t-87 26h-97v-333h65v133h9zM2304 1274v-1268q0 -56 -38.5 -95t-93.5 -39h-2040q-55 0 -93.5 39t-38.5 95v1268q0 56 38.5 95t93.5 39h2040q55 0 93.5 -39t38.5 -95z" /> +<glyph unicode="" horiz-adv-x="2304" d="M119 854h89l-45 108zM740 328l74 79l-70 79h-163v-49h142v-55h-142v-54h159zM898 406l99 -110v217zM1186 453q0 33 -40 33h-84v-69h83q41 0 41 36zM1475 457q0 29 -42 29h-82v-61h81q43 0 43 32zM1197 923q0 29 -42 29h-82v-60h81q43 0 43 31zM1656 854h89l-44 108z M699 1009v-271h-66v212l-94 -212h-57l-94 212v-212h-132l-25 60h-135l-25 -60h-70l116 271h96l110 -257v257h106l85 -184l77 184h108zM1255 453q0 -20 -5.5 -35t-14 -25t-22.5 -16.5t-26 -10t-31.5 -4.5t-31.5 -1t-32.5 0.5t-29.5 0.5v-91h-126l-80 90l-83 -90h-256v271h260 l80 -89l82 89h207q109 0 109 -89zM964 794v-56h-217v271h217v-57h-152v-49h148v-55h-148v-54h152zM2304 235v-229q0 -55 -38.5 -94.5t-93.5 -39.5h-2040q-55 0 -93.5 39.5t-38.5 94.5v678h111l25 61h55l25 -61h218v46l19 -46h113l20 47v-47h541v99l10 1q10 0 10 -14v-86h279 v23q23 -12 55 -18t52.5 -6.5t63 0.5t51.5 1l25 61h56l25 -61h227v58l34 -58h182v378h-180v-44l-25 44h-185v-44l-23 44h-249q-69 0 -109 -22v22h-172v-22q-24 22 -73 22h-628l-43 -97l-43 97h-198v-44l-22 44h-169l-78 -179v391q0 55 38.5 94.5t93.5 39.5h2040 q55 0 93.5 -39.5t38.5 -94.5v-678h-120q-51 0 -81 -22v22h-177q-55 0 -78 -22v22h-316v-22q-31 22 -87 22h-209v-22q-23 22 -91 22h-234l-54 -58l-50 58h-349v-378h343l55 59l52 -59h211v89h21q59 0 90 13v-102h174v99h8q8 0 10 -2t2 -10v-87h529q57 0 88 24v-24h168 q60 0 95 17zM1546 469q0 -23 -12 -43t-34 -29q25 -9 34 -26t9 -46v-54h-65v45q0 33 -12 43.5t-46 10.5h-69v-99h-65v271h154q48 0 77 -15t29 -58zM1269 936q0 -24 -12.5 -44t-33.5 -29q26 -9 34.5 -25.5t8.5 -46.5v-53h-65q0 9 0.5 26.5t0 25t-3 18.5t-8.5 16t-17.5 8.5 t-29.5 3.5h-70v-98h-64v271l153 -1q49 0 78 -14.5t29 -57.5zM1798 327v-56h-216v271h216v-56h-151v-49h148v-55h-148v-54zM1372 1009v-271h-66v271h66zM2065 357q0 -86 -102 -86h-126v58h126q34 0 34 25q0 16 -17 21t-41.5 5t-49.5 3.5t-42 22.5t-17 55q0 39 26 60t66 21 h130v-57h-119q-36 0 -36 -25q0 -16 17.5 -20.5t42 -4t49 -2.5t42 -21.5t17.5 -54.5zM2304 407v-101q-24 -35 -88 -35h-125v58h125q33 0 33 25q0 13 -12.5 19t-31 5.5t-40 2t-40 8t-31 24t-12.5 48.5q0 39 26.5 60t66.5 21h129v-57h-118q-36 0 -36 -25q0 -20 29 -22t68.5 -5 t56.5 -26zM2139 1008v-270h-92l-122 203v-203h-132l-26 60h-134l-25 -60h-75q-129 0 -129 133q0 138 133 138h63v-59q-7 0 -28 1t-28.5 0.5t-23 -2t-21.5 -6.5t-14.5 -13.5t-11.5 -23t-3 -33.5q0 -38 13.5 -58t49.5 -20h29l92 213h97l109 -256v256h99l114 -188v188h66z" /> +<glyph unicode="" horiz-adv-x="2304" d="M745 630q0 -37 -25.5 -61.5t-62.5 -24.5q-29 0 -46.5 16t-17.5 44q0 37 25 62.5t62 25.5q28 0 46.5 -16.5t18.5 -45.5zM1530 779q0 -42 -22 -57t-66 -15l-32 -1l17 107q2 11 13 11h18q22 0 35 -2t25 -12.5t12 -30.5zM1881 630q0 -36 -25.5 -61t-61.5 -25q-29 0 -47 16 t-18 44q0 37 25 62.5t62 25.5q28 0 46.5 -16.5t18.5 -45.5zM513 801q0 59 -38.5 85.5t-100.5 26.5h-160q-19 0 -21 -19l-65 -408q-1 -6 3 -11t10 -5h76q20 0 22 19l18 110q1 8 7 13t15 6.5t17 1.5t19 -1t14 -1q86 0 135 48.5t49 134.5zM822 489l41 261q1 6 -3 11t-10 5h-76 q-14 0 -17 -33q-27 40 -95 40q-72 0 -122.5 -54t-50.5 -127q0 -59 34.5 -94t92.5 -35q28 0 58 12t48 32q-4 -12 -4 -21q0 -16 13 -16h69q19 0 22 19zM1269 752q0 5 -4 9.5t-9 4.5h-77q-11 0 -18 -10l-106 -156l-44 150q-5 16 -22 16h-75q-5 0 -9 -4.5t-4 -9.5q0 -2 19.5 -59 t42 -123t23.5 -70q-82 -112 -82 -120q0 -13 13 -13h77q11 0 18 10l255 368q2 2 2 7zM1649 801q0 59 -38.5 85.5t-100.5 26.5h-159q-20 0 -22 -19l-65 -408q-1 -6 3 -11t10 -5h82q12 0 16 13l18 116q1 8 7 13t15 6.5t17 1.5t19 -1t14 -1q86 0 135 48.5t49 134.5zM1958 489 l41 261q1 6 -3 11t-10 5h-76q-14 0 -17 -33q-26 40 -95 40q-72 0 -122.5 -54t-50.5 -127q0 -59 34.5 -94t92.5 -35q29 0 59 12t47 32q0 -1 -2 -9t-2 -12q0 -16 13 -16h69q19 0 22 19zM2176 898v1q0 14 -13 14h-74q-11 0 -13 -11l-65 -416l-1 -2q0 -5 4 -9.5t10 -4.5h66 q19 0 21 19zM392 764q-5 -35 -26 -46t-60 -11l-33 -1l17 107q2 11 13 11h19q40 0 58 -11.5t12 -48.5zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" /> +<glyph unicode="" horiz-adv-x="2304" d="M1597 633q0 -69 -21 -106q-19 -35 -52 -35q-23 0 -41 9v224q29 30 57 30q57 0 57 -122zM2035 669h-110q6 98 56 98q51 0 54 -98zM476 534q0 59 -33 91.5t-101 57.5q-36 13 -52 24t-16 25q0 26 38 26q58 0 124 -33l18 112q-67 32 -149 32q-77 0 -123 -38q-48 -39 -48 -109 q0 -58 32.5 -90.5t99.5 -56.5q39 -14 54.5 -25.5t15.5 -27.5q0 -31 -48 -31q-29 0 -70 12.5t-72 30.5l-18 -113q72 -41 168 -41q81 0 129 37q51 41 51 117zM771 749l19 111h-96v135l-129 -21l-18 -114l-46 -8l-17 -103h62v-219q0 -84 44 -120q38 -30 111 -30q32 0 79 11v118 q-32 -7 -44 -7q-42 0 -42 50v197h77zM1087 724v139q-15 3 -28 3q-32 0 -55.5 -16t-33.5 -46l-10 56h-131v-471h150v306q26 31 82 31q16 0 26 -2zM1124 389h150v471h-150v-471zM1746 638q0 122 -45 179q-40 52 -111 52q-64 0 -117 -56l-8 47h-132v-645l150 25v151 q36 -11 68 -11q83 0 134 56q61 65 61 202zM1278 986q0 33 -23 56t-56 23t-56 -23t-23 -56t23 -56.5t56 -23.5t56 23.5t23 56.5zM2176 629q0 113 -48 176q-50 64 -144 64q-96 0 -151.5 -66t-55.5 -180q0 -128 63 -188q55 -55 161 -55q101 0 160 40l-16 103q-57 -31 -128 -31 q-43 0 -63 19q-23 19 -28 66h248q2 14 2 52zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" /> +<glyph unicode="" horiz-adv-x="2048" d="M1558 684q61 -356 298 -556q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-180.5 74.5t-75.5 180.5zM1024 -176q16 0 16 16t-16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5zM2026 1424q8 -10 7.5 -23.5t-10.5 -22.5 l-1872 -1622q-10 -8 -23.5 -7t-21.5 11l-84 96q-8 10 -7.5 23.5t10.5 21.5l186 161q-19 32 -19 66q50 42 91 88t85 119.5t74.5 158.5t50 206t19.5 260q0 152 117 282.5t307 158.5q-8 19 -8 39q0 40 28 68t68 28t68 -28t28 -68q0 -20 -8 -39q124 -18 219 -82.5t148 -157.5 l418 363q10 8 23.5 7t21.5 -11z" /> +<glyph unicode="" horiz-adv-x="2048" d="M1040 -160q0 16 -16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5q16 0 16 16zM503 315l877 760q-42 88 -132.5 146.5t-223.5 58.5q-93 0 -169.5 -31.5t-121.5 -80.5t-69 -103t-24 -105q0 -384 -137 -645zM1856 128 q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-180.5 74.5t-75.5 180.5l149 129h757q-166 187 -227 459l111 97q61 -356 298 -556zM1942 1520l84 -96q8 -10 7.5 -23.5t-10.5 -22.5l-1872 -1622q-10 -8 -23.5 -7t-21.5 11l-84 96q-8 10 -7.5 23.5t10.5 21.5l186 161 q-19 32 -19 66q50 42 91 88t85 119.5t74.5 158.5t50 206t19.5 260q0 152 117 282.5t307 158.5q-8 19 -8 39q0 40 28 68t68 28t68 -28t28 -68q0 -20 -8 -39q124 -18 219 -82.5t148 -157.5l418 363q10 8 23.5 7t21.5 -11z" /> +<glyph unicode="" horiz-adv-x="1408" d="M512 160v704q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-704q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM768 160v704q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-704q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1024 160v704q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-704 q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM480 1152h448l-48 117q-7 9 -17 11h-317q-10 -2 -17 -11zM1408 1120v-64q0 -14 -9 -23t-23 -9h-96v-948q0 -83 -47 -143.5t-113 -60.5h-832q-66 0 -113 58.5t-47 141.5v952h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h309l70 167 q15 37 54 63t79 26h320q40 0 79 -26t54 -63l70 -167h309q14 0 23 -9t9 -23z" /> +<glyph unicode="" d="M1150 462v-109q0 -50 -36.5 -89t-94 -60.5t-118 -32.5t-117.5 -11q-205 0 -342.5 139t-137.5 346q0 203 136 339t339 136q34 0 75.5 -4.5t93 -18t92.5 -34t69 -56.5t28 -81v-109q0 -16 -16 -16h-118q-16 0 -16 16v70q0 43 -65.5 67.5t-137.5 24.5q-140 0 -228.5 -91.5 t-88.5 -237.5q0 -151 91.5 -249.5t233.5 -98.5q68 0 138 24t70 66v70q0 7 4.5 11.5t10.5 4.5h119q6 0 11 -4.5t5 -11.5zM768 1280q-130 0 -248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5 t-51 248.5t-136.5 204t-204 136.5t-248.5 51zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M972 761q0 108 -53.5 169t-147.5 61q-63 0 -124 -30.5t-110 -84.5t-79.5 -137t-30.5 -180q0 -112 53.5 -173t150.5 -61q96 0 176 66.5t122.5 166t42.5 203.5zM1536 640q0 -111 -37 -197t-98.5 -135t-131.5 -74.5t-145 -27.5q-6 0 -15.5 -0.5t-16.5 -0.5q-95 0 -142 53 q-28 33 -33 83q-52 -66 -131.5 -110t-173.5 -44q-161 0 -249.5 95.5t-88.5 269.5q0 157 66 290t179 210.5t246 77.5q87 0 155 -35.5t106 -99.5l2 19l11 56q1 6 5.5 12t9.5 6h118q5 0 13 -11q5 -5 3 -16l-120 -614q-5 -24 -5 -48q0 -39 12.5 -52t44.5 -13q28 1 57 5.5t73 24 t77 50t57 89.5t24 137q0 292 -174 466t-466 174q-130 0 -248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51q228 0 405 144q11 9 24 8t21 -12l41 -49q8 -12 7 -24q-2 -13 -12 -22q-102 -83 -227.5 -128t-258.5 -45q-156 0 -298 61 t-245 164t-164 245t-61 298t61 298t164 245t245 164t298 61q344 0 556 -212t212 -556z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1698 1442q94 -94 94 -226.5t-94 -225.5l-225 -223l104 -104q10 -10 10 -23t-10 -23l-210 -210q-10 -10 -23 -10t-23 10l-105 105l-603 -603q-37 -37 -90 -37h-203l-256 -128l-64 64l128 256v203q0 53 37 90l603 603l-105 105q-10 10 -10 23t10 23l210 210q10 10 23 10 t23 -10l104 -104l223 225q93 94 225.5 94t226.5 -94zM512 64l576 576l-192 192l-576 -576v-192h192z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1615 1536q70 0 122.5 -46.5t52.5 -116.5q0 -63 -45 -151q-332 -629 -465 -752q-97 -91 -218 -91q-126 0 -216.5 92.5t-90.5 219.5q0 128 92 212l638 579q59 54 130 54zM706 502q39 -76 106.5 -130t150.5 -76l1 -71q4 -213 -129.5 -347t-348.5 -134q-123 0 -218 46.5 t-152.5 127.5t-86.5 183t-29 220q7 -5 41 -30t62 -44.5t59 -36.5t46 -17q41 0 55 37q25 66 57.5 112.5t69.5 76t88 47.5t103 25.5t125 10.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1792 128v-384h-1792v384q45 0 85 14t59 27.5t47 37.5q30 27 51.5 38t56.5 11t55.5 -11t52.5 -38q29 -25 47 -38t58 -27t86 -14q45 0 85 14.5t58 27t48 37.5q21 19 32.5 27t31 15t43.5 7q35 0 56.5 -11t51.5 -38q28 -24 47 -37.5t59 -27.5t85 -14t85 14t59 27.5t47 37.5 q30 27 51.5 38t56.5 11q34 0 55.5 -11t51.5 -38q28 -24 47 -37.5t59 -27.5t85 -14zM1792 448v-192q-35 0 -55.5 11t-52.5 38q-29 25 -47 38t-58 27t-85 14q-46 0 -86 -14t-58 -27t-47 -38q-22 -19 -33 -27t-31 -15t-44 -7q-35 0 -56.5 11t-51.5 38q-29 25 -47 38t-58 27 t-86 14q-45 0 -85 -14.5t-58 -27t-48 -37.5q-21 -19 -32.5 -27t-31 -15t-43.5 -7q-35 0 -56.5 11t-51.5 38q-28 24 -47 37.5t-59 27.5t-85 14q-46 0 -86 -14t-58 -27t-47 -38q-30 -27 -51.5 -38t-56.5 -11v192q0 80 56 136t136 56h64v448h256v-448h256v448h256v-448h256v448 h256v-448h64q80 0 136 -56t56 -136zM512 1312q0 -77 -36 -118.5t-92 -41.5q-53 0 -90.5 37.5t-37.5 90.5q0 29 9.5 51t23.5 34t31 28t31 31.5t23.5 44.5t9.5 67q38 0 83 -74t45 -150zM1024 1312q0 -77 -36 -118.5t-92 -41.5q-53 0 -90.5 37.5t-37.5 90.5q0 29 9.5 51 t23.5 34t31 28t31 31.5t23.5 44.5t9.5 67q38 0 83 -74t45 -150zM1536 1312q0 -77 -36 -118.5t-92 -41.5q-53 0 -90.5 37.5t-37.5 90.5q0 29 9.5 51t23.5 34t31 28t31 31.5t23.5 44.5t9.5 67q38 0 83 -74t45 -150z" /> +<glyph unicode="" horiz-adv-x="2048" d="M2048 0v-128h-2048v1536h128v-1408h1920zM1664 1024l256 -896h-1664v576l448 576l576 -576z" /> +<glyph unicode="" horiz-adv-x="1792" d="M768 646l546 -546q-106 -108 -247.5 -168t-298.5 -60q-209 0 -385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103v-762zM955 640h773q0 -157 -60 -298.5t-168 -247.5zM1664 768h-768v768q209 0 385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" horiz-adv-x="2048" d="M2048 0v-128h-2048v1536h128v-1408h1920zM1920 1248v-435q0 -21 -19.5 -29.5t-35.5 7.5l-121 121l-633 -633q-10 -10 -23 -10t-23 10l-233 233l-416 -416l-192 192l585 585q10 10 23 10t23 -10l233 -233l464 464l-121 121q-16 16 -7.5 35.5t29.5 19.5h435q14 0 23 -9 t9 -23z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1292 832q0 -6 10 -41q10 -29 25 -49.5t41 -34t44 -20t55 -16.5q325 -91 325 -332q0 -146 -105.5 -242.5t-254.5 -96.5q-59 0 -111.5 18.5t-91.5 45.5t-77 74.5t-63 87.5t-53.5 103.5t-43.5 103t-39.5 106.5t-35.5 95q-32 81 -61.5 133.5t-73.5 96.5t-104 64t-142 20 q-96 0 -183 -55.5t-138 -144.5t-51 -185q0 -160 106.5 -279.5t263.5 -119.5q177 0 258 95q56 63 83 116l84 -152q-15 -34 -44 -70l1 -1q-131 -152 -388 -152q-147 0 -269.5 79t-190.5 207.5t-68 274.5q0 105 43.5 206t116 176.5t172 121.5t204.5 46q87 0 159 -19t123.5 -50 t95 -80t72.5 -99t58.5 -117t50.5 -124.5t50 -130.5t55 -127q96 -200 233 -200q81 0 138.5 48.5t57.5 128.5q0 42 -19 72t-50.5 46t-72.5 31.5t-84.5 27t-87.5 34t-81 52t-65 82t-39 122.5q-3 16 -3 33q0 110 87.5 192t198.5 78q78 -3 120.5 -14.5t90.5 -53.5h-1 q12 -11 23 -24.5t26 -36t19 -27.5l-129 -99q-26 49 -54 70v1q-23 21 -97 21q-49 0 -84 -33t-35 -83z" /> +<glyph unicode="" d="M1432 484q0 173 -234 239q-35 10 -53 16.5t-38 25t-29 46.5q0 2 -2 8.5t-3 12t-1 7.5q0 36 24.5 59.5t60.5 23.5q54 0 71 -15h-1q20 -15 39 -51l93 71q-39 54 -49 64q-33 29 -67.5 39t-85.5 10q-80 0 -142 -57.5t-62 -137.5q0 -7 2 -23q16 -96 64.5 -140t148.5 -73 q29 -8 49 -15.5t45 -21.5t38.5 -34.5t13.5 -46.5v-5q1 -58 -40.5 -93t-100.5 -35q-97 0 -167 144q-23 47 -51.5 121.5t-48 125.5t-54 110.5t-74 95.5t-103.5 60.5t-147 24.5q-101 0 -192 -56t-144 -148t-50 -192v-1q4 -108 50.5 -199t133.5 -147.5t196 -56.5q186 0 279 110 q20 27 31 51l-60 109q-42 -80 -99 -116t-146 -36q-115 0 -191 87t-76 204q0 105 82 189t186 84q112 0 170 -53.5t104 -172.5q8 -21 25.5 -68.5t28.5 -76.5t31.5 -74.5t38.5 -74t45.5 -62.5t55.5 -53.5t66 -33t80 -13.5q107 0 183 69.5t76 174.5zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="2048" d="M1152 640q0 104 -40.5 198.5t-109.5 163.5t-163.5 109.5t-198.5 40.5t-198.5 -40.5t-163.5 -109.5t-109.5 -163.5t-40.5 -198.5t40.5 -198.5t109.5 -163.5t163.5 -109.5t198.5 -40.5t198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5zM1920 640q0 104 -40.5 198.5 t-109.5 163.5t-163.5 109.5t-198.5 40.5h-386q119 -90 188.5 -224t69.5 -288t-69.5 -288t-188.5 -224h386q104 0 198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5zM2048 640q0 -130 -51 -248.5t-136.5 -204t-204 -136.5t-248.5 -51h-768q-130 0 -248.5 51t-204 136.5 t-136.5 204t-51 248.5t51 248.5t136.5 204t204 136.5t248.5 51h768q130 0 248.5 -51t204 -136.5t136.5 -204t51 -248.5z" /> +<glyph unicode="" horiz-adv-x="2048" d="M0 640q0 130 51 248.5t136.5 204t204 136.5t248.5 51h768q130 0 248.5 -51t204 -136.5t136.5 -204t51 -248.5t-51 -248.5t-136.5 -204t-204 -136.5t-248.5 -51h-768q-130 0 -248.5 51t-204 136.5t-136.5 204t-51 248.5zM1408 128q104 0 198.5 40.5t163.5 109.5 t109.5 163.5t40.5 198.5t-40.5 198.5t-109.5 163.5t-163.5 109.5t-198.5 40.5t-198.5 -40.5t-163.5 -109.5t-109.5 -163.5t-40.5 -198.5t40.5 -198.5t109.5 -163.5t163.5 -109.5t198.5 -40.5z" /> +<glyph unicode="" horiz-adv-x="2304" d="M762 384h-314q-40 0 -57.5 35t6.5 67l188 251q-65 31 -137 31q-132 0 -226 -94t-94 -226t94 -226t226 -94q115 0 203 72.5t111 183.5zM576 512h186q-18 85 -75 148zM1056 512l288 384h-480l-99 -132q105 -103 126 -252h165zM2176 448q0 132 -94 226t-226 94 q-60 0 -121 -24l174 -260q15 -23 10 -49t-27 -40q-15 -11 -36 -11q-35 0 -53 29l-174 260q-93 -95 -93 -225q0 -132 94 -226t226 -94t226 94t94 226zM2304 448q0 -185 -131.5 -316.5t-316.5 -131.5t-316.5 131.5t-131.5 316.5q0 97 39.5 183.5t109.5 149.5l-65 98l-353 -469 q-18 -26 -51 -26h-197q-23 -164 -149 -274t-294 -110q-185 0 -316.5 131.5t-131.5 316.5t131.5 316.5t316.5 131.5q114 0 215 -55l137 183h-224q-26 0 -45 19t-19 45t19 45t45 19h384v-128h435l-85 128h-222q-26 0 -45 19t-19 45t19 45t45 19h256q33 0 53 -28l267 -400 q91 44 192 44q185 0 316.5 -131.5t131.5 -316.5z" /> +<glyph unicode="" d="M384 320q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1408 320q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1362 716l-72 384q-5 23 -22.5 37.5t-40.5 14.5 h-918q-23 0 -40.5 -14.5t-22.5 -37.5l-72 -384q-5 -30 14 -53t49 -23h1062q30 0 49 23t14 53zM1136 1328q0 20 -14 34t-34 14h-640q-20 0 -34 -14t-14 -34t14 -34t34 -14h640q20 0 34 14t14 34zM1536 603v-603h-128v-128q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5 t-37.5 90.5v128h-768v-128q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5v128h-128v603q0 112 25 223l103 454q9 78 97.5 137t230 89t312.5 30t312.5 -30t230 -89t97.5 -137l105 -454q23 -102 23 -223z" /> +<glyph unicode="" horiz-adv-x="2048" d="M1463 704q0 -35 -25 -60.5t-61 -25.5h-702q-36 0 -61 25.5t-25 60.5t25 60.5t61 25.5h702q36 0 61 -25.5t25 -60.5zM1677 704q0 86 -23 170h-982q-36 0 -61 25t-25 60q0 36 25 61t61 25h908q-88 143 -235 227t-320 84q-177 0 -327.5 -87.5t-238 -237.5t-87.5 -327 q0 -86 23 -170h982q36 0 61 -25t25 -60q0 -36 -25 -61t-61 -25h-908q88 -143 235.5 -227t320.5 -84q132 0 253 51.5t208 139t139 208t52 253.5zM2048 959q0 -35 -25 -60t-61 -25h-131q17 -85 17 -170q0 -167 -65.5 -319.5t-175.5 -263t-262.5 -176t-319.5 -65.5 q-246 0 -448.5 133t-301.5 350h-189q-36 0 -61 25t-25 61q0 35 25 60t61 25h132q-17 85 -17 170q0 167 65.5 319.5t175.5 263t262.5 176t320.5 65.5q245 0 447.5 -133t301.5 -350h188q36 0 61 -25t25 -61z" /> +<glyph unicode="" horiz-adv-x="1280" d="M953 1158l-114 -328l117 -21q165 451 165 518q0 56 -38 56q-57 0 -130 -225zM654 471l33 -88q37 42 71 67l-33 5.5t-38.5 7t-32.5 8.5zM362 1367q0 -98 159 -521q18 10 49 10q15 0 75 -5l-121 351q-75 220 -123 220q-19 0 -29 -17.5t-10 -37.5zM283 608q0 -36 51.5 -119 t117.5 -153t100 -70q14 0 25.5 13t11.5 27q0 24 -32 102q-13 32 -32 72t-47.5 89t-61.5 81t-62 32q-20 0 -45.5 -27t-25.5 -47zM125 273q0 -41 25 -104q59 -145 183.5 -227t281.5 -82q227 0 382 170q152 169 152 427q0 43 -1 67t-11.5 62t-30.5 56q-56 49 -211.5 75.5 t-270.5 26.5q-37 0 -49 -11q-12 -5 -12 -35q0 -34 21.5 -60t55.5 -40t77.5 -23.5t87.5 -11.5t85 -4t70 0h23q24 0 40 -19q15 -19 19 -55q-28 -28 -96 -54q-61 -22 -93 -46q-64 -46 -108.5 -114t-44.5 -137q0 -31 18.5 -88.5t18.5 -87.5l-3 -12q-4 -12 -4 -14 q-137 10 -146 216q-8 -2 -41 -2q2 -7 2 -21q0 -53 -40.5 -89.5t-94.5 -36.5q-82 0 -166.5 78t-84.5 159q0 34 33 67q52 -64 60 -76q77 -104 133 -104q12 0 26.5 8.5t14.5 20.5q0 34 -87.5 145t-116.5 111q-43 0 -70 -44.5t-27 -90.5zM11 264q0 101 42.5 163t136.5 88 q-28 74 -28 104q0 62 61 123t122 61q29 0 70 -15q-163 462 -163 567q0 80 41 130.5t119 50.5q131 0 325 -581q6 -17 8 -23q6 16 29 79.5t43.5 118.5t54 127.5t64.5 123t70.5 86.5t76.5 36q71 0 112 -49t41 -122q0 -108 -159 -550q61 -15 100.5 -46t58.5 -78t26 -93.5 t7 -110.5q0 -150 -47 -280t-132 -225t-211 -150t-278 -55q-111 0 -223 42q-149 57 -258 191.5t-109 286.5z" /> +<glyph unicode="" horiz-adv-x="2048" d="M785 528h207q-14 -158 -98.5 -248.5t-214.5 -90.5q-162 0 -254.5 116t-92.5 316q0 194 93 311.5t233 117.5q148 0 232 -87t97 -247h-203q-5 64 -35.5 99t-81.5 35q-57 0 -88.5 -60.5t-31.5 -177.5q0 -48 5 -84t18 -69.5t40 -51.5t66 -18q95 0 109 139zM1497 528h206 q-14 -158 -98 -248.5t-214 -90.5q-162 0 -254.5 116t-92.5 316q0 194 93 311.5t233 117.5q148 0 232 -87t97 -247h-204q-4 64 -35 99t-81 35q-57 0 -88.5 -60.5t-31.5 -177.5q0 -48 5 -84t18 -69.5t39.5 -51.5t65.5 -18q49 0 76.5 38t33.5 101zM1856 647q0 207 -15.5 307 t-60.5 161q-6 8 -13.5 14t-21.5 15t-16 11q-86 63 -697 63q-625 0 -710 -63q-5 -4 -17.5 -11.5t-21 -14t-14.5 -14.5q-45 -60 -60 -159.5t-15 -308.5q0 -208 15 -307.5t60 -160.5q6 -8 15 -15t20.5 -14t17.5 -12q44 -33 239.5 -49t470.5 -16q610 0 697 65q5 4 17 11t20.5 14 t13.5 16q46 60 61 159t15 309zM2048 1408v-1536h-2048v1536h2048z" /> +<glyph unicode="" d="M992 912v-496q0 -14 -9 -23t-23 -9h-160q-14 0 -23 9t-9 23v496q0 112 -80 192t-192 80h-272v-1152q0 -14 -9 -23t-23 -9h-160q-14 0 -23 9t-9 23v1344q0 14 9 23t23 9h464q135 0 249 -66.5t180.5 -180.5t66.5 -249zM1376 1376v-880q0 -135 -66.5 -249t-180.5 -180.5 t-249 -66.5h-464q-14 0 -23 9t-9 23v960q0 14 9 23t23 9h160q14 0 23 -9t9 -23v-768h272q112 0 192 80t80 192v880q0 14 9 23t23 9h160q14 0 23 -9t9 -23z" /> +<glyph unicode="" d="M1311 694v-114q0 -24 -13.5 -38t-37.5 -14h-202q-24 0 -38 14t-14 38v114q0 24 14 38t38 14h202q24 0 37.5 -14t13.5 -38zM821 464v250q0 53 -32.5 85.5t-85.5 32.5h-133q-68 0 -96 -52q-28 52 -96 52h-130q-53 0 -85.5 -32.5t-32.5 -85.5v-250q0 -22 21 -22h55 q22 0 22 22v230q0 24 13.5 38t38.5 14h94q24 0 38 -14t14 -38v-230q0 -22 21 -22h54q22 0 22 22v230q0 24 14 38t38 14h97q24 0 37.5 -14t13.5 -38v-230q0 -22 22 -22h55q21 0 21 22zM1410 560v154q0 53 -33 85.5t-86 32.5h-264q-53 0 -86 -32.5t-33 -85.5v-410 q0 -21 22 -21h55q21 0 21 21v180q31 -42 94 -42h191q53 0 86 32.5t33 85.5zM1536 1176v-1072q0 -96 -68 -164t-164 -68h-1072q-96 0 -164 68t-68 164v1072q0 96 68 164t164 68h1072q96 0 164 -68t68 -164z" /> +<glyph unicode="" d="M915 450h-294l147 551zM1001 128h311l-324 1024h-440l-324 -1024h311l383 314zM1536 1120v-960q0 -118 -85 -203t-203 -85h-960q-118 0 -203 85t-85 203v960q0 118 85 203t203 85h960q118 0 203 -85t85 -203z" /> +<glyph unicode="" horiz-adv-x="2048" d="M2048 641q0 -21 -13 -36.5t-33 -19.5l-205 -356q3 -9 3 -18q0 -20 -12.5 -35.5t-32.5 -19.5l-193 -337q3 -8 3 -16q0 -23 -16.5 -40t-40.5 -17q-25 0 -41 18h-400q-17 -20 -43 -20t-43 20h-399q-17 -20 -43 -20q-23 0 -40 16.5t-17 40.5q0 8 4 20l-193 335 q-20 4 -32.5 19.5t-12.5 35.5q0 9 3 18l-206 356q-20 5 -32.5 20.5t-12.5 35.5q0 21 13.5 36.5t33.5 19.5l199 344q0 1 -0.5 3t-0.5 3q0 36 34 51l209 363q-4 10 -4 18q0 24 17 40.5t40 16.5q26 0 44 -21h396q16 21 43 21t43 -21h398q18 21 44 21q23 0 40 -16.5t17 -40.5 q0 -6 -4 -18l207 -358q23 -1 39 -17.5t16 -38.5q0 -13 -7 -27l187 -324q19 -4 31.5 -19.5t12.5 -35.5zM1063 -158h389l-342 354h-143l-342 -354h360q18 16 39 16t39 -16zM112 654q1 -4 1 -13q0 -10 -2 -15l208 -360q2 0 4.5 -1t5.5 -2.5l5 -2.5l188 199v347l-187 194 q-13 -8 -29 -10zM986 1438h-388l190 -200l554 200h-280q-16 -16 -38 -16t-38 16zM1689 226q1 6 5 11l-64 68l-17 -79h76zM1583 226l22 105l-252 266l-296 -307l63 -64h463zM1495 -142l16 28l65 310h-427l333 -343q8 4 13 5zM578 -158h5l342 354h-373v-335l4 -6q14 -5 22 -13 zM552 226h402l64 66l-309 321l-157 -166v-221zM359 226h163v189l-168 -177q4 -8 5 -12zM358 1051q0 -1 0.5 -2t0.5 -2q0 -16 -8 -29l171 -177v269zM552 1121v-311l153 -157l297 314l-223 236zM556 1425l-4 -8v-264l205 74l-191 201q-6 -2 -10 -3zM1447 1438h-16l-621 -224 l213 -225zM1023 946l-297 -315l311 -319l296 307zM688 634l-136 141v-284zM1038 270l-42 -44h85zM1374 618l238 -251l132 624l-3 5l-1 1zM1718 1018q-8 13 -8 29v2l-216 376q-5 1 -13 5l-437 -463l310 -327zM522 1142v223l-163 -282zM522 196h-163l163 -283v283zM1607 196 l-48 -227l130 227h-82zM1729 266l207 361q-2 10 -2 14q0 1 3 16l-171 296l-129 -612l77 -82q5 3 15 7z" /> +<glyph unicode="" d="M0 856q0 131 91.5 226.5t222.5 95.5h742l352 358v-1470q0 -132 -91.5 -227t-222.5 -95h-780q-131 0 -222.5 95t-91.5 227v790zM1232 102l-176 180v425q0 46 -32 79t-78 33h-484q-46 0 -78 -33t-32 -79v-492q0 -46 32.5 -79.5t77.5 -33.5h770z" /> +<glyph unicode="" d="M934 1386q-317 -121 -556 -362.5t-358 -560.5q-20 89 -20 176q0 208 102.5 384.5t278.5 279t384 102.5q82 0 169 -19zM1203 1267q93 -65 164 -155q-389 -113 -674.5 -400.5t-396.5 -676.5q-93 72 -155 162q112 386 395 671t667 399zM470 -67q115 356 379.5 622t619.5 384 q40 -92 54 -195q-292 -120 -516 -345t-343 -518q-103 14 -194 52zM1536 -125q-193 50 -367 115q-135 -84 -290 -107q109 205 274 370.5t369 275.5q-21 -152 -101 -284q65 -175 115 -370z" /> +<glyph unicode="" horiz-adv-x="2048" d="M1893 1144l155 -1272q-131 0 -257 57q-200 91 -393 91q-226 0 -374 -148q-148 148 -374 148q-193 0 -393 -91q-128 -57 -252 -57h-5l155 1272q224 127 482 127q233 0 387 -106q154 106 387 106q258 0 482 -127zM1398 157q129 0 232 -28.5t260 -93.5l-124 1021 q-171 78 -368 78q-224 0 -374 -141q-150 141 -374 141q-197 0 -368 -78l-124 -1021q105 43 165.5 65t148.5 39.5t178 17.5q202 0 374 -108q172 108 374 108zM1438 191l-55 907q-211 -4 -359 -155q-152 155 -374 155q-176 0 -336 -66l-114 -941q124 51 228.5 76t221.5 25 q209 0 374 -102q172 107 374 102z" /> +<glyph unicode="" horiz-adv-x="2048" d="M1500 165v733q0 21 -15 36t-35 15h-93q-20 0 -35 -15t-15 -36v-733q0 -20 15 -35t35 -15h93q20 0 35 15t15 35zM1216 165v531q0 20 -15 35t-35 15h-101q-20 0 -35 -15t-15 -35v-531q0 -20 15 -35t35 -15h101q20 0 35 15t15 35zM924 165v429q0 20 -15 35t-35 15h-101 q-20 0 -35 -15t-15 -35v-429q0 -20 15 -35t35 -15h101q20 0 35 15t15 35zM632 165v362q0 20 -15 35t-35 15h-101q-20 0 -35 -15t-15 -35v-362q0 -20 15 -35t35 -15h101q20 0 35 15t15 35zM2048 311q0 -166 -118 -284t-284 -118h-1244q-166 0 -284 118t-118 284 q0 116 63 214.5t168 148.5q-10 34 -10 73q0 113 80.5 193.5t193.5 80.5q102 0 180 -67q45 183 194 300t338 117q149 0 275 -73.5t199.5 -199.5t73.5 -275q0 -66 -14 -122q135 -33 221 -142.5t86 -247.5z" /> +<glyph unicode="" d="M0 1536h1536v-1392l-776 -338l-760 338v1392zM1436 209v926h-1336v-926l661 -294zM1436 1235v201h-1336v-201h1336zM181 937v-115h-37v115h37zM181 789v-115h-37v115h37zM181 641v-115h-37v115h37zM181 493v-115h-37v115h37zM181 345v-115h-37v115h37zM207 202l15 34 l105 -47l-15 -33zM343 142l15 34l105 -46l-15 -34zM478 82l15 34l105 -46l-15 -34zM614 23l15 33l104 -46l-15 -34zM797 10l105 46l15 -33l-105 -47zM932 70l105 46l15 -34l-105 -46zM1068 130l105 46l15 -34l-105 -46zM1203 189l105 47l15 -34l-105 -46zM259 1389v-36h-114 v36h114zM421 1389v-36h-115v36h115zM583 1389v-36h-115v36h115zM744 1389v-36h-114v36h114zM906 1389v-36h-114v36h114zM1068 1389v-36h-115v36h115zM1230 1389v-36h-115v36h115zM1391 1389v-36h-114v36h114zM181 1049v-79h-37v115h115v-36h-78zM421 1085v-36h-115v36h115z M583 1085v-36h-115v36h115zM744 1085v-36h-114v36h114zM906 1085v-36h-114v36h114zM1068 1085v-36h-115v36h115zM1230 1085v-36h-115v36h115zM1355 970v79h-78v36h115v-115h-37zM1355 822v115h37v-115h-37zM1355 674v115h37v-115h-37zM1355 526v115h37v-115h-37zM1355 378 v115h37v-115h-37zM1355 230v115h37v-115h-37zM760 265q-129 0 -221 91.5t-92 221.5q0 129 92 221t221 92q130 0 221.5 -92t91.5 -221q0 -130 -91.5 -221.5t-221.5 -91.5zM595 646q0 -36 19.5 -56.5t49.5 -25t64 -7t64 -2t49.5 -9t19.5 -30.5q0 -49 -112 -49q-97 0 -123 51 h-3l-31 -63q67 -42 162 -42q29 0 56.5 5t55.5 16t45.5 33t17.5 53q0 46 -27.5 69.5t-67.5 27t-79.5 3t-67 5t-27.5 25.5q0 21 20.5 33t40.5 15t41 3q34 0 70.5 -11t51.5 -34h3l30 58q-3 1 -21 8.5t-22.5 9t-19.5 7t-22 7t-20 4.5t-24 4t-23 1q-29 0 -56.5 -5t-54 -16.5 t-43 -34t-16.5 -53.5z" /> +<glyph unicode="" horiz-adv-x="2048" d="M863 504q0 112 -79.5 191.5t-191.5 79.5t-191 -79.5t-79 -191.5t79 -191t191 -79t191.5 79t79.5 191zM1726 505q0 112 -79 191t-191 79t-191.5 -79t-79.5 -191q0 -113 79.5 -192t191.5 -79t191 79.5t79 191.5zM2048 1314v-1348q0 -44 -31.5 -75.5t-76.5 -31.5h-1832 q-45 0 -76.5 31.5t-31.5 75.5v1348q0 44 31.5 75.5t76.5 31.5h431q44 0 76 -31.5t32 -75.5v-161h754v161q0 44 32 75.5t76 31.5h431q45 0 76.5 -31.5t31.5 -75.5z" /> +<glyph unicode="" horiz-adv-x="2048" d="M1430 953zM1690 749q148 0 253 -98.5t105 -244.5q0 -157 -109 -261.5t-267 -104.5q-85 0 -162 27.5t-138 73.5t-118 106t-109 126.5t-103.5 132.5t-108.5 126t-117 106t-136 73.5t-159 27.5q-154 0 -251.5 -91.5t-97.5 -244.5q0 -157 104 -250t263 -93q100 0 208 37.5 t193 98.5q5 4 21 18.5t30 24t22 9.5q14 0 24.5 -10.5t10.5 -24.5q0 -24 -60 -77q-101 -88 -234.5 -142t-260.5 -54q-133 0 -245.5 58t-180 165t-67.5 241q0 205 141.5 341t347.5 136q120 0 226.5 -43.5t185.5 -113t151.5 -153t139 -167.5t133.5 -153.5t149.5 -113 t172.5 -43.5q102 0 168.5 61.5t66.5 162.5q0 95 -64.5 159t-159.5 64q-30 0 -81.5 -18.5t-68.5 -18.5q-20 0 -35.5 15t-15.5 35q0 18 8.5 57t8.5 59q0 159 -107.5 263t-266.5 104q-58 0 -111.5 -18.5t-84 -40.5t-55.5 -40.5t-33 -18.5q-15 0 -25.5 10.5t-10.5 25.5 q0 19 25 46q59 67 147 103.5t182 36.5q191 0 318 -125.5t127 -315.5q0 -37 -4 -66q57 15 115 15z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1216 832q0 26 -19 45t-45 19h-128v128q0 26 -19 45t-45 19t-45 -19t-19 -45v-128h-128q-26 0 -45 -19t-19 -45t19 -45t45 -19h128v-128q0 -26 19 -45t45 -19t45 19t19 45v128h128q26 0 45 19t19 45zM640 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5 t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1536 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1664 1088v-512q0 -24 -16 -42.5t-41 -21.5l-1044 -122q1 -7 4.5 -21.5t6 -26.5t2.5 -22q0 -16 -24 -64h920 q26 0 45 -19t19 -45t-19 -45t-45 -19h-1024q-26 0 -45 19t-19 45q0 14 11 39.5t29.5 59.5t20.5 38l-177 823h-204q-26 0 -45 19t-19 45t19 45t45 19h256q16 0 28.5 -6.5t20 -15.5t13 -24.5t7.5 -26.5t5.5 -29.5t4.5 -25.5h1201q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1664" d="M1280 832q0 26 -19 45t-45 19t-45 -19l-147 -146v293q0 26 -19 45t-45 19t-45 -19t-19 -45v-293l-147 146q-19 19 -45 19t-45 -19t-19 -45t19 -45l256 -256q19 -19 45 -19t45 19l256 256q19 19 19 45zM640 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5 t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1536 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1664 1088v-512q0 -24 -16 -42.5t-41 -21.5l-1044 -122q1 -7 4.5 -21.5t6 -26.5t2.5 -22q0 -16 -24 -64h920 q26 0 45 -19t19 -45t-19 -45t-45 -19h-1024q-26 0 -45 19t-19 45q0 14 11 39.5t29.5 59.5t20.5 38l-177 823h-204q-26 0 -45 19t-19 45t19 45t45 19h256q16 0 28.5 -6.5t20 -15.5t13 -24.5t7.5 -26.5t5.5 -29.5t4.5 -25.5h1201q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="2048" d="M212 768l623 -665l-300 665h-323zM1024 -4l349 772h-698zM538 896l204 384h-262l-288 -384h346zM1213 103l623 665h-323zM683 896h682l-204 384h-274zM1510 896h346l-288 384h-262zM1651 1382l384 -512q14 -18 13 -41.5t-17 -40.5l-960 -1024q-18 -20 -47 -20t-47 20 l-960 1024q-16 17 -17 40.5t13 41.5l384 512q18 26 51 26h1152q33 0 51 -26z" /> +<glyph unicode="" horiz-adv-x="2048" d="M1811 -19q19 19 45 19t45 -19l128 -128l-90 -90l-83 83l-83 -83q-18 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83 q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-128 128l90 90l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83 q19 19 45 19t45 -19l83 -83zM237 19q-19 -19 -45 -19t-45 19l-128 128l90 90l83 -82l83 82q19 19 45 19t45 -19l83 -82l64 64v293l-210 314q-17 26 -7 56.5t40 40.5l177 58v299h128v128h256v128h256v-128h256v-128h128v-299l177 -58q30 -10 40 -40.5t-7 -56.5l-210 -314 v-293l19 18q19 19 45 19t45 -19l83 -82l83 82q19 19 45 19t45 -19l128 -128l-90 -90l-83 83l-83 -83q-18 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83 q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83zM640 1152v-128l384 128l384 -128v128h-128v128h-512v-128h-128z" /> +<glyph unicode="" d="M576 0l96 448l-96 128l-128 64zM832 0l128 640l-128 -64l-96 -128zM992 1010q-2 4 -4 6q-10 8 -96 8q-70 0 -167 -19q-7 -2 -21 -2t-21 2q-97 19 -167 19q-86 0 -96 -8q-2 -2 -4 -6q2 -18 4 -27q2 -3 7.5 -6.5t7.5 -10.5q2 -4 7.5 -20.5t7 -20.5t7.5 -17t8.5 -17t9 -14 t12 -13.5t14 -9.5t17.5 -8t20.5 -4t24.5 -2q36 0 59 12.5t32.5 30t14.5 34.5t11.5 29.5t17.5 12.5h12q11 0 17.5 -12.5t11.5 -29.5t14.5 -34.5t32.5 -30t59 -12.5q13 0 24.5 2t20.5 4t17.5 8t14 9.5t12 13.5t9 14t8.5 17t7.5 17t7 20.5t7.5 20.5q2 7 7.5 10.5t7.5 6.5 q2 9 4 27zM1408 131q0 -121 -73 -190t-194 -69h-874q-121 0 -194 69t-73 190q0 61 4.5 118t19 125.5t37.5 123.5t63.5 103.5t93.5 74.5l-90 220h214q-22 64 -22 128q0 12 2 32q-194 40 -194 96q0 57 210 99q17 62 51.5 134t70.5 114q32 37 76 37q30 0 84 -31t84 -31t84 31 t84 31q44 0 76 -37q36 -42 70.5 -114t51.5 -134q210 -42 210 -99q0 -56 -194 -96q7 -81 -20 -160h214l-82 -225q63 -33 107.5 -96.5t65.5 -143.5t29 -151.5t8 -148.5z" /> +<glyph unicode="" horiz-adv-x="2304" d="M2301 500q12 -103 -22 -198.5t-99 -163.5t-158.5 -106t-196.5 -31q-161 11 -279.5 125t-134.5 274q-12 111 27.5 210.5t118.5 170.5l-71 107q-96 -80 -151 -194t-55 -244q0 -27 -18.5 -46.5t-45.5 -19.5h-256h-69q-23 -164 -149 -274t-294 -110q-185 0 -316.5 131.5 t-131.5 316.5t131.5 316.5t316.5 131.5q76 0 152 -27l24 45q-123 110 -304 110h-64q-26 0 -45 19t-19 45t19 45t45 19h128q78 0 145 -13.5t116.5 -38.5t71.5 -39.5t51 -36.5h512h115l-85 128h-222q-30 0 -49 22.5t-14 52.5q4 23 23 38t43 15h253q33 0 53 -28l70 -105 l114 114q19 19 46 19h101q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-179l115 -172q131 63 275 36q143 -26 244 -134.5t118 -253.5zM448 128q115 0 203 72.5t111 183.5h-314q-35 0 -55 31q-18 32 -1 63l147 277q-47 13 -91 13q-132 0 -226 -94t-94 -226t94 -226 t226 -94zM1856 128q132 0 226 94t94 226t-94 226t-226 94q-60 0 -121 -24l174 -260q15 -23 10 -49t-27 -40q-15 -11 -36 -11q-35 0 -53 29l-174 260q-93 -95 -93 -225q0 -132 94 -226t226 -94z" /> +<glyph unicode="" d="M1408 0q0 -63 -61.5 -113.5t-164 -81t-225 -46t-253.5 -15.5t-253.5 15.5t-225 46t-164 81t-61.5 113.5q0 49 33 88.5t91 66.5t118 44.5t131 29.5q26 5 48 -10.5t26 -41.5q5 -26 -10.5 -48t-41.5 -26q-58 -10 -106 -23.5t-76.5 -25.5t-48.5 -23.5t-27.5 -19.5t-8.5 -12 q3 -11 27 -26.5t73 -33t114 -32.5t160.5 -25t201.5 -10t201.5 10t160.5 25t114 33t73 33.5t27 27.5q-1 4 -8.5 11t-27.5 19t-48.5 23.5t-76.5 25t-106 23.5q-26 4 -41.5 26t-10.5 48q4 26 26 41.5t48 10.5q71 -12 131 -29.5t118 -44.5t91 -66.5t33 -88.5zM1024 896v-384 q0 -26 -19 -45t-45 -19h-64v-384q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v384h-64q-26 0 -45 19t-19 45v384q0 53 37.5 90.5t90.5 37.5h384q53 0 90.5 -37.5t37.5 -90.5zM928 1280q0 -93 -65.5 -158.5t-158.5 -65.5t-158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5 t158.5 -65.5t65.5 -158.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1280 512h305q-5 -6 -10 -10.5t-9 -7.5l-3 -4l-623 -600q-18 -18 -44 -18t-44 18l-624 602q-5 2 -21 20h369q22 0 39.5 13.5t22.5 34.5l70 281l190 -667q6 -20 23 -33t39 -13q21 0 38 13t23 33l146 485l56 -112q18 -35 57 -35zM1792 940q0 -145 -103 -300h-369l-111 221 q-8 17 -25.5 27t-36.5 8q-45 -5 -56 -46l-129 -430l-196 686q-6 20 -23.5 33t-39.5 13t-39 -13.5t-22 -34.5l-116 -464h-423q-103 155 -103 300q0 220 127 344t351 124q62 0 126.5 -21.5t120 -58t95.5 -68.5t76 -68q36 36 76 68t95.5 68.5t120 58t126.5 21.5q224 0 351 -124 t127 -344z" /> +<glyph unicode="" horiz-adv-x="1280" d="M1152 960q0 -221 -147.5 -384.5t-364.5 -187.5v-260h224q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-224v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-224q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h224v260q-150 16 -271.5 103t-186 224t-52.5 292 q11 134 80.5 249t182 188t245.5 88q170 19 319 -54t236 -212t87 -306zM128 960q0 -185 131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5z" /> +<glyph unicode="" d="M1472 1408q26 0 45 -19t19 -45v-416q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v262l-382 -383q126 -156 126 -359q0 -117 -45.5 -223.5t-123 -184t-184 -123t-223.5 -45.5t-223.5 45.5t-184 123t-123 184t-45.5 223.5t45.5 223.5t123 184t184 123t223.5 45.5 q203 0 359 -126l382 382h-261q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h416zM576 0q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" /> +<glyph unicode="" horiz-adv-x="1280" d="M830 1220q145 -72 233.5 -210.5t88.5 -305.5q0 -221 -147.5 -384.5t-364.5 -187.5v-132h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96v-96q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v96h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96v132q-217 24 -364.5 187.5 t-147.5 384.5q0 167 88.5 305.5t233.5 210.5q-165 96 -228 273q-6 16 3.5 29.5t26.5 13.5h69q21 0 29 -20q44 -106 140 -171t214 -65t214 65t140 171q8 20 37 20h61q17 0 26.5 -13.5t3.5 -29.5q-63 -177 -228 -273zM576 256q185 0 316.5 131.5t131.5 316.5t-131.5 316.5 t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" /> +<glyph unicode="" d="M1024 1504q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q126 -158 126 -359q0 -221 -147.5 -384.5t-364.5 -187.5v-132h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96v-96q0 -14 -9 -23t-23 -9h-64 q-14 0 -23 9t-9 23v96h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96v132q-149 16 -270.5 103t-186.5 223.5t-53 291.5q16 204 160 353.5t347 172.5q118 14 228 -19t198 -103l255 254h-134q-14 0 -23 9t-9 23v64zM576 256q185 0 316.5 131.5t131.5 316.5t-131.5 316.5 t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1280 1504q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q126 -158 126 -359q0 -221 -147.5 -384.5t-364.5 -187.5v-132h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96v-96q0 -14 -9 -23t-23 -9h-64 q-14 0 -23 9t-9 23v96h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96v132q-217 24 -364.5 187.5t-147.5 384.5q0 201 126 359l-52 53l-101 -111q-9 -10 -22 -10.5t-23 7.5l-48 44q-10 8 -10.5 21.5t8.5 23.5l105 115l-111 112v-134q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9 t-9 23v288q0 26 19 45t45 19h288q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-133l106 -107l86 94q9 10 22 10.5t23 -7.5l48 -44q10 -8 10.5 -21.5t-8.5 -23.5l-90 -99l57 -56q158 126 359 126t359 -126l255 254h-134q-14 0 -23 9t-9 23v64zM832 256q185 0 316.5 131.5 t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1790 1007q12 -155 -52.5 -292t-186 -224t-271.5 -103v-260h224q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-224v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-512v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-224q-14 0 -23 9t-9 23v64q0 14 9 23 t23 9h224v260q-150 16 -271.5 103t-186 224t-52.5 292q17 206 164.5 356.5t352.5 169.5q206 21 377 -94q171 115 377 94q205 -19 352.5 -169.5t164.5 -356.5zM896 647q128 131 128 313t-128 313q-128 -131 -128 -313t128 -313zM576 512q115 0 218 57q-154 165 -154 391 q0 224 154 391q-103 57 -218 57q-185 0 -316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5zM1152 128v260q-137 15 -256 94q-119 -79 -256 -94v-260h512zM1216 512q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5q-115 0 -218 -57q154 -167 154 -391 q0 -226 -154 -391q103 -57 218 -57z" /> +<glyph unicode="" horiz-adv-x="1920" d="M1536 1120q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q76 -95 107.5 -214t9.5 -247q-31 -182 -166 -312t-318 -156q-210 -29 -384.5 80t-241.5 300q-117 6 -221 57.5t-177.5 133t-113.5 192.5t-32 230 q9 135 78 252t182 191.5t248 89.5q118 14 227.5 -19t198.5 -103l255 254h-134q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q59 -74 93 -169q182 -9 328 -124l255 254h-134q-14 0 -23 9 t-9 23v64zM1024 704q0 20 -4 58q-162 -25 -271 -150t-109 -292q0 -20 4 -58q162 25 271 150t109 292zM128 704q0 -168 111 -294t276 -149q-3 29 -3 59q0 210 135 369.5t338 196.5q-53 120 -163.5 193t-245.5 73q-185 0 -316.5 -131.5t-131.5 -316.5zM1088 -128 q185 0 316.5 131.5t131.5 316.5q0 168 -111 294t-276 149q3 -29 3 -59q0 -210 -135 -369.5t-338 -196.5q53 -120 163.5 -193t245.5 -73z" /> +<glyph unicode="" horiz-adv-x="2048" d="M1664 1504q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q76 -95 107.5 -214t9.5 -247q-32 -180 -164.5 -310t-313.5 -157q-223 -34 -409 90q-117 -78 -256 -93v-132h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23 t-23 -9h-96v-96q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v96h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96v132q-155 17 -279.5 109.5t-187 237.5t-39.5 307q25 187 159.5 322.5t320.5 164.5q224 34 410 -90q146 97 320 97q201 0 359 -126l255 254h-134q-14 0 -23 9 t-9 23v64zM896 391q128 131 128 313t-128 313q-128 -131 -128 -313t128 -313zM128 704q0 -185 131.5 -316.5t316.5 -131.5q117 0 218 57q-154 167 -154 391t154 391q-101 57 -218 57q-185 0 -316.5 -131.5t-131.5 -316.5zM1216 256q185 0 316.5 131.5t131.5 316.5 t-131.5 316.5t-316.5 131.5q-117 0 -218 -57q154 -167 154 -391t-154 -391q101 -57 218 -57z" /> +<glyph unicode="" d="M1472 1408q26 0 45 -19t19 -45v-416q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v262l-213 -214l140 -140q9 -10 9 -23t-9 -22l-46 -46q-9 -9 -22 -9t-23 9l-140 141l-78 -79q126 -156 126 -359q0 -117 -45.5 -223.5t-123 -184t-184 -123t-223.5 -45.5t-223.5 45.5 t-184 123t-123 184t-45.5 223.5t45.5 223.5t123 184t184 123t223.5 45.5q203 0 359 -126l78 78l-172 172q-9 10 -9 23t9 22l46 46q9 9 22 9t23 -9l172 -172l213 213h-261q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h416zM576 0q185 0 316.5 131.5t131.5 316.5t-131.5 316.5 t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" /> +<glyph unicode="" horiz-adv-x="1280" d="M640 892q217 -24 364.5 -187.5t147.5 -384.5q0 -167 -87 -306t-236 -212t-319 -54q-133 15 -245.5 88t-182 188t-80.5 249q-12 155 52.5 292t186 224t271.5 103v132h-160q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h160v165l-92 -92q-10 -9 -23 -9t-22 9l-46 46q-9 9 -9 22 t9 23l202 201q19 19 45 19t45 -19l202 -201q9 -10 9 -23t-9 -22l-46 -46q-9 -9 -22 -9t-23 9l-92 92v-165h160q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-160v-132zM576 -128q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5 t131.5 -316.5t316.5 -131.5z" /> +<glyph unicode="" horiz-adv-x="2048" d="M1901 621q19 -19 19 -45t-19 -45l-294 -294q-9 -10 -22.5 -10t-22.5 10l-45 45q-10 9 -10 22.5t10 22.5l185 185h-294v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-132q-24 -217 -187.5 -364.5t-384.5 -147.5q-167 0 -306 87t-212 236t-54 319q15 133 88 245.5 t188 182t249 80.5q155 12 292 -52.5t224 -186t103 -271.5h132v224q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-224h294l-185 185q-10 9 -10 22.5t10 22.5l45 45q9 10 22.5 10t22.5 -10zM576 128q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5 t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" /> +<glyph unicode="" horiz-adv-x="1280" d="M1152 960q0 -221 -147.5 -384.5t-364.5 -187.5v-612q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v612q-217 24 -364.5 187.5t-147.5 384.5q0 117 45.5 223.5t123 184t184 123t223.5 45.5t223.5 -45.5t184 -123t123 -184t45.5 -223.5zM576 512q185 0 316.5 131.5 t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" /> +<glyph unicode="" horiz-adv-x="1280" d="M1024 576q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5zM1152 576q0 -117 -45.5 -223.5t-123 -184t-184 -123t-223.5 -45.5t-223.5 45.5t-184 123t-123 184t-45.5 223.5t45.5 223.5t123 184t184 123 t223.5 45.5t223.5 -45.5t184 -123t123 -184t45.5 -223.5z" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" d="M1451 1408q35 0 60 -25t25 -60v-1366q0 -35 -25 -60t-60 -25h-391v595h199l30 232h-229v148q0 56 23.5 84t91.5 28l122 1v207q-63 9 -178 9q-136 0 -217.5 -80t-81.5 -226v-171h-200v-232h200v-595h-735q-35 0 -60 25t-25 60v1366q0 35 25 60t60 25h1366z" /> +<glyph unicode="" horiz-adv-x="1280" d="M0 939q0 108 37.5 203.5t103.5 166.5t152 123t185 78t202 26q158 0 294 -66.5t221 -193.5t85 -287q0 -96 -19 -188t-60 -177t-100 -149.5t-145 -103t-189 -38.5q-68 0 -135 32t-96 88q-10 -39 -28 -112.5t-23.5 -95t-20.5 -71t-26 -71t-32 -62.5t-46 -77.5t-62 -86.5 l-14 -5l-9 10q-15 157 -15 188q0 92 21.5 206.5t66.5 287.5t52 203q-32 65 -32 169q0 83 52 156t132 73q61 0 95 -40.5t34 -102.5q0 -66 -44 -191t-44 -187q0 -63 45 -104.5t109 -41.5q55 0 102 25t78.5 68t56 95t38 110.5t20 111t6.5 99.5q0 173 -109.5 269.5t-285.5 96.5 q-200 0 -334 -129.5t-134 -328.5q0 -44 12.5 -85t27 -65t27 -45.5t12.5 -30.5q0 -28 -15 -73t-37 -45q-2 0 -17 3q-51 15 -90.5 56t-61 94.5t-32.5 108t-11 106.5z" /> +<glyph unicode="" d="M985 562q13 0 97.5 -44t89.5 -53q2 -5 2 -15q0 -33 -17 -76q-16 -39 -71 -65.5t-102 -26.5q-57 0 -190 62q-98 45 -170 118t-148 185q-72 107 -71 194v8q3 91 74 158q24 22 52 22q6 0 18 -1.5t19 -1.5q19 0 26.5 -6.5t15.5 -27.5q8 -20 33 -88t25 -75q0 -21 -34.5 -57.5 t-34.5 -46.5q0 -7 5 -15q34 -73 102 -137q56 -53 151 -101q12 -7 22 -7q15 0 54 48.5t52 48.5zM782 32q127 0 243.5 50t200.5 134t134 200.5t50 243.5t-50 243.5t-134 200.5t-200.5 134t-243.5 50t-243.5 -50t-200.5 -134t-134 -200.5t-50 -243.5q0 -203 120 -368l-79 -233 l242 77q158 -104 345 -104zM782 1414q153 0 292.5 -60t240.5 -161t161 -240.5t60 -292.5t-60 -292.5t-161 -240.5t-240.5 -161t-292.5 -60q-195 0 -365 94l-417 -134l136 405q-108 178 -108 389q0 153 60 292.5t161 240.5t240.5 161t292.5 60z" /> +<glyph unicode="" horiz-adv-x="1792" d="M128 128h1024v128h-1024v-128zM128 640h1024v128h-1024v-128zM1696 192q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM128 1152h1024v128h-1024v-128zM1696 704q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM1696 1216 q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM1792 384v-384h-1792v384h1792zM1792 896v-384h-1792v384h1792zM1792 1408v-384h-1792v384h1792z" /> +<glyph unicode="" horiz-adv-x="2048" d="M704 640q-159 0 -271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5t-112.5 -271.5t-271.5 -112.5zM1664 512h352q13 0 22.5 -9.5t9.5 -22.5v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-352v-352q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5 t-9.5 22.5v352h-352q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h352v352q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5t9.5 -22.5v-352zM928 288q0 -52 38 -90t90 -38h256v-238q-68 -50 -171 -50h-874q-121 0 -194 69t-73 190q0 53 3.5 103.5t14 109t26.5 108.5 t43 97.5t62 81t85.5 53.5t111.5 20q19 0 39 -17q79 -61 154.5 -91.5t164.5 -30.5t164.5 30.5t154.5 91.5q20 17 39 17q132 0 217 -96h-223q-52 0 -90 -38t-38 -90v-192z" /> +<glyph unicode="" horiz-adv-x="2048" d="M704 640q-159 0 -271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5t-112.5 -271.5t-271.5 -112.5zM1781 320l249 -249q9 -9 9 -23q0 -13 -9 -22l-136 -136q-9 -9 -22 -9q-14 0 -23 9l-249 249l-249 -249q-9 -9 -23 -9q-13 0 -22 9l-136 136 q-9 9 -9 22q0 14 9 23l249 249l-249 249q-9 9 -9 23q0 13 9 22l136 136q9 9 22 9q14 0 23 -9l249 -249l249 249q9 9 23 9q13 0 22 -9l136 -136q9 -9 9 -22q0 -14 -9 -23zM1283 320l-181 -181q-37 -37 -37 -91q0 -53 37 -90l83 -83q-21 -3 -44 -3h-874q-121 0 -194 69 t-73 190q0 53 3.5 103.5t14 109t26.5 108.5t43 97.5t62 81t85.5 53.5t111.5 20q19 0 39 -17q154 -122 319 -122t319 122q20 17 39 17q28 0 57 -6q-28 -27 -41 -50t-13 -56q0 -54 37 -91z" /> +<glyph unicode="" horiz-adv-x="2048" d="M256 512h1728q26 0 45 -19t19 -45v-448h-256v256h-1536v-256h-256v1216q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-704zM832 832q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM2048 576v64q0 159 -112.5 271.5t-271.5 112.5h-704 q-26 0 -45 -19t-19 -45v-384h1152z" /> +<glyph unicode="" d="M1536 1536l-192 -448h192v-192h-274l-55 -128h329v-192h-411l-357 -832l-357 832h-411v192h329l-55 128h-274v192h192l-192 448h256l323 -768h378l323 768h256zM768 320l108 256h-216z" /> +<glyph unicode="" d="M1088 1536q185 0 316.5 -93.5t131.5 -226.5v-896q0 -130 -125.5 -222t-305.5 -97l213 -202q16 -15 8 -35t-30 -20h-1056q-22 0 -30 20t8 35l213 202q-180 5 -305.5 97t-125.5 222v896q0 133 131.5 226.5t316.5 93.5h640zM768 192q80 0 136 56t56 136t-56 136t-136 56 t-136 -56t-56 -136t56 -136t136 -56zM1344 768v512h-1152v-512h1152z" /> +<glyph unicode="" d="M1088 1536q185 0 316.5 -93.5t131.5 -226.5v-896q0 -130 -125.5 -222t-305.5 -97l213 -202q16 -15 8 -35t-30 -20h-1056q-22 0 -30 20t8 35l213 202q-180 5 -305.5 97t-125.5 222v896q0 133 131.5 226.5t316.5 93.5h640zM288 224q66 0 113 47t47 113t-47 113t-113 47 t-113 -47t-47 -113t47 -113t113 -47zM704 768v512h-544v-512h544zM1248 224q66 0 113 47t47 113t-47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47zM1408 768v512h-576v-512h576z" /> +<glyph unicode="" horiz-adv-x="1792" d="M597 1115v-1173q0 -25 -12.5 -42.5t-36.5 -17.5q-17 0 -33 8l-465 233q-21 10 -35.5 33.5t-14.5 46.5v1140q0 20 10 34t29 14q14 0 44 -15l511 -256q3 -3 3 -5zM661 1014l534 -866l-534 266v600zM1792 996v-1054q0 -25 -14 -40.5t-38 -15.5t-47 13l-441 220zM1789 1116 q0 -3 -256.5 -419.5t-300.5 -487.5l-390 634l324 527q17 28 52 28q14 0 26 -6l541 -270q4 -2 4 -6z" /> +<glyph unicode="" d="M809 532l266 499h-112l-157 -312q-24 -48 -44 -92l-42 92l-155 312h-120l263 -493v-324h101v318zM1536 1408v-1536h-1536v1536h1536z" /> +<glyph unicode="" horiz-adv-x="2296" d="M478 -139q-8 -16 -27 -34.5t-37 -25.5q-25 -9 -51.5 3.5t-28.5 31.5q-1 22 40 55t68 38q23 4 34 -21.5t2 -46.5zM1819 -139q7 -16 26 -34.5t38 -25.5q25 -9 51.5 3.5t27.5 31.5q2 22 -39.5 55t-68.5 38q-22 4 -33 -21.5t-2 -46.5zM1867 -30q13 -27 56.5 -59.5t77.5 -41.5 q45 -13 82 4.5t37 50.5q0 46 -67.5 100.5t-115.5 59.5q-40 5 -63.5 -37.5t-6.5 -76.5zM428 -30q-13 -27 -56 -59.5t-77 -41.5q-45 -13 -82 4.5t-37 50.5q0 46 67.5 100.5t115.5 59.5q40 5 63 -37.5t6 -76.5zM1158 1094h1q-41 0 -76 -15q27 -8 44 -30.5t17 -49.5 q0 -35 -27 -60t-65 -25q-52 0 -80 43q-5 -23 -5 -42q0 -74 56 -126.5t135 -52.5q80 0 136 52.5t56 126.5t-56 126.5t-136 52.5zM1462 1312q-99 109 -220.5 131.5t-245.5 -44.5q27 60 82.5 96.5t118 39.5t121.5 -17t99.5 -74.5t44.5 -131.5zM2212 73q8 -11 -11 -42 q7 -23 7 -40q1 -56 -44.5 -112.5t-109.5 -91.5t-118 -37q-48 -2 -92 21.5t-66 65.5q-687 -25 -1259 0q-23 -41 -66.5 -65t-92.5 -22q-86 3 -179.5 80.5t-92.5 160.5q2 22 7 40q-19 31 -11 42q6 10 31 1q14 22 41 51q-7 29 2 38q11 10 39 -4q29 20 59 34q0 29 13 37 q23 12 51 -16q35 5 61 -2q18 -4 38 -19v73q-11 0 -18 2q-53 10 -97 44.5t-55 87.5q-9 38 0 81q15 62 93 95q2 17 19 35.5t36 23.5t33 -7.5t19 -30.5h13q46 -5 60 -23q3 -3 5 -7q10 1 30.5 3.5t30.5 3.5q-15 11 -30 17q-23 40 -91 43q0 6 1 10q-62 2 -118.5 18.5t-84.5 47.5 q-32 36 -42.5 92t-2.5 112q16 126 90 179q23 16 52 4.5t32 -40.5q0 -1 1.5 -14t2.5 -21t3 -20t5.5 -19t8.5 -10q27 -14 76 -12q48 46 98 74q-40 4 -162 -14l47 46q61 58 163 111q145 73 282 86q-20 8 -41 15.5t-47 14t-42.5 10.5t-47.5 11t-43 10q595 126 904 -139 q98 -84 158 -222q85 -10 121 9h1q5 3 8.5 10t5.5 19t3 19.5t3 21.5l1 14q3 28 32 40t52 -5q73 -52 91 -178q7 -57 -3.5 -113t-42.5 -91q-28 -32 -83.5 -48.5t-115.5 -18.5v-10q-71 -2 -95 -43q-14 -5 -31 -17q11 -1 32 -3.5t30 -3.5q1 4 5 8q16 18 60 23h13q5 18 19 30t33 8 t36 -23t19 -36q79 -32 93 -95q9 -40 1 -81q-12 -53 -56 -88t-97 -44q-10 -2 -17 -2q0 -49 -1 -73q20 15 38 19q26 7 61 2q28 28 51 16q14 -9 14 -37q33 -16 59 -34q27 13 38 4q10 -10 2 -38q28 -30 41 -51q23 8 31 -1zM1937 1025q0 -29 -9 -54q82 -32 112 -132 q4 37 -9.5 98.5t-41.5 90.5q-20 19 -36 17t-16 -20zM1859 925q35 -42 47.5 -108.5t-0.5 -124.5q67 13 97 45q13 14 18 28q-3 64 -31 114.5t-79 66.5q-15 -15 -52 -21zM1822 921q-30 0 -44 1q42 -115 53 -239q21 0 43 3q16 68 1 135t-53 100zM258 839q30 100 112 132 q-9 25 -9 54q0 18 -16.5 20t-35.5 -17q-28 -29 -41.5 -90.5t-9.5 -98.5zM294 737q29 -31 97 -45q-13 58 -0.5 124.5t47.5 108.5v0q-37 6 -52 21q-51 -16 -78.5 -66t-31.5 -115q9 -17 18 -28zM471 683q14 124 73 235q-19 -4 -55 -18l-45 -19v1q-46 -89 -20 -196q25 -3 47 -3z M1434 644q8 -38 16.5 -108.5t11.5 -89.5q3 -18 9.5 -21.5t23.5 4.5q40 20 62 85.5t23 125.5q-24 2 -146 4zM1152 1285q-116 0 -199 -82.5t-83 -198.5q0 -117 83 -199.5t199 -82.5t199 82.5t83 199.5q0 116 -83 198.5t-199 82.5zM1380 646q-106 2 -211 0v1q-1 -27 2.5 -86 t13.5 -66q29 -14 93.5 -14.5t95.5 10.5q9 3 11 39t-0.5 69.5t-4.5 46.5zM1112 447q8 4 9.5 48t-0.5 88t-4 63v1q-212 -3 -214 -3q-4 -20 -7 -62t0 -83t14 -46q34 -15 101 -16t101 10zM718 636q-16 -59 4.5 -118.5t77.5 -84.5q15 -8 24 -5t12 21q3 16 8 90t10 103 q-69 -2 -136 -6zM591 510q3 -23 -34 -36q132 -141 271.5 -240t305.5 -154q172 49 310.5 146t293.5 250q-33 13 -30 34l3 9v1v-1q-17 2 -50 5.5t-48 4.5q-26 -90 -82 -132q-51 -38 -82 1q-5 6 -9 14q-7 13 -17 62q-2 -5 -5 -9t-7.5 -7t-8 -5.5t-9.5 -4l-10 -2.5t-12 -2 l-12 -1.5t-13.5 -1t-13.5 -0.5q-106 -9 -163 11q-4 -17 -10 -26.5t-21 -15t-23 -7t-36 -3.5q-2 0 -3 -0.5t-3 -0.5h-3q-179 -17 -203 40q-2 -63 -56 -54q-47 8 -91 54q-12 13 -20 26q-17 29 -26 65q-58 -6 -87 -10q1 -2 4 -10zM507 -118q3 14 3 30q-17 71 -51 130t-73 70 q-41 12 -101.5 -14.5t-104.5 -80t-39 -107.5q35 -53 100 -93t119 -42q51 -2 94 28t53 79zM510 53q23 -63 27 -119q195 113 392 174q-98 52 -180.5 120t-179.5 165q-6 -4 -29 -13q0 -2 -1 -5t-1 -4q31 -18 22 -37q-12 -23 -56 -34q-10 -13 -29 -24h-1q-2 -83 1 -150 q19 -34 35 -73zM579 -113q532 -21 1145 0q-254 147 -428 196q-76 -35 -156 -57q-8 -3 -16 0q-65 21 -129 49q-208 -60 -416 -188h-1v-1q1 0 1 1zM1763 -67q4 54 28 120q14 38 33 71l-1 -1q3 77 3 153q-15 8 -30 25q-42 9 -56 33q-9 20 22 38q-2 4 -2 9q-16 4 -28 12 q-204 -190 -383 -284q198 -59 414 -176zM2155 -90q5 54 -39 107.5t-104 80t-102 14.5q-38 -11 -72.5 -70.5t-51.5 -129.5q0 -16 3 -30q10 -49 53 -79t94 -28q54 2 119 42t100 93z" /> +<glyph unicode="" horiz-adv-x="2304" d="M1524 -25q0 -68 -48 -116t-116 -48t-116.5 48t-48.5 116t48.5 116.5t116.5 48.5t116 -48.5t48 -116.5zM775 -25q0 -68 -48.5 -116t-116.5 -48t-116 48t-48 116t48 116.5t116 48.5t116.5 -48.5t48.5 -116.5zM0 1469q57 -60 110.5 -104.5t121 -82t136 -63t166 -45.5 t200 -31.5t250 -18.5t304 -9.5t372.5 -2.5q139 0 244.5 -5t181 -16.5t124 -27.5t71 -39.5t24 -51.5t-19.5 -64t-56.5 -76.5t-89.5 -91t-116 -104.5t-139 -119q-185 -157 -286 -247q29 51 76.5 109t94 105.5t94.5 98.5t83 91.5t54 80.5t13 70t-45.5 55.5t-116.5 41t-204 23.5 t-304 5q-168 -2 -314 6t-256 23t-204.5 41t-159.5 51.5t-122.5 62.5t-91.5 66.5t-68 71.5t-50.5 69.5t-40 68t-36.5 59.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M896 1472q-169 0 -323 -66t-265.5 -177.5t-177.5 -265.5t-66 -323t66 -323t177.5 -265.5t265.5 -177.5t323 -66t323 66t265.5 177.5t177.5 265.5t66 323t-66 323t-177.5 265.5t-265.5 177.5t-323 66zM896 1536q182 0 348 -71t286 -191t191 -286t71 -348t-71 -348 t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71zM496 704q16 0 16 -16v-480q0 -16 -16 -16h-32q-16 0 -16 16v480q0 16 16 16h32zM896 640q53 0 90.5 -37.5t37.5 -90.5q0 -35 -17.5 -64t-46.5 -46v-114q0 -14 -9 -23 t-23 -9h-64q-14 0 -23 9t-9 23v114q-29 17 -46.5 46t-17.5 64q0 53 37.5 90.5t90.5 37.5zM896 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM544 928v-96 q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v96q0 93 65.5 158.5t158.5 65.5t158.5 -65.5t65.5 -158.5v-96q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v96q0 146 -103 249t-249 103t-249 -103t-103 -249zM1408 192v512q0 26 -19 45t-45 19h-896q-26 0 -45 -19t-19 -45v-512 q0 -26 19 -45t45 -19h896q26 0 45 19t19 45z" /> +<glyph unicode="" horiz-adv-x="2304" d="M1920 1024v-768h-1664v768h1664zM2048 448h128v384h-128v288q0 14 -9 23t-23 9h-1856q-14 0 -23 -9t-9 -23v-960q0 -14 9 -23t23 -9h1856q14 0 23 9t9 23v288zM2304 832v-384q0 -53 -37.5 -90.5t-90.5 -37.5v-160q0 -66 -47 -113t-113 -47h-1856q-66 0 -113 47t-47 113 v960q0 66 47 113t113 47h1856q66 0 113 -47t47 -113v-160q53 0 90.5 -37.5t37.5 -90.5z" /> +<glyph unicode="" horiz-adv-x="2304" d="M256 256v768h1280v-768h-1280zM2176 960q53 0 90.5 -37.5t37.5 -90.5v-384q0 -53 -37.5 -90.5t-90.5 -37.5v-160q0 -66 -47 -113t-113 -47h-1856q-66 0 -113 47t-47 113v960q0 66 47 113t113 47h1856q66 0 113 -47t47 -113v-160zM2176 448v384h-128v288q0 14 -9 23t-23 9 h-1856q-14 0 -23 -9t-9 -23v-960q0 -14 9 -23t23 -9h1856q14 0 23 9t9 23v288h128z" /> +<glyph unicode="" horiz-adv-x="2304" d="M256 256v768h896v-768h-896zM2176 960q53 0 90.5 -37.5t37.5 -90.5v-384q0 -53 -37.5 -90.5t-90.5 -37.5v-160q0 -66 -47 -113t-113 -47h-1856q-66 0 -113 47t-47 113v960q0 66 47 113t113 47h1856q66 0 113 -47t47 -113v-160zM2176 448v384h-128v288q0 14 -9 23t-23 9 h-1856q-14 0 -23 -9t-9 -23v-960q0 -14 9 -23t23 -9h1856q14 0 23 9t9 23v288h128z" /> +<glyph unicode="" horiz-adv-x="2304" d="M256 256v768h512v-768h-512zM2176 960q53 0 90.5 -37.5t37.5 -90.5v-384q0 -53 -37.5 -90.5t-90.5 -37.5v-160q0 -66 -47 -113t-113 -47h-1856q-66 0 -113 47t-47 113v960q0 66 47 113t113 47h1856q66 0 113 -47t47 -113v-160zM2176 448v384h-128v288q0 14 -9 23t-23 9 h-1856q-14 0 -23 -9t-9 -23v-960q0 -14 9 -23t23 -9h1856q14 0 23 9t9 23v288h128z" /> +<glyph unicode="" horiz-adv-x="2304" d="M2176 960q53 0 90.5 -37.5t37.5 -90.5v-384q0 -53 -37.5 -90.5t-90.5 -37.5v-160q0 -66 -47 -113t-113 -47h-1856q-66 0 -113 47t-47 113v960q0 66 47 113t113 47h1856q66 0 113 -47t47 -113v-160zM2176 448v384h-128v288q0 14 -9 23t-23 9h-1856q-14 0 -23 -9t-9 -23 v-960q0 -14 9 -23t23 -9h1856q14 0 23 9t9 23v288h128z" /> +<glyph unicode="" horiz-adv-x="1280" d="M1133 493q31 -30 14 -69q-17 -40 -59 -40h-382l201 -476q10 -25 0 -49t-34 -35l-177 -75q-25 -10 -49 0t-35 34l-191 452l-312 -312q-19 -19 -45 -19q-12 0 -24 5q-40 17 -40 59v1504q0 42 40 59q12 5 24 5q27 0 45 -19z" /> +<glyph unicode="" horiz-adv-x="1024" d="M832 1408q-320 0 -320 -224v-416h128v-128h-128v-544q0 -224 320 -224h64v-128h-64q-272 0 -384 146q-112 -146 -384 -146h-64v128h64q320 0 320 224v544h-128v128h128v416q0 224 -320 224h-64v128h64q272 0 384 -146q112 146 384 146h64v-128h-64z" /> +<glyph unicode="" horiz-adv-x="2048" d="M2048 1152h-128v-1024h128v-384h-384v128h-1280v-128h-384v384h128v1024h-128v384h384v-128h1280v128h384v-384zM1792 1408v-128h128v128h-128zM128 1408v-128h128v128h-128zM256 -128v128h-128v-128h128zM1664 0v128h128v1024h-128v128h-1280v-128h-128v-1024h128v-128 h1280zM1920 -128v128h-128v-128h128zM1280 896h384v-768h-896v256h-384v768h896v-256zM512 512h640v512h-640v-512zM1536 256v512h-256v-384h-384v-128h640z" /> +<glyph unicode="" horiz-adv-x="2304" d="M2304 768h-128v-640h128v-384h-384v128h-896v-128h-384v384h128v128h-384v-128h-384v384h128v640h-128v384h384v-128h896v128h384v-384h-128v-128h384v128h384v-384zM2048 1024v-128h128v128h-128zM1408 1408v-128h128v128h-128zM128 1408v-128h128v128h-128zM256 256 v128h-128v-128h128zM1536 384h-128v-128h128v128zM384 384h896v128h128v640h-128v128h-896v-128h-128v-640h128v-128zM896 -128v128h-128v-128h128zM2176 -128v128h-128v-128h128zM2048 128v640h-128v128h-384v-384h128v-384h-384v128h-384v-128h128v-128h896v128h128z" /> +<glyph unicode="" d="M1024 288v-416h-928q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h1344q40 0 68 -28t28 -68v-928h-416q-40 0 -68 -28t-28 -68zM1152 256h381q-15 -82 -65 -132l-184 -184q-50 -50 -132 -65v381z" /> +<glyph unicode="" d="M1400 256h-248v-248q29 10 41 22l185 185q12 12 22 41zM1120 384h288v896h-1280v-1280h896v288q0 40 28 68t68 28zM1536 1312v-1024q0 -40 -20 -88t-48 -76l-184 -184q-28 -28 -76 -48t-88 -20h-1024q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h1344q40 0 68 -28t28 -68 z" /> +<glyph unicode="" horiz-adv-x="2304" d="M1951 538q0 -26 -15.5 -44.5t-38.5 -23.5q-8 -2 -18 -2h-153v140h153q10 0 18 -2q23 -5 38.5 -23.5t15.5 -44.5zM1933 751q0 -25 -15 -42t-38 -21q-3 -1 -15 -1h-139v129h139q3 0 8.5 -0.5t6.5 -0.5q23 -4 38 -21.5t15 -42.5zM728 587v308h-228v-308q0 -58 -38 -94.5 t-105 -36.5q-108 0 -229 59v-112q53 -15 121 -23t109 -9l42 -1q328 0 328 217zM1442 403v113q-99 -52 -200 -59q-108 -8 -169 41t-61 142t61 142t169 41q101 -7 200 -58v112q-48 12 -100 19.5t-80 9.5l-28 2q-127 6 -218.5 -14t-140.5 -60t-71 -88t-22 -106t22 -106t71 -88 t140.5 -60t218.5 -14q101 4 208 31zM2176 518q0 54 -43 88.5t-109 39.5v3q57 8 89 41.5t32 79.5q0 55 -41 88t-107 36q-3 0 -12 0.5t-14 0.5h-455v-510h491q74 0 121.5 36.5t47.5 96.5zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90 t90 38h2048q52 0 90 -38t38 -90z" /> +<glyph unicode="" horiz-adv-x="2304" d="M858 295v693q-106 -41 -172 -135.5t-66 -211.5t66 -211.5t172 -134.5zM1362 641q0 117 -66 211.5t-172 135.5v-694q106 41 172 135.5t66 211.5zM1577 641q0 -159 -78.5 -294t-213.5 -213.5t-294 -78.5q-119 0 -227.5 46.5t-187 125t-125 187t-46.5 227.5q0 159 78.5 294 t213.5 213.5t294 78.5t294 -78.5t213.5 -213.5t78.5 -294zM1960 634q0 139 -55.5 261.5t-147.5 205.5t-213.5 131t-252.5 48h-301q-176 0 -323.5 -81t-235 -230t-87.5 -335q0 -171 87 -317.5t236 -231.5t323 -85h301q129 0 251.5 50.5t214.5 135t147.5 202.5t55.5 246z M2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1664 -96v1088q0 13 -9.5 22.5t-22.5 9.5h-1088q-13 0 -22.5 -9.5t-9.5 -22.5v-1088q0 -13 9.5 -22.5t22.5 -9.5h1088q13 0 22.5 9.5t9.5 22.5zM1792 992v-1088q0 -66 -47 -113t-113 -47h-1088q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1088q66 0 113 -47t47 -113 zM1408 1376v-160h-128v160q0 13 -9.5 22.5t-22.5 9.5h-1088q-13 0 -22.5 -9.5t-9.5 -22.5v-1088q0 -13 9.5 -22.5t22.5 -9.5h160v-128h-160q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1088q66 0 113 -47t47 -113z" /> +<glyph unicode="" horiz-adv-x="2304" d="M1728 1088l-384 -704h768zM448 1088l-384 -704h768zM1269 1280q-14 -40 -45.5 -71.5t-71.5 -45.5v-1291h608q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-1344q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h608v1291q-40 14 -71.5 45.5t-45.5 71.5h-491q-14 0 -23 9t-9 23v64 q0 14 9 23t23 9h491q21 57 70 92.5t111 35.5t111 -35.5t70 -92.5h491q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-491zM1088 1264q33 0 56.5 23.5t23.5 56.5t-23.5 56.5t-56.5 23.5t-56.5 -23.5t-23.5 -56.5t23.5 -56.5t56.5 -23.5zM2176 384q0 -73 -46.5 -131t-117.5 -91 t-144.5 -49.5t-139.5 -16.5t-139.5 16.5t-144.5 49.5t-117.5 91t-46.5 131q0 11 35 81t92 174.5t107 195.5t102 184t56 100q18 33 56 33t56 -33q4 -7 56 -100t102 -184t107 -195.5t92 -174.5t35 -81zM896 384q0 -73 -46.5 -131t-117.5 -91t-144.5 -49.5t-139.5 -16.5 t-139.5 16.5t-144.5 49.5t-117.5 91t-46.5 131q0 11 35 81t92 174.5t107 195.5t102 184t56 100q18 33 56 33t56 -33q4 -7 56 -100t102 -184t107 -195.5t92 -174.5t35 -81z" /> +<glyph unicode="" d="M1408 1408q0 -261 -106.5 -461.5t-266.5 -306.5q160 -106 266.5 -306.5t106.5 -461.5h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-1472q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96q0 261 106.5 461.5t266.5 306.5q-160 106 -266.5 306.5t-106.5 461.5h-96q-14 0 -23 9 t-9 23v64q0 14 9 23t23 9h1472q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96zM874 700q77 29 149 92.5t129.5 152.5t92.5 210t35 253h-1024q0 -132 35 -253t92.5 -210t129.5 -152.5t149 -92.5q19 -7 30.5 -23.5t11.5 -36.5t-11.5 -36.5t-30.5 -23.5q-77 -29 -149 -92.5 t-129.5 -152.5t-92.5 -210t-35 -253h1024q0 132 -35 253t-92.5 210t-129.5 152.5t-149 92.5q-19 7 -30.5 23.5t-11.5 36.5t11.5 36.5t30.5 23.5z" /> +<glyph unicode="" d="M1408 1408q0 -261 -106.5 -461.5t-266.5 -306.5q160 -106 266.5 -306.5t106.5 -461.5h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-1472q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96q0 261 106.5 461.5t266.5 306.5q-160 106 -266.5 306.5t-106.5 461.5h-96q-14 0 -23 9 t-9 23v64q0 14 9 23t23 9h1472q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96zM1280 1408h-1024q0 -66 9 -128h1006q9 61 9 128zM1280 -128q0 130 -34 249.5t-90.5 208t-126.5 152t-146 94.5h-230q-76 -31 -146 -94.5t-126.5 -152t-90.5 -208t-34 -249.5h1024z" /> +<glyph unicode="" d="M1408 1408q0 -261 -106.5 -461.5t-266.5 -306.5q160 -106 266.5 -306.5t106.5 -461.5h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-1472q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96q0 261 106.5 461.5t266.5 306.5q-160 106 -266.5 306.5t-106.5 461.5h-96q-14 0 -23 9 t-9 23v64q0 14 9 23t23 9h1472q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96zM1280 1408h-1024q0 -206 85 -384h854q85 178 85 384zM1223 192q-54 141 -145.5 241.5t-194.5 142.5h-230q-103 -42 -194.5 -142.5t-145.5 -241.5h910z" /> +<glyph unicode="" d="M1408 1408q0 -261 -106.5 -461.5t-266.5 -306.5q160 -106 266.5 -306.5t106.5 -461.5h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-1472q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96q0 261 106.5 461.5t266.5 306.5q-160 106 -266.5 306.5t-106.5 461.5h-96q-14 0 -23 9 t-9 23v64q0 14 9 23t23 9h1472q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96zM874 700q77 29 149 92.5t129.5 152.5t92.5 210t35 253h-1024q0 -132 35 -253t92.5 -210t129.5 -152.5t149 -92.5q19 -7 30.5 -23.5t11.5 -36.5t-11.5 -36.5t-30.5 -23.5q-137 -51 -244 -196 h700q-107 145 -244 196q-19 7 -30.5 23.5t-11.5 36.5t11.5 36.5t30.5 23.5z" /> +<glyph unicode="" d="M1504 -64q14 0 23 -9t9 -23v-128q0 -14 -9 -23t-23 -9h-1472q-14 0 -23 9t-9 23v128q0 14 9 23t23 9h1472zM130 0q3 55 16 107t30 95t46 87t53.5 76t64.5 69.5t66 60t70.5 55t66.5 47.5t65 43q-43 28 -65 43t-66.5 47.5t-70.5 55t-66 60t-64.5 69.5t-53.5 76t-46 87 t-30 95t-16 107h1276q-3 -55 -16 -107t-30 -95t-46 -87t-53.5 -76t-64.5 -69.5t-66 -60t-70.5 -55t-66.5 -47.5t-65 -43q43 -28 65 -43t66.5 -47.5t70.5 -55t66 -60t64.5 -69.5t53.5 -76t46 -87t30 -95t16 -107h-1276zM1504 1536q14 0 23 -9t9 -23v-128q0 -14 -9 -23t-23 -9 h-1472q-14 0 -23 9t-9 23v128q0 14 9 23t23 9h1472z" /> +<glyph unicode="" d="M768 1152q-53 0 -90.5 -37.5t-37.5 -90.5v-128h-32v93q0 48 -32 81.5t-80 33.5q-46 0 -79 -33t-33 -79v-429l-32 30v172q0 48 -32 81.5t-80 33.5q-46 0 -79 -33t-33 -79v-224q0 -47 35 -82l310 -296q39 -39 39 -102q0 -26 19 -45t45 -19h640q26 0 45 19t19 45v25 q0 41 10 77l108 436q10 36 10 77v246q0 48 -32 81.5t-80 33.5q-46 0 -79 -33t-33 -79v-32h-32v125q0 40 -25 72.5t-64 40.5q-14 2 -23 2q-46 0 -79 -33t-33 -79v-128h-32v122q0 51 -32.5 89.5t-82.5 43.5q-5 1 -13 1zM768 1280q84 0 149 -50q57 34 123 34q59 0 111 -27 t86 -76q27 7 59 7q100 0 170 -71.5t70 -171.5v-246q0 -51 -13 -108l-109 -436q-6 -24 -6 -71q0 -80 -56 -136t-136 -56h-640q-84 0 -138 58.5t-54 142.5l-308 296q-76 73 -76 175v224q0 99 70.5 169.5t169.5 70.5q11 0 16 -1q6 95 75.5 160t164.5 65q52 0 98 -21 q72 69 174 69z" /> +<glyph unicode="" horiz-adv-x="1792" d="M880 1408q-46 0 -79 -33t-33 -79v-656h-32v528q0 46 -33 79t-79 33t-79 -33t-33 -79v-528v-256l-154 205q-38 51 -102 51q-53 0 -90.5 -37.5t-37.5 -90.5q0 -43 26 -77l384 -512q38 -51 102 -51h688q34 0 61 22t34 56l76 405q5 32 5 59v498q0 46 -33 79t-79 33t-79 -33 t-33 -79v-272h-32v528q0 46 -33 79t-79 33t-79 -33t-33 -79v-528h-32v656q0 46 -33 79t-79 33zM880 1536q68 0 125.5 -35.5t88.5 -96.5q19 4 42 4q99 0 169.5 -70.5t70.5 -169.5v-17q105 6 180.5 -64t75.5 -175v-498q0 -40 -8 -83l-76 -404q-14 -79 -76.5 -131t-143.5 -52 h-688q-60 0 -114.5 27.5t-90.5 74.5l-384 512q-51 68 -51 154q0 106 75 181t181 75q78 0 128 -34v434q0 99 70.5 169.5t169.5 70.5q23 0 42 -4q31 61 88.5 96.5t125.5 35.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1073 -128h-177q-163 0 -226 141q-23 49 -23 102v5q-62 30 -98.5 88.5t-36.5 127.5q0 38 5 48h-261q-106 0 -181 75t-75 181t75 181t181 75h113l-44 17q-74 28 -119.5 93.5t-45.5 145.5q0 106 75 181t181 75q46 0 91 -17l628 -239h401q106 0 181 -75t75 -181v-668 q0 -88 -54 -157.5t-140 -90.5l-339 -85q-92 -23 -186 -23zM1024 583l-155 -71l-163 -74q-30 -14 -48 -41.5t-18 -60.5q0 -46 33 -79t79 -33q26 0 46 10l338 154q-49 10 -80.5 50t-31.5 90v55zM1344 272q0 46 -33 79t-79 33q-26 0 -46 -10l-290 -132q-28 -13 -37 -17 t-30.5 -17t-29.5 -23.5t-16 -29t-8 -40.5q0 -50 31.5 -82t81.5 -32q20 0 38 9l352 160q30 14 48 41.5t18 60.5zM1112 1024l-650 248q-24 8 -46 8q-53 0 -90.5 -37.5t-37.5 -90.5q0 -40 22.5 -73t59.5 -47l526 -200v-64h-640q-53 0 -90.5 -37.5t-37.5 -90.5t37.5 -90.5 t90.5 -37.5h535l233 106v198q0 63 46 106l111 102h-69zM1073 0q82 0 155 19l339 85q43 11 70 45.5t27 78.5v668q0 53 -37.5 90.5t-90.5 37.5h-308l-136 -126q-36 -33 -36 -82v-296q0 -46 33 -77t79 -31t79 35t33 81v208h32v-208q0 -70 -57 -114q52 -8 86.5 -48.5t34.5 -93.5 q0 -42 -23 -78t-61 -53l-310 -141h91z" /> +<glyph unicode="" horiz-adv-x="2048" d="M1151 1536q61 0 116 -28t91 -77l572 -781q118 -159 118 -359v-355q0 -80 -56 -136t-136 -56h-384q-80 0 -136 56t-56 136v177l-286 143h-546q-80 0 -136 56t-56 136v32q0 119 84.5 203.5t203.5 84.5h420l42 128h-686q-100 0 -173.5 67.5t-81.5 166.5q-65 79 -65 182v32 q0 80 56 136t136 56h959zM1920 -64v355q0 157 -93 284l-573 781q-39 52 -103 52h-959q-26 0 -45 -19t-19 -45q0 -32 1.5 -49.5t9.5 -40.5t25 -43q10 31 35.5 50t56.5 19h832v-32h-832q-26 0 -45 -19t-19 -45q0 -44 3 -58q8 -44 44 -73t81 -29h640h91q40 0 68 -28t28 -68 q0 -15 -5 -30l-64 -192q-10 -29 -35 -47.5t-56 -18.5h-443q-66 0 -113 -47t-47 -113v-32q0 -26 19 -45t45 -19h561q16 0 29 -7l317 -158q24 -13 38.5 -36t14.5 -50v-197q0 -26 19 -45t45 -19h384q26 0 45 19t19 45z" /> +<glyph unicode="" horiz-adv-x="2048" d="M816 1408q-48 0 -79.5 -34t-31.5 -82q0 -14 3 -28l150 -624h-26l-116 482q-9 38 -39.5 62t-69.5 24q-47 0 -79 -34t-32 -81q0 -11 4 -29q3 -13 39 -161t68 -282t32 -138v-227l-307 230q-34 26 -77 26q-52 0 -89.5 -36.5t-37.5 -88.5q0 -67 56 -110l507 -379 q34 -26 76 -26h694q33 0 59 20.5t34 52.5l100 401q8 30 10 88t9 86l116 478q3 12 3 26q0 46 -33 79t-80 33q-38 0 -69 -25.5t-40 -62.5l-99 -408h-26l132 547q3 14 3 28q0 47 -32 80t-80 33q-38 0 -68.5 -24t-39.5 -62l-145 -602h-127l-164 682q-9 38 -39.5 62t-68.5 24z M1461 -256h-694q-85 0 -153 51l-507 380q-50 38 -78.5 94t-28.5 118q0 105 75 179t180 74q25 0 49.5 -5.5t41.5 -11t41 -20.5t35 -23t38.5 -29.5t37.5 -28.5l-123 512q-7 35 -7 59q0 93 60 162t152 79q14 87 80.5 144.5t155.5 57.5q83 0 148 -51.5t85 -132.5l103 -428 l83 348q20 81 85 132.5t148 51.5q87 0 152.5 -54t82.5 -139q93 -10 155 -78t62 -161q0 -30 -7 -57l-116 -477q-5 -22 -5 -67q0 -51 -13 -108l-101 -401q-19 -75 -79.5 -122.5t-137.5 -47.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M640 1408q-53 0 -90.5 -37.5t-37.5 -90.5v-512v-384l-151 202q-41 54 -107 54q-52 0 -89 -38t-37 -90q0 -43 26 -77l384 -512q38 -51 102 -51h718q22 0 39.5 13.5t22.5 34.5l92 368q24 96 24 194v217q0 41 -28 71t-68 30t-68 -28t-28 -68h-32v61q0 48 -32 81.5t-80 33.5 q-46 0 -79 -33t-33 -79v-64h-32v90q0 55 -37 94.5t-91 39.5q-53 0 -90.5 -37.5t-37.5 -90.5v-96h-32v570q0 55 -37 94.5t-91 39.5zM640 1536q107 0 181.5 -77.5t74.5 -184.5v-220q22 2 32 2q99 0 173 -69q47 21 99 21q113 0 184 -87q27 7 56 7q94 0 159 -67.5t65 -161.5 v-217q0 -116 -28 -225l-92 -368q-16 -64 -68 -104.5t-118 -40.5h-718q-60 0 -114.5 27.5t-90.5 74.5l-384 512q-51 68 -51 154q0 105 74.5 180.5t179.5 75.5q71 0 130 -35v547q0 106 75 181t181 75zM768 128v384h-32v-384h32zM1024 128v384h-32v-384h32zM1280 128v384h-32 v-384h32z" /> +<glyph unicode="" d="M1288 889q60 0 107 -23q141 -63 141 -226v-177q0 -94 -23 -186l-85 -339q-21 -86 -90.5 -140t-157.5 -54h-668q-106 0 -181 75t-75 181v401l-239 628q-17 45 -17 91q0 106 75 181t181 75q80 0 145.5 -45.5t93.5 -119.5l17 -44v113q0 106 75 181t181 75t181 -75t75 -181 v-261q27 5 48 5q69 0 127.5 -36.5t88.5 -98.5zM1072 896q-33 0 -60.5 -18t-41.5 -48l-74 -163l-71 -155h55q50 0 90 -31.5t50 -80.5l154 338q10 20 10 46q0 46 -33 79t-79 33zM1293 761q-22 0 -40.5 -8t-29 -16t-23.5 -29.5t-17 -30.5t-17 -37l-132 -290q-10 -20 -10 -46 q0 -46 33 -79t79 -33q33 0 60.5 18t41.5 48l160 352q9 18 9 38q0 50 -32 81.5t-82 31.5zM128 1120q0 -22 8 -46l248 -650v-69l102 111q43 46 106 46h198l106 233v535q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5v-640h-64l-200 526q-14 37 -47 59.5t-73 22.5 q-53 0 -90.5 -37.5t-37.5 -90.5zM1180 -128q44 0 78.5 27t45.5 70l85 339q19 73 19 155v91l-141 -310q-17 -38 -53 -61t-78 -23q-53 0 -93.5 34.5t-48.5 86.5q-44 -57 -114 -57h-208v32h208q46 0 81 33t35 79t-31 79t-77 33h-296q-49 0 -82 -36l-126 -136v-308 q0 -53 37.5 -90.5t90.5 -37.5h668z" /> +<glyph unicode="" horiz-adv-x="1973" d="M857 992v-117q0 -13 -9.5 -22t-22.5 -9h-298v-812q0 -13 -9 -22.5t-22 -9.5h-135q-13 0 -22.5 9t-9.5 23v812h-297q-13 0 -22.5 9t-9.5 22v117q0 14 9 23t23 9h793q13 0 22.5 -9.5t9.5 -22.5zM1895 995l77 -961q1 -13 -8 -24q-10 -10 -23 -10h-134q-12 0 -21 8.5 t-10 20.5l-46 588l-189 -425q-8 -19 -29 -19h-120q-20 0 -29 19l-188 427l-45 -590q-1 -12 -10 -20.5t-21 -8.5h-135q-13 0 -23 10q-9 10 -9 24l78 961q1 12 10 20.5t21 8.5h142q20 0 29 -19l220 -520q10 -24 20 -51q3 7 9.5 24.5t10.5 26.5l221 520q9 19 29 19h141 q13 0 22 -8.5t10 -20.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1042 833q0 88 -60 121q-33 18 -117 18h-123v-281h162q66 0 102 37t36 105zM1094 548l205 -373q8 -17 -1 -31q-8 -16 -27 -16h-152q-20 0 -28 17l-194 365h-155v-350q0 -14 -9 -23t-23 -9h-134q-14 0 -23 9t-9 23v960q0 14 9 23t23 9h294q128 0 190 -24q85 -31 134 -109 t49 -180q0 -92 -42.5 -165.5t-115.5 -109.5q6 -10 9 -16zM896 1376q-150 0 -286 -58.5t-234.5 -157t-157 -234.5t-58.5 -286t58.5 -286t157 -234.5t234.5 -157t286 -58.5t286 58.5t234.5 157t157 234.5t58.5 286t-58.5 286t-157 234.5t-234.5 157t-286 58.5zM1792 640 q0 -182 -71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" /> +<glyph unicode="" horiz-adv-x="1792" d="M605 303q153 0 257 104q14 18 3 36l-45 82q-6 13 -24 17q-16 2 -27 -11l-4 -3q-4 -4 -11.5 -10t-17.5 -13t-23.5 -14.5t-28.5 -13.5t-33.5 -9.5t-37.5 -3.5q-76 0 -125 50t-49 127q0 76 48 125.5t122 49.5q37 0 71.5 -14t50.5 -28l16 -14q11 -11 26 -10q16 2 24 14l53 78 q13 20 -2 39q-3 4 -11 12t-30 23.5t-48.5 28t-67.5 22.5t-86 10q-148 0 -246 -96.5t-98 -240.5q0 -146 97 -241.5t247 -95.5zM1235 303q153 0 257 104q14 18 4 36l-45 82q-8 14 -25 17q-16 2 -27 -11l-4 -3q-4 -4 -11.5 -10t-17.5 -13t-23.5 -14.5t-28.5 -13.5t-33.5 -9.5 t-37.5 -3.5q-76 0 -125 50t-49 127q0 76 48 125.5t122 49.5q37 0 71.5 -14t50.5 -28l16 -14q11 -11 26 -10q16 2 24 14l53 78q13 20 -2 39q-3 4 -11 12t-30 23.5t-48.5 28t-67.5 22.5t-86 10q-147 0 -245.5 -96.5t-98.5 -240.5q0 -146 97 -241.5t247 -95.5zM896 1376 q-150 0 -286 -58.5t-234.5 -157t-157 -234.5t-58.5 -286t58.5 -286t157 -234.5t234.5 -157t286 -58.5t286 58.5t234.5 157t157 234.5t58.5 286t-58.5 286t-157 234.5t-234.5 157t-286 58.5zM896 1536q182 0 348 -71t286 -191t191 -286t71 -348t-71 -348t-191 -286t-286 -191 t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71z" /> +<glyph unicode="" horiz-adv-x="2048" d="M736 736l384 -384l-384 -384l-672 672l672 672l168 -168l-96 -96l-72 72l-480 -480l480 -480l193 193l-289 287zM1312 1312l672 -672l-672 -672l-168 168l96 96l72 -72l480 480l-480 480l-193 -193l289 -287l-96 -96l-384 384z" /> +<glyph unicode="" horiz-adv-x="1792" d="M717 182l271 271l-279 279l-88 -88l192 -191l-96 -96l-279 279l279 279l40 -40l87 87l-127 128l-454 -454zM1075 190l454 454l-454 454l-271 -271l279 -279l88 88l-192 191l96 96l279 -279l-279 -279l-40 40l-87 -88zM1792 640q0 -182 -71 -348t-191 -286t-286 -191 t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" /> +<glyph unicode="" horiz-adv-x="2304" d="M651 539q0 -39 -27.5 -66.5t-65.5 -27.5q-39 0 -66.5 27.5t-27.5 66.5q0 38 27.5 65.5t66.5 27.5q38 0 65.5 -27.5t27.5 -65.5zM1805 540q0 -39 -27.5 -66.5t-66.5 -27.5t-66.5 27.5t-27.5 66.5t27.5 66t66.5 27t66.5 -27t27.5 -66zM765 539q0 79 -56.5 136t-136.5 57 t-136.5 -56.5t-56.5 -136.5t56.5 -136.5t136.5 -56.5t136.5 56.5t56.5 136.5zM1918 540q0 80 -56.5 136.5t-136.5 56.5q-79 0 -136 -56.5t-57 -136.5t56.5 -136.5t136.5 -56.5t136.5 56.5t56.5 136.5zM850 539q0 -116 -81.5 -197.5t-196.5 -81.5q-116 0 -197.5 82t-81.5 197 t82 196.5t197 81.5t196.5 -81.5t81.5 -196.5zM2004 540q0 -115 -81.5 -196.5t-197.5 -81.5q-115 0 -196.5 81.5t-81.5 196.5t81.5 196.5t196.5 81.5q116 0 197.5 -81.5t81.5 -196.5zM1040 537q0 191 -135.5 326.5t-326.5 135.5q-125 0 -231 -62t-168 -168.5t-62 -231.5 t62 -231.5t168 -168.5t231 -62q191 0 326.5 135.5t135.5 326.5zM1708 1110q-254 111 -556 111q-319 0 -573 -110q117 0 223 -45.5t182.5 -122.5t122 -183t45.5 -223q0 115 43.5 219.5t118 180.5t177.5 123t217 50zM2187 537q0 191 -135 326.5t-326 135.5t-326.5 -135.5 t-135.5 -326.5t135.5 -326.5t326.5 -135.5t326 135.5t135 326.5zM1921 1103h383q-44 -51 -75 -114.5t-40 -114.5q110 -151 110 -337q0 -156 -77 -288t-209 -208.5t-287 -76.5q-133 0 -249 56t-196 155q-47 -56 -129 -179q-11 22 -53.5 82.5t-74.5 97.5 q-80 -99 -196.5 -155.5t-249.5 -56.5q-155 0 -287 76.5t-209 208.5t-77 288q0 186 110 337q-9 51 -40 114.5t-75 114.5h365q149 100 355 156.5t432 56.5q224 0 421 -56t348 -157z" /> +<glyph unicode="" horiz-adv-x="1280" d="M640 629q-188 0 -321 133t-133 320q0 188 133 321t321 133t321 -133t133 -321q0 -187 -133 -320t-321 -133zM640 1306q-92 0 -157.5 -65.5t-65.5 -158.5q0 -92 65.5 -157.5t157.5 -65.5t157.5 65.5t65.5 157.5q0 93 -65.5 158.5t-157.5 65.5zM1163 574q13 -27 15 -49.5 t-4.5 -40.5t-26.5 -38.5t-42.5 -37t-61.5 -41.5q-115 -73 -315 -94l73 -72l267 -267q30 -31 30 -74t-30 -73l-12 -13q-31 -30 -74 -30t-74 30q-67 68 -267 268l-267 -268q-31 -30 -74 -30t-73 30l-12 13q-31 30 -31 73t31 74l267 267l72 72q-203 21 -317 94 q-39 25 -61.5 41.5t-42.5 37t-26.5 38.5t-4.5 40.5t15 49.5q10 20 28 35t42 22t56 -2t65 -35q5 -4 15 -11t43 -24.5t69 -30.5t92 -24t113 -11q91 0 174 25.5t120 50.5l38 25q33 26 65 35t56 2t42 -22t28 -35z" /> +<glyph unicode="" d="M927 956q0 -66 -46.5 -112.5t-112.5 -46.5t-112.5 46.5t-46.5 112.5t46.5 112.5t112.5 46.5t112.5 -46.5t46.5 -112.5zM1141 593q-10 20 -28 32t-47.5 9.5t-60.5 -27.5q-10 -8 -29 -20t-81 -32t-127 -20t-124 18t-86 36l-27 18q-31 25 -60.5 27.5t-47.5 -9.5t-28 -32 q-22 -45 -2 -74.5t87 -73.5q83 -53 226 -67l-51 -52q-142 -142 -191 -190q-22 -22 -22 -52.5t22 -52.5l9 -9q22 -22 52.5 -22t52.5 22l191 191q114 -115 191 -191q22 -22 52.5 -22t52.5 22l9 9q22 22 22 52.5t-22 52.5l-191 190l-52 52q141 14 225 67q67 44 87 73.5t-2 74.5 zM1092 956q0 134 -95 229t-229 95t-229 -95t-95 -229t95 -229t229 -95t229 95t95 229zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="1720" d="M1565 1408q65 0 110 -45.5t45 -110.5v-519q0 -176 -68 -336t-182.5 -275t-274 -182.5t-334.5 -67.5q-176 0 -335.5 67.5t-274.5 182.5t-183 275t-68 336v519q0 64 46 110t110 46h1409zM861 344q47 0 82 33l404 388q37 35 37 85q0 49 -34.5 83.5t-83.5 34.5q-47 0 -82 -33 l-323 -310l-323 310q-35 33 -81 33q-49 0 -83.5 -34.5t-34.5 -83.5q0 -51 36 -85l405 -388q33 -33 81 -33z" /> +<glyph unicode="" horiz-adv-x="2304" d="M1494 -103l-295 695q-25 -49 -158.5 -305.5t-198.5 -389.5q-1 -1 -27.5 -0.5t-26.5 1.5q-82 193 -255.5 587t-259.5 596q-21 50 -66.5 107.5t-103.5 100.5t-102 43q0 5 -0.5 24t-0.5 27h583v-50q-39 -2 -79.5 -16t-66.5 -43t-10 -64q26 -59 216.5 -499t235.5 -540 q31 61 140 266.5t131 247.5q-19 39 -126 281t-136 295q-38 69 -201 71v50l513 -1v-47q-60 -2 -93.5 -25t-12.5 -69q33 -70 87 -189.5t86 -187.5q110 214 173 363q24 55 -10 79.5t-129 26.5q1 7 1 25v24q64 0 170.5 0.5t180 1t92.5 0.5v-49q-62 -2 -119 -33t-90 -81 l-213 -442q13 -33 127.5 -290t121.5 -274l441 1017q-14 38 -49.5 62.5t-65 31.5t-55.5 8v50l460 -4l1 -2l-1 -44q-139 -4 -201 -145q-526 -1216 -559 -1291h-49z" /> +<glyph unicode="" horiz-adv-x="1792" d="M949 643q0 -26 -16.5 -45t-41.5 -19q-26 0 -45 16.5t-19 41.5q0 26 17 45t42 19t44 -16.5t19 -41.5zM964 585l350 581q-9 -8 -67.5 -62.5t-125.5 -116.5t-136.5 -127t-117 -110.5t-50.5 -51.5l-349 -580q7 7 67 62t126 116.5t136 127t117 111t50 50.5zM1611 640 q0 -201 -104 -371q-3 2 -17 11t-26.5 16.5t-16.5 7.5q-13 0 -13 -13q0 -10 59 -44q-74 -112 -184.5 -190.5t-241.5 -110.5l-16 67q-1 10 -15 10q-5 0 -8 -5.5t-2 -9.5l16 -68q-72 -15 -146 -15q-199 0 -372 105q1 2 13 20.5t21.5 33.5t9.5 19q0 13 -13 13q-6 0 -17 -14.5 t-22.5 -34.5t-13.5 -23q-113 75 -192 187.5t-110 244.5l69 15q10 3 10 15q0 5 -5.5 8t-10.5 2l-68 -15q-14 72 -14 139q0 206 109 379q2 -1 18.5 -12t30 -19t17.5 -8q13 0 13 12q0 6 -12.5 15.5t-32.5 21.5l-20 12q77 112 189 189t244 107l15 -67q2 -10 15 -10q5 0 8 5.5 t2 10.5l-15 66q71 13 134 13q204 0 379 -109q-39 -56 -39 -65q0 -13 12 -13q11 0 48 64q111 -75 187.5 -186t107.5 -241l-56 -12q-10 -2 -10 -16q0 -5 5.5 -8t9.5 -2l57 13q14 -72 14 -140zM1696 640q0 163 -63.5 311t-170.5 255t-255 170.5t-311 63.5t-311 -63.5 t-255 -170.5t-170.5 -255t-63.5 -311t63.5 -311t170.5 -255t255 -170.5t311 -63.5t311 63.5t255 170.5t170.5 255t63.5 311zM1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71t348 -71t286 -191 t191 -286t71 -348z" /> +<glyph unicode="" horiz-adv-x="1792" d="M893 1536q240 2 451 -120q232 -134 352 -372l-742 39q-160 9 -294 -74.5t-185 -229.5l-276 424q128 159 311 245.5t383 87.5zM146 1131l337 -663q72 -143 211 -217t293 -45l-230 -451q-212 33 -385 157.5t-272.5 316t-99.5 411.5q0 267 146 491zM1732 962 q58 -150 59.5 -310.5t-48.5 -306t-153 -272t-246 -209.5q-230 -133 -498 -119l405 623q88 131 82.5 290.5t-106.5 277.5zM896 942q125 0 213.5 -88.5t88.5 -213.5t-88.5 -213.5t-213.5 -88.5t-213.5 88.5t-88.5 213.5t88.5 213.5t213.5 88.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M903 -256q-283 0 -504.5 150.5t-329.5 398.5q-58 131 -67 301t26 332.5t111 312t179 242.5l-11 -281q11 14 68 15.5t70 -15.5q42 81 160.5 138t234.5 59q-54 -45 -119.5 -148.5t-58.5 -163.5q25 -8 62.5 -13.5t63 -7.5t68 -4t50.5 -3q15 -5 9.5 -45.5t-30.5 -75.5 q-5 -7 -16.5 -18.5t-56.5 -35.5t-101 -34l15 -189l-139 67q-18 -43 -7.5 -81.5t36 -66.5t65.5 -41.5t81 -6.5q51 9 98 34.5t83.5 45t73.5 17.5q61 -4 89.5 -33t19.5 -65q-1 -2 -2.5 -5.5t-8.5 -12.5t-18 -15.5t-31.5 -10.5t-46.5 -1q-60 -95 -144.5 -135.5t-209.5 -29.5 q74 -61 162.5 -82.5t168.5 -6t154.5 52t128 87.5t80.5 104q43 91 39 192.5t-37.5 188.5t-78.5 125q87 -38 137 -79.5t77 -112.5q15 170 -57.5 343t-209.5 284q265 -77 412 -279.5t151 -517.5q2 -127 -40.5 -255t-123.5 -238t-189 -196t-247.5 -135.5t-288.5 -49.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1493 1308q-165 110 -359 110q-155 0 -293 -73t-240 -200q-75 -93 -119.5 -218t-48.5 -266v-42q4 -141 48.5 -266t119.5 -218q102 -127 240 -200t293 -73q194 0 359 110q-121 -108 -274.5 -168t-322.5 -60q-29 0 -43 1q-175 8 -333 82t-272 193t-181 281t-67 339 q0 182 71 348t191 286t286 191t348 71h3q168 -1 320.5 -60.5t273.5 -167.5zM1792 640q0 -192 -77 -362.5t-213 -296.5q-104 -63 -222 -63q-137 0 -255 84q154 56 253.5 233t99.5 405q0 227 -99 404t-253 234q119 83 254 83q119 0 226 -65q135 -125 210.5 -295t75.5 -361z " /> +<glyph unicode="" horiz-adv-x="1792" d="M1792 599q0 -56 -7 -104h-1151q0 -146 109.5 -244.5t257.5 -98.5q99 0 185.5 46.5t136.5 130.5h423q-56 -159 -170.5 -281t-267.5 -188.5t-321 -66.5q-187 0 -356 83q-228 -116 -394 -116q-237 0 -237 263q0 115 45 275q17 60 109 229q199 360 475 606 q-184 -79 -427 -354q63 274 283.5 449.5t501.5 175.5q30 0 45 -1q255 117 433 117q64 0 116 -13t94.5 -40.5t66.5 -76.5t24 -115q0 -116 -75 -286q101 -182 101 -390zM1722 1239q0 83 -53 132t-137 49q-108 0 -254 -70q121 -47 222.5 -131.5t170.5 -195.5q51 135 51 216z M128 2q0 -86 48.5 -132.5t134.5 -46.5q115 0 266 83q-122 72 -213.5 183t-137.5 245q-98 -205 -98 -332zM632 715h728q-5 142 -113 237t-251 95q-144 0 -251.5 -95t-112.5 -237z" /> +<glyph unicode="" horiz-adv-x="2048" d="M1792 288v960q0 13 -9.5 22.5t-22.5 9.5h-1600q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h1600q13 0 22.5 9.5t9.5 22.5zM1920 1248v-960q0 -66 -47 -113t-113 -47h-736v-128h352q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23 v64q0 14 9 23t23 9h352v128h-736q-66 0 -113 47t-47 113v960q0 66 47 113t113 47h1600q66 0 113 -47t47 -113z" /> +<glyph unicode="" horiz-adv-x="1792" d="M138 1408h197q-70 -64 -126 -149q-36 -56 -59 -115t-30 -125.5t-8.5 -120t10.5 -132t21 -126t28 -136.5q4 -19 6 -28q51 -238 81 -329q57 -171 152 -275h-272q-48 0 -82 34t-34 82v1304q0 48 34 82t82 34zM1346 1408h308q48 0 82 -34t34 -82v-1304q0 -48 -34 -82t-82 -34 h-178q212 210 196 565l-469 -101q-2 -45 -12 -82t-31 -72t-59.5 -59.5t-93.5 -36.5q-123 -26 -199 40q-32 27 -53 61t-51.5 129t-64.5 258q-35 163 -45.5 263t-5.5 139t23 77q20 41 62.5 73t102.5 45q45 12 83.5 6.5t67 -17t54 -35t43 -48t34.5 -56.5l468 100 q-68 175 -180 287z" /> +<glyph unicode="" d="M1401 -11l-6 -6q-113 -114 -259 -175q-154 -64 -317 -64q-165 0 -317 64q-148 63 -259 175q-113 112 -175 258q-42 103 -54 189q-4 28 48 36q51 8 56 -20q1 -1 1 -4q18 -90 46 -159q50 -124 152 -226q98 -98 226 -152q132 -56 276 -56q143 0 276 56q128 55 225 152l6 6 q10 10 25 6q12 -3 33 -22q36 -37 17 -58zM929 604l-66 -66l63 -63q21 -21 -7 -49q-17 -17 -32 -17q-10 0 -19 10l-62 61l-66 -66q-5 -5 -15 -5q-15 0 -31 16l-2 2q-18 15 -18 29q0 7 8 17l66 65l-66 66q-16 16 14 45q18 18 31 18q6 0 13 -5l65 -66l65 65q18 17 48 -13 q27 -27 11 -44zM1400 547q0 -118 -46 -228q-45 -105 -126 -186q-80 -80 -187 -126t-228 -46t-228 46t-187 126q-82 82 -125 186q-15 32 -15 40h-1q-9 27 43 44q50 16 60 -12q37 -99 97 -167h1v339v2q3 136 102 232q105 103 253 103q147 0 251 -103t104 -249 q0 -147 -104.5 -251t-250.5 -104q-58 0 -112 16q-28 11 -13 61q16 51 44 43l14 -3q14 -3 32.5 -6t30.5 -3q104 0 176 71.5t72 174.5q0 101 -72 171q-71 71 -175 71q-107 0 -178 -80q-64 -72 -64 -160v-413q110 -67 242 -67q96 0 185 36.5t156 103.5t103.5 155t36.5 183 q0 198 -141 339q-140 140 -339 140q-200 0 -340 -140q-53 -53 -77 -87l-2 -2q-8 -11 -13 -15.5t-21.5 -9.5t-38.5 3q-21 5 -36.5 16.5t-15.5 26.5v680q0 15 10.5 26.5t27.5 11.5h877q30 0 30 -55t-30 -55h-811v-483h1q40 42 102 84t108 61q109 46 231 46q121 0 228 -46 t187 -126q81 -81 126 -186q46 -112 46 -229zM1369 1128q9 -8 9 -18t-5.5 -18t-16.5 -21q-26 -26 -39 -26q-9 0 -16 7q-106 91 -207 133q-128 56 -276 56q-133 0 -262 -49q-27 -10 -45 37q-9 25 -8 38q3 16 16 20q130 57 299 57q164 0 316 -64q137 -58 235 -152z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1551 60q15 6 26 3t11 -17.5t-15 -33.5q-13 -16 -44 -43.5t-95.5 -68t-141 -74t-188 -58t-229.5 -24.5q-119 0 -238 31t-209 76.5t-172.5 104t-132.5 105t-84 87.5q-8 9 -10 16.5t1 12t8 7t11.5 2t11.5 -4.5q192 -117 300 -166q389 -176 799 -90q190 40 391 135z M1758 175q11 -16 2.5 -69.5t-28.5 -102.5q-34 -83 -85 -124q-17 -14 -26 -9t0 24q21 45 44.5 121.5t6.5 98.5q-5 7 -15.5 11.5t-27 6t-29.5 2.5t-35 0t-31.5 -2t-31 -3t-22.5 -2q-6 -1 -13 -1.5t-11 -1t-8.5 -1t-7 -0.5h-5.5h-4.5t-3 0.5t-2 1.5l-1.5 3q-6 16 47 40t103 30 q46 7 108 1t76 -24zM1364 618q0 -31 13.5 -64t32 -58t37.5 -46t33 -32l13 -11l-227 -224q-40 37 -79 75.5t-58 58.5l-19 20q-11 11 -25 33q-38 -59 -97.5 -102.5t-127.5 -63.5t-140 -23t-137.5 21t-117.5 65.5t-83 113t-31 162.5q0 84 28 154t72 116.5t106.5 83t122.5 57 t130 34.5t119.5 18.5t99.5 6.5v127q0 65 -21 97q-34 53 -121 53q-6 0 -16.5 -1t-40.5 -12t-56 -29.5t-56 -59.5t-48 -96l-294 27q0 60 22 119t67 113t108 95t151.5 65.5t190.5 24.5q100 0 181 -25t129.5 -61.5t81 -83t45 -86t12.5 -73.5v-589zM692 597q0 -86 70 -133 q66 -44 139 -22q84 25 114 123q14 45 14 101v162q-59 -2 -111 -12t-106.5 -33.5t-87 -71t-32.5 -114.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1536 1280q52 0 90 -38t38 -90v-1280q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h384v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h128zM1152 1376v-288q0 -14 9 -23t23 -9 h64q14 0 23 9t9 23v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM384 1376v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM1536 -128v1024h-1408v-1024h1408zM896 448h224q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-224 v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-224q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h224v224q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-224z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1152 416v-64q0 -14 -9 -23t-23 -9h-576q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h576q14 0 23 -9t9 -23zM128 -128h1408v1024h-1408v-1024zM512 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1280 1088v288q0 14 -9 23 t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1664 1152v-1280q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h384v96q0 66 47 113t113 47h64q66 0 113 -47 t47 -113v-96h128q52 0 90 -38t38 -90z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1111 151l-46 -46q-9 -9 -22 -9t-23 9l-188 189l-188 -189q-10 -9 -23 -9t-22 9l-46 46q-9 9 -9 22t9 23l189 188l-189 188q-9 10 -9 23t9 22l46 46q9 9 22 9t23 -9l188 -188l188 188q10 9 23 9t22 -9l46 -46q9 -9 9 -22t-9 -23l-188 -188l188 -188q9 -10 9 -23t-9 -22z M128 -128h1408v1024h-1408v-1024zM512 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1280 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1664 1152v-1280 q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h384v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h128q52 0 90 -38t38 -90z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1303 572l-512 -512q-10 -9 -23 -9t-23 9l-288 288q-9 10 -9 23t9 22l46 46q9 9 22 9t23 -9l220 -220l444 444q10 9 23 9t22 -9l46 -46q9 -9 9 -22t-9 -23zM128 -128h1408v1024h-1408v-1024zM512 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23 t23 -9h64q14 0 23 9t9 23zM1280 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1664 1152v-1280q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47 t47 -113v-96h384v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h128q52 0 90 -38t38 -90z" /> +<glyph unicode="" horiz-adv-x="1792" d="M448 1536q26 0 45 -19t19 -45v-891l536 429q17 14 40 14q26 0 45 -19t19 -45v-379l536 429q17 14 40 14q26 0 45 -19t19 -45v-1152q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v1664q0 26 19 45t45 19h384z" /> +<glyph unicode="" horiz-adv-x="1024" d="M512 448q66 0 128 15v-655q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v655q61 -15 128 -15zM512 1536q212 0 362 -150t150 -362t-150 -362t-362 -150t-362 150t-150 362t150 362t362 150zM512 1312q14 0 23 9t9 23t-9 23t-23 9q-146 0 -249 -103t-103 -249 q0 -14 9 -23t23 -9t23 9t9 23q0 119 84.5 203.5t203.5 84.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1745 1239q10 -10 10 -23t-10 -23l-141 -141q-28 -28 -68 -28h-1344q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h576v64q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-64h512q40 0 68 -28zM768 320h256v-512q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v512zM1600 768 q26 0 45 -19t19 -45v-256q0 -26 -19 -45t-45 -19h-1344q-40 0 -68 28l-141 141q-10 10 -10 23t10 23l141 141q28 28 68 28h512v192h256v-192h576z" /> +<glyph unicode="" horiz-adv-x="2048" d="M2020 1525q28 -20 28 -53v-1408q0 -20 -11 -36t-29 -23l-640 -256q-24 -11 -48 0l-616 246l-616 -246q-10 -5 -24 -5q-19 0 -36 11q-28 20 -28 53v1408q0 20 11 36t29 23l640 256q24 11 48 0l616 -246l616 246q32 13 60 -6zM736 1390v-1270l576 -230v1270zM128 1173 v-1270l544 217v1270zM1920 107v1270l-544 -217v-1270z" /> +<glyph unicode="" horiz-adv-x="1792" d="M512 1536q13 0 22.5 -9.5t9.5 -22.5v-1472q0 -20 -17 -28l-480 -256q-7 -4 -15 -4q-13 0 -22.5 9.5t-9.5 22.5v1472q0 20 17 28l480 256q7 4 15 4zM1760 1536q13 0 22.5 -9.5t9.5 -22.5v-1472q0 -20 -17 -28l-480 -256q-7 -4 -15 -4q-13 0 -22.5 9.5t-9.5 22.5v1472 q0 20 17 28l480 256q7 4 15 4zM640 1536q8 0 14 -3l512 -256q18 -10 18 -29v-1472q0 -13 -9.5 -22.5t-22.5 -9.5q-8 0 -14 3l-512 256q-18 10 -18 29v1472q0 13 9.5 22.5t22.5 9.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M640 640q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1024 640q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1408 640q0 53 -37.5 90.5t-90.5 37.5 t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1792 640q0 -174 -120 -321.5t-326 -233t-450 -85.5q-110 0 -211 18q-173 -173 -435 -229q-52 -10 -86 -13q-12 -1 -22 6t-13 18q-4 15 20 37q5 5 23.5 21.5t25.5 23.5t23.5 25.5t24 31.5t20.5 37 t20 48t14.5 57.5t12.5 72.5q-146 90 -229.5 216.5t-83.5 269.5q0 174 120 321.5t326 233t450 85.5t450 -85.5t326 -233t120 -321.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M640 640q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1024 640q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1408 640q0 -53 -37.5 -90.5t-90.5 -37.5 t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM896 1152q-204 0 -381.5 -69.5t-282 -187.5t-104.5 -255q0 -112 71.5 -213.5t201.5 -175.5l87 -50l-27 -96q-24 -91 -70 -172q152 63 275 171l43 38l57 -6q69 -8 130 -8q204 0 381.5 69.5t282 187.5 t104.5 255t-104.5 255t-282 187.5t-381.5 69.5zM1792 640q0 -174 -120 -321.5t-326 -233t-450 -85.5q-70 0 -145 8q-198 -175 -460 -242q-49 -14 -114 -22h-5q-15 0 -27 10.5t-16 27.5v1q-3 4 -0.5 12t2 10t4.5 9.5l6 9t7 8.5t8 9q7 8 31 34.5t34.5 38t31 39.5t32.5 51 t27 59t26 76q-157 89 -247.5 220t-90.5 281q0 130 71 248.5t191 204.5t286 136.5t348 50.5t348 -50.5t286 -136.5t191 -204.5t71 -248.5z" /> +<glyph unicode="" horiz-adv-x="1024" d="M512 345l512 295v-591l-512 -296v592zM0 640v-591l512 296zM512 1527v-591l-512 -296v591zM512 936l512 295v-591z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1709 1018q-10 -236 -332 -651q-333 -431 -562 -431q-142 0 -240 263q-44 160 -132 482q-72 262 -157 262q-18 0 -127 -76l-77 98q24 21 108 96.5t130 115.5q156 138 241 146q95 9 153 -55.5t81 -203.5q44 -287 66 -373q55 -249 120 -249q51 0 154 161q101 161 109 246 q13 139 -109 139q-57 0 -121 -26q120 393 459 382q251 -8 236 -326z" /> +<glyph unicode="" d="M0 1408h1536v-1536h-1536v1536zM1085 293l-221 631l221 297h-634l221 -297l-221 -631l317 -304z" /> +<glyph unicode="" d="M0 1408h1536v-1536h-1536v1536zM908 1088l-12 -33l75 -83l-31 -114l25 -25l107 57l107 -57l25 25l-31 114l75 83l-12 33h-95l-53 96h-32l-53 -96h-95zM641 925q32 0 44.5 -16t11.5 -63l174 21q0 55 -17.5 92.5t-50.5 56t-69 25.5t-85 7q-133 0 -199 -57.5t-66 -182.5v-72 h-96v-128h76q20 0 20 -8v-382q0 -14 -5 -20t-18 -7l-73 -7v-88h448v86l-149 14q-6 1 -8.5 1.5t-3.5 2.5t-0.5 4t1 7t0.5 10v387h191l38 128h-231q-6 0 -2 6t4 9v80q0 27 1.5 40.5t7.5 28t19.5 20t36.5 5.5zM1248 96v86l-54 9q-7 1 -9.5 2.5t-2.5 3t1 7.5t1 12v520h-275 l-23 -101l83 -22q23 -7 23 -27v-370q0 -14 -6 -18.5t-20 -6.5l-70 -9v-86h352z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1792 690q0 -58 -29.5 -105.5t-79.5 -72.5q12 -46 12 -96q0 -155 -106.5 -287t-290.5 -208.5t-400 -76.5t-399.5 76.5t-290 208.5t-106.5 287q0 47 11 94q-51 25 -82 73.5t-31 106.5q0 82 58 140.5t141 58.5q85 0 145 -63q218 152 515 162l116 521q3 13 15 21t26 5 l369 -81q18 37 54 59.5t79 22.5q62 0 106 -43.5t44 -105.5t-44 -106t-106 -44t-105.5 43.5t-43.5 105.5l-334 74l-104 -472q300 -9 519 -160q58 61 143 61q83 0 141 -58.5t58 -140.5zM418 491q0 -62 43.5 -106t105.5 -44t106 44t44 106t-44 105.5t-106 43.5q-61 0 -105 -44 t-44 -105zM1228 136q11 11 11 26t-11 26q-10 10 -25 10t-26 -10q-41 -42 -121 -62t-160 -20t-160 20t-121 62q-11 10 -26 10t-25 -10q-11 -10 -11 -25.5t11 -26.5q43 -43 118.5 -68t122.5 -29.5t91 -4.5t91 4.5t122.5 29.5t118.5 68zM1225 341q62 0 105.5 44t43.5 106 q0 61 -44 105t-105 44q-62 0 -106 -43.5t-44 -105.5t44 -106t106 -44z" /> +<glyph unicode="" horiz-adv-x="1792" d="M69 741h1q16 126 58.5 241.5t115 217t167.5 176t223.5 117.5t276.5 43q231 0 414 -105.5t294 -303.5q104 -187 104 -442v-188h-1125q1 -111 53.5 -192.5t136.5 -122.5t189.5 -57t213 -3t208 46.5t173.5 84.5v-377q-92 -55 -229.5 -92t-312.5 -38t-316 53 q-189 73 -311.5 249t-124.5 372q-3 242 111 412t325 268q-48 -60 -78 -125.5t-46 -159.5h635q8 77 -8 140t-47 101.5t-70.5 66.5t-80.5 41t-75 20.5t-56 8.5l-22 1q-135 -5 -259.5 -44.5t-223.5 -104.5t-176 -140.5t-138 -163.5z" /> +<glyph unicode="" horiz-adv-x="2304" d="M0 32v608h2304v-608q0 -66 -47 -113t-113 -47h-1984q-66 0 -113 47t-47 113zM640 256v-128h384v128h-384zM256 256v-128h256v128h-256zM2144 1408q66 0 113 -47t47 -113v-224h-2304v224q0 66 47 113t113 47h1984z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1549 857q55 0 85.5 -28.5t30.5 -83.5t-34 -82t-91 -27h-136v-177h-25v398h170zM1710 267l-4 -11l-5 -10q-113 -230 -330.5 -366t-474.5 -136q-182 0 -348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71q244 0 454.5 -124t329.5 -338l2 -4l8 -16 q-30 -15 -136.5 -68.5t-163.5 -84.5q-6 -3 -479 -268q384 -183 799 -366zM896 -234q250 0 462.5 132.5t322.5 357.5l-287 129q-72 -140 -206 -222t-292 -82q-151 0 -280 75t-204 204t-75 280t75 280t204 204t280 75t280 -73.5t204 -204.5l280 143q-116 208 -321 329 t-443 121q-119 0 -232.5 -31.5t-209 -87.5t-176.5 -137t-137 -176.5t-87.5 -209t-31.5 -232.5t31.5 -232.5t87.5 -209t137 -176.5t176.5 -137t209 -87.5t232.5 -31.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1427 827l-614 386l92 151h855zM405 562l-184 116v858l1183 -743zM1424 697l147 -95v-858l-532 335zM1387 718l-500 -802h-855l356 571z" /> +<glyph unicode="" horiz-adv-x="1792" d="M640 528v224q0 16 -16 16h-96q-16 0 -16 -16v-224q0 -16 16 -16h96q16 0 16 16zM1152 528v224q0 16 -16 16h-96q-16 0 -16 -16v-224q0 -16 16 -16h96q16 0 16 16zM1664 496v-752h-640v320q0 80 -56 136t-136 56t-136 -56t-56 -136v-320h-640v752q0 16 16 16h96 q16 0 16 -16v-112h128v624q0 16 16 16h96q16 0 16 -16v-112h128v112q0 16 16 16h96q16 0 16 -16v-112h128v112q0 16 16 16h16v393q-32 19 -32 55q0 26 19 45t45 19t45 -19t19 -45q0 -36 -32 -55v-9h272q16 0 16 -16v-224q0 -16 -16 -16h-272v-128h16q16 0 16 -16v-112h128 v112q0 16 16 16h96q16 0 16 -16v-112h128v112q0 16 16 16h96q16 0 16 -16v-624h128v112q0 16 16 16h96q16 0 16 -16z" /> +<glyph unicode="" horiz-adv-x="2304" d="M2288 731q16 -8 16 -27t-16 -27l-320 -192q-8 -5 -16 -5q-9 0 -16 4q-16 10 -16 28v128h-858q37 -58 83 -165q16 -37 24.5 -55t24 -49t27 -47t27 -34t31.5 -26t33 -8h96v96q0 14 9 23t23 9h320q14 0 23 -9t9 -23v-320q0 -14 -9 -23t-23 -9h-320q-14 0 -23 9t-9 23v96h-96 q-32 0 -61 10t-51 23.5t-45 40.5t-37 46t-33.5 57t-28.5 57.5t-28 60.5q-23 53 -37 81.5t-36 65t-44.5 53.5t-46.5 17h-360q-22 -84 -91 -138t-157 -54q-106 0 -181 75t-75 181t75 181t181 75q88 0 157 -54t91 -138h104q24 0 46.5 17t44.5 53.5t36 65t37 81.5q19 41 28 60.5 t28.5 57.5t33.5 57t37 46t45 40.5t51 23.5t61 10h107q21 57 70 92.5t111 35.5q80 0 136 -56t56 -136t-56 -136t-136 -56q-62 0 -111 35.5t-70 92.5h-107q-17 0 -33 -8t-31.5 -26t-27 -34t-27 -47t-24 -49t-24.5 -55q-46 -107 -83 -165h1114v128q0 18 16 28t32 -1z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1150 774q0 -56 -39.5 -95t-95.5 -39h-253v269h253q56 0 95.5 -39.5t39.5 -95.5zM1329 774q0 130 -91.5 222t-222.5 92h-433v-896h180v269h253q130 0 222 91.5t92 221.5zM1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348 t71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" /> +<glyph unicode="" horiz-adv-x="2304" d="M1645 438q0 59 -34 106.5t-87 68.5q-7 -45 -23 -92q-7 -24 -27.5 -38t-44.5 -14q-12 0 -24 3q-31 10 -45 38.5t-4 58.5q23 71 23 143q0 123 -61 227.5t-166 165.5t-228 61q-134 0 -247 -73t-167 -194q108 -28 188 -106q22 -23 22 -55t-22 -54t-54 -22t-55 22 q-75 75 -180 75q-106 0 -181 -74.5t-75 -180.5t75 -180.5t181 -74.5h1046q79 0 134.5 55.5t55.5 133.5zM1798 438q0 -142 -100.5 -242t-242.5 -100h-1046q-169 0 -289 119.5t-120 288.5q0 153 100 267t249 136q62 184 221 298t354 114q235 0 408.5 -158.5t196.5 -389.5 q116 -25 192.5 -118.5t76.5 -214.5zM2048 438q0 -175 -97 -319q-23 -33 -64 -33q-24 0 -43 13q-26 17 -32 48.5t12 57.5q71 104 71 233t-71 233q-18 26 -12 57t32 49t57.5 11.5t49.5 -32.5q97 -142 97 -318zM2304 438q0 -244 -134 -443q-23 -34 -64 -34q-23 0 -42 13 q-26 18 -32.5 49t11.5 57q108 164 108 358q0 195 -108 357q-18 26 -11.5 57.5t32.5 48.5q26 18 57 12t49 -33q134 -198 134 -442z" /> +<glyph unicode="" d="M1500 -13q0 -89 -63 -152.5t-153 -63.5t-153.5 63.5t-63.5 152.5q0 90 63.5 153.5t153.5 63.5t153 -63.5t63 -153.5zM1267 268q-115 -15 -192.5 -102.5t-77.5 -205.5q0 -74 33 -138q-146 -78 -379 -78q-109 0 -201 21t-153.5 54.5t-110.5 76.5t-76 85t-44.5 83 t-23.5 66.5t-6 39.5q0 19 4.5 42.5t18.5 56t36.5 58t64 43.5t94.5 18t94 -17.5t63 -41t35.5 -53t17.5 -49t4 -33.5q0 -34 -23 -81q28 -27 82 -42t93 -17l40 -1q115 0 190 51t75 133q0 26 -9 48.5t-31.5 44.5t-49.5 41t-74 44t-93.5 47.5t-119.5 56.5q-28 13 -43 20 q-116 55 -187 100t-122.5 102t-72 125.5t-20.5 162.5q0 78 20.5 150t66 137.5t112.5 114t166.5 77t221.5 28.5q120 0 220 -26t164.5 -67t109.5 -94t64 -105.5t19 -103.5q0 -46 -15 -82.5t-36.5 -58t-48.5 -36t-49 -19.5t-39 -5h-8h-32t-39 5t-44 14t-41 28t-37 46t-24 70.5 t-10 97.5q-15 16 -59 25.5t-81 10.5l-37 1q-68 0 -117.5 -31t-70.5 -70t-21 -76q0 -24 5 -43t24 -46t53 -51t97 -53.5t150 -58.5q76 -25 138.5 -53.5t109 -55.5t83 -59t60.5 -59.5t41 -62.5t26.5 -62t14.5 -63.5t6 -62t1 -62.5z" /> +<glyph unicode="" d="M704 352v576q0 14 -9 23t-23 9h-256q-14 0 -23 -9t-9 -23v-576q0 -14 9 -23t23 -9h256q14 0 23 9t9 23zM1152 352v576q0 14 -9 23t-23 9h-256q-14 0 -23 -9t-9 -23v-576q0 -14 9 -23t23 -9h256q14 0 23 9t9 23zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103 t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM768 96q148 0 273 73t198 198t73 273t-73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273 t73 -273t198 -198t273 -73zM864 320q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-576q0 -14 -9 -23t-23 -9h-192zM480 320q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-576q0 -14 -9 -23t-23 -9h-192z" /> +<glyph unicode="" d="M1088 352v576q0 14 -9 23t-23 9h-576q-14 0 -23 -9t-9 -23v-576q0 -14 9 -23t23 -9h576q14 0 23 9t9 23zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5 t103 -385.5z" /> +<glyph unicode="" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM768 96q148 0 273 73t198 198t73 273t-73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273 t73 -273t198 -198t273 -73zM480 320q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h576q14 0 23 -9t9 -23v-576q0 -14 -9 -23t-23 -9h-576z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1757 128l35 -313q3 -28 -16 -50q-19 -21 -48 -21h-1664q-29 0 -48 21q-19 22 -16 50l35 313h1722zM1664 967l86 -775h-1708l86 775q3 24 21 40.5t43 16.5h256v-128q0 -53 37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5v128h384v-128q0 -53 37.5 -90.5t90.5 -37.5 t90.5 37.5t37.5 90.5v128h256q25 0 43 -16.5t21 -40.5zM1280 1152v-256q0 -26 -19 -45t-45 -19t-45 19t-19 45v256q0 106 -75 181t-181 75t-181 -75t-75 -181v-256q0 -26 -19 -45t-45 -19t-45 19t-19 45v256q0 159 112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5z" /> +<glyph unicode="" horiz-adv-x="2048" d="M1920 768q53 0 90.5 -37.5t37.5 -90.5t-37.5 -90.5t-90.5 -37.5h-15l-115 -662q-8 -46 -44 -76t-82 -30h-1280q-46 0 -82 30t-44 76l-115 662h-15q-53 0 -90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5h1792zM485 -32q26 2 43.5 22.5t15.5 46.5l-32 416q-2 26 -22.5 43.5 t-46.5 15.5t-43.5 -22.5t-15.5 -46.5l32 -416q2 -25 20.5 -42t43.5 -17h5zM896 32v416q0 26 -19 45t-45 19t-45 -19t-19 -45v-416q0 -26 19 -45t45 -19t45 19t19 45zM1280 32v416q0 26 -19 45t-45 19t-45 -19t-19 -45v-416q0 -26 19 -45t45 -19t45 19t19 45zM1632 27l32 416 q2 26 -15.5 46.5t-43.5 22.5t-46.5 -15.5t-22.5 -43.5l-32 -416q-2 -26 15.5 -46.5t43.5 -22.5h5q25 0 43.5 17t20.5 42zM476 1244l-93 -412h-132l101 441q19 88 89 143.5t160 55.5h167q0 26 19 45t45 19h384q26 0 45 -19t19 -45h167q90 0 160 -55.5t89 -143.5l101 -441 h-132l-93 412q-11 44 -45.5 72t-79.5 28h-167q0 -26 -19 -45t-45 -19h-384q-26 0 -45 19t-19 45h-167q-45 0 -79.5 -28t-45.5 -72z" /> +<glyph unicode="" horiz-adv-x="1792" d="M991 512l64 256h-254l-64 -256h254zM1759 1016l-56 -224q-7 -24 -31 -24h-327l-64 -256h311q15 0 25 -12q10 -14 6 -28l-56 -224q-5 -24 -31 -24h-327l-81 -328q-7 -24 -31 -24h-224q-16 0 -26 12q-9 12 -6 28l78 312h-254l-81 -328q-7 -24 -31 -24h-225q-15 0 -25 12 q-9 12 -6 28l78 312h-311q-15 0 -25 12q-9 12 -6 28l56 224q7 24 31 24h327l64 256h-311q-15 0 -25 12q-10 14 -6 28l56 224q5 24 31 24h327l81 328q7 24 32 24h224q15 0 25 -12q9 -12 6 -28l-78 -312h254l81 328q7 24 32 24h224q15 0 25 -12q9 -12 6 -28l-78 -312h311 q15 0 25 -12q9 -12 6 -28z" /> +<glyph unicode="" d="M841 483l148 -148l-149 -149zM840 1094l149 -149l-148 -148zM710 -130l464 464l-306 306l306 306l-464 464v-611l-255 255l-93 -93l320 -321l-320 -321l93 -93l255 255v-611zM1429 640q0 -209 -32 -365.5t-87.5 -257t-140.5 -162.5t-181.5 -86.5t-219.5 -24.5 t-219.5 24.5t-181.5 86.5t-140.5 162.5t-87.5 257t-32 365.5t32 365.5t87.5 257t140.5 162.5t181.5 86.5t219.5 24.5t219.5 -24.5t181.5 -86.5t140.5 -162.5t87.5 -257t32 -365.5z" /> +<glyph unicode="" horiz-adv-x="1024" d="M596 113l173 172l-173 172v-344zM596 823l173 172l-173 172v-344zM628 640l356 -356l-539 -540v711l-297 -296l-108 108l372 373l-372 373l108 108l297 -296v711l539 -540z" /> +<glyph unicode="" d="M1280 256q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM512 1024q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM1536 256q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5 t112.5 -271.5zM1440 1344q0 -20 -13 -38l-1056 -1408q-19 -26 -51 -26h-160q-26 0 -45 19t-19 45q0 20 13 38l1056 1408q19 26 51 26h160q26 0 45 -19t19 -45zM768 1024q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5 t271.5 -112.5t112.5 -271.5z" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" /> +</font> +</defs></svg>
\ No newline at end of file diff --git a/examples/blog/static/fonts/fontawesome-webfont.ttf b/examples/blog/static/fonts/fontawesome-webfont.ttf Binary files differnew file mode 100644 index 000000000..26dea7951 --- /dev/null +++ b/examples/blog/static/fonts/fontawesome-webfont.ttf diff --git a/examples/blog/static/fonts/fontawesome-webfont.woff b/examples/blog/static/fonts/fontawesome-webfont.woff Binary files differnew file mode 100644 index 000000000..dc35ce3c2 --- /dev/null +++ b/examples/blog/static/fonts/fontawesome-webfont.woff diff --git a/examples/blog/static/fonts/fontawesome-webfont.woff2 b/examples/blog/static/fonts/fontawesome-webfont.woff2 Binary files differnew file mode 100644 index 000000000..500e51725 --- /dev/null +++ b/examples/blog/static/fonts/fontawesome-webfont.woff2 diff --git a/examples/blog/static/fonts/glyphicons-halflings-regular.eot b/examples/blog/static/fonts/glyphicons-halflings-regular.eot Binary files differnew file mode 100644 index 000000000..b93a4953f --- /dev/null +++ b/examples/blog/static/fonts/glyphicons-halflings-regular.eot diff --git a/examples/blog/static/fonts/glyphicons-halflings-regular.svg b/examples/blog/static/fonts/glyphicons-halflings-regular.svg new file mode 100644 index 000000000..94fb5490a --- /dev/null +++ b/examples/blog/static/fonts/glyphicons-halflings-regular.svg @@ -0,0 +1,288 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" > +<svg xmlns="http://www.w3.org/2000/svg"> +<metadata></metadata> +<defs> +<font id="glyphicons_halflingsregular" horiz-adv-x="1200" > +<font-face units-per-em="1200" ascent="960" descent="-240" /> +<missing-glyph horiz-adv-x="500" /> +<glyph horiz-adv-x="0" /> +<glyph horiz-adv-x="400" /> +<glyph unicode=" " /> +<glyph unicode="*" d="M600 1100q15 0 34 -1.5t30 -3.5l11 -1q10 -2 17.5 -10.5t7.5 -18.5v-224l158 158q7 7 18 8t19 -6l106 -106q7 -8 6 -19t-8 -18l-158 -158h224q10 0 18.5 -7.5t10.5 -17.5q6 -41 6 -75q0 -15 -1.5 -34t-3.5 -30l-1 -11q-2 -10 -10.5 -17.5t-18.5 -7.5h-224l158 -158 q7 -7 8 -18t-6 -19l-106 -106q-8 -7 -19 -6t-18 8l-158 158v-224q0 -10 -7.5 -18.5t-17.5 -10.5q-41 -6 -75 -6q-15 0 -34 1.5t-30 3.5l-11 1q-10 2 -17.5 10.5t-7.5 18.5v224l-158 -158q-7 -7 -18 -8t-19 6l-106 106q-7 8 -6 19t8 18l158 158h-224q-10 0 -18.5 7.5 t-10.5 17.5q-6 41 -6 75q0 15 1.5 34t3.5 30l1 11q2 10 10.5 17.5t18.5 7.5h224l-158 158q-7 7 -8 18t6 19l106 106q8 7 19 6t18 -8l158 -158v224q0 10 7.5 18.5t17.5 10.5q41 6 75 6z" /> +<glyph unicode="+" d="M450 1100h200q21 0 35.5 -14.5t14.5 -35.5v-350h350q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-350v-350q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v350h-350q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5 h350v350q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode=" " /> +<glyph unicode="¥" d="M825 1100h250q10 0 12.5 -5t-5.5 -13l-364 -364q-6 -6 -11 -18h268q10 0 13 -6t-3 -14l-120 -160q-6 -8 -18 -14t-22 -6h-125v-100h275q10 0 13 -6t-3 -14l-120 -160q-6 -8 -18 -14t-22 -6h-125v-174q0 -11 -7.5 -18.5t-18.5 -7.5h-148q-11 0 -18.5 7.5t-7.5 18.5v174 h-275q-10 0 -13 6t3 14l120 160q6 8 18 14t22 6h125v100h-275q-10 0 -13 6t3 14l120 160q6 8 18 14t22 6h118q-5 12 -11 18l-364 364q-8 8 -5.5 13t12.5 5h250q25 0 43 -18l164 -164q8 -8 18 -8t18 8l164 164q18 18 43 18z" /> +<glyph unicode=" " horiz-adv-x="650" /> +<glyph unicode=" " horiz-adv-x="1300" /> +<glyph unicode=" " horiz-adv-x="650" /> +<glyph unicode=" " horiz-adv-x="1300" /> +<glyph unicode=" " horiz-adv-x="433" /> +<glyph unicode=" " horiz-adv-x="325" /> +<glyph unicode=" " horiz-adv-x="216" /> +<glyph unicode=" " horiz-adv-x="216" /> +<glyph unicode=" " horiz-adv-x="162" /> +<glyph unicode=" " horiz-adv-x="260" /> +<glyph unicode=" " horiz-adv-x="72" /> +<glyph unicode=" " horiz-adv-x="260" /> +<glyph unicode=" " horiz-adv-x="325" /> +<glyph unicode="€" d="M744 1198q242 0 354 -189q60 -104 66 -209h-181q0 45 -17.5 82.5t-43.5 61.5t-58 40.5t-60.5 24t-51.5 7.5q-19 0 -40.5 -5.5t-49.5 -20.5t-53 -38t-49 -62.5t-39 -89.5h379l-100 -100h-300q-6 -50 -6 -100h406l-100 -100h-300q9 -74 33 -132t52.5 -91t61.5 -54.5t59 -29 t47 -7.5q22 0 50.5 7.5t60.5 24.5t58 41t43.5 61t17.5 80h174q-30 -171 -128 -278q-107 -117 -274 -117q-206 0 -324 158q-36 48 -69 133t-45 204h-217l100 100h112q1 47 6 100h-218l100 100h134q20 87 51 153.5t62 103.5q117 141 297 141z" /> +<glyph unicode="₽" d="M428 1200h350q67 0 120 -13t86 -31t57 -49.5t35 -56.5t17 -64.5t6.5 -60.5t0.5 -57v-16.5v-16.5q0 -36 -0.5 -57t-6.5 -61t-17 -65t-35 -57t-57 -50.5t-86 -31.5t-120 -13h-178l-2 -100h288q10 0 13 -6t-3 -14l-120 -160q-6 -8 -18 -14t-22 -6h-138v-175q0 -11 -5.5 -18 t-15.5 -7h-149q-10 0 -17.5 7.5t-7.5 17.5v175h-267q-10 0 -13 6t3 14l120 160q6 8 18 14t22 6h117v100h-267q-10 0 -13 6t3 14l120 160q6 8 18 14t22 6h117v475q0 10 7.5 17.5t17.5 7.5zM600 1000v-300h203q64 0 86.5 33t22.5 119q0 84 -22.5 116t-86.5 32h-203z" /> +<glyph unicode="−" d="M250 700h800q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="⌛" d="M1000 1200v-150q0 -21 -14.5 -35.5t-35.5 -14.5h-50v-100q0 -91 -49.5 -165.5t-130.5 -109.5q81 -35 130.5 -109.5t49.5 -165.5v-150h50q21 0 35.5 -14.5t14.5 -35.5v-150h-800v150q0 21 14.5 35.5t35.5 14.5h50v150q0 91 49.5 165.5t130.5 109.5q-81 35 -130.5 109.5 t-49.5 165.5v100h-50q-21 0 -35.5 14.5t-14.5 35.5v150h800zM400 1000v-100q0 -60 32.5 -109.5t87.5 -73.5q28 -12 44 -37t16 -55t-16 -55t-44 -37q-55 -24 -87.5 -73.5t-32.5 -109.5v-150h400v150q0 60 -32.5 109.5t-87.5 73.5q-28 12 -44 37t-16 55t16 55t44 37 q55 24 87.5 73.5t32.5 109.5v100h-400z" /> +<glyph unicode="◼" horiz-adv-x="500" d="M0 0z" /> +<glyph unicode="☁" d="M503 1089q110 0 200.5 -59.5t134.5 -156.5q44 14 90 14q120 0 205 -86.5t85 -206.5q0 -121 -85 -207.5t-205 -86.5h-750q-79 0 -135.5 57t-56.5 137q0 69 42.5 122.5t108.5 67.5q-2 12 -2 37q0 153 108 260.5t260 107.5z" /> +<glyph unicode="⛺" d="M774 1193.5q16 -9.5 20.5 -27t-5.5 -33.5l-136 -187l467 -746h30q20 0 35 -18.5t15 -39.5v-42h-1200v42q0 21 15 39.5t35 18.5h30l468 746l-135 183q-10 16 -5.5 34t20.5 28t34 5.5t28 -20.5l111 -148l112 150q9 16 27 20.5t34 -5zM600 200h377l-182 112l-195 534v-646z " /> +<glyph unicode="✉" d="M25 1100h1150q10 0 12.5 -5t-5.5 -13l-564 -567q-8 -8 -18 -8t-18 8l-564 567q-8 8 -5.5 13t12.5 5zM18 882l264 -264q8 -8 8 -18t-8 -18l-264 -264q-8 -8 -13 -5.5t-5 12.5v550q0 10 5 12.5t13 -5.5zM918 618l264 264q8 8 13 5.5t5 -12.5v-550q0 -10 -5 -12.5t-13 5.5 l-264 264q-8 8 -8 18t8 18zM818 482l364 -364q8 -8 5.5 -13t-12.5 -5h-1150q-10 0 -12.5 5t5.5 13l364 364q8 8 18 8t18 -8l164 -164q8 -8 18 -8t18 8l164 164q8 8 18 8t18 -8z" /> +<glyph unicode="✏" d="M1011 1210q19 0 33 -13l153 -153q13 -14 13 -33t-13 -33l-99 -92l-214 214l95 96q13 14 32 14zM1013 800l-615 -614l-214 214l614 614zM317 96l-333 -112l110 335z" /> +<glyph unicode="" d="M700 650v-550h250q21 0 35.5 -14.5t14.5 -35.5v-50h-800v50q0 21 14.5 35.5t35.5 14.5h250v550l-500 550h1200z" /> +<glyph unicode="" d="M368 1017l645 163q39 15 63 0t24 -49v-831q0 -55 -41.5 -95.5t-111.5 -63.5q-79 -25 -147 -4.5t-86 75t25.5 111.5t122.5 82q72 24 138 8v521l-600 -155v-606q0 -42 -44 -90t-109 -69q-79 -26 -147 -5.5t-86 75.5t25.5 111.5t122.5 82.5q72 24 138 7v639q0 38 14.5 59 t53.5 34z" /> +<glyph unicode="" d="M500 1191q100 0 191 -39t156.5 -104.5t104.5 -156.5t39 -191l-1 -2l1 -5q0 -141 -78 -262l275 -274q23 -26 22.5 -44.5t-22.5 -42.5l-59 -58q-26 -20 -46.5 -20t-39.5 20l-275 274q-119 -77 -261 -77l-5 1l-2 -1q-100 0 -191 39t-156.5 104.5t-104.5 156.5t-39 191 t39 191t104.5 156.5t156.5 104.5t191 39zM500 1022q-88 0 -162 -43t-117 -117t-43 -162t43 -162t117 -117t162 -43t162 43t117 117t43 162t-43 162t-117 117t-162 43z" /> +<glyph unicode="" d="M649 949q48 68 109.5 104t121.5 38.5t118.5 -20t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-150 152.5t-126.5 127.5t-93.5 124.5t-33.5 117.5q0 64 28 123t73 100.5t104 64t119 20 t120.5 -38.5t104.5 -104z" /> +<glyph unicode="" d="M407 800l131 353q7 19 17.5 19t17.5 -19l129 -353h421q21 0 24 -8.5t-14 -20.5l-342 -249l130 -401q7 -20 -0.5 -25.5t-24.5 6.5l-343 246l-342 -247q-17 -12 -24.5 -6.5t-0.5 25.5l130 400l-347 251q-17 12 -14 20.5t23 8.5h429z" /> +<glyph unicode="" d="M407 800l131 353q7 19 17.5 19t17.5 -19l129 -353h421q21 0 24 -8.5t-14 -20.5l-342 -249l130 -401q7 -20 -0.5 -25.5t-24.5 6.5l-343 246l-342 -247q-17 -12 -24.5 -6.5t-0.5 25.5l130 400l-347 251q-17 12 -14 20.5t23 8.5h429zM477 700h-240l197 -142l-74 -226 l193 139l195 -140l-74 229l192 140h-234l-78 211z" /> +<glyph unicode="" d="M600 1200q124 0 212 -88t88 -212v-250q0 -46 -31 -98t-69 -52v-75q0 -10 6 -21.5t15 -17.5l358 -230q9 -5 15 -16.5t6 -21.5v-93q0 -10 -7.5 -17.5t-17.5 -7.5h-1150q-10 0 -17.5 7.5t-7.5 17.5v93q0 10 6 21.5t15 16.5l358 230q9 6 15 17.5t6 21.5v75q-38 0 -69 52 t-31 98v250q0 124 88 212t212 88z" /> +<glyph unicode="" d="M25 1100h1150q10 0 17.5 -7.5t7.5 -17.5v-1050q0 -10 -7.5 -17.5t-17.5 -7.5h-1150q-10 0 -17.5 7.5t-7.5 17.5v1050q0 10 7.5 17.5t17.5 7.5zM100 1000v-100h100v100h-100zM875 1000h-550q-10 0 -17.5 -7.5t-7.5 -17.5v-350q0 -10 7.5 -17.5t17.5 -7.5h550 q10 0 17.5 7.5t7.5 17.5v350q0 10 -7.5 17.5t-17.5 7.5zM1000 1000v-100h100v100h-100zM100 800v-100h100v100h-100zM1000 800v-100h100v100h-100zM100 600v-100h100v100h-100zM1000 600v-100h100v100h-100zM875 500h-550q-10 0 -17.5 -7.5t-7.5 -17.5v-350q0 -10 7.5 -17.5 t17.5 -7.5h550q10 0 17.5 7.5t7.5 17.5v350q0 10 -7.5 17.5t-17.5 7.5zM100 400v-100h100v100h-100zM1000 400v-100h100v100h-100zM100 200v-100h100v100h-100zM1000 200v-100h100v100h-100z" /> +<glyph unicode="" d="M50 1100h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM650 1100h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400 q0 21 14.5 35.5t35.5 14.5zM50 500h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM650 500h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M50 1100h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM450 1100h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200 q0 21 14.5 35.5t35.5 14.5zM850 1100h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM50 700h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200 q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM450 700h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM850 700h200q21 0 35.5 -14.5t14.5 -35.5v-200 q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM50 300h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM450 300h200 q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM850 300h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5 t35.5 14.5z" /> +<glyph unicode="" d="M50 1100h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM450 1100h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5v200 q0 21 14.5 35.5t35.5 14.5zM50 700h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM450 700h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700 q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM50 300h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM450 300h700q21 0 35.5 -14.5t14.5 -35.5v-200 q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M465 477l571 571q8 8 18 8t17 -8l177 -177q8 -7 8 -17t-8 -18l-783 -784q-7 -8 -17.5 -8t-17.5 8l-384 384q-8 8 -8 18t8 17l177 177q7 8 17 8t18 -8l171 -171q7 -7 18 -7t18 7z" /> +<glyph unicode="" d="M904 1083l178 -179q8 -8 8 -18.5t-8 -17.5l-267 -268l267 -268q8 -7 8 -17.5t-8 -18.5l-178 -178q-8 -8 -18.5 -8t-17.5 8l-268 267l-268 -267q-7 -8 -17.5 -8t-18.5 8l-178 178q-8 8 -8 18.5t8 17.5l267 268l-267 268q-8 7 -8 17.5t8 18.5l178 178q8 8 18.5 8t17.5 -8 l268 -267l268 268q7 7 17.5 7t18.5 -7z" /> +<glyph unicode="" d="M507 1177q98 0 187.5 -38.5t154.5 -103.5t103.5 -154.5t38.5 -187.5q0 -141 -78 -262l300 -299q8 -8 8 -18.5t-8 -18.5l-109 -108q-7 -8 -17.5 -8t-18.5 8l-300 299q-119 -77 -261 -77q-98 0 -188 38.5t-154.5 103t-103 154.5t-38.5 188t38.5 187.5t103 154.5 t154.5 103.5t188 38.5zM506.5 1023q-89.5 0 -165.5 -44t-120 -120.5t-44 -166t44 -165.5t120 -120t165.5 -44t166 44t120.5 120t44 165.5t-44 166t-120.5 120.5t-166 44zM425 900h150q10 0 17.5 -7.5t7.5 -17.5v-75h75q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5 t-17.5 -7.5h-75v-75q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v75h-75q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5h75v75q0 10 7.5 17.5t17.5 7.5z" /> +<glyph unicode="" d="M507 1177q98 0 187.5 -38.5t154.5 -103.5t103.5 -154.5t38.5 -187.5q0 -141 -78 -262l300 -299q8 -8 8 -18.5t-8 -18.5l-109 -108q-7 -8 -17.5 -8t-18.5 8l-300 299q-119 -77 -261 -77q-98 0 -188 38.5t-154.5 103t-103 154.5t-38.5 188t38.5 187.5t103 154.5 t154.5 103.5t188 38.5zM506.5 1023q-89.5 0 -165.5 -44t-120 -120.5t-44 -166t44 -165.5t120 -120t165.5 -44t166 44t120.5 120t44 165.5t-44 166t-120.5 120.5t-166 44zM325 800h350q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-350q-10 0 -17.5 7.5 t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5z" /> +<glyph unicode="" d="M550 1200h100q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM800 975v166q167 -62 272 -209.5t105 -331.5q0 -117 -45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5 t-184.5 123t-123 184.5t-45.5 224q0 184 105 331.5t272 209.5v-166q-103 -55 -165 -155t-62 -220q0 -116 57 -214.5t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5q0 120 -62 220t-165 155z" /> +<glyph unicode="" d="M1025 1200h150q10 0 17.5 -7.5t7.5 -17.5v-1150q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v1150q0 10 7.5 17.5t17.5 7.5zM725 800h150q10 0 17.5 -7.5t7.5 -17.5v-750q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v750 q0 10 7.5 17.5t17.5 7.5zM425 500h150q10 0 17.5 -7.5t7.5 -17.5v-450q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v450q0 10 7.5 17.5t17.5 7.5zM125 300h150q10 0 17.5 -7.5t7.5 -17.5v-250q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5 v250q0 10 7.5 17.5t17.5 7.5z" /> +<glyph unicode="" d="M600 1174q33 0 74 -5l38 -152l5 -1q49 -14 94 -39l5 -2l134 80q61 -48 104 -105l-80 -134l3 -5q25 -44 39 -93l1 -6l152 -38q5 -43 5 -73q0 -34 -5 -74l-152 -38l-1 -6q-15 -49 -39 -93l-3 -5l80 -134q-48 -61 -104 -105l-134 81l-5 -3q-44 -25 -94 -39l-5 -2l-38 -151 q-43 -5 -74 -5q-33 0 -74 5l-38 151l-5 2q-49 14 -94 39l-5 3l-134 -81q-60 48 -104 105l80 134l-3 5q-25 45 -38 93l-2 6l-151 38q-6 42 -6 74q0 33 6 73l151 38l2 6q13 48 38 93l3 5l-80 134q47 61 105 105l133 -80l5 2q45 25 94 39l5 1l38 152q43 5 74 5zM600 815 q-89 0 -152 -63t-63 -151.5t63 -151.5t152 -63t152 63t63 151.5t-63 151.5t-152 63z" /> +<glyph unicode="" d="M500 1300h300q41 0 70.5 -29.5t29.5 -70.5v-100h275q10 0 17.5 -7.5t7.5 -17.5v-75h-1100v75q0 10 7.5 17.5t17.5 7.5h275v100q0 41 29.5 70.5t70.5 29.5zM500 1200v-100h300v100h-300zM1100 900v-800q0 -41 -29.5 -70.5t-70.5 -29.5h-700q-41 0 -70.5 29.5t-29.5 70.5 v800h900zM300 800v-700h100v700h-100zM500 800v-700h100v700h-100zM700 800v-700h100v700h-100zM900 800v-700h100v700h-100z" /> +<glyph unicode="" d="M18 618l620 608q8 7 18.5 7t17.5 -7l608 -608q8 -8 5.5 -13t-12.5 -5h-175v-575q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v375h-300v-375q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v575h-175q-10 0 -12.5 5t5.5 13z" /> +<glyph unicode="" d="M600 1200v-400q0 -41 29.5 -70.5t70.5 -29.5h300v-650q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v1100q0 21 14.5 35.5t35.5 14.5h450zM1000 800h-250q-21 0 -35.5 14.5t-14.5 35.5v250z" /> +<glyph unicode="" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5t-57 214.5t-155.5 155.5t-214.5 57zM525 900h50q10 0 17.5 -7.5t7.5 -17.5v-275h175q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v350q0 10 7.5 17.5t17.5 7.5z" /> +<glyph unicode="" d="M1300 0h-538l-41 400h-242l-41 -400h-538l431 1200h209l-21 -300h162l-20 300h208zM515 800l-27 -300h224l-27 300h-170z" /> +<glyph unicode="" d="M550 1200h200q21 0 35.5 -14.5t14.5 -35.5v-450h191q20 0 25.5 -11.5t-7.5 -27.5l-327 -400q-13 -16 -32 -16t-32 16l-327 400q-13 16 -7.5 27.5t25.5 11.5h191v450q0 21 14.5 35.5t35.5 14.5zM1125 400h50q10 0 17.5 -7.5t7.5 -17.5v-350q0 -10 -7.5 -17.5t-17.5 -7.5 h-1050q-10 0 -17.5 7.5t-7.5 17.5v350q0 10 7.5 17.5t17.5 7.5h50q10 0 17.5 -7.5t7.5 -17.5v-175h900v175q0 10 7.5 17.5t17.5 7.5z" /> +<glyph unicode="" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5t-57 214.5t-155.5 155.5t-214.5 57zM525 900h150q10 0 17.5 -7.5t7.5 -17.5v-275h137q21 0 26 -11.5t-8 -27.5l-223 -275q-13 -16 -32 -16t-32 16l-223 275q-13 16 -8 27.5t26 11.5h137v275q0 10 7.5 17.5t17.5 7.5z " /> +<glyph unicode="" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5t-57 214.5t-155.5 155.5t-214.5 57zM632 914l223 -275q13 -16 8 -27.5t-26 -11.5h-137v-275q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v275h-137q-21 0 -26 11.5t8 27.5l223 275q13 16 32 16 t32 -16z" /> +<glyph unicode="" d="M225 1200h750q10 0 19.5 -7t12.5 -17l186 -652q7 -24 7 -49v-425q0 -12 -4 -27t-9 -17q-12 -6 -37 -6h-1100q-12 0 -27 4t-17 8q-6 13 -6 38l1 425q0 25 7 49l185 652q3 10 12.5 17t19.5 7zM878 1000h-556q-10 0 -19 -7t-11 -18l-87 -450q-2 -11 4 -18t16 -7h150 q10 0 19.5 -7t11.5 -17l38 -152q2 -10 11.5 -17t19.5 -7h250q10 0 19.5 7t11.5 17l38 152q2 10 11.5 17t19.5 7h150q10 0 16 7t4 18l-87 450q-2 11 -11 18t-19 7z" /> +<glyph unicode="" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5t-57 214.5t-155.5 155.5t-214.5 57zM540 820l253 -190q17 -12 17 -30t-17 -30l-253 -190q-16 -12 -28 -6.5t-12 26.5v400q0 21 12 26.5t28 -6.5z" /> +<glyph unicode="" d="M947 1060l135 135q7 7 12.5 5t5.5 -13v-362q0 -10 -7.5 -17.5t-17.5 -7.5h-362q-11 0 -13 5.5t5 12.5l133 133q-109 76 -238 76q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5h150q0 -117 -45.5 -224 t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5q192 0 347 -117z" /> +<glyph unicode="" d="M947 1060l135 135q7 7 12.5 5t5.5 -13v-361q0 -11 -7.5 -18.5t-18.5 -7.5h-361q-11 0 -13 5.5t5 12.5l134 134q-110 75 -239 75q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5h-150q0 117 45.5 224t123 184.5t184.5 123t224 45.5q192 0 347 -117zM1027 600h150 q0 -117 -45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5q-192 0 -348 118l-134 -134q-7 -8 -12.5 -5.5t-5.5 12.5v360q0 11 7.5 18.5t18.5 7.5h360q10 0 12.5 -5.5t-5.5 -12.5l-133 -133q110 -76 240 -76q116 0 214.5 57t155.5 155.5t57 214.5z" /> +<glyph unicode="" d="M125 1200h1050q10 0 17.5 -7.5t7.5 -17.5v-1150q0 -10 -7.5 -17.5t-17.5 -7.5h-1050q-10 0 -17.5 7.5t-7.5 17.5v1150q0 10 7.5 17.5t17.5 7.5zM1075 1000h-850q-10 0 -17.5 -7.5t-7.5 -17.5v-850q0 -10 7.5 -17.5t17.5 -7.5h850q10 0 17.5 7.5t7.5 17.5v850 q0 10 -7.5 17.5t-17.5 7.5zM325 900h50q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-50q-10 0 -17.5 7.5t-7.5 17.5v50q0 10 7.5 17.5t17.5 7.5zM525 900h450q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-450q-10 0 -17.5 7.5t-7.5 17.5v50 q0 10 7.5 17.5t17.5 7.5zM325 700h50q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-50q-10 0 -17.5 7.5t-7.5 17.5v50q0 10 7.5 17.5t17.5 7.5zM525 700h450q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-450q-10 0 -17.5 7.5t-7.5 17.5v50 q0 10 7.5 17.5t17.5 7.5zM325 500h50q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-50q-10 0 -17.5 7.5t-7.5 17.5v50q0 10 7.5 17.5t17.5 7.5zM525 500h450q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-450q-10 0 -17.5 7.5t-7.5 17.5v50 q0 10 7.5 17.5t17.5 7.5zM325 300h50q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-50q-10 0 -17.5 7.5t-7.5 17.5v50q0 10 7.5 17.5t17.5 7.5zM525 300h450q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-450q-10 0 -17.5 7.5t-7.5 17.5v50 q0 10 7.5 17.5t17.5 7.5z" /> +<glyph unicode="" d="M900 800v200q0 83 -58.5 141.5t-141.5 58.5h-300q-82 0 -141 -59t-59 -141v-200h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-600q0 -41 29.5 -70.5t70.5 -29.5h900q41 0 70.5 29.5t29.5 70.5v600q0 41 -29.5 70.5t-70.5 29.5h-100zM400 800v150q0 21 15 35.5t35 14.5h200 q20 0 35 -14.5t15 -35.5v-150h-300z" /> +<glyph unicode="" d="M125 1100h50q10 0 17.5 -7.5t7.5 -17.5v-1075h-100v1075q0 10 7.5 17.5t17.5 7.5zM1075 1052q4 0 9 -2q16 -6 16 -23v-421q0 -6 -3 -12q-33 -59 -66.5 -99t-65.5 -58t-56.5 -24.5t-52.5 -6.5q-26 0 -57.5 6.5t-52.5 13.5t-60 21q-41 15 -63 22.5t-57.5 15t-65.5 7.5 q-85 0 -160 -57q-7 -5 -15 -5q-6 0 -11 3q-14 7 -14 22v438q22 55 82 98.5t119 46.5q23 2 43 0.5t43 -7t32.5 -8.5t38 -13t32.5 -11q41 -14 63.5 -21t57 -14t63.5 -7q103 0 183 87q7 8 18 8z" /> +<glyph unicode="" d="M600 1175q116 0 227 -49.5t192.5 -131t131 -192.5t49.5 -227v-300q0 -10 -7.5 -17.5t-17.5 -7.5h-50q-10 0 -17.5 7.5t-7.5 17.5v300q0 127 -70.5 231.5t-184.5 161.5t-245 57t-245 -57t-184.5 -161.5t-70.5 -231.5v-300q0 -10 -7.5 -17.5t-17.5 -7.5h-50 q-10 0 -17.5 7.5t-7.5 17.5v300q0 116 49.5 227t131 192.5t192.5 131t227 49.5zM220 500h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14v460q0 8 6 14t14 6zM820 500h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14v460 q0 8 6 14t14 6z" /> +<glyph unicode="" d="M321 814l258 172q9 6 15 2.5t6 -13.5v-750q0 -10 -6 -13.5t-15 2.5l-258 172q-21 14 -46 14h-250q-10 0 -17.5 7.5t-7.5 17.5v350q0 10 7.5 17.5t17.5 7.5h250q25 0 46 14zM900 668l120 120q7 7 17 7t17 -7l34 -34q7 -7 7 -17t-7 -17l-120 -120l120 -120q7 -7 7 -17 t-7 -17l-34 -34q-7 -7 -17 -7t-17 7l-120 119l-120 -119q-7 -7 -17 -7t-17 7l-34 34q-7 7 -7 17t7 17l119 120l-119 120q-7 7 -7 17t7 17l34 34q7 8 17 8t17 -8z" /> +<glyph unicode="" d="M321 814l258 172q9 6 15 2.5t6 -13.5v-750q0 -10 -6 -13.5t-15 2.5l-258 172q-21 14 -46 14h-250q-10 0 -17.5 7.5t-7.5 17.5v350q0 10 7.5 17.5t17.5 7.5h250q25 0 46 14zM766 900h4q10 -1 16 -10q96 -129 96 -290q0 -154 -90 -281q-6 -9 -17 -10l-3 -1q-9 0 -16 6 l-29 23q-7 7 -8.5 16.5t4.5 17.5q72 103 72 229q0 132 -78 238q-6 8 -4.5 18t9.5 17l29 22q7 5 15 5z" /> +<glyph unicode="" d="M967 1004h3q11 -1 17 -10q135 -179 135 -396q0 -105 -34 -206.5t-98 -185.5q-7 -9 -17 -10h-3q-9 0 -16 6l-42 34q-8 6 -9 16t5 18q111 150 111 328q0 90 -29.5 176t-84.5 157q-6 9 -5 19t10 16l42 33q7 5 15 5zM321 814l258 172q9 6 15 2.5t6 -13.5v-750q0 -10 -6 -13.5 t-15 2.5l-258 172q-21 14 -46 14h-250q-10 0 -17.5 7.5t-7.5 17.5v350q0 10 7.5 17.5t17.5 7.5h250q25 0 46 14zM766 900h4q10 -1 16 -10q96 -129 96 -290q0 -154 -90 -281q-6 -9 -17 -10l-3 -1q-9 0 -16 6l-29 23q-7 7 -8.5 16.5t4.5 17.5q72 103 72 229q0 132 -78 238 q-6 8 -4.5 18.5t9.5 16.5l29 22q7 5 15 5z" /> +<glyph unicode="" d="M500 900h100v-100h-100v-100h-400v-100h-100v600h500v-300zM1200 700h-200v-100h200v-200h-300v300h-200v300h-100v200h600v-500zM100 1100v-300h300v300h-300zM800 1100v-300h300v300h-300zM300 900h-100v100h100v-100zM1000 900h-100v100h100v-100zM300 500h200v-500 h-500v500h200v100h100v-100zM800 300h200v-100h-100v-100h-200v100h-100v100h100v200h-200v100h300v-300zM100 400v-300h300v300h-300zM300 200h-100v100h100v-100zM1200 200h-100v100h100v-100zM700 0h-100v100h100v-100zM1200 0h-300v100h300v-100z" /> +<glyph unicode="" d="M100 200h-100v1000h100v-1000zM300 200h-100v1000h100v-1000zM700 200h-200v1000h200v-1000zM900 200h-100v1000h100v-1000zM1200 200h-200v1000h200v-1000zM400 0h-300v100h300v-100zM600 0h-100v91h100v-91zM800 0h-100v91h100v-91zM1100 0h-200v91h200v-91z" /> +<glyph unicode="" d="M500 1200l682 -682q8 -8 8 -18t-8 -18l-464 -464q-8 -8 -18 -8t-18 8l-682 682l1 475q0 10 7.5 17.5t17.5 7.5h474zM319.5 1024.5q-29.5 29.5 -71 29.5t-71 -29.5t-29.5 -71.5t29.5 -71.5t71 -29.5t71 29.5t29.5 71.5t-29.5 71.5z" /> +<glyph unicode="" d="M500 1200l682 -682q8 -8 8 -18t-8 -18l-464 -464q-8 -8 -18 -8t-18 8l-682 682l1 475q0 10 7.5 17.5t17.5 7.5h474zM800 1200l682 -682q8 -8 8 -18t-8 -18l-464 -464q-8 -8 -18 -8t-18 8l-56 56l424 426l-700 700h150zM319.5 1024.5q-29.5 29.5 -71 29.5t-71 -29.5 t-29.5 -71.5t29.5 -71.5t71 -29.5t71 29.5t29.5 71.5t-29.5 71.5z" /> +<glyph unicode="" d="M300 1200h825q75 0 75 -75v-900q0 -25 -18 -43l-64 -64q-8 -8 -13 -5.5t-5 12.5v950q0 10 -7.5 17.5t-17.5 7.5h-700q-25 0 -43 -18l-64 -64q-8 -8 -5.5 -13t12.5 -5h700q10 0 17.5 -7.5t7.5 -17.5v-950q0 -10 -7.5 -17.5t-17.5 -7.5h-850q-10 0 -17.5 7.5t-7.5 17.5v975 q0 25 18 43l139 139q18 18 43 18z" /> +<glyph unicode="" d="M250 1200h800q21 0 35.5 -14.5t14.5 -35.5v-1150l-450 444l-450 -445v1151q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M822 1200h-444q-11 0 -19 -7.5t-9 -17.5l-78 -301q-7 -24 7 -45l57 -108q6 -9 17.5 -15t21.5 -6h450q10 0 21.5 6t17.5 15l62 108q14 21 7 45l-83 301q-1 10 -9 17.5t-19 7.5zM1175 800h-150q-10 0 -21 -6.5t-15 -15.5l-78 -156q-4 -9 -15 -15.5t-21 -6.5h-550 q-10 0 -21 6.5t-15 15.5l-78 156q-4 9 -15 15.5t-21 6.5h-150q-10 0 -17.5 -7.5t-7.5 -17.5v-650q0 -10 7.5 -17.5t17.5 -7.5h150q10 0 17.5 7.5t7.5 17.5v150q0 10 7.5 17.5t17.5 7.5h750q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 7.5 -17.5t17.5 -7.5h150q10 0 17.5 7.5 t7.5 17.5v650q0 10 -7.5 17.5t-17.5 7.5zM850 200h-500q-10 0 -19.5 -7t-11.5 -17l-38 -152q-2 -10 3.5 -17t15.5 -7h600q10 0 15.5 7t3.5 17l-38 152q-2 10 -11.5 17t-19.5 7z" /> +<glyph unicode="" d="M500 1100h200q56 0 102.5 -20.5t72.5 -50t44 -59t25 -50.5l6 -20h150q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5v600q0 41 29.5 70.5t70.5 29.5h150q2 8 6.5 21.5t24 48t45 61t72 48t102.5 21.5zM900 800v-100 h100v100h-100zM600 730q-95 0 -162.5 -67.5t-67.5 -162.5t67.5 -162.5t162.5 -67.5t162.5 67.5t67.5 162.5t-67.5 162.5t-162.5 67.5zM600 603q43 0 73 -30t30 -73t-30 -73t-73 -30t-73 30t-30 73t30 73t73 30z" /> +<glyph unicode="" d="M681 1199l385 -998q20 -50 60 -92q18 -19 36.5 -29.5t27.5 -11.5l10 -2v-66h-417v66q53 0 75 43.5t5 88.5l-82 222h-391q-58 -145 -92 -234q-11 -34 -6.5 -57t25.5 -37t46 -20t55 -6v-66h-365v66q56 24 84 52q12 12 25 30.5t20 31.5l7 13l399 1006h93zM416 521h340 l-162 457z" /> +<glyph unicode="" d="M753 641q5 -1 14.5 -4.5t36 -15.5t50.5 -26.5t53.5 -40t50.5 -54.5t35.5 -70t14.5 -87q0 -67 -27.5 -125.5t-71.5 -97.5t-98.5 -66.5t-108.5 -40.5t-102 -13h-500v89q41 7 70.5 32.5t29.5 65.5v827q0 24 -0.5 34t-3.5 24t-8.5 19.5t-17 13.5t-28 12.5t-42.5 11.5v71 l471 -1q57 0 115.5 -20.5t108 -57t80.5 -94t31 -124.5q0 -51 -15.5 -96.5t-38 -74.5t-45 -50.5t-38.5 -30.5zM400 700h139q78 0 130.5 48.5t52.5 122.5q0 41 -8.5 70.5t-29.5 55.5t-62.5 39.5t-103.5 13.5h-118v-350zM400 200h216q80 0 121 50.5t41 130.5q0 90 -62.5 154.5 t-156.5 64.5h-159v-400z" /> +<glyph unicode="" d="M877 1200l2 -57q-83 -19 -116 -45.5t-40 -66.5l-132 -839q-9 -49 13 -69t96 -26v-97h-500v97q186 16 200 98l173 832q3 17 3 30t-1.5 22.5t-9 17.5t-13.5 12.5t-21.5 10t-26 8.5t-33.5 10q-13 3 -19 5v57h425z" /> +<glyph unicode="" d="M1300 900h-50q0 21 -4 37t-9.5 26.5t-18 17.5t-22 11t-28.5 5.5t-31 2t-37 0.5h-200v-850q0 -22 25 -34.5t50 -13.5l25 -2v-100h-400v100q4 0 11 0.5t24 3t30 7t24 15t11 24.5v850h-200q-25 0 -37 -0.5t-31 -2t-28.5 -5.5t-22 -11t-18 -17.5t-9.5 -26.5t-4 -37h-50v300 h1000v-300zM175 1000h-75v-800h75l-125 -167l-125 167h75v800h-75l125 167z" /> +<glyph unicode="" d="M1100 900h-50q0 21 -4 37t-9.5 26.5t-18 17.5t-22 11t-28.5 5.5t-31 2t-37 0.5h-200v-650q0 -22 25 -34.5t50 -13.5l25 -2v-100h-400v100q4 0 11 0.5t24 3t30 7t24 15t11 24.5v650h-200q-25 0 -37 -0.5t-31 -2t-28.5 -5.5t-22 -11t-18 -17.5t-9.5 -26.5t-4 -37h-50v300 h1000v-300zM1167 50l-167 -125v75h-800v-75l-167 125l167 125v-75h800v75z" /> +<glyph unicode="" d="M50 1100h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-600q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 800h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5v100 q0 21 14.5 35.5t35.5 14.5zM50 500h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 200h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M250 1100h700q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 800h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v100 q0 21 14.5 35.5t35.5 14.5zM250 500h700q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 200h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M500 950v100q0 21 14.5 35.5t35.5 14.5h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-600q-21 0 -35.5 14.5t-14.5 35.5zM100 650v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000 q-21 0 -35.5 14.5t-14.5 35.5zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5zM0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100 q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5z" /> +<glyph unicode="" d="M50 1100h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 800h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v100 q0 21 14.5 35.5t35.5 14.5zM50 500h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 200h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M50 1100h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM350 1100h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v100 q0 21 14.5 35.5t35.5 14.5zM50 800h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM350 800h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-800 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 500h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM350 500h800q21 0 35.5 -14.5t14.5 -35.5v-100 q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 200h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM350 200h800 q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M400 0h-100v1100h100v-1100zM550 1100h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM550 800h500q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-500 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM267 550l-167 -125v75h-200v100h200v75zM550 500h300q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-300q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM550 200h600 q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-600q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M50 1100h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM900 0h-100v1100h100v-1100zM50 800h500q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-500 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM1100 600h200v-100h-200v-75l-167 125l167 125v-75zM50 500h300q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-300q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 200h600 q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-600q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M75 1000h750q31 0 53 -22t22 -53v-650q0 -31 -22 -53t-53 -22h-750q-31 0 -53 22t-22 53v650q0 31 22 53t53 22zM1200 300l-300 300l300 300v-600z" /> +<glyph unicode="" d="M44 1100h1112q18 0 31 -13t13 -31v-1012q0 -18 -13 -31t-31 -13h-1112q-18 0 -31 13t-13 31v1012q0 18 13 31t31 13zM100 1000v-737l247 182l298 -131l-74 156l293 318l236 -288v500h-1000zM342 884q56 0 95 -39t39 -94.5t-39 -95t-95 -39.5t-95 39.5t-39 95t39 94.5 t95 39z" /> +<glyph unicode="" d="M648 1169q117 0 216 -60t156.5 -161t57.5 -218q0 -115 -70 -258q-69 -109 -158 -225.5t-143 -179.5l-54 -62q-9 8 -25.5 24.5t-63.5 67.5t-91 103t-98.5 128t-95.5 148q-60 132 -60 249q0 88 34 169.5t91.5 142t137 96.5t166.5 36zM652.5 974q-91.5 0 -156.5 -65 t-65 -157t65 -156.5t156.5 -64.5t156.5 64.5t65 156.5t-65 157t-156.5 65z" /> +<glyph unicode="" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 173v854q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57z" /> +<glyph unicode="" d="M554 1295q21 -72 57.5 -143.5t76 -130t83 -118t82.5 -117t70 -116t49.5 -126t18.5 -136.5q0 -71 -25.5 -135t-68.5 -111t-99 -82t-118.5 -54t-125.5 -23q-84 5 -161.5 34t-139.5 78.5t-99 125t-37 164.5q0 69 18 136.5t49.5 126.5t69.5 116.5t81.5 117.5t83.5 119 t76.5 131t58.5 143zM344 710q-23 -33 -43.5 -70.5t-40.5 -102.5t-17 -123q1 -37 14.5 -69.5t30 -52t41 -37t38.5 -24.5t33 -15q21 -7 32 -1t13 22l6 34q2 10 -2.5 22t-13.5 19q-5 4 -14 12t-29.5 40.5t-32.5 73.5q-26 89 6 271q2 11 -6 11q-8 1 -15 -10z" /> +<glyph unicode="" d="M1000 1013l108 115q2 1 5 2t13 2t20.5 -1t25 -9.5t28.5 -21.5q22 -22 27 -43t0 -32l-6 -10l-108 -115zM350 1100h400q50 0 105 -13l-187 -187h-368q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v182l200 200v-332 q0 -165 -93.5 -257.5t-256.5 -92.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400q0 165 92.5 257.5t257.5 92.5zM1009 803l-362 -362l-161 -50l55 170l355 355z" /> +<glyph unicode="" d="M350 1100h361q-164 -146 -216 -200h-195q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5l200 153v-103q0 -165 -92.5 -257.5t-257.5 -92.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400q0 165 92.5 257.5t257.5 92.5z M824 1073l339 -301q8 -7 8 -17.5t-8 -17.5l-340 -306q-7 -6 -12.5 -4t-6.5 11v203q-26 1 -54.5 0t-78.5 -7.5t-92 -17.5t-86 -35t-70 -57q10 59 33 108t51.5 81.5t65 58.5t68.5 40.5t67 24.5t56 13.5t40 4.5v210q1 10 6.5 12.5t13.5 -4.5z" /> +<glyph unicode="" d="M350 1100h350q60 0 127 -23l-178 -177h-349q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v69l200 200v-219q0 -165 -92.5 -257.5t-257.5 -92.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400q0 165 92.5 257.5t257.5 92.5z M643 639l395 395q7 7 17.5 7t17.5 -7l101 -101q7 -7 7 -17.5t-7 -17.5l-531 -532q-7 -7 -17.5 -7t-17.5 7l-248 248q-7 7 -7 17.5t7 17.5l101 101q7 7 17.5 7t17.5 -7l111 -111q8 -7 18 -7t18 7z" /> +<glyph unicode="" d="M318 918l264 264q8 8 18 8t18 -8l260 -264q7 -8 4.5 -13t-12.5 -5h-170v-200h200v173q0 10 5 12t13 -5l264 -260q8 -7 8 -17.5t-8 -17.5l-264 -265q-8 -7 -13 -5t-5 12v173h-200v-200h170q10 0 12.5 -5t-4.5 -13l-260 -264q-8 -8 -18 -8t-18 8l-264 264q-8 8 -5.5 13 t12.5 5h175v200h-200v-173q0 -10 -5 -12t-13 5l-264 265q-8 7 -8 17.5t8 17.5l264 260q8 7 13 5t5 -12v-173h200v200h-175q-10 0 -12.5 5t5.5 13z" /> +<glyph unicode="" d="M250 1100h100q21 0 35.5 -14.5t14.5 -35.5v-438l464 453q15 14 25.5 10t10.5 -25v-1000q0 -21 -10.5 -25t-25.5 10l-464 453v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v1000q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M50 1100h100q21 0 35.5 -14.5t14.5 -35.5v-438l464 453q15 14 25.5 10t10.5 -25v-438l464 453q15 14 25.5 10t10.5 -25v-1000q0 -21 -10.5 -25t-25.5 10l-464 453v-438q0 -21 -10.5 -25t-25.5 10l-464 453v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5 t-14.5 35.5v1000q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M1200 1050v-1000q0 -21 -10.5 -25t-25.5 10l-464 453v-438q0 -21 -10.5 -25t-25.5 10l-492 480q-15 14 -15 35t15 35l492 480q15 14 25.5 10t10.5 -25v-438l464 453q15 14 25.5 10t10.5 -25z" /> +<glyph unicode="" d="M243 1074l814 -498q18 -11 18 -26t-18 -26l-814 -498q-18 -11 -30.5 -4t-12.5 28v1000q0 21 12.5 28t30.5 -4z" /> +<glyph unicode="" d="M250 1000h200q21 0 35.5 -14.5t14.5 -35.5v-800q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v800q0 21 14.5 35.5t35.5 14.5zM650 1000h200q21 0 35.5 -14.5t14.5 -35.5v-800q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v800 q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M1100 950v-800q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v800q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5z" /> +<glyph unicode="" d="M500 612v438q0 21 10.5 25t25.5 -10l492 -480q15 -14 15 -35t-15 -35l-492 -480q-15 -14 -25.5 -10t-10.5 25v438l-464 -453q-15 -14 -25.5 -10t-10.5 25v1000q0 21 10.5 25t25.5 -10z" /> +<glyph unicode="" d="M1048 1102l100 1q20 0 35 -14.5t15 -35.5l5 -1000q0 -21 -14.5 -35.5t-35.5 -14.5l-100 -1q-21 0 -35.5 14.5t-14.5 35.5l-2 437l-463 -454q-14 -15 -24.5 -10.5t-10.5 25.5l-2 437l-462 -455q-15 -14 -25.5 -9.5t-10.5 24.5l-5 1000q0 21 10.5 25.5t25.5 -10.5l466 -450 l-2 438q0 20 10.5 24.5t25.5 -9.5l466 -451l-2 438q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M850 1100h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-464 -453q-15 -14 -25.5 -10t-10.5 25v1000q0 21 10.5 25t25.5 -10l464 -453v438q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M686 1081l501 -540q15 -15 10.5 -26t-26.5 -11h-1042q-22 0 -26.5 11t10.5 26l501 540q15 15 36 15t36 -15zM150 400h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M885 900l-352 -353l352 -353l-197 -198l-552 552l552 550z" /> +<glyph unicode="" d="M1064 547l-551 -551l-198 198l353 353l-353 353l198 198z" /> +<glyph unicode="" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM650 900h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-150h-150 q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -21 14.5 -35.5t35.5 -14.5h150v-150q0 -21 14.5 -35.5t35.5 -14.5h100q21 0 35.5 14.5t14.5 35.5v150h150q21 0 35.5 14.5t14.5 35.5v100q0 21 -14.5 35.5t-35.5 14.5h-150v150q0 21 -14.5 35.5t-35.5 14.5z" /> +<glyph unicode="" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM850 700h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -21 14.5 -35.5 t35.5 -14.5h500q21 0 35.5 14.5t14.5 35.5v100q0 21 -14.5 35.5t-35.5 14.5z" /> +<glyph unicode="" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM741.5 913q-12.5 0 -21.5 -9l-120 -120l-120 120q-9 9 -21.5 9 t-21.5 -9l-141 -141q-9 -9 -9 -21.5t9 -21.5l120 -120l-120 -120q-9 -9 -9 -21.5t9 -21.5l141 -141q9 -9 21.5 -9t21.5 9l120 120l120 -120q9 -9 21.5 -9t21.5 9l141 141q9 9 9 21.5t-9 21.5l-120 120l120 120q9 9 9 21.5t-9 21.5l-141 141q-9 9 -21.5 9z" /> +<glyph unicode="" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM546 623l-84 85q-7 7 -17.5 7t-18.5 -7l-139 -139q-7 -8 -7 -18t7 -18 l242 -241q7 -8 17.5 -8t17.5 8l375 375q7 7 7 17.5t-7 18.5l-139 139q-7 7 -17.5 7t-17.5 -7z" /> +<glyph unicode="" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM588 941q-29 0 -59 -5.5t-63 -20.5t-58 -38.5t-41.5 -63t-16.5 -89.5 q0 -25 20 -25h131q30 -5 35 11q6 20 20.5 28t45.5 8q20 0 31.5 -10.5t11.5 -28.5q0 -23 -7 -34t-26 -18q-1 0 -13.5 -4t-19.5 -7.5t-20 -10.5t-22 -17t-18.5 -24t-15.5 -35t-8 -46q-1 -8 5.5 -16.5t20.5 -8.5h173q7 0 22 8t35 28t37.5 48t29.5 74t12 100q0 47 -17 83 t-42.5 57t-59.5 34.5t-64 18t-59 4.5zM675 400h-150q-10 0 -17.5 -7.5t-7.5 -17.5v-150q0 -10 7.5 -17.5t17.5 -7.5h150q10 0 17.5 7.5t7.5 17.5v150q0 10 -7.5 17.5t-17.5 7.5z" /> +<glyph unicode="" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM675 1000h-150q-10 0 -17.5 -7.5t-7.5 -17.5v-150q0 -10 7.5 -17.5 t17.5 -7.5h150q10 0 17.5 7.5t7.5 17.5v150q0 10 -7.5 17.5t-17.5 7.5zM675 700h-250q-10 0 -17.5 -7.5t-7.5 -17.5v-50q0 -10 7.5 -17.5t17.5 -7.5h75v-200h-75q-10 0 -17.5 -7.5t-7.5 -17.5v-50q0 -10 7.5 -17.5t17.5 -7.5h350q10 0 17.5 7.5t7.5 17.5v50q0 10 -7.5 17.5 t-17.5 7.5h-75v275q0 10 -7.5 17.5t-17.5 7.5z" /> +<glyph unicode="" d="M525 1200h150q10 0 17.5 -7.5t7.5 -17.5v-194q103 -27 178.5 -102.5t102.5 -178.5h194q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-194q-27 -103 -102.5 -178.5t-178.5 -102.5v-194q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v194 q-103 27 -178.5 102.5t-102.5 178.5h-194q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5h194q27 103 102.5 178.5t178.5 102.5v194q0 10 7.5 17.5t17.5 7.5zM700 893v-168q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v168q-68 -23 -119 -74 t-74 -119h168q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-168q23 -68 74 -119t119 -74v168q0 10 7.5 17.5t17.5 7.5h150q10 0 17.5 -7.5t7.5 -17.5v-168q68 23 119 74t74 119h-168q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5h168 q-23 68 -74 119t-119 74z" /> +<glyph unicode="" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5t-57 214.5t-155.5 155.5t-214.5 57zM759 823l64 -64q7 -7 7 -17.5t-7 -17.5l-124 -124l124 -124q7 -7 7 -17.5t-7 -17.5l-64 -64q-7 -7 -17.5 -7t-17.5 7l-124 124l-124 -124q-7 -7 -17.5 -7t-17.5 7l-64 64 q-7 7 -7 17.5t7 17.5l124 124l-124 124q-7 7 -7 17.5t7 17.5l64 64q7 7 17.5 7t17.5 -7l124 -124l124 124q7 7 17.5 7t17.5 -7z" /> +<glyph unicode="" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5t-57 214.5t-155.5 155.5t-214.5 57zM782 788l106 -106q7 -7 7 -17.5t-7 -17.5l-320 -321q-8 -7 -18 -7t-18 7l-202 203q-8 7 -8 17.5t8 17.5l106 106q7 8 17.5 8t17.5 -8l79 -79l197 197q7 7 17.5 7t17.5 -7z" /> +<glyph unicode="" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5q0 -120 65 -225 l587 587q-105 65 -225 65zM965 819l-584 -584q104 -62 219 -62q116 0 214.5 57t155.5 155.5t57 214.5q0 115 -62 219z" /> +<glyph unicode="" d="M39 582l522 427q16 13 27.5 8t11.5 -26v-291h550q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-550v-291q0 -21 -11.5 -26t-27.5 8l-522 427q-16 13 -16 32t16 32z" /> +<glyph unicode="" d="M639 1009l522 -427q16 -13 16 -32t-16 -32l-522 -427q-16 -13 -27.5 -8t-11.5 26v291h-550q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h550v291q0 21 11.5 26t27.5 -8z" /> +<glyph unicode="" d="M682 1161l427 -522q13 -16 8 -27.5t-26 -11.5h-291v-550q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v550h-291q-21 0 -26 11.5t8 27.5l427 522q13 16 32 16t32 -16z" /> +<glyph unicode="" d="M550 1200h200q21 0 35.5 -14.5t14.5 -35.5v-550h291q21 0 26 -11.5t-8 -27.5l-427 -522q-13 -16 -32 -16t-32 16l-427 522q-13 16 -8 27.5t26 11.5h291v550q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M639 1109l522 -427q16 -13 16 -32t-16 -32l-522 -427q-16 -13 -27.5 -8t-11.5 26v291q-94 -2 -182 -20t-170.5 -52t-147 -92.5t-100.5 -135.5q5 105 27 193.5t67.5 167t113 135t167 91.5t225.5 42v262q0 21 11.5 26t27.5 -8z" /> +<glyph unicode="" d="M850 1200h300q21 0 35.5 -14.5t14.5 -35.5v-300q0 -21 -10.5 -25t-24.5 10l-94 94l-249 -249q-8 -7 -18 -7t-18 7l-106 106q-7 8 -7 18t7 18l249 249l-94 94q-14 14 -10 24.5t25 10.5zM350 0h-300q-21 0 -35.5 14.5t-14.5 35.5v300q0 21 10.5 25t24.5 -10l94 -94l249 249 q8 7 18 7t18 -7l106 -106q7 -8 7 -18t-7 -18l-249 -249l94 -94q14 -14 10 -24.5t-25 -10.5z" /> +<glyph unicode="" d="M1014 1120l106 -106q7 -8 7 -18t-7 -18l-249 -249l94 -94q14 -14 10 -24.5t-25 -10.5h-300q-21 0 -35.5 14.5t-14.5 35.5v300q0 21 10.5 25t24.5 -10l94 -94l249 249q8 7 18 7t18 -7zM250 600h300q21 0 35.5 -14.5t14.5 -35.5v-300q0 -21 -10.5 -25t-24.5 10l-94 94 l-249 -249q-8 -7 -18 -7t-18 7l-106 106q-7 8 -7 18t7 18l249 249l-94 94q-14 14 -10 24.5t25 10.5z" /> +<glyph unicode="" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM704 900h-208q-20 0 -32 -14.5t-8 -34.5l58 -302q4 -20 21.5 -34.5 t37.5 -14.5h54q20 0 37.5 14.5t21.5 34.5l58 302q4 20 -8 34.5t-32 14.5zM675 400h-150q-10 0 -17.5 -7.5t-7.5 -17.5v-150q0 -10 7.5 -17.5t17.5 -7.5h150q10 0 17.5 7.5t7.5 17.5v150q0 10 -7.5 17.5t-17.5 7.5z" /> +<glyph unicode="" d="M260 1200q9 0 19 -2t15 -4l5 -2q22 -10 44 -23l196 -118q21 -13 36 -24q29 -21 37 -12q11 13 49 35l196 118q22 13 45 23q17 7 38 7q23 0 47 -16.5t37 -33.5l13 -16q14 -21 18 -45l25 -123l8 -44q1 -9 8.5 -14.5t17.5 -5.5h61q10 0 17.5 -7.5t7.5 -17.5v-50 q0 -10 -7.5 -17.5t-17.5 -7.5h-50q-10 0 -17.5 -7.5t-7.5 -17.5v-175h-400v300h-200v-300h-400v175q0 10 -7.5 17.5t-17.5 7.5h-50q-10 0 -17.5 7.5t-7.5 17.5v50q0 10 7.5 17.5t17.5 7.5h61q11 0 18 3t7 8q0 4 9 52l25 128q5 25 19 45q2 3 5 7t13.5 15t21.5 19.5t26.5 15.5 t29.5 7zM915 1079l-166 -162q-7 -7 -5 -12t12 -5h219q10 0 15 7t2 17l-51 149q-3 10 -11 12t-15 -6zM463 917l-177 157q-8 7 -16 5t-11 -12l-51 -143q-3 -10 2 -17t15 -7h231q11 0 12.5 5t-5.5 12zM500 0h-375q-10 0 -17.5 7.5t-7.5 17.5v375h400v-400zM1100 400v-375 q0 -10 -7.5 -17.5t-17.5 -7.5h-375v400h400z" /> +<glyph unicode="" d="M1165 1190q8 3 21 -6.5t13 -17.5q-2 -178 -24.5 -323.5t-55.5 -245.5t-87 -174.5t-102.5 -118.5t-118 -68.5t-118.5 -33t-120 -4.5t-105 9.5t-90 16.5q-61 12 -78 11q-4 1 -12.5 0t-34 -14.5t-52.5 -40.5l-153 -153q-26 -24 -37 -14.5t-11 43.5q0 64 42 102q8 8 50.5 45 t66.5 58q19 17 35 47t13 61q-9 55 -10 102.5t7 111t37 130t78 129.5q39 51 80 88t89.5 63.5t94.5 45t113.5 36t129 31t157.5 37t182 47.5zM1116 1098q-8 9 -22.5 -3t-45.5 -50q-38 -47 -119 -103.5t-142 -89.5l-62 -33q-56 -30 -102 -57t-104 -68t-102.5 -80.5t-85.5 -91 t-64 -104.5q-24 -56 -31 -86t2 -32t31.5 17.5t55.5 59.5q25 30 94 75.5t125.5 77.5t147.5 81q70 37 118.5 69t102 79.5t99 111t86.5 148.5q22 50 24 60t-6 19z" /> +<glyph unicode="" d="M653 1231q-39 -67 -54.5 -131t-10.5 -114.5t24.5 -96.5t47.5 -80t63.5 -62.5t68.5 -46.5t65 -30q-4 7 -17.5 35t-18.5 39.5t-17 39.5t-17 43t-13 42t-9.5 44.5t-2 42t4 43t13.5 39t23 38.5q96 -42 165 -107.5t105 -138t52 -156t13 -159t-19 -149.5q-13 -55 -44 -106.5 t-68 -87t-78.5 -64.5t-72.5 -45t-53 -22q-72 -22 -127 -11q-31 6 -13 19q6 3 17 7q13 5 32.5 21t41 44t38.5 63.5t21.5 81.5t-6.5 94.5t-50 107t-104 115.5q10 -104 -0.5 -189t-37 -140.5t-65 -93t-84 -52t-93.5 -11t-95 24.5q-80 36 -131.5 114t-53.5 171q-2 23 0 49.5 t4.5 52.5t13.5 56t27.5 60t46 64.5t69.5 68.5q-8 -53 -5 -102.5t17.5 -90t34 -68.5t44.5 -39t49 -2q31 13 38.5 36t-4.5 55t-29 64.5t-36 75t-26 75.5q-15 85 2 161.5t53.5 128.5t85.5 92.5t93.5 61t81.5 25.5z" /> +<glyph unicode="" d="M600 1094q82 0 160.5 -22.5t140 -59t116.5 -82.5t94.5 -95t68 -95t42.5 -82.5t14 -57.5t-14 -57.5t-43 -82.5t-68.5 -95t-94.5 -95t-116.5 -82.5t-140 -59t-159.5 -22.5t-159.5 22.5t-140 59t-116.5 82.5t-94.5 95t-68.5 95t-43 82.5t-14 57.5t14 57.5t42.5 82.5t68 95 t94.5 95t116.5 82.5t140 59t160.5 22.5zM888 829q-15 15 -18 12t5 -22q25 -57 25 -119q0 -124 -88 -212t-212 -88t-212 88t-88 212q0 59 23 114q8 19 4.5 22t-17.5 -12q-70 -69 -160 -184q-13 -16 -15 -40.5t9 -42.5q22 -36 47 -71t70 -82t92.5 -81t113 -58.5t133.5 -24.5 t133.5 24t113 58.5t92.5 81.5t70 81.5t47 70.5q11 18 9 42.5t-14 41.5q-90 117 -163 189zM448 727l-35 -36q-15 -15 -19.5 -38.5t4.5 -41.5q37 -68 93 -116q16 -13 38.5 -11t36.5 17l35 34q14 15 12.5 33.5t-16.5 33.5q-44 44 -89 117q-11 18 -28 20t-32 -12z" /> +<glyph unicode="" d="M592 0h-148l31 120q-91 20 -175.5 68.5t-143.5 106.5t-103.5 119t-66.5 110t-22 76q0 21 14 57.5t42.5 82.5t68 95t94.5 95t116.5 82.5t140 59t160.5 22.5q61 0 126 -15l32 121h148zM944 770l47 181q108 -85 176.5 -192t68.5 -159q0 -26 -19.5 -71t-59.5 -102t-93 -112 t-129 -104.5t-158 -75.5l46 173q77 49 136 117t97 131q11 18 9 42.5t-14 41.5q-54 70 -107 130zM310 824q-70 -69 -160 -184q-13 -16 -15 -40.5t9 -42.5q18 -30 39 -60t57 -70.5t74 -73t90 -61t105 -41.5l41 154q-107 18 -178.5 101.5t-71.5 193.5q0 59 23 114q8 19 4.5 22 t-17.5 -12zM448 727l-35 -36q-15 -15 -19.5 -38.5t4.5 -41.5q37 -68 93 -116q16 -13 38.5 -11t36.5 17l12 11l22 86l-3 4q-44 44 -89 117q-11 18 -28 20t-32 -12z" /> +<glyph unicode="" d="M-90 100l642 1066q20 31 48 28.5t48 -35.5l642 -1056q21 -32 7.5 -67.5t-50.5 -35.5h-1294q-37 0 -50.5 34t7.5 66zM155 200h345v75q0 10 7.5 17.5t17.5 7.5h150q10 0 17.5 -7.5t7.5 -17.5v-75h345l-445 723zM496 700h208q20 0 32 -14.5t8 -34.5l-58 -252 q-4 -20 -21.5 -34.5t-37.5 -14.5h-54q-20 0 -37.5 14.5t-21.5 34.5l-58 252q-4 20 8 34.5t32 14.5z" /> +<glyph unicode="" d="M650 1200q62 0 106 -44t44 -106v-339l363 -325q15 -14 26 -38.5t11 -44.5v-41q0 -20 -12 -26.5t-29 5.5l-359 249v-263q100 -93 100 -113v-64q0 -21 -13 -29t-32 1l-205 128l-205 -128q-19 -9 -32 -1t-13 29v64q0 20 100 113v263l-359 -249q-17 -12 -29 -5.5t-12 26.5v41 q0 20 11 44.5t26 38.5l363 325v339q0 62 44 106t106 44z" /> +<glyph unicode="" d="M850 1200h100q21 0 35.5 -14.5t14.5 -35.5v-50h50q21 0 35.5 -14.5t14.5 -35.5v-150h-1100v150q0 21 14.5 35.5t35.5 14.5h50v50q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-50h500v50q0 21 14.5 35.5t35.5 14.5zM1100 800v-750q0 -21 -14.5 -35.5 t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5v750h1100zM100 600v-100h100v100h-100zM300 600v-100h100v100h-100zM500 600v-100h100v100h-100zM700 600v-100h100v100h-100zM900 600v-100h100v100h-100zM100 400v-100h100v100h-100zM300 400v-100h100v100h-100zM500 400 v-100h100v100h-100zM700 400v-100h100v100h-100zM900 400v-100h100v100h-100zM100 200v-100h100v100h-100zM300 200v-100h100v100h-100zM500 200v-100h100v100h-100zM700 200v-100h100v100h-100zM900 200v-100h100v100h-100z" /> +<glyph unicode="" d="M1135 1165l249 -230q15 -14 15 -35t-15 -35l-249 -230q-14 -14 -24.5 -10t-10.5 25v150h-159l-600 -600h-291q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h209l600 600h241v150q0 21 10.5 25t24.5 -10zM522 819l-141 -141l-122 122h-209q-21 0 -35.5 14.5 t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h291zM1135 565l249 -230q15 -14 15 -35t-15 -35l-249 -230q-14 -14 -24.5 -10t-10.5 25v150h-241l-181 181l141 141l122 -122h159v150q0 21 10.5 25t24.5 -10z" /> +<glyph unicode="" d="M100 1100h1000q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-596l-304 -300v300h-100q-41 0 -70.5 29.5t-29.5 70.5v600q0 41 29.5 70.5t70.5 29.5z" /> +<glyph unicode="" d="M150 1200h200q21 0 35.5 -14.5t14.5 -35.5v-250h-300v250q0 21 14.5 35.5t35.5 14.5zM850 1200h200q21 0 35.5 -14.5t14.5 -35.5v-250h-300v250q0 21 14.5 35.5t35.5 14.5zM1100 800v-300q0 -41 -3 -77.5t-15 -89.5t-32 -96t-58 -89t-89 -77t-129 -51t-174 -20t-174 20 t-129 51t-89 77t-58 89t-32 96t-15 89.5t-3 77.5v300h300v-250v-27v-42.5t1.5 -41t5 -38t10 -35t16.5 -30t25.5 -24.5t35 -19t46.5 -12t60 -4t60 4.5t46.5 12.5t35 19.5t25 25.5t17 30.5t10 35t5 38t2 40.5t-0.5 42v25v250h300z" /> +<glyph unicode="" d="M1100 411l-198 -199l-353 353l-353 -353l-197 199l551 551z" /> +<glyph unicode="" d="M1101 789l-550 -551l-551 551l198 199l353 -353l353 353z" /> +<glyph unicode="" d="M404 1000h746q21 0 35.5 -14.5t14.5 -35.5v-551h150q21 0 25 -10.5t-10 -24.5l-230 -249q-14 -15 -35 -15t-35 15l-230 249q-14 14 -10 24.5t25 10.5h150v401h-381zM135 984l230 -249q14 -14 10 -24.5t-25 -10.5h-150v-400h385l215 -200h-750q-21 0 -35.5 14.5 t-14.5 35.5v550h-150q-21 0 -25 10.5t10 24.5l230 249q14 15 35 15t35 -15z" /> +<glyph unicode="" d="M56 1200h94q17 0 31 -11t18 -27l38 -162h896q24 0 39 -18.5t10 -42.5l-100 -475q-5 -21 -27 -42.5t-55 -21.5h-633l48 -200h535q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-50v-50q0 -21 -14.5 -35.5t-35.5 -14.5t-35.5 14.5t-14.5 35.5v50h-300v-50 q0 -21 -14.5 -35.5t-35.5 -14.5t-35.5 14.5t-14.5 35.5v50h-31q-18 0 -32.5 10t-20.5 19l-5 10l-201 961h-54q-20 0 -35 14.5t-15 35.5t15 35.5t35 14.5z" /> +<glyph unicode="" d="M1200 1000v-100h-1200v100h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500zM0 800h1200v-800h-1200v800z" /> +<glyph unicode="" d="M200 800l-200 -400v600h200q0 41 29.5 70.5t70.5 29.5h300q42 0 71 -29.5t29 -70.5h500v-200h-1000zM1500 700l-300 -700h-1200l300 700h1200z" /> +<glyph unicode="" d="M635 1184l230 -249q14 -14 10 -24.5t-25 -10.5h-150v-601h150q21 0 25 -10.5t-10 -24.5l-230 -249q-14 -15 -35 -15t-35 15l-230 249q-14 14 -10 24.5t25 10.5h150v601h-150q-21 0 -25 10.5t10 24.5l230 249q14 15 35 15t35 -15z" /> +<glyph unicode="" d="M936 864l249 -229q14 -15 14 -35.5t-14 -35.5l-249 -229q-15 -15 -25.5 -10.5t-10.5 24.5v151h-600v-151q0 -20 -10.5 -24.5t-25.5 10.5l-249 229q-14 15 -14 35.5t14 35.5l249 229q15 15 25.5 10.5t10.5 -25.5v-149h600v149q0 21 10.5 25.5t25.5 -10.5z" /> +<glyph unicode="" d="M1169 400l-172 732q-5 23 -23 45.5t-38 22.5h-672q-20 0 -38 -20t-23 -41l-172 -739h1138zM1100 300h-1000q-41 0 -70.5 -29.5t-29.5 -70.5v-100q0 -41 29.5 -70.5t70.5 -29.5h1000q41 0 70.5 29.5t29.5 70.5v100q0 41 -29.5 70.5t-70.5 29.5zM800 100v100h100v-100h-100 zM1000 100v100h100v-100h-100z" /> +<glyph unicode="" d="M1150 1100q21 0 35.5 -14.5t14.5 -35.5v-850q0 -21 -14.5 -35.5t-35.5 -14.5t-35.5 14.5t-14.5 35.5v850q0 21 14.5 35.5t35.5 14.5zM1000 200l-675 200h-38l47 -276q3 -16 -5.5 -20t-29.5 -4h-7h-84q-20 0 -34.5 14t-18.5 35q-55 337 -55 351v250v6q0 16 1 23.5t6.5 14 t17.5 6.5h200l675 250v-850zM0 750v-250q-4 0 -11 0.5t-24 6t-30 15t-24 30t-11 48.5v50q0 26 10.5 46t25 30t29 16t25.5 7z" /> +<glyph unicode="" d="M553 1200h94q20 0 29 -10.5t3 -29.5l-18 -37q83 -19 144 -82.5t76 -140.5l63 -327l118 -173h17q19 0 33 -14.5t14 -35t-13 -40.5t-31 -27q-8 -4 -23 -9.5t-65 -19.5t-103 -25t-132.5 -20t-158.5 -9q-57 0 -115 5t-104 12t-88.5 15.5t-73.5 17.5t-54.5 16t-35.5 12l-11 4 q-18 8 -31 28t-13 40.5t14 35t33 14.5h17l118 173l63 327q15 77 76 140t144 83l-18 32q-6 19 3.5 32t28.5 13zM498 110q50 -6 102 -6q53 0 102 6q-12 -49 -39.5 -79.5t-62.5 -30.5t-63 30.5t-39 79.5z" /> +<glyph unicode="" d="M800 946l224 78l-78 -224l234 -45l-180 -155l180 -155l-234 -45l78 -224l-224 78l-45 -234l-155 180l-155 -180l-45 234l-224 -78l78 224l-234 45l180 155l-180 155l234 45l-78 224l224 -78l45 234l155 -180l155 180z" /> +<glyph unicode="" d="M650 1200h50q40 0 70 -40.5t30 -84.5v-150l-28 -125h328q40 0 70 -40.5t30 -84.5v-100q0 -45 -29 -74l-238 -344q-16 -24 -38 -40.5t-45 -16.5h-250q-7 0 -42 25t-66 50l-31 25h-61q-45 0 -72.5 18t-27.5 57v400q0 36 20 63l145 196l96 198q13 28 37.5 48t51.5 20z M650 1100l-100 -212l-150 -213v-375h100l136 -100h214l250 375v125h-450l50 225v175h-50zM50 800h100q21 0 35.5 -14.5t14.5 -35.5v-500q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v500q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M600 1100h250q23 0 45 -16.5t38 -40.5l238 -344q29 -29 29 -74v-100q0 -44 -30 -84.5t-70 -40.5h-328q28 -118 28 -125v-150q0 -44 -30 -84.5t-70 -40.5h-50q-27 0 -51.5 20t-37.5 48l-96 198l-145 196q-20 27 -20 63v400q0 39 27.5 57t72.5 18h61q124 100 139 100z M50 1000h100q21 0 35.5 -14.5t14.5 -35.5v-500q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v500q0 21 14.5 35.5t35.5 14.5zM636 1000l-136 -100h-100v-375l150 -213l100 -212h50v175l-50 225h450v125l-250 375h-214z" /> +<glyph unicode="" d="M356 873l363 230q31 16 53 -6l110 -112q13 -13 13.5 -32t-11.5 -34l-84 -121h302q84 0 138 -38t54 -110t-55 -111t-139 -39h-106l-131 -339q-6 -21 -19.5 -41t-28.5 -20h-342q-7 0 -90 81t-83 94v525q0 17 14 35.5t28 28.5zM400 792v-503l100 -89h293l131 339 q6 21 19.5 41t28.5 20h203q21 0 30.5 25t0.5 50t-31 25h-456h-7h-6h-5.5t-6 0.5t-5 1.5t-5 2t-4 2.5t-4 4t-2.5 4.5q-12 25 5 47l146 183l-86 83zM50 800h100q21 0 35.5 -14.5t14.5 -35.5v-500q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v500 q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M475 1103l366 -230q2 -1 6 -3.5t14 -10.5t18 -16.5t14.5 -20t6.5 -22.5v-525q0 -13 -86 -94t-93 -81h-342q-15 0 -28.5 20t-19.5 41l-131 339h-106q-85 0 -139.5 39t-54.5 111t54 110t138 38h302l-85 121q-11 15 -10.5 34t13.5 32l110 112q22 22 53 6zM370 945l146 -183 q17 -22 5 -47q-2 -2 -3.5 -4.5t-4 -4t-4 -2.5t-5 -2t-5 -1.5t-6 -0.5h-6h-6.5h-6h-475v-100h221q15 0 29 -20t20 -41l130 -339h294l106 89v503l-342 236zM1050 800h100q21 0 35.5 -14.5t14.5 -35.5v-500q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5 v500q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M550 1294q72 0 111 -55t39 -139v-106l339 -131q21 -6 41 -19.5t20 -28.5v-342q0 -7 -81 -90t-94 -83h-525q-17 0 -35.5 14t-28.5 28l-9 14l-230 363q-16 31 6 53l112 110q13 13 32 13.5t34 -11.5l121 -84v302q0 84 38 138t110 54zM600 972v203q0 21 -25 30.5t-50 0.5 t-25 -31v-456v-7v-6v-5.5t-0.5 -6t-1.5 -5t-2 -5t-2.5 -4t-4 -4t-4.5 -2.5q-25 -12 -47 5l-183 146l-83 -86l236 -339h503l89 100v293l-339 131q-21 6 -41 19.5t-20 28.5zM450 200h500q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-500 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M350 1100h500q21 0 35.5 14.5t14.5 35.5v100q0 21 -14.5 35.5t-35.5 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -21 14.5 -35.5t35.5 -14.5zM600 306v-106q0 -84 -39 -139t-111 -55t-110 54t-38 138v302l-121 -84q-15 -12 -34 -11.5t-32 13.5l-112 110 q-22 22 -6 53l230 363q1 2 3.5 6t10.5 13.5t16.5 17t20 13.5t22.5 6h525q13 0 94 -83t81 -90v-342q0 -15 -20 -28.5t-41 -19.5zM308 900l-236 -339l83 -86l183 146q22 17 47 5q2 -1 4.5 -2.5t4 -4t2.5 -4t2 -5t1.5 -5t0.5 -6v-5.5v-6v-7v-456q0 -22 25 -31t50 0.5t25 30.5 v203q0 15 20 28.5t41 19.5l339 131v293l-89 100h-503z" /> +<glyph unicode="" d="M600 1178q118 0 225 -45.5t184.5 -123t123 -184.5t45.5 -225t-45.5 -225t-123 -184.5t-184.5 -123t-225 -45.5t-225 45.5t-184.5 123t-123 184.5t-45.5 225t45.5 225t123 184.5t184.5 123t225 45.5zM914 632l-275 223q-16 13 -27.5 8t-11.5 -26v-137h-275 q-10 0 -17.5 -7.5t-7.5 -17.5v-150q0 -10 7.5 -17.5t17.5 -7.5h275v-137q0 -21 11.5 -26t27.5 8l275 223q16 13 16 32t-16 32z" /> +<glyph unicode="" d="M600 1178q118 0 225 -45.5t184.5 -123t123 -184.5t45.5 -225t-45.5 -225t-123 -184.5t-184.5 -123t-225 -45.5t-225 45.5t-184.5 123t-123 184.5t-45.5 225t45.5 225t123 184.5t184.5 123t225 45.5zM561 855l-275 -223q-16 -13 -16 -32t16 -32l275 -223q16 -13 27.5 -8 t11.5 26v137h275q10 0 17.5 7.5t7.5 17.5v150q0 10 -7.5 17.5t-17.5 7.5h-275v137q0 21 -11.5 26t-27.5 -8z" /> +<glyph unicode="" d="M600 1178q118 0 225 -45.5t184.5 -123t123 -184.5t45.5 -225t-45.5 -225t-123 -184.5t-184.5 -123t-225 -45.5t-225 45.5t-184.5 123t-123 184.5t-45.5 225t45.5 225t123 184.5t184.5 123t225 45.5zM855 639l-223 275q-13 16 -32 16t-32 -16l-223 -275q-13 -16 -8 -27.5 t26 -11.5h137v-275q0 -10 7.5 -17.5t17.5 -7.5h150q10 0 17.5 7.5t7.5 17.5v275h137q21 0 26 11.5t-8 27.5z" /> +<glyph unicode="" d="M600 1178q118 0 225 -45.5t184.5 -123t123 -184.5t45.5 -225t-45.5 -225t-123 -184.5t-184.5 -123t-225 -45.5t-225 45.5t-184.5 123t-123 184.5t-45.5 225t45.5 225t123 184.5t184.5 123t225 45.5zM675 900h-150q-10 0 -17.5 -7.5t-7.5 -17.5v-275h-137q-21 0 -26 -11.5 t8 -27.5l223 -275q13 -16 32 -16t32 16l223 275q13 16 8 27.5t-26 11.5h-137v275q0 10 -7.5 17.5t-17.5 7.5z" /> +<glyph unicode="" d="M600 1176q116 0 222.5 -46t184 -123.5t123.5 -184t46 -222.5t-46 -222.5t-123.5 -184t-184 -123.5t-222.5 -46t-222.5 46t-184 123.5t-123.5 184t-46 222.5t46 222.5t123.5 184t184 123.5t222.5 46zM627 1101q-15 -12 -36.5 -20.5t-35.5 -12t-43 -8t-39 -6.5 q-15 -3 -45.5 0t-45.5 -2q-20 -7 -51.5 -26.5t-34.5 -34.5q-3 -11 6.5 -22.5t8.5 -18.5q-3 -34 -27.5 -91t-29.5 -79q-9 -34 5 -93t8 -87q0 -9 17 -44.5t16 -59.5q12 0 23 -5t23.5 -15t19.5 -14q16 -8 33 -15t40.5 -15t34.5 -12q21 -9 52.5 -32t60 -38t57.5 -11 q7 -15 -3 -34t-22.5 -40t-9.5 -38q13 -21 23 -34.5t27.5 -27.5t36.5 -18q0 -7 -3.5 -16t-3.5 -14t5 -17q104 -2 221 112q30 29 46.5 47t34.5 49t21 63q-13 8 -37 8.5t-36 7.5q-15 7 -49.5 15t-51.5 19q-18 0 -41 -0.5t-43 -1.5t-42 -6.5t-38 -16.5q-51 -35 -66 -12 q-4 1 -3.5 25.5t0.5 25.5q-6 13 -26.5 17.5t-24.5 6.5q1 15 -0.5 30.5t-7 28t-18.5 11.5t-31 -21q-23 -25 -42 4q-19 28 -8 58q6 16 22 22q6 -1 26 -1.5t33.5 -4t19.5 -13.5q7 -12 18 -24t21.5 -20.5t20 -15t15.5 -10.5l5 -3q2 12 7.5 30.5t8 34.5t-0.5 32q-3 18 3.5 29 t18 22.5t15.5 24.5q6 14 10.5 35t8 31t15.5 22.5t34 22.5q-6 18 10 36q8 0 24 -1.5t24.5 -1.5t20 4.5t20.5 15.5q-10 23 -31 42.5t-37.5 29.5t-49 27t-43.5 23q0 1 2 8t3 11.5t1.5 10.5t-1 9.5t-4.5 4.5q31 -13 58.5 -14.5t38.5 2.5l12 5q5 28 -9.5 46t-36.5 24t-50 15 t-41 20q-18 -4 -37 0zM613 994q0 -17 8 -42t17 -45t9 -23q-8 1 -39.5 5.5t-52.5 10t-37 16.5q3 11 16 29.5t16 25.5q10 -10 19 -10t14 6t13.5 14.5t16.5 12.5z" /> +<glyph unicode="" d="M756 1157q164 92 306 -9l-259 -138l145 -232l251 126q6 -89 -34 -156.5t-117 -110.5q-60 -34 -127 -39.5t-126 16.5l-596 -596q-15 -16 -36.5 -16t-36.5 16l-111 110q-15 15 -15 36.5t15 37.5l600 599q-34 101 5.5 201.5t135.5 154.5z" /> +<glyph unicode="" horiz-adv-x="1220" d="M100 1196h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM1100 1096h-200v-100h200v100zM100 796h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM1100 696h-500v-100h500v100zM100 396h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM1100 296h-300v-100h300v100z " /> +<glyph unicode="" d="M150 1200h900q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-900q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM700 500v-300l-200 -200v500l-350 500h900z" /> +<glyph unicode="" d="M500 1200h200q41 0 70.5 -29.5t29.5 -70.5v-100h300q41 0 70.5 -29.5t29.5 -70.5v-400h-500v100h-200v-100h-500v400q0 41 29.5 70.5t70.5 29.5h300v100q0 41 29.5 70.5t70.5 29.5zM500 1100v-100h200v100h-200zM1200 400v-200q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5v200h1200z" /> +<glyph unicode="" d="M50 1200h300q21 0 25 -10.5t-10 -24.5l-94 -94l199 -199q7 -8 7 -18t-7 -18l-106 -106q-8 -7 -18 -7t-18 7l-199 199l-94 -94q-14 -14 -24.5 -10t-10.5 25v300q0 21 14.5 35.5t35.5 14.5zM850 1200h300q21 0 35.5 -14.5t14.5 -35.5v-300q0 -21 -10.5 -25t-24.5 10l-94 94 l-199 -199q-8 -7 -18 -7t-18 7l-106 106q-7 8 -7 18t7 18l199 199l-94 94q-14 14 -10 24.5t25 10.5zM364 470l106 -106q7 -8 7 -18t-7 -18l-199 -199l94 -94q14 -14 10 -24.5t-25 -10.5h-300q-21 0 -35.5 14.5t-14.5 35.5v300q0 21 10.5 25t24.5 -10l94 -94l199 199 q8 7 18 7t18 -7zM1071 271l94 94q14 14 24.5 10t10.5 -25v-300q0 -21 -14.5 -35.5t-35.5 -14.5h-300q-21 0 -25 10.5t10 24.5l94 94l-199 199q-7 8 -7 18t7 18l106 106q8 7 18 7t18 -7z" /> +<glyph unicode="" d="M596 1192q121 0 231.5 -47.5t190 -127t127 -190t47.5 -231.5t-47.5 -231.5t-127 -190.5t-190 -127t-231.5 -47t-231.5 47t-190.5 127t-127 190.5t-47 231.5t47 231.5t127 190t190.5 127t231.5 47.5zM596 1010q-112 0 -207.5 -55.5t-151 -151t-55.5 -207.5t55.5 -207.5 t151 -151t207.5 -55.5t207.5 55.5t151 151t55.5 207.5t-55.5 207.5t-151 151t-207.5 55.5zM454.5 905q22.5 0 38.5 -16t16 -38.5t-16 -39t-38.5 -16.5t-38.5 16.5t-16 39t16 38.5t38.5 16zM754.5 905q22.5 0 38.5 -16t16 -38.5t-16 -39t-38 -16.5q-14 0 -29 10l-55 -145 q17 -23 17 -51q0 -36 -25.5 -61.5t-61.5 -25.5t-61.5 25.5t-25.5 61.5q0 32 20.5 56.5t51.5 29.5l122 126l1 1q-9 14 -9 28q0 23 16 39t38.5 16zM345.5 709q22.5 0 38.5 -16t16 -38.5t-16 -38.5t-38.5 -16t-38.5 16t-16 38.5t16 38.5t38.5 16zM854.5 709q22.5 0 38.5 -16 t16 -38.5t-16 -38.5t-38.5 -16t-38.5 16t-16 38.5t16 38.5t38.5 16z" /> +<glyph unicode="" d="M546 173l469 470q91 91 99 192q7 98 -52 175.5t-154 94.5q-22 4 -47 4q-34 0 -66.5 -10t-56.5 -23t-55.5 -38t-48 -41.5t-48.5 -47.5q-376 -375 -391 -390q-30 -27 -45 -41.5t-37.5 -41t-32 -46.5t-16 -47.5t-1.5 -56.5q9 -62 53.5 -95t99.5 -33q74 0 125 51l548 548 q36 36 20 75q-7 16 -21.5 26t-32.5 10q-26 0 -50 -23q-13 -12 -39 -38l-341 -338q-15 -15 -35.5 -15.5t-34.5 13.5t-14 34.5t14 34.5q327 333 361 367q35 35 67.5 51.5t78.5 16.5q14 0 29 -1q44 -8 74.5 -35.5t43.5 -68.5q14 -47 2 -96.5t-47 -84.5q-12 -11 -32 -32 t-79.5 -81t-114.5 -115t-124.5 -123.5t-123 -119.5t-96.5 -89t-57 -45q-56 -27 -120 -27q-70 0 -129 32t-93 89q-48 78 -35 173t81 163l511 511q71 72 111 96q91 55 198 55q80 0 152 -33q78 -36 129.5 -103t66.5 -154q17 -93 -11 -183.5t-94 -156.5l-482 -476 q-15 -15 -36 -16t-37 14t-17.5 34t14.5 35z" /> +<glyph unicode="" d="M649 949q48 68 109.5 104t121.5 38.5t118.5 -20t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-150 152.5t-126.5 127.5t-93.5 124.5t-33.5 117.5q0 64 28 123t73 100.5t104 64t119 20 t120.5 -38.5t104.5 -104zM896 972q-33 0 -64.5 -19t-56.5 -46t-47.5 -53.5t-43.5 -45.5t-37.5 -19t-36 19t-40 45.5t-43 53.5t-54 46t-65.5 19q-67 0 -122.5 -55.5t-55.5 -132.5q0 -23 13.5 -51t46 -65t57.5 -63t76 -75l22 -22q15 -14 44 -44t50.5 -51t46 -44t41 -35t23 -12 t23.5 12t42.5 36t46 44t52.5 52t44 43q4 4 12 13q43 41 63.5 62t52 55t46 55t26 46t11.5 44q0 79 -53 133.5t-120 54.5z" /> +<glyph unicode="" d="M776.5 1214q93.5 0 159.5 -66l141 -141q66 -66 66 -160q0 -42 -28 -95.5t-62 -87.5l-29 -29q-31 53 -77 99l-18 18l95 95l-247 248l-389 -389l212 -212l-105 -106l-19 18l-141 141q-66 66 -66 159t66 159l283 283q65 66 158.5 66zM600 706l105 105q10 -8 19 -17l141 -141 q66 -66 66 -159t-66 -159l-283 -283q-66 -66 -159 -66t-159 66l-141 141q-66 66 -66 159.5t66 159.5l55 55q29 -55 75 -102l18 -17l-95 -95l247 -248l389 389z" /> +<glyph unicode="" d="M603 1200q85 0 162 -15t127 -38t79 -48t29 -46v-953q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-41 0 -70.5 29.5t-29.5 70.5v953q0 21 30 46.5t81 48t129 37.5t163 15zM300 1000v-700h600v700h-600zM600 254q-43 0 -73.5 -30.5t-30.5 -73.5t30.5 -73.5t73.5 -30.5t73.5 30.5 t30.5 73.5t-30.5 73.5t-73.5 30.5z" /> +<glyph unicode="" d="M902 1185l283 -282q15 -15 15 -36t-14.5 -35.5t-35.5 -14.5t-35 15l-36 35l-279 -267v-300l-212 210l-308 -307l-280 -203l203 280l307 308l-210 212h300l267 279l-35 36q-15 14 -15 35t14.5 35.5t35.5 14.5t35 -15z" /> +<glyph unicode="" d="M700 1248v-78q38 -5 72.5 -14.5t75.5 -31.5t71 -53.5t52 -84t24 -118.5h-159q-4 36 -10.5 59t-21 45t-40 35.5t-64.5 20.5v-307l64 -13q34 -7 64 -16.5t70 -32t67.5 -52.5t47.5 -80t20 -112q0 -139 -89 -224t-244 -97v-77h-100v79q-150 16 -237 103q-40 40 -52.5 93.5 t-15.5 139.5h139q5 -77 48.5 -126t117.5 -65v335l-27 8q-46 14 -79 26.5t-72 36t-63 52t-40 72.5t-16 98q0 70 25 126t67.5 92t94.5 57t110 27v77h100zM600 754v274q-29 -4 -50 -11t-42 -21.5t-31.5 -41.5t-10.5 -65q0 -29 7 -50.5t16.5 -34t28.5 -22.5t31.5 -14t37.5 -10 q9 -3 13 -4zM700 547v-310q22 2 42.5 6.5t45 15.5t41.5 27t29 42t12 59.5t-12.5 59.5t-38 44.5t-53 31t-66.5 24.5z" /> +<glyph unicode="" d="M561 1197q84 0 160.5 -40t123.5 -109.5t47 -147.5h-153q0 40 -19.5 71.5t-49.5 48.5t-59.5 26t-55.5 9q-37 0 -79 -14.5t-62 -35.5q-41 -44 -41 -101q0 -26 13.5 -63t26.5 -61t37 -66q6 -9 9 -14h241v-100h-197q8 -50 -2.5 -115t-31.5 -95q-45 -62 -99 -112 q34 10 83 17.5t71 7.5q32 1 102 -16t104 -17q83 0 136 30l50 -147q-31 -19 -58 -30.5t-55 -15.5t-42 -4.5t-46 -0.5q-23 0 -76 17t-111 32.5t-96 11.5q-39 -3 -82 -16t-67 -25l-23 -11l-55 145q4 3 16 11t15.5 10.5t13 9t15.5 12t14.5 14t17.5 18.5q48 55 54 126.5 t-30 142.5h-221v100h166q-23 47 -44 104q-7 20 -12 41.5t-6 55.5t6 66.5t29.5 70.5t58.5 71q97 88 263 88z" /> +<glyph unicode="" d="M400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM935 1184l230 -249q14 -14 10 -24.5t-25 -10.5h-150v-900h-200v900h-150q-21 0 -25 10.5t10 24.5l230 249q14 15 35 15t35 -15z" /> +<glyph unicode="" d="M1000 700h-100v100h-100v-100h-100v500h300v-500zM400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM801 1100v-200h100v200h-100zM1000 350l-200 -250h200v-100h-300v150l200 250h-200v100h300v-150z " /> +<glyph unicode="" d="M400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM1000 1050l-200 -250h200v-100h-300v150l200 250h-200v100h300v-150zM1000 0h-100v100h-100v-100h-100v500h300v-500zM801 400v-200h100v200h-100z " /> +<glyph unicode="" d="M400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM1000 700h-100v400h-100v100h200v-500zM1100 0h-100v100h-200v400h300v-500zM901 400v-200h100v200h-100z" /> +<glyph unicode="" d="M400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM1100 700h-100v100h-200v400h300v-500zM901 1100v-200h100v200h-100zM1000 0h-100v400h-100v100h200v-500z" /> +<glyph unicode="" d="M400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM900 1000h-200v200h200v-200zM1000 700h-300v200h300v-200zM1100 400h-400v200h400v-200zM1200 100h-500v200h500v-200z" /> +<glyph unicode="" d="M400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM1200 1000h-500v200h500v-200zM1100 700h-400v200h400v-200zM1000 400h-300v200h300v-200zM900 100h-200v200h200v-200z" /> +<glyph unicode="" d="M350 1100h400q162 0 256 -93.5t94 -256.5v-400q0 -165 -93.5 -257.5t-256.5 -92.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400q0 165 92.5 257.5t257.5 92.5zM800 900h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5 v500q0 41 -29.5 70.5t-70.5 29.5z" /> +<glyph unicode="" d="M350 1100h400q165 0 257.5 -92.5t92.5 -257.5v-400q0 -165 -92.5 -257.5t-257.5 -92.5h-400q-163 0 -256.5 92.5t-93.5 257.5v400q0 163 94 256.5t256 93.5zM800 900h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5 v500q0 41 -29.5 70.5t-70.5 29.5zM440 770l253 -190q17 -12 17 -30t-17 -30l-253 -190q-16 -12 -28 -6.5t-12 26.5v400q0 21 12 26.5t28 -6.5z" /> +<glyph unicode="" d="M350 1100h400q163 0 256.5 -94t93.5 -256v-400q0 -165 -92.5 -257.5t-257.5 -92.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400q0 163 92.5 256.5t257.5 93.5zM800 900h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5 v500q0 41 -29.5 70.5t-70.5 29.5zM350 700h400q21 0 26.5 -12t-6.5 -28l-190 -253q-12 -17 -30 -17t-30 17l-190 253q-12 16 -6.5 28t26.5 12z" /> +<glyph unicode="" d="M350 1100h400q165 0 257.5 -92.5t92.5 -257.5v-400q0 -163 -92.5 -256.5t-257.5 -93.5h-400q-163 0 -256.5 94t-93.5 256v400q0 165 92.5 257.5t257.5 92.5zM800 900h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5 v500q0 41 -29.5 70.5t-70.5 29.5zM580 693l190 -253q12 -16 6.5 -28t-26.5 -12h-400q-21 0 -26.5 12t6.5 28l190 253q12 17 30 17t30 -17z" /> +<glyph unicode="" d="M550 1100h400q165 0 257.5 -92.5t92.5 -257.5v-400q0 -165 -92.5 -257.5t-257.5 -92.5h-400q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h450q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-450q-21 0 -35.5 14.5t-14.5 35.5v100 q0 21 14.5 35.5t35.5 14.5zM338 867l324 -284q16 -14 16 -33t-16 -33l-324 -284q-16 -14 -27 -9t-11 26v150h-250q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h250v150q0 21 11 26t27 -9z" /> +<glyph unicode="" d="M793 1182l9 -9q8 -10 5 -27q-3 -11 -79 -225.5t-78 -221.5l300 1q24 0 32.5 -17.5t-5.5 -35.5q-1 0 -133.5 -155t-267 -312.5t-138.5 -162.5q-12 -15 -26 -15h-9l-9 8q-9 11 -4 32q2 9 42 123.5t79 224.5l39 110h-302q-23 0 -31 19q-10 21 6 41q75 86 209.5 237.5 t228 257t98.5 111.5q9 16 25 16h9z" /> +<glyph unicode="" d="M350 1100h400q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-450q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h450q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400 q0 165 92.5 257.5t257.5 92.5zM938 867l324 -284q16 -14 16 -33t-16 -33l-324 -284q-16 -14 -27 -9t-11 26v150h-250q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h250v150q0 21 11 26t27 -9z" /> +<glyph unicode="" d="M750 1200h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -10.5 -25t-24.5 10l-109 109l-312 -312q-15 -15 -35.5 -15t-35.5 15l-141 141q-15 15 -15 35.5t15 35.5l312 312l-109 109q-14 14 -10 24.5t25 10.5zM456 900h-156q-41 0 -70.5 -29.5t-29.5 -70.5v-500 q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v148l200 200v-298q0 -165 -93.5 -257.5t-256.5 -92.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400q0 165 92.5 257.5t257.5 92.5h300z" /> +<glyph unicode="" d="M600 1186q119 0 227.5 -46.5t187 -125t125 -187t46.5 -227.5t-46.5 -227.5t-125 -187t-187 -125t-227.5 -46.5t-227.5 46.5t-187 125t-125 187t-46.5 227.5t46.5 227.5t125 187t187 125t227.5 46.5zM600 1022q-115 0 -212 -56.5t-153.5 -153.5t-56.5 -212t56.5 -212 t153.5 -153.5t212 -56.5t212 56.5t153.5 153.5t56.5 212t-56.5 212t-153.5 153.5t-212 56.5zM600 794q80 0 137 -57t57 -137t-57 -137t-137 -57t-137 57t-57 137t57 137t137 57z" /> +<glyph unicode="" d="M450 1200h200q21 0 35.5 -14.5t14.5 -35.5v-350h245q20 0 25 -11t-9 -26l-383 -426q-14 -15 -33.5 -15t-32.5 15l-379 426q-13 15 -8.5 26t25.5 11h250v350q0 21 14.5 35.5t35.5 14.5zM50 300h1000q21 0 35.5 -14.5t14.5 -35.5v-250h-1100v250q0 21 14.5 35.5t35.5 14.5z M900 200v-50h100v50h-100z" /> +<glyph unicode="" d="M583 1182l378 -435q14 -15 9 -31t-26 -16h-244v-250q0 -20 -17 -35t-39 -15h-200q-20 0 -32 14.5t-12 35.5v250h-250q-20 0 -25.5 16.5t8.5 31.5l383 431q14 16 33.5 17t33.5 -14zM50 300h1000q21 0 35.5 -14.5t14.5 -35.5v-250h-1100v250q0 21 14.5 35.5t35.5 14.5z M900 200v-50h100v50h-100z" /> +<glyph unicode="" d="M396 723l369 369q7 7 17.5 7t17.5 -7l139 -139q7 -8 7 -18.5t-7 -17.5l-525 -525q-7 -8 -17.5 -8t-17.5 8l-292 291q-7 8 -7 18t7 18l139 139q8 7 18.5 7t17.5 -7zM50 300h1000q21 0 35.5 -14.5t14.5 -35.5v-250h-1100v250q0 21 14.5 35.5t35.5 14.5zM900 200v-50h100v50 h-100z" /> +<glyph unicode="" d="M135 1023l142 142q14 14 35 14t35 -14l77 -77l-212 -212l-77 76q-14 15 -14 36t14 35zM655 855l210 210q14 14 24.5 10t10.5 -25l-2 -599q-1 -20 -15.5 -35t-35.5 -15l-597 -1q-21 0 -25 10.5t10 24.5l208 208l-154 155l212 212zM50 300h1000q21 0 35.5 -14.5t14.5 -35.5 v-250h-1100v250q0 21 14.5 35.5t35.5 14.5zM900 200v-50h100v50h-100z" /> +<glyph unicode="" d="M350 1200l599 -2q20 -1 35 -15.5t15 -35.5l1 -597q0 -21 -10.5 -25t-24.5 10l-208 208l-155 -154l-212 212l155 154l-210 210q-14 14 -10 24.5t25 10.5zM524 512l-76 -77q-15 -14 -36 -14t-35 14l-142 142q-14 14 -14 35t14 35l77 77zM50 300h1000q21 0 35.5 -14.5 t14.5 -35.5v-250h-1100v250q0 21 14.5 35.5t35.5 14.5zM900 200v-50h100v50h-100z" /> +<glyph unicode="" d="M1200 103l-483 276l-314 -399v423h-399l1196 796v-1096zM483 424v-230l683 953z" /> +<glyph unicode="" d="M1100 1000v-850q0 -21 -14.5 -35.5t-35.5 -14.5h-150v400h-700v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100zM700 1000h-100v200h100v-200z" /> +<glyph unicode="" d="M1100 1000l-2 -149l-299 -299l-95 95q-9 9 -21.5 9t-21.5 -9l-149 -147h-312v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100zM700 1000h-100v200h100v-200zM1132 638l106 -106q7 -7 7 -17.5t-7 -17.5l-420 -421q-8 -7 -18 -7 t-18 7l-202 203q-8 7 -8 17.5t8 17.5l106 106q7 8 17.5 8t17.5 -8l79 -79l297 297q7 7 17.5 7t17.5 -7z" /> +<glyph unicode="" d="M1100 1000v-269l-103 -103l-134 134q-15 15 -33.5 16.5t-34.5 -12.5l-266 -266h-329v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100zM700 1000h-100v200h100v-200zM1202 572l70 -70q15 -15 15 -35.5t-15 -35.5l-131 -131 l131 -131q15 -15 15 -35.5t-15 -35.5l-70 -70q-15 -15 -35.5 -15t-35.5 15l-131 131l-131 -131q-15 -15 -35.5 -15t-35.5 15l-70 70q-15 15 -15 35.5t15 35.5l131 131l-131 131q-15 15 -15 35.5t15 35.5l70 70q15 15 35.5 15t35.5 -15l131 -131l131 131q15 15 35.5 15 t35.5 -15z" /> +<glyph unicode="" d="M1100 1000v-300h-350q-21 0 -35.5 -14.5t-14.5 -35.5v-150h-500v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100zM700 1000h-100v200h100v-200zM850 600h100q21 0 35.5 -14.5t14.5 -35.5v-250h150q21 0 25 -10.5t-10 -24.5 l-230 -230q-14 -14 -35 -14t-35 14l-230 230q-14 14 -10 24.5t25 10.5h150v250q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M1100 1000v-400l-165 165q-14 15 -35 15t-35 -15l-263 -265h-402v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100zM700 1000h-100v200h100v-200zM935 565l230 -229q14 -15 10 -25.5t-25 -10.5h-150v-250q0 -20 -14.5 -35 t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35v250h-150q-21 0 -25 10.5t10 25.5l230 229q14 15 35 15t35 -15z" /> +<glyph unicode="" d="M50 1100h1100q21 0 35.5 -14.5t14.5 -35.5v-150h-1200v150q0 21 14.5 35.5t35.5 14.5zM1200 800v-550q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v550h1200zM100 500v-200h400v200h-400z" /> +<glyph unicode="" d="M935 1165l248 -230q14 -14 14 -35t-14 -35l-248 -230q-14 -14 -24.5 -10t-10.5 25v150h-400v200h400v150q0 21 10.5 25t24.5 -10zM200 800h-50q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h50v-200zM400 800h-100v200h100v-200zM18 435l247 230 q14 14 24.5 10t10.5 -25v-150h400v-200h-400v-150q0 -21 -10.5 -25t-24.5 10l-247 230q-15 14 -15 35t15 35zM900 300h-100v200h100v-200zM1000 500h51q20 0 34.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-34.5 -14.5h-51v200z" /> +<glyph unicode="" d="M862 1073l276 116q25 18 43.5 8t18.5 -41v-1106q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v397q-4 1 -11 5t-24 17.5t-30 29t-24 42t-11 56.5v359q0 31 18.5 65t43.5 52zM550 1200q22 0 34.5 -12.5t14.5 -24.5l1 -13v-450q0 -28 -10.5 -59.5 t-25 -56t-29 -45t-25.5 -31.5l-10 -11v-447q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v447q-4 4 -11 11.5t-24 30.5t-30 46t-24 55t-11 60v450q0 2 0.5 5.5t4 12t8.5 15t14.5 12t22.5 5.5q20 0 32.5 -12.5t14.5 -24.5l3 -13v-350h100v350v5.5t2.5 12 t7 15t15 12t25.5 5.5q23 0 35.5 -12.5t13.5 -24.5l1 -13v-350h100v350q0 2 0.5 5.5t3 12t7 15t15 12t24.5 5.5z" /> +<glyph unicode="" d="M1200 1100v-56q-4 0 -11 -0.5t-24 -3t-30 -7.5t-24 -15t-11 -24v-888q0 -22 25 -34.5t50 -13.5l25 -2v-56h-400v56q75 0 87.5 6.5t12.5 43.5v394h-500v-394q0 -37 12.5 -43.5t87.5 -6.5v-56h-400v56q4 0 11 0.5t24 3t30 7.5t24 15t11 24v888q0 22 -25 34.5t-50 13.5 l-25 2v56h400v-56q-75 0 -87.5 -6.5t-12.5 -43.5v-394h500v394q0 37 -12.5 43.5t-87.5 6.5v56h400z" /> +<glyph unicode="" d="M675 1000h375q21 0 35.5 -14.5t14.5 -35.5v-150h-105l-295 -98v98l-200 200h-400l100 100h375zM100 900h300q41 0 70.5 -29.5t29.5 -70.5v-500q0 -41 -29.5 -70.5t-70.5 -29.5h-300q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5zM100 800v-200h300v200 h-300zM1100 535l-400 -133v163l400 133v-163zM100 500v-200h300v200h-300zM1100 398v-248q0 -21 -14.5 -35.5t-35.5 -14.5h-375l-100 -100h-375l-100 100h400l200 200h105z" /> +<glyph unicode="" d="M17 1007l162 162q17 17 40 14t37 -22l139 -194q14 -20 11 -44.5t-20 -41.5l-119 -118q102 -142 228 -268t267 -227l119 118q17 17 42.5 19t44.5 -12l192 -136q19 -14 22.5 -37.5t-13.5 -40.5l-163 -162q-3 -1 -9.5 -1t-29.5 2t-47.5 6t-62.5 14.5t-77.5 26.5t-90 42.5 t-101.5 60t-111 83t-119 108.5q-74 74 -133.5 150.5t-94.5 138.5t-60 119.5t-34.5 100t-15 74.5t-4.5 48z" /> +<glyph unicode="" d="M600 1100q92 0 175 -10.5t141.5 -27t108.5 -36.5t81.5 -40t53.5 -37t31 -27l9 -10v-200q0 -21 -14.5 -33t-34.5 -9l-202 34q-20 3 -34.5 20t-14.5 38v146q-141 24 -300 24t-300 -24v-146q0 -21 -14.5 -38t-34.5 -20l-202 -34q-20 -3 -34.5 9t-14.5 33v200q3 4 9.5 10.5 t31 26t54 37.5t80.5 39.5t109 37.5t141 26.5t175 10.5zM600 795q56 0 97 -9.5t60 -23.5t30 -28t12 -24l1 -10v-50l365 -303q14 -15 24.5 -40t10.5 -45v-212q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v212q0 20 10.5 45t24.5 40l365 303v50 q0 4 1 10.5t12 23t30 29t60 22.5t97 10z" /> +<glyph unicode="" d="M1100 700l-200 -200h-600l-200 200v500h200v-200h200v200h200v-200h200v200h200v-500zM250 400h700q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-12l137 -100h-950l137 100h-12q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM50 100h1100q21 0 35.5 -14.5 t14.5 -35.5v-50h-1200v50q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M700 1100h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-1000h300v1000q0 41 -29.5 70.5t-70.5 29.5zM1100 800h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-700h300v700q0 41 -29.5 70.5t-70.5 29.5zM400 0h-300v400q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-400z " /> +<glyph unicode="" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM500 700h-200v-100h200v-300h-300v100h200v100h-200v300h300v-100zM900 700v-300l-100 -100h-200v500h200z M700 700v-300h100v300h-100z" /> +<glyph unicode="" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM500 300h-100v200h-100v-200h-100v500h100v-200h100v200h100v-500zM900 700v-300l-100 -100h-200v500h200z M700 700v-300h100v300h-100z" /> +<glyph unicode="" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM500 700h-200v-300h200v-100h-300v500h300v-100zM900 700h-200v-300h200v-100h-300v500h300v-100z" /> +<glyph unicode="" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM500 400l-300 150l300 150v-300zM900 550l-300 -150v300z" /> +<glyph unicode="" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM900 300h-700v500h700v-500zM800 700h-130q-38 0 -66.5 -43t-28.5 -108t27 -107t68 -42h130v300zM300 700v-300 h130q41 0 68 42t27 107t-28.5 108t-66.5 43h-130z" /> +<glyph unicode="" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM500 700h-200v-100h200v-300h-300v100h200v100h-200v300h300v-100zM900 300h-100v400h-100v100h200v-500z M700 300h-100v100h100v-100z" /> +<glyph unicode="" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM300 700h200v-400h-300v500h100v-100zM900 300h-100v400h-100v100h200v-500zM300 600v-200h100v200h-100z M700 300h-100v100h100v-100z" /> +<glyph unicode="" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM500 500l-199 -200h-100v50l199 200v150h-200v100h300v-300zM900 300h-100v400h-100v100h200v-500zM701 300h-100 v100h100v-100z" /> +<glyph unicode="" d="M600 1191q120 0 229.5 -47t188.5 -126t126 -188.5t47 -229.5t-47 -229.5t-126 -188.5t-188.5 -126t-229.5 -47t-229.5 47t-188.5 126t-126 188.5t-47 229.5t47 229.5t126 188.5t188.5 126t229.5 47zM600 1021q-114 0 -211 -56.5t-153.5 -153.5t-56.5 -211t56.5 -211 t153.5 -153.5t211 -56.5t211 56.5t153.5 153.5t56.5 211t-56.5 211t-153.5 153.5t-211 56.5zM800 700h-300v-200h300v-100h-300l-100 100v200l100 100h300v-100z" /> +<glyph unicode="" d="M600 1191q120 0 229.5 -47t188.5 -126t126 -188.5t47 -229.5t-47 -229.5t-126 -188.5t-188.5 -126t-229.5 -47t-229.5 47t-188.5 126t-126 188.5t-47 229.5t47 229.5t126 188.5t188.5 126t229.5 47zM600 1021q-114 0 -211 -56.5t-153.5 -153.5t-56.5 -211t56.5 -211 t153.5 -153.5t211 -56.5t211 56.5t153.5 153.5t56.5 211t-56.5 211t-153.5 153.5t-211 56.5zM800 700v-100l-50 -50l100 -100v-50h-100l-100 100h-150v-100h-100v400h300zM500 700v-100h200v100h-200z" /> +<glyph unicode="" d="M503 1089q110 0 200.5 -59.5t134.5 -156.5q44 14 90 14q120 0 205 -86.5t85 -207t-85 -207t-205 -86.5h-128v250q0 21 -14.5 35.5t-35.5 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-250h-222q-80 0 -136 57.5t-56 136.5q0 69 43 122.5t108 67.5q-2 19 -2 37q0 100 49 185 t134 134t185 49zM525 500h150q10 0 17.5 -7.5t7.5 -17.5v-275h137q21 0 26 -11.5t-8 -27.5l-223 -244q-13 -16 -32 -16t-32 16l-223 244q-13 16 -8 27.5t26 11.5h137v275q0 10 7.5 17.5t17.5 7.5z" /> +<glyph unicode="" d="M502 1089q110 0 201 -59.5t135 -156.5q43 15 89 15q121 0 206 -86.5t86 -206.5q0 -99 -60 -181t-150 -110l-378 360q-13 16 -31.5 16t-31.5 -16l-381 -365h-9q-79 0 -135.5 57.5t-56.5 136.5q0 69 43 122.5t108 67.5q-2 19 -2 38q0 100 49 184.5t133.5 134t184.5 49.5z M632 467l223 -228q13 -16 8 -27.5t-26 -11.5h-137v-275q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v275h-137q-21 0 -26 11.5t8 27.5q199 204 223 228q19 19 31.5 19t32.5 -19z" /> +<glyph unicode="" d="M700 100v100h400l-270 300h170l-270 300h170l-300 333l-300 -333h170l-270 -300h170l-270 -300h400v-100h-50q-21 0 -35.5 -14.5t-14.5 -35.5v-50h400v50q0 21 -14.5 35.5t-35.5 14.5h-50z" /> +<glyph unicode="" d="M600 1179q94 0 167.5 -56.5t99.5 -145.5q89 -6 150.5 -71.5t61.5 -155.5q0 -61 -29.5 -112.5t-79.5 -82.5q9 -29 9 -55q0 -74 -52.5 -126.5t-126.5 -52.5q-55 0 -100 30v-251q21 0 35.5 -14.5t14.5 -35.5v-50h-300v50q0 21 14.5 35.5t35.5 14.5v251q-45 -30 -100 -30 q-74 0 -126.5 52.5t-52.5 126.5q0 18 4 38q-47 21 -75.5 65t-28.5 97q0 74 52.5 126.5t126.5 52.5q5 0 23 -2q0 2 -1 10t-1 13q0 116 81.5 197.5t197.5 81.5z" /> +<glyph unicode="" d="M1010 1010q111 -111 150.5 -260.5t0 -299t-150.5 -260.5q-83 -83 -191.5 -126.5t-218.5 -43.5t-218.5 43.5t-191.5 126.5q-111 111 -150.5 260.5t0 299t150.5 260.5q83 83 191.5 126.5t218.5 43.5t218.5 -43.5t191.5 -126.5zM476 1065q-4 0 -8 -1q-121 -34 -209.5 -122.5 t-122.5 -209.5q-4 -12 2.5 -23t18.5 -14l36 -9q3 -1 7 -1q23 0 29 22q27 96 98 166q70 71 166 98q11 3 17.5 13.5t3.5 22.5l-9 35q-3 13 -14 19q-7 4 -15 4zM512 920q-4 0 -9 -2q-80 -24 -138.5 -82.5t-82.5 -138.5q-4 -13 2 -24t19 -14l34 -9q4 -1 8 -1q22 0 28 21 q18 58 58.5 98.5t97.5 58.5q12 3 18 13.5t3 21.5l-9 35q-3 12 -14 19q-7 4 -15 4zM719.5 719.5q-49.5 49.5 -119.5 49.5t-119.5 -49.5t-49.5 -119.5t49.5 -119.5t119.5 -49.5t119.5 49.5t49.5 119.5t-49.5 119.5zM855 551q-22 0 -28 -21q-18 -58 -58.5 -98.5t-98.5 -57.5 q-11 -4 -17 -14.5t-3 -21.5l9 -35q3 -12 14 -19q7 -4 15 -4q4 0 9 2q80 24 138.5 82.5t82.5 138.5q4 13 -2.5 24t-18.5 14l-34 9q-4 1 -8 1zM1000 515q-23 0 -29 -22q-27 -96 -98 -166q-70 -71 -166 -98q-11 -3 -17.5 -13.5t-3.5 -22.5l9 -35q3 -13 14 -19q7 -4 15 -4 q4 0 8 1q121 34 209.5 122.5t122.5 209.5q4 12 -2.5 23t-18.5 14l-36 9q-3 1 -7 1z" /> +<glyph unicode="" d="M700 800h300v-380h-180v200h-340v-200h-380v755q0 10 7.5 17.5t17.5 7.5h575v-400zM1000 900h-200v200zM700 300h162l-212 -212l-212 212h162v200h100v-200zM520 0h-395q-10 0 -17.5 7.5t-7.5 17.5v395zM1000 220v-195q0 -10 -7.5 -17.5t-17.5 -7.5h-195z" /> +<glyph unicode="" d="M700 800h300v-520l-350 350l-550 -550v1095q0 10 7.5 17.5t17.5 7.5h575v-400zM1000 900h-200v200zM862 200h-162v-200h-100v200h-162l212 212zM480 0h-355q-10 0 -17.5 7.5t-7.5 17.5v55h380v-80zM1000 80v-55q0 -10 -7.5 -17.5t-17.5 -7.5h-155v80h180z" /> +<glyph unicode="" d="M1162 800h-162v-200h100l100 -100h-300v300h-162l212 212zM200 800h200q27 0 40 -2t29.5 -10.5t23.5 -30t7 -57.5h300v-100h-600l-200 -350v450h100q0 36 7 57.5t23.5 30t29.5 10.5t40 2zM800 400h240l-240 -400h-800l300 500h500v-100z" /> +<glyph unicode="" d="M650 1100h100q21 0 35.5 -14.5t14.5 -35.5v-50h50q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-300q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h50v50q0 21 14.5 35.5t35.5 14.5zM1000 850v150q41 0 70.5 -29.5t29.5 -70.5v-800 q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-1 0 -20 4l246 246l-326 326v324q0 41 29.5 70.5t70.5 29.5v-150q0 -62 44 -106t106 -44h300q62 0 106 44t44 106zM412 250l-212 -212v162h-200v100h200v162z" /> +<glyph unicode="" d="M450 1100h100q21 0 35.5 -14.5t14.5 -35.5v-50h50q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-300q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h50v50q0 21 14.5 35.5t35.5 14.5zM800 850v150q41 0 70.5 -29.5t29.5 -70.5v-500 h-200v-300h200q0 -36 -7 -57.5t-23.5 -30t-29.5 -10.5t-40 -2h-600q-41 0 -70.5 29.5t-29.5 70.5v800q0 41 29.5 70.5t70.5 29.5v-150q0 -62 44 -106t106 -44h300q62 0 106 44t44 106zM1212 250l-212 -212v162h-200v100h200v162z" /> +<glyph unicode="" d="M658 1197l637 -1104q23 -38 7 -65.5t-60 -27.5h-1276q-44 0 -60 27.5t7 65.5l637 1104q22 39 54 39t54 -39zM704 800h-208q-20 0 -32 -14.5t-8 -34.5l58 -302q4 -20 21.5 -34.5t37.5 -14.5h54q20 0 37.5 14.5t21.5 34.5l58 302q4 20 -8 34.5t-32 14.5zM500 300v-100h200 v100h-200z" /> +<glyph unicode="" d="M425 1100h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5zM425 800h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5 t17.5 7.5zM825 800h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5zM25 500h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150 q0 10 7.5 17.5t17.5 7.5zM425 500h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5zM825 500h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5 v150q0 10 7.5 17.5t17.5 7.5zM25 200h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5zM425 200h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5 t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5zM825 200h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5z" /> +<glyph unicode="" d="M700 1200h100v-200h-100v-100h350q62 0 86.5 -39.5t-3.5 -94.5l-66 -132q-41 -83 -81 -134h-772q-40 51 -81 134l-66 132q-28 55 -3.5 94.5t86.5 39.5h350v100h-100v200h100v100h200v-100zM250 400h700q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-12l137 -100 h-950l138 100h-13q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM50 100h1100q21 0 35.5 -14.5t14.5 -35.5v-50h-1200v50q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M600 1300q40 0 68.5 -29.5t28.5 -70.5h-194q0 41 28.5 70.5t68.5 29.5zM443 1100h314q18 -37 18 -75q0 -8 -3 -25h328q41 0 44.5 -16.5t-30.5 -38.5l-175 -145h-678l-178 145q-34 22 -29 38.5t46 16.5h328q-3 17 -3 25q0 38 18 75zM250 700h700q21 0 35.5 -14.5 t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-150v-200l275 -200h-950l275 200v200h-150q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM50 100h1100q21 0 35.5 -14.5t14.5 -35.5v-50h-1200v50q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M600 1181q75 0 128 -53t53 -128t-53 -128t-128 -53t-128 53t-53 128t53 128t128 53zM602 798h46q34 0 55.5 -28.5t21.5 -86.5q0 -76 39 -183h-324q39 107 39 183q0 58 21.5 86.5t56.5 28.5h45zM250 400h700q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-13 l138 -100h-950l137 100h-12q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM50 100h1100q21 0 35.5 -14.5t14.5 -35.5v-50h-1200v50q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M600 1300q47 0 92.5 -53.5t71 -123t25.5 -123.5q0 -78 -55.5 -133.5t-133.5 -55.5t-133.5 55.5t-55.5 133.5q0 62 34 143l144 -143l111 111l-163 163q34 26 63 26zM602 798h46q34 0 55.5 -28.5t21.5 -86.5q0 -76 39 -183h-324q39 107 39 183q0 58 21.5 86.5t56.5 28.5h45 zM250 400h700q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-13l138 -100h-950l137 100h-12q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM50 100h1100q21 0 35.5 -14.5t14.5 -35.5v-50h-1200v50q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M600 1200l300 -161v-139h-300q0 -57 18.5 -108t50 -91.5t63 -72t70 -67.5t57.5 -61h-530q-60 83 -90.5 177.5t-30.5 178.5t33 164.5t87.5 139.5t126 96.5t145.5 41.5v-98zM250 400h700q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-13l138 -100h-950l137 100 h-12q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM50 100h1100q21 0 35.5 -14.5t14.5 -35.5v-50h-1200v50q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M600 1300q41 0 70.5 -29.5t29.5 -70.5v-78q46 -26 73 -72t27 -100v-50h-400v50q0 54 27 100t73 72v78q0 41 29.5 70.5t70.5 29.5zM400 800h400q54 0 100 -27t72 -73h-172v-100h200v-100h-200v-100h200v-100h-200v-100h200q0 -83 -58.5 -141.5t-141.5 -58.5h-400 q-83 0 -141.5 58.5t-58.5 141.5v400q0 83 58.5 141.5t141.5 58.5z" /> +<glyph unicode="" d="M150 1100h900q21 0 35.5 -14.5t14.5 -35.5v-500q0 -21 -14.5 -35.5t-35.5 -14.5h-900q-21 0 -35.5 14.5t-14.5 35.5v500q0 21 14.5 35.5t35.5 14.5zM125 400h950q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-283l224 -224q13 -13 13 -31.5t-13 -32 t-31.5 -13.5t-31.5 13l-88 88h-524l-87 -88q-13 -13 -32 -13t-32 13.5t-13 32t13 31.5l224 224h-289q-10 0 -17.5 7.5t-7.5 17.5v50q0 10 7.5 17.5t17.5 7.5zM541 300l-100 -100h324l-100 100h-124z" /> +<glyph unicode="" d="M200 1100h800q83 0 141.5 -58.5t58.5 -141.5v-200h-100q0 41 -29.5 70.5t-70.5 29.5h-250q-41 0 -70.5 -29.5t-29.5 -70.5h-100q0 41 -29.5 70.5t-70.5 29.5h-250q-41 0 -70.5 -29.5t-29.5 -70.5h-100v200q0 83 58.5 141.5t141.5 58.5zM100 600h1000q41 0 70.5 -29.5 t29.5 -70.5v-300h-1200v300q0 41 29.5 70.5t70.5 29.5zM300 100v-50q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v50h200zM1100 100v-50q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v50h200z" /> +<glyph unicode="" d="M480 1165l682 -683q31 -31 31 -75.5t-31 -75.5l-131 -131h-481l-517 518q-32 31 -32 75.5t32 75.5l295 296q31 31 75.5 31t76.5 -31zM108 794l342 -342l303 304l-341 341zM250 100h800q21 0 35.5 -14.5t14.5 -35.5v-50h-900v50q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M1057 647l-189 506q-8 19 -27.5 33t-40.5 14h-400q-21 0 -40.5 -14t-27.5 -33l-189 -506q-8 -19 1.5 -33t30.5 -14h625v-150q0 -21 14.5 -35.5t35.5 -14.5t35.5 14.5t14.5 35.5v150h125q21 0 30.5 14t1.5 33zM897 0h-595v50q0 21 14.5 35.5t35.5 14.5h50v50 q0 21 14.5 35.5t35.5 14.5h48v300h200v-300h47q21 0 35.5 -14.5t14.5 -35.5v-50h50q21 0 35.5 -14.5t14.5 -35.5v-50z" /> +<glyph unicode="" d="M900 800h300v-575q0 -10 -7.5 -17.5t-17.5 -7.5h-375v591l-300 300v84q0 10 7.5 17.5t17.5 7.5h375v-400zM1200 900h-200v200zM400 600h300v-575q0 -10 -7.5 -17.5t-17.5 -7.5h-650q-10 0 -17.5 7.5t-7.5 17.5v950q0 10 7.5 17.5t17.5 7.5h375v-400zM700 700h-200v200z " /> +<glyph unicode="" d="M484 1095h195q75 0 146 -32.5t124 -86t89.5 -122.5t48.5 -142q18 -14 35 -20q31 -10 64.5 6.5t43.5 48.5q10 34 -15 71q-19 27 -9 43q5 8 12.5 11t19 -1t23.5 -16q41 -44 39 -105q-3 -63 -46 -106.5t-104 -43.5h-62q-7 -55 -35 -117t-56 -100l-39 -234q-3 -20 -20 -34.5 t-38 -14.5h-100q-21 0 -33 14.5t-9 34.5l12 70q-49 -14 -91 -14h-195q-24 0 -65 8l-11 -64q-3 -20 -20 -34.5t-38 -14.5h-100q-21 0 -33 14.5t-9 34.5l26 157q-84 74 -128 175l-159 53q-19 7 -33 26t-14 40v50q0 21 14.5 35.5t35.5 14.5h124q11 87 56 166l-111 95 q-16 14 -12.5 23.5t24.5 9.5h203q116 101 250 101zM675 1000h-250q-10 0 -17.5 -7.5t-7.5 -17.5v-50q0 -10 7.5 -17.5t17.5 -7.5h250q10 0 17.5 7.5t7.5 17.5v50q0 10 -7.5 17.5t-17.5 7.5z" /> +<glyph unicode="" d="M641 900l423 247q19 8 42 2.5t37 -21.5l32 -38q14 -15 12.5 -36t-17.5 -34l-139 -120h-390zM50 1100h106q67 0 103 -17t66 -71l102 -212h823q21 0 35.5 -14.5t14.5 -35.5v-50q0 -21 -14 -40t-33 -26l-737 -132q-23 -4 -40 6t-26 25q-42 67 -100 67h-300q-62 0 -106 44 t-44 106v200q0 62 44 106t106 44zM173 928h-80q-19 0 -28 -14t-9 -35v-56q0 -51 42 -51h134q16 0 21.5 8t5.5 24q0 11 -16 45t-27 51q-18 28 -43 28zM550 727q-32 0 -54.5 -22.5t-22.5 -54.5t22.5 -54.5t54.5 -22.5t54.5 22.5t22.5 54.5t-22.5 54.5t-54.5 22.5zM130 389 l152 130q18 19 34 24t31 -3.5t24.5 -17.5t25.5 -28q28 -35 50.5 -51t48.5 -13l63 5l48 -179q13 -61 -3.5 -97.5t-67.5 -79.5l-80 -69q-47 -40 -109 -35.5t-103 51.5l-130 151q-40 47 -35.5 109.5t51.5 102.5zM380 377l-102 -88q-31 -27 2 -65l37 -43q13 -15 27.5 -19.5 t31.5 6.5l61 53q19 16 14 49q-2 20 -12 56t-17 45q-11 12 -19 14t-23 -8z" /> +<glyph unicode="" d="M625 1200h150q10 0 17.5 -7.5t7.5 -17.5v-109q79 -33 131 -87.5t53 -128.5q1 -46 -15 -84.5t-39 -61t-46 -38t-39 -21.5l-17 -6q6 0 15 -1.5t35 -9t50 -17.5t53 -30t50 -45t35.5 -64t14.5 -84q0 -59 -11.5 -105.5t-28.5 -76.5t-44 -51t-49.5 -31.5t-54.5 -16t-49.5 -6.5 t-43.5 -1v-75q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v75h-100v-75q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v75h-175q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5h75v600h-75q-10 0 -17.5 7.5t-7.5 17.5v150 q0 10 7.5 17.5t17.5 7.5h175v75q0 10 7.5 17.5t17.5 7.5h150q10 0 17.5 -7.5t7.5 -17.5v-75h100v75q0 10 7.5 17.5t17.5 7.5zM400 900v-200h263q28 0 48.5 10.5t30 25t15 29t5.5 25.5l1 10q0 4 -0.5 11t-6 24t-15 30t-30 24t-48.5 11h-263zM400 500v-200h363q28 0 48.5 10.5 t30 25t15 29t5.5 25.5l1 10q0 4 -0.5 11t-6 24t-15 30t-30 24t-48.5 11h-363z" /> +<glyph unicode="" d="M212 1198h780q86 0 147 -61t61 -147v-416q0 -51 -18 -142.5t-36 -157.5l-18 -66q-29 -87 -93.5 -146.5t-146.5 -59.5h-572q-82 0 -147 59t-93 147q-8 28 -20 73t-32 143.5t-20 149.5v416q0 86 61 147t147 61zM600 1045q-70 0 -132.5 -11.5t-105.5 -30.5t-78.5 -41.5 t-57 -45t-36 -41t-20.5 -30.5l-6 -12l156 -243h560l156 243q-2 5 -6 12.5t-20 29.5t-36.5 42t-57 44.5t-79 42t-105 29.5t-132.5 12zM762 703h-157l195 261z" /> +<glyph unicode="" d="M475 1300h150q103 0 189 -86t86 -189v-500q0 -41 -42 -83t-83 -42h-450q-41 0 -83 42t-42 83v500q0 103 86 189t189 86zM700 300v-225q0 -21 -27 -48t-48 -27h-150q-21 0 -48 27t-27 48v225h300z" /> +<glyph unicode="" d="M475 1300h96q0 -150 89.5 -239.5t239.5 -89.5v-446q0 -41 -42 -83t-83 -42h-450q-41 0 -83 42t-42 83v500q0 103 86 189t189 86zM700 300v-225q0 -21 -27 -48t-48 -27h-150q-21 0 -48 27t-27 48v225h300z" /> +<glyph unicode="" d="M1294 767l-638 -283l-378 170l-78 -60v-224l100 -150v-199l-150 148l-150 -149v200l100 150v250q0 4 -0.5 10.5t0 9.5t1 8t3 8t6.5 6l47 40l-147 65l642 283zM1000 380l-350 -166l-350 166v147l350 -165l350 165v-147z" /> +<glyph unicode="" d="M250 800q62 0 106 -44t44 -106t-44 -106t-106 -44t-106 44t-44 106t44 106t106 44zM650 800q62 0 106 -44t44 -106t-44 -106t-106 -44t-106 44t-44 106t44 106t106 44zM1050 800q62 0 106 -44t44 -106t-44 -106t-106 -44t-106 44t-44 106t44 106t106 44z" /> +<glyph unicode="" d="M550 1100q62 0 106 -44t44 -106t-44 -106t-106 -44t-106 44t-44 106t44 106t106 44zM550 700q62 0 106 -44t44 -106t-44 -106t-106 -44t-106 44t-44 106t44 106t106 44zM550 300q62 0 106 -44t44 -106t-44 -106t-106 -44t-106 44t-44 106t44 106t106 44z" /> +<glyph unicode="" d="M125 1100h950q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-950q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5zM125 700h950q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-950q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5 t17.5 7.5zM125 300h950q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-950q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5z" /> +<glyph unicode="" d="M350 1200h500q162 0 256 -93.5t94 -256.5v-500q0 -165 -93.5 -257.5t-256.5 -92.5h-500q-165 0 -257.5 92.5t-92.5 257.5v500q0 165 92.5 257.5t257.5 92.5zM900 1000h-600q-41 0 -70.5 -29.5t-29.5 -70.5v-600q0 -41 29.5 -70.5t70.5 -29.5h600q41 0 70.5 29.5 t29.5 70.5v600q0 41 -29.5 70.5t-70.5 29.5zM350 900h500q21 0 35.5 -14.5t14.5 -35.5v-300q0 -21 -14.5 -35.5t-35.5 -14.5h-500q-21 0 -35.5 14.5t-14.5 35.5v300q0 21 14.5 35.5t35.5 14.5zM400 800v-200h400v200h-400z" /> +<glyph unicode="" d="M150 1100h1000q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-50v-200h50q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-50v-200h50q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-50v-200h50q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5 t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5h50v200h-50q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5h50v200h-50q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5h50v200h-50q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M650 1187q87 -67 118.5 -156t0 -178t-118.5 -155q-87 66 -118.5 155t0 178t118.5 156zM300 800q124 0 212 -88t88 -212q-124 0 -212 88t-88 212zM1000 800q0 -124 -88 -212t-212 -88q0 124 88 212t212 88zM300 500q124 0 212 -88t88 -212q-124 0 -212 88t-88 212z M1000 500q0 -124 -88 -212t-212 -88q0 124 88 212t212 88zM700 199v-144q0 -21 -14.5 -35.5t-35.5 -14.5t-35.5 14.5t-14.5 35.5v142q40 -4 43 -4q17 0 57 6z" /> +<glyph unicode="" d="M745 878l69 19q25 6 45 -12l298 -295q11 -11 15 -26.5t-2 -30.5q-5 -14 -18 -23.5t-28 -9.5h-8q1 0 1 -13q0 -29 -2 -56t-8.5 -62t-20 -63t-33 -53t-51 -39t-72.5 -14h-146q-184 0 -184 288q0 24 10 47q-20 4 -62 4t-63 -4q11 -24 11 -47q0 -288 -184 -288h-142 q-48 0 -84.5 21t-56 51t-32 71.5t-16 75t-3.5 68.5q0 13 2 13h-7q-15 0 -27.5 9.5t-18.5 23.5q-6 15 -2 30.5t15 25.5l298 296q20 18 46 11l76 -19q20 -5 30.5 -22.5t5.5 -37.5t-22.5 -31t-37.5 -5l-51 12l-182 -193h891l-182 193l-44 -12q-20 -5 -37.5 6t-22.5 31t6 37.5 t31 22.5z" /> +<glyph unicode="" d="M1200 900h-50q0 21 -4 37t-9.5 26.5t-18 17.5t-22 11t-28.5 5.5t-31 2t-37 0.5h-200v-850q0 -22 25 -34.5t50 -13.5l25 -2v-100h-400v100q4 0 11 0.5t24 3t30 7t24 15t11 24.5v850h-200q-25 0 -37 -0.5t-31 -2t-28.5 -5.5t-22 -11t-18 -17.5t-9.5 -26.5t-4 -37h-50v300 h1000v-300zM500 450h-25q0 15 -4 24.5t-9 14.5t-17 7.5t-20 3t-25 0.5h-100v-425q0 -11 12.5 -17.5t25.5 -7.5h12v-50h-200v50q50 0 50 25v425h-100q-17 0 -25 -0.5t-20 -3t-17 -7.5t-9 -14.5t-4 -24.5h-25v150h500v-150z" /> +<glyph unicode="" d="M1000 300v50q-25 0 -55 32q-14 14 -25 31t-16 27l-4 11l-289 747h-69l-300 -754q-18 -35 -39 -56q-9 -9 -24.5 -18.5t-26.5 -14.5l-11 -5v-50h273v50q-49 0 -78.5 21.5t-11.5 67.5l69 176h293l61 -166q13 -34 -3.5 -66.5t-55.5 -32.5v-50h312zM412 691l134 342l121 -342 h-255zM1100 150v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5z" /> +<glyph unicode="" d="M50 1200h1100q21 0 35.5 -14.5t14.5 -35.5v-1100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v1100q0 21 14.5 35.5t35.5 14.5zM611 1118h-70q-13 0 -18 -12l-299 -753q-17 -32 -35 -51q-18 -18 -56 -34q-12 -5 -12 -18v-50q0 -8 5.5 -14t14.5 -6 h273q8 0 14 6t6 14v50q0 8 -6 14t-14 6q-55 0 -71 23q-10 14 0 39l63 163h266l57 -153q11 -31 -6 -55q-12 -17 -36 -17q-8 0 -14 -6t-6 -14v-50q0 -8 6 -14t14 -6h313q8 0 14 6t6 14v50q0 7 -5.5 13t-13.5 7q-17 0 -42 25q-25 27 -40 63h-1l-288 748q-5 12 -19 12zM639 611 h-197l103 264z" /> +<glyph unicode="" d="M1200 1100h-1200v100h1200v-100zM50 1000h400q21 0 35.5 -14.5t14.5 -35.5v-900q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v900q0 21 14.5 35.5t35.5 14.5zM650 1000h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM700 900v-300h300v300h-300z" /> +<glyph unicode="" d="M50 1200h400q21 0 35.5 -14.5t14.5 -35.5v-900q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v900q0 21 14.5 35.5t35.5 14.5zM650 700h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400 q0 21 14.5 35.5t35.5 14.5zM700 600v-300h300v300h-300zM1200 0h-1200v100h1200v-100z" /> +<glyph unicode="" d="M50 1000h400q21 0 35.5 -14.5t14.5 -35.5v-350h100v150q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-150h100v-100h-100v-150q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v150h-100v-350q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5v800q0 21 14.5 35.5t35.5 14.5zM700 700v-300h300v300h-300z" /> +<glyph unicode="" d="M100 0h-100v1200h100v-1200zM250 1100h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM300 1000v-300h300v300h-300zM250 500h900q21 0 35.5 -14.5t14.5 -35.5v-400 q0 -21 -14.5 -35.5t-35.5 -14.5h-900q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M600 1100h150q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-150v-100h450q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-900q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5h350v100h-150q-21 0 -35.5 14.5 t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5h150v100h100v-100zM400 1000v-300h300v300h-300z" /> +<glyph unicode="" d="M1200 0h-100v1200h100v-1200zM550 1100h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM600 1000v-300h300v300h-300zM50 500h900q21 0 35.5 -14.5t14.5 -35.5v-400 q0 -21 -14.5 -35.5t-35.5 -14.5h-900q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5z" /> +<glyph unicode="" d="M865 565l-494 -494q-23 -23 -41 -23q-14 0 -22 13.5t-8 38.5v1000q0 25 8 38.5t22 13.5q18 0 41 -23l494 -494q14 -14 14 -35t-14 -35z" /> +<glyph unicode="" d="M335 635l494 494q29 29 50 20.5t21 -49.5v-1000q0 -41 -21 -49.5t-50 20.5l-494 494q-14 14 -14 35t14 35z" /> +<glyph unicode="" d="M100 900h1000q41 0 49.5 -21t-20.5 -50l-494 -494q-14 -14 -35 -14t-35 14l-494 494q-29 29 -20.5 50t49.5 21z" /> +<glyph unicode="" d="M635 865l494 -494q29 -29 20.5 -50t-49.5 -21h-1000q-41 0 -49.5 21t20.5 50l494 494q14 14 35 14t35 -14z" /> +<glyph unicode="" d="M700 741v-182l-692 -323v221l413 193l-413 193v221zM1200 0h-800v200h800v-200z" /> +<glyph unicode="" d="M1200 900h-200v-100h200v-100h-300v300h200v100h-200v100h300v-300zM0 700h50q0 21 4 37t9.5 26.5t18 17.5t22 11t28.5 5.5t31 2t37 0.5h100v-550q0 -22 -25 -34.5t-50 -13.5l-25 -2v-100h400v100q-4 0 -11 0.5t-24 3t-30 7t-24 15t-11 24.5v550h100q25 0 37 -0.5t31 -2 t28.5 -5.5t22 -11t18 -17.5t9.5 -26.5t4 -37h50v300h-800v-300z" /> +<glyph unicode="" d="M800 700h-50q0 21 -4 37t-9.5 26.5t-18 17.5t-22 11t-28.5 5.5t-31 2t-37 0.5h-100v-550q0 -22 25 -34.5t50 -14.5l25 -1v-100h-400v100q4 0 11 0.5t24 3t30 7t24 15t11 24.5v550h-100q-25 0 -37 -0.5t-31 -2t-28.5 -5.5t-22 -11t-18 -17.5t-9.5 -26.5t-4 -37h-50v300 h800v-300zM1100 200h-200v-100h200v-100h-300v300h200v100h-200v100h300v-300z" /> +<glyph unicode="" d="M701 1098h160q16 0 21 -11t-7 -23l-464 -464l464 -464q12 -12 7 -23t-21 -11h-160q-13 0 -23 9l-471 471q-7 8 -7 18t7 18l471 471q10 9 23 9z" /> +<glyph unicode="" d="M339 1098h160q13 0 23 -9l471 -471q7 -8 7 -18t-7 -18l-471 -471q-10 -9 -23 -9h-160q-16 0 -21 11t7 23l464 464l-464 464q-12 12 -7 23t21 11z" /> +<glyph unicode="" d="M1087 882q11 -5 11 -21v-160q0 -13 -9 -23l-471 -471q-8 -7 -18 -7t-18 7l-471 471q-9 10 -9 23v160q0 16 11 21t23 -7l464 -464l464 464q12 12 23 7z" /> +<glyph unicode="" d="M618 993l471 -471q9 -10 9 -23v-160q0 -16 -11 -21t-23 7l-464 464l-464 -464q-12 -12 -23 -7t-11 21v160q0 13 9 23l471 471q8 7 18 7t18 -7z" /> +<glyph unicode="" d="M1000 1200q0 -124 -88 -212t-212 -88q0 124 88 212t212 88zM450 1000h100q21 0 40 -14t26 -33l79 -194q5 1 16 3q34 6 54 9.5t60 7t65.5 1t61 -10t56.5 -23t42.5 -42t29 -64t5 -92t-19.5 -121.5q-1 -7 -3 -19.5t-11 -50t-20.5 -73t-32.5 -81.5t-46.5 -83t-64 -70 t-82.5 -50q-13 -5 -42 -5t-65.5 2.5t-47.5 2.5q-14 0 -49.5 -3.5t-63 -3.5t-43.5 7q-57 25 -104.5 78.5t-75 111.5t-46.5 112t-26 90l-7 35q-15 63 -18 115t4.5 88.5t26 64t39.5 43.5t52 25.5t58.5 13t62.5 2t59.5 -4.5t55.5 -8l-147 192q-12 18 -5.5 30t27.5 12z" /> +<glyph unicode="🔑" d="M250 1200h600q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-150v-500l-255 -178q-19 -9 -32 -1t-13 29v650h-150q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM400 1100v-100h300v100h-300z" /> +<glyph unicode="🚪" d="M250 1200h750q39 0 69.5 -40.5t30.5 -84.5v-933l-700 -117v950l600 125h-700v-1000h-100v1025q0 23 15.5 49t34.5 26zM500 525v-100l100 20v100z" /> +</font> +</defs></svg>
\ No newline at end of file diff --git a/examples/blog/static/fonts/glyphicons-halflings-regular.ttf b/examples/blog/static/fonts/glyphicons-halflings-regular.ttf Binary files differnew file mode 100644 index 000000000..1413fc609 --- /dev/null +++ b/examples/blog/static/fonts/glyphicons-halflings-regular.ttf diff --git a/examples/blog/static/fonts/glyphicons-halflings-regular.woff b/examples/blog/static/fonts/glyphicons-halflings-regular.woff Binary files differnew file mode 100644 index 000000000..9e612858f --- /dev/null +++ b/examples/blog/static/fonts/glyphicons-halflings-regular.woff diff --git a/examples/blog/static/fonts/glyphicons-halflings-regular.woff2 b/examples/blog/static/fonts/glyphicons-halflings-regular.woff2 Binary files differnew file mode 100644 index 000000000..64539b54c --- /dev/null +++ b/examples/blog/static/fonts/glyphicons-halflings-regular.woff2 diff --git a/examples/blog/static/js/bootstrap.js b/examples/blog/static/js/bootstrap.js new file mode 100644 index 000000000..01fbbcbaa --- /dev/null +++ b/examples/blog/static/js/bootstrap.js @@ -0,0 +1,2363 @@ +/*! + * Bootstrap v3.3.6 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under the MIT license + */ + +if (typeof jQuery === 'undefined') { + throw new Error('Bootstrap\'s JavaScript requires jQuery') +} + ++function ($) { + 'use strict'; + var version = $.fn.jquery.split(' ')[0].split('.') + if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1) || (version[0] > 2)) { + throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher, but lower than version 3') + } +}(jQuery); + +/* ======================================================================== + * Bootstrap: transition.js v3.3.6 + * http://getbootstrap.com/javascript/#transitions + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) + // ============================================================ + + function transitionEnd() { + var el = document.createElement('bootstrap') + + var transEndEventNames = { + WebkitTransition : 'webkitTransitionEnd', + MozTransition : 'transitionend', + OTransition : 'oTransitionEnd otransitionend', + transition : 'transitionend' + } + + for (var name in transEndEventNames) { + if (el.style[name] !== undefined) { + return { end: transEndEventNames[name] } + } + } + + return false // explicit for ie8 ( ._.) + } + + // http://blog.alexmaccaw.com/css-transitions + $.fn.emulateTransitionEnd = function (duration) { + var called = false + var $el = this + $(this).one('bsTransitionEnd', function () { called = true }) + var callback = function () { if (!called) $($el).trigger($.support.transition.end) } + setTimeout(callback, duration) + return this + } + + $(function () { + $.support.transition = transitionEnd() + + if (!$.support.transition) return + + $.event.special.bsTransitionEnd = { + bindType: $.support.transition.end, + delegateType: $.support.transition.end, + handle: function (e) { + if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) + } + } + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: alert.js v3.3.6 + * http://getbootstrap.com/javascript/#alerts + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // ALERT CLASS DEFINITION + // ====================== + + var dismiss = '[data-dismiss="alert"]' + var Alert = function (el) { + $(el).on('click', dismiss, this.close) + } + + Alert.VERSION = '3.3.6' + + Alert.TRANSITION_DURATION = 150 + + Alert.prototype.close = function (e) { + var $this = $(this) + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + var $parent = $(selector) + + if (e) e.preventDefault() + + if (!$parent.length) { + $parent = $this.closest('.alert') + } + + $parent.trigger(e = $.Event('close.bs.alert')) + + if (e.isDefaultPrevented()) return + + $parent.removeClass('in') + + function removeElement() { + // detach from parent, fire event then clean up data + $parent.detach().trigger('closed.bs.alert').remove() + } + + $.support.transition && $parent.hasClass('fade') ? + $parent + .one('bsTransitionEnd', removeElement) + .emulateTransitionEnd(Alert.TRANSITION_DURATION) : + removeElement() + } + + + // ALERT PLUGIN DEFINITION + // ======================= + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.alert') + + if (!data) $this.data('bs.alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + var old = $.fn.alert + + $.fn.alert = Plugin + $.fn.alert.Constructor = Alert + + + // ALERT NO CONFLICT + // ================= + + $.fn.alert.noConflict = function () { + $.fn.alert = old + return this + } + + + // ALERT DATA-API + // ============== + + $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: button.js v3.3.6 + * http://getbootstrap.com/javascript/#buttons + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // BUTTON PUBLIC CLASS DEFINITION + // ============================== + + var Button = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, Button.DEFAULTS, options) + this.isLoading = false + } + + Button.VERSION = '3.3.6' + + Button.DEFAULTS = { + loadingText: 'loading...' + } + + Button.prototype.setState = function (state) { + var d = 'disabled' + var $el = this.$element + var val = $el.is('input') ? 'val' : 'html' + var data = $el.data() + + state += 'Text' + + if (data.resetText == null) $el.data('resetText', $el[val]()) + + // push to event loop to allow forms to submit + setTimeout($.proxy(function () { + $el[val](data[state] == null ? this.options[state] : data[state]) + + if (state == 'loadingText') { + this.isLoading = true + $el.addClass(d).attr(d, d) + } else if (this.isLoading) { + this.isLoading = false + $el.removeClass(d).removeAttr(d) + } + }, this), 0) + } + + Button.prototype.toggle = function () { + var changed = true + var $parent = this.$element.closest('[data-toggle="buttons"]') + + if ($parent.length) { + var $input = this.$element.find('input') + if ($input.prop('type') == 'radio') { + if ($input.prop('checked')) changed = false + $parent.find('.active').removeClass('active') + this.$element.addClass('active') + } else if ($input.prop('type') == 'checkbox') { + if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false + this.$element.toggleClass('active') + } + $input.prop('checked', this.$element.hasClass('active')) + if (changed) $input.trigger('change') + } else { + this.$element.attr('aria-pressed', !this.$element.hasClass('active')) + this.$element.toggleClass('active') + } + } + + + // BUTTON PLUGIN DEFINITION + // ======================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.button') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.button', (data = new Button(this, options))) + + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + var old = $.fn.button + + $.fn.button = Plugin + $.fn.button.Constructor = Button + + + // BUTTON NO CONFLICT + // ================== + + $.fn.button.noConflict = function () { + $.fn.button = old + return this + } + + + // BUTTON DATA-API + // =============== + + $(document) + .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { + var $btn = $(e.target) + if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') + Plugin.call($btn, 'toggle') + if (!($(e.target).is('input[type="radio"]') || $(e.target).is('input[type="checkbox"]'))) e.preventDefault() + }) + .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { + $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type)) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: carousel.js v3.3.6 + * http://getbootstrap.com/javascript/#carousel + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // CAROUSEL CLASS DEFINITION + // ========================= + + var Carousel = function (element, options) { + this.$element = $(element) + this.$indicators = this.$element.find('.carousel-indicators') + this.options = options + this.paused = null + this.sliding = null + this.interval = null + this.$active = null + this.$items = null + + this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) + + this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element + .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) + .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) + } + + Carousel.VERSION = '3.3.6' + + Carousel.TRANSITION_DURATION = 600 + + Carousel.DEFAULTS = { + interval: 5000, + pause: 'hover', + wrap: true, + keyboard: true + } + + Carousel.prototype.keydown = function (e) { + if (/input|textarea/i.test(e.target.tagName)) return + switch (e.which) { + case 37: this.prev(); break + case 39: this.next(); break + default: return + } + + e.preventDefault() + } + + Carousel.prototype.cycle = function (e) { + e || (this.paused = false) + + this.interval && clearInterval(this.interval) + + this.options.interval + && !this.paused + && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) + + return this + } + + Carousel.prototype.getItemIndex = function (item) { + this.$items = item.parent().children('.item') + return this.$items.index(item || this.$active) + } + + Carousel.prototype.getItemForDirection = function (direction, active) { + var activeIndex = this.getItemIndex(active) + var willWrap = (direction == 'prev' && activeIndex === 0) + || (direction == 'next' && activeIndex == (this.$items.length - 1)) + if (willWrap && !this.options.wrap) return active + var delta = direction == 'prev' ? -1 : 1 + var itemIndex = (activeIndex + delta) % this.$items.length + return this.$items.eq(itemIndex) + } + + Carousel.prototype.to = function (pos) { + var that = this + var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active')) + + if (pos > (this.$items.length - 1) || pos < 0) return + + if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid" + if (activeIndex == pos) return this.pause().cycle() + + return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos)) + } + + Carousel.prototype.pause = function (e) { + e || (this.paused = true) + + if (this.$element.find('.next, .prev').length && $.support.transition) { + this.$element.trigger($.support.transition.end) + this.cycle(true) + } + + this.interval = clearInterval(this.interval) + + return this + } + + Carousel.prototype.next = function () { + if (this.sliding) return + return this.slide('next') + } + + Carousel.prototype.prev = function () { + if (this.sliding) return + return this.slide('prev') + } + + Carousel.prototype.slide = function (type, next) { + var $active = this.$element.find('.item.active') + var $next = next || this.getItemForDirection(type, $active) + var isCycling = this.interval + var direction = type == 'next' ? 'left' : 'right' + var that = this + + if ($next.hasClass('active')) return (this.sliding = false) + + var relatedTarget = $next[0] + var slideEvent = $.Event('slide.bs.carousel', { + relatedTarget: relatedTarget, + direction: direction + }) + this.$element.trigger(slideEvent) + if (slideEvent.isDefaultPrevented()) return + + this.sliding = true + + isCycling && this.pause() + + if (this.$indicators.length) { + this.$indicators.find('.active').removeClass('active') + var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) + $nextIndicator && $nextIndicator.addClass('active') + } + + var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid" + if ($.support.transition && this.$element.hasClass('slide')) { + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + $active + .one('bsTransitionEnd', function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { + that.$element.trigger(slidEvent) + }, 0) + }) + .emulateTransitionEnd(Carousel.TRANSITION_DURATION) + } else { + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger(slidEvent) + } + + isCycling && this.cycle() + + return this + } + + + // CAROUSEL PLUGIN DEFINITION + // ========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.carousel') + var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) + var action = typeof option == 'string' ? option : options.slide + + if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (action) data[action]() + else if (options.interval) data.pause().cycle() + }) + } + + var old = $.fn.carousel + + $.fn.carousel = Plugin + $.fn.carousel.Constructor = Carousel + + + // CAROUSEL NO CONFLICT + // ==================== + + $.fn.carousel.noConflict = function () { + $.fn.carousel = old + return this + } + + + // CAROUSEL DATA-API + // ================= + + var clickHandler = function (e) { + var href + var $this = $(this) + var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 + if (!$target.hasClass('carousel')) return + var options = $.extend({}, $target.data(), $this.data()) + var slideIndex = $this.attr('data-slide-to') + if (slideIndex) options.interval = false + + Plugin.call($target, options) + + if (slideIndex) { + $target.data('bs.carousel').to(slideIndex) + } + + e.preventDefault() + } + + $(document) + .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) + .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler) + + $(window).on('load', function () { + $('[data-ride="carousel"]').each(function () { + var $carousel = $(this) + Plugin.call($carousel, $carousel.data()) + }) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: collapse.js v3.3.6 + * http://getbootstrap.com/javascript/#collapse + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // COLLAPSE PUBLIC CLASS DEFINITION + // ================================ + + var Collapse = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, Collapse.DEFAULTS, options) + this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + + '[data-toggle="collapse"][data-target="#' + element.id + '"]') + this.transitioning = null + + if (this.options.parent) { + this.$parent = this.getParent() + } else { + this.addAriaAndCollapsedClass(this.$element, this.$trigger) + } + + if (this.options.toggle) this.toggle() + } + + Collapse.VERSION = '3.3.6' + + Collapse.TRANSITION_DURATION = 350 + + Collapse.DEFAULTS = { + toggle: true + } + + Collapse.prototype.dimension = function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + Collapse.prototype.show = function () { + if (this.transitioning || this.$element.hasClass('in')) return + + var activesData + var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing') + + if (actives && actives.length) { + activesData = actives.data('bs.collapse') + if (activesData && activesData.transitioning) return + } + + var startEvent = $.Event('show.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + if (actives && actives.length) { + Plugin.call(actives, 'hide') + activesData || actives.data('bs.collapse', null) + } + + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + .addClass('collapsing')[dimension](0) + .attr('aria-expanded', true) + + this.$trigger + .removeClass('collapsed') + .attr('aria-expanded', true) + + this.transitioning = 1 + + var complete = function () { + this.$element + .removeClass('collapsing') + .addClass('collapse in')[dimension]('') + this.transitioning = 0 + this.$element + .trigger('shown.bs.collapse') + } + + if (!$.support.transition) return complete.call(this) + + var scrollSize = $.camelCase(['scroll', dimension].join('-')) + + this.$element + .one('bsTransitionEnd', $.proxy(complete, this)) + .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]) + } + + Collapse.prototype.hide = function () { + if (this.transitioning || !this.$element.hasClass('in')) return + + var startEvent = $.Event('hide.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + var dimension = this.dimension() + + this.$element[dimension](this.$element[dimension]())[0].offsetHeight + + this.$element + .addClass('collapsing') + .removeClass('collapse in') + .attr('aria-expanded', false) + + this.$trigger + .addClass('collapsed') + .attr('aria-expanded', false) + + this.transitioning = 1 + + var complete = function () { + this.transitioning = 0 + this.$element + .removeClass('collapsing') + .addClass('collapse') + .trigger('hidden.bs.collapse') + } + + if (!$.support.transition) return complete.call(this) + + this.$element + [dimension](0) + .one('bsTransitionEnd', $.proxy(complete, this)) + .emulateTransitionEnd(Collapse.TRANSITION_DURATION) + } + + Collapse.prototype.toggle = function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + Collapse.prototype.getParent = function () { + return $(this.options.parent) + .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') + .each($.proxy(function (i, element) { + var $element = $(element) + this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) + }, this)) + .end() + } + + Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { + var isOpen = $element.hasClass('in') + + $element.attr('aria-expanded', isOpen) + $trigger + .toggleClass('collapsed', !isOpen) + .attr('aria-expanded', isOpen) + } + + function getTargetFromTrigger($trigger) { + var href + var target = $trigger.attr('data-target') + || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 + + return $(target) + } + + + // COLLAPSE PLUGIN DEFINITION + // ========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.collapse') + var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) + + if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false + if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.collapse + + $.fn.collapse = Plugin + $.fn.collapse.Constructor = Collapse + + + // COLLAPSE NO CONFLICT + // ==================== + + $.fn.collapse.noConflict = function () { + $.fn.collapse = old + return this + } + + + // COLLAPSE DATA-API + // ================= + + $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { + var $this = $(this) + + if (!$this.attr('data-target')) e.preventDefault() + + var $target = getTargetFromTrigger($this) + var data = $target.data('bs.collapse') + var option = data ? 'toggle' : $this.data() + + Plugin.call($target, option) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: dropdown.js v3.3.6 + * http://getbootstrap.com/javascript/#dropdowns + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // DROPDOWN CLASS DEFINITION + // ========================= + + var backdrop = '.dropdown-backdrop' + var toggle = '[data-toggle="dropdown"]' + var Dropdown = function (element) { + $(element).on('click.bs.dropdown', this.toggle) + } + + Dropdown.VERSION = '3.3.6' + + function getParent($this) { + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + var $parent = selector && $(selector) + + return $parent && $parent.length ? $parent : $this.parent() + } + + function clearMenus(e) { + if (e && e.which === 3) return + $(backdrop).remove() + $(toggle).each(function () { + var $this = $(this) + var $parent = getParent($this) + var relatedTarget = { relatedTarget: this } + + if (!$parent.hasClass('open')) return + + if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return + + $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) + + if (e.isDefaultPrevented()) return + + $this.attr('aria-expanded', 'false') + $parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget)) + }) + } + + Dropdown.prototype.toggle = function (e) { + var $this = $(this) + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { + // if mobile we use a backdrop because click events don't delegate + $(document.createElement('div')) + .addClass('dropdown-backdrop') + .insertAfter($(this)) + .on('click', clearMenus) + } + + var relatedTarget = { relatedTarget: this } + $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) + + if (e.isDefaultPrevented()) return + + $this + .trigger('focus') + .attr('aria-expanded', 'true') + + $parent + .toggleClass('open') + .trigger($.Event('shown.bs.dropdown', relatedTarget)) + } + + return false + } + + Dropdown.prototype.keydown = function (e) { + if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return + + var $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + if (!isActive && e.which != 27 || isActive && e.which == 27) { + if (e.which == 27) $parent.find(toggle).trigger('focus') + return $this.trigger('click') + } + + var desc = ' li:not(.disabled):visible a' + var $items = $parent.find('.dropdown-menu' + desc) + + if (!$items.length) return + + var index = $items.index(e.target) + + if (e.which == 38 && index > 0) index-- // up + if (e.which == 40 && index < $items.length - 1) index++ // down + if (!~index) index = 0 + + $items.eq(index).trigger('focus') + } + + + // DROPDOWN PLUGIN DEFINITION + // ========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.dropdown') + + if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + var old = $.fn.dropdown + + $.fn.dropdown = Plugin + $.fn.dropdown.Constructor = Dropdown + + + // DROPDOWN NO CONFLICT + // ==================== + + $.fn.dropdown.noConflict = function () { + $.fn.dropdown = old + return this + } + + + // APPLY TO STANDARD DROPDOWN ELEMENTS + // =================================== + + $(document) + .on('click.bs.dropdown.data-api', clearMenus) + .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) + .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) + .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: modal.js v3.3.6 + * http://getbootstrap.com/javascript/#modals + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // MODAL CLASS DEFINITION + // ====================== + + var Modal = function (element, options) { + this.options = options + this.$body = $(document.body) + this.$element = $(element) + this.$dialog = this.$element.find('.modal-dialog') + this.$backdrop = null + this.isShown = null + this.originalBodyPad = null + this.scrollbarWidth = 0 + this.ignoreBackdropClick = false + + if (this.options.remote) { + this.$element + .find('.modal-content') + .load(this.options.remote, $.proxy(function () { + this.$element.trigger('loaded.bs.modal') + }, this)) + } + } + + Modal.VERSION = '3.3.6' + + Modal.TRANSITION_DURATION = 300 + Modal.BACKDROP_TRANSITION_DURATION = 150 + + Modal.DEFAULTS = { + backdrop: true, + keyboard: true, + show: true + } + + Modal.prototype.toggle = function (_relatedTarget) { + return this.isShown ? this.hide() : this.show(_relatedTarget) + } + + Modal.prototype.show = function (_relatedTarget) { + var that = this + var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) + + this.$element.trigger(e) + + if (this.isShown || e.isDefaultPrevented()) return + + this.isShown = true + + this.checkScrollbar() + this.setScrollbar() + this.$body.addClass('modal-open') + + this.escape() + this.resize() + + this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) + + this.$dialog.on('mousedown.dismiss.bs.modal', function () { + that.$element.one('mouseup.dismiss.bs.modal', function (e) { + if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true + }) + }) + + this.backdrop(function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + if (!that.$element.parent().length) { + that.$element.appendTo(that.$body) // don't move modals dom position + } + + that.$element + .show() + .scrollTop(0) + + that.adjustDialog() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element.addClass('in') + + that.enforceFocus() + + var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) + + transition ? + that.$dialog // wait for modal to slide in + .one('bsTransitionEnd', function () { + that.$element.trigger('focus').trigger(e) + }) + .emulateTransitionEnd(Modal.TRANSITION_DURATION) : + that.$element.trigger('focus').trigger(e) + }) + } + + Modal.prototype.hide = function (e) { + if (e) e.preventDefault() + + e = $.Event('hide.bs.modal') + + this.$element.trigger(e) + + if (!this.isShown || e.isDefaultPrevented()) return + + this.isShown = false + + this.escape() + this.resize() + + $(document).off('focusin.bs.modal') + + this.$element + .removeClass('in') + .off('click.dismiss.bs.modal') + .off('mouseup.dismiss.bs.modal') + + this.$dialog.off('mousedown.dismiss.bs.modal') + + $.support.transition && this.$element.hasClass('fade') ? + this.$element + .one('bsTransitionEnd', $.proxy(this.hideModal, this)) + .emulateTransitionEnd(Modal.TRANSITION_DURATION) : + this.hideModal() + } + + Modal.prototype.enforceFocus = function () { + $(document) + .off('focusin.bs.modal') // guard against infinite focus loop + .on('focusin.bs.modal', $.proxy(function (e) { + if (this.$element[0] !== e.target && !this.$element.has(e.target).length) { + this.$element.trigger('focus') + } + }, this)) + } + + Modal.prototype.escape = function () { + if (this.isShown && this.options.keyboard) { + this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) { + e.which == 27 && this.hide() + }, this)) + } else if (!this.isShown) { + this.$element.off('keydown.dismiss.bs.modal') + } + } + + Modal.prototype.resize = function () { + if (this.isShown) { + $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this)) + } else { + $(window).off('resize.bs.modal') + } + } + + Modal.prototype.hideModal = function () { + var that = this + this.$element.hide() + this.backdrop(function () { + that.$body.removeClass('modal-open') + that.resetAdjustments() + that.resetScrollbar() + that.$element.trigger('hidden.bs.modal') + }) + } + + Modal.prototype.removeBackdrop = function () { + this.$backdrop && this.$backdrop.remove() + this.$backdrop = null + } + + Modal.prototype.backdrop = function (callback) { + var that = this + var animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $(document.createElement('div')) + .addClass('modal-backdrop ' + animate) + .appendTo(this.$body) + + this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { + if (this.ignoreBackdropClick) { + this.ignoreBackdropClick = false + return + } + if (e.target !== e.currentTarget) return + this.options.backdrop == 'static' + ? this.$element[0].focus() + : this.hide() + }, this)) + + if (doAnimate) this.$backdrop[0].offsetWidth // force reflow + + this.$backdrop.addClass('in') + + if (!callback) return + + doAnimate ? + this.$backdrop + .one('bsTransitionEnd', callback) + .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : + callback() + + } else if (!this.isShown && this.$backdrop) { + this.$backdrop.removeClass('in') + + var callbackRemove = function () { + that.removeBackdrop() + callback && callback() + } + $.support.transition && this.$element.hasClass('fade') ? + this.$backdrop + .one('bsTransitionEnd', callbackRemove) + .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : + callbackRemove() + + } else if (callback) { + callback() + } + } + + // these following methods are used to handle overflowing modals + + Modal.prototype.handleUpdate = function () { + this.adjustDialog() + } + + Modal.prototype.adjustDialog = function () { + var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight + + this.$element.css({ + paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', + paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' + }) + } + + Modal.prototype.resetAdjustments = function () { + this.$element.css({ + paddingLeft: '', + paddingRight: '' + }) + } + + Modal.prototype.checkScrollbar = function () { + var fullWindowWidth = window.innerWidth + if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8 + var documentElementRect = document.documentElement.getBoundingClientRect() + fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left) + } + this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth + this.scrollbarWidth = this.measureScrollbar() + } + + Modal.prototype.setScrollbar = function () { + var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10) + this.originalBodyPad = document.body.style.paddingRight || '' + if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth) + } + + Modal.prototype.resetScrollbar = function () { + this.$body.css('padding-right', this.originalBodyPad) + } + + Modal.prototype.measureScrollbar = function () { // thx walsh + var scrollDiv = document.createElement('div') + scrollDiv.className = 'modal-scrollbar-measure' + this.$body.append(scrollDiv) + var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth + this.$body[0].removeChild(scrollDiv) + return scrollbarWidth + } + + + // MODAL PLUGIN DEFINITION + // ======================= + + function Plugin(option, _relatedTarget) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.modal') + var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) + + if (!data) $this.data('bs.modal', (data = new Modal(this, options))) + if (typeof option == 'string') data[option](_relatedTarget) + else if (options.show) data.show(_relatedTarget) + }) + } + + var old = $.fn.modal + + $.fn.modal = Plugin + $.fn.modal.Constructor = Modal + + + // MODAL NO CONFLICT + // ================= + + $.fn.modal.noConflict = function () { + $.fn.modal = old + return this + } + + + // MODAL DATA-API + // ============== + + $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { + var $this = $(this) + var href = $this.attr('href') + var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 + var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) + + if ($this.is('a')) e.preventDefault() + + $target.one('show.bs.modal', function (showEvent) { + if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown + $target.one('hidden.bs.modal', function () { + $this.is(':visible') && $this.trigger('focus') + }) + }) + Plugin.call($target, option, this) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: tooltip.js v3.3.6 + * http://getbootstrap.com/javascript/#tooltip + * Inspired by the original jQuery.tipsy by Jason Frame + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // TOOLTIP PUBLIC CLASS DEFINITION + // =============================== + + var Tooltip = function (element, options) { + this.type = null + this.options = null + this.enabled = null + this.timeout = null + this.hoverState = null + this.$element = null + this.inState = null + + this.init('tooltip', element, options) + } + + Tooltip.VERSION = '3.3.6' + + Tooltip.TRANSITION_DURATION = 150 + + Tooltip.DEFAULTS = { + animation: true, + placement: 'top', + selector: false, + template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>', + trigger: 'hover focus', + title: '', + delay: 0, + html: false, + container: false, + viewport: { + selector: 'body', + padding: 0 + } + } + + Tooltip.prototype.init = function (type, element, options) { + this.enabled = true + this.type = type + this.$element = $(element) + this.options = this.getOptions(options) + this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport)) + this.inState = { click: false, hover: false, focus: false } + + if (this.$element[0] instanceof document.constructor && !this.options.selector) { + throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!') + } + + var triggers = this.options.trigger.split(' ') + + for (var i = triggers.length; i--;) { + var trigger = triggers[i] + + if (trigger == 'click') { + this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) + } else if (trigger != 'manual') { + var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' + var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' + + this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) + this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) + } + } + + this.options.selector ? + (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : + this.fixTitle() + } + + Tooltip.prototype.getDefaults = function () { + return Tooltip.DEFAULTS + } + + Tooltip.prototype.getOptions = function (options) { + options = $.extend({}, this.getDefaults(), this.$element.data(), options) + + if (options.delay && typeof options.delay == 'number') { + options.delay = { + show: options.delay, + hide: options.delay + } + } + + return options + } + + Tooltip.prototype.getDelegateOptions = function () { + var options = {} + var defaults = this.getDefaults() + + this._options && $.each(this._options, function (key, value) { + if (defaults[key] != value) options[key] = value + }) + + return options + } + + Tooltip.prototype.enter = function (obj) { + var self = obj instanceof this.constructor ? + obj : $(obj.currentTarget).data('bs.' + this.type) + + if (!self) { + self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) + $(obj.currentTarget).data('bs.' + this.type, self) + } + + if (obj instanceof $.Event) { + self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true + } + + if (self.tip().hasClass('in') || self.hoverState == 'in') { + self.hoverState = 'in' + return + } + + clearTimeout(self.timeout) + + self.hoverState = 'in' + + if (!self.options.delay || !self.options.delay.show) return self.show() + + self.timeout = setTimeout(function () { + if (self.hoverState == 'in') self.show() + }, self.options.delay.show) + } + + Tooltip.prototype.isInStateTrue = function () { + for (var key in this.inState) { + if (this.inState[key]) return true + } + + return false + } + + Tooltip.prototype.leave = function (obj) { + var self = obj instanceof this.constructor ? + obj : $(obj.currentTarget).data('bs.' + this.type) + + if (!self) { + self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) + $(obj.currentTarget).data('bs.' + this.type, self) + } + + if (obj instanceof $.Event) { + self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false + } + + if (self.isInStateTrue()) return + + clearTimeout(self.timeout) + + self.hoverState = 'out' + + if (!self.options.delay || !self.options.delay.hide) return self.hide() + + self.timeout = setTimeout(function () { + if (self.hoverState == 'out') self.hide() + }, self.options.delay.hide) + } + + Tooltip.prototype.show = function () { + var e = $.Event('show.bs.' + this.type) + + if (this.hasContent() && this.enabled) { + this.$element.trigger(e) + + var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]) + if (e.isDefaultPrevented() || !inDom) return + var that = this + + var $tip = this.tip() + + var tipId = this.getUID(this.type) + + this.setContent() + $tip.attr('id', tipId) + this.$element.attr('aria-describedby', tipId) + + if (this.options.animation) $tip.addClass('fade') + + var placement = typeof this.options.placement == 'function' ? + this.options.placement.call(this, $tip[0], this.$element[0]) : + this.options.placement + + var autoToken = /\s?auto?\s?/i + var autoPlace = autoToken.test(placement) + if (autoPlace) placement = placement.replace(autoToken, '') || 'top' + + $tip + .detach() + .css({ top: 0, left: 0, display: 'block' }) + .addClass(placement) + .data('bs.' + this.type, this) + + this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) + this.$element.trigger('inserted.bs.' + this.type) + + var pos = this.getPosition() + var actualWidth = $tip[0].offsetWidth + var actualHeight = $tip[0].offsetHeight + + if (autoPlace) { + var orgPlacement = placement + var viewportDim = this.getPosition(this.$viewport) + + placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' : + placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' : + placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' : + placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' : + placement + + $tip + .removeClass(orgPlacement) + .addClass(placement) + } + + var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) + + this.applyPlacement(calculatedOffset, placement) + + var complete = function () { + var prevHoverState = that.hoverState + that.$element.trigger('shown.bs.' + that.type) + that.hoverState = null + + if (prevHoverState == 'out') that.leave(that) + } + + $.support.transition && this.$tip.hasClass('fade') ? + $tip + .one('bsTransitionEnd', complete) + .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : + complete() + } + } + + Tooltip.prototype.applyPlacement = function (offset, placement) { + var $tip = this.tip() + var width = $tip[0].offsetWidth + var height = $tip[0].offsetHeight + + // manually read margins because getBoundingClientRect includes difference + var marginTop = parseInt($tip.css('margin-top'), 10) + var marginLeft = parseInt($tip.css('margin-left'), 10) + + // we must check for NaN for ie 8/9 + if (isNaN(marginTop)) marginTop = 0 + if (isNaN(marginLeft)) marginLeft = 0 + + offset.top += marginTop + offset.left += marginLeft + + // $.fn.offset doesn't round pixel values + // so we use setOffset directly with our own function B-0 + $.offset.setOffset($tip[0], $.extend({ + using: function (props) { + $tip.css({ + top: Math.round(props.top), + left: Math.round(props.left) + }) + } + }, offset), 0) + + $tip.addClass('in') + + // check to see if placing tip in new offset caused the tip to resize itself + var actualWidth = $tip[0].offsetWidth + var actualHeight = $tip[0].offsetHeight + + if (placement == 'top' && actualHeight != height) { + offset.top = offset.top + height - actualHeight + } + + var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) + + if (delta.left) offset.left += delta.left + else offset.top += delta.top + + var isVertical = /top|bottom/.test(placement) + var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight + var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight' + + $tip.offset(offset) + this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical) + } + + Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) { + this.arrow() + .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%') + .css(isVertical ? 'top' : 'left', '') + } + + Tooltip.prototype.setContent = function () { + var $tip = this.tip() + var title = this.getTitle() + + $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) + $tip.removeClass('fade in top bottom left right') + } + + Tooltip.prototype.hide = function (callback) { + var that = this + var $tip = $(this.$tip) + var e = $.Event('hide.bs.' + this.type) + + function complete() { + if (that.hoverState != 'in') $tip.detach() + that.$element + .removeAttr('aria-describedby') + .trigger('hidden.bs.' + that.type) + callback && callback() + } + + this.$element.trigger(e) + + if (e.isDefaultPrevented()) return + + $tip.removeClass('in') + + $.support.transition && $tip.hasClass('fade') ? + $tip + .one('bsTransitionEnd', complete) + .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : + complete() + + this.hoverState = null + + return this + } + + Tooltip.prototype.fixTitle = function () { + var $e = this.$element + if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') { + $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') + } + } + + Tooltip.prototype.hasContent = function () { + return this.getTitle() + } + + Tooltip.prototype.getPosition = function ($element) { + $element = $element || this.$element + + var el = $element[0] + var isBody = el.tagName == 'BODY' + + var elRect = el.getBoundingClientRect() + if (elRect.width == null) { + // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093 + elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top }) + } + var elOffset = isBody ? { top: 0, left: 0 } : $element.offset() + var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() } + var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null + + return $.extend({}, elRect, scroll, outerDims, elOffset) + } + + Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { + return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : + placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : + placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : + /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } + + } + + Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) { + var delta = { top: 0, left: 0 } + if (!this.$viewport) return delta + + var viewportPadding = this.options.viewport && this.options.viewport.padding || 0 + var viewportDimensions = this.getPosition(this.$viewport) + + if (/right|left/.test(placement)) { + var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll + var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight + if (topEdgeOffset < viewportDimensions.top) { // top overflow + delta.top = viewportDimensions.top - topEdgeOffset + } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow + delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset + } + } else { + var leftEdgeOffset = pos.left - viewportPadding + var rightEdgeOffset = pos.left + viewportPadding + actualWidth + if (leftEdgeOffset < viewportDimensions.left) { // left overflow + delta.left = viewportDimensions.left - leftEdgeOffset + } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow + delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset + } + } + + return delta + } + + Tooltip.prototype.getTitle = function () { + var title + var $e = this.$element + var o = this.options + + title = $e.attr('data-original-title') + || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) + + return title + } + + Tooltip.prototype.getUID = function (prefix) { + do prefix += ~~(Math.random() * 1000000) + while (document.getElementById(prefix)) + return prefix + } + + Tooltip.prototype.tip = function () { + if (!this.$tip) { + this.$tip = $(this.options.template) + if (this.$tip.length != 1) { + throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!') + } + } + return this.$tip + } + + Tooltip.prototype.arrow = function () { + return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')) + } + + Tooltip.prototype.enable = function () { + this.enabled = true + } + + Tooltip.prototype.disable = function () { + this.enabled = false + } + + Tooltip.prototype.toggleEnabled = function () { + this.enabled = !this.enabled + } + + Tooltip.prototype.toggle = function (e) { + var self = this + if (e) { + self = $(e.currentTarget).data('bs.' + this.type) + if (!self) { + self = new this.constructor(e.currentTarget, this.getDelegateOptions()) + $(e.currentTarget).data('bs.' + this.type, self) + } + } + + if (e) { + self.inState.click = !self.inState.click + if (self.isInStateTrue()) self.enter(self) + else self.leave(self) + } else { + self.tip().hasClass('in') ? self.leave(self) : self.enter(self) + } + } + + Tooltip.prototype.destroy = function () { + var that = this + clearTimeout(this.timeout) + this.hide(function () { + that.$element.off('.' + that.type).removeData('bs.' + that.type) + if (that.$tip) { + that.$tip.detach() + } + that.$tip = null + that.$arrow = null + that.$viewport = null + }) + } + + + // TOOLTIP PLUGIN DEFINITION + // ========================= + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.tooltip') + var options = typeof option == 'object' && option + + if (!data && /destroy|hide/.test(option)) return + if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.tooltip + + $.fn.tooltip = Plugin + $.fn.tooltip.Constructor = Tooltip + + + // TOOLTIP NO CONFLICT + // =================== + + $.fn.tooltip.noConflict = function () { + $.fn.tooltip = old + return this + } + +}(jQuery); + +/* ======================================================================== + * Bootstrap: popover.js v3.3.6 + * http://getbootstrap.com/javascript/#popovers + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // POPOVER PUBLIC CLASS DEFINITION + // =============================== + + var Popover = function (element, options) { + this.init('popover', element, options) + } + + if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') + + Popover.VERSION = '3.3.6' + + Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { + placement: 'right', + trigger: 'click', + content: '', + template: '<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>' + }) + + + // NOTE: POPOVER EXTENDS tooltip.js + // ================================ + + Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) + + Popover.prototype.constructor = Popover + + Popover.prototype.getDefaults = function () { + return Popover.DEFAULTS + } + + Popover.prototype.setContent = function () { + var $tip = this.tip() + var title = this.getTitle() + var content = this.getContent() + + $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) + $tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events + this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' + ](content) + + $tip.removeClass('fade top bottom left right in') + + // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do + // this manually by checking the contents. + if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() + } + + Popover.prototype.hasContent = function () { + return this.getTitle() || this.getContent() + } + + Popover.prototype.getContent = function () { + var $e = this.$element + var o = this.options + + return $e.attr('data-content') + || (typeof o.content == 'function' ? + o.content.call($e[0]) : + o.content) + } + + Popover.prototype.arrow = function () { + return (this.$arrow = this.$arrow || this.tip().find('.arrow')) + } + + + // POPOVER PLUGIN DEFINITION + // ========================= + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.popover') + var options = typeof option == 'object' && option + + if (!data && /destroy|hide/.test(option)) return + if (!data) $this.data('bs.popover', (data = new Popover(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.popover + + $.fn.popover = Plugin + $.fn.popover.Constructor = Popover + + + // POPOVER NO CONFLICT + // =================== + + $.fn.popover.noConflict = function () { + $.fn.popover = old + return this + } + +}(jQuery); + +/* ======================================================================== + * Bootstrap: scrollspy.js v3.3.6 + * http://getbootstrap.com/javascript/#scrollspy + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // SCROLLSPY CLASS DEFINITION + // ========================== + + function ScrollSpy(element, options) { + this.$body = $(document.body) + this.$scrollElement = $(element).is(document.body) ? $(window) : $(element) + this.options = $.extend({}, ScrollSpy.DEFAULTS, options) + this.selector = (this.options.target || '') + ' .nav li > a' + this.offsets = [] + this.targets = [] + this.activeTarget = null + this.scrollHeight = 0 + + this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this)) + this.refresh() + this.process() + } + + ScrollSpy.VERSION = '3.3.6' + + ScrollSpy.DEFAULTS = { + offset: 10 + } + + ScrollSpy.prototype.getScrollHeight = function () { + return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight) + } + + ScrollSpy.prototype.refresh = function () { + var that = this + var offsetMethod = 'offset' + var offsetBase = 0 + + this.offsets = [] + this.targets = [] + this.scrollHeight = this.getScrollHeight() + + if (!$.isWindow(this.$scrollElement[0])) { + offsetMethod = 'position' + offsetBase = this.$scrollElement.scrollTop() + } + + this.$body + .find(this.selector) + .map(function () { + var $el = $(this) + var href = $el.data('target') || $el.attr('href') + var $href = /^#./.test(href) && $(href) + + return ($href + && $href.length + && $href.is(':visible') + && [[$href[offsetMethod]().top + offsetBase, href]]) || null + }) + .sort(function (a, b) { return a[0] - b[0] }) + .each(function () { + that.offsets.push(this[0]) + that.targets.push(this[1]) + }) + } + + ScrollSpy.prototype.process = function () { + var scrollTop = this.$scrollElement.scrollTop() + this.options.offset + var scrollHeight = this.getScrollHeight() + var maxScroll = this.options.offset + scrollHeight - this.$scrollElement.height() + var offsets = this.offsets + var targets = this.targets + var activeTarget = this.activeTarget + var i + + if (this.scrollHeight != scrollHeight) { + this.refresh() + } + + if (scrollTop >= maxScroll) { + return activeTarget != (i = targets[targets.length - 1]) && this.activate(i) + } + + if (activeTarget && scrollTop < offsets[0]) { + this.activeTarget = null + return this.clear() + } + + for (i = offsets.length; i--;) { + activeTarget != targets[i] + && scrollTop >= offsets[i] + && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1]) + && this.activate(targets[i]) + } + } + + ScrollSpy.prototype.activate = function (target) { + this.activeTarget = target + + this.clear() + + var selector = this.selector + + '[data-target="' + target + '"],' + + this.selector + '[href="' + target + '"]' + + var active = $(selector) + .parents('li') + .addClass('active') + + if (active.parent('.dropdown-menu').length) { + active = active + .closest('li.dropdown') + .addClass('active') + } + + active.trigger('activate.bs.scrollspy') + } + + ScrollSpy.prototype.clear = function () { + $(this.selector) + .parentsUntil(this.options.target, '.active') + .removeClass('active') + } + + + // SCROLLSPY PLUGIN DEFINITION + // =========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.scrollspy') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.scrollspy + + $.fn.scrollspy = Plugin + $.fn.scrollspy.Constructor = ScrollSpy + + + // SCROLLSPY NO CONFLICT + // ===================== + + $.fn.scrollspy.noConflict = function () { + $.fn.scrollspy = old + return this + } + + + // SCROLLSPY DATA-API + // ================== + + $(window).on('load.bs.scrollspy.data-api', function () { + $('[data-spy="scroll"]').each(function () { + var $spy = $(this) + Plugin.call($spy, $spy.data()) + }) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: tab.js v3.3.6 + * http://getbootstrap.com/javascript/#tabs + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // TAB CLASS DEFINITION + // ==================== + + var Tab = function (element) { + // jscs:disable requireDollarBeforejQueryAssignment + this.element = $(element) + // jscs:enable requireDollarBeforejQueryAssignment + } + + Tab.VERSION = '3.3.6' + + Tab.TRANSITION_DURATION = 150 + + Tab.prototype.show = function () { + var $this = this.element + var $ul = $this.closest('ul:not(.dropdown-menu)') + var selector = $this.data('target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + if ($this.parent('li').hasClass('active')) return + + var $previous = $ul.find('.active:last a') + var hideEvent = $.Event('hide.bs.tab', { + relatedTarget: $this[0] + }) + var showEvent = $.Event('show.bs.tab', { + relatedTarget: $previous[0] + }) + + $previous.trigger(hideEvent) + $this.trigger(showEvent) + + if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return + + var $target = $(selector) + + this.activate($this.closest('li'), $ul) + this.activate($target, $target.parent(), function () { + $previous.trigger({ + type: 'hidden.bs.tab', + relatedTarget: $this[0] + }) + $this.trigger({ + type: 'shown.bs.tab', + relatedTarget: $previous[0] + }) + }) + } + + Tab.prototype.activate = function (element, container, callback) { + var $active = container.find('> .active') + var transition = callback + && $.support.transition + && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length) + + function next() { + $active + .removeClass('active') + .find('> .dropdown-menu > .active') + .removeClass('active') + .end() + .find('[data-toggle="tab"]') + .attr('aria-expanded', false) + + element + .addClass('active') + .find('[data-toggle="tab"]') + .attr('aria-expanded', true) + + if (transition) { + element[0].offsetWidth // reflow for transition + element.addClass('in') + } else { + element.removeClass('fade') + } + + if (element.parent('.dropdown-menu').length) { + element + .closest('li.dropdown') + .addClass('active') + .end() + .find('[data-toggle="tab"]') + .attr('aria-expanded', true) + } + + callback && callback() + } + + $active.length && transition ? + $active + .one('bsTransitionEnd', next) + .emulateTransitionEnd(Tab.TRANSITION_DURATION) : + next() + + $active.removeClass('in') + } + + + // TAB PLUGIN DEFINITION + // ===================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.tab') + + if (!data) $this.data('bs.tab', (data = new Tab(this))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.tab + + $.fn.tab = Plugin + $.fn.tab.Constructor = Tab + + + // TAB NO CONFLICT + // =============== + + $.fn.tab.noConflict = function () { + $.fn.tab = old + return this + } + + + // TAB DATA-API + // ============ + + var clickHandler = function (e) { + e.preventDefault() + Plugin.call($(this), 'show') + } + + $(document) + .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler) + .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: affix.js v3.3.6 + * http://getbootstrap.com/javascript/#affix + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // AFFIX CLASS DEFINITION + // ====================== + + var Affix = function (element, options) { + this.options = $.extend({}, Affix.DEFAULTS, options) + + this.$target = $(this.options.target) + .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) + .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) + + this.$element = $(element) + this.affixed = null + this.unpin = null + this.pinnedOffset = null + + this.checkPosition() + } + + Affix.VERSION = '3.3.6' + + Affix.RESET = 'affix affix-top affix-bottom' + + Affix.DEFAULTS = { + offset: 0, + target: window + } + + Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) { + var scrollTop = this.$target.scrollTop() + var position = this.$element.offset() + var targetHeight = this.$target.height() + + if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false + + if (this.affixed == 'bottom') { + if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom' + return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom' + } + + var initializing = this.affixed == null + var colliderTop = initializing ? scrollTop : position.top + var colliderHeight = initializing ? targetHeight : height + + if (offsetTop != null && scrollTop <= offsetTop) return 'top' + if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom' + + return false + } + + Affix.prototype.getPinnedOffset = function () { + if (this.pinnedOffset) return this.pinnedOffset + this.$element.removeClass(Affix.RESET).addClass('affix') + var scrollTop = this.$target.scrollTop() + var position = this.$element.offset() + return (this.pinnedOffset = position.top - scrollTop) + } + + Affix.prototype.checkPositionWithEventLoop = function () { + setTimeout($.proxy(this.checkPosition, this), 1) + } + + Affix.prototype.checkPosition = function () { + if (!this.$element.is(':visible')) return + + var height = this.$element.height() + var offset = this.options.offset + var offsetTop = offset.top + var offsetBottom = offset.bottom + var scrollHeight = Math.max($(document).height(), $(document.body).height()) + + if (typeof offset != 'object') offsetBottom = offsetTop = offset + if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) + if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element) + + var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom) + + if (this.affixed != affix) { + if (this.unpin != null) this.$element.css('top', '') + + var affixType = 'affix' + (affix ? '-' + affix : '') + var e = $.Event(affixType + '.bs.affix') + + this.$element.trigger(e) + + if (e.isDefaultPrevented()) return + + this.affixed = affix + this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null + + this.$element + .removeClass(Affix.RESET) + .addClass(affixType) + .trigger(affixType.replace('affix', 'affixed') + '.bs.affix') + } + + if (affix == 'bottom') { + this.$element.offset({ + top: scrollHeight - height - offsetBottom + }) + } + } + + + // AFFIX PLUGIN DEFINITION + // ======================= + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.affix') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.affix', (data = new Affix(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.affix + + $.fn.affix = Plugin + $.fn.affix.Constructor = Affix + + + // AFFIX NO CONFLICT + // ================= + + $.fn.affix.noConflict = function () { + $.fn.affix = old + return this + } + + + // AFFIX DATA-API + // ============== + + $(window).on('load', function () { + $('[data-spy="affix"]').each(function () { + var $spy = $(this) + var data = $spy.data() + + data.offset = data.offset || {} + + if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom + if (data.offsetTop != null) data.offset.top = data.offsetTop + + Plugin.call($spy, data) + }) + }) + +}(jQuery); diff --git a/examples/blog/static/js/jquery-1.11.3.min.js b/examples/blog/static/js/jquery-1.11.3.min.js new file mode 100644 index 000000000..0f60b7bd0 --- /dev/null +++ b/examples/blog/static/js/jquery-1.11.3.min.js @@ -0,0 +1,5 @@ +/*! jQuery v1.11.3 | (c) 2005, 2015 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.3",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b="length"in a&&a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,aa=/[+~]/,ba=/'|\\/g,ca=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),da=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ea=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fa){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(ba,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+ra(o[l]);w=aa.test(a)&&pa(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",ea,!1):e.attachEvent&&e.attachEvent("onunload",ea)),p=!f(g),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\f]' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?la(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ca,da),a[3]=(a[3]||a[4]||a[5]||"").replace(ca,da),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ca,da).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(ca,da),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return W.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(ca,da).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:oa(function(){return[0]}),last:oa(function(a,b){return[b-1]}),eq:oa(function(a,b,c){return[0>c?c+b:c]}),even:oa(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:oa(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:oa(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:oa(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=ma(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=na(b);function qa(){}qa.prototype=d.filters=d.pseudos,d.setFilters=new qa,g=ga.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){(!c||(e=S.exec(h)))&&(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=T.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(R," ")}),h=h.slice(c.length));for(g in d.filter)!(e=X[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?ga.error(a):z(a,i).slice(0)};function ra(a){for(var b=0,c=a.length,d="";c>b;b++)d+=a[b].value;return d}function sa(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function ta(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ua(a,b,c){for(var d=0,e=b.length;e>d;d++)ga(a,b[d],c);return c}function va(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wa(a,b,c,d,e,f){return d&&!d[u]&&(d=wa(d)),e&&!e[u]&&(e=wa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ua(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:va(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=va(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=va(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sa(function(a){return a===b},h,!0),l=sa(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sa(ta(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wa(i>1&&ta(m),i>1&&ra(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xa(a.slice(i,e)),f>e&&xa(a=a.slice(e)),f>e&&ra(a))}m.push(c)}return ta(m)}function ya(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=va(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&ga.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,ya(e,d)),f.selector=a}return f},i=ga.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ca,da),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ca,da),aa.test(j[0].type)&&pa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&ra(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,aa.test(a)&&pa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ja(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1; + +return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?m.queue(this[0],a):void 0===b?this:this.each(function(){var c=m.queue(this,a,b);m._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&m.dequeue(this,a)})},dequeue:function(a){return this.each(function(){m.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=m.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=m._data(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var S=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=["Top","Right","Bottom","Left"],U=function(a,b){return a=b||a,"none"===m.css(a,"display")||!m.contains(a.ownerDocument,a)},V=m.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===m.type(c)){e=!0;for(h in c)m.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,m.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(m(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav></:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="<textarea>x</textarea>",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="<input type='radio' checked='checked' name='t'/>",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function aa(){return!0}function ba(){return!1}function ca(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},fix:function(a){if(a[m.expando])return a;var b,c,d,e=a.type,f=a,g=this.fixHooks[e];g||(this.fixHooks[e]=g=Z.test(e)?this.mouseHooks:Y.test(e)?this.keyHooks:{}),d=g.props?this.props.concat(g.props):this.props,a=new m.Event(f),b=d.length;while(b--)c=d[b],a[c]=f[c];return a.target||(a.target=f.srcElement||y),3===a.target.nodeType&&(a.target=a.target.parentNode),a.metaKey=!!a.metaKey,g.filter?g.filter(a,f):a},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,d,e,f=b.button,g=b.fromElement;return null==a.pageX&&null!=b.clientX&&(d=a.target.ownerDocument||y,e=d.documentElement,c=d.body,a.pageX=b.clientX+(e&&e.scrollLeft||c&&c.scrollLeft||0)-(e&&e.clientLeft||c&&c.clientLeft||0),a.pageY=b.clientY+(e&&e.scrollTop||c&&c.scrollTop||0)-(e&&e.clientTop||c&&c.clientTop||0)),!a.relatedTarget&&g&&(a.relatedTarget=g===a.target?b.toElement:g),a.which||void 0===f||(a.which=1&f?1:2&f?3:4&f?2:0),a}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==ca()&&this.focus)try{return this.focus(),!1}catch(a){}},delegateType:"focusin"},blur:{trigger:function(){return this===ca()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return m.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):void 0},_default:function(a){return m.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c,d){var e=m.extend(new m.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?m.event.trigger(e,null,b):m.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},m.removeEvent=y.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){var d="on"+b;a.detachEvent&&(typeof a[d]===K&&(a[d]=null),a.detachEvent(d,c))},m.Event=function(a,b){return this instanceof m.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?aa:ba):this.type=a,b&&m.extend(this,b),this.timeStamp=a&&a.timeStamp||m.now(),void(this[m.expando]=!0)):new m.Event(a,b)},m.Event.prototype={isDefaultPrevented:ba,isPropagationStopped:ba,isImmediatePropagationStopped:ba,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=aa,a&&(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=aa,a&&(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=aa,a&&a.stopImmediatePropagation&&a.stopImmediatePropagation(),this.stopPropagation()}},m.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){m.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return(!e||e!==d&&!m.contains(d,e))&&(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),k.submitBubbles||(m.event.special.submit={setup:function(){return m.nodeName(this,"form")?!1:void m.event.add(this,"click._submit keypress._submit",function(a){var b=a.target,c=m.nodeName(b,"input")||m.nodeName(b,"button")?b.form:void 0;c&&!m._data(c,"submitBubbles")&&(m.event.add(c,"submit._submit",function(a){a._submit_bubble=!0}),m._data(c,"submitBubbles",!0))})},postDispatch:function(a){a._submit_bubble&&(delete a._submit_bubble,this.parentNode&&!a.isTrigger&&m.event.simulate("submit",this.parentNode,a,!0))},teardown:function(){return m.nodeName(this,"form")?!1:void m.event.remove(this,"._submit")}}),k.changeBubbles||(m.event.special.change={setup:function(){return X.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(m.event.add(this,"propertychange._change",function(a){"checked"===a.originalEvent.propertyName&&(this._just_changed=!0)}),m.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1),m.event.simulate("change",this,a,!0)})),!1):void m.event.add(this,"beforeactivate._change",function(a){var b=a.target;X.test(b.nodeName)&&!m._data(b,"changeBubbles")&&(m.event.add(b,"change._change",function(a){!this.parentNode||a.isSimulated||a.isTrigger||m.event.simulate("change",this.parentNode,a,!0)}),m._data(b,"changeBubbles",!0))})},handle:function(a){var b=a.target;return this!==b||a.isSimulated||a.isTrigger||"radio"!==b.type&&"checkbox"!==b.type?a.handleObj.handler.apply(this,arguments):void 0},teardown:function(){return m.event.remove(this,"._change"),!X.test(this.nodeName)}}),k.focusinBubbles||m.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){m.event.simulate(b,a.target,m.event.fix(a),!0)};m.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=m._data(d,b);e||d.addEventListener(a,c,!0),m._data(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=m._data(d,b)-1;e?m._data(d,b,e):(d.removeEventListener(a,c,!0),m._removeData(d,b))}}}),m.fn.extend({on:function(a,b,c,d,e){var f,g;if("object"==typeof a){"string"!=typeof b&&(c=c||b,b=void 0);for(f in a)this.on(f,b,c,a[f],e);return this}if(null==c&&null==d?(d=b,c=b=void 0):null==d&&("string"==typeof b?(d=c,c=void 0):(d=c,c=b,b=void 0)),d===!1)d=ba;else if(!d)return this;return 1===e&&(g=d,d=function(a){return m().off(a),g.apply(this,arguments)},d.guid=g.guid||(g.guid=m.guid++)),this.each(function(){m.event.add(this,a,d,c,b)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,m(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return(b===!1||"function"==typeof b)&&(c=b,b=void 0),c===!1&&(c=ba),this.each(function(){m.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){m.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?m.event.trigger(a,b,c,!0):void 0}});function da(a){var b=ea.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}var ea="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",fa=/ jQuery\d+="(?:null|\d+)"/g,ga=new RegExp("<(?:"+ea+")[\\s/>]","i"),ha=/^\s+/,ia=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,ja=/<([\w:]+)/,ka=/<tbody/i,la=/<|&#?\w+;/,ma=/<(?:script|style|link)/i,na=/checked\s*(?:[^=]|=\s*.checked.)/i,oa=/^$|\/(?:java|ecma)script/i,pa=/^true\/(.*)/,qa=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,ra={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:k.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},sa=da(y),ta=sa.appendChild(y.createElement("div"));ra.optgroup=ra.option,ra.tbody=ra.tfoot=ra.colgroup=ra.caption=ra.thead,ra.th=ra.td;function ua(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ua(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function va(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wa(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xa(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function ya(a){var b=pa.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function za(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Aa(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Ba(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xa(b).text=a.text,ya(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!ga.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(ta.innerHTML=a.outerHTML,ta.removeChild(f=ta.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ua(f),h=ua(a),g=0;null!=(e=h[g]);++g)d[g]&&Ba(e,d[g]);if(b)if(c)for(h=h||ua(a),d=d||ua(f),g=0;null!=(e=h[g]);g++)Aa(e,d[g]);else Aa(a,f);return d=ua(f,"script"),d.length>0&&za(d,!i&&ua(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=da(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(la.test(f)){h=h||o.appendChild(b.createElement("div")),i=(ja.exec(f)||["",""])[1].toLowerCase(),l=ra[i]||ra._default,h.innerHTML=l[1]+f.replace(ia,"<$1></$2>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&ha.test(f)&&p.push(b.createTextNode(ha.exec(f)[0])),!k.tbody){f="table"!==i||ka.test(f)?"<table>"!==l[1]||ka.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ua(p,"input"),va),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ua(o.appendChild(f),"script"),g&&za(h),c)){e=0;while(f=h[e++])oa.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wa(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wa(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ua(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&za(ua(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ua(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fa,""):void 0;if(!("string"!=typeof a||ma.test(a)||!k.htmlSerialize&&ga.test(a)||!k.leadingWhitespace&&ha.test(a)||ra[(ja.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ia,"<$1></$2>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ua(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ua(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&na.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ua(i,"script"),xa),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ua(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,ya),j=0;f>j;j++)d=g[j],oa.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qa,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Ca,Da={};function Ea(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fa(a){var b=y,c=Da[a];return c||(c=Ea(a,b),"none"!==c&&c||(Ca=(Ca||m("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=(Ca[0].contentWindow||Ca[0].contentDocument).document,b.write(),b.close(),c=Ea(a,b),Ca.detach()),Da[a]=c),c}!function(){var a;k.shrinkWrapBlocks=function(){if(null!=a)return a;a=!1;var b,c,d;return c=y.getElementsByTagName("body")[0],c&&c.style?(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:1px;width:1px;zoom:1",b.appendChild(y.createElement("div")).style.width="5px",a=3!==b.offsetWidth),c.removeChild(d),a):void 0}}();var Ga=/^margin/,Ha=new RegExp("^("+S+")(?!px)[a-z%]+$","i"),Ia,Ja,Ka=/^(top|right|bottom|left)$/;a.getComputedStyle?(Ia=function(b){return b.ownerDocument.defaultView.opener?b.ownerDocument.defaultView.getComputedStyle(b,null):a.getComputedStyle(b,null)},Ja=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ia(a),g=c?c.getPropertyValue(b)||c[b]:void 0,c&&(""!==g||m.contains(a.ownerDocument,a)||(g=m.style(a,b)),Ha.test(g)&&Ga.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0===g?g:g+""}):y.documentElement.currentStyle&&(Ia=function(a){return a.currentStyle},Ja=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ia(a),g=c?c[b]:void 0,null==g&&h&&h[b]&&(g=h[b]),Ha.test(g)&&!Ka.test(b)&&(d=h.left,e=a.runtimeStyle,f=e&&e.left,f&&(e.left=a.currentStyle.left),h.left="fontSize"===b?"1em":g,g=h.pixelLeft+"px",h.left=d,f&&(e.left=f)),void 0===g?g:g+""||"auto"});function La(a,b){return{get:function(){var c=a();if(null!=c)return c?void delete this.get:(this.get=b).apply(this,arguments)}}}!function(){var b,c,d,e,f,g,h;if(b=y.createElement("div"),b.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",d=b.getElementsByTagName("a")[0],c=d&&d.style){c.cssText="float:left;opacity:.5",k.opacity="0.5"===c.opacity,k.cssFloat=!!c.cssFloat,b.style.backgroundClip="content-box",b.cloneNode(!0).style.backgroundClip="",k.clearCloneStyle="content-box"===b.style.backgroundClip,k.boxSizing=""===c.boxSizing||""===c.MozBoxSizing||""===c.WebkitBoxSizing,m.extend(k,{reliableHiddenOffsets:function(){return null==g&&i(),g},boxSizingReliable:function(){return null==f&&i(),f},pixelPosition:function(){return null==e&&i(),e},reliableMarginRight:function(){return null==h&&i(),h}});function i(){var b,c,d,i;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),b.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",e=f=!1,h=!0,a.getComputedStyle&&(e="1%"!==(a.getComputedStyle(b,null)||{}).top,f="4px"===(a.getComputedStyle(b,null)||{width:"4px"}).width,i=b.appendChild(y.createElement("div")),i.style.cssText=b.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",i.style.marginRight=i.style.width="0",b.style.width="1px",h=!parseFloat((a.getComputedStyle(i,null)||{}).marginRight),b.removeChild(i)),b.innerHTML="<table><tr><td></td><td>t</td></tr></table>",i=b.getElementsByTagName("td"),i[0].style.cssText="margin:0;border:0;padding:0;display:none",g=0===i[0].offsetHeight,g&&(i[0].style.display="",i[1].style.display="none",g=0===i[0].offsetHeight),c.removeChild(d))}}}(),m.swap=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};var Ma=/alpha\([^)]*\)/i,Na=/opacity\s*=\s*([^)]*)/,Oa=/^(none|table(?!-c[ea]).+)/,Pa=new RegExp("^("+S+")(.*)$","i"),Qa=new RegExp("^([+-])=("+S+")","i"),Ra={position:"absolute",visibility:"hidden",display:"block"},Sa={letterSpacing:"0",fontWeight:"400"},Ta=["Webkit","O","Moz","ms"];function Ua(a,b){if(b in a)return b;var c=b.charAt(0).toUpperCase()+b.slice(1),d=b,e=Ta.length;while(e--)if(b=Ta[e]+c,b in a)return b;return d}function Va(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=m._data(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&U(d)&&(f[g]=m._data(d,"olddisplay",Fa(d.nodeName)))):(e=U(d),(c&&"none"!==c||!e)&&m._data(d,"olddisplay",e?c:m.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}function Wa(a,b,c){var d=Pa.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function Xa(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=m.css(a,c+T[f],!0,e)),d?("content"===c&&(g-=m.css(a,"padding"+T[f],!0,e)),"margin"!==c&&(g-=m.css(a,"border"+T[f]+"Width",!0,e))):(g+=m.css(a,"padding"+T[f],!0,e),"padding"!==c&&(g+=m.css(a,"border"+T[f]+"Width",!0,e)));return g}function Ya(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=Ia(a),g=k.boxSizing&&"border-box"===m.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=Ja(a,b,f),(0>e||null==e)&&(e=a.style[b]),Ha.test(e))return e;d=g&&(k.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Xa(a,b,c||(g?"border":"content"),d,f)+"px"}m.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Ja(a,"opacity");return""===c?"1":c}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":k.cssFloat?"cssFloat":"styleFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=m.camelCase(b),i=a.style;if(b=m.cssProps[h]||(m.cssProps[h]=Ua(i,h)),g=m.cssHooks[b]||m.cssHooks[h],void 0===c)return g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b];if(f=typeof c,"string"===f&&(e=Qa.exec(c))&&(c=(e[1]+1)*e[2]+parseFloat(m.css(a,b)),f="number"),null!=c&&c===c&&("number"!==f||m.cssNumber[h]||(c+="px"),k.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),!(g&&"set"in g&&void 0===(c=g.set(a,c,d)))))try{i[b]=c}catch(j){}}},css:function(a,b,c,d){var e,f,g,h=m.camelCase(b);return b=m.cssProps[h]||(m.cssProps[h]=Ua(a.style,h)),g=m.cssHooks[b]||m.cssHooks[h],g&&"get"in g&&(f=g.get(a,!0,c)),void 0===f&&(f=Ja(a,b,d)),"normal"===f&&b in Sa&&(f=Sa[b]),""===c||c?(e=parseFloat(f),c===!0||m.isNumeric(e)?e||0:f):f}}),m.each(["height","width"],function(a,b){m.cssHooks[b]={get:function(a,c,d){return c?Oa.test(m.css(a,"display"))&&0===a.offsetWidth?m.swap(a,Ra,function(){return Ya(a,b,d)}):Ya(a,b,d):void 0},set:function(a,c,d){var e=d&&Ia(a);return Wa(a,c,d?Xa(a,b,d,k.boxSizing&&"border-box"===m.css(a,"boxSizing",!1,e),e):0)}}}),k.opacity||(m.cssHooks.opacity={get:function(a,b){return Na.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=m.isNumeric(b)?"alpha(opacity="+100*b+")":"",f=d&&d.filter||c.filter||"";c.zoom=1,(b>=1||""===b)&&""===m.trim(f.replace(Ma,""))&&c.removeAttribute&&(c.removeAttribute("filter"),""===b||d&&!d.filter)||(c.filter=Ma.test(f)?f.replace(Ma,e):f+" "+e)}}),m.cssHooks.marginRight=La(k.reliableMarginRight,function(a,b){return b?m.swap(a,{display:"inline-block"},Ja,[a,"marginRight"]):void 0}),m.each({margin:"",padding:"",border:"Width"},function(a,b){m.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+T[d]+b]=f[d]||f[d-2]||f[0];return e}},Ga.test(a)||(m.cssHooks[a+b].set=Wa)}),m.fn.extend({css:function(a,b){return V(this,function(a,b,c){var d,e,f={},g=0;if(m.isArray(b)){for(d=Ia(a),e=b.length;e>g;g++)f[b[g]]=m.css(a,b[g],!1,d);return f}return void 0!==c?m.style(a,b,c):m.css(a,b)},a,b,arguments.length>1)},show:function(){return Va(this,!0)},hide:function(){return Va(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){U(this)?m(this).show():m(this).hide()})}});function Za(a,b,c,d,e){ +return new Za.prototype.init(a,b,c,d,e)}m.Tween=Za,Za.prototype={constructor:Za,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(m.cssNumber[c]?"":"px")},cur:function(){var a=Za.propHooks[this.prop];return a&&a.get?a.get(this):Za.propHooks._default.get(this)},run:function(a){var b,c=Za.propHooks[this.prop];return this.options.duration?this.pos=b=m.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Za.propHooks._default.set(this),this}},Za.prototype.init.prototype=Za.prototype,Za.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=m.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){m.fx.step[a.prop]?m.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[m.cssProps[a.prop]]||m.cssHooks[a.prop])?m.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Za.propHooks.scrollTop=Za.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},m.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},m.fx=Za.prototype.init,m.fx.step={};var $a,_a,ab=/^(?:toggle|show|hide)$/,bb=new RegExp("^(?:([+-])=|)("+S+")([a-z%]*)$","i"),cb=/queueHooks$/,db=[ib],eb={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=bb.exec(b),f=e&&e[3]||(m.cssNumber[a]?"":"px"),g=(m.cssNumber[a]||"px"!==f&&+d)&&bb.exec(m.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,m.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function fb(){return setTimeout(function(){$a=void 0}),$a=m.now()}function gb(a,b){var c,d={height:a},e=0;for(b=b?1:0;4>e;e+=2-b)c=T[e],d["margin"+c]=d["padding"+c]=a;return b&&(d.opacity=d.width=a),d}function hb(a,b,c){for(var d,e=(eb[b]||[]).concat(eb["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function ib(a,b,c){var d,e,f,g,h,i,j,l,n=this,o={},p=a.style,q=a.nodeType&&U(a),r=m._data(a,"fxshow");c.queue||(h=m._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,n.always(function(){n.always(function(){h.unqueued--,m.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[p.overflow,p.overflowX,p.overflowY],j=m.css(a,"display"),l="none"===j?m._data(a,"olddisplay")||Fa(a.nodeName):j,"inline"===l&&"none"===m.css(a,"float")&&(k.inlineBlockNeedsLayout&&"inline"!==Fa(a.nodeName)?p.zoom=1:p.display="inline-block")),c.overflow&&(p.overflow="hidden",k.shrinkWrapBlocks()||n.always(function(){p.overflow=c.overflow[0],p.overflowX=c.overflow[1],p.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],ab.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(q?"hide":"show")){if("show"!==e||!r||void 0===r[d])continue;q=!0}o[d]=r&&r[d]||m.style(a,d)}else j=void 0;if(m.isEmptyObject(o))"inline"===("none"===j?Fa(a.nodeName):j)&&(p.display=j);else{r?"hidden"in r&&(q=r.hidden):r=m._data(a,"fxshow",{}),f&&(r.hidden=!q),q?m(a).show():n.done(function(){m(a).hide()}),n.done(function(){var b;m._removeData(a,"fxshow");for(b in o)m.style(a,b,o[b])});for(d in o)g=hb(q?r[d]:0,d,n),d in r||(r[d]=g.start,q&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function jb(a,b){var c,d,e,f,g;for(c in a)if(d=m.camelCase(c),e=b[d],f=a[c],m.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=m.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function kb(a,b,c){var d,e,f=0,g=db.length,h=m.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=$a||fb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:m.extend({},b),opts:m.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:$a||fb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=m.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(jb(k,j.opts.specialEasing);g>f;f++)if(d=db[f].call(j,a,k,j.opts))return d;return m.map(k,hb,j),m.isFunction(j.opts.start)&&j.opts.start.call(a,j),m.fx.timer(m.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}m.Animation=m.extend(kb,{tweener:function(a,b){m.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],eb[c]=eb[c]||[],eb[c].unshift(b)},prefilter:function(a,b){b?db.unshift(a):db.push(a)}}),m.speed=function(a,b,c){var d=a&&"object"==typeof a?m.extend({},a):{complete:c||!c&&b||m.isFunction(a)&&a,duration:a,easing:c&&b||b&&!m.isFunction(b)&&b};return d.duration=m.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in m.fx.speeds?m.fx.speeds[d.duration]:m.fx.speeds._default,(null==d.queue||d.queue===!0)&&(d.queue="fx"),d.old=d.complete,d.complete=function(){m.isFunction(d.old)&&d.old.call(this),d.queue&&m.dequeue(this,d.queue)},d},m.fn.extend({fadeTo:function(a,b,c,d){return this.filter(U).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=m.isEmptyObject(a),f=m.speed(b,c,d),g=function(){var b=kb(this,m.extend({},a),f);(e||m._data(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=m.timers,g=m._data(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&cb.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));(b||!c)&&m.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=m._data(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=m.timers,g=d?d.length:0;for(c.finish=!0,m.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),m.each(["toggle","show","hide"],function(a,b){var c=m.fn[b];m.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(gb(b,!0),a,d,e)}}),m.each({slideDown:gb("show"),slideUp:gb("hide"),slideToggle:gb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){m.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),m.timers=[],m.fx.tick=function(){var a,b=m.timers,c=0;for($a=m.now();c<b.length;c++)a=b[c],a()||b[c]!==a||b.splice(c--,1);b.length||m.fx.stop(),$a=void 0},m.fx.timer=function(a){m.timers.push(a),a()?m.fx.start():m.timers.pop()},m.fx.interval=13,m.fx.start=function(){_a||(_a=setInterval(m.fx.tick,m.fx.interval))},m.fx.stop=function(){clearInterval(_a),_a=null},m.fx.speeds={slow:600,fast:200,_default:400},m.fn.delay=function(a,b){return a=m.fx?m.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},function(){var a,b,c,d,e;b=y.createElement("div"),b.setAttribute("className","t"),b.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",d=b.getElementsByTagName("a")[0],c=y.createElement("select"),e=c.appendChild(y.createElement("option")),a=b.getElementsByTagName("input")[0],d.style.cssText="top:1px",k.getSetAttribute="t"!==b.className,k.style=/top/.test(d.getAttribute("style")),k.hrefNormalized="/a"===d.getAttribute("href"),k.checkOn=!!a.value,k.optSelected=e.selected,k.enctype=!!y.createElement("form").enctype,c.disabled=!0,k.optDisabled=!e.disabled,a=y.createElement("input"),a.setAttribute("value",""),k.input=""===a.getAttribute("value"),a.value="t",a.setAttribute("type","radio"),k.radioValue="t"===a.value}();var lb=/\r/g;m.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=m.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,m(this).val()):a,null==e?e="":"number"==typeof e?e+="":m.isArray(e)&&(e=m.map(e,function(a){return null==a?"":a+""})),b=m.valHooks[this.type]||m.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=m.valHooks[e.type]||m.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(lb,""):null==c?"":c)}}}),m.extend({valHooks:{option:{get:function(a){var b=m.find.attr(a,"value");return null!=b?b:m.trim(m.text(a))}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(k.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&m.nodeName(c.parentNode,"optgroup"))){if(b=m(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=m.makeArray(b),g=e.length;while(g--)if(d=e[g],m.inArray(m.valHooks.option.get(d),f)>=0)try{d.selected=c=!0}catch(h){d.scrollHeight}else d.selected=!1;return c||(a.selectedIndex=-1),e}}}}),m.each(["radio","checkbox"],function(){m.valHooks[this]={set:function(a,b){return m.isArray(b)?a.checked=m.inArray(m(a).val(),b)>=0:void 0}},k.checkOn||(m.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var mb,nb,ob=m.expr.attrHandle,pb=/^(?:checked|selected)$/i,qb=k.getSetAttribute,rb=k.input;m.fn.extend({attr:function(a,b){return V(this,m.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){m.removeAttr(this,a)})}}),m.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===K?m.prop(a,b,c):(1===f&&m.isXMLDoc(a)||(b=b.toLowerCase(),d=m.attrHooks[b]||(m.expr.match.bool.test(b)?nb:mb)),void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=m.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void m.removeAttr(a,b))},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=m.propFix[c]||c,m.expr.match.bool.test(c)?rb&&qb||!pb.test(c)?a[d]=!1:a[m.camelCase("default-"+c)]=a[d]=!1:m.attr(a,c,""),a.removeAttribute(qb?c:d)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&m.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),nb={set:function(a,b,c){return b===!1?m.removeAttr(a,c):rb&&qb||!pb.test(c)?a.setAttribute(!qb&&m.propFix[c]||c,c):a[m.camelCase("default-"+c)]=a[c]=!0,c}},m.each(m.expr.match.bool.source.match(/\w+/g),function(a,b){var c=ob[b]||m.find.attr;ob[b]=rb&&qb||!pb.test(b)?function(a,b,d){var e,f;return d||(f=ob[b],ob[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,ob[b]=f),e}:function(a,b,c){return c?void 0:a[m.camelCase("default-"+b)]?b.toLowerCase():null}}),rb&&qb||(m.attrHooks.value={set:function(a,b,c){return m.nodeName(a,"input")?void(a.defaultValue=b):mb&&mb.set(a,b,c)}}),qb||(mb={set:function(a,b,c){var d=a.getAttributeNode(c);return d||a.setAttributeNode(d=a.ownerDocument.createAttribute(c)),d.value=b+="","value"===c||b===a.getAttribute(c)?b:void 0}},ob.id=ob.name=ob.coords=function(a,b,c){var d;return c?void 0:(d=a.getAttributeNode(b))&&""!==d.value?d.value:null},m.valHooks.button={get:function(a,b){var c=a.getAttributeNode(b);return c&&c.specified?c.value:void 0},set:mb.set},m.attrHooks.contenteditable={set:function(a,b,c){mb.set(a,""===b?!1:b,c)}},m.each(["width","height"],function(a,b){m.attrHooks[b]={set:function(a,c){return""===c?(a.setAttribute(b,"auto"),c):void 0}}})),k.style||(m.attrHooks.style={get:function(a){return a.style.cssText||void 0},set:function(a,b){return a.style.cssText=b+""}});var sb=/^(?:input|select|textarea|button|object)$/i,tb=/^(?:a|area)$/i;m.fn.extend({prop:function(a,b){return V(this,m.prop,a,b,arguments.length>1)},removeProp:function(a){return a=m.propFix[a]||a,this.each(function(){try{this[a]=void 0,delete this[a]}catch(b){}})}}),m.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!m.isXMLDoc(a),f&&(b=m.propFix[b]||b,e=m.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=m.find.attr(a,"tabindex");return b?parseInt(b,10):sb.test(a.nodeName)||tb.test(a.nodeName)&&a.href?0:-1}}}}),k.hrefNormalized||m.each(["href","src"],function(a,b){m.propHooks[b]={get:function(a){return a.getAttribute(b,4)}}}),k.optSelected||(m.propHooks.selected={get:function(a){var b=a.parentNode;return b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex),null}}),m.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){m.propFix[this.toLowerCase()]=this}),k.enctype||(m.propFix.enctype="encoding");var ub=/[\t\r\n\f]/g;m.fn.extend({addClass:function(a){var b,c,d,e,f,g,h=0,i=this.length,j="string"==typeof a&&a;if(m.isFunction(a))return this.each(function(b){m(this).addClass(a.call(this,b,this.className))});if(j)for(b=(a||"").match(E)||[];i>h;h++)if(c=this[h],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ub," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=m.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0,i=this.length,j=0===arguments.length||"string"==typeof a&&a;if(m.isFunction(a))return this.each(function(b){m(this).removeClass(a.call(this,b,this.className))});if(j)for(b=(a||"").match(E)||[];i>h;h++)if(c=this[h],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ub," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?m.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(m.isFunction(a)?function(c){m(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=m(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===K||"boolean"===c)&&(this.className&&m._data(this,"__className__",this.className),this.className=this.className||a===!1?"":m._data(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(ub," ").indexOf(b)>=0)return!0;return!1}}),m.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){m.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),m.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var vb=m.now(),wb=/\?/,xb=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;m.parseJSON=function(b){if(a.JSON&&a.JSON.parse)return a.JSON.parse(b+"");var c,d=null,e=m.trim(b+"");return e&&!m.trim(e.replace(xb,function(a,b,e,f){return c&&b&&(d=0),0===d?a:(c=e||b,d+=!f-!e,"")}))?Function("return "+e)():m.error("Invalid JSON: "+b)},m.parseXML=function(b){var c,d;if(!b||"string"!=typeof b)return null;try{a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b))}catch(e){c=void 0}return c&&c.documentElement&&!c.getElementsByTagName("parsererror").length||m.error("Invalid XML: "+b),c};var yb,zb,Ab=/#.*$/,Bb=/([?&])_=[^&]*/,Cb=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Db=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Eb=/^(?:GET|HEAD)$/,Fb=/^\/\//,Gb=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,Hb={},Ib={},Jb="*/".concat("*");try{zb=location.href}catch(Kb){zb=y.createElement("a"),zb.href="",zb=zb.href}yb=Gb.exec(zb.toLowerCase())||[];function Lb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(E)||[];if(m.isFunction(c))while(d=f[e++])"+"===d.charAt(0)?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Mb(a,b,c,d){var e={},f=a===Ib;function g(h){var i;return e[h]=!0,m.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Nb(a,b){var c,d,e=m.ajaxSettings.flatOptions||{};for(d in b)void 0!==b[d]&&((e[d]?a:c||(c={}))[d]=b[d]);return c&&m.extend(!0,a,c),a}function Ob(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===e&&(e=a.mimeType||b.getResponseHeader("Content-Type"));if(e)for(g in h)if(h[g]&&h[g].test(e)){i.unshift(g);break}if(i[0]in c)f=i[0];else{for(g in c){if(!i[0]||a.converters[g+" "+i[0]]){f=g;break}d||(d=g)}f=f||d}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function Pb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}m.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:zb,type:"GET",isLocal:Db.test(yb[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Jb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":m.parseJSON,"text xml":m.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Nb(Nb(a,m.ajaxSettings),b):Nb(m.ajaxSettings,a)},ajaxPrefilter:Lb(Hb),ajaxTransport:Lb(Ib),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=m.ajaxSetup({},b),l=k.context||k,n=k.context&&(l.nodeType||l.jquery)?m(l):m.event,o=m.Deferred(),p=m.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!j){j={};while(b=Cb.exec(f))j[b[1].toLowerCase()]=b[2]}b=j[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?f:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return i&&i.abort(b),x(0,b),this}};if(o.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||zb)+"").replace(Ab,"").replace(Fb,yb[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=m.trim(k.dataType||"*").toLowerCase().match(E)||[""],null==k.crossDomain&&(c=Gb.exec(k.url.toLowerCase()),k.crossDomain=!(!c||c[1]===yb[1]&&c[2]===yb[2]&&(c[3]||("http:"===c[1]?"80":"443"))===(yb[3]||("http:"===yb[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=m.param(k.data,k.traditional)),Mb(Hb,k,b,v),2===t)return v;h=m.event&&k.global,h&&0===m.active++&&m.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!Eb.test(k.type),e=k.url,k.hasContent||(k.data&&(e=k.url+=(wb.test(e)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=Bb.test(e)?e.replace(Bb,"$1_="+vb++):e+(wb.test(e)?"&":"?")+"_="+vb++)),k.ifModified&&(m.lastModified[e]&&v.setRequestHeader("If-Modified-Since",m.lastModified[e]),m.etag[e]&&v.setRequestHeader("If-None-Match",m.etag[e])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+Jb+"; q=0.01":""):k.accepts["*"]);for(d in k.headers)v.setRequestHeader(d,k.headers[d]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(d in{success:1,error:1,complete:1})v[d](k[d]);if(i=Mb(Ib,k,b,v)){v.readyState=1,h&&n.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,i.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,c,d){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),i=void 0,f=d||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,c&&(u=Ob(k,v,c)),u=Pb(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(m.lastModified[e]=w),w=v.getResponseHeader("etag"),w&&(m.etag[e]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?o.resolveWith(l,[r,x,v]):o.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,h&&n.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),h&&(n.trigger("ajaxComplete",[v,k]),--m.active||m.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return m.get(a,b,c,"json")},getScript:function(a,b){return m.get(a,void 0,b,"script")}}),m.each(["get","post"],function(a,b){m[b]=function(a,c,d,e){return m.isFunction(c)&&(e=e||d,d=c,c=void 0),m.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),m._evalUrl=function(a){return m.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},m.fn.extend({wrapAll:function(a){if(m.isFunction(a))return this.each(function(b){m(this).wrapAll(a.call(this,b))});if(this[0]){var b=m(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&1===a.firstChild.nodeType)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return this.each(m.isFunction(a)?function(b){m(this).wrapInner(a.call(this,b))}:function(){var b=m(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=m.isFunction(a);return this.each(function(c){m(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){m.nodeName(this,"body")||m(this).replaceWith(this.childNodes)}).end()}}),m.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0||!k.reliableHiddenOffsets()&&"none"===(a.style&&a.style.display||m.css(a,"display"))},m.expr.filters.visible=function(a){return!m.expr.filters.hidden(a)};var Qb=/%20/g,Rb=/\[\]$/,Sb=/\r?\n/g,Tb=/^(?:submit|button|image|reset|file)$/i,Ub=/^(?:input|select|textarea|keygen)/i;function Vb(a,b,c,d){var e;if(m.isArray(b))m.each(b,function(b,e){c||Rb.test(a)?d(a,e):Vb(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==m.type(b))d(a,b);else for(e in b)Vb(a+"["+e+"]",b[e],c,d)}m.param=function(a,b){var c,d=[],e=function(a,b){b=m.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=m.ajaxSettings&&m.ajaxSettings.traditional),m.isArray(a)||a.jquery&&!m.isPlainObject(a))m.each(a,function(){e(this.name,this.value)});else for(c in a)Vb(c,a[c],b,e);return d.join("&").replace(Qb,"+")},m.fn.extend({serialize:function(){return m.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=m.prop(this,"elements");return a?m.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!m(this).is(":disabled")&&Ub.test(this.nodeName)&&!Tb.test(a)&&(this.checked||!W.test(a))}).map(function(a,b){var c=m(this).val();return null==c?null:m.isArray(c)?m.map(c,function(a){return{name:b.name,value:a.replace(Sb,"\r\n")}}):{name:b.name,value:c.replace(Sb,"\r\n")}}).get()}}),m.ajaxSettings.xhr=void 0!==a.ActiveXObject?function(){return!this.isLocal&&/^(get|post|head|put|delete|options)$/i.test(this.type)&&Zb()||$b()}:Zb;var Wb=0,Xb={},Yb=m.ajaxSettings.xhr();a.attachEvent&&a.attachEvent("onunload",function(){for(var a in Xb)Xb[a](void 0,!0)}),k.cors=!!Yb&&"withCredentials"in Yb,Yb=k.ajax=!!Yb,Yb&&m.ajaxTransport(function(a){if(!a.crossDomain||k.cors){var b;return{send:function(c,d){var e,f=a.xhr(),g=++Wb;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)void 0!==c[e]&&f.setRequestHeader(e,c[e]+"");f.send(a.hasContent&&a.data||null),b=function(c,e){var h,i,j;if(b&&(e||4===f.readyState))if(delete Xb[g],b=void 0,f.onreadystatechange=m.noop,e)4!==f.readyState&&f.abort();else{j={},h=f.status,"string"==typeof f.responseText&&(j.text=f.responseText);try{i=f.statusText}catch(k){i=""}h||!a.isLocal||a.crossDomain?1223===h&&(h=204):h=j.text?200:404}j&&d(h,i,j,f.getAllResponseHeaders())},a.async?4===f.readyState?setTimeout(b):f.onreadystatechange=Xb[g]=b:b()},abort:function(){b&&b(void 0,!0)}}}});function Zb(){try{return new a.XMLHttpRequest}catch(b){}}function $b(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}m.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return m.globalEval(a),a}}}),m.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),m.ajaxTransport("script",function(a){if(a.crossDomain){var b,c=y.head||m("head")[0]||y.documentElement;return{send:function(d,e){b=y.createElement("script"),b.async=!0,a.scriptCharset&&(b.charset=a.scriptCharset),b.src=a.url,b.onload=b.onreadystatechange=function(a,c){(c||!b.readyState||/loaded|complete/.test(b.readyState))&&(b.onload=b.onreadystatechange=null,b.parentNode&&b.parentNode.removeChild(b),b=null,c||e(200,"success"))},c.insertBefore(b,c.firstChild)},abort:function(){b&&b.onload(void 0,!0)}}}});var _b=[],ac=/(=)\?(?=&|$)|\?\?/;m.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=_b.pop()||m.expando+"_"+vb++;return this[a]=!0,a}}),m.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(ac.test(b.url)?"url":"string"==typeof b.data&&!(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&ac.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=m.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(ac,"$1"+e):b.jsonp!==!1&&(b.url+=(wb.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||m.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,_b.push(e)),g&&m.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),m.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||y;var d=u.exec(a),e=!c&&[];return d?[b.createElement(d[1])]:(d=m.buildFragment([a],b,e),e&&e.length&&m(e).remove(),m.merge([],d.childNodes))};var bc=m.fn.load;m.fn.load=function(a,b,c){if("string"!=typeof a&&bc)return bc.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>=0&&(d=m.trim(a.slice(h,a.length)),a=a.slice(0,h)),m.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(f="POST"),g.length>0&&m.ajax({url:a,type:f,dataType:"html",data:b}).done(function(a){e=arguments,g.html(d?m("<div>").append(m.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,e||[a.responseText,b,a])}),this},m.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){m.fn[b]=function(a){return this.on(b,a)}}),m.expr.filters.animated=function(a){return m.grep(m.timers,function(b){return a===b.elem}).length};var cc=a.document.documentElement;function dc(a){return m.isWindow(a)?a:9===a.nodeType?a.defaultView||a.parentWindow:!1}m.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=m.css(a,"position"),l=m(a),n={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=m.css(a,"top"),i=m.css(a,"left"),j=("absolute"===k||"fixed"===k)&&m.inArray("auto",[f,i])>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),m.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(n.top=b.top-h.top+g),null!=b.left&&(n.left=b.left-h.left+e),"using"in b?b.using.call(a,n):l.css(n)}},m.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){m.offset.setOffset(this,a,b)});var b,c,d={top:0,left:0},e=this[0],f=e&&e.ownerDocument;if(f)return b=f.documentElement,m.contains(b,e)?(typeof e.getBoundingClientRect!==K&&(d=e.getBoundingClientRect()),c=dc(f),{top:d.top+(c.pageYOffset||b.scrollTop)-(b.clientTop||0),left:d.left+(c.pageXOffset||b.scrollLeft)-(b.clientLeft||0)}):d},position:function(){if(this[0]){var a,b,c={top:0,left:0},d=this[0];return"fixed"===m.css(d,"position")?b=d.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),m.nodeName(a[0],"html")||(c=a.offset()),c.top+=m.css(a[0],"borderTopWidth",!0),c.left+=m.css(a[0],"borderLeftWidth",!0)),{top:b.top-c.top-m.css(d,"marginTop",!0),left:b.left-c.left-m.css(d,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||cc;while(a&&!m.nodeName(a,"html")&&"static"===m.css(a,"position"))a=a.offsetParent;return a||cc})}}),m.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c=/Y/.test(b);m.fn[a]=function(d){return V(this,function(a,d,e){var f=dc(a);return void 0===e?f?b in f?f[b]:f.document.documentElement[d]:a[d]:void(f?f.scrollTo(c?m(f).scrollLeft():e,c?e:m(f).scrollTop()):a[d]=e)},a,d,arguments.length,null)}}),m.each(["top","left"],function(a,b){m.cssHooks[b]=La(k.pixelPosition,function(a,c){return c?(c=Ja(a,b),Ha.test(c)?m(a).position()[b]+"px":c):void 0})}),m.each({Height:"height",Width:"width"},function(a,b){m.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){m.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return V(this,function(b,c,d){var e;return m.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?m.css(b,c,g):m.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),m.fn.size=function(){return this.length},m.fn.andSelf=m.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return m});var ec=a.jQuery,fc=a.$;return m.noConflict=function(b){return a.$===m&&(a.$=fc),b&&a.jQuery===m&&(a.jQuery=ec),m},typeof b===K&&(a.jQuery=a.$=m),m}); diff --git a/examples/multilingual/.gitignore b/examples/multilingual/.gitignore new file mode 100644 index 000000000..a48cf0de7 --- /dev/null +++ b/examples/multilingual/.gitignore @@ -0,0 +1 @@ +public diff --git a/examples/multilingual/README.md b/examples/multilingual/README.md new file mode 100644 index 000000000..5c51a6f3f --- /dev/null +++ b/examples/multilingual/README.md @@ -0,0 +1,15 @@ +# Multilingual website with Hugo + +This example was kindly contributed by Egon Elbre in November 2013 +as a wonderful proof-of-concept for internationalization (i18n) +and multilingualization (m17n) in Hugo-generated websites. + +The example works well for the most part, though some minor issues remain. +Please see relevant discussions below: + +* https://github.com/gohugoio/hugo/issues/129 Multiple languages +* https://github.com/gohugoio/hugo/issues/134 Example of a multilingual site + +Alternatively follow our [multilingual site tutorial](http://gohugo.io/tutorials/create-a-multilingual-site/). + +All contributions are welcome! diff --git a/examples/multilingual/config.toml b/examples/multilingual/config.toml new file mode 100644 index 000000000..2c285f0e0 --- /dev/null +++ b/examples/multilingual/config.toml @@ -0,0 +1,39 @@ +baseURL = "http://example.com" + +defaultContentLanguage = "en" + +[taxonomies] +group = "groups" + +[languages] +[languages.en] +weight = 0 +title = "My multilingual site" +[[languages.en.menu.main]] +url = "/home" +name = "Home" +weight = 0 +[[languages.en.menu.main]] +url = "/news" +name = "News" +weight = 1 +[[languages.en.menu.main]] +url = "/about" +name = "About" +weight = 2 + +[languages.et] +weight = 1 +title = "Minu mitmekeelne leht" +[[languages.et.menu.main]] +url = "/kodu" +name = "Kodu" +weight = 0 +[[languages.et.menu.main]] +url = "/uudised" +name = "Uudised" +weight = 1 +[[languages.et.menu.main]] +url = "/minust" +name = "Minust" +weight = 2 diff --git a/examples/multilingual/content/about.en.md b/examples/multilingual/content/about.en.md new file mode 100644 index 000000000..c125eea52 --- /dev/null +++ b/examples/multilingual/content/about.en.md @@ -0,0 +1,12 @@ ++++ +title = "About" +url = "/about" ++++ + +Lorem ipsum dolor sit amet, consectetur adipisicing elit. Illum ex deleniti ut tenetur amet accusantium dolores nam provident! Ipsum, dicta voluptatum quas architecto nostrum sapiente eos commodi numquam accusantium reprehenderit. + +Doloremque, veritatis qui impedit expedita quas distinctio temporibus repellendus dicta debitis iure molestias recusandae cum facere natus esse saepe inventore beatae ipsum soluta voluptas in quaerat nam culpa id autem! + +## History + +Sequi eum impedit distinctio facilis repudiandae provident iure illo quia autem optio. Ea, facilis, possimus dolor nobis explicabo recusandae numquam ducimus minus eum totam odio architecto nesciunt accusamus expedita natus. diff --git a/examples/multilingual/content/about.et.md b/examples/multilingual/content/about.et.md new file mode 100644 index 000000000..57354e886 --- /dev/null +++ b/examples/multilingual/content/about.et.md @@ -0,0 +1,12 @@ ++++ +title = "Minust" +url = "/minust" ++++ + +Lorem ipsum dolor sit amet, consectetur adipisicing elit. Illum ex deleniti ut tenetur amet accusantium dolores nam provident! Ipsum, dicta voluptatum quas architecto nostrum sapiente eos commodi numquam accusantium reprehenderit. + +Doloremque, veritatis qui impedit expedita quas distinctio temporibus repellendus dicta debitis iure molestias recusandae cum facere natus esse saepe inventore beatae ipsum soluta voluptas in quaerat nam culpa id autem! + +## Ajalugu + +Sequi eum impedit distinctio facilis repudiandae provident iure illo quia autem optio. Ea, facilis, possimus dolor nobis explicabo recusandae numquam ducimus minus eum totam odio architecto nesciunt accusamus expedita natus. diff --git a/examples/multilingual/content/index.en.md b/examples/multilingual/content/index.en.md new file mode 100644 index 000000000..04ce0e544 --- /dev/null +++ b/examples/multilingual/content/index.en.md @@ -0,0 +1,10 @@ ++++ +title = "Home" +url = "/home" ++++ + +Lorem ipsum dolor sit amet, consectetur adipisicing elit. Illum ex deleniti ut tenetur amet accusantium dolores nam provident! Ipsum, dicta voluptatum quas architecto nostrum sapiente eos commodi numquam accusantium reprehenderit. + +Doloremque, veritatis qui impedit expedita quas distinctio temporibus repellendus dicta debitis iure molestias recusandae cum facere natus esse saepe inventore beatae ipsum soluta voluptas in quaerat nam culpa id autem! + +Sequi eum impedit distinctio facilis repudiandae provident iure illo quia autem optio. Ea, facilis, possimus dolor nobis explicabo recusandae numquam ducimus minus eum totam odio architecto nesciunt accusamus expedita natus. diff --git a/examples/multilingual/content/index.et.md b/examples/multilingual/content/index.et.md new file mode 100644 index 000000000..eee0da2a2 --- /dev/null +++ b/examples/multilingual/content/index.et.md @@ -0,0 +1,10 @@ ++++ +title = "Kodu" +url = "/kodu" ++++ + +Lorem ipsum dolor sit amet, consectetur adipisicing elit. Illum ex deleniti ut tenetur amet accusantium dolores nam provident! Ipsum, dicta voluptatum quas architecto nostrum sapiente eos commodi numquam accusantium reprehenderit. + +Doloremque, veritatis qui impedit expedita quas distinctio temporibus repellendus dicta debitis iure molestias recusandae cum facere natus esse saepe inventore beatae ipsum soluta voluptas in quaerat nam culpa id autem! + +Sequi eum impedit distinctio facilis repudiandae provident iure illo quia autem optio. Ea, facilis, possimus dolor nobis explicabo recusandae numquam ducimus minus eum totam odio architecto nesciunt accusamus expedita natus. diff --git a/examples/multilingual/content/story/alpha.en.md b/examples/multilingual/content/story/alpha.en.md new file mode 100644 index 000000000..9cd84f6d1 --- /dev/null +++ b/examples/multilingual/content/story/alpha.en.md @@ -0,0 +1,14 @@ ++++ +title = "Alpha" +groups = ["news"] ++++ + +Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ratione, porro, doloribus ducimus reprehenderit nobis at voluptates ipsa dicta nostrum perferendis in vitae. Magnam, quia officia modi incidunt tenetur ratione cum. + +Magni, maxime, eum, veniam nam iusto rem error id tenetur porro sed modi reprehenderit excepturi impedit saepe vero ducimus quae consequuntur cupiditate est aperiam in cumque sapiente. Ullam, ex, dolorum. + +Pariatur, mollitia dignissimos commodi nostrum dicta accusantium nisi doloremque ratione molestias ex similique a porro quibusdam harum incidunt veniam laborum ipsum facere impedit maiores quam ad vero in obcaecati molestiae. + +Nam, nisi minus voluptatum dolorem quia doloremque officia architecto facere laborum ullam doloribus voluptates dolores quaerat necessitatibus hic expedita reiciendis inventore tenetur aliquam ab! Aliquid odit veniam accusantium maxime necessitatibus. + +Eos ipsam iusto optio odit id et nisi corporis hic. Iusto, cum, facere officiis ad modi numquam quam recusandae soluta rem consequuntur esse tenetur tempore vel. Veritatis, labore et aliquid? diff --git a/examples/multilingual/content/story/beta.en.md b/examples/multilingual/content/story/beta.en.md new file mode 100644 index 000000000..74cd9be3c --- /dev/null +++ b/examples/multilingual/content/story/beta.en.md @@ -0,0 +1,14 @@ ++++ +title = "Beta" +groups = ["news"] ++++ + +Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ratione, porro, doloribus ducimus reprehenderit nobis at voluptates ipsa dicta nostrum perferendis in vitae. Magnam, quia officia modi incidunt tenetur ratione cum. + +Magni, maxime, eum, veniam nam iusto rem error id tenetur porro sed modi reprehenderit excepturi impedit saepe vero ducimus quae consequuntur cupiditate est aperiam in cumque sapiente. Ullam, ex, dolorum. + +Pariatur, mollitia dignissimos commodi nostrum dicta accusantium nisi doloremque ratione molestias ex similique a porro quibusdam harum incidunt veniam laborum ipsum facere impedit maiores quam ad vero in obcaecati molestiae. + +Nam, nisi minus voluptatum dolorem quia doloremque officia architecto facere laborum ullam doloribus voluptates dolores quaerat necessitatibus hic expedita reiciendis inventore tenetur aliquam ab! Aliquid odit veniam accusantium maxime necessitatibus. + +Eos ipsam iusto optio odit id et nisi corporis hic. Iusto, cum, facere officiis ad modi numquam quam recusandae soluta rem consequuntur esse tenetur tempore vel. Veritatis, labore et aliquid? diff --git a/examples/multilingual/content/story/index.en.md b/examples/multilingual/content/story/index.en.md new file mode 100644 index 000000000..5eaf8e7c2 --- /dev/null +++ b/examples/multilingual/content/story/index.en.md @@ -0,0 +1,5 @@ ++++ +title = "News" +url = "/news" +listing = true ++++ diff --git a/examples/multilingual/content/uudis/alfa.et.md b/examples/multilingual/content/uudis/alfa.et.md new file mode 100644 index 000000000..c7ecdd823 --- /dev/null +++ b/examples/multilingual/content/uudis/alfa.et.md @@ -0,0 +1,15 @@ ++++ +title = "Alfa" +url = "/uudis/alfa" +groups = ["uudised"] ++++ + +Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ratione, porro, doloribus ducimus reprehenderit nobis at voluptates ipsa dicta nostrum perferendis in vitae. Magnam, quia officia modi incidunt tenetur ratione cum. + +Magni, maxime, eum, veniam nam iusto rem error id tenetur porro sed modi reprehenderit excepturi impedit saepe vero ducimus quae consequuntur cupiditate est aperiam in cumque sapiente. Ullam, ex, dolorum. + +Pariatur, mollitia dignissimos commodi nostrum dicta accusantium nisi doloremque ratione molestias ex similique a porro quibusdam harum incidunt veniam laborum ipsum facere impedit maiores quam ad vero in obcaecati molestiae. + +Nam, nisi minus voluptatum dolorem quia doloremque officia architecto facere laborum ullam doloribus voluptates dolores quaerat necessitatibus hic expedita reiciendis inventore tenetur aliquam ab! Aliquid odit veniam accusantium maxime necessitatibus. + +Eos ipsam iusto optio odit id et nisi corporis hic. Iusto, cum, facere officiis ad modi numquam quam recusandae soluta rem consequuntur esse tenetur tempore vel. Veritatis, labore et aliquid? diff --git a/examples/multilingual/content/uudis/beeta.et.md b/examples/multilingual/content/uudis/beeta.et.md new file mode 100644 index 000000000..b50cb4c4c --- /dev/null +++ b/examples/multilingual/content/uudis/beeta.et.md @@ -0,0 +1,15 @@ ++++ +title = "Beeta" +url = "/uudis/beeta" +groups = ["uudised"] ++++ + +Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ratione, porro, doloribus ducimus reprehenderit nobis at voluptates ipsa dicta nostrum perferendis in vitae. Magnam, quia officia modi incidunt tenetur ratione cum. + +Magni, maxime, eum, veniam nam iusto rem error id tenetur porro sed modi reprehenderit excepturi impedit saepe vero ducimus quae consequuntur cupiditate est aperiam in cumque sapiente. Ullam, ex, dolorum. + +Pariatur, mollitia dignissimos commodi nostrum dicta accusantium nisi doloremque ratione molestias ex similique a porro quibusdam harum incidunt veniam laborum ipsum facere impedit maiores quam ad vero in obcaecati molestiae. + +Nam, nisi minus voluptatum dolorem quia doloremque officia architecto facere laborum ullam doloribus voluptates dolores quaerat necessitatibus hic expedita reiciendis inventore tenetur aliquam ab! Aliquid odit veniam accusantium maxime necessitatibus. + +Eos ipsam iusto optio odit id et nisi corporis hic. Iusto, cum, facere officiis ad modi numquam quam recusandae soluta rem consequuntur esse tenetur tempore vel. Veritatis, labore et aliquid? diff --git a/examples/multilingual/content/uudis/index.et.md b/examples/multilingual/content/uudis/index.et.md new file mode 100644 index 000000000..4363c2f6a --- /dev/null +++ b/examples/multilingual/content/uudis/index.et.md @@ -0,0 +1,5 @@ ++++ +title = "Uudised" +url = "/uudised" +listing = true ++++ diff --git a/examples/multilingual/i18n/en.toml b/examples/multilingual/i18n/en.toml new file mode 100644 index 000000000..30893b411 --- /dev/null +++ b/examples/multilingual/i18n/en.toml @@ -0,0 +1,2 @@ +[head_title] +other = "Multilingual" diff --git a/examples/multilingual/i18n/et.toml b/examples/multilingual/i18n/et.toml new file mode 100644 index 000000000..a96203eff --- /dev/null +++ b/examples/multilingual/i18n/et.toml @@ -0,0 +1,2 @@ +[head_title] +other = "Mitmekeelne" diff --git a/examples/multilingual/layouts/_default/single.html b/examples/multilingual/layouts/_default/single.html new file mode 100644 index 000000000..831cfaf94 --- /dev/null +++ b/examples/multilingual/layouts/_default/single.html @@ -0,0 +1,4 @@ +{{ partial "head.html" . }} +{{ partial "header.html" . }} +{{ .Content }} +{{ partial "footer.html" . }} diff --git a/examples/multilingual/layouts/index.html b/examples/multilingual/layouts/index.html new file mode 100644 index 000000000..a4a1e5072 --- /dev/null +++ b/examples/multilingual/layouts/index.html @@ -0,0 +1 @@ +<meta http-equiv="refresh" content="0; url=/home" /> diff --git a/examples/multilingual/layouts/partials/footer.html b/examples/multilingual/layouts/partials/footer.html new file mode 100644 index 000000000..a12f744cc --- /dev/null +++ b/examples/multilingual/layouts/partials/footer.html @@ -0,0 +1,3 @@ +<footer id="footer"><span class="copy-left">©</span> 2015 Egon Elbre</footer> +</body> +</html> diff --git a/examples/multilingual/layouts/partials/head.html b/examples/multilingual/layouts/partials/head.html new file mode 100644 index 000000000..e493add1e --- /dev/null +++ b/examples/multilingual/layouts/partials/head.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html lang="{{ .Params.lang }}"> +<head> + <meta charset="utf-8"> + {{ if .Title }} + <title>{{ i18n "head_title" }} - {{ .Title }}</title> + {{ end }} + <meta name="viewport" content="width=device-width, initial-scale=1"> + <link rel="stylesheet" href="/main.css"> +</head> +<body> diff --git a/examples/multilingual/layouts/partials/header.html b/examples/multilingual/layouts/partials/header.html new file mode 100644 index 000000000..15f67ba72 --- /dev/null +++ b/examples/multilingual/layouts/partials/header.html @@ -0,0 +1,17 @@ +<header> + <nav id="language-menu"> + <a href="/home">English</a> + <a href="/kodu">Eesti</a> + </nav> + + <h1 id="title">{{ .Site.Title }}</h1> + + <nav id="main-menu"> + {{ range .Site.Menus.main }} + <a href="{{ .URL }}">{{ .Name }}</a> + {{ end }} + <div class="clear"></div> + </nav> +</header> + +<h2 id="subtitle">{{ .Title }}</h2> diff --git a/examples/multilingual/layouts/story/single.html b/examples/multilingual/layouts/story/single.html new file mode 100644 index 000000000..beb811cc2 --- /dev/null +++ b/examples/multilingual/layouts/story/single.html @@ -0,0 +1,17 @@ +{{ partial "head.html" . }} +{{ partial "header.html" . }} + +{{ if .Params.listing }} + {{ range .Site.Taxonomies.groups.news.Pages }} + <article class="post"> + <h3><a href='{{ .Permalink }}'>{{ .Title }}</a> </h3> + <div class="post-meta">{{ .Date.Format "Mon, Jan 2, 2006" }} - {{ .FuzzyWordCount }} Words</div> + {{ .Summary }} + <a href='{{ .Permalink }}'><nobr>read more →</nobr></a> + </article> + {{ end }} +{{ else }} + {{ .Content }} +{{ end }} + +{{ partial "footer.html" . }} diff --git a/examples/multilingual/layouts/uudis/single.html b/examples/multilingual/layouts/uudis/single.html new file mode 100644 index 000000000..1af874d2a --- /dev/null +++ b/examples/multilingual/layouts/uudis/single.html @@ -0,0 +1,17 @@ +{{ partial "head.html" . }} +{{ partial "header.html" . }} + +{{ if .Params.listing }} + {{ range .Site.Taxonomies.groups.uudised.Pages }} + <article class="post"> + <h3><a href='{{ .Permalink }}'>{{ .Title }}</a> </h3> + <div class="post-meta">{{ .Date.Format "Mon, Jan 2, 2006" }} - {{ .FuzzyWordCount }} sõna</div> + {{ .Summary }} + <a href='{{ .Permalink }}'><nobr>loe edasi →</nobr></a> + </article> + {{ end }} +{{ else }} + {{ .Content }} +{{ end }} + +{{ partial "footer.html" . }} diff --git a/examples/multilingual/static/main.css b/examples/multilingual/static/main.css new file mode 100644 index 000000000..1a1575ca9 --- /dev/null +++ b/examples/multilingual/static/main.css @@ -0,0 +1,90 @@ +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } + +body { + padding: 0 20px; + max-width: 800px; + margin: 0 auto; + + color: #333; +} + +.clear { clear: both; } + + +#language-menu, #main-menu, #title, #subtitle { + font-family: Georgia; + font-variant: small-caps; +} + +.copy-left { + display: inline-block; + text-align: right; + margin: 0px; + -moz-transform: scaleX(-1); + -o-transform: scaleX(-1); + -webkit-transform: scaleX(-1); + transform: scaleX(-1); + filter: FlipH; + -ms-filter: "FlipH"; +} + +/* Language Menu */ + +#language-menu { float: right; } +#language-menu a { + display: block; + padding: 8px 10px; + width: 100px; + + transition: border-left 0.3s ease-in-out; + border-left: 2px solid #FFF; +} +#language-menu a:hover { border-left: 2px solid #A00; } +#language-menu a, #language-menu a:visited { + color: #333; +} + +/* Main Menu */ + +#main-menu { + margin-top: 20px; + border-left: 2px solid #A00; + padding-left: 10px; +} + +#main-menu a { + float: left; + width: 100px; + text-align: center; + + padding: 5px 10px; + margin: 0; + + text-decoration: none; + font-size: 18px; + + transition: border-bottom 0.3s ease-in-out; + border-bottom: 2px solid #FFF; +} + +#main-menu a:hover { + border-bottom: 2px solid #A00; +} + +/* Content */ + +article h3 { + margin-bottom: 3px; +} +.post-meta { + color: #888; + margin-bottom: 10px; +} + +/* Footer */ + +#footer { + margin: 50px 0; + text-align: center; +} diff --git a/goreleaser.yml b/goreleaser.yml new file mode 100644 index 000000000..438f14f51 --- /dev/null +++ b/goreleaser.yml @@ -0,0 +1,48 @@ +build: + main: main.go + binary: hugo + ldflags: -s -w -X hugolib.BuildDate={{.Date}} + goos: + - darwin + - linux + - windows + - freebsd + - netbsd + - openbsd + - dragonfly + goarch: + - amd64 + - 386 + - arm + - arm64 +fpm: + formats: + - deb + vendor: "gohugo.io" + homepage: "https://gohugo.io/" + maintainer: "Bjørn Erik Pedersen <[email protected]>" + description: "A Fast and Flexible Static Site Generator built with love in GoLang." + license: "Apache 2.0" +archive: + format: tar.gz + format_overrides: + - goos: windows + format: zip + name_template: "{{.Binary}}_{{.Version}}_{{.Os}}-{{.Arch}}" + replacements: + amd64: 64bit + 386: 32bit + arm: ARM + arm64: ARM64 + darwin: macOS + linux: Linux + windows: Windows + openbsd: OpenBSD + netbsd: NetBSD + freebsd: FreeBSD + dragonfly: DragonFlyBSD + files: + - README.md + - LICENSE.md +release: + draft: true diff --git a/helpers/baseURL.go b/helpers/baseURL.go new file mode 100644 index 000000000..5ea82b26d --- /dev/null +++ b/helpers/baseURL.go @@ -0,0 +1,73 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "fmt" + "net/url" + "strings" +) + +// A BaseURL in Hugo is normally on the form scheme://path, but the +// form scheme: is also valid (mailto:[email protected]). +type BaseURL struct { + url *url.URL + urlStr string +} + +func (b BaseURL) String() string { + return b.urlStr +} + +// Protocol is normally on the form "scheme://", i.e. "webcal://". +func (b BaseURL) WithProtocol(protocol string) (string, error) { + u := b.URL() + + scheme := protocol + isFullProtocol := strings.HasSuffix(scheme, "://") + isOpaqueProtocol := strings.HasSuffix(scheme, ":") + + if isFullProtocol { + scheme = strings.TrimSuffix(scheme, "://") + } else if isOpaqueProtocol { + scheme = strings.TrimSuffix(scheme, ":") + } + + u.Scheme = scheme + + if isFullProtocol && u.Opaque != "" { + u.Opaque = "//" + u.Opaque + } else if isOpaqueProtocol && u.Opaque == "" { + return "", fmt.Errorf("Cannot determine BaseURL for protocol %q", protocol) + } + + return u.String(), nil +} + +func (b BaseURL) URL() *url.URL { + // create a copy as it will be modified. + c := *b.url + return &c +} + +func newBaseURLFromString(b string) (BaseURL, error) { + var result BaseURL + + base, err := url.Parse(b) + if err != nil { + return result, err + } + + return BaseURL{url: base, urlStr: base.String()}, nil +} diff --git a/helpers/baseURL_test.go b/helpers/baseURL_test.go new file mode 100644 index 000000000..437152f34 --- /dev/null +++ b/helpers/baseURL_test.go @@ -0,0 +1,61 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestBaseURL(t *testing.T) { + b, err := newBaseURLFromString("http://example.com") + require.NoError(t, err) + require.Equal(t, "http://example.com", b.String()) + + p, err := b.WithProtocol("webcal://") + require.NoError(t, err) + require.Equal(t, "webcal://example.com", p) + + p, err = b.WithProtocol("webcal") + require.NoError(t, err) + require.Equal(t, "webcal://example.com", p) + + _, err = b.WithProtocol("mailto:") + require.Error(t, err) + + b, err = newBaseURLFromString("mailto:[email protected]") + require.NoError(t, err) + require.Equal(t, "mailto:[email protected]", b.String()) + + // These are pretty constructed + p, err = b.WithProtocol("webcal") + require.NoError(t, err) + require.Equal(t, "webcal:[email protected]", p) + + p, err = b.WithProtocol("webcal://") + require.NoError(t, err) + require.Equal(t, "webcal://[email protected]", p) + + // Test with "non-URLs". Some people will try to use these as a way to get + // relative URLs working etc. + b, err = newBaseURLFromString("/") + require.NoError(t, err) + require.Equal(t, "/", b.String()) + + b, err = newBaseURLFromString("") + require.NoError(t, err) + require.Equal(t, "", b.String()) + +} diff --git a/helpers/content.go b/helpers/content.go new file mode 100644 index 000000000..bb547de25 --- /dev/null +++ b/helpers/content.go @@ -0,0 +1,682 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package helpers implements general utility functions that work with +// and on content. The helper functions defined here lay down the +// foundation of how Hugo works with files and filepaths, and perform +// string operations on content. +package helpers + +import ( + "bytes" + "fmt" + "html/template" + "os/exec" + "unicode" + "unicode/utf8" + + "github.com/chaseadamsio/goorgeous" + bp "github.com/gohugoio/hugo/bufferpool" + "github.com/gohugoio/hugo/config" + "github.com/miekg/mmark" + "github.com/mitchellh/mapstructure" + "github.com/russross/blackfriday" + jww "github.com/spf13/jwalterweatherman" + + "strings" +) + +// SummaryLength is the length of the summary that Hugo extracts from a content. +var SummaryLength = 70 + +// SummaryDivider denotes where content summarization should end. The default is "<!--more-->". +var SummaryDivider = []byte("<!--more-->") + +type ContentSpec struct { + blackfriday map[string]interface{} + footnoteAnchorPrefix string + footnoteReturnLinkContents string + + cfg config.Provider +} + +func NewContentSpec(cfg config.Provider) *ContentSpec { + return &ContentSpec{ + blackfriday: cfg.GetStringMap("blackfriday"), + footnoteAnchorPrefix: cfg.GetString("footnoteAnchorPrefix"), + footnoteReturnLinkContents: cfg.GetString("footnoteReturnLinkContents"), + + cfg: cfg, + } +} + +// Blackfriday holds configuration values for Blackfriday rendering. +type Blackfriday struct { + Smartypants bool + AngledQuotes bool + Fractions bool + HrefTargetBlank bool + SmartDashes bool + LatexDashes bool + TaskLists bool + PlainIDAnchors bool + SourceRelativeLinksEval bool + SourceRelativeLinksProjectFolder string + Extensions []string + ExtensionsMask []string +} + +// NewBlackfriday creates a new Blackfriday filled with site config or some sane defaults. +func (c ContentSpec) NewBlackfriday() *Blackfriday { + defaultParam := map[string]interface{}{ + "smartypants": true, + "angledQuotes": false, + "fractions": true, + "hrefTargetBlank": false, + "smartDashes": true, + "latexDashes": true, + "plainIDAnchors": true, + "taskLists": true, + "sourceRelativeLinks": false, + "sourceRelativeLinksProjectFolder": "/docs/content", + } + + ToLowerMap(defaultParam) + + siteConfig := make(map[string]interface{}) + + for k, v := range defaultParam { + siteConfig[k] = v + } + + if c.blackfriday != nil { + for k, v := range c.blackfriday { + siteConfig[k] = v + } + } + + combinedConfig := &Blackfriday{} + if err := mapstructure.Decode(siteConfig, combinedConfig); err != nil { + jww.FATAL.Printf("Failed to get site rendering config\n%s", err.Error()) + } + + if combinedConfig.SourceRelativeLinksEval { + // Remove in Hugo 0.21 + Deprecated("blackfriday", "sourceRelativeLinksEval", + `There is no replacement for this feature, as no developer has stepped up to the plate and volunteered to maintain this feature`, false) + + } + + return combinedConfig +} + +var blackfridayExtensionMap = map[string]int{ + "noIntraEmphasis": blackfriday.EXTENSION_NO_INTRA_EMPHASIS, + "tables": blackfriday.EXTENSION_TABLES, + "fencedCode": blackfriday.EXTENSION_FENCED_CODE, + "autolink": blackfriday.EXTENSION_AUTOLINK, + "strikethrough": blackfriday.EXTENSION_STRIKETHROUGH, + "laxHtmlBlocks": blackfriday.EXTENSION_LAX_HTML_BLOCKS, + "spaceHeaders": blackfriday.EXTENSION_SPACE_HEADERS, + "hardLineBreak": blackfriday.EXTENSION_HARD_LINE_BREAK, + "tabSizeEight": blackfriday.EXTENSION_TAB_SIZE_EIGHT, + "footnotes": blackfriday.EXTENSION_FOOTNOTES, + "noEmptyLineBeforeBlock": blackfriday.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK, + "headerIds": blackfriday.EXTENSION_HEADER_IDS, + "titleblock": blackfriday.EXTENSION_TITLEBLOCK, + "autoHeaderIds": blackfriday.EXTENSION_AUTO_HEADER_IDS, + "backslashLineBreak": blackfriday.EXTENSION_BACKSLASH_LINE_BREAK, + "definitionLists": blackfriday.EXTENSION_DEFINITION_LISTS, +} + +var stripHTMLReplacer = strings.NewReplacer("\n", " ", "</p>", "\n", "<br>", "\n", "<br />", "\n") + +var mmarkExtensionMap = map[string]int{ + "tables": mmark.EXTENSION_TABLES, + "fencedCode": mmark.EXTENSION_FENCED_CODE, + "autolink": mmark.EXTENSION_AUTOLINK, + "laxHtmlBlocks": mmark.EXTENSION_LAX_HTML_BLOCKS, + "spaceHeaders": mmark.EXTENSION_SPACE_HEADERS, + "hardLineBreak": mmark.EXTENSION_HARD_LINE_BREAK, + "footnotes": mmark.EXTENSION_FOOTNOTES, + "noEmptyLineBeforeBlock": mmark.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK, + "headerIds": mmark.EXTENSION_HEADER_IDS, + "autoHeaderIds": mmark.EXTENSION_AUTO_HEADER_IDS, +} + +// StripHTML accepts a string, strips out all HTML tags and returns it. +func StripHTML(s string) string { + + // Shortcut strings with no tags in them + if !strings.ContainsAny(s, "<>") { + return s + } + s = stripHTMLReplacer.Replace(s) + + // Walk through the string removing all tags + b := bp.GetBuffer() + defer bp.PutBuffer(b) + var inTag, isSpace, wasSpace bool + for _, r := range s { + if !inTag { + isSpace = false + } + + switch { + case r == '<': + inTag = true + case r == '>': + inTag = false + case unicode.IsSpace(r): + isSpace = true + fallthrough + default: + if !inTag && (!isSpace || (isSpace && !wasSpace)) { + b.WriteRune(r) + } + } + + wasSpace = isSpace + + } + return b.String() +} + +// stripEmptyNav strips out empty <nav> tags from content. +func stripEmptyNav(in []byte) []byte { + return bytes.Replace(in, []byte("<nav>\n</nav>\n\n"), []byte(``), -1) +} + +// BytesToHTML converts bytes to type template.HTML. +func BytesToHTML(b []byte) template.HTML { + return template.HTML(string(b)) +} + +// getHTMLRenderer creates a new Blackfriday HTML Renderer with the given configuration. +func (c ContentSpec) getHTMLRenderer(defaultFlags int, ctx *RenderingContext) blackfriday.Renderer { + renderParameters := blackfriday.HtmlRendererParameters{ + FootnoteAnchorPrefix: c.footnoteAnchorPrefix, + FootnoteReturnLinkContents: c.footnoteReturnLinkContents, + } + + b := len(ctx.DocumentID) != 0 + + if ctx.Config == nil { + panic(fmt.Sprintf("RenderingContext of %q doesn't have a config", ctx.DocumentID)) + } + + if b && !ctx.Config.PlainIDAnchors { + renderParameters.FootnoteAnchorPrefix = ctx.DocumentID + ":" + renderParameters.FootnoteAnchorPrefix + renderParameters.HeaderIDSuffix = ":" + ctx.DocumentID + } + + htmlFlags := defaultFlags + htmlFlags |= blackfriday.HTML_USE_XHTML + htmlFlags |= blackfriday.HTML_FOOTNOTE_RETURN_LINKS + + if ctx.Config.Smartypants { + htmlFlags |= blackfriday.HTML_USE_SMARTYPANTS + } + + if ctx.Config.AngledQuotes { + htmlFlags |= blackfriday.HTML_SMARTYPANTS_ANGLED_QUOTES + } + + if ctx.Config.Fractions { + htmlFlags |= blackfriday.HTML_SMARTYPANTS_FRACTIONS + } + + if ctx.Config.HrefTargetBlank { + htmlFlags |= blackfriday.HTML_HREF_TARGET_BLANK + } + + if ctx.Config.SmartDashes { + htmlFlags |= blackfriday.HTML_SMARTYPANTS_DASHES + } + + if ctx.Config.LatexDashes { + htmlFlags |= blackfriday.HTML_SMARTYPANTS_LATEX_DASHES + } + + return &HugoHTMLRenderer{ + RenderingContext: ctx, + Renderer: blackfriday.HtmlRendererWithParameters(htmlFlags, "", "", renderParameters), + } +} + +func getMarkdownExtensions(ctx *RenderingContext) int { + // Default Blackfriday common extensions + commonExtensions := 0 | + blackfriday.EXTENSION_NO_INTRA_EMPHASIS | + blackfriday.EXTENSION_TABLES | + blackfriday.EXTENSION_FENCED_CODE | + blackfriday.EXTENSION_AUTOLINK | + blackfriday.EXTENSION_STRIKETHROUGH | + blackfriday.EXTENSION_SPACE_HEADERS | + blackfriday.EXTENSION_HEADER_IDS | + blackfriday.EXTENSION_BACKSLASH_LINE_BREAK | + blackfriday.EXTENSION_DEFINITION_LISTS + + // Extra Blackfriday extensions that Hugo enables by default + flags := commonExtensions | + blackfriday.EXTENSION_AUTO_HEADER_IDS | + blackfriday.EXTENSION_FOOTNOTES + + if ctx.Config == nil { + panic(fmt.Sprintf("RenderingContext of %q doesn't have a config", ctx.DocumentID)) + } + + for _, extension := range ctx.Config.Extensions { + if flag, ok := blackfridayExtensionMap[extension]; ok { + flags |= flag + } + } + for _, extension := range ctx.Config.ExtensionsMask { + if flag, ok := blackfridayExtensionMap[extension]; ok { + flags &= ^flag + } + } + return flags +} + +func (c ContentSpec) markdownRender(ctx *RenderingContext) []byte { + if ctx.RenderTOC { + return blackfriday.Markdown(ctx.Content, + c.getHTMLRenderer(blackfriday.HTML_TOC, ctx), + getMarkdownExtensions(ctx)) + } + return blackfriday.Markdown(ctx.Content, c.getHTMLRenderer(0, ctx), + getMarkdownExtensions(ctx)) +} + +// getMmarkHTMLRenderer creates a new mmark HTML Renderer with the given configuration. +func (c ContentSpec) getMmarkHTMLRenderer(defaultFlags int, ctx *RenderingContext) mmark.Renderer { + renderParameters := mmark.HtmlRendererParameters{ + FootnoteAnchorPrefix: c.footnoteAnchorPrefix, + FootnoteReturnLinkContents: c.footnoteReturnLinkContents, + } + + b := len(ctx.DocumentID) != 0 + + if ctx.Config == nil { + panic(fmt.Sprintf("RenderingContext of %q doesn't have a config", ctx.DocumentID)) + } + + if b && !ctx.Config.PlainIDAnchors { + renderParameters.FootnoteAnchorPrefix = ctx.DocumentID + ":" + renderParameters.FootnoteAnchorPrefix + // renderParameters.HeaderIDSuffix = ":" + ctx.DocumentId + } + + htmlFlags := defaultFlags + htmlFlags |= mmark.HTML_FOOTNOTE_RETURN_LINKS + + return &HugoMmarkHTMLRenderer{ + mmark.HtmlRendererWithParameters(htmlFlags, "", "", renderParameters), + c.cfg, + } +} + +func getMmarkExtensions(ctx *RenderingContext) int { + flags := 0 + flags |= mmark.EXTENSION_TABLES + flags |= mmark.EXTENSION_FENCED_CODE + flags |= mmark.EXTENSION_AUTOLINK + flags |= mmark.EXTENSION_SPACE_HEADERS + flags |= mmark.EXTENSION_CITATION + flags |= mmark.EXTENSION_TITLEBLOCK_TOML + flags |= mmark.EXTENSION_HEADER_IDS + flags |= mmark.EXTENSION_AUTO_HEADER_IDS + flags |= mmark.EXTENSION_UNIQUE_HEADER_IDS + flags |= mmark.EXTENSION_FOOTNOTES + flags |= mmark.EXTENSION_SHORT_REF + flags |= mmark.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK + flags |= mmark.EXTENSION_INCLUDE + + if ctx.Config == nil { + panic(fmt.Sprintf("RenderingContext of %q doesn't have a config", ctx.DocumentID)) + } + + for _, extension := range ctx.Config.Extensions { + if flag, ok := mmarkExtensionMap[extension]; ok { + flags |= flag + } + } + return flags +} + +func (c ContentSpec) mmarkRender(ctx *RenderingContext) []byte { + return mmark.Parse(ctx.Content, c.getMmarkHTMLRenderer(0, ctx), + getMmarkExtensions(ctx)).Bytes() +} + +// ExtractTOC extracts Table of Contents from content. +func ExtractTOC(content []byte) (newcontent []byte, toc []byte) { + origContent := make([]byte, len(content)) + copy(origContent, content) + first := []byte(`<nav> +<ul>`) + + last := []byte(`</ul> +</nav>`) + + replacement := []byte(`<nav id="TableOfContents"> +<ul>`) + + startOfTOC := bytes.Index(content, first) + + peekEnd := len(content) + if peekEnd > 70+startOfTOC { + peekEnd = 70 + startOfTOC + } + + if startOfTOC < 0 { + return stripEmptyNav(content), toc + } + // Need to peek ahead to see if this nav element is actually the right one. + correctNav := bytes.Index(content[startOfTOC:peekEnd], []byte(`<li><a href="#`)) + if correctNav < 0 { // no match found + return content, toc + } + lengthOfTOC := bytes.Index(content[startOfTOC:], last) + len(last) + endOfTOC := startOfTOC + lengthOfTOC + + newcontent = append(content[:startOfTOC], content[endOfTOC:]...) + toc = append(replacement, origContent[startOfTOC+len(first):endOfTOC]...) + return +} + +// RenderingContext holds contextual information, like content and configuration, +// for a given content rendering. +// By creating you must set the Config, otherwise it will panic. +type RenderingContext struct { + Content []byte + PageFmt string + DocumentID string + DocumentName string + Config *Blackfriday + RenderTOC bool + FileResolver FileResolverFunc + LinkResolver LinkResolverFunc + Cfg config.Provider +} + +// RenderBytes renders a []byte. +func (c ContentSpec) RenderBytes(ctx *RenderingContext) []byte { + switch ctx.PageFmt { + default: + return c.markdownRender(ctx) + case "markdown": + return c.markdownRender(ctx) + case "asciidoc": + return getAsciidocContent(ctx) + case "mmark": + return c.mmarkRender(ctx) + case "rst": + return getRstContent(ctx) + case "org": + return orgRender(ctx, c) + } +} + +// TotalWords counts instance of one or more consecutive white space +// characters, as defined by unicode.IsSpace, in s. +// This is a cheaper way of word counting than the obvious len(strings.Fields(s)). +func TotalWords(s string) int { + n := 0 + inWord := false + for _, r := range s { + wasInWord := inWord + inWord = !unicode.IsSpace(r) + if inWord && !wasInWord { + n++ + } + } + return n +} + +// Old implementation only kept for benchmark comparison. +// TODO(bep) remove +func totalWordsOld(s string) int { + return len(strings.Fields(s)) +} + +// TruncateWordsByRune truncates words by runes. +func TruncateWordsByRune(words []string, max int) (string, bool) { + count := 0 + for index, word := range words { + if count >= max { + return strings.Join(words[:index], " "), true + } + runeCount := utf8.RuneCountInString(word) + if len(word) == runeCount { + count++ + } else if count+runeCount < max { + count += runeCount + } else { + for ri := range word { + if count >= max { + truncatedWords := append(words[:index], word[:ri]) + return strings.Join(truncatedWords, " "), true + } + count++ + } + } + } + + return strings.Join(words, " "), false +} + +// TruncateWordsToWholeSentence takes content and truncates to whole sentence +// limited by max number of words. It also returns whether it is truncated. +func TruncateWordsToWholeSentence(s string, max int) (string, bool) { + + var ( + wordCount = 0 + lastWordIndex = -1 + ) + + for i, r := range s { + if unicode.IsSpace(r) { + wordCount++ + lastWordIndex = i + + if wordCount >= max { + break + } + + } + } + + if lastWordIndex == -1 { + return s, false + } + + endIndex := -1 + + for j, r := range s[lastWordIndex:] { + if isEndOfSentence(r) { + endIndex = j + lastWordIndex + utf8.RuneLen(r) + break + } + } + + if endIndex == -1 { + return s, false + } + + return strings.TrimSpace(s[:endIndex]), endIndex < len(s) +} + +func isEndOfSentence(r rune) bool { + return r == '.' || r == '?' || r == '!' || r == '"' || r == '\n' +} + +// Kept only for benchmark. +func truncateWordsToWholeSentenceOld(content string, max int) (string, bool) { + words := strings.Fields(content) + + if max >= len(words) { + return strings.Join(words, " "), false + } + + for counter, word := range words[max:] { + if strings.HasSuffix(word, ".") || + strings.HasSuffix(word, "?") || + strings.HasSuffix(word, ".\"") || + strings.HasSuffix(word, "!") { + upper := max + counter + 1 + return strings.Join(words[:upper], " "), (upper < len(words)) + } + } + + return strings.Join(words[:max], " "), true +} + +func getAsciidocExecPath() string { + path, err := exec.LookPath("asciidoctor") + if err != nil { + path, err = exec.LookPath("asciidoc") + if err != nil { + return "" + } + } + return path +} + +// HasAsciidoc returns whether Asciidoctor or Asciidoc is installed on this computer. +func HasAsciidoc() bool { + return getAsciidocExecPath() != "" +} + +// getAsciidocContent calls asciidoctor or asciidoc as an external helper +// to convert AsciiDoc content to HTML. +func getAsciidocContent(ctx *RenderingContext) []byte { + content := ctx.Content + cleanContent := bytes.Replace(content, SummaryDivider, []byte(""), 1) + + path := getAsciidocExecPath() + if path == "" { + jww.ERROR.Println("asciidoctor / asciidoc not found in $PATH: Please install.\n", + " Leaving AsciiDoc content unrendered.") + return content + } + + jww.INFO.Println("Rendering", ctx.DocumentName, "with", path, "...") + cmd := exec.Command(path, "--no-header-footer", "--safe", "-") + cmd.Stdin = bytes.NewReader(cleanContent) + var out, cmderr bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &cmderr + err := cmd.Run() + // asciidoctor has exit code 0 even if there are errors in stderr + // -> log stderr output regardless of state of err + for _, item := range strings.Split(string(cmderr.Bytes()), "\n") { + item := strings.TrimSpace(item) + if item != "" { + jww.ERROR.Println(strings.Replace(item, "<stdin>", ctx.DocumentName, 1)) + } + } + if err != nil { + jww.ERROR.Printf("%s rendering %s: %v", path, ctx.DocumentName, err) + } + + return normalizeExternalHelperLineFeeds(out.Bytes()) +} + +// HasRst returns whether rst2html is installed on this computer. +func HasRst() bool { + return getRstExecPath() != "" +} + +func getRstExecPath() string { + path, err := exec.LookPath("rst2html") + if err != nil { + path, err = exec.LookPath("rst2html.py") + if err != nil { + return "" + } + } + return path +} + +func getPythonExecPath() string { + path, err := exec.LookPath("python") + if err != nil { + path, err = exec.LookPath("python.exe") + if err != nil { + return "" + } + } + return path +} + +// getRstContent calls the Python script rst2html as an external helper +// to convert reStructuredText content to HTML. +func getRstContent(ctx *RenderingContext) []byte { + content := ctx.Content + cleanContent := bytes.Replace(content, SummaryDivider, []byte(""), 1) + + python := getPythonExecPath() + path := getRstExecPath() + + if path == "" { + jww.ERROR.Println("rst2html / rst2html.py not found in $PATH: Please install.\n", + " Leaving reStructuredText content unrendered.") + return content + + } + + jww.INFO.Println("Rendering", ctx.DocumentName, "with", path, "...") + cmd := exec.Command(python, path, "--leave-comments") + cmd.Stdin = bytes.NewReader(cleanContent) + var out, cmderr bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &cmderr + err := cmd.Run() + // By default rst2html exits w/ non-zero exit code only if severe, i.e. + // halting errors occurred. -> log stderr output regardless of state of err + for _, item := range strings.Split(string(cmderr.Bytes()), "\n") { + item := strings.TrimSpace(item) + if item != "" { + jww.ERROR.Println(strings.Replace(item, "<stdin>", ctx.DocumentName, 1)) + } + } + if err != nil { + jww.ERROR.Printf("%s rendering %s: %v", path, ctx.DocumentName, err) + } + + result := normalizeExternalHelperLineFeeds(out.Bytes()) + + // TODO(bep) check if rst2html has a body only option. + bodyStart := bytes.Index(result, []byte("<body>\n")) + if bodyStart < 0 { + bodyStart = -7 //compensate for length + } + + bodyEnd := bytes.Index(result, []byte("\n</body>")) + if bodyEnd < 0 || bodyEnd >= len(result) { + bodyEnd = len(result) - 1 + if bodyEnd < 0 { + bodyEnd = 0 + } + } + + return result[bodyStart+7 : bodyEnd] +} + +func orgRender(ctx *RenderingContext, c ContentSpec) []byte { + content := ctx.Content + cleanContent := bytes.Replace(content, []byte("# more"), []byte(""), 1) + return goorgeous.Org(cleanContent, + c.getHTMLRenderer(blackfriday.HTML_TOC, ctx)) +} diff --git a/helpers/content_renderer.go b/helpers/content_renderer.go new file mode 100644 index 000000000..f0d8cda12 --- /dev/null +++ b/helpers/content_renderer.go @@ -0,0 +1,127 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "bytes" + "html" + + "github.com/gohugoio/hugo/config" + "github.com/miekg/mmark" + "github.com/russross/blackfriday" + jww "github.com/spf13/jwalterweatherman" +) + +type LinkResolverFunc func(ref string) (string, error) +type FileResolverFunc func(ref string) (string, error) + +// HugoHTMLRenderer wraps a blackfriday.Renderer, typically a blackfriday.Html +// Enabling Hugo to customise the rendering experience +type HugoHTMLRenderer struct { + *RenderingContext + blackfriday.Renderer +} + +func (r *HugoHTMLRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string) { + if r.Cfg.GetBool("pygmentsCodeFences") && (lang != "" || r.Cfg.GetBool("pygmentsCodeFencesGuessSyntax")) { + opts := r.Cfg.GetString("pygmentsOptions") + str := html.UnescapeString(string(text)) + out.WriteString(Highlight(r.RenderingContext.Cfg, str, lang, opts)) + } else { + r.Renderer.BlockCode(out, text, lang) + } +} + +func (r *HugoHTMLRenderer) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) { + if r.LinkResolver == nil || bytes.HasPrefix(link, []byte("HAHAHUGOSHORTCODE")) { + // Use the blackfriday built in Link handler + r.Renderer.Link(out, link, title, content) + } else { + // set by SourceRelativeLinksEval + newLink, err := r.LinkResolver(string(link)) + if err != nil { + newLink = string(link) + jww.ERROR.Printf("LinkResolver: %s", err) + } + r.Renderer.Link(out, []byte(newLink), title, content) + } +} +func (r *HugoHTMLRenderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) { + if r.FileResolver == nil || bytes.HasPrefix(link, []byte("HAHAHUGOSHORTCODE")) { + // Use the blackfriday built in Image handler + r.Renderer.Image(out, link, title, alt) + } else { + // set by SourceRelativeLinksEval + newLink, err := r.FileResolver(string(link)) + if err != nil { + newLink = string(link) + jww.ERROR.Printf("FileResolver: %s", err) + } + r.Renderer.Image(out, []byte(newLink), title, alt) + } +} + +// ListItem adds task list support to the Blackfriday renderer. +func (r *HugoHTMLRenderer) ListItem(out *bytes.Buffer, text []byte, flags int) { + if !r.Config.TaskLists { + r.Renderer.ListItem(out, text, flags) + return + } + + switch { + case bytes.HasPrefix(text, []byte("[ ] ")): + text = append([]byte(`<input type="checkbox" disabled class="task-list-item">`), text[3:]...) + + case bytes.HasPrefix(text, []byte("[x] ")) || bytes.HasPrefix(text, []byte("[X] ")): + text = append([]byte(`<input type="checkbox" checked disabled class="task-list-item">`), text[3:]...) + } + + r.Renderer.ListItem(out, text, flags) +} + +// List adds task list support to the Blackfriday renderer. +func (r *HugoHTMLRenderer) List(out *bytes.Buffer, text func() bool, flags int) { + if !r.Config.TaskLists { + r.Renderer.List(out, text, flags) + return + } + marker := out.Len() + r.Renderer.List(out, text, flags) + if out.Len() > marker { + list := out.Bytes()[marker:] + if bytes.Contains(list, []byte("task-list-item")) { + // Rewrite the buffer from the marker + out.Truncate(marker) + // May be either dl, ul or ol + list := append(list[:4], append([]byte(` class="task-list"`), list[4:]...)...) + out.Write(list) + } + } +} + +// HugoMmarkHTMLRenderer wraps a mmark.Renderer, typically a mmark.html +// Enabling Hugo to customise the rendering experience +type HugoMmarkHTMLRenderer struct { + mmark.Renderer + Cfg config.Provider +} + +func (r *HugoMmarkHTMLRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string, caption []byte, subfigure bool, callouts bool) { + if r.Cfg.GetBool("pygmentsCodeFences") && (lang != "" || r.Cfg.GetBool("pygmentsCodeFencesGuessSyntax")) { + str := html.UnescapeString(string(text)) + out.WriteString(Highlight(r.Cfg, str, lang, "")) + } else { + r.Renderer.BlockCode(out, text, lang, caption, subfigure, callouts) + } +} diff --git a/helpers/content_renderer_test.go b/helpers/content_renderer_test.go new file mode 100644 index 000000000..2f155de07 --- /dev/null +++ b/helpers/content_renderer_test.go @@ -0,0 +1,133 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "bytes" + "regexp" + "testing" + + "github.com/spf13/viper" +) + +// Renders a codeblock using Blackfriday +func (c ContentSpec) render(input string) string { + ctx := &RenderingContext{Cfg: c.cfg, Config: c.NewBlackfriday()} + render := c.getHTMLRenderer(0, ctx) + + buf := &bytes.Buffer{} + render.BlockCode(buf, []byte(input), "html") + return buf.String() +} + +// Renders a codeblock using Mmark +func (c ContentSpec) renderWithMmark(input string) string { + ctx := &RenderingContext{Cfg: c.cfg, Config: c.NewBlackfriday()} + render := c.getMmarkHTMLRenderer(0, ctx) + + buf := &bytes.Buffer{} + render.BlockCode(buf, []byte(input), "html", []byte(""), false, false) + return buf.String() +} + +func TestCodeFence(t *testing.T) { + + if !HasPygments() { + t.Skip("Skipping Pygments test as Pygments is not installed or available.") + return + } + + type test struct { + enabled bool + input, expected string + } + + // Pygments 2.0 and 2.1 have slightly different outputs so only do partial matching + data := []test{ + {true, "<html></html>", `(?s)^<div class="highlight"><pre><code class="language-html" data-lang="html">.*?</code></pre></div>\n$`}, + {false, "<html></html>", `(?s)^<pre><code class="language-html">.*?</code></pre>\n$`}, + } + + for i, d := range data { + v := viper.New() + + v.Set("pygmentsStyle", "monokai") + v.Set("pygmentsUseClasses", true) + v.Set("pygmentsCodeFences", d.enabled) + + c := NewContentSpec(v) + + result := c.render(d.input) + + expectedRe, err := regexp.Compile(d.expected) + + if err != nil { + t.Fatal("Invalid regexp", err) + } + matched := expectedRe.MatchString(result) + + if !matched { + t.Errorf("Test %d failed. BlackFriday enabled:%t, Expected:\n%q got:\n%q", i, d.enabled, d.expected, result) + } + + result = c.renderWithMmark(d.input) + matched = expectedRe.MatchString(result) + if !matched { + t.Errorf("Test %d failed. Mmark enabled:%t, Expected:\n%q got:\n%q", i, d.enabled, d.expected, result) + } + } +} + +func TestBlackfridayTaskList(t *testing.T) { + c := newTestContentSpec() + + for i, this := range []struct { + markdown string + taskListEnabled bool + expect string + }{ + {` +TODO: + +- [x] On1 +- [X] On2 +- [ ] Off + +END +`, true, `<p>TODO:</p> + +<ul class="task-list"> +<li><input type="checkbox" checked disabled class="task-list-item"> On1</li> +<li><input type="checkbox" checked disabled class="task-list-item"> On2</li> +<li><input type="checkbox" disabled class="task-list-item"> Off</li> +</ul> + +<p>END</p> +`}, + {`- [x] On1`, false, `<ul> +<li>[x] On1</li> +</ul> +`}, + } { + blackFridayConfig := c.NewBlackfriday() + blackFridayConfig.TaskLists = this.taskListEnabled + ctx := &RenderingContext{Content: []byte(this.markdown), PageFmt: "markdown", Config: blackFridayConfig} + + result := string(c.RenderBytes(ctx)) + + if result != this.expect { + t.Errorf("[%d] got \n%v but expected \n%v", i, result, this.expect) + } + } +} diff --git a/helpers/content_test.go b/helpers/content_test.go new file mode 100644 index 000000000..95261efdf --- /dev/null +++ b/helpers/content_test.go @@ -0,0 +1,462 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "bytes" + "html/template" + "strings" + "testing" + + "github.com/miekg/mmark" + "github.com/russross/blackfriday" + "github.com/stretchr/testify/assert" +) + +const tstHTMLContent = "<!DOCTYPE html><html><head><script src=\"http://two/foobar.js\"></script></head><body><nav><ul><li hugo-nav=\"section_0\"></li><li hugo-nav=\"section_1\"></li></ul></nav><article>content <a href=\"http://two/foobar\">foobar</a>. Follow up</article><p>This is some text.<br>And some more.</p></body></html>" + +func TestStripHTML(t *testing.T) { + type test struct { + input, expected string + } + data := []test{ + {"<h1>strip h1 tag <h1>", "strip h1 tag "}, + {"<p> strip p tag </p>", " strip p tag "}, + {"</br> strip br<br>", " strip br\n"}, + {"</br> strip br2<br />", " strip br2\n"}, + {"This <strong>is</strong> a\nnewline", "This is a newline"}, + {"No Tags", "No Tags"}, + {`<p>Summary Next Line. +<figure > + + <img src="/not/real" /> + + +</figure> +. +More text here.</p> + +<p>Some more text</p>`, "Summary Next Line. . More text here.\nSome more text\n"}, + } + for i, d := range data { + output := StripHTML(d.input) + if d.expected != output { + t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) + } + } +} + +func BenchmarkStripHTML(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + StripHTML(tstHTMLContent) + } +} + +func TestStripEmptyNav(t *testing.T) { + cleaned := stripEmptyNav([]byte("do<nav>\n</nav>\n\nbedobedo")) + assert.Equal(t, []byte("dobedobedo"), cleaned) +} + +func TestBytesToHTML(t *testing.T) { + assert.Equal(t, template.HTML("dobedobedo"), BytesToHTML([]byte("dobedobedo"))) +} + +var benchmarkTruncateString = strings.Repeat("This is a sentence about nothing.", 20) + +func BenchmarkTestTruncateWordsToWholeSentence(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + TruncateWordsToWholeSentence(benchmarkTruncateString, SummaryLength) + } +} + +func BenchmarkTestTruncateWordsToWholeSentenceOld(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + truncateWordsToWholeSentenceOld(benchmarkTruncateString, SummaryLength) + } +} + +func TestTruncateWordsToWholeSentence(t *testing.T) { + type test struct { + input, expected string + max int + truncated bool + } + data := []test{ + {"a b c", "a b c", 12, false}, + {"a b c", "a b c", 3, false}, + {"a", "a", 1, false}, + {"This is a sentence.", "This is a sentence.", 5, false}, + {"This is also a sentence!", "This is also a sentence!", 1, false}, + {"To be. Or not to be. That's the question.", "To be.", 1, true}, + {" \nThis is not a sentence\nAnd this is another", "This is not a sentence", 4, true}, + {"", "", 10, false}, + } + for i, d := range data { + output, truncated := TruncateWordsToWholeSentence(d.input, d.max) + if d.expected != output { + t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) + } + + if d.truncated != truncated { + t.Errorf("Test %d failed. Expected truncated=%t got %t", i, d.truncated, truncated) + } + } +} + +func TestTruncateWordsByRune(t *testing.T) { + type test struct { + input, expected string + max int + truncated bool + } + data := []test{ + {"", "", 1, false}, + {"a b c", "a b c", 12, false}, + {"a b c", "a b c", 3, false}, + {"a", "a", 1, false}, + {"Hello 中国", "", 0, true}, + {"这是中文,全中文。", "这是中文,", 5, true}, + {"Hello 中国", "Hello 中", 2, true}, + {"Hello 中国", "Hello 中国", 3, false}, + {"Hello中国 Good 好的", "Hello中国 Good 好", 9, true}, + {"This is a sentence.", "This is", 2, true}, + {"This is also a sentence!", "This", 1, true}, + {"To be. Or not to be. That's the question.", "To be. Or not", 4, true}, + {" \nThis is not a sentence\n ", "This is not", 3, true}, + } + for i, d := range data { + output, truncated := TruncateWordsByRune(strings.Fields(d.input), d.max) + if d.expected != output { + t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) + } + + if d.truncated != truncated { + t.Errorf("Test %d failed. Expected truncated=%t got %t", i, d.truncated, truncated) + } + } +} + +func TestGetHTMLRendererFlags(t *testing.T) { + c := newTestContentSpec() + ctx := &RenderingContext{Cfg: c.cfg, Config: c.NewBlackfriday()} + renderer := c.getHTMLRenderer(blackfriday.HTML_USE_XHTML, ctx) + flags := renderer.GetFlags() + if flags&blackfriday.HTML_USE_XHTML != blackfriday.HTML_USE_XHTML { + t.Errorf("Test flag: %d was not found amongs set flags:%d; Result: %d", blackfriday.HTML_USE_XHTML, flags, flags&blackfriday.HTML_USE_XHTML) + } +} + +func TestGetHTMLRendererAllFlags(t *testing.T) { + c := newTestContentSpec() + + type data struct { + testFlag int + } + + allFlags := []data{ + {blackfriday.HTML_USE_XHTML}, + {blackfriday.HTML_FOOTNOTE_RETURN_LINKS}, + {blackfriday.HTML_USE_SMARTYPANTS}, + {blackfriday.HTML_SMARTYPANTS_ANGLED_QUOTES}, + {blackfriday.HTML_SMARTYPANTS_FRACTIONS}, + {blackfriday.HTML_HREF_TARGET_BLANK}, + {blackfriday.HTML_SMARTYPANTS_DASHES}, + {blackfriday.HTML_SMARTYPANTS_LATEX_DASHES}, + } + defaultFlags := blackfriday.HTML_USE_XHTML + ctx := &RenderingContext{Cfg: c.cfg, Config: c.NewBlackfriday()} + ctx.Config.AngledQuotes = true + ctx.Config.Fractions = true + ctx.Config.HrefTargetBlank = true + ctx.Config.LatexDashes = true + ctx.Config.PlainIDAnchors = true + ctx.Config.SmartDashes = true + ctx.Config.Smartypants = true + ctx.Config.SourceRelativeLinksEval = true + renderer := c.getHTMLRenderer(defaultFlags, ctx) + actualFlags := renderer.GetFlags() + var expectedFlags int + //OR-ing flags together... + for _, d := range allFlags { + expectedFlags |= d.testFlag + } + if expectedFlags != actualFlags { + t.Errorf("Expected flags (%d) did not equal actual (%d) flags.", expectedFlags, actualFlags) + } +} + +func TestGetHTMLRendererAnchors(t *testing.T) { + c := newTestContentSpec() + ctx := &RenderingContext{Cfg: c.cfg, Config: c.NewBlackfriday()} + ctx.DocumentID = "testid" + ctx.Config.PlainIDAnchors = false + + actualRenderer := c.getHTMLRenderer(0, ctx) + headerBuffer := &bytes.Buffer{} + footnoteBuffer := &bytes.Buffer{} + expectedFootnoteHref := []byte("href=\"#fn:testid:href\"") + expectedHeaderID := []byte("<h1 id=\"id:testid\"></h1>\n") + + actualRenderer.Header(headerBuffer, func() bool { return true }, 1, "id") + actualRenderer.FootnoteRef(footnoteBuffer, []byte("href"), 1) + + if !bytes.Contains(footnoteBuffer.Bytes(), expectedFootnoteHref) { + t.Errorf("Footnote anchor prefix not applied. Actual:%s Expected:%s", footnoteBuffer.String(), expectedFootnoteHref) + } + + if !bytes.Equal(headerBuffer.Bytes(), expectedHeaderID) { + t.Errorf("Header Id Postfix not applied. Actual:%s Expected:%s", headerBuffer.String(), expectedHeaderID) + } +} + +func TestGetMmarkHTMLRenderer(t *testing.T) { + c := newTestContentSpec() + ctx := &RenderingContext{Cfg: c.cfg, Config: c.NewBlackfriday()} + ctx.DocumentID = "testid" + ctx.Config.PlainIDAnchors = false + actualRenderer := c.getMmarkHTMLRenderer(0, ctx) + + headerBuffer := &bytes.Buffer{} + footnoteBuffer := &bytes.Buffer{} + expectedFootnoteHref := []byte("href=\"#fn:testid:href\"") + expectedHeaderID := []byte("<h1 id=\"id\"></h1>") + + actualRenderer.FootnoteRef(footnoteBuffer, []byte("href"), 1) + actualRenderer.Header(headerBuffer, func() bool { return true }, 1, "id") + + if !bytes.Contains(footnoteBuffer.Bytes(), expectedFootnoteHref) { + t.Errorf("Footnote anchor prefix not applied. Actual:%s Expected:%s", footnoteBuffer.String(), expectedFootnoteHref) + } + + if bytes.Equal(headerBuffer.Bytes(), expectedHeaderID) { + t.Errorf("Header Id Postfix applied. Actual:%s Expected:%s", headerBuffer.String(), expectedHeaderID) + } +} + +func TestGetMarkdownExtensionsMasksAreRemovedFromExtensions(t *testing.T) { + c := newTestContentSpec() + ctx := &RenderingContext{Cfg: c.cfg, Config: c.NewBlackfriday()} + ctx.Config.Extensions = []string{"headerId"} + ctx.Config.ExtensionsMask = []string{"noIntraEmphasis"} + + actualFlags := getMarkdownExtensions(ctx) + if actualFlags&blackfriday.EXTENSION_NO_INTRA_EMPHASIS == blackfriday.EXTENSION_NO_INTRA_EMPHASIS { + t.Errorf("Masked out flag {%v} found amongst returned extensions.", blackfriday.EXTENSION_NO_INTRA_EMPHASIS) + } +} + +func TestGetMarkdownExtensionsByDefaultAllExtensionsAreEnabled(t *testing.T) { + type data struct { + testFlag int + } + c := newTestContentSpec() + ctx := &RenderingContext{Cfg: c.cfg, Config: c.NewBlackfriday()} + ctx.Config.Extensions = []string{""} + ctx.Config.ExtensionsMask = []string{""} + allExtensions := []data{ + {blackfriday.EXTENSION_NO_INTRA_EMPHASIS}, + {blackfriday.EXTENSION_TABLES}, + {blackfriday.EXTENSION_FENCED_CODE}, + {blackfriday.EXTENSION_AUTOLINK}, + {blackfriday.EXTENSION_STRIKETHROUGH}, + // {blackfriday.EXTENSION_LAX_HTML_BLOCKS}, + {blackfriday.EXTENSION_SPACE_HEADERS}, + // {blackfriday.EXTENSION_HARD_LINE_BREAK}, + // {blackfriday.EXTENSION_TAB_SIZE_EIGHT}, + {blackfriday.EXTENSION_FOOTNOTES}, + // {blackfriday.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK}, + {blackfriday.EXTENSION_HEADER_IDS}, + // {blackfriday.EXTENSION_TITLEBLOCK}, + {blackfriday.EXTENSION_AUTO_HEADER_IDS}, + {blackfriday.EXTENSION_BACKSLASH_LINE_BREAK}, + {blackfriday.EXTENSION_DEFINITION_LISTS}, + } + + actualFlags := getMarkdownExtensions(ctx) + for _, e := range allExtensions { + if actualFlags&e.testFlag != e.testFlag { + t.Errorf("Flag %v was not found in the list of extensions.", e) + } + } +} + +func TestGetMarkdownExtensionsAddingFlagsThroughRenderingContext(t *testing.T) { + c := newTestContentSpec() + ctx := &RenderingContext{Cfg: c.cfg, Config: c.NewBlackfriday()} + ctx.Config.Extensions = []string{"definitionLists"} + ctx.Config.ExtensionsMask = []string{""} + + actualFlags := getMarkdownExtensions(ctx) + if actualFlags&blackfriday.EXTENSION_DEFINITION_LISTS != blackfriday.EXTENSION_DEFINITION_LISTS { + t.Errorf("Masked out flag {%v} found amongst returned extensions.", blackfriday.EXTENSION_DEFINITION_LISTS) + } +} + +func TestGetMarkdownRenderer(t *testing.T) { + c := newTestContentSpec() + ctx := &RenderingContext{Cfg: c.cfg, Config: c.NewBlackfriday()} + ctx.Content = []byte("testContent") + actualRenderedMarkdown := c.markdownRender(ctx) + expectedRenderedMarkdown := []byte("<p>testContent</p>\n") + if !bytes.Equal(actualRenderedMarkdown, expectedRenderedMarkdown) { + t.Errorf("Actual rendered Markdown (%s) did not match expected markdown (%s)", actualRenderedMarkdown, expectedRenderedMarkdown) + } +} + +func TestGetMarkdownRendererWithTOC(t *testing.T) { + c := newTestContentSpec() + ctx := &RenderingContext{RenderTOC: true, Cfg: c.cfg, Config: c.NewBlackfriday()} + ctx.Content = []byte("testContent") + actualRenderedMarkdown := c.markdownRender(ctx) + expectedRenderedMarkdown := []byte("<nav>\n</nav>\n\n<p>testContent</p>\n") + if !bytes.Equal(actualRenderedMarkdown, expectedRenderedMarkdown) { + t.Errorf("Actual rendered Markdown (%s) did not match expected markdown (%s)", actualRenderedMarkdown, expectedRenderedMarkdown) + } +} + +func TestGetMmarkExtensions(t *testing.T) { + //TODO: This is doing the same just with different marks... + type data struct { + testFlag int + } + c := newTestContentSpec() + ctx := &RenderingContext{Cfg: c.cfg, Config: c.NewBlackfriday()} + ctx.Config.Extensions = []string{"tables"} + ctx.Config.ExtensionsMask = []string{""} + allExtensions := []data{ + {mmark.EXTENSION_TABLES}, + {mmark.EXTENSION_FENCED_CODE}, + {mmark.EXTENSION_AUTOLINK}, + {mmark.EXTENSION_SPACE_HEADERS}, + {mmark.EXTENSION_CITATION}, + {mmark.EXTENSION_TITLEBLOCK_TOML}, + {mmark.EXTENSION_HEADER_IDS}, + {mmark.EXTENSION_AUTO_HEADER_IDS}, + {mmark.EXTENSION_UNIQUE_HEADER_IDS}, + {mmark.EXTENSION_FOOTNOTES}, + {mmark.EXTENSION_SHORT_REF}, + {mmark.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK}, + {mmark.EXTENSION_INCLUDE}, + } + + actualFlags := getMmarkExtensions(ctx) + for _, e := range allExtensions { + if actualFlags&e.testFlag != e.testFlag { + t.Errorf("Flag %v was not found in the list of extensions.", e) + } + } +} + +func TestMmarkRender(t *testing.T) { + c := newTestContentSpec() + ctx := &RenderingContext{Cfg: c.cfg, Config: c.NewBlackfriday()} + ctx.Content = []byte("testContent") + actualRenderedMarkdown := c.mmarkRender(ctx) + expectedRenderedMarkdown := []byte("<p>testContent</p>\n") + if !bytes.Equal(actualRenderedMarkdown, expectedRenderedMarkdown) { + t.Errorf("Actual rendered Markdown (%s) did not match expected markdown (%s)", actualRenderedMarkdown, expectedRenderedMarkdown) + } +} + +func TestExtractTOCNormalContent(t *testing.T) { + content := []byte("<nav>\n<ul>\nTOC<li><a href=\"#") + + actualTocLessContent, actualToc := ExtractTOC(content) + expectedTocLess := []byte("TOC<li><a href=\"#") + expectedToc := []byte("<nav id=\"TableOfContents\">\n<ul>\n") + + if !bytes.Equal(actualTocLessContent, expectedTocLess) { + t.Errorf("Actual tocless (%s) did not equal expected (%s) tocless content", actualTocLessContent, expectedTocLess) + } + + if !bytes.Equal(actualToc, expectedToc) { + t.Errorf("Actual toc (%s) did not equal expected (%s) toc content", actualToc, expectedToc) + } +} + +func TestExtractTOCGreaterThanSeventy(t *testing.T) { + content := []byte("<nav>\n<ul>\nTOC This is a very long content which will definitely be greater than seventy, I promise you that.<li><a href=\"#") + + actualTocLessContent, actualToc := ExtractTOC(content) + //Because the start of Toc is greater than 70+startpoint of <li> content and empty TOC will be returned + expectedToc := []byte("") + + if !bytes.Equal(actualTocLessContent, content) { + t.Errorf("Actual tocless (%s) did not equal expected (%s) tocless content", actualTocLessContent, content) + } + + if !bytes.Equal(actualToc, expectedToc) { + t.Errorf("Actual toc (%s) did not equal expected (%s) toc content", actualToc, expectedToc) + } +} + +func TestExtractNoTOC(t *testing.T) { + content := []byte("TOC") + + actualTocLessContent, actualToc := ExtractTOC(content) + expectedToc := []byte("") + + if !bytes.Equal(actualTocLessContent, content) { + t.Errorf("Actual tocless (%s) did not equal expected (%s) tocless content", actualTocLessContent, content) + } + + if !bytes.Equal(actualToc, expectedToc) { + t.Errorf("Actual toc (%s) did not equal expected (%s) toc content", actualToc, expectedToc) + } +} + +var totalWordsBenchmarkString = strings.Repeat("Hugo Rocks ", 200) + +func TestTotalWords(t *testing.T) { + + for i, this := range []struct { + s string + words int + }{ + {"Two, Words!", 2}, + {"Word", 1}, + {"", 0}, + {"One, Two, Three", 3}, + {totalWordsBenchmarkString, 400}, + } { + actualWordCount := TotalWords(this.s) + + if actualWordCount != this.words { + t.Errorf("[%d] Actual word count (%d) for test string (%s) did not match %d", i, actualWordCount, this.s, this.words) + } + } +} + +func BenchmarkTotalWords(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + wordCount := TotalWords(totalWordsBenchmarkString) + if wordCount != 400 { + b.Fatal("Wordcount error") + } + } +} + +func BenchmarkTotalWordsOld(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + wordCount := totalWordsOld(totalWordsBenchmarkString) + if wordCount != 400 { + b.Fatal("Wordcount error") + } + } +} diff --git a/helpers/emoji.go b/helpers/emoji.go new file mode 100644 index 000000000..31cd29deb --- /dev/null +++ b/helpers/emoji.go @@ -0,0 +1,91 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "bytes" + "sync" + + "github.com/kyokomi/emoji" +) + +var ( + emojiInit sync.Once + + emojis = make(map[string][]byte) + + emojiDelim = []byte(":") + emojiWordDelim = []byte(" ") + emojiMaxSize int +) + +// Emojify "emojifies" the input source. +// Note that the input byte slice will be modified if needed. +// See http://www.emoji-cheat-sheet.com/ +func Emojify(source []byte) []byte { + emojiInit.Do(initEmoji) + + start := 0 + k := bytes.Index(source[start:], emojiDelim) + + for k != -1 { + + j := start + k + + upper := j + emojiMaxSize + + if upper > len(source) { + upper = len(source) + } + + endEmoji := bytes.Index(source[j+1:upper], emojiDelim) + nextWordDelim := bytes.Index(source[j:upper], emojiWordDelim) + + if endEmoji < 0 { + start++ + } else if endEmoji == 0 || (nextWordDelim != -1 && nextWordDelim < endEmoji) { + start += endEmoji + 1 + } else { + endKey := endEmoji + j + 2 + emojiKey := source[j:endKey] + + if emoji, ok := emojis[string(emojiKey)]; ok { + source = append(source[:j], append(emoji, source[endKey:]...)...) + } + + start += endEmoji + } + + if start >= len(source) { + break + } + + k = bytes.Index(source[start:], emojiDelim) + } + + return source +} + +func initEmoji() { + emojiMap := emoji.CodeMap() + + for k, v := range emojiMap { + emojis[k] = []byte(v) + + if len(k) > emojiMaxSize { + emojiMaxSize = len(k) + } + } + +} diff --git a/helpers/emoji_test.go b/helpers/emoji_test.go new file mode 100644 index 000000000..f9189eb43 --- /dev/null +++ b/helpers/emoji_test.go @@ -0,0 +1,147 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package helpers + +import ( + "math" + "reflect" + "strings" + "testing" + + "github.com/gohugoio/hugo/bufferpool" + "github.com/kyokomi/emoji" +) + +func TestEmojiCustom(t *testing.T) { + for i, this := range []struct { + input string + expect []byte + }{ + {"A :smile: a day", []byte("A 😄 a day")}, + {"A few :smile:s a day", []byte("A few 😄s a day")}, + {"A :smile: and a :beer: makes the day for sure.", []byte("A 😄 and a 🍺 makes the day for sure.")}, + {"A :smile: and: a :beer:", []byte("A 😄 and: a 🍺")}, + {"A :diamond_shape_with_a_dot_inside: and then some.", []byte("A 💠 and then some.")}, + {":smile:", []byte("😄")}, + {":smi", []byte(":smi")}, + {"A :smile:", []byte("A 😄")}, + {":beer:!", []byte("🍺!")}, + {"::smile:", []byte(":😄")}, + {":beer::", []byte("🍺:")}, + {" :beer: :", []byte(" 🍺 :")}, + {":beer: and :smile: and another :beer:!", []byte("🍺 and 😄 and another 🍺!")}, + {" :beer: : ", []byte(" 🍺 : ")}, + {"No smilies for you!", []byte("No smilies for you!")}, + {" The motto: no smiles! ", []byte(" The motto: no smiles! ")}, + {":hugo_is_the_best_static_gen:", []byte(":hugo_is_the_best_static_gen:")}, + {"은행 :smile: 은행", []byte("은행 😄 은행")}, + // #2198 + {"See: A :beer:!", []byte("See: A 🍺!")}, + {`Aaaaaaaaaa: aaaaaaaaaa aaaaaaaaaa aaaaaaaaaa. + +:beer:`, []byte(`Aaaaaaaaaa: aaaaaaaaaa aaaaaaaaaa aaaaaaaaaa. + +🍺`)}, + {"test :\n```bash\nthis is a test\n```\n\ntest\n\n:cool::blush:::pizza:\\:blush : : blush: :pizza:", []byte("test :\n```bash\nthis is a test\n```\n\ntest\n\n🆒😊:🍕\\:blush : : blush: 🍕")}, + { + // 2391 + "[a](http://gohugo.io) :smile: [r](http://gohugo.io/introduction/overview/) :beer:", + []byte(`[a](http://gohugo.io) 😄 [r](http://gohugo.io/introduction/overview/) 🍺`), + }, + } { + + result := Emojify([]byte(this.input)) + + if !reflect.DeepEqual(result, this.expect) { + t.Errorf("[%d] got %q but expected %q", i, result, this.expect) + } + + } +} + +// The Emoji benchmarks below are heavily skewed in Hugo's direction: +// +// Hugo have a byte slice, wants a byte slice and doesn't mind if the original is modified. + +func BenchmarkEmojiKyokomiFprint(b *testing.B) { + + f := func(in []byte) []byte { + buff := bufferpool.GetBuffer() + defer bufferpool.PutBuffer(buff) + emoji.Fprint(buff, string(in)) + + bc := make([]byte, buff.Len(), buff.Len()) + copy(bc, buff.Bytes()) + return bc + } + + doBenchmarkEmoji(b, f) +} + +func BenchmarkEmojiKyokomiSprint(b *testing.B) { + + f := func(in []byte) []byte { + return []byte(emoji.Sprint(string(in))) + } + + doBenchmarkEmoji(b, f) +} + +func BenchmarkHugoEmoji(b *testing.B) { + doBenchmarkEmoji(b, Emojify) +} + +func doBenchmarkEmoji(b *testing.B, f func(in []byte) []byte) { + + type input struct { + in []byte + expect []byte + } + + data := []struct { + input string + expect string + }{ + {"A :smile: a day", emoji.Sprint("A :smile: a day")}, + {"A :smile: and a :beer: day keeps the doctor away", emoji.Sprint("A :smile: and a :beer: day keeps the doctor away")}, + {"A :smile: a day and 10 " + strings.Repeat(":beer: ", 10), emoji.Sprint("A :smile: a day and 10 " + strings.Repeat(":beer: ", 10))}, + {"No smiles today.", "No smiles today."}, + {"No smiles for you or " + strings.Repeat("you ", 1000), "No smiles for you or " + strings.Repeat("you ", 1000)}, + } + + var in = make([]input, b.N*len(data)) + var cnt = 0 + for i := 0; i < b.N; i++ { + for _, this := range data { + in[cnt] = input{[]byte(this.input), []byte(this.expect)} + cnt++ + } + } + + b.ResetTimer() + cnt = 0 + for i := 0; i < b.N; i++ { + for j := range data { + currIn := in[cnt] + cnt++ + result := f(currIn.in) + // The Emoji implementations gives slightly different output. + diffLen := len(result) - len(currIn.expect) + diffLen = int(math.Abs(float64(diffLen))) + if diffLen > 30 { + b.Fatalf("[%d] emoji std, got \n%q but expected \n%q", j, result, currIn.expect) + } + } + + } +} diff --git a/helpers/general.go b/helpers/general.go new file mode 100644 index 000000000..7901be654 --- /dev/null +++ b/helpers/general.go @@ -0,0 +1,357 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "fmt" + "io" + "net" + "path/filepath" + "strings" + "sync" + "unicode" + "unicode/utf8" + + bp "github.com/gohugoio/hugo/bufferpool" + "github.com/spf13/cast" + jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/pflag" +) + +// FilePathSeparator as defined by os.Separator. +const FilePathSeparator = string(filepath.Separator) + +// Strips carriage returns from third-party / external processes (useful for Windows) +func normalizeExternalHelperLineFeeds(content []byte) []byte { + return bytes.Replace(content, []byte("\r"), []byte(""), -1) +} + +// FindAvailablePort returns an available and valid TCP port. +func FindAvailablePort() (*net.TCPAddr, error) { + l, err := net.Listen("tcp", ":0") + if err == nil { + defer l.Close() + addr := l.Addr() + if a, ok := addr.(*net.TCPAddr); ok { + return a, nil + } + return nil, fmt.Errorf("Unable to obtain a valid tcp port. %v", addr) + } + return nil, err +} + +// InStringArray checks if a string is an element of a slice of strings +// and returns a boolean value. +func InStringArray(arr []string, el string) bool { + for _, v := range arr { + if v == el { + return true + } + } + return false +} + +// GuessType attempts to guess the type of file from a given string. +func GuessType(in string) string { + switch strings.ToLower(in) { + case "md", "markdown", "mdown": + return "markdown" + case "asciidoc", "adoc", "ad": + return "asciidoc" + case "mmark": + return "mmark" + case "rst": + return "rst" + case "html", "htm": + return "html" + case "org": + return "org" + } + + return "unknown" +} + +// FirstUpper returns a string with the first character as upper case. +func FirstUpper(s string) string { + if s == "" { + return "" + } + r, n := utf8.DecodeRuneInString(s) + return string(unicode.ToUpper(r)) + s[n:] +} + +// UniqueStrings returns a new slice with any duplicates removed. +func UniqueStrings(s []string) []string { + var unique []string + set := map[string]interface{}{} + for _, val := range s { + if _, ok := set[val]; !ok { + unique = append(unique, val) + set[val] = val + } + } + return unique +} + +// ReaderToBytes takes an io.Reader argument, reads from it +// and returns bytes. +func ReaderToBytes(lines io.Reader) []byte { + if lines == nil { + return []byte{} + } + b := bp.GetBuffer() + defer bp.PutBuffer(b) + + b.ReadFrom(lines) + + bc := make([]byte, b.Len(), b.Len()) + copy(bc, b.Bytes()) + return bc +} + +// ToLowerMap makes all the keys in the given map lower cased and will do so +// recursively. +// Notes: +// * This will modify the map given. +// * Any nested map[interface{}]interface{} will be converted to map[string]interface{}. +func ToLowerMap(m map[string]interface{}) { + for k, v := range m { + switch v.(type) { + case map[interface{}]interface{}: + v = cast.ToStringMap(v) + ToLowerMap(v.(map[string]interface{})) + case map[string]interface{}: + ToLowerMap(v.(map[string]interface{})) + } + + lKey := strings.ToLower(k) + if k != lKey { + delete(m, k) + m[lKey] = v + } + + } +} + +// ReaderToString is the same as ReaderToBytes, but returns a string. +func ReaderToString(lines io.Reader) string { + if lines == nil { + return "" + } + b := bp.GetBuffer() + defer bp.PutBuffer(b) + b.ReadFrom(lines) + return b.String() +} + +// ReaderContains reports whether subslice is within r. +func ReaderContains(r io.Reader, subslice []byte) bool { + + if r == nil || len(subslice) == 0 { + return false + } + + bufflen := len(subslice) * 4 + halflen := bufflen / 2 + buff := make([]byte, bufflen) + var err error + var n, i int + + for { + i++ + if i == 1 { + n, err = io.ReadAtLeast(r, buff[:halflen], halflen) + } else { + if i != 2 { + // shift left to catch overlapping matches + copy(buff[:], buff[halflen:]) + } + n, err = io.ReadAtLeast(r, buff[halflen:], halflen) + } + + if n > 0 && bytes.Contains(buff, subslice) { + return true + } + + if err != nil { + break + } + } + return false +} + +// ThemeSet checks whether a theme is in use or not. +func (p *PathSpec) ThemeSet() bool { + return p.theme != "" +} + +type logPrinter interface { + // Println is the only common method that works in all of JWWs loggers. + Println(a ...interface{}) +} + +// DistinctLogger ignores duplicate log statements. +type DistinctLogger struct { + sync.RWMutex + logger logPrinter + m map[string]bool +} + +// Println will log the string returned from fmt.Sprintln given the arguments, +// but not if it has been logged before. +func (l *DistinctLogger) Println(v ...interface{}) { + // fmt.Sprint doesn't add space between string arguments + logStatement := strings.TrimSpace(fmt.Sprintln(v...)) + l.print(logStatement) +} + +// Printf will log the string returned from fmt.Sprintf given the arguments, +// but not if it has been logged before. +// Note: A newline is appended. +func (l *DistinctLogger) Printf(format string, v ...interface{}) { + logStatement := fmt.Sprintf(format, v...) + l.print(logStatement) +} + +func (l *DistinctLogger) print(logStatement string) { + l.RLock() + if l.m[logStatement] { + l.RUnlock() + return + } + l.RUnlock() + + l.Lock() + if !l.m[logStatement] { + l.logger.Println(logStatement) + l.m[logStatement] = true + } + l.Unlock() +} + +// NewDistinctErrorLogger creates a new DistinctLogger that logs ERRORs +func NewDistinctErrorLogger() *DistinctLogger { + return &DistinctLogger{m: make(map[string]bool), logger: jww.ERROR} +} + +// NewDistinctWarnLogger creates a new DistinctLogger that logs WARNs +func NewDistinctWarnLogger() *DistinctLogger { + return &DistinctLogger{m: make(map[string]bool), logger: jww.WARN} +} + +// NewDistinctFeedbackLogger creates a new DistinctLogger that can be used +// to give feedback to the user while not spamming with duplicates. +func NewDistinctFeedbackLogger() *DistinctLogger { + return &DistinctLogger{m: make(map[string]bool), logger: jww.FEEDBACK} +} + +var ( + // DistinctErrorLog can be used to avoid spamming the logs with errors. + DistinctErrorLog = NewDistinctErrorLogger() + + // DistinctWarnLog can be used to avoid spamming the logs with warnings. + DistinctWarnLog = NewDistinctWarnLogger() + + // DistinctFeedbackLog can be used to avoid spamming the logs with info messages. + DistinctFeedbackLog = NewDistinctFeedbackLogger() +) + +// InitLoggers sets up the global distinct loggers. +func InitLoggers() { + DistinctErrorLog = NewDistinctErrorLogger() + DistinctWarnLog = NewDistinctWarnLogger() + DistinctFeedbackLog = NewDistinctFeedbackLogger() +} + +// Deprecated informs about a deprecation, but only once for a given set of arguments' values. +// If the err flag is enabled, it logs as an ERROR (will exit with -1) and the text will +// point at the next Hugo release. +// The idea is two remove an item in two Hugo releases to give users and theme authors +// plenty of time to fix their templates. +func Deprecated(object, item, alternative string, err bool) { + if err { + DistinctErrorLog.Printf("%s's %s is deprecated and will be removed in Hugo %s. %s.", object, item, CurrentHugoVersion.Next().ReleaseVersion(), alternative) + + } else { + // Make sure the users see this while avoiding build breakage. This will not lead to an os.Exit(-1) + DistinctFeedbackLog.Printf("WARNING: %s's %s is deprecated and will be removed in a future release. %s.", object, item, alternative) + } +} + +// SliceToLower goes through the source slice and lowers all values. +func SliceToLower(s []string) []string { + if s == nil { + return nil + } + + l := make([]string, len(s)) + for i, v := range s { + l[i] = strings.ToLower(v) + } + + return l +} + +// Md5String takes a string and returns its MD5 hash. +func Md5String(f string) string { + h := md5.New() + h.Write([]byte(f)) + return hex.EncodeToString(h.Sum([]byte{})) +} + +// IsWhitespace determines if the given rune is whitespace. +func IsWhitespace(r rune) bool { + return r == ' ' || r == '\t' || r == '\n' || r == '\r' +} + +// NormalizeHugoFlags facilitates transitions of Hugo command-line flags, +// e.g. --baseUrl to --baseURL, --uglyUrls to --uglyURLs +func NormalizeHugoFlags(f *pflag.FlagSet, name string) pflag.NormalizedName { + switch name { + case "baseUrl": + name = "baseURL" + break + case "uglyUrls": + name = "uglyURLs" + break + } + return pflag.NormalizedName(name) +} + +// DiffStringSlices returns the difference between two string slices. +// Useful in tests. +// See: +// http://stackoverflow.com/questions/19374219/how-to-find-the-difference-between-two-slices-of-strings-in-golang +func DiffStringSlices(slice1 []string, slice2 []string) []string { + diffStr := []string{} + m := map[string]int{} + + for _, s1Val := range slice1 { + m[s1Val] = 1 + } + for _, s2Val := range slice2 { + m[s2Val] = m[s2Val] + 1 + } + + for mKey, mVal := range m { + if mVal == 1 { + diffStr = append(diffStr, mKey) + } + } + + return diffStr +} diff --git a/helpers/general_test.go b/helpers/general_test.go new file mode 100644 index 000000000..ee4ed2370 --- /dev/null +++ b/helpers/general_test.go @@ -0,0 +1,216 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "reflect" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGuessType(t *testing.T) { + for i, this := range []struct { + in string + expect string + }{ + {"md", "markdown"}, + {"markdown", "markdown"}, + {"mdown", "markdown"}, + {"asciidoc", "asciidoc"}, + {"adoc", "asciidoc"}, + {"ad", "asciidoc"}, + {"rst", "rst"}, + {"mmark", "mmark"}, + {"html", "html"}, + {"htm", "html"}, + {"org", "org"}, + {"excel", "unknown"}, + } { + result := GuessType(this.in) + if result != this.expect { + t.Errorf("[%d] got %s but expected %s", i, result, this.expect) + } + } +} + +func TestFirstUpper(t *testing.T) { + for i, this := range []struct { + in string + expect string + }{ + {"foo", "Foo"}, + {"foo bar", "Foo bar"}, + {"Foo Bar", "Foo Bar"}, + {"", ""}, + {"å", "Å"}, + } { + result := FirstUpper(this.in) + if result != this.expect { + t.Errorf("[%d] got %s but expected %s", i, result, this.expect) + } + } +} + +var containsTestText = (`На берегу пустынных волн +Стоял он, дум великих полн, +И вдаль глядел. Пред ним широко +Река неслася; бедный чёлн +По ней стремился одиноко. +По мшистым, топким берегам +Чернели избы здесь и там, +Приют убогого чухонца; +И лес, неведомый лучам +В тумане спрятанного солнца, +Кругом шумел. + +Τη γλώσσα μου έδωσαν ελληνική +το σπίτι φτωχικό στις αμμουδιές του Ομήρου. +Μονάχη έγνοια η γλώσσα μου στις αμμουδιές του Ομήρου. + +από το Άξιον Εστί +του Οδυσσέα Ελύτη + +Sîne klâwen durh die wolken sint geslagen, +er stîget ûf mit grôzer kraft, +ich sih in grâwen tägelîch als er wil tagen, +den tac, der im geselleschaft +erwenden wil, dem werden man, +den ich mit sorgen în verliez. +ich bringe in hinnen, ob ich kan. +sîn vil manegiu tugent michz leisten hiez. +`) + +var containsBenchTestData = []struct { + v1 string + v2 []byte + expect bool +}{ + {"abc", []byte("a"), true}, + {"abc", []byte("b"), true}, + {"abcdefg", []byte("efg"), true}, + {"abc", []byte("d"), false}, + {containsTestText, []byte("стремился"), true}, + {containsTestText, []byte(containsTestText[10:80]), true}, + {containsTestText, []byte(containsTestText[100:111]), true}, + {containsTestText, []byte(containsTestText[len(containsTestText)-100 : len(containsTestText)-10]), true}, + {containsTestText, []byte(containsTestText[len(containsTestText)-20:]), true}, + {containsTestText, []byte("notfound"), false}, +} + +// some corner cases +var containsAdditionalTestData = []struct { + v1 string + v2 []byte + expect bool +}{ + {"", nil, false}, + {"", []byte("a"), false}, + {"a", []byte(""), false}, + {"", []byte(""), false}, +} + +func TestReaderContains(t *testing.T) { + for i, this := range append(containsBenchTestData, containsAdditionalTestData...) { + result := ReaderContains(strings.NewReader(this.v1), this.v2) + if result != this.expect { + t.Errorf("[%d] got %t but expected %t", i, result, this.expect) + } + } + + assert.False(t, ReaderContains(nil, []byte("a"))) + assert.False(t, ReaderContains(nil, nil)) +} + +func BenchmarkReaderContains(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + for i, this := range containsBenchTestData { + result := ReaderContains(strings.NewReader(this.v1), this.v2) + if result != this.expect { + b.Errorf("[%d] got %t but expected %t", i, result, this.expect) + } + } + } +} + +func TestUniqueStrings(t *testing.T) { + in := []string{"a", "b", "a", "b", "c", "", "a", "", "d"} + output := UniqueStrings(in) + expected := []string{"a", "b", "c", "", "d"} + if !reflect.DeepEqual(output, expected) { + t.Errorf("Expected %#v, got %#v\n", expected, output) + } +} + +func TestFindAvailablePort(t *testing.T) { + addr, err := FindAvailablePort() + assert.Nil(t, err) + assert.NotNil(t, addr) + assert.True(t, addr.Port > 0) +} + +func TestToLowerMap(t *testing.T) { + + tests := []struct { + input map[string]interface{} + expected map[string]interface{} + }{ + { + map[string]interface{}{ + "abC": 32, + }, + map[string]interface{}{ + "abc": 32, + }, + }, + { + map[string]interface{}{ + "abC": 32, + "deF": map[interface{}]interface{}{ + 23: "A value", + 24: map[string]interface{}{ + "AbCDe": "A value", + "eFgHi": "Another value", + }, + }, + "gHi": map[string]interface{}{ + "J": 25, + }, + }, + map[string]interface{}{ + "abc": 32, + "def": map[string]interface{}{ + "23": "A value", + "24": map[string]interface{}{ + "abcde": "A value", + "efghi": "Another value", + }, + }, + "ghi": map[string]interface{}{ + "j": 25, + }, + }, + }, + } + + for i, test := range tests { + // ToLowerMap modifies input. + ToLowerMap(test.input) + if !reflect.DeepEqual(test.expected, test.input) { + t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, test.input) + } + } +} diff --git a/helpers/hugo.go b/helpers/hugo.go new file mode 100644 index 000000000..6315847ae --- /dev/null +++ b/helpers/hugo.go @@ -0,0 +1,136 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "fmt" + "strings" + + "github.com/spf13/cast" +) + +// HugoVersion represents the Hugo build version. +type HugoVersion struct { + // Major and minor version. + Number float32 + + // Increment this for bug releases + PatchLevel int + + // HugoVersionSuffix is the suffix used in the Hugo version string. + // It will be blank for release versions. + Suffix string +} + +func (v HugoVersion) String() string { + return hugoVersion(v.Number, v.PatchLevel, v.Suffix) +} + +// ReleaseVersion represents the release version. +func (v HugoVersion) ReleaseVersion() HugoVersion { + v.Suffix = "" + return v +} + +// Next returns the next Hugo release version. +func (v HugoVersion) Next() HugoVersion { + return HugoVersion{Number: v.Number + 0.01} +} + +// Pre returns the previous Hugo release version. +func (v HugoVersion) Prev() HugoVersion { + return HugoVersion{Number: v.Number - 0.01} +} + +// NextPatchLevel returns the next patch/bugfix Hugo version. +// This will be a patch increment on the previous Hugo version. +func (v HugoVersion) NextPatchLevel(level int) HugoVersion { + return HugoVersion{Number: v.Number - 0.01, PatchLevel: level} +} + +// CurrentHugoVersion represents the current build version. +// This should be the only one. +var CurrentHugoVersion = HugoVersion{ + Number: 0.25, + PatchLevel: 0, + Suffix: "-DEV", +} + +func hugoVersion(version float32, patchVersion int, suffix string) string { + if patchVersion > 0 { + return fmt.Sprintf("%.2f.%d%s", version, patchVersion, suffix) + } + return fmt.Sprintf("%.2f%s", version, suffix) +} + +// CompareVersion compares the given version string or number against the +// running Hugo version. +// It returns -1 if the given version is less than, 0 if equal and 1 if greater than +// the running version. +func CompareVersion(version interface{}) int { + return compareVersions(CurrentHugoVersion.Number, CurrentHugoVersion.PatchLevel, version) +} + +func compareVersions(inVersion float32, inPatchVersion int, in interface{}) int { + switch d := in.(type) { + case float64: + return compareFloatVersions(inVersion, float32(d)) + case float32: + return compareFloatVersions(inVersion, d) + case int: + return compareFloatVersions(inVersion, float32(d)) + case int32: + return compareFloatVersions(inVersion, float32(d)) + case int64: + return compareFloatVersions(inVersion, float32(d)) + default: + s, err := cast.ToStringE(in) + if err != nil { + return -1 + } + + var ( + v float32 + p int + ) + + if strings.Count(s, ".") == 2 { + li := strings.LastIndex(s, ".") + p = cast.ToInt(s[li+1:]) + s = s[:li] + } + + v = float32(cast.ToFloat64(s)) + + if v == inVersion && p == inPatchVersion { + return 0 + } + + if v < inVersion || (v == inVersion && p < inPatchVersion) { + return -1 + } + + return 1 + } +} + +func compareFloatVersions(version float32, v float32) int { + if v == version { + return 0 + } + if v < version { + return -1 + } + return 1 +} diff --git a/helpers/hugo_test.go b/helpers/hugo_test.go new file mode 100644 index 000000000..c96a1351b --- /dev/null +++ b/helpers/hugo_test.go @@ -0,0 +1,51 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHugoVersion(t *testing.T) { + assert.Equal(t, "0.15-DEV", hugoVersion(0.15, 0, "-DEV")) + assert.Equal(t, "0.15.2-DEV", hugoVersion(0.15, 2, "-DEV")) + + v := HugoVersion{Number: 0.21, PatchLevel: 0, Suffix: "-DEV"} + + require.Equal(t, v.ReleaseVersion().String(), "0.21") + require.Equal(t, "0.21-DEV", v.String()) + require.Equal(t, "0.22", v.Next().String()) + require.Equal(t, "0.20.3", v.NextPatchLevel(3).String()) +} + +func TestCompareVersions(t *testing.T) { + require.Equal(t, 0, compareVersions(0.20, 0, 0.20)) + require.Equal(t, 0, compareVersions(0.20, 0, float32(0.20))) + require.Equal(t, 0, compareVersions(0.20, 0, float64(0.20))) + require.Equal(t, 1, compareVersions(0.19, 1, 0.20)) + require.Equal(t, 1, compareVersions(0.19, 3, "0.20.2")) + require.Equal(t, -1, compareVersions(0.19, 1, 0.01)) + require.Equal(t, 1, compareVersions(0, 1, 3)) + require.Equal(t, 1, compareVersions(0, 1, int32(3))) + require.Equal(t, 1, compareVersions(0, 1, int64(3))) + require.Equal(t, 0, compareVersions(0.20, 0, "0.20")) + require.Equal(t, 0, compareVersions(0.20, 1, "0.20.1")) + require.Equal(t, -1, compareVersions(0.20, 1, "0.20")) + require.Equal(t, 1, compareVersions(0.20, 0, "0.20.1")) + require.Equal(t, 1, compareVersions(0.20, 1, "0.20.2")) + require.Equal(t, 1, compareVersions(0.21, 1, "0.22.1")) +} diff --git a/helpers/language.go b/helpers/language.go new file mode 100644 index 000000000..67db59d25 --- /dev/null +++ b/helpers/language.go @@ -0,0 +1,160 @@ +// Copyright 2016-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "sort" + "strings" + "sync" + + "github.com/gohugoio/hugo/config" + "github.com/spf13/cast" +) + +// These are the settings that should only be looked up in the global Viper +// config and not per language. +// This list may not be complete, but contains only settings that we know +// will be looked up in both. +// This isn't perfect, but it is ultimately the user who shoots him/herself in +// the foot. +// See the pathSpec. +var globalOnlySettings = map[string]bool{ + strings.ToLower("defaultContentLanguageInSubdir"): true, + strings.ToLower("defaultContentLanguage"): true, + strings.ToLower("multilingual"): true, +} + +type Language struct { + Lang string + LanguageName string + Title string + Weight int + + Cfg config.Provider + params map[string]interface{} + paramsInit sync.Once +} + +func (l *Language) String() string { + return l.Lang +} + +func NewLanguage(lang string, cfg config.Provider) *Language { + return &Language{Lang: lang, Cfg: cfg, params: make(map[string]interface{})} +} + +func NewDefaultLanguage(cfg config.Provider) *Language { + defaultLang := cfg.GetString("defaultContentLanguage") + + if defaultLang == "" { + defaultLang = "en" + } + + return NewLanguage(defaultLang, cfg) +} + +type Languages []*Language + +func NewLanguages(l ...*Language) Languages { + languages := make(Languages, len(l)) + for i := 0; i < len(l); i++ { + languages[i] = l[i] + } + sort.Sort(languages) + return languages +} + +func (l Languages) Len() int { return len(l) } +func (l Languages) Less(i, j int) bool { return l[i].Weight < l[j].Weight } +func (l Languages) Swap(i, j int) { l[i], l[j] = l[j], l[i] } + +func (l *Language) Params() map[string]interface{} { + l.paramsInit.Do(func() { + // Merge with global config. + // TODO(bep) consider making this part of a constructor func. + + globalParams := l.Cfg.GetStringMap("params") + for k, v := range globalParams { + if _, ok := l.params[k]; !ok { + l.params[k] = v + } + } + }) + return l.params +} + +// SetParam sets param with the given key and value. +// SetParam is case-insensitive. +func (l *Language) SetParam(k string, v interface{}) { + l.params[strings.ToLower(k)] = v +} + +// GetBool returns the value associated with the key as a boolean. +func (l *Language) GetBool(key string) bool { return cast.ToBool(l.Get(key)) } + +// GetString returns the value associated with the key as a string. +func (l *Language) GetString(key string) string { return cast.ToString(l.Get(key)) } + +// GetInt returns the value associated with the key as an int. +func (l *Language) GetInt(key string) int { return cast.ToInt(l.Get(key)) } + +// GetStringMap returns the value associated with the key as a map of interfaces. +func (l *Language) GetStringMap(key string) map[string]interface{} { + return cast.ToStringMap(l.Get(key)) +} + +// GetStringMapString returns the value associated with the key as a map of strings. +func (l *Language) GetStringMapString(key string) map[string]string { + return cast.ToStringMapString(l.Get(key)) +} + +// Get returns a value associated with the key relying on specified language. +// Get is case-insensitive for a key. +// +// Get returns an interface. For a specific value use one of the Get____ methods. +func (l *Language) Get(key string) interface{} { + if l == nil { + panic("language not set") + } + key = strings.ToLower(key) + if !globalOnlySettings[key] { + if v, ok := l.params[key]; ok { + return v + } + } + return l.Cfg.Get(key) +} + +// Set sets the value for the key in the language's params. +func (l *Language) Set(key string, value interface{}) { + if l == nil { + panic("language not set") + } + key = strings.ToLower(key) + l.params[key] = value +} + +// IsSet checks whether the key is set in the language or the related config store. +func (l *Language) IsSet(key string) bool { + key = strings.ToLower(key) + + key = strings.ToLower(key) + if !globalOnlySettings[key] { + if _, ok := l.params[key]; ok { + return true + } + } + return l.Cfg.IsSet(key) + +} diff --git a/helpers/language_test.go b/helpers/language_test.go new file mode 100644 index 000000000..902177e1a --- /dev/null +++ b/helpers/language_test.go @@ -0,0 +1,33 @@ +// Copyright 2016-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/require" +) + +func TestGetGlobalOnlySetting(t *testing.T) { + v := viper.New() + lang := NewDefaultLanguage(v) + lang.SetParam("defaultContentLanguageInSubdir", false) + lang.SetParam("paginatePath", "side") + v.Set("defaultContentLanguageInSubdir", true) + v.Set("paginatePath", "page") + + require.True(t, lang.GetBool("defaultContentLanguageInSubdir")) + require.Equal(t, "side", lang.GetString("paginatePath")) +} diff --git a/helpers/path.go b/helpers/path.go new file mode 100644 index 000000000..640c97aa2 --- /dev/null +++ b/helpers/path.go @@ -0,0 +1,612 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strings" + "unicode" + + "github.com/spf13/afero" + "golang.org/x/text/transform" + "golang.org/x/text/unicode/norm" +) + +var ( + // ErrThemeUndefined is returned when a theme has not be defined by the user. + ErrThemeUndefined = errors.New("no theme set") + + ErrWalkRootTooShort = errors.New("Path too short. Stop walking.") +) + +// filepathPathBridge is a bridge for common functionality in filepath vs path +type filepathPathBridge interface { + Base(in string) string + Clean(in string) string + Dir(in string) string + Ext(in string) string + Join(elem ...string) string + Separator() string +} + +type filepathBridge struct { +} + +func (filepathBridge) Base(in string) string { + return filepath.Base(in) +} + +func (filepathBridge) Clean(in string) string { + return filepath.Clean(in) +} + +func (filepathBridge) Dir(in string) string { + return filepath.Dir(in) +} + +func (filepathBridge) Ext(in string) string { + return filepath.Ext(in) +} + +func (filepathBridge) Join(elem ...string) string { + return filepath.Join(elem...) +} + +func (filepathBridge) Separator() string { + return FilePathSeparator +} + +var fpb filepathBridge + +// MakePath takes a string with any characters and replace it +// so the string could be used in a path. +// It does so by creating a Unicode-sanitized string, with the spaces replaced, +// whilst preserving the original casing of the string. +// E.g. Social Media -> Social-Media +func (p *PathSpec) MakePath(s string) string { + return p.UnicodeSanitize(strings.Replace(strings.TrimSpace(s), " ", "-", -1)) +} + +// MakePathSanitized creates a Unicode-sanitized string, with the spaces replaced +func (p *PathSpec) MakePathSanitized(s string) string { + if p.disablePathToLower { + return p.MakePath(s) + } + return strings.ToLower(p.MakePath(s)) +} + +// MakeTitle converts the path given to a suitable title, trimming whitespace +// and replacing hyphens with whitespace. +func MakeTitle(inpath string) string { + return strings.Replace(strings.TrimSpace(inpath), "-", " ", -1) +} + +// From https://golang.org/src/net/url/url.go +func ishex(c rune) bool { + switch { + case '0' <= c && c <= '9': + return true + case 'a' <= c && c <= 'f': + return true + case 'A' <= c && c <= 'F': + return true + } + return false +} + +// UnicodeSanitize sanitizes string to be used in Hugo URL's, allowing only +// a predefined set of special Unicode characters. +// If RemovePathAccents configuration flag is enabled, Uniccode accents +// are also removed. +func (p *PathSpec) UnicodeSanitize(s string) string { + source := []rune(s) + target := make([]rune, 0, len(source)) + + for i, r := range source { + if r == '%' && i+2 < len(source) && ishex(source[i+1]) && ishex(source[i+2]) { + target = append(target, r) + } else if unicode.IsLetter(r) || unicode.IsDigit(r) || unicode.IsMark(r) || r == '.' || r == '/' || r == '\\' || r == '_' || r == '-' || r == '#' || r == '+' || r == '~' { + target = append(target, r) + } + } + + var result string + + if p.removePathAccents { + // remove accents - see https://blog.golang.org/normalization + t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC) + result, _, _ = transform.String(t, string(target)) + } else { + result = string(target) + } + + return result +} + +func isMn(r rune) bool { + return unicode.Is(unicode.Mn, r) // Mn: nonspacing marks +} + +// ReplaceExtension takes a path and an extension, strips the old extension +// and returns the path with the new extension. +func ReplaceExtension(path string, newExt string) string { + f, _ := fileAndExt(path, fpb) + return f + "." + newExt +} + +// AbsPathify creates an absolute path if given a relative path. If already +// absolute, the path is just cleaned. +func (p *PathSpec) AbsPathify(inPath string) string { + if filepath.IsAbs(inPath) { + return filepath.Clean(inPath) + } + + // TODO(bep): Consider moving workingDir to argument list + return filepath.Join(p.workingDir, inPath) +} + +// GetLayoutDirPath returns the absolute path to the layout file dir +// for the current Hugo project. +func (p *PathSpec) GetLayoutDirPath() string { + return p.AbsPathify(p.layoutDir) +} + +// GetStaticDirPath returns the absolute path to the static file dir +// for the current Hugo project. +func (p *PathSpec) GetStaticDirPath() string { + return p.AbsPathify(p.staticDir) +} + +// GetThemeDir gets the root directory of the current theme, if there is one. +// If there is no theme, returns the empty string. +func (p *PathSpec) GetThemeDir() string { + if p.ThemeSet() { + return p.AbsPathify(filepath.Join(p.themesDir, p.theme)) + } + return "" +} + +// GetRelativeThemeDir gets the relative root directory of the current theme, if there is one. +// If there is no theme, returns the empty string. +func (p *PathSpec) GetRelativeThemeDir() string { + if p.ThemeSet() { + return strings.TrimPrefix(filepath.Join(p.themesDir, p.theme), FilePathSeparator) + } + return "" +} + +// GetThemeStaticDirPath returns the theme's static dir path if theme is set. +// If theme is set and the static dir doesn't exist, an error is returned. +func (p *PathSpec) GetThemeStaticDirPath() (string, error) { + return p.getThemeDirPath("static") +} + +// GetThemeDataDirPath returns the theme's data dir path if theme is set. +// If theme is set and the data dir doesn't exist, an error is returned. +func (p *PathSpec) GetThemeDataDirPath() (string, error) { + return p.getThemeDirPath("data") +} + +// GetThemeI18nDirPath returns the theme's i18n dir path if theme is set. +// If theme is set and the i18n dir doesn't exist, an error is returned. +func (p *PathSpec) GetThemeI18nDirPath() (string, error) { + return p.getThemeDirPath("i18n") +} + +func (p *PathSpec) getThemeDirPath(path string) (string, error) { + if !p.ThemeSet() { + return "", ErrThemeUndefined + } + + themeDir := filepath.Join(p.GetThemeDir(), path) + if _, err := p.Fs.Source.Stat(themeDir); os.IsNotExist(err) { + return "", fmt.Errorf("Unable to find %s directory for theme %s in %s", path, p.theme, themeDir) + } + + return themeDir, nil +} + +// GetThemesDirPath gets the static files directory of the current theme, if there is one. +// Ignores underlying errors. +// TODO(bep) Candidate for deprecation? +func (p *PathSpec) GetThemesDirPath() string { + dir, _ := p.getThemeDirPath("static") + return dir +} + +// MakeStaticPathRelative makes a relative path to the static files directory. +// It does so by taking either the project's static path or the theme's static +// path into consideration. +func (p *PathSpec) MakeStaticPathRelative(inPath string) (string, error) { + staticDir := p.GetStaticDirPath() + themeStaticDir := p.GetThemesDirPath() + + return makePathRelative(inPath, staticDir, themeStaticDir) +} + +func makePathRelative(inPath string, possibleDirectories ...string) (string, error) { + + for _, currentPath := range possibleDirectories { + if strings.HasPrefix(inPath, currentPath) { + return strings.TrimPrefix(inPath, currentPath), nil + } + } + return inPath, errors.New("Can't extract relative path, unknown prefix") +} + +// Should be good enough for Hugo. +var isFileRe = regexp.MustCompile(`.*\..{1,6}$`) + +// GetDottedRelativePath expects a relative path starting after the content directory. +// It returns a relative path with dots ("..") navigating up the path structure. +func GetDottedRelativePath(inPath string) string { + inPath = filepath.Clean(filepath.FromSlash(inPath)) + + if inPath == "." { + return "./" + } + + if !isFileRe.MatchString(inPath) && !strings.HasSuffix(inPath, FilePathSeparator) { + inPath += FilePathSeparator + } + + if !strings.HasPrefix(inPath, FilePathSeparator) { + inPath = FilePathSeparator + inPath + } + + dir, _ := filepath.Split(inPath) + + sectionCount := strings.Count(dir, FilePathSeparator) + + if sectionCount == 0 || dir == FilePathSeparator { + return "./" + } + + var dottedPath string + + for i := 1; i < sectionCount; i++ { + dottedPath += "../" + } + + return dottedPath +} + +// Ext takes a path and returns the extension, including the delmiter, i.e. ".md". +func Ext(in string) string { + _, ext := fileAndExt(in, fpb) + return ext +} + +// Filename takes a path, strips out the extension, +// and returns the name of the file. +func Filename(in string) (name string) { + name, _ = fileAndExt(in, fpb) + return +} + +// FileAndExt returns the filename and any extension of a file path as +// two separate strings. +// +// If the path, in, contains a directory name ending in a slash, +// then both name and ext will be empty strings. +// +// If the path, in, is either the current directory, the parent +// directory or the root directory, or an empty string, +// then both name and ext will be empty strings. +// +// If the path, in, represents the path of a file without an extension, +// then name will be the name of the file and ext will be an empty string. +// +// If the path, in, represents a filename with an extension, +// then name will be the filename minus any extension - including the dot +// and ext will contain the extension - minus the dot. +func fileAndExt(in string, b filepathPathBridge) (name string, ext string) { + ext = b.Ext(in) + base := b.Base(in) + + return extractFilename(in, ext, base, b.Separator()), ext +} + +func extractFilename(in, ext, base, pathSeparator string) (name string) { + + // No file name cases. These are defined as: + // 1. any "in" path that ends in a pathSeparator + // 2. any "base" consisting of just an pathSeparator + // 3. any "base" consisting of just an empty string + // 4. any "base" consisting of just the current directory i.e. "." + // 5. any "base" consisting of just the parent directory i.e. ".." + if (strings.LastIndex(in, pathSeparator) == len(in)-1) || base == "" || base == "." || base == ".." || base == pathSeparator { + name = "" // there is NO filename + } else if ext != "" { // there was an Extension + // return the filename minus the extension (and the ".") + name = base[:strings.LastIndex(base, ".")] + } else { + // no extension case so just return base, which willi + // be the filename + name = base + } + return + +} + +// GetRelativePath returns the relative path of a given path. +func GetRelativePath(path, base string) (final string, err error) { + if filepath.IsAbs(path) && base == "" { + return "", errors.New("source: missing base directory") + } + name := filepath.Clean(path) + base = filepath.Clean(base) + + name, err = filepath.Rel(base, name) + if err != nil { + return "", err + } + + if strings.HasSuffix(filepath.FromSlash(path), FilePathSeparator) && !strings.HasSuffix(name, FilePathSeparator) { + name += FilePathSeparator + } + return name, nil +} + +// GuessSection returns the section given a source path. +// A section is the part between the root slash and the second slash +// or before the first slash. +func GuessSection(in string) string { + parts := strings.Split(in, FilePathSeparator) + // This will include an empty entry before and after paths with leading and trailing slashes + // eg... /sect/one/ -> ["", "sect", "one", ""] + + // Needs to have at least a value and a slash + if len(parts) < 2 { + return "" + } + + // If it doesn't have a leading slash and value and file or trailing slash, then return "" + if parts[0] == "" && len(parts) < 3 { + return "" + } + + // strip leading slash + if parts[0] == "" { + parts = parts[1:] + } + + // if first directory is "content", return second directory + if parts[0] == "content" { + if len(parts) > 2 { + return parts[1] + } + return "" + } + + return parts[0] +} + +// PathPrep prepares the path using the uglify setting to create paths on +// either the form /section/name/index.html or /section/name.html. +func PathPrep(ugly bool, in string) string { + if ugly { + return Uglify(in) + } + return PrettifyPath(in) +} + +// PrettifyPath is the same as PrettifyURLPath but for file paths. +// /section/name.html becomes /section/name/index.html +// /section/name/ becomes /section/name/index.html +// /section/name/index.html becomes /section/name/index.html +func PrettifyPath(in string) string { + return prettifyPath(in, fpb) +} + +func prettifyPath(in string, b filepathPathBridge) string { + if filepath.Ext(in) == "" { + // /section/name/ -> /section/name/index.html + if len(in) < 2 { + return b.Separator() + } + return b.Join(in, "index.html") + } + name, ext := fileAndExt(in, b) + if name == "index" { + // /section/name/index.html -> /section/name/index.html + return b.Clean(in) + } + // /section/name.html -> /section/name/index.html + return b.Join(b.Dir(in), name, "index"+ext) +} + +// ExtractRootPaths extracts the root paths from the supplied list of paths. +// The resulting root path will not contain any file separators, but there +// may be duplicates. +// So "/content/section/" becomes "content" +func ExtractRootPaths(paths []string) []string { + r := make([]string, len(paths)) + for i, p := range paths { + root := filepath.ToSlash(p) + sections := strings.Split(root, "/") + for _, section := range sections { + if section != "" { + root = section + break + } + } + r[i] = root + } + return r + +} + +// FindCWD returns the current working directory from where the Hugo +// executable is run. +func FindCWD() (string, error) { + serverFile, err := filepath.Abs(os.Args[0]) + + if err != nil { + return "", fmt.Errorf("Can't get absolute path for executable: %v", err) + } + + path := filepath.Dir(serverFile) + realFile, err := filepath.EvalSymlinks(serverFile) + + if err != nil { + if _, err = os.Stat(serverFile + ".exe"); err == nil { + realFile = filepath.Clean(serverFile + ".exe") + } + } + + if err == nil && realFile != serverFile { + path = filepath.Dir(realFile) + } + + return path, nil +} + +// SymbolicWalk is like filepath.Walk, but it supports the root being a +// symbolic link. It will still not follow symbolic links deeper down in +// the file structure +func SymbolicWalk(fs afero.Fs, root string, walker filepath.WalkFunc) error { + + // Sanity check + if len(root) < 4 { + return ErrWalkRootTooShort + } + + // Handle the root first + fileInfo, realPath, err := getRealFileInfo(fs, root) + + if err != nil { + return walker(root, nil, err) + } + + if !fileInfo.IsDir() { + return fmt.Errorf("Cannot walk regular file %s", root) + } + + if err := walker(realPath, fileInfo, err); err != nil && err != filepath.SkipDir { + return err + } + + rootContent, err := afero.ReadDir(fs, root) + + if err != nil { + return walker(root, nil, err) + } + + for _, fi := range rootContent { + if err := afero.Walk(fs, filepath.Join(root, fi.Name()), walker); err != nil { + return err + } + } + + return nil + +} + +func getRealFileInfo(fs afero.Fs, path string) (os.FileInfo, string, error) { + fileInfo, err := lstatIfOs(fs, path) + realPath := path + + if err != nil { + return nil, "", err + } + + if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink { + link, err := filepath.EvalSymlinks(path) + if err != nil { + return nil, "", fmt.Errorf("Cannot read symbolic link '%s', error was: %s", path, err) + } + fileInfo, err = lstatIfOs(fs, link) + if err != nil { + return nil, "", fmt.Errorf("Cannot stat '%s', error was: %s", link, err) + } + realPath = link + } + return fileInfo, realPath, nil +} + +// GetRealPath returns the real file path for the given path, whether it is a +// symlink or not. +func GetRealPath(fs afero.Fs, path string) (string, error) { + _, realPath, err := getRealFileInfo(fs, path) + + if err != nil { + return "", err + } + + return realPath, nil +} + +// Code copied from Afero's path.go +// if the filesystem is OsFs use Lstat, else use fs.Stat +func lstatIfOs(fs afero.Fs, path string) (info os.FileInfo, err error) { + _, ok := fs.(*afero.OsFs) + if ok { + info, err = os.Lstat(path) + } else { + info, err = fs.Stat(path) + } + return +} + +// SafeWriteToDisk is the same as WriteToDisk +// but it also checks to see if file/directory already exists. +func SafeWriteToDisk(inpath string, r io.Reader, fs afero.Fs) (err error) { + return afero.SafeWriteReader(fs, inpath, r) +} + +// WriteToDisk writes content to disk. +func WriteToDisk(inpath string, r io.Reader, fs afero.Fs) (err error) { + return afero.WriteReader(fs, inpath, r) +} + +// GetTempDir returns a temporary directory with the given sub path. +func GetTempDir(subPath string, fs afero.Fs) string { + return afero.GetTempDir(fs, subPath) +} + +// DirExists checks if a path exists and is a directory. +func DirExists(path string, fs afero.Fs) (bool, error) { + return afero.DirExists(fs, path) +} + +// IsDir checks if a given path is a directory. +func IsDir(path string, fs afero.Fs) (bool, error) { + return afero.IsDir(fs, path) +} + +// IsEmpty checks if a given path is empty. +func IsEmpty(path string, fs afero.Fs) (bool, error) { + return afero.IsEmpty(fs, path) +} + +// FileContains checks if a file contains a specified string. +func FileContains(filename string, subslice []byte, fs afero.Fs) (bool, error) { + return afero.FileContainsBytes(fs, filename, subslice) +} + +// FileContainsAny checks if a file contains any of the specified strings. +func FileContainsAny(filename string, subslices [][]byte, fs afero.Fs) (bool, error) { + return afero.FileContainsAnyBytes(fs, filename, subslices) +} + +// Exists checks if a file or directory exists. +func Exists(path string, fs afero.Fs) (bool, error) { + return afero.Exists(fs, path) +} diff --git a/helpers/path_test.go b/helpers/path_test.go new file mode 100644 index 000000000..5c0ae10ea --- /dev/null +++ b/helpers/path_test.go @@ -0,0 +1,828 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "runtime" + "strconv" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/stretchr/testify/assert" + + "github.com/gohugoio/hugo/hugofs" + "github.com/spf13/afero" + "github.com/spf13/viper" +) + +func TestMakePath(t *testing.T) { + tests := []struct { + input string + expected string + removeAccents bool + }{ + {" Foo bar ", "Foo-bar", true}, + {"Foo.Bar/foo_Bar-Foo", "Foo.Bar/foo_Bar-Foo", true}, + {"fOO,bar:foobAR", "fOObarfoobAR", true}, + {"FOo/BaR.html", "FOo/BaR.html", true}, + {"трям/трям", "трям/трям", true}, + {"은행", "은행", true}, + {"Банковский кассир", "Банковскии-кассир", true}, + // Issue #1488 + {"संस्कृत", "संस्कृत", false}, + {"a%C3%B1ame", "a%C3%B1ame", false}, // Issue #1292 + {"this+is+a+test", "this+is+a+test", false}, // Issue #1290 + {"~foo", "~foo", false}, // Issue #2177 + + } + + for _, test := range tests { + v := viper.New() + l := NewDefaultLanguage(v) + v.Set("removePathAccents", test.removeAccents) + p, _ := NewPathSpec(hugofs.NewMem(v), l) + + output := p.MakePath(test.input) + if output != test.expected { + t.Errorf("Expected %#v, got %#v\n", test.expected, output) + } + } +} + +func TestMakePathSanitized(t *testing.T) { + v := viper.New() + l := NewDefaultLanguage(v) + p, _ := NewPathSpec(hugofs.NewMem(v), l) + + tests := []struct { + input string + expected string + }{ + {" FOO bar ", "foo-bar"}, + {"Foo.Bar/fOO_bAr-Foo", "foo.bar/foo_bar-foo"}, + {"FOO,bar:FooBar", "foobarfoobar"}, + {"foo/BAR.HTML", "foo/bar.html"}, + {"трям/трям", "трям/трям"}, + {"은행", "은행"}, + } + + for _, test := range tests { + output := p.MakePathSanitized(test.input) + if output != test.expected { + t.Errorf("Expected %#v, got %#v\n", test.expected, output) + } + } +} + +func TestMakePathSanitizedDisablePathToLower(t *testing.T) { + v := viper.New() + + v.Set("disablePathToLower", true) + + l := NewDefaultLanguage(v) + p, _ := NewPathSpec(hugofs.NewMem(v), l) + + tests := []struct { + input string + expected string + }{ + {" FOO bar ", "FOO-bar"}, + {"Foo.Bar/fOO_bAr-Foo", "Foo.Bar/fOO_bAr-Foo"}, + {"FOO,bar:FooBar", "FOObarFooBar"}, + {"foo/BAR.HTML", "foo/BAR.HTML"}, + {"трям/трям", "трям/трям"}, + {"은행", "은행"}, + } + + for _, test := range tests { + output := p.MakePathSanitized(test.input) + if output != test.expected { + t.Errorf("Expected %#v, got %#v\n", test.expected, output) + } + } +} + +func TestGetRelativePath(t *testing.T) { + tests := []struct { + path string + base string + expect interface{} + }{ + {filepath.FromSlash("/a/b"), filepath.FromSlash("/a"), filepath.FromSlash("b")}, + {filepath.FromSlash("/a/b/c/"), filepath.FromSlash("/a"), filepath.FromSlash("b/c/")}, + {filepath.FromSlash("/c"), filepath.FromSlash("/a/b"), filepath.FromSlash("../../c")}, + {filepath.FromSlash("/c"), "", false}, + } + for i, this := range tests { + // ultimately a fancy wrapper around filepath.Rel + result, err := GetRelativePath(this.path, this.base) + + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] GetRelativePath didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] GetRelativePath failed: %s", i, err) + continue + } + if result != this.expect { + t.Errorf("[%d] GetRelativePath got %v but expected %v", i, result, this.expect) + } + } + + } +} + +func TestGetRealPath(t *testing.T) { + if runtime.GOOS == "windows" && os.Getenv("CI") == "" { + t.Skip("Skip TestGetRealPath as os.Symlink needs administrator rights on Windows") + } + + d1, err := ioutil.TempDir("", "d1") + defer os.Remove(d1) + fs := afero.NewOsFs() + + rp1, err := GetRealPath(fs, d1) + require.NoError(t, err) + assert.Equal(t, d1, rp1) + + sym := filepath.Join(os.TempDir(), "d1sym") + err = os.Symlink(d1, sym) + require.NoError(t, err) + defer os.Remove(sym) + + rp2, err := GetRealPath(fs, sym) + require.NoError(t, err) + + // On OS X, the temp folder is itself a symbolic link (to /private...) + // This has to do for now. + assert.True(t, strings.HasSuffix(rp2, d1)) + +} + +func TestMakePathRelative(t *testing.T) { + type test struct { + inPath, path1, path2, output string + } + + data := []test{ + {"/abc/bcd/ab.css", "/abc/bcd", "/bbc/bcd", "/ab.css"}, + {"/abc/bcd/ab.css", "/abcd/bcd", "/abc/bcd", "/ab.css"}, + } + + for i, d := range data { + output, _ := makePathRelative(d.inPath, d.path1, d.path2) + if d.output != output { + t.Errorf("Test #%d failed. Expected %q got %q", i, d.output, output) + } + } + _, error := makePathRelative("a/b/c.ss", "/a/c", "/d/c", "/e/f") + + if error == nil { + t.Errorf("Test failed, expected error") + } +} + +func TestGetDottedRelativePath(t *testing.T) { + // on Windows this will receive both kinds, both country and western ... + for _, f := range []func(string) string{filepath.FromSlash, func(s string) string { return s }} { + doTestGetDottedRelativePath(f, t) + } + +} + +func doTestGetDottedRelativePath(urlFixer func(string) string, t *testing.T) { + type test struct { + input, expected string + } + data := []test{ + {"", "./"}, + {urlFixer("/"), "./"}, + {urlFixer("post"), "../"}, + {urlFixer("/post"), "../"}, + {urlFixer("post/"), "../"}, + {urlFixer("tags/foo.html"), "../"}, + {urlFixer("/tags/foo.html"), "../"}, + {urlFixer("/post/"), "../"}, + {urlFixer("////post/////"), "../"}, + {urlFixer("/foo/bar/index.html"), "../../"}, + {urlFixer("/foo/bar/foo/"), "../../../"}, + {urlFixer("/foo/bar/foo"), "../../../"}, + {urlFixer("foo/bar/foo/"), "../../../"}, + {urlFixer("foo/bar/foo/bar"), "../../../../"}, + {"404.html", "./"}, + {"404.xml", "./"}, + {"/404.html", "./"}, + } + for i, d := range data { + output := GetDottedRelativePath(d.input) + if d.expected != output { + t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) + } + } +} + +func TestMakeTitle(t *testing.T) { + type test struct { + input, expected string + } + data := []test{ + {"Make-Title", "Make Title"}, + {"MakeTitle", "MakeTitle"}, + {"make_title", "make_title"}, + } + for i, d := range data { + output := MakeTitle(d.input) + if d.expected != output { + t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) + } + } +} + +// Replace Extension is probably poorly named, but the intent of the +// function is to accept a path and return only the file name with a +// new extension. It's intentionally designed to strip out the path +// and only provide the name. We should probably rename the function to +// be more explicit at some point. +func TestReplaceExtension(t *testing.T) { + type test struct { + input, newext, expected string + } + data := []test{ + // These work according to the above definition + {"/some/random/path/file.xml", "html", "file.html"}, + {"/banana.html", "xml", "banana.xml"}, + {"./banana.html", "xml", "banana.xml"}, + {"banana/pie/index.html", "xml", "index.xml"}, + {"../pies/fish/index.html", "xml", "index.xml"}, + // but these all fail + {"filename-without-an-ext", "ext", "filename-without-an-ext.ext"}, + {"/filename-without-an-ext", "ext", "filename-without-an-ext.ext"}, + {"/directory/mydir/", "ext", ".ext"}, + {"mydir/", "ext", ".ext"}, + } + + for i, d := range data { + output := ReplaceExtension(filepath.FromSlash(d.input), d.newext) + if d.expected != output { + t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) + } + } +} + +func TestDirExists(t *testing.T) { + type test struct { + input string + expected bool + } + + data := []test{ + {".", true}, + {"./", true}, + {"..", true}, + {"../", true}, + {"./..", true}, + {"./../", true}, + {os.TempDir(), true}, + {os.TempDir() + FilePathSeparator, true}, + {"/", true}, + {"/some-really-random-directory-name", false}, + {"/some/really/random/directory/name", false}, + {"./some-really-random-local-directory-name", false}, + {"./some/really/random/local/directory/name", false}, + } + + for i, d := range data { + exists, _ := DirExists(filepath.FromSlash(d.input), new(afero.OsFs)) + if d.expected != exists { + t.Errorf("Test %d failed. Expected %t got %t", i, d.expected, exists) + } + } +} + +func TestIsDir(t *testing.T) { + type test struct { + input string + expected bool + } + data := []test{ + {"./", true}, + {"/", true}, + {"./this-directory-does-not-existi", false}, + {"/this-absolute-directory/does-not-exist", false}, + } + + for i, d := range data { + + exists, _ := IsDir(d.input, new(afero.OsFs)) + if d.expected != exists { + t.Errorf("Test %d failed. Expected %t got %t", i, d.expected, exists) + } + } +} + +func TestIsEmpty(t *testing.T) { + zeroSizedFile, _ := createZeroSizedFileInTempDir() + defer deleteFileInTempDir(zeroSizedFile) + nonZeroSizedFile, _ := createNonZeroSizedFileInTempDir() + defer deleteFileInTempDir(nonZeroSizedFile) + emptyDirectory, _ := createEmptyTempDir() + defer deleteTempDir(emptyDirectory) + nonEmptyZeroLengthFilesDirectory, _ := createTempDirWithZeroLengthFiles() + defer deleteTempDir(nonEmptyZeroLengthFilesDirectory) + nonEmptyNonZeroLengthFilesDirectory, _ := createTempDirWithNonZeroLengthFiles() + defer deleteTempDir(nonEmptyNonZeroLengthFilesDirectory) + nonExistentFile := os.TempDir() + "/this-file-does-not-exist.txt" + nonExistentDir := os.TempDir() + "/this/directory/does/not/exist/" + + fileDoesNotExist := fmt.Errorf("%q path does not exist", nonExistentFile) + dirDoesNotExist := fmt.Errorf("%q path does not exist", nonExistentDir) + + type test struct { + input string + expectedResult bool + expectedErr error + } + + data := []test{ + {zeroSizedFile.Name(), true, nil}, + {nonZeroSizedFile.Name(), false, nil}, + {emptyDirectory, true, nil}, + {nonEmptyZeroLengthFilesDirectory, false, nil}, + {nonEmptyNonZeroLengthFilesDirectory, false, nil}, + {nonExistentFile, false, fileDoesNotExist}, + {nonExistentDir, false, dirDoesNotExist}, + } + for i, d := range data { + exists, err := IsEmpty(d.input, new(afero.OsFs)) + if d.expectedResult != exists { + t.Errorf("Test %d failed. Expected result %t got %t", i, d.expectedResult, exists) + } + if d.expectedErr != nil { + if d.expectedErr.Error() != err.Error() { + t.Errorf("Test %d failed. Expected %q(%#v) got %q(%#v)", i, d.expectedErr, d.expectedErr, err, err) + } + } else { + if d.expectedErr != err { + t.Errorf("Test %d failed. Expected %q(%#v) got %q(%#v)", i, d.expectedErr, d.expectedErr, err, err) + } + } + } +} + +func createZeroSizedFileInTempDir() (*os.File, error) { + filePrefix := "_path_test_" + f, e := ioutil.TempFile("", filePrefix) // dir is os.TempDir() + if e != nil { + // if there was an error no file was created. + // => no requirement to delete the file + return nil, e + } + return f, nil +} + +func createNonZeroSizedFileInTempDir() (*os.File, error) { + f, err := createZeroSizedFileInTempDir() + if err != nil { + // no file ?? + } + byteString := []byte("byteString") + err = ioutil.WriteFile(f.Name(), byteString, 0644) + if err != nil { + // delete the file + deleteFileInTempDir(f) + return nil, err + } + return f, nil +} + +func deleteFileInTempDir(f *os.File) { + err := os.Remove(f.Name()) + if err != nil { + // now what? + } +} + +func createEmptyTempDir() (string, error) { + dirPrefix := "_dir_prefix_" + d, e := ioutil.TempDir("", dirPrefix) // will be in os.TempDir() + if e != nil { + // no directory to delete - it was never created + return "", e + } + return d, nil +} + +func createTempDirWithZeroLengthFiles() (string, error) { + d, dirErr := createEmptyTempDir() + if dirErr != nil { + //now what? + } + filePrefix := "_path_test_" + _, fileErr := ioutil.TempFile(d, filePrefix) // dir is os.TempDir() + if fileErr != nil { + // if there was an error no file was created. + // but we need to remove the directory to clean-up + deleteTempDir(d) + return "", fileErr + } + // the dir now has one, zero length file in it + return d, nil + +} + +func createTempDirWithNonZeroLengthFiles() (string, error) { + d, dirErr := createEmptyTempDir() + if dirErr != nil { + //now what? + } + filePrefix := "_path_test_" + f, fileErr := ioutil.TempFile(d, filePrefix) // dir is os.TempDir() + if fileErr != nil { + // if there was an error no file was created. + // but we need to remove the directory to clean-up + deleteTempDir(d) + return "", fileErr + } + byteString := []byte("byteString") + fileErr = ioutil.WriteFile(f.Name(), byteString, 0644) + if fileErr != nil { + // delete the file + deleteFileInTempDir(f) + // also delete the directory + deleteTempDir(d) + return "", fileErr + } + + // the dir now has one, zero length file in it + return d, nil + +} + +func deleteTempDir(d string) { + err := os.RemoveAll(d) + if err != nil { + // now what? + } +} + +func TestExists(t *testing.T) { + zeroSizedFile, _ := createZeroSizedFileInTempDir() + defer deleteFileInTempDir(zeroSizedFile) + nonZeroSizedFile, _ := createNonZeroSizedFileInTempDir() + defer deleteFileInTempDir(nonZeroSizedFile) + emptyDirectory, _ := createEmptyTempDir() + defer deleteTempDir(emptyDirectory) + nonExistentFile := os.TempDir() + "/this-file-does-not-exist.txt" + nonExistentDir := os.TempDir() + "/this/directory/does/not/exist/" + + type test struct { + input string + expectedResult bool + expectedErr error + } + + data := []test{ + {zeroSizedFile.Name(), true, nil}, + {nonZeroSizedFile.Name(), true, nil}, + {emptyDirectory, true, nil}, + {nonExistentFile, false, nil}, + {nonExistentDir, false, nil}, + } + for i, d := range data { + exists, err := Exists(d.input, new(afero.OsFs)) + if d.expectedResult != exists { + t.Errorf("Test %d failed. Expected result %t got %t", i, d.expectedResult, exists) + } + if d.expectedErr != err { + t.Errorf("Test %d failed. Expected %q got %q", i, d.expectedErr, err) + } + } + +} + +func TestAbsPathify(t *testing.T) { + defer viper.Reset() + + type test struct { + inPath, workingDir, expected string + } + data := []test{ + {os.TempDir(), filepath.FromSlash("/work"), filepath.Clean(os.TempDir())}, // TempDir has trailing slash + {"dir", filepath.FromSlash("/work"), filepath.FromSlash("/work/dir")}, + } + + windowsData := []test{ + {"c:\\banana\\..\\dir", "c:\\foo", "c:\\dir"}, + {"\\dir", "c:\\foo", "c:\\foo\\dir"}, + {"c:\\", "c:\\foo", "c:\\"}, + } + + unixData := []test{ + {"/banana/../dir/", "/work", "/dir"}, + } + + for i, d := range data { + viper.Reset() + // todo see comment in AbsPathify + ps := newTestDefaultPathSpec("workingDir", d.workingDir) + + expected := ps.AbsPathify(d.inPath) + if d.expected != expected { + t.Errorf("Test %d failed. Expected %q but got %q", i, d.expected, expected) + } + } + t.Logf("Running platform specific path tests for %s", runtime.GOOS) + if runtime.GOOS == "windows" { + for i, d := range windowsData { + ps := newTestDefaultPathSpec("workingDir", d.workingDir) + + expected := ps.AbsPathify(d.inPath) + if d.expected != expected { + t.Errorf("Test %d failed. Expected %q but got %q", i, d.expected, expected) + } + } + } else { + for i, d := range unixData { + ps := newTestDefaultPathSpec("workingDir", d.workingDir) + + expected := ps.AbsPathify(d.inPath) + if d.expected != expected { + t.Errorf("Test %d failed. Expected %q but got %q", i, d.expected, expected) + } + } + } + +} + +func TestFilename(t *testing.T) { + type test struct { + input, expected string + } + data := []test{ + {"index.html", "index"}, + {"./index.html", "index"}, + {"/index.html", "index"}, + {"index", "index"}, + {"/tmp/index.html", "index"}, + {"./filename-no-ext", "filename-no-ext"}, + {"/filename-no-ext", "filename-no-ext"}, + {"filename-no-ext", "filename-no-ext"}, + {"directory/", ""}, // no filename case?? + {"directory/.hidden.ext", ".hidden"}, + {"./directory/../~/banana/gold.fish", "gold"}, + {"../directory/banana.man", "banana"}, + {"~/mydir/filename.ext", "filename"}, + {"./directory//tmp/filename.ext", "filename"}, + } + + for i, d := range data { + output := Filename(filepath.FromSlash(d.input)) + if d.expected != output { + t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) + } + } +} + +func TestFileAndExt(t *testing.T) { + type test struct { + input, expectedFile, expectedExt string + } + data := []test{ + {"index.html", "index", ".html"}, + {"./index.html", "index", ".html"}, + {"/index.html", "index", ".html"}, + {"index", "index", ""}, + {"/tmp/index.html", "index", ".html"}, + {"./filename-no-ext", "filename-no-ext", ""}, + {"/filename-no-ext", "filename-no-ext", ""}, + {"filename-no-ext", "filename-no-ext", ""}, + {"directory/", "", ""}, // no filename case?? + {"directory/.hidden.ext", ".hidden", ".ext"}, + {"./directory/../~/banana/gold.fish", "gold", ".fish"}, + {"../directory/banana.man", "banana", ".man"}, + {"~/mydir/filename.ext", "filename", ".ext"}, + {"./directory//tmp/filename.ext", "filename", ".ext"}, + } + + for i, d := range data { + file, ext := fileAndExt(filepath.FromSlash(d.input), fpb) + if d.expectedFile != file { + t.Errorf("Test %d failed. Expected filename %q got %q.", i, d.expectedFile, file) + } + if d.expectedExt != ext { + t.Errorf("Test %d failed. Expected extension %q got %q.", i, d.expectedExt, ext) + } + } + +} + +func TestGuessSection(t *testing.T) { + type test struct { + input, expected string + } + + data := []test{ + {"/", ""}, + {"", ""}, + {"/content", ""}, + {"content/", ""}, + {"/content/", ""}, // /content/ is a special case. It will never be the section + {"/blog", ""}, + {"/blog/", "blog"}, + {"blog", ""}, + {"content/blog", ""}, + {"/content/blog/", "blog"}, + {"/content/blog", ""}, // Lack of trailing slash indicates 'blog' is not a directory. + {"content/blog/", "blog"}, + {"/contents/myblog/", "contents"}, + {"/contents/yourblog", "contents"}, + {"/contents/ourblog/", "contents"}, + {"/content/myblog/", "myblog"}, + {"/content/yourblog", ""}, + {"/content/ourblog/", "ourblog"}, + } + + for i, d := range data { + expected := GuessSection(filepath.FromSlash(d.input)) + if d.expected != expected { + t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, expected) + } + } +} + +func TestPathPrep(t *testing.T) { + +} + +func TestPrettifyPath(t *testing.T) { + +} + +func TestExtractRootPaths(t *testing.T) { + tests := []struct { + input []string + expected []string + }{{[]string{filepath.FromSlash("a/b"), filepath.FromSlash("a/b/c/"), "b", + filepath.FromSlash("/c/d"), filepath.FromSlash("d/"), filepath.FromSlash("//e//")}, + []string{"a", "a", "b", "c", "d", "e"}}} + + for _, test := range tests { + output := ExtractRootPaths(test.input) + if !reflect.DeepEqual(output, test.expected) { + t.Errorf("Expected %#v, got %#v\n", test.expected, output) + } + } +} + +func TestFindCWD(t *testing.T) { + type test struct { + expectedDir string + expectedErr error + } + + //cwd, _ := os.Getwd() + data := []test{ + //{cwd, nil}, + // Commenting this out. It doesn't work properly. + // There's a good reason why we don't use os.Getwd(), it doesn't actually work the way we want it to. + // I really don't know a better way to test this function. - SPF 2014.11.04 + } + for i, d := range data { + dir, err := FindCWD() + if d.expectedDir != dir { + t.Errorf("Test %d failed. Expected %q but got %q", i, d.expectedDir, dir) + } + if d.expectedErr != err { + t.Errorf("Test %d failed. Expected %q but got %q", i, d.expectedErr, err) + } + } +} + +func TestSafeWriteToDisk(t *testing.T) { + emptyFile, _ := createZeroSizedFileInTempDir() + defer deleteFileInTempDir(emptyFile) + tmpDir, _ := createEmptyTempDir() + defer deleteTempDir(tmpDir) + + randomString := "This is a random string!" + reader := strings.NewReader(randomString) + + fileExists := fmt.Errorf("%v already exists", emptyFile.Name()) + + type test struct { + filename string + expectedErr error + } + + now := time.Now().Unix() + nowStr := strconv.FormatInt(now, 10) + data := []test{ + {emptyFile.Name(), fileExists}, + {tmpDir + "/" + nowStr, nil}, + } + + for i, d := range data { + e := SafeWriteToDisk(d.filename, reader, new(afero.OsFs)) + if d.expectedErr != nil { + if d.expectedErr.Error() != e.Error() { + t.Errorf("Test %d failed. Expected error %q but got %q", i, d.expectedErr.Error(), e.Error()) + } + } else { + if d.expectedErr != e { + t.Errorf("Test %d failed. Expected %q but got %q", i, d.expectedErr, e) + } + contents, _ := ioutil.ReadFile(d.filename) + if randomString != string(contents) { + t.Errorf("Test %d failed. Expected contents %q but got %q", i, randomString, string(contents)) + } + } + reader.Seek(0, 0) + } +} + +func TestWriteToDisk(t *testing.T) { + emptyFile, _ := createZeroSizedFileInTempDir() + defer deleteFileInTempDir(emptyFile) + tmpDir, _ := createEmptyTempDir() + defer deleteTempDir(tmpDir) + + randomString := "This is a random string!" + reader := strings.NewReader(randomString) + + type test struct { + filename string + expectedErr error + } + + now := time.Now().Unix() + nowStr := strconv.FormatInt(now, 10) + data := []test{ + {emptyFile.Name(), nil}, + {tmpDir + "/" + nowStr, nil}, + } + + for i, d := range data { + e := WriteToDisk(d.filename, reader, new(afero.OsFs)) + if d.expectedErr != e { + t.Errorf("Test %d failed. WriteToDisk Error Expected %q but got %q", i, d.expectedErr, e) + } + contents, e := ioutil.ReadFile(d.filename) + if e != nil { + t.Errorf("Test %d failed. Could not read file %s. Reason: %s\n", i, d.filename, e) + } + if randomString != string(contents) { + t.Errorf("Test %d failed. Expected contents %q but got %q", i, randomString, string(contents)) + } + reader.Seek(0, 0) + } +} + +func TestGetTempDir(t *testing.T) { + dir := os.TempDir() + if FilePathSeparator != dir[len(dir)-1:] { + dir = dir + FilePathSeparator + } + testDir := "hugoTestFolder" + FilePathSeparator + tests := []struct { + input string + expected string + }{ + {"", dir}, + {testDir + " Foo bar ", dir + testDir + " Foo bar " + FilePathSeparator}, + {testDir + "Foo.Bar/foo_Bar-Foo", dir + testDir + "Foo.Bar/foo_Bar-Foo" + FilePathSeparator}, + {testDir + "fOO,bar:foo%bAR", dir + testDir + "fOObarfoo%bAR" + FilePathSeparator}, + {testDir + "fOO,bar:foobAR", dir + testDir + "fOObarfoobAR" + FilePathSeparator}, + {testDir + "FOo/BaR.html", dir + testDir + "FOo/BaR.html" + FilePathSeparator}, + {testDir + "трям/трям", dir + testDir + "трям/трям" + FilePathSeparator}, + {testDir + "은행", dir + testDir + "은행" + FilePathSeparator}, + {testDir + "Банковский кассир", dir + testDir + "Банковский кассир" + FilePathSeparator}, + } + + for _, test := range tests { + output := GetTempDir(test.input, new(afero.MemMapFs)) + if output != test.expected { + t.Errorf("Expected %#v, got %#v\n", test.expected, output) + } + } +} diff --git a/helpers/pathspec.go b/helpers/pathspec.go new file mode 100644 index 000000000..643d05646 --- /dev/null +++ b/helpers/pathspec.go @@ -0,0 +1,119 @@ +// Copyright 2016-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "fmt" + + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/hugofs" +) + +// PathSpec holds methods that decides how paths in URLs and files in Hugo should look like. +type PathSpec struct { + BaseURL + + disablePathToLower bool + removePathAccents bool + uglyURLs bool + canonifyURLs bool + + language *Language + + // pagination path handling + paginatePath string + + theme string + + // Directories + themesDir string + layoutDir string + workingDir string + staticDir string + + // The PathSpec looks up its config settings in both the current language + // and then in the global Viper config. + // Some settings, the settings listed below, does not make sense to be set + // on per-language-basis. We have no good way of protecting against this + // other than a "white-list". See language.go. + defaultContentLanguageInSubdir bool + defaultContentLanguage string + multilingual bool + + // The file systems to use + Fs *hugofs.Fs + + // The config provider to use + Cfg config.Provider +} + +func (p PathSpec) String() string { + return fmt.Sprintf("PathSpec, language %q, prefix %q, multilingual: %T", p.language.Lang, p.getLanguagePrefix(), p.multilingual) +} + +// NewPathSpec creats a new PathSpec from the given filesystems and Language. +func NewPathSpec(fs *hugofs.Fs, cfg config.Provider) (*PathSpec, error) { + + baseURLstr := cfg.GetString("baseURL") + baseURL, err := newBaseURLFromString(baseURLstr) + + if err != nil { + return nil, fmt.Errorf("Failed to create baseURL from %q: %s", baseURLstr, err) + } + + ps := &PathSpec{ + Fs: fs, + Cfg: cfg, + disablePathToLower: cfg.GetBool("disablePathToLower"), + removePathAccents: cfg.GetBool("removePathAccents"), + uglyURLs: cfg.GetBool("uglyURLs"), + canonifyURLs: cfg.GetBool("canonifyURLs"), + multilingual: cfg.GetBool("multilingual"), + defaultContentLanguageInSubdir: cfg.GetBool("defaultContentLanguageInSubdir"), + defaultContentLanguage: cfg.GetString("defaultContentLanguage"), + paginatePath: cfg.GetString("paginatePath"), + BaseURL: baseURL, + themesDir: cfg.GetString("themesDir"), + layoutDir: cfg.GetString("layoutDir"), + workingDir: cfg.GetString("workingDir"), + staticDir: cfg.GetString("staticDir"), + theme: cfg.GetString("theme"), + } + + if language, ok := cfg.(*Language); ok { + ps.language = language + } + + return ps, nil +} + +// PaginatePath returns the configured root path used for paginator pages. +func (p *PathSpec) PaginatePath() string { + return p.paginatePath +} + +// WorkingDir returns the configured workingDir. +func (p *PathSpec) WorkingDir() string { + return p.workingDir +} + +// LayoutDir returns the relative layout dir in the currenct Hugo project. +func (p *PathSpec) LayoutDir() string { + return p.layoutDir +} + +// Theme returns the theme name if set. +func (p *PathSpec) Theme() string { + return p.theme +} diff --git a/helpers/pathspec_test.go b/helpers/pathspec_test.go new file mode 100644 index 000000000..04ec7cac7 --- /dev/null +++ b/helpers/pathspec_test.go @@ -0,0 +1,62 @@ +// Copyright 2016-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "testing" + + "github.com/gohugoio/hugo/hugofs" + + "github.com/spf13/viper" + "github.com/stretchr/testify/require" +) + +func TestNewPathSpecFromConfig(t *testing.T) { + v := viper.New() + l := NewLanguage("no", v) + v.Set("disablePathToLower", true) + v.Set("removePathAccents", true) + v.Set("uglyURLs", true) + v.Set("multilingual", true) + v.Set("defaultContentLanguageInSubdir", true) + v.Set("defaultContentLanguage", "no") + v.Set("canonifyURLs", true) + v.Set("paginatePath", "side") + v.Set("baseURL", "http://base.com") + v.Set("themesDir", "thethemes") + v.Set("layoutDir", "thelayouts") + v.Set("workingDir", "thework") + v.Set("staticDir", "thestatic") + v.Set("theme", "thetheme") + + p, err := NewPathSpec(hugofs.NewMem(v), l) + + require.NoError(t, err) + require.True(t, p.canonifyURLs) + require.True(t, p.defaultContentLanguageInSubdir) + require.True(t, p.disablePathToLower) + require.True(t, p.multilingual) + require.True(t, p.removePathAccents) + require.True(t, p.uglyURLs) + require.Equal(t, "no", p.defaultContentLanguage) + require.Equal(t, "no", p.language.Lang) + require.Equal(t, "side", p.paginatePath) + + require.Equal(t, "http://base.com", p.BaseURL.String()) + require.Equal(t, "thethemes", p.themesDir) + require.Equal(t, "thelayouts", p.layoutDir) + require.Equal(t, "thework", p.workingDir) + require.Equal(t, "thestatic", p.staticDir) + require.Equal(t, "thetheme", p.theme) +} diff --git a/helpers/pygments.go b/helpers/pygments.go new file mode 100644 index 000000000..60f62a88f --- /dev/null +++ b/helpers/pygments.go @@ -0,0 +1,237 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "bytes" + "crypto/sha1" + "fmt" + "io" + "io/ioutil" + "os/exec" + "path/filepath" + "sort" + "strings" + + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/hugofs" + jww "github.com/spf13/jwalterweatherman" +) + +const pygmentsBin = "pygmentize" + +// HasPygments checks to see if Pygments is installed and available +// on the system. +func HasPygments() bool { + if _, err := exec.LookPath(pygmentsBin); err != nil { + return false + } + return true +} + +// Highlight takes some code and returns highlighted code. +func Highlight(cfg config.Provider, code, lang, optsStr string) string { + if !HasPygments() { + jww.WARN.Println("Highlighting requires Pygments to be installed and in the path") + return code + } + + options, err := parsePygmentsOpts(cfg, optsStr) + + if err != nil { + jww.ERROR.Print(err.Error()) + return code + } + + // Try to read from cache first + hash := sha1.New() + io.WriteString(hash, code) + io.WriteString(hash, lang) + io.WriteString(hash, options) + + fs := hugofs.Os + + ignoreCache := cfg.GetBool("ignoreCache") + cacheDir := cfg.GetString("cacheDir") + var cachefile string + + if !ignoreCache && cacheDir != "" { + cachefile = filepath.Join(cacheDir, fmt.Sprintf("pygments-%x", hash.Sum(nil))) + + exists, err := Exists(cachefile, fs) + if err != nil { + jww.ERROR.Print(err.Error()) + return code + } + if exists { + f, err := fs.Open(cachefile) + if err != nil { + jww.ERROR.Print(err.Error()) + return code + } + + s, err := ioutil.ReadAll(f) + if err != nil { + jww.ERROR.Print(err.Error()) + return code + } + + return string(s) + } + } + + // No cache file, render and cache it + var out bytes.Buffer + var stderr bytes.Buffer + + var langOpt string + if lang == "" { + langOpt = "-g" // Try guessing the language + } else { + langOpt = "-l" + lang + } + + cmd := exec.Command(pygmentsBin, langOpt, "-fhtml", "-O", options) + cmd.Stdin = strings.NewReader(code) + cmd.Stdout = &out + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + jww.ERROR.Print(stderr.String()) + return code + } + + str := string(normalizeExternalHelperLineFeeds([]byte(out.String()))) + + // inject code tag into Pygments output + if lang != "" && strings.Contains(str, "<pre>") { + codeTag := fmt.Sprintf(`<pre><code class="language-%s" data-lang="%s">`, lang, lang) + str = strings.Replace(str, "<pre>", codeTag, 1) + str = strings.Replace(str, "</pre>", "</code></pre>", 1) + } + + if !ignoreCache && cachefile != "" { + // Write cache file + if err := WriteToDisk(cachefile, strings.NewReader(str), fs); err != nil { + jww.ERROR.Print(stderr.String()) + } + } + + return str +} + +var pygmentsKeywords = make(map[string]bool) + +func init() { + pygmentsKeywords["encoding"] = true + pygmentsKeywords["outencoding"] = true + pygmentsKeywords["nowrap"] = true + pygmentsKeywords["full"] = true + pygmentsKeywords["title"] = true + pygmentsKeywords["style"] = true + pygmentsKeywords["noclasses"] = true + pygmentsKeywords["classprefix"] = true + pygmentsKeywords["cssclass"] = true + pygmentsKeywords["cssstyles"] = true + pygmentsKeywords["prestyles"] = true + pygmentsKeywords["linenos"] = true + pygmentsKeywords["hl_lines"] = true + pygmentsKeywords["linenostart"] = true + pygmentsKeywords["linenostep"] = true + pygmentsKeywords["linenospecial"] = true + pygmentsKeywords["nobackground"] = true + pygmentsKeywords["lineseparator"] = true + pygmentsKeywords["lineanchors"] = true + pygmentsKeywords["linespans"] = true + pygmentsKeywords["anchorlinenos"] = true + pygmentsKeywords["startinline"] = true +} + +func parseOptions(options map[string]string, in string) error { + in = strings.Trim(in, " ") + + if in == "" { + return nil + } + + for _, v := range strings.Split(in, ",") { + keyVal := strings.Split(v, "=") + key := strings.ToLower(strings.Trim(keyVal[0], " ")) + if len(keyVal) != 2 || !pygmentsKeywords[key] { + return fmt.Errorf("invalid Pygments option: %s", key) + } + options[key] = keyVal[1] + } + + return nil +} + +func createOptionsString(options map[string]string) string { + var keys []string + for k := range options { + keys = append(keys, k) + } + sort.Strings(keys) + + var optionsStr string + for i, k := range keys { + optionsStr += fmt.Sprintf("%s=%s", k, options[k]) + if i < len(options)-1 { + optionsStr += "," + } + } + + return optionsStr +} + +func parseDefaultPygmentsOpts(cfg config.Provider) (map[string]string, error) { + options := make(map[string]string) + err := parseOptions(options, cfg.GetString("pygmentsOptions")) + if err != nil { + return nil, err + } + + if cfg.IsSet("pygmentsStyle") { + options["style"] = cfg.GetString("pygmentsStyle") + } + + if cfg.IsSet("pygmentsUseClasses") { + if cfg.GetBool("pygmentsUseClasses") { + options["noclasses"] = "false" + } else { + options["noclasses"] = "true" + } + + } + + if _, ok := options["encoding"]; !ok { + options["encoding"] = "utf8" + } + + return options, nil +} + +func parsePygmentsOpts(cfg config.Provider, in string) (string, error) { + options, err := parseDefaultPygmentsOpts(cfg) + if err != nil { + return "", err + } + + err = parseOptions(options, in) + if err != nil { + return "", err + } + + return createOptionsString(options), nil +} diff --git a/helpers/pygments_test.go b/helpers/pygments_test.go new file mode 100644 index 000000000..1fce17859 --- /dev/null +++ b/helpers/pygments_test.go @@ -0,0 +1,95 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "testing" + + "github.com/spf13/viper" +) + +func TestParsePygmentsArgs(t *testing.T) { + for i, this := range []struct { + in string + pygmentsStyle string + pygmentsUseClasses bool + expect1 interface{} + }{ + {"", "foo", true, "encoding=utf8,noclasses=false,style=foo"}, + {"style=boo,noclasses=true", "foo", true, "encoding=utf8,noclasses=true,style=boo"}, + {"Style=boo, noClasses=true", "foo", true, "encoding=utf8,noclasses=true,style=boo"}, + {"noclasses=true", "foo", true, "encoding=utf8,noclasses=true,style=foo"}, + {"style=boo", "foo", true, "encoding=utf8,noclasses=false,style=boo"}, + {"boo=invalid", "foo", false, false}, + {"style", "foo", false, false}, + } { + + v := viper.New() + v.Set("pygmentsStyle", this.pygmentsStyle) + v.Set("pygmentsUseClasses", this.pygmentsUseClasses) + + result1, err := parsePygmentsOpts(v, this.in) + if b, ok := this.expect1.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] parsePygmentArgs didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] parsePygmentArgs failed: %s", i, err) + continue + } + if result1 != this.expect1 { + t.Errorf("[%d] parsePygmentArgs got %v but expected %v", i, result1, this.expect1) + } + + } + } +} + +func TestParseDefaultPygmentsArgs(t *testing.T) { + expect := "encoding=utf8,noclasses=false,style=foo" + + for i, this := range []struct { + in string + pygmentsStyle interface{} + pygmentsUseClasses interface{} + pygmentsOptions string + }{ + {"", "foo", true, "style=override,noclasses=override"}, + {"", nil, nil, "style=foo,noclasses=false"}, + {"style=foo,noclasses=false", nil, nil, "style=override,noclasses=override"}, + {"style=foo,noclasses=false", "override", false, "style=override,noclasses=override"}, + } { + v := viper.New() + + v.Set("pygmentsOptions", this.pygmentsOptions) + + if s, ok := this.pygmentsStyle.(string); ok { + v.Set("pygmentsStyle", s) + } + + if b, ok := this.pygmentsUseClasses.(bool); ok { + v.Set("pygmentsUseClasses", b) + } + + result, err := parsePygmentsOpts(v, this.in) + if err != nil { + t.Errorf("[%d] parsePygmentArgs failed: %s", i, err) + continue + } + if result != expect { + t.Errorf("[%d] parsePygmentArgs got %v but expected %v", i, result, expect) + } + } +} diff --git a/helpers/testhelpers_test.go b/helpers/testhelpers_test.go new file mode 100644 index 000000000..86f141146 --- /dev/null +++ b/helpers/testhelpers_test.go @@ -0,0 +1,38 @@ +package helpers + +import ( + "github.com/spf13/viper" + + "github.com/gohugoio/hugo/hugofs" +) + +func newTestPathSpec(fs *hugofs.Fs, v *viper.Viper) *PathSpec { + l := NewDefaultLanguage(v) + ps, _ := NewPathSpec(fs, l) + return ps +} + +func newTestDefaultPathSpec(configKeyValues ...interface{}) *PathSpec { + v := viper.New() + fs := hugofs.NewMem(v) + cfg := newTestCfg(fs) + + for i := 0; i < len(configKeyValues); i += 2 { + cfg.Set(configKeyValues[i].(string), configKeyValues[i+1]) + } + return newTestPathSpec(fs, cfg) +} + +func newTestCfg(fs *hugofs.Fs) *viper.Viper { + v := viper.New() + + v.SetFs(fs.Source) + + return v + +} + +func newTestContentSpec() *ContentSpec { + v := viper.New() + return NewContentSpec(v) +} diff --git a/helpers/url.go b/helpers/url.go new file mode 100644 index 000000000..9c1a643cc --- /dev/null +++ b/helpers/url.go @@ -0,0 +1,392 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "fmt" + "net/url" + "path" + "path/filepath" + "strings" + + "github.com/PuerkitoBio/purell" +) + +type pathBridge struct { +} + +func (pathBridge) Base(in string) string { + return path.Base(in) +} + +func (pathBridge) Clean(in string) string { + return path.Clean(in) +} + +func (pathBridge) Dir(in string) string { + return path.Dir(in) +} + +func (pathBridge) Ext(in string) string { + return path.Ext(in) +} + +func (pathBridge) Join(elem ...string) string { + return path.Join(elem...) +} + +func (pathBridge) Separator() string { + return "/" +} + +var pb pathBridge + +func sanitizeURLWithFlags(in string, f purell.NormalizationFlags) string { + s, err := purell.NormalizeURLString(in, f) + if err != nil { + return in + } + + // Temporary workaround for the bug fix and resulting + // behavioral change in purell.NormalizeURLString(): + // a leading '/' was inadvertently added to relative links, + // but no longer, see #878. + // + // I think the real solution is to allow Hugo to + // make relative URL with relative path, + // e.g. "../../post/hello-again/", as wished by users + // in issues #157, #622, etc., without forcing + // relative URLs to begin with '/'. + // Once the fixes are in, let's remove this kludge + // and restore SanitizeURL() to the way it was. + // -- @anthonyfok, 2015-02-16 + // + // Begin temporary kludge + u, err := url.Parse(s) + if err != nil { + panic(err) + } + if len(u.Path) > 0 && !strings.HasPrefix(u.Path, "/") { + u.Path = "/" + u.Path + } + return u.String() + // End temporary kludge + + //return s + +} + +// SanitizeURL sanitizes the input URL string. +func SanitizeURL(in string) string { + return sanitizeURLWithFlags(in, purell.FlagsSafe|purell.FlagRemoveTrailingSlash|purell.FlagRemoveDotSegments|purell.FlagRemoveDuplicateSlashes|purell.FlagRemoveUnnecessaryHostDots|purell.FlagRemoveEmptyPortSeparator) +} + +// SanitizeURLKeepTrailingSlash is the same as SanitizeURL, but will keep any trailing slash. +func SanitizeURLKeepTrailingSlash(in string) string { + return sanitizeURLWithFlags(in, purell.FlagsSafe|purell.FlagRemoveDotSegments|purell.FlagRemoveDuplicateSlashes|purell.FlagRemoveUnnecessaryHostDots|purell.FlagRemoveEmptyPortSeparator) +} + +// URLize is similar to MakePath, but with Unicode handling +// Example: +// uri: Vim (text editor) +// urlize: vim-text-editor +func (p *PathSpec) URLize(uri string) string { + return p.URLEscape(p.MakePathSanitized(uri)) + +} + +// URLizeFilename creates an URL from a filename by esacaping unicode letters +// and turn any filepath separator into forward slashes. +func (p *PathSpec) URLizeFilename(filename string) string { + return p.URLEscape(filepath.ToSlash(filename)) +} + +// URLEscape escapes unicode letters. +func (p *PathSpec) URLEscape(uri string) string { + // escape unicode letters + parsedURI, err := url.Parse(uri) + if err != nil { + // if net/url can not parse URL it means Sanitize works incorrectly + panic(err) + } + x := parsedURI.String() + return x +} + +// MakePermalink combines base URL with content path to create full URL paths. +// Example +// base: http://spf13.com/ +// path: post/how-i-blog +// result: http://spf13.com/post/how-i-blog +func MakePermalink(host, plink string) *url.URL { + + base, err := url.Parse(host) + if err != nil { + panic(err) + } + + p, err := url.Parse(plink) + if err != nil { + panic(err) + } + + if p.Host != "" { + panic(fmt.Errorf("Can't make permalink from absolute link %q", plink)) + } + + base.Path = path.Join(base.Path, p.Path) + + // path.Join will strip off the last /, so put it back if it was there. + hadTrailingSlash := (plink == "" && strings.HasSuffix(host, "/")) || strings.HasSuffix(p.Path, "/") + if hadTrailingSlash && !strings.HasSuffix(base.Path, "/") { + base.Path = base.Path + "/" + } + + return base +} + +// AbsURL creates an absolute URL from the relative path given and the BaseURL set in config. +func (p *PathSpec) AbsURL(in string, addLanguage bool) string { + url, err := url.Parse(in) + if err != nil { + return in + } + + if url.IsAbs() || strings.HasPrefix(in, "//") { + return in + } + + var baseURL string + if strings.HasPrefix(in, "/") { + u := p.BaseURL.URL() + u.Path = "" + baseURL = u.String() + } else { + baseURL = p.BaseURL.String() + } + + if addLanguage { + prefix := p.getLanguagePrefix() + if prefix != "" { + hasPrefix := false + // avoid adding language prefix if already present + if strings.HasPrefix(in, "/") { + hasPrefix = strings.HasPrefix(in[1:], prefix) + } else { + hasPrefix = strings.HasPrefix(in, prefix) + } + + if !hasPrefix { + addSlash := in == "" || strings.HasSuffix(in, "/") + in = path.Join(prefix, in) + + if addSlash { + in += "/" + } + } + } + } + return MakePermalink(baseURL, in).String() +} + +func (p *PathSpec) getLanguagePrefix() string { + if !p.multilingual { + return "" + } + + defaultLang := p.defaultContentLanguage + defaultInSubDir := p.defaultContentLanguageInSubdir + + currentLang := p.language.Lang + if currentLang == "" || (currentLang == defaultLang && !defaultInSubDir) { + return "" + } + return currentLang +} + +// IsAbsURL determines whether the given path points to an absolute URL. +func IsAbsURL(path string) bool { + url, err := url.Parse(path) + if err != nil { + return false + } + + return url.IsAbs() || strings.HasPrefix(path, "//") +} + +// RelURL creates a URL relative to the BaseURL root. +// Note: The result URL will not include the context root if canonifyURLs is enabled. +func (p *PathSpec) RelURL(in string, addLanguage bool) string { + baseURL := p.BaseURL.String() + canonifyURLs := p.canonifyURLs + if (!strings.HasPrefix(in, baseURL) && strings.HasPrefix(in, "http")) || strings.HasPrefix(in, "//") { + return in + } + + u := in + + if strings.HasPrefix(in, baseURL) { + u = strings.TrimPrefix(u, baseURL) + } + + if addLanguage { + prefix := p.getLanguagePrefix() + if prefix != "" { + hasPrefix := false + // avoid adding language prefix if already present + if strings.HasPrefix(in, "/") { + hasPrefix = strings.HasPrefix(in[1:], prefix) + } else { + hasPrefix = strings.HasPrefix(in, prefix) + } + + if !hasPrefix { + hadSlash := strings.HasSuffix(u, "/") + + u = path.Join(prefix, u) + + if hadSlash { + u += "/" + } + } + } + } + + if !canonifyURLs { + u = AddContextRoot(baseURL, u) + } + + if in == "" && !strings.HasSuffix(u, "/") && strings.HasSuffix(baseURL, "/") { + u += "/" + } + + if !strings.HasPrefix(u, "/") { + u = "/" + u + } + + return u +} + +// AddContextRoot adds the context root to an URL if it's not already set. +// For relative URL entries on sites with a base url with a context root set (i.e. http://example.com/mysite), +// relative URLs must not include the context root if canonifyURLs is enabled. But if it's disabled, it must be set. +func AddContextRoot(baseURL, relativePath string) string { + + url, err := url.Parse(baseURL) + if err != nil { + panic(err) + } + + newPath := path.Join(url.Path, relativePath) + + // path strips traling slash, ignore root path. + if newPath != "/" && strings.HasSuffix(relativePath, "/") { + newPath += "/" + } + return newPath +} + +// PrependBasePath prepends any baseURL sub-folder to the given resource +// if canonifyURLs is disabled. +// If canonifyURLs is set, we will globally prepend the absURL with any sub-folder, +// so avoid doing anything here to avoid getting double paths. +func (p *PathSpec) PrependBasePath(rel string) string { + basePath := p.BaseURL.url.Path + if !p.canonifyURLs && basePath != "" && basePath != "/" { + rel = filepath.ToSlash(rel) + // Need to prepend any path from the baseURL + hadSlash := strings.HasSuffix(rel, "/") + rel = path.Join(basePath, rel) + if hadSlash { + rel += "/" + } + } + return rel +} + +// URLizeAndPrep applies misc sanitation to the given URL to get it in line +// with the Hugo standard. +func (p *PathSpec) URLizeAndPrep(in string) string { + return p.URLPrep(p.URLize(in)) +} + +// URLPrep applies misc sanitation to the given URL. +func (p *PathSpec) URLPrep(in string) string { + if p.uglyURLs { + return Uglify(SanitizeURL(in)) + } + pretty := PrettifyURL(SanitizeURL(in)) + if path.Ext(pretty) == ".xml" { + return pretty + } + url, err := purell.NormalizeURLString(pretty, purell.FlagAddTrailingSlash) + if err != nil { + return pretty + } + return url +} + +// PrettifyURL takes a URL string and returns a semantic, clean URL. +func PrettifyURL(in string) string { + x := PrettifyURLPath(in) + + if path.Base(x) == "index.html" { + return path.Dir(x) + } + + if in == "" { + return "/" + } + + return x +} + +// PrettifyURLPath takes a URL path to a content and converts it +// to enable pretty URLs. +// /section/name.html becomes /section/name/index.html +// /section/name/ becomes /section/name/index.html +// /section/name/index.html becomes /section/name/index.html +func PrettifyURLPath(in string) string { + return prettifyPath(in, pb) +} + +// Uglify does the opposite of PrettifyURLPath(). +// /section/name/index.html becomes /section/name.html +// /section/name/ becomes /section/name.html +// /section/name.html becomes /section/name.html +func Uglify(in string) string { + if path.Ext(in) == "" { + if len(in) < 2 { + return "/" + } + // /section/name/ -> /section/name.html + return path.Clean(in) + ".html" + } + + name, ext := fileAndExt(in, pb) + if name == "index" { + // /section/name/index.html -> /section/name.html + d := path.Dir(in) + if len(d) > 1 { + return d + ext + } + return in + } + // /.xml -> /index.xml + if name == "" { + return path.Dir(in) + "index" + ext + } + // /section/name.html -> /section/name.html + return path.Clean(in) +} diff --git a/helpers/url_test.go b/helpers/url_test.go new file mode 100644 index 000000000..9572547c7 --- /dev/null +++ b/helpers/url_test.go @@ -0,0 +1,321 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "fmt" + "strings" + "testing" + + "github.com/gohugoio/hugo/hugofs" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestURLize(t *testing.T) { + + v := viper.New() + l := NewDefaultLanguage(v) + p, _ := NewPathSpec(hugofs.NewMem(v), l) + + tests := []struct { + input string + expected string + }{ + {" foo bar ", "foo-bar"}, + {"foo.bar/foo_bar-foo", "foo.bar/foo_bar-foo"}, + {"foo,bar:foobar", "foobarfoobar"}, + {"foo/bar.html", "foo/bar.html"}, + {"трям/трям", "%D1%82%D1%80%D1%8F%D0%BC/%D1%82%D1%80%D1%8F%D0%BC"}, + {"100%-google", "100-google"}, + } + + for _, test := range tests { + output := p.URLize(test.input) + if output != test.expected { + t.Errorf("Expected %#v, got %#v\n", test.expected, output) + } + } +} + +func TestAbsURL(t *testing.T) { + for _, defaultInSubDir := range []bool{true, false} { + for _, addLanguage := range []bool{true, false} { + for _, m := range []bool{true, false} { + for _, l := range []string{"en", "fr"} { + doTestAbsURL(t, defaultInSubDir, addLanguage, m, l) + } + } + } + } +} + +func doTestAbsURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool, lang string) { + v := viper.New() + v.Set("multilingual", multilingual) + v.Set("defaultContentLanguage", "en") + v.Set("defaultContentLanguageInSubdir", defaultInSubDir) + + tests := []struct { + input string + baseURL string + expected string + }{ + {"/test/foo", "http://base/", "http://base/MULTItest/foo"}, + {"/" + lang + "/test/foo", "http://base/", "http://base/" + lang + "/test/foo"}, + {"", "http://base/ace/", "http://base/ace/MULTI"}, + {"/test/2/foo/", "http://base", "http://base/MULTItest/2/foo/"}, + {"http://abs", "http://base/", "http://abs"}, + {"schema://abs", "http://base/", "schema://abs"}, + {"//schemaless", "http://base/", "//schemaless"}, + {"test/2/foo/", "http://base/path", "http://base/path/MULTItest/2/foo/"}, + {lang + "/test/2/foo/", "http://base/path", "http://base/path/" + lang + "/test/2/foo/"}, + {"/test/2/foo/", "http://base/path", "http://base/MULTItest/2/foo/"}, + {"http//foo", "http://base/path", "http://base/path/MULTIhttp/foo"}, + } + + for _, test := range tests { + v.Set("baseURL", test.baseURL) + l := NewLanguage(lang, v) + p, _ := NewPathSpec(hugofs.NewMem(v), l) + + output := p.AbsURL(test.input, addLanguage) + expected := test.expected + if multilingual && addLanguage { + if !defaultInSubDir && lang == "en" { + expected = strings.Replace(expected, "MULTI", "", 1) + } else { + expected = strings.Replace(expected, "MULTI", lang+"/", 1) + } + + } else { + expected = strings.Replace(expected, "MULTI", "", 1) + } + if output != expected { + t.Fatalf("Expected %#v, got %#v\n", expected, output) + } + } +} + +func TestIsAbsURL(t *testing.T) { + for i, this := range []struct { + a string + b bool + }{ + {"http://gohugo.io", true}, + {"https://gohugo.io", true}, + {"//gohugo.io", true}, + {"http//gohugo.io", false}, + {"/content", false}, + {"content", false}, + } { + require.True(t, IsAbsURL(this.a) == this.b, fmt.Sprintf("Test %d", i)) + } +} + +func TestRelURL(t *testing.T) { + for _, defaultInSubDir := range []bool{true, false} { + for _, addLanguage := range []bool{true, false} { + for _, m := range []bool{true, false} { + for _, l := range []string{"en", "fr"} { + doTestRelURL(t, defaultInSubDir, addLanguage, m, l) + } + } + } + } +} + +func doTestRelURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool, lang string) { + v := viper.New() + v.Set("multilingual", multilingual) + v.Set("defaultContentLanguage", "en") + v.Set("defaultContentLanguageInSubdir", defaultInSubDir) + + tests := []struct { + input string + baseURL string + canonify bool + expected string + }{ + {"/test/foo", "http://base/", false, "MULTI/test/foo"}, + {"/" + lang + "/test/foo", "http://base/", false, "/" + lang + "/test/foo"}, + {lang + "/test/foo", "http://base/", false, "/" + lang + "/test/foo"}, + {"test.css", "http://base/sub", false, "/subMULTI/test.css"}, + {"test.css", "http://base/sub", true, "MULTI/test.css"}, + {"/test/", "http://base/", false, "MULTI/test/"}, + {"/test/", "http://base/sub/", false, "/subMULTI/test/"}, + {"/test/", "http://base/sub/", true, "MULTI/test/"}, + {"", "http://base/ace/", false, "/aceMULTI/"}, + {"", "http://base/ace", false, "/aceMULTI"}, + {"http://abs", "http://base/", false, "http://abs"}, + {"//schemaless", "http://base/", false, "//schemaless"}, + } + + for i, test := range tests { + v.Set("baseURL", test.baseURL) + v.Set("canonifyURLs", test.canonify) + l := NewLanguage(lang, v) + p, _ := NewPathSpec(hugofs.NewMem(v), l) + + output := p.RelURL(test.input, addLanguage) + + expected := test.expected + if multilingual && addLanguage { + if !defaultInSubDir && lang == "en" { + expected = strings.Replace(expected, "MULTI", "", 1) + } else { + expected = strings.Replace(expected, "MULTI", "/"+lang, 1) + } + } else { + expected = strings.Replace(expected, "MULTI", "", 1) + } + + if output != expected { + t.Errorf("[%d][%t] Expected %#v, got %#v\n", i, test.canonify, expected, output) + } + } +} + +func TestSanitizeURL(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"http://foo.bar/", "http://foo.bar"}, + {"http://foo.bar", "http://foo.bar"}, // issue #1105 + {"http://foo.bar/zoo/", "http://foo.bar/zoo"}, // issue #931 + } + + for i, test := range tests { + o1 := SanitizeURL(test.input) + o2 := SanitizeURLKeepTrailingSlash(test.input) + + expected2 := test.expected + + if strings.HasSuffix(test.input, "/") && !strings.HasSuffix(expected2, "/") { + expected2 += "/" + } + + if o1 != test.expected { + t.Errorf("[%d] 1: Expected %#v, got %#v\n", i, test.expected, o1) + } + if o2 != expected2 { + t.Errorf("[%d] 2: Expected %#v, got %#v\n", i, expected2, o2) + } + } +} + +func TestMakePermalink(t *testing.T) { + type test struct { + host, link, output string + } + + data := []test{ + {"http://abc.com/foo", "post/bar", "http://abc.com/foo/post/bar"}, + {"http://abc.com/foo/", "post/bar", "http://abc.com/foo/post/bar"}, + {"http://abc.com", "post/bar", "http://abc.com/post/bar"}, + {"http://abc.com", "bar", "http://abc.com/bar"}, + {"http://abc.com/foo/bar", "post/bar", "http://abc.com/foo/bar/post/bar"}, + {"http://abc.com/foo/bar", "post/bar/", "http://abc.com/foo/bar/post/bar/"}, + } + + for i, d := range data { + output := MakePermalink(d.host, d.link).String() + if d.output != output { + t.Errorf("Test #%d failed. Expected %q got %q", i, d.output, output) + } + } +} + +func TestURLPrep(t *testing.T) { + type test struct { + ugly bool + input string + output string + } + + data := []test{ + {false, "/section/name.html", "/section/name/"}, + {true, "/section/name/index.html", "/section/name.html"}, + } + + for i, d := range data { + v := viper.New() + v.Set("uglyURLs", d.ugly) + l := NewDefaultLanguage(v) + p, _ := NewPathSpec(hugofs.NewMem(v), l) + + output := p.URLPrep(d.input) + if d.output != output { + t.Errorf("Test #%d failed. Expected %q got %q", i, d.output, output) + } + } + +} + +func TestAddContextRoot(t *testing.T) { + tests := []struct { + baseURL string + url string + expected string + }{ + {"http://example.com/sub/", "/foo", "/sub/foo"}, + {"http://example.com/sub/", "/foo/index.html", "/sub/foo/index.html"}, + {"http://example.com/sub1/sub2", "/foo", "/sub1/sub2/foo"}, + {"http://example.com", "/foo", "/foo"}, + // cannot guess that the context root is already added int the example below + {"http://example.com/sub/", "/sub/foo", "/sub/sub/foo"}, + {"http://example.com/тря", "/трям/", "/тря/трям/"}, + {"http://example.com", "/", "/"}, + {"http://example.com/bar", "//", "/bar/"}, + } + + for _, test := range tests { + output := AddContextRoot(test.baseURL, test.url) + if output != test.expected { + t.Errorf("Expected %#v, got %#v\n", test.expected, output) + } + } +} + +func TestPretty(t *testing.T) { + assert.Equal(t, PrettifyURLPath("/section/name.html"), "/section/name/index.html") + assert.Equal(t, PrettifyURLPath("/section/sub/name.html"), "/section/sub/name/index.html") + assert.Equal(t, PrettifyURLPath("/section/name/"), "/section/name/index.html") + assert.Equal(t, PrettifyURLPath("/section/name/index.html"), "/section/name/index.html") + assert.Equal(t, PrettifyURLPath("/index.html"), "/index.html") + assert.Equal(t, PrettifyURLPath("/name.xml"), "/name/index.xml") + assert.Equal(t, PrettifyURLPath("/"), "/") + assert.Equal(t, PrettifyURLPath(""), "/") + assert.Equal(t, PrettifyURL("/section/name.html"), "/section/name") + assert.Equal(t, PrettifyURL("/section/sub/name.html"), "/section/sub/name") + assert.Equal(t, PrettifyURL("/section/name/"), "/section/name") + assert.Equal(t, PrettifyURL("/section/name/index.html"), "/section/name") + assert.Equal(t, PrettifyURL("/index.html"), "/") + assert.Equal(t, PrettifyURL("/name.xml"), "/name/index.xml") + assert.Equal(t, PrettifyURL("/"), "/") + assert.Equal(t, PrettifyURL(""), "/") +} + +func TestUgly(t *testing.T) { + assert.Equal(t, Uglify("/section/name.html"), "/section/name.html") + assert.Equal(t, Uglify("/section/sub/name.html"), "/section/sub/name.html") + assert.Equal(t, Uglify("/section/name/"), "/section/name.html") + assert.Equal(t, Uglify("/section/name/index.html"), "/section/name.html") + assert.Equal(t, Uglify("/index.html"), "/index.html") + assert.Equal(t, Uglify("/name.xml"), "/name.xml") + assert.Equal(t, Uglify("/"), "/") + assert.Equal(t, Uglify(""), "/") +} diff --git a/hugofs/fs.go b/hugofs/fs.go new file mode 100644 index 000000000..71c0dade0 --- /dev/null +++ b/hugofs/fs.go @@ -0,0 +1,79 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package hugofs provides the file systems used by Hugo. +package hugofs + +import ( + "github.com/gohugoio/hugo/config" + "github.com/spf13/afero" +) + +// Os points to an Os Afero file system. +var Os = &afero.OsFs{} + +type Fs struct { + // Source is Hugo's source file system. + Source afero.Fs + + // Destination is Hugo's destionation file system. + Destination afero.Fs + + // Os is an OS file system. + Os afero.Fs + + // WorkingDir is a read-only file system + // restricted to the project working dir. + WorkingDir *afero.BasePathFs +} + +// NewDefault creates a new Fs with the OS file system +// as source and destination file systems. +func NewDefault(cfg config.Provider) *Fs { + fs := &afero.OsFs{} + return newFs(fs, cfg) +} + +// NewMem creates a new Fs with the MemMapFs +// as source and destination file systems. +// Useful for testing. +func NewMem(cfg config.Provider) *Fs { + fs := &afero.MemMapFs{} + return newFs(fs, cfg) +} + +// NewFrom creates a new Fs based on the provided Afero Fs +// as source and destination file systems. +// Useful for testing. +func NewFrom(fs afero.Fs, cfg config.Provider) *Fs { + return newFs(fs, cfg) +} + +func newFs(base afero.Fs, cfg config.Provider) *Fs { + return &Fs{ + Source: base, + Destination: base, + Os: &afero.OsFs{}, + WorkingDir: getWorkingDirFs(base, cfg), + } +} + +func getWorkingDirFs(base afero.Fs, cfg config.Provider) *afero.BasePathFs { + workingDir := cfg.GetString("workingDir") + + if workingDir != "" { + return afero.NewBasePathFs(afero.NewReadOnlyFs(base), workingDir).(*afero.BasePathFs) + } + + return nil +} diff --git a/hugofs/fs_test.go b/hugofs/fs_test.go new file mode 100644 index 000000000..95900e6a2 --- /dev/null +++ b/hugofs/fs_test.go @@ -0,0 +1,60 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugofs + +import ( + "testing" + + "github.com/spf13/afero" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func TestNewDefault(t *testing.T) { + v := viper.New() + f := NewDefault(v) + + assert.NotNil(t, f.Source) + assert.IsType(t, new(afero.OsFs), f.Source) + assert.NotNil(t, f.Destination) + assert.IsType(t, new(afero.OsFs), f.Destination) + assert.NotNil(t, f.Os) + assert.IsType(t, new(afero.OsFs), f.Os) + assert.Nil(t, f.WorkingDir) + + assert.IsType(t, new(afero.OsFs), Os) +} + +func TestNewMem(t *testing.T) { + v := viper.New() + f := NewMem(v) + + assert.NotNil(t, f.Source) + assert.IsType(t, new(afero.MemMapFs), f.Source) + assert.NotNil(t, f.Destination) + assert.IsType(t, new(afero.MemMapFs), f.Destination) + assert.IsType(t, new(afero.OsFs), f.Os) + assert.Nil(t, f.WorkingDir) +} + +func TestWorkingDir(t *testing.T) { + v := viper.New() + + v.Set("workingDir", "/a/b/") + + f := NewMem(v) + + assert.NotNil(t, f.WorkingDir) + assert.IsType(t, new(afero.BasePathFs), f.WorkingDir) +} diff --git a/hugolib/404_test.go b/hugolib/404_test.go new file mode 100644 index 000000000..bbaed61d7 --- /dev/null +++ b/hugolib/404_test.go @@ -0,0 +1,43 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "path/filepath" + + "testing" + + "github.com/gohugoio/hugo/deps" +) + +func Test404(t *testing.T) { + t.Parallel() + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + cfg.Set("baseURL", "http://auth/bub/") + + writeSource(t, fs, filepath.Join("layouts", "404.html"), "<html><body>Not Found!</body></html>") + writeSource(t, fs, filepath.Join("content", "page.md"), "A page") + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + // Note: We currently have only 1 404 page. One might think that we should have + // multiple, to follow the Custom Output scheme, but I don't see how that wold work + // right now. + th.assertFileContent("public/404.html", "Not Found") + +} diff --git a/hugolib/alias.go b/hugolib/alias.go new file mode 100644 index 000000000..a3fe5c24a --- /dev/null +++ b/hugolib/alias.go @@ -0,0 +1,183 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "bytes" + "fmt" + "html/template" + "io" + "path/filepath" + "runtime" + "strings" + + "github.com/gohugoio/hugo/tpl" + + jww "github.com/spf13/jwalterweatherman" + + "github.com/gohugoio/hugo/helpers" +) + +const ( + alias = "<!DOCTYPE html><html><head><title>{{ .Permalink }}</title><link rel=\"canonical\" href=\"{{ .Permalink }}\"/><meta name=\"robots\" content=\"noindex\"><meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" /><meta http-equiv=\"refresh\" content=\"0; url={{ .Permalink }}\" /></head></html>" + aliasXHtml = "<!DOCTYPE html><html xmlns=\"http://www.w3.org/1999/xhtml\"><head><title>{{ .Permalink }}</title><link rel=\"canonical\" href=\"{{ .Permalink }}\"/><meta name=\"robots\" content=\"noindex\"><meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" /><meta http-equiv=\"refresh\" content=\"0; url={{ .Permalink }}\" /></head></html>" +) + +var defaultAliasTemplates *template.Template + +func init() { + //TODO(bep) consolidate + defaultAliasTemplates = template.New("") + template.Must(defaultAliasTemplates.New("alias").Parse(alias)) + template.Must(defaultAliasTemplates.New("alias-xhtml").Parse(aliasXHtml)) +} + +type aliasHandler struct { + t tpl.TemplateFinder + log *jww.Notepad + allowRoot bool +} + +func newAliasHandler(t tpl.TemplateFinder, l *jww.Notepad, allowRoot bool) aliasHandler { + return aliasHandler{t, l, allowRoot} +} + +func (a aliasHandler) renderAlias(isXHTML bool, permalink string, page *Page) (io.Reader, error) { + t := "alias" + if isXHTML { + t = "alias-xhtml" + } + + var templ *tpl.TemplateAdapter + + if a.t != nil { + templ = a.t.Lookup("alias.html") + } + + if templ == nil { + def := defaultAliasTemplates.Lookup(t) + if def != nil { + templ = &tpl.TemplateAdapter{Template: def} + } + + } + data := struct { + Permalink string + Page *Page + }{ + permalink, + page, + } + + buffer := new(bytes.Buffer) + err := templ.Execute(buffer, data) + if err != nil { + return nil, err + } + return buffer, nil +} + +func (s *Site) writeDestAlias(path, permalink string, p *Page) (err error) { + return s.publishDestAlias(false, path, permalink, p) +} + +func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, p *Page) (err error) { + handler := newAliasHandler(s.Tmpl, s.Log, allowRoot) + + isXHTML := strings.HasSuffix(path, ".xhtml") + + s.Log.DEBUG.Println("creating alias:", path, "redirecting to", permalink) + + targetPath, err := handler.targetPathAlias(path) + if err != nil { + return err + } + + aliasContent, err := handler.renderAlias(isXHTML, permalink, p) + if err != nil { + return err + } + + return s.publish(targetPath, aliasContent) + +} + +func (a aliasHandler) targetPathAlias(src string) (string, error) { + originalAlias := src + if len(src) <= 0 { + return "", fmt.Errorf("Alias \"\" is an empty string") + } + + alias := filepath.Clean(src) + components := strings.Split(alias, helpers.FilePathSeparator) + + if !a.allowRoot && alias == helpers.FilePathSeparator { + return "", fmt.Errorf("Alias \"%s\" resolves to website root directory", originalAlias) + } + + // Validate against directory traversal + if components[0] == ".." { + return "", fmt.Errorf("Alias \"%s\" traverses outside the website root directory", originalAlias) + } + + // Handle Windows file and directory naming restrictions + // See "Naming Files, Paths, and Namespaces" on MSDN + // https://msdn.microsoft.com/en-us/library/aa365247%28v=VS.85%29.aspx?f=255&MSPPError=-2147217396 + msgs := []string{} + reservedNames := []string{"CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"} + + if strings.ContainsAny(alias, ":*?\"<>|") { + msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains invalid characters on Windows: : * ? \" < > |", originalAlias)) + } + for _, ch := range alias { + if ch < ' ' { + msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains ASCII control code (0x00 to 0x1F), invalid on Windows: : * ? \" < > |", originalAlias)) + continue + } + } + for _, comp := range components { + if strings.HasSuffix(comp, " ") || strings.HasSuffix(comp, ".") { + msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains component with a trailing space or period, problematic on Windows", originalAlias)) + } + for _, r := range reservedNames { + if comp == r { + msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains component with reserved name \"%s\" on Windows", originalAlias, r)) + } + } + } + if len(msgs) > 0 { + if runtime.GOOS == "windows" { + for _, m := range msgs { + a.log.ERROR.Println(m) + } + return "", fmt.Errorf("Cannot create \"%s\": Windows filename restriction", originalAlias) + } + for _, m := range msgs { + a.log.WARN.Println(m) + } + } + + // Add the final touch + alias = strings.TrimPrefix(alias, helpers.FilePathSeparator) + if strings.HasSuffix(alias, helpers.FilePathSeparator) { + alias = alias + "index.html" + } else if !strings.HasSuffix(alias, ".html") { + alias = alias + helpers.FilePathSeparator + "index.html" + } + if originalAlias != alias { + a.log.INFO.Printf("Alias \"%s\" translated to \"%s\"\n", originalAlias, alias) + } + + return alias, nil +} diff --git a/hugolib/alias_test.go b/hugolib/alias_test.go new file mode 100644 index 000000000..1d6824dba --- /dev/null +++ b/hugolib/alias_test.go @@ -0,0 +1,152 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "path/filepath" + "runtime" + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/stretchr/testify/require" +) + +const pageWithAlias = `--- +title: Has Alias +aliases: ["foo/bar/"] +--- +For some moments the old man did not reply. He stood with bowed head, buried in deep thought. But at last he spoke. +` + +const pageWithAliasMultipleOutputs = `--- +title: Has Alias for HTML and AMP +aliases: ["foo/bar/"] +outputs: ["HTML", "AMP", "JSON"] +--- +For some moments the old man did not reply. He stood with bowed head, buried in deep thought. But at last he spoke. +` + +const basicTemplate = "<html><body>{{.Content}}</body></html>" +const aliasTemplate = "<html><body>ALIASTEMPLATE</body></html>" + +func TestAlias(t *testing.T) { + t.Parallel() + + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + writeSource(t, fs, filepath.Join("content", "page.md"), pageWithAlias) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), basicTemplate) + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + // the real page + th.assertFileContent(filepath.Join("public", "page", "index.html"), "For some moments the old man") + // the alias redirector + th.assertFileContent(filepath.Join("public", "foo", "bar", "index.html"), "<meta http-equiv=\"refresh\" content=\"0; ") +} + +func TestAliasMultipleOutputFormats(t *testing.T) { + t.Parallel() + + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + writeSource(t, fs, filepath.Join("content", "page.md"), pageWithAliasMultipleOutputs) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), basicTemplate) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.amp.html"), basicTemplate) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.json"), basicTemplate) + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + // the real pages + th.assertFileContent(filepath.Join("public", "page", "index.html"), "For some moments the old man") + th.assertFileContent(filepath.Join("public", "amp", "page", "index.html"), "For some moments the old man") + th.assertFileContent(filepath.Join("public", "page", "index.json"), "For some moments the old man") + + // the alias redirectors + th.assertFileContent(filepath.Join("public", "foo", "bar", "index.html"), "<meta http-equiv=\"refresh\" content=\"0; ") + th.assertFileContent(filepath.Join("public", "foo", "bar", "amp", "index.html"), "<meta http-equiv=\"refresh\" content=\"0; ") + require.False(t, destinationExists(th.Fs, filepath.Join("public", "foo", "bar", "index.json"))) +} + +func TestAliasTemplate(t *testing.T) { + t.Parallel() + + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + writeSource(t, fs, filepath.Join("content", "page.md"), pageWithAlias) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), basicTemplate) + writeSource(t, fs, filepath.Join("layouts", "alias.html"), aliasTemplate) + + sites, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + require.NoError(t, err) + + require.NoError(t, sites.Build(BuildCfg{})) + + // the real page + th.assertFileContent(filepath.Join("public", "page", "index.html"), "For some moments the old man") + // the alias redirector + th.assertFileContent(filepath.Join("public", "foo", "bar", "index.html"), "ALIASTEMPLATE") +} + +func TestTargetPathHTMLRedirectAlias(t *testing.T) { + h := newAliasHandler(nil, newErrorLogger(), false) + + errIsNilForThisOS := runtime.GOOS != "windows" + + tests := []struct { + value string + expected string + errIsNil bool + }{ + {"", "", false}, + {"s", filepath.FromSlash("s/index.html"), true}, + {"/", "", false}, + {"alias 1", filepath.FromSlash("alias 1/index.html"), true}, + {"alias 2/", filepath.FromSlash("alias 2/index.html"), true}, + {"alias 3.html", "alias 3.html", true}, + {"alias4.html", "alias4.html", true}, + {"/alias 5.html", "alias 5.html", true}, + {"/трям.html", "трям.html", true}, + {"../../../../tmp/passwd", "", false}, + {"/foo/../../../../tmp/passwd", filepath.FromSlash("tmp/passwd/index.html"), true}, + {"foo/../../../../tmp/passwd", "", false}, + {"C:\\Windows", filepath.FromSlash("C:\\Windows/index.html"), errIsNilForThisOS}, + {"/trailing-space /", filepath.FromSlash("trailing-space /index.html"), errIsNilForThisOS}, + {"/trailing-period./", filepath.FromSlash("trailing-period./index.html"), errIsNilForThisOS}, + {"/tab\tseparated/", filepath.FromSlash("tab\tseparated/index.html"), errIsNilForThisOS}, + {"/chrome/?p=help&ctx=keyboard#topic=3227046", filepath.FromSlash("chrome/?p=help&ctx=keyboard#topic=3227046/index.html"), errIsNilForThisOS}, + {"/LPT1/Printer/", filepath.FromSlash("LPT1/Printer/index.html"), errIsNilForThisOS}, + } + + for _, test := range tests { + path, err := h.targetPathAlias(test.value) + if (err == nil) != test.errIsNil { + t.Errorf("Expected err == nil => %t, got: %t. err: %s", test.errIsNil, err == nil, err) + continue + } + if err == nil && path != test.expected { + t.Errorf("Expected: \"%s\", got: \"%s\"", test.expected, path) + } + } +} diff --git a/hugolib/author.go b/hugolib/author.go new file mode 100644 index 000000000..0f4327097 --- /dev/null +++ b/hugolib/author.go @@ -0,0 +1,45 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +// AuthorList is a list of all authors and their metadata. +type AuthorList map[string]Author + +// Author contains details about the author of a page. +type Author struct { + GivenName string + FamilyName string + DisplayName string + Thumbnail string + Image string + ShortBio string + LongBio string + Email string + Social AuthorSocial +} + +// AuthorSocial is a place to put social details per author. These are the +// standard keys that themes will expect to have available, but can be +// expanded to any others on a per site basis +// - website +// - github +// - facebook +// - twitter +// - googleplus +// - pinterest +// - instagram +// - youtube +// - linkedin +// - skype +type AuthorSocial map[string]string diff --git a/hugolib/case_insensitive_test.go b/hugolib/case_insensitive_test.go new file mode 100644 index 000000000..ca63196b3 --- /dev/null +++ b/hugolib/case_insensitive_test.go @@ -0,0 +1,306 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "path/filepath" + "strings" + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/hugofs" + "github.com/spf13/afero" + "github.com/stretchr/testify/require" +) + +var ( + caseMixingSiteConfigTOML = ` +Title = "In an Insensitive Mood" +DefaultContentLanguage = "nn" +defaultContentLanguageInSubdir = true + +[Blackfriday] +AngledQuotes = true +HrefTargetBlank = true + +[Params] +Search = true +Color = "green" +mood = "Happy" +[Params.Colors] +Blue = "blue" +Yellow = "yellow" + +[Languages] +[Languages.nn] +title = "Nynorsk title" +languageName = "Nynorsk" +weight = 1 + +[Languages.en] +TITLE = "English title" +LanguageName = "English" +Mood = "Thoughtful" +Weight = 2 +COLOR = "Pink" +[Languages.en.blackfriday] +angledQuotes = false +hrefTargetBlank = false +[Languages.en.Colors] +BLUE = "blues" +yellow = "golden" +` + caseMixingPage1En = ` +--- +TITLE: Page1 En Translation +BlackFriday: + AngledQuotes: false +Color: "black" +Search: true +mooD: "sad and lonely" +ColorS: + Blue: "bluesy" + Yellow: "sunny" +--- +# "Hi" +{{< shortcode >}} +` + + caseMixingPage1 = ` +--- +titLe: Side 1 +blackFriday: + angledQuotes: true +color: "red" +search: false +MooD: "sad" +COLORS: + blue: "heavenly" + yelloW: "Sunny" +--- +# "Hi" +{{< shortcode >}} +` + + caseMixingPage2 = ` +--- +TITLE: Page2 Title +BlackFriday: + AngledQuotes: false +Color: "black" +search: true +MooD: "moody" +ColorS: + Blue: "sky" + YELLOW: "flower" +--- +# Hi +{{< shortcode >}} +` +) + +func caseMixingTestsWriteCommonSources(t *testing.T, fs afero.Fs) { + writeToFs(t, fs, filepath.Join("content", "sect1", "page1.md"), caseMixingPage1) + writeToFs(t, fs, filepath.Join("content", "sect2", "page2.md"), caseMixingPage2) + writeToFs(t, fs, filepath.Join("content", "sect1", "page1.en.md"), caseMixingPage1En) + + writeToFs(t, fs, "layouts/shortcodes/shortcode.html", ` +Shortcode Page: {{ .Page.Params.COLOR }}|{{ .Page.Params.Colors.Blue }} +Shortcode Site: {{ .Page.Site.Params.COLOR }}|{{ .Site.Params.COLORS.YELLOW }} +`) + + writeToFs(t, fs, "layouts/partials/partial.html", ` +Partial Page: {{ .Params.COLOR }}|{{ .Params.Colors.Blue }} +Partial Site: {{ .Site.Params.COLOR }}|{{ .Site.Params.COLORS.YELLOW }} +`) + + writeToFs(t, fs, "config.toml", caseMixingSiteConfigTOML) + +} + +func TestCaseInsensitiveConfigurationVariations(t *testing.T) { + t.Parallel() + + // See issues 2615, 1129, 2590 and maybe some others + // Also see 2598 + // + // Viper is now, at least for the Hugo part, case insensitive + // So we need tests for all of it, with needed adjustments on the Hugo side. + // Not sure what that will be. Let us see. + + // So all the below with case variations: + // config: regular fields, blackfriday config, param with nested map + // language: new and overridden values, in regular fields and nested paramsmap + // page frontmatter: regular fields, blackfriday config, param with nested map + + mm := afero.NewMemMapFs() + + caseMixingTestsWriteCommonSources(t, mm) + + cfg, err := LoadConfig(mm, "", "config.toml") + require.NoError(t, err) + + fs := hugofs.NewFrom(mm, cfg) + + th := testHelper{cfg, fs, t} + + writeSource(t, fs, filepath.Join("layouts", "_default", "baseof.html"), ` +Block Page Colors: {{ .Params.COLOR }}|{{ .Params.Colors.Blue }} +{{ block "main" . }}default{{end}}`) + + writeSource(t, fs, filepath.Join("layouts", "sect2", "single.html"), ` +{{ define "main"}} +Page Colors: {{ .Params.CoLOR }}|{{ .Params.Colors.Blue }} +Site Colors: {{ .Site.Params.COlOR }}|{{ .Site.Params.COLORS.YELLOW }} +{{ .Content }} +{{ partial "partial.html" . }} +{{ end }} +`) + + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), ` +Page Title: {{ .Title }} +Site Title: {{ .Site.Title }} +Site Lang Mood: {{ .Site.Language.Params.MOoD }} +Page Colors: {{ .Params.COLOR }}|{{ .Params.Colors.Blue }} +Site Colors: {{ .Site.Params.COLOR }}|{{ .Site.Params.COLORS.YELLOW }} +{{ .Content }} +{{ partial "partial.html" . }} +`) + + sites, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + if err != nil { + t.Fatalf("Failed to create sites: %s", err) + } + + err = sites.Build(BuildCfg{}) + + if err != nil { + t.Fatalf("Failed to build sites: %s", err) + } + + th.assertFileContent(filepath.Join("public", "nn", "sect1", "page1", "index.html"), + "Page Colors: red|heavenly", + "Site Colors: green|yellow", + "Site Lang Mood: Happy", + "Shortcode Page: red|heavenly", + "Shortcode Site: green|yellow", + "Partial Page: red|heavenly", + "Partial Site: green|yellow", + "Page Title: Side 1", + "Site Title: Nynorsk title", + "«Hi»", // angled quotes + ) + + th.assertFileContent(filepath.Join("public", "en", "sect1", "page1", "index.html"), + "Site Colors: Pink|golden", + "Page Colors: black|bluesy", + "Site Lang Mood: Thoughtful", + "Page Title: Page1 En Translation", + "Site Title: English title", + "“Hi”", + ) + + th.assertFileContent(filepath.Join("public", "nn", "sect2", "page2", "index.html"), + "Page Colors: black|sky", + "Site Colors: green|yellow", + "Shortcode Page: black|sky", + "Block Page Colors: black|sky", + "Partial Page: black|sky", + "Partial Site: green|yellow", + ) +} + +func TestCaseInsensitiveConfigurationForAllTemplateEngines(t *testing.T) { + t.Parallel() + + noOp := func(s string) string { + return s + } + + amberFixer := func(s string) string { + fixed := strings.Replace(s, "{{ .Site.Params", "{{ Site.Params", -1) + fixed = strings.Replace(fixed, "{{ .Params", "{{ Params", -1) + fixed = strings.Replace(fixed, ".Content", "Content", -1) + fixed = strings.Replace(fixed, "{{", "#{", -1) + fixed = strings.Replace(fixed, "}}", "}", -1) + + return fixed + } + + for _, config := range []struct { + suffix string + templateFixer func(s string) string + }{ + {"amber", amberFixer}, + {"html", noOp}, + {"ace", noOp}, + } { + doTestCaseInsensitiveConfigurationForTemplateEngine(t, config.suffix, config.templateFixer) + + } + +} + +func doTestCaseInsensitiveConfigurationForTemplateEngine(t *testing.T, suffix string, templateFixer func(s string) string) { + + mm := afero.NewMemMapFs() + + caseMixingTestsWriteCommonSources(t, mm) + + cfg, err := LoadConfig(mm, "", "config.toml") + require.NoError(t, err) + + fs := hugofs.NewFrom(mm, cfg) + + th := testHelper{cfg, fs, t} + + t.Log("Testing", suffix) + + templTemplate := ` +p + | + | Page Colors: {{ .Params.CoLOR }}|{{ .Params.Colors.Blue }} + | Site Colors: {{ .Site.Params.COlOR }}|{{ .Site.Params.COLORS.YELLOW }} + | {{ .Content }} + +` + + templ := templateFixer(templTemplate) + + t.Log(templ) + + writeSource(t, fs, filepath.Join("layouts", "_default", fmt.Sprintf("single.%s", suffix)), templ) + + sites, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + if err != nil { + t.Fatalf("Failed to create sites: %s", err) + } + + err = sites.Build(BuildCfg{}) + + if err != nil { + t.Fatalf("Failed to build sites: %s", err) + } + + th.assertFileContent(filepath.Join("public", "nn", "sect1", "page1", "index.html"), + "Page Colors: red|heavenly", + "Site Colors: green|yellow", + "Shortcode Page: red|heavenly", + "Shortcode Site: green|yellow", + ) + +} diff --git a/hugolib/config.go b/hugolib/config.go new file mode 100644 index 000000000..7779a4d83 --- /dev/null +++ b/hugolib/config.go @@ -0,0 +1,135 @@ +// Copyright 2016-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + + "github.com/gohugoio/hugo/helpers" + "github.com/spf13/afero" + "github.com/spf13/viper" +) + +// LoadConfig loads Hugo configuration into a new Viper and then adds +// a set of defaults. +func LoadConfig(fs afero.Fs, relativeSourcePath, configFilename string) (*viper.Viper, error) { + v := viper.New() + v.SetFs(fs) + if relativeSourcePath == "" { + relativeSourcePath = "." + } + + v.AutomaticEnv() + v.SetEnvPrefix("hugo") + v.SetConfigFile(configFilename) + // See https://github.com/spf13/viper/issues/73#issuecomment-126970794 + if relativeSourcePath == "" { + v.AddConfigPath(".") + } else { + v.AddConfigPath(relativeSourcePath) + } + err := v.ReadInConfig() + if err != nil { + if _, ok := err.(viper.ConfigParseError); ok { + return nil, err + } + return nil, fmt.Errorf("Unable to locate Config file. Perhaps you need to create a new site.\n Run `hugo help new` for details. (%s)\n", err) + } + + v.RegisterAlias("indexes", "taxonomies") + + // Remove these in Hugo 0.23. + if v.IsSet("disable404") { + helpers.Deprecated("site config", "disable404", "Use disableKinds=[\"404\"]", false) + } + + if v.IsSet("disableRSS") { + helpers.Deprecated("site config", "disableRSS", "Use disableKinds=[\"RSS\"]", false) + } + + if v.IsSet("disableSitemap") { + // NOTE: Do not remove this until Hugo 0.24, ERROR in 0.23. + helpers.Deprecated("site config", "disableSitemap", "Use disableKinds= [\"sitemap\"]", false) + } + + if v.IsSet("disableRobotsTXT") { + helpers.Deprecated("site config", "disableRobotsTXT", "Use disableKinds= [\"robotsTXT\"]", false) + } + + loadDefaultSettingsFor(v) + + return v, nil +} + +func loadDefaultSettingsFor(v *viper.Viper) { + + c := helpers.NewContentSpec(v) + + v.SetDefault("cleanDestinationDir", false) + v.SetDefault("watch", false) + v.SetDefault("metaDataFormat", "toml") + v.SetDefault("disable404", false) + v.SetDefault("disableRSS", false) + v.SetDefault("disableSitemap", false) + v.SetDefault("disableRobotsTXT", false) + v.SetDefault("contentDir", "content") + v.SetDefault("layoutDir", "layouts") + v.SetDefault("staticDir", "static") + v.SetDefault("archetypeDir", "archetypes") + v.SetDefault("publishDir", "public") + v.SetDefault("dataDir", "data") + v.SetDefault("i18nDir", "i18n") + v.SetDefault("themesDir", "themes") + v.SetDefault("defaultLayout", "post") + v.SetDefault("buildDrafts", false) + v.SetDefault("buildFuture", false) + v.SetDefault("buildExpired", false) + v.SetDefault("uglyURLs", false) + v.SetDefault("verbose", false) + v.SetDefault("ignoreCache", false) + v.SetDefault("canonifyURLs", false) + v.SetDefault("relativeURLs", false) + v.SetDefault("removePathAccents", false) + v.SetDefault("taxonomies", map[string]string{"tag": "tags", "category": "categories"}) + v.SetDefault("permalinks", make(PermalinkOverrides, 0)) + v.SetDefault("sitemap", Sitemap{Priority: -1, Filename: "sitemap.xml"}) + v.SetDefault("pygmentsStyle", "monokai") + v.SetDefault("pygmentsUseClasses", false) + v.SetDefault("pygmentsCodeFences", false) + v.SetDefault("pygmentsOptions", "") + v.SetDefault("disableLiveReload", false) + v.SetDefault("pluralizeListTitles", true) + v.SetDefault("preserveTaxonomyNames", false) + v.SetDefault("forceSyncStatic", false) + v.SetDefault("footnoteAnchorPrefix", "") + v.SetDefault("footnoteReturnLinkContents", "") + v.SetDefault("newContentEditor", "") + v.SetDefault("paginate", 10) + v.SetDefault("paginatePath", "page") + v.SetDefault("blackfriday", c.NewBlackfriday()) + v.SetDefault("rSSUri", "index.xml") + v.SetDefault("rssLimit", -1) + v.SetDefault("sectionPagesMenu", "") + v.SetDefault("disablePathToLower", false) + v.SetDefault("hasCJKLanguage", false) + v.SetDefault("enableEmoji", false) + v.SetDefault("pygmentsCodeFencesGuessSyntax", false) + v.SetDefault("useModTimeAsFallback", false) + v.SetDefault("defaultContentLanguage", "en") + v.SetDefault("defaultContentLanguageInSubdir", false) + v.SetDefault("enableMissingTranslationPlaceholders", false) + v.SetDefault("enableGitInfo", false) + v.SetDefault("ignoreFiles", make([]string, 0)) + v.SetDefault("disableAliases", false) +} diff --git a/hugolib/config_test.go b/hugolib/config_test.go new file mode 100644 index 000000000..780e5c33d --- /dev/null +++ b/hugolib/config_test.go @@ -0,0 +1,43 @@ +// Copyright 2016-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoadConfig(t *testing.T) { + t.Parallel() + + // Add a random config variable for testing. + // side = page in Norwegian. + configContent := ` + PaginatePath = "side" + ` + + mm := afero.NewMemMapFs() + + writeToFs(t, mm, "hugo.toml", configContent) + + cfg, err := LoadConfig(mm, "", "hugo.toml") + require.NoError(t, err) + + assert.Equal(t, "side", cfg.GetString("paginatePath")) + // default + assert.Equal(t, "layouts", cfg.GetString("layoutDir")) +} diff --git a/hugolib/datafiles_test.go b/hugolib/datafiles_test.go new file mode 100644 index 000000000..b62fb197d --- /dev/null +++ b/hugolib/datafiles_test.go @@ -0,0 +1,154 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "path/filepath" + "reflect" + "strings" + "testing" + + "io/ioutil" + "log" + "os" + + "github.com/gohugoio/hugo/deps" + jww "github.com/spf13/jwalterweatherman" + + "github.com/gohugoio/hugo/parser" + "github.com/gohugoio/hugo/source" + "github.com/stretchr/testify/require" +) + +func TestDataDirJSON(t *testing.T) { + t.Parallel() + + sources := []source.ByteSource{ + {Name: filepath.FromSlash("data/test/foo.json"), Content: []byte(`{ "bar": "foofoo" }`)}, + {Name: filepath.FromSlash("data/test.json"), Content: []byte(`{ "hello": [ { "world": "foo" } ] }`)}, + } + + expected, err := parser.HandleJSONMetaData([]byte(`{ "test": { "hello": [{ "world": "foo" }] , "foo": { "bar":"foofoo" } } }`)) + + if err != nil { + t.Fatalf("Error %s", err) + } + + doTestDataDir(t, expected, sources) +} + +func TestDataDirToml(t *testing.T) { + t.Parallel() + + sources := []source.ByteSource{ + {Name: filepath.FromSlash("data/test/kung.toml"), Content: []byte("[foo]\nbar = 1")}, + } + + expected, err := parser.HandleTOMLMetaData([]byte("[test]\n[test.kung]\n[test.kung.foo]\nbar = 1")) + + if err != nil { + t.Fatalf("Error %s", err) + } + + doTestDataDir(t, expected, sources) +} + +func TestDataDirYAMLWithOverridenValue(t *testing.T) { + t.Parallel() + + sources := []source.ByteSource{ + // filepath.Walk walks the files in lexical order, '/' comes before '.'. Simulate this: + {Name: filepath.FromSlash("data/a.yaml"), Content: []byte("a: 1")}, + {Name: filepath.FromSlash("data/test/v1.yaml"), Content: []byte("v1-2: 2")}, + {Name: filepath.FromSlash("data/test/v2.yaml"), Content: []byte("v2:\n- 2\n- 3")}, + {Name: filepath.FromSlash("data/test.yaml"), Content: []byte("v1: 1")}, + } + + expected := map[string]interface{}{"a": map[string]interface{}{"a": 1}, + "test": map[string]interface{}{"v1": map[string]interface{}{"v1-2": 2}, "v2": map[string]interface{}{"v2": []interface{}{2, 3}}}} + + doTestDataDir(t, expected, sources) +} + +// issue 892 +func TestDataDirMultipleSources(t *testing.T) { + t.Parallel() + + sources := []source.ByteSource{ + {Name: filepath.FromSlash("data/test/first.toml"), Content: []byte("bar = 1")}, + {Name: filepath.FromSlash("themes/mytheme/data/test/first.toml"), Content: []byte("bar = 2")}, + {Name: filepath.FromSlash("data/test/second.toml"), Content: []byte("tender = 2")}, + } + + expected, _ := parser.HandleTOMLMetaData([]byte("[test.first]\nbar = 1\n[test.second]\ntender=2")) + + doTestDataDir(t, expected, sources, + "theme", "mytheme") + +} + +func doTestDataDir(t *testing.T, expected interface{}, sources []source.ByteSource, configKeyValues ...interface{}) { + var ( + cfg, fs = newTestCfg() + ) + + for i := 0; i < len(configKeyValues); i += 2 { + cfg.Set(configKeyValues[i].(string), configKeyValues[i+1]) + } + + var ( + logger = jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) + depsCfg = deps.DepsCfg{Fs: fs, Cfg: cfg, Logger: logger} + ) + + writeSource(t, fs, filepath.Join("content", "dummy.md"), "content") + writeSourcesToSource(t, "", fs, sources...) + + expectBuildError := false + + if ok, shouldFail := expected.(bool); ok && shouldFail { + expectBuildError = true + } + + s := buildSingleSiteExpected(t, expectBuildError, depsCfg, BuildCfg{SkipRender: true}) + + if !expectBuildError && !reflect.DeepEqual(expected, s.Data) { + t.Errorf("Expected structure\n%#v got\n%#v", expected, s.Data) + } +} + +func TestDataFromShortcode(t *testing.T) { + t.Parallel() + + var ( + cfg, fs = newTestCfg() + ) + + writeSource(t, fs, "data/hugo.toml", "slogan = \"Hugo Rocks!\"") + writeSource(t, fs, "layouts/_default/single.html", ` +* Slogan from template: {{ .Site.Data.hugo.slogan }} +* {{ .Content }}`) + writeSource(t, fs, "layouts/shortcodes/d.html", `{{ .Page.Site.Data.hugo.slogan }}`) + writeSource(t, fs, "content/c.md", `--- +--- +Slogan from shortcode: {{< d >}} +`) + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + content := readSource(t, fs, "public/c/index.html") + require.True(t, strings.Contains(content, "Slogan from template: Hugo Rocks!"), content) + require.True(t, strings.Contains(content, "Slogan from shortcode: Hugo Rocks!"), content) + +} diff --git a/hugolib/disableKinds_test.go b/hugolib/disableKinds_test.go new file mode 100644 index 000000000..736d461db --- /dev/null +++ b/hugolib/disableKinds_test.go @@ -0,0 +1,221 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package hugolib + +import ( + "strings" + "testing" + + "fmt" + + "github.com/gohugoio/hugo/deps" + "github.com/spf13/afero" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" + "github.com/stretchr/testify/require" +) + +func TestDisableKindsNoneDisabled(t *testing.T) { + t.Parallel() + doTestDisableKinds(t) +} + +func TestDisableKindsSomeDisabled(t *testing.T) { + t.Parallel() + doTestDisableKinds(t, KindSection, kind404) +} + +func TestDisableKindsOneDisabled(t *testing.T) { + t.Parallel() + for _, kind := range allKinds { + if kind == KindPage { + // Turning off regular page generation have some side-effects + // not handled by the assertions below (no sections), so + // skip that for now. + continue + } + doTestDisableKinds(t, kind) + } +} + +func TestDisableKindsAllDisabled(t *testing.T) { + t.Parallel() + doTestDisableKinds(t, allKinds...) +} + +func doTestDisableKinds(t *testing.T, disabled ...string) { + siteConfigTemplate := ` +baseURL = "http://example.com/blog" +enableRobotsTXT = true +disableKinds = %s + +paginate = 1 +defaultContentLanguage = "en" + +[Taxonomies] +tag = "tags" +category = "categories" +` + + pageTemplate := `--- +title: "%s" +tags: +%s +categories: +- Hugo +--- +# Doc +` + + mf := afero.NewMemMapFs() + + disabledStr := "[]" + + if len(disabled) > 0 { + disabledStr = strings.Replace(fmt.Sprintf("%#v", disabled), "[]string{", "[", -1) + disabledStr = strings.Replace(disabledStr, "}", "]", -1) + } + + siteConfig := fmt.Sprintf(siteConfigTemplate, disabledStr) + writeToFs(t, mf, "config.toml", siteConfig) + + cfg, err := LoadConfig(mf, "", "config.toml") + require.NoError(t, err) + + fs := hugofs.NewFrom(mf, cfg) + th := testHelper{cfg, fs, t} + + writeSource(t, fs, "layouts/index.html", "Home|{{ .Title }}|{{ .Content }}") + writeSource(t, fs, "layouts/_default/single.html", "Single|{{ .Title }}|{{ .Content }}") + writeSource(t, fs, "layouts/_default/list.html", "List|{{ .Title }}|{{ .Content }}") + writeSource(t, fs, "layouts/_default/terms.html", "Terms List|{{ .Title }}|{{ .Content }}") + writeSource(t, fs, "layouts/404.html", "Page Not Found") + + writeSource(t, fs, "content/sect/p1.md", fmt.Sprintf(pageTemplate, "P1", "- tag1")) + + writeNewContentFile(t, fs, "Category Terms", "2017-01-01", "content/categories/_index.md", 10) + writeNewContentFile(t, fs, "Tag1 List", "2017-01-01", "content/tags/tag1/_index.md", 10) + + h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + require.NoError(t, err) + require.Len(t, h.Sites, 1) + + err = h.Build(BuildCfg{}) + + require.NoError(t, err) + + assertDisabledKinds(th, h.Sites[0], disabled...) + +} + +func assertDisabledKinds(th testHelper, s *Site, disabled ...string) { + assertDisabledKind(th, + func(isDisabled bool) bool { + if isDisabled { + return len(s.RegularPages) == 0 + } + return len(s.RegularPages) > 0 + }, disabled, KindPage, "public/sect/p1/index.html", "Single|P1") + assertDisabledKind(th, + func(isDisabled bool) bool { + p := s.getPage(KindHome) + if isDisabled { + return p == nil + } + return p != nil + }, disabled, KindHome, "public/index.html", "Home") + assertDisabledKind(th, + func(isDisabled bool) bool { + p := s.getPage(KindSection, "sect") + if isDisabled { + return p == nil + } + return p != nil + }, disabled, KindSection, "public/sect/index.html", "Sects") + assertDisabledKind(th, + func(isDisabled bool) bool { + p := s.getPage(KindTaxonomy, "tags", "tag1") + + if isDisabled { + return p == nil + } + return p != nil + + }, disabled, KindTaxonomy, "public/tags/tag1/index.html", "Tag1") + assertDisabledKind(th, + func(isDisabled bool) bool { + p := s.getPage(KindTaxonomyTerm, "tags") + if isDisabled { + return p == nil + } + return p != nil + + }, disabled, KindTaxonomyTerm, "public/tags/index.html", "Tags") + assertDisabledKind(th, + func(isDisabled bool) bool { + p := s.getPage(KindTaxonomyTerm, "categories") + + if isDisabled { + return p == nil + } + return p != nil + + }, disabled, KindTaxonomyTerm, "public/categories/index.html", "Category Terms") + assertDisabledKind(th, + func(isDisabled bool) bool { + p := s.getPage(KindTaxonomy, "categories", "hugo") + if isDisabled { + return p == nil + } + return p != nil + + }, disabled, KindTaxonomy, "public/categories/hugo/index.html", "Hugo") + // The below have no page in any collection. + assertDisabledKind(th, func(isDisabled bool) bool { return true }, disabled, kindRSS, "public/index.xml", "<link>") + assertDisabledKind(th, func(isDisabled bool) bool { return true }, disabled, kindSitemap, "public/sitemap.xml", "sitemap") + assertDisabledKind(th, func(isDisabled bool) bool { return true }, disabled, kindRobotsTXT, "public/robots.txt", "User-agent") + assertDisabledKind(th, func(isDisabled bool) bool { return true }, disabled, kind404, "public/404.html", "Page Not Found") +} + +func assertDisabledKind(th testHelper, kindAssert func(bool) bool, disabled []string, kind, path, matcher string) { + isDisabled := stringSliceContains(kind, disabled...) + require.True(th.T, kindAssert(isDisabled), fmt.Sprintf("%s: %t", kind, isDisabled)) + + if kind == kindRSS && !isDisabled { + // If the home page is also disabled, there is not RSS to look for. + if stringSliceContains(KindHome, disabled...) { + isDisabled = true + } + } + + if isDisabled { + // Path should not exist + fileExists, err := helpers.Exists(path, th.Fs.Destination) + require.False(th.T, fileExists) + require.NoError(th.T, err) + + } else { + th.assertFileContent(path, matcher) + } +} + +func stringSliceContains(k string, values ...string) bool { + for _, v := range values { + if k == v { + return true + } + } + return false +} diff --git a/hugolib/embedded_shortcodes_test.go b/hugolib/embedded_shortcodes_test.go new file mode 100644 index 000000000..6f47f98c3 --- /dev/null +++ b/hugolib/embedded_shortcodes_test.go @@ -0,0 +1,409 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "encoding/json" + "fmt" + "html/template" + "strings" + "testing" + + "path/filepath" + + "github.com/gohugoio/hugo/deps" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/tpl" + "github.com/stretchr/testify/require" +) + +const ( + testBaseURL = "http://foo/bar" +) + +func TestShortcodeCrossrefs(t *testing.T) { + t.Parallel() + + for _, relative := range []bool{true, false} { + doTestShortcodeCrossrefs(t, relative) + } +} + +func doTestShortcodeCrossrefs(t *testing.T, relative bool) { + var ( + cfg, fs = newTestCfg() + ) + + cfg.Set("baseURL", testBaseURL) + + var refShortcode string + var expectedBase string + + if relative { + refShortcode = "relref" + expectedBase = "/bar" + } else { + refShortcode = "ref" + expectedBase = testBaseURL + } + + path := filepath.FromSlash("blog/post.md") + in := fmt.Sprintf(`{{< %s "%s" >}}`, refShortcode, path) + + writeSource(t, fs, "content/"+path, simplePageWithURL+": "+in) + + expected := fmt.Sprintf(`%s/simple/url/`, expectedBase) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + require.Len(t, s.RegularPages, 1) + + output := string(s.RegularPages[0].Content) + + if !strings.Contains(output, expected) { + t.Errorf("Got\n%q\nExpected\n%q", output, expected) + } +} + +func TestShortcodeHighlight(t *testing.T) { + t.Parallel() + + if !helpers.HasPygments() { + t.Skip("Skip test as Pygments is not installed") + } + + for _, this := range []struct { + in, expected string + }{ + {`{{< highlight java >}} +void do(); +{{< /highlight >}}`, + "(?s)<div class=\"highlight\" style=\"background: #ffffff\"><pre style=\"line-height: 125%\">.*?void</span> do().*?</pre></div>\n", + }, + {`{{< highlight java "style=friendly" >}} +void do(); +{{< /highlight >}}`, + "(?s)<div class=\"highlight\" style=\"background: #f0f0f0\"><pre style=\"line-height: 125%\">.*?void</span>.*?do</span>.*?().*?</pre></div>\n", + }, + } { + + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + cfg.Set("pygmentsStyle", "bw") + cfg.Set("pygmentsUseClasses", false) + + writeSource(t, fs, filepath.Join("content", "simple.md"), fmt.Sprintf(`--- +title: Shorty +--- +%s`, this.in)) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{ .Content }}`) + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + th.assertFileContentRegexp(filepath.Join("public", "simple", "index.html"), this.expected) + + } +} + +func TestShortcodeFigure(t *testing.T) { + t.Parallel() + + for _, this := range []struct { + in, expected string + }{ + { + `{{< figure src="/img/hugo-logo.png" >}}`, + "(?s)\n<figure >.*?<img src=\"/img/hugo-logo.png\" />.*?</figure>\n", + }, + { + // set alt + `{{< figure src="/img/hugo-logo.png" alt="Hugo logo" >}}`, + "(?s)\n<figure >.*?<img src=\"/img/hugo-logo.png\" alt=\"Hugo logo\" />.*?</figure>\n", + }, + // set title + { + `{{< figure src="/img/hugo-logo.png" title="Hugo logo" >}}`, + "(?s)\n<figure >.*?<img src=\"/img/hugo-logo.png\" />.*?<figcaption>.*?<h4>Hugo logo</h4>.*?</figcaption>.*?</figure>\n", + }, + // set attr and attrlink + { + `{{< figure src="/img/hugo-logo.png" attr="Hugo logo" attrlink="/img/hugo-logo.png" >}}`, + "(?s)\n<figure >.*?<img src=\"/img/hugo-logo.png\" />.*?<figcaption>.*?<p>.*?<a href=\"/img/hugo-logo.png\">.*?Hugo logo.*?</a>.*?</p>.*?</figcaption>.*?</figure>\n", + }, + } { + + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + writeSource(t, fs, filepath.Join("content", "simple.md"), fmt.Sprintf(`--- +title: Shorty +--- +%s`, this.in)) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{ .Content }}`) + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + th.assertFileContentRegexp(filepath.Join("public", "simple", "index.html"), this.expected) + + } +} + +func TestShortcodeSpeakerdeck(t *testing.T) { + t.Parallel() + + for _, this := range []struct { + in, expected string + }{ + { + `{{< speakerdeck 4e8126e72d853c0060001f97 >}}`, + "(?s)<script async class='speakerdeck-embed' data-id='4e8126e72d853c0060001f97'.*?>.*?</script>", + }, + } { + + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + writeSource(t, fs, filepath.Join("content", "simple.md"), fmt.Sprintf(`--- +title: Shorty +--- +%s`, this.in)) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{ .Content }}`) + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + th.assertFileContentRegexp(filepath.Join("public", "simple", "index.html"), this.expected) + } +} + +func TestShortcodeYoutube(t *testing.T) { + t.Parallel() + + for _, this := range []struct { + in, expected string + }{ + { + `{{< youtube w7Ft2ymGmfc >}}`, + "(?s)\n<div style=\".*?\">.*?<iframe src=\"//www.youtube.com/embed/w7Ft2ymGmfc\" style=\".*?\" allowfullscreen frameborder=\"0\">.*?</iframe>.*?</div>\n", + }, + // set class + { + `{{< youtube w7Ft2ymGmfc video>}}`, + "(?s)\n<div class=\"video\">.*?<iframe src=\"//www.youtube.com/embed/w7Ft2ymGmfc\" allowfullscreen frameborder=\"0\">.*?</iframe>.*?</div>\n", + }, + // set class and autoplay (using named params) + { + `{{< youtube id="w7Ft2ymGmfc" class="video" autoplay="true" >}}`, + "(?s)\n<div class=\"video\">.*?<iframe src=\"//www.youtube.com/embed/w7Ft2ymGmfc\\?autoplay=1\".*?allowfullscreen frameborder=\"0\">.*?</iframe>.*?</div>", + }, + } { + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + writeSource(t, fs, filepath.Join("content", "simple.md"), fmt.Sprintf(`--- +title: Shorty +--- +%s`, this.in)) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{ .Content }}`) + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + th.assertFileContentRegexp(filepath.Join("public", "simple", "index.html"), this.expected) + } + +} + +func TestShortcodeVimeo(t *testing.T) { + t.Parallel() + + for _, this := range []struct { + in, expected string + }{ + { + `{{< vimeo 146022717 >}}`, + "(?s)\n<div style=\".*?\">.*?<iframe src=\"//player.vimeo.com/video/146022717\" style=\".*?\" webkitallowfullscreen mozallowfullscreen allowfullscreen>.*?</iframe>.*?</div>\n", + }, + // set class + { + `{{< vimeo 146022717 video >}}`, + "(?s)\n<div class=\"video\">.*?<iframe src=\"//player.vimeo.com/video/146022717\" webkitallowfullscreen mozallowfullscreen allowfullscreen>.*?</iframe>.*?</div>\n", + }, + // set class (using named params) + { + `{{< vimeo id="146022717" class="video" >}}`, + "(?s)^<div class=\"video\">.*?<iframe src=\"//player.vimeo.com/video/146022717\" webkitallowfullscreen mozallowfullscreen allowfullscreen>.*?</iframe>.*?</div>", + }, + } { + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + writeSource(t, fs, filepath.Join("content", "simple.md"), fmt.Sprintf(`--- +title: Shorty +--- +%s`, this.in)) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{ .Content }}`) + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + th.assertFileContentRegexp(filepath.Join("public", "simple", "index.html"), this.expected) + + } +} + +func TestShortcodeGist(t *testing.T) { + t.Parallel() + + for _, this := range []struct { + in, expected string + }{ + { + `{{< gist spf13 7896402 >}}`, + "(?s)^<script src=\"//gist.github.com/spf13/7896402.js\"></script>", + }, + { + `{{< gist spf13 7896402 "img.html" >}}`, + "(?s)^<script src=\"//gist.github.com/spf13/7896402.js\\?file=img.html\"></script>", + }, + } { + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + writeSource(t, fs, filepath.Join("content", "simple.md"), fmt.Sprintf(`--- +title: Shorty +--- +%s`, this.in)) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{ .Content }}`) + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + th.assertFileContentRegexp(filepath.Join("public", "simple", "index.html"), this.expected) + + } +} + +func TestShortcodeTweet(t *testing.T) { + t.Parallel() + + for i, this := range []struct { + in, resp, expected string + }{ + { + `{{< tweet 666616452582129664 >}}`, + `{"url":"https:\/\/twitter.com\/spf13\/status\/666616452582129664","author_name":"Steve Francia","author_url":"https:\/\/twitter.com\/spf13","html":"\u003Cblockquote class=\"twitter-tweet\"\u003E\u003Cp lang=\"en\" dir=\"ltr\"\u003EHugo 0.15 will have 30%+ faster render times thanks to this commit \u003Ca href=\"https:\/\/t.co\/FfzhM8bNhT\"\u003Ehttps:\/\/t.co\/FfzhM8bNhT\u003C\/a\u003E \u003Ca href=\"https:\/\/twitter.com\/hashtag\/gohugo?src=hash\"\u003E#gohugo\u003C\/a\u003E \u003Ca href=\"https:\/\/twitter.com\/hashtag\/golang?src=hash\"\u003E#golang\u003C\/a\u003E \u003Ca href=\"https:\/\/t.co\/ITbMNU2BUf\"\u003Ehttps:\/\/t.co\/ITbMNU2BUf\u003C\/a\u003E\u003C\/p\u003E— Steve Francia (@spf13) \u003Ca href=\"https:\/\/twitter.com\/spf13\/status\/666616452582129664\"\u003ENovember 17, 2015\u003C\/a\u003E\u003C\/blockquote\u003E\n\u003Cscript async src=\"\/\/platform.twitter.com\/widgets.js\" charset=\"utf-8\"\u003E\u003C\/script\u003E","width":550,"height":null,"type":"rich","cache_age":"3153600000","provider_name":"Twitter","provider_url":"https:\/\/twitter.com","version":"1.0"}`, + `(?s)^<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Hugo 0.15 will have 30%. faster render times thanks to this commit <a href="https://t.co/FfzhM8bNhT">https://t.co/FfzhM8bNhT</a> <a href="https://twitter.com/hashtag/gohugo.src=hash">#gohugo</a> <a href="https://twitter.com/hashtag/golang.src=hash">#golang</a> <a href="https://t.co/ITbMNU2BUf">https://t.co/ITbMNU2BUf</a></p>— Steve Francia .@spf13. <a href="https://twitter.com/spf13/status/666616452582129664">November 17, 2015</a></blockquote>.*?<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>`, + }, + } { + // overload getJSON to return mock API response from Twitter + tweetFuncMap := template.FuncMap{ + "getJSON": func(urlParts ...string) interface{} { + var v interface{} + err := json.Unmarshal([]byte(this.resp), &v) + if err != nil { + t.Fatalf("[%d] unexpected error in json.Unmarshal: %s", i, err) + return err + } + return v + }, + } + + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + withTemplate := func(templ tpl.TemplateHandler) error { + templ.(tpl.TemplateTestMocker).SetFuncs(tweetFuncMap) + return nil + } + + writeSource(t, fs, filepath.Join("content", "simple.md"), fmt.Sprintf(`--- +title: Shorty +--- +%s`, this.in)) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{ .Content }}`) + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg, WithTemplate: withTemplate}, BuildCfg{}) + + th.assertFileContentRegexp(filepath.Join("public", "simple", "index.html"), this.expected) + + } +} + +func TestShortcodeInstagram(t *testing.T) { + t.Parallel() + + for i, this := range []struct { + in, hidecaption, resp, expected string + }{ + { + `{{< instagram BMokmydjG-M >}}`, + `0`, + `{"provider_url": "https://www.instagram.com", "media_id": "1380514280986406796_25025320", "author_name": "instagram", "height": null, "thumbnail_url": "https://scontent-amt2-1.cdninstagram.com/t51.2885-15/s640x640/sh0.08/e35/15048135_1880160212214218_7827880881132929024_n.jpg?ig_cache_key=MTM4MDUxNDI4MDk4NjQwNjc5Ng%3D%3D.2", "thumbnail_width": 640, "thumbnail_height": 640, "provider_name": "Instagram", "title": "Today, we\u2019re introducing a few new tools to help you make your story even more fun: Boomerang and mentions. We\u2019re also starting to test links inside some stories.\nBoomerang lets you turn everyday moments into something fun and unexpected. Now you can easily take a Boomerang right inside Instagram. Swipe right from your feed to open the stories camera. A new format picker under the record button lets you select \u201cBoomerang\u201d mode.\nYou can also now share who you\u2019re with or who you\u2019re thinking of by mentioning them in your story. When you add text to your story, type \u201c@\u201d followed by a username and select the person you\u2019d like to mention. Their username will appear underlined in your story. And when someone taps the mention, they'll see a pop-up that takes them to that profile.\nYou may begin to spot \u201cSee More\u201d links at the bottom of some stories. This is a test that lets verified accounts add links so it\u2019s easy to learn more. From your favorite chefs\u2019 recipes to articles from top journalists or concert dates from the musicians you love, tap \u201cSee More\u201d or swipe up to view the link right inside the app.\nTo learn more about today\u2019s updates, check out help.instagram.com.\nThese updates for Instagram Stories are available as part of Instagram version 9.7 available for iOS in the Apple App Store, for Android in Google Play and for Windows 10 in the Windows Store.", "html": "\u003cblockquote class=\"instagram-media\" data-instgrm-captioned data-instgrm-version=\"7\" style=\" background:#FFF; border:0; border-radius:3px; box-shadow:0 0 1px 0 rgba(0,0,0,0.5),0 1px 10px 0 rgba(0,0,0,0.15); margin: 1px; max-width:658px; padding:0; width:99.375%; width:-webkit-calc(100% - 2px); width:calc(100% - 2px);\"\u003e\u003cdiv style=\"padding:8px;\"\u003e \u003cdiv style=\" background:#F8F8F8; line-height:0; margin-top:40px; padding:50.0% 0; text-align:center; width:100%;\"\u003e \u003cdiv style=\" background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAMAAAApWqozAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAMUExURczMzPf399fX1+bm5mzY9AMAAADiSURBVDjLvZXbEsMgCES5/P8/t9FuRVCRmU73JWlzosgSIIZURCjo/ad+EQJJB4Hv8BFt+IDpQoCx1wjOSBFhh2XssxEIYn3ulI/6MNReE07UIWJEv8UEOWDS88LY97kqyTliJKKtuYBbruAyVh5wOHiXmpi5we58Ek028czwyuQdLKPG1Bkb4NnM+VeAnfHqn1k4+GPT6uGQcvu2h2OVuIf/gWUFyy8OWEpdyZSa3aVCqpVoVvzZZ2VTnn2wU8qzVjDDetO90GSy9mVLqtgYSy231MxrY6I2gGqjrTY0L8fxCxfCBbhWrsYYAAAAAElFTkSuQmCC); display:block; height:44px; margin:0 auto -44px; position:relative; top:-22px; width:44px;\"\u003e\u003c/div\u003e\u003c/div\u003e \u003cp style=\" margin:8px 0 0 0; padding:0 4px;\"\u003e \u003ca href=\"https://www.instagram.com/p/BMokmydjG-M/\" style=\" color:#000; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px; text-decoration:none; word-wrap:break-word;\" target=\"_blank\"\u003eToday, we\u2019re introducing a few new tools to help you make your story even more fun: Boomerang and mentions. We\u2019re also starting to test links inside some stories. Boomerang lets you turn everyday moments into something fun and unexpected. Now you can easily take a Boomerang right inside Instagram. Swipe right from your feed to open the stories camera. A new format picker under the record button lets you select \u201cBoomerang\u201d mode. You can also now share who you\u2019re with or who you\u2019re thinking of by mentioning them in your story. When you add text to your story, type \u201c@\u201d followed by a username and select the person you\u2019d like to mention. Their username will appear underlined in your story. And when someone taps the mention, they\u0026#39;ll see a pop-up that takes them to that profile. You may begin to spot \u201cSee More\u201d links at the bottom of some stories. This is a test that lets verified accounts add links so it\u2019s easy to learn more. From your favorite chefs\u2019 recipes to articles from top journalists or concert dates from the musicians you love, tap \u201cSee More\u201d or swipe up to view the link right inside the app. To learn more about today\u2019s updates, check out help.instagram.com. These updates for Instagram Stories are available as part of Instagram version 9.7 available for iOS in the Apple App Store, for Android in Google Play and for Windows 10 in the Windows Store.\u003c/a\u003e\u003c/p\u003e \u003cp style=\" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; line-height:17px; margin-bottom:0; margin-top:8px; overflow:hidden; padding:8px 0 7px; text-align:center; text-overflow:ellipsis; white-space:nowrap;\"\u003eA photo posted by Instagram (@instagram) on \u003ctime style=\" font-family:Arial,sans-serif; font-size:14px; line-height:17px;\" datetime=\"2016-11-10T15:02:28+00:00\"\u003eNov 10, 2016 at 7:02am PST\u003c/time\u003e\u003c/p\u003e\u003c/div\u003e\u003c/blockquote\u003e\n\u003cscript async defer src=\"//platform.instagram.com/en_US/embeds.js\"\u003e\u003c/script\u003e", "width": 658, "version": "1.0", "author_url": "https://www.instagram.com/instagram", "author_id": 25025320, "type": "rich"}`, + `(?s)<blockquote class="instagram-media" data-instgrm-captioned data-instgrm-version="7" .*defer src="//platform.instagram.com/en_US/embeds.js"></script>`, + }, + { + `{{< instagram BMokmydjG-M hidecaption >}}`, + `1`, + `{"provider_url": "https://www.instagram.com", "media_id": "1380514280986406796_25025320", "author_name": "instagram", "height": null, "thumbnail_url": "https://scontent-amt2-1.cdninstagram.com/t51.2885-15/s640x640/sh0.08/e35/15048135_1880160212214218_7827880881132929024_n.jpg?ig_cache_key=MTM4MDUxNDI4MDk4NjQwNjc5Ng%3D%3D.2", "thumbnail_width": 640, "thumbnail_height": 640, "provider_name": "Instagram", "title": "Today, we\u2019re introducing a few new tools to help you make your story even more fun: Boomerang and mentions. We\u2019re also starting to test links inside some stories.\nBoomerang lets you turn everyday moments into something fun and unexpected. Now you can easily take a Boomerang right inside Instagram. Swipe right from your feed to open the stories camera. A new format picker under the record button lets you select \u201cBoomerang\u201d mode.\nYou can also now share who you\u2019re with or who you\u2019re thinking of by mentioning them in your story. When you add text to your story, type \u201c@\u201d followed by a username and select the person you\u2019d like to mention. Their username will appear underlined in your story. And when someone taps the mention, they'll see a pop-up that takes them to that profile.\nYou may begin to spot \u201cSee More\u201d links at the bottom of some stories. This is a test that lets verified accounts add links so it\u2019s easy to learn more. From your favorite chefs\u2019 recipes to articles from top journalists or concert dates from the musicians you love, tap \u201cSee More\u201d or swipe up to view the link right inside the app.\nTo learn more about today\u2019s updates, check out help.instagram.com.\nThese updates for Instagram Stories are available as part of Instagram version 9.7 available for iOS in the Apple App Store, for Android in Google Play and for Windows 10 in the Windows Store.", "html": "\u003cblockquote class=\"instagram-media\" data-instgrm-version=\"7\" style=\" background:#FFF; border:0; border-radius:3px; box-shadow:0 0 1px 0 rgba(0,0,0,0.5),0 1px 10px 0 rgba(0,0,0,0.15); margin: 1px; max-width:658px; padding:0; width:99.375%; width:-webkit-calc(100% - 2px); width:calc(100% - 2px);\"\u003e\u003cdiv style=\"padding:8px;\"\u003e \u003cdiv style=\" background:#F8F8F8; line-height:0; margin-top:40px; padding:50.0% 0; text-align:center; width:100%;\"\u003e \u003cdiv style=\" background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAMAAAApWqozAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAMUExURczMzPf399fX1+bm5mzY9AMAAADiSURBVDjLvZXbEsMgCES5/P8/t9FuRVCRmU73JWlzosgSIIZURCjo/ad+EQJJB4Hv8BFt+IDpQoCx1wjOSBFhh2XssxEIYn3ulI/6MNReE07UIWJEv8UEOWDS88LY97kqyTliJKKtuYBbruAyVh5wOHiXmpi5we58Ek028czwyuQdLKPG1Bkb4NnM+VeAnfHqn1k4+GPT6uGQcvu2h2OVuIf/gWUFyy8OWEpdyZSa3aVCqpVoVvzZZ2VTnn2wU8qzVjDDetO90GSy9mVLqtgYSy231MxrY6I2gGqjrTY0L8fxCxfCBbhWrsYYAAAAAElFTkSuQmCC); display:block; height:44px; margin:0 auto -44px; position:relative; top:-22px; width:44px;\"\u003e\u003c/div\u003e\u003c/div\u003e\u003cp style=\" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; line-height:17px; margin-bottom:0; margin-top:8px; overflow:hidden; padding:8px 0 7px; text-align:center; text-overflow:ellipsis; white-space:nowrap;\"\u003e\u003ca href=\"https://www.instagram.com/p/BMokmydjG-M/\" style=\" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px; text-decoration:none;\" target=\"_blank\"\u003eA photo posted by Instagram (@instagram)\u003c/a\u003e on \u003ctime style=\" font-family:Arial,sans-serif; font-size:14px; line-height:17px;\" datetime=\"2016-11-10T15:02:28+00:00\"\u003eNov 10, 2016 at 7:02am PST\u003c/time\u003e\u003c/p\u003e\u003c/div\u003e\u003c/blockquote\u003e\n\u003cscript async defer src=\"//platform.instagram.com/en_US/embeds.js\"\u003e\u003c/script\u003e", "width": 658, "version": "1.0", "author_url": "https://www.instagram.com/instagram", "author_id": 25025320, "type": "rich"}`, + `(?s)<blockquote class="instagram-media" data-instgrm-version="7" style=" background:#FFF; border:0; .*<script async defer src="//platform.instagram.com/en_US/embeds.js"></script>`, + }, + } { + // overload getJSON to return mock API response from Instagram + instagramFuncMap := template.FuncMap{ + "getJSON": func(urlParts ...string) interface{} { + var v interface{} + err := json.Unmarshal([]byte(this.resp), &v) + if err != nil { + t.Fatalf("[%d] unexpected error in json.Unmarshal: %s", i, err) + return err + } + return v + }, + } + + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + withTemplate := func(templ tpl.TemplateHandler) error { + templ.(tpl.TemplateTestMocker).SetFuncs(instagramFuncMap) + return nil + } + + writeSource(t, fs, filepath.Join("content", "simple.md"), fmt.Sprintf(`--- +title: Shorty +--- +%s`, this.in)) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{ .Content | safeHTML }}`) + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg, WithTemplate: withTemplate}, BuildCfg{}) + + th.assertFileContentRegexp(filepath.Join("public", "simple", "index.html"), this.expected) + + } +} diff --git a/hugolib/gitinfo.go b/hugolib/gitinfo.go new file mode 100644 index 000000000..16d8c43a5 --- /dev/null +++ b/hugolib/gitinfo.go @@ -0,0 +1,69 @@ +// Copyright 2016-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "path" + "path/filepath" + "strings" + + "github.com/bep/gitmap" + "github.com/gohugoio/hugo/helpers" +) + +func (h *HugoSites) assembleGitInfo() { + if !h.Cfg.GetBool("enableGitInfo") { + return + } + + var ( + workingDir = h.Cfg.GetString("workingDir") + contentDir = h.Cfg.GetString("contentDir") + ) + + gitRepo, err := gitmap.Map(workingDir, "") + if err != nil { + h.Log.ERROR.Printf("Got error reading Git log: %s", err) + return + } + + gitMap := gitRepo.Files + repoPath := filepath.FromSlash(gitRepo.TopLevelAbsPath) + + // The Hugo site may be placed in a sub folder in the Git repo, + // one example being the Hugo docs. + // We have to find the root folder to the Hugo site below the Git root. + contentRoot := strings.TrimPrefix(workingDir, repoPath) + contentRoot = strings.TrimPrefix(contentRoot, helpers.FilePathSeparator) + + s := h.Sites[0] + + for _, p := range s.AllPages { + if p.Path() == "" { + // Home page etc. with no content file. + continue + } + // Git normalizes file paths on this form: + filename := path.Join(filepath.ToSlash(contentRoot), contentDir, filepath.ToSlash(p.Path())) + g, ok := gitMap[filename] + if !ok { + h.Log.WARN.Printf("Failed to find GitInfo for %q", filename) + return + } + + p.GitInfo = g + p.Lastmod = p.GitInfo.AuthorDate + } + +} diff --git a/hugolib/handler_base.go b/hugolib/handler_base.go new file mode 100644 index 000000000..99c15e15d --- /dev/null +++ b/hugolib/handler_base.go @@ -0,0 +1,60 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "github.com/gohugoio/hugo/source" +) + +type Handler interface { + FileConvert(*source.File, *Site) HandledResult + PageConvert(*Page) HandledResult + Read(*source.File, *Site) HandledResult + Extensions() []string +} + +type Handle struct { + extensions []string +} + +func (h Handle) Extensions() []string { + return h.extensions +} + +type HandledResult struct { + page *Page + file *source.File + err error +} + +// HandledResult is an error +func (h HandledResult) Error() string { + if h.err != nil { + if h.page != nil { + return "Error: " + h.err.Error() + " for " + h.page.File.LogicalName() + } + if h.file != nil { + return "Error: " + h.err.Error() + " for " + h.file.LogicalName() + } + } + return h.err.Error() +} + +func (h HandledResult) String() string { + return h.Error() +} + +func (h HandledResult) Page() *Page { + return h.page +} diff --git a/hugolib/handler_file.go b/hugolib/handler_file.go new file mode 100644 index 000000000..82ea85fb2 --- /dev/null +++ b/hugolib/handler_file.go @@ -0,0 +1,59 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "bytes" + + "github.com/dchest/cssmin" + "github.com/gohugoio/hugo/source" +) + +func init() { + RegisterHandler(new(cssHandler)) + RegisterHandler(new(defaultHandler)) +} + +type basicFileHandler Handle + +func (h basicFileHandler) Read(f *source.File, s *Site) HandledResult { + return HandledResult{file: f} +} + +func (h basicFileHandler) PageConvert(*Page) HandledResult { + return HandledResult{} +} + +type defaultHandler struct{ basicFileHandler } + +func (h defaultHandler) Extensions() []string { return []string{"*"} } +func (h defaultHandler) FileConvert(f *source.File, s *Site) HandledResult { + err := s.publish(f.Path(), f.Contents) + if err != nil { + return HandledResult{err: err} + } + return HandledResult{file: f} +} + +type cssHandler struct{ basicFileHandler } + +func (h cssHandler) Extensions() []string { return []string{"css"} } +func (h cssHandler) FileConvert(f *source.File, s *Site) HandledResult { + x := cssmin.Minify(f.Bytes()) + err := s.publish(f.Path(), bytes.NewReader(x)) + if err != nil { + return HandledResult{err: err} + } + return HandledResult{file: f} +} diff --git a/hugolib/handler_meta.go b/hugolib/handler_meta.go new file mode 100644 index 000000000..d2702a39e --- /dev/null +++ b/hugolib/handler_meta.go @@ -0,0 +1,117 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "errors" + + "fmt" + + "github.com/gohugoio/hugo/source" +) + +var handlers []Handler + +type MetaHandler interface { + // Read the Files in and register + Read(*source.File, *Site, HandleResults) + + // Generic Convert Function with coordination + Convert(interface{}, *Site, HandleResults) + + Handle() Handler +} + +type HandleResults chan<- HandledResult + +func NewMetaHandler(in string) *MetaHandle { + x := &MetaHandle{ext: in} + x.Handler() + return x +} + +type MetaHandle struct { + handler Handler + ext string +} + +func (mh *MetaHandle) Read(f *source.File, s *Site, results HandleResults) { + if h := mh.Handler(); h != nil { + results <- h.Read(f, s) + return + } + + results <- HandledResult{err: errors.New("No handler found"), file: f} +} + +func (mh *MetaHandle) Convert(i interface{}, s *Site, results HandleResults) { + h := mh.Handler() + + if f, ok := i.(*source.File); ok { + results <- h.FileConvert(f, s) + return + } + + if p, ok := i.(*Page); ok { + if p == nil { + results <- HandledResult{err: errors.New("file resulted in a nil page")} + return + } + + if h == nil { + results <- HandledResult{err: fmt.Errorf("No handler found for page '%s'. Verify the markup is supported by Hugo.", p.FullFilePath())} + return + } + + results <- h.PageConvert(p) + } +} + +func (mh *MetaHandle) Handler() Handler { + if mh.handler == nil { + mh.handler = FindHandler(mh.ext) + + // if no handler found, use default handler + if mh.handler == nil { + mh.handler = FindHandler("*") + } + } + return mh.handler +} + +func FindHandler(ext string) Handler { + for _, h := range Handlers() { + if HandlerMatch(h, ext) { + return h + } + } + return nil +} + +func HandlerMatch(h Handler, ext string) bool { + for _, x := range h.Extensions() { + if ext == x { + return true + } + } + return false +} + +func RegisterHandler(h Handler) { + handlers = append(handlers, h) +} + +func Handlers() []Handler { + return handlers +} diff --git a/hugolib/handler_page.go b/hugolib/handler_page.go new file mode 100644 index 000000000..6e230dad0 --- /dev/null +++ b/hugolib/handler_page.go @@ -0,0 +1,147 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/source" +) + +func init() { + RegisterHandler(new(markdownHandler)) + RegisterHandler(new(htmlHandler)) + RegisterHandler(new(asciidocHandler)) + RegisterHandler(new(rstHandler)) + RegisterHandler(new(mmarkHandler)) + RegisterHandler(new(orgHandler)) +} + +type basicPageHandler Handle + +func (b basicPageHandler) Read(f *source.File, s *Site) HandledResult { + page, err := s.NewPage(f.Path()) + + if err != nil { + return HandledResult{file: f, err: err} + } + + if _, err := page.ReadFrom(f.Contents); err != nil { + return HandledResult{file: f, err: err} + } + + // In a multilanguage setup, we use the first site to + // do the initial processing. + // That site may be different than where the page will end up, + // so we do the assignment here. + // We should clean up this, but that will have to wait. + s.assignSiteByLanguage(page) + + return HandledResult{file: f, page: page, err: err} +} + +func (b basicPageHandler) FileConvert(*source.File, *Site) HandledResult { + return HandledResult{} +} + +type markdownHandler struct { + basicPageHandler +} + +func (h markdownHandler) Extensions() []string { return []string{"mdown", "markdown", "md"} } +func (h markdownHandler) PageConvert(p *Page) HandledResult { + return commonConvert(p) +} + +type htmlHandler struct { + basicPageHandler +} + +func (h htmlHandler) Extensions() []string { return []string{"html", "htm"} } + +func (h htmlHandler) PageConvert(p *Page) HandledResult { + if p.rendered { + panic(fmt.Sprintf("Page %q already rendered, does not need conversion", p.BaseFileName())) + } + + // Work on a copy of the raw content from now on. + p.createWorkContentCopy() + + if err := p.processShortcodes(); err != nil { + p.s.Log.ERROR.Println(err) + } + + return HandledResult{err: nil} +} + +type asciidocHandler struct { + basicPageHandler +} + +func (h asciidocHandler) Extensions() []string { return []string{"asciidoc", "adoc", "ad"} } +func (h asciidocHandler) PageConvert(p *Page) HandledResult { + return commonConvert(p) +} + +type rstHandler struct { + basicPageHandler +} + +func (h rstHandler) Extensions() []string { return []string{"rest", "rst"} } +func (h rstHandler) PageConvert(p *Page) HandledResult { + return commonConvert(p) +} + +type mmarkHandler struct { + basicPageHandler +} + +func (h mmarkHandler) Extensions() []string { return []string{"mmark"} } +func (h mmarkHandler) PageConvert(p *Page) HandledResult { + return commonConvert(p) +} + +type orgHandler struct { + basicPageHandler +} + +func (h orgHandler) Extensions() []string { return []string{"org"} } +func (h orgHandler) PageConvert(p *Page) HandledResult { + return commonConvert(p) +} + +func commonConvert(p *Page) HandledResult { + if p.rendered { + panic(fmt.Sprintf("Page %q already rendered, does not need conversion", p.BaseFileName())) + } + + // Work on a copy of the raw content from now on. + p.createWorkContentCopy() + + if err := p.processShortcodes(); err != nil { + p.s.Log.ERROR.Println(err) + } + + // TODO(bep) these page handlers need to be re-evaluated, as it is hard to + // process a page in isolation. See the new preRender func. + if p.s.Cfg.GetBool("enableEmoji") { + p.workContent = helpers.Emojify(p.workContent) + } + + p.workContent = p.replaceDivider(p.workContent) + p.workContent = p.renderContent(p.workContent) + + return HandledResult{err: nil} +} diff --git a/hugolib/handler_test.go b/hugolib/handler_test.go new file mode 100644 index 000000000..aa58d1c43 --- /dev/null +++ b/hugolib/handler_test.go @@ -0,0 +1,77 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "path/filepath" + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" +) + +func TestDefaultHandler(t *testing.T) { + t.Parallel() + + var ( + cfg, fs = newTestCfg() + ) + + cfg.Set("verbose", true) + cfg.Set("uglyURLs", true) + + writeSource(t, fs, filepath.FromSlash("content/sect/doc1.html"), "---\nmarkup: markdown\n---\n# title\nsome *content*") + writeSource(t, fs, filepath.FromSlash("content/sect/doc2.html"), "<!doctype html><html><body>more content</body></html>") + writeSource(t, fs, filepath.FromSlash("content/sect/doc3.md"), "# doc3\n*some* content") + writeSource(t, fs, filepath.FromSlash("content/sect/doc4.md"), "---\ntitle: doc4\n---\n# doc4\n*some content*") + writeSource(t, fs, filepath.FromSlash("content/sect/doc3/img1.png"), "‰PNG ��� IHDR����������:~›U��� IDATWcø��ZMoñ����IEND®B`‚") + writeSource(t, fs, filepath.FromSlash("content/sect/img2.gif"), "GIF89a��€��ÿÿÿ���,�������D�;") + writeSource(t, fs, filepath.FromSlash("content/sect/img2.spf"), "****FAKE-FILETYPE****") + writeSource(t, fs, filepath.FromSlash("content/doc7.html"), "<html><body>doc7 content</body></html>") + writeSource(t, fs, filepath.FromSlash("content/sect/doc8.html"), "---\nmarkup: md\n---\n# title\nsome *content*") + + writeSource(t, fs, filepath.FromSlash("layouts/_default/single.html"), "{{.Content}}") + writeSource(t, fs, filepath.FromSlash("head"), "<head><script src=\"script.js\"></script></head>") + writeSource(t, fs, filepath.FromSlash("head_abs"), "<head><script src=\"/script.js\"></script></head") + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + tests := []struct { + doc string + expected string + }{ + {filepath.FromSlash("public/sect/doc1.html"), "\n\n<h1 id=\"title\">title</h1>\n\n<p>some <em>content</em></p>\n"}, + {filepath.FromSlash("public/sect/doc2.html"), "<!doctype html><html><body>more content</body></html>"}, + {filepath.FromSlash("public/sect/doc3.html"), "\n\n<h1 id=\"doc3\">doc3</h1>\n\n<p><em>some</em> content</p>\n"}, + {filepath.FromSlash("public/sect/doc3/img1.png"), string([]byte("‰PNG ��� IHDR����������:~›U��� IDATWcø��ZMoñ����IEND®B`‚"))}, + {filepath.FromSlash("public/sect/img2.gif"), string([]byte("GIF89a��€��ÿÿÿ���,�������D�;"))}, + {filepath.FromSlash("public/sect/img2.spf"), string([]byte("****FAKE-FILETYPE****"))}, + {filepath.FromSlash("public/doc7.html"), "<html><body>doc7 content</body></html>"}, + {filepath.FromSlash("public/sect/doc8.html"), "\n\n<h1 id=\"title\">title</h1>\n\n<p>some <em>content</em></p>\n"}, + } + + for _, test := range tests { + file, err := fs.Destination.Open(test.doc) + if err != nil { + t.Fatalf("Did not find %s in target.", test.doc) + } + + content := helpers.ReaderToString(file) + + if content != test.expected { + t.Errorf("%s content expected:\n%q\ngot:\n%q", test.doc, test.expected, content) + } + } + +} diff --git a/hugolib/hugo_info.go b/hugolib/hugo_info.go new file mode 100644 index 000000000..1e0c192e5 --- /dev/null +++ b/hugolib/hugo_info.go @@ -0,0 +1,49 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "html/template" + + "github.com/gohugoio/hugo/helpers" +) + +var ( + // CommitHash contains the current Git revision. Use make to build to make + // sure this gets set. + CommitHash string + + // BuildDate contains the date of the current build. + BuildDate string +) + +var hugoInfo *HugoInfo + +// HugoInfo contains information about the current Hugo environment +type HugoInfo struct { + Version string + Generator template.HTML + CommitHash string + BuildDate string +} + +func init() { + hugoInfo = &HugoInfo{ + Version: helpers.CurrentHugoVersion.String(), + CommitHash: CommitHash, + BuildDate: BuildDate, + Generator: template.HTML(fmt.Sprintf(`<meta name="generator" content="Hugo %s" />`, helpers.CurrentHugoVersion.String())), + } +} diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go new file mode 100644 index 000000000..4fe3f8771 --- /dev/null +++ b/hugolib/hugo_sites.go @@ -0,0 +1,633 @@ +// Copyright 2016-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "errors" + "fmt" + "strings" + "sync" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" + + "github.com/gohugoio/hugo/i18n" + "github.com/gohugoio/hugo/tpl" + "github.com/gohugoio/hugo/tpl/tplimpl" +) + +// HugoSites represents the sites to build. Each site represents a language. +type HugoSites struct { + Sites []*Site + + runMode runmode + + multilingual *Multilingual + + *deps.Deps +} + +// NewHugoSites creates a new collection of sites given the input sites, building +// a language configuration based on those. +func newHugoSites(cfg deps.DepsCfg, sites ...*Site) (*HugoSites, error) { + + if cfg.Language != nil { + return nil, errors.New("Cannot provide Language in Cfg when sites are provided") + } + + langConfig, err := newMultiLingualFromSites(cfg.Cfg, sites...) + + if err != nil { + return nil, err + } + + h := &HugoSites{ + multilingual: langConfig, + Sites: sites} + + for _, s := range sites { + s.owner = h + } + + // TODO(bep) + cfg.Cfg.Set("multilingual", sites[0].multilingualEnabled()) + + if err := applyDepsIfNeeded(cfg, sites...); err != nil { + return nil, err + } + + h.Deps = sites[0].Deps + + return h, nil +} + +func applyDepsIfNeeded(cfg deps.DepsCfg, sites ...*Site) error { + if cfg.TemplateProvider == nil { + cfg.TemplateProvider = tplimpl.DefaultTemplateProvider + } + + if cfg.TranslationProvider == nil { + cfg.TranslationProvider = i18n.NewTranslationProvider() + } + + var ( + d *deps.Deps + err error + ) + + for _, s := range sites { + if s.Deps != nil { + continue + } + + if d == nil { + cfg.Language = s.Language + cfg.WithTemplate = s.withSiteTemplates(cfg.WithTemplate) + + var err error + d, err = deps.New(cfg) + if err != nil { + return err + } + + d.OutputFormatsConfig = s.outputFormatsConfig + s.Deps = d + + if err = d.LoadResources(); err != nil { + return err + } + + } else { + d, err = d.ForLanguage(s.Language) + if err != nil { + return err + } + d.OutputFormatsConfig = s.outputFormatsConfig + s.Deps = d + } + + } + + return nil +} + +// NewHugoSites creates HugoSites from the given config. +func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) { + sites, err := createSitesFromConfig(cfg) + if err != nil { + return nil, err + } + return newHugoSites(cfg, sites...) +} + +func (s *Site) withSiteTemplates(withTemplates ...func(templ tpl.TemplateHandler) error) func(templ tpl.TemplateHandler) error { + return func(templ tpl.TemplateHandler) error { + templ.LoadTemplates(s.PathSpec.GetLayoutDirPath(), "") + if s.PathSpec.ThemeSet() { + templ.LoadTemplates(s.PathSpec.GetThemeDir()+"/layouts", "theme") + } + + for _, wt := range withTemplates { + if wt == nil { + continue + } + if err := wt(templ); err != nil { + return err + } + } + + return nil + } +} + +func createSitesFromConfig(cfg deps.DepsCfg) ([]*Site, error) { + + var ( + sites []*Site + ) + + multilingual := cfg.Cfg.GetStringMap("languages") + + if len(multilingual) == 0 { + l := helpers.NewDefaultLanguage(cfg.Cfg) + cfg.Language = l + s, err := newSite(cfg) + if err != nil { + return nil, err + } + sites = append(sites, s) + } + + if len(multilingual) > 0 { + var err error + + languages, err := toSortedLanguages(cfg.Cfg, multilingual) + + if err != nil { + return nil, fmt.Errorf("Failed to parse multilingual config: %s", err) + } + + for _, lang := range languages { + var s *Site + var err error + cfg.Language = lang + s, err = newSite(cfg) + + if err != nil { + return nil, err + } + + sites = append(sites, s) + } + } + + return sites, nil +} + +// Reset resets the sites and template caches, making it ready for a full rebuild. +func (h *HugoSites) reset() { + for i, s := range h.Sites { + h.Sites[i] = s.reset() + } +} + +func (h *HugoSites) createSitesFromConfig() error { + + depsCfg := deps.DepsCfg{Fs: h.Fs, Cfg: h.Cfg} + sites, err := createSitesFromConfig(depsCfg) + + if err != nil { + return err + } + + langConfig, err := newMultiLingualFromSites(depsCfg.Cfg, sites...) + + if err != nil { + return err + } + + h.Sites = sites + + for _, s := range sites { + s.owner = h + } + + if err := applyDepsIfNeeded(depsCfg, sites...); err != nil { + return err + } + + h.Deps = sites[0].Deps + + h.multilingual = langConfig + + return nil +} + +func (h *HugoSites) toSiteInfos() []*SiteInfo { + infos := make([]*SiteInfo, len(h.Sites)) + for i, s := range h.Sites { + infos[i] = &s.Info + } + return infos +} + +// BuildCfg holds build options used to, as an example, skip the render step. +type BuildCfg struct { + // Whether we are in watch (server) mode + Watching bool + // Print build stats at the end of a build + PrintStats bool + // Reset site state before build. Use to force full rebuilds. + ResetState bool + // Re-creates the sites from configuration before a build. + // This is needed if new languages are added. + CreateSitesFromConfig bool + // Skip rendering. Useful for testing. + SkipRender bool + // Use this to indicate what changed (for rebuilds). + whatChanged *whatChanged +} + +func (h *HugoSites) renderCrossSitesArtifacts() error { + + if !h.multilingual.enabled() { + return nil + } + + if h.Cfg.GetBool("disableSitemap") { + return nil + } + + sitemapEnabled := false + for _, s := range h.Sites { + if s.isEnabled(kindSitemap) { + sitemapEnabled = true + break + } + } + + if !sitemapEnabled { + return nil + } + + // TODO(bep) DRY + sitemapDefault := parseSitemap(h.Cfg.GetStringMap("sitemap")) + + s := h.Sites[0] + + smLayouts := []string{"sitemapindex.xml", "_default/sitemapindex.xml", "_internal/_default/sitemapindex.xml"} + + return s.renderAndWriteXML("sitemapindex", + sitemapDefault.Filename, h.toSiteInfos(), s.appendThemeTemplates(smLayouts)...) +} + +func (h *HugoSites) assignMissingTranslations() error { + // This looks heavy, but it should be a small number of nodes by now. + allPages := h.findAllPagesByKindNotIn(KindPage) + for _, nodeType := range []string{KindHome, KindSection, KindTaxonomy, KindTaxonomyTerm} { + nodes := h.findPagesByKindIn(nodeType, allPages) + + // Assign translations + for _, t1 := range nodes { + for _, t2 := range nodes { + if t1.isNewTranslation(t2) { + t1.translations = append(t1.translations, t2) + } + } + } + } + + // Now we can sort the translations. + for _, p := range allPages { + if len(p.translations) > 0 { + pageBy(languagePageSort).Sort(p.translations) + } + } + return nil + +} + +// createMissingPages creates home page, taxonomies etc. that isnt't created as an +// effect of having a content file. +func (h *HugoSites) createMissingPages() error { + var newPages Pages + + for _, s := range h.Sites { + if s.isEnabled(KindHome) { + // home pages + home := s.findPagesByKind(KindHome) + if len(home) > 1 { + panic("Too many homes") + } + if len(home) == 0 { + n := s.newHomePage() + s.Pages = append(s.Pages, n) + newPages = append(newPages, n) + } + } + + // Will create content-less root sections. + newSections := s.assembleSections() + s.Pages = append(s.Pages, newSections...) + newPages = append(newPages, newSections...) + + // taxonomy list and terms pages + taxonomies := s.Language.GetStringMapString("taxonomies") + if len(taxonomies) > 0 { + taxonomyPages := s.findPagesByKind(KindTaxonomy) + taxonomyTermsPages := s.findPagesByKind(KindTaxonomyTerm) + for _, plural := range taxonomies { + if s.isEnabled(KindTaxonomyTerm) { + foundTaxonomyTermsPage := false + for _, p := range taxonomyTermsPages { + if p.sections[0] == plural { + foundTaxonomyTermsPage = true + break + } + } + + if !foundTaxonomyTermsPage { + foundTaxonomyTermsPage = true + n := s.newTaxonomyTermsPage(plural) + s.Pages = append(s.Pages, n) + newPages = append(newPages, n) + } + } + + if s.isEnabled(KindTaxonomy) { + for key := range s.Taxonomies[plural] { + foundTaxonomyPage := false + origKey := key + + if s.Info.preserveTaxonomyNames { + key = s.PathSpec.MakePathSanitized(key) + } + for _, p := range taxonomyPages { + if p.sections[0] == plural && p.sections[1] == key { + foundTaxonomyPage = true + break + } + } + + if !foundTaxonomyPage { + n := s.newTaxonomyPage(plural, origKey) + s.Pages = append(s.Pages, n) + newPages = append(newPages, n) + } + } + } + } + } + } + + if len(newPages) > 0 { + // This resorting is unfortunate, but it also needs to be sorted + // when sections are created. + first := h.Sites[0] + + first.AllPages = append(first.AllPages, newPages...) + + first.AllPages.Sort() + + for _, s := range h.Sites { + s.Pages.Sort() + } + + for i := 1; i < len(h.Sites); i++ { + h.Sites[i].AllPages = first.AllPages + } + } + + return nil +} + +func (s *Site) assignSiteByLanguage(p *Page) { + + pageLang := p.Lang() + + if pageLang == "" { + panic("Page language missing: " + p.Title) + } + + for _, site := range s.owner.Sites { + if strings.HasPrefix(site.Language.Lang, pageLang) { + p.s = site + p.Site = &site.Info + return + } + } + +} + +func (h *HugoSites) setupTranslations() { + + master := h.Sites[0] + + for _, p := range master.rawAllPages { + if p.Lang() == "" { + panic("Page language missing: " + p.Title) + } + + if p.Kind == kindUnknown { + p.Kind = p.s.kindFromSections(p.sections) + } + + if !p.s.isEnabled(p.Kind) { + continue + } + + shouldBuild := p.shouldBuild() + + for i, site := range h.Sites { + // The site is assigned by language when read. + if site == p.s { + site.updateBuildStats(p) + if shouldBuild { + site.Pages = append(site.Pages, p) + } + } + + if !shouldBuild { + continue + } + + if i == 0 { + site.AllPages = append(site.AllPages, p) + } + } + + } + + // Pull over the collections from the master site + for i := 1; i < len(h.Sites); i++ { + h.Sites[i].AllPages = h.Sites[0].AllPages + h.Sites[i].Data = h.Sites[0].Data + } + + if len(h.Sites) > 1 { + pages := h.Sites[0].AllPages + allTranslations := pagesToTranslationsMap(pages) + assignTranslationsToPages(allTranslations, pages) + } +} + +func (s *Site) preparePagesForRender(cfg *BuildCfg) { + + pageChan := make(chan *Page) + wg := &sync.WaitGroup{} + numWorkers := getGoMaxProcs() * 4 + + for i := 0; i < numWorkers; i++ { + wg.Add(1) + go func(pages <-chan *Page, wg *sync.WaitGroup) { + defer wg.Done() + for p := range pages { + if !p.shouldRenderTo(s.rc.Format) { + // No need to prepare + continue + } + var shortcodeUpdate bool + if p.shortcodeState != nil { + shortcodeUpdate = p.shortcodeState.updateDelta() + } + + if !shortcodeUpdate && !cfg.whatChanged.other && p.rendered { + // No need to process it again. + continue + } + + // If we got this far it means that this is either a new Page pointer + // or a template or similar has changed so wee need to do a rerendering + // of the shortcodes etc. + + // Mark it as rendered + p.rendered = true + + // If in watch mode or if we have multiple output formats, + // we need to keep the original so we can + // potentially repeat this process on rebuild. + needsACopy := cfg.Watching || len(p.outputFormats) > 1 + var workContentCopy []byte + if needsACopy { + workContentCopy = make([]byte, len(p.workContent)) + copy(workContentCopy, p.workContent) + } else { + // Just reuse the same slice. + workContentCopy = p.workContent + } + + if p.Markup == "markdown" { + tmpContent, tmpTableOfContents := helpers.ExtractTOC(workContentCopy) + p.TableOfContents = helpers.BytesToHTML(tmpTableOfContents) + workContentCopy = tmpContent + } + + var err error + if workContentCopy, err = handleShortcodes(p, workContentCopy); err != nil { + s.Log.ERROR.Printf("Failed to handle shortcodes for page %s: %s", p.BaseFileName(), err) + } + + if p.Markup != "html" { + + // Now we know enough to create a summary of the page and count some words + summaryContent, err := p.setUserDefinedSummaryIfProvided(workContentCopy) + + if err != nil { + s.Log.ERROR.Printf("Failed to set user defined summary for page %q: %s", p.Path(), err) + } else if summaryContent != nil { + workContentCopy = summaryContent.content + } + + p.Content = helpers.BytesToHTML(workContentCopy) + + if summaryContent == nil { + if err := p.setAutoSummary(); err != nil { + s.Log.ERROR.Printf("Failed to set user auto summary for page %q: %s", p.pathOrTitle(), err) + } + } + + } else { + p.Content = helpers.BytesToHTML(workContentCopy) + } + + //analyze for raw stats + p.analyzePage() + + } + }(pageChan, wg) + } + + for _, p := range s.Pages { + pageChan <- p + } + + close(pageChan) + + wg.Wait() + +} + +// Pages returns all pages for all sites. +func (h *HugoSites) Pages() Pages { + return h.Sites[0].AllPages +} + +func handleShortcodes(p *Page, rawContentCopy []byte) ([]byte, error) { + if p.shortcodeState != nil && len(p.shortcodeState.contentShortcodes) > 0 { + p.s.Log.DEBUG.Printf("Replace %d shortcodes in %q", len(p.shortcodeState.contentShortcodes), p.BaseFileName()) + err := p.shortcodeState.executeShortcodesForDelta(p) + + if err != nil { + return rawContentCopy, err + } + + rawContentCopy, err = replaceShortcodeTokens(rawContentCopy, shortcodePlaceholderPrefix, p.shortcodeState.renderedShortcodes) + + if err != nil { + p.s.Log.FATAL.Printf("Failed to replace shortcode tokens in %s:\n%s", p.BaseFileName(), err.Error()) + } + } + + return rawContentCopy, nil +} + +func (s *Site) updateBuildStats(page *Page) { + if page.IsDraft() { + s.draftCount++ + } + + if page.IsFuture() { + s.futureCount++ + } + + if page.IsExpired() { + s.expiredCount++ + } +} + +func (h *HugoSites) findPagesByKindNotIn(kind string, inPages Pages) Pages { + return h.Sites[0].findPagesByKindNotIn(kind, inPages) +} + +func (h *HugoSites) findPagesByKindIn(kind string, inPages Pages) Pages { + return h.Sites[0].findPagesByKindIn(kind, inPages) +} + +func (h *HugoSites) findAllPagesByKind(kind string) Pages { + return h.findPagesByKindIn(kind, h.Sites[0].AllPages) +} + +func (h *HugoSites) findAllPagesByKindNotIn(kind string) Pages { + return h.findPagesByKindNotIn(kind, h.Sites[0].AllPages) +} diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go new file mode 100644 index 000000000..fa0eac702 --- /dev/null +++ b/hugolib/hugo_sites_build.go @@ -0,0 +1,237 @@ +// Copyright 2016-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "time" + + "errors" + + "github.com/fsnotify/fsnotify" + "github.com/gohugoio/hugo/helpers" +) + +// Build builds all sites. If filesystem events are provided, +// this is considered to be a potential partial rebuild. +func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error { + t0 := time.Now() + + // Need a pointer as this may be modified. + conf := &config + + if conf.whatChanged == nil { + // Assume everything has changed + conf.whatChanged = &whatChanged{source: true, other: true} + } + + if len(events) > 0 { + // Rebuild + if err := h.initRebuild(conf); err != nil { + return err + } + } else { + if err := h.init(conf); err != nil { + return err + } + } + + if err := h.process(conf, events...); err != nil { + return err + } + + if err := h.assemble(conf); err != nil { + return err + } + + if err := h.render(conf); err != nil { + return err + } + + if config.PrintStats { + h.Log.FEEDBACK.Printf("total in %v ms\n", int(1000*time.Since(t0).Seconds())) + } + + return nil + +} + +// Build lifecycle methods below. +// The order listed matches the order of execution. + +func (h *HugoSites) init(config *BuildCfg) error { + + for _, s := range h.Sites { + if s.PageCollections == nil { + s.PageCollections = newPageCollections() + } + } + + if config.ResetState { + h.reset() + } + + if config.CreateSitesFromConfig { + if err := h.createSitesFromConfig(); err != nil { + return err + } + } + + h.runMode.Watching = config.Watching + + return nil +} + +func (h *HugoSites) initRebuild(config *BuildCfg) error { + if config.CreateSitesFromConfig { + return errors.New("Rebuild does not support 'CreateSitesFromConfig'.") + } + + if config.ResetState { + return errors.New("Rebuild does not support 'ResetState'.") + } + + if !config.Watching { + return errors.New("Rebuild called when not in watch mode") + } + + h.runMode.Watching = config.Watching + + if config.whatChanged.source { + // This is for the non-renderable content pages (rarely used, I guess). + // We could maybe detect if this is really needed, but it should be + // pretty fast. + h.TemplateHandler().RebuildClone() + } + + for _, s := range h.Sites { + s.resetBuildState() + } + + helpers.InitLoggers() + + return nil +} + +func (h *HugoSites) process(config *BuildCfg, events ...fsnotify.Event) error { + // We should probably refactor the Site and pull up most of the logic from there to here, + // but that seems like a daunting task. + // So for now, if there are more than one site (language), + // we pre-process the first one, then configure all the sites based on that. + + firstSite := h.Sites[0] + + if len(events) > 0 { + // This is a rebuild + changed, err := firstSite.reProcess(events) + config.whatChanged = &changed + return err + } + + return firstSite.process(*config) + +} + +func (h *HugoSites) assemble(config *BuildCfg) error { + if config.whatChanged.source { + for _, s := range h.Sites { + s.createTaxonomiesEntries() + } + } + + // TODO(bep) we could probably wait and do this in one go later + h.setupTranslations() + + if len(h.Sites) > 1 { + // The first is initialized during process; initialize the rest + for _, site := range h.Sites[1:] { + site.initializeSiteInfo() + } + } + + if config.whatChanged.source { + h.assembleGitInfo() + + for _, s := range h.Sites { + if err := s.buildSiteMeta(); err != nil { + return err + } + } + } + + if err := h.createMissingPages(); err != nil { + return err + } + + for _, s := range h.Sites { + s.siteStats = &siteStats{} + for _, p := range s.Pages { + // May have been set in front matter + if len(p.outputFormats) == 0 { + p.outputFormats = s.outputFormats[p.Kind] + } + + cnt := len(p.outputFormats) + if p.Kind == KindPage { + s.siteStats.pageCountRegular += cnt + } + s.siteStats.pageCount += cnt + + if err := p.initTargetPathDescriptor(); err != nil { + return err + } + if err := p.initURLs(); err != nil { + return err + } + } + s.assembleMenus() + s.refreshPageCaches() + s.setupSitePages() + } + + if err := h.assignMissingTranslations(); err != nil { + return err + } + + return nil + +} + +func (h *HugoSites) render(config *BuildCfg) error { + + for _, s := range h.Sites { + s.initRenderFormats() + for i, rf := range s.renderFormats { + s.rc = &siteRenderingContext{Format: rf} + s.preparePagesForRender(config) + + if !config.SkipRender { + if err := s.render(i); err != nil { + return err + } + } + } + + if !config.SkipRender && config.PrintStats { + s.Stats() + } + } + + if !config.SkipRender { + if err := h.renderCrossSitesArtifacts(); err != nil { + return err + } + } + + return nil +} diff --git a/hugolib/hugo_sites_build_test.go b/hugolib/hugo_sites_build_test.go new file mode 100644 index 000000000..e7d791652 --- /dev/null +++ b/hugolib/hugo_sites_build_test.go @@ -0,0 +1,1320 @@ +package hugolib + +import ( + "bytes" + "fmt" + "strings" + "testing" + + "html/template" + "os" + "path/filepath" + "time" + + "github.com/fortytw2/leaktest" + "github.com/fsnotify/fsnotify" + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/source" + "github.com/spf13/afero" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" +) + +type testSiteConfig struct { + DefaultContentLanguage string + DefaultContentLanguageInSubdir bool + Fs afero.Fs +} + +func TestMultiSitesMainLangInRoot(t *testing.T) { + t.Parallel() + for _, b := range []bool{true, false} { + doTestMultiSitesMainLangInRoot(t, b) + } +} + +func doTestMultiSitesMainLangInRoot(t *testing.T, defaultInSubDir bool) { + + siteConfig := testSiteConfig{Fs: afero.NewMemMapFs(), DefaultContentLanguage: "fr", DefaultContentLanguageInSubdir: defaultInSubDir} + + sites := createMultiTestSites(t, siteConfig, multiSiteTOMLConfigTemplate) + fs := sites.Fs + th := testHelper{sites.Cfg, fs, t} + + err := sites.Build(BuildCfg{}) + + if err != nil { + t.Fatalf("Failed to build sites: %s", err) + } + + require.Len(t, sites.Sites, 4) + + enSite := sites.Sites[0] + frSite := sites.Sites[1] + + require.Equal(t, "/en", enSite.Info.LanguagePrefix) + + if defaultInSubDir { + require.Equal(t, "/fr", frSite.Info.LanguagePrefix) + } else { + require.Equal(t, "", frSite.Info.LanguagePrefix) + } + + require.Equal(t, "/blog/en/foo", enSite.PathSpec.RelURL("foo", true)) + + doc1en := enSite.RegularPages[0] + doc1fr := frSite.RegularPages[0] + + enPerm := doc1en.Permalink() + enRelPerm := doc1en.RelPermalink() + require.Equal(t, "http://example.com/blog/en/sect/doc1-slug/", enPerm) + require.Equal(t, "/blog/en/sect/doc1-slug/", enRelPerm) + + frPerm := doc1fr.Permalink() + frRelPerm := doc1fr.RelPermalink() + // Main language in root + require.Equal(t, th.replaceDefaultContentLanguageValue("http://example.com/blog/fr/sect/doc1/"), frPerm) + require.Equal(t, th.replaceDefaultContentLanguageValue("/blog/fr/sect/doc1/"), frRelPerm) + + th.assertFileContent("public/fr/sect/doc1/index.html", "Single", "Bonjour") + th.assertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Hello") + + // Check home + if defaultInSubDir { + // should have a redirect on top level. + th.assertFileContentStraight("public/index.html", `<meta http-equiv="refresh" content="0; url=http://example.com/blog/fr" />`) + } else { + // should have redirect back to root + th.assertFileContentStraight("public/fr/index.html", `<meta http-equiv="refresh" content="0; url=http://example.com/blog" />`) + } + th.assertFileContent("public/fr/index.html", "Home", "Bonjour") + th.assertFileContent("public/en/index.html", "Home", "Hello") + + // Check list pages + th.assertFileContent("public/fr/sect/index.html", "List", "Bonjour") + th.assertFileContent("public/en/sect/index.html", "List", "Hello") + th.assertFileContent("public/fr/plaques/frtag1/index.html", "List", "Bonjour") + th.assertFileContent("public/en/tags/tag1/index.html", "List", "Hello") + + // Check sitemaps + // Sitemaps behaves different: In a multilanguage setup there will always be a index file and + // one sitemap in each lang folder. + th.assertFileContentStraight("public/sitemap.xml", + "<loc>http://example.com/blog/en/sitemap.xml</loc>", + "<loc>http://example.com/blog/fr/sitemap.xml</loc>") + + if defaultInSubDir { + th.assertFileContentStraight("public/fr/sitemap.xml", "<loc>http://example.com/blog/fr/</loc>") + } else { + th.assertFileContentStraight("public/fr/sitemap.xml", "<loc>http://example.com/blog/</loc>") + } + th.assertFileContent("public/en/sitemap.xml", "<loc>http://example.com/blog/en/</loc>") + + // Check rss + th.assertFileContent("public/fr/index.xml", `<atom:link href="http://example.com/blog/fr/index.xml"`, + `rel="self" type="application/rss+xml"`) + th.assertFileContent("public/en/index.xml", `<atom:link href="http://example.com/blog/en/index.xml"`) + th.assertFileContent("public/fr/sect/index.xml", `<atom:link href="http://example.com/blog/fr/sect/index.xml"`) + th.assertFileContent("public/en/sect/index.xml", `<atom:link href="http://example.com/blog/en/sect/index.xml"`) + th.assertFileContent("public/fr/plaques/frtag1/index.xml", `<atom:link href="http://example.com/blog/fr/plaques/frtag1/index.xml"`) + th.assertFileContent("public/en/tags/tag1/index.xml", `<atom:link href="http://example.com/blog/en/tags/tag1/index.xml"`) + + // Check paginators + th.assertFileContent("public/fr/page/1/index.html", `refresh" content="0; url=http://example.com/blog/fr/"`) + th.assertFileContent("public/en/page/1/index.html", `refresh" content="0; url=http://example.com/blog/en/"`) + th.assertFileContent("public/fr/page/2/index.html", "Home Page 2", "Bonjour", "http://example.com/blog/fr/") + th.assertFileContent("public/en/page/2/index.html", "Home Page 2", "Hello", "http://example.com/blog/en/") + th.assertFileContent("public/fr/sect/page/1/index.html", `refresh" content="0; url=http://example.com/blog/fr/sect/"`) + th.assertFileContent("public/en/sect/page/1/index.html", `refresh" content="0; url=http://example.com/blog/en/sect/"`) + th.assertFileContent("public/fr/sect/page/2/index.html", "List Page 2", "Bonjour", "http://example.com/blog/fr/sect/") + th.assertFileContent("public/en/sect/page/2/index.html", "List Page 2", "Hello", "http://example.com/blog/en/sect/") + th.assertFileContent("public/fr/plaques/frtag1/page/1/index.html", `refresh" content="0; url=http://example.com/blog/fr/plaques/frtag1/"`) + th.assertFileContent("public/en/tags/tag1/page/1/index.html", `refresh" content="0; url=http://example.com/blog/en/tags/tag1/"`) + th.assertFileContent("public/fr/plaques/frtag1/page/2/index.html", "List Page 2", "Bonjour", "http://example.com/blog/fr/plaques/frtag1/") + th.assertFileContent("public/en/tags/tag1/page/2/index.html", "List Page 2", "Hello", "http://example.com/blog/en/tags/tag1/") + // nn (Nynorsk) and nb (Bokmål) have custom pagePath: side ("page" in Norwegian) + th.assertFileContent("public/nn/side/1/index.html", `refresh" content="0; url=http://example.com/blog/nn/"`) + th.assertFileContent("public/nb/side/1/index.html", `refresh" content="0; url=http://example.com/blog/nb/"`) +} + +func TestMultiSitesWithTwoLanguages(t *testing.T) { + t.Parallel() + mm := afero.NewMemMapFs() + + writeToFs(t, mm, "config.toml", ` + +defaultContentLanguage = "nn" + +[languages] +[languages.nn] +languageName = "Nynorsk" +weight = 1 +title = "Tittel på Nynorsk" + +[languages.en] +title = "Title in English" +languageName = "English" +weight = 2 +`, + ) + + cfg, err := LoadConfig(mm, "", "config.toml") + require.NoError(t, err) + + fs := hugofs.NewFrom(mm, cfg) + + sites, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + if err != nil { + t.Fatalf("Failed to create sites: %s", err) + } + + writeSource(t, fs, filepath.Join("content", "foo.md"), "foo") + + // Add some data + writeSource(t, fs, filepath.Join("data", "hugo.toml"), "slogan = \"Hugo Rocks!\"") + + require.NoError(t, sites.Build(BuildCfg{})) + require.Len(t, sites.Sites, 2) + + nnSite := sites.Sites[0] + nnSiteHome := nnSite.getPage(KindHome) + require.Len(t, nnSiteHome.AllTranslations(), 2) + require.Len(t, nnSiteHome.Translations(), 1) + require.True(t, nnSiteHome.IsTranslated()) + +} + +// +func TestMultiSitesBuild(t *testing.T) { + t.Parallel() + + for _, config := range []struct { + content string + suffix string + }{ + {multiSiteTOMLConfigTemplate, "toml"}, + {multiSiteYAMLConfigTemplate, "yml"}, + {multiSiteJSONConfigTemplate, "json"}, + } { + doTestMultiSitesBuild(t, config.content, config.suffix) + } +} + +func doTestMultiSitesBuild(t *testing.T, configTemplate, configSuffix string) { + siteConfig := testSiteConfig{Fs: afero.NewMemMapFs(), DefaultContentLanguage: "fr", DefaultContentLanguageInSubdir: true} + sites := createMultiTestSitesForConfig(t, siteConfig, configTemplate, configSuffix) + + require.Len(t, sites.Sites, 4) + + fs := sites.Fs + th := testHelper{sites.Cfg, fs, t} + err := sites.Build(BuildCfg{}) + + if err != nil { + t.Fatalf("Failed to build sites: %s", err) + } + + // Check site config + for _, s := range sites.Sites { + require.True(t, s.Info.defaultContentLanguageInSubdir, s.Info.Title) + require.NotNil(t, s.disabledKinds) + } + + enSite := sites.Sites[0] + enSiteHome := enSite.getPage(KindHome) + require.True(t, enSiteHome.IsTranslated()) + + require.Equal(t, "en", enSite.Language.Lang) + + if len(enSite.RegularPages) != 4 { + t.Fatal("Expected 4 english pages") + } + require.Len(t, enSite.Source.Files(), 14, "should have 13 source files") + require.Len(t, enSite.AllPages, 28, "should have 28 total pages (including translations and index types)") + + doc1en := enSite.RegularPages[0] + permalink := doc1en.Permalink() + require.NoError(t, err, "permalink call failed") + require.Equal(t, "http://example.com/blog/en/sect/doc1-slug/", permalink, "invalid doc1.en permalink") + require.Len(t, doc1en.Translations(), 1, "doc1-en should have one translation, excluding itself") + + doc2 := enSite.RegularPages[1] + permalink = doc2.Permalink() + require.NoError(t, err, "permalink call failed") + require.Equal(t, "http://example.com/blog/en/sect/doc2/", permalink, "invalid doc2 permalink") + + doc3 := enSite.RegularPages[2] + permalink = doc3.Permalink() + require.NoError(t, err, "permalink call failed") + // Note that /superbob is a custom URL set in frontmatter. + // We respect that URL literally (it can be /search.json) + // and do no not do any language code prefixing. + require.Equal(t, "http://example.com/blog/superbob/", permalink, "invalid doc3 permalink") + + require.Equal(t, "/superbob", doc3.URL(), "invalid url, was specified on doc3") + th.assertFileContent("public/superbob/index.html", "doc3|Hello|en") + require.Equal(t, doc2.Next, doc3, "doc3 should follow doc2, in .Next") + + doc1fr := doc1en.Translations()[0] + permalink = doc1fr.Permalink() + require.NoError(t, err, "permalink call failed") + require.Equal(t, "http://example.com/blog/fr/sect/doc1/", permalink, "invalid doc1fr permalink") + + require.Equal(t, doc1en.Translations()[0], doc1fr, "doc1-en should have doc1-fr as translation") + require.Equal(t, doc1fr.Translations()[0], doc1en, "doc1-fr should have doc1-en as translation") + require.Equal(t, "fr", doc1fr.Language().Lang) + + doc4 := enSite.AllPages[4] + permalink = doc4.Permalink() + require.Equal(t, "http://example.com/blog/fr/sect/doc4/", permalink, "invalid doc4 permalink") + require.Equal(t, "/blog/fr/sect/doc4/", doc4.URL()) + + require.Len(t, doc4.Translations(), 0, "found translations for doc4") + + doc5 := enSite.AllPages[5] + permalink = doc5.Permalink() + require.Equal(t, "http://example.com/blog/fr/somewhere/else/doc5/", permalink, "invalid doc5 permalink") + + // Taxonomies and their URLs + require.Len(t, enSite.Taxonomies, 1, "should have 1 taxonomy") + tags := enSite.Taxonomies["tags"] + require.Len(t, tags, 2, "should have 2 different tags") + require.Equal(t, tags["tag1"][0].Page, doc1en, "first tag1 page should be doc1") + + frSite := sites.Sites[1] + + require.Equal(t, "fr", frSite.Language.Lang) + require.Len(t, frSite.RegularPages, 3, "should have 3 pages") + require.Len(t, frSite.AllPages, 28, "should have 28 total pages (including translations and nodes)") + + for _, frenchPage := range frSite.RegularPages { + require.Equal(t, "fr", frenchPage.Lang()) + } + + // Check redirect to main language, French + languageRedirect := readDestination(t, fs, "public/index.html") + require.True(t, strings.Contains(languageRedirect, "0; url=http://example.com/blog/fr"), languageRedirect) + + // check home page content (including data files rendering) + th.assertFileContent("public/en/index.html", "Home Page 1", "Hello", "Hugo Rocks!") + th.assertFileContent("public/fr/index.html", "Home Page 1", "Bonjour", "Hugo Rocks!") + + // check single page content + th.assertFileContent("public/fr/sect/doc1/index.html", "Single", "Shortcode: Bonjour") + th.assertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Shortcode: Hello") + + // Check node translations + homeEn := enSite.getPage(KindHome) + require.NotNil(t, homeEn) + require.Len(t, homeEn.Translations(), 3) + require.Equal(t, "fr", homeEn.Translations()[0].Lang()) + require.Equal(t, "nn", homeEn.Translations()[1].Lang()) + require.Equal(t, "På nynorsk", homeEn.Translations()[1].Title) + require.Equal(t, "nb", homeEn.Translations()[2].Lang()) + require.Equal(t, "På bokmål", homeEn.Translations()[2].Title, configSuffix) + require.Equal(t, "Bokmål", homeEn.Translations()[2].Language().LanguageName, configSuffix) + + sectFr := frSite.getPage(KindSection, "sect") + require.NotNil(t, sectFr) + + require.Equal(t, "fr", sectFr.Lang()) + require.Len(t, sectFr.Translations(), 1) + require.Equal(t, "en", sectFr.Translations()[0].Lang()) + require.Equal(t, "Sects", sectFr.Translations()[0].Title) + + nnSite := sites.Sites[2] + require.Equal(t, "nn", nnSite.Language.Lang) + taxNn := nnSite.getPage(KindTaxonomyTerm, "lag") + require.NotNil(t, taxNn) + require.Len(t, taxNn.Translations(), 1) + require.Equal(t, "nb", taxNn.Translations()[0].Lang()) + + taxTermNn := nnSite.getPage(KindTaxonomy, "lag", "sogndal") + require.NotNil(t, taxTermNn) + require.Len(t, taxTermNn.Translations(), 1) + require.Equal(t, "nb", taxTermNn.Translations()[0].Lang()) + + // Check sitemap(s) + sitemapIndex := readDestination(t, fs, "public/sitemap.xml") + require.True(t, strings.Contains(sitemapIndex, "<loc>http://example.com/blog/en/sitemap.xml</loc>"), sitemapIndex) + require.True(t, strings.Contains(sitemapIndex, "<loc>http://example.com/blog/fr/sitemap.xml</loc>"), sitemapIndex) + sitemapEn := readDestination(t, fs, "public/en/sitemap.xml") + sitemapFr := readDestination(t, fs, "public/fr/sitemap.xml") + require.True(t, strings.Contains(sitemapEn, "http://example.com/blog/en/sect/doc2/"), sitemapEn) + require.True(t, strings.Contains(sitemapFr, "http://example.com/blog/fr/sect/doc1/"), sitemapFr) + + // Check taxonomies + enTags := enSite.Taxonomies["tags"] + frTags := frSite.Taxonomies["plaques"] + require.Len(t, enTags, 2, fmt.Sprintf("Tags in en: %v", enTags)) + require.Len(t, frTags, 2, fmt.Sprintf("Tags in fr: %v", frTags)) + require.NotNil(t, enTags["tag1"]) + require.NotNil(t, frTags["frtag1"]) + readDestination(t, fs, "public/fr/plaques/frtag1/index.html") + readDestination(t, fs, "public/en/tags/tag1/index.html") + + // Check Blackfriday config + require.True(t, strings.Contains(string(doc1fr.Content), "«"), string(doc1fr.Content)) + require.False(t, strings.Contains(string(doc1en.Content), "«"), string(doc1en.Content)) + require.True(t, strings.Contains(string(doc1en.Content), "“"), string(doc1en.Content)) + + // Check that the drafts etc. are not built/processed/rendered. + assertShouldNotBuild(t, sites) + + // en and nn have custom site menus + require.Len(t, frSite.Menus, 0, "fr: "+configSuffix) + require.Len(t, enSite.Menus, 1, "en: "+configSuffix) + require.Len(t, nnSite.Menus, 1, "nn: "+configSuffix) + + require.Equal(t, "Home", enSite.Menus["main"].ByName()[0].Name) + require.Equal(t, "Heim", nnSite.Menus["main"].ByName()[0].Name) + + // Issue #1302 + require.Equal(t, template.URL(""), enSite.RegularPages[0].RSSLink()) + + // Issue #3108 + next := enSite.RegularPages[0].Next + require.NotNil(t, next) + require.Equal(t, KindPage, next.Kind) + + for { + if next == nil { + break + } + require.Equal(t, KindPage, next.Kind) + next = next.Next + } + +} + +func TestMultiSitesRebuild(t *testing.T) { + // t.Parallel() not supported, see https://github.com/fortytw2/leaktest/issues/4 + // This leaktest seems to be a little bit shaky on Travis. + if !isCI() { + defer leaktest.CheckTimeout(t, 30*time.Second)() + } + siteConfig := testSiteConfig{Fs: afero.NewMemMapFs(), DefaultContentLanguage: "fr", DefaultContentLanguageInSubdir: true} + sites := createMultiTestSites(t, siteConfig, multiSiteTOMLConfigTemplate) + fs := sites.Fs + cfg := BuildCfg{Watching: true} + th := testHelper{sites.Cfg, fs, t} + + err := sites.Build(cfg) + + if err != nil { + t.Fatalf("Failed to build sites: %s", err) + } + + _, err = fs.Destination.Open("public/en/sect/doc2/index.html") + + if err != nil { + t.Fatalf("Unable to locate file") + } + + enSite := sites.Sites[0] + frSite := sites.Sites[1] + + require.Len(t, enSite.RegularPages, 4) + require.Len(t, frSite.RegularPages, 3) + + // Verify translations + th.assertFileContent("public/en/sect/doc1-slug/index.html", "Hello") + th.assertFileContent("public/fr/sect/doc1/index.html", "Bonjour") + + // check single page content + th.assertFileContent("public/fr/sect/doc1/index.html", "Single", "Shortcode: Bonjour") + th.assertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Shortcode: Hello") + + for i, this := range []struct { + preFunc func(t *testing.T) + events []fsnotify.Event + assertFunc func(t *testing.T) + }{ + // * Remove doc + // * Add docs existing languages + // (Add doc new language: TODO(bep) we should load config.toml as part of these so we can add languages). + // * Rename file + // * Change doc + // * Change a template + // * Change language file + { + nil, + []fsnotify.Event{{Name: "content/sect/doc2.en.md", Op: fsnotify.Remove}}, + func(t *testing.T) { + require.Len(t, enSite.RegularPages, 3, "1 en removed") + + // Check build stats + require.Equal(t, 1, enSite.draftCount, "Draft") + require.Equal(t, 1, enSite.futureCount, "Future") + require.Equal(t, 1, enSite.expiredCount, "Expired") + require.Equal(t, 0, frSite.draftCount, "Draft") + require.Equal(t, 1, frSite.futureCount, "Future") + require.Equal(t, 1, frSite.expiredCount, "Expired") + }, + }, + { + func(t *testing.T) { + writeNewContentFile(t, fs, "new_en_1", "2016-07-31", "content/new1.en.md", -5) + writeNewContentFile(t, fs, "new_en_2", "1989-07-30", "content/new2.en.md", -10) + writeNewContentFile(t, fs, "new_fr_1", "2016-07-30", "content/new1.fr.md", 10) + }, + []fsnotify.Event{ + {Name: "content/new1.en.md", Op: fsnotify.Create}, + {Name: "content/new2.en.md", Op: fsnotify.Create}, + {Name: "content/new1.fr.md", Op: fsnotify.Create}, + }, + func(t *testing.T) { + require.Len(t, enSite.RegularPages, 5) + require.Len(t, enSite.AllPages, 30) + require.Len(t, frSite.RegularPages, 4) + require.Equal(t, "new_fr_1", frSite.RegularPages[3].Title) + require.Equal(t, "new_en_2", enSite.RegularPages[0].Title) + require.Equal(t, "new_en_1", enSite.RegularPages[1].Title) + + rendered := readDestination(t, fs, "public/en/new1/index.html") + require.True(t, strings.Contains(rendered, "new_en_1"), rendered) + }, + }, + { + func(t *testing.T) { + p := "content/sect/doc1.en.md" + doc1 := readSource(t, fs, p) + doc1 += "CHANGED" + writeSource(t, fs, p, doc1) + }, + []fsnotify.Event{{Name: "content/sect/doc1.en.md", Op: fsnotify.Write}}, + func(t *testing.T) { + require.Len(t, enSite.RegularPages, 5) + doc1 := readDestination(t, fs, "public/en/sect/doc1-slug/index.html") + require.True(t, strings.Contains(doc1, "CHANGED"), doc1) + + }, + }, + // Rename a file + { + func(t *testing.T) { + if err := fs.Source.Rename("content/new1.en.md", "content/new1renamed.en.md"); err != nil { + t.Fatalf("Rename failed: %s", err) + } + }, + []fsnotify.Event{ + {Name: "content/new1renamed.en.md", Op: fsnotify.Rename}, + {Name: "content/new1.en.md", Op: fsnotify.Rename}, + }, + func(t *testing.T) { + require.Len(t, enSite.RegularPages, 5, "Rename") + require.Equal(t, "new_en_1", enSite.RegularPages[1].Title) + rendered := readDestination(t, fs, "public/en/new1renamed/index.html") + require.True(t, strings.Contains(rendered, "new_en_1"), rendered) + }}, + { + // Change a template + func(t *testing.T) { + template := "layouts/_default/single.html" + templateContent := readSource(t, fs, template) + templateContent += "{{ print \"Template Changed\"}}" + writeSource(t, fs, template, templateContent) + }, + []fsnotify.Event{{Name: "layouts/_default/single.html", Op: fsnotify.Write}}, + func(t *testing.T) { + require.Len(t, enSite.RegularPages, 5) + require.Len(t, enSite.AllPages, 30) + require.Len(t, frSite.RegularPages, 4) + doc1 := readDestination(t, fs, "public/en/sect/doc1-slug/index.html") + require.True(t, strings.Contains(doc1, "Template Changed"), doc1) + }, + }, + { + // Change a language file + func(t *testing.T) { + languageFile := "i18n/fr.yaml" + langContent := readSource(t, fs, languageFile) + langContent = strings.Replace(langContent, "Bonjour", "Salut", 1) + writeSource(t, fs, languageFile, langContent) + }, + []fsnotify.Event{{Name: "i18n/fr.yaml", Op: fsnotify.Write}}, + func(t *testing.T) { + require.Len(t, enSite.RegularPages, 5) + require.Len(t, enSite.AllPages, 30) + require.Len(t, frSite.RegularPages, 4) + docEn := readDestination(t, fs, "public/en/sect/doc1-slug/index.html") + require.True(t, strings.Contains(docEn, "Hello"), "No Hello") + docFr := readDestination(t, fs, "public/fr/sect/doc1/index.html") + require.True(t, strings.Contains(docFr, "Salut"), "No Salut") + + homeEn := enSite.getPage(KindHome) + require.NotNil(t, homeEn) + require.Len(t, homeEn.Translations(), 3) + require.Equal(t, "fr", homeEn.Translations()[0].Lang()) + + }, + }, + // Change a shortcode + { + func(t *testing.T) { + writeSource(t, fs, "layouts/shortcodes/shortcode.html", "Modified Shortcode: {{ i18n \"hello\" }}") + }, + []fsnotify.Event{ + {Name: "layouts/shortcodes/shortcode.html", Op: fsnotify.Write}, + }, + func(t *testing.T) { + require.Len(t, enSite.RegularPages, 5) + require.Len(t, enSite.AllPages, 30) + require.Len(t, frSite.RegularPages, 4) + th.assertFileContent("public/fr/sect/doc1/index.html", "Single", "Modified Shortcode: Salut") + th.assertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Modified Shortcode: Hello") + }, + }, + } { + + if this.preFunc != nil { + this.preFunc(t) + } + + err = sites.Build(cfg, this.events...) + + if err != nil { + t.Fatalf("[%d] Failed to rebuild sites: %s", i, err) + } + + this.assertFunc(t) + } + + // Check that the drafts etc. are not built/processed/rendered. + assertShouldNotBuild(t, sites) + +} + +func assertShouldNotBuild(t *testing.T, sites *HugoSites) { + s := sites.Sites[0] + + for _, p := range s.rawAllPages { + // No HTML when not processed + require.Equal(t, p.shouldBuild(), bytes.Contains(p.workContent, []byte("</")), p.BaseFileName()+": "+string(p.workContent)) + require.Equal(t, p.shouldBuild(), p.Content != "", p.BaseFileName()) + + require.Equal(t, p.shouldBuild(), p.Content != "", p.BaseFileName()) + + } +} + +func TestAddNewLanguage(t *testing.T) { + t.Parallel() + siteConfig := testSiteConfig{Fs: afero.NewMemMapFs(), DefaultContentLanguage: "fr", DefaultContentLanguageInSubdir: true} + + sites := createMultiTestSites(t, siteConfig, multiSiteTOMLConfigTemplate) + cfg := BuildCfg{} + + err := sites.Build(cfg) + + if err != nil { + t.Fatalf("Failed to build sites: %s", err) + } + + fs := sites.Fs + + newConfig := multiSiteTOMLConfigTemplate + ` + +[Languages.sv] +weight = 15 +title = "Svenska" +` + + newConfig = createConfig(t, siteConfig, newConfig) + + writeNewContentFile(t, fs, "Swedish Contentfile", "2016-01-01", "content/sect/doc1.sv.md", 10) + // replace the config + writeSource(t, fs, "multilangconfig.toml", newConfig) + + // Watching does not work with in-memory fs, so we trigger a reload manually + require.NoError(t, sites.Cfg.(*helpers.Language).Cfg.(*viper.Viper).ReadInConfig()) + err = sites.Build(BuildCfg{CreateSitesFromConfig: true}) + + if err != nil { + t.Fatalf("Failed to rebuild sites: %s", err) + } + + require.Len(t, sites.Sites, 5, fmt.Sprintf("Len %d", len(sites.Sites))) + + // The Swedish site should be put in the middle (language weight=15) + enSite := sites.Sites[0] + svSite := sites.Sites[1] + frSite := sites.Sites[2] + require.True(t, enSite.Language.Lang == "en", enSite.Language.Lang) + require.True(t, svSite.Language.Lang == "sv", svSite.Language.Lang) + require.True(t, frSite.Language.Lang == "fr", frSite.Language.Lang) + + homeEn := enSite.getPage(KindHome) + require.NotNil(t, homeEn) + require.Len(t, homeEn.Translations(), 4) + require.Equal(t, "sv", homeEn.Translations()[0].Lang()) + + require.Len(t, enSite.RegularPages, 4) + require.Len(t, frSite.RegularPages, 3) + + // Veriy Swedish site + require.Len(t, svSite.RegularPages, 1) + svPage := svSite.RegularPages[0] + require.Equal(t, "Swedish Contentfile", svPage.Title) + require.Equal(t, "sv", svPage.Lang()) + require.Len(t, svPage.Translations(), 2) + require.Len(t, svPage.AllTranslations(), 3) + require.Equal(t, "en", svPage.Translations()[0].Lang()) + + // Regular pages have no children + require.Len(t, svPage.Pages, 0) + require.Len(t, svPage.Data["Pages"], 0) + +} + +func TestChangeDefaultLanguage(t *testing.T) { + t.Parallel() + mf := afero.NewMemMapFs() + + sites := createMultiTestSites(t, testSiteConfig{Fs: mf, DefaultContentLanguage: "fr", DefaultContentLanguageInSubdir: false}, multiSiteTOMLConfigTemplate) + + require.Equal(t, mf, sites.Fs.Source) + + cfg := BuildCfg{} + fs := sites.Fs + th := testHelper{sites.Cfg, fs, t} + + err := sites.Build(cfg) + + if err != nil { + t.Fatalf("Failed to build sites: %s", err) + } + + th.assertFileContent("public/sect/doc1/index.html", "Single", "Bonjour") + th.assertFileContent("public/en/sect/doc2/index.html", "Single", "Hello") + + newConfig := createConfig(t, testSiteConfig{Fs: mf, DefaultContentLanguage: "en", DefaultContentLanguageInSubdir: false}, multiSiteTOMLConfigTemplate) + + // replace the config + writeSource(t, fs, "multilangconfig.toml", newConfig) + + // Watching does not work with in-memory fs, so we trigger a reload manually + // This does not look pretty, so we should think of something else. + require.NoError(t, th.Cfg.(*helpers.Language).Cfg.(*viper.Viper).ReadInConfig()) + err = sites.Build(BuildCfg{CreateSitesFromConfig: true}) + + if err != nil { + t.Fatalf("Failed to rebuild sites: %s", err) + } + + // Default language is now en, so that should now be the "root" language + th.assertFileContent("public/fr/sect/doc1/index.html", "Single", "Bonjour") + th.assertFileContent("public/sect/doc2/index.html", "Single", "Hello") +} + +func TestTableOfContentsInShortcodes(t *testing.T) { + t.Parallel() + mf := afero.NewMemMapFs() + + writeToFs(t, mf, "layouts/shortcodes/toc.html", tocShortcode) + writeToFs(t, mf, "content/post/simple.en.md", tocPageSimple) + writeToFs(t, mf, "content/post/withSCInHeading.en.md", tocPageWithShortcodesInHeadings) + + sites := createMultiTestSites(t, testSiteConfig{Fs: mf, DefaultContentLanguage: "en", DefaultContentLanguageInSubdir: true}, multiSiteTOMLConfigTemplate) + + cfg := BuildCfg{} + + err := sites.Build(cfg) + + if err != nil { + t.Fatalf("Failed to build sites: %s", err) + } + + fs := sites.Fs + th := testHelper{sites.Cfg, fs, t} + + th.assertFileContent("public/en/post/simple/index.html", tocPageSimpleExpected) + th.assertFileContent("public/en/post/withSCInHeading/index.html", tocPageWithShortcodesInHeadingsExpected) +} + +var tocShortcode = ` +{{ .Page.TableOfContents }} +` + +var tocPageSimple = `--- +title: tocTest +publishdate: "2000-01-01" +--- + +{{< toc >}} + +# Heading 1 {#1} + +Some text. + +## Subheading 1.1 {#1-1} + +Some more text. + +# Heading 2 {#2} + +Even more text. + +## Subheading 2.1 {#2-1} + +Lorem ipsum... +` + +var tocPageSimpleExpected = `<nav id="TableOfContents"> +<ul> +<li><a href="#1">Heading 1</a> +<ul> +<li><a href="#1-1">Subheading 1.1</a></li> +</ul></li> +<li><a href="#2">Heading 2</a> +<ul> +<li><a href="#2-1">Subheading 2.1</a></li> +</ul></li> +</ul> +</nav>` + +var tocPageWithShortcodesInHeadings = `--- +title: tocTest +publishdate: "2000-01-01" +--- + +{{< toc >}} + +# Heading 1 {#1} + +Some text. + +## Subheading 1.1 {{< shortcode >}} {#1-1} + +Some more text. + +# Heading 2 {{% shortcode %}} {#2} + +Even more text. + +## Subheading 2.1 {#2-1} + +Lorem ipsum... +` + +var tocPageWithShortcodesInHeadingsExpected = `<nav id="TableOfContents"> +<ul> +<li><a href="#1">Heading 1</a> +<ul> +<li><a href="#1-1">Subheading 1.1 Shortcode: Hello</a></li> +</ul></li> +<li><a href="#2">Heading 2 Shortcode: Hello</a> +<ul> +<li><a href="#2-1">Subheading 2.1</a></li> +</ul></li> +</ul> +</nav>` + +var multiSiteTOMLConfigTemplate = ` +baseURL = "http://example.com/blog" +disableSitemap = false +disableRSS = false +rssURI = "index.xml" + +paginate = 1 +disablePathToLower = true +defaultContentLanguage = "{{ .DefaultContentLanguage }}" +defaultContentLanguageInSubdir = {{ .DefaultContentLanguageInSubdir }} + +[permalinks] +other = "/somewhere/else/:filename" + +[blackfriday] +angledQuotes = true + +[Taxonomies] +tag = "tags" + +[Languages] +[Languages.en] +weight = 10 +title = "In English" +languageName = "English" +[Languages.en.blackfriday] +angledQuotes = false +[[Languages.en.menu.main]] +url = "/" +name = "Home" +weight = 0 + +[Languages.fr] +weight = 20 +title = "Le Français" +languageName = "Français" +[Languages.fr.Taxonomies] +plaque = "plaques" + +[Languages.nn] +weight = 30 +title = "På nynorsk" +languageName = "Nynorsk" +paginatePath = "side" +[Languages.nn.Taxonomies] +lag = "lag" +[[Languages.nn.menu.main]] +url = "/" +name = "Heim" +weight = 1 + +[Languages.nb] +weight = 40 +title = "På bokmål" +languageName = "Bokmål" +paginatePath = "side" +[Languages.nb.Taxonomies] +lag = "lag" +` + +var multiSiteYAMLConfigTemplate = ` +baseURL: "http://example.com/blog" +disableSitemap: false +disableRSS: false +rssURI: "index.xml" + +disablePathToLower: true +paginate: 1 +defaultContentLanguage: "{{ .DefaultContentLanguage }}" +defaultContentLanguageInSubdir: {{ .DefaultContentLanguageInSubdir }} + +permalinks: + other: "/somewhere/else/:filename" + +blackfriday: + angledQuotes: true + +Taxonomies: + tag: "tags" + +Languages: + en: + weight: 10 + title: "In English" + languageName: "English" + blackfriday: + angledQuotes: false + menu: + main: + - url: "/" + name: "Home" + weight: 0 + fr: + weight: 20 + title: "Le Français" + languageName: "Français" + Taxonomies: + plaque: "plaques" + nn: + weight: 30 + title: "På nynorsk" + languageName: "Nynorsk" + paginatePath: "side" + Taxonomies: + lag: "lag" + menu: + main: + - url: "/" + name: "Heim" + weight: 1 + nb: + weight: 40 + title: "På bokmål" + languageName: "Bokmål" + paginatePath: "side" + Taxonomies: + lag: "lag" + +` + +var multiSiteJSONConfigTemplate = ` +{ + "baseURL": "http://example.com/blog", + "disableSitemap": false, + "disableRSS": false, + "rssURI": "index.xml", + "paginate": 1, + "disablePathToLower": true, + "defaultContentLanguage": "{{ .DefaultContentLanguage }}", + "defaultContentLanguageInSubdir": true, + "permalinks": { + "other": "/somewhere/else/:filename" + }, + "blackfriday": { + "angledQuotes": true + }, + "Taxonomies": { + "tag": "tags" + }, + "Languages": { + "en": { + "weight": 10, + "title": "In English", + "languageName": "English", + "blackfriday": { + "angledQuotes": false + }, + "menu": { + "main": [ + { + "url": "/", + "name": "Home", + "weight": 0 + } + ] + } + }, + "fr": { + "weight": 20, + "title": "Le Français", + "languageName": "Français", + "Taxonomies": { + "plaque": "plaques" + } + }, + "nn": { + "weight": 30, + "title": "På nynorsk", + "paginatePath": "side", + "languageName": "Nynorsk", + "Taxonomies": { + "lag": "lag" + }, + "menu": { + "main": [ + { + "url": "/", + "name": "Heim", + "weight": 1 + } + ] + } + }, + "nb": { + "weight": 40, + "title": "På bokmål", + "paginatePath": "side", + "languageName": "Bokmål", + "Taxonomies": { + "lag": "lag" + } + } + } +} +` + +func createMultiTestSites(t *testing.T, siteConfig testSiteConfig, tomlConfigTemplate string) *HugoSites { + return createMultiTestSitesForConfig(t, siteConfig, tomlConfigTemplate, "toml") +} + +func createMultiTestSitesForConfig(t *testing.T, siteConfig testSiteConfig, configTemplate, configSuffix string) *HugoSites { + + configContent := createConfig(t, siteConfig, configTemplate) + + mf := siteConfig.Fs + + // Add some layouts + if err := afero.WriteFile(mf, + filepath.Join("layouts", "_default/single.html"), + []byte("Single: {{ .Title }}|{{ i18n \"hello\" }}|{{.Lang}}|{{ .Content }}"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) + } + + if err := afero.WriteFile(mf, + filepath.Join("layouts", "_default/list.html"), + []byte("{{ $p := .Paginator }}List Page {{ $p.PageNumber }}: {{ .Title }}|{{ i18n \"hello\" }}|{{ .Permalink }}"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) + } + + if err := afero.WriteFile(mf, + filepath.Join("layouts", "index.html"), + []byte("{{ $p := .Paginator }}Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) + } + + // Add a shortcode + if err := afero.WriteFile(mf, + filepath.Join("layouts", "shortcodes", "shortcode.html"), + []byte("Shortcode: {{ i18n \"hello\" }}"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) + } + + // Add some language files + if err := afero.WriteFile(mf, + filepath.Join("i18n", "en.yaml"), + []byte(` +hello: + other: "Hello" +`), + 0755); err != nil { + t.Fatalf("Failed to write language file: %s", err) + } + if err := afero.WriteFile(mf, + filepath.Join("i18n", "fr.yaml"), + []byte(` +hello: + other: "Bonjour" +`), + 0755); err != nil { + t.Fatalf("Failed to write language file: %s", err) + } + + // Sources + sources := []source.ByteSource{ + {Name: filepath.FromSlash("root.en.md"), Content: []byte(`--- +title: root +weight: 10000 +slug: root +publishdate: "2000-01-01" +--- +# root +`)}, + {Name: filepath.FromSlash("sect/doc1.en.md"), Content: []byte(`--- +title: doc1 +weight: 1 +slug: doc1-slug +tags: + - tag1 +publishdate: "2000-01-01" +--- +# doc1 +*some "content"* + +{{< shortcode >}} + +NOTE: slug should be used as URL +`)}, + {Name: filepath.FromSlash("sect/doc1.fr.md"), Content: []byte(`--- +title: doc1 +weight: 1 +plaques: + - frtag1 + - frtag2 +publishdate: "2000-01-04" +--- +# doc1 +*quelque "contenu"* + +{{< shortcode >}} + +NOTE: should be in the 'en' Page's 'Translations' field. +NOTE: date is after "doc3" +`)}, + {Name: filepath.FromSlash("sect/doc2.en.md"), Content: []byte(`--- +title: doc2 +weight: 2 +publishdate: "2000-01-02" +--- +# doc2 +*some content* +NOTE: without slug, "doc2" should be used, without ".en" as URL +`)}, + {Name: filepath.FromSlash("sect/doc3.en.md"), Content: []byte(`--- +title: doc3 +weight: 3 +publishdate: "2000-01-03" +tags: + - tag2 + - tag1 +url: /superbob +--- +# doc3 +*some content* +NOTE: third 'en' doc, should trigger pagination on home page. +`)}, + {Name: filepath.FromSlash("sect/doc4.md"), Content: []byte(`--- +title: doc4 +weight: 4 +plaques: + - frtag1 +publishdate: "2000-01-05" +--- +# doc4 +*du contenu francophone* +NOTE: should use the defaultContentLanguage and mark this doc as 'fr'. +NOTE: doesn't have any corresponding translation in 'en' +`)}, + {Name: filepath.FromSlash("other/doc5.fr.md"), Content: []byte(`--- +title: doc5 +weight: 5 +publishdate: "2000-01-06" +--- +# doc5 +*autre contenu francophone* +NOTE: should use the "permalinks" configuration with :filename +`)}, + // Add some for the stats + {Name: filepath.FromSlash("stats/expired.fr.md"), Content: []byte(`--- +title: expired +publishdate: "2000-01-06" +expiryDate: "2001-01-06" +--- +# Expired +`)}, + {Name: filepath.FromSlash("stats/future.fr.md"), Content: []byte(`--- +title: future +weight: 6 +publishdate: "2100-01-06" +--- +# Future +`)}, + {Name: filepath.FromSlash("stats/expired.en.md"), Content: []byte(`--- +title: expired +weight: 7 +publishdate: "2000-01-06" +expiryDate: "2001-01-06" +--- +# Expired +`)}, + {Name: filepath.FromSlash("stats/future.en.md"), Content: []byte(`--- +title: future +weight: 6 +publishdate: "2100-01-06" +--- +# Future +`)}, + {Name: filepath.FromSlash("stats/draft.en.md"), Content: []byte(`--- +title: expired +publishdate: "2000-01-06" +draft: true +--- +# Draft +`)}, + {Name: filepath.FromSlash("stats/tax.nn.md"), Content: []byte(`--- +title: Tax NN +weight: 8 +publishdate: "2000-01-06" +weight: 1001 +lag: +- Sogndal +--- +# Tax NN +`)}, + {Name: filepath.FromSlash("stats/tax.nb.md"), Content: []byte(`--- +title: Tax NB +weight: 8 +publishdate: "2000-01-06" +weight: 1002 +lag: +- Sogndal +--- +# Tax NB +`)}, + } + + configFile := "multilangconfig." + configSuffix + writeToFs(t, mf, configFile, configContent) + + cfg, err := LoadConfig(mf, "", configFile) + require.NoError(t, err) + + fs := hugofs.NewFrom(mf, cfg) + + // Hugo support using ByteSource's directly (for testing), + // but to make it more real, we write them to the mem file system. + for _, s := range sources { + if err := afero.WriteFile(mf, filepath.Join("content", s.Name), s.Content, 0755); err != nil { + t.Fatalf("Failed to write file: %s", err) + } + } + + // Add some data + writeSource(t, fs, "data/hugo.toml", "slogan = \"Hugo Rocks!\"") + + sites, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) //, Logger: newDebugLogger()}) + + if err != nil { + t.Fatalf("Failed to create sites: %s", err) + } + + if len(sites.Sites) != 4 { + t.Fatalf("Got %d sites", len(sites.Sites)) + } + + if sites.Fs.Source != mf { + t.Fatal("FS mismatch") + } + + return sites +} + +func writeSource(t testing.TB, fs *hugofs.Fs, filename, content string) { + writeToFs(t, fs.Source, filename, content) +} + +func writeToFs(t testing.TB, fs afero.Fs, filename, content string) { + if err := afero.WriteFile(fs, filepath.FromSlash(filename), []byte(content), 0755); err != nil { + t.Fatalf("Failed to write file: %s", err) + } +} + +func readDestination(t testing.TB, fs *hugofs.Fs, filename string) string { + return readFileFromFs(t, fs.Destination, filename) +} + +func destinationExists(fs *hugofs.Fs, filename string) bool { + b, err := helpers.Exists(filename, fs.Destination) + if err != nil { + panic(err) + } + return b +} + +func readSource(t *testing.T, fs *hugofs.Fs, filename string) string { + return readFileFromFs(t, fs.Source, filename) +} + +func readFileFromFs(t testing.TB, fs afero.Fs, filename string) string { + filename = filepath.FromSlash(filename) + b, err := afero.ReadFile(fs, filename) + if err != nil { + // Print some debug info + root := strings.Split(filename, helpers.FilePathSeparator)[0] + afero.Walk(fs, root, func(path string, info os.FileInfo, err error) error { + if info != nil && !info.IsDir() { + fmt.Println(" ", path) + } + + return nil + }) + t.Fatalf("Failed to read file: %s", err) + } + return string(b) +} + +const testPageTemplate = `--- +title: "%s" +publishdate: "%s" +weight: %d +--- +# Doc %s +` + +func newTestPage(title, date string, weight int) string { + return fmt.Sprintf(testPageTemplate, title, date, weight, title) +} + +func writeNewContentFile(t *testing.T, fs *hugofs.Fs, title, date, filename string, weight int) { + content := newTestPage(title, date, weight) + writeSource(t, fs, filename, content) +} + +func createConfig(t *testing.T, config testSiteConfig, configTemplate string) string { + templ, err := template.New("test").Parse(configTemplate) + if err != nil { + t.Fatal("Template parse failed:", err) + } + var b bytes.Buffer + templ.Execute(&b, config) + return b.String() +} diff --git a/hugolib/media.go b/hugolib/media.go new file mode 100644 index 000000000..aae9a7870 --- /dev/null +++ b/hugolib/media.go @@ -0,0 +1,60 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +// An Image contains metadata for images + image sitemaps +// https://support.google.com/webmasters/answer/178636?hl=en +type Image struct { + + // The URL of the image. In some cases, the image URL may not be on the + // same domain as your main site. This is fine, as long as both domains + // are verified in Webmaster Tools. If, for example, you use a + // content delivery network (CDN) to host your images, make sure that the + // hosting site is verified in Webmaster Tools OR that you submit your + // sitemap using robots.txt. In addition, make sure that your robots.txt + // file doesn’t disallow the crawling of any content you want indexed. + URL string + Title string + Caption string + AltText string + + // The geographic location of the image. For example, + // <image:geo_location>Limerick, Ireland</image:geo_location>. + GeoLocation string + + // A URL to the license of the image. + License string +} + +// A Video contains metadata for videos + video sitemaps +// https://support.google.com/webmasters/answer/80471?hl=en +type Video struct { + ThumbnailLoc string + Title string + Description string + ContentLoc string + PlayerLoc string + Duration string + ExpirationDate string + Rating string + ViewCount string + PublicationDate string + FamilyFriendly string + Restriction string + GalleryLoc string + Price string + RequiresSubscription string + Uploader string + Live string +} diff --git a/hugolib/menu.go b/hugolib/menu.go new file mode 100644 index 000000000..4f6bd2b4e --- /dev/null +++ b/hugolib/menu.go @@ -0,0 +1,215 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "html/template" + "sort" + "strings" + + "github.com/spf13/cast" +) + +// MenuEntry represents a menu item defined in either Page front matter +// or in the site config. +type MenuEntry struct { + URL string + Name string + Menu string + Identifier string + Pre template.HTML + Post template.HTML + Weight int + Parent string + Children Menu +} + +// Menu is a collection of menu entries. +type Menu []*MenuEntry + +// Menus is a dictionary of menus. +type Menus map[string]*Menu + +// PageMenus is a dictionary of menus defined in the Pages. +type PageMenus map[string]*MenuEntry + +// addChild adds a new child to this menu entry. +// The default sort order will then be applied. +func (m *MenuEntry) addChild(child *MenuEntry) { + m.Children = append(m.Children, child) + m.Children.Sort() +} + +// HasChildren returns whether this menu item has any children. +func (m *MenuEntry) HasChildren() bool { + return m.Children != nil +} + +// KeyName returns the key used to identify this menu entry. +func (m *MenuEntry) KeyName() string { + if m.Identifier != "" { + return m.Identifier + } + return m.Name +} + +func (m *MenuEntry) hopefullyUniqueID() string { + if m.Identifier != "" { + return m.Identifier + } else if m.URL != "" { + return m.URL + } else { + return m.Name + } +} + +// IsEqual returns whether the two menu entries represents the same menu entry. +func (m *MenuEntry) IsEqual(inme *MenuEntry) bool { + return m.hopefullyUniqueID() == inme.hopefullyUniqueID() && m.Parent == inme.Parent +} + +// IsSameResource returns whether the two menu entries points to the same +// resource (URL). +func (m *MenuEntry) IsSameResource(inme *MenuEntry) bool { + return m.URL != "" && inme.URL != "" && m.URL == inme.URL +} + +func (m *MenuEntry) marshallMap(ime map[string]interface{}) { + for k, v := range ime { + loki := strings.ToLower(k) + switch loki { + case "url": + m.URL = cast.ToString(v) + case "weight": + m.Weight = cast.ToInt(v) + case "name": + m.Name = cast.ToString(v) + case "pre": + m.Pre = template.HTML(cast.ToString(v)) + case "post": + m.Post = template.HTML(cast.ToString(v)) + case "identifier": + m.Identifier = cast.ToString(v) + case "parent": + m.Parent = cast.ToString(v) + } + } +} + +func (m Menu) add(me *MenuEntry) Menu { + app := func(slice Menu, x ...*MenuEntry) Menu { + n := len(slice) + len(x) + if n > cap(slice) { + size := cap(slice) * 2 + if size < n { + size = n + } + new := make(Menu, size) + copy(new, slice) + slice = new + } + slice = slice[0:n] + copy(slice[n-len(x):], x) + return slice + } + + m = app(m, me) + m.Sort() + return m +} + +/* + * Implementation of a custom sorter for Menu + */ + +// A type to implement the sort interface for Menu +type menuSorter struct { + menu Menu + by menuEntryBy +} + +// Closure used in the Sort.Less method. +type menuEntryBy func(m1, m2 *MenuEntry) bool + +func (by menuEntryBy) Sort(menu Menu) { + ms := &menuSorter{ + menu: menu, + by: by, // The Sort method's receiver is the function (closure) that defines the sort order. + } + sort.Stable(ms) +} + +var defaultMenuEntrySort = func(m1, m2 *MenuEntry) bool { + if m1.Weight == m2.Weight { + if m1.Name == m2.Name { + return m1.Identifier < m2.Identifier + } + return m1.Name < m2.Name + } + + if m2.Weight == 0 { + return true + } + + if m1.Weight == 0 { + return false + } + + return m1.Weight < m2.Weight +} + +func (ms *menuSorter) Len() int { return len(ms.menu) } +func (ms *menuSorter) Swap(i, j int) { ms.menu[i], ms.menu[j] = ms.menu[j], ms.menu[i] } + +// Less is part of sort.Interface. It is implemented by calling the "by" closure in the sorter. +func (ms *menuSorter) Less(i, j int) bool { return ms.by(ms.menu[i], ms.menu[j]) } + +// Sort sorts the menu by weight, name and then by identifier. +func (m Menu) Sort() Menu { + menuEntryBy(defaultMenuEntrySort).Sort(m) + return m +} + +// Limit limits the returned menu to n entries. +func (m Menu) Limit(n int) Menu { + if len(m) > n { + return m[0:n] + } + return m +} + +// ByWeight sorts the menu by the weight defined in the menu configuration. +func (m Menu) ByWeight() Menu { + menuEntryBy(defaultMenuEntrySort).Sort(m) + return m +} + +// ByName sorts the menu by the name defined in the menu configuration. +func (m Menu) ByName() Menu { + title := func(m1, m2 *MenuEntry) bool { + return m1.Name < m2.Name + } + + menuEntryBy(title).Sort(m) + return m +} + +// Reverse reverses the order of the menu entries. +func (m Menu) Reverse() Menu { + for i, j := 0, len(m)-1; i < j; i, j = i+1, j-1 { + m[i], m[j] = m[j], m[i] + } + + return m +} diff --git a/hugolib/menu_old_test.go b/hugolib/menu_old_test.go new file mode 100644 index 000000000..7c49ed908 --- /dev/null +++ b/hugolib/menu_old_test.go @@ -0,0 +1,642 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +// TODO(bep) remove this file when the reworked tests in menu_test.go is done. +// NOTE: Do not add more tests to this file! + +import ( + "fmt" + "strings" + "testing" + + "github.com/gohugoio/hugo/deps" + + "path/filepath" + + "github.com/BurntSushi/toml" + "github.com/gohugoio/hugo/source" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + confMenu1 = ` +[[menu.main]] + name = "Go Home" + url = "/" + weight = 1 + pre = "<div>" + post = "</div>" +[[menu.main]] + name = "Blog" + url = "/posts" +[[menu.main]] + name = "ext" + url = "http://gohugo.io" + identifier = "ext" +[[menu.main]] + name = "ext2" + url = "http://foo.local/Zoo/foo" + identifier = "ext2" +[[menu.grandparent]] + name = "grandparent" + url = "/grandparent" + identifier = "grandparentId" +[[menu.grandparent]] + name = "parent" + url = "/parent" + identifier = "parentId" + parent = "grandparentId" +[[menu.grandparent]] + name = "Go Home3" + url = "/" + identifier = "grandchildId" + parent = "parentId" +[[menu.tax]] + name = "Tax1" + url = "/two/key/" + identifier="1" +[[menu.tax]] + name = "Tax2" + url = "/two/key/" + identifier="2" +[[menu.tax]] + name = "Tax RSS" + url = "/two/key.xml" + identifier="xml" +[[menu.hash]] + name = "Tax With #" + url = "/resource#anchor" + identifier="hash" +[[menu.unicode]] + name = "Unicode Russian" + identifier = "unicode-russian" + url = "/новости-проекта"` // Russian => "news-project" +) + +var menuPage1 = []byte(`+++ +title = "One" +weight = 1 +[menu] + [menu.p_one] ++++ +Front Matter with Menu Pages`) + +var menuPage2 = []byte(`+++ +title = "Two" +weight = 2 +[menu] + [menu.p_one] + [menu.p_two] + identifier = "Two" + ++++ +Front Matter with Menu Pages`) + +var menuPage3 = []byte(`+++ +title = "Three" +weight = 3 +[menu] + [menu.p_two] + Name = "Three" + Parent = "Two" ++++ +Front Matter with Menu Pages`) + +var menuPage4 = []byte(`+++ +title = "Four" +weight = 4 +[menu] + [menu.p_two] + Name = "Four" + Parent = "Three" ++++ +Front Matter with Menu Pages`) + +var menuPageSources = []source.ByteSource{ + {Name: filepath.FromSlash("sect/doc1.md"), Content: menuPage1}, + {Name: filepath.FromSlash("sect/doc2.md"), Content: menuPage2}, + {Name: filepath.FromSlash("sect/doc3.md"), Content: menuPage3}, +} + +var menuPageSectionsSources = []source.ByteSource{ + {Name: filepath.FromSlash("first/doc1.md"), Content: menuPage1}, + {Name: filepath.FromSlash("first/doc2.md"), Content: menuPage2}, + {Name: filepath.FromSlash("second-section/doc3.md"), Content: menuPage3}, + {Name: filepath.FromSlash("Fish and Chips/doc4.md"), Content: menuPage4}, +} + +func tstCreateMenuPageWithNameTOML(title, menu, name string) []byte { + return []byte(fmt.Sprintf(`+++ +title = "%s" +weight = 1 +[menu] + [menu.%s] + name = "%s" ++++ +Front Matter with Menu with Name`, title, menu, name)) +} + +func tstCreateMenuPageWithIdentifierTOML(title, menu, identifier string) []byte { + return []byte(fmt.Sprintf(`+++ +title = "%s" +weight = 1 +[menu] + [menu.%s] + identifier = "%s" + name = "somename" ++++ +Front Matter with Menu with Identifier`, title, menu, identifier)) +} + +func tstCreateMenuPageWithNameYAML(title, menu, name string) []byte { + return []byte(fmt.Sprintf(`--- +title: "%s" +weight: 1 +menu: + %s: + name: "%s" +--- +Front Matter with Menu with Name`, title, menu, name)) +} + +func tstCreateMenuPageWithIdentifierYAML(title, menu, identifier string) []byte { + return []byte(fmt.Sprintf(`--- +title: "%s" +weight: 1 +menu: + %s: + identifier: "%s" + name: "somename" +--- +Front Matter with Menu with Identifier`, title, menu, identifier)) +} + +// Issue 817 - identifier should trump everything +func TestPageMenuWithIdentifier(t *testing.T) { + t.Parallel() + toml := []source.ByteSource{ + {Name: "sect/doc1.md", Content: tstCreateMenuPageWithIdentifierTOML("t1", "m1", "i1")}, + {Name: "sect/doc2.md", Content: tstCreateMenuPageWithIdentifierTOML("t1", "m1", "i2")}, + {Name: "sect/doc3.md", Content: tstCreateMenuPageWithIdentifierTOML("t1", "m1", "i2")}, // duplicate + } + + yaml := []source.ByteSource{ + {Name: "sect/doc1.md", Content: tstCreateMenuPageWithIdentifierYAML("t1", "m1", "i1")}, + {Name: "sect/doc2.md", Content: tstCreateMenuPageWithIdentifierYAML("t1", "m1", "i2")}, + {Name: "sect/doc3.md", Content: tstCreateMenuPageWithIdentifierYAML("t1", "m1", "i2")}, // duplicate + } + + doTestPageMenuWithIdentifier(t, toml) + doTestPageMenuWithIdentifier(t, yaml) + +} + +func doTestPageMenuWithIdentifier(t *testing.T, menuPageSources []source.ByteSource) { + + s := setupMenuTests(t, menuPageSources) + + assert.Equal(t, 3, len(s.RegularPages), "Not enough pages") + + me1 := findTestMenuEntryByID(s, "m1", "i1") + me2 := findTestMenuEntryByID(s, "m1", "i2") + + require.NotNil(t, me1) + require.NotNil(t, me2) + + assert.True(t, strings.Contains(me1.URL, "doc1"), me1.URL) + assert.True(t, strings.Contains(me2.URL, "doc2") || strings.Contains(me2.URL, "doc3"), me2.URL) + +} + +// Issue 817 contd - name should be second identifier in +func TestPageMenuWithDuplicateName(t *testing.T) { + t.Parallel() + toml := []source.ByteSource{ + {Name: "sect/doc1.md", Content: tstCreateMenuPageWithNameTOML("t1", "m1", "n1")}, + {Name: "sect/doc2.md", Content: tstCreateMenuPageWithNameTOML("t1", "m1", "n2")}, + {Name: "sect/doc3.md", Content: tstCreateMenuPageWithNameTOML("t1", "m1", "n2")}, // duplicate + } + + yaml := []source.ByteSource{ + {Name: "sect/doc1.md", Content: tstCreateMenuPageWithNameYAML("t1", "m1", "n1")}, + {Name: "sect/doc2.md", Content: tstCreateMenuPageWithNameYAML("t1", "m1", "n2")}, + {Name: "sect/doc3.md", Content: tstCreateMenuPageWithNameYAML("t1", "m1", "n2")}, // duplicate + } + + doTestPageMenuWithDuplicateName(t, toml) + doTestPageMenuWithDuplicateName(t, yaml) + +} + +func doTestPageMenuWithDuplicateName(t *testing.T, menuPageSources []source.ByteSource) { + + s := setupMenuTests(t, menuPageSources) + + assert.Equal(t, 3, len(s.RegularPages), "Not enough pages") + + me1 := findTestMenuEntryByName(s, "m1", "n1") + me2 := findTestMenuEntryByName(s, "m1", "n2") + + require.NotNil(t, me1) + require.NotNil(t, me2) + + assert.True(t, strings.Contains(me1.URL, "doc1"), me1.URL) + assert.True(t, strings.Contains(me2.URL, "doc2") || strings.Contains(me2.URL, "doc3"), me2.URL) + +} + +func TestPageMenu(t *testing.T) { + t.Parallel() + s := setupMenuTests(t, menuPageSources) + + if len(s.RegularPages) != 3 { + t.Fatalf("Posts not created, expected 3 got %d", len(s.RegularPages)) + } + + first := s.RegularPages[0] + second := s.RegularPages[1] + third := s.RegularPages[2] + + pOne := findTestMenuEntryByName(s, "p_one", "One") + pTwo := findTestMenuEntryByID(s, "p_two", "Two") + + for i, this := range []struct { + menu string + page *Page + menuItem *MenuEntry + isMenuCurrent bool + hasMenuCurrent bool + }{ + {"p_one", first, pOne, true, false}, + {"p_one", first, pTwo, false, false}, + {"p_one", second, pTwo, false, false}, + {"p_two", second, pTwo, true, false}, + {"p_two", third, pTwo, false, true}, + {"p_one", third, pTwo, false, false}, + } { + + if i != 4 { + continue + } + + isMenuCurrent := this.page.IsMenuCurrent(this.menu, this.menuItem) + hasMenuCurrent := this.page.HasMenuCurrent(this.menu, this.menuItem) + + if isMenuCurrent != this.isMenuCurrent { + t.Errorf("[%d] Wrong result from IsMenuCurrent: %v", i, isMenuCurrent) + } + + if hasMenuCurrent != this.hasMenuCurrent { + t.Errorf("[%d] Wrong result for menuItem %v for HasMenuCurrent: %v", i, this.menuItem, hasMenuCurrent) + } + + } + +} + +func TestMenuURL(t *testing.T) { + t.Parallel() + s := setupMenuTests(t, menuPageSources) + + for i, this := range []struct { + me *MenuEntry + expectedURL string + }{ + // issue #888 + {findTestMenuEntryByID(s, "hash", "hash"), "/Zoo/resource#anchor"}, + // issue #1774 + {findTestMenuEntryByID(s, "main", "ext"), "http://gohugo.io"}, + {findTestMenuEntryByID(s, "main", "ext2"), "http://foo.local/Zoo/foo"}, + } { + + if this.me == nil { + t.Errorf("[%d] MenuEntry not found", i) + continue + } + + if this.me.URL != this.expectedURL { + t.Errorf("[%d] Got URL %s expected %s", i, this.me.URL, this.expectedURL) + } + + } + +} + +// Issue #1934 +func TestYAMLMenuWithMultipleEntries(t *testing.T) { + t.Parallel() + ps1 := []byte(`--- +title: "Yaml 1" +weight: 5 +menu: ["p_one", "p_two"] +--- +Yaml Front Matter with Menu Pages`) + + ps2 := []byte(`--- +title: "Yaml 2" +weight: 5 +menu: + p_three: + p_four: +--- +Yaml Front Matter with Menu Pages`) + + s := setupMenuTests(t, []source.ByteSource{ + {Name: filepath.FromSlash("sect/yaml1.md"), Content: ps1}, + {Name: filepath.FromSlash("sect/yaml2.md"), Content: ps2}}) + + p1 := s.RegularPages[0] + assert.Len(t, p1.Menus(), 2, "List YAML") + p2 := s.RegularPages[1] + assert.Len(t, p2.Menus(), 2, "Map YAML") + +} + +// issue #719 +func TestMenuWithUnicodeURLs(t *testing.T) { + t.Parallel() + for _, canonifyURLs := range []bool{true, false} { + doTestMenuWithUnicodeURLs(t, canonifyURLs) + } +} + +func doTestMenuWithUnicodeURLs(t *testing.T, canonifyURLs bool) { + + s := setupMenuTests(t, menuPageSources, "canonifyURLs", canonifyURLs) + + unicodeRussian := findTestMenuEntryByID(s, "unicode", "unicode-russian") + + expected := "/%D0%BD%D0%BE%D0%B2%D0%BE%D1%81%D1%82%D0%B8-%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B0" + + if !canonifyURLs { + expected = "/Zoo" + expected + } + + assert.Equal(t, expected, unicodeRussian.URL) +} + +// Issue #1114 +func TestSectionPagesMenu2(t *testing.T) { + t.Parallel() + doTestSectionPagesMenu(true, t) + doTestSectionPagesMenu(false, t) +} + +func doTestSectionPagesMenu(canonifyURLs bool, t *testing.T) { + + s := setupMenuTests(t, menuPageSectionsSources, + "sectionPagesMenu", "spm", + "canonifyURLs", canonifyURLs, + ) + + sects := s.getPage(KindHome).Sections() + + require.Equal(t, 3, len(sects)) + + firstSectionPages := s.getPage(KindSection, "first").Pages + require.Equal(t, 2, len(firstSectionPages)) + secondSectionPages := s.getPage(KindSection, "second-section").Pages + require.Equal(t, 1, len(secondSectionPages)) + fishySectionPages := s.getPage(KindSection, "Fish and Chips").Pages + require.Equal(t, 1, len(fishySectionPages)) + + nodeFirst := s.getPage(KindSection, "first") + require.NotNil(t, nodeFirst) + nodeSecond := s.getPage(KindSection, "second-section") + require.NotNil(t, nodeSecond) + nodeFishy := s.getPage(KindSection, "Fish and Chips") + require.Equal(t, "Fish and Chips", nodeFishy.sections[0]) + + firstSectionMenuEntry := findTestMenuEntryByID(s, "spm", "first") + secondSectionMenuEntry := findTestMenuEntryByID(s, "spm", "second-section") + fishySectionMenuEntry := findTestMenuEntryByID(s, "spm", "Fish and Chips") + + require.NotNil(t, firstSectionMenuEntry) + require.NotNil(t, secondSectionMenuEntry) + require.NotNil(t, nodeFirst) + require.NotNil(t, nodeSecond) + require.NotNil(t, fishySectionMenuEntry) + require.NotNil(t, nodeFishy) + + require.True(t, nodeFirst.IsMenuCurrent("spm", firstSectionMenuEntry)) + require.False(t, nodeFirst.IsMenuCurrent("spm", secondSectionMenuEntry)) + require.False(t, nodeFirst.IsMenuCurrent("spm", fishySectionMenuEntry)) + require.True(t, nodeFishy.IsMenuCurrent("spm", fishySectionMenuEntry)) + require.Equal(t, "Fish and Chips", fishySectionMenuEntry.Name) + + for _, p := range firstSectionPages { + require.True(t, p.HasMenuCurrent("spm", firstSectionMenuEntry)) + require.False(t, p.HasMenuCurrent("spm", secondSectionMenuEntry)) + } + + for _, p := range secondSectionPages { + require.False(t, p.HasMenuCurrent("spm", firstSectionMenuEntry)) + require.True(t, p.HasMenuCurrent("spm", secondSectionMenuEntry)) + } + + for _, p := range fishySectionPages { + require.False(t, p.HasMenuCurrent("spm", firstSectionMenuEntry)) + require.False(t, p.HasMenuCurrent("spm", secondSectionMenuEntry)) + require.True(t, p.HasMenuCurrent("spm", fishySectionMenuEntry)) + } +} + +func TestMenuLimit(t *testing.T) { + t.Parallel() + s := setupMenuTests(t, menuPageSources) + m := *s.Menus["main"] + + // main menu has 4 entries + firstTwo := m.Limit(2) + assert.Equal(t, 2, len(firstTwo)) + for i := 0; i < 2; i++ { + assert.Equal(t, m[i], firstTwo[i]) + } + assert.Equal(t, m, m.Limit(4)) + assert.Equal(t, m, m.Limit(5)) +} + +func TestMenuSortByN(t *testing.T) { + t.Parallel() + for i, this := range []struct { + sortFunc func(p Menu) Menu + assertFunc func(p Menu) bool + }{ + {(Menu).Sort, func(p Menu) bool { return p[0].Weight == 1 && p[1].Name == "nx" && p[2].Identifier == "ib" }}, + {(Menu).ByWeight, func(p Menu) bool { return p[0].Weight == 1 && p[1].Name == "nx" && p[2].Identifier == "ib" }}, + {(Menu).ByName, func(p Menu) bool { return p[0].Name == "na" }}, + {(Menu).Reverse, func(p Menu) bool { return p[0].Identifier == "ib" && p[len(p)-1].Identifier == "ia" }}, + } { + menu := Menu{&MenuEntry{Weight: 3, Name: "nb", Identifier: "ia"}, + &MenuEntry{Weight: 1, Name: "na", Identifier: "ic"}, + &MenuEntry{Weight: 1, Name: "nx", Identifier: "ic"}, + &MenuEntry{Weight: 2, Name: "nb", Identifier: "ix"}, + &MenuEntry{Weight: 2, Name: "nb", Identifier: "ib"}} + + sorted := this.sortFunc(menu) + + if !this.assertFunc(sorted) { + t.Errorf("[%d] sort error", i) + } + } + +} + +func TestHomeNodeMenu(t *testing.T) { + t.Parallel() + s := setupMenuTests(t, menuPageSources, + "canonifyURLs", true, + "uglyURLs", false, + ) + + home := s.getPage(KindHome) + homeMenuEntry := &MenuEntry{Name: home.Title, URL: home.URL()} + + for i, this := range []struct { + menu string + menuItem *MenuEntry + isMenuCurrent bool + hasMenuCurrent bool + }{ + {"main", homeMenuEntry, true, false}, + {"doesnotexist", homeMenuEntry, false, false}, + {"main", &MenuEntry{Name: "Somewhere else", URL: "/somewhereelse"}, false, false}, + {"grandparent", findTestMenuEntryByID(s, "grandparent", "grandparentId"), false, true}, + {"grandparent", findTestMenuEntryByID(s, "grandparent", "parentId"), false, true}, + {"grandparent", findTestMenuEntryByID(s, "grandparent", "grandchildId"), true, false}, + } { + + isMenuCurrent := home.IsMenuCurrent(this.menu, this.menuItem) + hasMenuCurrent := home.HasMenuCurrent(this.menu, this.menuItem) + + if isMenuCurrent != this.isMenuCurrent { + fmt.Println("isMenuCurrent", isMenuCurrent) + fmt.Printf("this: %#v\n", this) + t.Errorf("[%d] Wrong result from IsMenuCurrent: %v for %q", i, isMenuCurrent, this.menuItem) + } + + if hasMenuCurrent != this.hasMenuCurrent { + fmt.Println("hasMenuCurrent", hasMenuCurrent) + fmt.Printf("this: %#v\n", this) + t.Errorf("[%d] Wrong result for menu %q menuItem %v for HasMenuCurrent: %v", i, this.menu, this.menuItem, hasMenuCurrent) + } + } +} + +func TestHopefullyUniqueID(t *testing.T) { + t.Parallel() + assert.Equal(t, "i", (&MenuEntry{Identifier: "i", URL: "u", Name: "n"}).hopefullyUniqueID()) + assert.Equal(t, "u", (&MenuEntry{Identifier: "", URL: "u", Name: "n"}).hopefullyUniqueID()) + assert.Equal(t, "n", (&MenuEntry{Identifier: "", URL: "", Name: "n"}).hopefullyUniqueID()) +} + +func TestAddMenuEntryChild(t *testing.T) { + t.Parallel() + root := &MenuEntry{Weight: 1} + root.addChild(&MenuEntry{Weight: 2}) + root.addChild(&MenuEntry{Weight: 1}) + assert.Equal(t, 2, len(root.Children)) + assert.Equal(t, 1, root.Children[0].Weight) +} + +var testMenuIdentityMatcher = func(me *MenuEntry, id string) bool { return me.Identifier == id } +var testMenuNameMatcher = func(me *MenuEntry, id string) bool { return me.Name == id } + +func findTestMenuEntryByID(s *Site, mn string, id string) *MenuEntry { + return findTestMenuEntry(s, mn, id, testMenuIdentityMatcher) +} +func findTestMenuEntryByName(s *Site, mn string, id string) *MenuEntry { + return findTestMenuEntry(s, mn, id, testMenuNameMatcher) +} + +func findTestMenuEntry(s *Site, mn string, id string, matcher func(me *MenuEntry, id string) bool) *MenuEntry { + var found *MenuEntry + if menu, ok := s.Menus[mn]; ok { + for _, me := range *menu { + + if matcher(me, id) { + if found != nil { + panic(fmt.Sprintf("Duplicate menu entry in menu %s with id/name %s", mn, id)) + } + found = me + } + + descendant := findDescendantTestMenuEntry(me, id, matcher) + if descendant != nil { + if found != nil { + panic(fmt.Sprintf("Duplicate menu entry in menu %s with id/name %s", mn, id)) + } + found = descendant + } + } + } + return found +} + +func findDescendantTestMenuEntry(parent *MenuEntry, id string, matcher func(me *MenuEntry, id string) bool) *MenuEntry { + var found *MenuEntry + if parent.HasChildren() { + for _, child := range parent.Children { + + if matcher(child, id) { + if found != nil { + panic(fmt.Sprintf("Duplicate menu entry in menuitem %s with id/name %s", parent.KeyName(), id)) + } + found = child + } + + descendant := findDescendantTestMenuEntry(child, id, matcher) + if descendant != nil { + if found != nil { + panic(fmt.Sprintf("Duplicate menu entry in menuitem %s with id/name %s", parent.KeyName(), id)) + } + found = descendant + } + } + } + return found +} + +func setupMenuTests(t *testing.T, pageSources []source.ByteSource, configKeyValues ...interface{}) *Site { + + var ( + cfg, fs = newTestCfg() + ) + + menus, err := tomlToMap(confMenu1) + require.NoError(t, err) + + cfg.Set("menu", menus["menu"]) + cfg.Set("baseURL", "http://foo.local/Zoo/") + + for i := 0; i < len(configKeyValues); i += 2 { + cfg.Set(configKeyValues[i].(string), configKeyValues[i+1]) + } + + for _, src := range pageSources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + + } + + return buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + +} + +func tomlToMap(s string) (map[string]interface{}, error) { + var data = make(map[string]interface{}) + _, err := toml.Decode(s, &data) + return data, err +} diff --git a/hugolib/menu_test.go b/hugolib/menu_test.go new file mode 100644 index 000000000..f044fb5e0 --- /dev/null +++ b/hugolib/menu_test.go @@ -0,0 +1,96 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "testing" + + "fmt" + + "github.com/spf13/afero" + + "github.com/stretchr/testify/require" +) + +const ( + menuPageTemplate = `--- +title: %q +weight: %d +menu: + %s: + weight: %d +--- +# Doc Menu +` +) + +func TestSectionPagesMenu(t *testing.T) { + t.Parallel() + + siteConfig := ` +baseurl = "http://example.com/" +title = "Section Menu" +sectionPagesMenu = "sect" +` + + th, h := newTestSitesFromConfig(t, afero.NewMemMapFs(), siteConfig, + "layouts/partials/menu.html", `{{- $p := .page -}} +{{- $m := .menu -}} +{{ range (index $p.Site.Menus $m) -}} +{{- .URL }}|{{ .Name }}|{{ .Weight -}}| +{{- if $p.IsMenuCurrent $m . }}IsMenuCurrent{{ else }}-{{ end -}}| +{{- if $p.HasMenuCurrent $m . }}HasMenuCurrent{{ else }}-{{ end -}}| +{{- end -}} +`, + "layouts/_default/single.html", + `Single|{{ .Title }} +Menu Sect: {{ partial "menu.html" (dict "page" . "menu" "sect") }} +Menu Main: {{ partial "menu.html" (dict "page" . "menu" "main") }}`, + "layouts/_default/list.html", "List|{{ .Title }}|{{ .Content }}", + ) + require.Len(t, h.Sites, 1) + + fs := th.Fs + + writeSource(t, fs, "content/sect1/p1.md", fmt.Sprintf(menuPageTemplate, "p1", 1, "main", 40)) + writeSource(t, fs, "content/sect1/p2.md", fmt.Sprintf(menuPageTemplate, "p2", 2, "main", 30)) + writeSource(t, fs, "content/sect2/p3.md", fmt.Sprintf(menuPageTemplate, "p3", 3, "main", 20)) + writeSource(t, fs, "content/sect2/p4.md", fmt.Sprintf(menuPageTemplate, "p4", 4, "main", 10)) + writeSource(t, fs, "content/sect3/p5.md", fmt.Sprintf(menuPageTemplate, "p5", 5, "main", 5)) + + writeNewContentFile(t, fs, "Section One", "2017-01-01", "content/sect1/_index.md", 100) + writeNewContentFile(t, fs, "Section Five", "2017-01-01", "content/sect5/_index.md", 10) + + err := h.Build(BuildCfg{}) + + require.NoError(t, err) + + s := h.Sites[0] + + require.Len(t, s.Menus, 2) + + p1 := s.RegularPages[0].Menus() + + // There is only one menu in the page, but it is "member of" 2 + require.Len(t, p1, 1) + + th.assertFileContent("public/sect1/p1/index.html", "Single", + "Menu Sect: /sect5/|Section Five|10|-|-|/sect1/|Section One|100|-|HasMenuCurrent|/sect2/|Sect2s|0|-|-|/sect3/|Sect3s|0|-|-|", + "Menu Main: /sect3/p5/|p5|5|-|-|/sect2/p4/|p4|10|-|-|/sect2/p3/|p3|20|-|-|/sect1/p2/|p2|30|-|-|/sect1/p1/|p1|40|IsMenuCurrent|-|", + ) + + th.assertFileContent("public/sect2/p3/index.html", "Single", + "Menu Sect: /sect5/|Section Five|10|-|-|/sect1/|Section One|100|-|-|/sect2/|Sect2s|0|-|HasMenuCurrent|/sect3/|Sect3s|0|-|-|") + +} diff --git a/hugolib/multilingual.go b/hugolib/multilingual.go new file mode 100644 index 000000000..575be1396 --- /dev/null +++ b/hugolib/multilingual.go @@ -0,0 +1,117 @@ +// Copyright 2016-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "sync" + + "sort" + + "errors" + "fmt" + + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/helpers" + "github.com/spf13/cast" +) + +type Multilingual struct { + Languages helpers.Languages + + DefaultLang *helpers.Language + + langMap map[string]*helpers.Language + langMapInit sync.Once +} + +func (ml *Multilingual) Language(lang string) *helpers.Language { + ml.langMapInit.Do(func() { + ml.langMap = make(map[string]*helpers.Language) + for _, l := range ml.Languages { + ml.langMap[l.Lang] = l + } + }) + return ml.langMap[lang] +} + +func newMultiLingualFromSites(cfg config.Provider, sites ...*Site) (*Multilingual, error) { + languages := make(helpers.Languages, len(sites)) + + for i, s := range sites { + if s.Language == nil { + return nil, errors.New("Missing language for site") + } + languages[i] = s.Language + } + + defaultLang := cfg.GetString("defaultContentLanguage") + + if defaultLang == "" { + defaultLang = "en" + } + + return &Multilingual{Languages: languages, DefaultLang: helpers.NewLanguage(defaultLang, cfg)}, nil + +} + +func newMultiLingualForLanguage(language *helpers.Language) *Multilingual { + languages := helpers.Languages{language} + return &Multilingual{Languages: languages, DefaultLang: language} +} +func (ml *Multilingual) enabled() bool { + return len(ml.Languages) > 1 +} + +func (s *Site) multilingualEnabled() bool { + if s.owner == nil { + return false + } + return s.owner.multilingual != nil && s.owner.multilingual.enabled() +} + +func toSortedLanguages(cfg config.Provider, l map[string]interface{}) (helpers.Languages, error) { + langs := make(helpers.Languages, len(l)) + i := 0 + + for lang, langConf := range l { + langsMap, err := cast.ToStringMapE(langConf) + + if err != nil { + return nil, fmt.Errorf("Language config is not a map: %T", langConf) + } + + language := helpers.NewLanguage(lang, cfg) + + for loki, v := range langsMap { + switch loki { + case "title": + language.Title = cast.ToString(v) + case "languagename": + language.LanguageName = cast.ToString(v) + case "weight": + language.Weight = cast.ToInt(v) + } + + // Put all into the Params map + language.SetParam(loki, v) + } + + langs[i] = language + i++ + } + + sort.Sort(langs) + + return langs, nil +} diff --git a/hugolib/node_as_page_test.go b/hugolib/node_as_page_test.go new file mode 100644 index 000000000..6cadafc0d --- /dev/null +++ b/hugolib/node_as_page_test.go @@ -0,0 +1,831 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "path/filepath" + "strings" + "testing" + + "time" + + "github.com/spf13/afero" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/hugofs" + "github.com/stretchr/testify/require" +) + +/* + This file will test the "making everything a page" transition. + + See https://github.com/gohugoio/hugo/issues/2297 + +*/ + +func TestNodesAsPage(t *testing.T) { + t.Parallel() + for _, preserveTaxonomyNames := range []bool{false, true} { + for _, ugly := range []bool{true, false} { + doTestNodeAsPage(t, ugly, preserveTaxonomyNames) + } + } +} + +func doTestNodeAsPage(t *testing.T, ugly, preserveTaxonomyNames bool) { + + /* Will have to decide what to name the node content files, but: + + Home page should have: + Content, shortcode support + Metadata (title, dates etc.) + Params + Taxonomies (categories, tags) + + */ + + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + cfg.Set("uglyURLs", ugly) + cfg.Set("preserveTaxonomyNames", preserveTaxonomyNames) + + cfg.Set("paginate", 1) + cfg.Set("title", "Hugo Rocks") + cfg.Set("rssURI", "customrss.xml") + + writeLayoutsForNodeAsPageTests(t, fs) + writeNodePagesForNodeAsPageTests(t, fs, "") + + writeRegularPagesForNodeAsPageTests(t, fs) + + sites, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + require.NoError(t, err) + + require.NoError(t, sites.Build(BuildCfg{})) + + // date order: home, sect1, sect2, cat/hugo, cat/web, categories + + th.assertFileContent(filepath.Join("public", "index.html"), + "Index Title: Home Sweet Home!", + "Home <strong>Content!</strong>", + "# Pages: 4", + "Date: 2009-01-02", + "Lastmod: 2009-01-03", + "GetPage: Section1 ", + ) + + th.assertFileContent(expectedFilePath(ugly, "public", "sect1", "regular1"), "Single Title: Page 01", "Content Page 01") + + nodes := sites.findAllPagesByKindNotIn(KindPage) + + require.Len(t, nodes, 8) + + home := nodes[7] // oldest + + require.True(t, home.IsHome()) + require.True(t, home.IsNode()) + require.False(t, home.IsPage()) + require.True(t, home.Path() != "") + + section2 := nodes[5] + require.Equal(t, "Section2", section2.Title) + + pages := sites.findAllPagesByKind(KindPage) + require.Len(t, pages, 4) + + first := pages[0] + + require.False(t, first.IsHome()) + require.False(t, first.IsNode()) + require.True(t, first.IsPage()) + + // Check Home paginator + th.assertFileContent(expectedFilePath(ugly, "public", "page", "2"), + "Pag: Page 02") + + // Check Sections + th.assertFileContent(expectedFilePath(ugly, "public", "sect1"), + "Section Title: Section", "Section1 <strong>Content!</strong>", + "Date: 2009-01-04", + "Lastmod: 2009-01-05", + ) + + th.assertFileContent(expectedFilePath(ugly, "public", "sect2"), + "Section Title: Section", "Section2 <strong>Content!</strong>", + "Date: 2009-01-06", + "Lastmod: 2009-01-07", + ) + + // Check Sections paginator + th.assertFileContent(expectedFilePath(ugly, "public", "sect1", "page", "2"), + "Pag: Page 02") + + sections := sites.findAllPagesByKind(KindSection) + + require.Len(t, sections, 2) + + // Check taxonomy lists + th.assertFileContent(expectedFilePath(ugly, "public", "categories", "hugo"), + "Taxonomy Title: Taxonomy Hugo", "Taxonomy Hugo <strong>Content!</strong>", + "Date: 2009-01-08", + "Lastmod: 2009-01-09", + ) + + th.assertFileContent(expectedFilePath(ugly, "public", "categories", "hugo-rocks"), + "Taxonomy Title: Taxonomy Hugo Rocks", + ) + + s := sites.Sites[0] + + web := s.getPage(KindTaxonomy, "categories", "web") + require.NotNil(t, web) + require.Len(t, web.Data["Pages"].(Pages), 4) + + th.assertFileContent(expectedFilePath(ugly, "public", "categories", "web"), + "Taxonomy Title: Taxonomy Web", + "Taxonomy Web <strong>Content!</strong>", + "Date: 2009-01-10", + "Lastmod: 2009-01-11", + ) + + // Check taxonomy list paginator + th.assertFileContent(expectedFilePath(ugly, "public", "categories", "hugo", "page", "2"), + "Taxonomy Title: Taxonomy Hugo", + "Pag: Page 02") + + // Check taxonomy terms + th.assertFileContent(expectedFilePath(ugly, "public", "categories"), + "Taxonomy Terms Title: Taxonomy Term Categories", "Taxonomy Term Categories <strong>Content!</strong>", "k/v: hugo", + "Date: 2009-01-14", + "Lastmod: 2009-01-15", + ) + + // Check taxonomy terms paginator + th.assertFileContent(expectedFilePath(ugly, "public", "categories", "page", "2"), + "Taxonomy Terms Title: Taxonomy Term Categories", + "Pag: Taxonomy Web") + + // RSS + th.assertFileContent(filepath.Join("public", "customrss.xml"), "Recent content in Home Sweet Home! on Hugo Rocks", "<rss") + th.assertFileContent(filepath.Join("public", "sect1", "customrss.xml"), "Recent content in Section1 on Hugo Rocks", "<rss") + th.assertFileContent(filepath.Join("public", "sect2", "customrss.xml"), "Recent content in Section2 on Hugo Rocks", "<rss") + th.assertFileContent(filepath.Join("public", "categories", "hugo", "customrss.xml"), "Recent content in Taxonomy Hugo on Hugo Rocks", "<rss") + th.assertFileContent(filepath.Join("public", "categories", "web", "customrss.xml"), "Recent content in Taxonomy Web on Hugo Rocks", "<rss") + th.assertFileContent(filepath.Join("public", "categories", "customrss.xml"), "Recent content in Taxonomy Term Categories on Hugo Rocks", "<rss") + +} + +func TestNodesWithNoContentFile(t *testing.T) { + t.Parallel() + for _, ugly := range []bool{false, true} { + doTestNodesWithNoContentFile(t, ugly) + } +} + +func doTestNodesWithNoContentFile(t *testing.T, ugly bool) { + + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + cfg.Set("uglyURLs", ugly) + cfg.Set("paginate", 1) + cfg.Set("title", "Hugo Rocks!") + cfg.Set("rssURI", "customrss.xml") + + writeLayoutsForNodeAsPageTests(t, fs) + writeRegularPagesForNodeAsPageTests(t, fs) + + sites, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + require.NoError(t, err) + + require.NoError(t, sites.Build(BuildCfg{})) + + s := sites.Sites[0] + + // Home page + homePages := s.findPagesByKind(KindHome) + require.Len(t, homePages, 1) + + homePage := homePages[0] + require.Len(t, homePage.Data["Pages"], 4) + require.Len(t, homePage.Pages, 4) + require.True(t, homePage.Path() == "") + + th.assertFileContent(filepath.Join("public", "index.html"), + "Index Title: Hugo Rocks!", + "Date: 2010-06-12", + "Lastmod: 2010-06-13", + ) + + // Taxonomy list + th.assertFileContent(expectedFilePath(ugly, "public", "categories", "hugo"), + "Taxonomy Title: Hugo", + "Date: 2010-06-12", + "Lastmod: 2010-06-13", + ) + + // Taxonomy terms + th.assertFileContent(expectedFilePath(ugly, "public", "categories"), + "Taxonomy Terms Title: Categories", + ) + + pages := s.findPagesByKind(KindTaxonomyTerm) + for _, p := range pages { + var want string + if ugly { + want = "/" + p.s.PathSpec.URLize(p.Title) + ".html" + } else { + want = "/" + p.s.PathSpec.URLize(p.Title) + "/" + } + if p.URL() != want { + t.Errorf("Taxonomy term URL mismatch: want %q, got %q", want, p.URL()) + } + } + + // Sections + th.assertFileContent(expectedFilePath(ugly, "public", "sect1"), + "Section Title: Sect1s", + "Date: 2010-06-12", + "Lastmod: 2010-06-13", + ) + + th.assertFileContent(expectedFilePath(ugly, "public", "sect2"), + "Section Title: Sect2s", + "Date: 2008-07-06", + "Lastmod: 2008-07-09", + ) + + // RSS + th.assertFileContent(filepath.Join("public", "customrss.xml"), "Hugo Rocks!", "<rss") + th.assertFileContent(filepath.Join("public", "sect1", "customrss.xml"), "Recent content in Sect1s on Hugo Rocks!", "<rss") + th.assertFileContent(filepath.Join("public", "sect2", "customrss.xml"), "Recent content in Sect2s on Hugo Rocks!", "<rss") + th.assertFileContent(filepath.Join("public", "categories", "hugo", "customrss.xml"), "Recent content in Hugo on Hugo Rocks!", "<rss") + th.assertFileContent(filepath.Join("public", "categories", "web", "customrss.xml"), "Recent content in Web on Hugo Rocks!", "<rss") + +} + +func TestNodesAsPageMultilingual(t *testing.T) { + t.Parallel() + for _, ugly := range []bool{false, true} { + t.Run(fmt.Sprintf("ugly=%t", ugly), func(t *testing.T) { + doTestNodesAsPageMultilingual(t, ugly) + }) + } +} + +func doTestNodesAsPageMultilingual(t *testing.T, ugly bool) { + + mf := afero.NewMemMapFs() + + writeToFs(t, mf, "config.toml", + ` +paginage = 1 +title = "Hugo Multilingual Rocks!" +rssURI = "customrss.xml" +defaultContentLanguage = "nn" +defaultContentLanguageInSubdir = true + + +[languages] +[languages.nn] +languageName = "Nynorsk" +weight = 1 +title = "Hugo på norsk" + +[languages.en] +languageName = "English" +weight = 2 +title = "Hugo in English" + +[languages.de] +languageName = "Deutsch" +weight = 3 +title = "Deutsche Hugo" +`) + + cfg, err := LoadConfig(mf, "", "config.toml") + require.NoError(t, err) + + cfg.Set("uglyURLs", ugly) + + fs := hugofs.NewFrom(mf, cfg) + + writeLayoutsForNodeAsPageTests(t, fs) + + for _, lang := range []string{"nn", "en"} { + writeRegularPagesForNodeAsPageTestsWithLang(t, fs, lang) + } + + th := testHelper{cfg, fs, t} + + sites, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + if err != nil { + t.Fatalf("Failed to create sites: %s", err) + } + + if len(sites.Sites) != 3 { + t.Fatalf("Got %d sites", len(sites.Sites)) + } + + // Only write node pages for the English and Deutsch + writeNodePagesForNodeAsPageTests(t, fs, "en") + writeNodePagesForNodeAsPageTests(t, fs, "de") + + err = sites.Build(BuildCfg{}) + + if err != nil { + t.Fatalf("Failed to build sites: %s", err) + } + + // The en and de language have content pages + enHome := sites.Sites[1].getPage("home") + require.NotNil(t, enHome) + require.Equal(t, "en", enHome.Language().Lang) + require.Contains(t, enHome.Content, "l-en") + + deHome := sites.Sites[2].getPage("home") + require.NotNil(t, deHome) + require.Equal(t, "de", deHome.Language().Lang) + require.Contains(t, deHome.Content, "l-de") + + require.Len(t, deHome.Translations(), 2, deHome.Translations()[0].Language().Lang) + require.Equal(t, "en", deHome.Translations()[1].Language().Lang) + require.Equal(t, "nn", deHome.Translations()[0].Language().Lang) + // See issue #3179 + require.Equal(t, expetedPermalink(false, "/de/"), deHome.Permalink()) + + enSect := sites.Sites[1].getPage("section", "sect1") + require.NotNil(t, enSect) + require.Equal(t, "en", enSect.Language().Lang) + require.Len(t, enSect.Translations(), 2, enSect.Translations()[0].Language().Lang) + require.Equal(t, "de", enSect.Translations()[1].Language().Lang) + require.Equal(t, "nn", enSect.Translations()[0].Language().Lang) + + require.Equal(t, expetedPermalink(ugly, "/en/sect1/"), enSect.Permalink()) + + th.assertFileContent(filepath.Join("public", "nn", "index.html"), + "Index Title: Hugo på norsk") + th.assertFileContent(filepath.Join("public", "en", "index.html"), + "Index Title: Home Sweet Home!", "<strong>Content!</strong>") + th.assertFileContent(filepath.Join("public", "de", "index.html"), + "Index Title: Home Sweet Home!", "<strong>Content!</strong>") + + // Taxonomy list + th.assertFileContent(expectedFilePath(ugly, "public", "nn", "categories", "hugo"), + "Taxonomy Title: Hugo") + th.assertFileContent(expectedFilePath(ugly, "public", "en", "categories", "hugo"), + "Taxonomy Title: Taxonomy Hugo") + + // Taxonomy terms + th.assertFileContent(expectedFilePath(ugly, "public", "nn", "categories"), + "Taxonomy Terms Title: Categories") + th.assertFileContent(expectedFilePath(ugly, "public", "en", "categories"), + "Taxonomy Terms Title: Taxonomy Term Categories") + + // Sections + th.assertFileContent(expectedFilePath(ugly, "public", "nn", "sect1"), + "Section Title: Sect1s") + th.assertFileContent(expectedFilePath(ugly, "public", "nn", "sect2"), + "Section Title: Sect2s") + th.assertFileContent(expectedFilePath(ugly, "public", "en", "sect1"), + "Section Title: Section1") + th.assertFileContent(expectedFilePath(ugly, "public", "en", "sect2"), + "Section Title: Section2") + + // Regular pages + th.assertFileContent(expectedFilePath(ugly, "public", "en", "sect1", "regular1"), + "Single Title: Page 01") + th.assertFileContent(expectedFilePath(ugly, "public", "nn", "sect1", "regular2"), + "Single Title: Page 02") + + // RSS + th.assertFileContent(filepath.Join("public", "nn", "customrss.xml"), "Hugo på norsk", "<rss") + th.assertFileContent(filepath.Join("public", "nn", "sect1", "customrss.xml"), "Recent content in Sect1s on Hugo på norsk", "<rss") + th.assertFileContent(filepath.Join("public", "nn", "sect2", "customrss.xml"), "Recent content in Sect2s on Hugo på norsk", "<rss") + th.assertFileContent(filepath.Join("public", "nn", "categories", "hugo", "customrss.xml"), "Recent content in Hugo on Hugo på norsk", "<rss") + th.assertFileContent(filepath.Join("public", "nn", "categories", "web", "customrss.xml"), "Recent content in Web on Hugo på norsk", "<rss") + + th.assertFileContent(filepath.Join("public", "en", "customrss.xml"), "Recent content in Home Sweet Home! on Hugo in English", "<rss") + th.assertFileContent(filepath.Join("public", "en", "sect1", "customrss.xml"), "Recent content in Section1 on Hugo in English", "<rss") + th.assertFileContent(filepath.Join("public", "en", "sect2", "customrss.xml"), "Recent content in Section2 on Hugo in English", "<rss") + th.assertFileContent(filepath.Join("public", "en", "categories", "hugo", "customrss.xml"), "Recent content in Taxonomy Hugo on Hugo in English", "<rss") + th.assertFileContent(filepath.Join("public", "en", "categories", "web", "customrss.xml"), "Recent content in Taxonomy Web on Hugo in English", "<rss") + +} + +func TestNodesWithTaxonomies(t *testing.T) { + t.Parallel() + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + cfg.Set("paginate", 1) + cfg.Set("title", "Hugo Rocks!") + + writeLayoutsForNodeAsPageTests(t, fs) + writeRegularPagesForNodeAsPageTests(t, fs) + + writeSource(t, fs, filepath.Join("content", "_index.md"), `--- +title: Home With Taxonomies +categories: [ + "Hugo", + "Home" +] +--- +`) + + h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + require.NoError(t, err) + + require.NoError(t, h.Build(BuildCfg{})) + + th.assertFileContent(filepath.Join("public", "categories", "hugo", "index.html"), "Taxonomy Title: Hugo", "# Pages: 5") + th.assertFileContent(filepath.Join("public", "categories", "home", "index.html"), "Taxonomy Title: Home", "# Pages: 1") + +} + +func TestNodesWithMenu(t *testing.T) { + t.Parallel() + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + cfg.Set("paginate", 1) + cfg.Set("title", "Hugo Rocks!") + + writeLayoutsForNodeAsPageTests(t, fs) + writeRegularPagesForNodeAsPageTests(t, fs) + + writeSource(t, fs, filepath.Join("content", "_index.md"), `--- +title: Home With Menu +menu: + mymenu: + name: "Go Home!" +--- +`) + + writeSource(t, fs, filepath.Join("content", "sect1", "_index.md"), `--- +title: Sect1 With Menu +menu: + mymenu: + name: "Go Sect1!" +--- +`) + + writeSource(t, fs, filepath.Join("content", "categories", "hugo", "_index.md"), `--- +title: Taxonomy With Menu +menu: + mymenu: + name: "Go Tax Hugo!" +--- +`) + + h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + require.NoError(t, err) + + require.NoError(t, h.Build(BuildCfg{})) + + th.assertFileContent(filepath.Join("public", "index.html"), "Home With Menu", "Home Menu Item: Go Home!: /") + th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "Sect1 With Menu", "Section Menu Item: Go Sect1!: /sect1/") + th.assertFileContent(filepath.Join("public", "categories", "hugo", "index.html"), "Taxonomy With Menu", "Taxonomy Menu Item: Go Tax Hugo!: /categories/hugo/") + +} + +func TestNodesWithAlias(t *testing.T) { + t.Parallel() + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + cfg.Set("paginate", 1) + cfg.Set("baseURL", "http://base/") + cfg.Set("title", "Hugo Rocks!") + + writeLayoutsForNodeAsPageTests(t, fs) + writeRegularPagesForNodeAsPageTests(t, fs) + + writeSource(t, fs, filepath.Join("content", "_index.md"), `--- +title: Home With Alias +aliases: + - /my/new/home.html +--- +`) + + h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + require.NoError(t, err) + + require.NoError(t, h.Build(BuildCfg{})) + + th.assertFileContent(filepath.Join("public", "index.html"), "Home With Alias") + th.assertFileContent(filepath.Join("public", "my", "new", "home.html"), "content=\"0; url=http://base/") + +} + +func TestNodesWithSectionWithIndexPageOnly(t *testing.T) { + t.Parallel() + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + cfg.Set("paginate", 1) + cfg.Set("title", "Hugo Rocks!") + + writeLayoutsForNodeAsPageTests(t, fs) + + writeSource(t, fs, filepath.Join("content", "sect", "_index.md"), `--- +title: MySection +--- +My Section Content +`) + + h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + require.NoError(t, err) + + require.NoError(t, h.Build(BuildCfg{})) + + th.assertFileContent(filepath.Join("public", "sect", "index.html"), "My Section") + +} + +func TestNodesWithURLs(t *testing.T) { + t.Parallel() + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + cfg.Set("paginate", 1) + cfg.Set("title", "Hugo Rocks!") + cfg.Set("baseURL", "http://bep.is/base/") + + writeLayoutsForNodeAsPageTests(t, fs) + writeRegularPagesForNodeAsPageTests(t, fs) + + writeSource(t, fs, filepath.Join("content", "sect", "_index.md"), `--- +title: MySection +url: foo.html +--- +My Section Content +`) + + h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + require.NoError(t, err) + + require.NoError(t, h.Build(BuildCfg{})) + + th.assertFileContent(filepath.Join("public", "sect", "index.html"), "My Section") + + s := h.Sites[0] + + p := s.RegularPages[0] + + require.Equal(t, "/base/sect1/regular1/", p.URL()) + + // Section with front matter and url set (which should not be used) + sect := s.getPage(KindSection, "sect") + require.Equal(t, "/base/sect/", sect.URL()) + require.Equal(t, "http://bep.is/base/sect/", sect.Permalink()) + require.Equal(t, "/base/sect/", sect.RelPermalink()) + + // Home page without front matter + require.Equal(t, "/base/", s.getPage(KindHome).URL()) + +} + +func writeRegularPagesForNodeAsPageTests(t *testing.T, fs *hugofs.Fs) { + writeRegularPagesForNodeAsPageTestsWithLang(t, fs, "") +} + +func writeRegularPagesForNodeAsPageTestsWithLang(t *testing.T, fs *hugofs.Fs, lang string) { + var langStr string + + if lang != "" { + langStr = lang + "." + } + + format := "2006-01-02" + + date, _ := time.Parse(format, "2010-06-15") + + for i := 1; i <= 4; i++ { + sect := "sect1" + if i > 2 { + sect = "sect2" + + date, _ = time.Parse(format, "2008-07-15") // Nodes are placed in 2009 + + } + date = date.Add(-24 * time.Duration(i) * time.Hour) + writeSource(t, fs, filepath.Join("content", sect, fmt.Sprintf("regular%d.%smd", i, langStr)), fmt.Sprintf(`--- +title: Page %02d +lastMod : %q +date : %q +categories: [ + "Hugo", + "Web", + "Hugo Rocks!" +] +--- +Content Page %02d +`, i, date.Add(time.Duration(i)*-24*time.Hour).Format(time.RFC822), date.Add(time.Duration(i)*-2*24*time.Hour).Format(time.RFC822), i)) + } +} + +func writeNodePagesForNodeAsPageTests(t *testing.T, fs *hugofs.Fs, lang string) { + + filename := "_index.md" + + if lang != "" { + filename = fmt.Sprintf("_index.%s.md", lang) + } + + format := "2006-01-02" + + date, _ := time.Parse(format, "2009-01-01") + + writeSource(t, fs, filepath.Join("content", filename), fmt.Sprintf(`--- +title: Home Sweet Home! +date : %q +lastMod : %q +--- +l-%s Home **Content!** +`, date.Add(1*24*time.Hour).Format(time.RFC822), date.Add(2*24*time.Hour).Format(time.RFC822), lang)) + + writeSource(t, fs, filepath.Join("content", "sect1", filename), fmt.Sprintf(`--- +title: Section1 +date : %q +lastMod : %q +--- +Section1 **Content!** +`, date.Add(3*24*time.Hour).Format(time.RFC822), date.Add(4*24*time.Hour).Format(time.RFC822))) + writeSource(t, fs, filepath.Join("content", "sect2", filename), fmt.Sprintf(`--- +title: Section2 +date : %q +lastMod : %q +--- +Section2 **Content!** +`, date.Add(5*24*time.Hour).Format(time.RFC822), date.Add(6*24*time.Hour).Format(time.RFC822))) + + writeSource(t, fs, filepath.Join("content", "categories", "hugo", filename), fmt.Sprintf(`--- +title: Taxonomy Hugo +date : %q +lastMod : %q +--- +Taxonomy Hugo **Content!** +`, date.Add(7*24*time.Hour).Format(time.RFC822), date.Add(8*24*time.Hour).Format(time.RFC822))) + + writeSource(t, fs, filepath.Join("content", "categories", "web", filename), fmt.Sprintf(`--- +title: Taxonomy Web +date : %q +lastMod : %q +--- +Taxonomy Web **Content!** +`, date.Add(9*24*time.Hour).Format(time.RFC822), date.Add(10*24*time.Hour).Format(time.RFC822))) + + writeSource(t, fs, filepath.Join("content", "categories", "hugo-rocks", filename), fmt.Sprintf(`--- +title: Taxonomy Hugo Rocks +date : %q +lastMod : %q +--- +Taxonomy Hugo Rocks **Content!** +`, date.Add(11*24*time.Hour).Format(time.RFC822), date.Add(12*24*time.Hour).Format(time.RFC822))) + + writeSource(t, fs, filepath.Join("content", "categories", filename), fmt.Sprintf(`--- +title: Taxonomy Term Categories +date : %q +lastMod : %q +--- +Taxonomy Term Categories **Content!** +`, date.Add(13*24*time.Hour).Format(time.RFC822), date.Add(14*24*time.Hour).Format(time.RFC822))) + + writeSource(t, fs, filepath.Join("content", "tags", filename), fmt.Sprintf(`--- +title: Taxonomy Term Tags +date : %q +lastMod : %q +--- +Taxonomy Term Tags **Content!** +`, date.Add(15*24*time.Hour).Format(time.RFC822), date.Add(16*24*time.Hour).Format(time.RFC822))) + +} + +func writeLayoutsForNodeAsPageTests(t *testing.T, fs *hugofs.Fs) { + writeSource(t, fs, filepath.Join("layouts", "index.html"), ` +Index Title: {{ .Title }} +Index Content: {{ .Content }} +# Pages: {{ len .Data.Pages }} +{{ range .Paginator.Pages }} + Pag: {{ .Title }} +{{ end }} +{{ with .Site.Menus.mymenu }} +{{ range . }} +Home Menu Item: {{ .Name }}: {{ .URL }} +{{ end }} +{{ end }} +Date: {{ .Date.Format "2006-01-02" }} +Lastmod: {{ .Lastmod.Format "2006-01-02" }} +GetPage: {{ with .Site.GetPage "section" "sect1" }}{{ .Title }}{{ end }} +`) + + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), ` +Single Title: {{ .Title }} +Single Content: {{ .Content }} +Date: {{ .Date.Format "2006-01-02" }} +Lastmod: {{ .Lastmod.Format "2006-01-02" }} +`) + + writeSource(t, fs, filepath.Join("layouts", "_default", "section.html"), ` +Section Title: {{ .Title }} +Section Content: {{ .Content }} +# Pages: {{ len .Data.Pages }} +{{ range .Paginator.Pages }} + Pag: {{ .Title }} +{{ end }} +{{ with .Site.Menus.mymenu }} +{{ range . }} +Section Menu Item: {{ .Name }}: {{ .URL }} +{{ end }} +{{ end }} +Date: {{ .Date.Format "2006-01-02" }} +Lastmod: {{ .Lastmod.Format "2006-01-02" }} +`) + + // Taxonomy lists + writeSource(t, fs, filepath.Join("layouts", "_default", "taxonomy.html"), ` +Taxonomy Title: {{ .Title }} +Taxonomy Content: {{ .Content }} +# Pages: {{ len .Data.Pages }} +{{ range .Paginator.Pages }} + Pag: {{ .Title }} +{{ end }} +{{ with .Site.Menus.mymenu }} +{{ range . }} +Taxonomy Menu Item: {{ .Name }}: {{ .URL }} +{{ end }} +{{ end }} +Date: {{ .Date.Format "2006-01-02" }} +Lastmod: {{ .Lastmod.Format "2006-01-02" }} +`) + + // Taxonomy terms + writeSource(t, fs, filepath.Join("layouts", "_default", "terms.html"), ` +Taxonomy Terms Title: {{ .Title }} +Taxonomy Terms Content: {{ .Content }} +# Pages: {{ len .Data.Pages }} +{{ range .Paginator.Pages }} + Pag: {{ .Title }} +{{ end }} +{{ range $key, $value := .Data.Terms }} + k/v: {{ $key | lower }} / {{ printf "%s" $value }} +{{ end }} +{{ with .Site.Menus.mymenu }} +{{ range . }} +Taxonomy Terms Menu Item: {{ .Name }}: {{ .URL }} +{{ end }} +{{ end }} +Date: {{ .Date.Format "2006-01-02" }} +Lastmod: {{ .Lastmod.Format "2006-01-02" }} +`) +} + +func expectedFilePath(ugly bool, path ...string) string { + if ugly { + return filepath.Join(append(path[0:len(path)-1], path[len(path)-1]+".html")...) + } + return filepath.Join(append(path, "index.html")...) +} + +func expetedPermalink(ugly bool, path string) string { + if ugly { + return strings.TrimSuffix(path, "/") + ".html" + } + return path +} diff --git a/hugolib/page.go b/hugolib/page.go new file mode 100644 index 000000000..cf0a2144c --- /dev/null +++ b/hugolib/page.go @@ -0,0 +1,1776 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "bytes" + "errors" + "fmt" + "reflect" + + "github.com/bep/gitmap" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/output" + "github.com/gohugoio/hugo/parser" + "github.com/mitchellh/mapstructure" + + "html/template" + "io" + "path" + "path/filepath" + "regexp" + "strings" + "sync" + "time" + "unicode/utf8" + + bp "github.com/gohugoio/hugo/bufferpool" + "github.com/gohugoio/hugo/source" + "github.com/spf13/cast" +) + +var ( + cjk = regexp.MustCompile(`\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}`) + + // This is all the kinds we can expect to find in .Site.Pages. + allKindsInPages = []string{KindPage, KindHome, KindSection, KindTaxonomy, KindTaxonomyTerm} + + allKinds = append(allKindsInPages, []string{kindRSS, kindSitemap, kindRobotsTXT, kind404}...) +) + +const ( + KindPage = "page" + + // The rest are node types; home page, sections etc. + KindHome = "home" + KindSection = "section" + KindTaxonomy = "taxonomy" + KindTaxonomyTerm = "taxonomyTerm" + + // Temporary state. + kindUnknown = "unknown" + + // The following are (currently) temporary nodes, + // i.e. nodes we create just to render in isolation. + kindRSS = "RSS" + kindSitemap = "sitemap" + kindRobotsTXT = "robotsTXT" + kind404 = "404" +) + +type Page struct { + *pageInit + + // Kind is the discriminator that identifies the different page types + // in the different page collections. This can, as an example, be used + // to to filter regular pages, find sections etc. + // Kind will, for the pages available to the templates, be one of: + // page, home, section, taxonomy and taxonomyTerm. + // It is of string type to make it easy to reason about in + // the templates. + Kind string + + // Since Hugo 0.18 we got rid of the Node type. So now all pages are ... + // pages (regular pages, home page, sections etc.). + // Sections etc. will have child pages. These were earlier placed in .Data.Pages, + // but can now be more intuitively also be fetched directly from .Pages. + // This collection will be nil for regular pages. + Pages Pages + + // translations will contain references to this page in other language + // if available. + translations Pages + + // Params contains configuration defined in the params section of page frontmatter. + Params map[string]interface{} + + // Content sections + Content template.HTML + Summary template.HTML + TableOfContents template.HTML + + Aliases []string + + Images []Image + Videos []Video + + Truncated bool + Draft bool + Status string + + PublishDate time.Time + ExpiryDate time.Time + + // PageMeta contains page stats such as word count etc. + PageMeta + + // Markup contains the markup type for the content. + Markup string + + extension string + contentType string + renderable bool + + Layout string + + // For npn-renderable pages (see IsRenderable), the content itself + // is used as template and the template name is stored here. + selfLayout string + + linkTitle string + + frontmatter []byte + + // rawContent is the raw content read from the content file. + rawContent []byte + + // workContent is a copy of rawContent that may be mutated during site build. + workContent []byte + + // state telling if this is a "new page" or if we have rendered it previously. + rendered bool + + // whether the content is in a CJK language. + isCJKLanguage bool + + shortcodeState *shortcodeHandler + + // the content stripped for HTML + plain string // TODO should be []byte + plainWords []string + + // rendering configuration + renderingConfig *helpers.Blackfriday + + // menus + pageMenus PageMenus + + Source + + Position `json:"-"` + + GitInfo *gitmap.GitInfo + + // This was added as part of getting the Nodes (taxonomies etc.) to work as + // Pages in Hugo 0.18. + // It is deliberately named similar to Section, but not exported (for now). + // We currently have only one level of section in Hugo, but the page can live + // any number of levels down the file path. + // To support taxonomies like /categories/hugo etc. we will need to keep track + // of that information in a general way. + // So, sections represents the path to the content, i.e. a content file or a + // virtual content file in the situations where a taxonomy or a section etc. + // isn't accomanied by one. + sections []string + + // Will only be set for sections and regular pages. + parent *Page + + // When we create paginator pages, we create a copy of the original, + // but keep track of it here. + origOnCopy *Page + + // Will only be set for section pages and the home page. + subSections Pages + + s *Site + + // Pulled over from old Node. TODO(bep) reorg and group (embed) + + Site *SiteInfo `json:"-"` + + Title string + Description string + Keywords []string + Data map[string]interface{} + + Date time.Time + Lastmod time.Time + + Sitemap Sitemap + + URLPath + permalink string + relPermalink string + + layoutDescriptor output.LayoutDescriptor + + scratch *Scratch + + // It would be tempting to use the language set on the Site, but in they way we do + // multi-site processing, these values may differ during the initial page processing. + language *helpers.Language + + lang string + + // The output formats this page will be rendered to. + outputFormats output.Formats + + // This is the PageOutput that represents the first item in outputFormats. + // Use with care, as there are potential for inifinite loops. + mainPageOutput *PageOutput + + targetPathDescriptorPrototype *targetPathDescriptor +} + +func (p *Page) RSSLink() template.URL { + f, found := p.outputFormats.GetByName(output.RSSFormat.Name) + if !found { + return "" + } + return template.URL(newOutputFormat(p, f).Permalink()) +} + +func (p *Page) createLayoutDescriptor() output.LayoutDescriptor { + var section string + + switch p.Kind { + case KindSection: + // In Hugo 0.22 we introduce nested sections, but we still only + // use the first level to pick the correct template. This may change in + // the future. + section = p.sections[0] + case KindTaxonomy, KindTaxonomyTerm: + section = p.s.taxonomiesPluralSingular[p.sections[0]] + default: + } + + return output.LayoutDescriptor{ + Kind: p.Kind, + Type: p.Type(), + Layout: p.Layout, + Section: section, + } +} + +// pageInit lazy initializes different parts of the page. It is extracted +// into its own type so we can easily create a copy of a given page. +type pageInit struct { + languageInit sync.Once + pageMenusInit sync.Once + pageMetaInit sync.Once + pageOutputInit sync.Once + plainInit sync.Once + plainWordsInit sync.Once + renderingConfigInit sync.Once +} + +// IsNode returns whether this is an item of one of the list types in Hugo, +// i.e. not a regular content page. +func (p *Page) IsNode() bool { + return p.Kind != KindPage +} + +// IsHome returns whether this is the home page. +func (p *Page) IsHome() bool { + return p.Kind == KindHome +} + +// IsSection returns whether this is a section page. +func (p *Page) IsSection() bool { + return p.Kind == KindSection +} + +// IsPage returns whether this is a regular content page. +func (p *Page) IsPage() bool { + return p.Kind == KindPage +} + +type Source struct { + Frontmatter []byte + Content []byte + source.File +} +type PageMeta struct { + wordCount int + fuzzyWordCount int + readingTime int + Weight int +} + +type Position struct { + Prev *Page + Next *Page + PrevInSection *Page + NextInSection *Page +} + +type Pages []*Page + +func (ps Pages) String() string { + return fmt.Sprintf("Pages(%d)", len(ps)) +} + +func (ps Pages) findPagePosByFilePath(inPath string) int { + for i, x := range ps { + if x.Source.Path() == inPath { + return i + } + } + return -1 +} + +func (ps Pages) findFirstPagePosByFilePathPrefix(prefix string) int { + if prefix == "" { + return -1 + } + for i, x := range ps { + if strings.HasPrefix(x.Source.Path(), prefix) { + return i + } + } + return -1 +} + +// findPagePos Given a page, it will find the position in Pages +// will return -1 if not found +func (ps Pages) findPagePos(page *Page) int { + for i, x := range ps { + if x.Source.Path() == page.Source.Path() { + return i + } + } + return -1 +} + +func (p *Page) createWorkContentCopy() { + p.workContent = make([]byte, len(p.rawContent)) + copy(p.workContent, p.rawContent) +} + +func (p *Page) Plain() string { + p.initPlain() + return p.plain +} + +func (p *Page) PlainWords() []string { + p.initPlainWords() + return p.plainWords +} + +func (p *Page) initPlain() { + p.plainInit.Do(func() { + p.plain = helpers.StripHTML(string(p.Content)) + return + }) +} + +func (p *Page) initPlainWords() { + p.plainWordsInit.Do(func() { + p.plainWords = strings.Fields(p.Plain()) + return + }) +} + +// Param is a convenience method to do lookups in Page's and Site's Params map, +// in that order. +// +// This method is also implemented on Node and SiteInfo. +func (p *Page) Param(key interface{}) (interface{}, error) { + keyStr, err := cast.ToStringE(key) + if err != nil { + return nil, err + } + + keyStr = strings.ToLower(keyStr) + result, _ := p.traverseDirect(keyStr) + if result != nil { + return result, nil + } + + keySegments := strings.Split(keyStr, ".") + if len(keySegments) == 1 { + return nil, nil + } + + return p.traverseNested(keySegments) +} + +func (p *Page) traverseDirect(key string) (interface{}, error) { + keyStr := strings.ToLower(key) + if val, ok := p.Params[keyStr]; ok { + return val, nil + } + + return p.Site.Params[keyStr], nil +} + +func (p *Page) traverseNested(keySegments []string) (interface{}, error) { + result := traverse(keySegments, p.Params) + if result != nil { + return result, nil + } + + result = traverse(keySegments, p.Site.Params) + if result != nil { + return result, nil + } + + // Didn't find anything, but also no problems. + return nil, nil +} + +func traverse(keys []string, m map[string]interface{}) interface{} { + // Shift first element off. + firstKey, rest := keys[0], keys[1:] + result := m[firstKey] + + // No point in continuing here. + if result == nil { + return result + } + + if len(rest) == 0 { + // That was the last key. + return result + } else { + // That was not the last key. + return traverse(rest, cast.ToStringMap(result)) + } +} + +func (p *Page) Author() Author { + authors := p.Authors() + + for _, author := range authors { + return author + } + return Author{} +} + +func (p *Page) Authors() AuthorList { + authorKeys, ok := p.Params["authors"] + if !ok { + return AuthorList{} + } + authors := authorKeys.([]string) + if len(authors) < 1 || len(p.Site.Authors) < 1 { + return AuthorList{} + } + + al := make(AuthorList) + for _, author := range authors { + a, ok := p.Site.Authors[author] + if ok { + al[author] = a + } + } + return al +} + +func (p *Page) UniqueID() string { + return p.Source.UniqueID() +} + +// for logging +func (p *Page) lineNumRawContentStart() int { + return bytes.Count(p.frontmatter, []byte("\n")) + 1 +} + +var ( + internalSummaryDivider = []byte("HUGOMORE42") +) + +// We have to replace the <!--more--> with something that survives all the +// rendering engines. +// TODO(bep) inline replace +func (p *Page) replaceDivider(content []byte) []byte { + summaryDivider := helpers.SummaryDivider + // TODO(bep) handle better. + if p.Ext() == "org" || p.Markup == "org" { + summaryDivider = []byte("# more") + } + sections := bytes.Split(content, summaryDivider) + + // If the raw content has nothing but whitespace after the summary + // marker then the page shouldn't be marked as truncated. This check + // is simplest against the raw content because different markup engines + // (rst and asciidoc in particular) add div and p elements after the + // summary marker. + p.Truncated = (len(sections) == 2 && + len(bytes.Trim(sections[1], " \n\r")) > 0) + + return bytes.Join(sections, internalSummaryDivider) +} + +// Returns the page as summary and main if a user defined split is provided. +func (p *Page) setUserDefinedSummaryIfProvided(rawContentCopy []byte) (*summaryContent, error) { + + sc, err := splitUserDefinedSummaryAndContent(p.Markup, rawContentCopy) + + if err != nil { + return nil, err + } + + if sc == nil { + // No divider found + return nil, nil + } + + p.Summary = helpers.BytesToHTML(sc.summary) + + return sc, nil +} + +// Make this explicit so there is no doubt about what is what. +type summaryContent struct { + summary []byte + content []byte +} + +func splitUserDefinedSummaryAndContent(markup string, c []byte) (sc *summaryContent, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("summary split failed: %s", r) + } + }() + + c = bytes.TrimSpace(c) + startDivider := bytes.Index(c, internalSummaryDivider) + + if startDivider == -1 { + return + } + + endDivider := startDivider + len(internalSummaryDivider) + endSummary := startDivider + + var ( + startMarkup []byte + endMarkup []byte + addDiv bool + ) + + switch markup { + default: + startMarkup = []byte("<p>") + endMarkup = []byte("</p>") + case "asciidoc": + startMarkup = []byte("<div class=\"paragraph\">") + endMarkup = []byte("</div>") + case "rst": + startMarkup = []byte("<p>") + endMarkup = []byte("</p>") + addDiv = true + } + + // Find the closest end/start markup string to the divider + fromStart := -1 + fromIdx := bytes.LastIndex(c[:startDivider], startMarkup) + if fromIdx != -1 { + fromStart = startDivider - fromIdx - len(startMarkup) + } + fromEnd := bytes.Index(c[endDivider:], endMarkup) + + if fromEnd != -1 && fromEnd <= fromStart { + endSummary = startDivider + fromEnd + len(endMarkup) + } else if fromStart != -1 && fromEnd != -1 { + endSummary = startDivider - fromStart - len(startMarkup) + } + + withoutDivider := bytes.TrimSpace(append(c[:startDivider], c[endDivider:]...)) + var ( + summary []byte + ) + + if len(withoutDivider) > 0 { + summary = bytes.TrimSpace(withoutDivider[:endSummary]) + } + + if addDiv { + // For the rst + summary = append(append([]byte(nil), summary...), []byte("</div>")...) + } + + if err != nil { + return + } + + sc = &summaryContent{ + summary: summary, + content: withoutDivider, + } + + return +} + +func (p *Page) setAutoSummary() error { + var summary string + var truncated bool + if p.isCJKLanguage { + summary, truncated = helpers.TruncateWordsByRune(p.PlainWords(), helpers.SummaryLength) + } else { + summary, truncated = helpers.TruncateWordsToWholeSentence(p.Plain(), helpers.SummaryLength) + } + p.Summary = template.HTML(summary) + p.Truncated = truncated + + return nil +} + +func (p *Page) renderContent(content []byte) []byte { + var fn helpers.LinkResolverFunc + var fileFn helpers.FileResolverFunc + if p.getRenderingConfig().SourceRelativeLinksEval { + fn = func(ref string) (string, error) { + return p.Site.SourceRelativeLink(ref, p) + } + fileFn = func(ref string) (string, error) { + return p.Site.SourceRelativeLinkFile(ref, p) + } + } + + return p.s.ContentSpec.RenderBytes(&helpers.RenderingContext{ + Content: content, RenderTOC: true, PageFmt: p.determineMarkupType(), + Cfg: p.Language(), + DocumentID: p.UniqueID(), DocumentName: p.Path(), + Config: p.getRenderingConfig(), LinkResolver: fn, FileResolver: fileFn}) +} + +func (p *Page) getRenderingConfig() *helpers.Blackfriday { + p.renderingConfigInit.Do(func() { + p.renderingConfig = p.s.ContentSpec.NewBlackfriday() + + if p.Language() == nil { + panic(fmt.Sprintf("nil language for %s with source lang %s", p.BaseFileName(), p.lang)) + } + + bfParam := p.GetParam("blackfriday") + if bfParam == nil { + return + } + + pageParam := cast.ToStringMap(bfParam) + if err := mapstructure.Decode(pageParam, &p.renderingConfig); err != nil { + p.s.Log.FATAL.Printf("Failed to get rendering config for %s:\n%s", p.BaseFileName(), err.Error()) + } + + }) + + return p.renderingConfig +} + +func (s *Site) newPage(filename string) *Page { + sp := source.NewSourceSpec(s.Cfg, s.Fs) + p := &Page{ + pageInit: &pageInit{}, + Kind: kindFromFilename(filename), + contentType: "", + Source: Source{File: *sp.NewFile(filename)}, + Keywords: []string{}, Sitemap: Sitemap{Priority: -1}, + Params: make(map[string]interface{}), + translations: make(Pages, 0), + sections: sectionsFromFilename(filename), + Site: &s.Info, + s: s, + } + + s.Log.DEBUG.Println("Reading from", p.File.Path()) + return p +} + +func (p *Page) IsRenderable() bool { + return p.renderable +} + +func (p *Page) Type() string { + if p.contentType != "" { + return p.contentType + } + + if x := p.Section(); x != "" { + return x + } + + return "page" +} + +// Section returns the first path element below the content root. Note that +// since Hugo 0.22 we support nested sections, but this will always be the first +// element of any nested path. +func (p *Page) Section() string { + if p.Kind == KindSection { + return p.sections[0] + } + return p.Source.Section() +} + +func (s *Site) NewPageFrom(buf io.Reader, name string) (*Page, error) { + p, err := s.NewPage(name) + if err != nil { + return p, err + } + _, err = p.ReadFrom(buf) + + return p, err +} + +func (s *Site) NewPage(name string) (*Page, error) { + if len(name) == 0 { + return nil, errors.New("Zero length page name") + } + + // Create new page + p := s.newPage(name) + p.s = s + p.Site = &s.Info + + return p, nil +} + +func (p *Page) ReadFrom(buf io.Reader) (int64, error) { + // Parse for metadata & body + if err := p.parse(buf); err != nil { + p.s.Log.ERROR.Print(err) + return 0, err + } + + return int64(len(p.rawContent)), nil +} + +func (p *Page) WordCount() int { + p.analyzePage() + return p.wordCount +} + +func (p *Page) ReadingTime() int { + p.analyzePage() + return p.readingTime +} + +func (p *Page) FuzzyWordCount() int { + p.analyzePage() + return p.fuzzyWordCount +} + +func (p *Page) analyzePage() { + p.pageMetaInit.Do(func() { + if p.isCJKLanguage { + p.wordCount = 0 + for _, word := range p.PlainWords() { + runeCount := utf8.RuneCountInString(word) + if len(word) == runeCount { + p.wordCount++ + } else { + p.wordCount += runeCount + } + } + } else { + p.wordCount = helpers.TotalWords(p.Plain()) + } + + // TODO(bep) is set in a test. Fix that. + if p.fuzzyWordCount == 0 { + p.fuzzyWordCount = (p.wordCount + 100) / 100 * 100 + } + + if p.isCJKLanguage { + p.readingTime = (p.wordCount + 500) / 501 + } else { + p.readingTime = (p.wordCount + 212) / 213 + } + }) +} + +func (p *Page) Extension() string { + // Remove in Hugo 0.22. + helpers.Deprecated("Page", "Extension", "See OutputFormats with its MediaType", true) + return p.extension +} + +// AllTranslations returns all translations, including the current Page. +func (p *Page) AllTranslations() Pages { + return p.translations +} + +// IsTranslated returns whether this content file is translated to +// other language(s). +func (p *Page) IsTranslated() bool { + return len(p.translations) > 1 +} + +// Translations returns the translations excluding the current Page. +func (p *Page) Translations() Pages { + translations := make(Pages, 0) + for _, t := range p.translations { + if t.Lang() != p.Lang() { + translations = append(translations, t) + } + } + return translations +} + +func (p *Page) LinkTitle() string { + if len(p.linkTitle) > 0 { + return p.linkTitle + } + return p.Title +} + +func (p *Page) shouldBuild() bool { + return shouldBuild(p.s.Cfg.GetBool("buildFuture"), p.s.Cfg.GetBool("buildExpired"), + p.s.Cfg.GetBool("buildDrafts"), p.Draft, p.PublishDate, p.ExpiryDate) +} + +func shouldBuild(buildFuture bool, buildExpired bool, buildDrafts bool, Draft bool, + publishDate time.Time, expiryDate time.Time) bool { + if !(buildDrafts || !Draft) { + return false + } + if !buildFuture && !publishDate.IsZero() && publishDate.After(time.Now()) { + return false + } + if !buildExpired && !expiryDate.IsZero() && expiryDate.Before(time.Now()) { + return false + } + return true +} + +func (p *Page) IsDraft() bool { + return p.Draft +} + +func (p *Page) IsFuture() bool { + if p.PublishDate.IsZero() { + return false + } + return p.PublishDate.After(time.Now()) +} + +func (p *Page) IsExpired() bool { + if p.ExpiryDate.IsZero() { + return false + } + return p.ExpiryDate.Before(time.Now()) +} + +func (p *Page) URL() string { + + if p.IsPage() && p.URLPath.URL != "" { + // This is the url set in front matter + return p.URLPath.URL + } + // Fall back to the relative permalink. + u := p.RelPermalink() + return u +} + +// Permalink returns the absolute URL to this Page. +func (p *Page) Permalink() string { + return p.permalink +} + +// RelPermalink gets a URL to the resource relative to the host. +func (p *Page) RelPermalink() string { + return p.relPermalink +} + +func (p *Page) initURLs() error { + if len(p.outputFormats) == 0 { + p.outputFormats = p.s.outputFormats[p.Kind] + } + rel := p.createRelativePermalink() + + var err error + p.permalink, err = p.s.permalinkForOutputFormat(rel, p.outputFormats[0]) + if err != nil { + return err + } + rel = p.s.PathSpec.PrependBasePath(rel) + p.relPermalink = rel + p.layoutDescriptor = p.createLayoutDescriptor() + return nil +} + +var ErrHasDraftAndPublished = errors.New("both draft and published parameters were found in page's frontmatter") + +func (p *Page) update(f interface{}) error { + if f == nil { + return errors.New("no metadata found") + } + m := f.(map[string]interface{}) + // Needed for case insensitive fetching of params values + helpers.ToLowerMap(m) + + var err error + var draft, published, isCJKLanguage *bool + for k, v := range m { + loki := strings.ToLower(k) + switch loki { + case "title": + p.Title = cast.ToString(v) + p.Params[loki] = p.Title + case "linktitle": + p.linkTitle = cast.ToString(v) + p.Params[loki] = p.linkTitle + case "description": + p.Description = cast.ToString(v) + p.Params[loki] = p.Description + case "slug": + p.Slug = cast.ToString(v) + p.Params[loki] = p.Slug + case "url": + if url := cast.ToString(v); strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") { + return fmt.Errorf("Only relative URLs are supported, %v provided", url) + } + p.URLPath.URL = cast.ToString(v) + p.Params[loki] = p.URLPath.URL + case "type": + p.contentType = cast.ToString(v) + p.Params[loki] = p.contentType + case "extension", "ext": + p.extension = cast.ToString(v) + p.Params[loki] = p.extension + case "keywords": + p.Keywords = cast.ToStringSlice(v) + p.Params[loki] = p.Keywords + case "date": + p.Date, err = cast.ToTimeE(v) + if err != nil { + p.s.Log.ERROR.Printf("Failed to parse date '%v' in page %s", v, p.File.Path()) + } + p.Params[loki] = p.Date + case "lastmod": + p.Lastmod, err = cast.ToTimeE(v) + if err != nil { + p.s.Log.ERROR.Printf("Failed to parse lastmod '%v' in page %s", v, p.File.Path()) + } + case "outputs": + o := cast.ToStringSlice(v) + if len(o) > 0 { + // Output formats are exlicitly set in front matter, use those. + outFormats, err := p.s.outputFormatsConfig.GetByNames(o...) + + if err != nil { + p.s.Log.ERROR.Printf("Failed to resolve output formats: %s", err) + } else { + p.outputFormats = outFormats + p.Params[loki] = outFormats + } + + } + //p.Params[loki] = p.Keywords + case "publishdate", "pubdate": + p.PublishDate, err = cast.ToTimeE(v) + if err != nil { + p.s.Log.ERROR.Printf("Failed to parse publishdate '%v' in page %s", v, p.File.Path()) + } + case "expirydate", "unpublishdate": + p.ExpiryDate, err = cast.ToTimeE(v) + if err != nil { + p.s.Log.ERROR.Printf("Failed to parse expirydate '%v' in page %s", v, p.File.Path()) + } + case "draft": + draft = new(bool) + *draft = cast.ToBool(v) + case "published": // Intentionally undocumented + published = new(bool) + *published = cast.ToBool(v) + case "layout": + p.Layout = cast.ToString(v) + p.Params[loki] = p.Layout + case "markup": + p.Markup = cast.ToString(v) + p.Params[loki] = p.Markup + case "weight": + p.Weight = cast.ToInt(v) + p.Params[loki] = p.Weight + case "aliases": + p.Aliases = cast.ToStringSlice(v) + for _, alias := range p.Aliases { + if strings.HasPrefix(alias, "http://") || strings.HasPrefix(alias, "https://") { + return fmt.Errorf("Only relative aliases are supported, %v provided", alias) + } + } + p.Params[loki] = p.Aliases + case "status": + p.Status = cast.ToString(v) + p.Params[loki] = p.Status + case "sitemap": + p.Sitemap = parseSitemap(cast.ToStringMap(v)) + p.Params[loki] = p.Sitemap + case "iscjklanguage": + isCJKLanguage = new(bool) + *isCJKLanguage = cast.ToBool(v) + default: + // If not one of the explicit values, store in Params + switch vv := v.(type) { + case bool: + p.Params[loki] = vv + case string: + p.Params[loki] = vv + case int64, int32, int16, int8, int: + p.Params[loki] = vv + case float64, float32: + p.Params[loki] = vv + case time.Time: + p.Params[loki] = vv + default: // handle array of strings as well + switch vvv := vv.(type) { + case []interface{}: + if len(vvv) > 0 { + switch vvv[0].(type) { + case map[interface{}]interface{}: // Proper parsing structured array from YAML based FrontMatter + p.Params[loki] = vvv + case map[string]interface{}: // Proper parsing structured array from JSON based FrontMatter + p.Params[loki] = vvv + case []interface{}: + p.Params[loki] = vvv + default: + a := make([]string, len(vvv)) + for i, u := range vvv { + a[i] = cast.ToString(u) + } + + p.Params[loki] = a + } + } else { + p.Params[loki] = []string{} + } + default: + p.Params[loki] = vv + } + } + } + } + + if draft != nil && published != nil { + p.Draft = *draft + p.s.Log.ERROR.Printf("page %s has both draft and published settings in its frontmatter. Using draft.", p.File.Path()) + return ErrHasDraftAndPublished + } else if draft != nil { + p.Draft = *draft + } else if published != nil { + p.Draft = !*published + } + p.Params["draft"] = p.Draft + + if p.Date.IsZero() && p.s.Cfg.GetBool("useModTimeAsFallback") { + fi, err := p.s.Fs.Source.Stat(filepath.Join(p.s.PathSpec.AbsPathify(p.s.Cfg.GetString("contentDir")), p.File.Path())) + if err == nil { + p.Date = fi.ModTime() + p.Params["date"] = p.Date + } + } + + if p.Lastmod.IsZero() { + p.Lastmod = p.Date + } + p.Params["lastmod"] = p.Lastmod + + if isCJKLanguage != nil { + p.isCJKLanguage = *isCJKLanguage + } else if p.s.Cfg.GetBool("hasCJKLanguage") { + if cjk.Match(p.rawContent) { + p.isCJKLanguage = true + } else { + p.isCJKLanguage = false + } + } + p.Params["iscjklanguage"] = p.isCJKLanguage + + return nil + +} + +func (p *Page) GetParam(key string) interface{} { + return p.getParam(key, true) +} + +func (p *Page) getParam(key string, stringToLower bool) interface{} { + v := p.Params[strings.ToLower(key)] + + if v == nil { + return nil + } + + switch val := v.(type) { + case bool: + return val + case string: + if stringToLower { + return strings.ToLower(val) + } + return val + case int64, int32, int16, int8, int: + return cast.ToInt(v) + case float64, float32: + return cast.ToFloat64(v) + case time.Time: + return val + case []string: + if stringToLower { + return helpers.SliceToLower(val) + } + return v + case map[string]interface{}: // JSON and TOML + return v + case map[interface{}]interface{}: // YAML + return v + } + + p.s.Log.ERROR.Printf("GetParam(\"%s\"): Unknown type %s\n", key, reflect.TypeOf(v)) + return nil +} + +func (p *Page) HasMenuCurrent(menuID string, me *MenuEntry) bool { + + sectionPagesMenu := p.Site.sectionPagesMenu + + // page is labeled as "shadow-member" of the menu with the same identifier as the section + if sectionPagesMenu != "" { + section := p.Section() + + if section != "" && sectionPagesMenu == menuID && section == me.Identifier { + return true + } + } + + if !me.HasChildren() { + return false + } + + menus := p.Menus() + + if m, ok := menus[menuID]; ok { + + for _, child := range me.Children { + if child.IsEqual(m) { + return true + } + if p.HasMenuCurrent(menuID, child) { + return true + } + } + + } + + if p.IsPage() { + return false + } + + // The following logic is kept from back when Hugo had both Page and Node types. + // TODO(bep) consolidate / clean + nme := MenuEntry{Name: p.Title, URL: p.URL()} + + for _, child := range me.Children { + if nme.IsSameResource(child) { + return true + } + if p.HasMenuCurrent(menuID, child) { + return true + } + } + + return false + +} + +func (p *Page) IsMenuCurrent(menuID string, inme *MenuEntry) bool { + + menus := p.Menus() + + if me, ok := menus[menuID]; ok { + if me.IsEqual(inme) { + return true + } + } + + if p.IsPage() { + return false + } + + // The following logic is kept from back when Hugo had both Page and Node types. + // TODO(bep) consolidate / clean + me := MenuEntry{Name: p.Title, URL: p.URL()} + + if !me.IsSameResource(inme) { + return false + } + + // this resource may be included in several menus + // search for it to make sure that it is in the menu with the given menuId + if menu, ok := (*p.Site.Menus)[menuID]; ok { + for _, menuEntry := range *menu { + if menuEntry.IsSameResource(inme) { + return true + } + + descendantFound := p.isSameAsDescendantMenu(inme, menuEntry) + if descendantFound { + return descendantFound + } + + } + } + + return false +} + +func (p *Page) isSameAsDescendantMenu(inme *MenuEntry, parent *MenuEntry) bool { + if parent.HasChildren() { + for _, child := range parent.Children { + if child.IsSameResource(inme) { + return true + } + descendantFound := p.isSameAsDescendantMenu(inme, child) + if descendantFound { + return descendantFound + } + } + } + return false +} + +func (p *Page) Menus() PageMenus { + p.pageMenusInit.Do(func() { + p.pageMenus = PageMenus{} + + if ms, ok := p.Params["menu"]; ok { + link := p.RelPermalink() + + me := MenuEntry{Name: p.LinkTitle(), Weight: p.Weight, URL: link} + + // Could be the name of the menu to attach it to + mname, err := cast.ToStringE(ms) + + if err == nil { + me.Menu = mname + p.pageMenus[mname] = &me + return + } + + // Could be a slice of strings + mnames, err := cast.ToStringSliceE(ms) + + if err == nil { + for _, mname := range mnames { + me.Menu = mname + p.pageMenus[mname] = &me + } + return + } + + // Could be a structured menu entry + menus, err := cast.ToStringMapE(ms) + + if err != nil { + p.s.Log.ERROR.Printf("unable to process menus for %q\n", p.Title) + } + + for name, menu := range menus { + menuEntry := MenuEntry{Name: p.LinkTitle(), URL: link, Weight: p.Weight, Menu: name} + if menu != nil { + p.s.Log.DEBUG.Printf("found menu: %q, in %q\n", name, p.Title) + ime, err := cast.ToStringMapE(menu) + if err != nil { + p.s.Log.ERROR.Printf("unable to process menus for %q: %s", p.Title, err) + } + + menuEntry.marshallMap(ime) + } + p.pageMenus[name] = &menuEntry + + } + } + }) + + return p.pageMenus +} + +func (p *Page) shouldRenderTo(f output.Format) bool { + _, found := p.outputFormats.GetByName(f.Name) + return found +} + +func (p *Page) determineMarkupType() string { + // Try markup explicitly set in the frontmatter + p.Markup = helpers.GuessType(p.Markup) + if p.Markup == "unknown" { + // Fall back to file extension (might also return "unknown") + p.Markup = helpers.GuessType(p.Source.Ext()) + } + + return p.Markup +} + +func (p *Page) parse(reader io.Reader) error { + psr, err := parser.ReadFrom(reader) + if err != nil { + return err + } + + p.renderable = psr.IsRenderable() + p.frontmatter = psr.FrontMatter() + p.rawContent = psr.Content() + p.lang = p.Source.File.Lang() + + meta, err := psr.Metadata() + if err != nil { + return fmt.Errorf("failed to parse page metadata for %q: %s", p.File.Path(), err) + } + + if meta != nil { + if err = p.update(meta); err != nil { + return err + } + } + + return nil +} + +func (p *Page) RawContent() string { + return string(p.rawContent) +} + +func (p *Page) SetSourceContent(content []byte) { + p.Source.Content = content +} + +func (p *Page) SetSourceMetaData(in interface{}, mark rune) (err error) { + // See https://github.com/gohugoio/hugo/issues/2458 + defer func() { + if r := recover(); r != nil { + var ok bool + err, ok = r.(error) + if !ok { + err = fmt.Errorf("error from marshal: %v", r) + } + } + }() + + buf := bp.GetBuffer() + defer bp.PutBuffer(buf) + + err = parser.InterfaceToFrontMatter(in, mark, buf) + if err != nil { + return + } + + _, err = buf.WriteRune('\n') + if err != nil { + return + } + + p.Source.Frontmatter = buf.Bytes() + + return +} + +func (p *Page) SafeSaveSourceAs(path string) error { + return p.saveSourceAs(path, true) +} + +func (p *Page) SaveSourceAs(path string) error { + return p.saveSourceAs(path, false) +} + +func (p *Page) saveSourceAs(path string, safe bool) error { + b := bp.GetBuffer() + defer bp.PutBuffer(b) + + b.Write(p.Source.Frontmatter) + b.Write(p.Source.Content) + + bc := make([]byte, b.Len(), b.Len()) + copy(bc, b.Bytes()) + + return p.saveSource(bc, path, safe) +} + +func (p *Page) saveSource(by []byte, inpath string, safe bool) (err error) { + if !filepath.IsAbs(inpath) { + inpath = p.s.PathSpec.AbsPathify(inpath) + } + p.s.Log.INFO.Println("creating", inpath) + if safe { + err = helpers.SafeWriteToDisk(inpath, bytes.NewReader(by), p.s.Fs.Source) + } else { + err = helpers.WriteToDisk(inpath, bytes.NewReader(by), p.s.Fs.Source) + } + if err != nil { + return + } + return nil +} + +func (p *Page) SaveSource() error { + return p.SaveSourceAs(p.FullFilePath()) +} + +func (p *Page) processShortcodes() error { + p.shortcodeState = newShortcodeHandler(p) + tmpContent, err := p.shortcodeState.extractShortcodes(string(p.workContent), p) + if err != nil { + return err + } + p.workContent = []byte(tmpContent) + + return nil + +} + +func (p *Page) FullFilePath() string { + return filepath.Join(p.Dir(), p.LogicalName()) +} + +// Pre render prepare steps + +func (p *Page) prepareLayouts() error { + // TODO(bep): Check the IsRenderable logic. + if p.Kind == KindPage { + if !p.IsRenderable() { + self := "__" + p.UniqueID() + err := p.s.TemplateHandler().AddLateTemplate(self, string(p.Content)) + if err != nil { + return err + } + p.selfLayout = self + } + } + + return nil +} + +func (p *Page) prepareData(s *Site) error { + if p.Kind != KindSection { + var pages Pages + p.Data = make(map[string]interface{}) + + switch p.Kind { + case KindPage: + case KindHome: + pages = s.RegularPages + case KindTaxonomy: + plural := p.sections[0] + term := p.sections[1] + + if s.Info.preserveTaxonomyNames { + if v, ok := s.taxonomiesOrigKey[fmt.Sprintf("%s-%s", plural, term)]; ok { + term = v + } + } + + singular := s.taxonomiesPluralSingular[plural] + taxonomy := s.Taxonomies[plural].Get(term) + + p.Data[singular] = taxonomy + p.Data["Singular"] = singular + p.Data["Plural"] = plural + p.Data["Term"] = term + pages = taxonomy.Pages() + case KindTaxonomyTerm: + plural := p.sections[0] + singular := s.taxonomiesPluralSingular[plural] + + p.Data["Singular"] = singular + p.Data["Plural"] = plural + p.Data["Terms"] = s.Taxonomies[plural] + // keep the following just for legacy reasons + p.Data["OrderedIndex"] = p.Data["Terms"] + p.Data["Index"] = p.Data["Terms"] + + // A list of all KindTaxonomy pages with matching plural + for _, p := range s.findPagesByKind(KindTaxonomy) { + if p.sections[0] == plural { + pages = append(pages, p) + } + } + } + + p.Data["Pages"] = pages + p.Pages = pages + } + + // Now we know enough to set missing dates on home page etc. + p.updatePageDates() + + return nil +} + +func (p *Page) updatePageDates() { + // TODO(bep) there is a potential issue with page sorting for home pages + // etc. without front matter dates set, but let us wrap the head around + // that in another time. + if !p.IsNode() { + return + } + + if !p.Date.IsZero() { + if p.Lastmod.IsZero() { + p.Lastmod = p.Date + } + return + } else if !p.Lastmod.IsZero() { + if p.Date.IsZero() { + p.Date = p.Lastmod + } + return + } + + // Set it to the first non Zero date in children + var foundDate, foundLastMod bool + + for _, child := range p.Pages { + if !child.Date.IsZero() { + p.Date = child.Date + foundDate = true + } + if !child.Lastmod.IsZero() { + p.Lastmod = child.Lastmod + foundLastMod = true + } + + if foundDate && foundLastMod { + break + } + } +} + +// copy creates a copy of this page with the lazy sync.Once vars reset +// so they will be evaluated again, for word count calculations etc. +func (p *Page) copy() *Page { + c := *p + c.pageInit = &pageInit{} + return &c +} + +func (p *Page) Now() time.Time { + // Delete in Hugo 0.22 + helpers.Deprecated("Page", "Now", "Use now (the template func)", true) + return time.Now() +} + +func (p *Page) Hugo() *HugoInfo { + return hugoInfo +} + +func (p *Page) Ref(refs ...string) (string, error) { + if len(refs) == 0 { + return "", nil + } + if len(refs) > 1 { + return p.Site.Ref(refs[0], nil, refs[1]) + } + return p.Site.Ref(refs[0], nil) +} + +func (p *Page) RelRef(refs ...string) (string, error) { + if len(refs) == 0 { + return "", nil + } + if len(refs) > 1 { + return p.Site.RelRef(refs[0], nil, refs[1]) + } + return p.Site.RelRef(refs[0], nil) +} + +func (p *Page) String() string { + return fmt.Sprintf("Page(%q)", p.Title) +} + +type URLPath struct { + URL string + Permalink string + Slug string + Section string +} + +// Scratch returns the writable context associated with this Page. +func (p *Page) Scratch() *Scratch { + if p.scratch == nil { + p.scratch = newScratch() + } + return p.scratch +} + +func (p *Page) Language() *helpers.Language { + p.initLanguage() + return p.language +} + +func (p *Page) Lang() string { + // When set, Language can be different from lang in the case where there is a + // content file (doc.sv.md) with language indicator, but there is no language + // config for that language. Then the language will fall back on the site default. + if p.Language() != nil { + return p.Language().Lang + } + return p.lang +} + +func (p *Page) isNewTranslation(candidate *Page) bool { + + if p.Kind != candidate.Kind { + return false + } + + if p.Kind == KindPage || p.Kind == kindUnknown { + panic("Node type not currently supported for this op") + } + + // At this point, we know that this is a traditional Node (home page, section, taxonomy) + // It represents the same node, but different language, if the sections is the same. + if len(p.sections) != len(candidate.sections) { + return false + } + + for i := 0; i < len(p.sections); i++ { + if p.sections[i] != candidate.sections[i] { + return false + } + } + + // Finally check that it is not already added. + for _, translation := range p.translations { + if candidate == translation { + return false + } + } + + return true + +} + +func (p *Page) shouldAddLanguagePrefix() bool { + if !p.Site.IsMultiLingual() { + return false + } + + if p.Lang() == "" { + return false + } + + if !p.Site.defaultContentLanguageInSubdir && p.Lang() == p.Site.multilingual.DefaultLang.Lang { + return false + } + + return true +} + +func (p *Page) initLanguage() { + p.languageInit.Do(func() { + if p.language != nil { + return + } + + ml := p.Site.multilingual + if ml == nil { + panic("Multilanguage not set") + } + if p.lang == "" { + p.lang = ml.DefaultLang.Lang + p.language = ml.DefaultLang + return + } + + language := ml.Language(p.lang) + + if language == nil { + // It can be a file named stefano.chiodino.md. + p.s.Log.WARN.Printf("Page language (if it is that) not found in multilang setup: %s.", p.lang) + language = ml.DefaultLang + } + + p.language = language + + }) +} + +func (p *Page) LanguagePrefix() string { + return p.Site.LanguagePrefix +} + +func (p *Page) addLangPathPrefix(outfile string) string { + return p.addLangPathPrefixIfFlagSet(outfile, p.shouldAddLanguagePrefix()) +} + +func (p *Page) addLangPathPrefixIfFlagSet(outfile string, should bool) string { + if helpers.IsAbsURL(outfile) { + return outfile + } + + if !should { + return outfile + } + + hadSlashSuffix := strings.HasSuffix(outfile, "/") + + outfile = "/" + path.Join(p.Lang(), outfile) + if hadSlashSuffix { + outfile += "/" + } + return outfile +} + +func sectionsFromFilename(filename string) []string { + var sections []string + dir, _ := filepath.Split(filename) + dir = strings.TrimSuffix(dir, helpers.FilePathSeparator) + if dir == "" { + return sections + } + sections = strings.Split(dir, helpers.FilePathSeparator) + return sections +} + +const ( + regularPageFileNameDoesNotStartWith = "_index" + + // There can be "my_regular_index_page.md but not /_index_file.md + regularPageFileNameDoesNotContain = helpers.FilePathSeparator + regularPageFileNameDoesNotStartWith +) + +func kindFromFilename(filename string) string { + if !strings.HasPrefix(filename, regularPageFileNameDoesNotStartWith) && !strings.Contains(filename, regularPageFileNameDoesNotContain) { + return KindPage + } + + if strings.HasPrefix(filename, "_index") { + return KindHome + } + + // We don't know enough yet to determine the type. + return kindUnknown +} + +func (p *Page) setValuesForKind(s *Site) { + if p.Kind == kindUnknown { + // This is either a taxonomy list, taxonomy term or a section + nodeType := s.kindFromSections(p.sections) + + if nodeType == kindUnknown { + panic(fmt.Sprintf("Unable to determine page kind from %q", p.sections)) + } + + p.Kind = nodeType + } + + switch p.Kind { + case KindHome: + p.URLPath.URL = "/" + case KindPage: + default: + p.URLPath.URL = "/" + path.Join(p.sections...) + "/" + } +} + +// Used in error logs. +func (p *Page) pathOrTitle() string { + if p.Path() != "" { + return p.Path() + } + return p.Title +} diff --git a/hugolib/pageCache.go b/hugolib/pageCache.go new file mode 100644 index 000000000..e0a3a160b --- /dev/null +++ b/hugolib/pageCache.go @@ -0,0 +1,108 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "sync" +) + +type pageCache struct { + sync.RWMutex + m map[string][][2]Pages +} + +func newPageCache() *pageCache { + return &pageCache{m: make(map[string][][2]Pages)} +} + +// get gets a Pages slice from the cache matching the given key and Pages slice. +// If none found in cache, a copy of the supplied slice is created. +// +// If an apply func is provided, that func is applied to the newly created copy. +// +// The cache and the execution of the apply func is protected by a RWMutex. +func (c *pageCache) get(key string, p Pages, apply func(p Pages)) (Pages, bool) { + c.RLock() + if cached, ok := c.m[key]; ok { + for _, ps := range cached { + if probablyEqualPages(p, ps[0]) { + c.RUnlock() + return ps[1], true + } + } + + } + c.RUnlock() + + c.Lock() + defer c.Unlock() + + // double-check + if cached, ok := c.m[key]; ok { + for _, ps := range cached { + if probablyEqualPages(p, ps[0]) { + return ps[1], true + } + } + } + + pagesCopy := append(Pages(nil), p...) + + if apply != nil { + apply(pagesCopy) + } + + if v, ok := c.m[key]; ok { + c.m[key] = append(v, [2]Pages{p, pagesCopy}) + } else { + c.m[key] = [][2]Pages{{p, pagesCopy}} + } + + return pagesCopy, false + +} + +// "probably" as in: we do not compare every element for big slices, but that is +// good enough for our use case. +// TODO(bep) there is a similar method in pagination.go. DRY. +func probablyEqualPages(p1, p2 Pages) bool { + if p1 == nil && p2 == nil { + return true + } + + if p1 == nil || p2 == nil { + return false + } + + if p1.Len() != p2.Len() { + return false + } + + if p1.Len() == 0 { + return true + } + + step := 1 + + if len(p1) >= 50 { + step = len(p1) / 10 + } + + for i := 0; i < len(p1); i += step { + if p1[i] != p2[i] { + return false + } + } + return true +} diff --git a/hugolib/pageCache_test.go b/hugolib/pageCache_test.go new file mode 100644 index 000000000..62837394f --- /dev/null +++ b/hugolib/pageCache_test.go @@ -0,0 +1,73 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "sync" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPageCache(t *testing.T) { + t.Parallel() + c1 := newPageCache() + + changeFirst := func(p Pages) { + p[0].Description = "changed" + } + + var o1 uint64 + var o2 uint64 + + var wg sync.WaitGroup + + var l1 sync.Mutex + var l2 sync.Mutex + + var testPageSets []Pages + + s := newTestSite(t) + + for i := 0; i < 50; i++ { + testPageSets = append(testPageSets, createSortTestPages(s, i+1)) + } + + for j := 0; j < 100; j++ { + wg.Add(1) + go func() { + defer wg.Done() + for k, pages := range testPageSets { + l1.Lock() + p, c := c1.get("k1", pages, nil) + assert.Equal(t, !atomic.CompareAndSwapUint64(&o1, uint64(k), uint64(k+1)), c) + l1.Unlock() + p2, c2 := c1.get("k1", p, nil) + assert.True(t, c2) + assert.True(t, probablyEqualPages(p, p2)) + assert.True(t, probablyEqualPages(p, pages)) + assert.NotNil(t, p) + + l2.Lock() + p3, c3 := c1.get("k2", pages, changeFirst) + assert.Equal(t, !atomic.CompareAndSwapUint64(&o2, uint64(k), uint64(k+1)), c3) + l2.Unlock() + assert.NotNil(t, p3) + assert.Equal(t, p3[0].Description, "changed") + } + }() + } + wg.Wait() +} diff --git a/hugolib/pageGroup.go b/hugolib/pageGroup.go new file mode 100644 index 000000000..343ecf52e --- /dev/null +++ b/hugolib/pageGroup.go @@ -0,0 +1,297 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "errors" + "reflect" + "sort" + "strings" + "time" +) + +// PageGroup represents a group of pages, grouped by the key. +// The key is typically a year or similar. +type PageGroup struct { + Key interface{} + Pages Pages +} + +type mapKeyValues []reflect.Value + +func (v mapKeyValues) Len() int { return len(v) } +func (v mapKeyValues) Swap(i, j int) { v[i], v[j] = v[j], v[i] } + +type mapKeyByInt struct{ mapKeyValues } + +func (s mapKeyByInt) Less(i, j int) bool { return s.mapKeyValues[i].Int() < s.mapKeyValues[j].Int() } + +type mapKeyByStr struct{ mapKeyValues } + +func (s mapKeyByStr) Less(i, j int) bool { + return s.mapKeyValues[i].String() < s.mapKeyValues[j].String() +} + +func sortKeys(v []reflect.Value, order string) []reflect.Value { + if len(v) <= 1 { + return v + } + + switch v[0].Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if order == "desc" { + sort.Sort(sort.Reverse(mapKeyByInt{v})) + } else { + sort.Sort(mapKeyByInt{v}) + } + case reflect.String: + if order == "desc" { + sort.Sort(sort.Reverse(mapKeyByStr{v})) + } else { + sort.Sort(mapKeyByStr{v}) + } + } + return v +} + +// PagesGroup represents a list of page groups. +// This is what you get when doing page grouping in the templates. +type PagesGroup []PageGroup + +// Reverse reverses the order of this list of page groups. +func (p PagesGroup) Reverse() PagesGroup { + for i, j := 0, len(p)-1; i < j; i, j = i+1, j-1 { + p[i], p[j] = p[j], p[i] + } + + return p +} + +var ( + errorType = reflect.TypeOf((*error)(nil)).Elem() + pagePtrType = reflect.TypeOf((*Page)(nil)) +) + +// GroupBy groups by the value in the given field or method name and with the given order. +// Valid values for order is asc, desc, rev and reverse. +func (p Pages) GroupBy(key string, order ...string) (PagesGroup, error) { + if len(p) < 1 { + return nil, nil + } + + direction := "asc" + + if len(order) > 0 && (strings.ToLower(order[0]) == "desc" || strings.ToLower(order[0]) == "rev" || strings.ToLower(order[0]) == "reverse") { + direction = "desc" + } + + var ft interface{} + m, ok := pagePtrType.MethodByName(key) + if ok { + if m.Type.NumIn() != 1 || m.Type.NumOut() == 0 || m.Type.NumOut() > 2 { + return nil, errors.New(key + " is a Page method but you can't use it with GroupBy") + } + if m.Type.NumOut() == 1 && m.Type.Out(0).Implements(errorType) { + return nil, errors.New(key + " is a Page method but you can't use it with GroupBy") + } + if m.Type.NumOut() == 2 && !m.Type.Out(1).Implements(errorType) { + return nil, errors.New(key + " is a Page method but you can't use it with GroupBy") + } + ft = m + } else { + ft, ok = pagePtrType.Elem().FieldByName(key) + if !ok { + return nil, errors.New(key + " is neither a field nor a method of Page") + } + } + + var tmp reflect.Value + switch e := ft.(type) { + case reflect.StructField: + tmp = reflect.MakeMap(reflect.MapOf(e.Type, reflect.SliceOf(pagePtrType))) + case reflect.Method: + tmp = reflect.MakeMap(reflect.MapOf(e.Type.Out(0), reflect.SliceOf(pagePtrType))) + } + + for _, e := range p { + ppv := reflect.ValueOf(e) + var fv reflect.Value + switch ft.(type) { + case reflect.StructField: + fv = ppv.Elem().FieldByName(key) + case reflect.Method: + fv = ppv.MethodByName(key).Call([]reflect.Value{})[0] + } + if !fv.IsValid() { + continue + } + if !tmp.MapIndex(fv).IsValid() { + tmp.SetMapIndex(fv, reflect.MakeSlice(reflect.SliceOf(pagePtrType), 0, 0)) + } + tmp.SetMapIndex(fv, reflect.Append(tmp.MapIndex(fv), ppv)) + } + + var r []PageGroup + for _, k := range sortKeys(tmp.MapKeys(), direction) { + r = append(r, PageGroup{Key: k.Interface(), Pages: tmp.MapIndex(k).Interface().([]*Page)}) + } + + return r, nil +} + +// GroupByParam groups by the given page parameter key's value and with the given order. +// Valid values for order is asc, desc, rev and reverse. +func (p Pages) GroupByParam(key string, order ...string) (PagesGroup, error) { + if len(p) < 1 { + return nil, nil + } + + direction := "asc" + + if len(order) > 0 && (strings.ToLower(order[0]) == "desc" || strings.ToLower(order[0]) == "rev" || strings.ToLower(order[0]) == "reverse") { + direction = "desc" + } + + var tmp reflect.Value + var keyt reflect.Type + for _, e := range p { + param := e.GetParam(key) + if param != nil { + if _, ok := param.([]string); !ok { + keyt = reflect.TypeOf(param) + tmp = reflect.MakeMap(reflect.MapOf(keyt, reflect.SliceOf(pagePtrType))) + break + } + } + } + if !tmp.IsValid() { + return nil, errors.New("There is no such a param") + } + + for _, e := range p { + param := e.getParam(key, false) + if param == nil || reflect.TypeOf(param) != keyt { + continue + } + v := reflect.ValueOf(param) + if !tmp.MapIndex(v).IsValid() { + tmp.SetMapIndex(v, reflect.MakeSlice(reflect.SliceOf(pagePtrType), 0, 0)) + } + tmp.SetMapIndex(v, reflect.Append(tmp.MapIndex(v), reflect.ValueOf(e))) + } + + var r []PageGroup + for _, k := range sortKeys(tmp.MapKeys(), direction) { + r = append(r, PageGroup{Key: k.Interface(), Pages: tmp.MapIndex(k).Interface().([]*Page)}) + } + + return r, nil +} + +func (p Pages) groupByDateField(sorter func(p Pages) Pages, formatter func(p *Page) string, order ...string) (PagesGroup, error) { + if len(p) < 1 { + return nil, nil + } + + sp := sorter(p) + + if !(len(order) > 0 && (strings.ToLower(order[0]) == "asc" || strings.ToLower(order[0]) == "rev" || strings.ToLower(order[0]) == "reverse")) { + sp = sp.Reverse() + } + + date := formatter(sp[0]) + var r []PageGroup + r = append(r, PageGroup{Key: date, Pages: make(Pages, 0)}) + r[0].Pages = append(r[0].Pages, sp[0]) + + i := 0 + for _, e := range sp[1:] { + date = formatter(e) + if r[i].Key.(string) != date { + r = append(r, PageGroup{Key: date}) + i++ + } + r[i].Pages = append(r[i].Pages, e) + } + return r, nil +} + +// GroupByDate groups by the given page's Date value in +// the given format and with the given order. +// Valid values for order is asc, desc, rev and reverse. +// For valid format strings, see https://golang.org/pkg/time/#Time.Format +func (p Pages) GroupByDate(format string, order ...string) (PagesGroup, error) { + sorter := func(p Pages) Pages { + return p.ByDate() + } + formatter := func(p *Page) string { + return p.Date.Format(format) + } + return p.groupByDateField(sorter, formatter, order...) +} + +// GroupByPublishDate groups by the given page's PublishDate value in +// the given format and with the given order. +// Valid values for order is asc, desc, rev and reverse. +// For valid format strings, see https://golang.org/pkg/time/#Time.Format +func (p Pages) GroupByPublishDate(format string, order ...string) (PagesGroup, error) { + sorter := func(p Pages) Pages { + return p.ByPublishDate() + } + formatter := func(p *Page) string { + return p.PublishDate.Format(format) + } + return p.groupByDateField(sorter, formatter, order...) +} + +// GroupByExpiryDate groups by the given page's ExpireDate value in +// the given format and with the given order. +// Valid values for order is asc, desc, rev and reverse. +// For valid format strings, see https://golang.org/pkg/time/#Time.Format +func (p Pages) GroupByExpiryDate(format string, order ...string) (PagesGroup, error) { + sorter := func(p Pages) Pages { + return p.ByExpiryDate() + } + formatter := func(p *Page) string { + return p.ExpiryDate.Format(format) + } + return p.groupByDateField(sorter, formatter, order...) +} + +// GroupByParamDate groups by a date set as a param on the page in +// the given format and with the given order. +// Valid values for order is asc, desc, rev and reverse. +// For valid format strings, see https://golang.org/pkg/time/#Time.Format +func (p Pages) GroupByParamDate(key string, format string, order ...string) (PagesGroup, error) { + sorter := func(p Pages) Pages { + var r Pages + for _, e := range p { + param := e.GetParam(key) + if param != nil { + if _, ok := param.(time.Time); ok { + r = append(r, e) + } + } + } + pdate := func(p1, p2 *Page) bool { + return p1.GetParam(key).(time.Time).Unix() < p2.GetParam(key).(time.Time).Unix() + } + pageBy(pdate).Sort(r) + return r + } + formatter := func(p *Page) string { + return p.GetParam(key).(time.Time).Format(format) + } + return p.groupByDateField(sorter, formatter, order...) +} diff --git a/hugolib/pageGroup_test.go b/hugolib/pageGroup_test.go new file mode 100644 index 000000000..8cc381b61 --- /dev/null +++ b/hugolib/pageGroup_test.go @@ -0,0 +1,457 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "errors" + "path/filepath" + "reflect" + "testing" + + "github.com/spf13/cast" +) + +type pageGroupTestObject struct { + path string + weight int + date string + param string +} + +var pageGroupTestSources = []pageGroupTestObject{ + {"/section1/testpage1.md", 3, "2012-04-06", "foo"}, + {"/section1/testpage2.md", 3, "2012-01-01", "bar"}, + {"/section1/testpage3.md", 2, "2012-04-06", "foo"}, + {"/section2/testpage4.md", 1, "2012-03-02", "bar"}, + {"/section2/testpage5.md", 1, "2012-04-06", "baz"}, +} + +func preparePageGroupTestPages(t *testing.T) Pages { + s := newTestSite(t) + var pages Pages + for _, src := range pageGroupTestSources { + p, err := s.NewPage(filepath.FromSlash(src.path)) + if err != nil { + t.Fatalf("failed to prepare test page %s", src.path) + } + p.Weight = src.weight + p.Date = cast.ToTime(src.date) + p.PublishDate = cast.ToTime(src.date) + p.ExpiryDate = cast.ToTime(src.date) + p.Params["custom_param"] = src.param + p.Params["custom_date"] = cast.ToTime(src.date) + pages = append(pages, p) + } + return pages +} + +func TestGroupByWithFieldNameArg(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: 1, Pages: Pages{pages[3], pages[4]}}, + {Key: 2, Pages: Pages{pages[2]}}, + {Key: 3, Pages: Pages{pages[0], pages[1]}}, + } + + groups, err := pages.GroupBy("Weight") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByWithMethodNameArg(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "section1", Pages: Pages{pages[0], pages[1], pages[2]}}, + {Key: "section2", Pages: Pages{pages[3], pages[4]}}, + } + + groups, err := pages.GroupBy("Type") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByWithSectionArg(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "section1", Pages: Pages{pages[0], pages[1], pages[2]}}, + {Key: "section2", Pages: Pages{pages[3], pages[4]}}, + } + + groups, err := pages.GroupBy("Section") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByInReverseOrder(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: 3, Pages: Pages{pages[0], pages[1]}}, + {Key: 2, Pages: Pages{pages[2]}}, + {Key: 1, Pages: Pages{pages[3], pages[4]}}, + } + + groups, err := pages.GroupBy("Weight", "desc") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByCalledWithEmptyPages(t *testing.T) { + t.Parallel() + var pages Pages + groups, err := pages.GroupBy("Weight") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if groups != nil { + t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups) + } +} + +func TestGroupByCalledWithUnavailableKey(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + _, err := pages.GroupBy("UnavailableKey") + if err == nil { + t.Errorf("GroupByParam should return an error but didn't") + } +} + +func (page *Page) DummyPageMethodWithArgForTest(s string) string { + return s +} + +func (page *Page) DummyPageMethodReturnThreeValueForTest() (string, string, string) { + return "foo", "bar", "baz" +} + +func (page *Page) DummyPageMethodReturnErrorOnlyForTest() error { + return errors.New("some error occurred") +} + +func (page *Page) dummyPageMethodReturnTwoValueForTest() (string, string) { + return "foo", "bar" +} + +func TestGroupByCalledWithInvalidMethod(t *testing.T) { + t.Parallel() + var err error + pages := preparePageGroupTestPages(t) + + _, err = pages.GroupBy("DummyPageMethodWithArgForTest") + if err == nil { + t.Errorf("GroupByParam should return an error but didn't") + } + + _, err = pages.GroupBy("DummyPageMethodReturnThreeValueForTest") + if err == nil { + t.Errorf("GroupByParam should return an error but didn't") + } + + _, err = pages.GroupBy("DummyPageMethodReturnErrorOnlyForTest") + if err == nil { + t.Errorf("GroupByParam should return an error but didn't") + } + + _, err = pages.GroupBy("DummyPageMethodReturnTwoValueForTest") + if err == nil { + t.Errorf("GroupByParam should return an error but didn't") + } +} + +func TestReverse(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + + groups1, err := pages.GroupBy("Weight", "desc") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + + groups2, err := pages.GroupBy("Weight") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + groups2 = groups2.Reverse() + + if !reflect.DeepEqual(groups2, groups1) { + t.Errorf("PagesGroup is sorted in unexpected order. It should be %#v, got %#v", groups2, groups1) + } +} + +func TestGroupByParam(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "bar", Pages: Pages{pages[1], pages[3]}}, + {Key: "baz", Pages: Pages{pages[4]}}, + {Key: "foo", Pages: Pages{pages[0], pages[2]}}, + } + + groups, err := pages.GroupByParam("custom_param") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByParamInReverseOrder(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "foo", Pages: Pages{pages[0], pages[2]}}, + {Key: "baz", Pages: Pages{pages[4]}}, + {Key: "bar", Pages: Pages{pages[1], pages[3]}}, + } + + groups, err := pages.GroupByParam("custom_param", "desc") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByParamCalledWithCapitalLetterString(t *testing.T) { + testStr := "TestString" + f := "/section1/test_capital.md" + s := newTestSite(t) + p, err := s.NewPage(filepath.FromSlash(f)) + if err != nil { + t.Fatalf("failed to prepare test page %s", f) + } + p.Params["custom_param"] = testStr + pages := Pages{p} + + groups, err := pages.GroupByParam("custom_param") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if groups[0].Key != testStr { + t.Errorf("PagesGroup key is converted to a lower character string. It should be %#v, got %#v", testStr, groups[0].Key) + } +} + +func TestGroupByParamCalledWithSomeUnavailableParams(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + delete(pages[1].Params, "custom_param") + delete(pages[3].Params, "custom_param") + delete(pages[4].Params, "custom_param") + + expect := PagesGroup{ + {Key: "foo", Pages: Pages{pages[0], pages[2]}}, + } + + groups, err := pages.GroupByParam("custom_param") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByParamCalledWithEmptyPages(t *testing.T) { + t.Parallel() + var pages Pages + groups, err := pages.GroupByParam("custom_param") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if groups != nil { + t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups) + } +} + +func TestGroupByParamCalledWithUnavailableParam(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + _, err := pages.GroupByParam("unavailable_param") + if err == nil { + t.Errorf("GroupByParam should return an error but didn't") + } +} + +func TestGroupByDate(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "2012-04", Pages: Pages{pages[4], pages[2], pages[0]}}, + {Key: "2012-03", Pages: Pages{pages[3]}}, + {Key: "2012-01", Pages: Pages{pages[1]}}, + } + + groups, err := pages.GroupByDate("2006-01") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByDateInReverseOrder(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "2012-01", Pages: Pages{pages[1]}}, + {Key: "2012-03", Pages: Pages{pages[3]}}, + {Key: "2012-04", Pages: Pages{pages[0], pages[2], pages[4]}}, + } + + groups, err := pages.GroupByDate("2006-01", "asc") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByPublishDate(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "2012-04", Pages: Pages{pages[4], pages[2], pages[0]}}, + {Key: "2012-03", Pages: Pages{pages[3]}}, + {Key: "2012-01", Pages: Pages{pages[1]}}, + } + + groups, err := pages.GroupByPublishDate("2006-01") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByPublishDateInReverseOrder(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "2012-01", Pages: Pages{pages[1]}}, + {Key: "2012-03", Pages: Pages{pages[3]}}, + {Key: "2012-04", Pages: Pages{pages[0], pages[2], pages[4]}}, + } + + groups, err := pages.GroupByDate("2006-01", "asc") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByPublishDateWithEmptyPages(t *testing.T) { + t.Parallel() + var pages Pages + groups, err := pages.GroupByPublishDate("2006-01") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if groups != nil { + t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups) + } +} + +func TestGroupByExpiryDate(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "2012-04", Pages: Pages{pages[4], pages[2], pages[0]}}, + {Key: "2012-03", Pages: Pages{pages[3]}}, + {Key: "2012-01", Pages: Pages{pages[1]}}, + } + + groups, err := pages.GroupByExpiryDate("2006-01") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByParamDate(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "2012-04", Pages: Pages{pages[4], pages[2], pages[0]}}, + {Key: "2012-03", Pages: Pages{pages[3]}}, + {Key: "2012-01", Pages: Pages{pages[1]}}, + } + + groups, err := pages.GroupByParamDate("custom_date", "2006-01") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByParamDateInReverseOrder(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "2012-01", Pages: Pages{pages[1]}}, + {Key: "2012-03", Pages: Pages{pages[3]}}, + {Key: "2012-04", Pages: Pages{pages[0], pages[2], pages[4]}}, + } + + groups, err := pages.GroupByParamDate("custom_date", "2006-01", "asc") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByParamDateWithEmptyPages(t *testing.T) { + t.Parallel() + var pages Pages + groups, err := pages.GroupByParamDate("custom_date", "2006-01") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if groups != nil { + t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups) + } +} diff --git a/hugolib/pageSort.go b/hugolib/pageSort.go new file mode 100644 index 000000000..6d2431cec --- /dev/null +++ b/hugolib/pageSort.go @@ -0,0 +1,303 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "github.com/spf13/cast" + "sort" +) + +var spc = newPageCache() + +/* + * Implementation of a custom sorter for Pages + */ + +// A pageSorter implements the sort interface for Pages +type pageSorter struct { + pages Pages + by pageBy +} + +// pageBy is a closure used in the Sort.Less method. +type pageBy func(p1, p2 *Page) bool + +// Sort stable sorts the pages given the receiver's sort order. +func (by pageBy) Sort(pages Pages) { + ps := &pageSorter{ + pages: pages, + by: by, // The Sort method's receiver is the function (closure) that defines the sort order. + } + sort.Stable(ps) +} + +// defaultPageSort is the default sort for pages in Hugo: +// Order by Weight, Date, LinkTitle and then full file path. +var defaultPageSort = func(p1, p2 *Page) bool { + if p1.Weight == p2.Weight { + if p1.Date.Unix() == p2.Date.Unix() { + if p1.LinkTitle() == p2.LinkTitle() { + return (p1.FullFilePath() < p2.FullFilePath()) + } + return (p1.LinkTitle() < p2.LinkTitle()) + } + return p1.Date.Unix() > p2.Date.Unix() + } + + if p2.Weight == 0 { + return true + } + + if p1.Weight == 0 { + return false + } + + return p1.Weight < p2.Weight +} + +var languagePageSort = func(p1, p2 *Page) bool { + if p1.Language().Weight == p2.Language().Weight { + if p1.Date.Unix() == p2.Date.Unix() { + if p1.LinkTitle() == p2.LinkTitle() { + return (p1.FullFilePath() < p2.FullFilePath()) + } + return (p1.LinkTitle() < p2.LinkTitle()) + } + return p1.Date.Unix() > p2.Date.Unix() + } + + if p2.Language().Weight == 0 { + return true + } + + if p1.Language().Weight == 0 { + return false + } + + return p1.Language().Weight < p2.Language().Weight +} + +func (ps *pageSorter) Len() int { return len(ps.pages) } +func (ps *pageSorter) Swap(i, j int) { ps.pages[i], ps.pages[j] = ps.pages[j], ps.pages[i] } + +// Less is part of sort.Interface. It is implemented by calling the "by" closure in the sorter. +func (ps *pageSorter) Less(i, j int) bool { return ps.by(ps.pages[i], ps.pages[j]) } + +// Sort sorts the pages by the default sort order defined: +// Order by Weight, Date, LinkTitle and then full file path. +func (p Pages) Sort() { + pageBy(defaultPageSort).Sort(p) +} + +// Limit limits the number of pages returned to n. +func (p Pages) Limit(n int) Pages { + if len(p) > n { + return p[0:n] + } + return p +} + +// ByWeight sorts the Pages by weight and returns a copy. +// +// Adjacent invocations on the same receiver will return a cached result. +// +// This may safely be executed in parallel. +func (p Pages) ByWeight() Pages { + key := "pageSort.ByWeight" + pages, _ := spc.get(key, p, pageBy(defaultPageSort).Sort) + return pages +} + +// ByTitle sorts the Pages by title and returns a copy. +// +// Adjacent invocations on the same receiver will return a cached result. +// +// This may safely be executed in parallel. +func (p Pages) ByTitle() Pages { + + key := "pageSort.ByTitle" + + title := func(p1, p2 *Page) bool { + return p1.Title < p2.Title + } + + pages, _ := spc.get(key, p, pageBy(title).Sort) + return pages +} + +// ByLinkTitle sorts the Pages by link title and returns a copy. +// +// Adjacent invocations on the same receiver will return a cached result. +// +// This may safely be executed in parallel. +func (p Pages) ByLinkTitle() Pages { + + key := "pageSort.ByLinkTitle" + + linkTitle := func(p1, p2 *Page) bool { + return p1.linkTitle < p2.linkTitle + } + + pages, _ := spc.get(key, p, pageBy(linkTitle).Sort) + + return pages +} + +// ByDate sorts the Pages by date and returns a copy. +// +// Adjacent invocations on the same receiver will return a cached result. +// +// This may safely be executed in parallel. +func (p Pages) ByDate() Pages { + + key := "pageSort.ByDate" + + date := func(p1, p2 *Page) bool { + return p1.Date.Unix() < p2.Date.Unix() + } + + pages, _ := spc.get(key, p, pageBy(date).Sort) + + return pages +} + +// ByPublishDate sorts the Pages by publish date and returns a copy. +// +// Adjacent invocations on the same receiver will return a cached result. +// +// This may safely be executed in parallel. +func (p Pages) ByPublishDate() Pages { + + key := "pageSort.ByPublishDate" + + pubDate := func(p1, p2 *Page) bool { + return p1.PublishDate.Unix() < p2.PublishDate.Unix() + } + + pages, _ := spc.get(key, p, pageBy(pubDate).Sort) + + return pages +} + +// ByExpiryDate sorts the Pages by publish date and returns a copy. +// +// Adjacent invocations on the same receiver will return a cached result. +// +// This may safely be executed in parallel. +func (p Pages) ByExpiryDate() Pages { + + key := "pageSort.ByExpiryDate" + + expDate := func(p1, p2 *Page) bool { + return p1.ExpiryDate.Unix() < p2.ExpiryDate.Unix() + } + + pages, _ := spc.get(key, p, pageBy(expDate).Sort) + + return pages +} + +// ByLastmod sorts the Pages by the last modification date and returns a copy. +// +// Adjacent invocations on the same receiver will return a cached result. +// +// This may safely be executed in parallel. +func (p Pages) ByLastmod() Pages { + + key := "pageSort.ByLastmod" + + date := func(p1, p2 *Page) bool { + return p1.Lastmod.Unix() < p2.Lastmod.Unix() + } + + pages, _ := spc.get(key, p, pageBy(date).Sort) + + return pages +} + +// ByLength sorts the Pages by length and returns a copy. +// +// Adjacent invocations on the same receiver will return a cached result. +// +// This may safely be executed in parallel. +func (p Pages) ByLength() Pages { + + key := "pageSort.ByLength" + + length := func(p1, p2 *Page) bool { + return len(p1.Content) < len(p2.Content) + } + + pages, _ := spc.get(key, p, pageBy(length).Sort) + + return pages +} + +// ByLanguage sorts the Pages by the language's Weight. +// +// Adjacent invocations on the same receiver will return a cached result. +// +// This may safely be executed in parallel. +func (p Pages) ByLanguage() Pages { + + key := "pageSort.ByLanguage" + + pages, _ := spc.get(key, p, pageBy(languagePageSort).Sort) + + return pages +} + +// Reverse reverses the order in Pages and returns a copy. +// +// Adjacent invocations on the same receiver will return a cached result. +// +// This may safely be executed in parallel. +func (p Pages) Reverse() Pages { + key := "pageSort.Reverse" + + reverseFunc := func(pages Pages) { + for i, j := 0, len(pages)-1; i < j; i, j = i+1, j-1 { + pages[i], pages[j] = pages[j], pages[i] + } + } + + pages, _ := spc.get(key, p, reverseFunc) + + return pages +} + +func (p Pages) ByParam(paramsKey interface{}) Pages { + paramsKeyStr := cast.ToString(paramsKey) + key := "pageSort.ByParam." + paramsKeyStr + + paramsKeyComparator := func(p1, p2 *Page) bool { + v1, _ := p1.Param(paramsKeyStr) + v2, _ := p2.Param(paramsKeyStr) + s1 := cast.ToString(v1) + s2 := cast.ToString(v2) + + // Sort nils last. + if s1 == "" { + return false + } else if s2 == "" { + return true + } + + return s1 < s2 + } + + pages, _ := spc.get(key, p, pageBy(paramsKeyComparator).Sort) + + return pages +} diff --git a/hugolib/pageSort_test.go b/hugolib/pageSort_test.go new file mode 100644 index 000000000..a17f53dc6 --- /dev/null +++ b/hugolib/pageSort_test.go @@ -0,0 +1,202 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "html/template" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestDefaultSort(t *testing.T) { + t.Parallel() + d1 := time.Now() + d2 := d1.Add(-1 * time.Hour) + d3 := d1.Add(-2 * time.Hour) + d4 := d1.Add(-3 * time.Hour) + + s := newTestSite(t) + + p := createSortTestPages(s, 4) + + // first by weight + setSortVals([4]time.Time{d1, d2, d3, d4}, [4]string{"b", "a", "c", "d"}, [4]int{4, 3, 2, 1}, p) + p.Sort() + + assert.Equal(t, 1, p[0].Weight) + + // Consider zero weight, issue #2673 + setSortVals([4]time.Time{d1, d2, d3, d4}, [4]string{"b", "a", "d", "c"}, [4]int{0, 0, 0, 1}, p) + p.Sort() + + assert.Equal(t, 1, p[0].Weight) + + // next by date + setSortVals([4]time.Time{d3, d4, d1, d2}, [4]string{"a", "b", "c", "d"}, [4]int{1, 1, 1, 1}, p) + p.Sort() + assert.Equal(t, d1, p[0].Date) + + // finally by link title + setSortVals([4]time.Time{d3, d3, d3, d3}, [4]string{"b", "c", "a", "d"}, [4]int{1, 1, 1, 1}, p) + p.Sort() + assert.Equal(t, "al", p[0].LinkTitle()) + assert.Equal(t, "bl", p[1].LinkTitle()) + assert.Equal(t, "cl", p[2].LinkTitle()) +} + +func TestSortByN(t *testing.T) { + t.Parallel() + s := newTestSite(t) + d1 := time.Now() + d2 := d1.Add(-2 * time.Hour) + d3 := d1.Add(-10 * time.Hour) + d4 := d1.Add(-20 * time.Hour) + + p := createSortTestPages(s, 4) + + for i, this := range []struct { + sortFunc func(p Pages) Pages + assertFunc func(p Pages) bool + }{ + {(Pages).ByWeight, func(p Pages) bool { return p[0].Weight == 1 }}, + {(Pages).ByTitle, func(p Pages) bool { return p[0].Title == "ab" }}, + {(Pages).ByLinkTitle, func(p Pages) bool { return p[0].LinkTitle() == "abl" }}, + {(Pages).ByDate, func(p Pages) bool { return p[0].Date == d4 }}, + {(Pages).ByPublishDate, func(p Pages) bool { return p[0].PublishDate == d4 }}, + {(Pages).ByExpiryDate, func(p Pages) bool { return p[0].ExpiryDate == d4 }}, + {(Pages).ByLastmod, func(p Pages) bool { return p[1].Lastmod == d3 }}, + {(Pages).ByLength, func(p Pages) bool { return p[0].Content == "b_content" }}, + } { + setSortVals([4]time.Time{d1, d2, d3, d4}, [4]string{"b", "ab", "cde", "fg"}, [4]int{0, 3, 2, 1}, p) + + sorted := this.sortFunc(p) + if !this.assertFunc(sorted) { + t.Errorf("[%d] sort error", i) + } + } + +} + +func TestLimit(t *testing.T) { + t.Parallel() + s := newTestSite(t) + p := createSortTestPages(s, 10) + firstFive := p.Limit(5) + assert.Equal(t, 5, len(firstFive)) + for i := 0; i < 5; i++ { + assert.Equal(t, p[i], firstFive[i]) + } + assert.Equal(t, p, p.Limit(10)) + assert.Equal(t, p, p.Limit(11)) +} + +func TestPageSortReverse(t *testing.T) { + t.Parallel() + s := newTestSite(t) + p1 := createSortTestPages(s, 10) + assert.Equal(t, 0, p1[0].fuzzyWordCount) + assert.Equal(t, 9, p1[9].fuzzyWordCount) + p2 := p1.Reverse() + assert.Equal(t, 9, p2[0].fuzzyWordCount) + assert.Equal(t, 0, p2[9].fuzzyWordCount) + // cached + assert.True(t, probablyEqualPages(p2, p1.Reverse())) +} + +func TestPageSortByParam(t *testing.T) { + t.Parallel() + var k interface{} = "arbitrarily.nested" + s := newTestSite(t) + + unsorted := createSortTestPages(s, 10) + delete(unsorted[9].Params, "arbitrarily") + + firstSetValue, _ := unsorted[0].Param(k) + secondSetValue, _ := unsorted[1].Param(k) + lastSetValue, _ := unsorted[8].Param(k) + unsetValue, _ := unsorted[9].Param(k) + + assert.Equal(t, "xyz100", firstSetValue) + assert.Equal(t, "xyz99", secondSetValue) + assert.Equal(t, "xyz92", lastSetValue) + assert.Equal(t, nil, unsetValue) + + sorted := unsorted.ByParam("arbitrarily.nested") + firstSetSortedValue, _ := sorted[0].Param(k) + secondSetSortedValue, _ := sorted[1].Param(k) + lastSetSortedValue, _ := sorted[8].Param(k) + unsetSortedValue, _ := sorted[9].Param(k) + + assert.Equal(t, firstSetValue, firstSetSortedValue) + assert.Equal(t, secondSetValue, lastSetSortedValue) + assert.Equal(t, lastSetValue, secondSetSortedValue) + assert.Equal(t, unsetValue, unsetSortedValue) +} + +func BenchmarkSortByWeightAndReverse(b *testing.B) { + s := newTestSite(b) + p := createSortTestPages(s, 300) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + p = p.ByWeight().Reverse() + } +} + +func setSortVals(dates [4]time.Time, titles [4]string, weights [4]int, pages Pages) { + for i := range dates { + pages[i].Date = dates[i] + pages[i].Lastmod = dates[i] + pages[i].Weight = weights[i] + pages[i].Title = titles[i] + // make sure we compare apples and ... apples ... + pages[len(dates)-1-i].linkTitle = pages[i].Title + "l" + pages[len(dates)-1-i].PublishDate = dates[i] + pages[len(dates)-1-i].ExpiryDate = dates[i] + pages[len(dates)-1-i].Content = template.HTML(titles[i] + "_content") + } + lastLastMod := pages[2].Lastmod + pages[2].Lastmod = pages[1].Lastmod + pages[1].Lastmod = lastLastMod +} + +func createSortTestPages(s *Site, num int) Pages { + pages := make(Pages, num) + + for i := 0; i < num; i++ { + p := s.newPage(filepath.FromSlash(fmt.Sprintf("/x/y/p%d.md", i))) + p.Params = map[string]interface{}{ + "arbitrarily": map[string]interface{}{ + "nested": ("xyz" + fmt.Sprintf("%v", 100-i)), + }, + } + + w := 5 + + if i%2 == 0 { + w = 10 + } + p.fuzzyWordCount = i + p.Weight = w + p.Description = "initial" + + pages[i] = p + } + + return pages +} diff --git a/hugolib/page_collections.go b/hugolib/page_collections.go new file mode 100644 index 000000000..e72f9a731 --- /dev/null +++ b/hugolib/page_collections.go @@ -0,0 +1,181 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "path" + "path/filepath" + + "github.com/gohugoio/hugo/cache" +) + +// PageCollections contains the page collections for a site. +type PageCollections struct { + // Includes only pages of all types, and only pages in the current language. + Pages Pages + + // Includes all pages in all languages, including the current one. + // Includes pages of all types. + AllPages Pages + + // A convenience cache for the traditional index types, taxonomies, home page etc. + // This is for the current language only. + indexPages Pages + + // A convenience cache for the regular pages. + // This is for the current language only. + RegularPages Pages + + // A convenience cache for the all the regular pages. + AllRegularPages Pages + + // Includes absolute all pages (of all types), including drafts etc. + rawAllPages Pages + + pageCache *cache.PartitionedLazyCache +} + +func (c *PageCollections) refreshPageCaches() { + c.indexPages = c.findPagesByKindNotIn(KindPage, c.Pages) + c.RegularPages = c.findPagesByKindIn(KindPage, c.Pages) + c.AllRegularPages = c.findPagesByKindIn(KindPage, c.AllPages) + + cacheLoader := func(kind string) func() (map[string]interface{}, error) { + return func() (map[string]interface{}, error) { + cache := make(map[string]interface{}) + switch kind { + case KindPage: + // Note that we deliberately use the pages from all sites + // in this cache, as we intend to use this in the ref and relref + // shortcodes. If the user says "sect/doc1.en.md", he/she knows + // what he/she is looking for. + for _, p := range c.AllRegularPages { + cache[filepath.ToSlash(p.Source.Path())] = p + // Ref/Relref supports this potentially ambiguous lookup. + cache[p.Source.LogicalName()] = p + } + default: + for _, p := range c.indexPages { + key := path.Join(p.sections...) + cache[key] = p + } + } + + return cache, nil + } + } + + var partitions []cache.Partition + + for _, kind := range allKindsInPages { + partitions = append(partitions, cache.Partition{Key: kind, Load: cacheLoader(kind)}) + } + + c.pageCache = cache.NewPartitionedLazyCache(partitions...) +} + +func newPageCollections() *PageCollections { + return &PageCollections{} +} + +func newPageCollectionsFromPages(pages Pages) *PageCollections { + return &PageCollections{rawAllPages: pages} +} + +func (c *PageCollections) getPage(typ string, sections ...string) *Page { + var key string + if len(sections) == 1 { + key = filepath.ToSlash(sections[0]) + } else { + key = path.Join(sections...) + } + + p, _ := c.pageCache.Get(typ, key) + if p == nil { + return nil + } + return p.(*Page) + +} + +func (*PageCollections) findPagesByKindIn(kind string, inPages Pages) Pages { + var pages Pages + for _, p := range inPages { + if p.Kind == kind { + pages = append(pages, p) + } + } + return pages +} + +func (*PageCollections) findPagesByKindNotIn(kind string, inPages Pages) Pages { + var pages Pages + for _, p := range inPages { + if p.Kind != kind { + pages = append(pages, p) + } + } + return pages +} + +func (c *PageCollections) findPagesByKind(kind string) Pages { + return c.findPagesByKindIn(kind, c.Pages) +} + +func (c *PageCollections) addPage(page *Page) { + c.rawAllPages = append(c.rawAllPages, page) +} + +// When we get a REMOVE event we're not always getting all the individual files, +// so we need to remove all below a given path. +func (c *PageCollections) removePageByPathPrefix(path string) { + for { + i := c.rawAllPages.findFirstPagePosByFilePathPrefix(path) + if i == -1 { + break + } + c.rawAllPages = append(c.rawAllPages[:i], c.rawAllPages[i+1:]...) + } +} + +func (c *PageCollections) removePageByPath(path string) { + if i := c.rawAllPages.findPagePosByFilePath(path); i >= 0 { + c.rawAllPages = append(c.rawAllPages[:i], c.rawAllPages[i+1:]...) + } +} + +func (c *PageCollections) removePage(page *Page) { + if i := c.rawAllPages.findPagePos(page); i >= 0 { + c.rawAllPages = append(c.rawAllPages[:i], c.rawAllPages[i+1:]...) + } +} + +func (c *PageCollections) findPagesByShortcode(shortcode string) Pages { + var pages Pages + + for _, p := range c.rawAllPages { + if p.shortcodeState != nil { + if _, ok := p.shortcodeState.nameSet[shortcode]; ok { + pages = append(pages, p) + } + } + } + return pages +} + +func (c *PageCollections) replacePage(page *Page) { + // will find existing page that matches filepath and remove it + c.removePage(page) + c.addPage(page) +} diff --git a/hugolib/page_collections_test.go b/hugolib/page_collections_test.go new file mode 100644 index 000000000..aee99040c --- /dev/null +++ b/hugolib/page_collections_test.go @@ -0,0 +1,140 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "math/rand" + "path" + "path/filepath" + "testing" + "time" + + "github.com/gohugoio/hugo/deps" + "github.com/stretchr/testify/require" +) + +const pageCollectionsPageTemplate = `--- +title: "%s" +categories: +- Hugo +--- +# Doc +` + +func BenchmarkGetPage(b *testing.B) { + var ( + cfg, fs = newTestCfg() + r = rand.New(rand.NewSource(time.Now().UnixNano())) + ) + + for i := 0; i < 10; i++ { + for j := 0; j < 100; j++ { + writeSource(b, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", j)), "CONTENT") + } + } + + s := buildSingleSite(b, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + + pagePaths := make([]string, b.N) + + for i := 0; i < b.N; i++ { + pagePaths[i] = fmt.Sprintf("sect%d", r.Intn(10)) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + home := s.getPage(KindHome) + if home == nil { + b.Fatal("Home is nil") + } + + p := s.getPage(KindSection, pagePaths[i]) + if p == nil { + b.Fatal("Section is nil") + } + + } +} + +func BenchmarkGetPageRegular(b *testing.B) { + var ( + cfg, fs = newTestCfg() + r = rand.New(rand.NewSource(time.Now().UnixNano())) + ) + + for i := 0; i < 10; i++ { + for j := 0; j < 100; j++ { + content := fmt.Sprintf(pageCollectionsPageTemplate, fmt.Sprintf("Title%d_%d", i, j)) + writeSource(b, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", j)), content) + } + } + + s := buildSingleSite(b, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + + pagePaths := make([]string, b.N) + + for i := 0; i < b.N; i++ { + pagePaths[i] = path.Join(fmt.Sprintf("sect%d", r.Intn(10)), fmt.Sprintf("page%d.md", r.Intn(100))) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + page := s.getPage(KindPage, pagePaths[i]) + require.NotNil(b, page) + } +} + +func TestGetPage(t *testing.T) { + + var ( + assert = require.New(t) + cfg, fs = newTestCfg() + ) + + for i := 0; i < 10; i++ { + for j := 0; j < 10; j++ { + content := fmt.Sprintf(pageCollectionsPageTemplate, fmt.Sprintf("Title%d_%d", i, j)) + writeSource(t, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", j)), content) + } + } + + content := fmt.Sprintf(pageCollectionsPageTemplate, "UniqueBase") + writeSource(t, fs, filepath.Join("content", "sect3", "unique.md"), content) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + + tests := []struct { + kind string + path []string + expectedTitle string + }{ + {KindHome, []string{}, ""}, + {KindSection, []string{"sect3"}, "Sect3s"}, + {KindPage, []string{"sect3", "page1.md"}, "Title3_1"}, + {KindPage, []string{"sect4/page2.md"}, "Title4_2"}, + {KindPage, []string{filepath.FromSlash("sect5/page3.md")}, "Title5_3"}, + // Ref/Relref supports this potentially ambiguous lookup. + {KindPage, []string{"unique.md"}, "UniqueBase"}, + } + + for i, test := range tests { + errorMsg := fmt.Sprintf("Test %d", i) + page := s.getPage(test.kind, test.path...) + assert.NotNil(page, errorMsg) + assert.Equal(test.kind, page.Kind) + assert.Equal(test.expectedTitle, page.Title) + } + +} diff --git a/hugolib/page_output.go b/hugolib/page_output.go new file mode 100644 index 000000000..6ea466b4f --- /dev/null +++ b/hugolib/page_output.go @@ -0,0 +1,273 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "html/template" + "strings" + "sync" + + "github.com/gohugoio/hugo/media" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/output" +) + +// PageOutput represents one of potentially many output formats of a given +// Page. +type PageOutput struct { + *Page + + // Pagination + paginator *Pager + paginatorInit sync.Once + + // Keep this to create URL/path variations, i.e. paginators. + targetPathDescriptor targetPathDescriptor + + outputFormat output.Format +} + +func (p *PageOutput) targetPath(addends ...string) (string, error) { + tp, err := p.createTargetPath(p.outputFormat, addends...) + if err != nil { + return "", err + } + return tp, nil +} + +func newPageOutput(p *Page, createCopy bool, f output.Format) (*PageOutput, error) { + // TODO(bep) This is only needed for tests and we should get rid of it. + if p.targetPathDescriptorPrototype == nil { + if err := p.initTargetPathDescriptor(); err != nil { + return nil, err + } + if err := p.initURLs(); err != nil { + return nil, err + } + } + + if createCopy { + p = p.copy() + } + + td, err := p.createTargetPathDescriptor(f) + + if err != nil { + return nil, err + } + + return &PageOutput{ + Page: p, + outputFormat: f, + targetPathDescriptor: td, + }, nil +} + +// copy creates a copy of this PageOutput with the lazy sync.Once vars reset +// so they will be evaluated again, for word count calculations etc. +func (p *PageOutput) copyWithFormat(f output.Format) (*PageOutput, error) { + c, err := newPageOutput(p.Page, true, f) + if err != nil { + return nil, err + } + c.paginator = p.paginator + return c, nil +} + +func (p *PageOutput) copy() (*PageOutput, error) { + return p.copyWithFormat(p.outputFormat) +} + +func (p *PageOutput) layouts(layouts ...string) ([]string, error) { + if len(layouts) == 0 && p.selfLayout != "" { + return []string{p.selfLayout}, nil + } + + layoutOverride := "" + if len(layouts) > 0 { + layoutOverride = layouts[0] + } + + return p.s.layoutHandler.For( + p.layoutDescriptor, + layoutOverride, + p.outputFormat) +} + +func (p *PageOutput) Render(layout ...string) template.HTML { + if !p.checkRender() { + return "" + } + + l, err := p.layouts(layout...) + if err != nil { + helpers.DistinctErrorLog.Printf("in .Render: Failed to resolve layout %q for page %q", layout, p.pathOrTitle()) + return "" + } + + for _, layout := range l { + templ := p.s.Tmpl.Lookup(layout) + if templ == nil { + // This is legacy from when we had only one output format and + // HTML templates only. Some have references to layouts without suffix. + // We default to good old HTML. + templ = p.s.Tmpl.Lookup(layout + ".html") + } + if templ != nil { + res, err := templ.ExecuteToString(p) + if err != nil { + helpers.DistinctErrorLog.Printf("in .Render: Failed to execute template %q for page %q", layout, p.pathOrTitle()) + return template.HTML("") + } + return template.HTML(res) + } + } + + return "" + +} + +func (p *Page) Render(layout ...string) template.HTML { + if !p.checkRender() { + return "" + } + + p.pageOutputInit.Do(func() { + if p.mainPageOutput != nil { + return + } + // If Render is called in a range loop, the page output isn't available. + // So, create one. + outFormat := p.outputFormats[0] + pageOutput, err := newPageOutput(p, true, outFormat) + + if err != nil { + p.s.Log.ERROR.Printf("Failed to create output page for type %q for page %q: %s", outFormat.Name, p.pathOrTitle(), err) + return + } + + p.mainPageOutput = pageOutput + + }) + + return p.mainPageOutput.Render(layout...) +} + +// We may fix this in the future, but the layout handling in Render isn't built +// for list pages. +func (p *Page) checkRender() bool { + if p.Kind != KindPage { + helpers.DistinctWarnLog.Printf(".Render only available for regular pages, not for of kind %q. You probably meant .Site.RegularPages and not.Site.Pages.", p.Kind) + return false + } + return true +} + +// OutputFormats holds a list of the relevant output formats for a given resource. +type OutputFormats []*OutputFormat + +// And OutputFormat links to a representation of a resource. +type OutputFormat struct { + // Rel constains a value that can be used to construct a rel link. + // This is value is fetched from the output format definition. + // Note that for pages with only one output format, + // this method will always return "canonical". + // As an example, the AMP output format will, by default, return "amphtml". + // + // See: + // https://www.ampproject.org/docs/guides/deploy/discovery + // + // Most other output formats will have "alternate" as value for this. + Rel string + + // It may be tempting to export this, but let us hold on to that horse for a while. + f output.Format + + p *Page +} + +// Name returns this OutputFormat's name, i.e. HTML, AMP, JSON etc. +func (o OutputFormat) Name() string { + return o.f.Name +} + +// MediaType returns this OutputFormat's MediaType (MIME type). +func (o OutputFormat) MediaType() media.Type { + return o.f.MediaType +} + +// OutputFormats gives the output formats for this Page. +func (p *Page) OutputFormats() OutputFormats { + var o OutputFormats + for _, f := range p.outputFormats { + o = append(o, newOutputFormat(p, f)) + } + return o +} + +func newOutputFormat(p *Page, f output.Format) *OutputFormat { + rel := f.Rel + isCanonical := len(p.outputFormats) == 1 + if isCanonical { + rel = "canonical" + } + return &OutputFormat{Rel: rel, f: f, p: p} +} + +// OutputFormats gives the alternative output formats for this PageOutput. +// Note that we use the term "alternative" and not "alternate" here, as it +// does not necessarily replace the other format, it is an alternative representation. +func (p *PageOutput) AlternativeOutputFormats() (OutputFormats, error) { + var o OutputFormats + for _, of := range p.OutputFormats() { + if of.f.NotAlternative || of.f == p.outputFormat { + continue + } + o = append(o, of) + } + return o, nil +} + +// AlternativeOutputFormats is only available on the top level rendering +// entry point, and not inside range loops on the Page collections. +// This method is just here to inform users of that restriction. +func (p *Page) AlternativeOutputFormats() (OutputFormats, error) { + return nil, fmt.Errorf("AlternativeOutputFormats only available from the top level template context for page %q", p.Path()) +} + +// Get gets a OutputFormat given its name, i.e. json, html etc. +// It returns nil if not found. +func (o OutputFormats) Get(name string) *OutputFormat { + for _, f := range o { + if strings.EqualFold(f.f.Name, name) { + return f + } + } + return nil +} + +// Permalink returns the absolute permalink to this output format. +func (o *OutputFormat) Permalink() string { + rel := o.p.createRelativePermalinkForOutputFormat(o.f) + perm, _ := o.p.s.permalinkForOutputFormat(rel, o.f) + return perm +} + +// Permalink returns the relative permalink to this output format. +func (o *OutputFormat) RelPermalink() string { + rel := o.p.createRelativePermalinkForOutputFormat(o.f) + return o.p.s.PathSpec.PrependBasePath(rel) +} diff --git a/hugolib/page_paths.go b/hugolib/page_paths.go new file mode 100644 index 000000000..73fd62278 --- /dev/null +++ b/hugolib/page_paths.go @@ -0,0 +1,256 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "path/filepath" + + "net/url" + "strings" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/output" +) + +// targetPathDescriptor describes how a file path for a given resource +// should look like on the file system. The same descriptor is then later used to +// create both the permalinks and the relative links, paginator URLs etc. +// +// The big motivating behind this is to have only one source of truth for URLs, +// and by that also get rid of most of the fragile string parsing/encoding etc. +// +// Page.createTargetPathDescriptor is the Page adapter. +// +type targetPathDescriptor struct { + PathSpec *helpers.PathSpec + + Type output.Format + Kind string + + Sections []string + + // For regular content pages this is either + // 1) the Slug, if set, + // 2) the file base name (TranslationBaseName). + BaseName string + + // Source directory. + Dir string + + // Language prefix, set if multilingual and if page should be placed in its + // language subdir. + LangPrefix string + + // Page.URLPath.URL. Will override any Slug etc. for regular pages. + URL string + + // Used to create paginator links. + Addends string + + // The expanded permalink if defined for the section, ready to use. + ExpandedPermalink string + + // Some types cannot have uglyURLs, even if globally enabled, RSS being one example. + UglyURLs bool +} + +// createTargetPathDescriptor adapts a Page and the given output.Format into +// a targetPathDescriptor. This descriptor can then be used to create paths +// and URLs for this Page. +func (p *Page) createTargetPathDescriptor(t output.Format) (targetPathDescriptor, error) { + if p.targetPathDescriptorPrototype == nil { + panic(fmt.Sprintf("Must run initTargetPathDescriptor() for page %q, kind %q", p.Title, p.Kind)) + } + d := *p.targetPathDescriptorPrototype + d.Type = t + return d, nil +} + +func (p *Page) initTargetPathDescriptor() error { + + d := &targetPathDescriptor{ + PathSpec: p.s.PathSpec, + Kind: p.Kind, + Sections: p.sections, + UglyURLs: p.s.Info.uglyURLs, + Dir: filepath.ToSlash(p.Source.Dir()), + URL: p.URLPath.URL, + } + + if p.Slug != "" { + d.BaseName = p.Slug + } else { + d.BaseName = p.TranslationBaseName() + } + + if p.shouldAddLanguagePrefix() { + d.LangPrefix = p.Lang() + } + + if override, ok := p.Site.Permalinks[p.Section()]; ok { + opath, err := override.Expand(p) + if err != nil { + return err + } + + opath, _ = url.QueryUnescape(opath) + opath = filepath.FromSlash(opath) + d.ExpandedPermalink = opath + } + + p.targetPathDescriptorPrototype = d + return nil + +} + +// createTargetPath creates the target filename for this Page for the given +// output.Format. Some additional URL parts can also be provided, the typical +// use case being pagination. +func (p *Page) createTargetPath(t output.Format, addends ...string) (string, error) { + d, err := p.createTargetPathDescriptor(t) + if err != nil { + return "", nil + } + + if len(addends) > 0 { + d.Addends = filepath.Join(addends...) + } + + return createTargetPath(d), nil +} + +func createTargetPath(d targetPathDescriptor) string { + + pagePath := helpers.FilePathSeparator + + // The top level index files, i.e. the home page etc., needs + // the index base even when uglyURLs is enabled. + needsBase := true + + isUgly := d.UglyURLs && !d.Type.NoUgly + + // If the page output format's base name is the same as the page base name, + // we treat it as an ugly path, i.e. + // my-blog-post-1/index.md => my-blog-post-1/index.html + // (given the default values for that content file, i.e. no slug set etc.). + // This introduces the behaviour from < Hugo 0.20, see issue #3396. + if d.BaseName != "" && d.BaseName == d.Type.BaseName { + isUgly = true + } + + if d.Kind != KindPage && len(d.Sections) > 0 { + pagePath = filepath.Join(d.Sections...) + needsBase = false + } + + if d.Type.Path != "" { + pagePath = filepath.Join(pagePath, d.Type.Path) + } + + if d.Kind == KindPage { + // Always use URL if it's specified + if d.URL != "" { + pagePath = filepath.Join(pagePath, d.URL) + if strings.HasSuffix(d.URL, "/") || !strings.Contains(d.URL, ".") { + pagePath = filepath.Join(pagePath, d.Type.BaseName+d.Type.MediaType.FullSuffix()) + } + } else { + if d.ExpandedPermalink != "" { + pagePath = filepath.Join(pagePath, d.ExpandedPermalink) + + } else { + if d.Dir != "" { + pagePath = filepath.Join(pagePath, d.Dir) + } + if d.BaseName != "" { + pagePath = filepath.Join(pagePath, d.BaseName) + } + } + + if d.Addends != "" { + pagePath = filepath.Join(pagePath, d.Addends) + } + + if isUgly { + pagePath += d.Type.MediaType.Delimiter + d.Type.MediaType.Suffix + } else { + pagePath = filepath.Join(pagePath, d.Type.BaseName+d.Type.MediaType.FullSuffix()) + } + + if d.LangPrefix != "" { + pagePath = filepath.Join(d.LangPrefix, pagePath) + } + } + } else { + if d.Addends != "" { + pagePath = filepath.Join(pagePath, d.Addends) + } + + needsBase = needsBase && d.Addends == "" + + // No permalink expansion etc. for node type pages (for now) + base := "" + + if needsBase || !isUgly { + base = helpers.FilePathSeparator + d.Type.BaseName + } + + pagePath += base + d.Type.MediaType.FullSuffix() + + if d.LangPrefix != "" { + pagePath = filepath.Join(d.LangPrefix, pagePath) + } + } + + pagePath = filepath.Join(helpers.FilePathSeparator, pagePath) + + // Note: MakePathSanitized will lower case the path if + // disablePathToLower isn't set. + return d.PathSpec.MakePathSanitized(pagePath) +} + +func (p *Page) createRelativePermalink() string { + + if len(p.outputFormats) == 0 { + panic(fmt.Sprintf("Page %q missing output format(s)", p.Title)) + } + + // Choose the main output format. In most cases, this will be HTML. + f := p.outputFormats[0] + + return p.createRelativePermalinkForOutputFormat(f) + +} + +func (p *Page) createRelativePermalinkForOutputFormat(f output.Format) string { + tp, err := p.createTargetPath(f) + + if err != nil { + p.s.Log.ERROR.Printf("Failed to create permalink for page %q: %s", p.FullFilePath(), err) + return "" + } + // For /index.json etc. we must use the full path. + if strings.HasSuffix(f.BaseFilename(), "html") { + tp = strings.TrimSuffix(tp, f.BaseFilename()) + } + + return p.s.PathSpec.URLizeFilename(tp) +} + +func (p *Page) TargetPath() (outfile string) { + // Delete in Hugo 0.22 + helpers.Deprecated("Page", "TargetPath", "This method does not make sanse any more.", true) + return "" +} diff --git a/hugolib/page_paths_test.go b/hugolib/page_paths_test.go new file mode 100644 index 000000000..80dc390cc --- /dev/null +++ b/hugolib/page_paths_test.go @@ -0,0 +1,190 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/gohugoio/hugo/media" + + "fmt" + + "github.com/gohugoio/hugo/output" +) + +func TestPageTargetPath(t *testing.T) { + + pathSpec := newTestDefaultPathSpec() + + noExtNoDelimMediaType := media.TextType + noExtNoDelimMediaType.Suffix = "" + noExtNoDelimMediaType.Delimiter = "" + + // Netlify style _redirects + noExtDelimFormat := output.Format{ + Name: "NER", + MediaType: noExtNoDelimMediaType, + BaseName: "_redirects", + } + + for _, langPrefix := range []string{"", "no"} { + for _, uglyURLs := range []bool{false, true} { + t.Run(fmt.Sprintf("langPrefix=%q,uglyURLs=%t", langPrefix, uglyURLs), + func(t *testing.T) { + + tests := []struct { + name string + d targetPathDescriptor + expected string + }{ + {"JSON home", targetPathDescriptor{Kind: KindHome, Type: output.JSONFormat}, "/index.json"}, + {"AMP home", targetPathDescriptor{Kind: KindHome, Type: output.AMPFormat}, "/amp/index.html"}, + {"HTML home", targetPathDescriptor{Kind: KindHome, BaseName: "_index", Type: output.HTMLFormat}, "/index.html"}, + {"Netlify redirects", targetPathDescriptor{Kind: KindHome, BaseName: "_index", Type: noExtDelimFormat}, "/_redirects"}, + {"HTML section list", targetPathDescriptor{ + Kind: KindSection, + Sections: []string{"sect1"}, + BaseName: "_index", + Type: output.HTMLFormat}, "/sect1/index.html"}, + {"HTML taxonomy list", targetPathDescriptor{ + Kind: KindTaxonomy, + Sections: []string{"tags", "hugo"}, + BaseName: "_index", + Type: output.HTMLFormat}, "/tags/hugo/index.html"}, + {"HTML taxonomy term", targetPathDescriptor{ + Kind: KindTaxonomy, + Sections: []string{"tags"}, + BaseName: "_index", + Type: output.HTMLFormat}, "/tags/index.html"}, + { + "HTML page", targetPathDescriptor{ + Kind: KindPage, + Dir: "/a/b", + BaseName: "mypage", + Sections: []string{"a"}, + Type: output.HTMLFormat}, "/a/b/mypage/index.html"}, + + { + // Issue #3396 + "HTML page with index as base", targetPathDescriptor{ + Kind: KindPage, + Dir: "/a/b", + BaseName: "index", + Sections: []string{"a"}, + Type: output.HTMLFormat}, "/a/b/index.html"}, + + { + "HTML page with special chars", targetPathDescriptor{ + Kind: KindPage, + Dir: "/a/b", + BaseName: "My Page!", + Type: output.HTMLFormat}, "/a/b/My-Page/index.html"}, + {"RSS home", targetPathDescriptor{Kind: kindRSS, Type: output.RSSFormat}, "/index.xml"}, + {"RSS section list", targetPathDescriptor{ + Kind: kindRSS, + Sections: []string{"sect1"}, + Type: output.RSSFormat}, "/sect1/index.xml"}, + { + "AMP page", targetPathDescriptor{ + Kind: KindPage, + Dir: "/a/b/c", + BaseName: "myamp", + Type: output.AMPFormat}, "/amp/a/b/c/myamp/index.html"}, + { + "AMP page with URL with suffix", targetPathDescriptor{ + Kind: KindPage, + Dir: "/sect/", + BaseName: "mypage", + URL: "/some/other/url.xhtml", + Type: output.HTMLFormat}, "/some/other/url.xhtml"}, + { + "JSON page with URL without suffix", targetPathDescriptor{ + Kind: KindPage, + Dir: "/sect/", + BaseName: "mypage", + URL: "/some/other/path/", + Type: output.JSONFormat}, "/some/other/path/index.json"}, + { + "JSON page with URL without suffix and no trailing slash", targetPathDescriptor{ + Kind: KindPage, + Dir: "/sect/", + BaseName: "mypage", + URL: "/some/other/path", + Type: output.JSONFormat}, "/some/other/path/index.json"}, + { + "HTML page with expanded permalink", targetPathDescriptor{ + Kind: KindPage, + Dir: "/a/b", + BaseName: "mypage", + ExpandedPermalink: "/2017/10/my-title", + Type: output.HTMLFormat}, "/2017/10/my-title/index.html"}, + { + "Paginated HTML home", targetPathDescriptor{ + Kind: KindHome, + BaseName: "_index", + Type: output.HTMLFormat, + Addends: "page/3"}, "/page/3/index.html"}, + { + "Paginated Taxonomy list", targetPathDescriptor{ + Kind: KindTaxonomy, + BaseName: "_index", + Sections: []string{"tags", "hugo"}, + Type: output.HTMLFormat, + Addends: "page/3"}, "/tags/hugo/page/3/index.html"}, + { + "Regular page with addend", targetPathDescriptor{ + Kind: KindPage, + Dir: "/a/b", + BaseName: "mypage", + Addends: "c/d/e", + Type: output.HTMLFormat}, "/a/b/mypage/c/d/e/index.html"}, + } + + for i, test := range tests { + test.d.PathSpec = pathSpec + test.d.UglyURLs = uglyURLs + test.d.LangPrefix = langPrefix + test.d.Dir = filepath.FromSlash(test.d.Dir) + isUgly := uglyURLs && !test.d.Type.NoUgly + + expected := test.expected + + // TODO(bep) simplify + if test.d.Kind == KindPage && test.d.BaseName == test.d.Type.BaseName { + + } else if test.d.Kind == KindHome && test.d.Type.Path != "" { + } else if (!strings.HasPrefix(expected, "/index") || test.d.Addends != "") && test.d.URL == "" && isUgly { + expected = strings.Replace(expected, + "/"+test.d.Type.BaseName+"."+test.d.Type.MediaType.Suffix, + "."+test.d.Type.MediaType.Suffix, -1) + } + + if test.d.LangPrefix != "" && !(test.d.Kind == KindPage && test.d.URL != "") { + expected = "/" + test.d.LangPrefix + expected + } + + expected = filepath.FromSlash(expected) + + pagePath := createTargetPath(test.d) + + if pagePath != expected { + t.Fatalf("[%d] [%s] targetPath expected %q, got: %q", i, test.name, expected, pagePath) + } + } + }) + } + } +} diff --git a/hugolib/page_permalink_test.go b/hugolib/page_permalink_test.go new file mode 100644 index 000000000..6f899efae --- /dev/null +++ b/hugolib/page_permalink_test.go @@ -0,0 +1,100 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "html/template" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/gohugoio/hugo/deps" +) + +func TestPermalink(t *testing.T) { + t.Parallel() + + tests := []struct { + file string + base template.URL + slug string + url string + uglyURLs bool + canonifyURLs bool + expectedAbs string + expectedRel string + }{ + {"x/y/z/boofar.md", "", "", "", false, false, "/x/y/z/boofar/", "/x/y/z/boofar/"}, + {"x/y/z/boofar.md", "", "", "", false, false, "/x/y/z/boofar/", "/x/y/z/boofar/"}, + // Issue #1174 + {"x/y/z/boofar.md", "http://gopher.com/", "", "", false, true, "http://gopher.com/x/y/z/boofar/", "/x/y/z/boofar/"}, + {"x/y/z/boofar.md", "http://gopher.com/", "", "", true, true, "http://gopher.com/x/y/z/boofar.html", "/x/y/z/boofar.html"}, + {"x/y/z/boofar.md", "", "boofar", "", false, false, "/x/y/z/boofar/", "/x/y/z/boofar/"}, + {"x/y/z/boofar.md", "http://barnew/", "", "", false, false, "http://barnew/x/y/z/boofar/", "/x/y/z/boofar/"}, + {"x/y/z/boofar.md", "http://barnew/", "boofar", "", false, false, "http://barnew/x/y/z/boofar/", "/x/y/z/boofar/"}, + {"x/y/z/boofar.md", "", "", "", true, false, "/x/y/z/boofar.html", "/x/y/z/boofar.html"}, + {"x/y/z/boofar.md", "", "", "", true, false, "/x/y/z/boofar.html", "/x/y/z/boofar.html"}, + {"x/y/z/boofar.md", "", "boofar", "", true, false, "/x/y/z/boofar.html", "/x/y/z/boofar.html"}, + {"x/y/z/boofar.md", "http://barnew/", "", "", true, false, "http://barnew/x/y/z/boofar.html", "/x/y/z/boofar.html"}, + {"x/y/z/boofar.md", "http://barnew/", "boofar", "", true, false, "http://barnew/x/y/z/boofar.html", "/x/y/z/boofar.html"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "", true, false, "http://barnew/boo/x/y/z/booslug.html", "/boo/x/y/z/booslug.html"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "", false, true, "http://barnew/boo/x/y/z/booslug/", "/x/y/z/booslug/"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "", false, false, "http://barnew/boo/x/y/z/booslug/", "/boo/x/y/z/booslug/"}, + {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "", true, true, "http://barnew/boo/x/y/z/booslug.html", "/x/y/z/booslug.html"}, + {"x/y/z/boofar.md", "http://barnew/boo", "booslug", "", true, true, "http://barnew/boo/x/y/z/booslug.html", "/x/y/z/booslug.html"}, + + // test URL overrides + {"x/y/z/boofar.md", "", "", "/z/y/q/", false, false, "/z/y/q/", "/z/y/q/"}, + } + + for i, test := range tests { + + cfg, fs := newTestCfg() + + cfg.Set("uglyURLs", test.uglyURLs) + cfg.Set("canonifyURLs", test.canonifyURLs) + cfg.Set("baseURL", test.base) + + pageContent := fmt.Sprintf(`--- +title: Page +slug: %q +url: %q +--- +Content +`, test.slug, test.url) + + writeSource(t, fs, filepath.Join("content", filepath.FromSlash(test.file)), pageContent) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + require.Len(t, s.RegularPages, 1) + + p := s.RegularPages[0] + + u := p.Permalink() + + expected := test.expectedAbs + if u != expected { + t.Fatalf("[%d] Expected abs url: %s, got: %s", i, expected, u) + } + + u = p.RelPermalink() + + expected = test.expectedRel + if u != expected { + t.Errorf("[%d] Expected rel url: %s, got: %s", i, expected, u) + } + } +} diff --git a/hugolib/page_taxonomy_test.go b/hugolib/page_taxonomy_test.go new file mode 100644 index 000000000..e0dc1ffbc --- /dev/null +++ b/hugolib/page_taxonomy_test.go @@ -0,0 +1,96 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "reflect" + "strings" + "testing" +) + +var pageYamlWithTaxonomiesA = `--- +tags: ['a', 'B', 'c'] +categories: 'd' +--- +YAML frontmatter with tags and categories taxonomy.` + +var pageYamlWithTaxonomiesB = `--- +tags: + - "a" + - "B" + - "c" +categories: 'd' +--- +YAML frontmatter with tags and categories taxonomy.` + +var pageYamlWithTaxonomiesC = `--- +tags: 'E' +categories: 'd' +--- +YAML frontmatter with tags and categories taxonomy.` + +var pageJSONWithTaxonomies = `{ + "categories": "D", + "tags": [ + "a", + "b", + "c" + ] +} +JSON Front Matter with tags and categories` + +var pageTomlWithTaxonomies = `+++ +tags = [ "a", "B", "c" ] +categories = "d" ++++ +TOML Front Matter with tags and categories` + +func TestParseTaxonomies(t *testing.T) { + t.Parallel() + for _, test := range []string{pageTomlWithTaxonomies, + pageJSONWithTaxonomies, + pageYamlWithTaxonomiesA, + pageYamlWithTaxonomiesB, + pageYamlWithTaxonomiesC, + } { + + s := newTestSite(t) + p, _ := s.NewPage("page/with/taxonomy") + _, err := p.ReadFrom(strings.NewReader(test)) + if err != nil { + t.Fatalf("Failed parsing %q: %s", test, err) + } + + param := p.GetParam("tags") + + if params, ok := param.([]string); ok { + expected := []string{"a", "b", "c"} + if !reflect.DeepEqual(params, expected) { + t.Errorf("Expected %s: got: %s", expected, params) + } + } else if params, ok := param.(string); ok { + expected := "e" + if params != expected { + t.Errorf("Expected %s: got: %s", expected, params) + } + } + + param = p.GetParam("categories") + singleparam := param.(string) + + if singleparam != "d" { + t.Fatalf("Expected: d, got: %s", singleparam) + } + } +} diff --git a/hugolib/page_test.go b/hugolib/page_test.go new file mode 100644 index 000000000..66fc5d253 --- /dev/null +++ b/hugolib/page_test.go @@ -0,0 +1,1502 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "bytes" + "fmt" + "html/template" + "os" + "path/filepath" + "reflect" + "sort" + "strings" + "testing" + "time" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" + "github.com/spf13/cast" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var emptyPage = "" + +const ( + simplePage = "---\ntitle: Simple\n---\nSimple Page\n" + invalidFrontMatterMissing = "This is a test" + renderNoFrontmatter = "<!doctype><html><head></head><body>This is a test</body></html>" + contentWithCommentedFrontmatter = "<!--\n+++\ntitle = \"Network configuration\"\ndescription = \"Docker networking\"\nkeywords = [\"network\"]\n[menu.main]\nparent= \"smn_administrate\"\n+++\n-->\n\n# Network configuration\n\n##\nSummary" + contentWithCommentedTextFrontmatter = "<!--[metaData]>\n+++\ntitle = \"Network configuration\"\ndescription = \"Docker networking\"\nkeywords = [\"network\"]\n[menu.main]\nparent= \"smn_administrate\"\n+++\n<![end-metadata]-->\n\n# Network configuration\n\n##\nSummary" + contentWithCommentedLongFrontmatter = "<!--[metaData123456789012345678901234567890]>\n+++\ntitle = \"Network configuration\"\ndescription = \"Docker networking\"\nkeywords = [\"network\"]\n[menu.main]\nparent= \"smn_administrate\"\n+++\n<![end-metadata]-->\n\n# Network configuration\n\n##\nSummary" + contentWithCommentedLong2Frontmatter = "<!--[metaData]>\n+++\ntitle = \"Network configuration\"\ndescription = \"Docker networking\"\nkeywords = [\"network\"]\n[menu.main]\nparent= \"smn_administrate\"\n+++\n<![end-metadata123456789012345678901234567890]-->\n\n# Network configuration\n\n##\nSummary" + invalidFrontmatterShortDelim = ` +-- +title: Short delim start +--- +Short Delim +` + + invalidFrontmatterShortDelimEnding = ` +--- +title: Short delim ending +-- +Short Delim +` + + invalidFrontmatterLadingWs = ` + + --- +title: Leading WS +--- +Leading +` + + simplePageJSON = ` +{ +"title": "spf13-vim 3.0 release and new website", +"description": "spf13-vim is a cross platform distribution of vim plugins and resources for Vim.", +"tags": [ ".vimrc", "plugins", "spf13-vim", "VIm" ], +"date": "2012-04-06", +"categories": [ + "Development", + "VIM" +], +"slug": "-spf13-vim-3-0-release-and-new-website-" +} + +Content of the file goes Here +` + + simplePageRFC3339Date = "---\ntitle: RFC3339 Date\ndate: \"2013-05-17T16:59:30Z\"\n---\nrfc3339 content" + simplePageJSONMultiple = ` +{ + "title": "foobar", + "customData": { "foo": "bar" }, + "date": "2012-08-06" +} +Some text +` + + simplePageWithSummaryDelimiter = `--- +title: Simple +--- +Summary Next Line + +<!--more--> +Some more text +` + + simplePageWithSummaryDelimiterAndMarkdownThatCrossesBorder = `--- +title: Simple +--- +The [best static site generator][hugo].[^1] +<!--more--> +[hugo]: http://gohugo.io/ +[^1]: Many people say so. +` + simplePageWithShortcodeInSummary = `--- +title: Simple +--- +Summary Next Line. {{<figure src="/not/real" >}}. +More text here. + +Some more text +` + + simplePageWithEmbeddedScript = `--- +title: Simple +--- +<script type='text/javascript'>alert('the script tags are still there, right?');</script> +` + + simplePageWithSummaryDelimiterSameLine = `--- +title: Simple +--- +Summary Same Line<!--more--> + +Some more text +` + + simplePageWithSummaryDelimiterOnlySummary = `--- +title: Simple +--- +Summary text + +<!--more--> +` + + simplePageWithAllCJKRunes = `--- +title: Simple +--- + + +€ € € € € +你好 +도형이 +カテゴリー + + +` + + simplePageWithMainEnglishWithCJKRunes = `--- +title: Simple +--- + + +In Chinese, 好 means good. In Chinese, 好 means good. +In Chinese, 好 means good. In Chinese, 好 means good. +In Chinese, 好 means good. In Chinese, 好 means good. +In Chinese, 好 means good. In Chinese, 好 means good. +In Chinese, 好 means good. In Chinese, 好 means good. +In Chinese, 好 means good. In Chinese, 好 means good. +In Chinese, 好 means good. In Chinese, 好 means good. +More then 70 words. + + +` + simplePageWithMainEnglishWithCJKRunesSummary = "In Chinese, 好 means good. In Chinese, 好 means good. " + + "In Chinese, 好 means good. In Chinese, 好 means good. " + + "In Chinese, 好 means good. In Chinese, 好 means good. " + + "In Chinese, 好 means good. In Chinese, 好 means good. " + + "In Chinese, 好 means good. In Chinese, 好 means good. " + + "In Chinese, 好 means good. In Chinese, 好 means good. " + + "In Chinese, 好 means good. In Chinese, 好 means good." + + simplePageWithIsCJKLanguageFalse = `--- +title: Simple +isCJKLanguage: false +--- + +In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. +In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. +In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. +In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. +In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. +In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. +In Chinese, 好的啊 means good. In Chinese, 好的呀呀 means good enough. +More then 70 words. + + +` + simplePageWithIsCJKLanguageFalseSummary = "In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. " + + "In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. " + + "In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. " + + "In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. " + + "In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. " + + "In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. " + + "In Chinese, 好的啊 means good. In Chinese, 好的呀呀 means good enough." + + simplePageWithLongContent = `--- +title: Simple +--- + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu +fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in +culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit +amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore +et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation +ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor +in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla +pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui +officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, +consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et +dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco +laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in +reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. +Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia +deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur +adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna +aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi +ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in +voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint +occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim +id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed +do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim +veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo +consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse +cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non +proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem +ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu +fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in +culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit +amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore +et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation +ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor +in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla +pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui +officia deserunt mollit anim id est laborum.` + + pageWithToC = `--- +title: TOC +--- +For some moments the old man did not reply. He stood with bowed head, buried in deep thought. But at last he spoke. + +## AA + +I have no idea, of course, how long it took me to reach the limit of the plain, +but at last I entered the foothills, following a pretty little canyon upward +toward the mountains. Beside me frolicked a laughing brooklet, hurrying upon +its noisy way down to the silent sea. In its quieter pools I discovered many +small fish, of four-or five-pound weight I should imagine. In appearance, +except as to size and color, they were not unlike the whale of our own seas. As +I watched them playing about I discovered, not only that they suckled their +young, but that at intervals they rose to the surface to breathe as well as to +feed upon certain grasses and a strange, scarlet lichen which grew upon the +rocks just above the water line. + +### AAA + +I remember I felt an extraordinary persuasion that I was being played with, +that presently, when I was upon the very verge of safety, this mysterious +death--as swift as the passage of light--would leap after me from the pit about +the cylinder and strike me down. ## BB + +### BBB + +"You're a great Granser," he cried delightedly, "always making believe them little marks mean something." +` + + simplePageWithAdditionalExtension = `+++ +[blackfriday] + extensions = ["hardLineBreak"] ++++ +first line. +second line. + +fourth line. +` + + simplePageWithURL = `--- +title: Simple +url: simple/url/ +--- +Simple Page With URL` + + simplePageWithSlug = `--- +title: Simple +slug: simple-slug +--- +Simple Page With Slug` + + simplePageWithDate = `--- +title: Simple +date: '2013-10-15T06:16:13' +--- +Simple Page With Date` + + UTF8Page = `--- +title: ラーメン +--- +UTF8 Page` + + UTF8PageWithURL = `--- +title: ラーメン +url: ラーメン/url/ +--- +UTF8 Page With URL` + + UTF8PageWithSlug = `--- +title: ラーメン +slug: ラーメン-slug +--- +UTF8 Page With Slug` + + UTF8PageWithDate = `--- +title: ラーメン +date: '2013-10-15T06:16:13' +--- +UTF8 Page With Date` +) + +var pageWithVariousFrontmatterTypes = `+++ +a_string = "bar" +an_integer = 1 +a_float = 1.3 +a_bool = false +a_date = 1979-05-27T07:32:00Z + +[a_table] +a_key = "a_value" ++++ +Front Matter with various frontmatter types` + +var pageWithCalendarYAMLFrontmatter = `--- +type: calendar +weeks: + - + start: "Jan 5" + days: + - activity: class + room: EN1000 + - activity: lab + - activity: class + - activity: lab + - activity: class + - + start: "Jan 12" + days: + - activity: class + - activity: lab + - activity: class + - activity: lab + - activity: exam +--- + +Hi. +` + +var pageWithCalendarJSONFrontmatter = `{ + "type": "calendar", + "weeks": [ + { + "start": "Jan 5", + "days": [ + { "activity": "class", "room": "EN1000" }, + { "activity": "lab" }, + { "activity": "class" }, + { "activity": "lab" }, + { "activity": "class" } + ] + }, + { + "start": "Jan 12", + "days": [ + { "activity": "class" }, + { "activity": "lab" }, + { "activity": "class" }, + { "activity": "lab" }, + { "activity": "exam" } + ] + } + ] +} + +Hi. +` + +var pageWithCalendarTOMLFrontmatter = `+++ +type = "calendar" + +[[weeks]] +start = "Jan 5" + +[[weeks.days]] +activity = "class" +room = "EN1000" + +[[weeks.days]] +activity = "lab" + +[[weeks.days]] +activity = "class" + +[[weeks.days]] +activity = "lab" + +[[weeks.days]] +activity = "class" + +[[weeks]] +start = "Jan 12" + +[[weeks.days]] +activity = "class" + +[[weeks.days]] +activity = "lab" + +[[weeks.days]] +activity = "class" + +[[weeks.days]] +activity = "lab" + +[[weeks.days]] +activity = "exam" ++++ + +Hi. +` + +func checkError(t *testing.T, err error, expected string) { + if err == nil { + t.Fatalf("err is nil. Expected: %s", expected) + } + if err.Error() != expected { + t.Errorf("err.Error() returned: '%s'. Expected: '%s'", err.Error(), expected) + } +} + +func TestDegenerateEmptyPageZeroLengthName(t *testing.T) { + t.Parallel() + s := newTestSite(t) + _, err := s.NewPage("") + if err == nil { + t.Fatalf("A zero length page name must return an error") + } + + checkError(t, err, "Zero length page name") +} + +func TestDegenerateEmptyPage(t *testing.T) { + t.Parallel() + s := newTestSite(t) + _, err := s.NewPageFrom(strings.NewReader(emptyPage), "test") + if err != nil { + t.Fatalf("Empty files should not trigger an error. Should be able to touch a file while watching without erroring out.") + } +} + +func checkPageTitle(t *testing.T, page *Page, title string) { + if page.Title != title { + t.Fatalf("Page title is: %s. Expected %s", page.Title, title) + } +} + +func checkPageContent(t *testing.T, page *Page, content string, msg ...interface{}) { + a := normalizeContent(content) + b := normalizeContent(string(page.Content)) + if a != b { + t.Fatalf("Page content is:\n%q\nExpected:\n%q (%q)", b, a, msg) + } +} + +func normalizeContent(c string) string { + norm := c + norm = strings.Replace(norm, "\n", " ", -1) + norm = strings.Replace(norm, " ", " ", -1) + norm = strings.Replace(norm, " ", " ", -1) + norm = strings.Replace(norm, " ", " ", -1) + norm = strings.Replace(norm, "p> ", "p>", -1) + norm = strings.Replace(norm, "> <", "> <", -1) + return strings.TrimSpace(norm) +} + +func checkPageTOC(t *testing.T, page *Page, toc string) { + if page.TableOfContents != template.HTML(toc) { + t.Fatalf("Page TableOfContents is: %q.\nExpected %q", page.TableOfContents, toc) + } +} + +func checkPageSummary(t *testing.T, page *Page, summary string, msg ...interface{}) { + a := normalizeContent(string(page.Summary)) + b := normalizeContent(summary) + if a != b { + t.Fatalf("Page summary is:\n%q.\nExpected\n%q (%q)", a, b, msg) + } +} + +func checkPageType(t *testing.T, page *Page, pageType string) { + if page.Type() != pageType { + t.Fatalf("Page type is: %s. Expected: %s", page.Type(), pageType) + } +} + +func checkPageDate(t *testing.T, page *Page, time time.Time) { + if page.Date != time { + t.Fatalf("Page date is: %s. Expected: %s", page.Date, time) + } +} + +func checkTruncation(t *testing.T, page *Page, shouldBe bool, msg string) { + if page.Summary == "" { + t.Fatal("page has no summary, can not check truncation") + } + if page.Truncated != shouldBe { + if shouldBe { + t.Fatalf("page wasn't truncated: %s", msg) + } else { + t.Fatalf("page was truncated: %s", msg) + } + } +} + +func normalizeExpected(ext, str string) string { + str = normalizeContent(str) + switch ext { + default: + return str + case "html": + return strings.Trim(helpers.StripHTML(str), " ") + case "ad": + paragraphs := strings.Split(str, "</p>") + expected := "" + for _, para := range paragraphs { + if para == "" { + continue + } + expected += fmt.Sprintf("<div class=\"paragraph\">\n%s</p></div>\n", para) + } + return expected + case "rst": + return fmt.Sprintf("<div class=\"document\">\n\n\n%s</div>", str) + } +} + +func testAllMarkdownEnginesForPages(t *testing.T, + assertFunc func(t *testing.T, ext string, pages Pages), settings map[string]interface{}, pageSources ...string) { + + engines := []struct { + ext string + shouldExecute func() bool + }{ + {"md", func() bool { return true }}, + {"mmark", func() bool { return true }}, + {"ad", func() bool { return helpers.HasAsciidoc() }}, + // TODO(bep) figure a way to include this without too much work.{"html", func() bool { return true }}, + {"rst", func() bool { return helpers.HasRst() }}, + } + + for _, e := range engines { + if !e.shouldExecute() { + continue + } + + cfg, fs := newTestCfg() + + if settings != nil { + for k, v := range settings { + cfg.Set(k, v) + } + } + + contentDir := "content" + + if s := cfg.GetString("contentDir"); s != "" { + contentDir = s + } + + var fileSourcePairs []string + + for i, source := range pageSources { + fileSourcePairs = append(fileSourcePairs, fmt.Sprintf("p%d.%s", i, e.ext), source) + } + + for i := 0; i < len(fileSourcePairs); i += 2 { + writeSource(t, fs, filepath.Join(contentDir, fileSourcePairs[i]), fileSourcePairs[i+1]) + } + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + + require.Len(t, s.RegularPages, len(pageSources)) + + assertFunc(t, e.ext, s.RegularPages) + + } + +} + +func TestCreateNewPage(t *testing.T) { + t.Parallel() + assertFunc := func(t *testing.T, ext string, pages Pages) { + p := pages[0] + + // issue #2290: Path is relative to the content dir and will continue to be so. + require.Equal(t, filepath.FromSlash(fmt.Sprintf("p0.%s", ext)), p.Path()) + assert.False(t, p.IsHome()) + checkPageTitle(t, p, "Simple") + checkPageContent(t, p, normalizeExpected(ext, "<p>Simple Page</p>\n")) + checkPageSummary(t, p, "Simple Page") + checkPageType(t, p, "page") + checkTruncation(t, p, false, "simple short page") + } + + settings := map[string]interface{}{ + "contentDir": "mycontent", + } + + testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePage) +} + +func TestSplitSummaryAndContent(t *testing.T) { + t.Parallel() + for i, this := range []struct { + markup string + content string + expectedSummary string + expectedContent string + }{ + {"markdown", `<p>Summary Same LineHUGOMORE42</p> + +<p>Some more text</p>`, "<p>Summary Same Line</p>", "<p>Summary Same Line</p>\n\n<p>Some more text</p>"}, + {"asciidoc", `<div class="paragraph"><p>sn</p></div><div class="paragraph"><p>HUGOMORE42Some more text</p></div>`, + "<div class=\"paragraph\"><p>sn</p></div>", + "<div class=\"paragraph\"><p>sn</p></div><div class=\"paragraph\"><p>Some more text</p></div>"}, + {"rst", + "<div class=\"document\"><p>Summary Next Line</p><p>HUGOMORE42Some more text</p></div>", + "<div class=\"document\"><p>Summary Next Line</p></div>", + "<div class=\"document\"><p>Summary Next Line</p><p>Some more text</p></div>"}, + {"markdown", "<p>a</p><p>b</p><p>HUGOMORE42c</p>", "<p>a</p><p>b</p>", "<p>a</p><p>b</p><p>c</p>"}, + {"markdown", "<p>a</p><p>b</p><p>cHUGOMORE42</p>", "<p>a</p><p>b</p><p>c</p>", "<p>a</p><p>b</p><p>c</p>"}, + {"markdown", "<p>a</p><p>bHUGOMORE42</p><p>c</p>", "<p>a</p><p>b</p>", "<p>a</p><p>b</p><p>c</p>"}, + {"markdown", "<p>aHUGOMORE42</p><p>b</p><p>c</p>", "<p>a</p>", "<p>a</p><p>b</p><p>c</p>"}, + {"markdown", " HUGOMORE42 ", "", ""}, + {"markdown", "HUGOMORE42", "", ""}, + {"markdown", "<p>HUGOMORE42", "<p>", "<p>"}, + {"markdown", "HUGOMORE42<p>", "", "<p>"}, + {"markdown", "\n\n<p>HUGOMORE42</p>\n", "<p></p>", "<p></p>"}, + // Issue #2586 + // Note: Hugo will not split mid-sentence but will look for the closest + // paragraph end marker. This may be a change from Hugo 0.16, but it makes sense. + {"markdown", `<p>this is an example HUGOMORE42of the issue.</p>`, + "<p>this is an example of the issue.</p>", + "<p>this is an example of the issue.</p>"}, + // Issue: #2538 + {"markdown", fmt.Sprintf(` <p class="lead">%s</p>HUGOMORE42<p>%s</p> +`, + strings.Repeat("A", 10), strings.Repeat("B", 31)), + fmt.Sprintf(`<p class="lead">%s</p>`, strings.Repeat("A", 10)), + fmt.Sprintf(`<p class="lead">%s</p><p>%s</p>`, strings.Repeat("A", 10), strings.Repeat("B", 31)), + }, + } { + + sc, err := splitUserDefinedSummaryAndContent(this.markup, []byte(this.content)) + + require.NoError(t, err) + require.NotNil(t, sc, fmt.Sprintf("[%d] Nil %s", i, this.markup)) + require.Equal(t, this.expectedSummary, string(sc.summary), fmt.Sprintf("[%d] Summary markup %s", i, this.markup)) + require.Equal(t, this.expectedContent, string(sc.content), fmt.Sprintf("[%d] Content markup %s", i, this.markup)) + } +} + +func TestPageWithDelimiter(t *testing.T) { + t.Parallel() + assertFunc := func(t *testing.T, ext string, pages Pages) { + p := pages[0] + checkPageTitle(t, p, "Simple") + checkPageContent(t, p, normalizeExpected(ext, "<p>Summary Next Line</p>\n\n<p>Some more text</p>\n"), ext) + checkPageSummary(t, p, normalizeExpected(ext, "<p>Summary Next Line</p>"), ext) + checkPageType(t, p, "page") + checkTruncation(t, p, true, "page with summary delimiter") + } + + testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryDelimiter) +} + +// Issue #1076 +func TestPageWithDelimiterForMarkdownThatCrossesBorder(t *testing.T) { + t.Parallel() + cfg, fs := newTestCfg() + + writeSource(t, fs, filepath.Join("content", "simple.md"), simplePageWithSummaryDelimiterAndMarkdownThatCrossesBorder) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + + require.Len(t, s.RegularPages, 1) + + p := s.RegularPages[0] + + if p.Summary != template.HTML("<p>The <a href=\"http://gohugo.io/\">best static site generator</a>.<sup class=\"footnote-ref\" id=\"fnref:1\"><a rel=\"footnote\" href=\"#fn:1\">1</a></sup>\n</p>") { + t.Fatalf("Got summary:\n%q", p.Summary) + } + + if p.Content != template.HTML("<p>The <a href=\"http://gohugo.io/\">best static site generator</a>.<sup class=\"footnote-ref\" id=\"fnref:1\"><a rel=\"footnote\" href=\"#fn:1\">1</a></sup>\n</p>\n<div class=\"footnotes\">\n\n<hr />\n\n<ol>\n<li id=\"fn:1\">Many people say so.\n <a class=\"footnote-return\" href=\"#fnref:1\"><sup>[return]</sup></a></li>\n</ol>\n</div>") { + t.Fatalf("Got content:\n%q", p.Content) + } +} + +// Issue #2601 +func TestPageRawContent(t *testing.T) { + t.Parallel() + cfg, fs := newTestCfg() + + writeSource(t, fs, filepath.Join("content", "raw.md"), `--- +title: Raw +--- +**Raw**`) + + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{ .RawContent }}`) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + + require.Len(t, s.RegularPages, 1) + p := s.RegularPages[0] + + require.Contains(t, p.RawContent(), "**Raw**") + +} + +func TestPageWithShortCodeInSummary(t *testing.T) { + t.Parallel() + assertFunc := func(t *testing.T, ext string, pages Pages) { + p := pages[0] + checkPageTitle(t, p, "Simple") + checkPageContent(t, p, normalizeExpected(ext, "<p>Summary Next Line. \n<figure >\n \n <img src=\"/not/real\" />\n \n \n</figure>\n.\nMore text here.</p>\n\n<p>Some more text</p>\n")) + checkPageSummary(t, p, "Summary Next Line. . More text here. Some more text") + checkPageType(t, p, "page") + } + + testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithShortcodeInSummary) +} + +func TestPageWithEmbeddedScriptTag(t *testing.T) { + t.Parallel() + assertFunc := func(t *testing.T, ext string, pages Pages) { + p := pages[0] + if ext == "ad" || ext == "rst" { + // TOD(bep) + return + } + checkPageContent(t, p, "<script type='text/javascript'>alert('the script tags are still there, right?');</script>\n", ext) + } + + testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithEmbeddedScript) +} + +func TestPageWithAdditionalExtension(t *testing.T) { + t.Parallel() + cfg, fs := newTestCfg() + + writeSource(t, fs, filepath.Join("content", "simple.md"), simplePageWithAdditionalExtension) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + + require.Len(t, s.RegularPages, 1) + + p := s.RegularPages[0] + + checkPageContent(t, p, "<p>first line.<br />\nsecond line.</p>\n\n<p>fourth line.</p>\n") +} + +func TestTableOfContents(t *testing.T) { + + cfg, fs := newTestCfg() + + writeSource(t, fs, filepath.Join("content", "tocpage.md"), pageWithToC) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + + require.Len(t, s.RegularPages, 1) + + p := s.RegularPages[0] + + checkPageContent(t, p, "\n\n<p>For some moments the old man did not reply. He stood with bowed head, buried in deep thought. But at last he spoke.</p>\n\n<h2 id=\"aa\">AA</h2>\n\n<p>I have no idea, of course, how long it took me to reach the limit of the plain,\nbut at last I entered the foothills, following a pretty little canyon upward\ntoward the mountains. Beside me frolicked a laughing brooklet, hurrying upon\nits noisy way down to the silent sea. In its quieter pools I discovered many\nsmall fish, of four-or five-pound weight I should imagine. In appearance,\nexcept as to size and color, they were not unlike the whale of our own seas. As\nI watched them playing about I discovered, not only that they suckled their\nyoung, but that at intervals they rose to the surface to breathe as well as to\nfeed upon certain grasses and a strange, scarlet lichen which grew upon the\nrocks just above the water line.</p>\n\n<h3 id=\"aaa\">AAA</h3>\n\n<p>I remember I felt an extraordinary persuasion that I was being played with,\nthat presently, when I was upon the very verge of safety, this mysterious\ndeath–as swift as the passage of light–would leap after me from the pit about\nthe cylinder and strike me down. ## BB</p>\n\n<h3 id=\"bbb\">BBB</h3>\n\n<p>“You’re a great Granser,” he cried delightedly, “always making believe them little marks mean something.”</p>\n") + checkPageTOC(t, p, "<nav id=\"TableOfContents\">\n<ul>\n<li>\n<ul>\n<li><a href=\"#aa\">AA</a>\n<ul>\n<li><a href=\"#aaa\">AAA</a></li>\n<li><a href=\"#bbb\">BBB</a></li>\n</ul></li>\n</ul></li>\n</ul>\n</nav>") +} + +func TestPageWithMoreTag(t *testing.T) { + t.Parallel() + assertFunc := func(t *testing.T, ext string, pages Pages) { + p := pages[0] + checkPageTitle(t, p, "Simple") + checkPageContent(t, p, normalizeExpected(ext, "<p>Summary Same Line</p>\n\n<p>Some more text</p>\n")) + checkPageSummary(t, p, normalizeExpected(ext, "<p>Summary Same Line</p>")) + checkPageType(t, p, "page") + + } + + testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryDelimiterSameLine) +} + +func TestPageWithMoreTagOnlySummary(t *testing.T) { + + assertFunc := func(t *testing.T, ext string, pages Pages) { + p := pages[0] + checkTruncation(t, p, false, "page with summary delimiter at end") + } + + testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryDelimiterOnlySummary) +} + +func TestPageWithDate(t *testing.T) { + t.Parallel() + cfg, fs := newTestCfg() + + writeSource(t, fs, filepath.Join("content", "simple.md"), simplePageRFC3339Date) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + + require.Len(t, s.RegularPages, 1) + + p := s.RegularPages[0] + d, _ := time.Parse(time.RFC3339, "2013-05-17T16:59:30Z") + + checkPageDate(t, p, d) +} + +func TestWordCountWithAllCJKRunesWithoutHasCJKLanguage(t *testing.T) { + t.Parallel() + assertFunc := func(t *testing.T, ext string, pages Pages) { + p := pages[0] + if p.WordCount() != 8 { + t.Fatalf("[%s] incorrect word count for content '%s'. expected %v, got %v", ext, p.plain, 8, p.WordCount()) + } + } + + testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithAllCJKRunes) +} + +func TestWordCountWithAllCJKRunesHasCJKLanguage(t *testing.T) { + t.Parallel() + settings := map[string]interface{}{"hasCJKLanguage": true} + + assertFunc := func(t *testing.T, ext string, pages Pages) { + p := pages[0] + if p.WordCount() != 15 { + t.Fatalf("[%s] incorrect word count for content '%s'. expected %v, got %v", ext, p.plain, 15, p.WordCount()) + } + } + testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePageWithAllCJKRunes) +} + +func TestWordCountWithMainEnglishWithCJKRunes(t *testing.T) { + t.Parallel() + settings := map[string]interface{}{"hasCJKLanguage": true} + + assertFunc := func(t *testing.T, ext string, pages Pages) { + p := pages[0] + if p.WordCount() != 74 { + t.Fatalf("[%s] incorrect word count for content '%s'. expected %v, got %v", ext, p.plain, 74, p.WordCount()) + } + + if p.Summary != simplePageWithMainEnglishWithCJKRunesSummary { + t.Fatalf("[%s] incorrect Summary for content '%s'. expected %v, got %v", ext, p.plain, + simplePageWithMainEnglishWithCJKRunesSummary, p.Summary) + } + } + + testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePageWithMainEnglishWithCJKRunes) +} + +func TestWordCountWithIsCJKLanguageFalse(t *testing.T) { + t.Parallel() + settings := map[string]interface{}{ + "hasCJKLanguage": true, + } + + assertFunc := func(t *testing.T, ext string, pages Pages) { + p := pages[0] + if p.WordCount() != 75 { + t.Fatalf("[%s] incorrect word count for content '%s'. expected %v, got %v", ext, p.plain, 74, p.WordCount()) + } + + if p.Summary != simplePageWithIsCJKLanguageFalseSummary { + t.Fatalf("[%s] incorrect Summary for content '%s'. expected %v, got %v", ext, p.plain, + simplePageWithIsCJKLanguageFalseSummary, p.Summary) + } + } + + testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePageWithIsCJKLanguageFalse) + +} + +func TestWordCount(t *testing.T) { + t.Parallel() + assertFunc := func(t *testing.T, ext string, pages Pages) { + p := pages[0] + if p.WordCount() != 483 { + t.Fatalf("[%s] incorrect word count. expected %v, got %v", ext, 483, p.WordCount()) + } + + if p.FuzzyWordCount() != 500 { + t.Fatalf("[%s] incorrect word count. expected %v, got %v", ext, 500, p.WordCount()) + } + + if p.ReadingTime() != 3 { + t.Fatalf("[%s] incorrect min read. expected %v, got %v", ext, 3, p.ReadingTime()) + } + + checkTruncation(t, p, true, "long page") + } + + testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithLongContent) +} + +func TestCreatePage(t *testing.T) { + t.Parallel() + var tests = []struct { + r string + }{ + {simplePageJSON}, + {simplePageJSONMultiple}, + //{strings.NewReader(SIMPLE_PAGE_JSON_COMPACT)}, + } + + for i, test := range tests { + s := newTestSite(t) + p, _ := s.NewPage("page") + if _, err := p.ReadFrom(strings.NewReader(test.r)); err != nil { + t.Fatalf("[%d] Unable to parse page: %s", i, err) + } + } +} + +func TestDegenerateInvalidFrontMatterShortDelim(t *testing.T) { + t.Parallel() + var tests = []struct { + r string + err string + }{ + {invalidFrontmatterShortDelimEnding, "unable to read frontmatter at filepos 45: EOF"}, + } + for _, test := range tests { + s := newTestSite(t) + p, _ := s.NewPage("invalid/front/matter/short/delim") + _, err := p.ReadFrom(strings.NewReader(test.r)) + checkError(t, err, test.err) + } +} + +func TestShouldRenderContent(t *testing.T) { + t.Parallel() + var tests = []struct { + text string + render bool + }{ + {invalidFrontMatterMissing, true}, + // TODO how to deal with malformed frontmatter. In this case it'll be rendered as markdown. + {invalidFrontmatterShortDelim, true}, + {renderNoFrontmatter, false}, + {contentWithCommentedFrontmatter, true}, + {contentWithCommentedTextFrontmatter, true}, + {contentWithCommentedLongFrontmatter, false}, + {contentWithCommentedLong2Frontmatter, true}, + } + + for _, test := range tests { + s := newTestSite(t) + p, _ := s.NewPage("render/front/matter") + _, err := p.ReadFrom(strings.NewReader(test.text)) + p = pageMust(p, err) + if p.IsRenderable() != test.render { + t.Errorf("expected p.IsRenderable() == %t, got %t", test.render, p.IsRenderable()) + } + } +} + +// Issue #768 +func TestCalendarParamsVariants(t *testing.T) { + t.Parallel() + s := newTestSite(t) + pageJSON, _ := s.NewPage("test/fileJSON.md") + _, _ = pageJSON.ReadFrom(strings.NewReader(pageWithCalendarJSONFrontmatter)) + + pageYAML, _ := s.NewPage("test/fileYAML.md") + _, _ = pageYAML.ReadFrom(strings.NewReader(pageWithCalendarYAMLFrontmatter)) + + pageTOML, _ := s.NewPage("test/fileTOML.md") + _, _ = pageTOML.ReadFrom(strings.NewReader(pageWithCalendarTOMLFrontmatter)) + + assert.True(t, compareObjects(pageJSON.Params, pageYAML.Params)) + assert.True(t, compareObjects(pageJSON.Params, pageTOML.Params)) + +} + +func TestDifferentFrontMatterVarTypes(t *testing.T) { + t.Parallel() + s := newTestSite(t) + page, _ := s.NewPage("test/file1.md") + _, _ = page.ReadFrom(strings.NewReader(pageWithVariousFrontmatterTypes)) + + dateval, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z") + if page.GetParam("a_string") != "bar" { + t.Errorf("frontmatter not handling strings correctly should be %s, got: %s", "bar", page.GetParam("a_string")) + } + if page.GetParam("an_integer") != 1 { + t.Errorf("frontmatter not handling ints correctly should be %s, got: %s", "1", page.GetParam("an_integer")) + } + if page.GetParam("a_float") != 1.3 { + t.Errorf("frontmatter not handling floats correctly should be %f, got: %s", 1.3, page.GetParam("a_float")) + } + if page.GetParam("a_bool") != false { + t.Errorf("frontmatter not handling bools correctly should be %t, got: %s", false, page.GetParam("a_bool")) + } + if page.GetParam("a_date") != dateval { + t.Errorf("frontmatter not handling dates correctly should be %s, got: %s", dateval, page.GetParam("a_date")) + } + param := page.GetParam("a_table") + if param == nil { + t.Errorf("frontmatter not handling tables correctly should be type of %v, got: type of %v", reflect.TypeOf(page.Params["a_table"]), reflect.TypeOf(param)) + } + if cast.ToStringMap(param)["a_key"] != "a_value" { + t.Errorf("frontmatter not handling values inside a table correctly should be %s, got: %s", "a_value", cast.ToStringMap(page.Params["a_table"])["a_key"]) + } +} + +func TestDegenerateInvalidFrontMatterLeadingWhitespace(t *testing.T) { + t.Parallel() + s := newTestSite(t) + p, _ := s.NewPage("invalid/front/matter/leading/ws") + _, err := p.ReadFrom(strings.NewReader(invalidFrontmatterLadingWs)) + if err != nil { + t.Fatalf("Unable to parse front matter given leading whitespace: %s", err) + } +} + +func TestSectionEvaluation(t *testing.T) { + t.Parallel() + s := newTestSite(t) + page, _ := s.NewPage(filepath.FromSlash("blue/file1.md")) + page.ReadFrom(strings.NewReader(simplePage)) + if page.Section() != "blue" { + t.Errorf("Section should be %s, got: %s", "blue", page.Section()) + } +} + +func TestSliceToLower(t *testing.T) { + t.Parallel() + tests := []struct { + value []string + expected []string + }{ + {[]string{"a", "b", "c"}, []string{"a", "b", "c"}}, + {[]string{"a", "B", "c"}, []string{"a", "b", "c"}}, + {[]string{"A", "B", "C"}, []string{"a", "b", "c"}}, + } + + for _, test := range tests { + res := helpers.SliceToLower(test.value) + for i, val := range res { + if val != test.expected[i] { + t.Errorf("Case mismatch. Expected %s, got %s", test.expected[i], res[i]) + } + } + } +} + +func TestPagePaths(t *testing.T) { + t.Parallel() + + siteParmalinksSetting := map[string]string{ + "post": ":year/:month/:day/:title/", + } + + tests := []struct { + content string + path string + hasPermalink bool + expected string + }{ + {simplePage, "post/x.md", false, "post/x.html"}, + {simplePageWithURL, "post/x.md", false, "simple/url/index.html"}, + {simplePageWithSlug, "post/x.md", false, "post/simple-slug.html"}, + {simplePageWithDate, "post/x.md", true, "2013/10/15/simple/index.html"}, + {UTF8Page, "post/x.md", false, "post/x.html"}, + {UTF8PageWithURL, "post/x.md", false, "ラーメン/url/index.html"}, + {UTF8PageWithSlug, "post/x.md", false, "post/ラーメン-slug.html"}, + {UTF8PageWithDate, "post/x.md", true, "2013/10/15/ラーメン/index.html"}, + } + + for _, test := range tests { + cfg, fs := newTestCfg() + + if test.hasPermalink { + cfg.Set("permalinks", siteParmalinksSetting) + } + + writeSource(t, fs, filepath.Join("content", filepath.FromSlash(test.path)), test.content) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + require.Len(t, s.RegularPages, 1) + + } +} + +var pageWithDraftAndPublished = `--- +title: broken +published: false +draft: true +--- +some content +` + +func TestDraftAndPublishedFrontMatterError(t *testing.T) { + t.Parallel() + s := newTestSite(t) + _, err := s.NewPageFrom(strings.NewReader(pageWithDraftAndPublished), "content/post/broken.md") + if err != ErrHasDraftAndPublished { + t.Errorf("expected ErrHasDraftAndPublished, was %#v", err) + } +} + +var pagesWithPublishedFalse = `--- +title: okay +published: false +--- +some content +` +var pageWithPublishedTrue = `--- +title: okay +published: true +--- +some content +` + +func TestPublishedFrontMatter(t *testing.T) { + t.Parallel() + s := newTestSite(t) + p, err := s.NewPageFrom(strings.NewReader(pagesWithPublishedFalse), "content/post/broken.md") + if err != nil { + t.Fatalf("err during parse: %s", err) + } + if !p.Draft { + t.Errorf("expected true, got %t", p.Draft) + } + p, err = s.NewPageFrom(strings.NewReader(pageWithPublishedTrue), "content/post/broken.md") + if err != nil { + t.Fatalf("err during parse: %s", err) + } + if p.Draft { + t.Errorf("expected false, got %t", p.Draft) + } +} + +var pagesDraftTemplate = []string{`--- +title: "okay" +draft: %t +--- +some content +`, + `+++ +title = "okay" +draft = %t ++++ + +some content +`, +} + +func TestDraft(t *testing.T) { + t.Parallel() + s := newTestSite(t) + for _, draft := range []bool{true, false} { + for i, templ := range pagesDraftTemplate { + pageContent := fmt.Sprintf(templ, draft) + p, err := s.NewPageFrom(strings.NewReader(pageContent), "content/post/broken.md") + if err != nil { + t.Fatalf("err during parse: %s", err) + } + if p.Draft != draft { + t.Errorf("[%d] expected %t, got %t", i, draft, p.Draft) + } + } + } +} + +var pagesParamsTemplate = []string{`+++ +title = "okay" +draft = false +tags = [ "hugo", "web" ] +social= [ + [ "a", "#" ], + [ "b", "#" ], +] ++++ +some content +`, + `--- +title: "okay" +draft: false +tags: + - hugo + - web +social: + - - a + - "#" + - - b + - "#" +--- +some content +`, + `{ + "title": "okay", + "draft": false, + "tags": [ "hugo", "web" ], + "social": [ + [ "a", "#" ], + [ "b", "#" ] + ] +} +some content +`, +} + +func TestPageParams(t *testing.T) { + t.Parallel() + s := newTestSite(t) + wantedMap := map[string]interface{}{ + "tags": []string{"hugo", "web"}, + // Issue #2752 + "social": []interface{}{ + []interface{}{"a", "#"}, + []interface{}{"b", "#"}, + }, + } + + for i, c := range pagesParamsTemplate { + p, err := s.NewPageFrom(strings.NewReader(c), "content/post/params.md") + require.NoError(t, err, "err during parse", "#%d", i) + for key := range wantedMap { + assert.Equal(t, wantedMap[key], p.Params[key], "#%d", key) + } + } +} + +func TestTraverse(t *testing.T) { + exampleParams := `--- +rating: "5 stars" +tags: + - hugo + - web +social: + twitter: "@jxxf" + facebook: "https://example.com" +---` + t.Parallel() + s := newTestSite(t) + p, _ := s.NewPageFrom(strings.NewReader(exampleParams), "content/post/params.md") + + topLevelKeyValue, _ := p.Param("rating") + assert.Equal(t, "5 stars", topLevelKeyValue) + + nestedStringKeyValue, _ := p.Param("social.twitter") + assert.Equal(t, "@jxxf", nestedStringKeyValue) + + nonexistentKeyValue, _ := p.Param("doesn't.exist") + assert.Nil(t, nonexistentKeyValue) +} + +func TestPageSimpleMethods(t *testing.T) { + t.Parallel() + s := newTestSite(t) + for i, this := range []struct { + assertFunc func(p *Page) bool + }{ + {func(p *Page) bool { return !p.IsNode() }}, + {func(p *Page) bool { return p.IsPage() }}, + {func(p *Page) bool { return p.Plain() == "Do Be Do Be Do" }}, + {func(p *Page) bool { return strings.Join(p.PlainWords(), " ") == "Do Be Do Be Do" }}, + } { + + p, _ := s.NewPage("Test") + p.Content = "<h1>Do Be Do Be Do</h1>" + if !this.assertFunc(p) { + t.Errorf("[%d] Page method error", i) + } + } +} + +func TestIndexPageSimpleMethods(t *testing.T) { + s := newTestSite(t) + t.Parallel() + for i, this := range []struct { + assertFunc func(n *Page) bool + }{ + {func(n *Page) bool { return n.IsNode() }}, + {func(n *Page) bool { return !n.IsPage() }}, + {func(n *Page) bool { return n.Scratch() != nil }}, + {func(n *Page) bool { return n.Hugo() != nil }}, + {func(n *Page) bool { return n.Now().Unix() == time.Now().Unix() }}, + } { + + n := s.newHomePage() + + if !this.assertFunc(n) { + t.Errorf("[%d] Node method error", i) + } + } +} + +func TestKind(t *testing.T) { + t.Parallel() + // Add tests for these constants to make sure they don't change + require.Equal(t, "page", KindPage) + require.Equal(t, "home", KindHome) + require.Equal(t, "section", KindSection) + require.Equal(t, "taxonomy", KindTaxonomy) + require.Equal(t, "taxonomyTerm", KindTaxonomyTerm) + +} + +func TestChompBOM(t *testing.T) { + t.Parallel() + const utf8BOM = "\xef\xbb\xbf" + + cfg, fs := newTestCfg() + + writeSource(t, fs, filepath.Join("content", "simple.md"), utf8BOM+simplePage) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + + require.Len(t, s.RegularPages, 1) + + p := s.RegularPages[0] + + checkPageTitle(t, p, "Simple") +} + +// TODO(bep) this may be useful for other tests. +func compareObjects(a interface{}, b interface{}) bool { + aStr := strings.Split(fmt.Sprintf("%v", a), "") + sort.Strings(aStr) + + bStr := strings.Split(fmt.Sprintf("%v", b), "") + sort.Strings(bStr) + + return strings.Join(aStr, "") == strings.Join(bStr, "") +} + +func TestShouldBuild(t *testing.T) { + t.Parallel() + var past = time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC) + var future = time.Date(2037, 11, 17, 20, 34, 58, 651387237, time.UTC) + var zero = time.Time{} + + var publishSettings = []struct { + buildFuture bool + buildExpired bool + buildDrafts bool + draft bool + publishDate time.Time + expiryDate time.Time + out bool + }{ + // publishDate and expiryDate + {false, false, false, false, zero, zero, true}, + {false, false, false, false, zero, future, true}, + {false, false, false, false, past, zero, true}, + {false, false, false, false, past, future, true}, + {false, false, false, false, past, past, false}, + {false, false, false, false, future, future, false}, + {false, false, false, false, future, past, false}, + + // buildFuture and buildExpired + {false, true, false, false, past, past, true}, + {true, true, false, false, past, past, true}, + {true, false, false, false, past, past, false}, + {true, false, false, false, future, future, true}, + {true, true, false, false, future, future, true}, + {false, true, false, false, future, past, false}, + + // buildDrafts and draft + {true, true, false, true, past, future, false}, + {true, true, true, true, past, future, true}, + {true, true, true, true, past, future, true}, + } + + for _, ps := range publishSettings { + s := shouldBuild(ps.buildFuture, ps.buildExpired, ps.buildDrafts, ps.draft, + ps.publishDate, ps.expiryDate) + if s != ps.out { + t.Errorf("AssertShouldBuild unexpected output with params: %+v", ps) + } + } +} + +// "dot" in path: #1885 and #2110 +// disablePathToLower regression: #3374 +func TestPathIssues(t *testing.T) { + t.Parallel() + for _, disablePathToLower := range []bool{false, true} { + for _, uglyURLs := range []bool{false, true} { + t.Run(fmt.Sprintf("disablePathToLower=%t,uglyURLs=%t", disablePathToLower, uglyURLs), func(t *testing.T) { + + cfg, fs := newTestCfg() + th := testHelper{cfg, fs, t} + + cfg.Set("permalinks", map[string]string{ + "post": ":section/:title", + }) + + cfg.Set("uglyURLs", uglyURLs) + cfg.Set("disablePathToLower", disablePathToLower) + cfg.Set("paginate", 1) + + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), "<html><body>{{.Content}}</body></html>") + writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"), + "<html><body>P{{.Paginator.PageNumber}}|URL: {{.Paginator.URL}}|{{ if .Paginator.HasNext }}Next: {{.Paginator.Next.URL }}{{ end }}</body></html>") + + for i := 0; i < 3; i++ { + writeSource(t, fs, filepath.Join("content", "post", fmt.Sprintf("doc%d.md", i)), + fmt.Sprintf(`--- +title: "test%d.dot" +tags: +- ".net" +--- +# doc1 +*some content*`, i)) + } + + writeSource(t, fs, filepath.Join("content", "Blog", "Blog1.md"), + fmt.Sprintf(`--- +title: "testBlog" +tags: +- "Blog" +--- +# doc1 +*some blog content*`)) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + require.Len(t, s.RegularPages, 4) + + pathFunc := func(s string) string { + if uglyURLs { + return strings.Replace(s, "/index.html", ".html", 1) + } + return s + } + + blog := "blog" + + if disablePathToLower { + blog = "Blog" + } + + th.assertFileContent(pathFunc("public/"+blog+"/"+blog+"1/index.html"), "some blog content") + + th.assertFileContent(pathFunc("public/post/test0.dot/index.html"), "some content") + + if uglyURLs { + th.assertFileContent("public/post/page/1.html", `canonical" href="/post.html"/`) + th.assertFileContent("public/post.html", `<body>P1|URL: /post.html|Next: /post/page/2.html</body>`) + th.assertFileContent("public/post/page/2.html", `<body>P2|URL: /post/page/2.html|Next: /post/page/3.html</body>`) + } else { + th.assertFileContent("public/post/page/1/index.html", `canonical" href="/post/"/`) + th.assertFileContent("public/post/index.html", `<body>P1|URL: /post/|Next: /post/page/2/</body>`) + th.assertFileContent("public/post/page/2/index.html", `<body>P2|URL: /post/page/2/|Next: /post/page/3/</body>`) + th.assertFileContent("public/tags/.net/index.html", `<body>P1|URL: /tags/.net/|Next: /tags/.net/page/2/</body>`) + + } + + p := s.RegularPages[0] + if uglyURLs { + require.Equal(t, "/post/test0.dot.html", p.RelPermalink()) + } else { + require.Equal(t, "/post/test0.dot/", p.RelPermalink()) + } + + }) + } + } +} + +func BenchmarkParsePage(b *testing.B) { + s := newTestSite(b) + f, _ := os.Open("testdata/redis.cn.md") + var buf bytes.Buffer + buf.ReadFrom(f) + b.ResetTimer() + for i := 0; i < b.N; i++ { + page, _ := s.NewPage("bench") + page.ReadFrom(bytes.NewReader(buf.Bytes())) + } +} diff --git a/hugolib/page_time_integration_test.go b/hugolib/page_time_integration_test.go new file mode 100644 index 000000000..1bf83bdca --- /dev/null +++ b/hugolib/page_time_integration_test.go @@ -0,0 +1,183 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "os" + "strings" + "sync" + "testing" + "time" + + "github.com/spf13/cast" +) + +const ( + pageWithInvalidDate = `--- +date: 2010-05-02_15:29:31+08:00 +--- +Page With Invalid Date (replace T with _ for RFC 3339)` + + pageWithDateRFC3339 = `--- +date: 2010-05-02T15:29:31+08:00 +--- +Page With Date RFC3339` + + pageWithDateRFC3339NoT = `--- +date: 2010-05-02 15:29:31+08:00 +--- +Page With Date RFC3339_NO_T` + + pageWithRFC1123 = `--- +date: Sun, 02 May 2010 15:29:31 PST +--- +Page With Date RFC1123` + + pageWithDateRFC1123Z = `--- +date: Sun, 02 May 2010 15:29:31 +0800 +--- +Page With Date RFC1123Z` + + pageWithDateRFC822 = `--- +date: 02 May 10 15:29 PST +--- +Page With Date RFC822` + + pageWithDateRFC822Z = `--- +date: 02 May 10 15:29 +0800 +--- +Page With Date RFC822Z` + + pageWithDateANSIC = `--- +date: Sun May 2 15:29:31 2010 +--- +Page With Date ANSIC` + + pageWithDateUnixDate = `--- +date: Sun May 2 15:29:31 PST 2010 +--- +Page With Date UnixDate` + + pageWithDateRubyDate = `--- +date: Sun May 02 15:29:31 +0800 2010 +--- +Page With Date RubyDate` + + pageWithDateHugoYearNumeric = `--- +date: 2010-05-02 +--- +Page With Date HugoYearNumeric` + + pageWithDateHugoYear = `--- +date: 02 May 2010 +--- +Page With Date HugoYear` + + pageWithDateHugoLong = `--- +date: 02 May 2010 15:29 PST +--- +Page With Date HugoLong` +) + +func TestDegenerateDateFrontMatter(t *testing.T) { + t.Parallel() + s := newTestSite(t) + p, _ := s.NewPageFrom(strings.NewReader(pageWithInvalidDate), "page/with/invalid/date") + if p.Date != *new(time.Time) { + t.Fatalf("Date should be set to time.Time zero value. Got: %s", p.Date) + } +} + +func TestParsingDateInFrontMatter(t *testing.T) { + t.Parallel() + s := newTestSite(t) + tests := []struct { + buf string + dt string + }{ + {pageWithDateRFC3339, "2010-05-02T15:29:31+08:00"}, + {pageWithDateRFC3339NoT, "2010-05-02T15:29:31+08:00"}, + {pageWithDateRFC1123Z, "2010-05-02T15:29:31+08:00"}, + {pageWithDateRFC822Z, "2010-05-02T15:29:00+08:00"}, + {pageWithDateANSIC, "2010-05-02T15:29:31Z"}, + {pageWithDateRubyDate, "2010-05-02T15:29:31+08:00"}, + {pageWithDateHugoYearNumeric, "2010-05-02T00:00:00Z"}, + {pageWithDateHugoYear, "2010-05-02T00:00:00Z"}, + } + + tzShortCodeTests := []struct { + buf string + dt string + }{ + {pageWithRFC1123, "2010-05-02T15:29:31-08:00"}, + {pageWithDateRFC822, "2010-05-02T15:29:00-08:00Z"}, + {pageWithDateUnixDate, "2010-05-02T15:29:31-08:00"}, + {pageWithDateHugoLong, "2010-05-02T15:21:00+08:00"}, + } + + if _, err := time.LoadLocation("PST"); err == nil { + tests = append(tests, tzShortCodeTests...) + } else { + fmt.Fprintf(os.Stderr, "Skipping shortname timezone tests.\n") + } + + for _, test := range tests { + dt, e := time.Parse(time.RFC3339, test.dt) + if e != nil { + t.Fatalf("Unable to parse date time (RFC3339) for running the test: %s", e) + } + p, err := s.NewPageFrom(strings.NewReader(test.buf), "page/with/date") + if err != nil { + t.Fatalf("Expected to be able to parse page.") + } + if !dt.Equal(p.Date) { + t.Errorf("Date does not equal frontmatter:\n%s\nExpecting: %s\n Got: %s. Diff: %s\n internal: %#v\n %#v", test.buf, dt, p.Date, dt.Sub(p.Date), dt, p.Date) + } + } +} + +// Temp test https://github.com/gohugoio/hugo/issues/3059 +func TestParsingDateParallel(t *testing.T) { + t.Parallel() + + var wg sync.WaitGroup + + for j := 0; j < 100; j++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 100; j++ { + dateStr := "2010-05-02 15:29:31 +08:00" + + dt, err := time.Parse("2006-01-02 15:04:05 -07:00", dateStr) + if err != nil { + t.Fatal(err) + } + + if dt.Year() != 2010 { + t.Fatal("time.Parse: Invalid date:", dt) + } + + dt2 := cast.ToTime(dateStr) + + if dt2.Year() != 2010 { + t.Fatal("cast.ToTime: Invalid date:", dt2.Year()) + } + } + }() + } + wg.Wait() + +} diff --git a/hugolib/pagesPrevNext.go b/hugolib/pagesPrevNext.go new file mode 100644 index 000000000..bb474c499 --- /dev/null +++ b/hugolib/pagesPrevNext.go @@ -0,0 +1,40 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +// Prev returns the previous page reletive to the given page. +func (p Pages) Prev(cur *Page) *Page { + for x, c := range p { + if c.UniqueID() == cur.UniqueID() { + if x == 0 { + return p[len(p)-1] + } + return p[x-1] + } + } + return nil +} + +// Next returns the next page reletive to the given page. +func (p Pages) Next(cur *Page) *Page { + for x, c := range p { + if c.UniqueID() == cur.UniqueID() { + if x < len(p)-1 { + return p[x+1] + } + return p[0] + } + } + return nil +} diff --git a/hugolib/pagesPrevNext_test.go b/hugolib/pagesPrevNext_test.go new file mode 100644 index 000000000..5945d8fe5 --- /dev/null +++ b/hugolib/pagesPrevNext_test.go @@ -0,0 +1,86 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "testing" + + "github.com/spf13/cast" + "github.com/stretchr/testify/assert" +) + +type pagePNTestObject struct { + path string + weight int + date string +} + +var pagePNTestSources = []pagePNTestObject{ + {"/section1/testpage1.md", 5, "2012-04-06"}, + {"/section1/testpage2.md", 4, "2012-01-01"}, + {"/section1/testpage3.md", 3, "2012-04-06"}, + {"/section2/testpage4.md", 2, "2012-03-02"}, + {"/section2/testpage5.md", 1, "2012-04-06"}, +} + +func TestPrev(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + assert.Equal(t, pages.Prev(pages[0]), pages[4]) + assert.Equal(t, pages.Prev(pages[1]), pages[0]) + assert.Equal(t, pages.Prev(pages[4]), pages[3]) +} + +func TestNext(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + assert.Equal(t, pages.Next(pages[0]), pages[1]) + assert.Equal(t, pages.Next(pages[1]), pages[2]) + assert.Equal(t, pages.Next(pages[4]), pages[0]) +} + +func prepareWeightedPagesPrevNext(t *testing.T) WeightedPages { + s := newTestSite(t) + w := WeightedPages{} + + for _, src := range pagePNTestSources { + p, err := s.NewPage(src.path) + if err != nil { + t.Fatalf("failed to prepare test page %s", src.path) + } + p.Weight = src.weight + p.Date = cast.ToTime(src.date) + p.PublishDate = cast.ToTime(src.date) + w = append(w, WeightedPage{p.Weight, p}) + } + + w.Sort() + return w +} + +func TestWeightedPagesPrev(t *testing.T) { + t.Parallel() + w := prepareWeightedPagesPrevNext(t) + assert.Equal(t, w.Prev(w[0].Page), w[4].Page) + assert.Equal(t, w.Prev(w[1].Page), w[0].Page) + assert.Equal(t, w.Prev(w[4].Page), w[3].Page) +} + +func TestWeightedPagesNext(t *testing.T) { + t.Parallel() + w := prepareWeightedPagesPrevNext(t) + assert.Equal(t, w.Next(w[0].Page), w[1].Page) + assert.Equal(t, w.Next(w[1].Page), w[2].Page) + assert.Equal(t, w.Next(w[4].Page), w[0].Page) +} diff --git a/hugolib/pagination.go b/hugolib/pagination.go new file mode 100644 index 000000000..4733cf7c8 --- /dev/null +++ b/hugolib/pagination.go @@ -0,0 +1,535 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "errors" + "fmt" + "html/template" + "math" + "reflect" + "strings" + + "github.com/gohugoio/hugo/config" + + "github.com/spf13/cast" +) + +// Pager represents one of the elements in a paginator. +// The number, starting on 1, represents its place. +type Pager struct { + number int + *paginator +} + +func (p Pager) String() string { + return fmt.Sprintf("Pager %d", p.number) +} + +type paginatedElement interface { + Len() int +} + +// Len returns the number of pages in the list. +func (p Pages) Len() int { + return len(p) +} + +// Len returns the number of pages in the page group. +func (psg PagesGroup) Len() int { + l := 0 + for _, pg := range psg { + l += len(pg.Pages) + } + return l +} + +type pagers []*Pager + +var ( + paginatorEmptyPages Pages + paginatorEmptyPageGroups PagesGroup +) + +type paginator struct { + paginatedElements []paginatedElement + pagers + paginationURLFactory + total int + size int + source interface{} + options []interface{} +} + +type paginationURLFactory func(int) string + +// PageNumber returns the current page's number in the pager sequence. +func (p *Pager) PageNumber() int { + return p.number +} + +// URL returns the URL to the current page. +func (p *Pager) URL() template.HTML { + return template.HTML(p.paginationURLFactory(p.PageNumber())) +} + +// Pages returns the Pages on this page. +// Note: If this return a non-empty result, then PageGroups() will return empty. +func (p *Pager) Pages() Pages { + if len(p.paginatedElements) == 0 { + return paginatorEmptyPages + } + + if pages, ok := p.element().(Pages); ok { + return pages + } + + return paginatorEmptyPages +} + +// PageGroups return Page groups for this page. +// Note: If this return non-empty result, then Pages() will return empty. +func (p *Pager) PageGroups() PagesGroup { + if len(p.paginatedElements) == 0 { + return paginatorEmptyPageGroups + } + + if groups, ok := p.element().(PagesGroup); ok { + return groups + } + + return paginatorEmptyPageGroups +} + +func (p *Pager) element() paginatedElement { + if len(p.paginatedElements) == 0 { + return paginatorEmptyPages + } + return p.paginatedElements[p.PageNumber()-1] +} + +// page returns the Page with the given index +func (p *Pager) page(index int) (*Page, error) { + + if pages, ok := p.element().(Pages); ok { + if pages != nil && len(pages) > index { + return pages[index], nil + } + return nil, nil + } + + // must be PagesGroup + // this construction looks clumsy, but ... + // ... it is the difference between 99.5% and 100% test coverage :-) + groups := p.element().(PagesGroup) + + i := 0 + for _, v := range groups { + for _, page := range v.Pages { + if i == index { + return page, nil + } + i++ + } + } + return nil, nil +} + +// NumberOfElements gets the number of elements on this page. +func (p *Pager) NumberOfElements() int { + return p.element().Len() +} + +// HasPrev tests whether there are page(s) before the current. +func (p *Pager) HasPrev() bool { + return p.PageNumber() > 1 +} + +// Prev returns the pager for the previous page. +func (p *Pager) Prev() *Pager { + if !p.HasPrev() { + return nil + } + return p.pagers[p.PageNumber()-2] +} + +// HasNext tests whether there are page(s) after the current. +func (p *Pager) HasNext() bool { + return p.PageNumber() < len(p.paginatedElements) +} + +// Next returns the pager for the next page. +func (p *Pager) Next() *Pager { + if !p.HasNext() { + return nil + } + return p.pagers[p.PageNumber()] +} + +// First returns the pager for the first page. +func (p *Pager) First() *Pager { + return p.pagers[0] +} + +// Last returns the pager for the last page. +func (p *Pager) Last() *Pager { + return p.pagers[len(p.pagers)-1] +} + +// Pagers returns a list of pagers that can be used to build a pagination menu. +func (p *paginator) Pagers() pagers { + return p.pagers +} + +// PageSize returns the size of each paginator page. +func (p *paginator) PageSize() int { + return p.size +} + +// TotalPages returns the number of pages in the paginator. +func (p *paginator) TotalPages() int { + return len(p.paginatedElements) +} + +// TotalNumberOfElements returns the number of elements on all pages in this paginator. +func (p *paginator) TotalNumberOfElements() int { + return p.total +} + +func splitPages(pages Pages, size int) []paginatedElement { + var split []paginatedElement + for low, j := 0, len(pages); low < j; low += size { + high := int(math.Min(float64(low+size), float64(len(pages)))) + split = append(split, pages[low:high]) + } + + return split +} + +func splitPageGroups(pageGroups PagesGroup, size int) []paginatedElement { + + type keyPage struct { + key interface{} + page *Page + } + + var ( + split []paginatedElement + flattened []keyPage + ) + + for _, g := range pageGroups { + for _, p := range g.Pages { + flattened = append(flattened, keyPage{g.Key, p}) + } + } + + numPages := len(flattened) + + for low, j := 0, numPages; low < j; low += size { + high := int(math.Min(float64(low+size), float64(numPages))) + + var ( + pg PagesGroup + key interface{} + groupIndex = -1 + ) + + for k := low; k < high; k++ { + kp := flattened[k] + if key == nil || key != kp.key { + key = kp.key + pg = append(pg, PageGroup{Key: key}) + groupIndex++ + } + pg[groupIndex].Pages = append(pg[groupIndex].Pages, kp.page) + } + split = append(split, pg) + } + + return split +} + +// Paginator get this Page's main output's paginator. +func (p *Page) Paginator(options ...interface{}) (*Pager, error) { + return p.mainPageOutput.Paginator(options...) +} + +// Paginator gets this PageOutput's paginator if it's already created. +// If it's not, one will be created with all pages in Data["Pages"]. +func (p *PageOutput) Paginator(options ...interface{}) (*Pager, error) { + if !p.IsNode() { + return nil, fmt.Errorf("Paginators not supported for pages of type %q (%q)", p.Kind, p.Title) + } + pagerSize, err := resolvePagerSize(p.s.Cfg, options...) + + if err != nil { + return nil, err + } + + var initError error + + p.paginatorInit.Do(func() { + if p.paginator != nil { + return + } + + pagers, err := paginatePages(p.targetPathDescriptor, p.Data["Pages"], pagerSize) + + if err != nil { + initError = err + } + + if len(pagers) > 0 { + // the rest of the nodes will be created later + p.paginator = pagers[0] + p.paginator.source = "paginator" + p.paginator.options = options + p.Site.addToPaginationPageCount(uint64(p.paginator.TotalPages())) + } + + }) + + if initError != nil { + return nil, initError + } + + return p.paginator, nil +} + +// Paginate invokes this Page's main output's Paginate method. +func (p *Page) Paginate(seq interface{}, options ...interface{}) (*Pager, error) { + return p.mainPageOutput.Paginate(seq, options...) +} + +// Paginate gets this PageOutput's paginator if it's already created. +// If it's not, one will be created with the qiven sequence. +// Note that repeated calls will return the same result, even if the sequence is different. +func (p *PageOutput) Paginate(seq interface{}, options ...interface{}) (*Pager, error) { + if !p.IsNode() { + return nil, fmt.Errorf("Paginators not supported for pages of type %q (%q)", p.Kind, p.Title) + } + + pagerSize, err := resolvePagerSize(p.s.Cfg, options...) + + if err != nil { + return nil, err + } + + var initError error + + p.paginatorInit.Do(func() { + if p.paginator != nil { + return + } + pagers, err := paginatePages(p.targetPathDescriptor, seq, pagerSize) + + if err != nil { + initError = err + } + + if len(pagers) > 0 { + // the rest of the nodes will be created later + p.paginator = pagers[0] + p.paginator.source = seq + p.paginator.options = options + p.Site.addToPaginationPageCount(uint64(p.paginator.TotalPages())) + } + + }) + + if initError != nil { + return nil, initError + } + + if p.paginator.source == "paginator" { + return nil, errors.New("a Paginator was previously built for this Node without filters; look for earlier .Paginator usage") + } + + if !reflect.DeepEqual(options, p.paginator.options) || !probablyEqualPageLists(p.paginator.source, seq) { + return nil, errors.New("invoked multiple times with different arguments") + } + + return p.paginator, nil +} + +func resolvePagerSize(cfg config.Provider, options ...interface{}) (int, error) { + if len(options) == 0 { + return cfg.GetInt("paginate"), nil + } + + if len(options) > 1 { + return -1, errors.New("too many arguments, 'pager size' is currently the only option") + } + + pas, err := cast.ToIntE(options[0]) + + if err != nil || pas <= 0 { + return -1, errors.New(("'pager size' must be a positive integer")) + } + + return pas, nil +} + +func paginatePages(td targetPathDescriptor, seq interface{}, pagerSize int) (pagers, error) { + + if pagerSize <= 0 { + return nil, errors.New("'paginate' configuration setting must be positive to paginate") + } + + urlFactory := newPaginationURLFactory(td) + + var paginator *paginator + + if groups, ok := seq.(PagesGroup); ok { + paginator, _ = newPaginatorFromPageGroups(groups, pagerSize, urlFactory) + } else { + pages, err := toPages(seq) + if err != nil { + return nil, err + } + paginator, _ = newPaginatorFromPages(pages, pagerSize, urlFactory) + } + + pagers := paginator.Pagers() + + return pagers, nil +} + +func toPages(seq interface{}) (Pages, error) { + switch seq.(type) { + case Pages: + return seq.(Pages), nil + case *Pages: + return *(seq.(*Pages)), nil + case WeightedPages: + return (seq.(WeightedPages)).Pages(), nil + case PageGroup: + return (seq.(PageGroup)).Pages, nil + default: + return nil, fmt.Errorf("unsupported type in paginate, got %T", seq) + } +} + +// probablyEqual checks page lists for probable equality. +// It may return false positives. +// The motivation behind this is to avoid potential costly reflect.DeepEqual +// when "probably" is good enough. +func probablyEqualPageLists(a1 interface{}, a2 interface{}) bool { + + if a1 == nil || a2 == nil { + return a1 == a2 + } + + t1 := reflect.TypeOf(a1) + t2 := reflect.TypeOf(a2) + + if t1 != t2 { + return false + } + + if g1, ok := a1.(PagesGroup); ok { + g2 := a2.(PagesGroup) + if len(g1) != len(g2) { + return false + } + if len(g1) == 0 { + return true + } + if g1.Len() != g2.Len() { + return false + } + + return g1[0].Pages[0] == g2[0].Pages[0] + } + + p1, err1 := toPages(a1) + p2, err2 := toPages(a2) + + // probably the same wrong type + if err1 != nil && err2 != nil { + return true + } + + if len(p1) != len(p2) { + return false + } + + if len(p1) == 0 { + return true + } + + return p1[0] == p2[0] +} + +func newPaginatorFromPages(pages Pages, size int, urlFactory paginationURLFactory) (*paginator, error) { + + if size <= 0 { + return nil, errors.New("Paginator size must be positive") + } + + split := splitPages(pages, size) + + return newPaginator(split, len(pages), size, urlFactory) +} + +func newPaginatorFromPageGroups(pageGroups PagesGroup, size int, urlFactory paginationURLFactory) (*paginator, error) { + + if size <= 0 { + return nil, errors.New("Paginator size must be positive") + } + + split := splitPageGroups(pageGroups, size) + + return newPaginator(split, pageGroups.Len(), size, urlFactory) +} + +func newPaginator(elements []paginatedElement, total, size int, urlFactory paginationURLFactory) (*paginator, error) { + p := &paginator{total: total, paginatedElements: elements, size: size, paginationURLFactory: urlFactory} + + var ps pagers + + if len(elements) > 0 { + ps = make(pagers, len(elements)) + for i := range p.paginatedElements { + ps[i] = &Pager{number: (i + 1), paginator: p} + } + } else { + ps = make(pagers, 1) + ps[0] = &Pager{number: 1, paginator: p} + } + + p.pagers = ps + + return p, nil +} + +func newPaginationURLFactory(d targetPathDescriptor) paginationURLFactory { + + return func(page int) string { + pathDescriptor := d + var rel string + if page > 1 { + rel = fmt.Sprintf("/%s/%d/", d.PathSpec.PaginatePath(), page) + pathDescriptor.Addends = rel + } + + targetPath := createTargetPath(pathDescriptor) + targetPath = strings.TrimSuffix(targetPath, d.Type.BaseFilename()) + link := d.PathSpec.PrependBasePath(targetPath) + + // Note: The targetPath is massaged with MakePathSanitized + return d.PathSpec.URLizeFilename(link) + } +} diff --git a/hugolib/pagination_test.go b/hugolib/pagination_test.go new file mode 100644 index 000000000..edfac3f3e --- /dev/null +++ b/hugolib/pagination_test.go @@ -0,0 +1,579 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "html/template" + "path/filepath" + "strings" + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/output" + "github.com/stretchr/testify/require" +) + +func TestSplitPages(t *testing.T) { + t.Parallel() + s := newTestSite(t) + + pages := createTestPages(s, 21) + chunks := splitPages(pages, 5) + require.Equal(t, 5, len(chunks)) + + for i := 0; i < 4; i++ { + require.Equal(t, 5, chunks[i].Len()) + } + + lastChunk := chunks[4] + require.Equal(t, 1, lastChunk.Len()) + +} + +func TestSplitPageGroups(t *testing.T) { + t.Parallel() + s := newTestSite(t) + pages := createTestPages(s, 21) + groups, _ := pages.GroupBy("Weight", "desc") + chunks := splitPageGroups(groups, 5) + require.Equal(t, 5, len(chunks)) + + firstChunk := chunks[0] + + // alternate weight 5 and 10 + if groups, ok := firstChunk.(PagesGroup); ok { + require.Equal(t, 5, groups.Len()) + for _, pg := range groups { + // first group 10 in weight + require.Equal(t, 10, pg.Key) + for _, p := range pg.Pages { + require.True(t, p.fuzzyWordCount%2 == 0) // magic test + } + } + } else { + t.Fatal("Excepted PageGroup") + } + + lastChunk := chunks[4] + + if groups, ok := lastChunk.(PagesGroup); ok { + require.Equal(t, 1, groups.Len()) + for _, pg := range groups { + // last should have 5 in weight + require.Equal(t, 5, pg.Key) + for _, p := range pg.Pages { + require.True(t, p.fuzzyWordCount%2 != 0) // magic test + } + } + } else { + t.Fatal("Excepted PageGroup") + } + +} + +func TestPager(t *testing.T) { + t.Parallel() + s := newTestSite(t) + pages := createTestPages(s, 21) + groups, _ := pages.GroupBy("Weight", "desc") + + urlFactory := func(page int) string { + return fmt.Sprintf("page/%d/", page) + } + + _, err := newPaginatorFromPages(pages, -1, urlFactory) + require.NotNil(t, err) + + _, err = newPaginatorFromPageGroups(groups, -1, urlFactory) + require.NotNil(t, err) + + pag, err := newPaginatorFromPages(pages, 5, urlFactory) + require.Nil(t, err) + doTestPages(t, pag) + first := pag.Pagers()[0].First() + require.Equal(t, "Pager 1", first.String()) + require.NotEmpty(t, first.Pages()) + require.Empty(t, first.PageGroups()) + + pag, err = newPaginatorFromPageGroups(groups, 5, urlFactory) + require.Nil(t, err) + doTestPages(t, pag) + first = pag.Pagers()[0].First() + require.NotEmpty(t, first.PageGroups()) + require.Empty(t, first.Pages()) + +} + +func doTestPages(t *testing.T, paginator *paginator) { + + paginatorPages := paginator.Pagers() + + require.Equal(t, 5, len(paginatorPages)) + require.Equal(t, 21, paginator.TotalNumberOfElements()) + require.Equal(t, 5, paginator.PageSize()) + require.Equal(t, 5, paginator.TotalPages()) + + first := paginatorPages[0] + require.Equal(t, template.HTML("page/1/"), first.URL()) + require.Equal(t, first, first.First()) + require.True(t, first.HasNext()) + require.Equal(t, paginatorPages[1], first.Next()) + require.False(t, first.HasPrev()) + require.Nil(t, first.Prev()) + require.Equal(t, 5, first.NumberOfElements()) + require.Equal(t, 1, first.PageNumber()) + + third := paginatorPages[2] + require.True(t, third.HasNext()) + require.True(t, third.HasPrev()) + require.Equal(t, paginatorPages[1], third.Prev()) + + last := paginatorPages[4] + require.Equal(t, template.HTML("page/5/"), last.URL()) + require.Equal(t, last, last.Last()) + require.False(t, last.HasNext()) + require.Nil(t, last.Next()) + require.True(t, last.HasPrev()) + require.Equal(t, 1, last.NumberOfElements()) + require.Equal(t, 5, last.PageNumber()) +} + +func TestPagerNoPages(t *testing.T) { + t.Parallel() + s := newTestSite(t) + pages := createTestPages(s, 0) + groups, _ := pages.GroupBy("Weight", "desc") + + urlFactory := func(page int) string { + return fmt.Sprintf("page/%d/", page) + } + + paginator, _ := newPaginatorFromPages(pages, 5, urlFactory) + doTestPagerNoPages(t, paginator) + + first := paginator.Pagers()[0].First() + require.Empty(t, first.PageGroups()) + require.Empty(t, first.Pages()) + + paginator, _ = newPaginatorFromPageGroups(groups, 5, urlFactory) + doTestPagerNoPages(t, paginator) + + first = paginator.Pagers()[0].First() + require.Empty(t, first.PageGroups()) + require.Empty(t, first.Pages()) + +} + +func doTestPagerNoPages(t *testing.T, paginator *paginator) { + paginatorPages := paginator.Pagers() + + require.Equal(t, 1, len(paginatorPages)) + require.Equal(t, 0, paginator.TotalNumberOfElements()) + require.Equal(t, 5, paginator.PageSize()) + require.Equal(t, 0, paginator.TotalPages()) + + // pageOne should be nothing but the first + pageOne := paginatorPages[0] + require.NotNil(t, pageOne.First()) + require.False(t, pageOne.HasNext()) + require.False(t, pageOne.HasPrev()) + require.Nil(t, pageOne.Next()) + require.Equal(t, 1, len(pageOne.Pagers())) + require.Equal(t, 0, pageOne.Pages().Len()) + require.Equal(t, 0, pageOne.NumberOfElements()) + require.Equal(t, 0, pageOne.TotalNumberOfElements()) + require.Equal(t, 0, pageOne.TotalPages()) + require.Equal(t, 1, pageOne.PageNumber()) + require.Equal(t, 5, pageOne.PageSize()) + +} + +func TestPaginationURLFactory(t *testing.T) { + t.Parallel() + cfg, fs := newTestCfg() + cfg.Set("paginatePath", "zoo") + + for _, uglyURLs := range []bool{false, true} { + for _, canonifyURLs := range []bool{false, true} { + t.Run(fmt.Sprintf("uglyURLs=%t,canonifyURLs=%t", uglyURLs, canonifyURLs), func(t *testing.T) { + + tests := []struct { + name string + d targetPathDescriptor + baseURL string + page int + expected string + }{ + {"HTML home page 32", + targetPathDescriptor{Kind: KindHome, Type: output.HTMLFormat}, "http://example.com/", 32, "/zoo/32/"}, + {"JSON home page 42", + targetPathDescriptor{Kind: KindHome, Type: output.JSONFormat}, "http://example.com/", 42, "/zoo/42/"}, + // Issue #1252 + {"BaseURL with sub path", + targetPathDescriptor{Kind: KindHome, Type: output.HTMLFormat}, "http://example.com/sub/", 999, "/sub/zoo/999/"}, + } + + for _, test := range tests { + d := test.d + cfg.Set("baseURL", test.baseURL) + cfg.Set("canonifyURLs", canonifyURLs) + cfg.Set("uglyURLs", uglyURLs) + d.UglyURLs = uglyURLs + + expected := test.expected + + if canonifyURLs { + expected = strings.Replace(expected, "/sub", "", 1) + } + + if uglyURLs { + expected = expected[:len(expected)-1] + "." + test.d.Type.MediaType.Suffix + } + + pathSpec := newTestPathSpec(fs, cfg) + d.PathSpec = pathSpec + + factory := newPaginationURLFactory(d) + + got := factory(test.page) + + require.Equal(t, expected, got) + + } + }) + } + } +} + +func TestPaginator(t *testing.T) { + t.Parallel() + for _, useViper := range []bool{false, true} { + doTestPaginator(t, useViper) + } +} + +func doTestPaginator(t *testing.T, useViper bool) { + + cfg, fs := newTestCfg() + + pagerSize := 5 + if useViper { + cfg.Set("paginate", pagerSize) + } else { + cfg.Set("paginate", -1) + } + + s, err := NewSiteForCfg(deps.DepsCfg{Cfg: cfg, Fs: fs}) + require.NoError(t, err) + + pages := createTestPages(s, 12) + n1, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + n2, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + n1.Data["Pages"] = pages + + var paginator1 *Pager + + if useViper { + paginator1, err = n1.Paginator() + } else { + paginator1, err = n1.Paginator(pagerSize) + } + + require.Nil(t, err) + require.NotNil(t, paginator1) + require.Equal(t, 3, paginator1.TotalPages()) + require.Equal(t, 12, paginator1.TotalNumberOfElements()) + + n2.paginator = paginator1.Next() + paginator2, err := n2.Paginator() + require.Nil(t, err) + require.Equal(t, paginator2, paginator1.Next()) + + n1.Data["Pages"] = createTestPages(s, 1) + samePaginator, _ := n1.Paginator() + require.Equal(t, paginator1, samePaginator) + + pp, _ := s.NewPage("test") + p, _ := newPageOutput(pp, false, output.HTMLFormat) + + _, err = p.Paginator() + require.NotNil(t, err) +} + +func TestPaginatorWithNegativePaginate(t *testing.T) { + t.Parallel() + s := newTestSite(t, "paginate", -1) + n1, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + _, err := n1.Paginator() + require.Error(t, err) +} + +func TestPaginate(t *testing.T) { + t.Parallel() + for _, useViper := range []bool{false, true} { + doTestPaginate(t, useViper) + } +} + +func TestPaginatorURL(t *testing.T) { + t.Parallel() + cfg, fs := newTestCfg() + + cfg.Set("paginate", 2) + cfg.Set("paginatePath", "testing") + + for i := 0; i < 10; i++ { + // Issue #2177, do not double encode URLs + writeSource(t, fs, filepath.Join("content", "阅读", fmt.Sprintf("page%d.md", (i+1))), + fmt.Sprintf(`--- +title: Page%d +--- +Conten%d +`, (i+1), i+1)) + + } + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), "<html><body>{{.Content}}</body></html>") + writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"), + ` +<html><body> +Count: {{ .Paginator.TotalNumberOfElements }} +Pages: {{ .Paginator.TotalPages }} +{{ range .Paginator.Pagers -}} + {{ .PageNumber }}: {{ .URL }} +{{ end }} +</body></html>`) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + th := testHelper{s.Cfg, s.Fs, t} + + th.assertFileContent(filepath.Join("public", "阅读", "testing", "2", "index.html"), "2: /%E9%98%85%E8%AF%BB/testing/2/") + +} + +func doTestPaginate(t *testing.T, useViper bool) { + pagerSize := 5 + + var ( + s *Site + err error + ) + + if useViper { + s = newTestSite(t, "paginate", pagerSize) + } else { + s = newTestSite(t, "paginate", -1) + } + + pages := createTestPages(s, 6) + n1, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + n2, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + + var paginator1, paginator2 *Pager + + if useViper { + paginator1, err = n1.Paginate(pages) + } else { + paginator1, err = n1.Paginate(pages, pagerSize) + } + + require.Nil(t, err) + require.NotNil(t, paginator1) + require.Equal(t, 2, paginator1.TotalPages()) + require.Equal(t, 6, paginator1.TotalNumberOfElements()) + + n2.paginator = paginator1.Next() + if useViper { + paginator2, err = n2.Paginate(pages) + } else { + paginator2, err = n2.Paginate(pages, pagerSize) + } + require.Nil(t, err) + require.Equal(t, paginator2, paginator1.Next()) + + pp, err := s.NewPage("test") + p, _ := newPageOutput(pp, false, output.HTMLFormat) + + _, err = p.Paginate(pages) + require.NotNil(t, err) +} + +func TestInvalidOptions(t *testing.T) { + t.Parallel() + s := newTestSite(t) + n1, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + + _, err := n1.Paginate(createTestPages(s, 1), 1, 2) + require.NotNil(t, err) + _, err = n1.Paginator(1, 2) + require.NotNil(t, err) + _, err = n1.Paginator(-1) + require.NotNil(t, err) +} + +func TestPaginateWithNegativePaginate(t *testing.T) { + t.Parallel() + cfg, fs := newTestCfg() + cfg.Set("paginate", -1) + + s, err := NewSiteForCfg(deps.DepsCfg{Cfg: cfg, Fs: fs}) + require.NoError(t, err) + + n, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + + _, err = n.Paginate(createTestPages(s, 2)) + require.NotNil(t, err) +} + +func TestPaginatePages(t *testing.T) { + t.Parallel() + s := newTestSite(t) + + groups, _ := createTestPages(s, 31).GroupBy("Weight", "desc") + pd := targetPathDescriptor{Kind: KindHome, Type: output.HTMLFormat, PathSpec: s.PathSpec, Addends: "t"} + + for i, seq := range []interface{}{createTestPages(s, 11), groups, WeightedPages{}, PageGroup{}, &Pages{}} { + v, err := paginatePages(pd, seq, 11) + require.NotNil(t, v, "Val %d", i) + require.Nil(t, err, "Err %d", i) + } + _, err := paginatePages(pd, Site{}, 11) + require.NotNil(t, err) + +} + +// Issue #993 +func TestPaginatorFollowedByPaginateShouldFail(t *testing.T) { + t.Parallel() + s := newTestSite(t, "paginate", 10) + n1, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + n2, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + + _, err := n1.Paginator() + require.Nil(t, err) + _, err = n1.Paginate(createTestPages(s, 2)) + require.NotNil(t, err) + + _, err = n2.Paginate(createTestPages(s, 2)) + require.Nil(t, err) + +} + +func TestPaginateFollowedByDifferentPaginateShouldFail(t *testing.T) { + t.Parallel() + s := newTestSite(t, "paginate", 10) + + n1, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + n2, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + + p1 := createTestPages(s, 2) + p2 := createTestPages(s, 10) + + _, err := n1.Paginate(p1) + require.Nil(t, err) + + _, err = n1.Paginate(p1) + require.Nil(t, err) + + _, err = n1.Paginate(p2) + require.NotNil(t, err) + + _, err = n2.Paginate(p2) + require.Nil(t, err) +} + +func TestProbablyEqualPageLists(t *testing.T) { + t.Parallel() + s := newTestSite(t) + fivePages := createTestPages(s, 5) + zeroPages := createTestPages(s, 0) + zeroPagesByWeight, _ := createTestPages(s, 0).GroupBy("Weight", "asc") + fivePagesByWeight, _ := createTestPages(s, 5).GroupBy("Weight", "asc") + ninePagesByWeight, _ := createTestPages(s, 9).GroupBy("Weight", "asc") + + for i, this := range []struct { + v1 interface{} + v2 interface{} + expect bool + }{ + {nil, nil, true}, + {"a", "b", true}, + {"a", fivePages, false}, + {fivePages, "a", false}, + {fivePages, createTestPages(s, 2), false}, + {fivePages, fivePages, true}, + {zeroPages, zeroPages, true}, + {fivePagesByWeight, fivePagesByWeight, true}, + {zeroPagesByWeight, fivePagesByWeight, false}, + {zeroPagesByWeight, zeroPagesByWeight, true}, + {fivePagesByWeight, fivePages, false}, + {fivePagesByWeight, ninePagesByWeight, false}, + } { + result := probablyEqualPageLists(this.v1, this.v2) + + if result != this.expect { + t.Errorf("[%d] got %t but expected %t", i, result, this.expect) + + } + } +} + +func TestPage(t *testing.T) { + t.Parallel() + urlFactory := func(page int) string { + return fmt.Sprintf("page/%d/", page) + } + + s := newTestSite(t) + + fivePages := createTestPages(s, 7) + fivePagesFuzzyWordCount, _ := createTestPages(s, 7).GroupBy("FuzzyWordCount", "asc") + + p1, _ := newPaginatorFromPages(fivePages, 2, urlFactory) + p2, _ := newPaginatorFromPageGroups(fivePagesFuzzyWordCount, 2, urlFactory) + + f1 := p1.pagers[0].First() + f2 := p2.pagers[0].First() + + page11, _ := f1.page(1) + page1Nil, _ := f1.page(3) + + page21, _ := f2.page(1) + page2Nil, _ := f2.page(3) + + require.Equal(t, 3, page11.fuzzyWordCount) + require.Nil(t, page1Nil) + + require.Equal(t, 3, page21.fuzzyWordCount) + require.Nil(t, page2Nil) +} + +func createTestPages(s *Site, num int) Pages { + pages := make(Pages, num) + + for i := 0; i < num; i++ { + p := s.newPage(filepath.FromSlash(fmt.Sprintf("/x/y/z/p%d.md", i))) + w := 5 + if i%2 == 0 { + w = 10 + } + p.fuzzyWordCount = i + 2 + p.Weight = w + pages[i] = p + + } + + return pages +} diff --git a/hugolib/path_separators_test.go b/hugolib/path_separators_test.go new file mode 100644 index 000000000..3a73869ad --- /dev/null +++ b/hugolib/path_separators_test.go @@ -0,0 +1,38 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "path/filepath" + "strings" + "testing" +) + +var simplePageYAML = `--- +contenttype: "" +--- +Sample Text +` + +func TestDegenerateMissingFolderInPageFilename(t *testing.T) { + t.Parallel() + s := newTestSite(t) + p, err := s.NewPageFrom(strings.NewReader(simplePageYAML), filepath.Join("foobar")) + if err != nil { + t.Fatalf("Error in NewPageFrom") + } + if p.Section() != "" { + t.Fatalf("No section should be set for a file path: foobar") + } +} diff --git a/hugolib/permalinker.go b/hugolib/permalinker.go new file mode 100644 index 000000000..5e7a13a02 --- /dev/null +++ b/hugolib/permalinker.go @@ -0,0 +1,25 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +var ( + _ Permalinker = (*Page)(nil) + _ Permalinker = (*OutputFormat)(nil) +) + +// Permalinker provides permalinks of both the relative and absolute kind. +type Permalinker interface { + Permalink() string + RelPermalink() string +} diff --git a/hugolib/permalinks.go b/hugolib/permalinks.go new file mode 100644 index 000000000..6f26f098a --- /dev/null +++ b/hugolib/permalinks.go @@ -0,0 +1,209 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "errors" + "fmt" + "path" + "regexp" + "strconv" + "strings" +) + +// pathPattern represents a string which builds up a URL from attributes +type pathPattern string + +// pageToPermaAttribute is the type of a function which, given a page and a tag +// can return a string to go in that position in the page (or an error) +type pageToPermaAttribute func(*Page, string) (string, error) + +// PermalinkOverrides maps a section name to a PathPattern +type PermalinkOverrides map[string]pathPattern + +// knownPermalinkAttributes maps :tags in a permalink specification to a +// function which, given a page and the tag, returns the resulting string +// to be used to replace that tag. +var knownPermalinkAttributes map[string]pageToPermaAttribute + +var attributeRegexp *regexp.Regexp + +// validate determines if a PathPattern is well-formed +func (pp pathPattern) validate() bool { + fragments := strings.Split(string(pp[1:]), "/") + var bail = false + for i := range fragments { + if bail { + return false + } + if len(fragments[i]) == 0 { + bail = true + continue + } + + matches := attributeRegexp.FindAllStringSubmatch(fragments[i], -1) + if matches == nil { + continue + } + + for _, match := range matches { + k := strings.ToLower(match[0][1:]) + if _, ok := knownPermalinkAttributes[k]; !ok { + return false + } + } + } + return true +} + +type permalinkExpandError struct { + pattern pathPattern + section string + err error +} + +func (pee *permalinkExpandError) Error() string { + return fmt.Sprintf("error expanding %q section %q: %s", string(pee.pattern), pee.section, pee.err) +} + +var ( + errPermalinkIllFormed = errors.New("permalink ill-formed") + errPermalinkAttributeUnknown = errors.New("permalink attribute not recognised") +) + +// Expand on a PathPattern takes a Page and returns the fully expanded Permalink +// or an error explaining the failure. +func (pp pathPattern) Expand(p *Page) (string, error) { + if !pp.validate() { + return "", &permalinkExpandError{pattern: pp, section: "<all>", err: errPermalinkIllFormed} + } + sections := strings.Split(string(pp), "/") + for i, field := range sections { + if len(field) == 0 { + continue + } + + matches := attributeRegexp.FindAllStringSubmatch(field, -1) + + if matches == nil { + continue + } + + newField := field + + for _, match := range matches { + attr := match[0][1:] + callback, ok := knownPermalinkAttributes[attr] + + if !ok { + return "", &permalinkExpandError{pattern: pp, section: strconv.Itoa(i), err: errPermalinkAttributeUnknown} + } + + newAttr, err := callback(p, attr) + + if err != nil { + return "", &permalinkExpandError{pattern: pp, section: strconv.Itoa(i), err: err} + } + + newField = strings.Replace(newField, match[0], newAttr, 1) + } + + sections[i] = newField + } + return strings.Join(sections, "/"), nil +} + +func pageToPermalinkDate(p *Page, dateField string) (string, error) { + // a Page contains a Node which provides a field Date, time.Time + switch dateField { + case "year": + return strconv.Itoa(p.Date.Year()), nil + case "month": + return fmt.Sprintf("%02d", int(p.Date.Month())), nil + case "monthname": + return p.Date.Month().String(), nil + case "day": + return fmt.Sprintf("%02d", p.Date.Day()), nil + case "weekday": + return strconv.Itoa(int(p.Date.Weekday())), nil + case "weekdayname": + return p.Date.Weekday().String(), nil + case "yearday": + return strconv.Itoa(p.Date.YearDay()), nil + } + //TODO: support classic strftime escapes too + // (and pass those through despite not being in the map) + panic("coding error: should not be here") +} + +// pageToPermalinkTitle returns the URL-safe form of the title +func pageToPermalinkTitle(p *Page, _ string) (string, error) { + // Page contains Node which has Title + // (also contains URLPath which has Slug, sometimes) + return p.s.PathSpec.URLize(p.Title), nil +} + +// pageToPermalinkFilename returns the URL-safe form of the filename +func pageToPermalinkFilename(p *Page, _ string) (string, error) { + //var extension = p.Source.Ext + //var name = p.Source.Path()[0 : len(p.Source.Path())-len(extension)] + return p.s.PathSpec.URLize(p.Source.TranslationBaseName()), nil +} + +// if the page has a slug, return the slug, else return the title +func pageToPermalinkSlugElseTitle(p *Page, a string) (string, error) { + if p.Slug != "" { + // Don't start or end with a - + // TODO(bep) this doesn't look good... Set the Slug once. + if strings.HasPrefix(p.Slug, "-") { + p.Slug = p.Slug[1:len(p.Slug)] + } + + if strings.HasSuffix(p.Slug, "-") { + p.Slug = p.Slug[0 : len(p.Slug)-1] + } + return p.s.PathSpec.URLize(p.Slug), nil + } + return pageToPermalinkTitle(p, a) +} + +func pageToPermalinkSection(p *Page, _ string) (string, error) { + // Page contains Node contains URLPath which has Section + return p.Section(), nil +} + +func pageToPermalinkSections(p *Page, _ string) (string, error) { + // TODO(bep) we have some superflous URLize in this file, but let's + // deal with that later. + return path.Join(p.current().sections...), nil +} + +func init() { + knownPermalinkAttributes = map[string]pageToPermaAttribute{ + "year": pageToPermalinkDate, + "month": pageToPermalinkDate, + "monthname": pageToPermalinkDate, + "day": pageToPermalinkDate, + "weekday": pageToPermalinkDate, + "weekdayname": pageToPermalinkDate, + "yearday": pageToPermalinkDate, + "section": pageToPermalinkSection, + "sections": pageToPermalinkSections, + "title": pageToPermalinkTitle, + "slug": pageToPermalinkSlugElseTitle, + "filename": pageToPermalinkFilename, + } + + attributeRegexp = regexp.MustCompile(`:\w+`) +} diff --git a/hugolib/permalinks_test.go b/hugolib/permalinks_test.go new file mode 100644 index 000000000..7a4bf78c2 --- /dev/null +++ b/hugolib/permalinks_test.go @@ -0,0 +1,94 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "strings" + "testing" +) + +// testdataPermalinks is used by a couple of tests; the expandsTo content is +// subject to the data in SIMPLE_PAGE_JSON. +var testdataPermalinks = []struct { + spec string + valid bool + expandsTo string +}{ + //{"/:year/:month/:title/", true, "/2012/04/spf13-vim-3.0-release-and-new-website/"}, + //{"/:title", true, "/spf13-vim-3.0-release-and-new-website"}, + //{":title", true, "spf13-vim-3.0-release-and-new-website"}, + //{"/blog/:year/:yearday/:title", true, "/blog/2012/97/spf13-vim-3.0-release-and-new-website"}, + {"/:year-:month-:title", true, "/2012-04-spf13-vim-3.0-release-and-new-website"}, + {"/blog/:year-:month-:title", true, "/blog/2012-04-spf13-vim-3.0-release-and-new-website"}, + {"/blog-:year-:month-:title", true, "/blog-2012-04-spf13-vim-3.0-release-and-new-website"}, + //{"/blog/:fred", false, ""}, + //{"/:year//:title", false, ""}, + //{ + //"/:section/:year/:month/:day/:weekdayname/:yearday/:title", + //true, + //"/blue/2012/04/06/Friday/97/spf13-vim-3.0-release-and-new-website", + //}, + //{ + //"/:weekday/:weekdayname/:month/:monthname", + //true, + //"/5/Friday/04/April", + //}, + //{ + //"/:slug/:title", + //true, + //"/spf13-vim-3-0-release-and-new-website/spf13-vim-3.0-release-and-new-website", + //}, +} + +func TestPermalinkValidation(t *testing.T) { + t.Parallel() + for _, item := range testdataPermalinks { + pp := pathPattern(item.spec) + have := pp.validate() + if have == item.valid { + continue + } + var howBad string + if have { + howBad = "validates but should not have" + } else { + howBad = "should have validated but did not" + } + t.Errorf("permlink spec %q %s", item.spec, howBad) + } +} + +func TestPermalinkExpansion(t *testing.T) { + t.Parallel() + s := newTestSite(t) + page, err := s.NewPageFrom(strings.NewReader(simplePageJSON), "blue/test-page.md") + + if err != nil { + t.Fatalf("failed before we began, could not parse SIMPLE_PAGE_JSON: %s", err) + } + for _, item := range testdataPermalinks { + if !item.valid { + continue + } + pp := pathPattern(item.spec) + result, err := pp.Expand(page) + if err != nil { + t.Errorf("failed to expand page: %s", err) + continue + } + if result != item.expandsTo { + t.Errorf("expansion mismatch!\n\tExpected: %q\n\tReceived: %q", item.expandsTo, result) + } + } +} diff --git a/hugolib/robotstxt_test.go b/hugolib/robotstxt_test.go new file mode 100644 index 000000000..03332cbce --- /dev/null +++ b/hugolib/robotstxt_test.go @@ -0,0 +1,46 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "path/filepath" + "testing" + + "github.com/gohugoio/hugo/deps" +) + +const robotTxtTemplate = `User-agent: Googlebot + {{ range .Data.Pages }} + Disallow: {{.RelPermalink}} + {{ end }} +` + +func TestRobotsTXTOutput(t *testing.T) { + t.Parallel() + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + cfg.Set("baseURL", "http://auth/bub/") + cfg.Set("enableRobotsTXT", true) + + writeSource(t, fs, filepath.Join("layouts", "robots.txt"), robotTxtTemplate) + writeSourcesToSource(t, "content", fs, weightedSources...) + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + th.assertFileContent("public/robots.txt", "User-agent: Googlebot") + +} diff --git a/hugolib/rss_test.go b/hugolib/rss_test.go new file mode 100644 index 000000000..268b13073 --- /dev/null +++ b/hugolib/rss_test.go @@ -0,0 +1,59 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/gohugoio/hugo/deps" +) + +func TestRSSOutput(t *testing.T) { + t.Parallel() + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + rssLimit := len(weightedSources) - 1 + + rssURI := "customrss.xml" + + cfg.Set("baseURL", "http://auth/bub/") + cfg.Set("rssURI", rssURI) + cfg.Set("title", "RSSTest") + cfg.Set("rssLimit", rssLimit) + + for _, src := range weightedSources { + writeSource(t, fs, filepath.Join("content", "sect", src.Name), string(src.Content)) + } + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + // Home RSS + th.assertFileContent(filepath.Join("public", rssURI), "<?xml", "rss version", "RSSTest") + // Section RSS + th.assertFileContent(filepath.Join("public", "sect", rssURI), "<?xml", "rss version", "Sects on RSSTest") + // Taxonomy RSS + th.assertFileContent(filepath.Join("public", "categories", "hugo", rssURI), "<?xml", "rss version", "Hugo on RSSTest") + + // RSS Item Limit + content := readDestination(t, fs, filepath.Join("public", rssURI)) + c := strings.Count(content, "<item>") + if c != rssLimit { + t.Errorf("incorrect RSS item count: expected %d, got %d", rssLimit, c) + } +} diff --git a/hugolib/scratch.go b/hugolib/scratch.go new file mode 100644 index 000000000..ca2c9d6a8 --- /dev/null +++ b/hugolib/scratch.go @@ -0,0 +1,127 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "reflect" + "sort" + "sync" + + "github.com/gohugoio/hugo/tpl/math" +) + +// Scratch is a writable context used for stateful operations in Page/Node rendering. +type Scratch struct { + values map[string]interface{} + mu sync.RWMutex +} + +// Add will, for single values, add (using the + operator) the addend to the existing addend (if found). +// Supports numeric values and strings. +// +// If the first add for a key is an array or slice, then the next value(s) will be appended. +func (c *Scratch) Add(key string, newAddend interface{}) (string, error) { + + var newVal interface{} + c.mu.RLock() + existingAddend, found := c.values[key] + c.mu.RUnlock() + if found { + var err error + + addendV := reflect.ValueOf(existingAddend) + + if addendV.Kind() == reflect.Slice || addendV.Kind() == reflect.Array { + nav := reflect.ValueOf(newAddend) + if nav.Kind() == reflect.Slice || nav.Kind() == reflect.Array { + newVal = reflect.AppendSlice(addendV, nav).Interface() + } else { + newVal = reflect.Append(addendV, nav).Interface() + } + } else { + newVal, err = math.DoArithmetic(existingAddend, newAddend, '+') + if err != nil { + return "", err + } + } + } else { + newVal = newAddend + } + c.mu.Lock() + c.values[key] = newVal + c.mu.Unlock() + return "", nil // have to return something to make it work with the Go templates +} + +// Set stores a value with the given key in the Node context. +// This value can later be retrieved with Get. +func (c *Scratch) Set(key string, value interface{}) string { + c.mu.Lock() + c.values[key] = value + c.mu.Unlock() + return "" +} + +// Get returns a value previously set by Add or Set +func (c *Scratch) Get(key string) interface{} { + c.mu.RLock() + val := c.values[key] + c.mu.RUnlock() + + return val +} + +// SetInMap stores a value to a map with the given key in the Node context. +// This map can later be retrieved with GetSortedMapValues. +func (c *Scratch) SetInMap(key string, mapKey string, value interface{}) string { + c.mu.Lock() + _, found := c.values[key] + if !found { + c.values[key] = make(map[string]interface{}) + } + + c.values[key].(map[string]interface{})[mapKey] = value + c.mu.Unlock() + return "" +} + +// GetSortedMapValues returns a sorted map previously filled with SetInMap +func (c *Scratch) GetSortedMapValues(key string) interface{} { + c.mu.RLock() + + if c.values[key] == nil { + c.mu.RUnlock() + return nil + } + + unsortedMap := c.values[key].(map[string]interface{}) + c.mu.RUnlock() + var keys []string + for mapKey := range unsortedMap { + keys = append(keys, mapKey) + } + + sort.Strings(keys) + + sortedArray := make([]interface{}, len(unsortedMap)) + for i, mapKey := range keys { + sortedArray[i] = unsortedMap[mapKey] + } + + return sortedArray +} + +func newScratch() *Scratch { + return &Scratch{values: make(map[string]interface{})} +} diff --git a/hugolib/scratch_test.go b/hugolib/scratch_test.go new file mode 100644 index 000000000..f65c2ddfe --- /dev/null +++ b/hugolib/scratch_test.go @@ -0,0 +1,161 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "reflect" + "sync" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestScratchAdd(t *testing.T) { + t.Parallel() + scratch := newScratch() + scratch.Add("int1", 10) + scratch.Add("int1", 20) + scratch.Add("int2", 20) + + assert.Equal(t, int64(30), scratch.Get("int1")) + assert.Equal(t, 20, scratch.Get("int2")) + + scratch.Add("float1", float64(10.5)) + scratch.Add("float1", float64(20.1)) + + assert.Equal(t, float64(30.6), scratch.Get("float1")) + + scratch.Add("string1", "Hello ") + scratch.Add("string1", "big ") + scratch.Add("string1", "World!") + + assert.Equal(t, "Hello big World!", scratch.Get("string1")) + + scratch.Add("scratch", scratch) + _, err := scratch.Add("scratch", scratch) + + if err == nil { + t.Errorf("Expected error from invalid arithmetic") + } + +} + +func TestScratchAddSlice(t *testing.T) { + t.Parallel() + scratch := newScratch() + + _, err := scratch.Add("intSlice", []int{1, 2}) + assert.Nil(t, err) + _, err = scratch.Add("intSlice", 3) + assert.Nil(t, err) + + sl := scratch.Get("intSlice") + expected := []int{1, 2, 3} + + if !reflect.DeepEqual(expected, sl) { + t.Errorf("Slice difference, go %q expected %q", sl, expected) + } + + _, err = scratch.Add("intSlice", []int{4, 5}) + + assert.Nil(t, err) + + sl = scratch.Get("intSlice") + expected = []int{1, 2, 3, 4, 5} + + if !reflect.DeepEqual(expected, sl) { + t.Errorf("Slice difference, go %q expected %q", sl, expected) + } + +} + +func TestScratchSet(t *testing.T) { + t.Parallel() + scratch := newScratch() + scratch.Set("key", "val") + assert.Equal(t, "val", scratch.Get("key")) +} + +// Issue #2005 +func TestScratchInParallel(t *testing.T) { + var wg sync.WaitGroup + scratch := newScratch() + key := "counter" + scratch.Set(key, int64(1)) + for i := 1; i <= 10; i++ { + wg.Add(1) + go func(j int) { + for k := 0; k < 10; k++ { + newVal := int64(k + j) + + _, err := scratch.Add(key, newVal) + if err != nil { + t.Errorf("Got err %s", err) + } + + scratch.Set(key, newVal) + + val := scratch.Get(key) + + if counter, ok := val.(int64); ok { + if counter < 1 { + t.Errorf("Got %d", counter) + } + } else { + t.Errorf("Got %T", val) + } + } + wg.Done() + }(i) + } + wg.Wait() +} + +func TestScratchGet(t *testing.T) { + t.Parallel() + scratch := newScratch() + nothing := scratch.Get("nothing") + if nothing != nil { + t.Errorf("Should not return anything, but got %v", nothing) + } +} + +func TestScratchSetInMap(t *testing.T) { + t.Parallel() + scratch := newScratch() + scratch.SetInMap("key", "lux", "Lux") + scratch.SetInMap("key", "abc", "Abc") + scratch.SetInMap("key", "zyx", "Zyx") + scratch.SetInMap("key", "abc", "Abc (updated)") + scratch.SetInMap("key", "def", "Def") + assert.Equal(t, []interface{}{0: "Abc (updated)", 1: "Def", 2: "Lux", 3: "Zyx"}, scratch.GetSortedMapValues("key")) +} + +func TestScratchGetSortedMapValues(t *testing.T) { + t.Parallel() + scratch := newScratch() + nothing := scratch.GetSortedMapValues("nothing") + if nothing != nil { + t.Errorf("Should not return anything, but got %v", nothing) + } +} + +func BenchmarkScratchGet(b *testing.B) { + scratch := newScratch() + scratch.Add("A", 1) + b.ResetTimer() + for i := 0; i < b.N; i++ { + scratch.Get("A") + } +} diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go new file mode 100644 index 000000000..150d82c44 --- /dev/null +++ b/hugolib/shortcode.go @@ -0,0 +1,716 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "bytes" + "errors" + "fmt" + "html/template" + "reflect" + "regexp" + "sort" + "strings" + "sync" + + "github.com/gohugoio/hugo/output" + + "github.com/gohugoio/hugo/media" + + bp "github.com/gohugoio/hugo/bufferpool" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/tpl" +) + +// ShortcodeWithPage is the "." context in a shortcode template. +type ShortcodeWithPage struct { + Params interface{} + Inner template.HTML + Page *Page + Parent *ShortcodeWithPage + IsNamedParams bool + scratch *Scratch +} + +// Site returns information about the current site. +func (scp *ShortcodeWithPage) Site() *SiteInfo { + return scp.Page.Site +} + +// Ref is a shortcut to the Ref method on Page. +func (scp *ShortcodeWithPage) Ref(ref string) (string, error) { + return scp.Page.Ref(ref) +} + +// RelRef is a shortcut to the RelRef method on Page. +func (scp *ShortcodeWithPage) RelRef(ref string) (string, error) { + return scp.Page.RelRef(ref) +} + +// Scratch returns a scratch-pad scoped for this shortcode. This can be used +// as a temporary storage for variables, counters etc. +func (scp *ShortcodeWithPage) Scratch() *Scratch { + if scp.scratch == nil { + scp.scratch = newScratch() + } + return scp.scratch +} + +// Get is a convenience method to look up shortcode parameters by its key. +func (scp *ShortcodeWithPage) Get(key interface{}) interface{} { + if scp.Params == nil { + return nil + } + if reflect.ValueOf(scp.Params).Len() == 0 { + return nil + } + + var x reflect.Value + + switch key.(type) { + case int64, int32, int16, int8, int: + if reflect.TypeOf(scp.Params).Kind() == reflect.Map { + return "error: cannot access named params by position" + } else if reflect.TypeOf(scp.Params).Kind() == reflect.Slice { + idx := int(reflect.ValueOf(key).Int()) + ln := reflect.ValueOf(scp.Params).Len() + if idx > ln-1 { + helpers.DistinctErrorLog.Printf("No shortcode param at .Get %d in page %s, have params: %v", idx, scp.Page.FullFilePath(), scp.Params) + return fmt.Sprintf("error: index out of range for positional param at position %d", idx) + } + x = reflect.ValueOf(scp.Params).Index(idx) + } + case string: + if reflect.TypeOf(scp.Params).Kind() == reflect.Map { + x = reflect.ValueOf(scp.Params).MapIndex(reflect.ValueOf(key)) + if !x.IsValid() { + return "" + } + } else if reflect.TypeOf(scp.Params).Kind() == reflect.Slice { + if reflect.ValueOf(scp.Params).Len() == 1 && reflect.ValueOf(scp.Params).Index(0).String() == "" { + return nil + } + return "error: cannot access positional params by string name" + } + } + + switch x.Kind() { + case reflect.String: + return x.String() + case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int: + return x.Int() + default: + return x + } + +} + +// Note - this value must not contain any markup syntax +const shortcodePlaceholderPrefix = "HUGOSHORTCODE" + +type shortcode struct { + name string + inner []interface{} // string or nested shortcode + params interface{} // map or array + err error + doMarkup bool +} + +func (sc shortcode) String() string { + // for testing (mostly), so any change here will break tests! + var params interface{} + switch v := sc.params.(type) { + case map[string]string: + // sort the keys so test assertions won't fail + var keys []string + for k := range v { + keys = append(keys, k) + } + sort.Strings(keys) + var tmp = make([]string, len(keys)) + + for i, k := range keys { + tmp[i] = k + ":" + v[k] + } + params = tmp + + default: + // use it as is + params = sc.params + } + + return fmt.Sprintf("%s(%q, %t){%s}", sc.name, params, sc.doMarkup, sc.inner) +} + +// We may have special shortcode templates for AMP etc. +// Note that in the below, OutputFormat may be empty. +// We will try to look for the most specific shortcode template available. +type scKey struct { + OutputFormat string + Suffix string + ShortcodePlaceholder string +} + +func newScKey(m media.Type, shortcodeplaceholder string) scKey { + return scKey{Suffix: m.Suffix, ShortcodePlaceholder: shortcodeplaceholder} +} + +func newScKeyFromOutputFormat(o output.Format, shortcodeplaceholder string) scKey { + return scKey{Suffix: o.MediaType.Suffix, OutputFormat: o.Name, ShortcodePlaceholder: shortcodeplaceholder} +} + +func newDefaultScKey(shortcodeplaceholder string) scKey { + return newScKey(media.HTMLType, shortcodeplaceholder) +} + +type shortcodeHandler struct { + init sync.Once + + p *Page + + // This is all shortcode rendering funcs for all potential output formats. + contentShortcodes map[scKey]func() (string, error) + + // This map contains the new or changed set of shortcodes that need + // to be rendered for the current output format. + contentShortcodesDelta map[scKey]func() (string, error) + + // This maps the shorcode placeholders with the rendered content. + // We will do (potential) partial re-rendering per output format, + // so keep this for the unchanged. + renderedShortcodes map[string]string + + // Maps the shortcodeplaceholder with the actual shortcode. + shortcodes map[string]shortcode + + // All the shortcode names in this set. + nameSet map[string]bool +} + +func newShortcodeHandler(p *Page) *shortcodeHandler { + return &shortcodeHandler{ + p: p, + contentShortcodes: make(map[scKey]func() (string, error)), + shortcodes: make(map[string]shortcode), + nameSet: make(map[string]bool), + renderedShortcodes: make(map[string]string), + } +} + +// TODO(bep) make it non-global +var isInnerShortcodeCache = struct { + sync.RWMutex + m map[string]bool +}{m: make(map[string]bool)} + +// to avoid potential costly look-aheads for closing tags we look inside the template itself +// we could change the syntax to self-closing tags, but that would make users cry +// the value found is cached +func isInnerShortcode(t tpl.TemplateExecutor) (bool, error) { + isInnerShortcodeCache.RLock() + m, ok := isInnerShortcodeCache.m[t.Name()] + isInnerShortcodeCache.RUnlock() + + if ok { + return m, nil + } + + isInnerShortcodeCache.Lock() + defer isInnerShortcodeCache.Unlock() + match, _ := regexp.MatchString("{{.*?\\.Inner.*?}}", t.Tree()) + isInnerShortcodeCache.m[t.Name()] = match + + return match, nil +} + +func clearIsInnerShortcodeCache() { + isInnerShortcodeCache.Lock() + defer isInnerShortcodeCache.Unlock() + isInnerShortcodeCache.m = make(map[string]bool) +} + +func createShortcodePlaceholder(id int) string { + return fmt.Sprintf("HAHA%s-%dHBHB", shortcodePlaceholderPrefix, id) +} + +const innerNewlineRegexp = "\n" +const innerCleanupRegexp = `\A<p>(.*)</p>\n\z` +const innerCleanupExpand = "$1" + +func prepareShortcodeForPage(placeholder string, sc shortcode, parent *ShortcodeWithPage, p *Page) map[scKey]func() (string, error) { + + m := make(map[scKey]func() (string, error)) + + for _, f := range p.outputFormats { + // The most specific template will win. + key := newScKeyFromOutputFormat(f, placeholder) + m[key] = func() (string, error) { + return renderShortcode(key, sc, nil, p), nil + } + } + + return m +} + +func renderShortcode( + tmplKey scKey, + sc shortcode, + parent *ShortcodeWithPage, + p *Page) string { + + tmpl := getShortcodeTemplateForTemplateKey(tmplKey, sc.name, p.s.Tmpl) + if tmpl == nil { + p.s.Log.ERROR.Printf("Unable to locate template for shortcode %q in page %q", sc.name, p.Path()) + return "" + } + + data := &ShortcodeWithPage{Params: sc.params, Page: p, Parent: parent} + if sc.params != nil { + data.IsNamedParams = reflect.TypeOf(sc.params).Kind() == reflect.Map + } + + if len(sc.inner) > 0 { + var inner string + for _, innerData := range sc.inner { + switch innerData.(type) { + case string: + inner += innerData.(string) + case shortcode: + inner += renderShortcode(tmplKey, innerData.(shortcode), data, p) + default: + p.s.Log.ERROR.Printf("Illegal state on shortcode rendering of %q in page %q. Illegal type in inner data: %s ", + sc.name, p.Path(), reflect.TypeOf(innerData)) + return "" + } + } + + if sc.doMarkup { + newInner := p.s.ContentSpec.RenderBytes(&helpers.RenderingContext{ + Content: []byte(inner), PageFmt: p.determineMarkupType(), + Cfg: p.Language(), + DocumentID: p.UniqueID(), + DocumentName: p.Path(), + Config: p.getRenderingConfig()}) + + // If the type is “unknown” or “markdown”, we assume the markdown + // generation has been performed. Given the input: `a line`, markdown + // specifies the HTML `<p>a line</p>\n`. When dealing with documents as a + // whole, this is OK. When dealing with an `{{ .Inner }}` block in Hugo, + // this is not so good. This code does two things: + // + // 1. Check to see if inner has a newline in it. If so, the Inner data is + // unchanged. + // 2 If inner does not have a newline, strip the wrapping <p> block and + // the newline. This was previously tricked out by wrapping shortcode + // substitutions in <div>HUGOSHORTCODE-1</div> which prevents the + // generation, but means that you can’t use shortcodes inside of + // markdown structures itself (e.g., `[foo]({{% ref foo.md %}})`). + switch p.determineMarkupType() { + case "unknown", "markdown": + if match, _ := regexp.MatchString(innerNewlineRegexp, inner); !match { + cleaner, err := regexp.Compile(innerCleanupRegexp) + + if err == nil { + newInner = cleaner.ReplaceAll(newInner, []byte(innerCleanupExpand)) + } + } + } + + // TODO(bep) we may have plain text inner templates. + data.Inner = template.HTML(newInner) + } else { + data.Inner = template.HTML(inner) + } + + } + + return renderShortcodeWithPage(tmpl, data) +} + +// The delta represents new output format-versions of the shortcodes, +// which, combined with the ones that do not have alternative representations, +// builds a complete set ready for a full rebuild of the Page content. +// This method returns false if there are no new shortcode variants in the +// current rendering context's output format. This mean we can safely reuse +// the content from the previous output format, if any. +func (s *shortcodeHandler) updateDelta() bool { + s.init.Do(func() { + s.contentShortcodes = createShortcodeRenderers(s.shortcodes, s.p) + }) + + contentShortcodes := s.contentShortcodesForOutputFormat(s.p.s.rc.Format) + + if s.contentShortcodesDelta == nil || len(s.contentShortcodesDelta) == 0 { + s.contentShortcodesDelta = contentShortcodes + return true + } + + delta := make(map[scKey]func() (string, error)) + + for k, v := range contentShortcodes { + if _, found := s.contentShortcodesDelta[k]; !found { + delta[k] = v + } + } + + s.contentShortcodesDelta = delta + + return len(delta) > 0 +} + +func (s *shortcodeHandler) contentShortcodesForOutputFormat(f output.Format) map[scKey]func() (string, error) { + contentShortcodesForOuputFormat := make(map[scKey]func() (string, error)) + for shortcodePlaceholder := range s.shortcodes { + + key := newScKeyFromOutputFormat(f, shortcodePlaceholder) + renderFn, found := s.contentShortcodes[key] + + if !found { + key.OutputFormat = "" + renderFn, found = s.contentShortcodes[key] + } + + // Fall back to HTML + if !found && key.Suffix != "html" { + key.Suffix = "html" + renderFn, found = s.contentShortcodes[key] + } + + if !found { + panic(fmt.Sprintf("Shortcode %q could not be found", shortcodePlaceholder)) + } + contentShortcodesForOuputFormat[newScKeyFromOutputFormat(f, shortcodePlaceholder)] = renderFn + } + + return contentShortcodesForOuputFormat +} + +func (s *shortcodeHandler) executeShortcodesForDelta(p *Page) error { + + for k, render := range s.contentShortcodesDelta { + renderedShortcode, err := render() + if err != nil { + return fmt.Errorf("Failed to execute shortcode in page %q: %s", p.Path(), err) + } + + s.renderedShortcodes[k.ShortcodePlaceholder] = renderedShortcode + } + + return nil + +} + +func createShortcodeRenderers(shortcodes map[string]shortcode, p *Page) map[scKey]func() (string, error) { + + shortcodeRenderers := make(map[scKey]func() (string, error)) + + for k, v := range shortcodes { + prepared := prepareShortcodeForPage(k, v, nil, p) + for kk, vv := range prepared { + shortcodeRenderers[kk] = vv + } + } + + return shortcodeRenderers +} + +var errShortCodeIllegalState = errors.New("Illegal shortcode state") + +// pageTokens state: +// - before: positioned just before the shortcode start +// - after: shortcode(s) consumed (plural when they are nested) +func (s *shortcodeHandler) extractShortcode(pt *pageTokens, p *Page) (shortcode, error) { + sc := shortcode{} + var isInner = false + + var currItem item + var cnt = 0 + +Loop: + for { + currItem = pt.next() + + switch currItem.typ { + case tLeftDelimScWithMarkup, tLeftDelimScNoMarkup: + next := pt.peek() + if next.typ == tScClose { + continue + } + + if cnt > 0 { + // nested shortcode; append it to inner content + pt.backup3(currItem, next) + nested, err := s.extractShortcode(pt, p) + if nested.name != "" { + s.nameSet[nested.name] = true + } + if err == nil { + sc.inner = append(sc.inner, nested) + } else { + return sc, err + } + + } else { + sc.doMarkup = currItem.typ == tLeftDelimScWithMarkup + } + + cnt++ + + case tRightDelimScWithMarkup, tRightDelimScNoMarkup: + // we trust the template on this: + // if there's no inner, we're done + if !isInner { + return sc, nil + } + + case tScClose: + next := pt.peek() + if !isInner { + if next.typ == tError { + // return that error, more specific + continue + } + return sc, fmt.Errorf("Shortcode '%s' in page '%s' has no .Inner, yet a closing tag was provided", next.val, p.FullFilePath()) + } + if next.typ == tRightDelimScWithMarkup || next.typ == tRightDelimScNoMarkup { + // self-closing + pt.consume(1) + } else { + pt.consume(2) + } + + return sc, nil + case tText: + sc.inner = append(sc.inner, currItem.val) + case tScName: + sc.name = currItem.val + // We pick the first template for an arbitrary output format + // if more than one. It is "all inner or no inner". + tmpl := getShortcodeTemplateForTemplateKey(scKey{}, sc.name, p.s.Tmpl) + if tmpl == nil { + return sc, fmt.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.Path()) + } + + var err error + isInner, err = isInnerShortcode(tmpl) + if err != nil { + return sc, fmt.Errorf("Failed to handle template for shortcode %q for page %q: %s", sc.name, p.Path(), err) + } + + case tScParam: + if !pt.isValueNext() { + continue + } else if pt.peek().typ == tScParamVal { + // named params + if sc.params == nil { + params := make(map[string]string) + params[currItem.val] = pt.next().val + sc.params = params + } else { + if params, ok := sc.params.(map[string]string); ok { + params[currItem.val] = pt.next().val + } else { + return sc, errShortCodeIllegalState + } + + } + } else { + // positional params + if sc.params == nil { + var params []string + params = append(params, currItem.val) + sc.params = params + } else { + if params, ok := sc.params.([]string); ok { + params = append(params, currItem.val) + sc.params = params + } else { + return sc, errShortCodeIllegalState + } + + } + } + + case tError, tEOF: + // handled by caller + pt.backup() + break Loop + + } + } + return sc, nil +} + +func (s *shortcodeHandler) extractShortcodes(stringToParse string, p *Page) (string, error) { + + startIdx := strings.Index(stringToParse, "{{") + + // short cut for docs with no shortcodes + if startIdx < 0 { + return stringToParse, nil + } + + // the parser takes a string; + // since this is an internal API, it could make sense to use the mutable []byte all the way, but + // it seems that the time isn't really spent in the byte copy operations, and the impl. gets a lot cleaner + pt := &pageTokens{lexer: newShortcodeLexer("parse-page", stringToParse, pos(startIdx))} + + id := 1 // incremented id, will be appended onto temp. shortcode placeholders + + result := bp.GetBuffer() + defer bp.PutBuffer(result) + //var result bytes.Buffer + + // the parser is guaranteed to return items in proper order or fail, so … + // … it's safe to keep some "global" state + var currItem item + var currShortcode shortcode + +Loop: + for { + currItem = pt.next() + + switch currItem.typ { + case tText: + result.WriteString(currItem.val) + case tLeftDelimScWithMarkup, tLeftDelimScNoMarkup: + // let extractShortcode handle left delim (will do so recursively) + pt.backup() + + currShortcode, err := s.extractShortcode(pt, p) + + if currShortcode.name != "" { + s.nameSet[currShortcode.name] = true + } + + if err != nil { + return result.String(), err + } + + if currShortcode.params == nil { + currShortcode.params = make([]string, 0) + } + + placeHolder := createShortcodePlaceholder(id) + result.WriteString(placeHolder) + s.shortcodes[placeHolder] = currShortcode + id++ + case tEOF: + break Loop + case tError: + err := fmt.Errorf("%s:%d: %s", + p.FullFilePath(), (p.lineNumRawContentStart() + pt.lexer.lineNum() - 1), currItem) + currShortcode.err = err + return result.String(), err + } + } + + return result.String(), nil + +} + +// Replace prefixed shortcode tokens (HUGOSHORTCODE-1, HUGOSHORTCODE-2) with the real content. +// Note: This function will rewrite the input slice. +func replaceShortcodeTokens(source []byte, prefix string, replacements map[string]string) ([]byte, error) { + + if len(replacements) == 0 { + return source, nil + } + + sourceLen := len(source) + start := 0 + + pre := []byte("HAHA" + prefix) + post := []byte("HBHB") + pStart := []byte("<p>") + pEnd := []byte("</p>") + + k := bytes.Index(source[start:], pre) + + for k != -1 { + j := start + k + postIdx := bytes.Index(source[j:], post) + if postIdx < 0 { + // this should never happen, but let the caller decide to panic or not + return nil, errors.New("illegal state in content; shortcode token missing end delim") + } + + end := j + postIdx + 4 + + newVal := []byte(replacements[string(source[j:end])]) + + // Issue #1148: Check for wrapping p-tags <p> + if j >= 3 && bytes.Equal(source[j-3:j], pStart) { + if (k+4) < sourceLen && bytes.Equal(source[end:end+4], pEnd) { + j -= 3 + end += 4 + } + } + + // This and other cool slice tricks: https://github.com/golang/go/wiki/SliceTricks + source = append(source[:j], append(newVal, source[end:]...)...) + start = j + k = bytes.Index(source[start:], pre) + + } + + return source, nil +} + +func getShortcodeTemplateForTemplateKey(key scKey, shortcodeName string, t tpl.TemplateFinder) *tpl.TemplateAdapter { + isInnerShortcodeCache.RLock() + defer isInnerShortcodeCache.RUnlock() + + var names []string + + suffix := strings.ToLower(key.Suffix) + outFormat := strings.ToLower(key.OutputFormat) + + if outFormat != "" && suffix != "" { + names = append(names, fmt.Sprintf("%s.%s.%s", shortcodeName, outFormat, suffix)) + } + + if suffix != "" { + names = append(names, fmt.Sprintf("%s.%s", shortcodeName, suffix)) + } + + names = append(names, shortcodeName) + + for _, name := range names { + + if x := t.Lookup("shortcodes/" + name); x != nil { + return x + } + if x := t.Lookup("theme/shortcodes/" + name); x != nil { + return x + } + if x := t.Lookup("_internal/shortcodes/" + name); x != nil { + return x + } + } + return nil +} + +func renderShortcodeWithPage(tmpl tpl.Template, data *ShortcodeWithPage) string { + buffer := bp.GetBuffer() + defer bp.PutBuffer(buffer) + + isInnerShortcodeCache.RLock() + err := tmpl.Execute(buffer, data) + isInnerShortcodeCache.RUnlock() + if err != nil { + data.Page.s.Log.ERROR.Printf("error processing shortcode %q for page %q: %s", tmpl.Name(), data.Page.Path(), err) + } + return buffer.String() +} diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go new file mode 100644 index 000000000..42da9e465 --- /dev/null +++ b/hugolib/shortcode_test.go @@ -0,0 +1,845 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "path/filepath" + "reflect" + "regexp" + "sort" + "strings" + "testing" + + jww "github.com/spf13/jwalterweatherman" + + "github.com/spf13/afero" + + "github.com/gohugoio/hugo/output" + + "github.com/gohugoio/hugo/media" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/source" + "github.com/gohugoio/hugo/tpl" + "github.com/stretchr/testify/require" +) + +// TODO(bep) remove +func pageFromString(in, filename string, withTemplate ...func(templ tpl.TemplateHandler) error) (*Page, error) { + s := newTestSite(nil) + if len(withTemplate) > 0 { + // Have to create a new site + var err error + cfg, fs := newTestCfg() + + d := deps.DepsCfg{Language: helpers.NewLanguage("en", cfg), Cfg: cfg, Fs: fs, WithTemplate: withTemplate[0]} + + s, err = NewSiteForCfg(d) + if err != nil { + return nil, err + } + } + return s.NewPageFrom(strings.NewReader(in), filename) +} + +func CheckShortCodeMatch(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error) { + CheckShortCodeMatchAndError(t, input, expected, withTemplate, false) +} + +func CheckShortCodeMatchAndError(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error, expectError bool) { + + cfg, fs := newTestCfg() + + // Need some front matter, see https://github.com/gohugoio/hugo/issues/2337 + contentFile := `--- +title: "Title" +--- +` + input + + writeSource(t, fs, "content/simple.md", contentFile) + + h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg, WithTemplate: withTemplate}) + + require.NoError(t, err) + require.Len(t, h.Sites, 1) + + err = h.Build(BuildCfg{}) + + if err != nil && !expectError { + t.Fatalf("Shortcode rendered error %s.", err) + } + + if err == nil && expectError { + t.Fatalf("No error from shortcode") + } + + require.Len(t, h.Sites[0].RegularPages, 1) + + output := strings.TrimSpace(string(h.Sites[0].RegularPages[0].Content)) + output = strings.TrimPrefix(output, "<p>") + output = strings.TrimSuffix(output, "</p>") + + expected = strings.TrimSpace(expected) + + if output != expected { + t.Fatalf("Shortcode render didn't match. got \n%q but expected \n%q", output, expected) + } +} + +func TestNonSC(t *testing.T) { + t.Parallel() + // notice the syntax diff from 0.12, now comment delims must be added + CheckShortCodeMatch(t, "{{%/* movie 47238zzb */%}}", "{{% movie 47238zzb %}}", nil) +} + +// Issue #929 +func TestHyphenatedSC(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + + tem.AddTemplate("_internal/shortcodes/hyphenated-video.html", `Playing Video {{ .Get 0 }}`) + return nil + } + + CheckShortCodeMatch(t, "{{< hyphenated-video 47238zzb >}}", "Playing Video 47238zzb", wt) +} + +// Issue #1753 +func TestNoTrailingNewline(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/a.html", `{{ .Get 0 }}`) + return nil + } + + CheckShortCodeMatch(t, "ab{{< a c >}}d", "abcd", wt) +} + +func TestPositionalParamSC(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/video.html", `Playing Video {{ .Get 0 }}`) + return nil + } + + CheckShortCodeMatch(t, "{{< video 47238zzb >}}", "Playing Video 47238zzb", wt) + CheckShortCodeMatch(t, "{{< video 47238zzb 132 >}}", "Playing Video 47238zzb", wt) + CheckShortCodeMatch(t, "{{<video 47238zzb>}}", "Playing Video 47238zzb", wt) + CheckShortCodeMatch(t, "{{<video 47238zzb >}}", "Playing Video 47238zzb", wt) + CheckShortCodeMatch(t, "{{< video 47238zzb >}}", "Playing Video 47238zzb", wt) +} + +func TestPositionalParamIndexOutOfBounds(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/video.html", `Playing Video {{ .Get 1 }}`) + return nil + } + CheckShortCodeMatch(t, "{{< video 47238zzb >}}", "Playing Video error: index out of range for positional param at position 1", wt) +} + +// some repro issues for panics in Go Fuzz testing + +func TestNamedParamSC(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/img.html", `<img{{ with .Get "src" }} src="{{.}}"{{end}}{{with .Get "class"}} class="{{.}}"{{end}}>`) + return nil + } + CheckShortCodeMatch(t, `{{< img src="one" >}}`, `<img src="one">`, wt) + CheckShortCodeMatch(t, `{{< img class="aspen" >}}`, `<img class="aspen">`, wt) + CheckShortCodeMatch(t, `{{< img src= "one" >}}`, `<img src="one">`, wt) + CheckShortCodeMatch(t, `{{< img src ="one" >}}`, `<img src="one">`, wt) + CheckShortCodeMatch(t, `{{< img src = "one" >}}`, `<img src="one">`, wt) + CheckShortCodeMatch(t, `{{< img src = "one" class = "aspen grove" >}}`, `<img src="one" class="aspen grove">`, wt) +} + +// Issue #2294 +func TestNestedNamedMissingParam(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/acc.html", `<div class="acc">{{ .Inner }}</div>`) + tem.AddTemplate("_internal/shortcodes/div.html", `<div {{with .Get "class"}} class="{{ . }}"{{ end }}>{{ .Inner }}</div>`) + tem.AddTemplate("_internal/shortcodes/div2.html", `<div {{with .Get 0}} class="{{ . }}"{{ end }}>{{ .Inner }}</div>`) + return nil + } + CheckShortCodeMatch(t, + `{{% acc %}}{{% div %}}d1{{% /div %}}{{% div2 %}}d2{{% /div2 %}}{{% /acc %}}`, + "<div class=\"acc\"><div >d1</div><div >d2</div>\n</div>", wt) +} + +func TestIsNamedParamsSC(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/byposition.html", `<div id="{{ .Get 0 }}">`) + tem.AddTemplate("_internal/shortcodes/byname.html", `<div id="{{ .Get "id" }}">`) + tem.AddTemplate("_internal/shortcodes/ifnamedparams.html", `<div id="{{ if .IsNamedParams }}{{ .Get "id" }}{{ else }}{{ .Get 0 }}{{end}}">`) + return nil + } + CheckShortCodeMatch(t, `{{< ifnamedparams id="name" >}}`, `<div id="name">`, wt) + CheckShortCodeMatch(t, `{{< ifnamedparams position >}}`, `<div id="position">`, wt) + CheckShortCodeMatch(t, `{{< byname id="name" >}}`, `<div id="name">`, wt) + CheckShortCodeMatch(t, `{{< byname position >}}`, `<div id="error: cannot access positional params by string name">`, wt) + CheckShortCodeMatch(t, `{{< byposition position >}}`, `<div id="position">`, wt) + CheckShortCodeMatch(t, `{{< byposition id="name" >}}`, `<div id="error: cannot access named params by position">`, wt) +} + +func TestInnerSC(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`) + return nil + } + CheckShortCodeMatch(t, `{{< inside class="aspen" >}}`, `<div class="aspen"></div>`, wt) + CheckShortCodeMatch(t, `{{< inside class="aspen" >}}More Here{{< /inside >}}`, "<div class=\"aspen\">More Here</div>", wt) + CheckShortCodeMatch(t, `{{< inside >}}More Here{{< /inside >}}`, "<div>More Here</div>", wt) +} + +func TestInnerSCWithMarkdown(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`) + return nil + } + CheckShortCodeMatch(t, `{{% inside %}} +# More Here + +[link](http://spf13.com) and text + +{{% /inside %}}`, "<div><h1 id=\"more-here\">More Here</h1>\n\n<p><a href=\"http://spf13.com\">link</a> and text</p>\n</div>", wt) +} + +func TestInnerSCWithAndWithoutMarkdown(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`) + return nil + } + CheckShortCodeMatch(t, `{{% inside %}} +# More Here + +[link](http://spf13.com) and text + +{{% /inside %}} + +And then: + +{{< inside >}} +# More Here + +This is **plain** text. + +{{< /inside >}} +`, "<div><h1 id=\"more-here\">More Here</h1>\n\n<p><a href=\"http://spf13.com\">link</a> and text</p>\n</div>\n\n<p>And then:</p>\n\n<p><div>\n# More Here\n\nThis is **plain** text.\n\n</div>", wt) +} + +func TestEmbeddedSC(t *testing.T) { + t.Parallel() + CheckShortCodeMatch(t, "{{% test %}}", "This is a simple Test", nil) + CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" %}}`, "\n<figure class=\"bananas orange\">\n \n <img src=\"/found/here\" />\n \n \n</figure>\n", nil) + CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" caption="This is a caption" %}}`, "\n<figure class=\"bananas orange\">\n \n <img src=\"/found/here\" alt=\"This is a caption\" />\n \n \n <figcaption>\n <p>\n This is a caption\n \n \n \n </p> \n </figcaption>\n \n</figure>\n", nil) +} + +func TestNestedSC(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/scn1.html", `<div>Outer, inner is {{ .Inner }}</div>`) + tem.AddTemplate("_internal/shortcodes/scn2.html", `<div>SC2</div>`) + return nil + } + CheckShortCodeMatch(t, `{{% scn1 %}}{{% scn2 %}}{{% /scn1 %}}`, "<div>Outer, inner is <div>SC2</div>\n</div>", wt) + + CheckShortCodeMatch(t, `{{< scn1 >}}{{% scn2 %}}{{< /scn1 >}}`, "<div>Outer, inner is <div>SC2</div></div>", wt) +} + +func TestNestedComplexSC(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/row.html", `-row-{{ .Inner}}-rowStop-`) + tem.AddTemplate("_internal/shortcodes/column.html", `-col-{{.Inner }}-colStop-`) + tem.AddTemplate("_internal/shortcodes/aside.html", `-aside-{{ .Inner }}-asideStop-`) + return nil + } + CheckShortCodeMatch(t, `{{< row >}}1-s{{% column %}}2-**s**{{< aside >}}3-**s**{{< /aside >}}4-s{{% /column %}}5-s{{< /row >}}6-s`, + "-row-1-s-col-2-<strong>s</strong>-aside-3-<strong>s</strong>-asideStop-4-s-colStop-5-s-rowStop-6-s", wt) + + // turn around the markup flag + CheckShortCodeMatch(t, `{{% row %}}1-s{{< column >}}2-**s**{{% aside %}}3-**s**{{% /aside %}}4-s{{< /column >}}5-s{{% /row %}}6-s`, + "-row-1-s-col-2-<strong>s</strong>-aside-3-<strong>s</strong>-asideStop-4-s-colStop-5-s-rowStop-6-s", wt) +} + +func TestParentShortcode(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/r1.html", `1: {{ .Get "pr1" }} {{ .Inner }}`) + tem.AddTemplate("_internal/shortcodes/r2.html", `2: {{ .Parent.Get "pr1" }}{{ .Get "pr2" }} {{ .Inner }}`) + tem.AddTemplate("_internal/shortcodes/r3.html", `3: {{ .Parent.Parent.Get "pr1" }}{{ .Parent.Get "pr2" }}{{ .Get "pr3" }} {{ .Inner }}`) + return nil + } + CheckShortCodeMatch(t, `{{< r1 pr1="p1" >}}1: {{< r2 pr2="p2" >}}2: {{< r3 pr3="p3" >}}{{< /r3 >}}{{< /r2 >}}{{< /r1 >}}`, + "1: p1 1: 2: p1p2 2: 3: p1p2p3 ", wt) + +} + +func TestFigureImgWidth(t *testing.T) { + t.Parallel() + CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" alt="apple" width="100px" %}}`, "\n<figure class=\"bananas orange\">\n \n <img src=\"/found/here\" alt=\"apple\" width=\"100px\" />\n \n \n</figure>\n", nil) +} + +const testScPlaceholderRegexp = "HAHAHUGOSHORTCODE-\\d+HBHB" + +func TestExtractShortcodes(t *testing.T) { + t.Parallel() + for i, this := range []struct { + name string + input string + expectShortCodes string + expect interface{} + expectErrorMsg string + }{ + {"text", "Some text.", "map[]", "Some text.", ""}, + {"invalid right delim", "{{< tag }}", "", false, "simple.md:4:.*unrecognized character.*}"}, + {"invalid close", "\n{{< /tag >}}", "", false, "simple.md:5:.*got closing shortcode, but none is open"}, + {"invalid close2", "\n\n{{< tag >}}{{< /anotherTag >}}", "", false, "simple.md:6: closing tag for shortcode 'anotherTag' does not match start tag"}, + {"unterminated quote 1", `{{< figure src="im caption="S" >}}`, "", false, "simple.md:4:.got pos.*"}, + {"unterminated quote 1", `{{< figure src="im" caption="S >}}`, "", false, "simple.md:4:.*unterm.*}"}, + {"one shortcode, no markup", "{{< tag >}}", "", testScPlaceholderRegexp, ""}, + {"one shortcode, markup", "{{% tag %}}", "", testScPlaceholderRegexp, ""}, + {"one pos param", "{{% tag param1 %}}", `tag([\"param1\"], true){[]}"]`, testScPlaceholderRegexp, ""}, + {"two pos params", "{{< tag param1 param2>}}", `tag([\"param1\" \"param2\"], false){[]}"]`, testScPlaceholderRegexp, ""}, + {"one named param", `{{% tag param1="value" %}}`, `tag([\"param1:value\"], true){[]}`, testScPlaceholderRegexp, ""}, + {"two named params", `{{< tag param1="value1" param2="value2" >}}`, `tag([\"param1:value1\" \"param2:value2\"], false){[]}"]`, + testScPlaceholderRegexp, ""}, + {"inner", `Some text. {{< inner >}}Inner Content{{< / inner >}}. Some more text.`, `inner([], false){[Inner Content]}`, + fmt.Sprintf("Some text. %s. Some more text.", testScPlaceholderRegexp), ""}, + // issue #934 + {"inner self-closing", `Some text. {{< inner />}}. Some more text.`, `inner([], false){[]}`, + fmt.Sprintf("Some text. %s. Some more text.", testScPlaceholderRegexp), ""}, + {"close, but not inner", "{{< tag >}}foo{{< /tag >}}", "", false, "Shortcode 'tag' in page 'simple.md' has no .Inner.*"}, + {"nested inner", `Inner->{{< inner >}}Inner Content->{{% inner2 param1 %}}inner2txt{{% /inner2 %}}Inner close->{{< / inner >}}<-done`, + `inner([], false){[Inner Content-> inner2([\"param1\"], true){[inner2txt]} Inner close->]}`, + fmt.Sprintf("Inner->%s<-done", testScPlaceholderRegexp), ""}, + {"nested, nested inner", `Inner->{{< inner >}}inner2->{{% inner2 param1 %}}inner2txt->inner3{{< inner3>}}inner3txt{{</ inner3 >}}{{% /inner2 %}}final close->{{< / inner >}}<-done`, + `inner([], false){[inner2-> inner2([\"param1\"], true){[inner2txt->inner3 inner3(%!q(<nil>), false){[inner3txt]}]} final close->`, + fmt.Sprintf("Inner->%s<-done", testScPlaceholderRegexp), ""}, + {"two inner", `Some text. {{% inner %}}First **Inner** Content{{% / inner %}} {{< inner >}}Inner **Content**{{< / inner >}}. Some more text.`, + `map["HAHAHUGOSHORTCODE-1HBHB:inner([], true){[First **Inner** Content]}" "HAHAHUGOSHORTCODE-2HBHB:inner([], false){[Inner **Content**]}"]`, + fmt.Sprintf("Some text. %s %s. Some more text.", testScPlaceholderRegexp, testScPlaceholderRegexp), ""}, + {"closed without content", `Some text. {{< inner param1 >}}{{< / inner >}}. Some more text.`, `inner([\"param1\"], false){[]}`, + fmt.Sprintf("Some text. %s. Some more text.", testScPlaceholderRegexp), ""}, + {"two shortcodes", "{{< sc1 >}}{{< sc2 >}}", + `map["HAHAHUGOSHORTCODE-1HBHB:sc1([], false){[]}" "HAHAHUGOSHORTCODE-2HBHB:sc2([], false){[]}"]`, + testScPlaceholderRegexp + testScPlaceholderRegexp, ""}, + {"mix of shortcodes", `Hello {{< sc1 >}}world{{% sc2 p2="2"%}}. And that's it.`, + `map["HAHAHUGOSHORTCODE-1HBHB:sc1([], false){[]}" "HAHAHUGOSHORTCODE-2HBHB:sc2([\"p2:2\"]`, + fmt.Sprintf("Hello %sworld%s. And that's it.", testScPlaceholderRegexp, testScPlaceholderRegexp), ""}, + {"mix with inner", `Hello {{< sc1 >}}world{{% inner p2="2"%}}Inner{{%/ inner %}}. And that's it.`, + `map["HAHAHUGOSHORTCODE-1HBHB:sc1([], false){[]}" "HAHAHUGOSHORTCODE-2HBHB:inner([\"p2:2\"], true){[Inner]}"]`, + fmt.Sprintf("Hello %sworld%s. And that's it.", testScPlaceholderRegexp, testScPlaceholderRegexp), ""}, + } { + + p, _ := pageFromString(simplePage, "simple.md", func(templ tpl.TemplateHandler) error { + templ.AddTemplate("_internal/shortcodes/tag.html", `tag`) + templ.AddTemplate("_internal/shortcodes/sc1.html", `sc1`) + templ.AddTemplate("_internal/shortcodes/sc2.html", `sc2`) + templ.AddTemplate("_internal/shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`) + templ.AddTemplate("_internal/shortcodes/inner2.html", `{{.Inner}}`) + templ.AddTemplate("_internal/shortcodes/inner3.html", `{{.Inner}}`) + return nil + }) + + s := newShortcodeHandler(p) + content, err := s.extractShortcodes(this.input, p) + + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Fatalf("[%d] %s: ExtractShortcodes didn't return an expected error", i, this.name) + } else { + r, _ := regexp.Compile(this.expectErrorMsg) + if !r.MatchString(err.Error()) { + t.Fatalf("[%d] %s: ExtractShortcodes didn't return an expected error message, got %s but expected %s", + i, this.name, err.Error(), this.expectErrorMsg) + } + } + continue + } else { + if err != nil { + t.Fatalf("[%d] %s: failed: %q", i, this.name, err) + } + } + + shortCodes := s.shortcodes + + var expected string + av := reflect.ValueOf(this.expect) + switch av.Kind() { + case reflect.String: + expected = av.String() + } + + r, err := regexp.Compile(expected) + + if err != nil { + t.Fatalf("[%d] %s: Failed to compile regexp %q: %q", i, this.name, expected, err) + } + + if strings.Count(content, shortcodePlaceholderPrefix) != len(shortCodes) { + t.Fatalf("[%d] %s: Not enough placeholders, found %d", i, this.name, len(shortCodes)) + } + + if !r.MatchString(content) { + t.Fatalf("[%d] %s: Shortcode extract didn't match. got %q but expected %q", i, this.name, content, expected) + } + + for placeHolder, sc := range shortCodes { + if !strings.Contains(content, placeHolder) { + t.Fatalf("[%d] %s: Output does not contain placeholder %q", i, this.name, placeHolder) + } + + if sc.params == nil { + t.Fatalf("[%d] %s: Params is nil for shortcode '%s'", i, this.name, sc.name) + } + } + + if this.expectShortCodes != "" { + shortCodesAsStr := fmt.Sprintf("map%q", collectAndSortShortcodes(shortCodes)) + if !strings.Contains(shortCodesAsStr, this.expectShortCodes) { + t.Fatalf("[%d] %s: Shortcodes not as expected, got %s but expected %s", i, this.name, shortCodesAsStr, this.expectShortCodes) + } + } + } +} + +func TestShortcodesInSite(t *testing.T) { + t.Parallel() + baseURL := "http://foo/bar" + + tests := []struct { + contentPath string + content string + outFile string + expected string + }{ + {"sect/doc1.md", `a{{< b >}}c`, + filepath.FromSlash("public/sect/doc1/index.html"), "<p>abc</p>\n"}, + // Issue #1642: Multiple shortcodes wrapped in P + // Deliberately forced to pass even if they maybe shouldn't. + {"sect/doc2.md", `a + +{{< b >}} +{{< c >}} +{{< d >}} + +e`, + filepath.FromSlash("public/sect/doc2/index.html"), + "<p>a</p>\n\n<p>b<br />\nc\nd</p>\n\n<p>e</p>\n"}, + {"sect/doc3.md", `a + +{{< b >}} +{{< c >}} + +{{< d >}} + +e`, + filepath.FromSlash("public/sect/doc3/index.html"), + "<p>a</p>\n\n<p>b<br />\nc</p>\n\nd\n\n<p>e</p>\n"}, + {"sect/doc4.md", `a +{{< b >}} +{{< b >}} +{{< b >}} +{{< b >}} +{{< b >}} + + + + + + + + + + +`, + filepath.FromSlash("public/sect/doc4/index.html"), + "<p>a\nb\nb\nb\nb\nb</p>\n"}, + // #2192 #2209: Shortcodes in markdown headers + {"sect/doc5.md", `# {{< b >}} +## {{% c %}}`, + filepath.FromSlash("public/sect/doc5/index.html"), "\n\n<h1 id=\"hahahugoshortcode-1hbhb\">b</h1>\n\n<h2 id=\"hahahugoshortcode-2hbhb\">c</h2>\n"}, + // #2223 pygments + {"sect/doc6.md", "\n```bash\nb: {{< b >}} c: {{% c %}}\n```\n", + filepath.FromSlash("public/sect/doc6/index.html"), + "b: b c: c\n</code></pre></div>\n"}, + // #2249 + {"sect/doc7.ad", `_Shortcodes:_ *b: {{< b >}} c: {{% c %}}*`, + filepath.FromSlash("public/sect/doc7/index.html"), + "<div class=\"paragraph\">\n<p><em>Shortcodes:</em> <strong>b: b c: c</strong></p>\n</div>\n"}, + {"sect/doc8.rst", `**Shortcodes:** *b: {{< b >}} c: {{% c %}}*`, + filepath.FromSlash("public/sect/doc8/index.html"), + "<div class=\"document\">\n\n\n<p><strong>Shortcodes:</strong> <em>b: b c: c</em></p>\n</div>"}, + {"sect/doc9.mmark", ` +--- +menu: + main: + parent: 'parent' +--- +**Shortcodes:** *b: {{< b >}} c: {{% c %}}*`, + filepath.FromSlash("public/sect/doc9/index.html"), + "<p><strong>Shortcodes:</strong> <em>b: b c: c</em></p>\n"}, + // Issue #1229: Menus not available in shortcode. + {"sect/doc10.md", `--- +menu: + main: + identifier: 'parent' +tags: +- Menu +--- +**Menus:** {{< menu >}}`, + filepath.FromSlash("public/sect/doc10/index.html"), + "<p><strong>Menus:</strong> 1</p>\n"}, + // Issue #2323: Taxonomies not available in shortcode. + {"sect/doc11.md", `--- +tags: +- Bugs +--- +**Tags:** {{< tags >}}`, + filepath.FromSlash("public/sect/doc11/index.html"), + "<p><strong>Tags:</strong> 2</p>\n"}, + } + + sources := make([]source.ByteSource, len(tests)) + + for i, test := range tests { + sources[i] = source.ByteSource{Name: filepath.FromSlash(test.contentPath), Content: []byte(test.content)} + } + + addTemplates := func(templ tpl.TemplateHandler) error { + templ.AddTemplate("_default/single.html", "{{.Content}}") + + templ.AddTemplate("_internal/shortcodes/b.html", `b`) + templ.AddTemplate("_internal/shortcodes/c.html", `c`) + templ.AddTemplate("_internal/shortcodes/d.html", `d`) + templ.AddTemplate("_internal/shortcodes/menu.html", `{{ len (index .Page.Menus "main").Children }}`) + templ.AddTemplate("_internal/shortcodes/tags.html", `{{ len .Page.Site.Taxonomies.tags }}`) + + return nil + + } + + cfg, fs := newTestCfg() + + cfg.Set("defaultContentLanguage", "en") + cfg.Set("baseURL", baseURL) + cfg.Set("uglyURLs", false) + cfg.Set("verbose", true) + + cfg.Set("pygmentsUseClasses", true) + cfg.Set("pygmentsCodefences", true) + + writeSourcesToSource(t, "content", fs, sources...) + + s := buildSingleSite(t, deps.DepsCfg{WithTemplate: addTemplates, Fs: fs, Cfg: cfg}, BuildCfg{}) + th := testHelper{s.Cfg, s.Fs, t} + + for _, test := range tests { + if strings.HasSuffix(test.contentPath, ".ad") && !helpers.HasAsciidoc() { + fmt.Println("Skip Asciidoc test case as no Asciidoc present.") + continue + } else if strings.HasSuffix(test.contentPath, ".rst") && !helpers.HasRst() { + fmt.Println("Skip Rst test case as no rst2html present.") + continue + } else if strings.Contains(test.expected, "code") && !helpers.HasPygments() { + fmt.Println("Skip Pygments test case as no pygments present.") + continue + } + + th.assertFileContent(test.outFile, test.expected) + } + +} + +func TestShortcodeMultipleOutputFormats(t *testing.T) { + t.Parallel() + + siteConfig := ` +baseURL = "http://example.com/blog" + +paginate = 1 + +disableKinds = ["section", "taxonomy", "taxonomyTerm", "RSS", "sitemap", "robotsTXT", "404"] + +[outputs] +home = [ "HTML", "AMP", "Calendar" ] +page = [ "HTML", "AMP", "JSON" ] + +` + + pageTemplate := `--- +title: "%s" +--- +# Doc + +{{< myShort >}} +{{< noExt >}} +{{%% onlyHTML %%}} + +{{< myInner >}}{{< myShort >}}{{< /myInner >}} + +` + + pageTemplateCSVOnly := `--- +title: "%s" +outputs: ["CSV"] +--- +# Doc + +CSV: {{< myShort >}} +` + + pageTemplateShortcodeNotFound := `--- +title: "%s" +outputs: ["CSV"] +--- +# Doc + +NotFound: {{< thisDoesNotExist >}} +` + + mf := afero.NewMemMapFs() + + th, h := newTestSitesFromConfig(t, mf, siteConfig, + "layouts/_default/single.html", `Single HTML: {{ .Title }}|{{ .Content }}`, + "layouts/_default/single.json", `Single JSON: {{ .Title }}|{{ .Content }}`, + "layouts/_default/single.csv", `Single CSV: {{ .Title }}|{{ .Content }}`, + "layouts/index.html", `Home HTML: {{ .Title }}|{{ .Content }}`, + "layouts/index.amp.html", `Home AMP: {{ .Title }}|{{ .Content }}`, + "layouts/index.ics", `Home Calendar: {{ .Title }}|{{ .Content }}`, + "layouts/shortcodes/myShort.html", `ShortHTML`, + "layouts/shortcodes/myShort.amp.html", `ShortAMP`, + "layouts/shortcodes/myShort.csv", `ShortCSV`, + "layouts/shortcodes/myShort.ics", `ShortCalendar`, + "layouts/shortcodes/myShort.json", `ShortJSON`, + "layouts/shortcodes/noExt", `ShortNoExt`, + "layouts/shortcodes/onlyHTML.html", `ShortOnlyHTML`, + "layouts/shortcodes/myInner.html", `myInner:--{{- .Inner -}}--`, + ) + + fs := th.Fs + + writeSource(t, fs, "content/_index.md", fmt.Sprintf(pageTemplate, "Home")) + writeSource(t, fs, "content/sect/mypage.md", fmt.Sprintf(pageTemplate, "Single")) + writeSource(t, fs, "content/sect/mycsvpage.md", fmt.Sprintf(pageTemplateCSVOnly, "Single CSV")) + writeSource(t, fs, "content/sect/notfound.md", fmt.Sprintf(pageTemplateShortcodeNotFound, "Single CSV")) + + require.NoError(t, h.Build(BuildCfg{})) + require.Len(t, h.Sites, 1) + + s := h.Sites[0] + home := s.getPage(KindHome) + require.NotNil(t, home) + require.Len(t, home.outputFormats, 3) + + th.assertFileContent("public/index.html", + "Home HTML", + "ShortHTML", + "ShortNoExt", + "ShortOnlyHTML", + "myInner:--ShortHTML--", + ) + + th.assertFileContent("public/amp/index.html", + "Home AMP", + "ShortAMP", + "ShortNoExt", + "ShortOnlyHTML", + "myInner:--ShortAMP--", + ) + + th.assertFileContent("public/index.ics", + "Home Calendar", + "ShortCalendar", + "ShortNoExt", + "ShortOnlyHTML", + "myInner:--ShortCalendar--", + ) + + th.assertFileContent("public/sect/mypage/index.html", + "Single HTML", + "ShortHTML", + "ShortNoExt", + "ShortOnlyHTML", + "myInner:--ShortHTML--", + ) + + th.assertFileContent("public/sect/mypage/index.json", + "Single JSON", + "ShortJSON", + "ShortNoExt", + "ShortOnlyHTML", + "myInner:--ShortJSON--", + ) + + th.assertFileContent("public/amp/sect/mypage/index.html", + // No special AMP template + "Single HTML", + "ShortAMP", + "ShortNoExt", + "ShortOnlyHTML", + "myInner:--ShortAMP--", + ) + + th.assertFileContent("public/sect/mycsvpage/index.csv", + "Single CSV", + "ShortCSV", + ) + + th.assertFileContent("public/sect/notfound/index.csv", + "NotFound:", + "thisDoesNotExist", + ) + + require.Equal(t, uint64(1), s.Log.LogCountForLevel(jww.LevelError)) + +} + +func collectAndSortShortcodes(shortcodes map[string]shortcode) []string { + var asArray []string + + for key, sc := range shortcodes { + asArray = append(asArray, fmt.Sprintf("%s:%s", key, sc)) + } + + sort.Strings(asArray) + return asArray + +} + +func BenchmarkReplaceShortcodeTokens(b *testing.B) { + + type input struct { + in []byte + replacements map[string]string + expect []byte + } + + data := []struct { + input string + replacements map[string]string + expect []byte + }{ + {"Hello HAHAHUGOSHORTCODE-1HBHB.", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, []byte("Hello World.")}, + {strings.Repeat("A", 100) + " HAHAHUGOSHORTCODE-1HBHB.", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "Hello World"}, []byte(strings.Repeat("A", 100) + " Hello World.")}, + {strings.Repeat("A", 500) + " HAHAHUGOSHORTCODE-1HBHB.", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "Hello World"}, []byte(strings.Repeat("A", 500) + " Hello World.")}, + {strings.Repeat("ABCD ", 500) + " HAHAHUGOSHORTCODE-1HBHB.", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "Hello World"}, []byte(strings.Repeat("ABCD ", 500) + " Hello World.")}, + {strings.Repeat("A ", 3000) + " HAHAHUGOSHORTCODE-1HBHB." + strings.Repeat("BC ", 1000) + " HAHAHUGOSHORTCODE-1HBHB.", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "Hello World"}, []byte(strings.Repeat("A ", 3000) + " Hello World." + strings.Repeat("BC ", 1000) + " Hello World.")}, + } + + var in = make([]input, b.N*len(data)) + var cnt = 0 + for i := 0; i < b.N; i++ { + for _, this := range data { + in[cnt] = input{[]byte(this.input), this.replacements, this.expect} + cnt++ + } + } + + b.ResetTimer() + cnt = 0 + for i := 0; i < b.N; i++ { + for j := range data { + currIn := in[cnt] + cnt++ + results, err := replaceShortcodeTokens(currIn.in, "HUGOSHORTCODE", currIn.replacements) + + if err != nil { + b.Fatalf("[%d] failed: %s", i, err) + continue + } + if len(results) != len(currIn.expect) { + b.Fatalf("[%d] replaceShortcodeTokens, got \n%q but expected \n%q", j, results, currIn.expect) + } + + } + + } +} + +func TestReplaceShortcodeTokens(t *testing.T) { + t.Parallel() + for i, this := range []struct { + input string + prefix string + replacements map[string]string + expect interface{} + }{ + {"Hello HAHAPREFIX-1HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "Hello World."}, + {"Hello HAHAPREFIX-1@}@.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, false}, + {"HAHAPREFIX2-1HBHB", "PREFIX2", map[string]string{"HAHAPREFIX2-1HBHB": "World"}, "World"}, + {"Hello World!", "PREFIX2", map[string]string{}, "Hello World!"}, + {"!HAHAPREFIX-1HBHB", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "!World"}, + {"HAHAPREFIX-1HBHB!", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "World!"}, + {"!HAHAPREFIX-1HBHB!", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "!World!"}, + {"_{_PREFIX-1HBHB", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "_{_PREFIX-1HBHB"}, + {"Hello HAHAPREFIX-1HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "To You My Old Friend Who Told Me This Fantastic Story"}, "Hello To You My Old Friend Who Told Me This Fantastic Story."}, + {"A HAHAA-1HBHB asdf HAHAA-2HBHB.", "A", map[string]string{"HAHAA-1HBHB": "v1", "HAHAA-2HBHB": "v2"}, "A v1 asdf v2."}, + {"Hello HAHAPREFIX2-1HBHB. Go HAHAPREFIX2-2HBHB, Go, Go HAHAPREFIX2-3HBHB Go Go!.", "PREFIX2", map[string]string{"HAHAPREFIX2-1HBHB": "Europe", "HAHAPREFIX2-2HBHB": "Jonny", "HAHAPREFIX2-3HBHB": "Johnny"}, "Hello Europe. Go Jonny, Go, Go Johnny Go Go!."}, + {"A HAHAPREFIX-2HBHB HAHAPREFIX-1HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A", "HAHAPREFIX-2HBHB": "B"}, "A B A."}, + {"A HAHAPREFIX-1HBHB HAHAPREFIX-2", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A"}, false}, + {"A HAHAPREFIX-1HBHB but not the second.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A", "HAHAPREFIX-2HBHB": "B"}, "A A but not the second."}, + {"An HAHAPREFIX-1HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A", "HAHAPREFIX-2HBHB": "B"}, "An A."}, + {"An HAHAPREFIX-1HBHB HAHAPREFIX-2HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A", "HAHAPREFIX-2HBHB": "B"}, "An A B."}, + {"A HAHAPREFIX-1HBHB HAHAPREFIX-2HBHB HAHAPREFIX-3HBHB HAHAPREFIX-1HBHB HAHAPREFIX-3HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A", "HAHAPREFIX-2HBHB": "B", "HAHAPREFIX-3HBHB": "C"}, "A A B C A C."}, + {"A HAHAPREFIX-1HBHB HAHAPREFIX-2HBHB HAHAPREFIX-3HBHB HAHAPREFIX-1HBHB HAHAPREFIX-3HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A", "HAHAPREFIX-2HBHB": "B", "HAHAPREFIX-3HBHB": "C"}, "A A B C A C."}, + // Issue #1148 remove p-tags 10 => + {"Hello <p>HAHAPREFIX-1HBHB</p>. END.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "Hello World. END."}, + {"Hello <p>HAHAPREFIX-1HBHB</p>. <p>HAHAPREFIX-2HBHB</p> END.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World", "HAHAPREFIX-2HBHB": "THE"}, "Hello World. THE END."}, + {"Hello <p>HAHAPREFIX-1HBHB. END</p>.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "Hello <p>World. END</p>."}, + {"<p>Hello HAHAPREFIX-1HBHB</p>. END.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "<p>Hello World</p>. END."}, + {"Hello <p>HAHAPREFIX-1HBHB12", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "Hello <p>World12"}, + {"Hello HAHAP-1HBHB. HAHAP-1HBHB-HAHAP-1HBHB HAHAP-1HBHB HAHAP-1HBHB HAHAP-1HBHB END", "P", map[string]string{"HAHAP-1HBHB": strings.Repeat("BC", 100)}, + fmt.Sprintf("Hello %s. %s-%s %s %s %s END", + strings.Repeat("BC", 100), strings.Repeat("BC", 100), strings.Repeat("BC", 100), strings.Repeat("BC", 100), strings.Repeat("BC", 100), strings.Repeat("BC", 100))}, + } { + + results, err := replaceShortcodeTokens([]byte(this.input), this.prefix, this.replacements) + + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] replaceShortcodeTokens didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(results, []byte(this.expect.(string))) { + t.Errorf("[%d] replaceShortcodeTokens, got \n%q but expected \n%q", i, results, this.expect) + } + } + + } + +} + +func TestScKey(t *testing.T) { + require.Equal(t, scKey{Suffix: "xml", ShortcodePlaceholder: "ABCD"}, + newScKey(media.XMLType, "ABCD")) + require.Equal(t, scKey{Suffix: "html", OutputFormat: "AMP", ShortcodePlaceholder: "EFGH"}, + newScKeyFromOutputFormat(output.AMPFormat, "EFGH")) + require.Equal(t, scKey{Suffix: "html", ShortcodePlaceholder: "IJKL"}, + newDefaultScKey("IJKL")) + +} diff --git a/hugolib/shortcodeparser.go b/hugolib/shortcodeparser.go new file mode 100644 index 000000000..bdbd3ae50 --- /dev/null +++ b/hugolib/shortcodeparser.go @@ -0,0 +1,588 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "strings" + "unicode" + "unicode/utf8" +) + +// The lexical scanning below is highly inspired by the great talk given by +// Rob Pike called "Lexical Scanning in Go" (it's on YouTube, Google it!). +// See slides here: http://cuddle.googlecode.com/hg/talk/lex.html + +// parsing + +type pageTokens struct { + lexer *pagelexer + token [3]item // 3-item look-ahead is what we currently need + peekCount int +} + +func (t *pageTokens) next() item { + if t.peekCount > 0 { + t.peekCount-- + } else { + t.token[0] = t.lexer.nextItem() + } + return t.token[t.peekCount] +} + +// backs up one token. +func (t *pageTokens) backup() { + t.peekCount++ +} + +// backs up two tokens. +func (t *pageTokens) backup2(t1 item) { + t.token[1] = t1 + t.peekCount = 2 +} + +// backs up three tokens. +func (t *pageTokens) backup3(t2, t1 item) { + t.token[1] = t1 + t.token[2] = t2 + t.peekCount = 3 +} + +// check for non-error and non-EOF types coming next +func (t *pageTokens) isValueNext() bool { + i := t.peek() + return i.typ != tError && i.typ != tEOF +} + +// look at, but do not consume, the next item +// repeated, sequential calls will return the same item +func (t *pageTokens) peek() item { + if t.peekCount > 0 { + return t.token[t.peekCount-1] + } + t.peekCount = 1 + t.token[0] = t.lexer.nextItem() + return t.token[0] +} + +// convencience method to consume the next n tokens, but back off Errors and EOF +func (t *pageTokens) consume(cnt int) { + for i := 0; i < cnt; i++ { + token := t.next() + if token.typ == tError || token.typ == tEOF { + t.backup() + break + } + } +} + +// lexical scanning + +// position (in bytes) +type pos int + +type item struct { + typ itemType + pos pos + val string +} + +func (i item) String() string { + switch { + case i.typ == tEOF: + return "EOF" + case i.typ == tError: + return i.val + case i.typ > tKeywordMarker: + return fmt.Sprintf("<%s>", i.val) + case len(i.val) > 20: + return fmt.Sprintf("%.20q...", i.val) + } + return fmt.Sprintf("[%s]", i.val) +} + +type itemType int + +const ( + tError itemType = iota + tEOF + + // shortcode items + tLeftDelimScNoMarkup + tRightDelimScNoMarkup + tLeftDelimScWithMarkup + tRightDelimScWithMarkup + tScClose + tScName + tScParam + tScParamVal + + //itemIdentifier + tText // plain text, used for everything outside the shortcodes + + // preserved for later - keywords come after this + tKeywordMarker +) + +const eof = -1 + +// returns the next state in scanner. +type stateFunc func(*pagelexer) stateFunc + +type pagelexer struct { + name string + input string + state stateFunc + pos pos // input position + start pos // item start position + width pos // width of last element + lastPos pos // position of the last item returned by nextItem + + // shortcode state + currLeftDelimItem itemType + currRightDelimItem itemType + currShortcodeName string // is only set when a shortcode is in opened state + closingState int // > 0 = on its way to be closed + elementStepNum int // step number in element + paramElements int // number of elements (name + value = 2) found first + openShortcodes map[string]bool // set of shortcodes in open state + + // items delivered to client + items chan item +} + +// note: the input position here is normally 0 (start), but +// can be set if position of first shortcode is known +func newShortcodeLexer(name, input string, inputPosition pos) *pagelexer { + lexer := &pagelexer{ + name: name, + input: input, + currLeftDelimItem: tLeftDelimScNoMarkup, + currRightDelimItem: tRightDelimScNoMarkup, + pos: inputPosition, + openShortcodes: make(map[string]bool), + items: make(chan item), + } + go lexer.runShortcodeLexer() + return lexer +} + +// main loop +// this looks kind of funky, but it works +func (l *pagelexer) runShortcodeLexer() { + for l.state = lexTextOutsideShortcodes; l.state != nil; { + l.state = l.state(l) + } + + close(l.items) +} + +// state functions + +const ( + leftDelimScNoMarkup = "{{<" + rightDelimScNoMarkup = ">}}" + leftDelimScWithMarkup = "{{%" + rightDelimScWithMarkup = "%}}" + leftComment = "/*" // comments in this context us used to to mark shortcodes as "not really a shortcode" + rightComment = "*/" +) + +func (l *pagelexer) next() rune { + if int(l.pos) >= len(l.input) { + l.width = 0 + return eof + } + + // looks expensive, but should produce the same iteration sequence as the string range loop + // see: http://blog.golang.org/strings + runeValue, runeWidth := utf8.DecodeRuneInString(l.input[l.pos:]) + l.width = pos(runeWidth) + l.pos += l.width + return runeValue +} + +// peek, but no consume +func (l *pagelexer) peek() rune { + r := l.next() + l.backup() + return r +} + +// steps back one +func (l *pagelexer) backup() { + l.pos -= l.width +} + +// sends an item back to the client. +func (l *pagelexer) emit(t itemType) { + l.items <- item{t, l.start, l.input[l.start:l.pos]} + l.start = l.pos +} + +// special case, do not send '\\' back to client +func (l *pagelexer) ignoreEscapesAndEmit(t itemType) { + val := strings.Map(func(r rune) rune { + if r == '\\' { + return -1 + } + return r + }, l.input[l.start:l.pos]) + l.items <- item{t, l.start, val} + l.start = l.pos +} + +// gets the current value (for debugging and error handling) +func (l *pagelexer) current() string { + return l.input[l.start:l.pos] +} + +// ignore current element +func (l *pagelexer) ignore() { + l.start = l.pos +} + +// nice to have in error logs +func (l *pagelexer) lineNum() int { + return strings.Count(l.input[:l.lastPos], "\n") + 1 +} + +// nil terminates the parser +func (l *pagelexer) errorf(format string, args ...interface{}) stateFunc { + l.items <- item{tError, l.start, fmt.Sprintf(format, args...)} + return nil +} + +// consumes and returns the next item +func (l *pagelexer) nextItem() item { + item := <-l.items + l.lastPos = item.pos + return item +} + +// scans until an opening shortcode opening bracket. +// if no shortcodes, it will keep on scanning until EOF +func lexTextOutsideShortcodes(l *pagelexer) stateFunc { + for { + if strings.HasPrefix(l.input[l.pos:], leftDelimScWithMarkup) || strings.HasPrefix(l.input[l.pos:], leftDelimScNoMarkup) { + if l.pos > l.start { + l.emit(tText) + } + if strings.HasPrefix(l.input[l.pos:], leftDelimScWithMarkup) { + l.currLeftDelimItem = tLeftDelimScWithMarkup + l.currRightDelimItem = tRightDelimScWithMarkup + } else { + l.currLeftDelimItem = tLeftDelimScNoMarkup + l.currRightDelimItem = tRightDelimScNoMarkup + } + return lexShortcodeLeftDelim + + } + if l.next() == eof { + break + } + } + // Done! + if l.pos > l.start { + l.emit(tText) + } + l.emit(tEOF) + return nil +} + +func lexShortcodeLeftDelim(l *pagelexer) stateFunc { + l.pos += pos(len(l.currentLeftShortcodeDelim())) + if strings.HasPrefix(l.input[l.pos:], leftComment) { + return lexShortcodeComment + } + l.emit(l.currentLeftShortcodeDelimItem()) + l.elementStepNum = 0 + l.paramElements = 0 + return lexInsideShortcode +} + +func lexShortcodeComment(l *pagelexer) stateFunc { + posRightComment := strings.Index(l.input[l.pos:], rightComment) + if posRightComment <= 1 { + return l.errorf("comment must be closed") + } + // we emit all as text, except the comment markers + l.emit(tText) + l.pos += pos(len(leftComment)) + l.ignore() + l.pos += pos(posRightComment - len(leftComment)) + l.emit(tText) + l.pos += pos(len(rightComment)) + l.ignore() + if !strings.HasPrefix(l.input[l.pos:], l.currentRightShortcodeDelim()) { + return l.errorf("comment ends before the right shortcode delimiter") + } + l.pos += pos(len(l.currentRightShortcodeDelim())) + l.emit(tText) + return lexTextOutsideShortcodes +} + +func lexShortcodeRightDelim(l *pagelexer) stateFunc { + l.closingState = 0 + l.pos += pos(len(l.currentRightShortcodeDelim())) + l.emit(l.currentRightShortcodeDelimItem()) + return lexTextOutsideShortcodes +} + +// either: +// 1. param +// 2. "param" or "param\" +// 3. param="123" or param="123\" +// 4. param="Some \"escaped\" text" +func lexShortcodeParam(l *pagelexer, escapedQuoteStart bool) stateFunc { + + first := true + nextEq := false + + var r rune + + for { + r = l.next() + if first { + if r == '"' { + // a positional param with quotes + if l.paramElements == 2 { + return l.errorf("got quoted positional parameter. Cannot mix named and positional parameters") + } + l.paramElements = 1 + l.backup() + return lexShortcodeQuotedParamVal(l, !escapedQuoteStart, tScParam) + } + first = false + } else if r == '=' { + // a named param + l.backup() + nextEq = true + break + } + + if !isAlphaNumericOrHyphen(r) { + l.backup() + break + } + } + + if l.paramElements == 0 { + l.paramElements++ + + if nextEq { + l.paramElements++ + } + } else { + if nextEq && l.paramElements == 1 { + return l.errorf("got named parameter '%s'. Cannot mix named and positional parameters", l.current()) + } else if !nextEq && l.paramElements == 2 { + return l.errorf("got positional parameter '%s'. Cannot mix named and positional parameters", l.current()) + } + } + + l.emit(tScParam) + return lexInsideShortcode + +} + +func lexShortcodeQuotedParamVal(l *pagelexer, escapedQuotedValuesAllowed bool, typ itemType) stateFunc { + openQuoteFound := false + escapedInnerQuoteFound := false + escapedQuoteState := 0 + +Loop: + for { + switch r := l.next(); { + case r == '\\': + if l.peek() == '"' { + if openQuoteFound && !escapedQuotedValuesAllowed { + l.backup() + break Loop + } else if openQuoteFound { + // the coming quoute is inside + escapedInnerQuoteFound = true + escapedQuoteState = 1 + } + } + case r == eof, r == '\n': + return l.errorf("unterminated quoted string in shortcode parameter-argument: '%s'", l.current()) + case r == '"': + if escapedQuoteState == 0 { + if openQuoteFound { + l.backup() + break Loop + + } else { + openQuoteFound = true + l.ignore() + } + } else { + escapedQuoteState = 0 + } + + } + } + + if escapedInnerQuoteFound { + l.ignoreEscapesAndEmit(typ) + } else { + l.emit(typ) + } + + r := l.next() + + if r == '\\' { + if l.peek() == '"' { + // ignore the escaped closing quote + l.ignore() + l.next() + l.ignore() + } + } else if r == '"' { + // ignore closing quote + l.ignore() + } else { + // handled by next state + l.backup() + } + + return lexInsideShortcode +} + +// scans an alphanumeric inside shortcode +func lexIdentifierInShortcode(l *pagelexer) stateFunc { + lookForEnd := false +Loop: + for { + switch r := l.next(); { + case isAlphaNumericOrHyphen(r): + default: + l.backup() + word := l.input[l.start:l.pos] + if l.closingState > 0 && !l.openShortcodes[word] { + return l.errorf("closing tag for shortcode '%s' does not match start tag", word) + } else if l.closingState > 0 { + l.openShortcodes[word] = false + lookForEnd = true + } + + l.closingState = 0 + l.currShortcodeName = word + l.openShortcodes[word] = true + l.elementStepNum++ + l.emit(tScName) + break Loop + } + } + + if lookForEnd { + return lexEndOfShortcode + } + return lexInsideShortcode +} + +func lexEndOfShortcode(l *pagelexer) stateFunc { + if strings.HasPrefix(l.input[l.pos:], l.currentRightShortcodeDelim()) { + return lexShortcodeRightDelim + } + switch r := l.next(); { + case isSpace(r): + l.ignore() + default: + return l.errorf("unclosed shortcode") + } + return lexEndOfShortcode +} + +// scans the elements inside shortcode tags +func lexInsideShortcode(l *pagelexer) stateFunc { + if strings.HasPrefix(l.input[l.pos:], l.currentRightShortcodeDelim()) { + return lexShortcodeRightDelim + } + switch r := l.next(); { + case r == eof: + // eol is allowed inside shortcodes; this may go to end of document before it fails + return l.errorf("unclosed shortcode action") + case isSpace(r), isEndOfLine(r): + l.ignore() + case r == '=': + l.ignore() + return lexShortcodeQuotedParamVal(l, l.peek() != '\\', tScParamVal) + case r == '/': + if l.currShortcodeName == "" { + return l.errorf("got closing shortcode, but none is open") + } + l.closingState++ + l.emit(tScClose) + case r == '\\': + l.ignore() + if l.peek() == '"' { + return lexShortcodeParam(l, true) + } + case l.elementStepNum > 0 && (isAlphaNumericOrHyphen(r) || r == '"'): // positional params can have quotes + l.backup() + return lexShortcodeParam(l, false) + case isAlphaNumeric(r): + l.backup() + return lexIdentifierInShortcode + default: + return l.errorf("unrecognized character in shortcode action: %#U. Note: Parameters with non-alphanumeric args must be quoted", r) + } + return lexInsideShortcode +} + +// state helpers + +func (l *pagelexer) currentLeftShortcodeDelimItem() itemType { + return l.currLeftDelimItem +} + +func (l *pagelexer) currentRightShortcodeDelimItem() itemType { + return l.currRightDelimItem +} + +func (l *pagelexer) currentLeftShortcodeDelim() string { + if l.currLeftDelimItem == tLeftDelimScWithMarkup { + return leftDelimScWithMarkup + } + return leftDelimScNoMarkup + +} + +func (l *pagelexer) currentRightShortcodeDelim() string { + if l.currRightDelimItem == tRightDelimScWithMarkup { + return rightDelimScWithMarkup + } + return rightDelimScNoMarkup +} + +// helper functions + +func isSpace(r rune) bool { + return r == ' ' || r == '\t' +} + +func isAlphaNumericOrHyphen(r rune) bool { + // let unquoted YouTube ids as positional params slip through (they contain hyphens) + return isAlphaNumeric(r) || r == '-' +} + +func isEndOfLine(r rune) bool { + return r == '\r' || r == '\n' +} + +func isAlphaNumeric(r rune) bool { + return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r) +} diff --git a/hugolib/shortcodeparser_test.go b/hugolib/shortcodeparser_test.go new file mode 100644 index 000000000..3103fd4de --- /dev/null +++ b/hugolib/shortcodeparser_test.go @@ -0,0 +1,202 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "testing" +) + +type shortCodeLexerTest struct { + name string + input string + items []item +} + +var ( + tstEOF = item{tEOF, 0, ""} + tstLeftNoMD = item{tLeftDelimScNoMarkup, 0, "{{<"} + tstRightNoMD = item{tRightDelimScNoMarkup, 0, ">}}"} + tstLeftMD = item{tLeftDelimScWithMarkup, 0, "{{%"} + tstRightMD = item{tRightDelimScWithMarkup, 0, "%}}"} + tstSCClose = item{tScClose, 0, "/"} + tstSC1 = item{tScName, 0, "sc1"} + tstSC2 = item{tScName, 0, "sc2"} + tstSC3 = item{tScName, 0, "sc3"} + tstParam1 = item{tScParam, 0, "param1"} + tstParam2 = item{tScParam, 0, "param2"} + tstVal = item{tScParamVal, 0, "Hello World"} +) + +var shortCodeLexerTests = []shortCodeLexerTest{ + {"empty", "", []item{tstEOF}}, + {"spaces", " \t\n", []item{{tText, 0, " \t\n"}, tstEOF}}, + {"text", `to be or not`, []item{{tText, 0, "to be or not"}, tstEOF}}, + {"no markup", `{{< sc1 >}}`, []item{tstLeftNoMD, tstSC1, tstRightNoMD, tstEOF}}, + {"with EOL", "{{< sc1 \n >}}", []item{tstLeftNoMD, tstSC1, tstRightNoMD, tstEOF}}, + + {"simple with markup", `{{% sc1 %}}`, []item{tstLeftMD, tstSC1, tstRightMD, tstEOF}}, + {"with spaces", `{{< sc1 >}}`, []item{tstLeftNoMD, tstSC1, tstRightNoMD, tstEOF}}, + {"mismatched rightDelim", `{{< sc1 %}}`, []item{tstLeftNoMD, tstSC1, + {tError, 0, "unrecognized character in shortcode action: U+0025 '%'. Note: Parameters with non-alphanumeric args must be quoted"}}}, + {"inner, markup", `{{% sc1 %}} inner {{% /sc1 %}}`, []item{ + tstLeftMD, + tstSC1, + tstRightMD, + {tText, 0, " inner "}, + tstLeftMD, + tstSCClose, + tstSC1, + tstRightMD, + tstEOF, + }}, + {"close, but no open", `{{< /sc1 >}}`, []item{ + tstLeftNoMD, {tError, 0, "got closing shortcode, but none is open"}}}, + {"close wrong", `{{< sc1 >}}{{< /another >}}`, []item{ + tstLeftNoMD, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose, + {tError, 0, "closing tag for shortcode 'another' does not match start tag"}}}, + {"close, but no open, more", `{{< sc1 >}}{{< /sc1 >}}{{< /another >}}`, []item{ + tstLeftNoMD, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose, + {tError, 0, "closing tag for shortcode 'another' does not match start tag"}}}, + {"close with extra keyword", `{{< sc1 >}}{{< /sc1 keyword>}}`, []item{ + tstLeftNoMD, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose, tstSC1, + {tError, 0, "unclosed shortcode"}}}, + {"Youtube id", `{{< sc1 -ziL-Q_456igdO-4 >}}`, []item{ + tstLeftNoMD, tstSC1, {tScParam, 0, "-ziL-Q_456igdO-4"}, tstRightNoMD, tstEOF}}, + {"non-alphanumerics param quoted", `{{< sc1 "-ziL-.%QigdO-4" >}}`, []item{ + tstLeftNoMD, tstSC1, {tScParam, 0, "-ziL-.%QigdO-4"}, tstRightNoMD, tstEOF}}, + + {"two params", `{{< sc1 param1 param2 >}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, tstParam2, tstRightNoMD, tstEOF}}, + // issue #934 + {"self-closing", `{{< sc1 />}}`, []item{ + tstLeftNoMD, tstSC1, tstSCClose, tstRightNoMD, tstEOF}}, + // Issue 2498 + {"multiple self-closing", `{{< sc1 />}}{{< sc1 />}}`, []item{ + tstLeftNoMD, tstSC1, tstSCClose, tstRightNoMD, + tstLeftNoMD, tstSC1, tstSCClose, tstRightNoMD, tstEOF}}, + {"self-closing with param", `{{< sc1 param1 />}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, tstSCClose, tstRightNoMD, tstEOF}}, + {"multiple self-closing with param", `{{< sc1 param1 />}}{{< sc1 param1 />}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, tstSCClose, tstRightNoMD, + tstLeftNoMD, tstSC1, tstParam1, tstSCClose, tstRightNoMD, tstEOF}}, + {"multiple different self-closing with param", `{{< sc1 param1 />}}{{< sc2 param1 />}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, tstSCClose, tstRightNoMD, + tstLeftNoMD, tstSC2, tstParam1, tstSCClose, tstRightNoMD, tstEOF}}, + {"nested simple", `{{< sc1 >}}{{< sc2 >}}{{< /sc1 >}}`, []item{ + tstLeftNoMD, tstSC1, tstRightNoMD, + tstLeftNoMD, tstSC2, tstRightNoMD, + tstLeftNoMD, tstSCClose, tstSC1, tstRightNoMD, tstEOF}}, + {"nested complex", `{{< sc1 >}}ab{{% sc2 param1 %}}cd{{< sc3 >}}ef{{< /sc3 >}}gh{{% /sc2 %}}ij{{< /sc1 >}}kl`, []item{ + tstLeftNoMD, tstSC1, tstRightNoMD, + {tText, 0, "ab"}, + tstLeftMD, tstSC2, tstParam1, tstRightMD, + {tText, 0, "cd"}, + tstLeftNoMD, tstSC3, tstRightNoMD, + {tText, 0, "ef"}, + tstLeftNoMD, tstSCClose, tstSC3, tstRightNoMD, + {tText, 0, "gh"}, + tstLeftMD, tstSCClose, tstSC2, tstRightMD, + {tText, 0, "ij"}, + tstLeftNoMD, tstSCClose, tstSC1, tstRightNoMD, + {tText, 0, "kl"}, tstEOF, + }}, + + {"two quoted params", `{{< sc1 "param nr. 1" "param nr. 2" >}}`, []item{ + tstLeftNoMD, tstSC1, {tScParam, 0, "param nr. 1"}, {tScParam, 0, "param nr. 2"}, tstRightNoMD, tstEOF}}, + {"two named params", `{{< sc1 param1="Hello World" param2="p2Val">}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, tstVal, tstParam2, {tScParamVal, 0, "p2Val"}, tstRightNoMD, tstEOF}}, + {"escaped quotes", `{{< sc1 param1=\"Hello World\" >}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, tstVal, tstRightNoMD, tstEOF}}, + {"escaped quotes, positional param", `{{< sc1 \"param1\" >}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, tstRightNoMD, tstEOF}}, + {"escaped quotes inside escaped quotes", `{{< sc1 param1=\"Hello \"escaped\" World\" >}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, + {tScParamVal, 0, `Hello `}, {tError, 0, `got positional parameter 'escaped'. Cannot mix named and positional parameters`}}}, + {"escaped quotes inside nonescaped quotes", + `{{< sc1 param1="Hello \"escaped\" World" >}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, {tScParamVal, 0, `Hello "escaped" World`}, tstRightNoMD, tstEOF}}, + {"escaped quotes inside nonescaped quotes in positional param", + `{{< sc1 "Hello \"escaped\" World" >}}`, []item{ + tstLeftNoMD, tstSC1, {tScParam, 0, `Hello "escaped" World`}, tstRightNoMD, tstEOF}}, + {"unterminated quote", `{{< sc1 param2="Hello World>}}`, []item{ + tstLeftNoMD, tstSC1, tstParam2, {tError, 0, "unterminated quoted string in shortcode parameter-argument: 'Hello World>}}'"}}}, + {"one named param, one not", `{{< sc1 param1="Hello World" p2 >}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, tstVal, + {tError, 0, "got positional parameter 'p2'. Cannot mix named and positional parameters"}}}, + {"one named param, one quoted positional param", `{{< sc1 param1="Hello World" "And Universe" >}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, tstVal, + {tError, 0, "got quoted positional parameter. Cannot mix named and positional parameters"}}}, + {"one quoted positional param, one named param", `{{< sc1 "param1" param2="And Universe" >}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, + {tError, 0, "got named parameter 'param2'. Cannot mix named and positional parameters"}}}, + {"ono positional param, one not", `{{< sc1 param1 param2="Hello World">}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, + {tError, 0, "got named parameter 'param2'. Cannot mix named and positional parameters"}}}, + {"commented out", `{{</* sc1 */>}}`, []item{ + {tText, 0, "{{<"}, {tText, 0, " sc1 "}, {tText, 0, ">}}"}, tstEOF}}, + {"commented out, missing close", `{{</* sc1 >}}`, []item{ + {tError, 0, "comment must be closed"}}}, + {"commented out, misplaced close", `{{</* sc1 >}}*/`, []item{ + {tText, 0, "{{<"}, {tText, 0, " sc1 >}}"}, {tError, 0, "comment ends before the right shortcode delimiter"}}}, +} + +func TestShortcodeLexer(t *testing.T) { + t.Parallel() + for i, test := range shortCodeLexerTests { + items := collect(&test) + if !equal(items, test.items) { + t.Errorf("[%d] %s: got\n\t%v\nexpected\n\t%v", i, test.name, items, test.items) + } + } +} + +func BenchmarkShortcodeLexer(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, test := range shortCodeLexerTests { + items := collect(&test) + if !equal(items, test.items) { + b.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items) + } + } + } +} + +func collect(t *shortCodeLexerTest) (items []item) { + l := newShortcodeLexer(t.name, t.input, 0) + for { + item := l.nextItem() + items = append(items, item) + if item.typ == tEOF || item.typ == tError { + break + } + } + return +} + +// no positional checking, for now ... +func equal(i1, i2 []item) bool { + if len(i1) != len(i2) { + return false + } + for k := range i1 { + if i1[k].typ != i2[k].typ { + return false + } + if i1[k].val != i2[k].val { + return false + } + } + return true +} diff --git a/hugolib/site.go b/hugolib/site.go new file mode 100644 index 000000000..8aa1e087f --- /dev/null +++ b/hugolib/site.go @@ -0,0 +1,2127 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "errors" + "fmt" + "html/template" + "io" + "mime" + "net/url" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/gohugoio/hugo/config" + + "github.com/gohugoio/hugo/media" + + "github.com/bep/inflect" + + "sync/atomic" + + "github.com/fsnotify/fsnotify" + bp "github.com/gohugoio/hugo/bufferpool" + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/output" + "github.com/gohugoio/hugo/parser" + "github.com/gohugoio/hugo/source" + "github.com/gohugoio/hugo/tpl" + "github.com/gohugoio/hugo/transform" + "github.com/spf13/afero" + "github.com/spf13/cast" + "github.com/spf13/nitro" + "github.com/spf13/viper" +) + +var _ = transform.AbsURL + +// used to indicate if run as a test. +var testMode bool + +var defaultTimer *nitro.B + +// Site contains all the information relevant for constructing a static +// site. The basic flow of information is as follows: +// +// 1. A list of Files is parsed and then converted into Pages. +// +// 2. Pages contain sections (based on the file they were generated from), +// aliases and slugs (included in a pages frontmatter) which are the +// various targets that will get generated. There will be canonical +// listing. The canonical path can be overruled based on a pattern. +// +// 3. Taxonomies are created via configuration and will present some aspect of +// the final page and typically a perm url. +// +// 4. All Pages are passed through a template based on their desired +// layout based on numerous different elements. +// +// 5. The entire collection of files is written to disk. +type Site struct { + owner *HugoSites + + *PageCollections + + Files []*source.File + Taxonomies TaxonomyList + + // Plural is what we get in the folder, so keep track of this mapping + // to get the singular form from that value. + taxonomiesPluralSingular map[string]string + + // This is temporary, see https://github.com/gohugoio/hugo/issues/2835 + // Maps "actors-gerard-depardieu" to "Gérard Depardieu" when preserveTaxonomyNames + // is set. + taxonomiesOrigKey map[string]string + + Source source.Input + Sections Taxonomy + Info SiteInfo + Menus Menus + timer *nitro.B + + layoutHandler *output.LayoutHandler + + draftCount int + futureCount int + expiredCount int + Data map[string]interface{} + Language *helpers.Language + + disabledKinds map[string]bool + + // Output formats defined in site config per Page Kind, or some defaults + // if not set. + // Output formats defined in Page front matter will override these. + outputFormats map[string]output.Formats + + // All the output formats and media types available for this site. + // These values will be merged from the Hugo defaults, the site config and, + // finally, the language settings. + outputFormatsConfig output.Formats + mediaTypesConfig media.Types + + // We render each site for all the relevant output formats in serial with + // this rendering context pointing to the current one. + rc *siteRenderingContext + + // The output formats that we need to render this site in. This slice + // will be fixed once set. + // This will be the union of Site.Pages' outputFormats. + // This slice will be sorted. + renderFormats output.Formats + + // Logger etc. + *deps.Deps `json:"-"` + + siteStats *siteStats +} + +type siteRenderingContext struct { + output.Format +} + +func (s *Site) initRenderFormats() { + formatSet := make(map[string]bool) + formats := output.Formats{} + for _, p := range s.Pages { + for _, f := range p.outputFormats { + if !formatSet[f.Name] { + formats = append(formats, f) + formatSet[f.Name] = true + } + } + } + + sort.Sort(formats) + s.renderFormats = formats +} + +type siteStats struct { + pageCount int + pageCountRegular int +} + +func (s *Site) isEnabled(kind string) bool { + if kind == kindUnknown { + panic("Unknown kind") + } + return !s.disabledKinds[kind] +} + +// reset returns a new Site prepared for rebuild. +func (s *Site) reset() *Site { + return &Site{Deps: s.Deps, + layoutHandler: output.NewLayoutHandler(s.PathSpec.ThemeSet()), + disabledKinds: s.disabledKinds, + outputFormats: s.outputFormats, + outputFormatsConfig: s.outputFormatsConfig, + mediaTypesConfig: s.mediaTypesConfig, + Language: s.Language, + owner: s.owner, + PageCollections: newPageCollections()} +} + +// newSite creates a new site with the given configuration. +func newSite(cfg deps.DepsCfg) (*Site, error) { + c := newPageCollections() + + if cfg.Language == nil { + cfg.Language = helpers.NewDefaultLanguage(cfg.Cfg) + } + + disabledKinds := make(map[string]bool) + for _, disabled := range cast.ToStringSlice(cfg.Language.Get("disableKinds")) { + disabledKinds[disabled] = true + } + + var ( + mediaTypesConfig []map[string]interface{} + outputFormatsConfig []map[string]interface{} + + siteOutputFormatsConfig output.Formats + siteMediaTypesConfig media.Types + err error + ) + + // Add language last, if set, so it gets precedence. + for _, cfg := range []config.Provider{cfg.Cfg, cfg.Language} { + if cfg.IsSet("mediaTypes") { + mediaTypesConfig = append(mediaTypesConfig, cfg.GetStringMap("mediaTypes")) + } + if cfg.IsSet("outputFormats") { + outputFormatsConfig = append(outputFormatsConfig, cfg.GetStringMap("outputFormats")) + } + } + + siteMediaTypesConfig, err = media.DecodeTypes(mediaTypesConfig...) + if err != nil { + return nil, err + } + + siteOutputFormatsConfig, err = output.DecodeFormats(siteMediaTypesConfig, outputFormatsConfig...) + if err != nil { + return nil, err + } + + outputFormats, err := createSiteOutputFormats(siteOutputFormatsConfig, cfg.Language) + if err != nil { + return nil, err + } + + s := &Site{ + PageCollections: c, + layoutHandler: output.NewLayoutHandler(cfg.Cfg.GetString("themesDir") != ""), + Language: cfg.Language, + disabledKinds: disabledKinds, + outputFormats: outputFormats, + outputFormatsConfig: siteOutputFormatsConfig, + mediaTypesConfig: siteMediaTypesConfig, + } + + s.Info = newSiteInfo(siteBuilderCfg{s: s, pageCollections: c, language: s.Language}) + + return s, nil + +} + +// NewSite creates a new site with the given dependency configuration. +// The site will have a template system loaded and ready to use. +// Note: This is mainly used in single site tests. +func NewSite(cfg deps.DepsCfg) (*Site, error) { + s, err := newSite(cfg) + if err != nil { + return nil, err + } + + if err = applyDepsIfNeeded(cfg, s); err != nil { + return nil, err + } + + return s, nil +} + +// NewSiteDefaultLang creates a new site in the default language. +// The site will have a template system loaded and ready to use. +// Note: This is mainly used in single site tests. +func NewSiteDefaultLang(withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) { + v := viper.New() + loadDefaultSettingsFor(v) + return newSiteForLang(helpers.NewDefaultLanguage(v), withTemplate...) +} + +// NewEnglishSite creates a new site in English language. +// The site will have a template system loaded and ready to use. +// Note: This is mainly used in single site tests. +func NewEnglishSite(withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) { + v := viper.New() + loadDefaultSettingsFor(v) + return newSiteForLang(helpers.NewLanguage("en", v), withTemplate...) +} + +// newSiteForLang creates a new site in the given language. +func newSiteForLang(lang *helpers.Language, withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) { + withTemplates := func(templ tpl.TemplateHandler) error { + for _, wt := range withTemplate { + if err := wt(templ); err != nil { + return err + } + } + return nil + } + + cfg := deps.DepsCfg{WithTemplate: withTemplates, Language: lang, Cfg: lang} + + return NewSiteForCfg(cfg) + +} + +// NewSiteForCfg creates a new site for the given configuration. +// The site will have a template system loaded and ready to use. +// Note: This is mainly used in single site tests. +func NewSiteForCfg(cfg deps.DepsCfg) (*Site, error) { + s, err := newSite(cfg) + + if err != nil { + return nil, err + } + + if err := applyDepsIfNeeded(cfg, s); err != nil { + return nil, err + } + return s, nil +} + +type SiteInfo struct { + // atomic requires 64-bit alignment for struct field access + // According to the docs, " The first word in a global variable or in an + // allocated struct or slice can be relied upon to be 64-bit aligned." + // Moving paginationPageCount to the top of this struct didn't do the + // magic, maybe due to the way SiteInfo is embedded. + // Adding the 4 byte padding below does the trick. + _ [4]byte + paginationPageCount uint64 + + Taxonomies TaxonomyList + Authors AuthorList + Social SiteSocial + *PageCollections + Files *[]*source.File + Menus *Menus + Hugo *HugoInfo + Title string + RSSLink string + Author map[string]interface{} + LanguageCode string + DisqusShortname string + GoogleAnalytics string + Copyright string + LastChange time.Time + Permalinks PermalinkOverrides + Params map[string]interface{} + BuildDrafts bool + canonifyURLs bool + relativeURLs bool + uglyURLs bool + preserveTaxonomyNames bool + Data *map[string]interface{} + + owner *HugoSites + s *Site + multilingual *Multilingual + Language *helpers.Language + LanguagePrefix string + Languages helpers.Languages + defaultContentLanguageInSubdir bool + sectionPagesMenu string +} + +func (s *SiteInfo) String() string { + return fmt.Sprintf("Site(%q)", s.Title) +} + +func (s *SiteInfo) BaseURL() template.URL { + return template.URL(s.s.PathSpec.BaseURL.String()) +} + +// Used in tests. + +type siteBuilderCfg struct { + language *helpers.Language + s *Site + pageCollections *PageCollections + baseURL string +} + +// TODO(bep) get rid of this +func newSiteInfo(cfg siteBuilderCfg) SiteInfo { + return SiteInfo{ + s: cfg.s, + multilingual: newMultiLingualForLanguage(cfg.language), + PageCollections: cfg.pageCollections, + Params: make(map[string]interface{}), + } +} + +// SiteSocial is a place to put social details on a site level. These are the +// standard keys that themes will expect to have available, but can be +// expanded to any others on a per site basis +// github +// facebook +// facebook_admin +// twitter +// twitter_domain +// googleplus +// pinterest +// instagram +// youtube +// linkedin +type SiteSocial map[string]string + +// Param is a convenience method to do lookups in SiteInfo's Params map. +// +// This method is also implemented on Page and Node. +func (s *SiteInfo) Param(key interface{}) (interface{}, error) { + keyStr, err := cast.ToStringE(key) + if err != nil { + return nil, err + } + keyStr = strings.ToLower(keyStr) + return s.Params[keyStr], nil +} + +func (s *SiteInfo) IsMultiLingual() bool { + return len(s.Languages) > 1 +} + +func (s *SiteInfo) refLink(ref string, page *Page, relative bool, outputFormat string) (string, error) { + var refURL *url.URL + var err error + + refURL, err = url.Parse(ref) + + if err != nil { + return "", err + } + + var target *Page + var link string + + if refURL.Path != "" { + target := s.getPage(KindPage, refURL.Path) + + if target == nil { + return "", fmt.Errorf("No page found with path or logical name \"%s\".\n", refURL.Path) + } + + var permalinker Permalinker = target + + if outputFormat != "" { + o := target.OutputFormats().Get(outputFormat) + + if o == nil { + return "", fmt.Errorf("Output format %q not found for page %q", outputFormat, refURL.Path) + } + permalinker = o + } + + if relative { + link = permalinker.RelPermalink() + } else { + link = permalinker.Permalink() + } + } + + if refURL.Fragment != "" { + link = link + "#" + refURL.Fragment + + if refURL.Path != "" && target != nil && !target.getRenderingConfig().PlainIDAnchors { + link = link + ":" + target.UniqueID() + } else if page != nil && !page.getRenderingConfig().PlainIDAnchors { + link = link + ":" + page.UniqueID() + } + } + + return link, nil +} + +// Ref will give an absolute URL to ref in the given Page. +func (s *SiteInfo) Ref(ref string, page *Page, options ...string) (string, error) { + outputFormat := "" + if len(options) > 0 { + outputFormat = options[0] + } + + return s.refLink(ref, page, false, outputFormat) +} + +// RelRef will give an relative URL to ref in the given Page. +func (s *SiteInfo) RelRef(ref string, page *Page, options ...string) (string, error) { + outputFormat := "" + if len(options) > 0 { + outputFormat = options[0] + } + + return s.refLink(ref, page, true, outputFormat) +} + +// SourceRelativeLink attempts to convert any source page relative links (like [../another.md]) into absolute links +func (s *SiteInfo) SourceRelativeLink(ref string, currentPage *Page) (string, error) { + var refURL *url.URL + var err error + + refURL, err = url.Parse(strings.TrimPrefix(ref, currentPage.getRenderingConfig().SourceRelativeLinksProjectFolder)) + if err != nil { + return "", err + } + + if refURL.Scheme != "" { + // Not a relative source level path + return ref, nil + } + + var target *Page + var link string + + if refURL.Path != "" { + refPath := filepath.Clean(filepath.FromSlash(refURL.Path)) + + if strings.IndexRune(refPath, os.PathSeparator) == 0 { // filepath.IsAbs fails to me. + refPath = refPath[1:] + } else { + if currentPage != nil { + refPath = filepath.Join(currentPage.Source.Dir(), refURL.Path) + } + } + + for _, page := range s.AllRegularPages { + if page.Source.Path() == refPath { + target = page + break + } + } + // need to exhaust the test, then try with the others :/ + // if the refPath doesn't end in a filename with extension `.md`, then try with `.md` , and then `/index.md` + mdPath := strings.TrimSuffix(refPath, string(os.PathSeparator)) + ".md" + for _, page := range s.AllRegularPages { + if page.Source.Path() == mdPath { + target = page + break + } + } + indexPath := filepath.Join(refPath, "index.md") + for _, page := range s.AllRegularPages { + if page.Source.Path() == indexPath { + target = page + break + } + } + + if target == nil { + return "", fmt.Errorf("No page found for \"%s\" on page \"%s\".\n", ref, currentPage.Source.Path()) + } + + link = target.RelPermalink() + + } + + if refURL.Fragment != "" { + link = link + "#" + refURL.Fragment + + if refURL.Path != "" && target != nil && !target.getRenderingConfig().PlainIDAnchors { + link = link + ":" + target.UniqueID() + } else if currentPage != nil && !currentPage.getRenderingConfig().PlainIDAnchors { + link = link + ":" + currentPage.UniqueID() + } + } + + return link, nil +} + +// SourceRelativeLinkFile attempts to convert any non-md source relative links (like [../another.gif]) into absolute links +func (s *SiteInfo) SourceRelativeLinkFile(ref string, currentPage *Page) (string, error) { + var refURL *url.URL + var err error + + refURL, err = url.Parse(strings.TrimPrefix(ref, currentPage.getRenderingConfig().SourceRelativeLinksProjectFolder)) + if err != nil { + return "", err + } + + if refURL.Scheme != "" { + // Not a relative source level path + return ref, nil + } + + var target *source.File + var link string + + if refURL.Path != "" { + refPath := filepath.Clean(filepath.FromSlash(refURL.Path)) + + if strings.IndexRune(refPath, os.PathSeparator) == 0 { // filepath.IsAbs fails to me. + refPath = refPath[1:] + } else { + if currentPage != nil { + refPath = filepath.Join(currentPage.Source.Dir(), refURL.Path) + } + } + + for _, file := range *s.Files { + if file.Path() == refPath { + target = file + break + } + } + + if target == nil { + return "", fmt.Errorf("No file found for \"%s\" on page \"%s\".\n", ref, currentPage.Source.Path()) + } + + link = target.Path() + return "/" + filepath.ToSlash(link), nil + } + + return "", fmt.Errorf("failed to find a file to match \"%s\" on page \"%s\"", ref, currentPage.Source.Path()) +} + +func (s *SiteInfo) addToPaginationPageCount(cnt uint64) { + atomic.AddUint64(&s.paginationPageCount, cnt) +} + +type runmode struct { + Watching bool +} + +func (s *Site) running() bool { + return s.owner.runMode.Watching +} + +func init() { + defaultTimer = nitro.Initalize() +} + +func (s *Site) timerStep(step string) { + if s.timer == nil { + s.timer = defaultTimer + } + s.timer.Step(step) +} + +type whatChanged struct { + source bool + other bool +} + +// RegisterMediaTypes will register the Site's media types in the mime +// package, so it will behave correctly with Hugo's built-in server. +func (s *Site) RegisterMediaTypes() { + for _, mt := range s.mediaTypesConfig { + // The last one will win if there are any duplicates. + _ = mime.AddExtensionType("."+mt.Suffix, mt.Type()+"; charset=utf-8") + } +} + +// reBuild partially rebuilds a site given the filesystem events. +// It returns whetever the content source was changed. +func (s *Site) reProcess(events []fsnotify.Event) (whatChanged, error) { + s.Log.DEBUG.Printf("Rebuild for events %q", events) + + s.timerStep("initialize rebuild") + + // First we need to determine what changed + + sourceChanged := []fsnotify.Event{} + sourceReallyChanged := []fsnotify.Event{} + tmplChanged := []fsnotify.Event{} + dataChanged := []fsnotify.Event{} + i18nChanged := []fsnotify.Event{} + shortcodesChanged := make(map[string]bool) + // prevent spamming the log on changes + logger := helpers.NewDistinctFeedbackLogger() + seen := make(map[fsnotify.Event]bool) + + for _, ev := range events { + // Avoid processing the same event twice. + if seen[ev] { + continue + } + seen[ev] = true + + if s.isContentDirEvent(ev) { + logger.Println("Source changed", ev) + sourceChanged = append(sourceChanged, ev) + } + if s.isLayoutDirEvent(ev) { + logger.Println("Template changed", ev) + tmplChanged = append(tmplChanged, ev) + + if strings.Contains(ev.Name, "shortcodes") { + clearIsInnerShortcodeCache() + shortcode := filepath.Base(ev.Name) + shortcode = strings.TrimSuffix(shortcode, filepath.Ext(shortcode)) + shortcodesChanged[shortcode] = true + } + } + if s.isDataDirEvent(ev) { + logger.Println("Data changed", ev) + dataChanged = append(dataChanged, ev) + } + if s.isI18nEvent(ev) { + logger.Println("i18n changed", ev) + i18nChanged = append(dataChanged, ev) + } + } + + if len(tmplChanged) > 0 || len(i18nChanged) > 0 { + sites := s.owner.Sites + first := sites[0] + + // TOD(bep) globals clean + if err := first.Deps.LoadResources(); err != nil { + s.Log.ERROR.Println(err) + } + + s.TemplateHandler().PrintErrors() + + for i := 1; i < len(sites); i++ { + site := sites[i] + var err error + site.Deps, err = first.Deps.ForLanguage(site.Language) + if err != nil { + return whatChanged{}, err + } + } + + s.timerStep("template prep") + } + + if len(dataChanged) > 0 { + if err := s.readDataFromSourceFS(); err != nil { + s.Log.ERROR.Println(err) + } + } + + // If a content file changes, we need to reload only it and re-render the entire site. + + // First step is to read the changed files and (re)place them in site.AllPages + // This includes processing any meta-data for that content + + // The second step is to convert the content into HTML + // This includes processing any shortcodes that may be present. + + // We do this in parallel... even though it's likely only one file at a time. + // We need to process the reading prior to the conversion for each file, but + // we can convert one file while another one is still reading. + errs := make(chan error, 2) + readResults := make(chan HandledResult) + filechan := make(chan *source.File) + convertResults := make(chan HandledResult) + pageChan := make(chan *Page) + fileConvChan := make(chan *source.File) + coordinator := make(chan bool) + + wg := &sync.WaitGroup{} + wg.Add(2) + for i := 0; i < 2; i++ { + go sourceReader(s, filechan, readResults, wg) + } + + wg2 := &sync.WaitGroup{} + wg2.Add(4) + for i := 0; i < 2; i++ { + go fileConverter(s, fileConvChan, convertResults, wg2) + go pageConverter(pageChan, convertResults, wg2) + } + + sp := source.NewSourceSpec(s.Cfg, s.Fs) + fs := sp.NewFilesystem("") + + for _, ev := range sourceChanged { + // The incrementalReadCollator below will also make changes to the site's pages, + // so we do this first to prevent races. + if ev.Op&fsnotify.Remove == fsnotify.Remove { + //remove the file & a create will follow + path, _ := helpers.GetRelativePath(ev.Name, s.getContentDir(ev.Name)) + s.removePageByPathPrefix(path) + continue + } + + // Some editors (Vim) sometimes issue only a Rename operation when writing an existing file + // Sometimes a rename operation means that file has been renamed other times it means + // it's been updated + if ev.Op&fsnotify.Rename == fsnotify.Rename { + // If the file is still on disk, it's only been updated, if it's not, it's been moved + if ex, err := afero.Exists(s.Fs.Source, ev.Name); !ex || err != nil { + path, _ := helpers.GetRelativePath(ev.Name, s.getContentDir(ev.Name)) + s.removePageByPath(path) + continue + } + } + + // ignore files shouldn't be proceed + if fi, err := s.Fs.Source.Stat(ev.Name); err != nil { + continue + } else { + if ok, err := fs.ShouldRead(ev.Name, fi); err != nil || !ok { + continue + } + } + + sourceReallyChanged = append(sourceReallyChanged, ev) + } + + go incrementalReadCollator(s, readResults, pageChan, fileConvChan, coordinator, errs) + go converterCollator(convertResults, errs) + + for _, ev := range sourceReallyChanged { + + file, err := s.reReadFile(ev.Name) + + if err != nil { + s.Log.ERROR.Println("Error reading file", ev.Name, ";", err) + } + + if file != nil { + filechan <- file + } + + } + + for shortcode := range shortcodesChanged { + // There are certain scenarios that, when a shortcode changes, + // it isn't sufficient to just rerender the already parsed shortcode. + // One example is if the user adds a new shortcode to the content file first, + // and then creates the shortcode on the file system. + // To handle these scenarios, we must do a full reprocessing of the + // pages that keeps a reference to the changed shortcode. + pagesWithShortcode := s.findPagesByShortcode(shortcode) + for _, p := range pagesWithShortcode { + p.rendered = false + pageChan <- p + } + } + + // we close the filechan as we have sent everything we want to send to it. + // this will tell the sourceReaders to stop iterating on that channel + close(filechan) + + // waiting for the sourceReaders to all finish + wg.Wait() + // Now closing readResults as this will tell the incrementalReadCollator to + // stop iterating over that. + close(readResults) + + // once readResults is finished it will close coordinator and move along + <-coordinator + // allow that routine to finish, then close page & fileconvchan as we've sent + // everything to them we need to. + close(pageChan) + close(fileConvChan) + + wg2.Wait() + close(convertResults) + + s.timerStep("read & convert pages from source") + + for i := 0; i < 2; i++ { + err := <-errs + if err != nil { + s.Log.ERROR.Println(err) + } + } + + changed := whatChanged{ + source: len(sourceChanged) > 0, + other: len(tmplChanged) > 0 || len(i18nChanged) > 0 || len(dataChanged) > 0, + } + + return changed, nil + +} + +func (s *Site) loadData(sources []source.Input) (err error) { + s.Log.DEBUG.Printf("Load Data from %d source(s)", len(sources)) + s.Data = make(map[string]interface{}) + var current map[string]interface{} + for _, currentSource := range sources { + for _, r := range currentSource.Files() { + // Crawl in data tree to insert data + current = s.Data + for _, key := range strings.Split(r.Dir(), helpers.FilePathSeparator) { + if key != "" { + if _, ok := current[key]; !ok { + current[key] = make(map[string]interface{}) + } + current = current[key].(map[string]interface{}) + } + } + + data, err := s.readData(r) + if err != nil { + s.Log.WARN.Printf("Failed to read data from %s: %s", filepath.Join(r.Path(), r.LogicalName()), err) + continue + } + + if data == nil { + continue + } + + // Copy content from current to data when needed + if _, ok := current[r.BaseFileName()]; ok { + data := data.(map[string]interface{}) + + for key, value := range current[r.BaseFileName()].(map[string]interface{}) { + if _, override := data[key]; override { + // filepath.Walk walks the files in lexical order, '/' comes before '.' + // this warning could happen if + // 1. A theme uses the same key; the main data folder wins + // 2. A sub folder uses the same key: the sub folder wins + s.Log.WARN.Printf("Data for key '%s' in path '%s' is overridden in subfolder", key, r.Path()) + } + data[key] = value + } + } + + // Insert data + current[r.BaseFileName()] = data + } + } + + return +} + +func (s *Site) readData(f *source.File) (interface{}, error) { + switch f.Extension() { + case "yaml", "yml": + return parser.HandleYAMLMetaData(f.Bytes()) + case "json": + return parser.HandleJSONMetaData(f.Bytes()) + case "toml": + return parser.HandleTOMLMetaData(f.Bytes()) + default: + return nil, fmt.Errorf("Data not supported for extension '%s'", f.Extension()) + } +} + +func (s *Site) readDataFromSourceFS() error { + sp := source.NewSourceSpec(s.Cfg, s.Fs) + dataSources := make([]source.Input, 0, 2) + dataSources = append(dataSources, sp.NewFilesystem(s.absDataDir())) + + // have to be last - duplicate keys in earlier entries will win + themeDataDir, err := s.PathSpec.GetThemeDataDirPath() + if err == nil { + dataSources = append(dataSources, sp.NewFilesystem(themeDataDir)) + } + + err = s.loadData(dataSources) + s.timerStep("load data") + return err +} + +func (s *Site) process(config BuildCfg) (err error) { + s.timerStep("Go initialization") + if err = s.initialize(); err != nil { + return + } + s.timerStep("initialize") + + if err = s.readDataFromSourceFS(); err != nil { + return + } + + s.timerStep("load i18n") + return s.createPages() + +} + +func (s *Site) setupSitePages() { + var siteLastChange time.Time + + for i, page := range s.RegularPages { + if i < len(s.RegularPages)-1 { + page.Next = s.RegularPages[i+1] + } + + if i > 0 { + page.Prev = s.RegularPages[i-1] + } + + // Determine Site.Info.LastChange + // Note that the logic to determine which date to use for Lastmod + // is already applied, so this is *the* date to use. + // We cannot just pick the last page in the default sort, because + // that may not be ordered by date. + if page.Lastmod.After(siteLastChange) { + siteLastChange = page.Lastmod + } + } + + s.Info.LastChange = siteLastChange +} + +func (s *Site) render(outFormatIdx int) (err error) { + + if outFormatIdx == 0 { + if err = s.preparePages(); err != nil { + return + } + s.timerStep("prepare pages") + + // Note that even if disableAliases is set, the aliases themselves are + // preserved on page. The motivation with this is to be able to generate + // 301 redirects in a .htacess file and similar using a custom output format. + if !s.Cfg.GetBool("disableAliases") { + // Aliases must be rendered before pages. + // Some sites, Hugo docs included, have faulty alias definitions that point + // to itself or another real page. These will be overwritten in the next + // step. + if err = s.renderAliases(); err != nil { + return + } + s.timerStep("render and write aliases") + } + + } + + if err = s.renderPages(); err != nil { + return + } + + s.timerStep("render and write pages") + + // TODO(bep) render consider this, ref. render404 etc. + if outFormatIdx > 0 { + return + } + + if err = s.renderSitemap(); err != nil { + return + } + s.timerStep("render and write Sitemap") + + if err = s.renderRobotsTXT(); err != nil { + return + } + s.timerStep("render and write robots.txt") + + if err = s.render404(); err != nil { + return + } + s.timerStep("render and write 404") + + return +} + +func (s *Site) Initialise() (err error) { + return s.initialize() +} + +func (s *Site) initialize() (err error) { + defer s.initializeSiteInfo() + s.Menus = Menus{} + + // May be supplied in tests. + if s.Source != nil && len(s.Source.Files()) > 0 { + s.Log.DEBUG.Println("initialize: Source is already set") + return + } + + if err = s.checkDirectories(); err != nil { + return err + } + + staticDir := s.PathSpec.GetStaticDirPath() + "/" + + sp := source.NewSourceSpec(s.Cfg, s.Fs) + s.Source = sp.NewFilesystem(s.absContentDir(), staticDir) + + return +} + +// HomeAbsURL is a convenience method giving the absolute URL to the home page. +func (s *SiteInfo) HomeAbsURL() string { + base := "" + if s.IsMultiLingual() { + base = s.Language.Lang + } + return s.owner.AbsURL(base, false) +} + +// SitemapAbsURL is a convenience method giving the absolute URL to the sitemap. +func (s *SiteInfo) SitemapAbsURL() string { + sitemapDefault := parseSitemap(s.s.Cfg.GetStringMap("sitemap")) + p := s.HomeAbsURL() + if !strings.HasSuffix(p, "/") { + p += "/" + } + p += sitemapDefault.Filename + return p +} + +func (s *Site) initializeSiteInfo() { + var ( + lang = s.Language + languages helpers.Languages + ) + + if s.owner != nil && s.owner.multilingual != nil { + languages = s.owner.multilingual.Languages + } + + params := lang.Params() + + permalinks := make(PermalinkOverrides) + for k, v := range s.Cfg.GetStringMapString("permalinks") { + permalinks[k] = pathPattern(v) + } + + defaultContentInSubDir := s.Cfg.GetBool("defaultContentLanguageInSubdir") + defaultContentLanguage := s.Cfg.GetString("defaultContentLanguage") + + languagePrefix := "" + if s.multilingualEnabled() && (defaultContentInSubDir || lang.Lang != defaultContentLanguage) { + languagePrefix = "/" + lang.Lang + } + + var multilingual *Multilingual + if s.owner != nil { + multilingual = s.owner.multilingual + } + + s.Info = SiteInfo{ + Title: lang.GetString("title"), + Author: lang.GetStringMap("author"), + Social: lang.GetStringMapString("social"), + LanguageCode: lang.GetString("languageCode"), + Copyright: lang.GetString("copyright"), + DisqusShortname: lang.GetString("disqusShortname"), + multilingual: multilingual, + Language: lang, + LanguagePrefix: languagePrefix, + Languages: languages, + defaultContentLanguageInSubdir: defaultContentInSubDir, + sectionPagesMenu: lang.GetString("sectionPagesMenu"), + GoogleAnalytics: lang.GetString("googleAnalytics"), + BuildDrafts: s.Cfg.GetBool("buildDrafts"), + canonifyURLs: s.Cfg.GetBool("canonifyURLs"), + relativeURLs: s.Cfg.GetBool("relativeURLs"), + uglyURLs: s.Cfg.GetBool("uglyURLs"), + preserveTaxonomyNames: lang.GetBool("preserveTaxonomyNames"), + PageCollections: s.PageCollections, + Files: &s.Files, + Menus: &s.Menus, + Params: params, + Permalinks: permalinks, + Data: &s.Data, + owner: s.owner, + s: s, + } + + rssOutputFormat, found := s.outputFormats[KindHome].GetByName(output.RSSFormat.Name) + + if found { + s.Info.RSSLink = s.permalink(rssOutputFormat.BaseFilename()) + } +} + +func (s *Site) dataDir() string { + return s.Cfg.GetString("dataDir") +} + +func (s *Site) absDataDir() string { + return s.PathSpec.AbsPathify(s.dataDir()) +} + +func (s *Site) i18nDir() string { + return s.Cfg.GetString("i18nDir") +} + +func (s *Site) absI18nDir() string { + return s.PathSpec.AbsPathify(s.i18nDir()) +} + +func (s *Site) isI18nEvent(e fsnotify.Event) bool { + if s.getI18nDir(e.Name) != "" { + return true + } + return s.getThemeI18nDir(e.Name) != "" +} + +func (s *Site) getI18nDir(path string) string { + return s.getRealDir(s.absI18nDir(), path) +} + +func (s *Site) getThemeI18nDir(path string) string { + if !s.PathSpec.ThemeSet() { + return "" + } + return s.getRealDir(filepath.Join(s.PathSpec.GetThemeDir(), s.i18nDir()), path) +} + +func (s *Site) isDataDirEvent(e fsnotify.Event) bool { + if s.getDataDir(e.Name) != "" { + return true + } + return s.getThemeDataDir(e.Name) != "" +} + +func (s *Site) getDataDir(path string) string { + return s.getRealDir(s.absDataDir(), path) +} + +func (s *Site) getThemeDataDir(path string) string { + if !s.PathSpec.ThemeSet() { + return "" + } + return s.getRealDir(filepath.Join(s.PathSpec.GetThemeDir(), s.dataDir()), path) +} + +func (s *Site) layoutDir() string { + return s.Cfg.GetString("layoutDir") +} + +func (s *Site) isLayoutDirEvent(e fsnotify.Event) bool { + if s.getLayoutDir(e.Name) != "" { + return true + } + return s.getThemeLayoutDir(e.Name) != "" +} + +func (s *Site) getLayoutDir(path string) string { + return s.getRealDir(s.PathSpec.GetLayoutDirPath(), path) +} + +func (s *Site) getThemeLayoutDir(path string) string { + if !s.PathSpec.ThemeSet() { + return "" + } + return s.getRealDir(filepath.Join(s.PathSpec.GetThemeDir(), s.layoutDir()), path) +} + +func (s *Site) absContentDir() string { + return s.PathSpec.AbsPathify(s.Cfg.GetString("contentDir")) +} + +func (s *Site) isContentDirEvent(e fsnotify.Event) bool { + return s.getContentDir(e.Name) != "" +} + +func (s *Site) getContentDir(path string) string { + return s.getRealDir(s.absContentDir(), path) +} + +// getRealDir gets the base path of the given path, also handling the case where +// base is a symlinked folder. +func (s *Site) getRealDir(base, path string) string { + + if strings.HasPrefix(path, base) { + return base + } + + realDir, err := helpers.GetRealPath(s.Fs.Source, base) + + if err != nil { + if !os.IsNotExist(err) { + s.Log.ERROR.Printf("Failed to get real path for %s: %s", path, err) + } + return "" + } + + if strings.HasPrefix(path, realDir) { + return realDir + } + + return "" +} + +func (s *Site) absPublishDir() string { + return s.PathSpec.AbsPathify(s.Cfg.GetString("publishDir")) +} + +func (s *Site) checkDirectories() (err error) { + if b, _ := helpers.DirExists(s.absContentDir(), s.Fs.Source); !b { + return errors.New("No source directory found, expecting to find it at " + s.absContentDir()) + } + return +} + +// reReadFile resets file to be read from disk again +func (s *Site) reReadFile(absFilePath string) (*source.File, error) { + s.Log.INFO.Println("rereading", absFilePath) + var file *source.File + + reader, err := source.NewLazyFileReader(s.Fs.Source, absFilePath) + if err != nil { + return nil, err + } + + sp := source.NewSourceSpec(s.Cfg, s.Fs) + file, err = sp.NewFileFromAbs(s.getContentDir(absFilePath), absFilePath, reader) + + if err != nil { + return nil, err + } + + return file, nil +} + +func (s *Site) readPagesFromSource() chan error { + if s.Source == nil { + panic(fmt.Sprintf("s.Source not set %s", s.absContentDir())) + } + + s.Log.DEBUG.Printf("Read %d pages from source", len(s.Source.Files())) + + errs := make(chan error) + if len(s.Source.Files()) < 1 { + close(errs) + return errs + } + + files := s.Source.Files() + results := make(chan HandledResult) + filechan := make(chan *source.File) + wg := &sync.WaitGroup{} + numWorkers := getGoMaxProcs() * 4 + wg.Add(numWorkers) + for i := 0; i < numWorkers; i++ { + go sourceReader(s, filechan, results, wg) + } + + // we can only have exactly one result collator, since it makes changes that + // must be synchronized. + go readCollator(s, results, errs) + + for _, file := range files { + filechan <- file + } + + close(filechan) + wg.Wait() + close(results) + + return errs +} + +func (s *Site) convertSource() chan error { + errs := make(chan error) + results := make(chan HandledResult) + pageChan := make(chan *Page) + fileConvChan := make(chan *source.File) + numWorkers := getGoMaxProcs() * 4 + wg := &sync.WaitGroup{} + + for i := 0; i < numWorkers; i++ { + wg.Add(2) + go fileConverter(s, fileConvChan, results, wg) + go pageConverter(pageChan, results, wg) + } + + go converterCollator(results, errs) + + for _, p := range s.rawAllPages { + if p.shouldBuild() { + pageChan <- p + } + } + + for _, f := range s.Files { + fileConvChan <- f + } + + close(pageChan) + close(fileConvChan) + wg.Wait() + close(results) + + return errs +} + +func (s *Site) createPages() error { + readErrs := <-s.readPagesFromSource() + s.timerStep("read pages from source") + + renderErrs := <-s.convertSource() + s.timerStep("convert source") + + if renderErrs == nil && readErrs == nil { + return nil + } + if renderErrs == nil { + return readErrs + } + if readErrs == nil { + return renderErrs + } + + return fmt.Errorf("%s\n%s", readErrs, renderErrs) +} + +func sourceReader(s *Site, files <-chan *source.File, results chan<- HandledResult, wg *sync.WaitGroup) { + defer wg.Done() + for file := range files { + readSourceFile(s, file, results) + } +} + +func readSourceFile(s *Site, file *source.File, results chan<- HandledResult) { + h := NewMetaHandler(file.Extension()) + if h != nil { + h.Read(file, s, results) + } else { + s.Log.ERROR.Println("Unsupported File Type", file.Path()) + } +} + +func pageConverter(pages <-chan *Page, results HandleResults, wg *sync.WaitGroup) { + defer wg.Done() + for page := range pages { + var h *MetaHandle + if page.Markup != "" { + h = NewMetaHandler(page.Markup) + } else { + h = NewMetaHandler(page.File.Extension()) + } + if h != nil { + // Note that we convert pages from the site's rawAllPages collection + // Which may contain pages from multiple sites, so we use the Page's site + // for the conversion. + h.Convert(page, page.s, results) + } + } +} + +func fileConverter(s *Site, files <-chan *source.File, results HandleResults, wg *sync.WaitGroup) { + defer wg.Done() + for file := range files { + h := NewMetaHandler(file.Extension()) + if h != nil { + h.Convert(file, s, results) + } + } +} + +func converterCollator(results <-chan HandledResult, errs chan<- error) { + errMsgs := []string{} + for r := range results { + if r.err != nil { + errMsgs = append(errMsgs, r.err.Error()) + continue + } + } + if len(errMsgs) == 0 { + errs <- nil + return + } + errs <- fmt.Errorf("Errors rendering pages: %s", strings.Join(errMsgs, "\n")) +} + +func (s *Site) replaceFile(sf *source.File) { + for i, f := range s.Files { + if f.Path() == sf.Path() { + s.Files[i] = sf + return + } + } + + // If a match isn't found, then append it + s.Files = append(s.Files, sf) +} + +func incrementalReadCollator(s *Site, results <-chan HandledResult, pageChan chan *Page, fileConvChan chan *source.File, coordinator chan bool, errs chan<- error) { + errMsgs := []string{} + for r := range results { + if r.err != nil { + errMsgs = append(errMsgs, r.Error()) + continue + } + + if r.page == nil { + s.replaceFile(r.file) + fileConvChan <- r.file + } else { + s.replacePage(r.page) + pageChan <- r.page + } + } + + s.rawAllPages.Sort() + close(coordinator) + + if len(errMsgs) == 0 { + errs <- nil + return + } + errs <- fmt.Errorf("Errors reading pages: %s", strings.Join(errMsgs, "\n")) +} + +func readCollator(s *Site, results <-chan HandledResult, errs chan<- error) { + if s.PageCollections == nil { + panic("No page collections") + } + errMsgs := []string{} + for r := range results { + if r.err != nil { + errMsgs = append(errMsgs, r.Error()) + continue + } + + // !page == file + if r.page == nil { + s.Files = append(s.Files, r.file) + } else { + s.addPage(r.page) + } + } + + s.rawAllPages.Sort() + if len(errMsgs) == 0 { + errs <- nil + return + } + errs <- fmt.Errorf("Errors reading pages: %s", strings.Join(errMsgs, "\n")) +} + +func (s *Site) buildSiteMeta() (err error) { + defer s.timerStep("build Site meta") + + if len(s.Pages) == 0 { + return + } + + s.assembleTaxonomies() + + for _, p := range s.AllPages { + // this depends on taxonomies + p.setValuesForKind(s) + } + + return +} + +func (s *Site) getMenusFromConfig() Menus { + + ret := Menus{} + + if menus := s.Language.GetStringMap("menu"); menus != nil { + for name, menu := range menus { + m, err := cast.ToSliceE(menu) + if err != nil { + s.Log.ERROR.Printf("unable to process menus in site config\n") + s.Log.ERROR.Println(err) + } else { + for _, entry := range m { + s.Log.DEBUG.Printf("found menu: %q, in site config\n", name) + + menuEntry := MenuEntry{Menu: name} + ime, err := cast.ToStringMapE(entry) + if err != nil { + s.Log.ERROR.Printf("unable to process menus in site config\n") + s.Log.ERROR.Println(err) + } + + menuEntry.marshallMap(ime) + menuEntry.URL = s.Info.createNodeMenuEntryURL(menuEntry.URL) + + if ret[name] == nil { + ret[name] = &Menu{} + } + *ret[name] = ret[name].add(&menuEntry) + } + } + } + return ret + } + return ret +} + +func (s *SiteInfo) createNodeMenuEntryURL(in string) string { + + if !strings.HasPrefix(in, "/") { + return in + } + // make it match the nodes + menuEntryURL := in + menuEntryURL = helpers.SanitizeURLKeepTrailingSlash(s.s.PathSpec.URLize(menuEntryURL)) + if !s.canonifyURLs { + menuEntryURL = helpers.AddContextRoot(s.s.PathSpec.BaseURL.String(), menuEntryURL) + } + return menuEntryURL +} + +func (s *Site) assembleMenus() { + s.Menus = Menus{} + + type twoD struct { + MenuName, EntryName string + } + flat := map[twoD]*MenuEntry{} + children := map[twoD]Menu{} + + // add menu entries from config to flat hash + menuConfig := s.getMenusFromConfig() + for name, menu := range menuConfig { + for _, me := range *menu { + flat[twoD{name, me.KeyName()}] = me + } + } + + sectionPagesMenu := s.Info.sectionPagesMenu + pages := s.Pages + + if sectionPagesMenu != "" { + for _, p := range pages { + if p.Kind == KindSection { + // From Hugo 0.22 we have nested sections, but until we get a + // feel of how that would work in this setting, let us keep + // this menu for the top level only. + id := p.Section() + if _, ok := flat[twoD{sectionPagesMenu, id}]; ok { + continue + } + + me := MenuEntry{Identifier: id, + Name: p.LinkTitle(), + Weight: p.Weight, + URL: p.RelPermalink()} + flat[twoD{sectionPagesMenu, me.KeyName()}] = &me + } + } + } + + // Add menu entries provided by pages + for _, p := range pages { + for name, me := range p.Menus() { + if _, ok := flat[twoD{name, me.KeyName()}]; ok { + s.Log.ERROR.Printf("Two or more menu items have the same name/identifier in Menu %q: %q.\nRename or set an unique identifier.\n", name, me.KeyName()) + continue + } + flat[twoD{name, me.KeyName()}] = me + } + } + + // Create Children Menus First + for _, e := range flat { + if e.Parent != "" { + children[twoD{e.Menu, e.Parent}] = children[twoD{e.Menu, e.Parent}].add(e) + } + } + + // Placing Children in Parents (in flat) + for p, childmenu := range children { + _, ok := flat[twoD{p.MenuName, p.EntryName}] + if !ok { + // if parent does not exist, create one without a URL + flat[twoD{p.MenuName, p.EntryName}] = &MenuEntry{Name: p.EntryName, URL: ""} + } + flat[twoD{p.MenuName, p.EntryName}].Children = childmenu + } + + // Assembling Top Level of Tree + for menu, e := range flat { + if e.Parent == "" { + _, ok := s.Menus[menu.MenuName] + if !ok { + s.Menus[menu.MenuName] = &Menu{} + } + *s.Menus[menu.MenuName] = s.Menus[menu.MenuName].add(e) + } + } +} + +func (s *Site) getTaxonomyKey(key string) string { + if s.Info.preserveTaxonomyNames { + // Keep as is + return key + } + return s.PathSpec.MakePathSanitized(key) +} + +// We need to create the top level taxonomy early in the build process +// to be able to determine the page Kind correctly. +func (s *Site) createTaxonomiesEntries() { + s.Taxonomies = make(TaxonomyList) + taxonomies := s.Language.GetStringMapString("taxonomies") + for _, plural := range taxonomies { + s.Taxonomies[plural] = make(Taxonomy) + } +} + +func (s *Site) assembleTaxonomies() { + s.taxonomiesPluralSingular = make(map[string]string) + s.taxonomiesOrigKey = make(map[string]string) + + taxonomies := s.Language.GetStringMapString("taxonomies") + + s.Log.INFO.Printf("found taxonomies: %#v\n", taxonomies) + + for singular, plural := range taxonomies { + s.taxonomiesPluralSingular[plural] = singular + + for _, p := range s.Pages { + vals := p.getParam(plural, !s.Info.preserveTaxonomyNames) + weight := p.GetParam(plural + "_weight") + if weight == nil { + weight = 0 + } + if vals != nil { + if v, ok := vals.([]string); ok { + for _, idx := range v { + x := WeightedPage{weight.(int), p} + s.Taxonomies[plural].add(s.getTaxonomyKey(idx), x) + if s.Info.preserveTaxonomyNames { + // Need to track the original + s.taxonomiesOrigKey[fmt.Sprintf("%s-%s", plural, s.PathSpec.MakePathSanitized(idx))] = idx + } + } + } else if v, ok := vals.(string); ok { + x := WeightedPage{weight.(int), p} + s.Taxonomies[plural].add(s.getTaxonomyKey(v), x) + if s.Info.preserveTaxonomyNames { + // Need to track the original + s.taxonomiesOrigKey[fmt.Sprintf("%s-%s", plural, s.PathSpec.MakePathSanitized(v))] = v + } + } else { + s.Log.ERROR.Printf("Invalid %s in %s\n", plural, p.File.Path()) + } + } + } + for k := range s.Taxonomies[plural] { + s.Taxonomies[plural][k].Sort() + } + } + + s.Info.Taxonomies = s.Taxonomies +} + +// Prepare site for a new full build. +func (s *Site) resetBuildState() { + + s.PageCollections = newPageCollectionsFromPages(s.rawAllPages) + // TODO(bep) get rid of this double + s.Info.PageCollections = s.PageCollections + + s.Info.paginationPageCount = 0 + s.draftCount = 0 + s.futureCount = 0 + + s.expiredCount = 0 + + for _, p := range s.rawAllPages { + p.scratch = newScratch() + p.subSections = Pages{} + p.parent = nil + } +} + +func (s *Site) kindFromSections(sections []string) string { + if _, isTaxonomy := s.Taxonomies[sections[0]]; isTaxonomy { + if len(sections) == 1 { + return KindTaxonomyTerm + } + return KindTaxonomy + } + return KindSection +} + +func (s *Site) layouts(p *PageOutput) ([]string, error) { + return s.layoutHandler.For(p.layoutDescriptor, "", p.outputFormat) +} + +func (s *Site) preparePages() error { + var errors []error + + for _, p := range s.Pages { + if err := p.prepareLayouts(); err != nil { + errors = append(errors, err) + } + if err := p.prepareData(s); err != nil { + errors = append(errors, err) + } + } + + if len(errors) != 0 { + return fmt.Errorf("Prepare pages failed: %.100q…", errors) + } + + return nil +} + +func errorCollator(results <-chan error, errs chan<- error) { + errMsgs := []string{} + for err := range results { + if err != nil { + errMsgs = append(errMsgs, err.Error()) + } + } + if len(errMsgs) == 0 { + errs <- nil + } else { + errs <- errors.New(strings.Join(errMsgs, "\n")) + } + close(errs) +} + +func (s *Site) appendThemeTemplates(in []string) []string { + if !s.PathSpec.ThemeSet() { + return in + } + + out := []string{} + // First place all non internal templates + for _, t := range in { + if !strings.HasPrefix(t, "_internal/") { + out = append(out, t) + } + } + + // Then place theme templates with the same names + for _, t := range in { + if !strings.HasPrefix(t, "_internal/") { + out = append(out, "theme/"+t) + } + } + + // Lastly place internal templates + for _, t := range in { + if strings.HasPrefix(t, "_internal/") { + out = append(out, t) + } + } + return out + +} + +// Stats prints Hugo builds stats to the console. +// This is what you see after a successful hugo build. +func (s *Site) Stats() { + + s.Log.FEEDBACK.Printf("Built site for language %s:\n", s.Language.Lang) + s.Log.FEEDBACK.Println(s.draftStats()) + s.Log.FEEDBACK.Println(s.futureStats()) + s.Log.FEEDBACK.Println(s.expiredStats()) + s.Log.FEEDBACK.Printf("%d regular pages created\n", s.siteStats.pageCountRegular) + s.Log.FEEDBACK.Printf("%d other pages created\n", (s.siteStats.pageCount - s.siteStats.pageCountRegular)) + s.Log.FEEDBACK.Printf("%d non-page files copied\n", len(s.Files)) + s.Log.FEEDBACK.Printf("%d paginator pages created\n", s.Info.paginationPageCount) + + if s.isEnabled(KindTaxonomy) { + taxonomies := s.Language.GetStringMapString("taxonomies") + + for _, pl := range taxonomies { + s.Log.FEEDBACK.Printf("%d %s created\n", len(s.Taxonomies[pl]), pl) + } + } + +} + +// GetPage looks up a page of a given type in the path given. +// {{ with .Site.GetPage "section" "blog" }}{{ .Title }}{{ end }} +// +// This will return nil when no page could be found, and will return the +// first page found if the key is ambigous. +func (s *SiteInfo) GetPage(typ string, path ...string) (*Page, error) { + return s.getPage(typ, path...), nil +} + +func (s *Site) permalinkForOutputFormat(link string, f output.Format) (string, error) { + var ( + baseURL string + err error + ) + + if f.Protocol != "" { + baseURL, err = s.PathSpec.BaseURL.WithProtocol(f.Protocol) + if err != nil { + return "", err + } + } else { + baseURL = s.PathSpec.BaseURL.String() + } + return s.permalinkForBaseURL(link, baseURL), nil +} + +func (s *Site) permalink(link string) string { + return s.permalinkForBaseURL(link, s.PathSpec.BaseURL.String()) + +} + +func (s *Site) permalinkForBaseURL(link, baseURL string) string { + link = strings.TrimPrefix(link, "/") + if !strings.HasSuffix(baseURL, "/") { + baseURL += "/" + } + return baseURL + link +} + +func (s *Site) renderAndWriteXML(name string, dest string, d interface{}, layouts ...string) error { + s.Log.DEBUG.Printf("Render XML for %q to %q", name, dest) + renderBuffer := bp.GetBuffer() + defer bp.PutBuffer(renderBuffer) + renderBuffer.WriteString("<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>\n") + + if err := s.renderForLayouts(name, d, renderBuffer, layouts...); err != nil { + helpers.DistinctWarnLog.Println(err) + return nil + } + + outBuffer := bp.GetBuffer() + defer bp.PutBuffer(outBuffer) + + var path []byte + if s.Info.relativeURLs { + path = []byte(helpers.GetDottedRelativePath(dest)) + } else { + s := s.Cfg.GetString("baseURL") + if !strings.HasSuffix(s, "/") { + s += "/" + } + path = []byte(s) + } + transformer := transform.NewChain(transform.AbsURLInXML) + if err := transformer.Apply(outBuffer, renderBuffer, path); err != nil { + helpers.DistinctErrorLog.Println(err) + return nil + } + + return s.publish(dest, outBuffer) + +} + +func (s *Site) renderAndWritePage(name string, dest string, p *PageOutput, layouts ...string) error { + renderBuffer := bp.GetBuffer() + defer bp.PutBuffer(renderBuffer) + + if err := s.renderForLayouts(p.Kind, p, renderBuffer, layouts...); err != nil { + helpers.DistinctWarnLog.Println(err) + return nil + } + + if renderBuffer.Len() == 0 { + return nil + } + + outBuffer := bp.GetBuffer() + defer bp.PutBuffer(outBuffer) + + transformLinks := transform.NewEmptyTransforms() + + isHTML := p.outputFormat.IsHTML + + if isHTML { + if s.Info.relativeURLs || s.Info.canonifyURLs { + transformLinks = append(transformLinks, transform.AbsURL) + } + + if s.running() && s.Cfg.GetBool("watch") && !s.Cfg.GetBool("disableLiveReload") { + transformLinks = append(transformLinks, transform.LiveReloadInject(s.Cfg.GetInt("port"))) + } + + // For performance reasons we only inject the Hugo generator tag on the home page. + if p.IsHome() { + if !s.Cfg.GetBool("disableHugoGeneratorInject") { + transformLinks = append(transformLinks, transform.HugoGeneratorInject) + } + } + } + + var path []byte + + if s.Info.relativeURLs { + path = []byte(helpers.GetDottedRelativePath(dest)) + } else if s.Info.canonifyURLs { + url := s.Cfg.GetString("baseURL") + if !strings.HasSuffix(url, "/") { + url += "/" + } + path = []byte(url) + } + + transformer := transform.NewChain(transformLinks...) + if err := transformer.Apply(outBuffer, renderBuffer, path); err != nil { + helpers.DistinctErrorLog.Println(err) + return nil + } + + return s.publish(dest, outBuffer) +} + +func (s *Site) renderForLayouts(name string, d interface{}, w io.Writer, layouts ...string) error { + templ := s.findFirstTemplate(layouts...) + if templ == nil { + return fmt.Errorf("[%s] Unable to locate layout for %q: %s\n", s.Language.Lang, name, layouts) + } + + if err := templ.Execute(w, d); err != nil { + // Behavior here should be dependent on if running in server or watch mode. + helpers.DistinctErrorLog.Printf("Error while rendering %q: %s", name, err) + if !s.running() && !testMode { + // TODO(bep) check if this can be propagated + os.Exit(-1) + } else if testMode { + return err + } + } + + return nil +} + +func (s *Site) findFirstTemplate(layouts ...string) tpl.Template { + for _, layout := range layouts { + if templ := s.Tmpl.Lookup(layout); templ != nil { + return templ + } + } + return nil +} + +func (s *Site) publish(path string, r io.Reader) (err error) { + path = filepath.Join(s.absPublishDir(), path) + return helpers.WriteToDisk(path, r, s.Fs.Destination) +} + +func (s *Site) draftStats() string { + var msg string + + switch s.draftCount { + case 0: + return "0 draft content" + case 1: + msg = "1 draft rendered" + default: + msg = fmt.Sprintf("%d drafts rendered", s.draftCount) + } + + if s.Cfg.GetBool("buildDrafts") { + return fmt.Sprintf("%d of ", s.draftCount) + msg + } + + return "0 of " + msg +} + +func (s *Site) futureStats() string { + var msg string + + switch s.futureCount { + case 0: + return "0 future content" + case 1: + msg = "1 future rendered" + default: + msg = fmt.Sprintf("%d futures rendered", s.futureCount) + } + + if s.Cfg.GetBool("buildFuture") { + return fmt.Sprintf("%d of ", s.futureCount) + msg + } + + return "0 of " + msg +} + +func (s *Site) expiredStats() string { + var msg string + + switch s.expiredCount { + case 0: + return "0 expired content" + case 1: + msg = "1 expired rendered" + default: + msg = fmt.Sprintf("%d expired rendered", s.expiredCount) + } + + if s.Cfg.GetBool("buildExpired") { + return fmt.Sprintf("%d of ", s.expiredCount) + msg + } + + return "0 of " + msg +} + +func getGoMaxProcs() int { + if gmp := os.Getenv("GOMAXPROCS"); gmp != "" { + if p, err := strconv.Atoi(gmp); err != nil { + return p + } + } + return 1 +} + +func (s *Site) newNodePage(typ string, sections ...string) *Page { + p := &Page{ + language: s.Language, + pageInit: &pageInit{}, + Kind: typ, + Data: make(map[string]interface{}), + Site: &s.Info, + sections: sections, + s: s} + + p.outputFormats = p.s.outputFormats[p.Kind] + + return p + +} + +func (s *Site) newHomePage() *Page { + p := s.newNodePage(KindHome) + p.Title = s.Info.Title + pages := Pages{} + p.Data["Pages"] = pages + p.Pages = pages + return p +} + +func (s *Site) newTaxonomyPage(plural, key string) *Page { + + p := s.newNodePage(KindTaxonomy, plural, key) + + if s.Info.preserveTaxonomyNames { + // Keep (mostly) as is in the title + // We make the first character upper case, mostly because + // it is easier to reason about in the tests. + p.Title = helpers.FirstUpper(key) + key = s.PathSpec.MakePathSanitized(key) + } else { + p.Title = strings.Replace(strings.Title(key), "-", " ", -1) + } + + return p +} + +func (s *Site) newSectionPage(name string) *Page { + p := s.newNodePage(KindSection, name) + + sectionName := helpers.FirstUpper(name) + if s.Cfg.GetBool("pluralizeListTitles") { + p.Title = inflect.Pluralize(sectionName) + } else { + p.Title = sectionName + } + return p +} + +func (s *Site) newTaxonomyTermsPage(plural string) *Page { + p := s.newNodePage(KindTaxonomyTerm, plural) + p.Title = strings.Title(plural) + return p +} diff --git a/hugolib/siteJSONEncode_test.go b/hugolib/siteJSONEncode_test.go new file mode 100644 index 000000000..9c83899fd --- /dev/null +++ b/hugolib/siteJSONEncode_test.go @@ -0,0 +1,51 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "encoding/json" + "testing" + + "path/filepath" + + "github.com/gohugoio/hugo/deps" +) + +// Issue #1123 +// Testing prevention of cyclic refs in JSON encoding +// May be smart to run with: -timeout 4000ms +func TestEncodePage(t *testing.T) { + t.Parallel() + cfg, fs := newTestCfg() + + // borrowed from menu_test.go + for _, src := range menuPageSources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + + } + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + _, err := json.Marshal(s) + check(t, err) + + _, err = json.Marshal(s.RegularPages[0]) + check(t, err) +} + +func check(t *testing.T, err error) { + if err != nil { + t.Fatalf("Failed %s", err) + } +} diff --git a/hugolib/site_benchmark_test.go b/hugolib/site_benchmark_test.go new file mode 100644 index 000000000..d23271af7 --- /dev/null +++ b/hugolib/site_benchmark_test.go @@ -0,0 +1,254 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "math/rand" + "path/filepath" + "strings" + "testing" + + "github.com/spf13/afero" +) + +type siteBuildingBenchmarkConfig struct { + Frontmatter string + NumPages int + RootSections int + Render bool + Shortcodes bool + NumTags int + TagsPerPage int +} + +func (s siteBuildingBenchmarkConfig) String() string { + // Make it comma separated with no spaces, so it is both Bash and regexp friendly. + // To make it a short as possible, we only shows bools when enabled and ints when >= 0 (RootSections > 1) + sep := "," + id := s.Frontmatter + sep + if s.RootSections > 1 { + id += fmt.Sprintf("num_root_sections=%d%s", s.RootSections, sep) + } + id += fmt.Sprintf("num_pages=%d%s", s.NumPages, sep) + + if s.NumTags > 0 { + id += fmt.Sprintf("num_tags=%d%s", s.NumTags, sep) + } + + if s.TagsPerPage > 0 { + id += fmt.Sprintf("tags_per_page=%d%s", s.TagsPerPage, sep) + } + + if s.Shortcodes { + id += "shortcodes" + sep + } + + if s.Render { + id += "render" + sep + } + + return strings.TrimSuffix(id, sep) + +} + +func BenchmarkSiteBuilding(b *testing.B) { + var conf siteBuildingBenchmarkConfig + for _, frontmatter := range []string{"YAML", "TOML"} { + conf.Frontmatter = frontmatter + for _, rootSections := range []int{1, 5} { + conf.RootSections = rootSections + for _, numTags := range []int{0, 1, 10, 20, 50, 100, 500, 1000, 5000} { + conf.NumTags = numTags + for _, tagsPerPage := range []int{0, 1, 5, 20, 50, 80} { + conf.TagsPerPage = tagsPerPage + for _, numPages := range []int{1, 10, 100, 500, 1000, 5000, 10000} { + conf.NumPages = numPages + for _, render := range []bool{false, true} { + conf.Render = render + for _, shortcodes := range []bool{false, true} { + conf.Shortcodes = shortcodes + doBenchMarkSiteBuilding(conf, b) + } + } + } + } + } + } + } +} + +func doBenchMarkSiteBuilding(conf siteBuildingBenchmarkConfig, b *testing.B) { + b.Run(conf.String(), func(b *testing.B) { + sites := createHugoBenchmarkSites(b, b.N, conf) + b.ResetTimer() + for i := 0; i < b.N; i++ { + h := sites[0] + + err := h.Build(BuildCfg{SkipRender: !conf.Render}) + if err != nil { + b.Fatal(err) + } + + // Try to help the GC + sites[0] = nil + sites = sites[1:len(sites)] + } + }) +} + +func createHugoBenchmarkSites(b *testing.B, count int, cfg siteBuildingBenchmarkConfig) []*HugoSites { + someMarkdown := ` +An h1 header +============ + +Paragraphs are separated by a blank line. + +2nd paragraph. *Italic* and **bold**. Itemized lists +look like: + + * this one + * that one + * the other one + +Note that --- not considering the asterisk --- the actual text +content starts at 4-columns in. + +> Block quotes are +> written like so. +> +> They can span multiple paragraphs, +> if you like. + +Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., "it's all +in chapters 12--14"). Three dots ... will be converted to an ellipsis. +Unicode is supported. ☺ +` + + someMarkdownWithShortCode := someMarkdown + ` + +{{< myShortcode >}} + +` + + pageTemplateTOML := `+++ +title = "%s" +tags = %s ++++ +%s + +` + + pageTemplateYAML := `--- +title: "%s" +tags: +%s +--- +%s + +` + + siteConfig := ` +baseURL = "http://example.com/blog" + +paginate = 10 +defaultContentLanguage = "en" + +[Taxonomies] +tag = "tags" +category = "categories" +` + + numTags := cfg.NumTags + + if cfg.TagsPerPage > numTags { + numTags = cfg.TagsPerPage + } + + var ( + contentPagesContent [3]string + tags = make([]string, numTags) + pageTemplate string + ) + + for i := 0; i < numTags; i++ { + tags[i] = fmt.Sprintf("Hugo %d", i+1) + } + + var tagsStr string + + if cfg.Shortcodes { + contentPagesContent = [3]string{ + someMarkdownWithShortCode, + strings.Repeat(someMarkdownWithShortCode, 2), + strings.Repeat(someMarkdownWithShortCode, 3), + } + } else { + contentPagesContent = [3]string{ + someMarkdown, + strings.Repeat(someMarkdown, 2), + strings.Repeat(someMarkdown, 3), + } + } + + sites := make([]*HugoSites, count) + for i := 0; i < count; i++ { + // Maybe consider reusing the Source fs + mf := afero.NewMemMapFs() + th, h := newTestSitesFromConfig(b, mf, siteConfig, + "layouts/_default/single.html", `Single HTML|{{ .Title }}|{{ .Content }}`, + "layouts/_default/list.html", `List HTML|{{ .Title }}|{{ .Content }}`, + "layouts/shortcodes/myShortcode.html", `<p>MyShortcode</p>`) + + fs := th.Fs + + pagesPerSection := cfg.NumPages / cfg.RootSections + + for i := 0; i < cfg.RootSections; i++ { + for j := 0; j < pagesPerSection; j++ { + var tagsSlice []string + + if numTags > 0 { + tagsStart := rand.Intn(numTags) - cfg.TagsPerPage + if tagsStart < 0 { + tagsStart = 0 + } + tagsSlice = tags[tagsStart : tagsStart+cfg.TagsPerPage] + } + + if cfg.Frontmatter == "TOML" { + pageTemplate = pageTemplateTOML + tagsStr = "[]" + if cfg.TagsPerPage > 0 { + tagsStr = strings.Replace(fmt.Sprintf("%q", tagsSlice), " ", ", ", -1) + } + } else { + // YAML + pageTemplate = pageTemplateYAML + for _, tag := range tagsSlice { + tagsStr += "\n- " + tag + } + } + + content := fmt.Sprintf(pageTemplate, fmt.Sprintf("Title%d_%d", i, j), tagsStr, contentPagesContent[rand.Intn(3)]) + + writeSource(b, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", j)), content) + } + } + + sites[i] = h + } + + return sites +} diff --git a/hugolib/site_output.go b/hugolib/site_output.go new file mode 100644 index 000000000..f5eb2ba57 --- /dev/null +++ b/hugolib/site_output.go @@ -0,0 +1,99 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "path" + "strings" + + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/output" + "github.com/spf13/cast" +) + +func createSiteOutputFormats(allFormats output.Formats, cfg config.Provider) (map[string]output.Formats, error) { + if !cfg.IsSet("outputs") { + return createDefaultOutputFormats(allFormats, cfg) + } + + outFormats := make(map[string]output.Formats) + + outputs := cfg.GetStringMap("outputs") + + if outputs == nil || len(outputs) == 0 { + return outFormats, nil + } + + for k, v := range outputs { + var formats output.Formats + vals := cast.ToStringSlice(v) + for _, format := range vals { + f, found := allFormats.GetByName(format) + if !found { + return nil, fmt.Errorf("Failed to resolve output format %q from site config", format) + } + formats = append(formats, f) + } + + if len(formats) > 0 { + outFormats[k] = formats + } + } + + // Make sure every kind has at least one output format + for _, kind := range allKinds { + if _, found := outFormats[kind]; !found { + outFormats[kind] = output.Formats{output.HTMLFormat} + } + } + + return outFormats, nil + +} + +func createDefaultOutputFormats(allFormats output.Formats, cfg config.Provider) (map[string]output.Formats, error) { + outFormats := make(map[string]output.Formats) + rssOut, _ := allFormats.GetByName(output.RSSFormat.Name) + htmlOut, _ := allFormats.GetByName(output.HTMLFormat.Name) + + for _, kind := range allKinds { + var formats output.Formats + // All have HTML + formats = append(formats, htmlOut) + + // All but page have RSS + if kind != KindPage { + + rssBase := cfg.GetString("rssURI") + if rssBase == "" || rssBase == "index.xml" { + rssBase = rssOut.BaseName + } else { + // Remove in Hugo 0.22. + helpers.Deprecated("Site config", "rssURI", "Set baseName in outputFormats.RSS", false) + // RSS has now a well defined media type, so strip any suffix provided + rssBase = strings.TrimSuffix(rssBase, path.Ext(rssBase)) + } + + rssOut.BaseName = rssBase + formats = append(formats, rssOut) + + } + + outFormats[kind] = formats + } + + return outFormats, nil +} diff --git a/hugolib/site_output_test.go b/hugolib/site_output_test.go new file mode 100644 index 000000000..8455a13f7 --- /dev/null +++ b/hugolib/site_output_test.go @@ -0,0 +1,365 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "reflect" + "strings" + "testing" + + "github.com/spf13/afero" + + "github.com/stretchr/testify/require" + + "fmt" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/output" + "github.com/spf13/viper" +) + +func TestDefaultOutputFormats(t *testing.T) { + t.Parallel() + defs, err := createDefaultOutputFormats(output.DefaultFormats, viper.New()) + + require.NoError(t, err) + + tests := []struct { + name string + kind string + want output.Formats + }{ + {"RSS not for regular pages", KindPage, output.Formats{output.HTMLFormat}}, + {"Home Sweet Home", KindHome, output.Formats{output.HTMLFormat, output.RSSFormat}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := defs[tt.kind]; !reflect.DeepEqual(got, tt.want) { + t.Errorf("createDefaultOutputFormats(%v) = %v, want %v", tt.kind, got, tt.want) + } + }) + } +} + +func TestDefaultOutputFormatsWithOverrides(t *testing.T) { + t.Parallel() + + htmlOut := output.HTMLFormat + htmlOut.BaseName = "htmlindex" + rssOut := output.RSSFormat + rssOut.BaseName = "feed" + + defs, err := createDefaultOutputFormats(output.Formats{htmlOut, rssOut}, viper.New()) + + homeDefs := defs[KindHome] + + rss, found := homeDefs.GetByName("RSS") + require.True(t, found) + require.Equal(t, rss.BaseName, "feed") + + html, found := homeDefs.GetByName("HTML") + require.True(t, found) + require.Equal(t, html.BaseName, "htmlindex") + + require.NoError(t, err) + +} + +func TestSiteWithPageOutputs(t *testing.T) { + for _, outputs := range [][]string{{"html", "json", "calendar"}, {"json"}} { + t.Run(fmt.Sprintf("%v", outputs), func(t *testing.T) { + doTestSiteWithPageOutputs(t, outputs) + }) + } +} + +func doTestSiteWithPageOutputs(t *testing.T, outputs []string) { + t.Parallel() + + outputsStr := strings.Replace(fmt.Sprintf("%q", outputs), " ", ", ", -1) + + siteConfig := ` +baseURL = "http://example.com/blog" + +paginate = 1 +defaultContentLanguage = "en" + +disableKinds = ["page", "section", "taxonomy", "taxonomyTerm", "RSS", "sitemap", "robotsTXT", "404"] + +[Taxonomies] +tag = "tags" +category = "categories" + +defaultContentLanguage = "en" + +[languages] + +[languages.en] +title = "Title in English" +languageName = "English" +weight = 1 + +[languages.nn] +languageName = "Nynorsk" +weight = 2 +title = "Tittel på Nynorsk" + +` + + pageTemplate := `--- +title: "%s" +outputs: %s +--- +# Doc + +{{< myShort >}} +` + + mf := afero.NewMemMapFs() + + writeToFs(t, mf, "i18n/en.toml", ` +[elbow] +other = "Elbow" +`) + writeToFs(t, mf, "i18n/nn.toml", ` +[elbow] +other = "Olboge" +`) + + th, h := newTestSitesFromConfig(t, mf, siteConfig, + + // Case issue partials #3333 + "layouts/partials/GoHugo.html", `Go Hugo Partial`, + "layouts/_default/baseof.json", `START JSON:{{block "main" .}}default content{{ end }}:END JSON`, + "layouts/_default/baseof.html", `START HTML:{{block "main" .}}default content{{ end }}:END HTML`, + "layouts/shortcodes/myShort.html", `ShortHTML`, + "layouts/shortcodes/myShort.json", `ShortJSON`, + + "layouts/_default/list.json", `{{ define "main" }} +List JSON|{{ .Title }}|{{ .Content }}|Alt formats: {{ len .AlternativeOutputFormats -}}| +{{- range .AlternativeOutputFormats -}} +Alt Output: {{ .Name -}}| +{{- end -}}| +{{- range .OutputFormats -}} +Output/Rel: {{ .Name -}}/{{ .Rel }}|{{ .MediaType }} +{{- end -}} + {{ with .OutputFormats.Get "JSON" }} +<atom:link href={{ .Permalink }} rel="self" type="{{ .MediaType }}" /> +{{ end }} +{{ .Site.Language.Lang }}: {{ T "elbow" -}} +{{ end }} +`, + "layouts/_default/list.html", `{{ define "main" }} +List HTML|{{.Title }}| +{{- with .OutputFormats.Get "HTML" -}} +<atom:link href={{ .Permalink }} rel="self" type="{{ .MediaType }}" /> +{{- end -}} +{{ .Site.Language.Lang }}: {{ T "elbow" -}} +Partial Hugo 1: {{ partial "GoHugo.html" . }} +Partial Hugo 2: {{ partial "GoHugo" . -}} +Content: {{ .Content }} +{{ end }} +`, + ) + require.Len(t, h.Sites, 2) + + fs := th.Fs + + writeSource(t, fs, "content/_index.md", fmt.Sprintf(pageTemplate, "JSON Home", outputsStr)) + writeSource(t, fs, "content/_index.nn.md", fmt.Sprintf(pageTemplate, "JSON Nynorsk Heim", outputsStr)) + + err := h.Build(BuildCfg{}) + + require.NoError(t, err) + + s := h.Sites[0] + require.Equal(t, "en", s.Language.Lang) + + home := s.getPage(KindHome) + + require.NotNil(t, home) + + lenOut := len(outputs) + + require.Len(t, home.outputFormats, lenOut) + + // There is currently always a JSON output to make it simpler ... + altFormats := lenOut - 1 + hasHTML := helpers.InStringArray(outputs, "html") + th.assertFileContent("public/index.json", + "List JSON", + fmt.Sprintf("Alt formats: %d", altFormats), + ) + + if hasHTML { + th.assertFileContent("public/index.json", + "Alt Output: HTML", + "Output/Rel: JSON/alternate|", + "Output/Rel: HTML/canonical|", + "en: Elbow", + "ShortJSON", + ) + + th.assertFileContent("public/index.html", + // The HTML entity is a deliberate part of this test: The HTML templates are + // parsed with html/template. + `List HTML|JSON Home|<atom:link href=http://example.com/blog/ rel="self" type="text/html+html" />`, + "en: Elbow", + "ShortHTML", + ) + th.assertFileContent("public/nn/index.html", + "List HTML|JSON Nynorsk Heim|", + "nn: Olboge") + } else { + th.assertFileContent("public/index.json", + "Output/Rel: JSON/canonical|", + // JSON is plain text, so no need to safeHTML this and that + `<atom:link href=http://example.com/blog/index.json rel="self" type="application/json+json" />`, + "ShortJSON", + ) + th.assertFileContent("public/nn/index.json", + "List JSON|JSON Nynorsk Heim|", + "nn: Olboge", + "ShortJSON", + ) + } + + of := home.OutputFormats() + require.Len(t, of, lenOut) + require.Nil(t, of.Get("Hugo")) + require.NotNil(t, of.Get("json")) + json := of.Get("JSON") + _, err = home.AlternativeOutputFormats() + require.Error(t, err) + require.NotNil(t, json) + require.Equal(t, "/blog/index.json", json.RelPermalink()) + require.Equal(t, "http://example.com/blog/index.json", json.Permalink()) + + if helpers.InStringArray(outputs, "cal") { + cal := of.Get("calendar") + require.NotNil(t, cal) + require.Equal(t, "/blog/index.ics", cal.RelPermalink()) + require.Equal(t, "webcal://example.com/blog/index.ics", cal.Permalink()) + } + +} + +// Issue #3447 +func TestRedefineRSSOutputFormat(t *testing.T) { + siteConfig := ` +baseURL = "http://example.com/blog" + +paginate = 1 +defaultContentLanguage = "en" + +disableKinds = ["page", "section", "taxonomy", "taxonomyTerm", "sitemap", "robotsTXT", "404"] + +[outputFormats] +[outputFormats.RSS] +mediatype = "application/rss" +baseName = "feed" + +` + + mf := afero.NewMemMapFs() + writeToFs(t, mf, "content/foo.html", `foo`) + + th, h := newTestSitesFromConfig(t, mf, siteConfig) + + err := h.Build(BuildCfg{}) + + require.NoError(t, err) + + th.assertFileContent("public/feed.xml", "Recent content on") + + s := h.Sites[0] + + //Issue #3450 + require.Equal(t, "http://example.com/blog/feed.xml", s.Info.RSSLink) + +} + +// Issue #3614 +func TestDotLessOutputFormat(t *testing.T) { + siteConfig := ` +baseURL = "http://example.com/blog" + +paginate = 1 +defaultContentLanguage = "en" + +disableKinds = ["page", "section", "taxonomy", "taxonomyTerm", "sitemap", "robotsTXT", "404"] + +[mediaTypes] +[mediaTypes."text/nodot"] +suffix = "" +delimiter = "" +[mediaTypes."text/defaultdelim"] +suffix = "defd" +[mediaTypes."text/nosuffix"] +suffix = "" +[mediaTypes."text/customdelim"] +suffix = "del" +delimiter = "_" + +[outputs] +home = [ "DOTLESS", "DEF", "NOS", "CUS" ] + +[outputFormats] +[outputFormats.DOTLESS] +mediatype = "text/nodot" +baseName = "_redirects" # This is how Netlify names their redirect files. +[outputFormats.DEF] +mediatype = "text/defaultdelim" +baseName = "defaultdelimbase" +[outputFormats.NOS] +mediatype = "text/nosuffix" +baseName = "nosuffixbase" +[outputFormats.CUS] +mediatype = "text/customdelim" +baseName = "customdelimbase" + +` + + mf := afero.NewMemMapFs() + writeToFs(t, mf, "content/foo.html", `foo`) + writeToFs(t, mf, "layouts/_default/list.dotless", `a dotless`) + writeToFs(t, mf, "layouts/_default/list.def.defd", `default delimim`) + writeToFs(t, mf, "layouts/_default/list.nos", `no suffix`) + writeToFs(t, mf, "layouts/_default/list.cus.del", `custom delim`) + + th, h := newTestSitesFromConfig(t, mf, siteConfig) + + err := h.Build(BuildCfg{}) + + require.NoError(t, err) + + th.assertFileContent("public/_redirects", "a dotless") + th.assertFileContent("public/defaultdelimbase.defd", "default delimim") + // This looks weird, but the user has chosen this definition. + th.assertFileContent("public/nosuffixbase.", "no suffix") + th.assertFileContent("public/customdelimbase_del", "custom delim") + + s := h.Sites[0] + home := s.getPage(KindHome) + require.NotNil(t, home) + + outputs := home.OutputFormats() + + require.Equal(t, "/blog/_redirects", outputs.Get("DOTLESS").RelPermalink()) + require.Equal(t, "/blog/defaultdelimbase.defd", outputs.Get("DEF").RelPermalink()) + require.Equal(t, "/blog/nosuffixbase.", outputs.Get("NOS").RelPermalink()) + require.Equal(t, "/blog/customdelimbase_del", outputs.Get("CUS").RelPermalink()) + +} diff --git a/hugolib/site_render.go b/hugolib/site_render.go new file mode 100644 index 000000000..a24946cf3 --- /dev/null +++ b/hugolib/site_render.go @@ -0,0 +1,400 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "path" + "sync" + + "github.com/gohugoio/hugo/helpers" + + "github.com/gohugoio/hugo/output" + + bp "github.com/gohugoio/hugo/bufferpool" +) + +// renderPages renders pages each corresponding to a markdown file. +// TODO(bep np doc +func (s *Site) renderPages() error { + + results := make(chan error) + pages := make(chan *Page) + errs := make(chan error) + + go errorCollator(results, errs) + + numWorkers := getGoMaxProcs() * 4 + + wg := &sync.WaitGroup{} + + for i := 0; i < numWorkers; i++ { + wg.Add(1) + go pageRenderer(s, pages, results, wg) + } + + for _, page := range s.Pages { + pages <- page + } + + close(pages) + + wg.Wait() + + close(results) + + err := <-errs + if err != nil { + return fmt.Errorf("Error(s) rendering pages: %s", err) + } + return nil +} + +func pageRenderer(s *Site, pages <-chan *Page, results chan<- error, wg *sync.WaitGroup) { + defer wg.Done() + + for page := range pages { + + for i, outFormat := range page.outputFormats { + + var ( + pageOutput *PageOutput + err error + ) + + if i == 0 { + pageOutput, err = newPageOutput(page, false, outFormat) + page.mainPageOutput = pageOutput + } + + if outFormat != page.s.rc.Format { + // Will be rendered ... later. + continue + } + + if pageOutput == nil { + pageOutput, err = page.mainPageOutput.copyWithFormat(outFormat) + } + + if err != nil { + s.Log.ERROR.Printf("Failed to create output page for type %q for page %q: %s", outFormat.Name, page, err) + continue + } + + var layouts []string + + if page.selfLayout != "" { + layouts = []string{page.selfLayout} + } else { + layouts, err = s.layouts(pageOutput) + if err != nil { + s.Log.ERROR.Printf("Failed to resolve layout output %q for page %q: %s", outFormat.Name, page, err) + continue + } + } + + switch pageOutput.outputFormat.Name { + + case "RSS": + if err := s.renderRSS(pageOutput); err != nil { + results <- err + } + default: + targetPath, err := pageOutput.targetPath() + if err != nil { + s.Log.ERROR.Printf("Failed to create target path for output %q for page %q: %s", outFormat.Name, page, err) + continue + } + + s.Log.DEBUG.Printf("Render %s to %q with layouts %q", pageOutput.Kind, targetPath, layouts) + + if err := s.renderAndWritePage("page "+pageOutput.FullFilePath(), targetPath, pageOutput, layouts...); err != nil { + results <- err + } + + if pageOutput.IsNode() { + if err := s.renderPaginator(pageOutput); err != nil { + results <- err + } + } + } + + } + } +} + +// renderPaginator must be run after the owning Page has been rendered. +func (s *Site) renderPaginator(p *PageOutput) error { + if p.paginator != nil { + s.Log.DEBUG.Printf("Render paginator for page %q", p.Path()) + paginatePath := s.Cfg.GetString("paginatePath") + + // write alias for page 1 + addend := fmt.Sprintf("/%s/%d", paginatePath, 1) + target, err := p.createTargetPath(p.outputFormat, addend) + if err != nil { + return err + } + + // TODO(bep) do better + link := newOutputFormat(p.Page, p.outputFormat).Permalink() + if err := s.writeDestAlias(target, link, nil); err != nil { + return err + } + + pagers := p.paginator.Pagers() + + for i, pager := range pagers { + if i == 0 { + // already created + continue + } + + pagerNode, err := p.copy() + if err != nil { + return err + } + + pagerNode.origOnCopy = p.Page + + pagerNode.paginator = pager + if pager.TotalPages() > 0 { + first, _ := pager.page(0) + pagerNode.Date = first.Date + pagerNode.Lastmod = first.Lastmod + } + + pageNumber := i + 1 + addend := fmt.Sprintf("/%s/%d", paginatePath, pageNumber) + targetPath, _ := p.targetPath(addend) + layouts, err := p.layouts() + + if err != nil { + return err + } + + if err := s.renderAndWritePage( + pagerNode.Title, + targetPath, pagerNode, layouts...); err != nil { + return err + } + + } + } + return nil +} + +func (s *Site) renderRSS(p *PageOutput) error { + + if !s.isEnabled(kindRSS) { + return nil + } + + if s.Cfg.GetBool("disableRSS") { + return nil + } + + p.Kind = kindRSS + + limit := s.Cfg.GetInt("rssLimit") + if limit >= 0 && len(p.Pages) > limit { + p.Pages = p.Pages[:limit] + p.Data["Pages"] = p.Pages + } + + layouts, err := s.layoutHandler.For( + p.layoutDescriptor, + "", + p.outputFormat) + if err != nil { + return err + } + + targetPath, err := p.targetPath() + if err != nil { + return err + } + + return s.renderAndWriteXML(p.Title, + targetPath, p, layouts...) +} + +func (s *Site) render404() error { + if !s.isEnabled(kind404) { + return nil + } + + if s.Cfg.GetBool("disable404") { + return nil + } + + if s.owner.multilingual.enabled() && (s.Language.Lang != s.owner.multilingual.DefaultLang.Lang) { + return nil + } + + p := s.newNodePage(kind404) + + p.Title = "404 Page not found" + p.Data["Pages"] = s.Pages + p.Pages = s.Pages + p.URLPath.URL = "404.html" + + if err := p.initTargetPathDescriptor(); err != nil { + return err + } + + nfLayouts := []string{"404.html"} + + pageOutput, err := newPageOutput(p, false, output.HTMLFormat) + if err != nil { + return err + } + + return s.renderAndWritePage("404 page", "404.html", pageOutput, s.appendThemeTemplates(nfLayouts)...) + +} + +func (s *Site) renderSitemap() error { + if !s.isEnabled(kindSitemap) { + return nil + } + + if s.Cfg.GetBool("disableSitemap") { + return nil + } + + sitemapDefault := parseSitemap(s.Cfg.GetStringMap("sitemap")) + + n := s.newNodePage(kindSitemap) + + // Include all pages (regular, home page, taxonomies etc.) + pages := s.Pages + + page := s.newNodePage(kindSitemap) + page.URLPath.URL = "" + if err := page.initTargetPathDescriptor(); err != nil { + return err + } + page.Sitemap.ChangeFreq = sitemapDefault.ChangeFreq + page.Sitemap.Priority = sitemapDefault.Priority + page.Sitemap.Filename = sitemapDefault.Filename + + n.Data["Pages"] = pages + n.Pages = pages + + // TODO(bep) we have several of these + if err := page.initTargetPathDescriptor(); err != nil { + return err + } + + // TODO(bep) this should be done somewhere else + for _, page := range pages { + if page.Sitemap.ChangeFreq == "" { + page.Sitemap.ChangeFreq = sitemapDefault.ChangeFreq + } + + if page.Sitemap.Priority == -1 { + page.Sitemap.Priority = sitemapDefault.Priority + } + + if page.Sitemap.Filename == "" { + page.Sitemap.Filename = sitemapDefault.Filename + } + } + + smLayouts := []string{"sitemap.xml", "_default/sitemap.xml", "_internal/_default/sitemap.xml"} + addLanguagePrefix := n.Site.IsMultiLingual() + + return s.renderAndWriteXML("sitemap", + n.addLangPathPrefixIfFlagSet(page.Sitemap.Filename, addLanguagePrefix), n, s.appendThemeTemplates(smLayouts)...) +} + +func (s *Site) renderRobotsTXT() error { + if !s.isEnabled(kindRobotsTXT) { + return nil + } + + if !s.Cfg.GetBool("enableRobotsTXT") { + return nil + } + + n := s.newNodePage(kindRobotsTXT) + if err := n.initTargetPathDescriptor(); err != nil { + return err + } + n.Data["Pages"] = s.Pages + n.Pages = s.Pages + + rLayouts := []string{"robots.txt", "_default/robots.txt", "_internal/_default/robots.txt"} + outBuffer := bp.GetBuffer() + defer bp.PutBuffer(outBuffer) + if err := s.renderForLayouts("robots", n, outBuffer, s.appendThemeTemplates(rLayouts)...); err != nil { + helpers.DistinctWarnLog.Println(err) + return nil + } + + if outBuffer.Len() == 0 { + return nil + } + + return s.publish("robots.txt", outBuffer) +} + +// renderAliases renders shell pages that simply have a redirect in the header. +func (s *Site) renderAliases() error { + for _, p := range s.Pages { + if len(p.Aliases) == 0 { + continue + } + + for _, f := range p.outputFormats { + if !f.IsHTML { + continue + } + + o := newOutputFormat(p, f) + plink := o.Permalink() + + for _, a := range p.Aliases { + if f.Path != "" { + // Make sure AMP and similar doesn't clash with regular aliases. + a = path.Join(a, f.Path) + } + + if err := s.writeDestAlias(a, plink, p); err != nil { + return err + } + } + } + } + + if s.owner.multilingual.enabled() { + mainLang := s.owner.multilingual.DefaultLang + if s.Info.defaultContentLanguageInSubdir { + mainLangURL := s.PathSpec.AbsURL(mainLang.Lang, false) + s.Log.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL) + if err := s.publishDestAlias(true, "/", mainLangURL, nil); err != nil { + return err + } + } else { + mainLangURL := s.PathSpec.AbsURL("", false) + s.Log.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL) + if err := s.publishDestAlias(true, mainLang.Lang, mainLangURL, nil); err != nil { + return err + } + } + } + + return nil +} diff --git a/hugolib/site_sections.go b/hugolib/site_sections.go new file mode 100644 index 000000000..f8d9c9d1f --- /dev/null +++ b/hugolib/site_sections.go @@ -0,0 +1,306 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "path" + "strconv" + "strings" + + "github.com/gohugoio/hugo/helpers" + radix "github.com/hashicorp/go-immutable-radix" +) + +// Deprecated: Use .Site.Home.Sections. +// TODO(bep) Hugo 0.23 = Reuse as an alias for home's sections. +func (s *SiteInfo) Sections() Taxonomy { + + helpText := `In Hugo 0.22 we introduced nested sections, so this method now returns an empty taxonomy. + +To list sections with its pages, you can do something like this: + +{{ range $.Site.Home.Sections }} + Section: {{ .Title }} + {{ range .Pages }} + Section Page: {{ .Title }} + {{ end }} +{{ end }} + +To get a specific section, you can do this: + +{{ $section := $.Site.GetPage "section" "blog" }} +` + + helpers.Deprecated("Site", "Sections", helpText, true) + + return Taxonomy{} +} + +// Home is a shortcut to the home page, equivalent to .Site.GetPage "home". +func (s *SiteInfo) Home() (*Page, error) { + return s.GetPage(KindHome) +} + +// Parent returns a section's parent section or a page's section. +// To get a section's subsections, see Page's Sections method. +func (p *Page) Parent() *Page { + return p.parent +} + +// current returns the page's current section. +// Note that this will return nil for pages that is not regular, home or section pages. +// Note that for paginated sections and home pages, this will return the original page pointer. +func (p *Page) current() *Page { + v := p + if v.origOnCopy != nil { + v = v.origOnCopy + } + if v.IsHome() || v.IsSection() { + return v + } + + return v.parent +} + +// InSection returns whether the given page is in the current section. +// Note that this will always return false for pages that are +// not either regular, home or section pages. +func (p *Page) InSection(other interface{}) (bool, error) { + if p == nil || other == nil { + return false, nil + } + + if po, ok := other.(*PageOutput); ok { + other = po.Page + } + + pp, ok := other.(*Page) + if !ok { + return false, fmt.Errorf("%T not supported in InSection", other) + } + + if pp == nil { + return false, nil + } + + return pp.current() == p.current(), nil +} + +// Sections returns this section's subsections, if any. +// Note that for non-sections, this method will always return an empty list. +func (p *Page) Sections() Pages { + return p.subSections +} + +func (s *Site) assembleSections() Pages { + var newPages Pages + + if !s.isEnabled(KindSection) { + return newPages + } + + // Maps section kind pages to their path, i.e. "my/section" + sectionPages := make(map[string]*Page) + + // The sections with content files will already have been created. + for _, sect := range s.findPagesByKind(KindSection) { + sectionPages[path.Join(sect.sections...)] = sect + } + + const ( + sectKey = "__hs" + sectSectKey = "_a" + sectKey + sectPageKey = "_b" + sectKey + ) + + var ( + home *Page + inPages = radix.New().Txn() + inSections = radix.New().Txn() + undecided Pages + ) + + for i, p := range s.Pages { + if p.Kind != KindPage { + if p.Kind == KindHome { + home = p + } + continue + } + + if len(p.sections) == 0 { + // Root level pages. These will have the home page as their Parent. + p.parent = home + continue + } + + sectionKey := path.Join(p.sections...) + sect, found := sectionPages[sectionKey] + + if !found && len(p.sections) == 1 { + // We only create content-file-less sections for the root sections. + sect = s.newSectionPage(p.sections[0]) + sectionPages[sectionKey] = sect + newPages = append(newPages, sect) + found = true + } + + if len(p.sections) > 1 { + // Create the root section if not found. + _, rootFound := sectionPages[p.sections[0]] + if !rootFound { + sect = s.newSectionPage(p.sections[0]) + sectionPages[p.sections[0]] = sect + newPages = append(newPages, sect) + } + } + + if found { + pagePath := path.Join(sectionKey, sectPageKey, strconv.Itoa(i)) + inPages.Insert([]byte(pagePath), p) + } else { + undecided = append(undecided, p) + } + } + + // Create any missing sections in the tree. + // A sub-section needs a content file, but to create a navigational tree, + // given a content file in /content/a/b/c/_index.md, we cannot create just + // the c section. + for _, sect := range sectionPages { + for i := len(sect.sections); i > 0; i-- { + sectionPath := sect.sections[:i] + sectionKey := path.Join(sectionPath...) + sect, found := sectionPages[sectionKey] + if !found { + sect = s.newSectionPage(sectionPath[len(sectionPath)-1]) + sect.sections = sectionPath + sectionPages[sectionKey] = sect + newPages = append(newPages, sect) + } + } + } + + for k, sect := range sectionPages { + inPages.Insert([]byte(path.Join(k, sectSectKey)), sect) + inSections.Insert([]byte(k), sect) + } + + var ( + currentSection *Page + children Pages + rootSections = inSections.Commit().Root() + ) + + for i, p := range undecided { + // Now we can decide where to put this page into the tree. + sectionKey := path.Join(p.sections...) + _, v, _ := rootSections.LongestPrefix([]byte(sectionKey)) + sect := v.(*Page) + pagePath := path.Join(path.Join(sect.sections...), sectSectKey, "u", strconv.Itoa(i)) + inPages.Insert([]byte(pagePath), p) + } + + var rootPages = inPages.Commit().Root() + + rootPages.Walk(func(path []byte, v interface{}) bool { + p := v.(*Page) + + if p.Kind == KindSection { + if currentSection != nil { + // A new section + currentSection.setPagePages(children) + } + + currentSection = p + children = make(Pages, 0) + + return false + + } + + // Regular page + p.parent = currentSection + children = append(children, p) + return false + }) + + if currentSection != nil { + currentSection.setPagePages(children) + } + + // Build the sections hierarchy + for _, sect := range sectionPages { + if len(sect.sections) == 1 { + sect.parent = home + } else { + parentSearchKey := path.Join(sect.sections[:len(sect.sections)-1]...) + _, v, _ := rootSections.LongestPrefix([]byte(parentSearchKey)) + p := v.(*Page) + sect.parent = p + } + + if sect.parent != nil { + sect.parent.subSections = append(sect.parent.subSections, sect) + } + } + + var ( + sectionsParamId = "mainSections" + sectionsParamIdLower = strings.ToLower(sectionsParamId) + mainSections interface{} + mainSectionsFound bool + maxSectionWeight int + ) + + mainSections, mainSectionsFound = s.Info.Params[sectionsParamIdLower] + + for _, sect := range sectionPages { + if sect.parent != nil { + sect.parent.subSections.Sort() + } + + for i, p := range sect.Pages { + if i > 0 { + p.NextInSection = sect.Pages[i-1] + } + if i < len(sect.Pages)-1 { + p.PrevInSection = sect.Pages[i+1] + } + } + + if !mainSectionsFound { + weight := len(sect.Pages) + (len(sect.Sections()) * 5) + if weight >= maxSectionWeight { + mainSections = []string{sect.Section()} + maxSectionWeight = weight + } + } + } + + // Try to make this as backwards compatible as possible. + s.Info.Params[sectionsParamId] = mainSections + s.Info.Params[sectionsParamIdLower] = mainSections + + return newPages + +} + +func (p *Page) setPagePages(pages Pages) { + pages.Sort() + p.Pages = pages + p.Data = make(map[string]interface{}) + p.Data["Pages"] = pages +} diff --git a/hugolib/site_sections_test.go b/hugolib/site_sections_test.go new file mode 100644 index 000000000..7479c45fc --- /dev/null +++ b/hugolib/site_sections_test.go @@ -0,0 +1,266 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "path/filepath" + "strings" + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/stretchr/testify/require" +) + +func TestNestedSections(t *testing.T) { + t.Parallel() + + var ( + assert = require.New(t) + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + cfg.Set("permalinks", map[string]string{ + "perm a": ":sections/:title", + }) + + pageTemplate := `--- +title: T%d_%d +--- +Content +` + + // Home page + writeSource(t, fs, filepath.Join("content", "_index.md"), fmt.Sprintf(pageTemplate, -1, -1)) + + // Top level content page + writeSource(t, fs, filepath.Join("content", "mypage.md"), fmt.Sprintf(pageTemplate, 1234, 5)) + + // Top level section without index content page + writeSource(t, fs, filepath.Join("content", "top", "mypage2.md"), fmt.Sprintf(pageTemplate, 12345, 6)) + // Just a page in a subfolder, i.e. not a section. + writeSource(t, fs, filepath.Join("content", "top", "folder", "mypage3.md"), fmt.Sprintf(pageTemplate, 12345, 67)) + + for level1 := 1; level1 < 3; level1++ { + writeSource(t, fs, filepath.Join("content", "l1", fmt.Sprintf("page_1_%d.md", level1)), + fmt.Sprintf(pageTemplate, 1, level1)) + } + + // Issue #3586 + writeSource(t, fs, filepath.Join("content", "post", "0000.md"), fmt.Sprintf(pageTemplate, 1, 2)) + writeSource(t, fs, filepath.Join("content", "post", "0000", "0001.md"), fmt.Sprintf(pageTemplate, 1, 3)) + writeSource(t, fs, filepath.Join("content", "elsewhere", "0003.md"), fmt.Sprintf(pageTemplate, 1, 4)) + + // Empty nested section, i.e. no regular content pages. + writeSource(t, fs, filepath.Join("content", "empty1", "b", "c", "_index.md"), fmt.Sprintf(pageTemplate, 33, -1)) + // Index content file a the end and in the middle. + writeSource(t, fs, filepath.Join("content", "empty2", "b", "_index.md"), fmt.Sprintf(pageTemplate, 40, -1)) + writeSource(t, fs, filepath.Join("content", "empty2", "b", "c", "d", "_index.md"), fmt.Sprintf(pageTemplate, 41, -1)) + + // Empty with content file in the middle. + writeSource(t, fs, filepath.Join("content", "empty3", "b", "c", "d", "_index.md"), fmt.Sprintf(pageTemplate, 41, -1)) + writeSource(t, fs, filepath.Join("content", "empty3", "b", "empty3.md"), fmt.Sprintf(pageTemplate, 3, -1)) + + // Section with permalink config + writeSource(t, fs, filepath.Join("content", "perm a", "link", "_index.md"), fmt.Sprintf(pageTemplate, 9, -1)) + for i := 1; i < 4; i++ { + writeSource(t, fs, filepath.Join("content", "perm a", "link", fmt.Sprintf("page_%d.md", i)), + fmt.Sprintf(pageTemplate, 1, i)) + } + writeSource(t, fs, filepath.Join("content", "perm a", "link", "regular", fmt.Sprintf("page_%d.md", 5)), + fmt.Sprintf(pageTemplate, 1, 5)) + + writeSource(t, fs, filepath.Join("content", "l1", "l2", "_index.md"), fmt.Sprintf(pageTemplate, 2, -1)) + writeSource(t, fs, filepath.Join("content", "l1", "l2_2", "_index.md"), fmt.Sprintf(pageTemplate, 22, -1)) + writeSource(t, fs, filepath.Join("content", "l1", "l2", "l3", "_index.md"), fmt.Sprintf(pageTemplate, 3, -1)) + + for level2 := 1; level2 < 4; level2++ { + writeSource(t, fs, filepath.Join("content", "l1", "l2", fmt.Sprintf("page_2_%d.md", level2)), + fmt.Sprintf(pageTemplate, 2, level2)) + } + for level2 := 1; level2 < 3; level2++ { + writeSource(t, fs, filepath.Join("content", "l1", "l2_2", fmt.Sprintf("page_2_2_%d.md", level2)), + fmt.Sprintf(pageTemplate, 2, level2)) + } + for level3 := 1; level3 < 3; level3++ { + writeSource(t, fs, filepath.Join("content", "l1", "l2", "l3", fmt.Sprintf("page_3_%d.md", level3)), + fmt.Sprintf(pageTemplate, 3, level3)) + } + + writeSource(t, fs, filepath.Join("content", "Spaces in Section", "page100.md"), fmt.Sprintf(pageTemplate, 10, 0)) + + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), "<html>Single|{{ .Title }}</html>") + writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"), + ` +{{ $sect := (.Site.GetPage "section" "l1" "l2") }} +<html>List|{{ .Title }}|L1/l2-IsActive: {{ .InSection $sect }} +{{ range .Paginator.Pages }} +PAG|{{ .Title }}|{{ $sect.InSection . }} +{{ end }} +</html>`) + + cfg.Set("paginate", 2) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + require.Len(t, s.RegularPages, 21) + + tests := []struct { + sections string + verify func(p *Page) + }{ + {"elsewhere", func(p *Page) { + assert.Len(p.Pages, 1) + for _, p := range p.Pages { + assert.Equal([]string{"elsewhere"}, p.sections) + } + }}, + {"post", func(p *Page) { + assert.Len(p.Pages, 2) + for _, p := range p.Pages { + assert.Equal("post", p.Section()) + } + }}, + {"empty1", func(p *Page) { + // > b,c + assert.NotNil(p.s.getPage(KindSection, "empty1", "b")) + assert.NotNil(p.s.getPage(KindSection, "empty1", "b", "c")) + + }}, + {"empty2", func(p *Page) { + // > b,c,d where b and d have content files. + b := p.s.getPage(KindSection, "empty2", "b") + assert.NotNil(b) + assert.Equal("T40_-1", b.Title) + c := p.s.getPage(KindSection, "empty2", "b", "c") + assert.NotNil(c) + assert.Equal("Cs", c.Title) + d := p.s.getPage(KindSection, "empty2", "b", "c", "d") + assert.NotNil(d) + assert.Equal("T41_-1", d.Title) + }}, + {"empty3", func(p *Page) { + // b,c,d with regular page in b + b := p.s.getPage(KindSection, "empty3", "b") + assert.NotNil(b) + assert.Len(b.Pages, 1) + assert.Equal("empty3.md", b.Pages[0].File.LogicalName()) + + }}, + {"top", func(p *Page) { + assert.Equal("Tops", p.Title) + assert.Len(p.Pages, 2) + assert.Equal("mypage2.md", p.Pages[0].LogicalName()) + assert.Equal("mypage3.md", p.Pages[1].LogicalName()) + home := p.Parent() + assert.True(home.IsHome()) + assert.Len(p.Sections(), 0) + assert.Equal(home, home.current()) + active, err := home.InSection(home) + assert.NoError(err) + assert.True(active) + }}, + {"l1", func(p *Page) { + assert.Equal("L1s", p.Title) + assert.Len(p.Pages, 2) + assert.True(p.Parent().IsHome()) + assert.Len(p.Sections(), 2) + }}, + {"l1,l2", func(p *Page) { + assert.Equal("T2_-1", p.Title) + assert.Len(p.Pages, 3) + assert.Equal(p, p.Pages[0].Parent()) + assert.Equal("L1s", p.Parent().Title) + assert.Equal("/l1/l2/", p.URLPath.URL) + assert.Equal("/l1/l2/", p.RelPermalink()) + assert.Len(p.Sections(), 1) + + for _, child := range p.Pages { + assert.Equal(p, child.current()) + active, err := child.InSection(p) + assert.NoError(err) + assert.True(active) + active, err = p.InSection(child) + assert.NoError(err) + assert.True(active) + active, err = p.InSection(p.s.getPage(KindHome)) + assert.NoError(err) + assert.False(active) + } + + assert.Equal(p, p.current()) + + }}, + {"l1,l2_2", func(p *Page) { + assert.Equal("T22_-1", p.Title) + assert.Len(p.Pages, 2) + assert.Equal(filepath.FromSlash("l1/l2_2/page_2_2_1.md"), p.Pages[0].Path()) + assert.Equal("L1s", p.Parent().Title) + assert.Len(p.Sections(), 0) + }}, + {"l1,l2,l3", func(p *Page) { + assert.Equal("T3_-1", p.Title) + assert.Len(p.Pages, 2) + assert.Equal("T2_-1", p.Parent().Title) + assert.Len(p.Sections(), 0) + }}, + {"perm a,link", func(p *Page) { + assert.Equal("T9_-1", p.Title) + assert.Equal("/perm-a/link/", p.RelPermalink()) + assert.Len(p.Pages, 4) + first := p.Pages[0] + assert.Equal("/perm-a/link/t1_1/", first.RelPermalink()) + th.assertFileContent("public/perm-a/link/t1_1/index.html", "Single|T1_1") + + last := p.Pages[3] + assert.Equal("/perm-a/link/t1_5/", last.RelPermalink()) + + }}, + } + + for _, test := range tests { + sections := strings.Split(test.sections, ",") + p := s.getPage(KindSection, sections...) + assert.NotNil(p, fmt.Sprint(sections)) + + if p.Pages != nil { + assert.Equal(p.Pages, p.Data["Pages"]) + } + assert.NotNil(p.Parent(), fmt.Sprintf("Parent nil: %q", test.sections)) + test.verify(p) + } + + home := s.getPage(KindHome) + + assert.NotNil(home) + + assert.Len(home.Sections(), 9) + + rootPage := s.getPage(KindPage, "mypage.md") + assert.NotNil(rootPage) + assert.True(rootPage.Parent().IsHome()) + + // Add a odd test for this as this looks a little bit off, but I'm not in the mood + // to think too hard a out this right now. It works, but people will have to spell + // out the directory name as is. + // If we later decide to do something about this, we will have to do some normalization in + // getPage. + // TODO(bep) + sectionWithSpace := s.getPage(KindSection, "Spaces in Section") + require.NotNil(t, sectionWithSpace) + require.Equal(t, "/spaces-in-section/", sectionWithSpace.RelPermalink()) + + th.assertFileContent("public/l1/l2/page/2/index.html", "L1/l2-IsActive: true", "PAG|T2_3|true") + +} diff --git a/hugolib/site_test.go b/hugolib/site_test.go new file mode 100644 index 000000000..ff8fdf48b --- /dev/null +++ b/hugolib/site_test.go @@ -0,0 +1,1100 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "path/filepath" + "strings" + "testing" + + "github.com/bep/inflect" + jww "github.com/spf13/jwalterweatherman" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/source" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/hugofs" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + pageSimpleTitle = `--- +title: simple template +--- +content` + + templateMissingFunc = "{{ .Title | funcdoesnotexists }}" + templateWithURLAbs = "<a href=\"/foobar.jpg\">Going</a>" +) + +func init() { + testMode = true +} + +func pageMust(p *Page, err error) *Page { + if err != nil { + panic(err) + } + return p +} + +func TestRenderWithInvalidTemplate(t *testing.T) { + t.Parallel() + cfg, fs := newTestCfg() + + writeSource(t, fs, filepath.Join("content", "foo.md"), "foo") + + withTemplate := createWithTemplateFromNameValues("missing", templateMissingFunc) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg, WithTemplate: withTemplate}, BuildCfg{}) + + errCount := s.Log.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError) + + // TODO(bep) clean up the template error handling + // The template errors are stored in a slice etc. so we get 4 log entries + // When we should get only 1 + if errCount == 0 { + t.Fatalf("Expecting the template to log 1 ERROR, got %d", errCount) + } +} + +func TestDraftAndFutureRender(t *testing.T) { + t.Parallel() + sources := []source.ByteSource{ + {Name: filepath.FromSlash("sect/doc1.md"), Content: []byte("---\ntitle: doc1\ndraft: true\npublishdate: \"2414-05-29\"\n---\n# doc1\n*some content*")}, + {Name: filepath.FromSlash("sect/doc2.md"), Content: []byte("---\ntitle: doc2\ndraft: true\npublishdate: \"2012-05-29\"\n---\n# doc2\n*some content*")}, + {Name: filepath.FromSlash("sect/doc3.md"), Content: []byte("---\ntitle: doc3\ndraft: false\npublishdate: \"2414-05-29\"\n---\n# doc3\n*some content*")}, + {Name: filepath.FromSlash("sect/doc4.md"), Content: []byte("---\ntitle: doc4\ndraft: false\npublishdate: \"2012-05-29\"\n---\n# doc4\n*some content*")}, + } + + siteSetup := func(t *testing.T, configKeyValues ...interface{}) *Site { + cfg, fs := newTestCfg() + + cfg.Set("baseURL", "http://auth/bub") + + for i := 0; i < len(configKeyValues); i += 2 { + cfg.Set(configKeyValues[i].(string), configKeyValues[i+1]) + } + + for _, src := range sources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + + } + + return buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + } + + // Testing Defaults.. Only draft:true and publishDate in the past should be rendered + s := siteSetup(t) + if len(s.RegularPages) != 1 { + t.Fatal("Draft or Future dated content published unexpectedly") + } + + // only publishDate in the past should be rendered + s = siteSetup(t, "buildDrafts", true) + if len(s.RegularPages) != 2 { + t.Fatal("Future Dated Posts published unexpectedly") + } + + // drafts should not be rendered, but all dates should + s = siteSetup(t, + "buildDrafts", false, + "buildFuture", true) + + if len(s.RegularPages) != 2 { + t.Fatal("Draft posts published unexpectedly") + } + + // all 4 should be included + s = siteSetup(t, + "buildDrafts", true, + "buildFuture", true) + + if len(s.RegularPages) != 4 { + t.Fatal("Drafts or Future posts not included as expected") + } + +} + +func TestFutureExpirationRender(t *testing.T) { + t.Parallel() + sources := []source.ByteSource{ + {Name: filepath.FromSlash("sect/doc3.md"), Content: []byte("---\ntitle: doc1\nexpirydate: \"2400-05-29\"\n---\n# doc1\n*some content*")}, + {Name: filepath.FromSlash("sect/doc4.md"), Content: []byte("---\ntitle: doc2\nexpirydate: \"2000-05-29\"\n---\n# doc2\n*some content*")}, + } + + siteSetup := func(t *testing.T) *Site { + cfg, fs := newTestCfg() + cfg.Set("baseURL", "http://auth/bub") + + for _, src := range sources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + + } + + return buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + } + + s := siteSetup(t) + + if len(s.AllPages) != 1 { + if len(s.RegularPages) > 1 { + t.Fatal("Expired content published unexpectedly") + } + + if len(s.RegularPages) < 1 { + t.Fatal("Valid content expired unexpectedly") + } + } + + if s.AllPages[0].Title == "doc2" { + t.Fatal("Expired content published unexpectedly") + } +} + +func TestLastChange(t *testing.T) { + t.Parallel() + + cfg, fs := newTestCfg() + + writeSource(t, fs, filepath.Join("content", "sect/doc1.md"), "---\ntitle: doc1\nweight: 1\ndate: 2014-05-29\n---\n# doc1\n*some content*") + writeSource(t, fs, filepath.Join("content", "sect/doc2.md"), "---\ntitle: doc2\nweight: 2\ndate: 2015-05-29\n---\n# doc2\n*some content*") + writeSource(t, fs, filepath.Join("content", "sect/doc3.md"), "---\ntitle: doc3\nweight: 3\ndate: 2017-05-29\n---\n# doc3\n*some content*") + writeSource(t, fs, filepath.Join("content", "sect/doc4.md"), "---\ntitle: doc4\nweight: 4\ndate: 2016-05-29\n---\n# doc4\n*some content*") + writeSource(t, fs, filepath.Join("content", "sect/doc5.md"), "---\ntitle: doc5\nweight: 3\n---\n# doc5\n*some content*") + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + + require.False(t, s.Info.LastChange.IsZero(), "Site.LastChange is zero") + require.Equal(t, 2017, s.Info.LastChange.Year(), "Site.LastChange should be set to the page with latest Lastmod (year 2017)") +} + +// Issue #_index +func TestPageWithUnderScoreIndexInFilename(t *testing.T) { + t.Parallel() + + cfg, fs := newTestCfg() + + writeSource(t, fs, filepath.Join("content", "sect/my_index_file.md"), "---\ntitle: doc1\nweight: 1\ndate: 2014-05-29\n---\n# doc1\n*some content*") + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + + require.Len(t, s.RegularPages, 1) + +} + +// Issue #957 +func TestCrossrefs(t *testing.T) { + t.Parallel() + for _, uglyURLs := range []bool{true, false} { + for _, relative := range []bool{true, false} { + doTestCrossrefs(t, relative, uglyURLs) + } + } +} + +func doTestCrossrefs(t *testing.T, relative, uglyURLs bool) { + + baseURL := "http://foo/bar" + + var refShortcode string + var expectedBase string + var expectedURLSuffix string + var expectedPathSuffix string + + if relative { + refShortcode = "relref" + expectedBase = "/bar" + } else { + refShortcode = "ref" + expectedBase = baseURL + } + + if uglyURLs { + expectedURLSuffix = ".html" + expectedPathSuffix = ".html" + } else { + expectedURLSuffix = "/" + expectedPathSuffix = "/index.html" + } + + sources := []source.ByteSource{ + { + Name: filepath.FromSlash("sect/doc1.md"), + Content: []byte(fmt.Sprintf(`Ref 2: {{< %s "sect/doc2.md" >}}`, refShortcode)), + }, + // Issue #1148: Make sure that no P-tags is added around shortcodes. + { + Name: filepath.FromSlash("sect/doc2.md"), + Content: []byte(fmt.Sprintf(`**Ref 1:** + +{{< %s "sect/doc1.md" >}} + +THE END.`, refShortcode)), + }, + // Issue #1753: Should not add a trailing newline after shortcode. + { + Name: filepath.FromSlash("sect/doc3.md"), + Content: []byte(fmt.Sprintf(`**Ref 1:**{{< %s "sect/doc3.md" >}}.`, refShortcode)), + }, + } + + cfg, fs := newTestCfg() + + cfg.Set("baseURL", baseURL) + cfg.Set("uglyURLs", uglyURLs) + cfg.Set("verbose", true) + + for _, src := range sources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + } + + s := buildSingleSite( + t, + deps.DepsCfg{ + Fs: fs, + Cfg: cfg, + WithTemplate: createWithTemplateFromNameValues("_default/single.html", "{{.Content}}")}, + BuildCfg{}) + + if len(s.RegularPages) != 3 { + t.Fatalf("Expected 3 got %d pages", len(s.AllPages)) + } + + th := testHelper{s.Cfg, s.Fs, t} + + tests := []struct { + doc string + expected string + }{ + {filepath.FromSlash(fmt.Sprintf("public/sect/doc1%s", expectedPathSuffix)), fmt.Sprintf("<p>Ref 2: %s/sect/doc2%s</p>\n", expectedBase, expectedURLSuffix)}, + {filepath.FromSlash(fmt.Sprintf("public/sect/doc2%s", expectedPathSuffix)), fmt.Sprintf("<p><strong>Ref 1:</strong></p>\n\n%s/sect/doc1%s\n\n<p>THE END.</p>\n", expectedBase, expectedURLSuffix)}, + {filepath.FromSlash(fmt.Sprintf("public/sect/doc3%s", expectedPathSuffix)), fmt.Sprintf("<p><strong>Ref 1:</strong>%s/sect/doc3%s.</p>\n", expectedBase, expectedURLSuffix)}, + } + + for _, test := range tests { + th.assertFileContent(test.doc, test.expected) + + } + +} + +// Issue #939 +// Issue #1923 +func TestShouldAlwaysHaveUglyURLs(t *testing.T) { + t.Parallel() + for _, uglyURLs := range []bool{true, false} { + doTestShouldAlwaysHaveUglyURLs(t, uglyURLs) + } +} + +func doTestShouldAlwaysHaveUglyURLs(t *testing.T, uglyURLs bool) { + + cfg, fs := newTestCfg() + + cfg.Set("verbose", true) + cfg.Set("baseURL", "http://auth/bub") + cfg.Set("disableSitemap", false) + cfg.Set("disableRSS", false) + cfg.Set("rssURI", "index.xml") + cfg.Set("blackfriday", + map[string]interface{}{ + "plainIDAnchors": true}) + + cfg.Set("uglyURLs", uglyURLs) + + sources := []source.ByteSource{ + {Name: filepath.FromSlash("sect/doc1.md"), Content: []byte("---\nmarkup: markdown\n---\n# title\nsome *content*")}, + {Name: filepath.FromSlash("sect/doc2.md"), Content: []byte("---\nurl: /ugly.html\nmarkup: markdown\n---\n# title\ndoc2 *content*")}, + } + + for _, src := range sources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + } + + writeSource(t, fs, filepath.Join("layouts", "index.html"), "Home Sweet {{ if.IsHome }}Home{{ end }}.") + writeSource(t, fs, filepath.Join("layouts", "_default/single.html"), "{{.Content}}{{ if.IsHome }}This is not home!{{ end }}") + writeSource(t, fs, filepath.Join("layouts", "404.html"), "Page Not Found.{{ if.IsHome }}This is not home!{{ end }}") + writeSource(t, fs, filepath.Join("layouts", "rss.xml"), "<root>RSS</root>") + writeSource(t, fs, filepath.Join("layouts", "sitemap.xml"), "<root>SITEMAP</root>") + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + var expectedPagePath string + if uglyURLs { + expectedPagePath = "public/sect/doc1.html" + } else { + expectedPagePath = "public/sect/doc1/index.html" + } + + tests := []struct { + doc string + expected string + }{ + {filepath.FromSlash("public/index.html"), "Home Sweet Home."}, + {filepath.FromSlash(expectedPagePath), "\n\n<h1 id=\"title\">title</h1>\n\n<p>some <em>content</em></p>\n"}, + {filepath.FromSlash("public/404.html"), "Page Not Found."}, + {filepath.FromSlash("public/index.xml"), "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>\n<root>RSS</root>"}, + {filepath.FromSlash("public/sitemap.xml"), "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>\n<root>SITEMAP</root>"}, + // Issue #1923 + {filepath.FromSlash("public/ugly.html"), "\n\n<h1 id=\"title\">title</h1>\n\n<p>doc2 <em>content</em></p>\n"}, + } + + for _, p := range s.RegularPages { + assert.False(t, p.IsHome()) + } + + for _, test := range tests { + content := readDestination(t, fs, test.doc) + + if content != test.expected { + t.Errorf("%s content expected:\n%q\ngot:\n%q", test.doc, test.expected, content) + } + } + +} + +func TestNewSiteDefaultLang(t *testing.T) { + t.Parallel() + s, err := NewSiteDefaultLang() + require.NoError(t, err) + require.Equal(t, hugofs.Os, s.Fs.Source) + require.Equal(t, hugofs.Os, s.Fs.Destination) +} + +// Issue #3355 +func TestShouldNotWriteZeroLengthFilesToDestination(t *testing.T) { + cfg, fs := newTestCfg() + + writeSource(t, fs, filepath.Join("content", "simple.html"), "simple") + writeSource(t, fs, filepath.Join("layouts", "_default/single.html"), "{{.Content}}") + writeSource(t, fs, filepath.Join("layouts", "_default/list.html"), "") + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + th := testHelper{s.Cfg, s.Fs, t} + + th.assertFileNotExist(filepath.Join("public", "index.html")) +} + +// Issue #1176 +func TestSectionNaming(t *testing.T) { + t.Parallel() + for _, canonify := range []bool{true, false} { + for _, uglify := range []bool{true, false} { + for _, pluralize := range []bool{true, false} { + doTestSectionNaming(t, canonify, uglify, pluralize) + } + } + } +} + +func doTestSectionNaming(t *testing.T, canonify, uglify, pluralize bool) { + + var expectedPathSuffix string + + if uglify { + expectedPathSuffix = ".html" + } else { + expectedPathSuffix = "/index.html" + } + + sources := []source.ByteSource{ + {Name: filepath.FromSlash("sect/doc1.html"), Content: []byte("doc1")}, + // Add one more page to sect to make sure sect is picked in mainSections + {Name: filepath.FromSlash("sect/sect.html"), Content: []byte("sect")}, + {Name: filepath.FromSlash("Fish and Chips/doc2.html"), Content: []byte("doc2")}, + {Name: filepath.FromSlash("ラーメン/doc3.html"), Content: []byte("doc3")}, + } + + cfg, fs := newTestCfg() + + cfg.Set("baseURL", "http://auth/sub/") + cfg.Set("uglyURLs", uglify) + cfg.Set("pluralizeListTitles", pluralize) + cfg.Set("canonifyURLs", canonify) + + for _, source := range sources { + writeSource(t, fs, filepath.Join("content", source.Name), string(source.Content)) + } + + writeSource(t, fs, filepath.Join("layouts", "_default/single.html"), "{{.Content}}") + writeSource(t, fs, filepath.Join("layouts", "_default/list.html"), "{{.Title}}") + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + mainSections, err := s.Info.Param("mainSections") + require.NoError(t, err) + require.Equal(t, []string{"sect"}, mainSections) + + th := testHelper{s.Cfg, s.Fs, t} + tests := []struct { + doc string + pluralAware bool + expected string + }{ + {filepath.FromSlash(fmt.Sprintf("sect/doc1%s", expectedPathSuffix)), false, "doc1"}, + {filepath.FromSlash(fmt.Sprintf("sect%s", expectedPathSuffix)), true, "Sect"}, + {filepath.FromSlash(fmt.Sprintf("fish-and-chips/doc2%s", expectedPathSuffix)), false, "doc2"}, + {filepath.FromSlash(fmt.Sprintf("fish-and-chips%s", expectedPathSuffix)), true, "Fish and Chips"}, + {filepath.FromSlash(fmt.Sprintf("ラーメン/doc3%s", expectedPathSuffix)), false, "doc3"}, + {filepath.FromSlash(fmt.Sprintf("ラーメン%s", expectedPathSuffix)), true, "ラーメン"}, + } + + for _, test := range tests { + + if test.pluralAware && pluralize { + test.expected = inflect.Pluralize(test.expected) + } + + th.assertFileContent(filepath.Join("public", test.doc), test.expected) + } + +} +func TestSkipRender(t *testing.T) { + t.Parallel() + sources := []source.ByteSource{ + {Name: filepath.FromSlash("sect/doc1.html"), Content: []byte("---\nmarkup: markdown\n---\n# title\nsome *content*")}, + {Name: filepath.FromSlash("sect/doc2.html"), Content: []byte("<!doctype html><html><body>more content</body></html>")}, + {Name: filepath.FromSlash("sect/doc3.md"), Content: []byte("# doc3\n*some* content")}, + {Name: filepath.FromSlash("sect/doc4.md"), Content: []byte("---\ntitle: doc4\n---\n# doc4\n*some content*")}, + {Name: filepath.FromSlash("sect/doc5.html"), Content: []byte("<!doctype html><html>{{ template \"head\" }}<body>body5</body></html>")}, + {Name: filepath.FromSlash("sect/doc6.html"), Content: []byte("<!doctype html><html>{{ template \"head_abs\" }}<body>body5</body></html>")}, + {Name: filepath.FromSlash("doc7.html"), Content: []byte("<html><body>doc7 content</body></html>")}, + {Name: filepath.FromSlash("sect/doc8.html"), Content: []byte("---\nmarkup: md\n---\n# title\nsome *content*")}, + // Issue #3021 + {Name: filepath.FromSlash("doc9.html"), Content: []byte("<html><body>doc9: {{< myshortcode >}}</body></html>")}, + } + + cfg, fs := newTestCfg() + + cfg.Set("verbose", true) + cfg.Set("canonifyURLs", true) + cfg.Set("uglyURLs", true) + cfg.Set("baseURL", "http://auth/bub") + + for _, src := range sources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + + } + + writeSource(t, fs, filepath.Join("layouts", "_default/single.html"), "{{.Content}}") + writeSource(t, fs, filepath.Join("layouts", "head"), "<head><script src=\"script.js\"></script></head>") + writeSource(t, fs, filepath.Join("layouts", "head_abs"), "<head><script src=\"/script.js\"></script></head>") + writeSource(t, fs, filepath.Join("layouts", "shortcodes", "myshortcode.html"), "SHORT") + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + tests := []struct { + doc string + expected string + }{ + {filepath.FromSlash("public/sect/doc1.html"), "\n\n<h1 id=\"title\">title</h1>\n\n<p>some <em>content</em></p>\n"}, + {filepath.FromSlash("public/sect/doc2.html"), "<!doctype html><html><body>more content</body></html>"}, + {filepath.FromSlash("public/sect/doc3.html"), "\n\n<h1 id=\"doc3\">doc3</h1>\n\n<p><em>some</em> content</p>\n"}, + {filepath.FromSlash("public/sect/doc4.html"), "\n\n<h1 id=\"doc4\">doc4</h1>\n\n<p><em>some content</em></p>\n"}, + {filepath.FromSlash("public/sect/doc5.html"), "<!doctype html><html><head><script src=\"script.js\"></script></head><body>body5</body></html>"}, + {filepath.FromSlash("public/sect/doc6.html"), "<!doctype html><html><head><script src=\"http://auth/bub/script.js\"></script></head><body>body5</body></html>"}, + {filepath.FromSlash("public/doc7.html"), "<html><body>doc7 content</body></html>"}, + {filepath.FromSlash("public/sect/doc8.html"), "\n\n<h1 id=\"title\">title</h1>\n\n<p>some <em>content</em></p>\n"}, + {filepath.FromSlash("public/doc9.html"), "<html><body>doc9: SHORT</body></html>"}, + } + + for _, test := range tests { + file, err := fs.Destination.Open(test.doc) + if err != nil { + t.Fatalf("Did not find %s in target.", test.doc) + } + + content := helpers.ReaderToString(file) + + if content != test.expected { + t.Errorf("%s content expected:\n%q\ngot:\n%q", test.doc, test.expected, content) + } + } +} + +func TestAbsURLify(t *testing.T) { + t.Parallel() + sources := []source.ByteSource{ + {Name: filepath.FromSlash("sect/doc1.html"), Content: []byte("<!doctype html><html><head></head><body><a href=\"#frag1\">link</a></body></html>")}, + {Name: filepath.FromSlash("blue/doc2.html"), Content: []byte("---\nf: t\n---\n<!doctype html><html><body>more content</body></html>")}, + } + for _, baseURL := range []string{"http://auth/bub", "http://base", "//base"} { + for _, canonify := range []bool{true, false} { + + cfg, fs := newTestCfg() + + cfg.Set("uglyURLs", true) + cfg.Set("canonifyURLs", canonify) + cfg.Set("baseURL", baseURL) + + for _, src := range sources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + + } + + writeSource(t, fs, filepath.Join("layouts", "blue/single.html"), templateWithURLAbs) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + th := testHelper{s.Cfg, s.Fs, t} + + tests := []struct { + file, expected string + }{ + {"public/blue/doc2.html", "<a href=\"%s/foobar.jpg\">Going</a>"}, + {"public/sect/doc1.html", "<!doctype html><html><head></head><body><a href=\"#frag1\">link</a></body></html>"}, + } + + for _, test := range tests { + + expected := test.expected + + if strings.Contains(expected, "%s") { + expected = fmt.Sprintf(expected, baseURL) + } + + if !canonify { + expected = strings.Replace(expected, baseURL, "", -1) + } + + th.assertFileContent(test.file, expected) + + } + } + } +} + +var weightedPage1 = []byte(`+++ +weight = "2" +title = "One" +my_param = "foo" +my_date = 1979-05-27T07:32:00Z ++++ +Front Matter with Ordered Pages`) + +var weightedPage2 = []byte(`+++ +weight = "6" +title = "Two" +publishdate = "2012-03-05" +my_param = "foo" ++++ +Front Matter with Ordered Pages 2`) + +var weightedPage3 = []byte(`+++ +weight = "4" +title = "Three" +date = "2012-04-06" +publishdate = "2012-04-06" +my_param = "bar" +only_one = "yes" +my_date = 2010-05-27T07:32:00Z ++++ +Front Matter with Ordered Pages 3`) + +var weightedPage4 = []byte(`+++ +weight = "4" +title = "Four" +date = "2012-01-01" +publishdate = "2012-01-01" +my_param = "baz" +my_date = 2010-05-27T07:32:00Z +categories = [ "hugo" ] ++++ +Front Matter with Ordered Pages 4. This is longer content`) + +var weightedSources = []source.ByteSource{ + {Name: filepath.FromSlash("sect/doc1.md"), Content: weightedPage1}, + {Name: filepath.FromSlash("sect/doc2.md"), Content: weightedPage2}, + {Name: filepath.FromSlash("sect/doc3.md"), Content: weightedPage3}, + {Name: filepath.FromSlash("sect/doc4.md"), Content: weightedPage4}, +} + +func TestOrderedPages(t *testing.T) { + t.Parallel() + cfg, fs := newTestCfg() + cfg.Set("baseURL", "http://auth/bub") + + for _, src := range weightedSources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + + } + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + + if s.getPage(KindSection, "sect").Pages[1].Title != "Three" || s.getPage(KindSection, "sect").Pages[2].Title != "Four" { + t.Error("Pages in unexpected order.") + } + + bydate := s.RegularPages.ByDate() + + if bydate[0].Title != "One" { + t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "One", bydate[0].Title) + } + + rev := bydate.Reverse() + if rev[0].Title != "Three" { + t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "Three", rev[0].Title) + } + + bypubdate := s.RegularPages.ByPublishDate() + + if bypubdate[0].Title != "One" { + t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "One", bypubdate[0].Title) + } + + rbypubdate := bypubdate.Reverse() + if rbypubdate[0].Title != "Three" { + t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "Three", rbypubdate[0].Title) + } + + bylength := s.RegularPages.ByLength() + if bylength[0].Title != "One" { + t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "One", bylength[0].Title) + } + + rbylength := bylength.Reverse() + if rbylength[0].Title != "Four" { + t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "Four", rbylength[0].Title) + } +} + +var groupedSources = []source.ByteSource{ + {Name: filepath.FromSlash("sect1/doc1.md"), Content: weightedPage1}, + {Name: filepath.FromSlash("sect1/doc2.md"), Content: weightedPage2}, + {Name: filepath.FromSlash("sect2/doc3.md"), Content: weightedPage3}, + {Name: filepath.FromSlash("sect3/doc4.md"), Content: weightedPage4}, +} + +func TestGroupedPages(t *testing.T) { + t.Parallel() + defer func() { + if r := recover(); r != nil { + fmt.Println("Recovered in f", r) + } + }() + + cfg, fs := newTestCfg() + cfg.Set("baseURL", "http://auth/bub") + + writeSourcesToSource(t, "content", fs, groupedSources...) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + rbysection, err := s.RegularPages.GroupBy("Section", "desc") + if err != nil { + t.Fatalf("Unable to make PageGroup array: %s", err) + } + + if rbysection[0].Key != "sect3" { + t.Errorf("PageGroup array in unexpected order. First group key should be '%s', got '%s'", "sect3", rbysection[0].Key) + } + if rbysection[1].Key != "sect2" { + t.Errorf("PageGroup array in unexpected order. Second group key should be '%s', got '%s'", "sect2", rbysection[1].Key) + } + if rbysection[2].Key != "sect1" { + t.Errorf("PageGroup array in unexpected order. Third group key should be '%s', got '%s'", "sect1", rbysection[2].Key) + } + if rbysection[0].Pages[0].Title != "Four" { + t.Errorf("PageGroup has an unexpected page. First group's pages should have '%s', got '%s'", "Four", rbysection[0].Pages[0].Title) + } + if len(rbysection[2].Pages) != 2 { + t.Errorf("PageGroup has unexpected number of pages. Third group should have '%d' pages, got '%d' pages", 2, len(rbysection[2].Pages)) + } + + bytype, err := s.RegularPages.GroupBy("Type", "asc") + if err != nil { + t.Fatalf("Unable to make PageGroup array: %s", err) + } + if bytype[0].Key != "sect1" { + t.Errorf("PageGroup array in unexpected order. First group key should be '%s', got '%s'", "sect1", bytype[0].Key) + } + if bytype[1].Key != "sect2" { + t.Errorf("PageGroup array in unexpected order. Second group key should be '%s', got '%s'", "sect2", bytype[1].Key) + } + if bytype[2].Key != "sect3" { + t.Errorf("PageGroup array in unexpected order. Third group key should be '%s', got '%s'", "sect3", bytype[2].Key) + } + if bytype[2].Pages[0].Title != "Four" { + t.Errorf("PageGroup has an unexpected page. Third group's data should have '%s', got '%s'", "Four", bytype[0].Pages[0].Title) + } + if len(bytype[0].Pages) != 2 { + t.Errorf("PageGroup has unexpected number of pages. First group should have '%d' pages, got '%d' pages", 2, len(bytype[2].Pages)) + } + + bydate, err := s.RegularPages.GroupByDate("2006-01", "asc") + if err != nil { + t.Fatalf("Unable to make PageGroup array: %s", err) + } + if bydate[0].Key != "0001-01" { + t.Errorf("PageGroup array in unexpected order. First group key should be '%s', got '%s'", "0001-01", bydate[0].Key) + } + if bydate[1].Key != "2012-01" { + t.Errorf("PageGroup array in unexpected order. Second group key should be '%s', got '%s'", "2012-01", bydate[1].Key) + } + if bydate[2].Key != "2012-04" { + t.Errorf("PageGroup array in unexpected order. Third group key should be '%s', got '%s'", "2012-04", bydate[2].Key) + } + if bydate[2].Pages[0].Title != "Three" { + t.Errorf("PageGroup has an unexpected page. Third group's pages should have '%s', got '%s'", "Three", bydate[2].Pages[0].Title) + } + if len(bydate[0].Pages) != 2 { + t.Errorf("PageGroup has unexpected number of pages. First group should have '%d' pages, got '%d' pages", 2, len(bydate[2].Pages)) + } + + bypubdate, err := s.RegularPages.GroupByPublishDate("2006") + if err != nil { + t.Fatalf("Unable to make PageGroup array: %s", err) + } + if bypubdate[0].Key != "2012" { + t.Errorf("PageGroup array in unexpected order. First group key should be '%s', got '%s'", "2012", bypubdate[0].Key) + } + if bypubdate[1].Key != "0001" { + t.Errorf("PageGroup array in unexpected order. Second group key should be '%s', got '%s'", "0001", bypubdate[1].Key) + } + if bypubdate[0].Pages[0].Title != "Three" { + t.Errorf("PageGroup has an unexpected page. Third group's pages should have '%s', got '%s'", "Three", bypubdate[0].Pages[0].Title) + } + if len(bypubdate[0].Pages) != 3 { + t.Errorf("PageGroup has unexpected number of pages. First group should have '%d' pages, got '%d' pages", 3, len(bypubdate[0].Pages)) + } + + byparam, err := s.RegularPages.GroupByParam("my_param", "desc") + if err != nil { + t.Fatalf("Unable to make PageGroup array: %s", err) + } + if byparam[0].Key != "foo" { + t.Errorf("PageGroup array in unexpected order. First group key should be '%s', got '%s'", "foo", byparam[0].Key) + } + if byparam[1].Key != "baz" { + t.Errorf("PageGroup array in unexpected order. Second group key should be '%s', got '%s'", "baz", byparam[1].Key) + } + if byparam[2].Key != "bar" { + t.Errorf("PageGroup array in unexpected order. Third group key should be '%s', got '%s'", "bar", byparam[2].Key) + } + if byparam[2].Pages[0].Title != "Three" { + t.Errorf("PageGroup has an unexpected page. Third group's pages should have '%s', got '%s'", "Three", byparam[2].Pages[0].Title) + } + if len(byparam[0].Pages) != 2 { + t.Errorf("PageGroup has unexpected number of pages. First group should have '%d' pages, got '%d' pages", 2, len(byparam[0].Pages)) + } + + _, err = s.RegularPages.GroupByParam("not_exist") + if err == nil { + t.Errorf("GroupByParam didn't return an expected error") + } + + byOnlyOneParam, err := s.RegularPages.GroupByParam("only_one") + if err != nil { + t.Fatalf("Unable to make PageGroup array: %s", err) + } + if len(byOnlyOneParam) != 1 { + t.Errorf("PageGroup array has unexpected elements. Group length should be '%d', got '%d'", 1, len(byOnlyOneParam)) + } + if byOnlyOneParam[0].Key != "yes" { + t.Errorf("PageGroup array in unexpected order. First group key should be '%s', got '%s'", "yes", byOnlyOneParam[0].Key) + } + + byParamDate, err := s.RegularPages.GroupByParamDate("my_date", "2006-01") + if err != nil { + t.Fatalf("Unable to make PageGroup array: %s", err) + } + if byParamDate[0].Key != "2010-05" { + t.Errorf("PageGroup array in unexpected order. First group key should be '%s', got '%s'", "2010-05", byParamDate[0].Key) + } + if byParamDate[1].Key != "1979-05" { + t.Errorf("PageGroup array in unexpected order. Second group key should be '%s', got '%s'", "1979-05", byParamDate[1].Key) + } + if byParamDate[1].Pages[0].Title != "One" { + t.Errorf("PageGroup has an unexpected page. Second group's pages should have '%s', got '%s'", "One", byParamDate[1].Pages[0].Title) + } + if len(byParamDate[0].Pages) != 2 { + t.Errorf("PageGroup has unexpected number of pages. First group should have '%d' pages, got '%d' pages", 2, len(byParamDate[2].Pages)) + } +} + +var pageWithWeightedTaxonomies1 = []byte(`+++ +tags = [ "a", "b", "c" ] +tags_weight = 22 +categories = ["d"] +title = "foo" +categories_weight = 44 ++++ +Front Matter with weighted tags and categories`) + +var pageWithWeightedTaxonomies2 = []byte(`+++ +tags = "a" +tags_weight = 33 +title = "bar" +categories = [ "d", "e" ] +categories_weight = 11 +alias = "spf13" +date = 1979-05-27T07:32:00Z ++++ +Front Matter with weighted tags and categories`) + +var pageWithWeightedTaxonomies3 = []byte(`+++ +title = "bza" +categories = [ "e" ] +categories_weight = 11 +alias = "spf13" +date = 2010-05-27T07:32:00Z ++++ +Front Matter with weighted tags and categories`) + +func TestWeightedTaxonomies(t *testing.T) { + t.Parallel() + sources := []source.ByteSource{ + {Name: filepath.FromSlash("sect/doc1.md"), Content: pageWithWeightedTaxonomies2}, + {Name: filepath.FromSlash("sect/doc2.md"), Content: pageWithWeightedTaxonomies1}, + {Name: filepath.FromSlash("sect/doc3.md"), Content: pageWithWeightedTaxonomies3}, + } + taxonomies := make(map[string]string) + + taxonomies["tag"] = "tags" + taxonomies["category"] = "categories" + + cfg, fs := newTestCfg() + + cfg.Set("baseURL", "http://auth/bub") + cfg.Set("taxonomies", taxonomies) + + writeSourcesToSource(t, "content", fs, sources...) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + if s.Taxonomies["tags"]["a"][0].Page.Title != "foo" { + t.Errorf("Pages in unexpected order, 'foo' expected first, got '%v'", s.Taxonomies["tags"]["a"][0].Page.Title) + } + + if s.Taxonomies["categories"]["d"][0].Page.Title != "bar" { + t.Errorf("Pages in unexpected order, 'bar' expected first, got '%v'", s.Taxonomies["categories"]["d"][0].Page.Title) + } + + if s.Taxonomies["categories"]["e"][0].Page.Title != "bza" { + t.Errorf("Pages in unexpected order, 'bza' expected first, got '%v'", s.Taxonomies["categories"]["e"][0].Page.Title) + } +} + +func findPage(site *Site, f string) *Page { + sp := source.NewSourceSpec(site.Cfg, site.Fs) + currentPath := sp.NewFile(filepath.FromSlash(f)) + //t.Logf("looking for currentPath: %s", currentPath.Path()) + + for _, page := range site.Pages { + //t.Logf("page: %s", page.Source.Path()) + if page.Source.Path() == currentPath.Path() { + return page + } + } + return nil +} + +func setupLinkingMockSite(t *testing.T) *Site { + sources := []source.ByteSource{ + {Name: filepath.FromSlash("level2/unique.md"), Content: []byte("")}, + {Name: filepath.FromSlash("index.md"), Content: []byte("")}, + {Name: filepath.FromSlash("rootfile.md"), Content: []byte("")}, + {Name: filepath.FromSlash("root-image.png"), Content: []byte("")}, + + {Name: filepath.FromSlash("level2/2-root.md"), Content: []byte("")}, + {Name: filepath.FromSlash("level2/index.md"), Content: []byte("")}, + {Name: filepath.FromSlash("level2/common.md"), Content: []byte("")}, + + {Name: filepath.FromSlash("level2/2-image.png"), Content: []byte("")}, + {Name: filepath.FromSlash("level2/common.png"), Content: []byte("")}, + + {Name: filepath.FromSlash("level2/level3/3-root.md"), Content: []byte("")}, + {Name: filepath.FromSlash("level2/level3/index.md"), Content: []byte("")}, + {Name: filepath.FromSlash("level2/level3/common.md"), Content: []byte("")}, + {Name: filepath.FromSlash("level2/level3/3-image.png"), Content: []byte("")}, + {Name: filepath.FromSlash("level2/level3/common.png"), Content: []byte("")}, + } + + cfg, fs := newTestCfg() + + cfg.Set("baseURL", "http://auth/") + cfg.Set("uglyURLs", false) + cfg.Set("outputs", map[string]interface{}{ + "page": []string{"HTML", "AMP"}, + }) + cfg.Set("pluralizeListTitles", false) + cfg.Set("canonifyURLs", false) + cfg.Set("blackfriday", + map[string]interface{}{ + "sourceRelativeLinksProjectFolder": "/docs"}) + writeSourcesToSource(t, "content", fs, sources...) + return buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + +} + +func TestRefLinking(t *testing.T) { + t.Parallel() + site := setupLinkingMockSite(t) + + currentPage := findPage(site, "level2/level3/index.md") + if currentPage == nil { + t.Fatalf("failed to find current page in site") + } + + for i, test := range []struct { + link string + outputFormat string + relative bool + expected string + }{ + {"unique.md", "", true, "/level2/unique/"}, + {"level2/common.md", "", true, "/level2/common/"}, + {"3-root.md", "", true, "/level2/level3/3-root/"}, + {"level2/level3/index.md", "amp", true, "/amp/level2/level3/"}, + {"level2/index.md", "amp", false, "http://auth/amp/level2/"}, + } { + if out, err := site.Info.refLink(test.link, currentPage, test.relative, test.outputFormat); err != nil || out != test.expected { + t.Errorf("[%d] Expected %s to resolve to (%s), got (%s) - error: %s", i, test.link, test.expected, out, err) + } + } + + // TODO: and then the failure cases. +} + +func TestSourceRelativeLinksing(t *testing.T) { + t.Parallel() + site := setupLinkingMockSite(t) + + type resultMap map[string]string + + okresults := map[string]resultMap{ + "index.md": map[string]string{ + "/docs/rootfile.md": "/rootfile/", + "rootfile.md": "/rootfile/", + // See #3396 -- this may potentially be ambiguous (i.e. name conflict with home page). + // But the user have chosen so. This index.md patterns is more relevant in /sub-folders. + "index.md": "/", + "level2/2-root.md": "/level2/2-root/", + "/docs/level2/2-root.md": "/level2/2-root/", + "level2/level3/3-root.md": "/level2/level3/3-root/", + "/docs/level2/level3/3-root.md": "/level2/level3/3-root/", + "/docs/level2/2-root/": "/level2/2-root/", + "/docs/level2/2-root": "/level2/2-root/", + "/level2/2-root/": "/level2/2-root/", + "/level2/2-root": "/level2/2-root/", + }, "rootfile.md": map[string]string{ + "/docs/rootfile.md": "/rootfile/", + "rootfile.md": "/rootfile/", + "level2/2-root.md": "/level2/2-root/", + "/docs/level2/2-root.md": "/level2/2-root/", + "level2/level3/3-root.md": "/level2/level3/3-root/", + "/docs/level2/level3/3-root.md": "/level2/level3/3-root/", + }, "level2/2-root.md": map[string]string{ + "../rootfile.md": "/rootfile/", + "/docs/rootfile.md": "/rootfile/", + "2-root.md": "/level2/2-root/", + "../level2/2-root.md": "/level2/2-root/", + "./2-root.md": "/level2/2-root/", + "/docs/level2/2-root.md": "/level2/2-root/", + "level3/3-root.md": "/level2/level3/3-root/", + "../level2/level3/3-root.md": "/level2/level3/3-root/", + "/docs/level2/level3/3-root.md": "/level2/level3/3-root/", + }, "level2/index.md": map[string]string{ + "../rootfile.md": "/rootfile/", + "/docs/rootfile.md": "/rootfile/", + "2-root.md": "/level2/2-root/", + "../level2/2-root.md": "/level2/2-root/", + "./2-root.md": "/level2/2-root/", + "/docs/level2/2-root.md": "/level2/2-root/", + "level3/3-root.md": "/level2/level3/3-root/", + "../level2/level3/3-root.md": "/level2/level3/3-root/", + "/docs/level2/level3/3-root.md": "/level2/level3/3-root/", + }, "level2/level3/3-root.md": map[string]string{ + "../../rootfile.md": "/rootfile/", + "/docs/rootfile.md": "/rootfile/", + "../2-root.md": "/level2/2-root/", + "/docs/level2/2-root.md": "/level2/2-root/", + "3-root.md": "/level2/level3/3-root/", + "./3-root.md": "/level2/level3/3-root/", + "/docs/level2/level3/3-root.md": "/level2/level3/3-root/", + }, "level2/level3/index.md": map[string]string{ + "../../rootfile.md": "/rootfile/", + "/docs/rootfile.md": "/rootfile/", + "../2-root.md": "/level2/2-root/", + "/docs/level2/2-root.md": "/level2/2-root/", + "3-root.md": "/level2/level3/3-root/", + "./3-root.md": "/level2/level3/3-root/", + "/docs/level2/level3/3-root.md": "/level2/level3/3-root/", + }, + } + + for currentFile, results := range okresults { + currentPage := findPage(site, currentFile) + if currentPage == nil { + t.Fatalf("failed to find current page in site") + } + for link, url := range results { + if out, err := site.Info.SourceRelativeLink(link, currentPage); err != nil || out != url { + t.Errorf("Expected %s to resolve to (%s), got (%s) - error: %s", link, url, out, err) + } else { + //t.Logf("tested ok %s maps to %s", link, out) + } + } + } + // TODO: and then the failure cases. + // "https://docker.com": "", + // site_test.go:1094: Expected https://docker.com to resolve to (), got () - error: Not a plain filepath link (https://docker.com) + +} + +func TestSourceRelativeLinkFileing(t *testing.T) { + t.Parallel() + site := setupLinkingMockSite(t) + + type resultMap map[string]string + + okresults := map[string]resultMap{ + "index.md": map[string]string{ + "/root-image.png": "/root-image.png", + "root-image.png": "/root-image.png", + }, "rootfile.md": map[string]string{ + "/root-image.png": "/root-image.png", + }, "level2/2-root.md": map[string]string{ + "/root-image.png": "/root-image.png", + "common.png": "/level2/common.png", + }, "level2/index.md": map[string]string{ + "/root-image.png": "/root-image.png", + "common.png": "/level2/common.png", + "./common.png": "/level2/common.png", + }, "level2/level3/3-root.md": map[string]string{ + "/root-image.png": "/root-image.png", + "common.png": "/level2/level3/common.png", + "../common.png": "/level2/common.png", + }, "level2/level3/index.md": map[string]string{ + "/root-image.png": "/root-image.png", + "common.png": "/level2/level3/common.png", + "../common.png": "/level2/common.png", + }, + } + + for currentFile, results := range okresults { + currentPage := findPage(site, currentFile) + if currentPage == nil { + t.Fatalf("failed to find current page in site") + } + for link, url := range results { + if out, err := site.Info.SourceRelativeLinkFile(link, currentPage); err != nil || out != url { + t.Errorf("Expected %s to resolve to (%s), got (%s) - error: %s", link, url, out, err) + } else { + //t.Logf("tested ok %s maps to %s", link, out) + } + } + } +} diff --git a/hugolib/site_url_test.go b/hugolib/site_url_test.go new file mode 100644 index 000000000..272c78c7e --- /dev/null +++ b/hugolib/site_url_test.go @@ -0,0 +1,90 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "path/filepath" + "testing" + + "html/template" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/source" + "github.com/stretchr/testify/require" +) + +const slugDoc1 = "---\ntitle: slug doc 1\nslug: slug-doc-1\naliases:\n - sd1/foo/\n - sd2\n - sd3/\n - sd4.html\n---\nslug doc 1 content\n" + +const slugDoc2 = `--- +title: slug doc 2 +slug: slug-doc-2 +--- +slug doc 2 content +` + +var urlFakeSource = []source.ByteSource{ + {Name: filepath.FromSlash("content/blue/doc1.md"), Content: []byte(slugDoc1)}, + {Name: filepath.FromSlash("content/blue/doc2.md"), Content: []byte(slugDoc2)}, +} + +// Issue #1105 +func TestShouldNotAddTrailingSlashToBaseURL(t *testing.T) { + t.Parallel() + for i, this := range []struct { + in string + expected string + }{ + {"http://base.com/", "http://base.com/"}, + {"http://base.com/sub/", "http://base.com/sub/"}, + {"http://base.com/sub", "http://base.com/sub"}, + {"http://base.com", "http://base.com"}} { + + cfg, fs := newTestCfg() + cfg.Set("baseURL", this.in) + d := deps.DepsCfg{Cfg: cfg, Fs: fs} + s, err := NewSiteForCfg(d) + require.NoError(t, err) + s.initializeSiteInfo() + + if s.Info.BaseURL() != template.URL(this.expected) { + t.Errorf("[%d] got %s expected %s", i, s.Info.BaseURL(), this.expected) + } + } +} + +func TestPageCount(t *testing.T) { + t.Parallel() + cfg, fs := newTestCfg() + cfg.Set("uglyURLs", false) + cfg.Set("paginate", 10) + + writeSourcesToSource(t, "", fs, urlFakeSource...) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + _, err := s.Fs.Destination.Open("public/blue") + if err != nil { + t.Errorf("No indexed rendered.") + } + + for _, pth := range []string{ + "public/sd1/foo/index.html", + "public/sd2/index.html", + "public/sd3/index.html", + "public/sd4.html", + } { + if _, err := s.Fs.Destination.Open(filepath.FromSlash(pth)); err != nil { + t.Errorf("No alias rendered: %s", pth) + } + } +} diff --git a/hugolib/sitemap.go b/hugolib/sitemap.go new file mode 100644 index 000000000..64d6f5b7a --- /dev/null +++ b/hugolib/sitemap.go @@ -0,0 +1,45 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "github.com/spf13/cast" + jww "github.com/spf13/jwalterweatherman" +) + +// Sitemap configures the sitemap to be generated. +type Sitemap struct { + ChangeFreq string + Priority float64 + Filename string +} + +func parseSitemap(input map[string]interface{}) Sitemap { + sitemap := Sitemap{Priority: -1, Filename: "sitemap.xml"} + + for key, value := range input { + switch key { + case "changefreq": + sitemap.ChangeFreq = cast.ToString(value) + case "priority": + sitemap.Priority = cast.ToFloat64(value) + case "filename": + sitemap.Filename = cast.ToString(value) + default: + jww.WARN.Printf("Unknown Sitemap field: %s\n", key) + } + } + + return sitemap +} diff --git a/hugolib/sitemap_test.go b/hugolib/sitemap_test.go new file mode 100644 index 000000000..002f772d8 --- /dev/null +++ b/hugolib/sitemap_test.go @@ -0,0 +1,102 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "testing" + + "reflect" + + "github.com/stretchr/testify/require" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl" +) + +const sitemapTemplate = `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> + {{ range .Data.Pages }} + <url> + <loc>{{ .Permalink }}</loc>{{ if not .Lastmod.IsZero }} + <lastmod>{{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}</lastmod>{{ end }}{{ with .Sitemap.ChangeFreq }} + <changefreq>{{ . }}</changefreq>{{ end }}{{ if ge .Sitemap.Priority 0.0 }} + <priority>{{ .Sitemap.Priority }}</priority>{{ end }} + </url> + {{ end }} +</urlset>` + +func TestSitemapOutput(t *testing.T) { + t.Parallel() + for _, internal := range []bool{false, true} { + doTestSitemapOutput(t, internal) + } +} + +func doTestSitemapOutput(t *testing.T, internal bool) { + + cfg, fs := newTestCfg() + cfg.Set("baseURL", "http://auth/bub/") + + depsCfg := deps.DepsCfg{Fs: fs, Cfg: cfg} + + depsCfg.WithTemplate = func(templ tpl.TemplateHandler) error { + if !internal { + templ.AddTemplate("sitemap.xml", sitemapTemplate) + } + + // We want to check that the 404 page is not included in the sitemap + // output. This template should have no effect either way, but include + // it for the clarity. + templ.AddTemplate("404.html", "Not found") + return nil + } + + writeSourcesToSource(t, "content", fs, weightedSources...) + s := buildSingleSite(t, depsCfg, BuildCfg{}) + th := testHelper{s.Cfg, s.Fs, t} + outputSitemap := "public/sitemap.xml" + + th.assertFileContent(outputSitemap, + // Regular page + " <loc>http://auth/bub/sect/doc1/</loc>", + // Home page + "<loc>http://auth/bub/</loc>", + // Section + "<loc>http://auth/bub/sect/</loc>", + // Tax terms + "<loc>http://auth/bub/categories/</loc>", + // Tax list + "<loc>http://auth/bub/categories/hugo/</loc>", + ) + + content := readDestination(th.T, th.Fs, outputSitemap) + require.NotContains(t, content, "404") + +} + +func TestParseSitemap(t *testing.T) { + t.Parallel() + expected := Sitemap{Priority: 3.0, Filename: "doo.xml", ChangeFreq: "3"} + input := map[string]interface{}{ + "changefreq": "3", + "priority": 3.0, + "filename": "doo.xml", + "unknown": "ignore", + } + result := parseSitemap(input) + + if !reflect.DeepEqual(expected, result) { + t.Errorf("Got \n%v expected \n%v", result, expected) + } + +} diff --git a/hugolib/taxonomy.go b/hugolib/taxonomy.go new file mode 100644 index 000000000..35e0795e5 --- /dev/null +++ b/hugolib/taxonomy.go @@ -0,0 +1,224 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "sort" +) + +// The TaxonomyList is a list of all taxonomies and their values +// e.g. List['tags'] => TagTaxonomy (from above) +type TaxonomyList map[string]Taxonomy + +func (tl TaxonomyList) String() string { + return fmt.Sprintf("TaxonomyList(%d)", len(tl)) +} + +// A Taxonomy is a map of keywords to a list of pages. +// For example +// TagTaxonomy['technology'] = WeightedPages +// TagTaxonomy['go'] = WeightedPages2 +type Taxonomy map[string]WeightedPages + +// WeightedPages is a list of Pages with their corresponding (and relative) weight +// [{Weight: 30, Page: *1}, {Weight: 40, Page: *2}] +type WeightedPages []WeightedPage + +// A WeightedPage is a Page with a weight. +type WeightedPage struct { + Weight int + *Page +} + +func (w WeightedPage) String() string { + return fmt.Sprintf("WeightedPage(%d,%q)", w.Weight, w.Page.Title) +} + +// OrderedTaxonomy is another representation of an Taxonomy using an array rather than a map. +// Important because you can't order a map. +type OrderedTaxonomy []OrderedTaxonomyEntry + +// OrderedTaxonomyEntry is similar to an element of a Taxonomy, but with the key embedded (as name) +// e.g: {Name: Technology, WeightedPages: Taxonomyedpages} +type OrderedTaxonomyEntry struct { + Name string + WeightedPages WeightedPages +} + +// Get the weighted pages for the given key. +func (i Taxonomy) Get(key string) WeightedPages { + return i[key] +} + +// Count the weighted pages for the given key. +func (i Taxonomy) Count(key string) int { return len(i[key]) } + +func (i Taxonomy) add(key string, w WeightedPage) { + i[key] = append(i[key], w) +} + +// TaxonomyArray returns an ordered taxonomy with a non defined order. +func (i Taxonomy) TaxonomyArray() OrderedTaxonomy { + ies := make([]OrderedTaxonomyEntry, len(i)) + count := 0 + for k, v := range i { + ies[count] = OrderedTaxonomyEntry{Name: k, WeightedPages: v} + count++ + } + return ies +} + +// Alphabetical returns an ordered taxonomy sorted by key name. +func (i Taxonomy) Alphabetical() OrderedTaxonomy { + name := func(i1, i2 *OrderedTaxonomyEntry) bool { + return i1.Name < i2.Name + } + + ia := i.TaxonomyArray() + oiBy(name).Sort(ia) + return ia +} + +// ByCount returns an ordered taxonomy sorted by # of pages per key. +// If taxonomies have the same # of pages, sort them alphabetical +func (i Taxonomy) ByCount() OrderedTaxonomy { + count := func(i1, i2 *OrderedTaxonomyEntry) bool { + li1 := len(i1.WeightedPages) + li2 := len(i2.WeightedPages) + + if li1 == li2 { + return i1.Name < i2.Name + } + return li1 > li2 + } + + ia := i.TaxonomyArray() + oiBy(count).Sort(ia) + return ia +} + +// Pages returns the Pages for this taxonomy. +func (ie OrderedTaxonomyEntry) Pages() Pages { + return ie.WeightedPages.Pages() +} + +// Count returns the count the pages in this taxonomy. +func (ie OrderedTaxonomyEntry) Count() int { + return len(ie.WeightedPages) +} + +// Term returns the name given to this taxonomy. +func (ie OrderedTaxonomyEntry) Term() string { + return ie.Name +} + +// Reverse reverses the order of the entries in this taxonomy. +func (t OrderedTaxonomy) Reverse() OrderedTaxonomy { + for i, j := 0, len(t)-1; i < j; i, j = i+1, j-1 { + t[i], t[j] = t[j], t[i] + } + + return t +} + +// A type to implement the sort interface for TaxonomyEntries. +type orderedTaxonomySorter struct { + taxonomy OrderedTaxonomy + by oiBy +} + +// Closure used in the Sort.Less method. +type oiBy func(i1, i2 *OrderedTaxonomyEntry) bool + +func (by oiBy) Sort(taxonomy OrderedTaxonomy) { + ps := &orderedTaxonomySorter{ + taxonomy: taxonomy, + by: by, // The Sort method's receiver is the function (closure) that defines the sort order. + } + sort.Stable(ps) +} + +// Len is part of sort.Interface. +func (s *orderedTaxonomySorter) Len() int { + return len(s.taxonomy) +} + +// Swap is part of sort.Interface. +func (s *orderedTaxonomySorter) Swap(i, j int) { + s.taxonomy[i], s.taxonomy[j] = s.taxonomy[j], s.taxonomy[i] +} + +// Less is part of sort.Interface. It is implemented by calling the "by" closure in the sorter. +func (s *orderedTaxonomySorter) Less(i, j int) bool { + return s.by(&s.taxonomy[i], &s.taxonomy[j]) +} + +// Pages returns the Pages in this weighted page set. +func (wp WeightedPages) Pages() Pages { + pages := make(Pages, len(wp)) + for i := range wp { + pages[i] = wp[i].Page + } + return pages +} + +// Prev returns the previous Page relative to the given Page in +// this weighted page set. +func (wp WeightedPages) Prev(cur *Page) *Page { + for x, c := range wp { + if c.Page.UniqueID() == cur.UniqueID() { + if x == 0 { + return wp[len(wp)-1].Page + } + return wp[x-1].Page + } + } + return nil +} + +// Next returns the next Page relative to the given Page in +// this weighted page set. +func (wp WeightedPages) Next(cur *Page) *Page { + for x, c := range wp { + if c.Page.UniqueID() == cur.UniqueID() { + if x < len(wp)-1 { + return wp[x+1].Page + } + return wp[0].Page + } + } + return nil +} + +func (wp WeightedPages) Len() int { return len(wp) } +func (wp WeightedPages) Swap(i, j int) { wp[i], wp[j] = wp[j], wp[i] } + +// Sort stable sorts this weighted page set. +func (wp WeightedPages) Sort() { sort.Stable(wp) } + +// Count returns the number of pages in this weighted page set. +func (wp WeightedPages) Count() int { return len(wp) } + +func (wp WeightedPages) Less(i, j int) bool { + if wp[i].Weight == wp[j].Weight { + if wp[i].Page.Date.Equal(wp[j].Page.Date) { + return wp[i].Page.Title < wp[j].Page.Title + } + return wp[i].Page.Date.After(wp[i].Page.Date) + } + return wp[i].Weight < wp[j].Weight +} + +// TODO mimic PagesSorter for WeightedPages diff --git a/hugolib/taxonomy_test.go b/hugolib/taxonomy_test.go new file mode 100644 index 000000000..4f8717d72 --- /dev/null +++ b/hugolib/taxonomy_test.go @@ -0,0 +1,187 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/gohugoio/hugo/deps" +) + +func TestByCountOrderOfTaxonomies(t *testing.T) { + t.Parallel() + taxonomies := make(map[string]string) + + taxonomies["tag"] = "tags" + taxonomies["category"] = "categories" + + cfg, fs := newTestCfg() + + cfg.Set("taxonomies", taxonomies) + + writeSource(t, fs, filepath.Join("content", "page.md"), pageYamlWithTaxonomiesA) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + st := make([]string, 0) + for _, t := range s.Taxonomies["tags"].ByCount() { + st = append(st, t.Name) + } + + if !reflect.DeepEqual(st, []string{"a", "b", "c"}) { + t.Fatalf("ordered taxonomies do not match [a, b, c]. Got: %s", st) + } +} + +// +func TestTaxonomiesWithAndWithoutContentFile(t *testing.T) { + for _, uglyURLs := range []bool{false, true} { + for _, preserveTaxonomyNames := range []bool{false, true} { + t.Run(fmt.Sprintf("uglyURLs=%t,preserveTaxonomyNames=%t", uglyURLs, preserveTaxonomyNames), func(t *testing.T) { + doTestTaxonomiesWithAndWithoutContentFile(t, preserveTaxonomyNames, uglyURLs) + }) + } + } +} + +func doTestTaxonomiesWithAndWithoutContentFile(t *testing.T, preserveTaxonomyNames, uglyURLs bool) { + t.Parallel() + + siteConfig := ` +baseURL = "http://example.com/blog" +preserveTaxonomyNames = %t +uglyURLs = %t + +paginate = 1 +defaultContentLanguage = "en" + +[Taxonomies] +tag = "tags" +category = "categories" +other = "others" +empty = "empties" +` + + pageTemplate := `--- +title: "%s" +tags: +%s +categories: +%s +others: +%s +--- +# Doc +` + + siteConfig = fmt.Sprintf(siteConfig, preserveTaxonomyNames, uglyURLs) + + th, h := newTestSitesFromConfigWithDefaultTemplates(t, siteConfig) + require.Len(t, h.Sites, 1) + + fs := th.Fs + + if preserveTaxonomyNames { + writeSource(t, fs, "content/p1.md", fmt.Sprintf(pageTemplate, "t1/c1", "- tag1", "- cat1", "- o1")) + } else { + // Check lower-casing of tags + writeSource(t, fs, "content/p1.md", fmt.Sprintf(pageTemplate, "t1/c1", "- Tag1", "- cAt1", "- o1")) + + } + writeSource(t, fs, "content/p2.md", fmt.Sprintf(pageTemplate, "t2/c1", "- tag2", "- cat1", "- o1")) + writeSource(t, fs, "content/p3.md", fmt.Sprintf(pageTemplate, "t2/c12", "- tag2", "- cat2", "- o1")) + writeSource(t, fs, "content/p4.md", fmt.Sprintf(pageTemplate, "Hello World", "", "", "- \"Hello Hugo world\"")) + + writeNewContentFile(t, fs, "Category Terms", "2017-01-01", "content/categories/_index.md", 10) + writeNewContentFile(t, fs, "Tag1 List", "2017-01-01", "content/tags/tag1/_index.md", 10) + + err := h.Build(BuildCfg{}) + + require.NoError(t, err) + + // So what we have now is: + // 1. categories with terms content page, but no content page for the only c1 category + // 2. tags with no terms content page, but content page for one of 2 tags (tag1) + // 3. the "others" taxonomy with no content pages. + + pathFunc := func(s string) string { + if uglyURLs { + return strings.Replace(s, "/index.html", ".html", 1) + } + return s + } + + // 1. + th.assertFileContent(pathFunc("public/categories/cat1/index.html"), "List", "Cat1") + th.assertFileContent(pathFunc("public/categories/index.html"), "Terms List", "Category Terms") + + // 2. + th.assertFileContent(pathFunc("public/tags/tag2/index.html"), "List", "Tag2") + th.assertFileContent(pathFunc("public/tags/tag1/index.html"), "List", "Tag1") + th.assertFileContent(pathFunc("public/tags/index.html"), "Terms List", "Tags") + + // 3. + th.assertFileContent(pathFunc("public/others/o1/index.html"), "List", "O1") + th.assertFileContent(pathFunc("public/others/index.html"), "Terms List", "Others") + + s := h.Sites[0] + + // Make sure that each KindTaxonomyTerm page has an appropriate number + // of KindTaxonomy pages in its Pages slice. + taxonomyTermPageCounts := map[string]int{ + "tags": 2, + "categories": 2, + "others": 2, + "empties": 0, + } + + for taxonomy, count := range taxonomyTermPageCounts { + term := s.getPage(KindTaxonomyTerm, taxonomy) + require.NotNil(t, term) + require.Len(t, term.Pages, count) + + for _, page := range term.Pages { + require.Equal(t, KindTaxonomy, page.Kind) + } + } + + cat1 := s.getPage(KindTaxonomy, "categories", "cat1") + require.NotNil(t, cat1) + if uglyURLs { + require.Equal(t, "/blog/categories/cat1.html", cat1.RelPermalink()) + } else { + require.Equal(t, "/blog/categories/cat1/", cat1.RelPermalink()) + } + + // Issue #3070 preserveTaxonomyNames + if preserveTaxonomyNames { + helloWorld := s.getPage(KindTaxonomy, "others", "Hello Hugo world") + require.NotNil(t, helloWorld) + require.Equal(t, "Hello Hugo world", helloWorld.Title) + } else { + helloWorld := s.getPage(KindTaxonomy, "others", "hello-hugo-world") + require.NotNil(t, helloWorld) + require.Equal(t, "Hello Hugo World", helloWorld.Title) + } + + // Issue #2977 + th.assertFileContent(pathFunc("public/empties/index.html"), "Terms List", "Empties") + +} diff --git a/hugolib/template_engines_test.go b/hugolib/template_engines_test.go new file mode 100644 index 000000000..e2e4ee986 --- /dev/null +++ b/hugolib/template_engines_test.go @@ -0,0 +1,95 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "path/filepath" + "testing" + + "strings" + + "github.com/gohugoio/hugo/deps" +) + +func TestAllTemplateEngines(t *testing.T) { + t.Parallel() + noOp := func(s string) string { + return s + } + + amberFixer := func(s string) string { + fixed := strings.Replace(s, "{{ .Title", "{{ Title", -1) + fixed = strings.Replace(fixed, ".Content", "Content", -1) + fixed = strings.Replace(fixed, "{{", "#{", -1) + fixed = strings.Replace(fixed, "}}", "}", -1) + fixed = strings.Replace(fixed, `title "hello world"`, `title("hello world")`, -1) + + return fixed + } + + for _, config := range []struct { + suffix string + templateFixer func(s string) string + }{ + {"amber", amberFixer}, + {"html", noOp}, + {"ace", noOp}, + } { + doTestTemplateEngine(t, config.suffix, config.templateFixer) + + } + +} + +func doTestTemplateEngine(t *testing.T, suffix string, templateFixer func(s string) string) { + + cfg, fs := newTestCfg() + + writeSource(t, fs, filepath.Join("content", "p.md"), ` +--- +title: My Title +--- +My Content +`) + + t.Log("Testing", suffix) + + templTemplate := ` +p + | + | Page Title: {{ .Title }} + br + | Page Content: {{ .Content }} + br + | {{ title "hello world" }} + +` + + templ := templateFixer(templTemplate) + + t.Log(templ) + + writeSource(t, fs, filepath.Join("layouts", "_default", fmt.Sprintf("single.%s", suffix)), templ) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + th := testHelper{s.Cfg, s.Fs, t} + + th.assertFileContent(filepath.Join("public", "p", "index.html"), + "Page Title: My Title", + "My Content", + "Hello World", + ) + +} diff --git a/hugolib/template_test.go b/hugolib/template_test.go new file mode 100644 index 000000000..a5bec103a --- /dev/null +++ b/hugolib/template_test.go @@ -0,0 +1,215 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/hugofs" + + "github.com/spf13/viper" +) + +func TestTemplateLookupOrder(t *testing.T) { + t.Parallel() + var ( + fs *hugofs.Fs + cfg *viper.Viper + th testHelper + ) + + // Variants base templates: + // 1. <current-path>/<template-name>-baseof.<suffix>, e.g. list-baseof.<suffix>. + // 2. <current-path>/baseof.<suffix> + // 3. _default/<template-name>-baseof.<suffix>, e.g. list-baseof.<suffix>. + // 4. _default/baseof.<suffix> + for i, this := range []struct { + setup func(t *testing.T) + assert func(t *testing.T) + }{ + { + // Variant 1 + func(t *testing.T) { + writeSource(t, fs, filepath.Join("layouts", "section", "sect1-baseof.html"), `Base: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("layouts", "section", "sect1.html"), `{{define "main"}}sect{{ end }}`) + + }, + func(t *testing.T) { + th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "Base: sect") + }, + }, + { + // Variant 2 + func(t *testing.T) { + writeSource(t, fs, filepath.Join("layouts", "baseof.html"), `Base: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("layouts", "index.html"), `{{define "main"}}index{{ end }}`) + + }, + func(t *testing.T) { + th.assertFileContent(filepath.Join("public", "index.html"), "Base: index") + }, + }, + { + // Variant 3 + func(t *testing.T) { + writeSource(t, fs, filepath.Join("layouts", "_default", "list-baseof.html"), `Base: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"), `{{define "main"}}list{{ end }}`) + + }, + func(t *testing.T) { + th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "Base: list") + }, + }, + { + // Variant 4 + func(t *testing.T) { + writeSource(t, fs, filepath.Join("layouts", "_default", "baseof.html"), `Base: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"), `{{define "main"}}list{{ end }}`) + + }, + func(t *testing.T) { + th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "Base: list") + }, + }, + { + // Variant 1, theme, use project's base + func(t *testing.T) { + cfg.Set("theme", "mytheme") + writeSource(t, fs, filepath.Join("layouts", "section", "sect1-baseof.html"), `Base: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "section", "sect-baseof.html"), `Base Theme: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("layouts", "section", "sect1.html"), `{{define "main"}}sect{{ end }}`) + + }, + func(t *testing.T) { + th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "Base: sect") + }, + }, + { + // Variant 1, theme, use theme's base + func(t *testing.T) { + cfg.Set("theme", "mytheme") + writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "section", "sect1-baseof.html"), `Base Theme: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("layouts", "section", "sect1.html"), `{{define "main"}}sect{{ end }}`) + + }, + func(t *testing.T) { + th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "Base Theme: sect") + }, + }, + { + // Variant 4, theme, use project's base + func(t *testing.T) { + cfg.Set("theme", "mytheme") + writeSource(t, fs, filepath.Join("layouts", "_default", "baseof.html"), `Base: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "_default", "baseof.html"), `Base Theme: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "_default", "list.html"), `{{define "main"}}list{{ end }}`) + + }, + func(t *testing.T) { + th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "Base: list") + }, + }, + { + // Variant 4, theme, use themes's base + func(t *testing.T) { + cfg.Set("theme", "mytheme") + writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "_default", "baseof.html"), `Base Theme: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "_default", "list.html"), `{{define "main"}}list{{ end }}`) + + }, + func(t *testing.T) { + th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "Base Theme: list") + }, + }, + { + // Test section list and single template selection. + // Issue #3116 + func(t *testing.T) { + cfg.Set("theme", "mytheme") + + writeSource(t, fs, filepath.Join("layouts", "_default", "baseof.html"), `Base: {{block "main" .}}block{{end}}`) + + // Both single and list template in /SECTION/ + writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "sect1", "list.html"), `sect list`) + writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "_default", "list.html"), `default list`) + writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "sect1", "single.html"), `sect single`) + writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "_default", "single.html"), `default single`) + + // sect2 with list template in /section + writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "section", "sect2.html"), `sect2 list`) + + }, + func(t *testing.T) { + th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "sect list") + th.assertFileContent(filepath.Join("public", "sect1", "page1", "index.html"), "sect single") + th.assertFileContent(filepath.Join("public", "sect2", "index.html"), "sect2 list") + }, + }, + { + // Test section list and single template selection with base template. + // Issue #2995 + func(t *testing.T) { + + writeSource(t, fs, filepath.Join("layouts", "_default", "baseof.html"), `Base Default: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("layouts", "sect1", "baseof.html"), `Base Sect1: {{block "main" .}}block{{end}}`) + writeSource(t, fs, filepath.Join("layouts", "section", "sect2-baseof.html"), `Base Sect2: {{block "main" .}}block{{end}}`) + + // Both single and list + base template in /SECTION/ + writeSource(t, fs, filepath.Join("layouts", "sect1", "list.html"), `{{define "main"}}sect1 list{{ end }}`) + writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"), `{{define "main"}}default list{{ end }}`) + writeSource(t, fs, filepath.Join("layouts", "sect1", "single.html"), `{{define "main"}}sect single{{ end }}`) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{define "main"}}default single{{ end }}`) + + // sect2 with list template in /section + writeSource(t, fs, filepath.Join("layouts", "section", "sect2.html"), `{{define "main"}}sect2 list{{ end }}`) + + }, + func(t *testing.T) { + th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "Base Sect1", "sect1 list") + th.assertFileContent(filepath.Join("public", "sect1", "page1", "index.html"), "Base Sect1", "sect single") + th.assertFileContent(filepath.Join("public", "sect2", "index.html"), "Base Sect2", "sect2 list") + + // Note that this will get the default base template and not the one in /sect2 -- because there are no + // single template defined in /sect2. + th.assertFileContent(filepath.Join("public", "sect2", "page2", "index.html"), "Base Default", "default single") + }, + }, + } { + + if i != 9 { + continue + } + + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + + for i := 1; i <= 3; i++ { + writeSource(t, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", i)), `--- +title: Template test +--- +Some content +`) + } + + this.setup(t) + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + t.Log("Template Lookup test", i) + this.assert(t) + + } +} diff --git a/hugolib/testdata/redis.cn.md b/hugolib/testdata/redis.cn.md new file mode 100644 index 000000000..d485061d5 --- /dev/null +++ b/hugolib/testdata/redis.cn.md @@ -0,0 +1,697 @@ +--- +title: The Little Redis Book cn +--- +\thispagestyle{empty} +\changepage{}{}{}{-0.5cm}{}{2cm}{}{}{} +![The Little Redis Book cn, By Karl Seguin, Translate By Jason Lai](title.png)\ + +\clearpage +\changepage{}{}{}{0.5cm}{}{-2cm}{}{}{} + +## 关于此书 + +### 许可证 + +《The Little Redis Book》是经由Attribution-NonCommercial 3.0 Unported license许可的,你不需要为此书付钱。 + +你可以自由地对此书进行复制,分发,修改或者展示等操作。当然,你必须知道且认可这本书的作者是Karl Seguin,译者是赖立维,而且不应该将此书用于商业用途。 + +关于这个**许可证**的*详细描述*在这里: + +<http://creativecommons.org/licenses/by-nc/3.0/legalcode> + +### 关于作者 + +作者Karl Seguin是一名在多项技术领域浸淫多年的开发者。他是开源软件计划的活跃贡献者,同时也是一名技术作者以及业余演讲者。他写过若干关于Radis的文章以及一些工具。在他的一个面向业余游戏开发者的免费服务里,Redis为其中的评级和统计功能提供了支持:[mogade.com](http://mogade.com/)。 + +Karl之前还写了[《The Little MongoDB Book》](http://openmymind.net/2011/3/28/The-Little-MongoDB-Book/),这是一本免费且受好评,关于MongoDB的书。 + +他的博客是<http://openmymind.net>,你也可以关注他的Twitter帐号,via [@karlseguin](http://twitter.com/karlseguin)。 + +### 关于译者 + +译者 赖立维 是一名长在天朝的普通程序员,对许多技术都有浓厚的兴趣,是开源软件的支持者,Emacs的轻度使用者。 + +虽然译者已经很认真地对待这次翻译,但是限于水平有限,肯定会有不少错漏,如果发现该书的翻译有什么需要修改,可以通过他的邮箱与他联系。他的邮箱是<[email protected]>。 + +### 致谢 + +必须特别感谢[Perry Neal](https://twitter.com/perryneal)一直以来的指导,我的眼界、触觉以及激情都来源于你。你为我提供了无价的帮助,感谢你。 + +### 最新版本 + +此书的最新有效资源在: +<https://github.com/karlseguin/the-little-redis-book> + +中文版是英文版的一个分支,最新的中文版本在: +<https://github.com/JasonLai256/the-little-redis-book> + +\clearpage + +## 简介 + +最近几年来,关于持久化和数据查询的相关技术,其需求已经增长到了让人惊讶的程度。可以断言,关系型数据库再也不是放之四海皆准。换一句话说,围绕数据的解决方案不可能再只有唯一一种。 + +对于我来说,在众多新出现的解决方案和工具里,最让人兴奋的,无疑是Redis。为什么?首先是因为其让人不可思议的容易学习,只需要简短的几个小时学习时间,就能对Redis有个大概的认识。还有,Redis在处理一组特定的问题集的同时能保持相当的通用性。更准确地说就是,Redis不会尝试去解决关于数据的所有事情。在你足够了解Redis后,事情就会变得越来越清晰,什么是可行的,什么是不应该由Redis来处理的。作为一名开发人员,如此的经验当是相当的美妙。 + +当你能仅使用Redis去构建一个完整系统时,我想大多数人将会发现,Redis能使得他们的许多数据方案变得更为通用,不论是一个传统的关系型数据库,一个面向文档的系统,或是其它更多的东西。这是一种用来实现某些特定特性的解决方法。就类似于一个索引引擎,你不会在Lucene上构建整个程序,但当你需要足够好的搜索,为什么不使用它呢?这对你和你的用户都有好处。当然,关于Redis和索引引擎之间相似性的讨论到此为止。 + +本书的目的是向读者传授掌握Redis所需要的基本知识。我们将会注重于学习Redis的5种数据结构,并研究各种数据建模方法。我们还会接触到一些主要的管理细节和调试技巧。 + +## 入门 + +每个人的学习方式都不一样,有的人喜欢亲自实践学习,有的喜欢观看教学视频,还有的喜欢通过阅读来学习。对于Redis,没有什么比亲自实践学习来得效果更好的了。Redis的安装非常简单。而且通过随之安装的一个简单的命令解析程序,就能处理我们想做的一切事情。让我们先花几分钟的时间把Redis安装到我们的机器上。 + +### Windows平台 + +Redis并没有官方支持Windows平台,但还是可供选择。你不会想在这里配置实际的生产环境,不过在我过往的开发经历里并没有感到有什么限制。 + +首先进入<https://github.com/dmajkic/redis/downloads>,然后下载最新的版本(应该会在列表的最上方)。 + +获取zip文件,然后根据你的系统架构,打开`64bit`或`32bit`文件夹。 + +### *nix和MacOSX平台 + +对于*nix和MacOSX平台的用户,从源文件来安装是你的最佳选择。通过最新的版本号来选择,有效地址于<http://redis.io/download>。在编写此书的时候,最新的版本是2.4.6,我们可以运行下面的命令来安装该版本: + + wget http://redis.googlecode.com/files/redis-2.4.6.tar.gz + tar xzf redis-2.4.6.tar.gz + cd redis-2.4.6 + make + +(当然,Redis同样可以通过套件管理程序来安装。例如,使用Homebrew的MaxOSX用户可以只键入`brew install redis`即可。) + +如果你是通过源文件来安装,二进制可执行文件会被放置在`src`目录里。通过运行`cd src`可跳转到`src`目录。 + +### 运行和连接Redis + +如果一切都工作正常,那Redis的二进制文件应该已经可以曼妙地跳跃于你的指尖之下。Redis只有少量的可执行文件,我们将着重于Redis的服务器和命令行界面(一个类DOS的客户端)。首先,让我们来运行服务器。在Windows平台,双击`redis-server`,在*nix/MacOSX平台则运行`./redis-server`. + +如果你仔细看了启动信息,你会看到一个警告,指没能找到`redis.conf`文件。Redis将会采用内置的默认设置,这对于我们将要做的已经足够了。 + +然后,通过双击`redis-cli`(Windows平台)或者运行`./redis-cli`(*nix/MacOSX平台),启动Redis的控制台。控制台将会通过默认的端口(6379)来连接本地运行的服务器。 + +可以在命令行界面键入`info`命令来查看一切是不是都运行正常。你会很乐意看到这么一大组关键字-值(key-value)对的显示,这为我们查看服务器的状态提供了大量有效信息。 + +如果在上面的启动步骤里遇到什么问题,我建议你到[Redis的官方支持组](https://groups.google.com/forum/#!forum/redis-db)里获取帮助。 + +## 驱动Redis + +很快你就会发现,Redis的API就如一组定义明确的函数那般容易理解。Redis具有让人难以置信的简单性,其操作过程也同样如此。这意味着,无论你是使用命令行程序,或是使用你喜欢的语言来驱动,整体的感觉都不会相差多少。因此,相对于命令行程序,如果你更愿意通过一种编程语言去驱动Redis,你不会感觉到有任何适应的问题。如果真想如此,可以到Redis的[客户端推荐页面](http://redis.io/clients)下载适合的Redis载体。 + +\clearpage + +## 第1章 - 基础知识 + +是什么使Redis显得这么特别?Redis具体能解决什么类型的问题?要实际应用Redis,开发者必须储备什么知识?在我们能回答这么一些问题之前,我们需要明白Redis到底是什么。 + +Redis通常被人们认为是一种持久化的存储器关键字-值型存储(in-memory persistent key-value store)。我认为这种对Redis的描述并不太准确。Redis的确是将所有的数据存放于存储器(更多是是按位存储),而且也确实通过将数据写入磁盘来实现持久化,但是Redis的实际意义比单纯的关键字-值型存储要来得深远。纠正脑海里的这种误解观点非常关键,否则你对于Redis之道以及其应用的洞察力就会变得越发狭义。 + +事实是,Redis引入了5种不同的数据结构,只有一个是典型的关键字-值型结构。理解Redis的关键就在于搞清楚这5种数据结构,其工作的原理都是如何,有什么关联方法以及你能怎样应用这些数据结构去构建模型。首先,让我们来弄明白这些数据结构的实际意义。 + +应用上面提及的数据结构概念到我们熟悉的关系型数据库里,我们可以认为其引入了一个单独的数据结构——表格。表格既复杂又灵活,基于表格的存储和管理,没有多少东西是你不能进行建模的。然而,这种通用性并不是没有缺点。具体来说就是,事情并不是总能达到假设中的简单或者快速。相对于这种普遍适用(one-size-fits-all)的结构体系,我们可以使用更为专门化的结构体系。当然,因此可能有些事情我们会完成不了(至少,达不到很好的程度)。但话说回来,这样做就能确定我们可以获得想象中的简单性和速度吗? + +针对特定类型的问题使用特定的数据结构?我们不就是这样进行编程的吗?你不会使用一个散列表去存储每份数据,也不会使用一个标量变量去存储。对我来说,这正是Redis的做法。如果你需要处理标量、列表、散列或者集合,为什么不直接就用标量、列表、散列和集合去存储他们?为什么不是直接调用`exists(key)`去检测一个已存在的值,而是要调用其他比O(1)(常量时间查找,不会因为待处理元素的增长而变慢)慢的操作? + +### 数据库(Databases) + +与你熟悉的关系型数据库一致,Redis有着相同的数据库基本概念,即一个数据库包含一组数据。典型的数据库应用案例是,将一个程序的所有数据组织起来,使之与另一个程序的数据保持独立。 + +在Redis里,数据库简单的使用一个数字编号来进行辨认,默认数据库的数字编号是`0`。如果你想切换到一个不同的数据库,你可以使用`select`命令来实现。在命令行界面里键入`select 1`,Redis应该会回复一条`OK`的信息,然后命令行界面里的提示符会变成类似`redis 127.0.0.1:6379[1]>`这样。如果你想切换回默认数据库,只要在命令行界面键入`select 0`即可。 + +### 命令、关键字和值(Commands, Keys and Values) + +Redis不仅仅是一种简单的关键字-值型存储,从其核心概念来看,Redis的5种数据结构中的每一个都至少有一个关键字和一个值。在转入其它关于Redis的有用信息之前,我们必须理解关键字和值的概念。 + +关键字(Keys)是用来标识数据块。我们将会很常跟关键字打交道,不过在现在,明白关键字就是类似于`users:leto`这样的表述就足够了。一般都能很好地理解到,这样关键字包含的信息是一个名为`leto`的用户。这个关键字里的冒号没有任何特殊含义,对于Redis而言,使用分隔符来组织关键字是很常见的方法。 + +值(Values)是关联于关键字的实际值,可以是任何东西。有时候你会存储字符串,有时候是整数,还有时候你会存储序列化对象(使用JSON、XML或其他格式)。在大多数情况下,Redis会把值看做是一个字节序列,而不会关注它们实质上是什么。要注意,不同的Redis载体处理序列化会有所不同(一些会让你自己决定)。因此,在这本书里,我们将仅讨论字符串、整数和JSON。 + +现在让我们活动一下手指吧。在命令行界面键入下面的命令: + + set users:leto "{name: leto, planet: dune, likes: [spice]}" + +这就是Redis命令的基本构成。首先我们要有一个确定的命令,在上面的语句里就是`set`。然后就是相应的参数,`set`命令接受两个参数,包括要设置的关键字,以及相应要设置的值。很多的情况是,命令接受一个关键字(当这种情况出现,其经常是第一个参数)。你能想到如何去获取这个值吗?我想你会说(当然一时拿不准也没什么): + + get users:leto + +关键字和值的是Redis的基本概念,而`get`和`set`命令是对此最简单的使用。你可以创建更多的用户,去尝试不同类型的关键字以及不同的值,看看一些不同的组合。 + +### 查询(Querying) + +随着学习的持续深入,两件事情将变得清晰起来。对于Redis而言,关键字就是一切,而值是没有任何意义。更通俗来看就是,Redis不允许你通过值来进行查询。回到上面的例子,我们就不能查询生活在`dune`行星上的用户。 + +对许多人来说,这会引起一些担忧。在我们生活的世界里,数据查询是如此的灵活和强大,而Redis的方式看起来是这么的原始和不高效。不要让这些扰乱你太久。要记住,Redis不是一种普遍使用(one-size-fits-all)的解决方案,确实存在这么一些事情是不应该由Redis来解决的(因为其查询的限制)。事实上,在考虑了这些情况后,你会找到新的方法去构建你的数据。 + +很快,我们就能看到更多实际的用例。很重要的一点是,我们要明白关于Redis的这些基本事实。这能帮助我们弄清楚为什么值可以是任何东西,因为Redis从来不需要去读取或理解它们。而且,这也可以帮助我们理清思路,然后去思考如何在这个新世界里建立模型。 + +### 存储器和持久化(Memory and Persistence) + +我们之前提及过,Redis是一种持久化的存储器内存储(in-memory persistent store)。对于持久化,默认情况下,Redis会根据已变更的关键字数量来进行判断,然后在磁盘里创建数据库的快照(snapshot)。你可以对此进行设置,如果X个关键字已变更,那么每隔Y秒存储数据库一次。默认情况下,如果1000个或更多的关键字已变更,Redis会每隔60秒存储数据库;而如果9个或更少的关键字已变更,Redis会每隔15分钟存储数据库。 + +除了创建磁盘快照外,Redis可以在附加模式下运行。任何时候,如果有一个关键字变更,一个单一附加(append-only)的文件会在磁盘里进行更新。在一些情况里,虽然硬件或软件可能发生错误,但用那60秒有效数据存储去换取更好性能是可以接受的。而在另一些情况里,这种损失就难以让人接受,Redis为你提供了选择。在第5章里,我们将会看到第三种选择,其将持久化任务减荷到一个从属数据库里。 + +至于存储器,Redis会将所有数据都保留在存储器中。显而易见,运行Redis具有不低的成本:因为RAM仍然是最昂贵的服务器硬件部件。 + +我很清楚有一些开发者对即使是一点点的数据空间都是那么的敏感。一本《威廉·莎士比亚全集》需要近5.5MB的存储空间。对于缩放的需求,其它的解决方案趋向于IO-bound或者CPU-bound。这些限制(RAM或者IO)将会需要你去理解更多机器实际依赖的数据类型,以及应该如何去进行存储和查询。除非你是存储大容量的多媒体文件到Redis中,否则存储器内存储应该不会是一个问题。如果这对于一个程序是个问题,你就很可能不会用IO-bound的解决方案。 + +Redis有虚拟存储器的支持。然而,这个功能已经被认为是失败的了(通过Redis的开发者),而且它的使用已经被废弃了。 + +(从另一个角度来看,一本5.5MB的《威廉·莎士比亚全集》可以通过压缩减小到近2MB。当然,Redis不会自动对值进行压缩,但是因为其将所有值都看作是字节,没有什么限制让你不能对数据进行压缩/解压,通过牺牲处理时间来换取存储空间。) + +### 整体来看(Putting It Together) + +我们已经接触了好几个高层次的主题。在继续深入Redis之前,我想做的最后一件事情是将这些主题整合起来。这些主题包括,查询的限制,数据结构以及Redis在存储器内存储数据的方法。 + +当你将这3个主题整合起来,你最终会得出一个绝妙的结论:速度。一些人可能会想,当然Redis会很快速,要知道所以的东西都在存储器里。但这仅仅是其中的一部分,让Redis闪耀的真正原因是其不同于其它解决方案的特殊数据结构。 + +能有多快速?这依赖于很多东西,包括你正在使用着哪个命令,数据的类型等等。但Redis的性能测试是趋向于数万或数十万次操作**每秒**。你可以通过运行`redis-benchmark`(就在`redis-server`和`redis-cli`的同一个文件夹里)来进行测试。 + +我曾经试过将一组使用传统模型的代码转向使用Redis。在传统模型里,运行一个我写的载入测试,需要超过5分钟的时间来完成。而在Redis里,只需要150毫秒就完成了。你不会总能得到这么好的收获,但希望这能让你对我们所谈的东西有更清晰的理解。 + +理解Redis的这个特性很重要,因为这将影响到你如何去与Redis进行交互。拥有SQL背景的程序员通常会致力于让数据库的数据往返次数减至最小。这对于任何系统都是个好建议,包括Redis。然而,考虑到我们是在处理比较简单的数据结构,有时候我们还是需要与Redis服务器频繁交互,以达到我们的目的。刚开始的时候,可能会对这种数据访问模式感到不太自然。实际上,相对于我们通过Redis获得的高性能而言,这仅仅是微不足道的损失。 + +### 小结 + +虽然我们只接触和摆弄了Redis的冰山一角,但我们讨论的主题已然覆盖了很大范围内的东西。如果觉得有些事情还是不太清楚(例如查询),不用为此而担心,在下一章我们将会继续深入探讨,希望你的问题都能得到解答。 + +这一章的要点包括: + +* 关键字(Keys)是用于标识一段数据的一个字符串 + +* 值(Values)是一段任意的字节序列,Redis不会关注它们实质上是什么 + +* Redis展示了(也实现了)5种专门的数据结构 + +* 上面的几点使得Redis快速而且容易使用,但要知道Redis并不适用于所有的应用场景 + +\clearpage + +## 第2章 - 数据结构 + +现在开始将探究Redis的5种数据结构,我们会解释每种数据结构都是什么,包含了什么有效的方法(Method),以及你能用这些数据结构处理哪些类型的特性和数据。 + +目前为止,我们所知道的Redis构成仅包括命令、关键字和值,还没有接触到关于数据结构的具体概念。当我们使用`set`命令时,Redis是怎么知道我们是在使用哪个数据结构?其解决方法是,每个命令都相对应于一种特定的数据结构。例如,当你使用`set`命令,你就是将值存储到一个字符串数据结构里。而当你使用`hset`命令,你就是将值存储到一个散列数据结构里。考虑到Redis的关键字集很小,这样的机制具有相当的可管理性。 + +**[Redis的网站](http://redis.io/commands)里有着非常优秀的参考文档,没有任何理由去重造轮子。但为了搞清楚这些数据结构的作用,我们将会覆盖那些必须知道的重要命令。** + +没有什么事情比高兴的玩和试验有趣的东西来得更重要的了。在任何时候,你都能通过键入`flushdb`命令将你数据库里的所有值清除掉,因此,不要再那么害羞了,去尝试做些疯狂的事情吧! + +### 字符串(Strings) + +在Redis里,字符串是最基本的数据结构。当你在思索着关键字-值对时,你就是在思索着字符串数据结构。不要被名字给搞混了,如之前说过的,你的值可以是任何东西。我更喜欢将他们称作“标量”(Scalars),但也许只有我才这样想。 + +我们已经看到了一个常见的字符串使用案例,即通过关键字存储对象的实例。有时候,你会频繁地用到这类操作: + + set users:leto "{name: leto, planet: dune, likes: [spice]}" + +除了这些外,Redis还有一些常用的操作。例如,`strlen <key>`能用来获取一个关键字对应值的长度;`getrange <key> <start> <end>`将返回指定范围内的关键字对应值;`append <key> <value>`会将value附加到已存在的关键字对应值中(如果该关键字并不存在,则会创建一个新的关键字-值对)。不要犹豫,去试试看这些命令吧。下面是我得到的: + + > strlen users:leto + (integer) 42 + + > getrange users:leto 27 40 + "likes: [spice]" + + > append users:leto " OVER 9000!!" + (integer) 54 + +现在你可能会想,这很好,但似乎没有什么意义。你不能有效地提取出一段范围内的JSON文件,或者为其附加一些值。你是对的,这里的经验是,一些命令,尤其是关于字符串数据结构的,只有在给定了明确的数据类型后,才会有实际意义。 + +之前我们知道了,Redis不会去关注你的值是什么东西。通常情况下,这没有错。然而,一些字符串命令是专门为一些类型或值的结构而设计的。作为一个有些含糊的用例,我们可以看到,对于一些自定义的空间效率很高的(space-efficient)串行化对象,`append`和`getrange`命令将会很有用。对于一个更为具体的用例,我们可以再看一下`incr`、`incrby`、`decr`和`decrby`命令。这些命令会增长或者缩减一个字符串数据结构的值: + + > incr stats:page:about + (integer) 1 + > incr stats:page:about + (integer) 2 + + > incrby ratings:video:12333 5 + (integer) 5 + > incrby ratings:video:12333 3 + (integer) 8 + +由此你可以想象到,Redis的字符串数据结构能很好地用于分析用途。你还可以去尝试增长`users:leto`(一个不是整数的值),然后看看会发生什么(应该会得到一个错误)。 + +更为进阶的用例是`setbit`和`getbit`命令。“今天我们有多少个独立用户访问”是个在Web应用里常见的问题,有一篇[精彩的博文](http://blog.getspool.com/2011/11/29/fast-easy-realtime-metrics-using-redis-bitmaps/),在里面可以看到Spool是如何使用这两个命令有效地解决此问题。对于1.28亿个用户,一部笔记本电脑在不到50毫秒的时间里就给出了答复,而且只用了16MB的存储空间。 + +最重要的事情不是在于你是否明白位图(Bitmaps)的工作原理,或者Spool是如何去使用这些命令,而是应该要清楚Redis的字符串数据结构比你当初所想的要有用许多。然而,最常见的应用案例还是上面我们给出的:存储对象(简单或复杂)和计数。同时,由于通过关键字来获取一个值是如此之快,字符串数据结构很常被用来缓存数据。 + +### 散列(Hashes) + +我们已经知道把Redis称为一种关键字-值型存储是不太准确的,散列数据结构是一个很好的例证。你会看到,在很多方面里,散列数据结构很像字符串数据结构。两者显著的区别在于,散列数据结构提供了一个额外的间接层:一个域(Field)。因此,散列数据结构中的`set`和`get`是: + + hset users:goku powerlevel 9000 + hget users:goku powerlevel + +相关的操作还包括在同一时间设置多个域、同一时间获取多个域、获取所有的域和值、列出所有的域或者删除指定的一个域: + + hmset users:goku race saiyan age 737 + hmget users:goku race powerlevel + hgetall users:goku + hkeys users:goku + hdel users:goku age + +如你所见,散列数据结构比普通的字符串数据结构具有更多的可操作性。我们可以使用一个散列数据结构去获得更精确的描述,是存储一个用户,而不是一个序列化对象。从而得到的好处是能够提取、更新和删除具体的数据片段,而不必去获取或写入整个值。 + +对于散列数据结构,可以从一个经过明确定义的对象的角度来考虑,例如一个用户,关键之处在于要理解他们是如何工作的。从性能上的原因来看,这是正确的,更具粒度化的控制可能会相当有用。在下一章我们将会看到,如何用散列数据结构去组织你的数据,使查询变得更为实效。在我看来,这是散列真正耀眼的地方。 + +### 列表(Lists) + +对于一个给定的关键字,列表数据结构让你可以存储和处理一组值。你可以添加一个值到列表里、获取列表的第一个值或最后一个值以及用给定的索引来处理值。列表数据结构维护了值的顺序,提供了基于索引的高效操作。为了跟踪在网站里注册的最新用户,我们可以维护一个`newusers`的列表: + + lpush newusers goku + ltrim newusers 0 50 + +**(译注:`ltrim`命令的具体构成是`LTRIM Key start stop`。要理解`ltrim`命令,首先要明白Key所存储的值是一个列表,理论上列表可以存放任意个值。对于指定的列表,根据所提供的两个范围参数start和stop,`ltrim`命令会将指定范围外的值都删除掉,只留下范围内的值。)** + +首先,我们将一个新用户推入到列表的前端,然后对列表进行调整,使得该列表只包含50个最近被推入的用户。这是一种常见的模式。`ltrim`是一个具有O(N)时间复杂度的操作,N是被删除的值的数量。从上面的例子来看,我们总是在插入了一个用户后再进行列表调整,实际上,其将具有O(1)的时间复杂度(因为N将永远等于1)的常数性能。 + +这是我们第一次看到一个关键字的对应值索引另一个值。如果我们想要获取最近的10个用户的详细资料,我们可以运行下面的组合操作: + + keys = redis.lrange('newusers', 0, 10) + redis.mget(*keys.map {|u| "users:#{u}"}) + +我们之前谈论过关于多次往返数据的模式,上面的两行Ruby代码为我们进行了很好的演示。 + +当然,对于存储和索引关键字的功能,并不是只有列表数据结构这种方式。值可以是任意的东西,你可以使用列表数据结构去存储日志,也可以用来跟踪用户浏览网站时的路径。如果你过往曾构建过游戏,你可能会使用列表数据结构去跟踪用户的排队活动。 + +### 集合 + +集合数据结构常常被用来存储只能唯一存在的值,并提供了许多的基于集合的操作,例如并集。集合数据结构没有对值进行排序,但是其提供了高效的基于值的操作。使用集合数据结构的典型用例是朋友名单的实现: + + sadd friends:leto ghanima paul chani jessica + sadd friends:duncan paul jessica alia + +不管一个用户有多少个朋友,我们都能高效地(O(1)时间复杂度)识别出用户X是不是用户Y的朋友: + + sismember friends:leto jessica + sismember friends:leto vladimir + +而且,我们可以查看两个或更多的人是不是有共同的朋友: + + sinter friends:leto friends:duncan + +甚至可以在一个新的关键字里存储结果: + + sinterstore friends:leto_duncan friends:leto friends:duncan + +有时候需要对值的属性进行标记和跟踪处理,但不能通过简单的复制操作完成,集合数据结构是解决此类问题的最好方法之一。当然,对于那些需要运用集合操作的地方(例如交集和并集),集合数据结构就是最好的选择。 + +### 分类集合(Sorted Sets) + +最后也是最强大的数据结构是分类集合数据结构。如果说散列数据结构类似于字符串数据结构,主要区分是域(field)的概念;那么分类集合数据结构就类似于集合数据结构,主要区分是标记(score)的概念。标记提供了排序(sorting)和秩划分(ranking)的功能。如果我们想要一个秩分类的朋友名单,可以这样做: + + zadd friends:duncan 70 ghanima 95 paul 95 chani 75 jessica 1 vladimir + +对于`duncan`的朋友,要怎样计算出标记(score)为90或更高的人数? + + zcount friends:duncan 90 100 + +如何获取`chani`在名单里的秩(rank)? + + zrevrank friends:duncan chani + +**(译注:`zrank`命令的具体构成是`ZRANK Key menber`,要知道Key存储的Sorted Set默认是根据Score对各个menber进行升序的排列,该命令就是用来获取menber在该排列里的次序,这就是所谓的秩。)** + +我们使用了`zrevrank`命令而不是`zrank`命令,这是因为Redis的默认排序是从低到高,但是在这个例子里我们的秩划分是从高到低。对于分类集合数据结构,最常见的应用案例是用来实现排行榜系统。事实上,对于一些基于整数排序,且能以标记(score)来进行有效操作的东西,使用分类集合数据结构来处理应该都是不错的选择。 + +### 小结 + +对于Redis的5种数据结构,我们进行了高层次的概述。一件有趣的事情是,相对于最初构建时的想法,你经常能用Redis创造出一些更具实效的事情。对于字符串数据结构和分类集合数据结构的使用,很有可能存在一些构建方法是还没有人想到的。当你理解了那些常用的应用案例后,你将发现Redis对于许多类型的问题,都是很理想的选择。还有,不要因为Redis展示了5种数据结构和相应的各种方法,就认为你必须要把所有的东西都用上。只使用一些命令去构建一个特性是很常见的。 + +\clearpage + +## 第3章 - 使用数据结构 + +在上一章里,我们谈论了Redis的5种数据结构,对于一些可能的用途也给出了用例。现在是时候来看看一些更高级,但依然很常见的主题和设计模式。 + +### 大O表示法(Big O Notation) + +在本书中,我们之前就已经看到过大O表示法,包括O(1)和O(N)的表示。大O表示法的惯常用途是,描述一些用于处理一定数量元素的行为的综合表现。在Redis里,对于一个要处理一定数量元素的命令,大O表示法让我们能了解该命令的大概运行速度。 + +在Redis的文档里,每一个命令的时间复杂度都用大O表示法进行了描述,还能知道各命令的具体性能会受什么因素影响。让我们来看看一些用例。 + +常数时间复杂度O(1)被认为是最快速的,无论我们是在处理5个元素还是5百万个元素,最终都能得到相同的性能。对于`sismember`命令,其作用是告诉我们一个值是否属于一个集合,时间复杂度为O(1)。`sismember`命令很强大,很大部分的原因是其高效的性能特征。许多Redis命令都具有O(1)的时间复杂度。 + +对数时间复杂度O(log(N))被认为是第二快速的,其通过使需扫描的区间不断皱缩来快速完成处理。使用这种“分而治之”的方式,大量的元素能在几个迭代过程里被快速分解完整。`zadd`命令的时间复杂度就是O(log(N)),其中N是在分类集合中的元素数量。 + +再下来就是线性时间复杂度O(N),在一个表格的非索引列里进行查找就需要O(N)次操作。`ltrim`命令具有O(N)的时间复杂度,但是,在`ltrim`命令里,N不是列表所拥有的元素数量,而是被删除的元素数量。从一个具有百万元素的列表里用`ltrim`命令删除1个元素,要比从一个具有一千个元素的列表里用`ltrim`命令删除10个元素来的快速(实际上,两者很可能会是一样快,因为两个时间都非常的小)。 + +根据给定的最小和最大的值的标记,`zremrangebyscore`命令会在一个分类集合里进行删除元素操作,其时间复杂度是O(log(N)+M)。这看起来似乎有点儿杂乱,通过阅读文档可以知道,这里的N指的是在分类集合里的总元素数量,而M则是被删除的元素数量。可以看出,对于性能而言,被删除的元素数量很可能会比分类集合里的总元素数量更为重要。 + +**(译注:`zremrangebyscore`命令的具体构成是`ZREMRANGEBYSCORE Key max mix`。)** + +对于`sort`命令,其时间复杂度为O(N+M*log(M)),我们将会在下一章谈论更多的相关细节。从`sort`命令的性能特征来看,可以说这是Redis里最复杂的一个命令。 + +还存在其他的时间复杂度描述,包括O(N^2)和O(C^N)。随着N的增大,其性能将急速下降。在Redis里,没有任何一个命令具有这些类型的时间复杂度。 + +值得指出的一点是,在Redis里,当我们发现一些操作具有O(N)的时间复杂度时,我们可能可以找到更为好的方法去处理。 + +**(译注:对于Big O Notation,相信大家都非常的熟悉,虽然原文仅仅是对该表示法进行简单的介绍,但限于个人的算法知识和文笔水平实在有限,此小节的翻译让我头痛颇久,最终成果也确实难以让人满意,望见谅。)** + +### 仿多关键字查询(Pseudo Multi Key Queries) + +时常,你会想通过不同的关键字去查询相同的值。例如,你会想通过电子邮件(当用户开始登录时)去获取用户的具体信息,或者通过用户id(在用户登录后)去获取。有一种很不实效的解决方法,其将用户对象分别放置到两个字符串值里去: + + set users:[email protected] "{id: 9001, email: '[email protected]', ...}" + set users:9001 "{id: 9001, email: '[email protected]', ...}" + +这种方法很糟糕,如此不但会产生两倍数量的内存,而且这将会成为数据管理的恶梦。 + +如果Redis允许你将一个关键字链接到另一个的话,可能情况会好很多,可惜Redis并没有提供这样的功能(而且很可能永远都不会提供)。Redis发展到现在,其开发的首要目的是要保持代码和API的整洁简单,关键字链接功能的内部实现并不符合这个前提(对于关键字,我们还有很多相关方法没有谈论到)。其实,Redis已经提供了解决的方法:散列。 + +使用散列数据结构,我们可以摆脱重复的缠绕: + + set users:9001 "{id: 9001, email: [email protected], ...}" + hset users:lookup:email [email protected] 9001 + +我们所做的是,使用域来作为一个二级索引,然后去引用单个用户对象。要通过id来获取用户信息,我们可以使用一个普通的`get`命令: + + get users:9001 + +而如果想通过电子邮箱来获取用户信息,我们可以使用`hget`命令再配合使用`get`命令(Ruby代码): + + id = redis.hget('users:lookup:email', '[email protected]') + user = redis.get("users:#{id}") + +你很可能将会经常使用这类用法。在我看来,这就是散列真正耀眼的地方。在你了解这类用法之前,这可能不是一个明显的用例。 + +### 引用和索引(References and Indexes) + +我们已经看过几个关于值引用的用例,包括介绍列表数据结构时的用例,以及在上面使用散列数据结构来使查询更灵活一些。进行归纳后会发现,对于那些值与值间的索引和引用,我们都必须手动的去管理。诚实来讲,这确实会让人有点沮丧,尤其是当你想到那些引用相关的操作,如管理、更新和删除等,都必须手动的进行时。在Redis里,这个问题还没有很好的解决方法。 + +我们已经看到,集合数据结构很常被用来实现这类索引: + + sadd friends:leto ghanima paul chani jessica + +这个集合里的每一个成员都是一个Redis字符串数据结构的引用,而每一个引用的值则包含着用户对象的具体信息。那么如果`chani`改变了她的名字,或者删除了她的帐号,应该如何处理?从整个朋友圈的关系结构来看可能会更好理解,我们知道,`chani`也有她的朋友: + + sadd friends_of:chani leto paul + +如果你有什么待处理情况像上面那样,那在维护成本之外,还会有对于额外索引值的处理和存储空间的成本。这可能会令你感到有点退缩。在下一小节里,我们将会谈论减少使用额外数据交互的性能成本的一些方法(在第1章我们粗略地讨论了下)。 + +如果你确实在担忧着这些情况,其实,关系型数据库也有同样的开销。索引需要一定的存储空间,必须通过扫描或查找,然后才能找到相应的记录。其开销也是存在的,当然他们对此做了很多的优化工作,使之变得更为有效。 + +再次说明,需要在Redis里手动地管理引用确实是颇为棘手。但是,对于你关心的那些问题,包括性能或存储空间等,应该在经过测试后,才会有真正的理解。我想你会发现这不会是一个大问题。 + +### 数据交互和流水线(Round Trips and Pipelining) + +我们已经提到过,与服务器频繁交互是Redis的一种常见模式。这类情况可能很常出现,为了使我们能获益更多,值得仔细去看看我们能利用哪些特性。 + +许多命令能接受一个或更多的参数,也有一种关联命令(sister-command)可以接受多个参数。例如早前我们看到过`mget`命令,接受多个关键字,然后返回值: + + keys = redis.lrange('newusers', 0, 10) + redis.mget(*keys.map {|u| "users:#{u}"}) + +或者是`sadd`命令,能添加一个或多个成员到集合里: + + sadd friends:vladimir piter + sadd friends:paul jessica leto "leto II" chani + +Redis还支持流水线功能。通常情况下,当一个客户端发送请求到Redis后,在发送下一个请求之前必须等待Redis的答复。使用流水线功能,你可以发送多个请求,而不需要等待Redis响应。这不但减少了网络开销,还能获得性能上的显著提高。 + +值得一提的是,Redis会使用存储器去排列命令,因此批量执行命令是一个好主意。至于具体要多大的批量,将取决于你要使用什么命令(更明确来说,该参数有多大)。另一方面来看,如果你要执行的命令需要差不多50个字符的关键字,你大概可以对此进行数千或数万的批量操作。 + +对于不同的Redis载体,在流水线里运行命令的方式会有所差异。在Ruby里,你传递一个代码块到`pipelined`方法: + + redis.pipelined do + 9001.times do + redis.incr('powerlevel') + end + end + +正如你可能猜想到的,流水线功能可以实际地加速一连串命令的处理。 + +### 事务(Transactions) + +每一个Redis命令都具有原子性,包括那些一次处理多项事情的命令。此外,对于使用多个命令,Redis支持事务功能。 + +你可能不知道,但Redis实际上是单线程运行的,这就是为什么每一个Redis命令都能够保证具有原子性。当一个命令在执行时,没有其他命令会运行(我们会在往后的章节里简略谈论一下Scaling)。在你考虑到一些命令去做多项事情时,这会特别的有用。例如: + +`incr`命令实际上就是一个`get`命令然后紧随一个`set`命令。 + +`getset`命令设置一个新的值然后返回原始值。 + +`setnx`命令首先测试关键字是否存在,只有当关键字不存在时才设置值 + +虽然这些都很有用,但在实际开发时,往往会需要运行具有原子性的一组命令。若要这样做,首先要执行`multi`命令,紧随其后的是所有你想要执行的命令(作为事务的一部分),最后执行`exec`命令去实际执行命令,或者使用`discard`命令放弃执行命令。Redis的事务功能保证了什么? + +* 事务中的命令将会按顺序地被执行 + +* 事务中的命令将会如单个原子操作般被执行(没有其它的客户端命令会在中途被执行) + +* 事务中的命令要么全部被执行,要么不会执行 + +你可以(也应该)在命令行界面对事务功能进行一下测试。还有一点要注意到,没有什么理由不能结合流水线功能和事务功能。 + + multi + hincrby groups:1percent balance -9000000000 + hincrby groups:99percent balance 9000000000 + exec + +最后,Redis能让你指定一个关键字(或多个关键字),当关键字有改变时,可以查看或者有条件地应用一个事务。这是用于当你需要获取值,且待运行的命令基于那些值时,所有都在一个事务里。对于上面展示的代码,我们不能去实现自己的`incr`命令,因为一旦`exec`命令被调用,他们会全部被执行在一块。我们不能这么做: + + redis.multi() + current = redis.get('powerlevel') + redis.set('powerlevel', current + 1) + redis.exec() + +**(译注:虽然Redis是单线程运行的,但是我们可以同时运行多个Redis客户端进程,常见的并发问题还是会出现。像上面的代码,在`get`运行之后,`set`运行之前,`powerlevel`的值可能会被另一个Redis客户端给改变,从而造成错误。)** + +这些不是Redis的事务功能的工作。但是,如果我们增加一个`watch`到`powerlevel`,我们可以这样做: + + redis.watch('powerlevel') + current = redis.get('powerlevel') + redis.multi() + redis.set('powerlevel', current + 1) + redis.exec() + +在我们调用`watch`后,如果另一个客户端改变了`powerlevel`的值,我们的事务将会运行失败。如果没有客户端改变`powerlevel`的值,那么事务会继续工作。我们可以在一个循环里运行这些代码,直到其能正常工作。 + +### 关键字反模式(Keys Anti-Pattern) + +在下一章中,我们将会谈论那些没有确切关联到数据结构的命令,其中的一些是管理或调试工具。然而有一个命令我想特别地在这里进行谈论:`keys`命令。这个命令需要一个模式,然后查找所有匹配的关键字。这个命令看起来很适合一些任务,但这不应该用在实际的产品代码里。为什么?因为这个命令通过线性扫描所有的关键字来进行匹配。或者,简单地说,这个命令太慢了。 + +人们会如此去使用这个命令?一般会用来构建一个本地的Bug追踪服务。每一个帐号都有一个`id`,你可能会通过一个看起来像`bug:account_id:bug_id`的关键字,把每一个Bug存储到一个字符串数据结构值中去。如果你在任何时候需要查询一个帐号的Bug(显示它们,或者当用户删除了帐号时删除掉这些Bugs),你可能会尝试去使用`keys`命令: + + keys bug:1233:* + +更好的解决方法应该使用一个散列数据结构,就像我们可以使用散列数据结构来提供一种方法去展示二级索引,因此我们可以使用域来组织数据: + + hset bugs:1233 1 "{id:1, account: 1233, subject: '...'}" + hset bugs:1233 2 "{id:2, account: 1233, subject: '...'}" + +从一个帐号里获取所有的Bug标识,可以简单地调用`hkeys bugs:1233`。去删除一个指定的Bug,可以调用`hdel bugs:1233 2`。如果要删除了一个帐号,可以通过`del bugs:1233`把关键字删除掉。 + +### 小结 + +结合这一章以及前一章,希望能让你得到一些洞察力,了解如何使用Redis去支持(Power)实际项目。还有其他的模式可以让你去构建各种类型的东西,但真正的关键是要理解基本的数据结构。你将能领悟到,这些数据结构是如何能够实现你最初视角之外的东西。 + +\clearpage + +## 第4章 超越数据结构 + +5种数据结构组成了Redis的基础,其他没有关联特定数据结构的命令也有很多。我们已经看过一些这样的命令:`info`, `select`, `flushdb`, `multi`, `exec`, `discard`, `watch`和`keys `。这一章将看看其他的一些重要命令。 + +### 使用期限(Expiration) + +Redis允许你标记一个关键字的使用期限。你可以给予一个Unix时间戳形式(自1970年1月1日起)的绝对时间,或者一个基于秒的存活时间。这是一个基于关键字的命令,因此其不在乎关键字表示的是哪种类型的数据结构。 + + expire pages:about 30 + expireat pages:about 1356933600 + +第一个命令将会在30秒后删除掉关键字(包括其关联的值)。第二个命令则会在2012年12月31日上午12点删除掉关键字。 + +这让Redis能成为一个理想的缓冲引擎。通过`ttl`命令,你可以知道一个关键字还能够存活多久。而通过`persist`命令,你可以把一个关键字的使用期限删除掉。 + + ttl pages:about + persist pages:about + +最后,有个特殊的字符串命令,`setex`命令让你可以在一个单独的原子命令里设置一个字符串值,同时里指定一个生存期(这比任何事情都要方便)。 + + setex pages:about 30 '<h1>about us</h1>....' + +### 发布和订阅(Publication and Subscriptions) + +Redis的列表数据结构有`blpop`和`brpop`命令,能从列表里返回且删除第一个(或最后一个)元素,或者被堵塞,直到有一个元素可供操作。这可以用来实现一个简单的队列。 + +**(译注:对于`blpop`和`brpop`命令,如果列表里没有关键字可供操作,连接将被堵塞,直到有另外的Redis客户端使用`lpush`或`rpush`命令推入关键字为止。)** + +此外,Redis对于消息发布和频道订阅有着一流的支持。你可以打开第二个`redis-cli`窗口,去尝试一下这些功能。在第一个窗口里订阅一个频道(我们会称它为`warnings`): + + subscribe warnings + +其将会答复你订阅的信息。现在,在另一个窗口,发布一条消息到`warnings`频道: + + publish warnings "it's over 9000!" + +如果你回到第一个窗口,你应该已经接收到`warnings`频道发来的消息。 + +你可以订阅多个频道(`subscribe channel1 channel2 ...`),订阅一组基于模式的频道(`psubscribe warnings:*`),以及使用`unsubscribe`和`punsubscribe`命令停止监听一个或多个频道,或一个频道模式。 + +最后,可以注意到`publish`命令的返回值是1,这指出了接收到消息的客户端数量。 + +### 监控和延迟日志(Monitor and Slow Log) + +`monitor`命令可以让你查看Redis正在做什么。这是一个优秀的调试工具,能让你了解你的程序如何与Redis进行交互。在两个`redis-cli`窗口中选一个(如果其中一个还处于订阅状态,你可以使用`unsubscribe`命令退订,或者直接关掉窗口再重新打开一个新窗口)键入`monitor`命令。在另一个窗口,执行任何其他类型的命令(例如`get`或`set`命令)。在第一个窗口里,你应该可以看到这些命令,包括他们的参数。 + +在实际生产环境里,你应该谨慎运行`monitor`命令,这真的仅仅就是一个很有用的调试和开发工具。除此之外,没有更多要说的了。 + +随同`monitor`命令一起,Redis拥有一个`slowlog`命令,这是一个优秀的性能剖析工具。其会记录执行时间超过一定数量**微秒**的命令。在下一章节,我们会简略地涉及如何配置Redis,现在你可以按下面的输入配置Redis去记录所有的命令: + + config set slowlog-log-slower-than 0 + +然后,执行一些命令。最后,你可以检索到所有日志,或者检索最近的那些日志: + + slowlog get + slowlog get 10 + +通过键入`slowlog len`,你可以获取延迟日志里的日志数量。 + +对于每个被你键入的命令,你应该查看4个参数: + +* 一个自动递增的id + +* 一个Unix时间戳,表示命令开始运行的时间 + +* 一个微妙级的时间,显示命令运行的总时间 + +* 该命令以及所带参数 + +延迟日志保存在存储器中,因此在生产环境中运行(即使有一个低阀值)也应该不是一个问题。默认情况下,它将会追踪最近的1024个日志。 + +### 排序(Sort) + +`sort`命令是Redis最强大的命令之一。它让你可以在一个列表、集合或者分类集合里对值进行排序(分类集合是通过标记来进行排序,而不是集合里的成员)。下面是一个`sort`命令的简单用例: + + rpush users:leto:guesses 5 9 10 2 4 10 19 2 + sort users:leto:guesses + +这将返回进行升序排序后的值。这里有一个更高级的例子: + + sadd friends:ghanima leto paul chani jessica alia duncan + sort friends:ghanima limit 0 3 desc alpha + +上面的命令向我们展示了,如何对已排序的记录进行分页(通过`limit`),如何返回降序排序的结果(通过`desc`),以及如何用字典序排序代替数值序排序(通过`alpha`)。 + +`sort`命令的真正力量是其基于引用对象来进行排序的能力。早先的时候,我们说明了列表、集合和分类集合很常被用于引用其他的Redis对象,`sort`命令能够解引用这些关系,而且通过潜在值来进行排序。例如,假设我们有一个Bug追踪器能让用户看到各类已存在问题。我们可能使用一个集合数据结构去追踪正在被监视的问题: + + sadd watch:leto 12339 1382 338 9338 + +你可能会有强烈的感觉,想要通过id来排序这些问题(默认的排序就是这样的),但是,我们更可能是通过问题的严重性来对这些问题进行排序。为此,我们要告诉Redis将使用什么模式来进行排序。首先,为了可以看到一个有意义的结果,让我们添加多一点数据: + + set severity:12339 3 + set severity:1382 2 + set severity:338 5 + set severity:9338 4 + +要通过问题的严重性来降序排序这些Bug,你可以这样做: + + sort watch:leto by severity:* desc + +Redis将会用存储在列表(集合或分类集合)中的值去替代模式中的`*`(通过`by`)。这会创建出关键字名字,Redis将通过查询其实际值来排序。 + +在Redis里,虽然你可以有成千上万个关键字,类似上面展示的关系还是会引起一些混乱。幸好,`sort`命令也可以工作在散列数据结构及其相关域里。相对于拥有大量的高层次关键字,你可以利用散列: + + hset bug:12339 severity 3 + hset bug:12339 priority 1 + hset bug:12339 details "{id: 12339, ....}" + + hset bug:1382 severity 2 + hset bug:1382 priority 2 + hset bug:1382 details "{id: 1382, ....}" + + hset bug:338 severity 5 + hset bug:338 priority 3 + hset bug:338 details "{id: 338, ....}" + + hset bug:9338 severity 4 + hset bug:9338 priority 2 + hset bug:9338 details "{id: 9338, ....}" + +所有的事情不仅变得更为容易管理,而且我们能通过`severity`或`priority`来进行排序,还可以告诉`sort`命令具体要检索出哪一个域的数据: + + sort watch:leto by bug:*->priority get bug:*->details + +相同的值替代出现了,但Redis还能识别`->`符号,用它来查看散列中指定的域。里面还包括了`get`参数,这里也会进行值替代和域查看,从而检索出Bug的细节(details域的数据)。 + +对于太大的集合,`sort`命令的执行可能会变得很慢。好消息是,`sort`命令的输出可以被存储起来: + + sort watch:leto by bug:*->priority get bug:*->details store watch_by_priority:leto + +使用我们已经看过的`expiration`命令,再结合`sort`命令的`store`能力,这是一个美妙的组合。 + +### 小结 + +这一章主要关注那些非特定数据结构关联的命令。和其他事情一样,它们的使用依情况而定。构建一个程序或特性时,可能不会用到使用期限、发布和订阅或者排序等功能。但知道这些功能的存在是很好的。而且,我们也只接触到了一些命令。还有更多的命令,当你消化理解完这本书后,非常值得去浏览一下[完整的命令列表](http://redis.io/commands)。 + +\clearpage + +## 第5章 - 管理 + +在最后一章里,我们将集中谈论Redis运行中的一些管理方面内容。这是一个不完整的Redis管理指南,我们将会回答一些基本的问题,初接触Redis的新用户可能会很感兴趣。 + +### 配置(Configuration) + +当你第一次运行Redis的服务器,它会向你显示一个警告,指`redis.conf`文件没有被找到。这个文件可以被用来配置Redis的各个方面。一个充分定义(well-documented)的`redis.conf`文件对各个版本的Redis都有效。范例文件包含了默认的配置选项,因此,对于想要了解设置在干什么,或默认设置是什么,都会很有用。你可以在<https://github.com/antirez/redis/raw/2.4.6/redis.conf>找到这个文件。 + +**这个配置文件针对的是Redis 2.4.6,你应该用你的版本号替代上面URL里的"2.4.6"。运行`info`命令,其显示的第一个值就是Redis的版本号。** + +因为这个文件已经是充分定义(well-documented),我们就不去再进行设置了。 + +除了通过`redis.conf`文件来配置Redis,`config set`命令可以用来对个别值进行设置。实际上,在将`slowlog-log-slower-than`设置为0时,我们就已经使用过这个命令了。 + +还有一个`config get`命令能显示一个设置值。这个命令支持模式匹配,因此如果我们想要显示关联于日志(logging)的所有设置,我们可以这样做: + + config get *log* + +### 验证(Authentication) + +通过设置`requirepass`(使用`config set`命令或`redis.conf`文件),可以让Redis需要一个密码验证。当`requirepass`被设置了一个值(就是待用的密码),客户端将需要执行一个`auth password`命令。 + +一旦一个客户端通过了验证,就可以在任意数据库里执行任何一条命令,包括`flushall`命令,这将会清除掉每一个数据库里的所有关键字。通过配置,你可以重命名一些重要命令为混乱的字符串,从而获得一些安全性。 + + rename-command CONFIG 5ec4db169f9d4dddacbfb0c26ea7e5ef + rename-command FLUSHALL 1041285018a942a4922cbf76623b741e + +或者,你可以将新名字设置为一个空字符串,从而禁用掉一个命令。 + +### 大小限制(Size Limitations) + +当你开始使用Redis,你可能会想知道,我能使用多少个关键字?还可能想知道,一个散列数据结构能有多少个域(尤其是当你用它来组织数据时),或者是,一个列表数据结构或集合数据结构能有多少个元素?对于每一个实例,实际限制都能达到亿万级别(hundreds of millions)。 + +### 复制(Replication) + +Redis支持复制功能,这意味着当你向一个Redis实例(Master)进行写入时,一个或多个其他实例(Slaves)能通过Master实例来保持更新。可以在配置文件里设置`slaveof`,或使用`slaveof`命令来配置一个Slave实例。对于那些没有进行这些设置的Redis实例,就可能一个Master实例。 + +为了更好保护你的数据,复制功能拷贝数据到不同的服务器。复制功能还能用于改善性能,因为读取请求可以被发送到Slave实例。他们可能会返回一些稍微滞后的数据,但对于大多数程序来说,这是一个值得做的折衷。 + +遗憾的是,Redis的复制功能还没有提供自动故障恢复。如果Master实例崩溃了,一个Slave实例需要手动的进行升级。如果你想使用Redis去达到某种高可用性,对于使用心跳监控(heartbeat monitoring)和脚本自动开关(scripts to automate the switch)的传统高可用性工具来说,现在还是一个棘手的难题。 + +### 备份文件(Backups) + +备份Redis非常简单,你可以将Redis的快照(snapshot)拷贝到任何地方,包括S3、FTP等。默认情况下,Redis会把快照存储为一个名为`dump.rdb`的文件。在任何时候,你都可以对这个文件执行`scp`、`ftp`或`cp`等常用命令。 + +有一种常见情况,在Master实例上会停用快照以及单一附加文件(aof),然后让一个Slave实例去处理备份事宜。这可以帮助减少Master实例的载荷。在不损害整体系统响应性的情况下,你还可以在Slave实例上设置更多主动存储的参数。 + +### 缩放和Redis集群(Scaling and Redis Cluster) + +复制功能(Replication)是一个成长中的网站可以利用的第一个工具。有一些命令会比另外一些来的昂贵(例如`sort`命令),将这些运行载荷转移到一个Slave实例里,可以保持整体系统对于查询的快速响应。 + +此外,通过分发你的关键字到多个Redis实例里,可以达到真正的缩放Redis(记住,Redis是单线程的,这些可以运行在同一个逻辑框里)。随着时间的推移,你将需要特别注意这些事情(尽管许多的Redis载体都提供了consistent-hashing算法)。对于数据水平分布(horizontal distribution)的考虑不在这本书所讨论的范围内。这些东西你也很可能不需要去担心,但是,无论你使用哪一种解决方案,有一些事情你还是必须意识到。 + +好消息是,这些工作都可在Redis集群下进行。不仅提供水平缩放(包括均衡),为了高可用性,还提供了自动故障恢复。 + +高可用性和缩放是可以达到的,只要你愿意为此付出时间和精力,Redis集群也使事情变得简单多了。 + +### 小结 + +在过去的一段时间里,已经有许多的计划和网站使用了Redis,毫无疑问,Redis已经可以应用于实际生产中了。然而,一些工具还是不够成熟,尤其是一些安全性和可用性相关的工具。对于Redis集群,我们希望很快就能看到其实现,这应该能为一些现有的管理挑战提供处理帮忙。 + +\clearpage + +## 总结 + +在许多方面,Redis体现了一种简易的数据处理方式,其剥离掉了大部分的复杂性和抽象,并可有效的在不同系统里运行。不少情况下,选择Redis不是最佳的选择。在另一些情况里,Redis就像是为你的数据提供了特别定制的解决方案。 + +最终,回到我最开始所说的:Redis很容易学习。现在有许多的新技术,很难弄清楚哪些才真正值得我们花时间去学习。如果你从实际好处来考虑,Redis提供了他的简单性。我坚信,对于你和你的团队,学习Redis是最好的技术投资之一。 diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go new file mode 100644 index 000000000..3db2d9d51 --- /dev/null +++ b/hugolib/testhelpers_test.go @@ -0,0 +1,214 @@ +package hugolib + +import ( + "path/filepath" + "testing" + + "regexp" + + "fmt" + "strings" + + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/deps" + "github.com/spf13/afero" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/source" + "github.com/gohugoio/hugo/tpl" + "github.com/spf13/viper" + + "io/ioutil" + "os" + + "log" + + "github.com/gohugoio/hugo/hugofs" + jww "github.com/spf13/jwalterweatherman" + "github.com/stretchr/testify/require" +) + +type testHelper struct { + Cfg config.Provider + Fs *hugofs.Fs + T testing.TB +} + +func (th testHelper) assertFileContent(filename string, matches ...string) { + filename = th.replaceDefaultContentLanguageValue(filename) + content := readDestination(th.T, th.Fs, filename) + for _, match := range matches { + match = th.replaceDefaultContentLanguageValue(match) + require.True(th.T, strings.Contains(content, match), fmt.Sprintf("File no match for\n%q in\n%q:\n%s", strings.Replace(match, "%", "%%", -1), filename, strings.Replace(content, "%", "%%", -1))) + } +} + +// TODO(bep) better name for this. It does no magic replacements depending on defaultontentLanguageInSubDir. +func (th testHelper) assertFileContentStraight(filename string, matches ...string) { + content := readDestination(th.T, th.Fs, filename) + for _, match := range matches { + require.True(th.T, strings.Contains(content, match), fmt.Sprintf("File no match for\n%q in\n%q:\n%s", strings.Replace(match, "%", "%%", -1), filename, strings.Replace(content, "%", "%%", -1))) + } +} + +func (th testHelper) assertFileContentRegexp(filename string, matches ...string) { + filename = th.replaceDefaultContentLanguageValue(filename) + content := readDestination(th.T, th.Fs, filename) + for _, match := range matches { + match = th.replaceDefaultContentLanguageValue(match) + r := regexp.MustCompile(match) + require.True(th.T, r.MatchString(content), fmt.Sprintf("File no match for\n%q in\n%q:\n%s", strings.Replace(match, "%", "%%", -1), filename, strings.Replace(content, "%", "%%", -1))) + } +} + +func (th testHelper) assertFileNotExist(filename string) { + exists, err := helpers.Exists(filename, th.Fs.Destination) + require.NoError(th.T, err) + require.False(th.T, exists) +} + +func (th testHelper) replaceDefaultContentLanguageValue(value string) string { + defaultInSubDir := th.Cfg.GetBool("defaultContentLanguageInSubDir") + replace := th.Cfg.GetString("defaultContentLanguage") + "/" + + if !defaultInSubDir { + value = strings.Replace(value, replace, "", 1) + + } + return value +} + +func newTestPathSpec(fs *hugofs.Fs, v *viper.Viper) *helpers.PathSpec { + l := helpers.NewDefaultLanguage(v) + ps, _ := helpers.NewPathSpec(fs, l) + return ps +} + +func newTestDefaultPathSpec() *helpers.PathSpec { + v := viper.New() + // Easier to reason about in tests. + v.Set("disablePathToLower", true) + fs := hugofs.NewDefault(v) + ps, _ := helpers.NewPathSpec(fs, v) + return ps +} + +func newTestCfg() (*viper.Viper, *hugofs.Fs) { + + v := viper.New() + fs := hugofs.NewMem(v) + + v.SetFs(fs.Source) + + loadDefaultSettingsFor(v) + + // Default is false, but true is easier to use as default in tests + v.Set("defaultContentLanguageInSubdir", true) + + return v, fs + +} + +// newTestSite creates a new site in the English language with in-memory Fs. +// The site will have a template system loaded and ready to use. +// Note: This is only used in single site tests. +func newTestSite(t testing.TB, configKeyValues ...interface{}) *Site { + + cfg, fs := newTestCfg() + + for i := 0; i < len(configKeyValues); i += 2 { + cfg.Set(configKeyValues[i].(string), configKeyValues[i+1]) + } + + d := deps.DepsCfg{Language: helpers.NewLanguage("en", cfg), Fs: fs, Cfg: cfg} + + s, err := NewSiteForCfg(d) + + if err != nil { + t.Fatalf("Failed to create Site: %s", err) + } + return s +} + +func newTestSitesFromConfig(t testing.TB, afs afero.Fs, tomlConfig string, layoutPathContentPairs ...string) (testHelper, *HugoSites) { + if len(layoutPathContentPairs)%2 != 0 { + t.Fatalf("Layouts must be provided in pairs") + } + + writeToFs(t, afs, "config.toml", tomlConfig) + + cfg, err := LoadConfig(afs, "", "config.toml") + require.NoError(t, err) + + fs := hugofs.NewFrom(afs, cfg) + th := testHelper{cfg, fs, t} + + for i := 0; i < len(layoutPathContentPairs); i += 2 { + writeSource(t, fs, layoutPathContentPairs[i], layoutPathContentPairs[i+1]) + } + + h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + require.NoError(t, err) + + return th, h +} + +func newTestSitesFromConfigWithDefaultTemplates(t testing.TB, tomlConfig string) (testHelper, *HugoSites) { + return newTestSitesFromConfig(t, afero.NewMemMapFs(), tomlConfig, + "layouts/_default/single.html", "Single|{{ .Title }}|{{ .Content }}", + "layouts/_default/list.html", "List|{{ .Title }}|{{ .Content }}", + "layouts/_default/terms.html", "Terms List|{{ .Title }}|{{ .Content }}", + ) +} + +func newDebugLogger() *jww.Notepad { + return jww.NewNotepad(jww.LevelDebug, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) +} + +func newErrorLogger() *jww.Notepad { + return jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) +} +func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ tpl.TemplateHandler) error { + + return func(templ tpl.TemplateHandler) error { + for i := 0; i < len(additionalTemplates); i += 2 { + err := templ.AddTemplate(additionalTemplates[i], additionalTemplates[i+1]) + if err != nil { + return err + } + } + return nil + } +} + +func buildSingleSite(t testing.TB, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site { + return buildSingleSiteExpected(t, false, depsCfg, buildCfg) +} + +func buildSingleSiteExpected(t testing.TB, expectBuildError bool, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site { + h, err := NewHugoSites(depsCfg) + + require.NoError(t, err) + require.Len(t, h.Sites, 1) + + if expectBuildError { + require.Error(t, h.Build(buildCfg)) + return nil + + } + + require.NoError(t, h.Build(buildCfg)) + + return h.Sites[0] +} + +func writeSourcesToSource(t *testing.T, base string, fs *hugofs.Fs, sources ...source.ByteSource) { + for _, src := range sources { + writeSource(t, fs, filepath.Join(base, src.Name), string(src.Content)) + } +} + +func isCI() bool { + return os.Getenv("CI") != "" +} diff --git a/hugolib/translations.go b/hugolib/translations.go new file mode 100644 index 000000000..53272ee14 --- /dev/null +++ b/hugolib/translations.go @@ -0,0 +1,75 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" +) + +// Translations represent the other translations for a given page. The +// string here is the language code, as affected by the `post.LANG.md` +// filename. +type Translations map[string]*Page + +func pagesToTranslationsMap(pages []*Page) map[string]Translations { + out := make(map[string]Translations) + + for _, page := range pages { + base := createTranslationKey(page) + + pageTranslation, present := out[base] + if !present { + pageTranslation = make(Translations) + } + + pageLang := page.Lang() + if pageLang == "" { + continue + } + + pageTranslation[pageLang] = page + out[base] = pageTranslation + } + + return out +} + +func createTranslationKey(p *Page) string { + base := p.TranslationBaseName() + + if p.IsNode() { + // TODO(bep) see https://github.com/gohugoio/hugo/issues/2699 + // Must prepend the section and kind to the key to make it unique + base = fmt.Sprintf("%s/%s/%s", p.Kind, p.sections, base) + } + + return base +} + +func assignTranslationsToPages(allTranslations map[string]Translations, pages []*Page) { + for _, page := range pages { + page.translations = page.translations[:0] + base := createTranslationKey(page) + trans, exist := allTranslations[base] + if !exist { + continue + } + + for _, translatedPage := range trans { + page.translations = append(page.translations, translatedPage) + } + + pageBy(languagePageSort).Sort(page.translations) + } +} diff --git a/i18n/i18n.go b/i18n/i18n.go new file mode 100644 index 000000000..73417fb32 --- /dev/null +++ b/i18n/i18n.go @@ -0,0 +1,113 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package i18n + +import ( + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/helpers" + "github.com/nicksnyder/go-i18n/i18n/bundle" + jww "github.com/spf13/jwalterweatherman" +) + +var ( + i18nWarningLogger = helpers.NewDistinctFeedbackLogger() +) + +// Translator handles i18n translations. +type Translator struct { + translateFuncs map[string]bundle.TranslateFunc + cfg config.Provider + logger *jww.Notepad +} + +// NewTranslator creates a new Translator for the given language bundle and configuration. +func NewTranslator(b *bundle.Bundle, cfg config.Provider, logger *jww.Notepad) Translator { + t := Translator{cfg: cfg, logger: logger, translateFuncs: make(map[string]bundle.TranslateFunc)} + t.initFuncs(b) + return t +} + +// Func gets the translate func for the given language, or for the default +// configured language if not found. +func (t Translator) Func(lang string) bundle.TranslateFunc { + if f, ok := t.translateFuncs[lang]; ok { + return f + } + t.logger.WARN.Printf("Translation func for language %v not found, use default.", lang) + if f, ok := t.translateFuncs[t.cfg.GetString("defaultContentLanguage")]; ok { + return f + } + t.logger.WARN.Println("i18n not initialized, check that you have language file (in i18n) that matches the site language or the default language.") + return func(translationID string, args ...interface{}) string { + return "" + } + +} + +func (t Translator) initFuncs(bndl *bundle.Bundle) { + defaultContentLanguage := t.cfg.GetString("defaultContentLanguage") + + defaultT, err := bndl.Tfunc(defaultContentLanguage) + if err != nil { + jww.WARN.Printf("No translation bundle found for default language %q", defaultContentLanguage) + } + + enableMissingTranslationPlaceholders := t.cfg.GetBool("enableMissingTranslationPlaceholders") + for _, lang := range bndl.LanguageTags() { + currentLang := lang + + t.translateFuncs[currentLang] = func(translationID string, args ...interface{}) string { + tFunc, err := bndl.Tfunc(currentLang) + if err != nil { + jww.WARN.Printf("could not load translations for language %q (%s), will use default content language.\n", lang, err) + } + + translated := tFunc(translationID, args...) + if translated != translationID { + return translated + } + // If there is no translation for translationID, + // then Tfunc returns translationID itself. + // But if user set same translationID and translation, we should check + // if it really untranslated: + if isIDTranslated(currentLang, translationID, bndl) { + return translated + } + + if t.cfg.GetBool("logI18nWarnings") { + i18nWarningLogger.Printf("i18n|MISSING_TRANSLATION|%s|%s", currentLang, translationID) + } + if enableMissingTranslationPlaceholders { + return "[i18n] " + translationID + } + if defaultT != nil { + translated := defaultT(translationID, args...) + if translated != translationID { + return translated + } + if isIDTranslated(defaultContentLanguage, translationID, bndl) { + return translated + } + } + return "" + } + } +} + +// If bndl contains the translationID for specified currentLang, +// then the translationID is actually translated. +func isIDTranslated(lang, id string, b *bundle.Bundle) bool { + _, contains := b.Translations()[lang][id] + return contains +} diff --git a/i18n/i18n_test.go b/i18n/i18n_test.go new file mode 100644 index 000000000..6a9d362b0 --- /dev/null +++ b/i18n/i18n_test.go @@ -0,0 +1,177 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package i18n + +import ( + "testing" + + "io/ioutil" + "os" + + "log" + + "github.com/gohugoio/hugo/config" + "github.com/nicksnyder/go-i18n/i18n/bundle" + jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" +) + +var logger = jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) + +type i18nTest struct { + data map[string][]byte + args interface{} + lang, id, expected, expectedFlag string +} + +var i18nTests = []i18nTest{ + // All translations present + { + data: map[string][]byte{ + "en.toml": []byte("[hello]\nother = \"Hello, World!\""), + "es.toml": []byte("[hello]\nother = \"¡Hola, Mundo!\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "¡Hola, Mundo!", + expectedFlag: "¡Hola, Mundo!", + }, + // Translation missing in current language but present in default + { + data: map[string][]byte{ + "en.toml": []byte("[hello]\nother = \"Hello, World!\""), + "es.toml": []byte("[goodbye]\nother = \"¡Adiós, Mundo!\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "Hello, World!", + expectedFlag: "[i18n] hello", + }, + // Translation missing in default language but present in current + { + data: map[string][]byte{ + "en.toml": []byte("[goodbye]\nother = \"Goodbye, World!\""), + "es.toml": []byte("[hello]\nother = \"¡Hola, Mundo!\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "¡Hola, Mundo!", + expectedFlag: "¡Hola, Mundo!", + }, + // Translation missing in both default and current language + { + data: map[string][]byte{ + "en.toml": []byte("[goodbye]\nother = \"Goodbye, World!\""), + "es.toml": []byte("[goodbye]\nother = \"¡Adiós, Mundo!\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "", + expectedFlag: "[i18n] hello", + }, + // Default translation file missing or empty + { + data: map[string][]byte{ + "en.toml": []byte(""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "", + expectedFlag: "[i18n] hello", + }, + // Context provided + { + data: map[string][]byte{ + "en.toml": []byte("[wordCount]\nother = \"Hello, {{.WordCount}} people!\""), + "es.toml": []byte("[wordCount]\nother = \"¡Hola, {{.WordCount}} gente!\""), + }, + args: struct { + WordCount int + }{ + 50, + }, + lang: "es", + id: "wordCount", + expected: "¡Hola, 50 gente!", + expectedFlag: "¡Hola, 50 gente!", + }, + // Same id and translation in current language + // https://github.com/gohugoio/hugo/issues/2607 + { + data: map[string][]byte{ + "es.toml": []byte("[hello]\nother = \"hello\""), + "en.toml": []byte("[hello]\nother = \"hi\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "hello", + expectedFlag: "hello", + }, + // Translation missing in current language, but same id and translation in default + { + data: map[string][]byte{ + "es.toml": []byte("[bye]\nother = \"bye\""), + "en.toml": []byte("[hello]\nother = \"hello\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "hello", + expectedFlag: "[i18n] hello", + }, +} + +func doTestI18nTranslate(t *testing.T, test i18nTest, cfg config.Provider) string { + i18nBundle := bundle.New() + + for file, content := range test.data { + err := i18nBundle.ParseTranslationFileBytes(file, content) + if err != nil { + t.Errorf("Error parsing translation file: %s", err) + } + } + + translator := NewTranslator(i18nBundle, cfg, logger) + f := translator.Func(test.lang) + translated := f(test.id, test.args) + return translated +} + +func TestI18nTranslate(t *testing.T) { + var actual, expected string + v := viper.New() + v.SetDefault("defaultContentLanguage", "en") + + // Test without and with placeholders + for _, enablePlaceholders := range []bool{false, true} { + v.Set("enableMissingTranslationPlaceholders", enablePlaceholders) + + for _, test := range i18nTests { + if enablePlaceholders { + expected = test.expectedFlag + } else { + expected = test.expected + } + actual = doTestI18nTranslate(t, test, v) + require.Equal(t, expected, actual) + } + } +} diff --git a/i18n/translationProvider.go b/i18n/translationProvider.go new file mode 100644 index 000000000..9947d3ce5 --- /dev/null +++ b/i18n/translationProvider.go @@ -0,0 +1,73 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package i18n + +import ( + "fmt" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/source" + "github.com/nicksnyder/go-i18n/i18n/bundle" +) + +// TranslationProvider provides translation handling, i.e. loading +// of bundles etc. +type TranslationProvider struct { + t Translator +} + +// NewTranslationProvider creates a new translation provider. +func NewTranslationProvider() *TranslationProvider { + return &TranslationProvider{} +} + +// Update updates the i18n func in the provided Deps. +func (tp *TranslationProvider) Update(d *deps.Deps) error { + dir := d.PathSpec.AbsPathify(d.Cfg.GetString("i18nDir")) + sp := source.NewSourceSpec(d.Cfg, d.Fs) + sources := []source.Input{sp.NewFilesystem(dir)} + + themeI18nDir, err := d.PathSpec.GetThemeI18nDirPath() + + if err == nil { + sources = []source.Input{sp.NewFilesystem(themeI18nDir), sources[0]} + } + + d.Log.DEBUG.Printf("Load I18n from %q", sources) + + i18nBundle := bundle.New() + + for _, currentSource := range sources { + for _, r := range currentSource.Files() { + err := i18nBundle.ParseTranslationFileBytes(r.LogicalName(), r.Bytes()) + if err != nil { + return fmt.Errorf("Failed to load translations in file %q: %s", r.LogicalName(), err) + } + } + } + + tp.t = NewTranslator(i18nBundle, d.Cfg, d.Log) + + d.Translate = tp.t.Func(d.Language.Lang) + + return nil + +} + +// Clone sets the language func for the new language. +func (tp *TranslationProvider) Clone(d *deps.Deps) error { + d.Translate = tp.t.Func(d.Language.Lang) + + return nil +} diff --git a/livereload/connection.go b/livereload/connection.go new file mode 100644 index 000000000..4e94e2ee0 --- /dev/null +++ b/livereload/connection.go @@ -0,0 +1,66 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package livereload + +import ( + "bytes" + "sync" + + "github.com/gorilla/websocket" +) + +type connection struct { + // The websocket connection. + ws *websocket.Conn + + // Buffered channel of outbound messages. + send chan []byte + + // There is a potential data race, especially visible with large files. + // This is protected by synchronisation of the send channel's close. + closer sync.Once +} + +func (c *connection) close() { + c.closer.Do(func() { + close(c.send) + }) +} + +func (c *connection) reader() { + for { + _, message, err := c.ws.ReadMessage() + if err != nil { + break + } + if bytes.Contains(message, []byte(`"command":"hello"`)) { + c.send <- []byte(`{ + "command": "hello", + "protocols": [ "http://livereload.com/protocols/official-7" ], + "serverName": "Hugo" + }`) + } + } + c.ws.Close() +} + +func (c *connection) writer() { + for message := range c.send { + err := c.ws.WriteMessage(websocket.TextMessage, message) + if err != nil { + break + } + } + c.ws.Close() +} diff --git a/livereload/hub.go b/livereload/hub.go new file mode 100644 index 000000000..8ab6083ad --- /dev/null +++ b/livereload/hub.go @@ -0,0 +1,56 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package livereload + +type hub struct { + // Registered connections. + connections map[*connection]bool + + // Inbound messages from the connections. + broadcast chan []byte + + // Register requests from the connections. + register chan *connection + + // Unregister requests from connections. + unregister chan *connection +} + +var wsHub = hub{ + broadcast: make(chan []byte), + register: make(chan *connection), + unregister: make(chan *connection), + connections: make(map[*connection]bool), +} + +func (h *hub) run() { + for { + select { + case c := <-h.register: + h.connections[c] = true + case c := <-h.unregister: + delete(h.connections, c) + c.close() + case m := <-h.broadcast: + for c := range h.connections { + select { + case c.send <- m: + default: + delete(h.connections, c) + c.close() + } + } + } + } +} diff --git a/livereload/livereload.go b/livereload/livereload.go new file mode 100644 index 000000000..04e3ac0f0 --- /dev/null +++ b/livereload/livereload.go @@ -0,0 +1,89 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Contains an embedded version of livereload.js +// +// Copyright (c) 2010-2015 Andrey Tarantsov +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package livereload + +import ( + "net/http" + "path/filepath" + + "github.com/gorilla/websocket" +) + +var upgrader = &websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024} + +// Handler is a HandlerFunc handling the livereload +// Websocket interaction. +func Handler(w http.ResponseWriter, r *http.Request) { + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return + } + c := &connection{send: make(chan []byte, 256), ws: ws} + wsHub.register <- c + defer func() { wsHub.unregister <- c }() + go c.writer() + c.reader() +} + +// Initialize starts the Websocket Hub handling live reloads. +func Initialize() { + go wsHub.run() +} + +// ForceRefresh tells livereload to force a hard refresh. +func ForceRefresh() { + RefreshPath("/x.js") +} + +// RefreshPath tells livereload to refresh only the given path. +// If that path points to a CSS stylesheet or an image, only the changes +// will be updated in the browser, not the entire page. +func RefreshPath(s string) { + // Tell livereload a file has changed - will force a hard refresh if not CSS or an image + urlPath := filepath.ToSlash(s) + wsHub.broadcast <- []byte(`{"command":"reload","path":"` + urlPath + `","originalPath":"","liveCSS":true,"liveImg":true}`) +} + +// ServeJS serves the liverreload.js who's reference is injected into the page. +func ServeJS(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/javascript") + w.Write(livereloadJS) +} + +// livereloadJS contains the output of `uglifyjs livereload.js -m toplevel` +// Source: https://github.com/livereload/livereload-js/archive/v2.2.1.tar.gz +var livereloadJS = []byte(`(function e(t,n,o){function i(s,l){if(!n[s]){if(!t[s]){var c=typeof require=="function"&&require;if(!l&&c)return c(s,!0);if(r)return r(s,!0);var a=new Error("Cannot find module '"+s+"'");throw a.code="MODULE_NOT_FOUND",a}var u=n[s]={exports:{}};t[s][0].call(u.exports,function(e){var n=t[s][1][e];return i(n?n:e)},u,u.exports,e,t,n,o)}return n[s].exports}var r=typeof require=="function"&&require;for(var s=0;s<o.length;s++)i(o[s]);return i})({1:[function(e,t,n){(function(){var t,o,i,r,s,l;l=e("./protocol"),r=l.Parser,o=l.PROTOCOL_6,i=l.PROTOCOL_7;s="2.2.1";n.Connector=t=function(){function e(e,t,n,o){this.options=e;this.WebSocket=t;this.Timer=n;this.handlers=o;this._uri="ws://"+this.options.host+":"+this.options.port+"/livereload";this._nextDelay=this.options.mindelay;this._connectionDesired=false;this.protocol=0;this.protocolParser=new r({connected:function(e){return function(t){e.protocol=t;e._handshakeTimeout.stop();e._nextDelay=e.options.mindelay;e._disconnectionReason="broken";return e.handlers.connected(t)}}(this),error:function(e){return function(t){e.handlers.error(t);return e._closeOnError()}}(this),message:function(e){return function(t){return e.handlers.message(t)}}(this)});this._handshakeTimeout=new n(function(e){return function(){if(!e._isSocketConnected()){return}e._disconnectionReason="handshake-timeout";return e.socket.close()}}(this));this._reconnectTimer=new n(function(e){return function(){if(!e._connectionDesired){return}return e.connect()}}(this));this.connect()}e.prototype._isSocketConnected=function(){return this.socket&&this.socket.readyState===this.WebSocket.OPEN};e.prototype.connect=function(){this._connectionDesired=true;if(this._isSocketConnected()){return}this._reconnectTimer.stop();this._disconnectionReason="cannot-connect";this.protocolParser.reset();this.handlers.connecting();this.socket=new this.WebSocket(this._uri);this.socket.onopen=function(e){return function(t){return e._onopen(t)}}(this);this.socket.onclose=function(e){return function(t){return e._onclose(t)}}(this);this.socket.onmessage=function(e){return function(t){return e._onmessage(t)}}(this);return this.socket.onerror=function(e){return function(t){return e._onerror(t)}}(this)};e.prototype.disconnect=function(){this._connectionDesired=false;this._reconnectTimer.stop();if(!this._isSocketConnected()){return}this._disconnectionReason="manual";return this.socket.close()};e.prototype._scheduleReconnection=function(){if(!this._connectionDesired){return}if(!this._reconnectTimer.running){this._reconnectTimer.start(this._nextDelay);return this._nextDelay=Math.min(this.options.maxdelay,this._nextDelay*2)}};e.prototype.sendCommand=function(e){if(this.protocol==null){return}return this._sendCommand(e)};e.prototype._sendCommand=function(e){return this.socket.send(JSON.stringify(e))};e.prototype._closeOnError=function(){this._handshakeTimeout.stop();this._disconnectionReason="error";return this.socket.close()};e.prototype._onopen=function(e){var t;this.handlers.socketConnected();this._disconnectionReason="handshake-failed";t={command:"hello",protocols:[o,i]};t.ver=s;if(this.options.ext){t.ext=this.options.ext}if(this.options.extver){t.extver=this.options.extver}if(this.options.snipver){t.snipver=this.options.snipver}this._sendCommand(t);return this._handshakeTimeout.start(this.options.handshake_timeout)};e.prototype._onclose=function(e){this.protocol=0;this.handlers.disconnected(this._disconnectionReason,this._nextDelay);return this._scheduleReconnection()};e.prototype._onerror=function(e){};e.prototype._onmessage=function(e){return this.protocolParser.process(e.data)};return e}()}).call(this)},{"./protocol":6}],2:[function(e,t,n){(function(){var e;e={bind:function(e,t,n){if(e.addEventListener){return e.addEventListener(t,n,false)}else if(e.attachEvent){e[t]=1;return e.attachEvent("onpropertychange",function(e){if(e.propertyName===t){return n()}})}else{throw new Error("Attempt to attach custom event "+t+" to something which isn't a DOMElement")}},fire:function(e,t){var n;if(e.addEventListener){n=document.createEvent("HTMLEvents");n.initEvent(t,true,true);return document.dispatchEvent(n)}else if(e.attachEvent){if(e[t]){return e[t]++}}else{throw new Error("Attempt to fire custom event "+t+" on something which isn't a DOMElement")}}};n.bind=e.bind;n.fire=e.fire}).call(this)},{}],3:[function(e,t,n){(function(){var e;t.exports=e=function(){e.identifier="less";e.version="1.0";function e(e,t){this.window=e;this.host=t}e.prototype.reload=function(e,t){if(this.window.less&&this.window.less.refresh){if(e.match(/\.less$/i)){return this.reloadLess(e)}if(t.originalPath.match(/\.less$/i)){return this.reloadLess(t.originalPath)}}return false};e.prototype.reloadLess=function(e){var t,n,o,i;n=function(){var e,n,o,i;o=document.getElementsByTagName("link");i=[];for(e=0,n=o.length;e<n;e++){t=o[e];if(t.href&&t.rel.match(/^stylesheet\/less$/i)||t.rel.match(/stylesheet/i)&&t.type.match(/^text\/(x-)?less$/i)){i.push(t)}}return i}();if(n.length===0){return false}for(o=0,i=n.length;o<i;o++){t=n[o];t.href=this.host.generateCacheBustUrl(t.href)}this.host.console.log("LiveReload is asking LESS to recompile all stylesheets");this.window.less.refresh(true);return true};e.prototype.analyze=function(){return{disable:!!(this.window.less&&this.window.less.refresh)}};return e}()}).call(this)},{}],4:[function(e,t,n){(function(){var t,o,i,r,s;t=e("./connector").Connector;s=e("./timer").Timer;i=e("./options").Options;r=e("./reloader").Reloader;n.LiveReload=o=function(){function e(e){this.window=e;this.listeners={};this.plugins=[];this.pluginIdentifiers={};this.console=this.window.location.href.match(/LR-verbose/)&&this.window.console&&this.window.console.log&&this.window.console.error?this.window.console:{log:function(){},error:function(){}};if(!(this.WebSocket=this.window.WebSocket||this.window.MozWebSocket)){this.console.error("LiveReload disabled because the browser does not seem to support web sockets");return}if(!(this.options=i.extract(this.window.document))){this.console.error("LiveReload disabled because it could not find its own <SCRIPT> tag");return}this.reloader=new r(this.window,this.console,s);this.connector=new t(this.options,this.WebSocket,s,{connecting:function(e){return function(){}}(this),socketConnected:function(e){return function(){}}(this),connected:function(e){return function(t){var n;if(typeof(n=e.listeners).connect==="function"){n.connect()}e.log("LiveReload is connected to "+e.options.host+":"+e.options.port+" (protocol v"+t+").");return e.analyze()}}(this),error:function(e){return function(e){if(e instanceof ProtocolError){if(typeof console!=="undefined"&&console!==null){return console.log(""+e.message+".")}}else{if(typeof console!=="undefined"&&console!==null){return console.log("LiveReload internal error: "+e.message)}}}}(this),disconnected:function(e){return function(t,n){var o;if(typeof(o=e.listeners).disconnect==="function"){o.disconnect()}switch(t){case"cannot-connect":return e.log("LiveReload cannot connect to "+e.options.host+":"+e.options.port+", will retry in "+n+" sec.");case"broken":return e.log("LiveReload disconnected from "+e.options.host+":"+e.options.port+", reconnecting in "+n+" sec.");case"handshake-timeout":return e.log("LiveReload cannot connect to "+e.options.host+":"+e.options.port+" (handshake timeout), will retry in "+n+" sec.");case"handshake-failed":return e.log("LiveReload cannot connect to "+e.options.host+":"+e.options.port+" (handshake failed), will retry in "+n+" sec.");case"manual":break;case"error":break;default:return e.log("LiveReload disconnected from "+e.options.host+":"+e.options.port+" ("+t+"), reconnecting in "+n+" sec.")}}}(this),message:function(e){return function(t){switch(t.command){case"reload":return e.performReload(t);case"alert":return e.performAlert(t)}}}(this)})}e.prototype.on=function(e,t){return this.listeners[e]=t};e.prototype.log=function(e){return this.console.log(""+e)};e.prototype.performReload=function(e){var t,n;this.log("LiveReload received reload request: "+JSON.stringify(e,null,2));return this.reloader.reload(e.path,{liveCSS:(t=e.liveCSS)!=null?t:true,liveImg:(n=e.liveImg)!=null?n:true,originalPath:e.originalPath||"",overrideURL:e.overrideURL||"",serverURL:"http://"+this.options.host+":"+this.options.port})};e.prototype.performAlert=function(e){return alert(e.message)};e.prototype.shutDown=function(){var e;this.connector.disconnect();this.log("LiveReload disconnected.");return typeof(e=this.listeners).shutdown==="function"?e.shutdown():void 0};e.prototype.hasPlugin=function(e){return!!this.pluginIdentifiers[e]};e.prototype.addPlugin=function(e){var t;if(this.hasPlugin(e.identifier)){return}this.pluginIdentifiers[e.identifier]=true;t=new e(this.window,{_livereload:this,_reloader:this.reloader,_connector:this.connector,console:this.console,Timer:s,generateCacheBustUrl:function(e){return function(t){return e.reloader.generateCacheBustUrl(t)}}(this)});this.plugins.push(t);this.reloader.addPlugin(t)};e.prototype.analyze=function(){var e,t,n,o,i,r;if(!(this.connector.protocol>=7)){return}n={};r=this.plugins;for(o=0,i=r.length;o<i;o++){e=r[o];n[e.constructor.identifier]=t=(typeof e.analyze==="function"?e.analyze():void 0)||{};t.version=e.constructor.version}this.connector.sendCommand({command:"info",plugins:n,url:this.window.location.href})};return e}()}).call(this)},{"./connector":1,"./options":5,"./reloader":7,"./timer":9}],5:[function(e,t,n){(function(){var e;n.Options=e=function(){function e(){this.host=null;this.port=35729;this.snipver=null;this.ext=null;this.extver=null;this.mindelay=1e3;this.maxdelay=6e4;this.handshake_timeout=5e3}e.prototype.set=function(e,t){if(typeof t==="undefined"){return}if(!isNaN(+t)){t=+t}return this[e]=t};return e}();e.extract=function(t){var n,o,i,r,s,l,c,a,u,h,d,f,p;f=t.getElementsByTagName("script");for(a=0,h=f.length;a<h;a++){n=f[a];if((c=n.src)&&(i=c.match(/^[^:]+:\/\/(.*)\/z?livereload\.js(?:\?(.*))?$/))){s=new e;if(r=i[1].match(/^([^\/:]+)(?::(\d+))?$/)){s.host=r[1];if(r[2]){s.port=parseInt(r[2],10)}}if(i[2]){p=i[2].split("&");for(u=0,d=p.length;u<d;u++){l=p[u];if((o=l.split("=")).length>1){s.set(o[0].replace(/-/g,"_"),o.slice(1).join("="))}}}return s}}return null}}).call(this)},{}],6:[function(e,t,n){(function(){var e,t,o,i,r=[].indexOf||function(e){for(var t=0,n=this.length;t<n;t++){if(t in this&&this[t]===e)return t}return-1};n.PROTOCOL_6=e="http://livereload.com/protocols/official-6";n.PROTOCOL_7=t="http://livereload.com/protocols/official-7";n.ProtocolError=i=function(){function e(e,t){this.message="LiveReload protocol error ("+e+') after receiving data: "'+t+'".'}return e}();n.Parser=o=function(){function n(e){this.handlers=e;this.reset()}n.prototype.reset=function(){return this.protocol=null};n.prototype.process=function(n){var o,s,l,c,a;try{if(this.protocol==null){if(n.match(/^!!ver:([\d.]+)$/)){this.protocol=6}else if(l=this._parseMessage(n,["hello"])){if(!l.protocols.length){throw new i("no protocols specified in handshake message")}else if(r.call(l.protocols,t)>=0){this.protocol=7}else if(r.call(l.protocols,e)>=0){this.protocol=6}else{throw new i("no supported protocols found")}}return this.handlers.connected(this.protocol)}else if(this.protocol===6){l=JSON.parse(n);if(!l.length){throw new i("protocol 6 messages must be arrays")}o=l[0],c=l[1];if(o!=="refresh"){throw new i("unknown protocol 6 command")}return this.handlers.message({command:"reload",path:c.path,liveCSS:(a=c.apply_css_live)!=null?a:true})}else{l=this._parseMessage(n,["reload","alert"]);return this.handlers.message(l)}}catch(u){s=u;if(s instanceof i){return this.handlers.error(s)}else{throw s}}};n.prototype._parseMessage=function(e,t){var n,o,s;try{o=JSON.parse(e)}catch(l){n=l;throw new i("unparsable JSON",e)}if(!o.command){throw new i('missing "command" key',e)}if(s=o.command,r.call(t,s)<0){throw new i("invalid command '"+o.command+"', only valid commands are: "+t.join(", ")+")",e)}return o};return n}()}).call(this)},{}],7:[function(e,t,n){(function(){var e,t,o,i,r,s,l;l=function(e){var t,n,o;if((n=e.indexOf("#"))>=0){t=e.slice(n);e=e.slice(0,n)}else{t=""}if((n=e.indexOf("?"))>=0){o=e.slice(n);e=e.slice(0,n)}else{o=""}return{url:e,params:o,hash:t}};i=function(e){var t;e=l(e).url;if(e.indexOf("file://")===0){t=e.replace(/^file:\/\/(localhost)?/,"")}else{t=e.replace(/^([^:]+:)?\/\/([^:\/]+)(:\d*)?\//,"/")}return decodeURIComponent(t)};s=function(e,t,n){var i,r,s,l,c;i={score:0};for(l=0,c=t.length;l<c;l++){r=t[l];s=o(e,n(r));if(s>i.score){i={object:r,score:s}}}if(i.score>0){return i}else{return null}};o=function(e,t){var n,o,i,r;e=e.replace(/^\/+/,"").toLowerCase();t=t.replace(/^\/+/,"").toLowerCase();if(e===t){return 1e4}n=e.split("/").reverse();o=t.split("/").reverse();r=Math.min(n.length,o.length);i=0;while(i<r&&n[i]===o[i]){++i}return i};r=function(e,t){return o(e,t)>0};e=[{selector:"background",styleNames:["backgroundImage"]},{selector:"border",styleNames:["borderImage","webkitBorderImage","MozBorderImage"]}];n.Reloader=t=function(){function t(e,t,n){this.window=e;this.console=t;this.Timer=n;this.document=this.window.document;this.importCacheWaitPeriod=200;this.plugins=[]}t.prototype.addPlugin=function(e){return this.plugins.push(e)};t.prototype.analyze=function(e){return results};t.prototype.reload=function(e,t){var n,o,i,r,s;this.options=t;if((o=this.options).stylesheetReloadTimeout==null){o.stylesheetReloadTimeout=15e3}s=this.plugins;for(i=0,r=s.length;i<r;i++){n=s[i];if(n.reload&&n.reload(e,t)){return}}if(t.liveCSS){if(e.match(/\.css$/i)){if(this.reloadStylesheet(e)){return}}}if(t.liveImg){if(e.match(/\.(jpe?g|png|gif)$/i)){this.reloadImages(e);return}}return this.reloadPage()};t.prototype.reloadPage=function(){return this.window.document.location.reload()};t.prototype.reloadImages=function(t){var n,o,s,l,c,a,u,h,d,f,p,m,g,v,y,w,_,R;n=this.generateUniqueString();v=this.document.images;for(a=0,f=v.length;a<f;a++){o=v[a];if(r(t,i(o.src))){o.src=this.generateCacheBustUrl(o.src,n)}}if(this.document.querySelectorAll){for(u=0,p=e.length;u<p;u++){y=e[u],s=y.selector,l=y.styleNames;w=this.document.querySelectorAll("[style*="+s+"]");for(h=0,m=w.length;h<m;h++){o=w[h];this.reloadStyleImages(o.style,l,t,n)}}}if(this.document.styleSheets){_=this.document.styleSheets;R=[];for(d=0,g=_.length;d<g;d++){c=_[d];R.push(this.reloadStylesheetImages(c,t,n))}return R}};t.prototype.reloadStylesheetImages=function(t,n,o){var i,r,s,l,c,a,u,h;try{s=t!=null?t.cssRules:void 0}catch(d){i=d}if(!s){return}for(c=0,u=s.length;c<u;c++){r=s[c];switch(r.type){case CSSRule.IMPORT_RULE:this.reloadStylesheetImages(r.styleSheet,n,o);break;case CSSRule.STYLE_RULE:for(a=0,h=e.length;a<h;a++){l=e[a].styleNames;this.reloadStyleImages(r.style,l,n,o)}break;case CSSRule.MEDIA_RULE:this.reloadStylesheetImages(r,n,o)}}};t.prototype.reloadStyleImages=function(e,t,n,o){var s,l,c,a,u;for(a=0,u=t.length;a<u;a++){l=t[a];c=e[l];if(typeof c==="string"){s=c.replace(/\burl\s*\(([^)]*)\)/,function(e){return function(t,s){if(r(n,i(s))){return"url("+e.generateCacheBustUrl(s,o)+")"}else{return t}}}(this));if(s!==c){e[l]=s}}}};t.prototype.reloadStylesheet=function(e){var t,n,o,r,l,c,a,u,h,d,f,p,m,g,v;o=function(){var e,t,o,i;o=this.document.getElementsByTagName("link");i=[];for(e=0,t=o.length;e<t;e++){n=o[e];if(n.rel.match(/^stylesheet$/i)&&!n.__LiveReload_pendingRemoval){i.push(n)}}return i}.call(this);t=[];g=this.document.getElementsByTagName("style");for(c=0,d=g.length;c<d;c++){l=g[c];if(l.sheet){this.collectImportedStylesheets(l,l.sheet,t)}}for(a=0,f=o.length;a<f;a++){n=o[a];this.collectImportedStylesheets(n,n.sheet,t)}if(this.window.StyleFix&&this.document.querySelectorAll){v=this.document.querySelectorAll("style[data-href]");for(u=0,p=v.length;u<p;u++){l=v[u];o.push(l)}}this.console.log("LiveReload found "+o.length+" LINKed stylesheets, "+t.length+" @imported stylesheets");r=s(e,o.concat(t),function(e){return function(t){return i(e.linkHref(t))}}(this));if(r){if(r.object.rule){this.console.log("LiveReload is reloading imported stylesheet: "+r.object.href);this.reattachImportedRule(r.object)}else{this.console.log("LiveReload is reloading stylesheet: "+this.linkHref(r.object));this.reattachStylesheetLink(r.object)}}else{this.console.log("LiveReload will reload all stylesheets because path '"+e+"' did not match any specific one");for(h=0,m=o.length;h<m;h++){n=o[h];this.reattachStylesheetLink(n)}}return true};t.prototype.collectImportedStylesheets=function(e,t,n){var o,i,r,s,l,c;try{s=t!=null?t.cssRules:void 0}catch(a){o=a}if(s&&s.length){for(i=l=0,c=s.length;l<c;i=++l){r=s[i];switch(r.type){case CSSRule.CHARSET_RULE:continue;case CSSRule.IMPORT_RULE:n.push({link:e,rule:r,index:i,href:r.href});this.collectImportedStylesheets(e,r.styleSheet,n);break;default:break}}}};t.prototype.waitUntilCssLoads=function(e,t){var n,o,i;n=false;o=function(e){return function(){if(n){return}n=true;return t()}}(this);e.onload=function(e){return function(){e.console.log("LiveReload: the new stylesheet has finished loading");e.knownToSupportCssOnLoad=true;return o()}}(this);if(!this.knownToSupportCssOnLoad){(i=function(t){return function(){if(e.sheet){t.console.log("LiveReload is polling until the new CSS finishes loading...");return o()}else{return t.Timer.start(50,i)}}}(this))()}return this.Timer.start(this.options.stylesheetReloadTimeout,o)};t.prototype.linkHref=function(e){return e.href||e.getAttribute("data-href")};t.prototype.reattachStylesheetLink=function(e){var t,n;if(e.__LiveReload_pendingRemoval){return}e.__LiveReload_pendingRemoval=true;if(e.tagName==="STYLE"){t=this.document.createElement("link");t.rel="stylesheet";t.media=e.media;t.disabled=e.disabled}else{t=e.cloneNode(false)}t.href=this.generateCacheBustUrl(this.linkHref(e));n=e.parentNode;if(n.lastChild===e){n.appendChild(t)}else{n.insertBefore(t,e.nextSibling)}return this.waitUntilCssLoads(t,function(n){return function(){var o;if(/AppleWebKit/.test(navigator.userAgent)){o=5}else{o=200}return n.Timer.start(o,function(){var o;if(!e.parentNode){return}e.parentNode.removeChild(e);t.onreadystatechange=null;return(o=n.window.StyleFix)!=null?o.link(t):void 0})}}(this))};t.prototype.reattachImportedRule=function(e){var t,n,o,i,r,s,l,c;l=e.rule,n=e.index,o=e.link;s=l.parentStyleSheet;t=this.generateCacheBustUrl(l.href);i=l.media.length?[].join.call(l.media,", "):"";r='@import url("'+t+'") '+i+";";l.__LiveReload_newHref=t;c=this.document.createElement("link");c.rel="stylesheet";c.href=t;c.__LiveReload_pendingRemoval=true;if(o.parentNode){o.parentNode.insertBefore(c,o)}return this.Timer.start(this.importCacheWaitPeriod,function(e){return function(){if(c.parentNode){c.parentNode.removeChild(c)}if(l.__LiveReload_newHref!==t){return}s.insertRule(r,n);s.deleteRule(n+1);l=s.cssRules[n];l.__LiveReload_newHref=t;return e.Timer.start(e.importCacheWaitPeriod,function(){if(l.__LiveReload_newHref!==t){return}s.insertRule(r,n);return s.deleteRule(n+1)})}}(this))};t.prototype.generateUniqueString=function(){return"livereload="+Date.now()};t.prototype.generateCacheBustUrl=function(e,t){var n,o,i,r,s;if(t==null){t=this.generateUniqueString()}s=l(e),e=s.url,n=s.hash,o=s.params;if(this.options.overrideURL){if(e.indexOf(this.options.serverURL)<0){i=e;e=this.options.serverURL+this.options.overrideURL+"?url="+encodeURIComponent(e);this.console.log("LiveReload is overriding source URL "+i+" with "+e)}}r=o.replace(/(\?|&)livereload=(\d+)/,function(e,n){return""+n+t});if(r===o){if(o.length===0){r="?"+t}else{r=""+o+"&"+t}}return e+r+n};return t}()}).call(this)},{}],8:[function(e,t,n){(function(){var t,n,o;t=e("./customevents");n=window.LiveReload=new(e("./livereload").LiveReload)(window);for(o in window){if(o.match(/^LiveReloadPlugin/)){n.addPlugin(window[o])}}n.addPlugin(e("./less"));n.on("shutdown",function(){return delete window.LiveReload});n.on("connect",function(){return t.fire(document,"LiveReloadConnect")});n.on("disconnect",function(){return t.fire(document,"LiveReloadDisconnect")});t.bind(document,"LiveReloadShutDown",function(){return n.shutDown()})}).call(this)},{"./customevents":2,"./less":3,"./livereload":4}],9:[function(e,t,n){(function(){var e;n.Timer=e=function(){function e(e){this.func=e;this.running=false;this.id=null;this._handler=function(e){return function(){e.running=false;e.id=null;return e.func()}}(this)}e.prototype.start=function(e){if(this.running){clearTimeout(this.id)}this.id=setTimeout(this._handler,e);return this.running=true};e.prototype.stop=function(){if(this.running){clearTimeout(this.id);this.running=false;return this.id=null}};return e}();e.start=function(e,t){return setTimeout(t,e)}}).call(this)},{}]},{},[8]);`) diff --git a/main.go b/main.go new file mode 100644 index 000000000..b408196fc --- /dev/null +++ b/main.go @@ -0,0 +1,38 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "runtime" + + "os" + + "github.com/gohugoio/hugo/commands" + jww "github.com/spf13/jwalterweatherman" +) + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) + commands.Execute() + + if jww.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError) > 0 { + os.Exit(-1) + } + + if commands.Hugo != nil { + if commands.Hugo.Log.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError) > 0 { + os.Exit(-1) + } + } +} diff --git a/media/docshelper.go b/media/docshelper.go new file mode 100644 index 000000000..f5afb52f0 --- /dev/null +++ b/media/docshelper.go @@ -0,0 +1,17 @@ +package media + +import ( + "github.com/gohugoio/hugo/docshelper" +) + +// This is is just some helpers used to create some JSON used in the Hugo docs. +func init() { + docsProvider := func() map[string]interface{} { + docs := make(map[string]interface{}) + + docs["types"] = DefaultTypes + return docs + } + + docshelper.AddDocProvider("media", docsProvider) +} diff --git a/media/mediaType.go b/media/mediaType.go new file mode 100644 index 000000000..2f238ba23 --- /dev/null +++ b/media/mediaType.go @@ -0,0 +1,203 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package media + +import ( + "encoding/json" + "fmt" + "sort" + "strings" + + "github.com/mitchellh/mapstructure" +) + +const ( + defaultDelimiter = "." +) + +// A media type (also known as MIME type and content type) is a two-part identifier for +// file formats and format contents transmitted on the Internet. +// For Hugo's use case, we use the top-level type name / subtype name + suffix. +// One example would be image/jpeg+jpg +// If suffix is not provided, the sub type will be used. +// See // https://en.wikipedia.org/wiki/Media_type +type Type struct { + MainType string // i.e. text + SubType string // i.e. html + Suffix string // i.e html + Delimiter string // defaults to "." +} + +// FromTypeString creates a new Type given a type sring on the form MainType/SubType and +// an optional suffix, e.g. "text/html" or "text/html+html". +func FromString(t string) (Type, error) { + t = strings.ToLower(t) + parts := strings.Split(t, "/") + if len(parts) != 2 { + return Type{}, fmt.Errorf("cannot parse %q as a media type", t) + } + mainType := parts[0] + subParts := strings.Split(parts[1], "+") + + subType := subParts[0] + var suffix string + + if len(subParts) == 1 { + suffix = subType + } else { + suffix = subParts[1] + } + + return Type{MainType: mainType, SubType: subType, Suffix: suffix, Delimiter: defaultDelimiter}, nil +} + +// Type returns a string representing the main- and sub-type of a media type, i.e. "text/css". +// Hugo will register a set of default media types. +// These can be overridden by the user in the configuration, +// by defining a media type with the same Type. +func (m Type) Type() string { + return fmt.Sprintf("%s/%s", m.MainType, m.SubType) +} + +func (m Type) String() string { + if m.Suffix != "" { + return fmt.Sprintf("%s/%s+%s", m.MainType, m.SubType, m.Suffix) + } + return fmt.Sprintf("%s/%s", m.MainType, m.SubType) +} + +// FullSuffix returns the file suffix with any delimiter prepended. +func (m Type) FullSuffix() string { + return m.Delimiter + m.Suffix +} + +var ( + CalendarType = Type{"text", "calendar", "ics", defaultDelimiter} + CSSType = Type{"text", "css", "css", defaultDelimiter} + CSVType = Type{"text", "csv", "csv", defaultDelimiter} + HTMLType = Type{"text", "html", "html", defaultDelimiter} + JavascriptType = Type{"application", "javascript", "js", defaultDelimiter} + JSONType = Type{"application", "json", "json", defaultDelimiter} + RSSType = Type{"application", "rss", "xml", defaultDelimiter} + XMLType = Type{"application", "xml", "xml", defaultDelimiter} + TextType = Type{"text", "plain", "txt", defaultDelimiter} +) + +var DefaultTypes = Types{ + CalendarType, + CSSType, + CSVType, + HTMLType, + JavascriptType, + JSONType, + RSSType, + XMLType, + TextType, +} + +func init() { + sort.Sort(DefaultTypes) +} + +type Types []Type + +func (t Types) Len() int { return len(t) } +func (t Types) Swap(i, j int) { t[i], t[j] = t[j], t[i] } +func (t Types) Less(i, j int) bool { return t[i].Type() < t[j].Type() } + +func (t Types) GetByType(tp string) (Type, bool) { + for _, tt := range t { + if strings.EqualFold(tt.Type(), tp) { + return tt, true + } + } + return Type{}, false +} + +// GetBySuffix gets a media type given as suffix, e.g. "html". +// It will return false if no format could be found, or if the suffix given +// is ambiguous. +// The lookup is case insensitive. +func (t Types) GetBySuffix(suffix string) (tp Type, found bool) { + for _, tt := range t { + if strings.EqualFold(suffix, tt.Suffix) { + if found { + // ambiguous + found = false + return + } + tp = tt + found = true + } + } + return +} + +// DecodeTypes takes a list of media type configurations and merges those, +// in the order given, with the Hugo defaults as the last resort. +func DecodeTypes(maps ...map[string]interface{}) (Types, error) { + m := make(Types, len(DefaultTypes)) + copy(m, DefaultTypes) + + for _, mm := range maps { + for k, v := range mm { + // It may be tempting to put the full media type in the key, e.g. + // "text/css+css", but that will break the logic below. + if strings.Contains(k, "+") { + return Types{}, fmt.Errorf("media type keys cannot contain any '+' chars. Valid example is %q", "text/css") + } + + found := false + for i, vv := range m { + // Match by type, i.e. "text/css" + if strings.EqualFold(k, vv.Type()) { + // Merge it with the existing + if err := mapstructure.WeakDecode(v, &m[i]); err != nil { + return m, err + } + found = true + } + } + if !found { + mediaType, err := FromString(k) + if err != nil { + return m, err + } + + if err := mapstructure.WeakDecode(v, &mediaType); err != nil { + return m, err + } + + m = append(m, mediaType) + } + } + } + + sort.Sort(m) + + return m, nil +} + +func (t Type) MarshalJSON() ([]byte, error) { + type Alias Type + return json.Marshal(&struct { + Type string + String string + Alias + }{ + Type: t.Type(), + String: t.String(), + Alias: (Alias)(t), + }) +} diff --git a/media/mediaType_test.go b/media/mediaType_test.go new file mode 100644 index 000000000..a6b18d1d6 --- /dev/null +++ b/media/mediaType_test.go @@ -0,0 +1,139 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package media + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDefaultTypes(t *testing.T) { + for _, test := range []struct { + tp Type + expectedMainType string + expectedSubType string + expectedSuffix string + expectedType string + expectedString string + }{ + {CalendarType, "text", "calendar", "ics", "text/calendar", "text/calendar+ics"}, + {CSSType, "text", "css", "css", "text/css", "text/css+css"}, + {CSVType, "text", "csv", "csv", "text/csv", "text/csv+csv"}, + {HTMLType, "text", "html", "html", "text/html", "text/html+html"}, + {JavascriptType, "application", "javascript", "js", "application/javascript", "application/javascript+js"}, + {JSONType, "application", "json", "json", "application/json", "application/json+json"}, + {RSSType, "application", "rss", "xml", "application/rss", "application/rss+xml"}, + {TextType, "text", "plain", "txt", "text/plain", "text/plain+txt"}, + } { + require.Equal(t, test.expectedMainType, test.tp.MainType) + require.Equal(t, test.expectedSubType, test.tp.SubType) + require.Equal(t, test.expectedSuffix, test.tp.Suffix) + require.Equal(t, defaultDelimiter, test.tp.Delimiter) + + require.Equal(t, test.expectedType, test.tp.Type()) + require.Equal(t, test.expectedString, test.tp.String()) + + } + +} + +func TestGetByType(t *testing.T) { + types := Types{HTMLType, RSSType} + + mt, found := types.GetByType("text/HTML") + require.True(t, found) + require.Equal(t, mt, HTMLType) + + _, found = types.GetByType("text/nono") + require.False(t, found) +} + +func TestFromTypeString(t *testing.T) { + f, err := FromString("text/html") + require.NoError(t, err) + require.Equal(t, HTMLType, f) + + f, err = FromString("application/custom") + require.NoError(t, err) + require.Equal(t, Type{MainType: "application", SubType: "custom", Suffix: "custom", Delimiter: defaultDelimiter}, f) + + f, err = FromString("application/custom+pdf") + require.NoError(t, err) + require.Equal(t, Type{MainType: "application", SubType: "custom", Suffix: "pdf", Delimiter: defaultDelimiter}, f) + + f, err = FromString("noslash") + require.Error(t, err) + +} + +func TestDecodeTypes(t *testing.T) { + + var tests = []struct { + name string + maps []map[string]interface{} + shouldError bool + assert func(t *testing.T, name string, tt Types) + }{ + { + "Redefine JSON", + []map[string]interface{}{ + map[string]interface{}{ + "application/json": map[string]interface{}{ + "suffix": "jsn"}}}, + false, + func(t *testing.T, name string, tt Types) { + require.Len(t, tt, len(DefaultTypes)) + json, found := tt.GetBySuffix("jsn") + require.True(t, found) + require.Equal(t, "application/json+jsn", json.String(), name) + }}, + { + "Add custom media type", + []map[string]interface{}{ + map[string]interface{}{ + "text/hugo": map[string]interface{}{ + "suffix": "hgo"}}}, + false, + func(t *testing.T, name string, tt Types) { + require.Len(t, tt, len(DefaultTypes)+1) + // Make sure we have not broken the default config. + _, found := tt.GetBySuffix("json") + require.True(t, found) + + hugo, found := tt.GetBySuffix("hgo") + require.True(t, found) + require.Equal(t, "text/hugo+hgo", hugo.String(), name) + }}, + { + "Add media type invalid key", + []map[string]interface{}{ + map[string]interface{}{ + "text/hugo+hgo": map[string]interface{}{}}}, + true, + func(t *testing.T, name string, tt Types) { + + }}, + } + + for _, test := range tests { + result, err := DecodeTypes(test.maps...) + if test.shouldError { + require.Error(t, err, test.name) + } else { + require.NoError(t, err, test.name) + test.assert(t, test.name, result) + } + } +} diff --git a/output/docshelper.go b/output/docshelper.go new file mode 100644 index 000000000..1df45726c --- /dev/null +++ b/output/docshelper.go @@ -0,0 +1,86 @@ +package output + +import ( + "strings" + + "fmt" + + "github.com/gohugoio/hugo/docshelper" +) + +// This is is just some helpers used to create some JSON used in the Hugo docs. +func init() { + docsProvider := func() map[string]interface{} { + docs := make(map[string]interface{}) + + docs["formats"] = DefaultFormats + docs["layouts"] = createLayoutExamples() + return docs + } + + docshelper.AddDocProvider("output", docsProvider) +} + +func createLayoutExamples() interface{} { + + type Example struct { + Example string + OutputFormat string + Suffix string + Layouts []string `json:"Template Lookup Order"` + } + + var ( + basicExamples []Example + demoLayout = "demolayout" + demoType = "demotype" + ) + + for _, example := range []struct { + name string + d LayoutDescriptor + hasTheme bool + layoutOverride string + f Format + }{ + {`AMP home, with theme "demoTheme".`, LayoutDescriptor{Kind: "home"}, true, "", AMPFormat}, + {"JSON home, no theme.", LayoutDescriptor{Kind: "home"}, false, "", JSONFormat}, + {fmt.Sprintf(`CSV regular, "layout: %s" in front matter.`, demoLayout), LayoutDescriptor{Kind: "page", Layout: demoLayout}, false, "", CSVFormat}, + {fmt.Sprintf(`JSON regular, "type: %s" in front matter.`, demoType), LayoutDescriptor{Kind: "page", Type: demoType}, false, "", JSONFormat}, + {"HTML regular.", LayoutDescriptor{Kind: "page"}, false, "", HTMLFormat}, + {"AMP regular.", LayoutDescriptor{Kind: "page"}, false, "", AMPFormat}, + {"Calendar blog section.", LayoutDescriptor{Kind: "section", Section: "blog"}, false, "", CalendarFormat}, + {"Calendar taxonomy list.", LayoutDescriptor{Kind: "taxonomy", Section: "tag"}, false, "", CalendarFormat}, + {"Calendar taxonomy term.", LayoutDescriptor{Kind: "taxonomyTerm", Section: "tag"}, false, "", CalendarFormat}, + } { + + l := NewLayoutHandler(example.hasTheme) + layouts, _ := l.For(example.d, example.layoutOverride, example.f) + + basicExamples = append(basicExamples, Example{ + Example: example.name, + OutputFormat: example.f.Name, + Suffix: example.f.MediaType.Suffix, + Layouts: makeLayoutsPresentable(layouts)}) + } + + return basicExamples + +} + +func makeLayoutsPresentable(l []string) []string { + var filtered []string + for _, ll := range l { + ll = strings.TrimPrefix(ll, "_text/") + if strings.Contains(ll, "theme/") { + ll = strings.Replace(ll, "theme/", "demoTheme/layouts/", -1) + } else { + ll = "layouts/" + ll + } + if !strings.Contains(ll, "indexes") { + filtered = append(filtered, ll) + } + } + + return filtered +} diff --git a/output/layout.go b/output/layout.go new file mode 100644 index 000000000..cacb92b80 --- /dev/null +++ b/output/layout.go @@ -0,0 +1,268 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package output + +import ( + "fmt" + "path" + "strings" + "sync" +) + +// LayoutDescriptor describes how a layout should be chosen. This is +// typically built from a Page. +type LayoutDescriptor struct { + Type string + Section string + Kind string + Layout string +} + +// Layout calculates the layout template to use to render a given output type. +type LayoutHandler struct { + hasTheme bool + + mu sync.RWMutex + cache map[layoutCacheKey][]string +} + +type layoutCacheKey struct { + d LayoutDescriptor + layoutOverride string + f Format +} + +func NewLayoutHandler(hasTheme bool) *LayoutHandler { + return &LayoutHandler{hasTheme: hasTheme, cache: make(map[layoutCacheKey][]string)} +} + +// RSS: +// Home:"rss.xml", "_default/rss.xml", "_internal/_default/rss.xml" +// Section: "section/" + section + ".rss.xml", "_default/rss.xml", "rss.xml", "_internal/_default/rss.xml" +// Taxonomy "taxonomy/" + singular + ".rss.xml", "_default/rss.xml", "rss.xml", "_internal/_default/rss.xml" +// Tax term: taxonomy/" + singular + ".terms.rss.xml", "_default/rss.xml", "rss.xml", "_internal/_default/rss.xml" + +const ( + + // The RSS templates doesn't map easily into the regular pages. + layoutsRSSHome = `NAME.SUFFIX _default/NAME.SUFFIX _internal/_default/rss.xml` + layoutsRSSSection = `section/SECTION.NAME.SUFFIX _default/NAME.SUFFIX NAME.SUFFIX _internal/_default/rss.xml` + layoutsRSSTaxonomy = `taxonomy/SECTION.NAME.SUFFIX _default/NAME.SUFFIX NAME.SUFFIX _internal/_default/rss.xml` + layoutsRSSTaxonomyTerm = `taxonomy/SECTION.terms.NAME.SUFFIX _default/NAME.SUFFIX NAME.SUFFIX _internal/_default/rss.xml` + + layoutsHome = "index.NAME.SUFFIX index.SUFFIX _default/list.NAME.SUFFIX _default/list.SUFFIX" + layoutsSection = ` +section/SECTION.NAME.SUFFIX section/SECTION.SUFFIX +SECTION/list.NAME.SUFFIX SECTION/list.SUFFIX +_default/section.NAME.SUFFIX _default/section.SUFFIX +_default/list.NAME.SUFFIX _default/list.SUFFIX +indexes/SECTION.NAME.SUFFIX indexes/SECTION.SUFFIX +_default/indexes.NAME.SUFFIX _default/indexes.SUFFIX +` + layoutsTaxonomy = ` +taxonomy/SECTION.NAME.SUFFIX taxonomy/SECTION.SUFFIX +indexes/SECTION.NAME.SUFFIX indexes/SECTION.SUFFIX +_default/taxonomy.NAME.SUFFIX _default/taxonomy.SUFFIX +_default/list.NAME.SUFFIX _default/list.SUFFIX +` + layoutsTaxonomyTerm = ` +taxonomy/SECTION.terms.NAME.SUFFIX taxonomy/SECTION.terms.SUFFIX +_default/terms.NAME.SUFFIX _default/terms.SUFFIX +indexes/indexes.NAME.SUFFIX indexes/indexes.SUFFIX +` +) + +func (l *LayoutHandler) For(d LayoutDescriptor, layoutOverride string, f Format) ([]string, error) { + + // We will get lots of requests for the same layouts, so avoid recalculations. + key := layoutCacheKey{d, layoutOverride, f} + l.mu.RLock() + if cacheVal, found := l.cache[key]; found { + l.mu.RUnlock() + return cacheVal, nil + } + l.mu.RUnlock() + + var layouts []string + + if layoutOverride != "" && d.Kind != "page" { + return layouts, fmt.Errorf("Custom layout (%q) only supported for regular pages, not kind %q", layoutOverride, d.Kind) + } + + layout := d.Layout + + if layoutOverride != "" { + layout = layoutOverride + } + + isRSS := f.Name == RSSFormat.Name + + if d.Kind == "page" { + if isRSS { + return []string{}, nil + } + layouts = regularPageLayouts(d.Type, layout, f) + } else { + if isRSS { + layouts = resolveListTemplate(d, f, + layoutsRSSHome, + layoutsRSSSection, + layoutsRSSTaxonomy, + layoutsRSSTaxonomyTerm) + } else { + layouts = resolveListTemplate(d, f, + layoutsHome, + layoutsSection, + layoutsTaxonomy, + layoutsTaxonomyTerm) + } + } + + if l.hasTheme { + layoutsWithThemeLayouts := []string{} + // First place all non internal templates + for _, t := range layouts { + if !strings.HasPrefix(t, "_internal/") { + layoutsWithThemeLayouts = append(layoutsWithThemeLayouts, t) + } + } + + // Then place theme templates with the same names + for _, t := range layouts { + if !strings.HasPrefix(t, "_internal/") { + layoutsWithThemeLayouts = append(layoutsWithThemeLayouts, "theme/"+t) + } + } + + // Lastly place internal templates + for _, t := range layouts { + if strings.HasPrefix(t, "_internal/") { + layoutsWithThemeLayouts = append(layoutsWithThemeLayouts, t) + } + } + + layouts = layoutsWithThemeLayouts + } + + layouts = prependTextPrefixIfNeeded(f, layouts...) + + l.mu.Lock() + l.cache[key] = layouts + l.mu.Unlock() + + return layouts, nil +} + +func resolveListTemplate(d LayoutDescriptor, f Format, + homeLayouts, + sectionLayouts, + taxonomyLayouts, + taxonomyTermLayouts string) []string { + var layouts []string + + switch d.Kind { + case "home": + layouts = resolveTemplate(homeLayouts, d, f) + case "section": + layouts = resolveTemplate(sectionLayouts, d, f) + case "taxonomy": + layouts = resolveTemplate(taxonomyLayouts, d, f) + case "taxonomyTerm": + layouts = resolveTemplate(taxonomyTermLayouts, d, f) + } + return layouts +} + +func resolveTemplate(templ string, d LayoutDescriptor, f Format) []string { + delim := "." + if f.MediaType.Delimiter == "" { + delim = "" + } + layouts := strings.Fields(replaceKeyValues(templ, + ".SUFFIX", delim+f.MediaType.Suffix, + "NAME", strings.ToLower(f.Name), + "SECTION", d.Section)) + + return filterDotLess(layouts) +} + +func filterDotLess(layouts []string) []string { + var filteredLayouts []string + + for _, l := range layouts { + // This may be constructed, but media types can be suffix-less, but can contain + // a delimiter. + l = strings.TrimSuffix(l, ".") + // If media type has no suffix, we have "index" type of layouts in this list, which + // doesn't make much sense. + if strings.Contains(l, ".") { + filteredLayouts = append(filteredLayouts, l) + } + } + + return filteredLayouts +} + +func prependTextPrefixIfNeeded(f Format, layouts ...string) []string { + if !f.IsPlainText { + return layouts + } + + newLayouts := make([]string, len(layouts)) + + for i, l := range layouts { + newLayouts[i] = "_text/" + l + } + + return newLayouts +} + +func replaceKeyValues(s string, oldNew ...string) string { + replacer := strings.NewReplacer(oldNew...) + return replacer.Replace(s) +} + +func regularPageLayouts(types string, layout string, f Format) []string { + var layouts []string + + if layout == "" { + layout = "single" + } + + delimiter := "." + if f.MediaType.Delimiter == "" { + delimiter = "" + } + + suffix := delimiter + f.MediaType.Suffix + name := strings.ToLower(f.Name) + + if types != "" { + t := strings.Split(types, "/") + + // Add type/layout.html + for i := range t { + search := t[:len(t)-i] + layouts = append(layouts, fmt.Sprintf("%s/%s.%s%s", strings.ToLower(path.Join(search...)), layout, name, suffix)) + layouts = append(layouts, fmt.Sprintf("%s/%s%s", strings.ToLower(path.Join(search...)), layout, suffix)) + + } + } + + // Add _default/layout.html + layouts = append(layouts, fmt.Sprintf("_default/%s.%s%s", layout, name, suffix)) + layouts = append(layouts, fmt.Sprintf("_default/%s%s", layout, suffix)) + + return filterDotLess(layouts) +} diff --git a/output/layout_base.go b/output/layout_base.go new file mode 100644 index 000000000..fc34b08be --- /dev/null +++ b/output/layout_base.go @@ -0,0 +1,213 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package output + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/gohugoio/hugo/helpers" +) + +const baseFileBase = "baseof" + +var ( + aceTemplateInnerMarkers = [][]byte{[]byte("= content")} + goTemplateInnerMarkers = [][]byte{[]byte("{{define"), []byte("{{ define")} +) + +type TemplateNames struct { + // The name used as key in the template map. Note that this will be + // prefixed with "_text/" if it should be parsed with text/template. + Name string + + OverlayFilename string + MasterFilename string +} + +type TemplateLookupDescriptor struct { + // TemplateDir is the project or theme root of the current template. + // This will be the same as WorkingDir for non-theme templates. + TemplateDir string + + // The full path to the site root. + WorkingDir string + + // Main project layout dir, defaults to "layouts" + LayoutDir string + + // The path to the template relative the the base. + // I.e. shortcodes/youtube.html + RelPath string + + // The template name prefix to look for, i.e. "theme". + Prefix string + + // The theme dir if theme active. + ThemeDir string + + // All the output formats in play. This is used to decide if text/template or + // html/template. + OutputFormats Formats + + FileExists func(filename string) (bool, error) + ContainsAny func(filename string, subslices [][]byte) (bool, error) +} + +func CreateTemplateNames(d TemplateLookupDescriptor) (TemplateNames, error) { + + name := filepath.ToSlash(d.RelPath) + + if d.Prefix != "" { + name = strings.Trim(d.Prefix, "/") + "/" + name + } + + var ( + id TemplateNames + + // This is the path to the actual template in process. This may + // be in the theme's or the project's /layouts. + baseLayoutDir = filepath.Join(d.TemplateDir, d.LayoutDir) + fullPath = filepath.Join(baseLayoutDir, d.RelPath) + + // This is always the project's layout dir. + baseWorkLayoutDir = filepath.Join(d.WorkingDir, d.LayoutDir) + + baseThemeLayoutDir string + ) + + if d.ThemeDir != "" { + baseThemeLayoutDir = filepath.Join(d.ThemeDir, "layouts") + } + + // The filename will have a suffix with an optional type indicator. + // Examples: + // index.html + // index.amp.html + // index.json + filename := filepath.Base(d.RelPath) + isPlainText := false + outputFormat, found := d.OutputFormats.FromFilename(filename) + + if found && outputFormat.IsPlainText { + isPlainText = true + } + + var ext, outFormat string + + parts := strings.Split(filename, ".") + if len(parts) > 2 { + outFormat = parts[1] + ext = parts[2] + } else if len(parts) > 1 { + ext = parts[1] + } + + filenameNoSuffix := parts[0] + + id.OverlayFilename = fullPath + id.Name = name + + if isPlainText { + id.Name = "_text/" + id.Name + } + + // Ace and Go templates may have both a base and inner template. + pathDir := filepath.Dir(fullPath) + + if ext == "amber" || strings.HasSuffix(pathDir, "partials") || strings.HasSuffix(pathDir, "shortcodes") { + // No base template support + return id, nil + } + + innerMarkers := goTemplateInnerMarkers + + var baseFilename string + + if outFormat != "" { + baseFilename = fmt.Sprintf("%s.%s.%s", baseFileBase, outFormat, ext) + } else { + baseFilename = fmt.Sprintf("%s.%s", baseFileBase, ext) + } + + if ext == "ace" { + innerMarkers = aceTemplateInnerMarkers + } + + // This may be a view that shouldn't have base template + // Have to look inside it to make sure + needsBase, err := d.ContainsAny(fullPath, innerMarkers) + if err != nil { + return id, err + } + + if needsBase { + currBaseFilename := fmt.Sprintf("%s-%s", filenameNoSuffix, baseFilename) + + templateDir := filepath.Dir(fullPath) + + // Find the base, e.g. "_default". + baseTemplatedDir := strings.TrimPrefix(templateDir, baseLayoutDir) + baseTemplatedDir = strings.TrimPrefix(baseTemplatedDir, helpers.FilePathSeparator) + + // Look for base template in the follwing order: + // 1. <current-path>/<template-name>-baseof.<outputFormat>(optional).<suffix>, e.g. list-baseof.<outputFormat>(optional).<suffix>. + // 2. <current-path>/baseof.<outputFormat>(optional).<suffix> + // 3. _default/<template-name>-baseof.<outputFormat>(optional).<suffix>, e.g. list-baseof.<outputFormat>(optional).<suffix>. + // 4. _default/baseof.<outputFormat>(optional).<suffix> + // For each of the steps above, it will first look in the project, then, if theme is set, + // in the theme's layouts folder. + // Also note that the <current-path> may be both the project's layout folder and the theme's. + pairsToCheck := [][]string{ + {baseTemplatedDir, currBaseFilename}, + {baseTemplatedDir, baseFilename}, + {"_default", currBaseFilename}, + {"_default", baseFilename}, + } + + Loop: + for _, pair := range pairsToCheck { + pathsToCheck := basePathsToCheck(pair, baseLayoutDir, baseWorkLayoutDir, baseThemeLayoutDir) + + for _, pathToCheck := range pathsToCheck { + if ok, err := d.FileExists(pathToCheck); err == nil && ok { + id.MasterFilename = pathToCheck + break Loop + } + } + } + } + + return id, nil + +} + +func basePathsToCheck(path []string, layoutDir, workLayoutDir, themeLayoutDir string) []string { + // workLayoutDir will always be the most specific, so start there. + pathsToCheck := []string{filepath.Join((append([]string{workLayoutDir}, path...))...)} + + if layoutDir != "" && layoutDir != workLayoutDir { + pathsToCheck = append(pathsToCheck, filepath.Join((append([]string{layoutDir}, path...))...)) + } + + // May have a theme + if themeLayoutDir != "" && themeLayoutDir != layoutDir { + pathsToCheck = append(pathsToCheck, filepath.Join((append([]string{themeLayoutDir}, path...))...)) + + } + + return pathsToCheck + +} diff --git a/output/layout_base_test.go b/output/layout_base_test.go new file mode 100644 index 000000000..b78f31352 --- /dev/null +++ b/output/layout_base_test.go @@ -0,0 +1,168 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package output + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLayoutBase(t *testing.T) { + + var ( + workingDir = "/sites/mysite/" + themeDir = "/themes/mytheme/" + layoutBase1 = "layouts" + layoutPath1 = "_default/single.html" + layoutPathAmp = "_default/single.amp.html" + layoutPathJSON = "_default/single.json" + ) + + for _, this := range []struct { + name string + d TemplateLookupDescriptor + needsBase bool + basePathMatchStrings string + expect TemplateNames + }{ + {"No base", TemplateLookupDescriptor{TemplateDir: workingDir, WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPath1}, false, "", + TemplateNames{ + Name: "_default/single.html", + OverlayFilename: "/sites/mysite/layouts/_default/single.html", + }}, + {"Base", TemplateLookupDescriptor{TemplateDir: workingDir, WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPath1}, true, "", + TemplateNames{ + Name: "_default/single.html", + OverlayFilename: "/sites/mysite/layouts/_default/single.html", + MasterFilename: "/sites/mysite/layouts/_default/single-baseof.html", + }}, + {"Base in theme", TemplateLookupDescriptor{TemplateDir: workingDir, WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPath1, ThemeDir: themeDir}, true, + "mytheme/layouts/_default/baseof.html", + TemplateNames{ + Name: "_default/single.html", + OverlayFilename: "/sites/mysite/layouts/_default/single.html", + MasterFilename: "/themes/mytheme/layouts/_default/baseof.html", + }}, + {"Template in theme, base in theme", TemplateLookupDescriptor{TemplateDir: themeDir, WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPath1, ThemeDir: themeDir}, true, + "mytheme/layouts/_default/baseof.html", + TemplateNames{ + Name: "_default/single.html", + OverlayFilename: "/themes/mytheme/layouts/_default/single.html", + MasterFilename: "/themes/mytheme/layouts/_default/baseof.html", + }}, + {"Template in theme, base in site", TemplateLookupDescriptor{TemplateDir: themeDir, WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPath1, ThemeDir: themeDir}, true, + "/sites/mysite/layouts/_default/baseof.html", + TemplateNames{ + Name: "_default/single.html", + OverlayFilename: "/themes/mytheme/layouts/_default/single.html", + MasterFilename: "/sites/mysite/layouts/_default/baseof.html", + }}, + {"Template in site, base in theme", TemplateLookupDescriptor{TemplateDir: workingDir, WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPath1, ThemeDir: themeDir}, true, + "/themes/mytheme", + TemplateNames{ + Name: "_default/single.html", + OverlayFilename: "/sites/mysite/layouts/_default/single.html", + MasterFilename: "/themes/mytheme/layouts/_default/single-baseof.html", + }}, + {"With prefix, base in theme", TemplateLookupDescriptor{TemplateDir: workingDir, WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPath1, + ThemeDir: themeDir, Prefix: "someprefix"}, true, + "mytheme/layouts/_default/baseof.html", + TemplateNames{ + Name: "someprefix/_default/single.html", + OverlayFilename: "/sites/mysite/layouts/_default/single.html", + MasterFilename: "/themes/mytheme/layouts/_default/baseof.html", + }}, + {"Partial", TemplateLookupDescriptor{TemplateDir: workingDir, WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: "partials/menu.html"}, true, + "mytheme/layouts/_default/baseof.html", + TemplateNames{ + Name: "partials/menu.html", + OverlayFilename: "/sites/mysite/layouts/partials/menu.html", + }}, + {"AMP, no base", TemplateLookupDescriptor{TemplateDir: workingDir, WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPathAmp}, false, "", + TemplateNames{ + Name: "_default/single.amp.html", + OverlayFilename: "/sites/mysite/layouts/_default/single.amp.html", + }}, + {"JSON, no base", TemplateLookupDescriptor{TemplateDir: workingDir, WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPathJSON}, false, "", + TemplateNames{ + Name: "_default/single.json", + OverlayFilename: "/sites/mysite/layouts/_default/single.json", + }}, + {"AMP with base", TemplateLookupDescriptor{TemplateDir: workingDir, WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPathAmp}, true, "single-baseof.html|single-baseof.amp.html", + TemplateNames{ + Name: "_default/single.amp.html", + OverlayFilename: "/sites/mysite/layouts/_default/single.amp.html", + MasterFilename: "/sites/mysite/layouts/_default/single-baseof.amp.html", + }}, + {"AMP with no match in base", TemplateLookupDescriptor{TemplateDir: workingDir, WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPathAmp}, true, "single-baseof.html", + TemplateNames{ + Name: "_default/single.amp.html", + OverlayFilename: "/sites/mysite/layouts/_default/single.amp.html", + // There is a single-baseof.html, but that makes no sense. + MasterFilename: "", + }}, + + {"JSON with base", TemplateLookupDescriptor{TemplateDir: workingDir, WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPathJSON}, true, "single-baseof.json", + TemplateNames{ + Name: "_default/single.json", + OverlayFilename: "/sites/mysite/layouts/_default/single.json", + MasterFilename: "/sites/mysite/layouts/_default/single-baseof.json", + }}, + } { + t.Run(this.name, func(t *testing.T) { + + this.basePathMatchStrings = filepath.FromSlash(this.basePathMatchStrings) + + fileExists := func(filename string) (bool, error) { + stringsToMatch := strings.Split(this.basePathMatchStrings, "|") + for _, s := range stringsToMatch { + if strings.Contains(filename, s) { + return true, nil + } + + } + return false, nil + } + + needsBase := func(filename string, subslices [][]byte) (bool, error) { + return this.needsBase, nil + } + + this.d.OutputFormats = Formats{AMPFormat, HTMLFormat, RSSFormat, JSONFormat} + this.d.WorkingDir = filepath.FromSlash(this.d.WorkingDir) + this.d.LayoutDir = filepath.FromSlash(this.d.LayoutDir) + this.d.RelPath = filepath.FromSlash(this.d.RelPath) + this.d.ContainsAny = needsBase + this.d.FileExists = fileExists + + this.expect.MasterFilename = filepath.FromSlash(this.expect.MasterFilename) + this.expect.OverlayFilename = filepath.FromSlash(this.expect.OverlayFilename) + + if strings.Contains(this.d.RelPath, "json") { + // currently the only plain text templates in this test. + this.expect.Name = "_text/" + this.expect.Name + } + + id, err := CreateTemplateNames(this.d) + + require.NoError(t, err) + require.Equal(t, this.expect, id, this.name) + + }) + } + +} diff --git a/output/layout_test.go b/output/layout_test.go new file mode 100644 index 000000000..9d4d2f6d5 --- /dev/null +++ b/output/layout_test.go @@ -0,0 +1,132 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package output + +import ( + "testing" + + "github.com/gohugoio/hugo/media" + + "github.com/stretchr/testify/require" +) + +func TestLayout(t *testing.T) { + + noExtNoDelimMediaType := media.TextType + noExtNoDelimMediaType.Suffix = "" + noExtNoDelimMediaType.Delimiter = "" + + noExtMediaType := media.TextType + noExtMediaType.Suffix = "" + + var ( + ampType = Format{ + Name: "AMP", + MediaType: media.HTMLType, + BaseName: "index", + } + + noExtDelimFormat = Format{ + Name: "NEM", + MediaType: noExtNoDelimMediaType, + BaseName: "_redirects", + } + noExt = Format{ + Name: "NEX", + MediaType: noExtMediaType, + BaseName: "next", + } + ) + + for _, this := range []struct { + name string + d LayoutDescriptor + hasTheme bool + layoutOverride string + tp Format + expect []string + }{ + {"Home", LayoutDescriptor{Kind: "home"}, true, "", ampType, + []string{"index.amp.html", "index.html", "_default/list.amp.html", "_default/list.html", "theme/index.amp.html", "theme/index.html"}}, + {"Home, no ext or delim", LayoutDescriptor{Kind: "home"}, true, "", noExtDelimFormat, + []string{"index.nem", "_default/list.nem"}}, + {"Home, no ext", LayoutDescriptor{Kind: "home"}, true, "", noExt, + []string{"index.nex", "_default/list.nex"}}, + {"Page, no ext or delim", LayoutDescriptor{Kind: "page"}, true, "", noExtDelimFormat, + []string{"_default/single.nem", "theme/_default/single.nem"}}, + {"Section", LayoutDescriptor{Kind: "section", Section: "sect1"}, false, "", ampType, + []string{"section/sect1.amp.html", "section/sect1.html"}}, + {"Taxonomy", LayoutDescriptor{Kind: "taxonomy", Section: "tag"}, false, "", ampType, + []string{"taxonomy/tag.amp.html", "taxonomy/tag.html"}}, + {"Taxonomy term", LayoutDescriptor{Kind: "taxonomyTerm", Section: "categories"}, false, "", ampType, + []string{"taxonomy/categories.terms.amp.html", "taxonomy/categories.terms.html", "_default/terms.amp.html"}}, + {"Page", LayoutDescriptor{Kind: "page"}, true, "", ampType, + []string{"_default/single.amp.html", "_default/single.html", "theme/_default/single.amp.html"}}, + {"Page with layout", LayoutDescriptor{Kind: "page", Layout: "mylayout"}, false, "", ampType, + []string{"_default/mylayout.amp.html", "_default/mylayout.html"}}, + {"Page with layout and type", LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype"}, false, "", ampType, + []string{"myttype/mylayout.amp.html", "myttype/mylayout.html", "_default/mylayout.amp.html"}}, + {"Page with layout and type with subtype", LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype/mysubtype"}, false, "", ampType, + []string{"myttype/mysubtype/mylayout.amp.html", "myttype/mysubtype/mylayout.html", "myttype/mylayout.amp.html"}}, + {"Page with overridden layout", LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype"}, false, "myotherlayout", ampType, + []string{"myttype/myotherlayout.amp.html", "myttype/myotherlayout.html"}}, + // RSS + {"RSS Home with theme", LayoutDescriptor{Kind: "home"}, true, "", RSSFormat, + []string{"rss.xml", "_default/rss.xml", "theme/rss.xml", "theme/_default/rss.xml", "_internal/_default/rss.xml"}}, + {"RSS Section", LayoutDescriptor{Kind: "section", Section: "sect1"}, false, "", RSSFormat, + []string{"section/sect1.rss.xml", "_default/rss.xml", "rss.xml", "_internal/_default/rss.xml"}}, + {"RSS Taxonomy", LayoutDescriptor{Kind: "taxonomy", Section: "tag"}, false, "", RSSFormat, + []string{"taxonomy/tag.rss.xml", "_default/rss.xml", "rss.xml", "_internal/_default/rss.xml"}}, + {"RSS Taxonomy term", LayoutDescriptor{Kind: "taxonomyTerm", Section: "tag"}, false, "", RSSFormat, + []string{"taxonomy/tag.terms.rss.xml", "_default/rss.xml", "rss.xml", "_internal/_default/rss.xml"}}, + {"Home plain text", LayoutDescriptor{Kind: "home"}, true, "", JSONFormat, + []string{"_text/index.json.json", "_text/index.json", "_text/_default/list.json.json", "_text/_default/list.json", "_text/theme/index.json.json", "_text/theme/index.json"}}, + {"Page plain text", LayoutDescriptor{Kind: "page"}, true, "", JSONFormat, + []string{"_text/_default/single.json.json", "_text/_default/single.json", "_text/theme/_default/single.json.json"}}, + } { + t.Run(this.name, func(t *testing.T) { + l := NewLayoutHandler(this.hasTheme) + + layouts, err := l.For(this.d, this.layoutOverride, this.tp) + + require.NoError(t, err) + require.NotNil(t, layouts) + require.True(t, len(layouts) >= len(this.expect)) + // Not checking the complete list for now ... + require.Equal(t, this.expect, layouts[:len(this.expect)]) + + if !this.hasTheme { + for _, layout := range layouts { + require.NotContains(t, layout, "theme") + } + } + }) + } + + l := NewLayoutHandler(false) + _, err := l.For(LayoutDescriptor{Kind: "taxonomyTerm", Section: "tag"}, "override", RSSFormat) + require.Error(t, err) + +} + +func BenchmarkLayout(b *testing.B) { + descriptor := LayoutDescriptor{Kind: "taxonomyTerm", Section: "categories"} + l := NewLayoutHandler(false) + + for i := 0; i < b.N; i++ { + layouts, err := l.For(descriptor, "", HTMLFormat) + require.NoError(b, err) + require.NotEmpty(b, layouts) + } +} diff --git a/output/outputFormat.go b/output/outputFormat.go new file mode 100644 index 000000000..2b75120f5 --- /dev/null +++ b/output/outputFormat.go @@ -0,0 +1,328 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package output + +import ( + "encoding/json" + "fmt" + "sort" + "strings" + + "reflect" + + "github.com/mitchellh/mapstructure" + + "github.com/gohugoio/hugo/media" +) + +// Format represents an output representation, usually to a file on disk. +type Format struct { + // The Name is used as an identifier. Internal output formats (i.e. HTML and RSS) + // can be overridden by providing a new definition for those types. + Name string + + MediaType media.Type + + // Must be set to a value when there are two or more conflicting mediatype for the same resource. + Path string + + // The base output file name used when not using "ugly URLs", defaults to "index". + BaseName string + + // The value to use for rel links + // + // See https://www.w3schools.com/tags/att_link_rel.asp + // + // AMP has a special requirement in this department, see: + // https://www.ampproject.org/docs/guides/deploy/discovery + // I.e.: + // <link rel="amphtml" href="https://www.example.com/url/to/amp/document.html"> + Rel string + + // The protocol to use, i.e. "webcal://". Defaults to the protocol of the baseURL. + Protocol string + + // IsPlainText decides whether to use text/template or html/template + // as template parser. + IsPlainText bool + + // IsHTML returns whether this format is int the HTML family. This includes + // HTML, AMP etc. This is used to decide when to create alias redirects etc. + IsHTML bool + + // Enable to ignore the global uglyURLs setting. + NoUgly bool + + // Enable if it doesn't make sense to include this format in an alternative + // format listing, CSS being one good example. + // Note that we use the term "alternative" and not "alternate" here, as it + // does not necessarily replace the other format, it is an alternative representation. + NotAlternative bool +} + +var ( + // An ordered list of built-in output formats + // + // See https://www.ampproject.org/learn/overview/ + AMPFormat = Format{ + Name: "AMP", + MediaType: media.HTMLType, + BaseName: "index", + Path: "amp", + Rel: "amphtml", + IsHTML: true, + } + + CalendarFormat = Format{ + Name: "Calendar", + MediaType: media.CalendarType, + IsPlainText: true, + Protocol: "webcal://", + BaseName: "index", + Rel: "alternate", + } + + CSSFormat = Format{ + Name: "CSS", + MediaType: media.CSSType, + BaseName: "styles", + IsPlainText: true, + Rel: "stylesheet", + NotAlternative: true, + } + CSVFormat = Format{ + Name: "CSV", + MediaType: media.CSVType, + BaseName: "index", + IsPlainText: true, + Rel: "alternate", + } + + HTMLFormat = Format{ + Name: "HTML", + MediaType: media.HTMLType, + BaseName: "index", + Rel: "canonical", + IsHTML: true, + } + + JSONFormat = Format{ + Name: "JSON", + MediaType: media.JSONType, + BaseName: "index", + IsPlainText: true, + Rel: "alternate", + } + + RSSFormat = Format{ + Name: "RSS", + MediaType: media.RSSType, + BaseName: "index", + NoUgly: true, + Rel: "alternate", + } +) + +var DefaultFormats = Formats{ + AMPFormat, + CalendarFormat, + CSSFormat, + CSVFormat, + HTMLFormat, + JSONFormat, + RSSFormat, +} + +func init() { + sort.Sort(DefaultFormats) +} + +type Formats []Format + +func (f Formats) Len() int { return len(f) } +func (f Formats) Swap(i, j int) { f[i], f[j] = f[j], f[i] } +func (f Formats) Less(i, j int) bool { return f[i].Name < f[j].Name } + +// GetBySuffix gets a output format given as suffix, e.g. "html". +// It will return false if no format could be found, or if the suffix given +// is ambiguous. +// The lookup is case insensitive. +func (formats Formats) GetBySuffix(suffix string) (f Format, found bool) { + for _, ff := range formats { + if strings.EqualFold(suffix, ff.MediaType.Suffix) { + if found { + // ambiguous + found = false + return + } + f = ff + found = true + } + } + return +} + +// GetByName gets a format by its identifier name. +func (formats Formats) GetByName(name string) (f Format, found bool) { + for _, ff := range formats { + if strings.EqualFold(name, ff.Name) { + f = ff + found = true + return + } + } + return +} + +// GetByNames gets a list of formats given a list of identifiers. +func (formats Formats) GetByNames(names ...string) (Formats, error) { + var types []Format + + for _, name := range names { + tpe, ok := formats.GetByName(name) + if !ok { + return types, fmt.Errorf("OutputFormat with key %q not found", name) + } + types = append(types, tpe) + } + return types, nil +} + +// FromFilename gets a Format given a filename. +func (formats Formats) FromFilename(filename string) (f Format, found bool) { + // mytemplate.amp.html + // mytemplate.html + // mytemplate + var ext, outFormat string + + parts := strings.Split(filename, ".") + if len(parts) > 2 { + outFormat = parts[1] + ext = parts[2] + } else if len(parts) > 1 { + ext = parts[1] + } + + if outFormat != "" { + return formats.GetByName(outFormat) + } + + if ext != "" { + f, found = formats.GetBySuffix(ext) + if !found && len(parts) == 2 { + // For extensionless output formats (e.g. Netlify's _redirects) + // we must fall back to using the extension as format lookup. + f, found = formats.GetByName(ext) + } + } + return +} + +// DecodeFormats takes a list of output format configurations and merges those, +// in the order given, with the Hugo defaults as the last resort. +func DecodeFormats(mediaTypes media.Types, maps ...map[string]interface{}) (Formats, error) { + f := make(Formats, len(DefaultFormats)) + copy(f, DefaultFormats) + + for _, m := range maps { + for k, v := range m { + found := false + for i, vv := range f { + if strings.EqualFold(k, vv.Name) { + // Merge it with the existing + if err := decode(mediaTypes, v, &f[i]); err != nil { + return f, err + } + found = true + } + } + if !found { + var newOutFormat Format + newOutFormat.Name = k + if err := decode(mediaTypes, v, &newOutFormat); err != nil { + return f, err + } + + // We need values for these + if newOutFormat.BaseName == "" { + newOutFormat.BaseName = "index" + } + if newOutFormat.Rel == "" { + newOutFormat.Rel = "alternate" + } + + f = append(f, newOutFormat) + } + } + } + + sort.Sort(f) + + return f, nil +} + +func decode(mediaTypes media.Types, input, output interface{}) error { + config := &mapstructure.DecoderConfig{ + Metadata: nil, + Result: output, + WeaklyTypedInput: true, + DecodeHook: func(a reflect.Type, b reflect.Type, c interface{}) (interface{}, error) { + if a.Kind() == reflect.Map { + dataVal := reflect.Indirect(reflect.ValueOf(c)) + for _, key := range dataVal.MapKeys() { + keyStr, ok := key.Interface().(string) + if !ok { + // Not a string key + continue + } + if strings.EqualFold(keyStr, "mediaType") { + // If mediaType is a string, look it up and replace it + // in the map. + vv := dataVal.MapIndex(key) + if mediaTypeStr, ok := vv.Interface().(string); ok { + mediaType, found := mediaTypes.GetByType(mediaTypeStr) + if !found { + return c, fmt.Errorf("media type %q not found", mediaTypeStr) + } + dataVal.SetMapIndex(key, reflect.ValueOf(mediaType)) + } + } + } + } + return c, nil + }, + } + + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return err + } + + return decoder.Decode(input) +} + +func (f Format) BaseFilename() string { + return f.BaseName + "." + f.MediaType.Suffix +} + +func (f Format) MarshalJSON() ([]byte, error) { + type Alias Format + return json.Marshal(&struct { + MediaType string + Alias + }{ + MediaType: f.MediaType.String(), + Alias: (Alias)(f), + }) +} diff --git a/output/outputFormat_test.go b/output/outputFormat_test.go new file mode 100644 index 000000000..18b84a0fa --- /dev/null +++ b/output/outputFormat_test.go @@ -0,0 +1,226 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package output + +import ( + "fmt" + "testing" + + "github.com/gohugoio/hugo/media" + "github.com/stretchr/testify/require" +) + +func TestDefaultTypes(t *testing.T) { + require.Equal(t, "Calendar", CalendarFormat.Name) + require.Equal(t, media.CalendarType, CalendarFormat.MediaType) + require.Equal(t, "webcal://", CalendarFormat.Protocol) + require.Empty(t, CalendarFormat.Path) + require.True(t, CalendarFormat.IsPlainText) + require.False(t, CalendarFormat.IsHTML) + + require.Equal(t, "CSS", CSSFormat.Name) + require.Equal(t, media.CSSType, CSSFormat.MediaType) + require.Empty(t, CSSFormat.Path) + require.Empty(t, CSSFormat.Protocol) // Will inherit the BaseURL protocol. + require.True(t, CSSFormat.IsPlainText) + require.False(t, CSSFormat.IsHTML) + + require.Equal(t, "CSV", CSVFormat.Name) + require.Equal(t, media.CSVType, CSVFormat.MediaType) + require.Empty(t, CSVFormat.Path) + require.Empty(t, CSVFormat.Protocol) + require.True(t, CSVFormat.IsPlainText) + require.False(t, CSVFormat.IsHTML) + + require.Equal(t, "HTML", HTMLFormat.Name) + require.Equal(t, media.HTMLType, HTMLFormat.MediaType) + require.Empty(t, HTMLFormat.Path) + require.Empty(t, HTMLFormat.Protocol) + require.False(t, HTMLFormat.IsPlainText) + require.True(t, HTMLFormat.IsHTML) + + require.Equal(t, "AMP", AMPFormat.Name) + require.Equal(t, media.HTMLType, AMPFormat.MediaType) + require.Equal(t, "amp", AMPFormat.Path) + require.Empty(t, AMPFormat.Protocol) + require.False(t, AMPFormat.IsPlainText) + require.True(t, AMPFormat.IsHTML) + + require.Equal(t, "RSS", RSSFormat.Name) + require.Equal(t, media.RSSType, RSSFormat.MediaType) + require.Empty(t, RSSFormat.Path) + require.False(t, RSSFormat.IsPlainText) + require.True(t, RSSFormat.NoUgly) + require.False(t, CalendarFormat.IsHTML) + +} + +func TestGetFormatByName(t *testing.T) { + formats := Formats{AMPFormat, CalendarFormat} + tp, _ := formats.GetByName("AMp") + require.Equal(t, AMPFormat, tp) + _, found := formats.GetByName("HTML") + require.False(t, found) + _, found = formats.GetByName("FOO") + require.False(t, found) +} + +func TestGetFormatByExt(t *testing.T) { + formats1 := Formats{AMPFormat, CalendarFormat} + formats2 := Formats{AMPFormat, HTMLFormat, CalendarFormat} + tp, _ := formats1.GetBySuffix("html") + require.Equal(t, AMPFormat, tp) + tp, _ = formats1.GetBySuffix("ics") + require.Equal(t, CalendarFormat, tp) + _, found := formats1.GetBySuffix("not") + require.False(t, found) + + // ambiguous + _, found = formats2.GetBySuffix("html") + require.False(t, found) +} + +func TestGetFormatByFilename(t *testing.T) { + noExtNoDelimMediaType := media.TextType + noExtNoDelimMediaType.Suffix = "" + noExtNoDelimMediaType.Delimiter = "" + + noExtMediaType := media.TextType + noExtMediaType.Suffix = "" + + var ( + noExtDelimFormat = Format{ + Name: "NEM", + MediaType: noExtNoDelimMediaType, + BaseName: "_redirects", + } + noExt = Format{ + Name: "NEX", + MediaType: noExtMediaType, + BaseName: "next", + } + ) + + formats := Formats{AMPFormat, HTMLFormat, noExtDelimFormat, noExt, CalendarFormat} + f, found := formats.FromFilename("my.amp.html") + require.True(t, found) + require.Equal(t, AMPFormat, f) + f, found = formats.FromFilename("my.ics") + require.True(t, found) + f, found = formats.FromFilename("my.html") + require.True(t, found) + require.Equal(t, HTMLFormat, f) + f, found = formats.FromFilename("my.nem") + require.True(t, found) + require.Equal(t, noExtDelimFormat, f) + f, found = formats.FromFilename("my.nex") + require.True(t, found) + require.Equal(t, noExt, f) + f, found = formats.FromFilename("my.css") + require.False(t, found) + +} + +func TestDecodeFormats(t *testing.T) { + + mediaTypes := media.Types{media.JSONType, media.XMLType} + + var tests = []struct { + name string + maps []map[string]interface{} + shouldError bool + assert func(t *testing.T, name string, f Formats) + }{ + { + "Redefine JSON", + []map[string]interface{}{ + map[string]interface{}{ + "JsON": map[string]interface{}{ + "baseName": "myindex", + "isPlainText": "false"}}}, + false, + func(t *testing.T, name string, f Formats) { + require.Len(t, f, len(DefaultFormats), name) + json, _ := f.GetByName("JSON") + require.Equal(t, "myindex", json.BaseName) + require.Equal(t, media.JSONType, json.MediaType) + require.False(t, json.IsPlainText) + + }}, + { + "Add XML format with string as mediatype", + []map[string]interface{}{ + map[string]interface{}{ + "MYXMLFORMAT": map[string]interface{}{ + "baseName": "myxml", + "mediaType": "application/xml", + }}}, + false, + func(t *testing.T, name string, f Formats) { + require.Len(t, f, len(DefaultFormats)+1, name) + xml, found := f.GetByName("MYXMLFORMAT") + require.True(t, found) + require.Equal(t, "myxml", xml.BaseName, fmt.Sprint(xml)) + require.Equal(t, media.XMLType, xml.MediaType) + + // Verify that we haven't changed the DefaultFormats slice. + json, _ := f.GetByName("JSON") + require.Equal(t, "index", json.BaseName, name) + + }}, + { + "Add format unknown mediatype", + []map[string]interface{}{ + map[string]interface{}{ + "MYINVALID": map[string]interface{}{ + "baseName": "mymy", + "mediaType": "application/hugo", + }}}, + true, + func(t *testing.T, name string, f Formats) { + + }}, + { + "Add and redefine XML format", + []map[string]interface{}{ + map[string]interface{}{ + "MYOTHERXMLFORMAT": map[string]interface{}{ + "baseName": "myotherxml", + "mediaType": media.XMLType, + }}, + map[string]interface{}{ + "MYOTHERXMLFORMAT": map[string]interface{}{ + "baseName": "myredefined", + }}, + }, + false, + func(t *testing.T, name string, f Formats) { + require.Len(t, f, len(DefaultFormats)+1, name) + xml, found := f.GetByName("MYOTHERXMLFORMAT") + require.True(t, found) + require.Equal(t, "myredefined", xml.BaseName, fmt.Sprint(xml)) + require.Equal(t, media.XMLType, xml.MediaType) + }}, + } + + for _, test := range tests { + result, err := DecodeFormats(mediaTypes, test.maps...) + if test.shouldError { + require.Error(t, err, test.name) + } else { + require.NoError(t, err, test.name) + test.assert(t, test.name, result) + } + } +} diff --git a/parser/frontmatter.go b/parser/frontmatter.go new file mode 100644 index 000000000..ab56b14d1 --- /dev/null +++ b/parser/frontmatter.go @@ -0,0 +1,226 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package parser + +// TODO(bep) archetype remove unused from this package. + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "strings" + + "github.com/BurntSushi/toml" + "github.com/chaseadamsio/goorgeous" + + "gopkg.in/yaml.v2" +) + +// FrontmatterType represents a type of frontmatter. +type FrontmatterType struct { + // Parse decodes content into a Go interface. + Parse func([]byte) (interface{}, error) + + markstart, markend []byte // starting and ending delimiters + includeMark bool // include start and end mark in output +} + +// InterfaceToConfig encodes a given input based upon the mark and writes to w. +func InterfaceToConfig(in interface{}, mark rune, w io.Writer) error { + if in == nil { + return errors.New("input was nil") + } + + switch mark { + case rune(YAMLLead[0]): + b, err := yaml.Marshal(in) + if err != nil { + return err + } + + _, err = w.Write(b) + return err + + case rune(TOMLLead[0]): + return toml.NewEncoder(w).Encode(in) + case rune(JSONLead[0]): + b, err := json.MarshalIndent(in, "", " ") + if err != nil { + return err + } + + _, err = w.Write(b) + if err != nil { + return err + } + + _, err = w.Write([]byte{'\n'}) + return err + + default: + return errors.New("Unsupported Format provided") + } +} + +// InterfaceToFrontMatter encodes a given input into a frontmatter +// representation based upon the mark with the appropriate front matter delimiters +// surrounding the output, which is written to w. +func InterfaceToFrontMatter(in interface{}, mark rune, w io.Writer) error { + if in == nil { + return errors.New("input was nil") + } + + switch mark { + case rune(YAMLLead[0]): + _, err := w.Write([]byte(YAMLDelimUnix)) + if err != nil { + return err + } + + err = InterfaceToConfig(in, mark, w) + if err != nil { + return err + } + + _, err = w.Write([]byte(YAMLDelimUnix)) + return err + + case rune(TOMLLead[0]): + _, err := w.Write([]byte(TOMLDelimUnix)) + if err != nil { + return err + } + + err = InterfaceToConfig(in, mark, w) + + if err != nil { + return err + } + + _, err = w.Write([]byte("\n" + TOMLDelimUnix)) + return err + + default: + return InterfaceToConfig(in, mark, w) + } +} + +// FormatToLeadRune takes a given format kind and return the leading front +// matter delimiter. +func FormatToLeadRune(kind string) rune { + switch FormatSanitize(kind) { + case "yaml": + return rune([]byte(YAMLLead)[0]) + case "json": + return rune([]byte(JSONLead)[0]) + case "org": + return '#' + default: + return rune([]byte(TOMLLead)[0]) + } +} + +// FormatSanitize returns the canonical format name for a given kind. +// +// TODO(bep) move to helpers +func FormatSanitize(kind string) string { + switch strings.ToLower(kind) { + case "yaml", "yml": + return "yaml" + case "toml", "tml": + return "toml" + case "json", "js": + return "json" + case "org": + return kind + default: + return "toml" + } +} + +// DetectFrontMatter detects the type of frontmatter analysing its first character. +func DetectFrontMatter(mark rune) (f *FrontmatterType) { + switch mark { + case '-': + return &FrontmatterType{HandleYAMLMetaData, []byte(YAMLDelim), []byte(YAMLDelim), false} + case '+': + return &FrontmatterType{HandleTOMLMetaData, []byte(TOMLDelim), []byte(TOMLDelim), false} + case '{': + return &FrontmatterType{HandleJSONMetaData, []byte{'{'}, []byte{'}'}, true} + case '#': + return &FrontmatterType{HandleOrgMetaData, []byte("#+"), []byte("\n"), false} + default: + return nil + } +} + +// HandleTOMLMetaData unmarshals TOML-encoded datum and returns a Go interface +// representing the encoded data structure. +func HandleTOMLMetaData(datum []byte) (interface{}, error) { + m := map[string]interface{}{} + datum = removeTOMLIdentifier(datum) + + _, err := toml.Decode(string(datum), &m) + + return m, err + +} + +// removeTOMLIdentifier removes, if necessary, beginning and ending TOML +// frontmatter delimiters from a byte slice. +func removeTOMLIdentifier(datum []byte) []byte { + ld := len(datum) + if ld < 8 { + return datum + } + + b := bytes.TrimPrefix(datum, []byte(TOMLDelim)) + if ld-len(b) != 3 { + // No TOML prefix trimmed, so bail out + return datum + } + + b = bytes.Trim(b, "\r\n") + return bytes.TrimSuffix(b, []byte(TOMLDelim)) +} + +// HandleYAMLMetaData unmarshals YAML-encoded datum and returns a Go interface +// representing the encoded data structure. +func HandleYAMLMetaData(datum []byte) (interface{}, error) { + m := map[string]interface{}{} + err := yaml.Unmarshal(datum, &m) + return m, err +} + +// HandleJSONMetaData unmarshals JSON-encoded datum and returns a Go interface +// representing the encoded data structure. +func HandleJSONMetaData(datum []byte) (interface{}, error) { + if datum == nil { + // Package json returns on error on nil input. + // Return an empty map to be consistent with our other supported + // formats. + return make(map[string]interface{}), nil + } + + var f interface{} + err := json.Unmarshal(datum, &f) + return f, err +} + +// HandleOrgMetaData unmarshals org-mode encoded datum and returns a Go +// interface representing the encoded data structure. +func HandleOrgMetaData(datum []byte) (interface{}, error) { + return goorgeous.OrgHeaders(datum) +} diff --git a/parser/frontmatter_test.go b/parser/frontmatter_test.go new file mode 100644 index 000000000..2e5bdec50 --- /dev/null +++ b/parser/frontmatter_test.go @@ -0,0 +1,398 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package parser + +import ( + "bytes" + "fmt" + "reflect" + "strings" + "testing" +) + +func TestInterfaceToConfig(t *testing.T) { + cases := []struct { + input interface{} + mark byte + want []byte + isErr bool + }{ + // TOML + {map[string]interface{}{}, TOMLLead[0], nil, false}, + { + map[string]interface{}{"title": "test 1"}, + TOMLLead[0], + []byte("title = \"test 1\"\n"), + false, + }, + + // YAML + {map[string]interface{}{}, YAMLLead[0], []byte("{}\n"), false}, + { + map[string]interface{}{"title": "test 1"}, + YAMLLead[0], + []byte("title: test 1\n"), + false, + }, + + // JSON + {map[string]interface{}{}, JSONLead[0], []byte("{}\n"), false}, + { + map[string]interface{}{"title": "test 1"}, + JSONLead[0], + []byte("{\n \"title\": \"test 1\"\n}\n"), + false, + }, + + // Errors + {nil, TOMLLead[0], nil, true}, + {map[string]interface{}{}, '$', nil, true}, + } + + for i, c := range cases { + var buf bytes.Buffer + + err := InterfaceToConfig(c.input, rune(c.mark), &buf) + if err != nil { + if c.isErr { + continue + } + t.Fatalf("[%d] unexpected error value: %v", i, err) + } + + if !reflect.DeepEqual(buf.Bytes(), c.want) { + t.Errorf("[%d] not equal:\nwant %q,\n got %q", i, c.want, buf.Bytes()) + } + } +} + +func TestInterfaceToFrontMatter(t *testing.T) { + cases := []struct { + input interface{} + mark rune + want []byte + isErr bool + }{ + // TOML + {map[string]interface{}{}, '+', []byte("+++\n\n+++\n"), false}, + { + map[string]interface{}{"title": "test 1"}, + '+', + []byte("+++\ntitle = \"test 1\"\n\n+++\n"), + false, + }, + + // YAML + {map[string]interface{}{}, '-', []byte("---\n{}\n---\n"), false}, // + { + map[string]interface{}{"title": "test 1"}, + '-', + []byte("---\ntitle: test 1\n---\n"), + false, + }, + + // JSON + {map[string]interface{}{}, '{', []byte("{}\n"), false}, + { + map[string]interface{}{"title": "test 1"}, + '{', + []byte("{\n \"title\": \"test 1\"\n}\n"), + false, + }, + + // Errors + {nil, '+', nil, true}, + {map[string]interface{}{}, '$', nil, true}, + } + + for i, c := range cases { + var buf bytes.Buffer + err := InterfaceToFrontMatter(c.input, c.mark, &buf) + if err != nil { + if c.isErr { + continue + } + t.Fatalf("[%d] unexpected error value: %v", i, err) + } + + if !reflect.DeepEqual(buf.Bytes(), c.want) { + t.Errorf("[%d] not equal:\nwant %q,\n got %q", i, c.want, buf.Bytes()) + } + } +} + +func TestHandleTOMLMetaData(t *testing.T) { + cases := []struct { + input []byte + want interface{} + isErr bool + }{ + {nil, map[string]interface{}{}, false}, + {[]byte("title = \"test 1\""), map[string]interface{}{"title": "test 1"}, false}, + {[]byte("a = [1, 2, 3]"), map[string]interface{}{"a": []interface{}{int64(1), int64(2), int64(3)}}, false}, + {[]byte("b = [\n[1, 2],\n[3, 4]\n]"), map[string]interface{}{"b": []interface{}{[]interface{}{int64(1), int64(2)}, []interface{}{int64(3), int64(4)}}}, false}, + // errors + {[]byte("z = [\n[1, 2]\n[3, 4]\n]"), nil, true}, + } + + for i, c := range cases { + res, err := HandleTOMLMetaData(c.input) + if err != nil { + if c.isErr { + continue + } + t.Fatalf("[%d] unexpected error value: %v", i, err) + } + + if !reflect.DeepEqual(res, c.want) { + t.Errorf("[%d] not equal: given %q\nwant %#v,\n got %#v", i, c.input, c.want, res) + } + } +} + +func TestHandleYAMLMetaData(t *testing.T) { + cases := []struct { + input []byte + want interface{} + isErr bool + }{ + {nil, map[string]interface{}{}, false}, + {[]byte("title: test 1"), map[string]interface{}{"title": "test 1"}, false}, + {[]byte("a: Easy!\nb:\n c: 2\n d: [3, 4]"), map[string]interface{}{"a": "Easy!", "b": map[interface{}]interface{}{"c": 2, "d": []interface{}{3, 4}}}, false}, + // errors + {[]byte("z = not toml"), nil, true}, + } + + for i, c := range cases { + res, err := HandleYAMLMetaData(c.input) + if err != nil { + if c.isErr { + continue + } + t.Fatalf("[%d] unexpected error value: %v", i, err) + } + + if !reflect.DeepEqual(res, c.want) { + t.Errorf("[%d] not equal: given %q\nwant %#v,\n got %#v", i, c.input, c.want, res) + } + } +} + +func TestHandleJSONMetaData(t *testing.T) { + cases := []struct { + input []byte + want interface{} + isErr bool + }{ + {nil, map[string]interface{}{}, false}, + {[]byte("{\"title\": \"test 1\"}"), map[string]interface{}{"title": "test 1"}, false}, + // errors + {[]byte("{noquotes}"), nil, true}, + } + + for i, c := range cases { + res, err := HandleJSONMetaData(c.input) + if err != nil { + if c.isErr { + continue + } + t.Fatalf("[%d] unexpected error value: %v", i, err) + } + + if !reflect.DeepEqual(res, c.want) { + t.Errorf("[%d] not equal: given %q\nwant %#v,\n got %#v", i, c.input, c.want, res) + } + } +} + +func TestHandleOrgMetaData(t *testing.T) { + cases := []struct { + input []byte + want interface{} + isErr bool + }{ + {nil, map[string]interface{}{}, false}, + {[]byte("#+title: test 1\n"), map[string]interface{}{"title": "test 1"}, false}, + } + + for i, c := range cases { + res, err := HandleOrgMetaData(c.input) + if err != nil { + if c.isErr { + continue + } + t.Fatalf("[%d] unexpected error value: %v", i, err) + } + + if !reflect.DeepEqual(res, c.want) { + t.Errorf("[%d] not equal: given %q\nwant %#v,\n got %#v", i, c.input, c.want, res) + } + } +} + +func TestFormatToLeadRune(t *testing.T) { + for i, this := range []struct { + kind string + expect rune + }{ + {"yaml", '-'}, + {"yml", '-'}, + {"toml", '+'}, + {"tml", '+'}, + {"json", '{'}, + {"js", '{'}, + {"org", '#'}, + {"unknown", '+'}, + } { + result := FormatToLeadRune(this.kind) + + if result != this.expect { + t.Errorf("[%d] got %q but expected %q", i, result, this.expect) + } + } +} + +func TestDetectFrontMatter(t *testing.T) { + cases := []struct { + mark rune + want *FrontmatterType + }{ + // funcs are uncomparable, so we ignore FrontmatterType.Parse in these tests + {'-', &FrontmatterType{nil, []byte(YAMLDelim), []byte(YAMLDelim), false}}, + {'+', &FrontmatterType{nil, []byte(TOMLDelim), []byte(TOMLDelim), false}}, + {'{', &FrontmatterType{nil, []byte("{"), []byte("}"), true}}, + {'#', &FrontmatterType{nil, []byte("#+"), []byte("\n"), false}}, + {'$', nil}, + } + + for _, c := range cases { + res := DetectFrontMatter(c.mark) + if res == nil { + if c.want == nil { + continue + } + + t.Fatalf("want %v, got %v", *c.want, res) + } + + if !reflect.DeepEqual(res.markstart, c.want.markstart) { + t.Errorf("markstart mismatch: want %v, got %v", c.want.markstart, res.markstart) + } + if !reflect.DeepEqual(res.markend, c.want.markend) { + t.Errorf("markend mismatch: want %v, got %v", c.want.markend, res.markend) + } + if !reflect.DeepEqual(res.includeMark, c.want.includeMark) { + t.Errorf("includeMark mismatch: want %v, got %v", c.want.includeMark, res.includeMark) + } + } +} + +func TestRemoveTOMLIdentifier(t *testing.T) { + cases := []struct { + input string + want string + }{ + {"a = 1", "a = 1"}, + {"a = 1\r\n", "a = 1\r\n"}, + {"+++\r\na = 1\r\n+++\r\n", "a = 1\r\n"}, + {"+++\na = 1\n+++\n", "a = 1\n"}, + {"+++\nb = \"+++ oops +++\"\n+++\n", "b = \"+++ oops +++\"\n"}, + {"+++\nc = \"\"\"+++\noops\n+++\n\"\"\"\"\n+++\n", "c = \"\"\"+++\noops\n+++\n\"\"\"\"\n"}, + {"+++\nd = 1\n+++", "d = 1\n"}, + } + + for i, c := range cases { + res := removeTOMLIdentifier([]byte(c.input)) + if string(res) != c.want { + t.Errorf("[%d] given %q\nwant: %q\n got: %q", i, c.input, c.want, res) + } + } +} + +func BenchmarkFrontmatterTags(b *testing.B) { + + for _, frontmatter := range []string{"JSON", "YAML", "YAML2", "TOML"} { + for i := 1; i < 60; i += 20 { + doBenchmarkFrontmatter(b, frontmatter, i) + } + } +} + +func doBenchmarkFrontmatter(b *testing.B, fileformat string, numTags int) { + yamlTemplate := `--- +name: "Tags" +tags: +%s +--- +` + + yaml2Template := `--- +name: "Tags" +tags: %s +--- +` + tomlTemplate := `+++ +name = "Tags" +tags = %s ++++ +` + + jsonTemplate := `{ + "name": "Tags", + "tags": [ + %s + ] +}` + name := fmt.Sprintf("%s:%d", fileformat, numTags) + b.Run(name, func(b *testing.B) { + tags := make([]string, numTags) + var ( + tagsStr string + frontmatterTemplate string + ) + for i := 0; i < numTags; i++ { + tags[i] = fmt.Sprintf("Hugo %d", i+1) + } + if fileformat == "TOML" { + frontmatterTemplate = tomlTemplate + tagsStr = strings.Replace(fmt.Sprintf("%q", tags), " ", ", ", -1) + } else if fileformat == "JSON" { + frontmatterTemplate = jsonTemplate + tagsStr = strings.Replace(fmt.Sprintf("%q", tags), " ", ", ", -1) + } else if fileformat == "YAML2" { + frontmatterTemplate = yaml2Template + tagsStr = strings.Replace(fmt.Sprintf("%q", tags), " ", ", ", -1) + } else { + frontmatterTemplate = yamlTemplate + for _, tag := range tags { + tagsStr += "\n- " + tag + } + } + + frontmatter := fmt.Sprintf(frontmatterTemplate, tagsStr) + + p := page{frontmatter: []byte(frontmatter)} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + meta, err := p.Metadata() + if err != nil { + b.Fatal(err) + } + if meta == nil { + b.Fatal("Meta is nil") + } + } + }) +} diff --git a/parser/long_text_test.md b/parser/long_text_test.md new file mode 100644 index 000000000..e87ceb8c6 --- /dev/null +++ b/parser/long_text_test.md @@ -0,0 +1,263 @@ +---
+title: The Git Book - Long Text
+---
+# Getting Started #
+
+This chapter will be about getting started with Git. We will begin at the beginning by explaining some background on version control tools, then move on to how to get Git running on your system and finally how to get it setup to start working with. At the end of this chapter you should understand why Git is around, why you should use it and you should be all setup to do so.
+
+## About Version Control ##
+
+What is version control, and why should you care? Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. Even though the examples in this book show software source code as the files under version control, in reality any type of file on a computer can be placed under version control.
+
+If you are a graphic or web designer and want to keep every version of an image or layout (which you certainly would), it is very wise to use a Version Control System (VCS). A VCS allows you to: revert files back to a previous state, revert the entire project back to a previous state, review changes made over time, see who last modified something that might be causing a problem, who introduced an issue and when, and more. Using a VCS also means that if you screw things up or lose files, you can generally recover easily. In addition, you get all this for very little overhead.
+
+### Local Version Control Systems ###
+
+Many people�s version-control method of choice is to copy files into another directory (perhaps a time-stamped directory, if they�re clever). This approach is very common because it is so simple, but it is also incredibly error prone. It is easy to forget which directory you�re in and accidentally write to the wrong file or copy over files you don�t mean to.
+
+To deal with this issue, programmers long ago developed local VCSs that had a simple database that kept all the changes to files under revision control (see Figure 1-1).
+
+Insert 18333fig0101.png
+Figure 1-1. Local version control diagram.
+
+One of the more popular VCS tools was a system called rcs, which is still distributed with many computers today. Even the popular Mac OS X operating system includes the rcs command when you install the Developer Tools. This tool basically works by keeping patch sets (that is, the differences between files) from one revision to another in a special format on disk; it can then recreate what any file looked like at any point in time by adding up all the patches.
+
+### Centralized Version Control Systems ###
+
+The next major issue that people encounter is that they need to collaborate with developers on other systems. To deal with this problem, Centralized Version Control Systems (CVCSs) were developed. These systems, such as CVS, Subversion, and Perforce, have a single server that contains all the versioned files, and a number of clients that check out files from that central place. For many years, this has been the standard for version control (see Figure 1-2).
+
+Insert 18333fig0102.png
+Figure 1-2. Centralized version control diagram.
+
+This setup offers many advantages, especially over local VCSs. For example, everyone knows to a certain degree what everyone else on the project is doing. Administrators have fine-grained control over who can do what; and it�s far easier to administer a CVCS than it is to deal with local databases on every client.
+
+However, this setup also has some serious downsides. The most obvious is the single point of failure that the centralized server represents. If that server goes down for an hour, then during that hour nobody can collaborate at all or save versioned changes to anything they�re working on. If the hard disk the central database is on becomes corrupted, and proper backups haven�t been kept, you lose absolutely everything�the entire history of the project except whatever single snapshots people happen to have on their local machines. Local VCS systems suffer from this same problem�whenever you have the entire history of the project in a single place, you risk losing everything.
+
+### Distributed Version Control Systems ###
+
+This is where Distributed Version Control Systems (DVCSs) step in. In a DVCS (such as Git, Mercurial, Bazaar or Darcs), clients don�t just check out the latest snapshot of the files: they fully mirror the repository. Thus if any server dies, and these systems were collaborating via it, any of the client repositories can be copied back up to the server to restore it. Every checkout is really a full backup of all the data (see Figure 1-3).
+
+Insert 18333fig0103.png
+Figure 1-3. Distributed version control diagram.
+
+Furthermore, many of these systems deal pretty well with having several remote repositories they can work with, so you can collaborate with different groups of people in different ways simultaneously within the same project. This allows you to set up several types of workflows that aren�t possible in centralized systems, such as hierarchical models.
+
+## A Short History of Git ##
+
+As with many great things in life, Git began with a bit of creative destruction and fiery controversy. The Linux kernel is an open source software project of fairly large scope. For most of the lifetime of the Linux kernel maintenance (1991�2002), changes to the software were passed around as patches and archived files. In 2002, the Linux kernel project began using a proprietary DVCS system called BitKeeper.
+
+In 2005, the relationship between the community that developed the Linux kernel and the commercial company that developed BitKeeper broke down, and the tool�s free-of-charge status was revoked. This prompted the Linux development community (and in particular Linus Torvalds, the creator of Linux) to develop their own tool based on some of the lessons they learned while using BitKeeper. Some of the goals of the new system were as follows:
+
+* Speed
+* Simple design
+* Strong support for non-linear development (thousands of parallel branches)
+* Fully distributed
+* Able to handle large projects like the Linux kernel efficiently (speed and data size)
+
+Since its birth in 2005, Git has evolved and matured to be easy to use and yet retain these initial qualities. It�s incredibly fast, it�s very efficient with large projects, and it has an incredible branching system for non-linear development (See Chapter 3).
+
+## Git Basics ##
+
+So, what is Git in a nutshell? This is an important section to absorb, because if you understand what Git is and the fundamentals of how it works, then using Git effectively will probably be much easier for you. As you learn Git, try to clear your mind of the things you may know about other VCSs, such as Subversion and Perforce; doing so will help you avoid subtle confusion when using the tool. Git stores and thinks about information much differently than these other systems, even though the user interface is fairly similar; understanding those differences will help prevent you from becoming confused while using it.
+
+### Snapshots, Not Differences ###
+
+The major difference between Git and any other VCS (Subversion and friends included) is the way Git thinks about its data. Conceptually, most other systems store information as a list of file-based changes. These systems (CVS, Subversion, Perforce, Bazaar, and so on) think of the information they keep as a set of files and the changes made to each file over time, as illustrated in Figure 1-4.
+
+Insert 18333fig0104.png
+Figure 1-4. Other systems tend to store data as changes to a base version of each file.
+
+Git doesn�t think of or store its data this way. Instead, Git thinks of its data more like a set of snapshots of a mini filesystem. Every time you commit, or save the state of your project in Git, it basically takes a picture of what all your files look like at that moment and stores a reference to that snapshot. To be efficient, if files have not changed, Git doesn�t store the file again�just a link to the previous identical file it has already stored. Git thinks about its data more like Figure 1-5.
+
+Insert 18333fig0105.png
+Figure 1-5. Git stores data as snapshots of the project over time.
+
+This is an important distinction between Git and nearly all other VCSs. It makes Git reconsider almost every aspect of version control that most other systems copied from the previous generation. This makes Git more like a mini filesystem with some incredibly powerful tools built on top of it, rather than simply a VCS. We�ll explore some of the benefits you gain by thinking of your data this way when we cover Git branching in Chapter 3.
+
+### Nearly Every Operation Is Local ###
+
+Most operations in Git only need local files and resources to operate � generally no information is needed from another computer on your network. If you�re used to a CVCS where most operations have that network latency overhead, this aspect of Git will make you think that the gods of speed have blessed Git with unworldly powers. Because you have the entire history of the project right there on your local disk, most operations seem almost instantaneous.
+
+For example, to browse the history of the project, Git doesn�t need to go out to the server to get the history and display it for you�it simply reads it directly from your local database. This means you see the project history almost instantly. If you want to see the changes introduced between the current version of a file and the file a month ago, Git can look up the file a month ago and do a local difference calculation, instead of having to either ask a remote server to do it or pull an older version of the file from the remote server to do it locally.
+
+This also means that there is very little you can�t do if you�re offline or off VPN. If you get on an airplane or a train and want to do a little work, you can commit happily until you get to a network connection to upload. If you go home and can�t get your VPN client working properly, you can still work. In many other systems, doing so is either impossible or painful. In Perforce, for example, you can�t do much when you aren�t connected to the server; and in Subversion and CVS, you can edit files, but you can�t commit changes to your database (because your database is offline). This may not seem like a huge deal, but you may be surprised what a big difference it can make.
+
+### Git Has Integrity ###
+
+Everything in Git is check-summed before it is stored and is then referred to by that checksum. This means it�s impossible to change the contents of any file or directory without Git knowing about it. This functionality is built into Git at the lowest levels and is integral to its philosophy. You can�t lose information in transit or get file corruption without Git being able to detect it.
+
+The mechanism that Git uses for this checksumming is called a SHA-1 hash. This is a 40-character string composed of hexadecimal characters (0�9 and a�f) and calculated based on the contents of a file or directory structure in Git. A SHA-1 hash looks something like this:
+
+ 24b9da6552252987aa493b52f8696cd6d3b00373
+
+You will see these hash values all over the place in Git because it uses them so much. In fact, Git stores everything not by file name but in the Git database addressable by the hash value of its contents.
+
+### Git Generally Only Adds Data ###
+
+When you do actions in Git, nearly all of them only add data to the Git database. It is very difficult to get the system to do anything that is not undoable or to make it erase data in any way. As in any VCS, you can lose or mess up changes you haven�t committed yet; but after you commit a snapshot into Git, it is very difficult to lose, especially if you regularly push your database to another repository.
+
+This makes using Git a joy because we know we can experiment without the danger of severely screwing things up. For a more in-depth look at how Git stores its data and how you can recover data that seems lost, see Chapter 9.
+
+### The Three States ###
+
+Now, pay attention. This is the main thing to remember about Git if you want the rest of your learning process to go smoothly. Git has three main states that your files can reside in: committed, modified, and staged. Committed means that the data is safely stored in your local database. Modified means that you have changed the file but have not committed it to your database yet. Staged means that you have marked a modified file in its current version to go into your next commit snapshot.
+
+This leads us to the three main sections of a Git project: the Git directory, the working directory, and the staging area.
+
+Insert 18333fig0106.png
+Figure 1-6. Working directory, staging area, and git directory.
+
+The Git directory is where Git stores the metadata and object database for your project. This is the most important part of Git, and it is what is copied when you clone a repository from another computer.
+
+The working directory is a single checkout of one version of the project. These files are pulled out of the compressed database in the Git directory and placed on disk for you to use or modify.
+
+The staging area is a simple file, generally contained in your Git directory, that stores information about what will go into your next commit. It�s sometimes referred to as the index, but it�s becoming standard to refer to it as the staging area.
+
+The basic Git workflow goes something like this:
+
+1. You modify files in your working directory.
+2. You stage the files, adding snapshots of them to your staging area.
+3. You do a commit, which takes the files as they are in the staging area and stores that snapshot permanently to your Git directory.
+
+If a particular version of a file is in the git directory, it�s considered committed. If it�s modified but has been added to the staging area, it is staged. And if it was changed since it was checked out but has not been staged, it is modified. In Chapter 2, you�ll learn more about these states and how you can either take advantage of them or skip the staged part entirely.
+
+## Installing Git ##
+
+Let�s get into using some Git. First things first�you have to install it. You can get it a number of ways; the two major ones are to install it from source or to install an existing package for your platform.
+
+### Installing from Source ###
+
+If you can, it�s generally useful to install Git from source, because you�ll get the most recent version. Each version of Git tends to include useful UI enhancements, so getting the latest version is often the best route if you feel comfortable compiling software from source. It is also the case that many Linux distributions contain very old packages; so unless you�re on a very up-to-date distro or are using backports, installing from source may be the best bet.
+
+To install Git, you need to have the following libraries that Git depends on: curl, zlib, openssl, expat, and libiconv. For example, if you�re on a system that has yum (such as Fedora) or apt-get (such as a Debian based system), you can use one of these commands to install all of the dependencies:
+
+ $ yum install curl-devel expat-devel gettext-devel \
+ openssl-devel zlib-devel
+
+ $ apt-get install libcurl4-gnutls-dev libexpat1-dev gettext \
+ libz-dev libssl-dev
+
+When you have all the necessary dependencies, you can go ahead and grab the latest snapshot from the Git web site:
+
+ http://git-scm.com/download
+
+Then, compile and install:
+
+ $ tar -zxf git-1.7.2.2.tar.gz
+ $ cd git-1.7.2.2
+ $ make prefix=/usr/local all
+ $ sudo make prefix=/usr/local install
+
+After this is done, you can also get Git via Git itself for updates:
+
+ $ git clone git://git.kernel.org/pub/scm/git/git.git
+
+### Installing on Linux ###
+
+If you want to install Git on Linux via a binary installer, you can generally do so through the basic package-management tool that comes with your distribution. If you�re on Fedora, you can use yum:
+
+ $ yum install git-core
+
+Or if you�re on a Debian-based distribution like Ubuntu, try apt-get:
+
+ $ apt-get install git
+
+### Installing on Mac ###
+
+There are two easy ways to install Git on a Mac. The easiest is to use the graphical Git installer, which you can download from the Google Code page (see Figure 1-7):
+
+ http://code.google.com/p/git-osx-installer
+
+Insert 18333fig0107.png
+Figure 1-7. Git OS X installer.
+
+The other major way is to install Git via MacPorts (`http://www.macports.org`). If you have MacPorts installed, install Git via
+
+ $ sudo port install git-core +svn +doc +bash_completion +gitweb
+
+You don�t have to add all the extras, but you�ll probably want to include +svn in case you ever have to use Git with Subversion repositories (see Chapter 8).
+
+### Installing on Windows ###
+
+Installing Git on Windows is very easy. The msysGit project has one of the easier installation procedures. Simply download the installer exe file from the GitHub page, and run it:
+
+ http://msysgit.github.com/
+
+After it�s installed, you have both a command-line version (including an SSH client that will come in handy later) and the standard GUI.
+
+Note on Windows usage: you should use Git with the provided msysGit shell (Unix style), it allows to use the complex lines of command given in this book. If you need, for some reason, to use the native Windows shell / command line console, you have to use double quotes instead of simple quotes (for parameters with spaces in them) and you must quote the parameters ending with the circumflex accent (^) if they are last on the line, as it is a continuation symbol in Windows.
+
+## First-Time Git Setup ##
+
+Now that you have Git on your system, you�ll want to do a few things to customize your Git environment. You should have to do these things only once; they�ll stick around between upgrades. You can also change them at any time by running through the commands again.
+
+Git comes with a tool called git config that lets you get and set configuration variables that control all aspects of how Git looks and operates. These variables can be stored in three different places:
+
+* `/etc/gitconfig` file: Contains values for every user on the system and all their repositories. If you pass the option` --system` to `git config`, it reads and writes from this file specifically.
+* `~/.gitconfig` file: Specific to your user. You can make Git read and write to this file specifically by passing the `--global` option.
+* config file in the git directory (that is, `.git/config`) of whatever repository you�re currently using: Specific to that single repository. Each level overrides values in the previous level, so values in `.git/config` trump those in `/etc/gitconfig`.
+
+On Windows systems, Git looks for the `.gitconfig` file in the `$HOME` directory (`%USERPROFILE%` in Windows� environment), which is `C:\Documents and Settings\$USER` or `C:\Users\$USER` for most people, depending on version (`$USER` is `%USERNAME%` in Windows� environment). It also still looks for /etc/gitconfig, although it�s relative to the MSys root, which is wherever you decide to install Git on your Windows system when you run the installer.
+
+### Your Identity ###
+
+The first thing you should do when you install Git is to set your user name and e-mail address. This is important because every Git commit uses this information, and it�s immutably baked into the commits you pass around:
+
+ $ git config --global user.name "John Doe"
+ $ git config --global user.email [email protected]
+
+Again, you need to do this only once if you pass the `--global` option, because then Git will always use that information for anything you do on that system. If you want to override this with a different name or e-mail address for specific projects, you can run the command without the `--global` option when you�re in that project.
+
+### Your Editor ###
+
+Now that your identity is set up, you can configure the default text editor that will be used when Git needs you to type in a message. By default, Git uses your system�s default editor, which is generally Vi or Vim. If you want to use a different text editor, such as Emacs, you can do the following:
+
+ $ git config --global core.editor emacs
+
+### Your Diff Tool ###
+
+Another useful option you may want to configure is the default diff tool to use to resolve merge conflicts. Say you want to use vimdiff:
+
+ $ git config --global merge.tool vimdiff
+
+Git accepts kdiff3, tkdiff, meld, xxdiff, emerge, vimdiff, gvimdiff, ecmerge, and opendiff as valid merge tools. You can also set up a custom tool; see Chapter 7 for more information about doing that.
+
+### Checking Your Settings ###
+
+If you want to check your settings, you can use the `git config --list` command to list all the settings Git can find at that point:
+
+ $ git config --list
+ user.name=Scott Chacon
+ color.status=auto
+ color.branch=auto
+ color.interactive=auto
+ color.diff=auto
+ ...
+
+You may see keys more than once, because Git reads the same key from different files (`/etc/gitconfig` and `~/.gitconfig`, for example). In this case, Git uses the last value for each unique key it sees.
+
+You can also check what Git thinks a specific key�s value is by typing `git config {key}`:
+
+ $ git config user.name
+ Scott Chacon
+
+## Getting Help ##
+
+If you ever need help while using Git, there are three ways to get the manual page (manpage) help for any of the Git commands:
+
+ $ git help <verb>
+ $ git <verb> --help
+ $ man git-<verb>
+
+For example, you can get the manpage help for the config command by running
+
+ $ git help config
+
+These commands are nice because you can access them anywhere, even offline.
+If the manpages and this book aren�t enough and you need in-person help, you can try the `#git` or `#github` channel on the Freenode IRC server (irc.freenode.net). These channels are regularly filled with hundreds of people who are all very knowledgeable about Git and are often willing to help.
+
+## Summary ##
+
+You should have a basic understanding of what Git is and how it�s different from the CVCS you may have been using. You should also now have a working version of Git on your system that�s set up with your personal identity. It�s now time to learn some Git basics.
+
diff --git a/parser/page.go b/parser/page.go new file mode 100644 index 000000000..bacc9754b --- /dev/null +++ b/parser/page.go @@ -0,0 +1,408 @@ +// Copyright 2016n The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package parser + +import ( + "bufio" + "bytes" + "fmt" + "io" + "regexp" + "strings" + "unicode" + + "github.com/chaseadamsio/goorgeous" +) + +const ( + // TODO(bep) Do we really have to export these? + + // HTMLLead identifies the start of HTML documents. + HTMLLead = "<" + // YAMLLead identifies the start of YAML frontmatter. + YAMLLead = "-" + // YAMLDelimUnix identifies the end of YAML front matter on Unix. + YAMLDelimUnix = "---\n" + // YAMLDelimDOS identifies the end of YAML front matter on Windows. + YAMLDelimDOS = "---\r\n" + // YAMLDelim identifies the YAML front matter delimiter. + YAMLDelim = "---" + // TOMLLead identifies the start of TOML front matter. + TOMLLead = "+" + // TOMLDelimUnix identifies the end of TOML front matter on Unix. + TOMLDelimUnix = "+++\n" + // TOMLDelimDOS identifies the end of TOML front matter on Windows. + TOMLDelimDOS = "+++\r\n" + // TOMLDelim identifies the TOML front matter delimiter. + TOMLDelim = "+++" + // JSONLead identifies the start of JSON frontmatter. + JSONLead = "{" + // HTMLCommentStart identifies the start of HTML comment. + HTMLCommentStart = "<!--" + // HTMLCommentEnd identifies the end of HTML comment. + HTMLCommentEnd = "-->" + // BOM Unicode byte order marker + BOM = '\ufeff' +) + +var ( + delims = regexp.MustCompile( + "^(" + regexp.QuoteMeta(YAMLDelim) + `\s*\n|` + regexp.QuoteMeta(TOMLDelim) + `\s*\n|` + regexp.QuoteMeta(JSONLead) + ")", + ) +) + +// Page represents a parsed content page. +type Page interface { + // FrontMatter contains the raw frontmatter with relevant delimiters. + FrontMatter() []byte + + // Content contains the raw page content. + Content() []byte + + // IsRenderable denotes that the page should be rendered. + IsRenderable() bool + + // Metadata returns the unmarshalled frontmatter data. + Metadata() (interface{}, error) +} + +// page implements the Page interface. +type page struct { + render bool + frontmatter []byte + content []byte +} + +// Content returns the raw page content. +func (p *page) Content() []byte { + return p.content +} + +// FrontMatter contains the raw frontmatter with relevant delimiters. +func (p *page) FrontMatter() []byte { + return p.frontmatter +} + +// IsRenderable denotes that the page should be rendered. +func (p *page) IsRenderable() bool { + return p.render +} + +// Metadata returns the unmarshalled frontmatter data. +func (p *page) Metadata() (meta interface{}, err error) { + frontmatter := p.FrontMatter() + + if len(frontmatter) != 0 { + fm := DetectFrontMatter(rune(frontmatter[0])) + if fm != nil { + meta, err = fm.Parse(frontmatter) + if err != nil { + return + } + } + } + return +} + +// ReadFrom reads the content from an io.Reader and constructs a page. +func ReadFrom(r io.Reader) (p Page, err error) { + reader := bufio.NewReader(r) + + // chomp BOM and assume UTF-8 + if err = chompBOM(reader); err != nil && err != io.EOF { + return + } + if err = chompWhitespace(reader); err != nil && err != io.EOF { + return + } + if err = chompFrontmatterStartComment(reader); err != nil && err != io.EOF { + return + } + + firstLine, err := peekLine(reader) + if err != nil && err != io.EOF { + return + } + + newp := new(page) + newp.render = shouldRender(firstLine) + + if newp.render && isFrontMatterDelim(firstLine) { + left, right := determineDelims(firstLine) + fm, err := extractFrontMatterDelims(reader, left, right) + if err != nil { + return nil, err + } + newp.frontmatter = fm + } else if newp.render && goorgeous.IsKeyword(firstLine) { + fm, err := goorgeous.ExtractOrgHeaders(reader) + if err != nil { + return nil, err + } + newp.frontmatter = fm + } + + content, err := extractContent(reader) + if err != nil { + return nil, err + } + + newp.content = content + + return newp, nil +} + +// chompBOM scans any leading Unicode Byte Order Markers from r. +func chompBOM(r io.RuneScanner) (err error) { + for { + c, _, err := r.ReadRune() + if err != nil { + return err + } + if c != BOM { + r.UnreadRune() + return nil + } + } +} + +// chompWhitespace scans any leading Unicode whitespace from r. +func chompWhitespace(r io.RuneScanner) (err error) { + for { + c, _, err := r.ReadRune() + if err != nil { + return err + } + if !unicode.IsSpace(c) { + r.UnreadRune() + return nil + } + } +} + +// chompFrontmatterStartComment checks r for a leading HTML comment. If a +// comment is found, it is read from r and then whitespace is trimmed from the +// beginning of r. +func chompFrontmatterStartComment(r *bufio.Reader) (err error) { + candidate, err := r.Peek(32) + if err != nil { + return err + } + + str := string(candidate) + if strings.HasPrefix(str, HTMLCommentStart) { + lineEnd := strings.IndexAny(str, "\n") + if lineEnd == -1 { + //TODO: if we can't find it, Peek more? + return nil + } + testStr := strings.TrimSuffix(str[0:lineEnd], "\r") + if strings.Contains(testStr, HTMLCommentEnd) { + return nil + } + buf := make([]byte, lineEnd) + if _, err = r.Read(buf); err != nil { + return + } + if err = chompWhitespace(r); err != nil { + return err + } + } + + return nil +} + +// chompFrontmatterEndComment checks r for a trailing HTML comment. +func chompFrontmatterEndComment(r *bufio.Reader) (err error) { + candidate, err := r.Peek(32) + if err != nil { + return err + } + + str := string(candidate) + lineEnd := strings.IndexAny(str, "\n") + if lineEnd == -1 { + return nil + } + testStr := strings.TrimSuffix(str[0:lineEnd], "\r") + if strings.Contains(testStr, HTMLCommentStart) { + return nil + } + + //TODO: if we can't find it, Peek more? + if strings.HasSuffix(testStr, HTMLCommentEnd) { + buf := make([]byte, lineEnd) + if _, err = r.Read(buf); err != nil { + return + } + if err = chompWhitespace(r); err != nil { + return err + } + } + + return nil +} + +func peekLine(r *bufio.Reader) (line []byte, err error) { + firstFive, err := r.Peek(5) + if err != nil { + return + } + idx := bytes.IndexByte(firstFive, '\n') + if idx == -1 { + return firstFive, nil + } + idx++ // include newline. + return firstFive[:idx], nil +} + +func shouldRender(lead []byte) (frontmatter bool) { + if len(lead) <= 0 { + return + } + + if bytes.Equal(lead[:1], []byte(HTMLLead)) { + return + } + return true +} + +func isFrontMatterDelim(data []byte) bool { + return delims.Match(data) +} + +func determineDelims(firstLine []byte) (left, right []byte) { + switch firstLine[0] { + case YAMLLead[0]: + return []byte(YAMLDelim), []byte(YAMLDelim) + case TOMLLead[0]: + return []byte(TOMLDelim), []byte(TOMLDelim) + case JSONLead[0]: + return []byte(JSONLead), []byte("}") + default: + panic(fmt.Sprintf("Unable to determine delims from %q", firstLine)) + } +} + +// extractFrontMatterDelims takes a frontmatter from the content bufio.Reader. +// Beginning white spaces of the bufio.Reader must be trimmed before call this +// function. +func extractFrontMatterDelims(r *bufio.Reader, left, right []byte) (fm []byte, err error) { + var ( + c byte + buf bytes.Buffer + level int + sameDelim = bytes.Equal(left, right) + inQuote bool + ) + // Frontmatter must start with a delimiter. To check it first, + // pre-reads beginning delimiter length - 1 bytes from Reader + for i := 0; i < len(left)-1; i++ { + if c, err = r.ReadByte(); err != nil { + return nil, fmt.Errorf("unable to read frontmatter at filepos %d: %s", buf.Len(), err) + } + if err = buf.WriteByte(c); err != nil { + return nil, err + } + } + + // Reads a character from Reader one by one and checks it matches the + // last character of one of delimiters to find the last character of + // frontmatter. If it matches, makes sure it contains the delimiter + // and if so, also checks it is followed by CR+LF or LF when YAML, + // TOML case. In JSON case, nested delimiters must be parsed and it + // is expected that the delimiter only contains one character. + for { + if c, err = r.ReadByte(); err != nil { + return nil, fmt.Errorf("unable to read frontmatter at filepos %d: %s", buf.Len(), err) + } + if err = buf.WriteByte(c); err != nil { + return nil, err + } + + switch c { + case '"': + inQuote = !inQuote + case left[len(left)-1]: + if sameDelim { // YAML, TOML case + if bytes.HasSuffix(buf.Bytes(), left) && (buf.Len() == len(left) || buf.Bytes()[buf.Len()-len(left)-1] == '\n') { + nextByte: + c, err = r.ReadByte() + if err != nil { + // It is ok that the end delimiter ends with EOF + if err != io.EOF || level != 1 { + return nil, fmt.Errorf("unable to read frontmatter at filepos %d: %s", buf.Len(), err) + } + } else { + switch c { + case '\n': + // ok + case ' ': + // Consume this byte and try to match again + goto nextByte + case '\r': + if err = buf.WriteByte(c); err != nil { + return nil, err + } + if c, err = r.ReadByte(); err != nil { + return nil, fmt.Errorf("unable to read frontmatter at filepos %d: %s", buf.Len(), err) + } + if c != '\n' { + return nil, fmt.Errorf("frontmatter delimiter must be followed by CR+LF or LF but those can't be found at filepos %d", buf.Len()) + } + default: + return nil, fmt.Errorf("frontmatter delimiter must be followed by CR+LF or LF but those can't be found at filepos %d", buf.Len()) + } + if err = buf.WriteByte(c); err != nil { + return nil, err + } + } + if level == 0 { + level = 1 + } else { + level = 0 + } + } + } else { // JSON case + if !inQuote { + level++ + } + } + case right[len(right)-1]: // JSON case only reaches here + if !inQuote { + level-- + } + } + + if level == 0 { + // Consumes white spaces immediately behind frontmatter + if err = chompWhitespace(r); err != nil && err != io.EOF { + return nil, err + } + if err = chompFrontmatterEndComment(r); err != nil && err != io.EOF { + return nil, err + } + + return buf.Bytes(), nil + } + } +} + +func extractContent(r io.Reader) (content []byte, err error) { + wr := new(bytes.Buffer) + if _, err = wr.ReadFrom(r); err != nil { + return + } + return wr.Bytes(), nil +} diff --git a/parser/page_test.go b/parser/page_test.go new file mode 100644 index 000000000..07d7660d4 --- /dev/null +++ b/parser/page_test.go @@ -0,0 +1,130 @@ +package parser + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPage(t *testing.T) { + cases := []struct { + raw string + + content string + frontmatter string + renderable bool + metadata map[string]interface{} + }{ + { + testPageLeader + jsonPageFrontMatter + "\n" + testPageTrailer + jsonPageContent, + jsonPageContent, + jsonPageFrontMatter, + true, + map[string]interface{}{ + "title": "JSON Test 1", + "social": []interface{}{ + []interface{}{"a", "#"}, + []interface{}{"b", "#"}, + }, + }, + }, + { + testPageLeader + tomlPageFrontMatter + testPageTrailer + tomlPageContent, + tomlPageContent, + tomlPageFrontMatter, + true, + map[string]interface{}{ + "title": "TOML Test 1", + "social": []interface{}{ + []interface{}{"a", "#"}, + []interface{}{"b", "#"}, + }, + }, + }, + { + testPageLeader + yamlPageFrontMatter + testPageTrailer + yamlPageContent, + yamlPageContent, + yamlPageFrontMatter, + true, + map[string]interface{}{ + "title": "YAML Test 1", + "social": []interface{}{ + []interface{}{"a", "#"}, + []interface{}{"b", "#"}, + }, + }, + }, + { + testPageLeader + orgPageFrontMatter + orgPageContent, + orgPageContent, + orgPageFrontMatter, + true, + map[string]interface{}{ + "TITLE": "Org Test 1", + "categories": []string{"a", "b"}, + }, + }, + } + + for i, c := range cases { + p := pageMust(ReadFrom(strings.NewReader(c.raw))) + meta, err := p.Metadata() + + mesg := fmt.Sprintf("[%d]", i) + + require.Nil(t, err, mesg) + assert.Equal(t, c.content, string(p.Content()), mesg+" content") + assert.Equal(t, c.frontmatter, string(p.FrontMatter()), mesg+" frontmatter") + assert.Equal(t, c.renderable, p.IsRenderable(), mesg+" renderable") + assert.Equal(t, c.metadata, meta, mesg+" metadata") + } +} + +var ( + testWhitespace = "\t\t\n\n" + testPageLeader = "\ufeff" + testWhitespace + "<!--[metadata]>\n" + testPageTrailer = "\n<![end-metadata]-->\n" + + jsonPageContent = "# JSON Test\n" + jsonPageFrontMatter = `{ + "title": "JSON Test 1", + "social": [ + ["a", "#"], + ["b", "#"] + ] +}` + + tomlPageContent = "# TOML Test\n" + tomlPageFrontMatter = `+++ +title = "TOML Test 1" +social = [ + ["a", "#"], + ["b", "#"], +] ++++ +` + + yamlPageContent = "# YAML Test\n" + yamlPageFrontMatter = `--- +title: YAML Test 1 +social: + - - "a" + - "#" + - - "b" + - "#" +--- +` + + orgPageContent = "* Org Test\n" + orgPageFrontMatter = `#+TITLE: Org Test 1 +#+categories: a b +` + + pageHTMLComment = `<!-- + This is a sample comment. +--> +` +) diff --git a/parser/parse_frontmatter_test.go b/parser/parse_frontmatter_test.go new file mode 100644 index 000000000..08a1cc42c --- /dev/null +++ b/parser/parse_frontmatter_test.go @@ -0,0 +1,316 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package parser + +// TODO Support Mac Encoding (\r) + +import ( + "bufio" + "bytes" + "io" + "os" + "path/filepath" + "strings" + "testing" +) + +const ( + contentNoFrontmatter = "a page with no front matter" + contentWithFrontmatter = "---\ntitle: front matter\n---\nContent with front matter" + contentHTMLNoDoctype = "<html>\n\t<body>\n\t</body>\n</html>" + contentHTMLWithDoctype = "<!doctype html><html><body></body></html>" + contentHTMLWithFrontmatter = "---\ntitle: front matter\n---\n<!doctype><html><body></body></html>" + contentHTML = " <html><body></body></html>" + contentLinefeedAndHTML = "\n<html><body></body></html>" + contentIncompleteEndFrontmatterDelim = "---\ntitle: incomplete end fm delim\n--\nincomplete frontmatter delim" + contentMissingEndFrontmatterDelim = "---\ntitle: incomplete end fm delim\nincomplete frontmatter delim" + contentSlugWorking = "---\ntitle: slug doc 2\nslug: slug-doc-2\n\n---\nslug doc 2 content" + contentSlugWorkingVariation = "---\ntitle: slug doc 3\nslug: slug-doc 3\n---\nslug doc 3 content" + contentSlugBug = "---\ntitle: slug doc 2\nslug: slug-doc-2\n---\nslug doc 2 content" + contentSlugWithJSONFrontMatter = "{\n \"categories\": \"d\",\n \"tags\": [\n \"a\", \n \"b\", \n \"c\"\n ]\n}\nJSON Front Matter with tags and categories" + contentWithJSONLooseFrontmatter = "{\n \"categories\": \"d\"\n \"tags\": [\n \"a\" \n \"b\" \n \"c\"\n ]\n}\nJSON Front Matter with tags and categories" + contentSlugWithJSONFrontMatterFirstLineOnly = "{\"categories\":\"d\",\"tags\":[\"a\",\"b\",\"c\"]}\nJSON Front Matter with tags and categories" + contentSlugWithJSONFrontMatterFirstLine = "{\"categories\":\"d\",\n \"tags\":[\"a\",\"b\",\"c\"]}\nJSON Front Matter with tags and categories" +) + +var lineEndings = []string{"\n", "\r\n"} +var delimiters = []string{"---", "+++"} + +func pageMust(p Page, err error) *page { + if err != nil { + panic(err) + } + return p.(*page) +} + +func TestDegenerateCreatePageFrom(t *testing.T) { + tests := []struct { + content string + }{ + {contentMissingEndFrontmatterDelim}, + {contentIncompleteEndFrontmatterDelim}, + } + + for _, test := range tests { + for _, ending := range lineEndings { + test.content = strings.Replace(test.content, "\n", ending, -1) + _, err := ReadFrom(strings.NewReader(test.content)) + if err == nil { + t.Errorf("Content should return an err:\n%q\n", test.content) + } + } + } +} + +func checkPageRender(t *testing.T, p *page, expected bool) { + if p.render != expected { + t.Errorf("page.render should be %t, got: %t", expected, p.render) + } +} + +func checkPageFrontMatterIsNil(t *testing.T, p *page, content string, expected bool) { + if bool(p.frontmatter == nil) != expected { + t.Logf("\n%q\n", content) + t.Errorf("page.frontmatter == nil? %t, got %t", expected, p.frontmatter == nil) + } +} + +func checkPageFrontMatterContent(t *testing.T, p *page, frontMatter string) { + if p.frontmatter == nil { + return + } + if !bytes.Equal(p.frontmatter, []byte(frontMatter)) { + t.Errorf("frontmatter mismatch\nexp: %q\ngot: %q", frontMatter, p.frontmatter) + } +} + +func checkPageContent(t *testing.T, p *page, expected string) { + if !bytes.Equal(p.content, []byte(expected)) { + t.Errorf("content mismatch\nexp: %q\ngot: %q", expected, p.content) + } +} + +func TestStandaloneCreatePageFrom(t *testing.T) { + tests := []struct { + content string + expectedMustRender bool + frontMatterIsNil bool + frontMatter string + bodycontent string + }{ + + {contentNoFrontmatter, true, true, "", "a page with no front matter"}, + {contentWithFrontmatter, true, false, "---\ntitle: front matter\n---\n", "Content with front matter"}, + {contentHTMLNoDoctype, false, true, "", "<html>\n\t<body>\n\t</body>\n</html>"}, + {contentHTMLWithDoctype, false, true, "", "<!doctype html><html><body></body></html>"}, + {contentHTMLWithFrontmatter, true, false, "---\ntitle: front matter\n---\n", "<!doctype><html><body></body></html>"}, + {contentHTML, false, true, "", "<html><body></body></html>"}, + {contentLinefeedAndHTML, false, true, "", "<html><body></body></html>"}, + {contentSlugWithJSONFrontMatter, true, false, "{\n \"categories\": \"d\",\n \"tags\": [\n \"a\", \n \"b\", \n \"c\"\n ]\n}", "JSON Front Matter with tags and categories"}, + {contentWithJSONLooseFrontmatter, true, false, "{\n \"categories\": \"d\"\n \"tags\": [\n \"a\" \n \"b\" \n \"c\"\n ]\n}", "JSON Front Matter with tags and categories"}, + {contentSlugWithJSONFrontMatterFirstLineOnly, true, false, "{\"categories\":\"d\",\"tags\":[\"a\",\"b\",\"c\"]}", "JSON Front Matter with tags and categories"}, + {contentSlugWithJSONFrontMatterFirstLine, true, false, "{\"categories\":\"d\",\n \"tags\":[\"a\",\"b\",\"c\"]}", "JSON Front Matter with tags and categories"}, + {contentSlugWorking, true, false, "---\ntitle: slug doc 2\nslug: slug-doc-2\n\n---\n", "slug doc 2 content"}, + {contentSlugWorkingVariation, true, false, "---\ntitle: slug doc 3\nslug: slug-doc 3\n---\n", "slug doc 3 content"}, + {contentSlugBug, true, false, "---\ntitle: slug doc 2\nslug: slug-doc-2\n---\n", "slug doc 2 content"}, + } + + for _, test := range tests { + for _, ending := range lineEndings { + test.content = strings.Replace(test.content, "\n", ending, -1) + test.frontMatter = strings.Replace(test.frontMatter, "\n", ending, -1) + test.bodycontent = strings.Replace(test.bodycontent, "\n", ending, -1) + + p := pageMust(ReadFrom(strings.NewReader(test.content))) + + checkPageRender(t, p, test.expectedMustRender) + checkPageFrontMatterIsNil(t, p, test.content, test.frontMatterIsNil) + checkPageFrontMatterContent(t, p, test.frontMatter) + checkPageContent(t, p, test.bodycontent) + } + } +} + +func BenchmarkLongFormRender(b *testing.B) { + + tests := []struct { + filename string + buf []byte + }{ + {filename: "long_text_test.md"}, + } + for i, test := range tests { + path := filepath.FromSlash(test.filename) + f, err := os.Open(path) + if err != nil { + b.Fatalf("Unable to open %s: %s", path, err) + } + defer f.Close() + membuf := new(bytes.Buffer) + if _, err := io.Copy(membuf, f); err != nil { + b.Fatalf("Unable to read %s: %s", path, err) + } + tests[i].buf = membuf.Bytes() + } + + b.ResetTimer() + + for i := 0; i <= b.N; i++ { + for _, test := range tests { + ReadFrom(bytes.NewReader(test.buf)) + } + } +} + +func TestPageShouldRender(t *testing.T) { + tests := []struct { + content []byte + expected bool + }{ + {[]byte{}, false}, + {[]byte{'<'}, false}, + {[]byte{'-'}, true}, + {[]byte("--"), true}, + {[]byte("---"), true}, + {[]byte("---\n"), true}, + {[]byte{'a'}, true}, + } + + for _, test := range tests { + for _, ending := range lineEndings { + test.content = bytes.Replace(test.content, []byte("\n"), []byte(ending), -1) + if render := shouldRender(test.content); render != test.expected { + + t.Errorf("Expected %s to shouldRender = %t, got: %t", test.content, test.expected, render) + } + } + } +} + +func TestPageHasFrontMatter(t *testing.T) { + tests := []struct { + content []byte + expected bool + }{ + {[]byte{'-'}, false}, + {[]byte("--"), false}, + {[]byte("---"), false}, + {[]byte("---\n"), true}, + {[]byte("---\n"), true}, + {[]byte("--- \n"), true}, + {[]byte("--- \n"), true}, + {[]byte{'a'}, false}, + {[]byte{'{'}, true}, + {[]byte("{\n "), true}, + {[]byte{'}'}, false}, + } + for _, test := range tests { + for _, ending := range lineEndings { + test.content = bytes.Replace(test.content, []byte("\n"), []byte(ending), -1) + if isFrontMatterDelim := isFrontMatterDelim(test.content); isFrontMatterDelim != test.expected { + t.Errorf("Expected %q isFrontMatterDelim = %t, got: %t", test.content, test.expected, isFrontMatterDelim) + } + } + } +} + +func TestExtractFrontMatter(t *testing.T) { + + tests := []struct { + frontmatter string + extracted []byte + errIsNil bool + }{ + {"", nil, false}, + {"-", nil, false}, + {"---\n", nil, false}, + {"---\nfoobar", nil, false}, + {"---\nfoobar\nbarfoo\nfizbaz\n", nil, false}, + {"---\nblar\n-\n", nil, false}, + {"---\nralb\n---\n", []byte("---\nralb\n---\n"), true}, + {"---\neof\n---", []byte("---\neof\n---"), true}, + {"--- \neof\n---", []byte("---\neof\n---"), true}, + {"---\nminc\n---\ncontent", []byte("---\nminc\n---\n"), true}, + {"---\nminc\n--- \ncontent", []byte("---\nminc\n---\n"), true}, + {"--- \nminc\n--- \ncontent", []byte("---\nminc\n---\n"), true}, + {"---\ncnim\n---\ncontent\n", []byte("---\ncnim\n---\n"), true}, + {"---\ntitle: slug doc 2\nslug: slug-doc-2\n---\ncontent\n", []byte("---\ntitle: slug doc 2\nslug: slug-doc-2\n---\n"), true}, + {"---\npermalink: '/blog/title---subtitle.html'\n---\ncontent\n", []byte("---\npermalink: '/blog/title---subtitle.html'\n---\n"), true}, + } + + for _, test := range tests { + for _, ending := range lineEndings { + test.frontmatter = strings.Replace(test.frontmatter, "\n", ending, -1) + test.extracted = bytes.Replace(test.extracted, []byte("\n"), []byte(ending), -1) + for _, delim := range delimiters { + test.frontmatter = strings.Replace(test.frontmatter, "---", delim, -1) + test.extracted = bytes.Replace(test.extracted, []byte("---"), []byte(delim), -1) + line, err := peekLine(bufio.NewReader(strings.NewReader(test.frontmatter))) + if err != nil { + continue + } + l, r := determineDelims(line) + fm, err := extractFrontMatterDelims(bufio.NewReader(strings.NewReader(test.frontmatter)), l, r) + if (err == nil) != test.errIsNil { + t.Logf("\n%q\n", string(test.frontmatter)) + t.Errorf("Expected err == nil => %t, got: %t. err: %s", test.errIsNil, err == nil, err) + continue + } + if !bytes.Equal(fm, test.extracted) { + t.Errorf("Frontmatter did not match:\nexp: %q\ngot: %q", string(test.extracted), fm) + } + } + } + } +} + +func TestExtractFrontMatterDelim(t *testing.T) { + var ( + noErrExpected = true + errExpected = false + ) + tests := []struct { + frontmatter string + extracted string + errIsNil bool + }{ + {"", "", errExpected}, + {"{", "", errExpected}, + {"{}", "{}", noErrExpected}, + {"{} ", "{}", noErrExpected}, + {"{ } ", "{ }", noErrExpected}, + {"{ { }", "", errExpected}, + {"{ { } }", "{ { } }", noErrExpected}, + {"{ { } { } }", "{ { } { } }", noErrExpected}, + {"{\n{\n}\n}\n", "{\n{\n}\n}", noErrExpected}, + {"{\n \"categories\": \"d\",\n \"tags\": [\n \"a\", \n \"b\", \n \"c\"\n ]\n}\nJSON Front Matter with tags and categories", "{\n \"categories\": \"d\",\n \"tags\": [\n \"a\", \n \"b\", \n \"c\"\n ]\n}", noErrExpected}, + {"{\n \"categories\": \"d\"\n \"tags\": [\n \"a\" \n \"b\" \n \"c\"\n ]\n}\nJSON Front Matter with tags and categories", "{\n \"categories\": \"d\"\n \"tags\": [\n \"a\" \n \"b\" \n \"c\"\n ]\n}", noErrExpected}, + // Issue #3511 + {`{ "title": "{" }`, `{ "title": "{" }`, noErrExpected}, + {`{ "title": "{}" }`, `{ "title": "{}" }`, noErrExpected}, + } + + for i, test := range tests { + fm, err := extractFrontMatterDelims(bufio.NewReader(strings.NewReader(test.frontmatter)), []byte("{"), []byte("}")) + if (err == nil) != test.errIsNil { + t.Logf("\n%q\n", string(test.frontmatter)) + t.Errorf("[%d] Expected err == nil => %t, got: %t. err: %s", i, test.errIsNil, err == nil, err) + continue + } + if !bytes.Equal(fm, []byte(test.extracted)) { + t.Logf("\n%q\n", string(test.frontmatter)) + t.Errorf("[%d] Frontmatter did not match:\nexp: %q\ngot: %q", i, string(test.extracted), fm) + } + } +} diff --git a/releaser/git.go b/releaser/git.go new file mode 100644 index 000000000..5cfce5464 --- /dev/null +++ b/releaser/git.go @@ -0,0 +1,296 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package releaser + +import ( + "fmt" + "os/exec" + "regexp" + "sort" + "strconv" + "strings" +) + +var issueRe = regexp.MustCompile(`(?i)[Updates?|Closes?|Fix.*|See] #(\d+)`) + +const ( + notesChanges = "notesChanges" + templateChanges = "templateChanges" + coreChanges = "coreChanges" + outChanges = "outChanges" + docsChanges = "docsChanges" + otherChanges = "otherChanges" +) + +type changeLog struct { + Version string + Enhancements map[string]gitInfos + Fixes map[string]gitInfos + Notes gitInfos + All gitInfos + + // Overall stats + Repo *gitHubRepo + ContributorCount int + ThemeCount int +} + +func newChangeLog(infos gitInfos) *changeLog { + return &changeLog{ + Enhancements: make(map[string]gitInfos), + Fixes: make(map[string]gitInfos), + All: infos, + } +} + +func (l *changeLog) addGitInfo(isFix bool, info gitInfo, category string) { + var ( + infos gitInfos + found bool + segment map[string]gitInfos + ) + + if category == notesChanges { + l.Notes = append(l.Notes, info) + return + } else if isFix { + segment = l.Fixes + } else { + segment = l.Enhancements + } + + infos, found = segment[category] + if !found { + infos = gitInfos{} + } + + infos = append(infos, info) + segment[category] = infos +} + +func gitInfosToChangeLog(infos gitInfos) *changeLog { + log := newChangeLog(infos) + for _, info := range infos { + los := strings.ToLower(info.Subject) + isFix := strings.Contains(los, "fix") + var category = otherChanges + + // TODO(bep) improve + if regexp.MustCompile("(?i)deprecate").MatchString(los) { + category = notesChanges + } else if regexp.MustCompile("(?i)tpl|tplimpl:|layout").MatchString(los) { + category = templateChanges + } else if regexp.MustCompile("(?i)docs?:|documentation:").MatchString(los) { + category = docsChanges + } else if regexp.MustCompile("(?i)hugolib:").MatchString(los) { + category = coreChanges + } else if regexp.MustCompile("(?i)out(put)?:|media:|Output|Media").MatchString(los) { + category = outChanges + } + + // Trim package prefix. + colonIdx := strings.Index(info.Subject, ":") + if colonIdx != -1 && colonIdx < (len(info.Subject)/2) { + info.Subject = info.Subject[colonIdx+1:] + } + + info.Subject = strings.TrimSpace(info.Subject) + + log.addGitInfo(isFix, info, category) + } + + return log +} + +type gitInfo struct { + Hash string + Author string + Subject string + Body string + + GitHubCommit *gitHubCommit +} + +func (g gitInfo) Issues() []int { + return extractIssues(g.Body) +} + +func (g gitInfo) AuthorID() string { + if g.GitHubCommit != nil { + return g.GitHubCommit.Author.Login + } + return g.Author +} + +func extractIssues(body string) []int { + var i []int + m := issueRe.FindAllStringSubmatch(body, -1) + for _, mm := range m { + issueID, err := strconv.Atoi(mm[1]) + if err != nil { + continue + } + i = append(i, issueID) + } + return i +} + +type gitInfos []gitInfo + +func git(args ...string) (string, error) { + cmd := exec.Command("git", args...) + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("git failed: %q: %q (%q)", err, out, args) + } + return string(out), nil +} + +func getGitInfos(tag string, remote bool) (gitInfos, error) { + return getGitInfosBefore("HEAD", tag, remote) +} + +type countribCount struct { + Author string + GitHubAuthor gitHubAuthor + Count int +} + +func (c countribCount) AuthorLink() string { + if c.GitHubAuthor.HtmlURL != "" { + return fmt.Sprintf("[@%s](%s)", c.GitHubAuthor.Login, c.GitHubAuthor.HtmlURL) + } + + if !strings.Contains(c.Author, "@") { + return c.Author + } + + return c.Author[:strings.Index(c.Author, "@")] + +} + +type contribCounts []countribCount + +func (c contribCounts) Less(i, j int) bool { return c[i].Count > c[j].Count } +func (c contribCounts) Len() int { return len(c) } +func (c contribCounts) Swap(i, j int) { c[i], c[j] = c[j], c[i] } + +func (g gitInfos) ContribCountPerAuthor() contribCounts { + var c contribCounts + + counters := make(map[string]countribCount) + + for _, gi := range g { + authorID := gi.AuthorID() + if count, ok := counters[authorID]; ok { + count.Count = count.Count + 1 + counters[authorID] = count + } else { + var ghA gitHubAuthor + if gi.GitHubCommit != nil { + ghA = gi.GitHubCommit.Author + } + authorCount := countribCount{Count: 1, Author: gi.Author, GitHubAuthor: ghA} + counters[authorID] = authorCount + } + } + + for _, v := range counters { + c = append(c, v) + } + + sort.Sort(c) + return c +} + +func getGitInfosBefore(ref, tag string, remote bool) (gitInfos, error) { + + var g gitInfos + + log, err := gitLogBefore(ref, tag) + if err != nil { + return g, err + } + + log = strings.Trim(log, "\n\x1e'") + entries := strings.Split(log, "\x1e") + + for _, entry := range entries { + items := strings.Split(entry, "\x1f") + gi := gitInfo{ + Hash: items[0], + Author: items[1], + Subject: items[2], + Body: items[3], + } + if remote { + gc, err := fetchCommit(gi.Hash) + if err == nil { + gi.GitHubCommit = &gc + } + } + g = append(g, gi) + } + + return g, nil +} + +// Ignore autogenerated commits etc. in change log. This is a regexp. +const ignoredCommits = "releaser?:|snapcraft:" + +func gitLogBefore(ref, tag string) (string, error) { + var prevTag string + var err error + if tag != "" { + prevTag = tag + } else { + prevTag, err = gitVersionTagBefore(ref) + if err != nil { + return "", err + } + } + log, err := git("log", "-E", fmt.Sprintf("--grep=%s", ignoredCommits), "--invert-grep", "--pretty=format:%x1e%h%x1f%aE%x1f%s%x1f%b", "--abbrev-commit", prevTag+".."+ref) + if err != nil { + return ",", err + } + + return log, err +} + +func gitVersionTagBefore(ref string) (string, error) { + return gitShort("describe", "--tags", "--abbrev=0", "--always", "--match", "v[0-9]*", ref+"^") +} + +func gitLog() (string, error) { + return gitLogBefore("HEAD", "") +} + +func gitShort(args ...string) (output string, err error) { + output, err = git(args...) + return strings.Replace(strings.Split(output, "\n")[0], "'", "", -1), err +} + +func tagExists(tag string) (bool, error) { + out, err := git("tag", "-l", tag) + + if err != nil { + return false, err + } + + if strings.Contains(out, tag) { + return true, nil + } + + return false, nil +} diff --git a/releaser/git_test.go b/releaser/git_test.go new file mode 100644 index 000000000..1c102520e --- /dev/null +++ b/releaser/git_test.go @@ -0,0 +1,76 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package releaser + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGitInfos(t *testing.T) { + skipIfCI(t) + infos, err := getGitInfos("v0.20", false) + + require.NoError(t, err) + require.True(t, len(infos) > 0) + +} + +func TestIssuesRe(t *testing.T) { + + body := ` +This is a commit message. + +Updates #123 +Fix #345 +closes #543 +See #456 + ` + + issues := extractIssues(body) + + require.Len(t, issues, 4) + require.Equal(t, 123, issues[0]) + require.Equal(t, 543, issues[2]) + +} + +func TestGitVersionTagBefore(t *testing.T) { + skipIfCI(t) + v1, err := gitVersionTagBefore("v0.18") + require.NoError(t, err) + require.Equal(t, "v0.17", v1) +} + +func TestTagExists(t *testing.T) { + skipIfCI(t) + b1, err := tagExists("v0.18") + require.NoError(t, err) + require.True(t, b1) + + b2, err := tagExists("adfagdsfg") + require.NoError(t, err) + require.False(t, b2) + +} + +func skipIfCI(t *testing.T) { + if os.Getenv("CI") != "" { + // Travis has an ancient git with no --invert-grep: https://github.com/travis-ci/travis-ci/issues/6328 + // Also Travis clones very shallowly, making some of the tests above shaky. + t.Skip("Skip git test on Linux to make Travis happy.") + } +} diff --git a/releaser/github.go b/releaser/github.go new file mode 100644 index 000000000..c1e7691b8 --- /dev/null +++ b/releaser/github.go @@ -0,0 +1,129 @@ +package releaser + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" +) + +var ( + gitHubCommitsApi = "https://api.github.com/repos/gohugoio/hugo/commits/%s" + gitHubRepoApi = "https://api.github.com/repos/gohugoio/hugo" + gitHubContributorsApi = "https://api.github.com/repos/gohugoio/hugo/contributors" +) + +type gitHubCommit struct { + Author gitHubAuthor `json:"author"` + HtmlURL string `json:"html_url"` +} + +type gitHubAuthor struct { + ID int `json:"id"` + Login string `json:"login"` + HtmlURL string `json:"html_url"` + AvatarURL string `json:"avatar_url"` +} + +type gitHubRepo struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + HtmlURL string `json:"html_url"` + Stars int `json:"stargazers_count"` + Contributors []gitHubContributor +} + +type gitHubContributor struct { + ID int `json:"id"` + Login string `json:"login"` + HtmlURL string `json:"html_url"` + Contributions int `json:"contributions"` +} + +func fetchCommit(ref string) (gitHubCommit, error) { + var commit gitHubCommit + + u := fmt.Sprintf(gitHubCommitsApi, ref) + + req, err := http.NewRequest("GET", u, nil) + if err != nil { + return commit, err + } + + err = doGitHubRequest(req, &commit) + + return commit, err +} + +func fetchRepo() (gitHubRepo, error) { + var repo gitHubRepo + + req, err := http.NewRequest("GET", gitHubRepoApi, nil) + if err != nil { + return repo, err + } + + err = doGitHubRequest(req, &repo) + if err != nil { + return repo, err + } + + var contributors []gitHubContributor + page := 0 + for { + page++ + var currPage []gitHubContributor + url := fmt.Sprintf(gitHubContributorsApi+"?page=%d", page) + + req, err = http.NewRequest("GET", url, nil) + if err != nil { + return repo, err + } + + err = doGitHubRequest(req, &currPage) + if err != nil { + return repo, err + } + if len(currPage) == 0 { + break + } + + contributors = append(contributors, currPage...) + + } + + repo.Contributors = contributors + + return repo, err + +} + +func doGitHubRequest(req *http.Request, v interface{}) error { + addGitHubToken(req) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if isError(resp) { + b, _ := ioutil.ReadAll(resp.Body) + return fmt.Errorf("GitHub lookup failed: %s", string(b)) + } + + return json.NewDecoder(resp.Body).Decode(v) +} + +func isError(resp *http.Response) bool { + return resp.StatusCode < 200 || resp.StatusCode > 299 +} + +func addGitHubToken(req *http.Request) { + gitHubToken := os.Getenv("GITHUB_TOKEN") + if gitHubToken != "" { + req.Header.Add("Authorization", "token "+gitHubToken) + } +} diff --git a/releaser/github_test.go b/releaser/github_test.go new file mode 100644 index 000000000..7feae75f5 --- /dev/null +++ b/releaser/github_test.go @@ -0,0 +1,42 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package releaser + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGitHubLookupCommit(t *testing.T) { + skipIfNoToken(t) + commit, err := fetchCommit("793554108763c0984f1a1b1a6ee5744b560d78d0") + require.NoError(t, err) + fmt.Println(commit) +} + +func TestFetchRepo(t *testing.T) { + skipIfNoToken(t) + repo, err := fetchRepo() + require.NoError(t, err) + fmt.Println(">>", len(repo.Contributors)) +} + +func skipIfNoToken(t *testing.T) { + if os.Getenv("GITHUB_TOKEN") == "" { + t.Skip("Skip test against GitHub as no GITHUB_TOKEN set.") + } +} diff --git a/releaser/releasenotes_writer.go b/releaser/releasenotes_writer.go new file mode 100644 index 000000000..9dbdf61a8 --- /dev/null +++ b/releaser/releasenotes_writer.go @@ -0,0 +1,248 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package release implements a set of utilities and a wrapper around Goreleaser +// to help automate the Hugo release process. +package releaser + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" + "text/template" + "time" +) + +const ( + issueLinkTemplate = "[#%d](https://github.com/gohugoio/hugo/issues/%d)" + linkTemplate = "[%s](%s)" + releaseNotesMarkdownTemplate = ` +{{- $patchRelease := isPatch . -}} +{{- $contribsPerAuthor := .All.ContribCountPerAuthor -}} + +{{- if $patchRelease }} +{{ if eq (len .All) 1 }} +This is a bug-fix release with one important fix. +{{ else }} +This is a bug-fix release with a couple of important fixes. +{{ end }} +{{ else }} +This release represents **{{ len .All }} contributions by {{ len $contribsPerAuthor }} contributors** to the main Hugo code base. +{{ end -}} + +{{- if gt (len $contribsPerAuthor) 3 -}} +{{- $u1 := index $contribsPerAuthor 0 -}} +{{- $u2 := index $contribsPerAuthor 1 -}} +{{- $u3 := index $contribsPerAuthor 2 -}} +{{- $u4 := index $contribsPerAuthor 3 -}} +{{- $u1.AuthorLink }} leads the Hugo development with a significant amount of contributions, but also a big shoutout to {{ $u2.AuthorLink }}, {{ $u3.AuthorLink }}, and {{ $u4.AuthorLink }} for their ongoing contributions. +And as always a big thanks to [@digitalcraftsman](https://github.com/digitalcraftsman) for his relentless work on keeping the documentation and the themes site in pristine condition. +{{ end }} +Hugo now has: + +{{ with .Repo -}} +* {{ .Stars }}+ [stars](https://github.com/gohugoio/hugo/stargazers) +* {{ len .Contributors }}+ [contributors](https://github.com/gohugoio/hugo/graphs/contributors) +{{- end -}} +{{ with .ThemeCount }} +* 156+ [themes](http://themes.gohugo.io/) +{{- end }} +{{ with .Notes }} +## Notes +{{ template "change-section" . }} +{{- end -}} +## Enhancements +{{ template "change-headers" .Enhancements -}} +## Fixes +{{ template "change-headers" .Fixes -}} + +{{ define "change-headers" }} +{{ $tmplChanges := index . "templateChanges" -}} +{{- $outChanges := index . "outChanges" -}} +{{- $coreChanges := index . "coreChanges" -}} +{{- $docsChanges := index . "docsChanges" -}} +{{- $otherChanges := index . "otherChanges" -}} +{{- with $tmplChanges -}} +### Templates +{{ template "change-section" . }} +{{- end -}} +{{- with $outChanges -}} +### Output +{{ template "change-section" . }} +{{- end -}} +{{- with $coreChanges -}} +### Core +{{ template "change-section" . }} +{{- end -}} +{{- with $docsChanges -}} +### Docs +{{ template "change-section" . }} +{{- end -}} +{{- with $otherChanges -}} +### Other +{{ template "change-section" . }} +{{- end -}} +{{ end }} + + +{{ define "change-section" }} +{{ range . }} +{{- if .GitHubCommit -}} +* {{ .Subject }} {{ . | commitURL }} {{ . | authorURL }} {{ range .Issues }}{{ . | issue }}{{ end }} +{{ else -}} +* {{ .Subject }} {{ range .Issues }}{{ . | issue }}{{ end }} +{{ end -}} +{{- end }} +{{ end }} +` +) + +var templateFuncs = template.FuncMap{ + "isPatch": func(c changeLog) bool { + return strings.Count(c.Version, ".") > 1 + }, + "issue": func(id int) string { + return fmt.Sprintf(issueLinkTemplate, id, id) + }, + "commitURL": func(info gitInfo) string { + if info.GitHubCommit.HtmlURL == "" { + return "" + } + return fmt.Sprintf(linkTemplate, info.Hash, info.GitHubCommit.HtmlURL) + }, + "authorURL": func(info gitInfo) string { + if info.GitHubCommit.Author.Login == "" { + return "" + } + return fmt.Sprintf(linkTemplate, "@"+info.GitHubCommit.Author.Login, info.GitHubCommit.Author.HtmlURL) + }, +} + +func writeReleaseNotes(version string, infos gitInfos, to io.Writer) error { + changes := gitInfosToChangeLog(infos) + changes.Version = version + repo, err := fetchRepo() + if err == nil { + changes.Repo = &repo + } + themeCount, err := fetchThemeCount() + if err == nil { + changes.ThemeCount = themeCount + } + + tmpl, err := template.New("").Funcs(templateFuncs).Parse(releaseNotesMarkdownTemplate) + if err != nil { + return err + } + + err = tmpl.Execute(to, changes) + if err != nil { + return err + } + + return nil + +} + +func fetchThemeCount() (int, error) { + resp, err := http.Get("https://raw.githubusercontent.com/gohugoio/hugoThemes/master/.gitmodules") + if err != nil { + return 0, err + } + defer resp.Body.Close() + + b, _ := ioutil.ReadAll(resp.Body) + return bytes.Count(b, []byte("submodule")), nil +} + +func writeReleaseNotesToTmpFile(version string, infos gitInfos) (string, error) { + f, err := ioutil.TempFile("", "hugorelease") + if err != nil { + return "", err + } + + defer f.Close() + + if err := writeReleaseNotes(version, infos, f); err != nil { + return "", err + } + + return f.Name(), nil +} + +func getReleaseNotesDocsTempDirAndName(version string) (string, string) { + return hugoFilepath("temp"), fmt.Sprintf("%s-relnotes.md", version) +} + +func getReleaseNotesDocsTempFilename(version string) string { + return filepath.Join(getReleaseNotesDocsTempDirAndName(version)) +} + +func writeReleaseNotesToTemp(version string, infos gitInfos) (string, error) { + docsTempPath, name := getReleaseNotesDocsTempDirAndName(version) + os.Mkdir(docsTempPath, os.ModePerm) + + f, err := os.Create(filepath.Join(docsTempPath, name)) + if err != nil { + return "", err + } + + defer f.Close() + + if err := writeReleaseNotes(version, infos, f); err != nil { + return "", err + } + + return f.Name(), nil + +} + +func writeReleaseNotesToDocs(title, sourceFilename string) (string, error) { + targetFilename := filepath.Base(sourceFilename) + contentDir := hugoFilepath("docs/content/release-notes") + targetFullFilename := filepath.Join(contentDir, targetFilename) + os.Mkdir(contentDir, os.ModePerm) + + b, err := ioutil.ReadFile(sourceFilename) + if err != nil { + return "", err + } + + f, err := os.Create(targetFullFilename) + if err != nil { + return "", err + } + defer f.Close() + + if _, err := f.WriteString(fmt.Sprintf(` +--- +date: %s +title: %s +--- + + `, time.Now().Format("2006-01-02"), title)); err != nil { + return "", err + } + + if _, err := f.Write(b); err != nil { + return "", err + } + + return targetFullFilename, nil + +} diff --git a/releaser/releasenotes_writer_test.go b/releaser/releasenotes_writer_test.go new file mode 100644 index 000000000..1b759b1d9 --- /dev/null +++ b/releaser/releasenotes_writer_test.go @@ -0,0 +1,44 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package commands defines and implements command-line commands and flags +// used by Hugo. Commands and flags are implemented using Cobra. + +package releaser + +import ( + "bytes" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func _TestReleaseNotesWriter(t *testing.T) { + if os.Getenv("CI") != "" { + // Travis has an ancient git with no --invert-grep: https://github.com/travis-ci/travis-ci/issues/6328 + t.Skip("Skip git test on CI to make Travis happy.") + } + + var b bytes.Buffer + + // TODO(bep) consider to query GitHub directly for the gitlog with author info, probably faster. + infos, err := getGitInfosBefore("HEAD", "v0.20", false) + require.NoError(t, err) + + require.NoError(t, writeReleaseNotes("0.21", infos, &b)) + + fmt.Println(b.String()) + +} diff --git a/releaser/releaser.go b/releaser/releaser.go new file mode 100644 index 000000000..f3b4358a6 --- /dev/null +++ b/releaser/releaser.go @@ -0,0 +1,331 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package releaser implements a set of utilities and a wrapper around Goreleaser +// to help automate the Hugo release process. +package releaser + +import ( + "errors" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "regexp" + + "github.com/gohugoio/hugo/helpers" +) + +const commitPrefix = "releaser:" + +type ReleaseHandler struct { + patch int + + // If set, we do the releases in 3 steps: + // 1: Create and write a draft release notes + // 2: Prepare files for new version. + // 3: Release + step int + skipPublish bool +} + +func (r ReleaseHandler) shouldRelease() bool { + return r.step < 1 || r.shouldContinue() +} + +func (r ReleaseHandler) shouldContinue() bool { + return r.step == 3 +} + +func (r ReleaseHandler) shouldPrepareReleasenotes() bool { + return r.step < 1 || r.step == 1 +} + +func (r ReleaseHandler) shouldPrepareVersions() bool { + return r.step < 1 || r.step == 2 +} + +func (r ReleaseHandler) calculateVersions(current helpers.HugoVersion) (helpers.HugoVersion, helpers.HugoVersion) { + var ( + newVersion = current + finalVersion = current + ) + + newVersion.Suffix = "" + + if r.shouldContinue() { + // The version in the current code base is in the state we want for + // the release. + if r.patch == 0 { + finalVersion = newVersion.Next() + } + } else if r.patch > 0 { + newVersion = helpers.CurrentHugoVersion.NextPatchLevel(r.patch) + } else { + finalVersion = newVersion.Next() + } + + finalVersion.Suffix = "-DEV" + + return newVersion, finalVersion +} + +func New(patch, step int, skipPublish bool) *ReleaseHandler { + return &ReleaseHandler{patch: patch, step: step, skipPublish: skipPublish} +} + +func (r *ReleaseHandler) Run() error { + if os.Getenv("GITHUB_TOKEN") == "" { + return errors.New("GITHUB_TOKEN not set, create one here with the repo scope selected: https://github.com/settings/tokens/new") + } + + newVersion, finalVersion := r.calculateVersions(helpers.CurrentHugoVersion) + + version := newVersion.String() + tag := "v" + version + + // Exit early if tag already exists + exists, err := tagExists(tag) + if err != nil { + return err + } + + if exists { + return fmt.Errorf("Tag %q already exists", tag) + } + + var changeLogFromTag string + + if newVersion.PatchLevel == 0 { + // There may have been patch releases inbetween, so set the tag explicitly. + changeLogFromTag = "v" + newVersion.Prev().String() + exists, _ := tagExists(changeLogFromTag) + if !exists { + // fall back to one that exists. + changeLogFromTag = "" + } + } + + var gitCommits gitInfos + + if r.shouldPrepareReleasenotes() || r.shouldRelease() { + gitCommits, err = getGitInfos(changeLogFromTag, true) + if err != nil { + return err + } + } + + if r.shouldPrepareReleasenotes() { + releaseNotesFile, err := writeReleaseNotesToTemp(version, gitCommits) + if err != nil { + return err + } + + if _, err := git("add", releaseNotesFile); err != nil { + return err + } + if _, err := git("commit", "-m", fmt.Sprintf("%s Add release notes draft for %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil { + return err + } + } + + if r.shouldPrepareVersions() { + if newVersion.PatchLevel == 0 { + // Make sure the docs submodule is up to date. + // TODO(bep) improve this. Maybe it was not such a good idea to do + // this in the sobmodule directly. + if _, err := git("submodule", "update", "--init"); err != nil { + return err + } + //git submodule update + if _, err := git("submodule", "update", "--remote", "--merge"); err != nil { + return err + } + + // TODO(bep) the above may not have changed anything. + if _, err := git("commit", "-a", "-m", fmt.Sprintf("%s Update /docs [ci skip]", commitPrefix)); err != nil { + return err + } + } + + if err := bumpVersions(newVersion); err != nil { + return err + } + + for _, repo := range []string{"docs", "."} { + if _, err := git("-C", repo, "commit", "-a", "-m", fmt.Sprintf("%s Bump versions for release of %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil { + return err + } + } + } + + if !r.shouldRelease() { + fmt.Println("Skip release ... Use --state=3 to continue.") + return nil + } + + releaseNotesFile := getReleaseNotesDocsTempFilename(version) + + // Write the release notes to the docs site as well. + docFile, err := writeReleaseNotesToDocs(version, releaseNotesFile) + if err != nil { + return err + } + + if _, err := git("-C", "docs", "add", docFile); err != nil { + return err + } + if _, err := git("-C", "docs", "commit", "-m", fmt.Sprintf("%s Add release notes to /docs for release of %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil { + return err + } + + for i, repo := range []string{"docs", "."} { + if i == 1 { + if _, err := git("add", "docs"); err != nil { + return err + } + if _, err := git("commit", "-m", fmt.Sprintf("%s Update /docs to %s [ci skip]", commitPrefix, newVersion)); err != nil { + return err + } + } + if _, err := git("-C", repo, "tag", "-a", tag, "-m", fmt.Sprintf("%s %s [ci deploy]", commitPrefix, newVersion)); err != nil { + return err + } + + repoURL := "[email protected]:gohugoio/hugo.git" + if i == 0 { + repoURL = "[email protected]:gohugoio/hugoDocs.git" + } + if _, err := git("-C", repo, "push", repoURL, "origin/master", tag); err != nil { + return err + } + } + + // We make changes to the submodule, which is in detached state. Reconsider this + // to get changes pushed to both. + // TODO(bep) git fetch [email protected]:gohugoio/hugoDocs.git -- master + // git branch -f master 8c9359b + + if err := r.release(releaseNotesFile); err != nil { + return err + } + + if err := bumpVersions(finalVersion); err != nil { + return err + } + + // No longer needed. + if err := os.Remove(releaseNotesFile); err != nil { + return err + } + + for _, repo := range []string{"docs", "."} { + if _, err := git("-C", repo, "commit", "-a", "-m", fmt.Sprintf("%s Prepare repository for %s\n\n[ci skip]", commitPrefix, finalVersion)); err != nil { + return err + } + } + + return nil +} + +func (r *ReleaseHandler) release(releaseNotesFile string) error { + cmd := exec.Command("goreleaser", "--release-notes", releaseNotesFile, "--skip-publish="+fmt.Sprint(r.skipPublish)) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + return fmt.Errorf("goreleaser failed: %s", err) + } + return nil +} + +func bumpVersions(ver helpers.HugoVersion) error { + fromDev := "" + toDev := "" + + if ver.Suffix != "" { + toDev = "-DEV" + } else { + fromDev = "-DEV" + } + + if err := replaceInFile("helpers/hugo.go", + `Number:(\s{4,})(.*),`, fmt.Sprintf(`Number:${1}%.2f,`, ver.Number), + `PatchLevel:(\s*)(.*),`, fmt.Sprintf(`PatchLevel:${1}%d,`, ver.PatchLevel), + fmt.Sprintf(`Suffix:(\s{4,})"%s",`, fromDev), fmt.Sprintf(`Suffix:${1}"%s",`, toDev)); err != nil { + return err + } + + snapcraftGrade := "stable" + if ver.Suffix != "" { + snapcraftGrade = "devel" + } + if err := replaceInFile("snapcraft.yaml", + `version: "(.*)"`, fmt.Sprintf(`version: "%s"`, ver), + `grade: (.*) #`, fmt.Sprintf(`grade: %s #`, snapcraftGrade)); err != nil { + return err + } + + var minVersion string + if ver.Suffix != "" { + // People use the DEV version in daily use, and we cannot create new themes + // with the next version before it is released. + minVersion = ver.Prev().String() + } else { + minVersion = ver.String() + } + + if err := replaceInFile("commands/new.go", + `min_version = "(.*)"`, fmt.Sprintf(`min_version = "%s"`, minVersion)); err != nil { + return err + } + + // docs/config.toml + if err := replaceInFile("docs/config.toml", + `release = "(.*)"`, fmt.Sprintf(`release = "%s"`, ver)); err != nil { + return err + } + + return nil +} + +func replaceInFile(filename string, oldNew ...string) error { + fullFilename := hugoFilepath(filename) + fi, err := os.Stat(fullFilename) + if err != nil { + return err + } + + b, err := ioutil.ReadFile(fullFilename) + if err != nil { + return err + } + newContent := string(b) + + for i := 0; i < len(oldNew); i += 2 { + re := regexp.MustCompile(oldNew[i]) + newContent = re.ReplaceAllString(newContent, oldNew[i+1]) + } + + return ioutil.WriteFile(fullFilename, []byte(newContent), fi.Mode()) +} + +func hugoFilepath(filename string) string { + pwd, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + return filepath.Join(pwd, filename) +} diff --git a/releaser/releaser_test.go b/releaser/releaser_test.go new file mode 100644 index 000000000..c20e17bf1 --- /dev/null +++ b/releaser/releaser_test.go @@ -0,0 +1,79 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package commands defines and implements command-line commands and flags +// used by Hugo. Commands and flags are implemented using Cobra. + +package releaser + +import ( + "testing" + + "github.com/gohugoio/hugo/helpers" + "github.com/stretchr/testify/require" +) + +// TODO(bep) fixme +func _TestCalculateVersions(t *testing.T) { + startVersion := helpers.HugoVersion{Number: 0.20, Suffix: "-DEV"} + + tests := []struct { + handler *ReleaseHandler + version helpers.HugoVersion + v1 string + v2 string + }{ + { + New(0, 0, true), + startVersion, + "0.20", + "0.21-DEV", + }, + { + New(2, 0, true), + startVersion, + "0.20.2", + "0.20-DEV", + }, + { + New(0, 1, true), + startVersion, + "0.20", + "0.21-DEV", + }, + { + New(0, 3, true), + startVersion, + "0.20", + "0.21-DEV", + }, + { + New(3, 1, true), + startVersion, + "0.20.3", + "0.20-DEV", + }, + { + New(3, 3, true), + startVersion.Next(), + "0.21", + "0.21-DEV", + }, + } + + for _, test := range tests { + v1, v2 := test.handler.calculateVersions(test.version) + require.Equal(t, test.v1, v1.String(), "Release version") + require.Equal(t, test.v2, v2.String(), "Final version") + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..e0f2f62df --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Pygments==2.1.3 diff --git a/snapcraft.yaml b/snapcraft.yaml new file mode 100644 index 000000000..8ee990890 --- /dev/null +++ b/snapcraft.yaml @@ -0,0 +1,37 @@ +name: hugo +version: "0.25-DEV" +summary: Fast and Flexible Static Site Generator +description: | + Hugo is a static HTML and CSS website generator written in Go. It is + optimized for speed, easy use and configurability. Hugo takes a directory + with content and templates and renders them into a full HTML website. +confinement: strict +grade: devel # "devel" or "stable" + +apps: + hugo: + command: bin/hugo + plugs: [home, network-bind] + +parts: + hugo: + source: . + plugin: go + go-importpath: github.com/gohugoio/hugo + build-packages: + - git + - make + stage-packages: + - python-pygments + prepare: | + export GOPATH=$(dirname $SNAPCRAFT_PART_INSTALL)/go + export PATH=$GOPATH/bin:$PATH + cd $GOPATH/src/github.com/gohugoio/hugo + make vendor + make test + rm -f $GOPATH/bin/govendor + install: | + strip --remove-section=.comment --remove-section=.note $SNAPCRAFT_PART_INSTALL/bin/hugo + after: [go] + go: + source-tag: go1.8.3 diff --git a/source/content_directory_test.go b/source/content_directory_test.go new file mode 100644 index 000000000..4ff12af8d --- /dev/null +++ b/source/content_directory_test.go @@ -0,0 +1,61 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package source + +import ( + "testing" + + "github.com/gohugoio/hugo/hugofs" + "github.com/spf13/viper" +) + +func TestIgnoreDotFilesAndDirectories(t *testing.T) { + + tests := []struct { + path string + ignore bool + ignoreFilesRegexpes interface{} + }{ + {".foobar/", true, nil}, + {"foobar/.barfoo/", true, nil}, + {"barfoo.md", false, nil}, + {"foobar/barfoo.md", false, nil}, + {"foobar/.barfoo.md", true, nil}, + {".barfoo.md", true, nil}, + {".md", true, nil}, + {"", true, nil}, + {"foobar/barfoo.md~", true, nil}, + {".foobar/barfoo.md~", true, nil}, + {"foobar~/barfoo.md", false, nil}, + {"foobar/bar~foo.md", false, nil}, + {"foobar/foo.md", true, []string{"\\.md$", "\\.boo$"}}, + {"foobar/foo.html", false, []string{"\\.md$", "\\.boo$"}}, + {"foobar/foo.md", true, []string{"^foo"}}, + {"foobar/foo.md", false, []string{"*", "\\.md$", "\\.boo$"}}, + {"foobar/.#content.md", true, []string{"/\\.#"}}, + {".#foobar.md", true, []string{"^\\.#"}}, + } + + for _, test := range tests { + + v := viper.New() + v.Set("ignoreFiles", test.ignoreFilesRegexpes) + + s := NewSourceSpec(v, hugofs.NewMem(v)) + + if ignored := s.isNonProcessablePath(test.path); test.ignore != ignored { + t.Errorf("File not ignored. Expected: %t, got: %t", test.ignore, ignored) + } + } +} diff --git a/source/file.go b/source/file.go new file mode 100644 index 000000000..e2322bc4c --- /dev/null +++ b/source/file.go @@ -0,0 +1,170 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package source + +import ( + "io" + "path/filepath" + "strings" + + "github.com/gohugoio/hugo/hugofs" + + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/helpers" +) + +type SourceSpec struct { + Cfg config.Provider + Fs *hugofs.Fs + + languages map[string]interface{} + defaultContentLanguage string +} + +func NewSourceSpec(cfg config.Provider, fs *hugofs.Fs) SourceSpec { + defaultLang := cfg.GetString("defaultContentLanguage") + languages := cfg.GetStringMap("languages") + return SourceSpec{Cfg: cfg, Fs: fs, languages: languages, defaultContentLanguage: defaultLang} +} + +// File represents a source content file. +// All paths are relative from the source directory base +type File struct { + relpath string // Original relative path, e.g. section/foo.txt + logicalName string // foo.txt + baseName string // `post` for `post.md`, also `post.en` for `post.en.md` + Contents io.Reader + section string // The first directory + dir string // The relative directory Path (minus file name) + ext string // Just the ext (eg txt) + uniqueID string // MD5 of the file's path + + translationBaseName string // `post` for `post.es.md` (if `Multilingual` is enabled.) + lang string // The language code if `Multilingual` is enabled +} + +// UniqueID is the MD5 hash of the file's path and is for most practical applications, +// Hugo content files being one of them, considered to be unique. +func (f *File) UniqueID() string { + return f.uniqueID +} + +// String returns the file's content as a string. +func (f *File) String() string { + return helpers.ReaderToString(f.Contents) +} + +// Bytes returns the file's content as a byte slice. +func (f *File) Bytes() []byte { + return helpers.ReaderToBytes(f.Contents) +} + +// BaseFileName is a filename without extension. +func (f *File) BaseFileName() string { + return f.baseName +} + +// TranslationBaseName is a filename with no extension, +// not even the optional language extension part. +func (f *File) TranslationBaseName() string { + return f.translationBaseName +} + +// Lang for this page, if `Multilingual` is enabled on your site. +func (f *File) Lang() string { + return f.lang +} + +// Section is first directory below the content root. +func (f *File) Section() string { + return f.section +} + +// LogicalName is filename and extension of the file. +func (f *File) LogicalName() string { + return f.logicalName +} + +// SetDir sets the relative directory where this file lives. +// TODO(bep) Get rid of this. +func (f *File) SetDir(dir string) { + f.dir = dir +} + +// Dir gets the name of the directory that contains this file. +// The directory is relative to the content root. +func (f *File) Dir() string { + return f.dir +} + +// Extension gets the file extension, i.e "myblogpost.md" will return "md". +func (f *File) Extension() string { + return f.ext +} + +// Ext is an alias for Extension. +func (f *File) Ext() string { + return f.Extension() +} + +// Path gets the relative path including file name and extension. +// The directory is relative to the content root. +func (f *File) Path() string { + return f.relpath +} + +// NewFileWithContents creates a new File pointer with the given relative path and +// content. The language defaults to "en". +func (sp SourceSpec) NewFileWithContents(relpath string, content io.Reader) *File { + file := sp.NewFile(relpath) + file.Contents = content + file.lang = "en" + return file +} + +// NewFile creates a new File pointer with the given relative path. +func (sp SourceSpec) NewFile(relpath string) *File { + f := &File{ + relpath: relpath, + } + + f.dir, f.logicalName = filepath.Split(f.relpath) + f.ext = strings.TrimPrefix(filepath.Ext(f.LogicalName()), ".") + f.baseName = helpers.Filename(f.LogicalName()) + + lang := strings.TrimPrefix(filepath.Ext(f.baseName), ".") + if _, ok := sp.languages[lang]; lang == "" || !ok { + f.lang = sp.defaultContentLanguage + f.translationBaseName = f.baseName + } else { + f.lang = lang + f.translationBaseName = helpers.Filename(f.baseName) + } + + f.section = helpers.GuessSection(f.Dir()) + f.uniqueID = helpers.Md5String(f.Path()) + + return f +} + +// NewFileFromAbs creates a new File pointer with the given full file path path and +// content. +func (sp SourceSpec) NewFileFromAbs(base, fullpath string, content io.Reader) (f *File, err error) { + var name string + if name, err = helpers.GetRelativePath(fullpath, base); err != nil { + return nil, err + } + + return sp.NewFileWithContents(name, content), nil +} diff --git a/source/file_test.go b/source/file_test.go new file mode 100644 index 000000000..a152b4bf5 --- /dev/null +++ b/source/file_test.go @@ -0,0 +1,57 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package source + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/gohugoio/hugo/hugofs" + "github.com/spf13/viper" + + "github.com/stretchr/testify/assert" +) + +func TestFileUniqueID(t *testing.T) { + ss := newTestSourceSpec() + + f1 := File{uniqueID: "123"} + f2 := ss.NewFile("a") + + assert.Equal(t, "123", f1.UniqueID()) + assert.Equal(t, "0cc175b9c0f1b6a831c399e269772661", f2.UniqueID()) + + f3 := ss.NewFile(filepath.FromSlash("test1/index.md")) + f4 := ss.NewFile(filepath.FromSlash("test2/index.md")) + + assert.NotEqual(t, f3.UniqueID(), f4.UniqueID()) +} + +func TestFileString(t *testing.T) { + ss := newTestSourceSpec() + assert.Equal(t, "abc", ss.NewFileWithContents("a", strings.NewReader("abc")).String()) + assert.Equal(t, "", ss.NewFile("a").String()) +} + +func TestFileBytes(t *testing.T) { + ss := newTestSourceSpec() + assert.Equal(t, []byte("abc"), ss.NewFileWithContents("a", strings.NewReader("abc")).Bytes()) + assert.Equal(t, []byte(""), ss.NewFile("a").Bytes()) +} + +func newTestSourceSpec() SourceSpec { + v := viper.New() + return SourceSpec{Fs: hugofs.NewMem(v), Cfg: v} +} diff --git a/source/filesystem.go b/source/filesystem.go new file mode 100644 index 000000000..446d8c06d --- /dev/null +++ b/source/filesystem.go @@ -0,0 +1,181 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package source + +import ( + "io" + "os" + "path/filepath" + "regexp" + "runtime" + "strings" + + "github.com/gohugoio/hugo/helpers" + "github.com/spf13/cast" + jww "github.com/spf13/jwalterweatherman" + "golang.org/x/text/unicode/norm" +) + +type Input interface { + Files() []*File +} + +type Filesystem struct { + files []*File + Base string + AvoidPaths []string + + SourceSpec +} + +func (sp SourceSpec) NewFilesystem(base string, avoidPaths ...string) *Filesystem { + return &Filesystem{SourceSpec: sp, Base: base, AvoidPaths: avoidPaths} +} + +func (f *Filesystem) FilesByExts(exts ...string) []*File { + var newFiles []*File + + if len(exts) == 0 { + return f.Files() + } + + for _, x := range f.Files() { + for _, e := range exts { + if x.Ext() == strings.TrimPrefix(e, ".") { + newFiles = append(newFiles, x) + } + } + } + return newFiles +} + +func (f *Filesystem) Files() []*File { + if len(f.files) < 1 { + f.captureFiles() + } + return f.files +} + +// add populates a file in the Filesystem.files +func (f *Filesystem) add(name string, reader io.Reader) (err error) { + var file *File + + if runtime.GOOS == "darwin" { + // When a file system is HFS+, its filepath is in NFD form. + name = norm.NFC.String(name) + } + + file, err = f.SourceSpec.NewFileFromAbs(f.Base, name, reader) + + if err == nil { + f.files = append(f.files, file) + } + return err +} + +func (f *Filesystem) captureFiles() { + walker := func(filePath string, fi os.FileInfo, err error) error { + if err != nil { + return nil + } + + b, err := f.ShouldRead(filePath, fi) + if err != nil { + return err + } + if b { + rd, err := NewLazyFileReader(f.Fs.Source, filePath) + if err != nil { + return err + } + f.add(filePath, rd) + } + return err + } + + if f.Fs == nil { + panic("Must have a fs") + } + err := helpers.SymbolicWalk(f.Fs.Source, f.Base, walker) + + if err != nil { + jww.ERROR.Println(err) + if err == helpers.ErrWalkRootTooShort { + panic("The root path is too short. If this is a test, make sure to init the content paths.") + } + } + +} + +func (f *Filesystem) ShouldRead(filePath string, fi os.FileInfo) (bool, error) { + if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + link, err := filepath.EvalSymlinks(filePath) + if err != nil { + jww.ERROR.Printf("Cannot read symbolic link '%s', error was: %s", filePath, err) + return false, nil + } + linkfi, err := f.Fs.Source.Stat(link) + if err != nil { + jww.ERROR.Printf("Cannot stat '%s', error was: %s", link, err) + return false, nil + } + if !linkfi.Mode().IsRegular() { + jww.ERROR.Printf("Symbolic links for directories not supported, skipping '%s'", filePath) + } + return false, nil + } + + if fi.IsDir() { + if f.avoid(filePath) || f.isNonProcessablePath(filePath) { + return false, filepath.SkipDir + } + return false, nil + } + + if f.isNonProcessablePath(filePath) { + return false, nil + } + return true, nil +} + +func (f *Filesystem) avoid(filePath string) bool { + for _, avoid := range f.AvoidPaths { + if avoid == filePath { + return true + } + } + return false +} + +func (s SourceSpec) isNonProcessablePath(filePath string) bool { + base := filepath.Base(filePath) + if strings.HasPrefix(base, ".") || + strings.HasPrefix(base, "#") || + strings.HasSuffix(base, "~") { + return true + } + ignoreFiles := cast.ToStringSlice(s.Cfg.Get("ignoreFiles")) + if len(ignoreFiles) > 0 { + for _, ignorePattern := range ignoreFiles { + match, err := regexp.MatchString(ignorePattern, filePath) + if err != nil { + helpers.DistinctErrorLog.Printf("Invalid regexp '%s' in ignoreFiles: %s", ignorePattern, err) + return false + } else if match { + return true + } + } + } + return false +} diff --git a/source/filesystem_test.go b/source/filesystem_test.go new file mode 100644 index 000000000..90512ce3f --- /dev/null +++ b/source/filesystem_test.go @@ -0,0 +1,113 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package source + +import ( + "bytes" + "path/filepath" + "runtime" + "strings" + "testing" +) + +func TestEmptySourceFilesystem(t *testing.T) { + ss := newTestSourceSpec() + src := ss.NewFilesystem("Empty") + if len(src.Files()) != 0 { + t.Errorf("new filesystem should contain 0 files.") + } +} + +type TestPath struct { + filename string + logical string + content string + section string + dir string +} + +func TestAddFile(t *testing.T) { + ss := newTestSourceSpec() + tests := platformPaths + for _, test := range tests { + base := platformBase + srcDefault := ss.NewFilesystem("") + srcWithBase := ss.NewFilesystem(base) + + for _, src := range []*Filesystem{srcDefault, srcWithBase} { + + p := test.filename + if !filepath.IsAbs(test.filename) { + p = filepath.Join(src.Base, test.filename) + } + + if err := src.add(p, bytes.NewReader([]byte(test.content))); err != nil { + if err.Error() == "source: missing base directory" { + continue + } + t.Fatalf("%s add returned an error: %s", p, err) + } + + if len(src.Files()) != 1 { + t.Fatalf("%s Files() should return 1 file", p) + } + + f := src.Files()[0] + if f.LogicalName() != test.logical { + t.Errorf("Filename (Base: %q) expected: %q, got: %q", src.Base, test.logical, f.LogicalName()) + } + + b := new(bytes.Buffer) + b.ReadFrom(f.Contents) + if b.String() != test.content { + t.Errorf("File (Base: %q) contents should be %q, got: %q", src.Base, test.content, b.String()) + } + + if f.Section() != test.section { + t.Errorf("File section (Base: %q) expected: %q, got: %q", src.Base, test.section, f.Section()) + } + + if f.Dir() != test.dir { + t.Errorf("Dir path (Base: %q) expected: %q, got: %q", src.Base, test.dir, f.Dir()) + } + } + } +} + +func TestUnicodeNorm(t *testing.T) { + if runtime.GOOS != "darwin" { + // Normalization code is only for Mac OS, since it is not necessary for other OSes. + return + } + + paths := []struct { + NFC string + NFD string + }{ + {NFC: "å", NFD: "\x61\xcc\x8a"}, + {NFC: "é", NFD: "\x65\xcc\x81"}, + } + + ss := newTestSourceSpec() + + for _, path := range paths { + src := ss.NewFilesystem("") + _ = src.add(path.NFD, strings.NewReader("")) + f := src.Files()[0] + if f.BaseFileName() != path.NFC { + t.Fatalf("file name in NFD form should be normalized (%s)", path.NFC) + } + } + +} diff --git a/source/filesystem_unix_test.go b/source/filesystem_unix_test.go new file mode 100644 index 000000000..560d824ae --- /dev/null +++ b/source/filesystem_unix_test.go @@ -0,0 +1,28 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build linux darwin !windows + +package source + +// +// NOTE, any changes here need to be reflected in filesystem_windows_test.go +// +var platformBase = "/base/" +var platformPaths = []TestPath{ + {"foobar", "foobar", "aaa", "", ""}, + {"b/1file", "1file", "aaa", "b", "b/"}, + {"c/d/2file", "2file", "aaa", "c", "c/d/"}, + {"/base/e/f/3file", "3file", "aaa", "e", "e/f/"}, + {"section/foo.rss", "foo.rss", "aaa", "section", "section/"}, +} diff --git a/source/filesystem_windows_test.go b/source/filesystem_windows_test.go new file mode 100644 index 000000000..4662c5fd6 --- /dev/null +++ b/source/filesystem_windows_test.go @@ -0,0 +1,28 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package source + +// +// NOTE, any changes here need to be reflected in filesystem_linux_test.go +// + +// Note the case of the volume drive. It must be the same in all examples. +var platformBase = "C:\\foo\\" +var platformPaths = []TestPath{ + {"foobar", "foobar", "aaa", "", ""}, + {"b\\1file", "1file", "aaa", "b", "b\\"}, + {"c\\d\\2file", "2file", "aaa", "c", "c\\d\\"}, + {"C:\\foo\\e\\f\\3file", "3file", "aaa", "e", "e\\f\\"}, // note volume case is equal to platformBase + {"section\\foo.rss", "foo.rss", "aaa", "section", "section\\"}, +} diff --git a/source/inmemory.go b/source/inmemory.go new file mode 100644 index 000000000..431236a56 --- /dev/null +++ b/source/inmemory.go @@ -0,0 +1,23 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package source + +type ByteSource struct { + Name string + Content []byte +} + +func (b *ByteSource) String() string { + return b.Name + " " + string(b.Content) +} diff --git a/source/lazy_file_reader.go b/source/lazy_file_reader.go new file mode 100644 index 000000000..7cc484f0b --- /dev/null +++ b/source/lazy_file_reader.go @@ -0,0 +1,170 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// Portions Copyright 2009 The Go Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package source + +import ( + "bytes" + "errors" + "fmt" + "io" + + "github.com/spf13/afero" +) + +// LazyFileReader is an io.Reader implementation to postpone reading the file +// contents until it is really needed. It keeps filename and file contents once +// it is read. +type LazyFileReader struct { + fs afero.Fs + filename string + contents *bytes.Reader + pos int64 +} + +// NewLazyFileReader creates and initializes a new LazyFileReader of filename. +// It checks whether the file can be opened. If it fails, it returns nil and an +// error. +func NewLazyFileReader(fs afero.Fs, filename string) (*LazyFileReader, error) { + f, err := fs.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + return &LazyFileReader{fs: fs, filename: filename, contents: nil, pos: 0}, nil +} + +// Filename returns a file name which LazyFileReader keeps +func (l *LazyFileReader) Filename() string { + return l.filename +} + +// Read reads up to len(p) bytes from the LazyFileReader's file and copies them +// into p. It returns the number of bytes read and any error encountered. If +// the file is once read, it returns its contents from cache, doesn't re-read +// the file. +func (l *LazyFileReader) Read(p []byte) (n int, err error) { + if l.contents == nil { + b, err := afero.ReadFile(l.fs, l.filename) + if err != nil { + return 0, fmt.Errorf("failed to read content from %s: %s", l.filename, err.Error()) + } + l.contents = bytes.NewReader(b) + } + if _, err = l.contents.Seek(l.pos, 0); err != nil { + return 0, errors.New("failed to set read position: " + err.Error()) + } + n, err = l.contents.Read(p) + l.pos += int64(n) + return n, err +} + +// Seek implements the io.Seeker interface. Once reader contents is consumed by +// Read, WriteTo etc, to read it again, it must be rewinded by this function +func (l *LazyFileReader) Seek(offset int64, whence int) (pos int64, err error) { + if l.contents == nil { + switch whence { + case 0: + pos = offset + case 1: + pos = l.pos + offset + case 2: + fi, err := l.fs.Stat(l.filename) + if err != nil { + return 0, fmt.Errorf("failed to get %q info: %s", l.filename, err.Error()) + } + pos = fi.Size() + offset + default: + return 0, errors.New("invalid whence") + } + if pos < 0 { + return 0, errors.New("negative position") + } + } else { + pos, err = l.contents.Seek(offset, whence) + if err != nil { + return 0, err + } + } + l.pos = pos + return pos, nil +} + +// WriteTo writes data to w until all the LazyFileReader's file contents is +// drained or an error occurs. If the file is once read, it just writes its +// read cache to w, doesn't re-read the file but this method itself doesn't try +// to keep the contents in cache. +func (l *LazyFileReader) WriteTo(w io.Writer) (n int64, err error) { + if l.contents != nil { + l.contents.Seek(l.pos, 0) + if err != nil { + return 0, errors.New("failed to set read position: " + err.Error()) + } + n, err = l.contents.WriteTo(w) + l.pos += n + return n, err + } + f, err := l.fs.Open(l.filename) + if err != nil { + return 0, fmt.Errorf("failed to open %s to read content: %s", l.filename, err.Error()) + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return 0, fmt.Errorf("failed to get %q info: %s", l.filename, err.Error()) + } + + if l.pos >= fi.Size() { + return 0, nil + } + + return l.copyBuffer(w, f, nil) +} + +// copyBuffer is the actual implementation of Copy and CopyBuffer. +// If buf is nil, one is allocated. +// +// Most of this function is copied from the Go stdlib 'io/io.go'. +func (l *LazyFileReader) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (written int64, err error) { + if buf == nil { + buf = make([]byte, 32*1024) + } + for { + nr, er := src.Read(buf) + if nr > 0 { + nw, ew := dst.Write(buf[0:nr]) + if nw > 0 { + l.pos += int64(nw) + written += int64(nw) + } + if ew != nil { + err = ew + break + } + if nr != nw { + err = io.ErrShortWrite + break + } + } + if er == io.EOF { + break + } + if er != nil { + err = er + break + } + } + return written, err +} diff --git a/source/lazy_file_reader_test.go b/source/lazy_file_reader_test.go new file mode 100644 index 000000000..778a9513b --- /dev/null +++ b/source/lazy_file_reader_test.go @@ -0,0 +1,236 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package source + +import ( + "bytes" + "io" + "os" + "testing" + + "github.com/spf13/afero" +) + +func TestNewLazyFileReader(t *testing.T) { + fs := afero.NewOsFs() + filename := "itdoesnotexistfile" + _, err := NewLazyFileReader(fs, filename) + if err == nil { + t.Errorf("NewLazyFileReader %s: error expected but no error is returned", filename) + } + + filename = "lazy_file_reader_test.go" + _, err = NewLazyFileReader(fs, filename) + if err != nil { + t.Errorf("NewLazyFileReader %s: %v", filename, err) + } +} + +func TestFilename(t *testing.T) { + fs := afero.NewOsFs() + filename := "lazy_file_reader_test.go" + rd, err := NewLazyFileReader(fs, filename) + if err != nil { + t.Fatalf("NewLazyFileReader %s: %v", filename, err) + } + if rd.Filename() != filename { + t.Errorf("Filename: expected filename %q, got %q", filename, rd.Filename()) + } +} + +func TestRead(t *testing.T) { + fs := afero.NewOsFs() + filename := "lazy_file_reader_test.go" + fi, err := fs.Stat(filename) + if err != nil { + t.Fatalf("os.Stat: %v", err) + } + + b, err := afero.ReadFile(fs, filename) + if err != nil { + t.Fatalf("afero.ReadFile: %v", err) + } + + rd, err := NewLazyFileReader(fs, filename) + if err != nil { + t.Fatalf("NewLazyFileReader %s: %v", filename, err) + } + + tst := func(testcase string) { + p := make([]byte, fi.Size()) + n, err := rd.Read(p) + if err != nil { + t.Fatalf("Read %s case: %v", testcase, err) + } + if int64(n) != fi.Size() { + t.Errorf("Read %s case: read bytes length expected %d, got %d", testcase, fi.Size(), n) + } + if !bytes.Equal(b, p) { + t.Errorf("Read %s case: read bytes are different from expected", testcase) + } + } + tst("No cache") + _, err = rd.Seek(0, 0) + if err != nil { + t.Fatalf("Seek: %v", err) + } + tst("Cache") +} + +func TestSeek(t *testing.T) { + type testcase struct { + seek int + offset int64 + length int + moveto int64 + expected []byte + } + fs := afero.NewOsFs() + filename := "lazy_file_reader_test.go" + b, err := afero.ReadFile(fs, filename) + if err != nil { + t.Fatalf("afero.ReadFile: %v", err) + } + + // no cache case + for i, this := range []testcase{ + {seek: os.SEEK_SET, offset: 0, length: 10, moveto: 0, expected: b[:10]}, + {seek: os.SEEK_SET, offset: 5, length: 10, moveto: 5, expected: b[5:15]}, + {seek: os.SEEK_CUR, offset: 5, length: 10, moveto: 5, expected: b[5:15]}, // current pos = 0 + {seek: os.SEEK_END, offset: -1, length: 1, moveto: int64(len(b) - 1), expected: b[len(b)-1:]}, + {seek: 3, expected: nil}, + {seek: os.SEEK_SET, offset: -1, expected: nil}, + } { + rd, err := NewLazyFileReader(fs, filename) + if err != nil { + t.Errorf("[%d] NewLazyFileReader %s: %v", i, filename, err) + continue + } + + pos, err := rd.Seek(this.offset, this.seek) + if this.expected == nil { + if err == nil { + t.Errorf("[%d] Seek didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] Seek failed unexpectedly: %v", i, err) + continue + } + if pos != this.moveto { + t.Errorf("[%d] Seek failed to move the pointer: got %d, expected: %d", i, pos, this.moveto) + } + + buf := make([]byte, this.length) + n, err := rd.Read(buf) + if err != nil { + t.Errorf("[%d] Read failed unexpectedly: %v", i, err) + } + if !bytes.Equal(this.expected, buf[:n]) { + t.Errorf("[%d] Seek and Read got %q but expected %q", i, buf[:n], this.expected) + } + } + } + + // cache case + rd, err := NewLazyFileReader(fs, filename) + if err != nil { + t.Fatalf("NewLazyFileReader %s: %v", filename, err) + } + dummy := make([]byte, len(b)) + _, err = rd.Read(dummy) + if err != nil { + t.Fatalf("Read failed unexpectedly: %v", err) + } + + for i, this := range []testcase{ + {seek: os.SEEK_SET, offset: 0, length: 10, moveto: 0, expected: b[:10]}, + {seek: os.SEEK_SET, offset: 5, length: 10, moveto: 5, expected: b[5:15]}, + {seek: os.SEEK_CUR, offset: 1, length: 10, moveto: 16, expected: b[16:26]}, // current pos = 15 + {seek: os.SEEK_END, offset: -1, length: 1, moveto: int64(len(b) - 1), expected: b[len(b)-1:]}, + {seek: 3, expected: nil}, + {seek: os.SEEK_SET, offset: -1, expected: nil}, + } { + pos, err := rd.Seek(this.offset, this.seek) + if this.expected == nil { + if err == nil { + t.Errorf("[%d] Seek didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] Seek failed unexpectedly: %v", i, err) + continue + } + if pos != this.moveto { + t.Errorf("[%d] Seek failed to move the pointer: got %d, expected: %d", i, pos, this.moveto) + } + + buf := make([]byte, this.length) + n, err := rd.Read(buf) + if err != nil { + t.Errorf("[%d] Read failed unexpectedly: %v", i, err) + } + if !bytes.Equal(this.expected, buf[:n]) { + t.Errorf("[%d] Seek and Read got %q but expected %q", i, buf[:n], this.expected) + } + } + } +} + +func TestWriteTo(t *testing.T) { + fs := afero.NewOsFs() + filename := "lazy_file_reader_test.go" + fi, err := fs.Stat(filename) + if err != nil { + t.Fatalf("os.Stat: %v", err) + } + + b, err := afero.ReadFile(fs, filename) + if err != nil { + t.Fatalf("afero.ReadFile: %v", err) + } + + rd, err := NewLazyFileReader(fs, filename) + if err != nil { + t.Fatalf("NewLazyFileReader %s: %v", filename, err) + } + + tst := func(testcase string, expectedSize int64, checkEqual bool) { + buf := bytes.NewBuffer(make([]byte, 0, bytes.MinRead)) + n, err := rd.WriteTo(buf) + if err != nil { + t.Fatalf("WriteTo %s case: %v", testcase, err) + } + if n != expectedSize { + t.Errorf("WriteTo %s case: written bytes length expected %d, got %d", testcase, expectedSize, n) + } + if checkEqual && !bytes.Equal(b, buf.Bytes()) { + t.Errorf("WriteTo %s case: written bytes are different from expected", testcase) + } + } + tst("No cache", fi.Size(), true) + tst("No cache 2nd", 0, false) + + p := make([]byte, fi.Size()) + _, err = rd.Read(p) + if err != nil && err != io.EOF { + t.Fatalf("Read: %v", err) + } + _, err = rd.Seek(0, 0) + if err != nil { + t.Fatalf("Seek: %v", err) + } + + tst("Cache", fi.Size(), true) +} diff --git a/tpl/cast/cast.go b/tpl/cast/cast.go new file mode 100644 index 000000000..378467178 --- /dev/null +++ b/tpl/cast/cast.go @@ -0,0 +1,51 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cast + +import ( + "html/template" + + _cast "github.com/spf13/cast" +) + +// New returns a new instance of the cast-namespaced template functions. +func New() *Namespace { + return &Namespace{} +} + +// Namespace provides template functions for the "cast" namespace. +type Namespace struct { +} + +// ToInt converts the given value to an int. +func (ns *Namespace) ToInt(v interface{}) (int, error) { + switch vv := v.(type) { + case template.HTML: + v = string(vv) + case template.CSS: + v = string(vv) + case template.HTMLAttr: + v = string(vv) + case template.JS: + v = string(vv) + case template.JSStr: + v = string(vv) + } + return _cast.ToIntE(v) +} + +// ToString converts the given value to a string. +func (ns *Namespace) ToString(v interface{}) (string, error) { + return _cast.ToStringE(v) +} diff --git a/tpl/cast/cast_test.go b/tpl/cast/cast_test.go new file mode 100644 index 000000000..a5e0db2af --- /dev/null +++ b/tpl/cast/cast_test.go @@ -0,0 +1,83 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cast + +import ( + "fmt" + "html/template" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestToInt(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + v interface{} + expect interface{} + }{ + {"1", 1}, + {template.HTML("2"), 2}, + {template.CSS("3"), 3}, + {template.HTMLAttr("4"), 4}, + {template.JS("5"), 5}, + {template.JSStr("6"), 6}, + {"a", false}, + {t, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.v) + + result, err := ns.ToInt(test.v) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestToString(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + v interface{} + expect interface{} + }{ + {1, "1"}, + {template.HTML("2"), "2"}, + {"a", "a"}, + {t, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.v) + + result, err := ns.ToString(test.v) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} diff --git a/tpl/cast/docshelper.go b/tpl/cast/docshelper.go new file mode 100644 index 000000000..e77c83d28 --- /dev/null +++ b/tpl/cast/docshelper.go @@ -0,0 +1,41 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cast + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/docshelper" + "github.com/gohugoio/hugo/tpl/internal" +) + +// This file provides documentation support and is randomly put into this package. +func init() { + docsProvider := func() map[string]interface{} { + docs := make(map[string]interface{}) + d := &deps.Deps{} + + var namespaces internal.TemplateFuncsNamespaces + + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + nf := nsf(d) + namespaces = append(namespaces, nf) + + } + + docs["funcs"] = namespaces + return docs + } + + docshelper.AddDocProvider("tpl", docsProvider) +} diff --git a/tpl/cast/init.go b/tpl/cast/init.go new file mode 100644 index 000000000..d1547310c --- /dev/null +++ b/tpl/cast/init.go @@ -0,0 +1,51 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cast + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" +) + +const name = "cast" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx := New() + + ns := &internal.TemplateFuncsNamespace{ + Name: name, + Context: func(args ...interface{}) interface{} { return ctx }, + } + + ns.AddMethodMapping(ctx.ToInt, + []string{"int"}, + [][2]string{ + {`{{ "1234" | int | printf "%T" }}`, `int`}, + }, + ) + + ns.AddMethodMapping(ctx.ToString, + []string{"string"}, + [][2]string{ + {`{{ 1234 | string | printf "%T" }}`, `string`}, + }, + ) + + return ns + + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/cast/init_test.go b/tpl/cast/init_test.go new file mode 100644 index 000000000..47cbd3d0b --- /dev/null +++ b/tpl/cast/init_test.go @@ -0,0 +1,38 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cast + +import ( + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + var found bool + var ns *internal.TemplateFuncsNamespace + + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + ns = nsf(&deps.Deps{}) + if ns.Name == name { + found = true + break + } + } + + require.True(t, found) + require.IsType(t, &Namespace{}, ns.Context()) +} diff --git a/tpl/collections/apply.go b/tpl/collections/apply.go new file mode 100644 index 000000000..c3c3a297b --- /dev/null +++ b/tpl/collections/apply.go @@ -0,0 +1,150 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "errors" + "fmt" + "reflect" + "strings" + + "github.com/gohugoio/hugo/tpl" +) + +// Apply takes a map, array, or slice and returns a new slice with the function fname applied over it. +func (ns *Namespace) Apply(seq interface{}, fname string, args ...interface{}) (interface{}, error) { + if seq == nil { + return make([]interface{}, 0), nil + } + + if fname == "apply" { + return nil, errors.New("can't apply myself (no turtles allowed)") + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return nil, errors.New("can't iterate over a nil value") + } + + fnv, found := ns.lookupFunc(fname) + if !found { + return nil, errors.New("can't find function " + fname) + } + + // fnv := reflect.ValueOf(fn) + + switch seqv.Kind() { + case reflect.Array, reflect.Slice: + r := make([]interface{}, seqv.Len()) + for i := 0; i < seqv.Len(); i++ { + vv := seqv.Index(i) + + vvv, err := applyFnToThis(fnv, vv, args...) + + if err != nil { + return nil, err + } + + r[i] = vvv.Interface() + } + + return r, nil + default: + return nil, fmt.Errorf("can't apply over %v", seq) + } +} + +func applyFnToThis(fn, this reflect.Value, args ...interface{}) (reflect.Value, error) { + n := make([]reflect.Value, len(args)) + for i, arg := range args { + if arg == "." { + n[i] = this + } else { + n[i] = reflect.ValueOf(arg) + } + } + + num := fn.Type().NumIn() + + if fn.Type().IsVariadic() { + num-- + } + + // TODO(bep) see #1098 - also see template_tests.go + /*if len(args) < num { + return reflect.ValueOf(nil), errors.New("Too few arguments") + } else if len(args) > num { + return reflect.ValueOf(nil), errors.New("Too many arguments") + }*/ + + for i := 0; i < num; i++ { + // AssignableTo reports whether xt is assignable to type targ. + if xt, targ := n[i].Type(), fn.Type().In(i); !xt.AssignableTo(targ) { + return reflect.ValueOf(nil), errors.New("called apply using " + xt.String() + " as type " + targ.String()) + } + } + + res := fn.Call(n) + + if len(res) == 1 || res[1].IsNil() { + return res[0], nil + } + return reflect.ValueOf(nil), res[1].Interface().(error) +} + +func (ns *Namespace) lookupFunc(fname string) (reflect.Value, bool) { + if !strings.ContainsRune(fname, '.') { + templ, ok := ns.deps.Tmpl.(tpl.TemplateFuncsGetter) + if !ok { + panic("Needs a tpl.TemplateFuncsGetter") + } + fm := templ.GetFuncs() + fn, found := fm[fname] + if !found { + return reflect.Value{}, false + } + + return reflect.ValueOf(fn), true + } + + ss := strings.SplitN(fname, ".", 2) + + // namespace + nv, found := ns.lookupFunc(ss[0]) + if !found { + return reflect.Value{}, false + } + + // method + m := nv.MethodByName(ss[1]) + // if reflect.DeepEqual(m, reflect.Value{}) { + if m.Kind() == reflect.Invalid { + return reflect.Value{}, false + } + return m, true +} + +// indirect is taken from 'text/template/exec.go' +func indirect(v reflect.Value) (rv reflect.Value, isNil bool) { + for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() { + if v.IsNil() { + return v, true + } + if v.Kind() == reflect.Interface && v.NumMethod() > 0 { + break + } + } + return v, false +} diff --git a/tpl/collections/apply_test.go b/tpl/collections/apply_test.go new file mode 100644 index 000000000..de24b06c8 --- /dev/null +++ b/tpl/collections/apply_test.go @@ -0,0 +1,64 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "testing" + + "fmt" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl" + "github.com/stretchr/testify/require" +) + +type templateFinder int + +func (templateFinder) Lookup(name string) *tpl.TemplateAdapter { + return nil +} + +func (templateFinder) GetFuncs() map[string]interface{} { + return map[string]interface{}{ + "print": fmt.Sprint, + } +} + +func TestApply(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{Tmpl: new(templateFinder)}) + + strings := []interface{}{"a\n", "b\n"} + + result, err := ns.Apply(strings, "print", "a", "b", "c") + require.NoError(t, err) + require.Equal(t, []interface{}{"abc", "abc"}, result, "testing variadic") + + _, err = ns.Apply(strings, "apply", ".") + require.Error(t, err) + + var nilErr *error + _, err = ns.Apply(nilErr, "chomp", ".") + require.Error(t, err) + + _, err = ns.Apply(strings, "dobedobedo", ".") + require.Error(t, err) + + _, err = ns.Apply(strings, "foo.Chomp", "c\n") + if err == nil { + t.Errorf("apply with unknown func should fail") + } + +} diff --git a/tpl/collections/collections.go b/tpl/collections/collections.go new file mode 100644 index 000000000..ae2d73b46 --- /dev/null +++ b/tpl/collections/collections.go @@ -0,0 +1,708 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "errors" + "fmt" + "html/template" + "math/rand" + "net/url" + "reflect" + "strings" + "time" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" + "github.com/spf13/cast" +) + +// New returns a new instance of the collections-namespaced template functions. +func New(deps *deps.Deps) *Namespace { + return &Namespace{ + deps: deps, + } +} + +// Namespace provides template functions for the "collections" namespace. +type Namespace struct { + deps *deps.Deps +} + +// After returns all the items after the first N in a rangeable list. +func (ns *Namespace) After(index interface{}, seq interface{}) (interface{}, error) { + if index == nil || seq == nil { + return nil, errors.New("both limit and seq must be provided") + } + + indexv, err := cast.ToIntE(index) + if err != nil { + return nil, err + } + + if indexv < 1 { + return nil, errors.New("can't return negative/empty count of items from sequence") + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return nil, errors.New("can't iterate over a nil value") + } + + switch seqv.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + // okay + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) + } + + if indexv >= seqv.Len() { + return nil, errors.New("no items left") + } + + return seqv.Slice(indexv, seqv.Len()).Interface(), nil +} + +// Delimit takes a given sequence and returns a delimited HTML string. +// If last is passed to the function, it will be used as the final delimiter. +func (ns *Namespace) Delimit(seq, delimiter interface{}, last ...interface{}) (template.HTML, error) { + d, err := cast.ToStringE(delimiter) + if err != nil { + return "", err + } + + var dLast *string + if len(last) > 0 { + l := last[0] + dStr, err := cast.ToStringE(l) + if err != nil { + dLast = nil + } + dLast = &dStr + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return "", errors.New("can't iterate over a nil value") + } + + var str string + switch seqv.Kind() { + case reflect.Map: + sortSeq, err := ns.Sort(seq) + if err != nil { + return "", err + } + seqv = reflect.ValueOf(sortSeq) + fallthrough + case reflect.Array, reflect.Slice, reflect.String: + for i := 0; i < seqv.Len(); i++ { + val := seqv.Index(i).Interface() + valStr, err := cast.ToStringE(val) + if err != nil { + continue + } + switch { + case i == seqv.Len()-2 && dLast != nil: + str += valStr + *dLast + case i == seqv.Len()-1: + str += valStr + default: + str += valStr + d + } + } + + default: + return "", fmt.Errorf("can't iterate over %v", seq) + } + + return template.HTML(str), nil +} + +// Dictionary creates a map[string]interface{} from the given parameters by +// walking the parameters and treating them as key-value pairs. The number +// of parameters must be even. +func (ns *Namespace) Dictionary(values ...interface{}) (map[string]interface{}, error) { + if len(values)%2 != 0 { + return nil, errors.New("invalid dictionary call") + } + + dict := make(map[string]interface{}, len(values)/2) + + for i := 0; i < len(values); i += 2 { + key, ok := values[i].(string) + if !ok { + return nil, errors.New("dictionary keys must be strings") + } + dict[key] = values[i+1] + } + + return dict, nil +} + +// EchoParam returns a given value if it is set; otherwise, it returns an +// empty string. +func (ns *Namespace) EchoParam(a, key interface{}) interface{} { + av, isNil := indirect(reflect.ValueOf(a)) + if isNil { + return "" + } + + var avv reflect.Value + switch av.Kind() { + case reflect.Array, reflect.Slice: + index, ok := key.(int) + if ok && av.Len() > index { + avv = av.Index(index) + } + case reflect.Map: + kv := reflect.ValueOf(key) + if kv.Type().AssignableTo(av.Type().Key()) { + avv = av.MapIndex(kv) + } + } + + avv, isNil = indirect(avv) + + if isNil { + return "" + } + + if avv.IsValid() { + switch avv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return avv.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return avv.Uint() + case reflect.Float32, reflect.Float64: + return avv.Float() + case reflect.String: + return avv.String() + } + } + + return "" +} + +// First returns the first N items in a rangeable list. +func (ns *Namespace) First(limit interface{}, seq interface{}) (interface{}, error) { + if limit == nil || seq == nil { + return nil, errors.New("both limit and seq must be provided") + } + + limitv, err := cast.ToIntE(limit) + if err != nil { + return nil, err + } + + if limitv < 1 { + return nil, errors.New("can't return negative/empty count of items from sequence") + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return nil, errors.New("can't iterate over a nil value") + } + + switch seqv.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + // okay + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) + } + + if limitv > seqv.Len() { + limitv = seqv.Len() + } + + return seqv.Slice(0, limitv).Interface(), nil +} + +// In returns whether v is in the set l. l may be an array or slice. +func (ns *Namespace) In(l interface{}, v interface{}) bool { + if l == nil || v == nil { + return false + } + + lv := reflect.ValueOf(l) + vv := reflect.ValueOf(v) + + switch lv.Kind() { + case reflect.Array, reflect.Slice: + for i := 0; i < lv.Len(); i++ { + lvv := lv.Index(i) + lvv, isNil := indirect(lvv) + if isNil { + continue + } + switch lvv.Kind() { + case reflect.String: + if vv.Type() == lvv.Type() && vv.String() == lvv.String() { + return true + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + switch vv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if vv.Int() == lvv.Int() { + return true + } + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + switch vv.Kind() { + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if vv.Uint() == lvv.Uint() { + return true + } + } + case reflect.Float32, reflect.Float64: + switch vv.Kind() { + case reflect.Float32, reflect.Float64: + if vv.Float() == lvv.Float() { + return true + } + } + } + } + case reflect.String: + if vv.Type() == lv.Type() && strings.Contains(lv.String(), vv.String()) { + return true + } + } + return false +} + +// Intersect returns the common elements in the given sets, l1 and l2. l1 and +// l2 must be of the same type and may be either arrays or slices. +func (ns *Namespace) Intersect(l1, l2 interface{}) (interface{}, error) { + if l1 == nil || l2 == nil { + return make([]interface{}, 0), nil + } + + l1v := reflect.ValueOf(l1) + l2v := reflect.ValueOf(l2) + + switch l1v.Kind() { + case reflect.Array, reflect.Slice: + switch l2v.Kind() { + case reflect.Array, reflect.Slice: + r := reflect.MakeSlice(l1v.Type(), 0, 0) + for i := 0; i < l1v.Len(); i++ { + l1vv := l1v.Index(i) + for j := 0; j < l2v.Len(); j++ { + l2vv := l2v.Index(j) + switch l1vv.Kind() { + case reflect.String: + l2t, err := toString(l2vv) + if err == nil && l1vv.String() == l2t && !ns.In(r.Interface(), l1vv.Interface()) { + r = reflect.Append(r, l1vv) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + l2t, err := toInt(l2vv) + if err == nil && l1vv.Int() == l2t && !ns.In(r.Interface(), l1vv.Interface()) { + r = reflect.Append(r, l1vv) + } + case reflect.Float32, reflect.Float64: + l2t, err := toFloat(l2vv) + if err == nil && l1vv.Float() == l2t && !ns.In(r.Interface(), l1vv.Interface()) { + r = reflect.Append(r, l1vv) + } + case reflect.Interface: + switch l1vvActual := l1vv.Interface().(type) { + case string: + switch l2vvActual := l2vv.Interface().(type) { + case string: + if l1vvActual == l2vvActual && !ns.In(r.Interface(), l1vvActual) { + r = reflect.Append(r, l1vv) + } + } + case int, int8, int16, int32, int64: + switch l2vvActual := l2vv.Interface().(type) { + case int, int8, int16, int32, int64: + if l1vvActual == l2vvActual && !ns.In(r.Interface(), l1vvActual) { + r = reflect.Append(r, l1vv) + } + } + case uint, uint8, uint16, uint32, uint64: + switch l2vvActual := l2vv.Interface().(type) { + case uint, uint8, uint16, uint32, uint64: + if l1vvActual == l2vvActual && !ns.In(r.Interface(), l1vvActual) { + r = reflect.Append(r, l1vv) + } + } + case float32, float64: + switch l2vvActual := l2vv.Interface().(type) { + case float32, float64: + if l1vvActual == l2vvActual && !ns.In(r.Interface(), l1vvActual) { + r = reflect.Append(r, l1vv) + } + } + } + } + } + } + return r.Interface(), nil + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(l2).Type().String()) + } + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(l1).Type().String()) + } +} + +// IsSet returns whether a given array, channel, slice, or map has a key +// defined. +func (ns *Namespace) IsSet(a interface{}, key interface{}) (bool, error) { + av := reflect.ValueOf(a) + kv := reflect.ValueOf(key) + + switch av.Kind() { + case reflect.Array, reflect.Chan, reflect.Slice: + if int64(av.Len()) > kv.Int() { + return true, nil + } + case reflect.Map: + if kv.Type() == av.Type().Key() { + return av.MapIndex(kv).IsValid(), nil + } + default: + helpers.DistinctFeedbackLog.Printf("WARNING: calling IsSet with unsupported type %q (%T) will always return false.\n", av.Kind(), a) + } + + return false, nil +} + +// Last returns the last N items in a rangeable list. +func (ns *Namespace) Last(limit interface{}, seq interface{}) (interface{}, error) { + if limit == nil || seq == nil { + return nil, errors.New("both limit and seq must be provided") + } + + limitv, err := cast.ToIntE(limit) + if err != nil { + return nil, err + } + + if limitv < 1 { + return nil, errors.New("can't return negative/empty count of items from sequence") + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return nil, errors.New("can't iterate over a nil value") + } + + switch seqv.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + // okay + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) + } + + if limitv > seqv.Len() { + limitv = seqv.Len() + } + + return seqv.Slice(seqv.Len()-limitv, seqv.Len()).Interface(), nil +} + +// Querify encodes the given parameters in URL-encoded form ("bar=baz&foo=quux") sorted by key. +func (ns *Namespace) Querify(params ...interface{}) (string, error) { + qs := url.Values{} + vals, err := ns.Dictionary(params...) + if err != nil { + return "", errors.New("querify keys must be strings") + } + + for name, value := range vals { + qs.Add(name, fmt.Sprintf("%v", value)) + } + + return qs.Encode(), nil +} + +// Seq creates a sequence of integers. It's named and used as GNU's seq. +// +// Examples: +// 3 => 1, 2, 3 +// 1 2 4 => 1, 3 +// -3 => -1, -2, -3 +// 1 4 => 1, 2, 3, 4 +// 1 -2 => 1, 0, -1, -2 +func (ns *Namespace) Seq(args ...interface{}) ([]int, error) { + if len(args) < 1 || len(args) > 3 { + return nil, errors.New("invalid number of arguments to Seq") + } + + intArgs := cast.ToIntSlice(args) + if len(intArgs) < 1 || len(intArgs) > 3 { + return nil, errors.New("invalid arguments to Seq") + } + + var inc = 1 + var last int + var first = intArgs[0] + + if len(intArgs) == 1 { + last = first + if last == 0 { + return []int{}, nil + } else if last > 0 { + first = 1 + } else { + first = -1 + inc = -1 + } + } else if len(intArgs) == 2 { + last = intArgs[1] + if last < first { + inc = -1 + } + } else { + inc = intArgs[1] + last = intArgs[2] + if inc == 0 { + return nil, errors.New("'increment' must not be 0") + } + if first < last && inc < 0 { + return nil, errors.New("'increment' must be > 0") + } + if first > last && inc > 0 { + return nil, errors.New("'increment' must be < 0") + } + } + + // sanity check + if last < -100000 { + return nil, errors.New("size of result exceeds limit") + } + size := ((last - first) / inc) + 1 + + // sanity check + if size <= 0 || size > 2000 { + return nil, errors.New("size of result exceeds limit") + } + + seq := make([]int, size) + val := first + for i := 0; ; i++ { + seq[i] = val + val += inc + if (inc < 0 && val < last) || (inc > 0 && val > last) { + break + } + } + + return seq, nil +} + +// Shuffle returns the given rangeable list in a randomised order. +func (ns *Namespace) Shuffle(seq interface{}) (interface{}, error) { + if seq == nil { + return nil, errors.New("both count and seq must be provided") + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return nil, errors.New("can't iterate over a nil value") + } + + switch seqv.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + // okay + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) + } + + shuffled := reflect.MakeSlice(reflect.TypeOf(seq), seqv.Len(), seqv.Len()) + + rand.Seed(time.Now().UTC().UnixNano()) + randomIndices := rand.Perm(seqv.Len()) + + for index, value := range randomIndices { + shuffled.Index(value).Set(seqv.Index(index)) + } + + return shuffled.Interface(), nil +} + +// Slice returns a slice of all passed arguments. +func (ns *Namespace) Slice(args ...interface{}) []interface{} { + return args +} + +// Union returns the union of the given sets, l1 and l2. l1 and +// l2 must be of the same type and may be either arrays or slices. +// If l1 and l2 aren't of the same type then l1 will be returned. +// If either l1 or l2 is nil then the non-nil list will be returned. +func (ns *Namespace) Union(l1, l2 interface{}) (interface{}, error) { + if l1 == nil && l2 == nil { + return []interface{}{}, nil + } else if l1 == nil && l2 != nil { + return l2, nil + } else if l1 != nil && l2 == nil { + return l1, nil + } + + l1v := reflect.ValueOf(l1) + l2v := reflect.ValueOf(l2) + + switch l1v.Kind() { + case reflect.Array, reflect.Slice: + switch l2v.Kind() { + case reflect.Array, reflect.Slice: + r := reflect.MakeSlice(l1v.Type(), 0, 0) + + if l1v.Type() != l2v.Type() && + l1v.Type().Elem().Kind() != reflect.Interface && + l2v.Type().Elem().Kind() != reflect.Interface { + return r.Interface(), nil + } + + var l1vv reflect.Value + for i := 0; i < l1v.Len(); i++ { + l1vv = l1v.Index(i) + if !ns.In(r.Interface(), l1vv.Interface()) { + r = reflect.Append(r, l1vv) + } + } + + for j := 0; j < l2v.Len(); j++ { + l2vv := l2v.Index(j) + + switch l1vv.Kind() { + case reflect.String: + l2t, err := toString(l2vv) + if err == nil && !ns.In(r.Interface(), l2t) { + r = reflect.Append(r, reflect.ValueOf(l2t)) + } + case reflect.Int: + l2t, err := toInt(l2vv) + if err == nil && !ns.In(r.Interface(), l2t) { + r = reflect.Append(r, reflect.ValueOf(int(l2t))) + } + case reflect.Int8: + l2t, err := toInt(l2vv) + if err == nil && !ns.In(r.Interface(), l2t) { + r = reflect.Append(r, reflect.ValueOf(int8(l2t))) + } + case reflect.Int16: + l2t, err := toInt(l2vv) + if err == nil && !ns.In(r.Interface(), l2t) { + r = reflect.Append(r, reflect.ValueOf(int16(l2t))) + } + case reflect.Int32: + l2t, err := toInt(l2vv) + if err == nil && !ns.In(r.Interface(), l2t) { + r = reflect.Append(r, reflect.ValueOf(int32(l2t))) + } + case reflect.Int64: + l2t, err := toInt(l2vv) + if err == nil && !ns.In(r.Interface(), l2t) { + r = reflect.Append(r, reflect.ValueOf(l2t)) + } + case reflect.Float32: + l2t, err := toFloat(l2vv) + if err == nil && !ns.In(r.Interface(), float32(l2t)) { + r = reflect.Append(r, reflect.ValueOf(float32(l2t))) + } + case reflect.Float64: + l2t, err := toFloat(l2vv) + if err == nil && !ns.In(r.Interface(), l2t) { + r = reflect.Append(r, reflect.ValueOf(l2t)) + } + case reflect.Interface: + switch l1vv.Interface().(type) { + case string: + switch l2vvActual := l2vv.Interface().(type) { + case string: + if !ns.In(r.Interface(), l2vvActual) { + r = reflect.Append(r, l2vv) + } + } + case int, int8, int16, int32, int64: + switch l2vvActual := l2vv.Interface().(type) { + case int, int8, int16, int32, int64: + if !ns.In(r.Interface(), l2vvActual) { + r = reflect.Append(r, l2vv) + } + } + case uint, uint8, uint16, uint32, uint64: + switch l2vvActual := l2vv.Interface().(type) { + case uint, uint8, uint16, uint32, uint64: + if !ns.In(r.Interface(), l2vvActual) { + r = reflect.Append(r, l2vv) + } + } + case float32, float64: + switch l2vvActual := l2vv.Interface().(type) { + case float32, float64: + if !ns.In(r.Interface(), l2vvActual) { + r = reflect.Append(r, l2vv) + } + } + } + } + } + + return r.Interface(), nil + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(l2).Type().String()) + } + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(l1).Type().String()) + } +} + +// Uniq takes in a slice or array and returns a slice with subsequent +// duplicate elements removed. +func (ns *Namespace) Uniq(l interface{}) (interface{}, error) { + if l == nil { + return make([]interface{}, 0), nil + } + + lv := reflect.ValueOf(l) + lv, isNil := indirect(lv) + if isNil { + return nil, errors.New("invalid nil argument to Uniq") + } + + var ret reflect.Value + + switch lv.Kind() { + case reflect.Slice: + ret = reflect.MakeSlice(lv.Type(), 0, 0) + case reflect.Array: + ret = reflect.MakeSlice(reflect.SliceOf(lv.Type().Elem()), 0, 0) + default: + return nil, errors.New("Can't use Uniq on " + reflect.ValueOf(lv).Type().String()) + } + + for i := 0; i != lv.Len(); i++ { + lvv := lv.Index(i) + lvv, isNil := indirect(lvv) + if isNil { + continue + } + + if !ns.In(ret.Interface(), lvv.Interface()) { + ret = reflect.Append(ret, lvv) + } + } + return ret.Interface(), nil +} diff --git a/tpl/collections/collections_test.go b/tpl/collections/collections_test.go new file mode 100644 index 000000000..64c358ddd --- /dev/null +++ b/tpl/collections/collections_test.go @@ -0,0 +1,716 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "errors" + "fmt" + "html/template" + "io/ioutil" + "log" + "math/rand" + "os" + "reflect" + "testing" + "time" + + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" + jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type tstNoStringer struct{} + +func TestAfter(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + index interface{} + seq interface{} + expect interface{} + }{ + {int(2), []string{"a", "b", "c", "d"}, []string{"c", "d"}}, + {int32(3), []string{"a", "b"}, false}, + {int64(2), []int{100, 200, 300}, []int{300}}, + {100, []int{100, 200}, false}, + {"1", []int{100, 200, 300}, []int{200, 300}}, + {int64(-1), []int{100, 200, 300}, false}, + {"noint", []int{100, 200, 300}, false}, + {1, nil, false}, + {nil, []int{100}, false}, + {1, t, false}, + {1, (*string)(nil), false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.After(test.index, test.seq) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestDelimit(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + seq interface{} + delimiter interface{} + last interface{} + expect template.HTML + }{ + {[]string{"class1", "class2", "class3"}, " ", nil, "class1 class2 class3"}, + {[]int{1, 2, 3, 4, 5}, ",", nil, "1,2,3,4,5"}, + {[]int{1, 2, 3, 4, 5}, ", ", nil, "1, 2, 3, 4, 5"}, + {[]string{"class1", "class2", "class3"}, " ", " and ", "class1 class2 and class3"}, + {[]int{1, 2, 3, 4, 5}, ",", ",", "1,2,3,4,5"}, + {[]int{1, 2, 3, 4, 5}, ", ", ", and ", "1, 2, 3, 4, and 5"}, + // test maps with and without sorting required + {map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, "--", nil, "10--20--30--40--50"}, + {map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, "--", nil, "30--20--10--40--50"}, + {map[string]string{"1": "10", "2": "20", "3": "30", "4": "40", "5": "50"}, "--", nil, "10--20--30--40--50"}, + {map[string]string{"3": "10", "2": "20", "1": "30", "4": "40", "5": "50"}, "--", nil, "30--20--10--40--50"}, + {map[string]string{"one": "10", "two": "20", "three": "30", "four": "40", "five": "50"}, "--", nil, "50--40--10--30--20"}, + {map[int]string{1: "10", 2: "20", 3: "30", 4: "40", 5: "50"}, "--", nil, "10--20--30--40--50"}, + {map[int]string{3: "10", 2: "20", 1: "30", 4: "40", 5: "50"}, "--", nil, "30--20--10--40--50"}, + {map[float64]string{3.3: "10", 2.3: "20", 1.3: "30", 4.3: "40", 5.3: "50"}, "--", nil, "30--20--10--40--50"}, + // test maps with a last delimiter + {map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, "--", "--and--", "10--20--30--40--and--50"}, + {map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, "--", "--and--", "30--20--10--40--and--50"}, + {map[string]string{"1": "10", "2": "20", "3": "30", "4": "40", "5": "50"}, "--", "--and--", "10--20--30--40--and--50"}, + {map[string]string{"3": "10", "2": "20", "1": "30", "4": "40", "5": "50"}, "--", "--and--", "30--20--10--40--and--50"}, + {map[string]string{"one": "10", "two": "20", "three": "30", "four": "40", "five": "50"}, "--", "--and--", "50--40--10--30--and--20"}, + {map[int]string{1: "10", 2: "20", 3: "30", 4: "40", 5: "50"}, "--", "--and--", "10--20--30--40--and--50"}, + {map[int]string{3: "10", 2: "20", 1: "30", 4: "40", 5: "50"}, "--", "--and--", "30--20--10--40--and--50"}, + {map[float64]string{3.5: "10", 2.5: "20", 1.5: "30", 4.5: "40", 5.5: "50"}, "--", "--and--", "30--20--10--40--and--50"}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + var result template.HTML + var err error + + if test.last == nil { + result, err = ns.Delimit(test.seq, test.delimiter) + } else { + result, err = ns.Delimit(test.seq, test.delimiter, test.last) + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestDictionary(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + values []interface{} + expect interface{} + }{ + {[]interface{}{"a", "b"}, map[string]interface{}{"a": "b"}}, + {[]interface{}{"a", 12, "b", []int{4}}, map[string]interface{}{"a": 12, "b": []int{4}}}, + // errors + {[]interface{}{5, "b"}, false}, + {[]interface{}{"a", "b", "c"}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.values) + + result, err := ns.Dictionary(test.values...) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestEchoParam(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + a interface{} + key interface{} + expect interface{} + }{ + {[]int{1, 2, 3}, 1, int64(2)}, + {[]uint{1, 2, 3}, 1, uint64(2)}, + {[]float64{1.1, 2.2, 3.3}, 1, float64(2.2)}, + {[]string{"foo", "bar", "baz"}, 1, "bar"}, + {[]TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}}, 1, ""}, + {map[string]int{"foo": 1, "bar": 2, "baz": 3}, "bar", int64(2)}, + {map[string]uint{"foo": 1, "bar": 2, "baz": 3}, "bar", uint64(2)}, + {map[string]float64{"foo": 1.1, "bar": 2.2, "baz": 3.3}, "bar", float64(2.2)}, + {map[string]string{"foo": "FOO", "bar": "BAR", "baz": "BAZ"}, "bar", "BAR"}, + {map[string]TstX{"foo": {A: "a", B: "b"}, "bar": {A: "c", B: "d"}, "baz": {A: "e", B: "f"}}, "bar", ""}, + {map[string]interface{}{"foo": nil}, "foo", ""}, + {(*[]string)(nil), "bar", ""}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result := ns.EchoParam(test.a, test.key) + + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestFirst(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + limit interface{} + seq interface{} + expect interface{} + }{ + {int(2), []string{"a", "b", "c"}, []string{"a", "b"}}, + {int32(3), []string{"a", "b"}, []string{"a", "b"}}, + {int64(2), []int{100, 200, 300}, []int{100, 200}}, + {100, []int{100, 200}, []int{100, 200}}, + {"1", []int{100, 200, 300}, []int{100}}, + {int64(-1), []int{100, 200, 300}, false}, + {"noint", []int{100, 200, 300}, false}, + {1, nil, false}, + {nil, []int{100}, false}, + {1, t, false}, + {1, (*string)(nil), false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.First(test.limit, test.seq) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestIn(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + l1 interface{} + l2 interface{} + expect bool + }{ + {[]string{"a", "b", "c"}, "b", true}, + {[]interface{}{"a", "b", "c"}, "b", true}, + {[]interface{}{"a", "b", "c"}, "d", false}, + {[]string{"a", "b", "c"}, "d", false}, + {[]string{"a", "12", "c"}, 12, false}, + {[]string{"a", "b", "c"}, nil, false}, + {[]int{1, 2, 4}, 2, true}, + {[]interface{}{1, 2, 4}, 2, true}, + {[]interface{}{1, 2, 4}, nil, false}, + {[]interface{}{nil}, nil, false}, + {[]int{1, 2, 4}, 3, false}, + {[]float64{1.23, 2.45, 4.67}, 1.23, true}, + {[]float64{1.234567, 2.45, 4.67}, 1.234568, false}, + {"this substring should be found", "substring", true}, + {"this substring should not be found", "subseastring", false}, + {nil, "foo", false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result := ns.In(test.l1, test.l2) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestIntersect(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + l1, l2 interface{} + expect interface{} + }{ + {[]string{"a", "b", "c", "c"}, []string{"a", "b", "b"}, []string{"a", "b"}}, + {[]string{"a", "b"}, []string{"a", "b", "c"}, []string{"a", "b"}}, + {[]string{"a", "b", "c"}, []string{"d", "e"}, []string{}}, + {[]string{}, []string{}, []string{}}, + {[]string{"a", "b"}, nil, []interface{}{}}, + {nil, []string{"a", "b"}, []interface{}{}}, + {nil, nil, []interface{}{}}, + {[]string{"1", "2"}, []int{1, 2}, []string{}}, + {[]int{1, 2}, []string{"1", "2"}, []int{}}, + {[]int{1, 2, 4}, []int{2, 4}, []int{2, 4}}, + {[]int{2, 4}, []int{1, 2, 4}, []int{2, 4}}, + {[]int{1, 2, 4}, []int{3, 6}, []int{}}, + {[]float64{2.2, 4.4}, []float64{1.1, 2.2, 4.4}, []float64{2.2, 4.4}}, + // errors + {"not array or slice", []string{"a"}, false}, + {[]string{"a"}, "not array or slice", false}, + + // []interface{} ∩ []interface{} + {[]interface{}{"a", "b", "c"}, []interface{}{"a", "b", "b"}, []interface{}{"a", "b"}}, + {[]interface{}{1, 2, 3}, []interface{}{1, 2, 2}, []interface{}{1, 2}}, + {[]interface{}{int8(1), int8(2), int8(3)}, []interface{}{int8(1), int8(2), int8(2)}, []interface{}{int8(1), int8(2)}}, + {[]interface{}{int16(1), int16(2), int16(3)}, []interface{}{int16(1), int16(2), int16(2)}, []interface{}{int16(1), int16(2)}}, + {[]interface{}{int32(1), int32(2), int32(3)}, []interface{}{int32(1), int32(2), int32(2)}, []interface{}{int32(1), int32(2)}}, + {[]interface{}{int64(1), int64(2), int64(3)}, []interface{}{int64(1), int64(2), int64(2)}, []interface{}{int64(1), int64(2)}}, + {[]interface{}{float32(1), float32(2), float32(3)}, []interface{}{float32(1), float32(2), float32(2)}, []interface{}{float32(1), float32(2)}}, + {[]interface{}{float64(1), float64(2), float64(3)}, []interface{}{float64(1), float64(2), float64(2)}, []interface{}{float64(1), float64(2)}}, + + // []interface{} ∩ []T + {[]interface{}{"a", "b", "c"}, []string{"a", "b", "b"}, []interface{}{"a", "b"}}, + {[]interface{}{1, 2, 3}, []int{1, 2, 2}, []interface{}{1, 2}}, + {[]interface{}{int8(1), int8(2), int8(3)}, []int8{1, 2, 2}, []interface{}{int8(1), int8(2)}}, + {[]interface{}{int16(1), int16(2), int16(3)}, []int16{1, 2, 2}, []interface{}{int16(1), int16(2)}}, + {[]interface{}{int32(1), int32(2), int32(3)}, []int32{1, 2, 2}, []interface{}{int32(1), int32(2)}}, + {[]interface{}{int64(1), int64(2), int64(3)}, []int64{1, 2, 2}, []interface{}{int64(1), int64(2)}}, + {[]interface{}{uint(1), uint(2), uint(3)}, []uint{1, 2, 2}, []interface{}{uint(1), uint(2)}}, + {[]interface{}{float32(1), float32(2), float32(3)}, []float32{1, 2, 2}, []interface{}{float32(1), float32(2)}}, + {[]interface{}{float64(1), float64(2), float64(3)}, []float64{1, 2, 2}, []interface{}{float64(1), float64(2)}}, + + // []T ∩ []interface{} + {[]string{"a", "b", "c"}, []interface{}{"a", "b", "b"}, []string{"a", "b"}}, + {[]int{1, 2, 3}, []interface{}{1, 2, 2}, []int{1, 2}}, + {[]int8{1, 2, 3}, []interface{}{int8(1), int8(2), int8(2)}, []int8{1, 2}}, + {[]int16{1, 2, 3}, []interface{}{int16(1), int16(2), int16(2)}, []int16{1, 2}}, + {[]int32{1, 2, 3}, []interface{}{int32(1), int32(2), int32(2)}, []int32{1, 2}}, + {[]int64{1, 2, 3}, []interface{}{int64(1), int64(2), int64(2)}, []int64{1, 2}}, + {[]float32{1, 2, 3}, []interface{}{float32(1), float32(2), float32(2)}, []float32{1, 2}}, + {[]float64{1, 2, 3}, []interface{}{float64(1), float64(2), float64(2)}, []float64{1, 2}}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Intersect(test.l1, test.l2) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + assert.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestIsSet(t *testing.T) { + t.Parallel() + + ns := New(newDeps(viper.New())) + + for i, test := range []struct { + a interface{} + key interface{} + expect bool + isErr bool + errStr string + }{ + {[]interface{}{1, 2, 3, 5}, 2, true, false, ""}, + {[]interface{}{1, 2, 3, 5}, 22, false, false, ""}, + + {map[string]interface{}{"a": 1, "b": 2}, "b", true, false, ""}, + {map[string]interface{}{"a": 1, "b": 2}, "bc", false, false, ""}, + + {time.Now(), "Day", false, false, ""}, + {nil, "nil", false, false, ""}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.IsSet(test.a, test.key) + if test.isErr { + assert.EqualError(t, err, test.errStr, errMsg) + continue + } + + assert.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestLast(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + limit interface{} + seq interface{} + expect interface{} + }{ + {int(2), []string{"a", "b", "c"}, []string{"b", "c"}}, + {int32(3), []string{"a", "b"}, []string{"a", "b"}}, + {int64(2), []int{100, 200, 300}, []int{200, 300}}, + {100, []int{100, 200}, []int{100, 200}}, + {"1", []int{100, 200, 300}, []int{300}}, + // errors + {int64(-1), []int{100, 200, 300}, false}, + {"noint", []int{100, 200, 300}, false}, + {1, nil, false}, + {nil, []int{100}, false}, + {1, t, false}, + {1, (*string)(nil), false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Last(test.limit, test.seq) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestQuerify(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + params []interface{} + expect interface{} + }{ + {[]interface{}{"a", "b"}, "a=b"}, + {[]interface{}{"a", "b", "c", "d", "f", " &"}, `a=b&c=d&f=+%26`}, + // errors + {[]interface{}{5, "b"}, false}, + {[]interface{}{"a", "b", "c"}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.params) + + result, err := ns.Querify(test.params...) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestSeq(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + args []interface{} + expect interface{} + }{ + {[]interface{}{-2, 5}, []int{-2, -1, 0, 1, 2, 3, 4, 5}}, + {[]interface{}{1, 2, 4}, []int{1, 3}}, + {[]interface{}{1}, []int{1}}, + {[]interface{}{3}, []int{1, 2, 3}}, + {[]interface{}{3.2}, []int{1, 2, 3}}, + {[]interface{}{0}, []int{}}, + {[]interface{}{-1}, []int{-1}}, + {[]interface{}{-3}, []int{-1, -2, -3}}, + {[]interface{}{3, -2}, []int{3, 2, 1, 0, -1, -2}}, + {[]interface{}{6, -2, 2}, []int{6, 4, 2}}, + // errors + {[]interface{}{1, 0, 2}, false}, + {[]interface{}{1, -1, 2}, false}, + {[]interface{}{2, 1, 1}, false}, + {[]interface{}{2, 1, 1, 1}, false}, + {[]interface{}{2001}, false}, + {[]interface{}{}, false}, + {[]interface{}{0, -1000000}, false}, + {[]interface{}{tstNoStringer{}}, false}, + {nil, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Seq(test.args...) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestShuffle(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + seq interface{} + success bool + }{ + {[]string{"a", "b", "c", "d"}, true}, + {[]int{100, 200, 300}, true}, + {[]int{100, 200, 300}, true}, + {[]int{100, 200}, true}, + {[]string{"a", "b"}, true}, + {[]int{100, 200, 300}, true}, + {[]int{100, 200, 300}, true}, + {[]int{100}, true}, + // errors + {nil, false}, + {t, false}, + {(*string)(nil), false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Shuffle(test.seq) + + if !test.success { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + + resultv := reflect.ValueOf(result) + seqv := reflect.ValueOf(test.seq) + + assert.Equal(t, resultv.Len(), seqv.Len(), errMsg) + } +} + +func TestShuffleRandomising(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + // Note that this test can fail with false negative result if the shuffle + // of the sequence happens to be the same as the original sequence. However + // the propability of the event is 10^-158 which is negligible. + seqLen := 100 + rand.Seed(time.Now().UTC().UnixNano()) + + for _, test := range []struct { + seq []int + }{ + {rand.Perm(seqLen)}, + } { + result, err := ns.Shuffle(test.seq) + resultv := reflect.ValueOf(result) + + require.NoError(t, err) + + allSame := true + for i, v := range test.seq { + allSame = allSame && (resultv.Index(i).Interface() == v) + } + + assert.False(t, allSame, "Expected sequence to be shuffled but was in the same order") + } +} + +func TestSlice(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + args []interface{} + }{ + {[]interface{}{"a", "b"}}, + // errors + {[]interface{}{5, "b"}}, + {[]interface{}{tstNoStringer{}}}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.args) + + result := ns.Slice(test.args...) + + assert.Equal(t, test.args, result, errMsg) + } +} + +func TestUnion(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + l1 interface{} + l2 interface{} + expect interface{} + isErr bool + }{ + {nil, nil, []interface{}{}, false}, + {nil, []string{"a", "b"}, []string{"a", "b"}, false}, + {[]string{"a", "b"}, nil, []string{"a", "b"}, false}, + + // []A ∪ []B + {[]string{"1", "2"}, []int{3}, []string{}, false}, + {[]int{1, 2}, []string{"1", "2"}, []int{}, false}, + + // []T ∪ []T + {[]string{"a", "b", "c", "c"}, []string{"a", "b", "b"}, []string{"a", "b", "c"}, false}, + {[]string{"a", "b"}, []string{"a", "b", "c"}, []string{"a", "b", "c"}, false}, + {[]string{"a", "b", "c"}, []string{"d", "e"}, []string{"a", "b", "c", "d", "e"}, false}, + {[]string{}, []string{}, []string{}, false}, + {[]int{1, 2, 3}, []int{3, 4, 5}, []int{1, 2, 3, 4, 5}, false}, + {[]int{1, 2, 3}, []int{1, 2, 3}, []int{1, 2, 3}, false}, + {[]int{1, 2, 4}, []int{2, 4}, []int{1, 2, 4}, false}, + {[]int{2, 4}, []int{1, 2, 4}, []int{2, 4, 1}, false}, + {[]int{1, 2, 4}, []int{3, 6}, []int{1, 2, 4, 3, 6}, false}, + {[]float64{2.2, 4.4}, []float64{1.1, 2.2, 4.4}, []float64{2.2, 4.4, 1.1}, false}, + {[]interface{}{"a", "b", "c", "c"}, []interface{}{"a", "b", "b"}, []interface{}{"a", "b", "c"}, false}, + + // []T ∪ []interface{} + {[]string{"1", "2"}, []interface{}{"9"}, []string{"1", "2", "9"}, false}, + {[]int{2, 4}, []interface{}{1, 2, 4}, []int{2, 4, 1}, false}, + {[]int8{2, 4}, []interface{}{int8(1), int8(2), int8(4)}, []int8{2, 4, 1}, false}, + {[]int8{2, 4}, []interface{}{1, 2, 4}, []int8{2, 4, 1}, false}, + {[]int16{2, 4}, []interface{}{1, 2, 4}, []int16{2, 4, 1}, false}, + {[]int32{2, 4}, []interface{}{1, 2, 4}, []int32{2, 4, 1}, false}, + {[]int64{2, 4}, []interface{}{1, 2, 4}, []int64{2, 4, 1}, false}, + {[]float64{2.2, 4.4}, []interface{}{1.1, 2.2, 4.4}, []float64{2.2, 4.4, 1.1}, false}, + {[]float32{2.2, 4.4}, []interface{}{1.1, 2.2, 4.4}, []float32{2.2, 4.4, 1.1}, false}, + + // []interface{} ∪ []T + {[]interface{}{"a", "b", "c", "c"}, []string{"a", "b", "d"}, []interface{}{"a", "b", "c", "d"}, false}, + {[]interface{}{}, []string{}, []interface{}{}, false}, + {[]interface{}{1, 2}, []int{2, 3}, []interface{}{1, 2, 3}, false}, + {[]interface{}{1, 2}, []int8{2, 3}, []interface{}{1, 2, int8(3)}, false}, + {[]interface{}{uint(1), uint(2)}, []uint{2, 3}, []interface{}{uint(1), uint(2), uint(3)}, false}, + {[]interface{}{1.1, 2.2}, []float64{2.2, 3.3}, []interface{}{1.1, 2.2, 3.3}, false}, + + // errors + {"not array or slice", []string{"a"}, false, true}, + {[]string{"a"}, "not array or slice", false, true}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Union(test.l1, test.l2) + if test.isErr { + assert.Error(t, err, errMsg) + continue + } + + assert.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestUniq(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + for i, test := range []struct { + l interface{} + expect interface{} + isErr bool + }{ + {[]string{"a", "b", "c"}, []string{"a", "b", "c"}, false}, + {[]string{"a", "b", "c", "c"}, []string{"a", "b", "c"}, false}, + {[]string{"a", "b", "b", "c"}, []string{"a", "b", "c"}, false}, + {[]string{"a", "b", "c", "b"}, []string{"a", "b", "c"}, false}, + {[]int{1, 2, 3}, []int{1, 2, 3}, false}, + {[]int{1, 2, 3, 3}, []int{1, 2, 3}, false}, + {[]int{1, 2, 2, 3}, []int{1, 2, 3}, false}, + {[]int{1, 2, 3, 2}, []int{1, 2, 3}, false}, + {[4]int{1, 2, 3, 2}, []int{1, 2, 3}, false}, + {nil, make([]interface{}, 0), false}, + // should-errors + {1, 1, true}, + {"foo", "fo", true}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Uniq(test.l) + if test.isErr { + assert.Error(t, err, errMsg) + continue + } + + assert.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func (x *TstX) TstRp() string { + return "r" + x.A +} + +func (x TstX) TstRv() string { + return "r" + x.B +} + +func (x TstX) unexportedMethod() string { + return x.unexported +} + +func (x TstX) MethodWithArg(s string) string { + return s +} + +func (x TstX) MethodReturnNothing() {} + +func (x TstX) MethodReturnErrorOnly() error { + return errors.New("some error occurred") +} + +func (x TstX) MethodReturnTwoValues() (string, string) { + return "foo", "bar" +} + +func (x TstX) MethodReturnValueWithError() (string, error) { + return "", errors.New("some error occurred") +} + +func (x TstX) String() string { + return fmt.Sprintf("A: %s, B: %s", x.A, x.B) +} + +type TstX struct { + A, B string + unexported string +} + +func newDeps(cfg config.Provider) *deps.Deps { + l := helpers.NewLanguage("en", cfg) + l.Set("i18nDir", "i18n") + return &deps.Deps{ + Cfg: cfg, + Fs: hugofs.NewMem(l), + ContentSpec: helpers.NewContentSpec(l), + Log: jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime), + } +} diff --git a/tpl/collections/index.go b/tpl/collections/index.go new file mode 100644 index 000000000..b08151188 --- /dev/null +++ b/tpl/collections/index.go @@ -0,0 +1,107 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "errors" + "fmt" + "reflect" +) + +// Index returns the result of indexing its first argument by the following +// arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each +// indexed item must be a map, slice, or array. +// +// Copied from Go stdlib src/text/template/funcs.go. +// +// We deviate from the stdlib due to https://github.com/golang/go/issues/14751. +// +// TODO(moorereason): merge upstream changes. +func (ns *Namespace) Index(item interface{}, indices ...interface{}) (interface{}, error) { + v := reflect.ValueOf(item) + if !v.IsValid() { + return nil, errors.New("index of untyped nil") + } + for _, i := range indices { + index := reflect.ValueOf(i) + var isNil bool + if v, isNil = indirect(v); isNil { + return nil, errors.New("index of nil pointer") + } + switch v.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + var x int64 + switch index.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + x = index.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + x = int64(index.Uint()) + case reflect.Invalid: + return nil, errors.New("cannot index slice/array with nil") + default: + return nil, fmt.Errorf("cannot index slice/array with type %s", index.Type()) + } + if x < 0 || x >= int64(v.Len()) { + // We deviate from stdlib here. Don't return an error if the + // index is out of range. + return nil, nil + } + v = v.Index(int(x)) + case reflect.Map: + index, err := prepareArg(index, v.Type().Key()) + if err != nil { + return nil, err + } + if x := v.MapIndex(index); x.IsValid() { + v = x + } else { + v = reflect.Zero(v.Type().Elem()) + } + case reflect.Invalid: + // the loop holds invariant: v.IsValid() + panic("unreachable") + default: + return nil, fmt.Errorf("can't index item of type %s", v.Type()) + } + } + return v.Interface(), nil +} + +// prepareArg checks if value can be used as an argument of type argType, and +// converts an invalid value to appropriate zero if possible. +// +// Copied from Go stdlib src/text/template/funcs.go. +func prepareArg(value reflect.Value, argType reflect.Type) (reflect.Value, error) { + if !value.IsValid() { + if !canBeNil(argType) { + return reflect.Value{}, fmt.Errorf("value is nil; should be of type %s", argType) + } + value = reflect.Zero(argType) + } + if !value.Type().AssignableTo(argType) { + return reflect.Value{}, fmt.Errorf("value has type %s; should be %s", value.Type(), argType) + } + return value, nil +} + +// canBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero. +// +// Copied from Go stdlib src/text/template/exec.go. +func canBeNil(typ reflect.Type) bool { + switch typ.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return true + } + return false +} diff --git a/tpl/collections/index_test.go b/tpl/collections/index_test.go new file mode 100644 index 000000000..b9261735f --- /dev/null +++ b/tpl/collections/index_test.go @@ -0,0 +1,60 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "fmt" + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIndex(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + item interface{} + indices []interface{} + expect interface{} + isErr bool + }{ + {[]int{0, 1}, []interface{}{0}, 0, false}, + {[]int{0, 1}, []interface{}{9}, nil, false}, // index out of range + {[]uint{0, 1}, nil, []uint{0, 1}, false}, + {[][]int{[]int{1, 2}, []int{3, 4}}, []interface{}{0, 0}, 1, false}, + {map[int]int{1: 10, 2: 20}, []interface{}{1}, 10, false}, + {map[int]int{1: 10, 2: 20}, []interface{}{0}, 0, false}, + // errors + {nil, nil, nil, true}, + {[]int{0, 1}, []interface{}{"1"}, nil, true}, + {[]int{0, 1}, []interface{}{nil}, nil, true}, + {tstNoStringer{}, []interface{}{0}, nil, true}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Index(test.item, test.indices...) + + if test.isErr { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} diff --git a/tpl/collections/init.go b/tpl/collections/init.go new file mode 100644 index 000000000..4a7c2d875 --- /dev/null +++ b/tpl/collections/init.go @@ -0,0 +1,152 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" +) + +const name = "collections" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx := New(d) + + ns := &internal.TemplateFuncsNamespace{ + Name: name, + Context: func(args ...interface{}) interface{} { return ctx }, + } + + ns.AddMethodMapping(ctx.After, + []string{"after"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.Apply, + []string{"apply"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.Delimit, + []string{"delimit"}, + [][2]string{ + {`{{ delimit (slice "A" "B" "C") ", " " and " }}`, `A, B and C`}, + }, + ) + + ns.AddMethodMapping(ctx.Dictionary, + []string{"dict"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.EchoParam, + []string{"echoParam"}, + [][2]string{ + {`{{ echoParam .Params "langCode" }}`, `en`}, + }, + ) + + ns.AddMethodMapping(ctx.First, + []string{"first"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.In, + []string{"in"}, + [][2]string{ + {`{{ if in "this string contains a substring" "substring" }}Substring found!{{ end }}`, `Substring found!`}, + }, + ) + + ns.AddMethodMapping(ctx.Index, + []string{"index"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.Intersect, + []string{"intersect"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.IsSet, + []string{"isSet", "isset"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.Last, + []string{"last"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.Querify, + []string{"querify"}, + [][2]string{ + { + `{{ (querify "foo" 1 "bar" 2 "baz" "with spaces" "qux" "this&that=those") | safeHTML }}`, + `bar=2&baz=with+spaces&foo=1&qux=this%26that%3Dthose`}, + { + `<a href="https://www.google.com?{{ (querify "q" "test" "page" 3) | safeURL }}">Search</a>`, + `<a href="https://www.google.com?page=3&q=test">Search</a>`}, + }, + ) + + ns.AddMethodMapping(ctx.Shuffle, + []string{"shuffle"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.Slice, + []string{"slice"}, + [][2]string{ + {`{{ slice "B" "C" "A" | sort }}`, `[A B C]`}, + }, + ) + + ns.AddMethodMapping(ctx.Sort, + []string{"sort"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.Union, + []string{"union"}, + [][2]string{ + {`{{ union (slice 1 2 3) (slice 3 4 5) }}`, `[1 2 3 4 5]`}, + }, + ) + + ns.AddMethodMapping(ctx.Where, + []string{"where"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.Seq, + []string{"seq"}, + [][2]string{ + {`{{ seq 3 }}`, `[1 2 3]`}, + }, + ) + ns.AddMethodMapping(ctx.Uniq, + []string{"uniq"}, + [][2]string{ + {`{{ slice 1 2 3 2 | uniq }}`, `[1 2 3]`}, + }, + ) + + return ns + + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/collections/init_test.go b/tpl/collections/init_test.go new file mode 100644 index 000000000..0739f0412 --- /dev/null +++ b/tpl/collections/init_test.go @@ -0,0 +1,38 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + var found bool + var ns *internal.TemplateFuncsNamespace + + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + ns = nsf(&deps.Deps{}) + if ns.Name == name { + found = true + break + } + } + + require.True(t, found) + require.IsType(t, &Namespace{}, ns.Context()) +} diff --git a/tpl/collections/sort.go b/tpl/collections/sort.go new file mode 100644 index 000000000..206a19cb5 --- /dev/null +++ b/tpl/collections/sort.go @@ -0,0 +1,161 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "errors" + "reflect" + "sort" + "strings" + + "github.com/gohugoio/hugo/tpl/compare" + "github.com/spf13/cast" +) + +var comp = compare.New() + +// Sort returns a sorted sequence. +func (ns *Namespace) Sort(seq interface{}, args ...interface{}) (interface{}, error) { + if seq == nil { + return nil, errors.New("sequence must be provided") + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return nil, errors.New("can't iterate over a nil value") + } + + switch seqv.Kind() { + case reflect.Array, reflect.Slice, reflect.Map: + // ok + default: + return nil, errors.New("can't sort " + reflect.ValueOf(seq).Type().String()) + } + + // Create a list of pairs that will be used to do the sort + p := pairList{SortAsc: true, SliceType: reflect.SliceOf(seqv.Type().Elem())} + p.Pairs = make([]pair, seqv.Len()) + + var sortByField string + for i, l := range args { + dStr, err := cast.ToStringE(l) + switch { + case i == 0 && err != nil: + sortByField = "" + case i == 0 && err == nil: + sortByField = dStr + case i == 1 && err == nil && dStr == "desc": + p.SortAsc = false + case i == 1: + p.SortAsc = true + } + } + path := strings.Split(strings.Trim(sortByField, "."), ".") + + switch seqv.Kind() { + case reflect.Array, reflect.Slice: + for i := 0; i < seqv.Len(); i++ { + p.Pairs[i].Value = seqv.Index(i) + if sortByField == "" || sortByField == "value" { + p.Pairs[i].Key = p.Pairs[i].Value + } else { + v := p.Pairs[i].Value + var err error + for _, elemName := range path { + v, err = evaluateSubElem(v, elemName) + if err != nil { + return nil, err + } + } + p.Pairs[i].Key = v + } + } + + case reflect.Map: + keys := seqv.MapKeys() + for i := 0; i < seqv.Len(); i++ { + p.Pairs[i].Value = seqv.MapIndex(keys[i]) + if sortByField == "" { + p.Pairs[i].Key = keys[i] + } else if sortByField == "value" { + p.Pairs[i].Key = p.Pairs[i].Value + } else { + v := p.Pairs[i].Value + var err error + for _, elemName := range path { + v, err = evaluateSubElem(v, elemName) + if err != nil { + return nil, err + } + } + p.Pairs[i].Key = v + } + } + } + return p.sort(), nil +} + +// Credit for pair sorting method goes to Andrew Gerrand +// https://groups.google.com/forum/#!topic/golang-nuts/FT7cjmcL7gw +// A data structure to hold a key/value pair. +type pair struct { + Key reflect.Value + Value reflect.Value +} + +// A slice of pairs that implements sort.Interface to sort by Value. +type pairList struct { + Pairs []pair + SortAsc bool + SliceType reflect.Type +} + +func (p pairList) Swap(i, j int) { p.Pairs[i], p.Pairs[j] = p.Pairs[j], p.Pairs[i] } +func (p pairList) Len() int { return len(p.Pairs) } +func (p pairList) Less(i, j int) bool { + iv := p.Pairs[i].Key + jv := p.Pairs[j].Key + + if iv.IsValid() { + if jv.IsValid() { + // can only call Interface() on valid reflect Values + return comp.Lt(iv.Interface(), jv.Interface()) + } + // if j is invalid, test i against i's zero value + return comp.Lt(iv.Interface(), reflect.Zero(iv.Type())) + } + + if jv.IsValid() { + // if i is invalid, test j against j's zero value + return comp.Lt(reflect.Zero(jv.Type()), jv.Interface()) + } + + return false +} + +// sorts a pairList and returns a slice of sorted values +func (p pairList) sort() interface{} { + if p.SortAsc { + sort.Sort(p) + } else { + sort.Sort(sort.Reverse(p)) + } + sorted := reflect.MakeSlice(p.SliceType, len(p.Pairs), len(p.Pairs)) + for i, v := range p.Pairs { + sorted.Index(i).Set(v.Value) + } + + return sorted.Interface() +} diff --git a/tpl/collections/sort_test.go b/tpl/collections/sort_test.go new file mode 100644 index 000000000..8db928f2d --- /dev/null +++ b/tpl/collections/sort_test.go @@ -0,0 +1,237 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "reflect" + "testing" + + "github.com/gohugoio/hugo/deps" +) + +func TestSort(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + type ts struct { + MyInt int + MyFloat float64 + MyString string + } + type mid struct { + Tst TstX + } + + for i, test := range []struct { + seq interface{} + sortByField interface{} + sortAsc string + expect interface{} + }{ + {[]string{"class1", "class2", "class3"}, nil, "asc", []string{"class1", "class2", "class3"}}, + {[]string{"class3", "class1", "class2"}, nil, "asc", []string{"class1", "class2", "class3"}}, + {[]int{1, 2, 3, 4, 5}, nil, "asc", []int{1, 2, 3, 4, 5}}, + {[]int{5, 4, 3, 1, 2}, nil, "asc", []int{1, 2, 3, 4, 5}}, + // test sort key parameter is focibly set empty + {[]string{"class3", "class1", "class2"}, map[int]string{1: "a"}, "asc", []string{"class1", "class2", "class3"}}, + // test map sorting by keys + {map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, nil, "asc", []int{10, 20, 30, 40, 50}}, + {map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, nil, "asc", []int{30, 20, 10, 40, 50}}, + {map[string]string{"1": "10", "2": "20", "3": "30", "4": "40", "5": "50"}, nil, "asc", []string{"10", "20", "30", "40", "50"}}, + {map[string]string{"3": "10", "2": "20", "1": "30", "4": "40", "5": "50"}, nil, "asc", []string{"30", "20", "10", "40", "50"}}, + {map[string]string{"one": "10", "two": "20", "three": "30", "four": "40", "five": "50"}, nil, "asc", []string{"50", "40", "10", "30", "20"}}, + {map[int]string{1: "10", 2: "20", 3: "30", 4: "40", 5: "50"}, nil, "asc", []string{"10", "20", "30", "40", "50"}}, + {map[int]string{3: "10", 2: "20", 1: "30", 4: "40", 5: "50"}, nil, "asc", []string{"30", "20", "10", "40", "50"}}, + {map[float64]string{3.3: "10", 2.3: "20", 1.3: "30", 4.3: "40", 5.3: "50"}, nil, "asc", []string{"30", "20", "10", "40", "50"}}, + // test map sorting by value + {map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, "value", "asc", []int{10, 20, 30, 40, 50}}, + {map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, "value", "asc", []int{10, 20, 30, 40, 50}}, + // test map sorting by field value + { + map[string]ts{"1": {10, 10.5, "ten"}, "2": {20, 20.5, "twenty"}, "3": {30, 30.5, "thirty"}, "4": {40, 40.5, "forty"}, "5": {50, 50.5, "fifty"}}, + "MyInt", + "asc", + []ts{{10, 10.5, "ten"}, {20, 20.5, "twenty"}, {30, 30.5, "thirty"}, {40, 40.5, "forty"}, {50, 50.5, "fifty"}}, + }, + { + map[string]ts{"1": {10, 10.5, "ten"}, "2": {20, 20.5, "twenty"}, "3": {30, 30.5, "thirty"}, "4": {40, 40.5, "forty"}, "5": {50, 50.5, "fifty"}}, + "MyFloat", + "asc", + []ts{{10, 10.5, "ten"}, {20, 20.5, "twenty"}, {30, 30.5, "thirty"}, {40, 40.5, "forty"}, {50, 50.5, "fifty"}}, + }, + { + map[string]ts{"1": {10, 10.5, "ten"}, "2": {20, 20.5, "twenty"}, "3": {30, 30.5, "thirty"}, "4": {40, 40.5, "forty"}, "5": {50, 50.5, "fifty"}}, + "MyString", + "asc", + []ts{{50, 50.5, "fifty"}, {40, 40.5, "forty"}, {10, 10.5, "ten"}, {30, 30.5, "thirty"}, {20, 20.5, "twenty"}}, + }, + // test sort desc + {[]string{"class1", "class2", "class3"}, "value", "desc", []string{"class3", "class2", "class1"}}, + {[]string{"class3", "class1", "class2"}, "value", "desc", []string{"class3", "class2", "class1"}}, + // test sort by struct's method + { + []TstX{{A: "i", B: "j"}, {A: "e", B: "f"}, {A: "c", B: "d"}, {A: "g", B: "h"}, {A: "a", B: "b"}}, + "TstRv", + "asc", + []TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, + }, + { + []*TstX{{A: "i", B: "j"}, {A: "e", B: "f"}, {A: "c", B: "d"}, {A: "g", B: "h"}, {A: "a", B: "b"}}, + "TstRp", + "asc", + []*TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, + }, + // test map sorting by struct's method + { + map[string]TstX{"1": {A: "i", B: "j"}, "2": {A: "e", B: "f"}, "3": {A: "c", B: "d"}, "4": {A: "g", B: "h"}, "5": {A: "a", B: "b"}}, + "TstRv", + "asc", + []TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, + }, + { + map[string]*TstX{"1": {A: "i", B: "j"}, "2": {A: "e", B: "f"}, "3": {A: "c", B: "d"}, "4": {A: "g", B: "h"}, "5": {A: "a", B: "b"}}, + "TstRp", + "asc", + []*TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, + }, + // test sort by dot chaining key argument + { + []map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}}, + "foo.A", + "asc", + []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, + }, + { + []map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}}, + ".foo.A", + "asc", + []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, + }, + { + []map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}}, + "foo.TstRv", + "asc", + []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, + }, + { + []map[string]*TstX{{"foo": &TstX{A: "e", B: "f"}}, {"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}}, + "foo.TstRp", + "asc", + []map[string]*TstX{{"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}, {"foo": &TstX{A: "e", B: "f"}}}, + }, + { + []map[string]mid{{"foo": mid{Tst: TstX{A: "e", B: "f"}}}, {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}}, + "foo.Tst.A", + "asc", + []map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}}, + }, + { + []map[string]mid{{"foo": mid{Tst: TstX{A: "e", B: "f"}}}, {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}}, + "foo.Tst.TstRv", + "asc", + []map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}}, + }, + // test map sorting by dot chaining key argument + { + map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}}, + "foo.A", + "asc", + []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, + }, + { + map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}}, + ".foo.A", + "asc", + []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, + }, + { + map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}}, + "foo.TstRv", + "asc", + []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, + }, + { + map[string]map[string]*TstX{"1": {"foo": &TstX{A: "e", B: "f"}}, "2": {"foo": &TstX{A: "a", B: "b"}}, "3": {"foo": &TstX{A: "c", B: "d"}}}, + "foo.TstRp", + "asc", + []map[string]*TstX{{"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}, {"foo": &TstX{A: "e", B: "f"}}}, + }, + { + map[string]map[string]mid{"1": {"foo": mid{Tst: TstX{A: "e", B: "f"}}}, "2": {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, "3": {"foo": mid{Tst: TstX{A: "c", B: "d"}}}}, + "foo.Tst.A", + "asc", + []map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}}, + }, + { + map[string]map[string]mid{"1": {"foo": mid{Tst: TstX{A: "e", B: "f"}}}, "2": {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, "3": {"foo": mid{Tst: TstX{A: "c", B: "d"}}}}, + "foo.Tst.TstRv", + "asc", + []map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}}, + }, + // interface slice with missing elements + { + []interface{}{ + map[interface{}]interface{}{"Title": "Foo", "Weight": 10}, + map[interface{}]interface{}{"Title": "Bar"}, + map[interface{}]interface{}{"Title": "Zap", "Weight": 5}, + }, + "Weight", + "asc", + []interface{}{ + map[interface{}]interface{}{"Title": "Bar"}, + map[interface{}]interface{}{"Title": "Zap", "Weight": 5}, + map[interface{}]interface{}{"Title": "Foo", "Weight": 10}, + }, + }, + // test error cases + {(*[]TstX)(nil), nil, "asc", false}, + {TstX{A: "a", B: "b"}, nil, "asc", false}, + { + []map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}}, + "foo.NotAvailable", + "asc", + false, + }, + { + map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}}, + "foo.NotAvailable", + "asc", + false, + }, + {nil, nil, "asc", false}, + } { + var result interface{} + var err error + if test.sortByField == nil { + result, err = ns.Sort(test.seq) + } else { + result, err = ns.Sort(test.seq, test.sortByField, test.sortAsc) + } + + if b, ok := test.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] Sort didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(result, test.expect) { + t.Errorf("[%d] Sort called on sequence: %v | sortByField: `%v` | got %v but expected %v", i, test.seq, test.sortByField, result, test.expect) + } + } + } +} diff --git a/tpl/collections/where.go b/tpl/collections/where.go new file mode 100644 index 000000000..e9528fb86 --- /dev/null +++ b/tpl/collections/where.go @@ -0,0 +1,431 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "errors" + "fmt" + "reflect" + "strings" + "time" +) + +// Where returns a filtered subset of a given data type. +func (ns *Namespace) Where(seq, key interface{}, args ...interface{}) (interface{}, error) { + seqv, isNil := indirect(reflect.ValueOf(seq)) + if isNil { + return nil, errors.New("can't iterate over a nil value of type " + reflect.ValueOf(seq).Type().String()) + } + + mv, op, err := parseWhereArgs(args...) + if err != nil { + return nil, err + } + + var path []string + kv := reflect.ValueOf(key) + if kv.Kind() == reflect.String { + path = strings.Split(strings.Trim(kv.String(), "."), ".") + } + + switch seqv.Kind() { + case reflect.Array, reflect.Slice: + return ns.checkWhereArray(seqv, kv, mv, path, op) + case reflect.Map: + return ns.checkWhereMap(seqv, kv, mv, path, op) + default: + return nil, fmt.Errorf("can't iterate over %v", seq) + } +} + +func (ns *Namespace) checkCondition(v, mv reflect.Value, op string) (bool, error) { + v, vIsNil := indirect(v) + if !v.IsValid() { + vIsNil = true + } + mv, mvIsNil := indirect(mv) + if !mv.IsValid() { + mvIsNil = true + } + if vIsNil || mvIsNil { + switch op { + case "", "=", "==", "eq": + return vIsNil == mvIsNil, nil + case "!=", "<>", "ne": + return vIsNil != mvIsNil, nil + } + return false, nil + } + + if v.Kind() == reflect.Bool && mv.Kind() == reflect.Bool { + switch op { + case "", "=", "==", "eq": + return v.Bool() == mv.Bool(), nil + case "!=", "<>", "ne": + return v.Bool() != mv.Bool(), nil + } + return false, nil + } + + var ivp, imvp *int64 + var svp, smvp *string + var slv, slmv interface{} + var ima []int64 + var sma []string + if mv.Type() == v.Type() { + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + iv := v.Int() + ivp = &iv + imv := mv.Int() + imvp = &imv + case reflect.String: + sv := v.String() + svp = &sv + smv := mv.String() + smvp = &smv + case reflect.Struct: + switch v.Type() { + case timeType: + iv := toTimeUnix(v) + ivp = &iv + imv := toTimeUnix(mv) + imvp = &imv + } + case reflect.Array, reflect.Slice: + slv = v.Interface() + slmv = mv.Interface() + } + } else { + if mv.Kind() != reflect.Array && mv.Kind() != reflect.Slice { + return false, nil + } + + if mv.Len() == 0 { + return false, nil + } + + if v.Kind() != reflect.Interface && mv.Type().Elem().Kind() != reflect.Interface && mv.Type().Elem() != v.Type() { + return false, nil + } + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + iv := v.Int() + ivp = &iv + for i := 0; i < mv.Len(); i++ { + if anInt, err := toInt(mv.Index(i)); err == nil { + ima = append(ima, anInt) + } + } + case reflect.String: + sv := v.String() + svp = &sv + for i := 0; i < mv.Len(); i++ { + if aString, err := toString(mv.Index(i)); err == nil { + sma = append(sma, aString) + } + } + case reflect.Struct: + switch v.Type() { + case timeType: + iv := toTimeUnix(v) + ivp = &iv + for i := 0; i < mv.Len(); i++ { + ima = append(ima, toTimeUnix(mv.Index(i))) + } + } + } + } + + switch op { + case "", "=", "==", "eq": + if ivp != nil && imvp != nil { + return *ivp == *imvp, nil + } else if svp != nil && smvp != nil { + return *svp == *smvp, nil + } + case "!=", "<>", "ne": + if ivp != nil && imvp != nil { + return *ivp != *imvp, nil + } else if svp != nil && smvp != nil { + return *svp != *smvp, nil + } + case ">=", "ge": + if ivp != nil && imvp != nil { + return *ivp >= *imvp, nil + } else if svp != nil && smvp != nil { + return *svp >= *smvp, nil + } + case ">", "gt": + if ivp != nil && imvp != nil { + return *ivp > *imvp, nil + } else if svp != nil && smvp != nil { + return *svp > *smvp, nil + } + case "<=", "le": + if ivp != nil && imvp != nil { + return *ivp <= *imvp, nil + } else if svp != nil && smvp != nil { + return *svp <= *smvp, nil + } + case "<", "lt": + if ivp != nil && imvp != nil { + return *ivp < *imvp, nil + } else if svp != nil && smvp != nil { + return *svp < *smvp, nil + } + case "in", "not in": + var r bool + if ivp != nil && len(ima) > 0 { + r = ns.In(ima, *ivp) + } else if svp != nil { + if len(sma) > 0 { + r = ns.In(sma, *svp) + } else if smvp != nil { + r = ns.In(*smvp, *svp) + } + } else { + return false, nil + } + if op == "not in" { + return !r, nil + } + return r, nil + case "intersect": + r, err := ns.Intersect(slv, slmv) + if err != nil { + return false, err + } + + if reflect.TypeOf(r).Kind() == reflect.Slice { + s := reflect.ValueOf(r) + + if s.Len() > 0 { + return true, nil + } + return false, nil + } + return false, errors.New("invalid intersect values") + default: + return false, errors.New("no such operator") + } + return false, nil +} + +func evaluateSubElem(obj reflect.Value, elemName string) (reflect.Value, error) { + if !obj.IsValid() { + return zero, errors.New("can't evaluate an invalid value") + } + typ := obj.Type() + obj, isNil := indirect(obj) + + // first, check whether obj has a method. In this case, obj is + // an interface, a struct or its pointer. If obj is a struct, + // to check all T and *T method, use obj pointer type Value + objPtr := obj + if objPtr.Kind() != reflect.Interface && objPtr.CanAddr() { + objPtr = objPtr.Addr() + } + mt, ok := objPtr.Type().MethodByName(elemName) + if ok { + if mt.PkgPath != "" { + return zero, fmt.Errorf("%s is an unexported method of type %s", elemName, typ) + } + // struct pointer has one receiver argument and interface doesn't have an argument + if mt.Type.NumIn() > 1 || mt.Type.NumOut() == 0 || mt.Type.NumOut() > 2 { + return zero, fmt.Errorf("%s is a method of type %s but doesn't satisfy requirements", elemName, typ) + } + if mt.Type.NumOut() == 1 && mt.Type.Out(0).Implements(errorType) { + return zero, fmt.Errorf("%s is a method of type %s but doesn't satisfy requirements", elemName, typ) + } + if mt.Type.NumOut() == 2 && !mt.Type.Out(1).Implements(errorType) { + return zero, fmt.Errorf("%s is a method of type %s but doesn't satisfy requirements", elemName, typ) + } + res := objPtr.Method(mt.Index).Call([]reflect.Value{}) + if len(res) == 2 && !res[1].IsNil() { + return zero, fmt.Errorf("error at calling a method %s of type %s: %s", elemName, typ, res[1].Interface().(error)) + } + return res[0], nil + } + + // elemName isn't a method so next start to check whether it is + // a struct field or a map value. In both cases, it mustn't be + // a nil value + if isNil { + return zero, fmt.Errorf("can't evaluate a nil pointer of type %s by a struct field or map key name %s", typ, elemName) + } + switch obj.Kind() { + case reflect.Struct: + ft, ok := obj.Type().FieldByName(elemName) + if ok { + if ft.PkgPath != "" && !ft.Anonymous { + return zero, fmt.Errorf("%s is an unexported field of struct type %s", elemName, typ) + } + return obj.FieldByIndex(ft.Index), nil + } + return zero, fmt.Errorf("%s isn't a field of struct type %s", elemName, typ) + case reflect.Map: + kv := reflect.ValueOf(elemName) + if kv.Type().AssignableTo(obj.Type().Key()) { + return obj.MapIndex(kv), nil + } + return zero, fmt.Errorf("%s isn't a key of map type %s", elemName, typ) + } + return zero, fmt.Errorf("%s is neither a struct field, a method nor a map element of type %s", elemName, typ) +} + +// parseWhereArgs parses the end arguments to the where function. Return a +// match value and an operator, if one is defined. +func parseWhereArgs(args ...interface{}) (mv reflect.Value, op string, err error) { + switch len(args) { + case 1: + mv = reflect.ValueOf(args[0]) + case 2: + var ok bool + if op, ok = args[0].(string); !ok { + err = errors.New("operator argument must be string type") + return + } + op = strings.TrimSpace(strings.ToLower(op)) + mv = reflect.ValueOf(args[1]) + default: + err = errors.New("can't evaluate the array by no match argument or more than or equal to two arguments") + } + return +} + +// checkWhereArray handles the where-matching logic when the seqv value is an +// Array or Slice. +func (ns *Namespace) checkWhereArray(seqv, kv, mv reflect.Value, path []string, op string) (interface{}, error) { + rv := reflect.MakeSlice(seqv.Type(), 0, 0) + for i := 0; i < seqv.Len(); i++ { + var vvv reflect.Value + rvv := seqv.Index(i) + if kv.Kind() == reflect.String { + vvv = rvv + for _, elemName := range path { + var err error + vvv, err = evaluateSubElem(vvv, elemName) + if err != nil { + return nil, err + } + } + } else { + vv, _ := indirect(rvv) + if vv.Kind() == reflect.Map && kv.Type().AssignableTo(vv.Type().Key()) { + vvv = vv.MapIndex(kv) + } + } + + if ok, err := ns.checkCondition(vvv, mv, op); ok { + rv = reflect.Append(rv, rvv) + } else if err != nil { + return nil, err + } + } + return rv.Interface(), nil +} + +// checkWhereMap handles the where-matching logic when the seqv value is a Map. +func (ns *Namespace) checkWhereMap(seqv, kv, mv reflect.Value, path []string, op string) (interface{}, error) { + rv := reflect.MakeMap(seqv.Type()) + keys := seqv.MapKeys() + for _, k := range keys { + elemv := seqv.MapIndex(k) + switch elemv.Kind() { + case reflect.Array, reflect.Slice: + r, err := ns.checkWhereArray(elemv, kv, mv, path, op) + if err != nil { + return nil, err + } + + switch rr := reflect.ValueOf(r); rr.Kind() { + case reflect.Slice: + if rr.Len() > 0 { + rv.SetMapIndex(k, elemv) + } + } + case reflect.Interface: + elemvv, isNil := indirect(elemv) + if isNil { + continue + } + + switch elemvv.Kind() { + case reflect.Array, reflect.Slice: + r, err := ns.checkWhereArray(elemvv, kv, mv, path, op) + if err != nil { + return nil, err + } + + switch rr := reflect.ValueOf(r); rr.Kind() { + case reflect.Slice: + if rr.Len() > 0 { + rv.SetMapIndex(k, elemv) + } + } + } + } + } + return rv.Interface(), nil +} + +// toFloat returns the int value if possible. +func toFloat(v reflect.Value) (float64, error) { + switch v.Kind() { + case reflect.Float32, reflect.Float64: + return v.Float(), nil + case reflect.Interface: + return toFloat(v.Elem()) + } + return -1, errors.New("unable to convert value to float") +} + +// toInt returns the int value if possible, -1 if not. +func toInt(v reflect.Value) (int64, error) { + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int(), nil + case reflect.Interface: + return toInt(v.Elem()) + } + return -1, errors.New("unable to convert value to int") +} + +// toString returns the string value if possible, "" if not. +func toString(v reflect.Value) (string, error) { + switch v.Kind() { + case reflect.String: + return v.String(), nil + case reflect.Interface: + return toString(v.Elem()) + } + return "", errors.New("unable to convert value to string") +} + +var ( + zero reflect.Value + errorType = reflect.TypeOf((*error)(nil)).Elem() + timeType = reflect.TypeOf((*time.Time)(nil)).Elem() +) + +func toTimeUnix(v reflect.Value) int64 { + if v.Kind() == reflect.Interface { + return toTimeUnix(v.Elem()) + } + if v.Type() != timeType { + panic("coding error: argument must be time.Time type reflect Value") + } + return v.MethodByName("Unix").Call([]reflect.Value{})[0].Int() +} diff --git a/tpl/collections/where_test.go b/tpl/collections/where_test.go new file mode 100644 index 000000000..771fafb61 --- /dev/null +++ b/tpl/collections/where_test.go @@ -0,0 +1,606 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "fmt" + "reflect" + "testing" + "time" + + "github.com/gohugoio/hugo/deps" +) + +func TestWhere(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + type Mid struct { + Tst TstX + } + + d1 := time.Now() + d2 := d1.Add(1 * time.Hour) + d3 := d2.Add(1 * time.Hour) + d4 := d3.Add(1 * time.Hour) + d5 := d4.Add(1 * time.Hour) + d6 := d5.Add(1 * time.Hour) + + for i, test := range []struct { + seq interface{} + key interface{} + op string + match interface{} + expect interface{} + }{ + { + seq: []map[int]string{ + {1: "a", 2: "m"}, {1: "c", 2: "d"}, {1: "e", 3: "m"}, + }, + key: 2, match: "m", + expect: []map[int]string{ + {1: "a", 2: "m"}, + }, + }, + { + seq: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "x": 4}, + }, + key: "b", match: 4, + expect: []map[string]int{ + {"a": 3, "b": 4}, + }, + }, + { + seq: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", match: "f", + expect: []TstX{ + {A: "e", B: "f"}, + }, + }, + { + seq: []*map[int]string{ + {1: "a", 2: "m"}, {1: "c", 2: "d"}, {1: "e", 3: "m"}, + }, + key: 2, match: "m", + expect: []*map[int]string{ + {1: "a", 2: "m"}, + }, + }, + { + seq: []*TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", match: "f", + expect: []*TstX{ + {A: "e", B: "f"}, + }, + }, + { + seq: []*TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "c"}, + }, + key: "TstRp", match: "rc", + expect: []*TstX{ + {A: "c", B: "d"}, + }, + }, + { + seq: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "c"}, + }, + key: "TstRv", match: "rc", + expect: []TstX{ + {A: "e", B: "c"}, + }, + }, + { + seq: []map[string]TstX{ + {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}, + }, + key: "foo.B", match: "d", + expect: []map[string]TstX{ + {"foo": TstX{A: "c", B: "d"}}, + }, + }, + { + seq: []map[string]TstX{ + {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}, + }, + key: ".foo.B", match: "d", + expect: []map[string]TstX{ + {"foo": TstX{A: "c", B: "d"}}, + }, + }, + { + seq: []map[string]TstX{ + {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}, + }, + key: "foo.TstRv", match: "rd", + expect: []map[string]TstX{ + {"foo": TstX{A: "c", B: "d"}}, + }, + }, + { + seq: []map[string]*TstX{ + {"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}, {"foo": &TstX{A: "e", B: "f"}}, + }, + key: "foo.TstRp", match: "rc", + expect: []map[string]*TstX{ + {"foo": &TstX{A: "c", B: "d"}}, + }, + }, + { + seq: []map[string]Mid{ + {"foo": Mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": Mid{Tst: TstX{A: "e", B: "f"}}}, + }, + key: "foo.Tst.B", match: "d", + expect: []map[string]Mid{ + {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, + }, + }, + { + seq: []map[string]Mid{ + {"foo": Mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": Mid{Tst: TstX{A: "e", B: "f"}}}, + }, + key: "foo.Tst.TstRv", match: "rd", + expect: []map[string]Mid{ + {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, + }, + }, + { + seq: []map[string]*Mid{ + {"foo": &Mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": &Mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": &Mid{Tst: TstX{A: "e", B: "f"}}}, + }, + key: "foo.Tst.TstRp", match: "rc", + expect: []map[string]*Mid{ + {"foo": &Mid{Tst: TstX{A: "c", B: "d"}}}, + }, + }, + { + seq: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}, + }, + key: "b", op: ">", match: 3, + expect: []map[string]int{ + {"a": 3, "b": 4}, {"a": 5, "b": 6}, + }, + }, + { + seq: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", op: "!=", match: "f", + expect: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, + }, + }, + { + seq: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}, + }, + key: "b", op: "in", match: []int{3, 4, 5}, + expect: []map[string]int{ + {"a": 3, "b": 4}, + }, + }, + { + seq: []map[string][]string{ + {"a": []string{"A", "B", "C"}, "b": []string{"D", "E", "F"}}, {"a": []string{"G", "H", "I"}, "b": []string{"J", "K", "L"}}, {"a": []string{"M", "N", "O"}, "b": []string{"P", "Q", "R"}}, + }, + key: "b", op: "intersect", match: []string{"D", "P", "Q"}, + expect: []map[string][]string{ + {"a": []string{"A", "B", "C"}, "b": []string{"D", "E", "F"}}, {"a": []string{"M", "N", "O"}, "b": []string{"P", "Q", "R"}}, + }, + }, + { + seq: []map[string][]int{ + {"a": []int{1, 2, 3}, "b": []int{4, 5, 6}}, {"a": []int{7, 8, 9}, "b": []int{10, 11, 12}}, {"a": []int{13, 14, 15}, "b": []int{16, 17, 18}}, + }, + key: "b", op: "intersect", match: []int{4, 10, 12}, + expect: []map[string][]int{ + {"a": []int{1, 2, 3}, "b": []int{4, 5, 6}}, {"a": []int{7, 8, 9}, "b": []int{10, 11, 12}}, + }, + }, + { + seq: []map[string][]int8{ + {"a": []int8{1, 2, 3}, "b": []int8{4, 5, 6}}, {"a": []int8{7, 8, 9}, "b": []int8{10, 11, 12}}, {"a": []int8{13, 14, 15}, "b": []int8{16, 17, 18}}, + }, + key: "b", op: "intersect", match: []int8{4, 10, 12}, + expect: []map[string][]int8{ + {"a": []int8{1, 2, 3}, "b": []int8{4, 5, 6}}, {"a": []int8{7, 8, 9}, "b": []int8{10, 11, 12}}, + }, + }, + { + seq: []map[string][]int16{ + {"a": []int16{1, 2, 3}, "b": []int16{4, 5, 6}}, {"a": []int16{7, 8, 9}, "b": []int16{10, 11, 12}}, {"a": []int16{13, 14, 15}, "b": []int16{16, 17, 18}}, + }, + key: "b", op: "intersect", match: []int16{4, 10, 12}, + expect: []map[string][]int16{ + {"a": []int16{1, 2, 3}, "b": []int16{4, 5, 6}}, {"a": []int16{7, 8, 9}, "b": []int16{10, 11, 12}}, + }, + }, + { + seq: []map[string][]int32{ + {"a": []int32{1, 2, 3}, "b": []int32{4, 5, 6}}, {"a": []int32{7, 8, 9}, "b": []int32{10, 11, 12}}, {"a": []int32{13, 14, 15}, "b": []int32{16, 17, 18}}, + }, + key: "b", op: "intersect", match: []int32{4, 10, 12}, + expect: []map[string][]int32{ + {"a": []int32{1, 2, 3}, "b": []int32{4, 5, 6}}, {"a": []int32{7, 8, 9}, "b": []int32{10, 11, 12}}, + }, + }, + { + seq: []map[string][]int64{ + {"a": []int64{1, 2, 3}, "b": []int64{4, 5, 6}}, {"a": []int64{7, 8, 9}, "b": []int64{10, 11, 12}}, {"a": []int64{13, 14, 15}, "b": []int64{16, 17, 18}}, + }, + key: "b", op: "intersect", match: []int64{4, 10, 12}, + expect: []map[string][]int64{ + {"a": []int64{1, 2, 3}, "b": []int64{4, 5, 6}}, {"a": []int64{7, 8, 9}, "b": []int64{10, 11, 12}}, + }, + }, + { + seq: []map[string][]float32{ + {"a": []float32{1.0, 2.0, 3.0}, "b": []float32{4.0, 5.0, 6.0}}, {"a": []float32{7.0, 8.0, 9.0}, "b": []float32{10.0, 11.0, 12.0}}, {"a": []float32{13.0, 14.0, 15.0}, "b": []float32{16.0, 17.0, 18.0}}, + }, + key: "b", op: "intersect", match: []float32{4, 10, 12}, + expect: []map[string][]float32{ + {"a": []float32{1.0, 2.0, 3.0}, "b": []float32{4.0, 5.0, 6.0}}, {"a": []float32{7.0, 8.0, 9.0}, "b": []float32{10.0, 11.0, 12.0}}, + }, + }, + { + seq: []map[string][]float64{ + {"a": []float64{1.0, 2.0, 3.0}, "b": []float64{4.0, 5.0, 6.0}}, {"a": []float64{7.0, 8.0, 9.0}, "b": []float64{10.0, 11.0, 12.0}}, {"a": []float64{13.0, 14.0, 15.0}, "b": []float64{16.0, 17.0, 18.0}}, + }, + key: "b", op: "intersect", match: []float64{4, 10, 12}, + expect: []map[string][]float64{ + {"a": []float64{1.0, 2.0, 3.0}, "b": []float64{4.0, 5.0, 6.0}}, {"a": []float64{7.0, 8.0, 9.0}, "b": []float64{10.0, 11.0, 12.0}}, + }, + }, + { + seq: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}, + }, + key: "b", op: "in", match: ns.Slice(3, 4, 5), + expect: []map[string]int{ + {"a": 3, "b": 4}, + }, + }, + { + seq: []map[string]time.Time{ + {"a": d1, "b": d2}, {"a": d3, "b": d4}, {"a": d5, "b": d6}, + }, + key: "b", op: "in", match: ns.Slice(d3, d4, d5), + expect: []map[string]time.Time{ + {"a": d3, "b": d4}, + }, + }, + { + seq: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", op: "not in", match: []string{"c", "d", "e"}, + expect: []TstX{ + {A: "a", B: "b"}, {A: "e", B: "f"}, + }, + }, + { + seq: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", op: "not in", match: ns.Slice("c", t, "d", "e"), + expect: []TstX{ + {A: "a", B: "b"}, {A: "e", B: "f"}, + }, + }, + { + seq: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3}, {"a": 5, "b": 6}, + }, + key: "b", op: "", match: nil, + expect: []map[string]int{ + {"a": 3}, + }, + }, + { + seq: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3}, {"a": 5, "b": 6}, + }, + key: "b", op: "!=", match: nil, + expect: []map[string]int{ + {"a": 1, "b": 2}, {"a": 5, "b": 6}, + }, + }, + { + seq: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3}, {"a": 5, "b": 6}, + }, + key: "b", op: ">", match: nil, + expect: []map[string]int{}, + }, + { + seq: []map[string]bool{ + {"a": true, "b": false}, {"c": true, "b": true}, {"d": true, "b": false}, + }, + key: "b", op: "", match: true, + expect: []map[string]bool{ + {"c": true, "b": true}, + }, + }, + { + seq: []map[string]bool{ + {"a": true, "b": false}, {"c": true, "b": true}, {"d": true, "b": false}, + }, + key: "b", op: "!=", match: true, + expect: []map[string]bool{ + {"a": true, "b": false}, {"d": true, "b": false}, + }, + }, + { + seq: []map[string]bool{ + {"a": true, "b": false}, {"c": true, "b": true}, {"d": true, "b": false}, + }, + key: "b", op: ">", match: false, + expect: []map[string]bool{}, + }, + {seq: (*[]TstX)(nil), key: "A", match: "a", expect: false}, + {seq: TstX{A: "a", B: "b"}, key: "A", match: "a", expect: false}, + {seq: []map[string]*TstX{{"foo": nil}}, key: "foo.B", match: "d", expect: false}, + { + seq: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", op: "op", match: "f", + expect: false, + }, + { + seq: map[string]interface{}{ + "foo": []interface{}{map[interface{}]interface{}{"a": 1, "b": 2}}, + "bar": []interface{}{map[interface{}]interface{}{"a": 3, "b": 4}}, + "zap": []interface{}{map[interface{}]interface{}{"a": 5, "b": 6}}, + }, + key: "b", op: "in", match: ns.Slice(3, 4, 5), + expect: map[string]interface{}{ + "bar": []interface{}{map[interface{}]interface{}{"a": 3, "b": 4}}, + }, + }, + { + seq: map[string]interface{}{ + "foo": []interface{}{map[interface{}]interface{}{"a": 1, "b": 2}}, + "bar": []interface{}{map[interface{}]interface{}{"a": 3, "b": 4}}, + "zap": []interface{}{map[interface{}]interface{}{"a": 5, "b": 6}}, + }, + key: "b", op: ">", match: 3, + expect: map[string]interface{}{ + "bar": []interface{}{map[interface{}]interface{}{"a": 3, "b": 4}}, + "zap": []interface{}{map[interface{}]interface{}{"a": 5, "b": 6}}, + }, + }, + } { + var results interface{} + var err error + + if len(test.op) > 0 { + results, err = ns.Where(test.seq, test.key, test.op, test.match) + } else { + results, err = ns.Where(test.seq, test.key, test.match) + } + if b, ok := test.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] Where didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(results, test.expect) { + t.Errorf("[%d] Where clause matching %v with %v, got %v but expected %v", i, test.key, test.match, results, test.expect) + } + } + } + + var err error + _, err = ns.Where(map[string]int{"a": 1, "b": 2}, "a", []byte("="), 1) + if err == nil { + t.Errorf("Where called with none string op value didn't return an expected error") + } + + _, err = ns.Where(map[string]int{"a": 1, "b": 2}, "a", []byte("="), 1, 2) + if err == nil { + t.Errorf("Where called with more than two variable arguments didn't return an expected error") + } + + _, err = ns.Where(map[string]int{"a": 1, "b": 2}, "a") + if err == nil { + t.Errorf("Where called with no variable arguments didn't return an expected error") + } +} + +func TestCheckCondition(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + type expect struct { + result bool + isError bool + } + + for i, test := range []struct { + value reflect.Value + match reflect.Value + op string + expect + }{ + {reflect.ValueOf(123), reflect.ValueOf(123), "", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("foo"), "", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + "", + expect{true, false}, + }, + {reflect.ValueOf(true), reflect.ValueOf(true), "", expect{true, false}}, + {reflect.ValueOf(nil), reflect.ValueOf(nil), "", expect{true, false}}, + {reflect.ValueOf(123), reflect.ValueOf(456), "!=", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("bar"), "!=", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), + "!=", + expect{true, false}, + }, + {reflect.ValueOf(true), reflect.ValueOf(false), "!=", expect{true, false}}, + {reflect.ValueOf(123), reflect.ValueOf(nil), "!=", expect{true, false}}, + {reflect.ValueOf(456), reflect.ValueOf(123), ">=", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("bar"), ">=", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), + ">=", + expect{true, false}, + }, + {reflect.ValueOf(456), reflect.ValueOf(123), ">", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("bar"), ">", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), + ">", + expect{true, false}, + }, + {reflect.ValueOf(123), reflect.ValueOf(456), "<=", expect{true, false}}, + {reflect.ValueOf("bar"), reflect.ValueOf("foo"), "<=", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + "<=", + expect{true, false}, + }, + {reflect.ValueOf(123), reflect.ValueOf(456), "<", expect{true, false}}, + {reflect.ValueOf("bar"), reflect.ValueOf("foo"), "<", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + "<", + expect{true, false}, + }, + {reflect.ValueOf(123), reflect.ValueOf([]int{123, 45, 678}), "in", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf([]string{"foo", "bar", "baz"}), "in", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf([]time.Time{ + time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC), + time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC), + time.Date(2015, time.June, 26, 19, 18, 56, 12345, time.UTC), + }), + "in", + expect{true, false}, + }, + {reflect.ValueOf(123), reflect.ValueOf([]int{45, 678}), "not in", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf([]string{"bar", "baz"}), "not in", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf([]time.Time{ + time.Date(2015, time.February, 26, 19, 18, 56, 12345, time.UTC), + time.Date(2015, time.March, 26, 19, 18, 56, 12345, time.UTC), + time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC), + }), + "not in", + expect{true, false}, + }, + {reflect.ValueOf("foo"), reflect.ValueOf("bar-foo-baz"), "in", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("bar--baz"), "not in", expect{true, false}}, + {reflect.Value{}, reflect.ValueOf("foo"), "", expect{false, false}}, + {reflect.ValueOf("foo"), reflect.Value{}, "", expect{false, false}}, + {reflect.ValueOf((*TstX)(nil)), reflect.ValueOf("foo"), "", expect{false, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf((*TstX)(nil)), "", expect{false, false}}, + {reflect.ValueOf(true), reflect.ValueOf("foo"), "", expect{false, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf(true), "", expect{false, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf(map[int]string{}), "", expect{false, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf([]int{1, 2}), "", expect{false, false}}, + {reflect.ValueOf((*TstX)(nil)), reflect.ValueOf((*TstX)(nil)), ">", expect{false, false}}, + {reflect.ValueOf(true), reflect.ValueOf(false), ">", expect{false, false}}, + {reflect.ValueOf(123), reflect.ValueOf([]int{}), "in", expect{false, false}}, + {reflect.ValueOf(123), reflect.ValueOf(123), "op", expect{false, true}}, + } { + result, err := ns.checkCondition(test.value, test.match, test.op) + if test.expect.isError { + if err == nil { + t.Errorf("[%d] checkCondition didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if result != test.expect.result { + t.Errorf("[%d] check condition %v %s %v, got %v but expected %v", i, test.value, test.op, test.match, result, test.expect.result) + } + } + } +} + +func TestEvaluateSubElem(t *testing.T) { + t.Parallel() + tstx := TstX{A: "foo", B: "bar"} + var inner struct { + S fmt.Stringer + } + inner.S = tstx + interfaceValue := reflect.ValueOf(&inner).Elem().Field(0) + + for i, test := range []struct { + value reflect.Value + key string + expect interface{} + }{ + {reflect.ValueOf(tstx), "A", "foo"}, + {reflect.ValueOf(&tstx), "TstRp", "rfoo"}, + {reflect.ValueOf(tstx), "TstRv", "rbar"}, + //{reflect.ValueOf(map[int]string{1: "foo", 2: "bar"}), 1, "foo"}, + {reflect.ValueOf(map[string]string{"key1": "foo", "key2": "bar"}), "key1", "foo"}, + {interfaceValue, "String", "A: foo, B: bar"}, + {reflect.Value{}, "foo", false}, + //{reflect.ValueOf(map[int]string{1: "foo", 2: "bar"}), 1.2, false}, + {reflect.ValueOf(tstx), "unexported", false}, + {reflect.ValueOf(tstx), "unexportedMethod", false}, + {reflect.ValueOf(tstx), "MethodWithArg", false}, + {reflect.ValueOf(tstx), "MethodReturnNothing", false}, + {reflect.ValueOf(tstx), "MethodReturnErrorOnly", false}, + {reflect.ValueOf(tstx), "MethodReturnTwoValues", false}, + {reflect.ValueOf(tstx), "MethodReturnValueWithError", false}, + {reflect.ValueOf((*TstX)(nil)), "A", false}, + {reflect.ValueOf(tstx), "C", false}, + {reflect.ValueOf(map[int]string{1: "foo", 2: "bar"}), "1", false}, + {reflect.ValueOf([]string{"foo", "bar"}), "1", false}, + } { + result, err := evaluateSubElem(test.value, test.key) + if b, ok := test.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] evaluateSubElem didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if result.Kind() != reflect.String || result.String() != test.expect { + t.Errorf("[%d] evaluateSubElem with %v got %v but expected %v", i, test.key, result, test.expect) + } + } + } +} diff --git a/tpl/compare/compare.go b/tpl/compare/compare.go new file mode 100644 index 000000000..1482c0afe --- /dev/null +++ b/tpl/compare/compare.go @@ -0,0 +1,207 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package compare + +import ( + "fmt" + "reflect" + "strconv" + "time" +) + +// New returns a new instance of the compare-namespaced template functions. +func New() *Namespace { + return &Namespace{} +} + +// Namespace provides template functions for the "compare" namespace. +type Namespace struct { +} + +// Default checks whether a given value is set and returns a default value if it +// is not. "Set" in this context means non-zero for numeric types and times; +// non-zero length for strings, arrays, slices, and maps; +// any boolean or struct value; or non-nil for any other types. +func (*Namespace) Default(dflt interface{}, given ...interface{}) (interface{}, error) { + // given is variadic because the following construct will not pass a piped + // argument when the key is missing: {{ index . "key" | default "foo" }} + // The Go template will complain that we got 1 argument when we expectd 2. + + if len(given) == 0 { + return dflt, nil + } + if len(given) != 1 { + return nil, fmt.Errorf("wrong number of args for default: want 2 got %d", len(given)+1) + } + + g := reflect.ValueOf(given[0]) + if !g.IsValid() { + return dflt, nil + } + + set := false + + switch g.Kind() { + case reflect.Bool: + set = true + case reflect.String, reflect.Array, reflect.Slice, reflect.Map: + set = g.Len() != 0 + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + set = g.Int() != 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + set = g.Uint() != 0 + case reflect.Float32, reflect.Float64: + set = g.Float() != 0 + case reflect.Complex64, reflect.Complex128: + set = g.Complex() != 0 + case reflect.Struct: + switch actual := given[0].(type) { + case time.Time: + set = !actual.IsZero() + default: + set = true + } + default: + set = !g.IsNil() + } + + if set { + return given[0], nil + } + + return dflt, nil +} + +// Eq returns the boolean truth of arg1 == arg2. +func (*Namespace) Eq(x, y interface{}) bool { + normalize := func(v interface{}) interface{} { + vv := reflect.ValueOf(v) + switch vv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return vv.Int() + case reflect.Float32, reflect.Float64: + return vv.Float() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return vv.Uint() + default: + return v + } + } + x = normalize(x) + y = normalize(y) + return reflect.DeepEqual(x, y) +} + +// Ne returns the boolean truth of arg1 != arg2. +func (n *Namespace) Ne(x, y interface{}) bool { + return !n.Eq(x, y) +} + +// Ge returns the boolean truth of arg1 >= arg2. +func (n *Namespace) Ge(a, b interface{}) bool { + left, right := n.compareGetFloat(a, b) + return left >= right +} + +// Gt returns the boolean truth of arg1 > arg2. +func (n *Namespace) Gt(a, b interface{}) bool { + left, right := n.compareGetFloat(a, b) + return left > right +} + +// Le returns the boolean truth of arg1 <= arg2. +func (n *Namespace) Le(a, b interface{}) bool { + left, right := n.compareGetFloat(a, b) + return left <= right +} + +// Lt returns the boolean truth of arg1 < arg2. +func (n *Namespace) Lt(a, b interface{}) bool { + left, right := n.compareGetFloat(a, b) + return left < right +} + +func (*Namespace) compareGetFloat(a interface{}, b interface{}) (float64, float64) { + var left, right float64 + var leftStr, rightStr *string + av := reflect.ValueOf(a) + + switch av.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + left = float64(av.Len()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + left = float64(av.Int()) + case reflect.Float32, reflect.Float64: + left = av.Float() + case reflect.String: + var err error + left, err = strconv.ParseFloat(av.String(), 64) + if err != nil { + str := av.String() + leftStr = &str + } + case reflect.Struct: + switch av.Type() { + case timeType: + left = float64(toTimeUnix(av)) + } + } + + bv := reflect.ValueOf(b) + + switch bv.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + right = float64(bv.Len()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + right = float64(bv.Int()) + case reflect.Float32, reflect.Float64: + right = bv.Float() + case reflect.String: + var err error + right, err = strconv.ParseFloat(bv.String(), 64) + if err != nil { + str := bv.String() + rightStr = &str + } + case reflect.Struct: + switch bv.Type() { + case timeType: + right = float64(toTimeUnix(bv)) + } + } + + switch { + case leftStr == nil || rightStr == nil: + case *leftStr < *rightStr: + return 0, 1 + case *leftStr > *rightStr: + return 1, 0 + default: + return 0, 0 + } + + return left, right +} + +var timeType = reflect.TypeOf((*time.Time)(nil)).Elem() + +func toTimeUnix(v reflect.Value) int64 { + if v.Kind() == reflect.Interface { + return toTimeUnix(v.Elem()) + } + if v.Type() != timeType { + panic("coding error: argument must be time.Time type reflect Value") + } + return v.MethodByName("Unix").Call([]reflect.Value{})[0].Int() +} diff --git a/tpl/compare/compare_test.go b/tpl/compare/compare_test.go new file mode 100644 index 000000000..57f061f4d --- /dev/null +++ b/tpl/compare/compare_test.go @@ -0,0 +1,200 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package compare + +import ( + "fmt" + "path" + "reflect" + "runtime" + "testing" + "time" + + "github.com/spf13/cast" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type tstCompareType int + +const ( + tstEq tstCompareType = iota + tstNe + tstGt + tstGe + tstLt + tstLe +) + +func tstIsEq(tp tstCompareType) bool { return tp == tstEq || tp == tstGe || tp == tstLe } +func tstIsGt(tp tstCompareType) bool { return tp == tstGt || tp == tstGe } +func tstIsLt(tp tstCompareType) bool { return tp == tstLt || tp == tstLe } + +func TestDefaultFunc(t *testing.T) { + t.Parallel() + + then := time.Now() + now := time.Now() + ns := New() + + for i, test := range []struct { + dflt interface{} + given interface{} + expect interface{} + }{ + {true, false, false}, + {"5", 0, "5"}, + + {"test1", "set", "set"}, + {"test2", "", "test2"}, + {"test3", nil, "test3"}, + + {[2]int{10, 20}, [2]int{1, 2}, [2]int{1, 2}}, + {[2]int{10, 20}, [0]int{}, [2]int{10, 20}}, + {[2]int{100, 200}, nil, [2]int{100, 200}}, + + {[]string{"one"}, []string{"uno"}, []string{"uno"}}, + {[]string{"two"}, []string{}, []string{"two"}}, + {[]string{"three"}, nil, []string{"three"}}, + + {map[string]int{"one": 1}, map[string]int{"uno": 1}, map[string]int{"uno": 1}}, + {map[string]int{"one": 1}, map[string]int{}, map[string]int{"one": 1}}, + {map[string]int{"two": 2}, nil, map[string]int{"two": 2}}, + + {10, 1, 1}, + {10, 0, 10}, + {20, nil, 20}, + + {float32(10), float32(1), float32(1)}, + {float32(10), 0, float32(10)}, + {float32(20), nil, float32(20)}, + + {complex(2, -2), complex(1, -1), complex(1, -1)}, + {complex(2, -2), complex(0, 0), complex(2, -2)}, + {complex(3, -3), nil, complex(3, -3)}, + + {struct{ f string }{f: "one"}, struct{}{}, struct{}{}}, + {struct{ f string }{f: "two"}, nil, struct{ f string }{f: "two"}}, + + {then, now, now}, + {then, time.Time{}, then}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Default(test.dflt, test.given) + + require.NoError(t, err, errMsg) + assert.Equal(t, result, test.expect, errMsg) + } +} + +func TestCompare(t *testing.T) { + t.Parallel() + + n := New() + + for _, test := range []struct { + tstCompareType + funcUnderTest func(a, b interface{}) bool + }{ + {tstGt, n.Gt}, + {tstLt, n.Lt}, + {tstGe, n.Ge}, + {tstLe, n.Le}, + {tstEq, n.Eq}, + {tstNe, n.Ne}, + } { + doTestCompare(t, test.tstCompareType, test.funcUnderTest) + } +} + +func doTestCompare(t *testing.T, tp tstCompareType, funcUnderTest func(a, b interface{}) bool) { + for i, test := range []struct { + left interface{} + right interface{} + expectIndicator int + }{ + {5, 8, -1}, + {8, 5, 1}, + {5, 5, 0}, + {int(5), int64(5), 0}, + {int32(5), int(5), 0}, + {int16(4), int(5), -1}, + {uint(15), uint64(15), 0}, + {-2, 1, -1}, + {2, -5, 1}, + {0.0, 1.23, -1}, + {1.1, 1.1, 0}, + {float32(1.0), float64(1.0), 0}, + {1.23, 0.0, 1}, + {"5", "5", 0}, + {"8", "5", 1}, + {"5", "0001", 1}, + {[]int{100, 99}, []int{1, 2, 3, 4}, -1}, + {cast.ToTime("2015-11-20"), cast.ToTime("2015-11-20"), 0}, + {cast.ToTime("2015-11-19"), cast.ToTime("2015-11-20"), -1}, + {cast.ToTime("2015-11-20"), cast.ToTime("2015-11-19"), 1}, + {"a", "a", 0}, + {"a", "b", -1}, + {"b", "a", 1}, + } { + result := funcUnderTest(test.left, test.right) + success := false + + if test.expectIndicator == 0 { + if tstIsEq(tp) { + success = result + } else { + success = !result + } + } + + if test.expectIndicator < 0 { + success = result && (tstIsLt(tp) || tp == tstNe) + success = success || (!result && !tstIsLt(tp)) + } + + if test.expectIndicator > 0 { + success = result && (tstIsGt(tp) || tp == tstNe) + success = success || (!result && (!tstIsGt(tp) || tp != tstNe)) + } + + if !success { + t.Errorf("[%d][%s] %v compared to %v: %t", i, path.Base(runtime.FuncForPC(reflect.ValueOf(funcUnderTest).Pointer()).Name()), test.left, test.right, result) + } + } +} + +func TestTimeUnix(t *testing.T) { + t.Parallel() + var sec int64 = 1234567890 + tv := reflect.ValueOf(time.Unix(sec, 0)) + i := 1 + + res := toTimeUnix(tv) + if sec != res { + t.Errorf("[%d] timeUnix got %v but expected %v", i, res, sec) + } + + i++ + func(t *testing.T) { + defer func() { + if err := recover(); err == nil { + t.Errorf("[%d] timeUnix didn't return an expected error", i) + } + }() + iv := reflect.ValueOf(sec) + toTimeUnix(iv) + }(t) +} diff --git a/tpl/compare/init.go b/tpl/compare/init.go new file mode 100644 index 000000000..fbc5e1fda --- /dev/null +++ b/tpl/compare/init.go @@ -0,0 +1,77 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package compare + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" +) + +const name = "compare" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx := New() + + ns := &internal.TemplateFuncsNamespace{ + Name: name, + Context: func(args ...interface{}) interface{} { return ctx }, + } + + ns.AddMethodMapping(ctx.Default, + []string{"default"}, + [][2]string{ + {`{{ "Hugo Rocks!" | default "Hugo Rules!" }}`, `Hugo Rocks!`}, + {`{{ "" | default "Hugo Rules!" }}`, `Hugo Rules!`}, + }, + ) + + ns.AddMethodMapping(ctx.Eq, + []string{"eq"}, + [][2]string{ + {`{{ if eq .Section "blog" }}current{{ end }}`, `current`}, + }, + ) + + ns.AddMethodMapping(ctx.Ge, + []string{"ge"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.Gt, + []string{"gt"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.Le, + []string{"le"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.Lt, + []string{"lt"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.Ne, + []string{"ne"}, + [][2]string{}, + ) + + return ns + + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/compare/init_test.go b/tpl/compare/init_test.go new file mode 100644 index 000000000..65e59b1aa --- /dev/null +++ b/tpl/compare/init_test.go @@ -0,0 +1,38 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package compare + +import ( + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + var found bool + var ns *internal.TemplateFuncsNamespace + + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + ns = nsf(&deps.Deps{}) + if ns.Name == name { + found = true + break + } + } + + require.True(t, found) + require.IsType(t, &Namespace{}, ns.Context()) +} diff --git a/tpl/crypto/crypto.go b/tpl/crypto/crypto.go new file mode 100644 index 000000000..7aaa9291e --- /dev/null +++ b/tpl/crypto/crypto.go @@ -0,0 +1,64 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crypto + +import ( + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "encoding/hex" + + "github.com/spf13/cast" +) + +// New returns a new instance of the crypto-namespaced template functions. +func New() *Namespace { + return &Namespace{} +} + +// Namespace provides template functions for the "crypto" namespace. +type Namespace struct{} + +// MD5 hashes the given input and returns its MD5 checksum. +func (ns *Namespace) MD5(in interface{}) (string, error) { + conv, err := cast.ToStringE(in) + if err != nil { + return "", err + } + + hash := md5.Sum([]byte(conv)) + return hex.EncodeToString(hash[:]), nil +} + +// SHA1 hashes the given input and returns its SHA1 checksum. +func (ns *Namespace) SHA1(in interface{}) (string, error) { + conv, err := cast.ToStringE(in) + if err != nil { + return "", err + } + + hash := sha1.Sum([]byte(conv)) + return hex.EncodeToString(hash[:]), nil +} + +// SHA256 hashes the given input and returns its SHA256 checksum. +func (ns *Namespace) SHA256(in interface{}) (string, error) { + conv, err := cast.ToStringE(in) + if err != nil { + return "", err + } + + hash := sha256.Sum256([]byte(conv)) + return hex.EncodeToString(hash[:]), nil +} diff --git a/tpl/crypto/crypto_test.go b/tpl/crypto/crypto_test.go new file mode 100644 index 000000000..1bd919c31 --- /dev/null +++ b/tpl/crypto/crypto_test.go @@ -0,0 +1,103 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crypto + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMD5(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + in interface{} + expect interface{} + }{ + {"Hello world, gophers!", "b3029f756f98f79e7f1b7f1d1f0dd53b"}, + {"Lorem ipsum dolor", "06ce65ac476fc656bea3fca5d02cfd81"}, + {t, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.in) + + result, err := ns.MD5(test.in) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestSHA1(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + in interface{} + expect interface{} + }{ + {"Hello world, gophers!", "c8b5b0e33d408246e30f53e32b8f7627a7a649d4"}, + {"Lorem ipsum dolor", "45f75b844be4d17b3394c6701768daf39419c99b"}, + {t, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.in) + + result, err := ns.SHA1(test.in) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestSHA256(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + in interface{} + expect interface{} + }{ + {"Hello world, gophers!", "6ec43b78da9669f50e4e422575c54bf87536954ccd58280219c393f2ce352b46"}, + {"Lorem ipsum dolor", "9b3e1beb7053e0f900a674dd1c99aca3355e1275e1b03d3cb1bc977f5154e196"}, + {t, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.in) + + result, err := ns.SHA256(test.in) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} diff --git a/tpl/crypto/init.go b/tpl/crypto/init.go new file mode 100644 index 000000000..db6a5f92c --- /dev/null +++ b/tpl/crypto/init.go @@ -0,0 +1,59 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crypto + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" +) + +const name = "crypto" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx := New() + + ns := &internal.TemplateFuncsNamespace{ + Name: name, + Context: func(args ...interface{}) interface{} { return ctx }, + } + + ns.AddMethodMapping(ctx.MD5, + []string{"md5"}, + [][2]string{ + {`{{ md5 "Hello world, gophers!" }}`, `b3029f756f98f79e7f1b7f1d1f0dd53b`}, + {`{{ crypto.MD5 "Hello world, gophers!" }}`, `b3029f756f98f79e7f1b7f1d1f0dd53b`}, + }, + ) + + ns.AddMethodMapping(ctx.SHA1, + []string{"sha1"}, + [][2]string{ + {`{{ sha1 "Hello world, gophers!" }}`, `c8b5b0e33d408246e30f53e32b8f7627a7a649d4`}, + }, + ) + + ns.AddMethodMapping(ctx.SHA256, + []string{"sha256"}, + [][2]string{ + {`{{ sha256 "Hello world, gophers!" }}`, `6ec43b78da9669f50e4e422575c54bf87536954ccd58280219c393f2ce352b46`}, + }, + ) + + return ns + + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/crypto/init_test.go b/tpl/crypto/init_test.go new file mode 100644 index 000000000..852a90a40 --- /dev/null +++ b/tpl/crypto/init_test.go @@ -0,0 +1,38 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crypto + +import ( + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + var found bool + var ns *internal.TemplateFuncsNamespace + + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + ns = nsf(&deps.Deps{}) + if ns.Name == name { + found = true + break + } + } + + require.True(t, found) + require.IsType(t, &Namespace{}, ns.Context()) +} diff --git a/tpl/data/cache.go b/tpl/data/cache.go new file mode 100644 index 000000000..b4f91c0f8 --- /dev/null +++ b/tpl/data/cache.go @@ -0,0 +1,83 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package data + +import ( + "errors" + "net/url" + "sync" + + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/helpers" + "github.com/spf13/afero" +) + +var cacheMu sync.RWMutex + +// getCacheFileID returns the cache ID for a string. +func getCacheFileID(cfg config.Provider, id string) string { + return cfg.GetString("cacheDir") + url.QueryEscape(id) +} + +// getCache returns the content for an ID from the file cache or an error. +// If the ID is not found, return nil,nil. +func getCache(id string, fs afero.Fs, cfg config.Provider, ignoreCache bool) ([]byte, error) { + if ignoreCache { + return nil, nil + } + + cacheMu.RLock() + defer cacheMu.RUnlock() + + fID := getCacheFileID(cfg, id) + isExists, err := helpers.Exists(fID, fs) + if err != nil { + return nil, err + } + if !isExists { + return nil, nil + } + + return afero.ReadFile(fs, fID) +} + +// writeCache writes bytes associated with an ID into the file cache. +func writeCache(id string, c []byte, fs afero.Fs, cfg config.Provider, ignoreCache bool) error { + if ignoreCache { + return nil + } + + cacheMu.Lock() + defer cacheMu.Unlock() + + fID := getCacheFileID(cfg, id) + f, err := fs.Create(fID) + if err != nil { + return errors.New("Error: " + err.Error() + ". Failed to create file: " + fID) + } + defer f.Close() + + n, err := f.Write(c) + if err != nil { + return errors.New("Error: " + err.Error() + ". Failed to write to file: " + fID) + } + if n == 0 { + return errors.New("No bytes written to file: " + fID) + } + return nil +} + +func deleteCache(id string, fs afero.Fs, cfg config.Provider) error { + return fs.Remove(getCacheFileID(cfg, id)) +} diff --git a/tpl/data/cache_test.go b/tpl/data/cache_test.go new file mode 100644 index 000000000..6057f0321 --- /dev/null +++ b/tpl/data/cache_test.go @@ -0,0 +1,63 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package data + +import ( + "fmt" + "testing" + + "github.com/spf13/afero" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func TestCache(t *testing.T) { + t.Parallel() + + fs := new(afero.MemMapFs) + + for i, test := range []struct { + path string + content []byte + ignore bool + }{ + {"http://Foo.Bar/foo_Bar-Foo", []byte(`T€st Content 123`), false}, + {"fOO,bar:foo%bAR", []byte(`T€st Content 123 fOO,bar:foo%bAR`), false}, + {"FOo/BaR.html", []byte(`FOo/BaR.html T€st Content 123`), false}, + {"трям/трям", []byte(`T€st трям/трям Content 123`), false}, + {"은행", []byte(`T€st C은행ontent 123`), false}, + {"Банковский кассир", []byte(`Банковский кассир T€st Content 123`), false}, + {"Банковский кассир", []byte(`Банковский кассир T€st Content 456`), true}, + } { + msg := fmt.Sprintf("Test #%d: %v", i, test) + + cfg := viper.New() + + c, err := getCache(test.path, fs, cfg, test.ignore) + assert.NoError(t, err, msg) + assert.Nil(t, c, msg) + + err = writeCache(test.path, test.content, fs, cfg, test.ignore) + assert.NoError(t, err, msg) + + c, err = getCache(test.path, fs, cfg, test.ignore) + assert.NoError(t, err, msg) + + if test.ignore { + assert.Nil(t, c, msg) + } else { + assert.Equal(t, string(test.content), string(c)) + } + } +} diff --git a/tpl/data/data.go b/tpl/data/data.go new file mode 100644 index 000000000..0c75fdbb7 --- /dev/null +++ b/tpl/data/data.go @@ -0,0 +1,138 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package data + +import ( + "bytes" + "encoding/csv" + "encoding/json" + "errors" + "net/http" + "strings" + "time" + + "github.com/gohugoio/hugo/deps" + jww "github.com/spf13/jwalterweatherman" +) + +// New returns a new instance of the data-namespaced template functions. +func New(deps *deps.Deps) *Namespace { + return &Namespace{ + deps: deps, + client: http.DefaultClient, + } +} + +// Namespace provides template functions for the "data" namespace. +type Namespace struct { + deps *deps.Deps + + client *http.Client +} + +// GetCSV expects a data separator and one or n-parts of a URL to a resource which +// can either be a local or a remote one. +// The data separator can be a comma, semi-colon, pipe, etc, but only one character. +// If you provide multiple parts for the URL they will be joined together to the final URL. +// GetCSV returns nil or a slice slice to use in a short code. +func (ns *Namespace) GetCSV(sep string, urlParts ...string) (d [][]string, err error) { + url := strings.Join(urlParts, "") + + var clearCacheSleep = func(i int, u string) { + jww.ERROR.Printf("Retry #%d for %s and sleeping for %s", i, url, resSleep) + time.Sleep(resSleep) + deleteCache(url, ns.deps.Fs.Source, ns.deps.Cfg) + } + + for i := 0; i <= resRetries; i++ { + var req *http.Request + req, err = http.NewRequest("GET", url, nil) + if err != nil { + jww.ERROR.Printf("Failed to create request for getJSON: %s", err) + return nil, err + } + + req.Header.Add("Accept", "text/csv") + req.Header.Add("Accept", "text/plain") + + var c []byte + c, err = ns.getResource(req) + if err != nil { + jww.ERROR.Printf("Failed to read csv resource %q with error message %s", url, err) + return nil, err + } + + if !bytes.Contains(c, []byte(sep)) { + err = errors.New("Cannot find separator " + sep + " in CSV.") + return + } + + if d, err = parseCSV(c, sep); err != nil { + jww.ERROR.Printf("Failed to parse csv file %s with error message %s", url, err) + clearCacheSleep(i, url) + continue + } + break + } + return +} + +// GetJSON expects one or n-parts of a URL to a resource which can either be a local or a remote one. +// If you provide multiple parts they will be joined together to the final URL. +// GetJSON returns nil or parsed JSON to use in a short code. +func (ns *Namespace) GetJSON(urlParts ...string) (v interface{}, err error) { + url := strings.Join(urlParts, "") + + for i := 0; i <= resRetries; i++ { + var req *http.Request + req, err = http.NewRequest("GET", url, nil) + if err != nil { + jww.ERROR.Printf("Failed to create request for getJSON: %s", err) + return nil, err + } + + req.Header.Add("Accept", "application/json") + + var c []byte + c, err = ns.getResource(req) + if err != nil { + jww.ERROR.Printf("Failed to get json resource %s with error message %s", url, err) + return nil, err + } + + err = json.Unmarshal(c, &v) + if err != nil { + jww.ERROR.Printf("Cannot read json from resource %s with error message %s", url, err) + jww.ERROR.Printf("Retry #%d for %s and sleeping for %s", i, url, resSleep) + time.Sleep(resSleep) + deleteCache(url, ns.deps.Fs.Source, ns.deps.Cfg) + continue + } + break + } + return +} + +// parseCSV parses bytes of CSV data into a slice slice string or an error +func parseCSV(c []byte, sep string) ([][]string, error) { + if len(sep) != 1 { + return nil, errors.New("Incorrect length of csv separator: " + sep) + } + b := bytes.NewReader(c) + r := csv.NewReader(b) + rSep := []rune(sep) + r.Comma = rSep[0] + r.FieldsPerRecord = 0 + return r.ReadAll() +} diff --git a/tpl/data/data_test.go b/tpl/data/data_test.go new file mode 100644 index 000000000..bcdddc9f4 --- /dev/null +++ b/tpl/data/data_test.go @@ -0,0 +1,251 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package data + +import ( + "fmt" + "net/http" + "net/http/httptest" + "path/filepath" + "strings" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetCSV(t *testing.T) { + t.Parallel() + + ns := New(newDeps(viper.New())) + + for i, test := range []struct { + sep string + url string + content string + expect interface{} + }{ + // Remotes + { + ",", + `http://success/`, + "gomeetup,city\nyes,Sydney\nyes,San Francisco\nyes,Stockholm\n", + [][]string{{"gomeetup", "city"}, {"yes", "Sydney"}, {"yes", "San Francisco"}, {"yes", "Stockholm"}}, + }, + { + ",", + `http://error.extra.field/`, + "gomeetup,city\nyes,Sydney\nyes,San Francisco\nyes,Stockholm,EXTRA\n", + false, + }, + { + ",", + `http://error.no.sep/`, + "gomeetup;city\nyes;Sydney\nyes;San Francisco\nyes;Stockholm\n", + false, + }, + { + ",", + `http://nofound/404`, + ``, + false, + }, + + // Locals + { + ";", + "pass/semi", + "gomeetup;city\nyes;Sydney\nyes;San Francisco\nyes;Stockholm\n", + [][]string{{"gomeetup", "city"}, {"yes", "Sydney"}, {"yes", "San Francisco"}, {"yes", "Stockholm"}}, + }, + { + ";", + "fail/no-file", + "", + false, + }, + } { + msg := fmt.Sprintf("Test %d", i) + + // Setup HTTP test server + var srv *httptest.Server + srv, ns.client = getTestServer(func(w http.ResponseWriter, r *http.Request) { + if !haveHeader(r.Header, "Accept", "text/csv") && !haveHeader(r.Header, "Accept", "text/plain") { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + if r.URL.Path == "/404" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + + w.Header().Add("Content-type", "text/csv") + + w.Write([]byte(test.content)) + }) + defer func() { srv.Close() }() + + // Setup local test file for schema-less URLs + if !strings.Contains(test.url, ":") && !strings.HasPrefix(test.url, "fail/") { + f, err := ns.deps.Fs.Source.Create(filepath.Join(ns.deps.Cfg.GetString("workingDir"), test.url)) + require.NoError(t, err, msg) + f.WriteString(test.content) + f.Close() + } + + // Get on with it + got, err := ns.GetCSV(test.sep, test.url) + + if _, ok := test.expect.(bool); ok { + assert.Error(t, err, msg) + continue + } + require.NoError(t, err, msg) + require.NotNil(t, got, msg) + + assert.EqualValues(t, test.expect, got, msg) + } +} + +func TestGetJSON(t *testing.T) { + t.Parallel() + + ns := New(newDeps(viper.New())) + + for i, test := range []struct { + url string + content string + expect interface{} + }{ + { + `http://success/`, + `{"gomeetup":["Sydney","San Francisco","Stockholm"]}`, + map[string]interface{}{"gomeetup": []interface{}{"Sydney", "San Francisco", "Stockholm"}}, + }, + { + `http://malformed/`, + `{gomeetup:["Sydney","San Francisco","Stockholm"]}`, + false, + }, + { + `http://nofound/404`, + ``, + false, + }, + // Locals + { + "pass/semi", + `{"gomeetup":["Sydney","San Francisco","Stockholm"]}`, + map[string]interface{}{"gomeetup": []interface{}{"Sydney", "San Francisco", "Stockholm"}}, + }, + { + "fail/no-file", + "", + false, + }, + } { + msg := fmt.Sprintf("Test %d", i) + + // Setup HTTP test server + var srv *httptest.Server + srv, ns.client = getTestServer(func(w http.ResponseWriter, r *http.Request) { + if !haveHeader(r.Header, "Accept", "application/json") { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + if r.URL.Path == "/404" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + + w.Header().Add("Content-type", "application/json") + + w.Write([]byte(test.content)) + }) + defer func() { srv.Close() }() + + // Setup local test file for schema-less URLs + if !strings.Contains(test.url, ":") && !strings.HasPrefix(test.url, "fail/") { + f, err := ns.deps.Fs.Source.Create(filepath.Join(ns.deps.Cfg.GetString("workingDir"), test.url)) + require.NoError(t, err, msg) + f.WriteString(test.content) + f.Close() + } + + // Get on with it + got, err := ns.GetJSON(test.url) + + if _, ok := test.expect.(bool); ok { + assert.Error(t, err, msg) + continue + } + require.NoError(t, err, msg) + require.NotNil(t, got, msg) + + assert.EqualValues(t, test.expect, got, msg) + } +} + +func TestParseCSV(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + csv []byte + sep string + exp string + err bool + }{ + {[]byte("a,b,c\nd,e,f\n"), "", "", true}, + {[]byte("a,b,c\nd,e,f\n"), "~/", "", true}, + {[]byte("a,b,c\nd,e,f"), "|", "a,b,cd,e,f", false}, + {[]byte("q,w,e\nd,e,f"), ",", "qwedef", false}, + {[]byte("a|b|c\nd|e|f|g"), "|", "abcdefg", true}, + {[]byte("z|y|c\nd|e|f"), "|", "zycdef", false}, + } { + msg := fmt.Sprintf("Test %d: %v", i, test) + + csv, err := parseCSV(test.csv, test.sep) + if test.err { + assert.Error(t, err, msg) + continue + } + require.NoError(t, err, msg) + + act := "" + for _, v := range csv { + act = act + strings.Join(v, "") + } + + assert.Equal(t, test.exp, act, msg) + } +} + +func haveHeader(m http.Header, key, needle string) bool { + var s []string + var ok bool + + if s, ok = m[key]; !ok { + return false + } + + for _, v := range s { + if v == needle { + return true + } + } + return false +} diff --git a/tpl/data/init.go b/tpl/data/init.go new file mode 100644 index 000000000..3bdc02786 --- /dev/null +++ b/tpl/data/init.go @@ -0,0 +1,45 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package data + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" +) + +const name = "data" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx := New(d) + + ns := &internal.TemplateFuncsNamespace{ + Name: name, + Context: func(args ...interface{}) interface{} { return ctx }, + } + + ns.AddMethodMapping(ctx.GetCSV, + []string{"getCSV"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.GetJSON, + []string{"getJSON"}, + [][2]string{}, + ) + return ns + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/data/init_test.go b/tpl/data/init_test.go new file mode 100644 index 000000000..6bb689a95 --- /dev/null +++ b/tpl/data/init_test.go @@ -0,0 +1,38 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package data + +import ( + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + var found bool + var ns *internal.TemplateFuncsNamespace + + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + ns = nsf(&deps.Deps{}) + if ns.Name == name { + found = true + break + } + } + + require.True(t, found) + require.IsType(t, &Namespace{}, ns.Context()) +} diff --git a/tpl/data/resources.go b/tpl/data/resources.go new file mode 100644 index 000000000..11c35f9d9 --- /dev/null +++ b/tpl/data/resources.go @@ -0,0 +1,134 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package data + +import ( + "fmt" + "io/ioutil" + "net/http" + "path/filepath" + "sync" + "time" + + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/helpers" + "github.com/spf13/afero" + jww "github.com/spf13/jwalterweatherman" +) + +var ( + remoteURLLock = &remoteLock{m: make(map[string]*sync.Mutex)} + resSleep = time.Second * 2 // if JSON decoding failed sleep for n seconds before retrying + resRetries = 1 // number of retries to load the JSON from URL or local file system +) + +type remoteLock struct { + sync.RWMutex + m map[string]*sync.Mutex +} + +// URLLock locks an URL during download +func (l *remoteLock) URLLock(url string) { + var ( + lock *sync.Mutex + ok bool + ) + l.Lock() + if lock, ok = l.m[url]; !ok { + lock = &sync.Mutex{} + l.m[url] = lock + } + l.Unlock() + lock.Lock() +} + +// URLUnlock unlocks an URL when the download has been finished. Use only in defer calls. +func (l *remoteLock) URLUnlock(url string) { + l.RLock() + defer l.RUnlock() + if um, ok := l.m[url]; ok { + um.Unlock() + } +} + +// getRemote loads the content of a remote file. This method is thread safe. +func getRemote(req *http.Request, fs afero.Fs, cfg config.Provider, hc *http.Client) ([]byte, error) { + url := req.URL.String() + + c, err := getCache(url, fs, cfg, cfg.GetBool("ignoreCache")) + if err != nil { + return nil, err + } + if c != nil { + return c, nil + } + + // avoid race condition with locks, block other goroutines if the current url is processing + remoteURLLock.URLLock(url) + defer func() { remoteURLLock.URLUnlock(url) }() + + // avoid multiple locks due to calling getCache twice + c, err = getCache(url, fs, cfg, cfg.GetBool("ignoreCache")) + if err != nil { + return nil, err + } + if c != nil { + return c, nil + } + + jww.INFO.Printf("Downloading: %s ...", url) + res, err := hc.Do(req) + if err != nil { + return nil, err + } + + if res.StatusCode < 200 || res.StatusCode > 299 { + return nil, fmt.Errorf("Failed to retrieve remote file: %s", http.StatusText(res.StatusCode)) + } + + c, err = ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + return nil, err + } + + err = writeCache(url, c, fs, cfg, cfg.GetBool("ignoreCache")) + if err != nil { + return nil, err + } + + jww.INFO.Printf("... and cached to: %s", getCacheFileID(cfg, url)) + return c, nil +} + +// getLocal loads the content of a local file +func getLocal(url string, fs afero.Fs, cfg config.Provider) ([]byte, error) { + filename := filepath.Join(cfg.GetString("workingDir"), url) + if e, err := helpers.Exists(filename, fs); !e { + return nil, err + } + + return afero.ReadFile(fs, filename) + +} + +// getResource loads the content of a local or remote file +func (ns *Namespace) getResource(req *http.Request) ([]byte, error) { + switch req.URL.Scheme { + case "": + return getLocal(req.URL.String(), ns.deps.Fs.Source, ns.deps.Cfg) + default: + return getRemote(req, ns.deps.Fs.Source, ns.deps.Cfg, ns.client) + } +} diff --git a/tpl/data/resources_test.go b/tpl/data/resources_test.go new file mode 100644 index 000000000..de83f771d --- /dev/null +++ b/tpl/data/resources_test.go @@ -0,0 +1,174 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package data + +import ( + "bytes" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "sync" + "testing" + "time" + + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" + "github.com/spf13/afero" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestScpGetLocal(t *testing.T) { + t.Parallel() + v := viper.New() + fs := hugofs.NewMem(v) + ps := helpers.FilePathSeparator + + tests := []struct { + path string + content []byte + }{ + {"testpath" + ps + "test.txt", []byte(`T€st Content 123 fOO,bar:foo%bAR`)}, + {"FOo" + ps + "BaR.html", []byte(`FOo/BaR.html T€st Content 123`)}, + {"трям" + ps + "трям", []byte(`T€st трям/трям Content 123`)}, + {"은행", []byte(`T€st C은행ontent 123`)}, + {"Банковский кассир", []byte(`Банковский кассир T€st Content 123`)}, + } + + for _, test := range tests { + r := bytes.NewReader(test.content) + err := helpers.WriteToDisk(test.path, r, fs.Source) + if err != nil { + t.Error(err) + } + + c, err := getLocal(test.path, fs.Source, v) + if err != nil { + t.Errorf("Error getting resource content: %s", err) + } + if !bytes.Equal(c, test.content) { + t.Errorf("\nExpected: %s\nActual: %s\n", string(test.content), string(c)) + } + } + +} + +func getTestServer(handler func(w http.ResponseWriter, r *http.Request)) (*httptest.Server, *http.Client) { + testServer := httptest.NewServer(http.HandlerFunc(handler)) + client := &http.Client{ + Transport: &http.Transport{Proxy: func(r *http.Request) (*url.URL, error) { + // Remove when https://github.com/golang/go/issues/13686 is fixed + r.Host = "gohugo.io" + return url.Parse(testServer.URL) + }}, + } + return testServer, client +} + +func TestScpGetRemote(t *testing.T) { + t.Parallel() + fs := new(afero.MemMapFs) + + tests := []struct { + path string + content []byte + ignore bool + }{ + {"http://Foo.Bar/foo_Bar-Foo", []byte(`T€st Content 123`), false}, + {"http://Doppel.Gänger/foo_Bar-Foo", []byte(`T€st Cont€nt 123`), false}, + {"http://Doppel.Gänger/Fizz_Bazz-Foo", []byte(`T€st Банковский кассир Cont€nt 123`), false}, + {"http://Doppel.Gänger/Fizz_Bazz-Bar", []byte(`T€st Банковский кассир Cont€nt 456`), true}, + } + + for _, test := range tests { + msg := fmt.Sprintf("%v", test) + + req, err := http.NewRequest("GET", test.path, nil) + require.NoError(t, err, msg) + + srv, cl := getTestServer(func(w http.ResponseWriter, r *http.Request) { + w.Write(test.content) + }) + defer func() { srv.Close() }() + + cfg := viper.New() + + c, err := getRemote(req, fs, cfg, cl) + require.NoError(t, err, msg) + assert.Equal(t, string(test.content), string(c)) + + c, err = getCache(req.URL.String(), fs, cfg, test.ignore) + require.NoError(t, err, msg) + + if test.ignore { + assert.Empty(t, c, msg) + } else { + assert.Equal(t, string(test.content), string(c)) + + } + } +} + +func TestScpGetRemoteParallel(t *testing.T) { + t.Parallel() + + ns := New(newDeps(viper.New())) + + content := []byte(`T€st Content 123`) + srv, cl := getTestServer(func(w http.ResponseWriter, r *http.Request) { + w.Write(content) + }) + defer func() { srv.Close() }() + + url := "http://Foo.Bar/foo_Bar-Foo" + req, err := http.NewRequest("GET", url, nil) + require.NoError(t, err) + + for _, ignoreCache := range []bool{false, true} { + cfg := viper.New() + cfg.Set("ignoreCache", ignoreCache) + + var wg sync.WaitGroup + + for i := 0; i < 50; i++ { + wg.Add(1) + go func(gor int) { + defer wg.Done() + for j := 0; j < 10; j++ { + c, err := getRemote(req, ns.deps.Fs.Source, ns.deps.Cfg, cl) + assert.NoError(t, err) + assert.Equal(t, string(content), string(c)) + + time.Sleep(23 * time.Millisecond) + } + }(i) + } + + wg.Wait() + } +} + +func newDeps(cfg config.Provider) *deps.Deps { + l := helpers.NewLanguage("en", cfg) + l.Set("i18nDir", "i18n") + return &deps.Deps{ + Cfg: cfg, + Fs: hugofs.NewMem(l), + ContentSpec: helpers.NewContentSpec(l), + } +} diff --git a/tpl/encoding/encoding.go b/tpl/encoding/encoding.go new file mode 100644 index 000000000..4b02c426a --- /dev/null +++ b/tpl/encoding/encoding.go @@ -0,0 +1,61 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package encoding + +import ( + "encoding/base64" + "encoding/json" + "html/template" + + "github.com/spf13/cast" +) + +// New returns a new instance of the encoding-namespaced template functions. +func New() *Namespace { + return &Namespace{} +} + +// Namespace provides template functions for the "encoding" namespace. +type Namespace struct{} + +// Base64Decode returns the base64 decoding of the given content. +func (ns *Namespace) Base64Decode(content interface{}) (string, error) { + conv, err := cast.ToStringE(content) + if err != nil { + return "", err + } + + dec, err := base64.StdEncoding.DecodeString(conv) + return string(dec), err +} + +// Base64Encode returns the base64 encoding of the given content. +func (ns *Namespace) Base64Encode(content interface{}) (string, error) { + conv, err := cast.ToStringE(content) + if err != nil { + return "", err + } + + return base64.StdEncoding.EncodeToString([]byte(conv)), nil +} + +// Jsonify encodes a given object to JSON. +func (ns *Namespace) Jsonify(v interface{}) (template.HTML, error) { + b, err := json.Marshal(v) + if err != nil { + return "", err + } + + return template.HTML(b), nil +} diff --git a/tpl/encoding/encoding_test.go b/tpl/encoding/encoding_test.go new file mode 100644 index 000000000..8242561b6 --- /dev/null +++ b/tpl/encoding/encoding_test.go @@ -0,0 +1,109 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package encoding + +import ( + "fmt" + "html/template" + "math" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type tstNoStringer struct{} + +func TestBase64Decode(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + v interface{} + expect interface{} + }{ + {"YWJjMTIzIT8kKiYoKSctPUB+", "abc123!?$*&()'-=@~"}, + // errors + {t, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.v) + + result, err := ns.Base64Decode(test.v) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestBase64Encode(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + v interface{} + expect interface{} + }{ + {"YWJjMTIzIT8kKiYoKSctPUB+", "WVdKak1USXpJVDhrS2lZb0tTY3RQVUIr"}, + // errors + {t, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.v) + + result, err := ns.Base64Encode(test.v) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestJsonify(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + v interface{} + expect interface{} + }{ + {[]string{"a", "b"}, template.HTML(`["a","b"]`)}, + {tstNoStringer{}, template.HTML("{}")}, + {nil, template.HTML("null")}, + // errors + {math.NaN(), false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.v) + + result, err := ns.Jsonify(test.v) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} diff --git a/tpl/encoding/init.go b/tpl/encoding/init.go new file mode 100644 index 000000000..bad1804de --- /dev/null +++ b/tpl/encoding/init.go @@ -0,0 +1,59 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package encoding + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" +) + +const name = "encoding" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx := New() + + ns := &internal.TemplateFuncsNamespace{ + Name: name, + Context: func(args ...interface{}) interface{} { return ctx }, + } + + ns.AddMethodMapping(ctx.Base64Decode, + []string{"base64Decode"}, + [][2]string{ + {`{{ "SGVsbG8gd29ybGQ=" | base64Decode }}`, `Hello world`}, + {`{{ 42 | base64Encode | base64Decode }}`, `42`}, + }, + ) + + ns.AddMethodMapping(ctx.Base64Encode, + []string{"base64Encode"}, + [][2]string{ + {`{{ "Hello world" | base64Encode }}`, `SGVsbG8gd29ybGQ=`}, + }, + ) + + ns.AddMethodMapping(ctx.Jsonify, + []string{"jsonify"}, + [][2]string{ + {`{{ (slice "A" "B" "C") | jsonify }}`, `["A","B","C"]`}, + }, + ) + + return ns + + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/encoding/init_test.go b/tpl/encoding/init_test.go new file mode 100644 index 000000000..6bbee99fa --- /dev/null +++ b/tpl/encoding/init_test.go @@ -0,0 +1,38 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package encoding + +import ( + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + var found bool + var ns *internal.TemplateFuncsNamespace + + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + ns = nsf(&deps.Deps{}) + if ns.Name == name { + found = true + break + } + } + + require.True(t, found) + require.IsType(t, &Namespace{}, ns.Context()) +} diff --git a/tpl/fmt/fmt.go b/tpl/fmt/fmt.go new file mode 100644 index 000000000..ca31ec522 --- /dev/null +++ b/tpl/fmt/fmt.go @@ -0,0 +1,40 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fmt + +import ( + _fmt "fmt" +) + +// New returns a new instance of the fmt-namespaced template functions. +func New() *Namespace { + return &Namespace{} +} + +// Namespace provides template functions for the "fmt" namespace. +type Namespace struct { +} + +func (ns *Namespace) Print(a ...interface{}) string { + return _fmt.Sprint(a...) +} + +func (ns *Namespace) Printf(format string, a ...interface{}) string { + return _fmt.Sprintf(format, a...) + +} + +func (ns *Namespace) Println(a ...interface{}) string { + return _fmt.Sprintln(a...) +} diff --git a/tpl/fmt/init.go b/tpl/fmt/init.go new file mode 100644 index 000000000..a3398b862 --- /dev/null +++ b/tpl/fmt/init.go @@ -0,0 +1,58 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fmt + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" +) + +const name = "fmt" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx := New() + + ns := &internal.TemplateFuncsNamespace{ + Name: name, + Context: func(args ...interface{}) interface{} { return ctx }, + } + + ns.AddMethodMapping(ctx.Print, + []string{"print"}, + [][2]string{ + {`{{ print "works!" }}`, `works!`}, + }, + ) + + ns.AddMethodMapping(ctx.Println, + []string{"println"}, + [][2]string{ + {`{{ println "works!" }}`, "works!\n"}, + }, + ) + + ns.AddMethodMapping(ctx.Printf, + []string{"printf"}, + [][2]string{ + {`{{ printf "%s!" "works" }}`, `works!`}, + }, + ) + + return ns + + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/fmt/init_test.go b/tpl/fmt/init_test.go new file mode 100644 index 000000000..01eb2fa69 --- /dev/null +++ b/tpl/fmt/init_test.go @@ -0,0 +1,38 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fmt + +import ( + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + var found bool + var ns *internal.TemplateFuncsNamespace + + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + ns = nsf(&deps.Deps{}) + if ns.Name == name { + found = true + break + } + } + + require.True(t, found) + require.IsType(t, &Namespace{}, ns.Context()) +} diff --git a/tpl/images/images.go b/tpl/images/images.go new file mode 100644 index 000000000..f98c24286 --- /dev/null +++ b/tpl/images/images.go @@ -0,0 +1,82 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package images + +import ( + "errors" + "image" + "sync" + + // Importing image codecs for image.DecodeConfig + _ "image/gif" + _ "image/jpeg" + _ "image/png" + + "github.com/gohugoio/hugo/deps" + "github.com/spf13/cast" +) + +// New returns a new instance of the images-namespaced template functions. +func New(deps *deps.Deps) *Namespace { + return &Namespace{ + cache: map[string]image.Config{}, + deps: deps, + } +} + +// Namespace provides template functions for the "images" namespace. +type Namespace struct { + cacheMu sync.RWMutex + cache map[string]image.Config + + deps *deps.Deps +} + +// Config returns the image.Config for the specified path relative to the +// working directory. +func (ns *Namespace) Config(path interface{}) (image.Config, error) { + filename, err := cast.ToStringE(path) + if err != nil { + return image.Config{}, err + } + + if filename == "" { + return image.Config{}, errors.New("config needs a filename") + } + + // Check cache for image config. + ns.cacheMu.RLock() + config, ok := ns.cache[filename] + ns.cacheMu.RUnlock() + + if ok { + return config, nil + } + + f, err := ns.deps.Fs.WorkingDir.Open(filename) + if err != nil { + return image.Config{}, err + } + + config, _, err = image.DecodeConfig(f) + if err != nil { + return config, err + } + + ns.cacheMu.Lock() + ns.cache[filename] = config + ns.cacheMu.Unlock() + + return config, nil +} diff --git a/tpl/images/images_test.go b/tpl/images/images_test.go new file mode 100644 index 000000000..c9b78ea9a --- /dev/null +++ b/tpl/images/images_test.go @@ -0,0 +1,121 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package images + +import ( + "bytes" + "fmt" + "image" + "image/color" + "image/png" + "path/filepath" + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/hugofs" + "github.com/spf13/afero" + "github.com/spf13/cast" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type tstNoStringer struct{} + +var configTests = []struct { + path interface{} + input []byte + expect interface{} +}{ + { + path: "a.png", + input: blankImage(10, 10), + expect: image.Config{ + Width: 10, + Height: 10, + ColorModel: color.NRGBAModel, + }, + }, + { + path: "a.png", + input: blankImage(10, 10), + expect: image.Config{ + Width: 10, + Height: 10, + ColorModel: color.NRGBAModel, + }, + }, + { + path: "b.png", + input: blankImage(20, 15), + expect: image.Config{ + Width: 20, + Height: 15, + ColorModel: color.NRGBAModel, + }, + }, + { + path: "a.png", + input: blankImage(20, 15), + expect: image.Config{ + Width: 10, + Height: 10, + ColorModel: color.NRGBAModel, + }, + }, + // errors + {path: tstNoStringer{}, expect: false}, + {path: "non-existent.png", expect: false}, + {path: "", expect: false}, +} + +func TestNSConfig(t *testing.T) { + t.Parallel() + + v := viper.New() + v.Set("workingDir", "/a/b") + + ns := New(&deps.Deps{Fs: hugofs.NewMem(v)}) + + for i, test := range configTests { + errMsg := fmt.Sprintf("[%d] %s", i, test.path) + + // check for expected errors early to avoid writing files + if b, ok := test.expect.(bool); ok && !b { + _, err := ns.Config(interface{}(test.path)) + require.Error(t, err, errMsg) + continue + } + + // cast path to string for afero.WriteFile + sp, err := cast.ToStringE(test.path) + require.NoError(t, err, errMsg) + afero.WriteFile(ns.deps.Fs.Source, filepath.Join(v.GetString("workingDir"), sp), test.input, 0755) + + result, err := ns.Config(test.path) + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + assert.NotEqual(t, 0, len(ns.cache), errMsg) + } +} + +func blankImage(width, height int) []byte { + var buf bytes.Buffer + img := image.NewRGBA(image.Rect(0, 0, width, height)) + if err := png.Encode(&buf, img); err != nil { + panic(err) + } + return buf.Bytes() +} diff --git a/tpl/images/init.go b/tpl/images/init.go new file mode 100644 index 000000000..299c76846 --- /dev/null +++ b/tpl/images/init.go @@ -0,0 +1,42 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package images + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" +) + +const name = "images" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx := New(d) + + ns := &internal.TemplateFuncsNamespace{ + Name: name, + Context: func(args ...interface{}) interface{} { return ctx }, + } + + ns.AddMethodMapping(ctx.Config, + []string{"imageConfig"}, + [][2]string{}, + ) + + return ns + + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/images/init_test.go b/tpl/images/init_test.go new file mode 100644 index 000000000..8a867f9d3 --- /dev/null +++ b/tpl/images/init_test.go @@ -0,0 +1,38 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package images + +import ( + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + var found bool + var ns *internal.TemplateFuncsNamespace + + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + ns = nsf(&deps.Deps{}) + if ns.Name == name { + found = true + break + } + } + + require.True(t, found) + require.IsType(t, &Namespace{}, ns.Context()) +} diff --git a/tpl/inflect/inflect.go b/tpl/inflect/inflect.go new file mode 100644 index 000000000..e66aee72f --- /dev/null +++ b/tpl/inflect/inflect.go @@ -0,0 +1,76 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package inflect + +import ( + "strconv" + + _inflect "github.com/bep/inflect" + "github.com/spf13/cast" +) + +// New returns a new instance of the inflect-namespaced template functions. +func New() *Namespace { + return &Namespace{} +} + +// Namespace provides template functions for the "inflect" namespace. +type Namespace struct{} + +// Humanize returns the humanized form of a single parameter. +// +// If the parameter is either an integer or a string containing an integer +// value, the behavior is to add the appropriate ordinal. +// +// Example: "my-first-post" -> "My first post" +// Example: "103" -> "103rd" +// Example: 52 -> "52nd" +func (ns *Namespace) Humanize(in interface{}) (string, error) { + word, err := cast.ToStringE(in) + if err != nil { + return "", err + } + + if word == "" { + return "", nil + } + + _, ok := in.(int) // original param was literal int value + _, err = strconv.Atoi(word) // original param was string containing an int value + if ok || err == nil { + return _inflect.Ordinalize(word), nil + } + + return _inflect.Humanize(word), nil +} + +// Pluralize returns the plural form of a single word. +func (ns *Namespace) Pluralize(in interface{}) (string, error) { + word, err := cast.ToStringE(in) + if err != nil { + return "", err + } + + return _inflect.Pluralize(word), nil +} + +// Singularize returns the singular form of a single word. +func (ns *Namespace) Singularize(in interface{}) (string, error) { + word, err := cast.ToStringE(in) + if err != nil { + return "", err + } + + return _inflect.Singularize(word), nil +} diff --git a/tpl/inflect/inflect_test.go b/tpl/inflect/inflect_test.go new file mode 100644 index 000000000..a2146a838 --- /dev/null +++ b/tpl/inflect/inflect_test.go @@ -0,0 +1,48 @@ +package inflect + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInflect(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + fn func(i interface{}) (string, error) + in interface{} + expect interface{} + }{ + {ns.Humanize, "MyCamel", "My camel"}, + {ns.Humanize, "", ""}, + {ns.Humanize, "103", "103rd"}, + {ns.Humanize, "41", "41st"}, + {ns.Humanize, 103, "103rd"}, + {ns.Humanize, int64(92), "92nd"}, + {ns.Humanize, "5.5", "5.5"}, + {ns.Humanize, t, false}, + {ns.Pluralize, "cat", "cats"}, + {ns.Pluralize, "", ""}, + {ns.Pluralize, t, false}, + {ns.Singularize, "cats", "cat"}, + {ns.Singularize, "", ""}, + {ns.Singularize, t, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := test.fn(test.in) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} diff --git a/tpl/inflect/init.go b/tpl/inflect/init.go new file mode 100644 index 000000000..3f258356b --- /dev/null +++ b/tpl/inflect/init.go @@ -0,0 +1,61 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package inflect + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" +) + +const name = "inflect" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx := New() + + ns := &internal.TemplateFuncsNamespace{ + Name: name, + Context: func(args ...interface{}) interface{} { return ctx }, + } + + ns.AddMethodMapping(ctx.Humanize, + []string{"humanize"}, + [][2]string{ + {`{{ humanize "my-first-post" }}`, `My first post`}, + {`{{ humanize "myCamelPost" }}`, `My camel post`}, + {`{{ humanize "52" }}`, `52nd`}, + {`{{ humanize 103 }}`, `103rd`}, + }, + ) + + ns.AddMethodMapping(ctx.Pluralize, + []string{"pluralize"}, + [][2]string{ + {`{{ "cat" | pluralize }}`, `cats`}, + }, + ) + + ns.AddMethodMapping(ctx.Singularize, + []string{"singularize"}, + [][2]string{ + {`{{ "cats" | singularize }}`, `cat`}, + }, + ) + + return ns + + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/inflect/init_test.go b/tpl/inflect/init_test.go new file mode 100644 index 000000000..cbcd312c7 --- /dev/null +++ b/tpl/inflect/init_test.go @@ -0,0 +1,38 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package inflect + +import ( + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + var found bool + var ns *internal.TemplateFuncsNamespace + + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + ns = nsf(&deps.Deps{}) + if ns.Name == name { + found = true + break + } + } + + require.True(t, found) + require.IsType(t, &Namespace{}, ns.Context()) +} diff --git a/tpl/internal/templatefuncRegistry_test.go b/tpl/internal/templatefuncRegistry_test.go new file mode 100644 index 000000000..dfc4ba09b --- /dev/null +++ b/tpl/internal/templatefuncRegistry_test.go @@ -0,0 +1,33 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +type Test struct { +} + +func (t *Test) MyTestMethod() string { + return "abcde" +} + +func TestMethodToName(t *testing.T) { + test := &Test{} + + require.Equal(t, "MyTestMethod", methodToName(test.MyTestMethod)) +} diff --git a/tpl/internal/templatefuncsRegistry.go b/tpl/internal/templatefuncsRegistry.go new file mode 100644 index 000000000..85d8c0b3b --- /dev/null +++ b/tpl/internal/templatefuncsRegistry.go @@ -0,0 +1,276 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Portions Copyright The Go Authors. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "bytes" + "encoding/json" + "fmt" + "go/doc" + "go/parser" + "go/token" + "io/ioutil" + "log" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "sync" + + "github.com/gohugoio/hugo/deps" +) + +var TemplateFuncsNamespaceRegistry []func(d *deps.Deps) *TemplateFuncsNamespace + +func AddTemplateFuncsNamespace(ns func(d *deps.Deps) *TemplateFuncsNamespace) { + TemplateFuncsNamespaceRegistry = append(TemplateFuncsNamespaceRegistry, ns) +} + +type TemplateFuncsNamespace struct { + // The namespace name, "strings", "lang", etc. + Name string + + // This is the method receiver. + Context func(v ...interface{}) interface{} + + // Additional info, aliases and examples, per method name. + MethodMappings map[string]TemplateFuncMethodMapping +} + +type TemplateFuncsNamespaces []*TemplateFuncsNamespace + +func (t *TemplateFuncsNamespace) AddMethodMapping(m interface{}, aliases []string, examples [][2]string) { + if t.MethodMappings == nil { + t.MethodMappings = make(map[string]TemplateFuncMethodMapping) + } + + name := methodToName(m) + + // sanity check + for _, e := range examples { + if e[0] == "" { + panic(t.Name + ": Empty example for " + name) + } + } + for _, a := range aliases { + if a == "" { + panic(t.Name + ": Empty alias for " + name) + } + } + + t.MethodMappings[name] = TemplateFuncMethodMapping{ + Method: m, + Aliases: aliases, + Examples: examples, + } + +} + +type TemplateFuncMethodMapping struct { + Method interface{} + + // Any template funcs aliases. This is mainly motivated by keeping + // backwards compability, but some new template funcs may also make + // sense to give short and snappy aliases. + // Note that these aliases are global and will be merged, so the last + // key will win. + Aliases []string + + // A slice of input/expected examples. + // We keep it a the namespace level for now, but may find a way to keep track + // of the single template func, for documentation purposes. + // Some of these, hopefully just a few, may depend on some test data to run. + Examples [][2]string +} + +func methodToName(m interface{}) string { + name := runtime.FuncForPC(reflect.ValueOf(m).Pointer()).Name() + name = filepath.Ext(name) + name = strings.TrimPrefix(name, ".") + name = strings.TrimSuffix(name, "-fm") + return name +} + +type goDocFunc struct { + Name string + Description string + Args []string + Aliases []string + Examples [][2]string +} + +func (t goDocFunc) toJSON() ([]byte, error) { + args, err := json.Marshal(t.Args) + if err != nil { + return nil, err + } + aliases, err := json.Marshal(t.Aliases) + if err != nil { + return nil, err + } + examples, err := json.Marshal(t.Examples) + if err != nil { + return nil, err + } + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf(`%q: + { "Description": %q, "Args": %s, "Aliases": %s, "Examples": %s } +`, t.Name, t.Description, args, aliases, examples)) + + return buf.Bytes(), nil +} + +func (namespaces TemplateFuncsNamespaces) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + + buf.WriteString("{") + + for i, ns := range namespaces { + if i != 0 { + buf.WriteString(",") + } + b, err := ns.toJSON() + if err != nil { + return nil, err + } + buf.Write(b) + } + + buf.WriteString("}") + + return buf.Bytes(), nil +} + +func (t *TemplateFuncsNamespace) toJSON() ([]byte, error) { + + var buf bytes.Buffer + + godoc := getGetTplPackagesGoDoc()[t.Name] + + var funcs []goDocFunc + + buf.WriteString(fmt.Sprintf(`%q: {`, t.Name)) + + ctx := t.Context() + ctxType := reflect.TypeOf(ctx) + for i := 0; i < ctxType.NumMethod(); i++ { + method := ctxType.Method(i) + f := goDocFunc{ + Name: method.Name, + } + + methodGoDoc := godoc[method.Name] + + if mapping, ok := t.MethodMappings[method.Name]; ok { + f.Aliases = mapping.Aliases + f.Examples = mapping.Examples + f.Description = methodGoDoc.Description + f.Args = methodGoDoc.Args + } + + funcs = append(funcs, f) + } + + for i, f := range funcs { + if i != 0 { + buf.WriteString(",") + } + funcStr, err := f.toJSON() + if err != nil { + return nil, err + } + buf.Write(funcStr) + } + + buf.WriteString("}") + + return buf.Bytes(), nil +} + +type methodGoDocInfo struct { + Description string + Args []string +} + +var ( + tplPackagesGoDoc map[string]map[string]methodGoDocInfo + tplPackagesGoDocInit sync.Once +) + +func getGetTplPackagesGoDoc() map[string]map[string]methodGoDocInfo { + tplPackagesGoDocInit.Do(func() { + tplPackagesGoDoc = make(map[string]map[string]methodGoDocInfo) + pwd, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + + fset := token.NewFileSet() + + // pwd will be inside one of the namespace packages during tests + var basePath string + if strings.Contains(pwd, "tpl") { + basePath = filepath.Join(pwd, "..") + } else { + basePath = filepath.Join(pwd, "tpl") + } + + files, err := ioutil.ReadDir(basePath) + if err != nil { + log.Fatal(err) + } + + for _, fi := range files { + if !fi.IsDir() { + continue + } + + namespaceDoc := make(map[string]methodGoDocInfo) + packagePath := filepath.Join(basePath, fi.Name()) + + d, err := parser.ParseDir(fset, packagePath, nil, parser.ParseComments) + if err != nil { + log.Fatal(err) + } + + for _, f := range d { + p := doc.New(f, "./", 0) + + for _, t := range p.Types { + if t.Name == "Namespace" { + for _, tt := range t.Methods { + var args []string + for _, p := range tt.Decl.Type.Params.List { + for _, pp := range p.Names { + args = append(args, pp.Name) + } + } + + description := strings.TrimSpace(tt.Doc) + di := methodGoDocInfo{Description: description, Args: args} + namespaceDoc[tt.Name] = di + } + } + } + } + + tplPackagesGoDoc[fi.Name()] = namespaceDoc + } + }) + + return tplPackagesGoDoc +} diff --git a/tpl/lang/init.go b/tpl/lang/init.go new file mode 100644 index 000000000..6a23cdc4c --- /dev/null +++ b/tpl/lang/init.go @@ -0,0 +1,52 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lang + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" +) + +const name = "lang" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx := New(d) + + ns := &internal.TemplateFuncsNamespace{ + Name: name, + Context: func(args ...interface{}) interface{} { return ctx }, + } + + ns.AddMethodMapping(ctx.Translate, + []string{"i18n", "T"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.NumFmt, + nil, + [][2]string{ + {`{{ lang.NumFmt 2 12345.6789 }}`, `12,345.68`}, + {`{{ lang.NumFmt 2 12345.6789 "- , ." }}`, `12.345,68`}, + {`{{ lang.NumFmt 6 -12345.6789 "- ." }}`, `-12345.678900`}, + {`{{ lang.NumFmt 0 -12345.6789 "- . ," }}`, `-12,346`}, + {`{{ -98765.4321 | lang.NumFmt 2 }}`, `-98,765.43`}, + }, + ) + return ns + + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/lang/init_test.go b/tpl/lang/init_test.go new file mode 100644 index 000000000..fc4893ad0 --- /dev/null +++ b/tpl/lang/init_test.go @@ -0,0 +1,38 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lang + +import ( + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + var found bool + var ns *internal.TemplateFuncsNamespace + + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + ns = nsf(&deps.Deps{}) + if ns.Name == name { + found = true + break + } + } + + require.True(t, found) + require.IsType(t, &Namespace{}, ns.Context()) +} diff --git a/tpl/lang/lang.go b/tpl/lang/lang.go new file mode 100644 index 000000000..3a659b119 --- /dev/null +++ b/tpl/lang/lang.go @@ -0,0 +1,136 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lang + +import ( + "errors" + "math" + "strconv" + "strings" + + "github.com/gohugoio/hugo/deps" + "github.com/spf13/cast" +) + +// New returns a new instance of the lang-namespaced template functions. +func New(deps *deps.Deps) *Namespace { + return &Namespace{ + deps: deps, + } +} + +// Namespace provides template functions for the "lang" namespace. +type Namespace struct { + deps *deps.Deps +} + +// Translate ... +func (ns *Namespace) Translate(id interface{}, args ...interface{}) (string, error) { + sid, err := cast.ToStringE(id) + if err != nil { + return "", nil + } + + return ns.deps.Translate(sid, args...), nil +} + +// NumFmt formats a number with the given precision using the +// negative, decimal, and grouping options. The `options` +// parameter is a string consisting of `<negative> <decimal> <grouping>`. The +// default `options` value is `- . ,`. +// +// Note that numbers are rounded up at 5 or greater. +// So, with precision set to 0, 1.5 becomes `2`, and 1.4 becomes `1`. +func (ns *Namespace) NumFmt(precision, number interface{}, options ...interface{}) (string, error) { + prec, err := cast.ToIntE(precision) + if err != nil { + return "", err + } + + n, err := cast.ToFloat64E(number) + if err != nil { + return "", err + } + + var neg, dec, grp string + + if len(options) == 0 { + // TODO(moorereason): move to site config + neg, dec, grp = "-", ".", "," + } else { + s, err := cast.ToStringE(options[0]) + if err != nil { + return "", nil + } + + rs := strings.Fields(s) + switch len(rs) { + case 0: + case 1: + neg = rs[0] + case 2: + neg, dec = rs[0], rs[1] + case 3: + neg, dec, grp = rs[0], rs[1], rs[2] + default: + return "", errors.New("too many fields in options parameter to NumFmt") + } + } + + // Logic from MIT Licensed github.com/go-playground/locales/ + // Original Copyright (c) 2016 Go Playground + + s := strconv.FormatFloat(math.Abs(n), 'f', prec, 64) + L := len(s) + 2 + len(s[:len(s)-1-prec])/3 + + var count int + inWhole := prec == 0 + b := make([]byte, 0, L) + + for i := len(s) - 1; i >= 0; i-- { + if s[i] == '.' { + for j := len(dec) - 1; j >= 0; j-- { + b = append(b, dec[j]) + } + inWhole = true + continue + } + + if inWhole { + if count == 3 { + for j := len(grp) - 1; j >= 0; j-- { + b = append(b, grp[j]) + } + count = 1 + } else { + count++ + } + } + + b = append(b, s[i]) + } + + if n < 0 { + for j := len(neg) - 1; j >= 0; j-- { + b = append(b, neg[j]) + } + } + + // reverse + for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 { + b[i], b[j] = b[j], b[i] + } + + return string(b), nil +} diff --git a/tpl/lang/lang_test.go b/tpl/lang/lang_test.go new file mode 100644 index 000000000..c494fd03a --- /dev/null +++ b/tpl/lang/lang_test.go @@ -0,0 +1,54 @@ +package lang + +import ( + "fmt" + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNumFormat(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + cases := []struct { + prec int + n float64 + runes string + + want string + }{ + {2, -12345.6789, "", "-12,345.68"}, + {2, -12345.6789, "- . ,", "-12,345.68"}, + {2, -12345.1234, "- . ,", "-12,345.12"}, + + {2, 12345.6789, "- . ,", "12,345.68"}, + {0, 12345.6789, "- . ,", "12,346"}, + {11, -12345.6789, "- . ,", "-12,345.67890000000"}, + + {3, -12345.6789, "- ,", "-12345,679"}, + {6, -12345.6789, "- , .", "-12.345,678900"}, + + // Arabic, ar_AE + {6, -12345.6789, "- ٫ ٬", "-12٬345٫678900"}, + } + + for i, c := range cases { + errMsg := fmt.Sprintf("[%d] %v", i, c) + + var s string + var err error + + if len(c.runes) == 0 { + s, err = ns.NumFmt(c.prec, c.n) + } else { + s, err = ns.NumFmt(c.prec, c.n, c.runes) + } + + require.NoError(t, err, errMsg) + assert.Equal(t, c.want, s, errMsg) + } +} diff --git a/tpl/math/init.go b/tpl/math/init.go new file mode 100644 index 000000000..3faf0e3a6 --- /dev/null +++ b/tpl/math/init.go @@ -0,0 +1,79 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package math + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" +) + +const name = "math" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx := New() + + ns := &internal.TemplateFuncsNamespace{ + Name: name, + Context: func(args ...interface{}) interface{} { return ctx }, + } + + ns.AddMethodMapping(ctx.Add, + []string{"add"}, + [][2]string{ + {"{{add 1 2}}", "3"}, + }, + ) + + ns.AddMethodMapping(ctx.Div, + []string{"div"}, + [][2]string{ + {"{{div 6 3}}", "2"}, + }, + ) + + ns.AddMethodMapping(ctx.Mod, + []string{"mod"}, + [][2]string{ + {"{{mod 15 3}}", "0"}, + }, + ) + + ns.AddMethodMapping(ctx.ModBool, + []string{"modBool"}, + [][2]string{ + {"{{modBool 15 3}}", "true"}, + }, + ) + + ns.AddMethodMapping(ctx.Mul, + []string{"mul"}, + [][2]string{ + {"{{mul 2 3}}", "6"}, + }, + ) + + ns.AddMethodMapping(ctx.Sub, + []string{"sub"}, + [][2]string{ + {"{{sub 3 2}}", "1"}, + }, + ) + + return ns + + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/math/init_test.go b/tpl/math/init_test.go new file mode 100644 index 000000000..f1882c1a2 --- /dev/null +++ b/tpl/math/init_test.go @@ -0,0 +1,38 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package math + +import ( + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + var found bool + var ns *internal.TemplateFuncsNamespace + + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + ns = nsf(&deps.Deps{}) + if ns.Name == name { + found = true + break + } + } + + require.True(t, found) + require.IsType(t, &Namespace{}, ns.Context()) +} diff --git a/tpl/math/math.go b/tpl/math/math.go new file mode 100644 index 000000000..57e7baf12 --- /dev/null +++ b/tpl/math/math.go @@ -0,0 +1,196 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package math + +import ( + "errors" + "reflect" +) + +// New returns a new instance of the math-namespaced template functions. +func New() *Namespace { + return &Namespace{} +} + +// Namespace provides template functions for the "math" namespace. +type Namespace struct{} + +func (ns *Namespace) Add(a, b interface{}) (interface{}, error) { + return DoArithmetic(a, b, '+') +} + +func (ns *Namespace) Div(a, b interface{}) (interface{}, error) { + return DoArithmetic(a, b, '/') +} + +// Mod returns a % b. +func (ns *Namespace) Mod(a, b interface{}) (int64, error) { + av := reflect.ValueOf(a) + bv := reflect.ValueOf(b) + var ai, bi int64 + + switch av.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + ai = av.Int() + default: + return 0, errors.New("Modulo operator can't be used with non integer value") + } + + switch bv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + bi = bv.Int() + default: + return 0, errors.New("Modulo operator can't be used with non integer value") + } + + if bi == 0 { + return 0, errors.New("The number can't be divided by zero at modulo operation") + } + + return ai % bi, nil +} + +// ModBool returns the boolean of a % b. If a % b == 0, return true. +func (ns *Namespace) ModBool(a, b interface{}) (bool, error) { + res, err := ns.Mod(a, b) + if err != nil { + return false, err + } + + return res == int64(0), nil +} + +func (ns *Namespace) Mul(a, b interface{}) (interface{}, error) { + return DoArithmetic(a, b, '*') +} + +func (ns *Namespace) Sub(a, b interface{}) (interface{}, error) { + return DoArithmetic(a, b, '-') +} + +// DoArithmetic performs arithmetic operations (+,-,*,/) using reflection to +// determine the type of the two terms. +func DoArithmetic(a, b interface{}, op rune) (interface{}, error) { + av := reflect.ValueOf(a) + bv := reflect.ValueOf(b) + var ai, bi int64 + var af, bf float64 + var au, bu uint64 + switch av.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + ai = av.Int() + switch bv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + bi = bv.Int() + case reflect.Float32, reflect.Float64: + af = float64(ai) // may overflow + ai = 0 + bf = bv.Float() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + bu = bv.Uint() + if ai >= 0 { + au = uint64(ai) + ai = 0 + } else { + bi = int64(bu) // may overflow + bu = 0 + } + default: + return nil, errors.New("Can't apply the operator to the values") + } + case reflect.Float32, reflect.Float64: + af = av.Float() + switch bv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + bf = float64(bv.Int()) // may overflow + case reflect.Float32, reflect.Float64: + bf = bv.Float() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + bf = float64(bv.Uint()) // may overflow + default: + return nil, errors.New("Can't apply the operator to the values") + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + au = av.Uint() + switch bv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + bi = bv.Int() + if bi >= 0 { + bu = uint64(bi) + bi = 0 + } else { + ai = int64(au) // may overflow + au = 0 + } + case reflect.Float32, reflect.Float64: + af = float64(au) // may overflow + au = 0 + bf = bv.Float() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + bu = bv.Uint() + default: + return nil, errors.New("Can't apply the operator to the values") + } + case reflect.String: + as := av.String() + if bv.Kind() == reflect.String && op == '+' { + bs := bv.String() + return as + bs, nil + } + return nil, errors.New("Can't apply the operator to the values") + default: + return nil, errors.New("Can't apply the operator to the values") + } + + switch op { + case '+': + if ai != 0 || bi != 0 { + return ai + bi, nil + } else if af != 0 || bf != 0 { + return af + bf, nil + } else if au != 0 || bu != 0 { + return au + bu, nil + } + return 0, nil + case '-': + if ai != 0 || bi != 0 { + return ai - bi, nil + } else if af != 0 || bf != 0 { + return af - bf, nil + } else if au != 0 || bu != 0 { + return au - bu, nil + } + return 0, nil + case '*': + if ai != 0 || bi != 0 { + return ai * bi, nil + } else if af != 0 || bf != 0 { + return af * bf, nil + } else if au != 0 || bu != 0 { + return au * bu, nil + } + return 0, nil + case '/': + if bi != 0 { + return ai / bi, nil + } else if bf != 0 { + return af / bf, nil + } else if bu != 0 { + return au / bu, nil + } + return nil, errors.New("Can't divide the value by 0") + default: + return nil, errors.New("There is no such an operation") + } +} diff --git a/tpl/math/math_test.go b/tpl/math/math_test.go new file mode 100644 index 000000000..40bed5539 --- /dev/null +++ b/tpl/math/math_test.go @@ -0,0 +1,220 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package math + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBasicNSArithmetic(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + fn func(a, b interface{}) (interface{}, error) + a interface{} + b interface{} + expect interface{} + }{ + {ns.Add, 4, 2, int64(6)}, + {ns.Add, 1.0, "foo", false}, + {ns.Sub, 4, 2, int64(2)}, + {ns.Sub, 1.0, "foo", false}, + {ns.Mul, 4, 2, int64(8)}, + {ns.Mul, 1.0, "foo", false}, + {ns.Div, 4, 2, int64(2)}, + {ns.Div, 1.0, "foo", false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := test.fn(test.a, test.b) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestDoArithmetic(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + a interface{} + b interface{} + op rune + expect interface{} + }{ + {3, 2, '+', int64(5)}, + {3, 2, '-', int64(1)}, + {3, 2, '*', int64(6)}, + {3, 2, '/', int64(1)}, + {3.0, 2, '+', float64(5)}, + {3.0, 2, '-', float64(1)}, + {3.0, 2, '*', float64(6)}, + {3.0, 2, '/', float64(1.5)}, + {3, 2.0, '+', float64(5)}, + {3, 2.0, '-', float64(1)}, + {3, 2.0, '*', float64(6)}, + {3, 2.0, '/', float64(1.5)}, + {3.0, 2.0, '+', float64(5)}, + {3.0, 2.0, '-', float64(1)}, + {3.0, 2.0, '*', float64(6)}, + {3.0, 2.0, '/', float64(1.5)}, + {uint(3), uint(2), '+', uint64(5)}, + {uint(3), uint(2), '-', uint64(1)}, + {uint(3), uint(2), '*', uint64(6)}, + {uint(3), uint(2), '/', uint64(1)}, + {uint(3), 2, '+', uint64(5)}, + {uint(3), 2, '-', uint64(1)}, + {uint(3), 2, '*', uint64(6)}, + {uint(3), 2, '/', uint64(1)}, + {3, uint(2), '+', uint64(5)}, + {3, uint(2), '-', uint64(1)}, + {3, uint(2), '*', uint64(6)}, + {3, uint(2), '/', uint64(1)}, + {uint(3), -2, '+', int64(1)}, + {uint(3), -2, '-', int64(5)}, + {uint(3), -2, '*', int64(-6)}, + {uint(3), -2, '/', int64(-1)}, + {-3, uint(2), '+', int64(-1)}, + {-3, uint(2), '-', int64(-5)}, + {-3, uint(2), '*', int64(-6)}, + {-3, uint(2), '/', int64(-1)}, + {uint(3), 2.0, '+', float64(5)}, + {uint(3), 2.0, '-', float64(1)}, + {uint(3), 2.0, '*', float64(6)}, + {uint(3), 2.0, '/', float64(1.5)}, + {3.0, uint(2), '+', float64(5)}, + {3.0, uint(2), '-', float64(1)}, + {3.0, uint(2), '*', float64(6)}, + {3.0, uint(2), '/', float64(1.5)}, + {0, 0, '+', 0}, + {0, 0, '-', 0}, + {0, 0, '*', 0}, + {"foo", "bar", '+', "foobar"}, + {3, 0, '/', false}, + {3.0, 0, '/', false}, + {3, 0.0, '/', false}, + {uint(3), uint(0), '/', false}, + {3, uint(0), '/', false}, + {-3, uint(0), '/', false}, + {uint(3), 0, '/', false}, + {3.0, uint(0), '/', false}, + {uint(3), 0.0, '/', false}, + {3, "foo", '+', false}, + {3.0, "foo", '+', false}, + {uint(3), "foo", '+', false}, + {"foo", 3, '+', false}, + {"foo", "bar", '-', false}, + {3, 2, '%', false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := DoArithmetic(test.a, test.b, test.op) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestMod(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + a interface{} + b interface{} + expect interface{} + }{ + {3, 2, int64(1)}, + {3, 1, int64(0)}, + {3, 0, false}, + {0, 3, int64(0)}, + {3.1, 2, false}, + {3, 2.1, false}, + {3.1, 2.1, false}, + {int8(3), int8(2), int64(1)}, + {int16(3), int16(2), int64(1)}, + {int32(3), int32(2), int64(1)}, + {int64(3), int64(2), int64(1)}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Mod(test.a, test.b) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestModBool(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + a interface{} + b interface{} + expect interface{} + }{ + {3, 3, true}, + {3, 2, false}, + {3, 1, true}, + {3, 0, nil}, + {0, 3, true}, + {3.1, 2, nil}, + {3, 2.1, nil}, + {3.1, 2.1, nil}, + {int8(3), int8(3), true}, + {int8(3), int8(2), false}, + {int16(3), int16(3), true}, + {int16(3), int16(2), false}, + {int32(3), int32(3), true}, + {int32(3), int32(2), false}, + {int64(3), int64(3), true}, + {int64(3), int64(2), false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.ModBool(test.a, test.b) + + if test.expect == nil { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} diff --git a/tpl/os/init.go b/tpl/os/init.go new file mode 100644 index 000000000..3fc3308f2 --- /dev/null +++ b/tpl/os/init.go @@ -0,0 +1,56 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package os + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" +) + +const name = "os" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx := New(d) + + ns := &internal.TemplateFuncsNamespace{ + Name: name, + Context: func(args ...interface{}) interface{} { return ctx }, + } + + ns.AddMethodMapping(ctx.Getenv, + []string{"getenv"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.ReadDir, + []string{"readDir"}, + [][2]string{ + {`{{ range (readDir ".") }}{{ .Name }}{{ end }}`, "README.txt"}, + }, + ) + + ns.AddMethodMapping(ctx.ReadFile, + []string{"readFile"}, + [][2]string{ + {`{{ readFile "README.txt" }}`, `Hugo Rocks!`}, + }, + ) + + return ns + + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/os/init_test.go b/tpl/os/init_test.go new file mode 100644 index 000000000..08d816cdf --- /dev/null +++ b/tpl/os/init_test.go @@ -0,0 +1,38 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package os + +import ( + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + var found bool + var ns *internal.TemplateFuncsNamespace + + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + ns = nsf(&deps.Deps{}) + if ns.Name == name { + found = true + break + } + } + + require.True(t, found) + require.IsType(t, &Namespace{}, ns.Context()) +} diff --git a/tpl/os/os.go b/tpl/os/os.go new file mode 100644 index 000000000..fb60f87dd --- /dev/null +++ b/tpl/os/os.go @@ -0,0 +1,98 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package os + +import ( + "errors" + "fmt" + _os "os" + + "github.com/gohugoio/hugo/deps" + "github.com/spf13/afero" + "github.com/spf13/cast" +) + +// New returns a new instance of the os-namespaced template functions. +func New(deps *deps.Deps) *Namespace { + return &Namespace{ + deps: deps, + } +} + +// Namespace provides template functions for the "os" namespace. +type Namespace struct { + deps *deps.Deps +} + +// Getenv retrieves the value of the environment variable named by the key. +// It returns the value, which will be empty if the variable is not present. +func (ns *Namespace) Getenv(key interface{}) (string, error) { + skey, err := cast.ToStringE(key) + if err != nil { + return "", nil + } + + return _os.Getenv(skey), nil +} + +// readFile reads the file named by filename relative to the given basepath +// and returns the contents as a string. +// There is a upper size limit set at 1 megabytes. +func readFile(fs *afero.BasePathFs, filename string) (string, error) { + if filename == "" { + return "", errors.New("readFile needs a filename") + } + + if info, err := fs.Stat(filename); err == nil { + if info.Size() > 1000000 { + return "", fmt.Errorf("File %q is too big", filename) + } + } else { + return "", err + } + b, err := afero.ReadFile(fs, filename) + + if err != nil { + return "", err + } + + return string(b), nil +} + +// ReadFilereads the file named by filename relative to the configured +// WorkingDir. It returns the contents as a string. There is a upper size +// limit set at 1 megabytes. +func (ns *Namespace) ReadFile(i interface{}) (string, error) { + s, err := cast.ToStringE(i) + if err != nil { + return "", err + } + + return readFile(ns.deps.Fs.WorkingDir, s) +} + +// ReadDir lists the directory contents relative to the configured WorkingDir. +func (ns *Namespace) ReadDir(i interface{}) ([]_os.FileInfo, error) { + path, err := cast.ToStringE(i) + if err != nil { + return nil, err + } + + list, err := afero.ReadDir(ns.deps.Fs.WorkingDir, path) + if err != nil { + return nil, fmt.Errorf("Failed to read Directory %s with error message %s", path, err) + } + + return list, nil +} diff --git a/tpl/os/os_test.go b/tpl/os/os_test.go new file mode 100644 index 000000000..383eb88c4 --- /dev/null +++ b/tpl/os/os_test.go @@ -0,0 +1,65 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package os + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/hugofs" + "github.com/spf13/afero" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReadFile(t *testing.T) { + t.Parallel() + + workingDir := "/home/hugo" + + v := viper.New() + v.Set("workingDir", workingDir) + + // f := newTestFuncsterWithViper(v) + ns := New(&deps.Deps{Fs: hugofs.NewMem(v)}) + + afero.WriteFile(ns.deps.Fs.Source, filepath.Join(workingDir, "/f/f1.txt"), []byte("f1-content"), 0755) + afero.WriteFile(ns.deps.Fs.Source, filepath.Join("/home", "f2.txt"), []byte("f2-content"), 0755) + + for i, test := range []struct { + filename string + expect interface{} + }{ + {filepath.FromSlash("/f/f1.txt"), "f1-content"}, + {filepath.FromSlash("f/f1.txt"), "f1-content"}, + {filepath.FromSlash("../f2.txt"), false}, + {"", false}, + {"b", false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.ReadFile(test.filename) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} diff --git a/tpl/partials/init.go b/tpl/partials/init.go new file mode 100644 index 000000000..eca93783c --- /dev/null +++ b/tpl/partials/init.go @@ -0,0 +1,49 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package partials + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" +) + +const name = "partials" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx := New(d) + + ns := &internal.TemplateFuncsNamespace{ + Name: name, + Context: func(args ...interface{}) interface{} { return ctx }, + } + + ns.AddMethodMapping(ctx.Include, + []string{"partial"}, + [][2]string{ + {`{{ partial "header.html" . }}`, `<title>Hugo Rocks!</title>`}, + }, + ) + + ns.AddMethodMapping(ctx.getCached, + []string{"partialCached"}, + [][2]string{}, + ) + + return ns + + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/partials/init_test.go b/tpl/partials/init_test.go new file mode 100644 index 000000000..ef284a826 --- /dev/null +++ b/tpl/partials/init_test.go @@ -0,0 +1,38 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package partials + +import ( + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + var found bool + var ns *internal.TemplateFuncsNamespace + + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + ns = nsf(&deps.Deps{}) + if ns.Name == name { + found = true + break + } + } + + require.True(t, found) + require.IsType(t, &Namespace{}, ns.Context()) +} diff --git a/tpl/partials/partials.go b/tpl/partials/partials.go new file mode 100644 index 000000000..d6e9fc9c5 --- /dev/null +++ b/tpl/partials/partials.go @@ -0,0 +1,126 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package partials + +import ( + "fmt" + "html/template" + "strings" + "sync" + texttemplate "text/template" + + bp "github.com/gohugoio/hugo/bufferpool" + "github.com/gohugoio/hugo/deps" +) + +var TestTemplateProvider deps.ResourceProvider + +// partialCache represents a cache of partials protected by a mutex. +type partialCache struct { + sync.RWMutex + p map[string]interface{} +} + +// New returns a new instance of the templates-namespaced template functions. +func New(deps *deps.Deps) *Namespace { + return &Namespace{ + deps: deps, + cachedPartials: partialCache{p: make(map[string]interface{})}, + } +} + +// Namespace provides template functions for the "templates" namespace. +type Namespace struct { + deps *deps.Deps + cachedPartials partialCache +} + +// Include executes the named partial and returns either a string, +// when the partial is a text/template, or template.HTML when html/template. +func (ns *Namespace) Include(name string, contextList ...interface{}) (interface{}, error) { + if strings.HasPrefix("partials/", name) { + name = name[8:] + } + var context interface{} + + if len(contextList) == 0 { + context = nil + } else { + context = contextList[0] + } + + for _, n := range []string{"partials/" + name, "theme/partials/" + name} { + templ := ns.deps.Tmpl.Lookup(n) + if templ == nil { + // For legacy reasons. + templ = ns.deps.Tmpl.Lookup(n + ".html") + } + if templ != nil { + b := bp.GetBuffer() + defer bp.PutBuffer(b) + + if err := templ.Execute(b, context); err != nil { + return "", err + } + + if _, ok := templ.Template.(*texttemplate.Template); ok { + return b.String(), nil + } + + return template.HTML(b.String()), nil + + } + } + + return "", fmt.Errorf("Partial %q not found", name) +} + +// getCached executes and caches partial templates. An optional variant +// string parameter (a string slice actually, but be only use a variadic +// argument to make it optional) can be passed so that a given partial can have +// multiple uses. The cache is created with name+variant as the key. +func (ns *Namespace) getCached(name string, context interface{}, variant ...string) (interface{}, error) { + key := name + if len(variant) > 0 { + for i := 0; i < len(variant); i++ { + key += variant[i] + } + } + return ns.getOrCreate(key, name, context) +} + +func (ns *Namespace) getOrCreate(key, name string, context interface{}) (p interface{}, err error) { + var ok bool + + ns.cachedPartials.RLock() + p, ok = ns.cachedPartials.p[key] + ns.cachedPartials.RUnlock() + + if ok { + return + } + + ns.cachedPartials.Lock() + if p, ok = ns.cachedPartials.p[key]; !ok { + ns.cachedPartials.Unlock() + p, err = ns.Include(name, context) + + ns.cachedPartials.Lock() + ns.cachedPartials.p[key] = p + + } + ns.cachedPartials.Unlock() + + return +} diff --git a/tpl/safe/init.go b/tpl/safe/init.go new file mode 100644 index 000000000..edb16ed87 --- /dev/null +++ b/tpl/safe/init.go @@ -0,0 +1,81 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package safe + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" +) + +const name = "safe" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx := New() + + ns := &internal.TemplateFuncsNamespace{ + Name: name, + Context: func(args ...interface{}) interface{} { return ctx }, + } + + ns.AddMethodMapping(ctx.CSS, + []string{"safeCSS"}, + [][2]string{ + {`{{ "Bat&Man" | safeCSS | safeCSS }}`, `Bat&Man`}, + }, + ) + + ns.AddMethodMapping(ctx.HTML, + []string{"safeHTML"}, + [][2]string{ + {`{{ "Bat&Man" | safeHTML | safeHTML }}`, `Bat&Man`}, + {`{{ "Bat&Man" | safeHTML }}`, `Bat&Man`}, + }, + ) + + ns.AddMethodMapping(ctx.HTMLAttr, + []string{"safeHTMLAttr"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.JS, + []string{"safeJS"}, + [][2]string{ + {`{{ "(1*2)" | safeJS | safeJS }}`, `(1*2)`}, + }, + ) + + ns.AddMethodMapping(ctx.JSStr, + []string{"safeJSStr"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.URL, + []string{"safeURL"}, + [][2]string{ + {`{{ "http://gohugo.io" | safeURL | safeURL }}`, `http://gohugo.io`}, + }, + ) + + ns.AddMethodMapping(ctx.SanitizeURL, + []string{"sanitizeURL", "sanitizeurl"}, + [][2]string{}, + ) + + return ns + + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/safe/init_test.go b/tpl/safe/init_test.go new file mode 100644 index 000000000..99305b53b --- /dev/null +++ b/tpl/safe/init_test.go @@ -0,0 +1,38 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package safe + +import ( + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + var found bool + var ns *internal.TemplateFuncsNamespace + + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + ns = nsf(&deps.Deps{}) + if ns.Name == name { + found = true + break + } + } + + require.True(t, found) + require.IsType(t, &Namespace{}, ns.Context()) +} diff --git a/tpl/safe/safe.go b/tpl/safe/safe.go new file mode 100644 index 000000000..64c36cc4d --- /dev/null +++ b/tpl/safe/safe.go @@ -0,0 +1,71 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package safe + +import ( + "html/template" + + "github.com/gohugoio/hugo/helpers" + "github.com/spf13/cast" +) + +// New returns a new instance of the safe-namespaced template functions. +func New() *Namespace { + return &Namespace{} +} + +// Namespace provides template functions for the "safe" namespace. +type Namespace struct{} + +// CSS returns a given string as html/template CSS content. +func (ns *Namespace) CSS(a interface{}) (template.CSS, error) { + s, err := cast.ToStringE(a) + return template.CSS(s), err +} + +// HTML returns a given string as html/template HTML content. +func (ns *Namespace) HTML(a interface{}) (template.HTML, error) { + s, err := cast.ToStringE(a) + return template.HTML(s), err +} + +// HTMLAttr returns a given string as html/template HTMLAttr content. +func (ns *Namespace) HTMLAttr(a interface{}) (template.HTMLAttr, error) { + s, err := cast.ToStringE(a) + return template.HTMLAttr(s), err +} + +// JS returns the given string as a html/template JS content. +func (ns *Namespace) JS(a interface{}) (template.JS, error) { + s, err := cast.ToStringE(a) + return template.JS(s), err +} + +// JSStr returns the given string as a html/template JSStr content. +func (ns *Namespace) JSStr(a interface{}) (template.JSStr, error) { + s, err := cast.ToStringE(a) + return template.JSStr(s), err +} + +// URL returns a given string as html/template URL content. +func (ns *Namespace) URL(a interface{}) (template.URL, error) { + s, err := cast.ToStringE(a) + return template.URL(s), err +} + +// SanitizeURL returns a given string as html/template URL content. +func (ns *Namespace) SanitizeURL(a interface{}) (string, error) { + s, err := cast.ToStringE(a) + return helpers.SanitizeURL(s), err +} diff --git a/tpl/safe/safe_test.go b/tpl/safe/safe_test.go new file mode 100644 index 000000000..346b448c9 --- /dev/null +++ b/tpl/safe/safe_test.go @@ -0,0 +1,214 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package safe + +import ( + "fmt" + "html/template" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type tstNoStringer struct{} + +func TestCSS(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + a interface{} + expect interface{} + }{ + {`a[href =~ "//example.com"]#foo`, template.CSS(`a[href =~ "//example.com"]#foo`)}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.CSS(test.a) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestHTML(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + a interface{} + expect interface{} + }{ + {`Hello, <b>World</b> &tc!`, template.HTML(`Hello, <b>World</b> &tc!`)}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.HTML(test.a) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestHTMLAttr(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + a interface{} + expect interface{} + }{ + {` dir="ltr"`, template.HTMLAttr(` dir="ltr"`)}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.HTMLAttr(test.a) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestJS(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + a interface{} + expect interface{} + }{ + {`c && alert("Hello, World!");`, template.JS(`c && alert("Hello, World!");`)}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.JS(test.a) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestJSStr(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + a interface{} + expect interface{} + }{ + {`Hello, World & O'Reilly\x21`, template.JSStr(`Hello, World & O'Reilly\x21`)}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.JSStr(test.a) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestURL(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + a interface{} + expect interface{} + }{ + {`greeting=H%69&addressee=(World)`, template.URL(`greeting=H%69&addressee=(World)`)}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.URL(test.a) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestSanitizeURL(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + a interface{} + expect interface{} + }{ + {"http://foo/../../bar", "http://foo/bar"}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.SanitizeURL(test.a) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} diff --git a/tpl/strings/init.go b/tpl/strings/init.go new file mode 100644 index 000000000..45d694b97 --- /dev/null +++ b/tpl/strings/init.go @@ -0,0 +1,142 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package strings + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" +) + +const name = "strings" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx := New(d) + + ns := &internal.TemplateFuncsNamespace{ + Name: name, + Context: func(args ...interface{}) interface{} { return ctx }, + } + + ns.AddMethodMapping(ctx.Chomp, + []string{"chomp"}, + [][2]string{ + {`{{chomp "<p>Blockhead</p>\n" }}`, `<p>Blockhead</p>`}, + }, + ) + + ns.AddMethodMapping(ctx.CountRunes, + []string{"countrunes"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.CountWords, + []string{"countwords"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.FindRE, + []string{"findRE"}, + [][2]string{ + { + `{{ findRE "[G|g]o" "Hugo is a static side generator written in Go." "1" }}`, + `[go]`}, + }, + ) + + ns.AddMethodMapping(ctx.HasPrefix, + []string{"hasPrefix"}, + [][2]string{ + {`{{ hasPrefix "Hugo" "Hu" }}`, `true`}, + {`{{ hasPrefix "Hugo" "Fu" }}`, `false`}, + }, + ) + + ns.AddMethodMapping(ctx.ToLower, + []string{"lower"}, + [][2]string{ + {`{{lower "BatMan"}}`, `batman`}, + }, + ) + + ns.AddMethodMapping(ctx.Replace, + []string{"replace"}, + [][2]string{ + { + `{{ replace "Batman and Robin" "Robin" "Catwoman" }}`, + `Batman and Catwoman`}, + }, + ) + + ns.AddMethodMapping(ctx.ReplaceRE, + []string{"replaceRE"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.SliceString, + []string{"slicestr"}, + [][2]string{ + {`{{slicestr "BatMan" 0 3}}`, `Bat`}, + {`{{slicestr "BatMan" 3}}`, `Man`}, + }, + ) + + ns.AddMethodMapping(ctx.Split, + []string{"split"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.Substr, + []string{"substr"}, + [][2]string{ + {`{{substr "BatMan" 0 -3}}`, `Bat`}, + {`{{substr "BatMan" 3 3}}`, `Man`}, + }, + ) + + ns.AddMethodMapping(ctx.Trim, + []string{"trim"}, + [][2]string{ + {`{{ trim "++Batman--" "+-" }}`, `Batman`}, + }, + ) + + ns.AddMethodMapping(ctx.Title, + []string{"title"}, + [][2]string{ + {`{{title "Bat man"}}`, `Bat Man`}, + }, + ) + + ns.AddMethodMapping(ctx.Truncate, + []string{"truncate"}, + [][2]string{ + {`{{ "this is a very long text" | truncate 10 " ..." }}`, `this is a ...`}, + {`{{ "With [Markdown](/markdown) inside." | markdownify | truncate 14 }}`, `With <a href="/markdown">Markdown …</a>`}, + }, + ) + + ns.AddMethodMapping(ctx.ToUpper, + []string{"upper"}, + [][2]string{ + {`{{upper "BatMan"}}`, `BATMAN`}, + }, + ) + + return ns + + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/strings/init_test.go b/tpl/strings/init_test.go new file mode 100644 index 000000000..a8ad8ffdf --- /dev/null +++ b/tpl/strings/init_test.go @@ -0,0 +1,38 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package strings + +import ( + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + var found bool + var ns *internal.TemplateFuncsNamespace + + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + ns = nsf(&deps.Deps{}) + if ns.Name == name { + found = true + break + } + } + + require.True(t, found) + require.IsType(t, &Namespace{}, ns.Context()) +} diff --git a/tpl/strings/regexp.go b/tpl/strings/regexp.go new file mode 100644 index 000000000..7b52c9f6e --- /dev/null +++ b/tpl/strings/regexp.go @@ -0,0 +1,109 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package strings + +import ( + "regexp" + "sync" + + "github.com/spf13/cast" +) + +// FindRE returns a list of strings that match the regular expression. By default all matches +// will be included. The number of matches can be limited with an optional third parameter. +func (ns *Namespace) FindRE(expr string, content interface{}, limit ...interface{}) ([]string, error) { + re, err := reCache.Get(expr) + if err != nil { + return nil, err + } + + conv, err := cast.ToStringE(content) + if err != nil { + return nil, err + } + + if len(limit) == 0 { + return re.FindAllString(conv, -1), nil + } + + lim, err := cast.ToIntE(limit[0]) + if err != nil { + return nil, err + } + + return re.FindAllString(conv, lim), nil +} + +// ReplaceRE returns a copy of s, replacing all matches of the regular +// expression pattern with the replacement text repl. +func (ns *Namespace) ReplaceRE(pattern, repl, s interface{}) (_ string, err error) { + sp, err := cast.ToStringE(pattern) + if err != nil { + return + } + + sr, err := cast.ToStringE(repl) + if err != nil { + return + } + + ss, err := cast.ToStringE(s) + if err != nil { + return + } + + re, err := reCache.Get(sp) + if err != nil { + return "", err + } + + return re.ReplaceAllString(ss, sr), nil +} + +// regexpCache represents a cache of regexp objects protected by a mutex. +type regexpCache struct { + mu sync.RWMutex + re map[string]*regexp.Regexp +} + +// Get retrieves a regexp object from the cache based upon the pattern. +// If the pattern is not found in the cache, create one +func (rc *regexpCache) Get(pattern string) (re *regexp.Regexp, err error) { + var ok bool + + if re, ok = rc.get(pattern); !ok { + re, err = regexp.Compile(pattern) + if err != nil { + return nil, err + } + rc.set(pattern, re) + } + + return re, nil +} + +func (rc *regexpCache) get(key string) (re *regexp.Regexp, ok bool) { + rc.mu.RLock() + re, ok = rc.re[key] + rc.mu.RUnlock() + return +} + +func (rc *regexpCache) set(key string, re *regexp.Regexp) { + rc.mu.Lock() + rc.re[key] = re + rc.mu.Unlock() +} + +var reCache = regexpCache{re: make(map[string]*regexp.Regexp)} diff --git a/tpl/strings/regexp_test.go b/tpl/strings/regexp_test.go new file mode 100644 index 000000000..3bacd2018 --- /dev/null +++ b/tpl/strings/regexp_test.go @@ -0,0 +1,86 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package strings + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFindRE(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + expr string + content interface{} + limit interface{} + expect interface{} + }{ + {"[G|g]o", "Hugo is a static site generator written in Go.", 2, []string{"go", "Go"}}, + {"[G|g]o", "Hugo is a static site generator written in Go.", -1, []string{"go", "Go"}}, + {"[G|g]o", "Hugo is a static site generator written in Go.", 1, []string{"go"}}, + {"[G|g]o", "Hugo is a static site generator written in Go.", "1", []string{"go"}}, + {"[G|g]o", "Hugo is a static site generator written in Go.", nil, []string(nil)}, + // errors + {"[G|go", "Hugo is a static site generator written in Go.", nil, false}, + {"[G|g]o", t, nil, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.FindRE(test.expr, test.content, test.limit) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestReplaceRE(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + pattern interface{} + repl interface{} + s interface{} + expect interface{} + }{ + {"^https?://([^/]+).*", "$1", "http://gohugo.io/docs", "gohugo.io"}, + {"^https?://([^/]+).*", "$2", "http://gohugo.io/docs", ""}, + {"(ab)", "AB", "aabbaab", "aABbaAB"}, + // errors + {"(ab", "AB", "aabb", false}, // invalid re + {tstNoStringer{}, "$2", "http://gohugo.io/docs", false}, + {"^https?://([^/]+).*", tstNoStringer{}, "http://gohugo.io/docs", false}, + {"^https?://([^/]+).*", "$2", tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.ReplaceRE(test.pattern, test.repl, test.s) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} diff --git a/tpl/strings/strings.go b/tpl/strings/strings.go new file mode 100644 index 000000000..ec95be730 --- /dev/null +++ b/tpl/strings/strings.go @@ -0,0 +1,377 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package strings + +import ( + "errors" + "fmt" + "html/template" + _strings "strings" + "unicode/utf8" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" + "github.com/spf13/cast" +) + +// New returns a new instance of the strings-namespaced template functions. +func New(d *deps.Deps) *Namespace { + return &Namespace{deps: d} +} + +// Namespace provides template functions for the "strings" namespace. +// Most functions mimic the Go stdlib, but the order of the parameters may be +// different to ease their use in the Go template system. +type Namespace struct { + deps *deps.Deps +} + +// CountRunes returns the number of runes in s, excluding whitepace. +func (ns *Namespace) CountRunes(s interface{}) (int, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return 0, fmt.Errorf("Failed to convert content to string: %s", err) + } + + counter := 0 + for _, r := range helpers.StripHTML(ss) { + if !helpers.IsWhitespace(r) { + counter++ + } + } + + return counter, nil +} + +// CountWords returns the approximate word count in s. +func (ns *Namespace) CountWords(s interface{}) (int, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return 0, fmt.Errorf("Failed to convert content to string: %s", err) + } + + counter := 0 + for _, word := range _strings.Fields(helpers.StripHTML(ss)) { + runeCount := utf8.RuneCountInString(word) + if len(word) == runeCount { + counter++ + } else { + counter += runeCount + } + } + + return counter, nil +} + +// Chomp returns a copy of s with all trailing newline characters removed. +func (ns *Namespace) Chomp(s interface{}) (template.HTML, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return template.HTML(_strings.TrimRight(ss, "\r\n")), nil +} + +// Contains reports whether substr is in s. +func (ns *Namespace) Contains(s, substr interface{}) (bool, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return false, err + } + + su, err := cast.ToStringE(substr) + if err != nil { + return false, err + } + + return _strings.Contains(ss, su), nil +} + +// ContainsAny reports whether any Unicode code points in chars are within s. +func (ns *Namespace) ContainsAny(s, chars interface{}) (bool, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return false, err + } + + sc, err := cast.ToStringE(chars) + if err != nil { + return false, err + } + + return _strings.ContainsAny(ss, sc), nil +} + +// HasPrefix tests whether the input s begins with prefix. +func (ns *Namespace) HasPrefix(s, prefix interface{}) (bool, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return false, err + } + + sx, err := cast.ToStringE(prefix) + if err != nil { + return false, err + } + + return _strings.HasPrefix(ss, sx), nil +} + +// HasSuffix tests whether the input s begins with suffix. +func (ns *Namespace) HasSuffix(s, suffix interface{}) (bool, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return false, err + } + + sx, err := cast.ToStringE(suffix) + if err != nil { + return false, err + } + + return _strings.HasSuffix(ss, sx), nil +} + +// Replace returns a copy of the string s with all occurrences of old replaced +// with new. +func (ns *Namespace) Replace(s, old, new interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + so, err := cast.ToStringE(old) + if err != nil { + return "", err + } + + sn, err := cast.ToStringE(new) + if err != nil { + return "", err + } + + return _strings.Replace(ss, so, sn, -1), nil +} + +// SliceString slices a string by specifying a half-open range with +// two indices, start and end. 1 and 4 creates a slice including elements 1 through 3. +// The end index can be omitted, it defaults to the string's length. +func (ns *Namespace) SliceString(a interface{}, startEnd ...interface{}) (string, error) { + aStr, err := cast.ToStringE(a) + if err != nil { + return "", err + } + + var argStart, argEnd int + + argNum := len(startEnd) + + if argNum > 0 { + if argStart, err = cast.ToIntE(startEnd[0]); err != nil { + return "", errors.New("start argument must be integer") + } + } + if argNum > 1 { + if argEnd, err = cast.ToIntE(startEnd[1]); err != nil { + return "", errors.New("end argument must be integer") + } + } + + if argNum > 2 { + return "", errors.New("too many arguments") + } + + asRunes := []rune(aStr) + + if argNum > 0 && (argStart < 0 || argStart >= len(asRunes)) { + return "", errors.New("slice bounds out of range") + } + + if argNum == 2 { + if argEnd < 0 || argEnd > len(asRunes) { + return "", errors.New("slice bounds out of range") + } + return string(asRunes[argStart:argEnd]), nil + } else if argNum == 1 { + return string(asRunes[argStart:]), nil + } else { + return string(asRunes[:]), nil + } + +} + +// Split slices an input string into all substrings separated by delimiter. +func (ns *Namespace) Split(a interface{}, delimiter string) ([]string, error) { + aStr, err := cast.ToStringE(a) + if err != nil { + return []string{}, err + } + + return _strings.Split(aStr, delimiter), nil +} + +// Substr extracts parts of a string, beginning at the character at the specified +// position, and returns the specified number of characters. +// +// It normally takes two parameters: start and length. +// It can also take one parameter: start, i.e. length is omitted, in which case +// the substring starting from start until the end of the string will be returned. +// +// To extract characters from the end of the string, use a negative start number. +// +// In addition, borrowing from the extended behavior described at http://php.net/substr, +// if length is given and is negative, then that many characters will be omitted from +// the end of string. +func (ns *Namespace) Substr(a interface{}, nums ...interface{}) (string, error) { + aStr, err := cast.ToStringE(a) + if err != nil { + return "", err + } + + var start, length int + + asRunes := []rune(aStr) + + switch len(nums) { + case 0: + return "", errors.New("too less arguments") + case 1: + if start, err = cast.ToIntE(nums[0]); err != nil { + return "", errors.New("start argument must be integer") + } + length = len(asRunes) + case 2: + if start, err = cast.ToIntE(nums[0]); err != nil { + return "", errors.New("start argument must be integer") + } + if length, err = cast.ToIntE(nums[1]); err != nil { + return "", errors.New("length argument must be integer") + } + default: + return "", errors.New("too many arguments") + } + + if start < -len(asRunes) { + start = 0 + } + if start > len(asRunes) { + return "", fmt.Errorf("start position out of bounds for %d-byte string", len(aStr)) + } + + var s, e int + if start >= 0 && length >= 0 { + s = start + e = start + length + } else if start < 0 && length >= 0 { + s = len(asRunes) + start - length + 1 + e = len(asRunes) + start + 1 + } else if start >= 0 && length < 0 { + s = start + e = len(asRunes) + length + } else { + s = len(asRunes) + start + e = len(asRunes) + length + } + + if s > e { + return "", fmt.Errorf("calculated start position greater than end position: %d > %d", s, e) + } + if e > len(asRunes) { + e = len(asRunes) + } + + return string(asRunes[s:e]), nil +} + +// Title returns a copy of the input s with all Unicode letters that begin words +// mapped to their title case. +func (ns *Namespace) Title(s interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return _strings.Title(ss), nil +} + +// ToLower returns a copy of the input s with all Unicode letters mapped to their +// lower case. +func (ns *Namespace) ToLower(s interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return _strings.ToLower(ss), nil +} + +// ToUpper returns a copy of the input s with all Unicode letters mapped to their +// upper case. +func (ns *Namespace) ToUpper(s interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return _strings.ToUpper(ss), nil +} + +// Trim returns a string with all leading and trailing characters defined +// contained in cutset removed. +func (ns *Namespace) Trim(s, cutset interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + sc, err := cast.ToStringE(cutset) + if err != nil { + return "", err + } + + return _strings.Trim(ss, sc), nil +} + +// TrimPrefix returns s without the provided leading prefix string. If s doesn't +// start with prefix, s is returned unchanged. +func (ns *Namespace) TrimPrefix(s, prefix interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + sx, err := cast.ToStringE(prefix) + if err != nil { + return "", err + } + + return _strings.TrimPrefix(ss, sx), nil +} + +// TrimSuffix returns s without the provided trailing suffix string. If s +// doesn't end with suffix, s is returned unchanged. +func (ns *Namespace) TrimSuffix(s, suffix interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + sx, err := cast.ToStringE(suffix) + if err != nil { + return "", err + } + + return _strings.TrimSuffix(ss, sx), nil +} diff --git a/tpl/strings/strings_test.go b/tpl/strings/strings_test.go new file mode 100644 index 000000000..ee10ac759 --- /dev/null +++ b/tpl/strings/strings_test.go @@ -0,0 +1,634 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package strings + +import ( + "fmt" + "html/template" + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ns = New(&deps.Deps{}) + +type tstNoStringer struct{} + +func TestChomp(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {"\n a\n", template.HTML("\n a")}, + {"\n a\n\n", template.HTML("\n a")}, + {"\n a\r\n", template.HTML("\n a")}, + {"\n a\n\r\n", template.HTML("\n a")}, + {"\n a\r\r", template.HTML("\n a")}, + {"\n a\r", template.HTML("\n a")}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Chomp(test.s) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestContains(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + substr interface{} + expect bool + isErr bool + }{ + {"", "", true, false}, + {"123", "23", true, false}, + {"123", "234", false, false}, + {"123", "", true, false}, + {"", "a", false, false}, + {123, "23", true, false}, + {123, "234", false, false}, + {123, "", true, false}, + {template.HTML("123"), []byte("23"), true, false}, + {template.HTML("123"), []byte("234"), false, false}, + {template.HTML("123"), []byte(""), true, false}, + // errors + {"", tstNoStringer{}, false, true}, + {tstNoStringer{}, "", false, true}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Contains(test.s, test.substr) + + if test.isErr { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestContainsAny(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + substr interface{} + expect bool + isErr bool + }{ + {"", "", false, false}, + {"", "1", false, false}, + {"", "123", false, false}, + {"1", "", false, false}, + {"1", "1", true, false}, + {"111", "1", true, false}, + {"123", "789", false, false}, + {"123", "729", true, false}, + {"a☺b☻c☹d", "uvw☻xyz", true, false}, + {1, "", false, false}, + {1, "1", true, false}, + {111, "1", true, false}, + {123, "789", false, false}, + {123, "729", true, false}, + {[]byte("123"), template.HTML("789"), false, false}, + {[]byte("123"), template.HTML("729"), true, false}, + {[]byte("a☺b☻c☹d"), template.HTML("uvw☻xyz"), true, false}, + // errors + {"", tstNoStringer{}, false, true}, + {tstNoStringer{}, "", false, true}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.ContainsAny(test.s, test.substr) + + if test.isErr { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestCountRunes(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {"foo bar", 6}, + {"旁边", 2}, + {`<div class="test">旁边</div>`, 2}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.s) + + result, err := ns.CountRunes(test.s) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestCountWords(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {"Do Be Do Be Do", 5}, + {"旁边", 2}, + {`<div class="test">旁边</div>`, 2}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.s) + + result, err := ns.CountWords(test.s) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestHasPrefix(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + prefix interface{} + expect interface{} + isErr bool + }{ + {"abcd", "ab", true, false}, + {"abcd", "cd", false, false}, + {template.HTML("abcd"), "ab", true, false}, + {template.HTML("abcd"), "cd", false, false}, + {template.HTML("1234"), 12, true, false}, + {template.HTML("1234"), 34, false, false}, + {[]byte("abcd"), "ab", true, false}, + // errors + {"", tstNoStringer{}, false, true}, + {tstNoStringer{}, "", false, true}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.HasPrefix(test.s, test.prefix) + + if test.isErr { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestHasSuffix(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + suffix interface{} + expect interface{} + isErr bool + }{ + {"abcd", "cd", true, false}, + {"abcd", "ab", false, false}, + {template.HTML("abcd"), "cd", true, false}, + {template.HTML("abcd"), "ab", false, false}, + {template.HTML("1234"), 34, true, false}, + {template.HTML("1234"), 12, false, false}, + {[]byte("abcd"), "cd", true, false}, + // errors + {"", tstNoStringer{}, false, true}, + {tstNoStringer{}, "", false, true}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.HasSuffix(test.s, test.suffix) + + if test.isErr { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestReplace(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + old interface{} + new interface{} + expect interface{} + }{ + {"aab", "a", "b", "bbb"}, + {"11a11", 1, 2, "22a22"}, + {12345, 1, 2, "22345"}, + // errors + {tstNoStringer{}, "a", "b", false}, + {"a", tstNoStringer{}, "b", false}, + {"a", "b", tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Replace(test.s, test.old, test.new) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestSliceString(t *testing.T) { + t.Parallel() + + var err error + for i, test := range []struct { + v1 interface{} + v2 interface{} + v3 interface{} + expect interface{} + }{ + {"abc", 1, 2, "b"}, + {"abc", 1, 3, "bc"}, + {"abcdef", 1, int8(3), "bc"}, + {"abcdef", 1, int16(3), "bc"}, + {"abcdef", 1, int32(3), "bc"}, + {"abcdef", 1, int64(3), "bc"}, + {"abc", 0, 1, "a"}, + {"abcdef", nil, nil, "abcdef"}, + {"abcdef", 0, 6, "abcdef"}, + {"abcdef", 0, 2, "ab"}, + {"abcdef", 2, nil, "cdef"}, + {"abcdef", int8(2), nil, "cdef"}, + {"abcdef", int16(2), nil, "cdef"}, + {"abcdef", int32(2), nil, "cdef"}, + {"abcdef", int64(2), nil, "cdef"}, + {123, 1, 3, "23"}, + {"abcdef", 6, nil, false}, + {"abcdef", 4, 7, false}, + {"abcdef", -1, nil, false}, + {"abcdef", -1, 7, false}, + {"abcdef", 1, -1, false}, + {tstNoStringer{}, 0, 1, false}, + {"ĀĀĀ", 0, 1, "Ā"}, // issue #1333 + {"a", t, nil, false}, + {"a", 1, t, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + var result string + if test.v2 == nil { + result, err = ns.SliceString(test.v1) + } else if test.v3 == nil { + result, err = ns.SliceString(test.v1, test.v2) + } else { + result, err = ns.SliceString(test.v1, test.v2, test.v3) + } + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } + + // Too many arguments + _, err = ns.SliceString("a", 1, 2, 3) + if err == nil { + t.Errorf("Should have errored") + } +} + +func TestSplit(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + v1 interface{} + v2 string + expect interface{} + }{ + {"a, b", ", ", []string{"a", "b"}}, + {"a & b & c", " & ", []string{"a", "b", "c"}}, + {"http://example.com", "http://", []string{"", "example.com"}}, + {123, "2", []string{"1", "3"}}, + {tstNoStringer{}, ",", false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Split(test.v1, test.v2) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestSubstr(t *testing.T) { + t.Parallel() + + var err error + var n int + for i, test := range []struct { + v1 interface{} + v2 interface{} + v3 interface{} + expect interface{} + }{ + {"abc", 1, 2, "bc"}, + {"abc", 0, 1, "a"}, + {"abcdef", -1, 2, "ef"}, + {"abcdef", -3, 3, "bcd"}, + {"abcdef", 0, -1, "abcde"}, + {"abcdef", 2, -1, "cde"}, + {"abcdef", 4, -4, false}, + {"abcdef", 7, 1, false}, + {"abcdef", 1, 100, "bcdef"}, + {"abcdef", -100, 3, "abc"}, + {"abcdef", -3, -1, "de"}, + {"abcdef", 2, nil, "cdef"}, + {"abcdef", int8(2), nil, "cdef"}, + {"abcdef", int16(2), nil, "cdef"}, + {"abcdef", int32(2), nil, "cdef"}, + {"abcdef", int64(2), nil, "cdef"}, + {"abcdef", 2, int8(3), "cde"}, + {"abcdef", 2, int16(3), "cde"}, + {"abcdef", 2, int32(3), "cde"}, + {"abcdef", 2, int64(3), "cde"}, + {123, 1, 3, "23"}, + {1.2e3, 0, 4, "1200"}, + {tstNoStringer{}, 0, 1, false}, + {"abcdef", 2.0, nil, "cdef"}, + {"abcdef", 2.0, 2, "cd"}, + {"abcdef", 2, 2.0, "cd"}, + {"ĀĀĀ", 1, 2, "ĀĀ"}, // # issue 1333 + {"abcdef", "doo", nil, false}, + {"abcdef", "doo", "doo", false}, + {"abcdef", 1, "doo", false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + var result string + n = i + + if test.v3 == nil { + result, err = ns.Substr(test.v1, test.v2) + } else { + result, err = ns.Substr(test.v1, test.v2, test.v3) + } + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } + + n++ + _, err = ns.Substr("abcdef") + if err == nil { + t.Errorf("[%d] Substr didn't return an expected error", n) + } + + n++ + _, err = ns.Substr("abcdef", 1, 2, 3) + if err == nil { + t.Errorf("[%d] Substr didn't return an expected error", n) + } +} + +func TestTitle(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {"test", "Test"}, + {template.HTML("hypertext"), "Hypertext"}, + {[]byte("bytes"), "Bytes"}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Title(test.s) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestToLower(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {"TEST", "test"}, + {template.HTML("LoWeR"), "lower"}, + {[]byte("BYTES"), "bytes"}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.ToLower(test.s) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestToUpper(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {"test", "TEST"}, + {template.HTML("UpPeR"), "UPPER"}, + {[]byte("bytes"), "BYTES"}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.ToUpper(test.s) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestTrim(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + cutset interface{} + expect interface{} + }{ + {"abba", "a", "bb"}, + {"abba", "ab", ""}, + {"<tag>", "<>", "tag"}, + {`"quote"`, `"`, "quote"}, + {1221, "1", "22"}, + {1221, "12", ""}, + {template.HTML("<tag>"), "<>", "tag"}, + {[]byte("<tag>"), "<>", "tag"}, + // errors + {"", tstNoStringer{}, false}, + {tstNoStringer{}, "", false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Trim(test.s, test.cutset) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestTrimPrefix(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + prefix interface{} + expect interface{} + }{ + {"aabbaa", "a", "abbaa"}, + {"aabb", "b", "aabb"}, + {1234, "12", "34"}, + {1234, "34", "1234"}, + // errors + {"", tstNoStringer{}, false}, + {tstNoStringer{}, "", false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.TrimPrefix(test.s, test.prefix) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestTrimSuffix(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + suffix interface{} + expect interface{} + }{ + {"aabbaa", "a", "aabba"}, + {"aabb", "b", "aab"}, + {1234, "12", "1234"}, + {1234, "34", "12"}, + // errors + {"", tstNoStringer{}, false}, + {tstNoStringer{}, "", false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.TrimSuffix(test.s, test.suffix) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} diff --git a/tpl/strings/truncate.go b/tpl/strings/truncate.go new file mode 100644 index 000000000..923816e9f --- /dev/null +++ b/tpl/strings/truncate.go @@ -0,0 +1,156 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package strings + +import ( + "errors" + "html" + "html/template" + "regexp" + "unicode" + "unicode/utf8" + + "github.com/spf13/cast" +) + +var ( + tagRE = regexp.MustCompile(`^<(/)?([^ ]+?)(?:(\s*/)| .*?)?>`) + htmlSinglets = map[string]bool{ + "br": true, "col": true, "link": true, + "base": true, "img": true, "param": true, + "area": true, "hr": true, "input": true, + } +) + +type htmlTag struct { + name string + pos int + openTag bool +} + +func (ns *Namespace) Truncate(a interface{}, options ...interface{}) (template.HTML, error) { + length, err := cast.ToIntE(a) + if err != nil { + return "", err + } + var textParam interface{} + var ellipsis string + + switch len(options) { + case 0: + return "", errors.New("truncate requires a length and a string") + case 1: + textParam = options[0] + ellipsis = " …" + case 2: + textParam = options[1] + ellipsis, err = cast.ToStringE(options[0]) + if err != nil { + return "", errors.New("ellipsis must be a string") + } + if _, ok := options[0].(template.HTML); !ok { + ellipsis = html.EscapeString(ellipsis) + } + default: + return "", errors.New("too many arguments passed to truncate") + } + if err != nil { + return "", errors.New("text to truncate must be a string") + } + text, err := cast.ToStringE(textParam) + if err != nil { + return "", errors.New("text must be a string") + } + + _, isHTML := textParam.(template.HTML) + + if utf8.RuneCountInString(text) <= length { + if isHTML { + return template.HTML(text), nil + } + return template.HTML(html.EscapeString(text)), nil + } + + tags := []htmlTag{} + var lastWordIndex, lastNonSpace, currentLen, endTextPos, nextTag int + + for i, r := range text { + if i < nextTag { + continue + } + + if isHTML { + // Make sure we keep tag of HTML tags + slice := text[i:] + m := tagRE.FindStringSubmatchIndex(slice) + if len(m) > 0 && m[0] == 0 { + nextTag = i + m[1] + tagname := slice[m[4]:m[5]] + lastWordIndex = lastNonSpace + _, singlet := htmlSinglets[tagname] + if !singlet && m[6] == -1 { + tags = append(tags, htmlTag{name: tagname, pos: i, openTag: m[2] == -1}) + } + + continue + } + } + + currentLen++ + if unicode.IsSpace(r) { + lastWordIndex = lastNonSpace + } else if unicode.In(r, unicode.Han, unicode.Hangul, unicode.Hiragana, unicode.Katakana) { + lastWordIndex = i + } else { + lastNonSpace = i + utf8.RuneLen(r) + } + + if currentLen > length { + if lastWordIndex == 0 { + endTextPos = i + } else { + endTextPos = lastWordIndex + } + out := text[0:endTextPos] + if isHTML { + out += ellipsis + // Close out any open HTML tags + var currentTag *htmlTag + for i := len(tags) - 1; i >= 0; i-- { + tag := tags[i] + if tag.pos >= endTextPos || currentTag != nil { + if currentTag != nil && currentTag.name == tag.name { + currentTag = nil + } + continue + } + + if tag.openTag { + out += ("</" + tag.name + ">") + } else { + currentTag = &tag + } + } + + return template.HTML(out), nil + } + return template.HTML(html.EscapeString(out) + ellipsis), nil + } + } + + if isHTML { + return template.HTML(text), nil + } + return template.HTML(html.EscapeString(text)), nil +} diff --git a/tpl/strings/truncate_test.go b/tpl/strings/truncate_test.go new file mode 100644 index 000000000..31c7028b5 --- /dev/null +++ b/tpl/strings/truncate_test.go @@ -0,0 +1,84 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package strings + +import ( + "html/template" + "reflect" + "strings" + "testing" +) + +func TestTruncate(t *testing.T) { + t.Parallel() + + var err error + cases := []struct { + v1 interface{} + v2 interface{} + v3 interface{} + want interface{} + isErr bool + }{ + {10, "I am a test sentence", nil, template.HTML("I am a …"), false}, + {10, "", "I am a test sentence", template.HTML("I am a"), false}, + {10, "", "a b c d e f g h i j k", template.HTML("a b c d e"), false}, + {12, "", "<b>Should be escaped</b>", template.HTML("<b>Should be"), false}, + {10, template.HTML(" <a href='#'>Read more</a>"), "I am a test sentence", template.HTML("I am a <a href='#'>Read more</a>"), false}, + {20, template.HTML("I have a <a href='/markdown'>Markdown link</a> inside."), nil, template.HTML("I have a <a href='/markdown'>Markdown …</a>"), false}, + {10, "IamanextremelylongwordthatjustgoesonandonandonjusttoannoyyoualmostasifIwaswritteninGermanActuallyIbettheresagermanwordforthis", nil, template.HTML("Iamanextre …"), false}, + {10, template.HTML("<p>IamanextremelylongwordthatjustgoesonandonandonjusttoannoyyoualmostasifIwaswritteninGermanActuallyIbettheresagermanwordforthis</p>"), nil, template.HTML("<p>Iamanextre …</p>"), false}, + {13, template.HTML("With <a href=\"/markdown\">Markdown</a> inside."), nil, template.HTML("With <a href=\"/markdown\">Markdown …</a>"), false}, + {14, "Hello中国 Good 好的", nil, template.HTML("Hello中国 Good 好 …"), false}, + {15, "", template.HTML("A <br> tag that's not closed"), template.HTML("A <br> tag that's"), false}, + {14, template.HTML("<p>Hello中国 Good 好的</p>"), nil, template.HTML("<p>Hello中国 Good 好 …</p>"), false}, + {2, template.HTML("<p>P1</p><p>P2</p>"), nil, template.HTML("<p>P1 …</p>"), false}, + {3, template.HTML(strings.Repeat("<p>P</p>", 20)), nil, template.HTML("<p>P</p><p>P</p><p>P …</p>"), false}, + {18, template.HTML("<p>test <b>hello</b> test something</p>"), nil, template.HTML("<p>test <b>hello</b> test …</p>"), false}, + {4, template.HTML("<p>a<b><i>b</b>c d e</p>"), nil, template.HTML("<p>a<b><i>b</b>c …</p>"), false}, + {10, nil, nil, template.HTML(""), true}, + {nil, nil, nil, template.HTML(""), true}, + } + for i, c := range cases { + var result template.HTML + if c.v2 == nil { + result, err = ns.Truncate(c.v1) + } else if c.v3 == nil { + result, err = ns.Truncate(c.v1, c.v2) + } else { + result, err = ns.Truncate(c.v1, c.v2, c.v3) + } + + if c.isErr { + if err == nil { + t.Errorf("[%d] Slice didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(result, c.want) { + t.Errorf("[%d] got '%s' but expected '%s'", i, result, c.want) + } + } + } + + // Too many arguments + _, err = ns.Truncate(10, " ...", "I am a test sentence", "wrong") + if err == nil { + t.Errorf("Should have errored") + } + +} diff --git a/tpl/template.go b/tpl/template.go new file mode 100644 index 000000000..2ee322498 --- /dev/null +++ b/tpl/template.go @@ -0,0 +1,112 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tpl + +import ( + "io" + + "text/template/parse" + + "html/template" + texttemplate "text/template" + + bp "github.com/gohugoio/hugo/bufferpool" +) + +var ( + _ TemplateExecutor = (*TemplateAdapter)(nil) +) + +// TemplateHandler manages the collection of templates. +type TemplateHandler interface { + TemplateFinder + AddTemplate(name, tpl string) error + AddLateTemplate(name, tpl string) error + LoadTemplates(absPath, prefix string) + PrintErrors() + + MarkReady() + RebuildClone() +} + +// TemplateFinder finds templates. +type TemplateFinder interface { + Lookup(name string) *TemplateAdapter +} + +// Template is the common interface between text/template and html/template. +type Template interface { + Execute(wr io.Writer, data interface{}) error + Name() string +} + +// TemplateExecutor adds some extras to Template. +type TemplateExecutor interface { + Template + ExecuteToString(data interface{}) (string, error) + Tree() string +} + +// TemplateDebugger prints some debug info to stdoud. +type TemplateDebugger interface { + Debug() +} + +// TemplateAdapter implements the TemplateExecutor interface. +type TemplateAdapter struct { + Template +} + +// ExecuteToString executes the current template and returns the result as a +// string. +func (t *TemplateAdapter) ExecuteToString(data interface{}) (string, error) { + b := bp.GetBuffer() + defer bp.PutBuffer(b) + if err := t.Execute(b, data); err != nil { + return "", err + } + return b.String(), nil +} + +// Tree returns the template Parse tree as a string. +// Note: this isn't safe for parallel execution on the same template +// vs Lookup and Execute. +func (t *TemplateAdapter) Tree() string { + var tree *parse.Tree + switch tt := t.Template.(type) { + case *template.Template: + tree = tt.Tree + case *texttemplate.Template: + tree = tt.Tree + default: + panic("Unknown template") + } + + if tree == nil || tree.Root == nil { + return "" + } + s := tree.Root.String() + + return s +} + +type TemplateFuncsGetter interface { + GetFuncs() map[string]interface{} +} + +// TemplateTestMocker adds a way to override some template funcs during tests. +// The interface is named so it's not used in regular application code. +type TemplateTestMocker interface { + SetFuncs(funcMap map[string]interface{}) +} diff --git a/tpl/time/init.go b/tpl/time/init.go new file mode 100644 index 000000000..db8294a6e --- /dev/null +++ b/tpl/time/init.go @@ -0,0 +1,73 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package time + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" +) + +const name = "time" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx := New() + + ns := &internal.TemplateFuncsNamespace{ + Name: name, + Context: func(args ...interface{}) interface{} { + // Handle overlapping "time" namespace and func. + // + // If no args are passed to `time`, assume namespace usage and + // return namespace context. + // + // If args are passed, call AsTime(). + + if len(args) == 0 { + return ctx + } + + t, err := ctx.AsTime(args[0]) + if err != nil { + return err + } + return t + }, + } + + ns.AddMethodMapping(ctx.Format, + []string{"dateFormat"}, + [][2]string{ + {`dateFormat: {{ dateFormat "Monday, Jan 2, 2006" "2015-01-21" }}`, `dateFormat: Wednesday, Jan 21, 2015`}, + }, + ) + + ns.AddMethodMapping(ctx.Now, + []string{"now"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.AsTime, + nil, + [][2]string{ + {`{{ (time "2015-01-21").Year }}`, `2015`}, + }, + ) + + return ns + + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/time/init_test.go b/tpl/time/init_test.go new file mode 100644 index 000000000..ed1091b5b --- /dev/null +++ b/tpl/time/init_test.go @@ -0,0 +1,38 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package time + +import ( + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + var found bool + var ns *internal.TemplateFuncsNamespace + + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + ns = nsf(&deps.Deps{}) + if ns.Name == name { + found = true + break + } + } + + require.True(t, found) + require.IsType(t, &Namespace{}, ns.Context()) +} diff --git a/tpl/time/time.go b/tpl/time/time.go new file mode 100644 index 000000000..889650c98 --- /dev/null +++ b/tpl/time/time.go @@ -0,0 +1,56 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package time + +import ( + _time "time" + + "github.com/spf13/cast" +) + +// New returns a new instance of the time-namespaced template functions. +func New() *Namespace { + return &Namespace{} +} + +// Namespace provides template functions for the "time" namespace. +type Namespace struct{} + +// AsTime converts the textual representation of the datetime string into +// a time.Time interface. +func (ns *Namespace) AsTime(v interface{}) (interface{}, error) { + t, err := cast.ToTimeE(v) + if err != nil { + return nil, err + } + + return t, nil +} + +// Format converts the textual representation of the datetime string into +// the other form or returns it of the time.Time value. These are formatted +// with the layout string +func (ns *Namespace) Format(layout string, v interface{}) (string, error) { + t, err := cast.ToTimeE(v) + if err != nil { + return "", err + } + + return t.Format(layout), nil +} + +// Now returns the current local time. +func (ns *Namespace) Now() _time.Time { + return _time.Now() +} diff --git a/tpl/time/time_test.go b/tpl/time/time_test.go new file mode 100644 index 000000000..2c54dacc6 --- /dev/null +++ b/tpl/time/time_test.go @@ -0,0 +1,57 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package time + +import ( + "testing" + "time" +) + +func TestFormat(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + layout string + value interface{} + expect interface{} + }{ + {"Monday, Jan 2, 2006", "2015-01-21", "Wednesday, Jan 21, 2015"}, + {"Monday, Jan 2, 2006", time.Date(2015, time.January, 21, 0, 0, 0, 0, time.UTC), "Wednesday, Jan 21, 2015"}, + {"This isn't a date layout string", "2015-01-21", "This isn't a date layout string"}, + // The following test case gives either "Tuesday, Jan 20, 2015" or "Monday, Jan 19, 2015" depending on the local time zone + {"Monday, Jan 2, 2006", 1421733600, time.Unix(1421733600, 0).Format("Monday, Jan 2, 2006")}, + {"Monday, Jan 2, 2006", 1421733600.123, false}, + {time.RFC3339, time.Date(2016, time.March, 3, 4, 5, 0, 0, time.UTC), "2016-03-03T04:05:00Z"}, + {time.RFC1123, time.Date(2016, time.March, 3, 4, 5, 0, 0, time.UTC), "Thu, 03 Mar 2016 04:05:00 UTC"}, + {time.RFC3339, "Thu, 03 Mar 2016 04:05:00 UTC", "2016-03-03T04:05:00Z"}, + {time.RFC1123, "2016-03-03T04:05:00Z", "Thu, 03 Mar 2016 04:05:00 UTC"}, + } { + result, err := ns.Format(test.layout, test.value) + if b, ok := test.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] DateFormat didn't return an expected error, got %v", i, result) + } + } else { + if err != nil { + t.Errorf("[%d] DateFormat failed: %s", i, err) + continue + } + if result != test.expect { + t.Errorf("[%d] DateFormat got %v but expected %v", i, result, test.expect) + } + } + } +} diff --git a/tpl/tplimpl/ace.go b/tpl/tplimpl/ace.go new file mode 100644 index 000000000..fc3a1e1b1 --- /dev/null +++ b/tpl/tplimpl/ace.go @@ -0,0 +1,51 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tplimpl + +import ( + "path/filepath" + + "strings" + + "github.com/yosssi/ace" +) + +func (t *templateHandler) addAceTemplate(name, basePath, innerPath string, baseContent, innerContent []byte) error { + t.checkState() + var base, inner *ace.File + name = name[:len(name)-len(filepath.Ext(innerPath))] + ".html" + + // Fixes issue #1178 + basePath = strings.Replace(basePath, "\\", "/", -1) + innerPath = strings.Replace(innerPath, "\\", "/", -1) + + if basePath != "" { + base = ace.NewFile(basePath, baseContent) + inner = ace.NewFile(innerPath, innerContent) + } else { + base = ace.NewFile(innerPath, innerContent) + inner = ace.NewFile("", []byte{}) + } + parsed, err := ace.ParseSource(ace.NewSource(base, inner, []*ace.File{}), nil) + if err != nil { + t.errors = append(t.errors, &templateErr{name: name, err: err}) + return err + } + templ, err := ace.CompileResultWithTemplate(t.html.t.New(name), parsed, nil) + if err != nil { + t.errors = append(t.errors, &templateErr{name: name, err: err}) + return err + } + return applyTemplateTransformersToHMLTTemplate(templ) +} diff --git a/tpl/tplimpl/amber_compiler.go b/tpl/tplimpl/amber_compiler.go new file mode 100644 index 000000000..10ed0443c --- /dev/null +++ b/tpl/tplimpl/amber_compiler.go @@ -0,0 +1,42 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tplimpl + +import ( + "html/template" + + "github.com/eknkc/amber" +) + +func (t *templateHandler) compileAmberWithTemplate(b []byte, path string, templ *template.Template) (*template.Template, error) { + c := amber.New() + + if err := c.ParseData(b, path); err != nil { + return nil, err + } + + data, err := c.CompileString() + + if err != nil { + return nil, err + } + + tpl, err := templ.Funcs(t.amberFuncMap).Parse(data) + + if err != nil { + return nil, err + } + + return tpl, nil +} diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go new file mode 100644 index 000000000..06ab775c3 --- /dev/null +++ b/tpl/tplimpl/template.go @@ -0,0 +1,706 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tplimpl + +import ( + "fmt" + "html/template" + "path" + "strings" + texttemplate "text/template" + + "github.com/eknkc/amber" + + "os" + + "github.com/gohugoio/hugo/output" + + "path/filepath" + "sync" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/tpl" + "github.com/spf13/afero" +) + +const ( + textTmplNamePrefix = "_text/" +) + +var ( + _ tpl.TemplateHandler = (*templateHandler)(nil) + _ tpl.TemplateDebugger = (*templateHandler)(nil) + _ tpl.TemplateFuncsGetter = (*templateHandler)(nil) + _ tpl.TemplateTestMocker = (*templateHandler)(nil) + _ tpl.TemplateFinder = (*htmlTemplates)(nil) + _ tpl.TemplateFinder = (*textTemplates)(nil) + _ templateLoader = (*htmlTemplates)(nil) + _ templateLoader = (*textTemplates)(nil) + _ templateLoader = (*templateHandler)(nil) + _ templateFuncsterTemplater = (*htmlTemplates)(nil) + _ templateFuncsterTemplater = (*textTemplates)(nil) +) + +// Protecting global map access (Amber) +var amberMu sync.Mutex + +type templateErr struct { + name string + err error +} + +type templateLoader interface { + handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error + addTemplate(name, tpl string) error + addLateTemplate(name, tpl string) error +} + +type templateFuncsterTemplater interface { + tpl.TemplateFinder + setFuncs(funcMap map[string]interface{}) + setTemplateFuncster(f *templateFuncster) +} + +// templateHandler holds the templates in play. +// It implements the templateLoader and tpl.TemplateHandler interfaces. +type templateHandler struct { + // text holds all the pure text templates. + text *textTemplates + html *htmlTemplates + + amberFuncMap template.FuncMap + + errors []*templateErr + + *deps.Deps +} + +func (t *templateHandler) addError(name string, err error) { + t.errors = append(t.errors, &templateErr{name, err}) +} + +func (t *templateHandler) Debug() { + fmt.Println("HTML templates:\n", t.html.t.DefinedTemplates()) + fmt.Println("\n\nText templates:\n", t.text.t.DefinedTemplates()) +} + +// PrintErrors prints the accumulated errors as ERROR to the log. +func (t *templateHandler) PrintErrors() { + for _, e := range t.errors { + t.Log.ERROR.Println(e.name, ":", e.err) + } +} + +// Lookup tries to find a template with the given name in both template +// collections: First HTML, then the plain text template collection. +func (t *templateHandler) Lookup(name string) *tpl.TemplateAdapter { + + if strings.HasPrefix(name, textTmplNamePrefix) { + // The caller has explicitly asked for a text template, so only look + // in the text template collection. + // The templates are stored without the prefix identificator. + name = strings.TrimPrefix(name, textTmplNamePrefix) + return t.text.Lookup(name) + } + + // Look in both + if te := t.html.Lookup(name); te != nil { + return te + } + + return t.text.Lookup(name) +} + +func (t *templateHandler) clone(d *deps.Deps) *templateHandler { + c := &templateHandler{ + Deps: d, + html: &htmlTemplates{t: template.Must(t.html.t.Clone()), overlays: make(map[string]*template.Template)}, + text: &textTemplates{t: texttemplate.Must(t.text.t.Clone()), overlays: make(map[string]*texttemplate.Template)}, + errors: make([]*templateErr, 0), + } + + d.Tmpl = c + + c.initFuncs() + + for k, v := range t.html.overlays { + vc := template.Must(v.Clone()) + // The extra lookup is a workaround, see + // * https://github.com/golang/go/issues/16101 + // * https://github.com/gohugoio/hugo/issues/2549 + vc = vc.Lookup(vc.Name()) + vc.Funcs(c.html.funcster.funcMap) + c.html.overlays[k] = vc + } + + for k, v := range t.text.overlays { + vc := texttemplate.Must(v.Clone()) + vc = vc.Lookup(vc.Name()) + vc.Funcs(texttemplate.FuncMap(c.text.funcster.funcMap)) + c.text.overlays[k] = vc + } + + return c + +} + +func newTemplateAdapter(deps *deps.Deps) *templateHandler { + htmlT := &htmlTemplates{ + t: template.New(""), + overlays: make(map[string]*template.Template), + } + textT := &textTemplates{ + t: texttemplate.New(""), + overlays: make(map[string]*texttemplate.Template), + } + return &templateHandler{ + Deps: deps, + html: htmlT, + text: textT, + errors: make([]*templateErr, 0), + } + +} + +type htmlTemplates struct { + funcster *templateFuncster + + t *template.Template + + // This looks, and is, strange. + // The clone is used by non-renderable content pages, and these need to be + // re-parsed on content change, and to avoid the + // "cannot Parse after Execute" error, we need to re-clone it from the original clone. + clone *template.Template + cloneClone *template.Template + + // a separate storage for the overlays created from cloned master templates. + // note: No mutex protection, so we add these in one Go routine, then just read. + overlays map[string]*template.Template +} + +func (t *htmlTemplates) setTemplateFuncster(f *templateFuncster) { + t.funcster = f +} + +func (t *htmlTemplates) Lookup(name string) *tpl.TemplateAdapter { + templ := t.lookup(name) + if templ == nil { + return nil + } + return &tpl.TemplateAdapter{Template: templ} +} + +func (t *htmlTemplates) lookup(name string) *template.Template { + if templ := t.t.Lookup(name); templ != nil { + return templ + } + if t.overlays != nil { + if templ, ok := t.overlays[name]; ok { + return templ + } + } + + if t.clone != nil { + return t.clone.Lookup(name) + } + + return nil +} + +type textTemplates struct { + funcster *templateFuncster + + t *texttemplate.Template + + clone *texttemplate.Template + cloneClone *texttemplate.Template + + overlays map[string]*texttemplate.Template +} + +func (t *textTemplates) setTemplateFuncster(f *templateFuncster) { + t.funcster = f +} + +func (t *textTemplates) Lookup(name string) *tpl.TemplateAdapter { + templ := t.lookup(name) + if templ == nil { + return nil + } + return &tpl.TemplateAdapter{Template: templ} +} + +func (t *textTemplates) lookup(name string) *texttemplate.Template { + if templ := t.t.Lookup(name); templ != nil { + return templ + } + if t.overlays != nil { + if templ, ok := t.overlays[name]; ok { + return templ + } + } + + if t.clone != nil { + return t.clone.Lookup(name) + } + + return nil +} + +func (t *templateHandler) setFuncs(funcMap map[string]interface{}) { + t.html.setFuncs(funcMap) + t.text.setFuncs(funcMap) +} + +// SetFuncs replaces the funcs in the func maps with new definitions. +// This is only used in tests. +func (t *templateHandler) SetFuncs(funcMap map[string]interface{}) { + t.setFuncs(funcMap) +} + +func (t *templateHandler) GetFuncs() map[string]interface{} { + return t.html.funcster.funcMap +} + +func (t *htmlTemplates) setFuncs(funcMap map[string]interface{}) { + t.t.Funcs(funcMap) +} + +func (t *textTemplates) setFuncs(funcMap map[string]interface{}) { + t.t.Funcs(funcMap) +} + +// LoadTemplates loads the templates, starting from the given absolute path. +// A prefix can be given to indicate a template namespace to load the templates +// into, i.e. "_internal" etc. +func (t *templateHandler) LoadTemplates(absPath, prefix string) { + t.loadTemplates(absPath, prefix) + +} + +func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, tpl string) error { + templ, err := tt.New(name).Parse(tpl) + if err != nil { + return err + } + + if err := applyTemplateTransformersToHMLTTemplate(templ); err != nil { + return err + } + + if strings.Contains(name, "shortcodes") { + // We need to keep track of one ot the output format's shortcode template + // without knowing the rendering context. + withoutExt := strings.TrimSuffix(name, path.Ext(name)) + tt.AddParseTree(withoutExt, templ.Tree) + } + + return nil +} + +func (t *htmlTemplates) addTemplate(name, tpl string) error { + return t.addTemplateIn(t.t, name, tpl) +} + +func (t *htmlTemplates) addLateTemplate(name, tpl string) error { + return t.addTemplateIn(t.clone, name, tpl) +} + +func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tpl string) error { + name = strings.TrimPrefix(name, textTmplNamePrefix) + templ, err := tt.New(name).Parse(tpl) + if err != nil { + return err + } + + if err := applyTemplateTransformersToTextTemplate(templ); err != nil { + return err + } + + if strings.Contains(name, "shortcodes") { + // We need to keep track of one ot the output format's shortcode template + // without knowing the rendering context. + withoutExt := strings.TrimSuffix(name, path.Ext(name)) + tt.AddParseTree(withoutExt, templ.Tree) + } + + return nil +} + +func (t *textTemplates) addTemplate(name, tpl string) error { + return t.addTemplateIn(t.t, name, tpl) +} + +func (t *textTemplates) addLateTemplate(name, tpl string) error { + return t.addTemplateIn(t.clone, name, tpl) +} + +func (t *templateHandler) addTemplate(name, tpl string) error { + return t.AddTemplate(name, tpl) +} + +func (t *templateHandler) addLateTemplate(name, tpl string) error { + return t.AddLateTemplate(name, tpl) +} + +// AddLateTemplate is used to add a template late, i.e. after the +// regular templates have started its execution. +func (t *templateHandler) AddLateTemplate(name, tpl string) error { + h := t.getTemplateHandler(name) + if err := h.addLateTemplate(name, tpl); err != nil { + t.addError(name, err) + return err + } + return nil +} + +// AddTemplate parses and adds a template to the collection. +// Templates with name prefixed with "_text" will be handled as plain +// text templates. +func (t *templateHandler) AddTemplate(name, tpl string) error { + h := t.getTemplateHandler(name) + if err := h.addTemplate(name, tpl); err != nil { + t.addError(name, err) + return err + } + return nil +} + +// MarkReady marks the templates as "ready for execution". No changes allowed +// after this is set. +// TODO(bep) if this proves to be resource heavy, we could detect +// earlier if we really need this, or make it lazy. +func (t *templateHandler) MarkReady() { + if t.html.clone == nil { + t.html.clone = template.Must(t.html.t.Clone()) + t.html.cloneClone = template.Must(t.html.clone.Clone()) + } + if t.text.clone == nil { + t.text.clone = texttemplate.Must(t.text.t.Clone()) + t.text.cloneClone = texttemplate.Must(t.text.clone.Clone()) + } +} + +// RebuildClone rebuilds the cloned templates. Used for live-reloads. +func (t *templateHandler) RebuildClone() { + t.html.clone = template.Must(t.html.cloneClone.Clone()) + t.text.clone = texttemplate.Must(t.text.cloneClone.Clone()) +} + +func (t *templateHandler) loadTemplates(absPath string, prefix string) { + t.Log.DEBUG.Printf("Load templates from path %q prefix %q", absPath, prefix) + walker := func(path string, fi os.FileInfo, err error) error { + if err != nil { + return nil + } + + t.Log.DEBUG.Println("Template path", path) + if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + link, err := filepath.EvalSymlinks(absPath) + if err != nil { + t.Log.ERROR.Printf("Cannot read symbolic link '%s', error was: %s", absPath, err) + return nil + } + + linkfi, err := t.Fs.Source.Stat(link) + if err != nil { + t.Log.ERROR.Printf("Cannot stat '%s', error was: %s", link, err) + return nil + } + + if !linkfi.Mode().IsRegular() { + t.Log.ERROR.Printf("Symbolic links for directories not supported, skipping '%s'", absPath) + } + return nil + } + + if !fi.IsDir() { + if isDotFile(path) || isBackupFile(path) || isBaseTemplate(path) { + return nil + } + + var ( + workingDir = t.PathSpec.WorkingDir() + themeDir = t.PathSpec.GetThemeDir() + layoutDir = t.PathSpec.LayoutDir() + ) + + if themeDir != "" && strings.HasPrefix(absPath, themeDir) { + workingDir = themeDir + layoutDir = "layouts" + } + + li := strings.LastIndex(path, layoutDir) + len(layoutDir) + 1 + relPath := path[li:] + templateDir := path[:li-len(layoutDir)-1] + + descriptor := output.TemplateLookupDescriptor{ + TemplateDir: templateDir, + WorkingDir: workingDir, + LayoutDir: layoutDir, + RelPath: relPath, + Prefix: prefix, + ThemeDir: themeDir, + OutputFormats: t.OutputFormatsConfig, + FileExists: func(filename string) (bool, error) { + return helpers.Exists(filename, t.Fs.Source) + }, + ContainsAny: func(filename string, subslices [][]byte) (bool, error) { + return helpers.FileContainsAny(filename, subslices, t.Fs.Source) + }, + } + + tplID, err := output.CreateTemplateNames(descriptor) + if err != nil { + t.Log.ERROR.Printf("Failed to resolve template in path %q: %s", path, err) + + return nil + } + + if err := t.addTemplateFile(tplID.Name, tplID.MasterFilename, tplID.OverlayFilename); err != nil { + t.Log.ERROR.Printf("Failed to add template %q in path %q: %s", tplID.Name, path, err) + } + + } + return nil + } + if err := helpers.SymbolicWalk(t.Fs.Source, absPath, walker); err != nil { + t.Log.ERROR.Printf("Failed to load templates: %s", err) + } +} + +func (t *templateHandler) initFuncs() { + + // Both template types will get their own funcster instance, which + // in the current case contains the same set of funcs. + for _, funcsterHolder := range []templateFuncsterTemplater{t.html, t.text} { + funcster := newTemplateFuncster(t.Deps) + + // The URL funcs in the funcMap is somewhat language dependent, + // so we need to wait until the language and site config is loaded. + funcster.initFuncMap() + + funcsterHolder.setTemplateFuncster(funcster) + + } + + // Amber is HTML only. + t.amberFuncMap = template.FuncMap{} + + amberMu.Lock() + for k, v := range amber.FuncMap { + t.amberFuncMap[k] = v + } + + for k, v := range t.html.funcster.funcMap { + t.amberFuncMap[k] = v + // Hacky, but we need to make sure that the func names are in the global map. + amber.FuncMap[k] = func() string { + panic("should never be invoked") + } + } + amberMu.Unlock() + +} + +func (t *templateHandler) getTemplateHandler(name string) templateLoader { + if strings.HasPrefix(name, textTmplNamePrefix) { + return t.text + } + return t.html +} + +func (t *templateHandler) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error { + h := t.getTemplateHandler(name) + return h.handleMaster(name, overlayFilename, masterFilename, onMissing) +} + +func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error { + masterTpl := t.lookup(masterFilename) + + if masterTpl == nil { + templ, err := onMissing(masterFilename) + if err != nil { + return err + } + + masterTpl, err = t.t.New(overlayFilename).Parse(templ) + if err != nil { + return err + } + } + + templ, err := onMissing(overlayFilename) + if err != nil { + return err + } + + overlayTpl, err := template.Must(masterTpl.Clone()).Parse(templ) + if err != nil { + return err + } + + // The extra lookup is a workaround, see + // * https://github.com/golang/go/issues/16101 + // * https://github.com/gohugoio/hugo/issues/2549 + overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) + if err := applyTemplateTransformersToHMLTTemplate(overlayTpl); err != nil { + return err + } + t.overlays[name] = overlayTpl + + return err + +} + +func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error { + name = strings.TrimPrefix(name, textTmplNamePrefix) + masterTpl := t.lookup(masterFilename) + + if masterTpl == nil { + templ, err := onMissing(masterFilename) + if err != nil { + return err + } + + masterTpl, err = t.t.New(overlayFilename).Parse(templ) + if err != nil { + return err + } + } + + templ, err := onMissing(overlayFilename) + if err != nil { + return err + } + + overlayTpl, err := texttemplate.Must(masterTpl.Clone()).Parse(templ) + if err != nil { + return err + } + + overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) + if err := applyTemplateTransformersToTextTemplate(overlayTpl); err != nil { + return err + } + t.overlays[name] = overlayTpl + + return err + +} + +func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) error { + t.checkState() + + getTemplate := func(filename string) (string, error) { + b, err := afero.ReadFile(t.Fs.Source, filename) + if err != nil { + return "", err + } + return string(b), nil + } + + // get the suffix and switch on that + ext := filepath.Ext(path) + switch ext { + case ".amber": + // Only HTML support for Amber + templateName := strings.TrimSuffix(name, filepath.Ext(name)) + ".html" + b, err := afero.ReadFile(t.Fs.Source, path) + + if err != nil { + return err + } + + amberMu.Lock() + templ, err := t.compileAmberWithTemplate(b, path, t.html.t.New(templateName)) + amberMu.Unlock() + if err != nil { + return err + } + + return applyTemplateTransformersToHMLTTemplate(templ) + case ".ace": + // Only HTML support for Ace + var innerContent, baseContent []byte + innerContent, err := afero.ReadFile(t.Fs.Source, path) + + if err != nil { + return err + } + + if baseTemplatePath != "" { + baseContent, err = afero.ReadFile(t.Fs.Source, baseTemplatePath) + if err != nil { + return err + } + } + + return t.addAceTemplate(name, baseTemplatePath, path, baseContent, innerContent) + default: + + if baseTemplatePath != "" { + return t.handleMaster(name, path, baseTemplatePath, getTemplate) + } + + templ, err := getTemplate(path) + + if err != nil { + return err + } + + t.Log.DEBUG.Printf("Add template file from path %s", path) + + return t.AddTemplate(name, templ) + } + +} + +func (t *templateHandler) loadEmbedded() { + t.embedShortcodes() + t.embedTemplates() +} + +func (t *templateHandler) addInternalTemplate(prefix, name, tpl string) error { + if prefix != "" { + return t.AddTemplate("_internal/"+prefix+"/"+name, tpl) + } + return t.AddTemplate("_internal/"+name, tpl) +} + +func (t *templateHandler) addInternalShortcode(name, content string) error { + return t.addInternalTemplate("shortcodes", name, content) +} + +func (t *templateHandler) checkState() { + if t.html.clone != nil || t.text.clone != nil { + panic("template is cloned and cannot be modfified") + } +} + +func isDotFile(path string) bool { + return filepath.Base(path)[0] == '.' +} + +func isBackupFile(path string) bool { + return path[len(path)-1] == '~' +} + +const baseFileBase = "baseof" + +func isBaseTemplate(path string) bool { + return strings.Contains(path, baseFileBase) +} diff --git a/tpl/tplimpl/templateFuncster.go b/tpl/tplimpl/templateFuncster.go new file mode 100644 index 000000000..e6bbde8ec --- /dev/null +++ b/tpl/tplimpl/templateFuncster.go @@ -0,0 +1,77 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tplimpl + +import ( + "fmt" + "html/template" + "strings" + texttemplate "text/template" + + bp "github.com/gohugoio/hugo/bufferpool" + "github.com/gohugoio/hugo/deps" +) + +// Some of the template funcs are'nt entirely stateless. +type templateFuncster struct { + funcMap template.FuncMap + + *deps.Deps +} + +func newTemplateFuncster(deps *deps.Deps) *templateFuncster { + return &templateFuncster{ + Deps: deps, + } +} + +// Partial executes the named partial and returns either a string, +// when called from text/template, for or a template.HTML. +func (t *templateFuncster) partial(name string, contextList ...interface{}) (interface{}, error) { + if strings.HasPrefix("partials/", name) { + name = name[8:] + } + var context interface{} + + if len(contextList) == 0 { + context = nil + } else { + context = contextList[0] + } + + for _, n := range []string{"partials/" + name, "theme/partials/" + name} { + templ := t.Tmpl.Lookup(n) + if templ == nil { + // For legacy reasons. + templ = t.Tmpl.Lookup(n + ".html") + } + if templ != nil { + b := bp.GetBuffer() + defer bp.PutBuffer(b) + + if err := templ.Execute(b, context); err != nil { + return "", err + } + + if _, ok := templ.Template.(*texttemplate.Template); ok { + return b.String(), nil + } + + return template.HTML(b.String()), nil + + } + } + + return "", fmt.Errorf("Partial %q not found", name) +} diff --git a/tpl/tplimpl/templateProvider.go b/tpl/tplimpl/templateProvider.go new file mode 100644 index 000000000..5d5ee44b6 --- /dev/null +++ b/tpl/tplimpl/templateProvider.go @@ -0,0 +1,59 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tplimpl + +import ( + "github.com/gohugoio/hugo/deps" +) + +type TemplateProvider struct{} + +var DefaultTemplateProvider *TemplateProvider + +// Update updates the Hugo Template System in the provided Deps. +// with all the additional features, templates & functions +func (*TemplateProvider) Update(deps *deps.Deps) error { + + newTmpl := newTemplateAdapter(deps) + deps.Tmpl = newTmpl + + newTmpl.initFuncs() + newTmpl.loadEmbedded() + + if deps.WithTemplate != nil { + err := deps.WithTemplate(newTmpl) + if err != nil { + newTmpl.addError("init", err) + } + + } + + newTmpl.MarkReady() + + return nil + +} + +// Clone clones. +func (*TemplateProvider) Clone(d *deps.Deps) error { + + t := d.Tmpl.(*templateHandler) + clone := t.clone(d) + + d.Tmpl = clone + + clone.MarkReady() + + return nil +} diff --git a/tpl/tplimpl/template_ast_transformers.go b/tpl/tplimpl/template_ast_transformers.go new file mode 100644 index 000000000..bbd0f28a4 --- /dev/null +++ b/tpl/tplimpl/template_ast_transformers.go @@ -0,0 +1,293 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tplimpl + +import ( + "errors" + "html/template" + "strings" + texttemplate "text/template" + "text/template/parse" +) + +// decl keeps track of the variable mappings, i.e. $mysite => .Site etc. +type decl map[string]string + +var paramsPaths = [][]string{ + {"Params"}, + {"Site", "Params"}, + + // Site and Pag referenced from shortcodes + {"Page", "Site", "Params"}, + {"Page", "Params"}, + + {"Site", "Language", "Params"}, +} + +type templateContext struct { + decl decl + visited map[string]bool + lookupFn func(name string) *parse.Tree +} + +func (c templateContext) getIfNotVisited(name string) *parse.Tree { + if c.visited[name] { + return nil + } + c.visited[name] = true + return c.lookupFn(name) +} + +func newTemplateContext(lookupFn func(name string) *parse.Tree) *templateContext { + return &templateContext{lookupFn: lookupFn, decl: make(map[string]string), visited: make(map[string]bool)} + +} + +func createParseTreeLookup(templ *template.Template) func(nn string) *parse.Tree { + return func(nn string) *parse.Tree { + tt := templ.Lookup(nn) + if tt != nil { + return tt.Tree + } + return nil + } +} + +func applyTemplateTransformersToHMLTTemplate(templ *template.Template) error { + return applyTemplateTransformers(templ.Tree, createParseTreeLookup(templ)) +} + +func applyTemplateTransformersToTextTemplate(templ *texttemplate.Template) error { + return applyTemplateTransformers(templ.Tree, + func(nn string) *parse.Tree { + tt := templ.Lookup(nn) + if tt != nil { + return tt.Tree + } + return nil + }) +} + +func applyTemplateTransformers(templ *parse.Tree, lookupFn func(name string) *parse.Tree) error { + if templ == nil { + return errors.New("expected template, but none provided") + } + + c := newTemplateContext(lookupFn) + + c.paramsKeysToLower(templ.Root) + + return nil +} + +// paramsKeysToLower is made purposely non-generic to make it not so tempting +// to do more of these hard-to-maintain AST transformations. +func (c *templateContext) paramsKeysToLower(n parse.Node) { + switch x := n.(type) { + case *parse.ListNode: + if x != nil { + c.paramsKeysToLowerForNodes(x.Nodes...) + } + case *parse.ActionNode: + c.paramsKeysToLowerForNodes(x.Pipe) + case *parse.IfNode: + c.paramsKeysToLowerForNodes(x.Pipe, x.List, x.ElseList) + case *parse.WithNode: + c.paramsKeysToLowerForNodes(x.Pipe, x.List, x.ElseList) + case *parse.RangeNode: + c.paramsKeysToLowerForNodes(x.Pipe, x.List, x.ElseList) + case *parse.TemplateNode: + subTempl := c.getIfNotVisited(x.Name) + if subTempl != nil { + c.paramsKeysToLowerForNodes(subTempl.Root) + } + case *parse.PipeNode: + for i, elem := range x.Decl { + if len(x.Cmds) > i { + // maps $site => .Site etc. + c.decl[elem.Ident[0]] = x.Cmds[i].String() + } + } + + for _, cmd := range x.Cmds { + c.paramsKeysToLower(cmd) + } + + case *parse.CommandNode: + for _, elem := range x.Args { + switch an := elem.(type) { + case *parse.FieldNode: + c.updateIdentsIfNeeded(an.Ident) + case *parse.VariableNode: + c.updateIdentsIfNeeded(an.Ident) + case *parse.PipeNode: + c.paramsKeysToLower(an) + } + + } + } +} + +func (c *templateContext) paramsKeysToLowerForNodes(nodes ...parse.Node) { + for _, node := range nodes { + c.paramsKeysToLower(node) + } +} + +func (c *templateContext) updateIdentsIfNeeded(idents []string) { + index := c.decl.indexOfReplacementStart(idents) + + if index == -1 { + return + } + + for i := index; i < len(idents); i++ { + idents[i] = strings.ToLower(idents[i]) + } +} + +// indexOfReplacementStart will return the index of where to start doing replacement, +// -1 if none needed. +func (d decl) indexOfReplacementStart(idents []string) int { + + l := len(idents) + + if l == 0 { + return -1 + } + + first := idents[0] + firstIsVar := first[0] == '$' + + if l == 1 && !firstIsVar { + // This can not be a Params.x + return -1 + } + + if !firstIsVar { + found := false + for _, paramsPath := range paramsPaths { + if first == paramsPath[0] { + found = true + break + } + } + if !found { + return -1 + } + } + + var ( + resolvedIdents []string + replacements []string + replaced []string + ) + + // An Ident can start out as one of + // [Params] [$blue] [$colors.Blue] + // We need to resolve the variables, so + // $blue => [Params Colors Blue] + // etc. + replacements = []string{idents[0]} + + // Loop until there are no more $vars to resolve. + for i := 0; i < len(replacements); i++ { + + if i > 20 { + // bail out + return -1 + } + + potentialVar := replacements[i] + + if potentialVar == "$" { + continue + } + + if potentialVar == "" || potentialVar[0] != '$' { + // leave it as is + replaced = append(replaced, strings.Split(potentialVar, ".")...) + continue + } + + replacement, ok := d[potentialVar] + + if !ok { + // Temporary range vars. We do not care about those. + return -1 + } + + replacement = strings.TrimPrefix(replacement, ".") + + if replacement == "" { + continue + } + + if replacement[0] == '$' { + // Needs further expansion + replacements = append(replacements, strings.Split(replacement, ".")...) + } else { + replaced = append(replaced, strings.Split(replacement, ".")...) + } + } + + resolvedIdents = append(replaced, idents[1:]...) + + for _, paramPath := range paramsPaths { + if index := indexOfFirstRealIdentAfterWords(resolvedIdents, idents, paramPath...); index != -1 { + return index + } + } + + return -1 + +} + +func indexOfFirstRealIdentAfterWords(resolvedIdents, idents []string, words ...string) int { + if !sliceStartsWith(resolvedIdents, words...) { + return -1 + } + + for i, ident := range idents { + if ident == "" || ident[0] == '$' { + continue + } + found := true + for _, word := range words { + if ident == word { + found = false + break + } + } + if found { + return i + } + } + + return -1 +} + +func sliceStartsWith(slice []string, words ...string) bool { + + if len(slice) < len(words) { + return false + } + + for i, word := range words { + if word != slice[i] { + return false + } + } + return true +} diff --git a/tpl/tplimpl/template_ast_transformers_test.go b/tpl/tplimpl/template_ast_transformers_test.go new file mode 100644 index 000000000..c3cf54940 --- /dev/null +++ b/tpl/tplimpl/template_ast_transformers_test.go @@ -0,0 +1,290 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package tplimpl + +import ( + "bytes" + "testing" + + "html/template" + + "github.com/stretchr/testify/require" +) + +var ( + testFuncs = map[string]interface{}{ + "Echo": func(v interface{}) interface{} { return v }, + } + + paramsData = map[string]interface{}{ + "NotParam": "Hi There", + "Slice": []int{1, 3}, + "Params": map[string]interface{}{ + "lower": "P1L", + }, + "Site": map[string]interface{}{ + "Params": map[string]interface{}{ + "lower": "P2L", + "slice": []int{1, 3}, + }, + "Language": map[string]interface{}{ + "Params": map[string]interface{}{ + "lower": "P22L", + }, + }, + "Data": map[string]interface{}{ + "Params": map[string]interface{}{ + "NOLOW": "P3H", + }, + }, + }, + } + + paramsTempl = ` +{{ $page := . }} +{{ $pageParams := .Params }} +{{ $site := .Site }} +{{ $siteParams := .Site.Params }} +{{ $data := .Site.Data }} +{{ $notparam := .NotParam }} + +P1: {{ .Params.LOWER }} +P1_2: {{ $.Params.LOWER }} +P1_3: {{ $page.Params.LOWER }} +P1_4: {{ $pageParams.LOWER }} +P2: {{ .Site.Params.LOWER }} +P2_2: {{ $.Site.Params.LOWER }} +P2_3: {{ $site.Params.LOWER }} +P2_4: {{ $siteParams.LOWER }} +P22: {{ .Site.Language.Params.LOWER }} +P3: {{ .Site.Data.Params.NOLOW }} +P3_2: {{ $.Site.Data.Params.NOLOW }} +P3_3: {{ $site.Data.Params.NOLOW }} +P3_4: {{ $data.Params.NOLOW }} +P4: {{ range $i, $e := .Site.Params.SLICE }}{{ $e }}{{ end }} +P5: {{ Echo .Params.LOWER }} +P5_2: {{ Echo $site.Params.LOWER }} +{{ if .Params.LOWER }} +IF: {{ .Params.LOWER }} +{{ end }} +{{ if .Params.NOT_EXIST }} +{{ else }} +ELSE: {{ .Params.LOWER }} +{{ end }} + + +{{ with .Params.LOWER }} +WITH: {{ . }} +{{ end }} + + +{{ range .Slice }} +RANGE: {{ . }}: {{ $.Params.LOWER }} +{{ end }} +{{ index .Slice 1 }} +{{ .NotParam }} +{{ .NotParam }} +{{ .NotParam }} +{{ .NotParam }} +{{ .NotParam }} +{{ .NotParam }} +{{ .NotParam }} +{{ .NotParam }} +{{ .NotParam }} +{{ .NotParam }} +{{ $notparam }} + + +{{ $lower := .Site.Params.LOWER }} +F1: {{ printf "themes/%s-theme" .Site.Params.LOWER }} +F2: {{ Echo (printf "themes/%s-theme" $lower) }} +F3: {{ Echo (printf "themes/%s-theme" .Site.Params.LOWER) }} +` +) + +func TestParamsKeysToLower(t *testing.T) { + t.Parallel() + + require.Error(t, applyTemplateTransformers(nil, nil)) + + templ, err := template.New("foo").Funcs(testFuncs).Parse(paramsTempl) + + require.NoError(t, err) + + c := newTemplateContext(createParseTreeLookup(templ)) + + require.Equal(t, -1, c.decl.indexOfReplacementStart([]string{})) + + c.paramsKeysToLower(templ.Tree.Root) + + var b bytes.Buffer + + require.NoError(t, templ.Execute(&b, paramsData)) + + result := b.String() + + require.Contains(t, result, "P1: P1L") + require.Contains(t, result, "P1_2: P1L") + require.Contains(t, result, "P1_3: P1L") + require.Contains(t, result, "P1_4: P1L") + require.Contains(t, result, "P2: P2L") + require.Contains(t, result, "P2_2: P2L") + require.Contains(t, result, "P2_3: P2L") + require.Contains(t, result, "P2_4: P2L") + require.Contains(t, result, "P22: P22L") + require.Contains(t, result, "P3: P3H") + require.Contains(t, result, "P3_2: P3H") + require.Contains(t, result, "P3_3: P3H") + require.Contains(t, result, "P3_4: P3H") + require.Contains(t, result, "P4: 13") + require.Contains(t, result, "P5: P1L") + require.Contains(t, result, "P5_2: P2L") + + require.Contains(t, result, "IF: P1L") + require.Contains(t, result, "ELSE: P1L") + + require.Contains(t, result, "WITH: P1L") + + require.Contains(t, result, "RANGE: 3: P1L") + + require.Contains(t, result, "Hi There") + + // Issue #2740 + require.Contains(t, result, "F1: themes/P2L-theme") + require.Contains(t, result, "F2: themes/P2L-theme") + require.Contains(t, result, "F3: themes/P2L-theme") + +} + +func BenchmarkTemplateParamsKeysToLower(b *testing.B) { + templ, err := template.New("foo").Funcs(testFuncs).Parse(paramsTempl) + + if err != nil { + b.Fatal(err) + } + + templates := make([]*template.Template, b.N) + + for i := 0; i < b.N; i++ { + templates[i], err = templ.Clone() + if err != nil { + b.Fatal(err) + } + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + c := newTemplateContext(createParseTreeLookup(templates[i])) + c.paramsKeysToLower(templ.Tree.Root) + } +} + +func TestParamsKeysToLowerVars(t *testing.T) { + t.Parallel() + var ( + ctx = map[string]interface{}{ + "Params": map[string]interface{}{ + "colors": map[string]interface{}{ + "blue": "Amber", + }, + }, + } + + // This is how Amber behaves: + paramsTempl = ` +{{$__amber_1 := .Params.Colors}} +{{$__amber_2 := $__amber_1.Blue}} +Color: {{$__amber_2}} +Blue: {{ $__amber_1.Blue}} +` + ) + + templ, err := template.New("foo").Parse(paramsTempl) + + require.NoError(t, err) + + c := newTemplateContext(createParseTreeLookup(templ)) + + c.paramsKeysToLower(templ.Tree.Root) + + var b bytes.Buffer + + require.NoError(t, templ.Execute(&b, ctx)) + + result := b.String() + + require.Contains(t, result, "Color: Amber") + +} + +func TestParamsKeysToLowerInBlockTemplate(t *testing.T) { + t.Parallel() + + var ( + ctx = map[string]interface{}{ + "Params": map[string]interface{}{ + "lower": "P1L", + }, + } + + master = ` +P1: {{ .Params.LOWER }} +{{ block "main" . }}DEFAULT{{ end }}` + overlay = ` +{{ define "main" }} +P2: {{ .Params.LOWER }} +{{ end }}` + ) + + masterTpl, err := template.New("foo").Parse(master) + require.NoError(t, err) + + overlayTpl, err := template.Must(masterTpl.Clone()).Parse(overlay) + require.NoError(t, err) + overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) + + c := newTemplateContext(createParseTreeLookup(overlayTpl)) + + c.paramsKeysToLower(overlayTpl.Tree.Root) + + var b bytes.Buffer + + require.NoError(t, overlayTpl.Execute(&b, ctx)) + + result := b.String() + + require.Contains(t, result, "P1: P1L") + require.Contains(t, result, "P2: P1L") +} + +// Issue #2927 +func TestTransformRecursiveTemplate(t *testing.T) { + + recursive := ` +{{ define "menu-nodes" }} +{{ template "menu-node" }} +{{ end }} +{{ define "menu-node" }} +{{ template "menu-node" }} +{{ end }} +{{ template "menu-nodes" }} +` + + templ, err := template.New("foo").Parse(recursive) + require.NoError(t, err) + + c := newTemplateContext(createParseTreeLookup(templ)) + c.paramsKeysToLower(templ.Tree.Root) + +} diff --git a/tpl/tplimpl/template_embedded.go b/tpl/tplimpl/template_embedded.go new file mode 100644 index 000000000..fb5ce71e9 --- /dev/null +++ b/tpl/tplimpl/template_embedded.go @@ -0,0 +1,289 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tplimpl + +func (t *templateHandler) embedShortcodes() { + t.addInternalShortcode("ref.html", `{{ if len .Params | eq 2 }}{{ ref .Page (.Get 0) (.Get 1) }}{{ else }}{{ ref .Page (.Get 0) }}{{ end }}`) + t.addInternalShortcode("relref.html", `{{ if len .Params | eq 2 }}{{ relref .Page (.Get 0) (.Get 1) }}{{ else }}{{ relref .Page (.Get 0) }}{{ end }}`) + t.addInternalShortcode("highlight.html", `{{ if len .Params | eq 2 }}{{ highlight .Inner (.Get 0) (.Get 1) }}{{ else }}{{ highlight .Inner (.Get 0) "" }}{{ end }}`) + t.addInternalShortcode("test.html", `This is a simple Test`) + t.addInternalShortcode("figure.html", `<!-- image --> +<figure {{ with .Get "class" }}class="{{.}}"{{ end }}> + {{ with .Get "link"}}<a href="{{.}}">{{ end }} + <img src="{{ .Get "src" }}" {{ if or (.Get "alt") (.Get "caption") }}alt="{{ with .Get "alt"}}{{.}}{{else}}{{ .Get "caption" }}{{ end }}" {{ end }}{{ with .Get "width" }}width="{{.}}" {{ end }}/> + {{ if .Get "link"}}</a>{{ end }} + {{ if or (or (.Get "title") (.Get "caption")) (.Get "attr")}} + <figcaption>{{ if isset .Params "title" }} + <h4>{{ .Get "title" }}</h4>{{ end }} + {{ if or (.Get "caption") (.Get "attr")}}<p> + {{ .Get "caption" }} + {{ with .Get "attrlink"}}<a href="{{.}}"> {{ end }} + {{ .Get "attr" }} + {{ if .Get "attrlink"}}</a> {{ end }} + </p> {{ end }} + </figcaption> + {{ end }} +</figure> +<!-- image -->`) + t.addInternalShortcode("speakerdeck.html", "<script async class='speakerdeck-embed' data-id='{{ index .Params 0 }}' data-ratio='1.33333333333333' src='//speakerdeck.com/assets/embed.js'></script>") + t.addInternalShortcode("youtube.html", `{{ if .IsNamedParams }} +<div {{ if .Get "class" }}class="{{ .Get "class" }}"{{ else }}style="position: relative; padding-bottom: 56.25%; padding-top: 30px; height: 0; overflow: hidden;"{{ end }}> + <iframe src="//www.youtube.com/embed/{{ .Get "id" }}?{{ with .Get "autoplay" }}{{ if eq . "true" }}autoplay=1{{ end }}{{ end }}" + {{ if not (.Get "class") }}style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" {{ end }}allowfullscreen frameborder="0"></iframe> +</div>{{ else }} +<div {{ if len .Params | eq 2 }}class="{{ .Get 1 }}"{{ else }}style="position: relative; padding-bottom: 56.25%; padding-top: 30px; height: 0; overflow: hidden;"{{ end }}> + <iframe src="//www.youtube.com/embed/{{ .Get 0 }}" {{ if len .Params | eq 1 }}style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" {{ end }}allowfullscreen frameborder="0"></iframe> + </div> +{{ end }}`) + t.addInternalShortcode("vimeo.html", `{{ if .IsNamedParams }}<div {{ if .Get "class" }}class="{{ .Get "class" }}"{{ else }}style="position: relative; padding-bottom: 56.25%; padding-top: 30px; height: 0; overflow: hidden;"{{ end }}> + <iframe src="//player.vimeo.com/video/{{ .Get "id" }}" {{ if not (.Get "class") }}style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" {{ end }}webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe> + </div>{{ else }} +<div {{ if len .Params | eq 2 }}class="{{ .Get 1 }}"{{ else }}style="position: relative; padding-bottom: 56.25%; padding-top: 30px; height: 0; overflow: hidden;"{{ end }}> + <iframe src="//player.vimeo.com/video/{{ .Get 0 }}" {{ if len .Params | eq 1 }}style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" {{ end }}webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe> + </div> +{{ end }}`) + t.addInternalShortcode("gist.html", `<script src="//gist.github.com/{{ index .Params 0 }}/{{ index .Params 1 }}.js{{if len .Params | eq 3 }}?file={{ index .Params 2 }}{{end}}"></script>`) + t.addInternalShortcode("tweet.html", `{{ (getJSON "https://api.twitter.com/1/statuses/oembed.json?id=" (index .Params 0)).html | safeHTML }}`) + t.addInternalShortcode("instagram.html", `{{ if len .Params | eq 2 }}{{ if eq (.Get 1) "hidecaption" }}{{ with getJSON "https://api.instagram.com/oembed/?url=https://instagram.com/p/" (index .Params 0) "/&hidecaption=1" }}{{ .html | safeHTML }}{{ end }}{{ end }}{{ else }}{{ with getJSON "https://api.instagram.com/oembed/?url=https://instagram.com/p/" (index .Params 0) "/&hidecaption=0" }}{{ .html | safeHTML }}{{ end }}{{ end }}`) +} + +func (t *templateHandler) embedTemplates() { + + t.addInternalTemplate("_default", "rss.xml", `<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> + <channel> + <title>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}</title> + <link>{{ .Permalink }}</link> + <description>Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }}</description> + <generator>Hugo -- gohugo.io</generator>{{ with .Site.LanguageCode }} + <language>{{.}}</language>{{end}}{{ with .Site.Author.email }} + <managingEditor>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</managingEditor>{{end}}{{ with .Site.Author.email }} + <webMaster>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Copyright }} + <copyright>{{.}}</copyright>{{end}}{{ if not .Date.IsZero }} + <lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }} + {{ with .OutputFormats.Get "RSS" }} + {{ printf "<atom:link href=%q rel=\"self\" type=%q />" .Permalink .MediaType | safeHTML }} + {{ end }} + {{ range .Data.Pages }} + <item> + <title>{{ .Title }}</title> + <link>{{ .Permalink }}</link> + <pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate> + {{ with .Site.Author.email }}<author>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</author>{{end}} + <guid>{{ .Permalink }}</guid> + <description>{{ .Summary | html }}</description> + </item> + {{ end }} + </channel> +</rss>`) + + t.addInternalTemplate("_default", "sitemap.xml", `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" + xmlns:xhtml="http://www.w3.org/1999/xhtml"> + {{ range .Data.Pages }} + <url> + <loc>{{ .Permalink }}</loc>{{ if not .Lastmod.IsZero }} + <lastmod>{{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}</lastmod>{{ end }}{{ with .Sitemap.ChangeFreq }} + <changefreq>{{ . }}</changefreq>{{ end }}{{ if ge .Sitemap.Priority 0.0 }} + <priority>{{ .Sitemap.Priority }}</priority>{{ end }}{{ if .IsTranslated }}{{ range .Translations }} + <xhtml:link + rel="alternate" + hreflang="{{ .Lang }}" + href="{{ .Permalink }}" + />{{ end }} + <xhtml:link + rel="alternate" + hreflang="{{ .Lang }}" + href="{{ .Permalink }}" + />{{ end }} + </url> + {{ end }} +</urlset>`) + + // For multilanguage sites + t.addInternalTemplate("_default", "sitemapindex.xml", `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> + {{ range . }} + <sitemap> + <loc>{{ .SitemapAbsURL }}</loc> + {{ if not .LastChange.IsZero }} + <lastmod>{{ .LastChange.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</lastmod> + {{ end }} + </sitemap> + {{ end }} +</sitemapindex> +`) + + t.addInternalTemplate("", "pagination.html", `{{ $pag := $.Paginator }} +{{ if gt $pag.TotalPages 1 }} +<ul class="pagination"> + {{ with $pag.First }} + <li> + <a href="{{ .URL }}" aria-label="First"><span aria-hidden="true">««</span></a> + </li> + {{ end }} + <li + {{ if not $pag.HasPrev }}class="disabled"{{ end }}> + <a href="{{ if $pag.HasPrev }}{{ $pag.Prev.URL }}{{ end }}" aria-label="Previous"><span aria-hidden="true">«</span></a> + </li> + {{ $.Scratch.Set "__paginator.ellipsed" false }} + {{ range $pag.Pagers }} + {{ $right := sub .TotalPages .PageNumber }} + {{ $showNumber := or (le .PageNumber 3) (eq $right 0) }} + {{ $showNumber := or $showNumber (and (gt .PageNumber (sub $pag.PageNumber 2)) (lt .PageNumber (add $pag.PageNumber 2))) }} + {{ if $showNumber }} + {{ $.Scratch.Set "__paginator.ellipsed" false }} + {{ $.Scratch.Set "__paginator.shouldEllipse" false }} + {{ else }} + {{ $.Scratch.Set "__paginator.shouldEllipse" (not ($.Scratch.Get "__paginator.ellipsed") ) }} + {{ $.Scratch.Set "__paginator.ellipsed" true }} + {{ end }} + {{ if $showNumber }} + <li + {{ if eq . $pag }}class="active"{{ end }}><a href="{{ .URL }}">{{ .PageNumber }}</a></li> + {{ else if ($.Scratch.Get "__paginator.shouldEllipse") }} + <li class="disabled"><span aria-hidden="true">…</span></li> + {{ end }} + {{ end }} + <li + {{ if not $pag.HasNext }}class="disabled"{{ end }}> + <a href="{{ if $pag.HasNext }}{{ $pag.Next.URL }}{{ end }}" aria-label="Next"><span aria-hidden="true">»</span></a> + </li> + {{ with $pag.Last }} + <li> + <a href="{{ .URL }}" aria-label="Last"><span aria-hidden="true">»»</span></a> + </li> + {{ end }} +</ul> +{{ end }}`) + + t.addInternalTemplate("", "disqus.html", `{{ if .Site.DisqusShortname }}<div id="disqus_thread"></div> +<script type="text/javascript"> + var disqus_shortname = '{{ .Site.DisqusShortname }}'; + var disqus_identifier = '{{with .GetParam "disqus_identifier" }}{{ . }}{{ else }}{{ .Permalink }}{{end}}'; + var disqus_title = '{{with .GetParam "disqus_title" }}{{ . }}{{ else }}{{ .Title }}{{end}}'; + var disqus_url = '{{with .GetParam "disqus_url" }}{{ . | html }}{{ else }}{{ .Permalink }}{{end}}'; + + (function() { + var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true; + dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js'; + (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq); + })(); +</script> +<noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript> +<a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>{{end}}`) + + // Add SEO & Social metadata + t.addInternalTemplate("", "opengraph.html", `<meta property="og:title" content="{{ .Title }}" /> +<meta property="og:description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}" /> +<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}" /> +<meta property="og:url" content="{{ .Permalink }}" /> +{{ with .Params.images }}{{ range first 6 . }} + <meta property="og:image" content="{{ . | absURL }}" /> +{{ end }}{{ end }} + +{{ if .IsPage }} +{{ if not .PublishDate.IsZero }}<meta property="article:published_time" content="{{ .PublishDate.Format "2006-01-02T15:04:05-07:00" | safeHTML }}"/> +{{ else if not .Date.IsZero }}<meta property="article:published_time" content="{{ .Date.Format "2006-01-02T15:04:05-07:00" | safeHTML }}"/>{{ end }} +{{ if not .Lastmod.IsZero }}<meta property="article:modified_time" content="{{ .Lastmod.Format "2006-01-02T15:04:05-07:00" | safeHTML }}"/>{{ end }} +{{ else }} +{{ if not .Date.IsZero }}<meta property="og:updated_time" content="{{ .Date.Format "2006-01-02T15:04:05-07:00" | safeHTML }}"/>{{ end }} +{{ end }}{{ with .Params.audio }} +<meta property="og:audio" content="{{ . }}" />{{ end }}{{ with .Params.locale }} +<meta property="og:locale" content="{{ . }}" />{{ end }}{{ with .Site.Params.title }} +<meta property="og:site_name" content="{{ . }}" />{{ end }}{{ with .Params.videos }} +{{ range .Params.videos }} + <meta property="og:video" content="{{ . | absURL }}" /> +{{ end }}{{ end }} + +<!-- If it is part of a series, link to related articles --> +{{ $permalink := .Permalink }} +{{ $siteSeries := .Site.Taxonomies.series }}{{ with .Params.series }} +{{ range $name := . }} + {{ $series := index $siteSeries $name }} + {{ range $page := first 6 $series.Pages }} + {{ if ne $page.Permalink $permalink }}<meta property="og:see_also" content="{{ $page.Permalink }}" />{{ end }} + {{ end }} +{{ end }}{{ end }} + +{{ if .IsPage }} +{{ range .Site.Authors }}{{ with .Social.facebook }} +<meta property="article:author" content="https://www.facebook.com/{{ . }}" />{{ end }}{{ with .Site.Social.facebook }} +<meta property="article:publisher" content="https://www.facebook.com/{{ . }}" />{{ end }} +<meta property="article:section" content="{{ .Section }}" /> +{{ with .Params.tags }}{{ range first 6 . }} + <meta property="article:tag" content="{{ . }}" />{{ end }}{{ end }} +{{ end }}{{ end }} + +<!-- Facebook Page Admin ID for Domain Insights --> +{{ with .Site.Social.facebook_admin }}<meta property="fb:admins" content="{{ . }}" />{{ end }}`) + + t.addInternalTemplate("", "twitter_cards.html", `{{ if .IsPage }} +{{ with .Params.images }} +<!-- Twitter summary card with large image must be at least 280x150px --> + <meta name="twitter:card" content="summary_large_image"/> + <meta name="twitter:image:src" content="{{ index . 0 | absURL }}"/> +{{ else }} + <meta name="twitter:card" content="summary"/> +{{ end }} + +<!-- Twitter Card data --> +<meta name="twitter:text:title" content="{{ .Title }}"/> +<meta name="twitter:title" content="{{ .Title }}"/> +<meta name="twitter:description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}"/> +{{ with .Site.Social.twitter }}<meta name="twitter:site" content="@{{ . }}"/>{{ end }} +{{ range .Site.Authors }} + {{ with .twitter }}<meta name="twitter:creator" content="@{{ . }}"/>{{ end }} +{{ end }}{{ end }}`) + + t.addInternalTemplate("", "google_news.html", `{{ if .IsPage }}{{ with .Params.news_keywords }} + <meta name="news_keywords" content="{{ range $i, $kw := first 10 . }}{{ if $i }},{{ end }}{{ $kw }}{{ end }}" /> +{{ end }}{{ end }}`) + + t.addInternalTemplate("", "schema.html", `{{ with .Site.Social.GooglePlus }}<link rel="publisher" href="{{ . }}"/>{{ end }} +<meta itemprop="name" content="{{ .Title }}"> +<meta itemprop="description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}"> + +{{if .IsPage}}{{ $ISO8601 := "2006-01-02T15:04:05-07:00" }}{{ if not .PublishDate.IsZero }} +<meta itemprop="datePublished" content="{{ .PublishDate.Format $ISO8601 | safeHTML }}" />{{ end }} +{{ if not .Date.IsZero }}<meta itemprop="dateModified" content="{{ .Date.Format $ISO8601 | safeHTML }}" />{{ end }} +<meta itemprop="wordCount" content="{{ .WordCount }}"> +{{ with .Params.images }}{{ range first 6 . }} + <meta itemprop="image" content="{{ . | absURL }}"> +{{ end }}{{ end }} + +<!-- Output all taxonomies as schema.org keywords --> +<meta itemprop="keywords" content="{{ range $plural, $terms := .Site.Taxonomies }}{{ range $term, $val := $terms }}{{ printf "%s," $term }}{{ end }}{{ end }}" /> +{{ end }}`) + + t.addInternalTemplate("", "google_analytics.html", `{{ with .Site.GoogleAnalytics }} +<script> +(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ +(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), +m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) +})(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); + +ga('create', '{{ . }}', 'auto'); +ga('send', 'pageview'); +</script> +{{ end }}`) + + t.addInternalTemplate("", "google_analytics_async.html", `{{ with .Site.GoogleAnalytics }} +<script> +window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date; +ga('create', '{{ . }}', 'auto'); +ga('send', 'pageview'); +</script> +<script async src='//www.google-analytics.com/analytics.js'></script> +{{ end }}`) + + t.addInternalTemplate("_default", "robots.txt", "User-agent: *") +} diff --git a/tpl/tplimpl/template_funcs.go b/tpl/tplimpl/template_funcs.go new file mode 100644 index 000000000..309a7b85f --- /dev/null +++ b/tpl/tplimpl/template_funcs.go @@ -0,0 +1,67 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Portions Copyright The Go Authors. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tplimpl + +import ( + "html/template" + + "github.com/gohugoio/hugo/tpl/internal" + + // Init the namespaces + _ "github.com/gohugoio/hugo/tpl/cast" + _ "github.com/gohugoio/hugo/tpl/collections" + _ "github.com/gohugoio/hugo/tpl/compare" + _ "github.com/gohugoio/hugo/tpl/crypto" + _ "github.com/gohugoio/hugo/tpl/data" + _ "github.com/gohugoio/hugo/tpl/encoding" + _ "github.com/gohugoio/hugo/tpl/fmt" + _ "github.com/gohugoio/hugo/tpl/images" + _ "github.com/gohugoio/hugo/tpl/inflect" + _ "github.com/gohugoio/hugo/tpl/lang" + _ "github.com/gohugoio/hugo/tpl/math" + _ "github.com/gohugoio/hugo/tpl/os" + _ "github.com/gohugoio/hugo/tpl/partials" + _ "github.com/gohugoio/hugo/tpl/safe" + _ "github.com/gohugoio/hugo/tpl/strings" + _ "github.com/gohugoio/hugo/tpl/time" + _ "github.com/gohugoio/hugo/tpl/transform" + _ "github.com/gohugoio/hugo/tpl/urls" +) + +func (t *templateFuncster) initFuncMap() { + funcMap := template.FuncMap{} + + // Merge the namespace funcs + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + ns := nsf(t.Deps) + if _, exists := funcMap[ns.Name]; exists { + panic(ns.Name + " is a duplicate template func") + } + funcMap[ns.Name] = ns.Context + for _, mm := range ns.MethodMappings { + for _, alias := range mm.Aliases { + if _, exists := funcMap[alias]; exists { + panic(alias + " is a duplicate template func") + } + funcMap[alias] = mm.Method + } + + } + } + + t.funcMap = funcMap + t.Tmpl.(*templateHandler).setFuncs(funcMap) +} diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go new file mode 100644 index 000000000..04f4464ec --- /dev/null +++ b/tpl/tplimpl/template_funcs_test.go @@ -0,0 +1,266 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tplimpl + +import ( + "bytes" + "fmt" + "path/filepath" + "reflect" + "testing" + + "io/ioutil" + "log" + "os" + + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/i18n" + "github.com/gohugoio/hugo/tpl" + "github.com/gohugoio/hugo/tpl/internal" + "github.com/spf13/afero" + jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" +) + +var ( + logger = jww.NewNotepad(jww.LevelFatal, jww.LevelFatal, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) +) + +func newDepsConfig(cfg config.Provider) deps.DepsCfg { + l := helpers.NewLanguage("en", cfg) + l.Set("i18nDir", "i18n") + return deps.DepsCfg{ + Language: l, + Cfg: cfg, + Fs: hugofs.NewMem(l), + Logger: logger, + TemplateProvider: DefaultTemplateProvider, + TranslationProvider: i18n.NewTranslationProvider(), + } +} + +func TestTemplateFuncsExamples(t *testing.T) { + t.Parallel() + + workingDir := "/home/hugo" + + v := viper.New() + + v.Set("workingDir", workingDir) + v.Set("multilingual", true) + v.Set("baseURL", "http://mysite.com/hugo/") + v.Set("CurrentContentLanguage", helpers.NewLanguage("en", v)) + + fs := hugofs.NewMem(v) + + afero.WriteFile(fs.Source, filepath.Join(workingDir, "README.txt"), []byte("Hugo Rocks!"), 0755) + + depsCfg := newDepsConfig(v) + depsCfg.Fs = fs + d, err := deps.New(depsCfg) + require.NoError(t, err) + + var data struct { + Title string + Section string + Params map[string]interface{} + } + + data.Title = "**BatMan**" + data.Section = "blog" + data.Params = map[string]interface{}{"langCode": "en"} + + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + ns := nsf(d) + for _, mm := range ns.MethodMappings { + for i, example := range mm.Examples { + in, expected := example[0], example[1] + d.WithTemplate = func(templ tpl.TemplateHandler) error { + require.NoError(t, templ.AddTemplate("test", in)) + require.NoError(t, templ.AddTemplate("partials/header.html", "<title>Hugo Rocks!</title>")) + return nil + } + require.NoError(t, d.LoadResources()) + + var b bytes.Buffer + require.NoError(t, d.Tmpl.Lookup("test").Execute(&b, &data)) + if b.String() != expected { + t.Fatalf("%s[%d]: got %q expected %q", ns.Name, i, b.String(), expected) + } + } + } + } + +} + +// TODO(bep) it would be dandy to put this one into the partials package, but +// we have some package cycle issues to solve first. +func TestPartialCached(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + partial string + tmpl string + variant string + }{ + // name and partial should match between test cases. + {"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . }}`, ""}, + {"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . "%s" }}`, "header"}, + {"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . "%s" }}`, "footer"}, + {"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . "%s" }}`, "header"}, + } + + var data struct { + Title string + Section string + Params map[string]interface{} + } + + data.Title = "**BatMan**" + data.Section = "blog" + data.Params = map[string]interface{}{"langCode": "en"} + + for i, tc := range testCases { + var tmp string + if tc.variant != "" { + tmp = fmt.Sprintf(tc.tmpl, tc.variant) + } else { + tmp = tc.tmpl + } + + config := newDepsConfig(viper.New()) + + config.WithTemplate = func(templ tpl.TemplateHandler) error { + err := templ.AddTemplate("testroot", tmp) + if err != nil { + return err + } + err = templ.AddTemplate("partials/"+tc.name, tc.partial) + if err != nil { + return err + } + + return nil + } + + de, err := deps.New(config) + require.NoError(t, err) + require.NoError(t, de.LoadResources()) + + buf := new(bytes.Buffer) + templ := de.Tmpl.Lookup("testroot") + err = templ.Execute(buf, &data) + if err != nil { + t.Fatalf("[%d] error executing template: %s", i, err) + } + + for j := 0; j < 10; j++ { + buf2 := new(bytes.Buffer) + err := templ.Execute(buf2, nil) + if err != nil { + t.Fatalf("[%d] error executing template 2nd time: %s", i, err) + } + + if !reflect.DeepEqual(buf, buf2) { + t.Fatalf("[%d] cached results do not match:\nResult 1:\n%q\nResult 2:\n%q", i, buf, buf2) + } + } + } +} + +func BenchmarkPartial(b *testing.B) { + config := newDepsConfig(viper.New()) + config.WithTemplate = func(templ tpl.TemplateHandler) error { + err := templ.AddTemplate("testroot", `{{ partial "bench1" . }}`) + if err != nil { + return err + } + err = templ.AddTemplate("partials/bench1", `{{ shuffle (seq 1 10) }}`) + if err != nil { + return err + } + + return nil + } + + de, err := deps.New(config) + require.NoError(b, err) + require.NoError(b, de.LoadResources()) + + buf := new(bytes.Buffer) + tmpl := de.Tmpl.Lookup("testroot") + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := tmpl.Execute(buf, nil); err != nil { + b.Fatalf("error executing template: %s", err) + } + buf.Reset() + } +} + +func BenchmarkPartialCached(b *testing.B) { + config := newDepsConfig(viper.New()) + config.WithTemplate = func(templ tpl.TemplateHandler) error { + err := templ.AddTemplate("testroot", `{{ partialCached "bench1" . }}`) + if err != nil { + return err + } + err = templ.AddTemplate("partials/bench1", `{{ shuffle (seq 1 10) }}`) + if err != nil { + return err + } + + return nil + } + + de, err := deps.New(config) + require.NoError(b, err) + require.NoError(b, de.LoadResources()) + + buf := new(bytes.Buffer) + tmpl := de.Tmpl.Lookup("testroot") + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := tmpl.Execute(buf, nil); err != nil { + b.Fatalf("error executing template: %s", err) + } + buf.Reset() + } +} + +func newTestFuncster() *templateFuncster { + return newTestFuncsterWithViper(viper.New()) +} + +func newTestFuncsterWithViper(v *viper.Viper) *templateFuncster { + config := newDepsConfig(v) + d, err := deps.New(config) + if err != nil { + panic(err) + } + + if err := d.LoadResources(); err != nil { + panic(err) + } + + return d.Tmpl.(*templateHandler).html.funcster +} diff --git a/tpl/transform/init.go b/tpl/transform/init.go new file mode 100644 index 000000000..c0e9b2d5d --- /dev/null +++ b/tpl/transform/init.go @@ -0,0 +1,96 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transform + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" +) + +const name = "transform" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx := New(d) + + ns := &internal.TemplateFuncsNamespace{ + Name: name, + Context: func(args ...interface{}) interface{} { return ctx }, + } + + ns.AddMethodMapping(ctx.Emojify, + []string{"emojify"}, + [][2]string{ + {`{{ "I :heart: Hugo" | emojify }}`, `I ❤️ Hugo`}, + }, + ) + + ns.AddMethodMapping(ctx.Highlight, + []string{"highlight"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.HTMLEscape, + []string{"htmlEscape"}, + [][2]string{ + { + `{{ htmlEscape "Cathal Garvey & The Sunshine Band <[email protected]>" | safeHTML}}`, + `Cathal Garvey & The Sunshine Band <[email protected]>`}, + { + `{{ htmlEscape "Cathal Garvey & The Sunshine Band <[email protected]>"}}`, + `Cathal Garvey &amp; The Sunshine Band &lt;[email protected]&gt;`}, + { + `{{ htmlEscape "Cathal Garvey & The Sunshine Band <[email protected]>" | htmlUnescape | safeHTML }}`, + `Cathal Garvey & The Sunshine Band <[email protected]>`}, + }, + ) + + ns.AddMethodMapping(ctx.HTMLUnescape, + []string{"htmlUnescape"}, + [][2]string{ + { + `{{ htmlUnescape "Cathal Garvey & The Sunshine Band <[email protected]>" | safeHTML}}`, + `Cathal Garvey & The Sunshine Band <[email protected]>`}, + { + `{{"Cathal Garvey &amp; The Sunshine Band &lt;[email protected]&gt;" | htmlUnescape | htmlUnescape | safeHTML}}`, + `Cathal Garvey & The Sunshine Band <[email protected]>`}, + { + `{{"Cathal Garvey &amp; The Sunshine Band &lt;[email protected]&gt;" | htmlUnescape | htmlUnescape }}`, + `Cathal Garvey & The Sunshine Band <[email protected]>`}, + { + `{{ htmlUnescape "Cathal Garvey & The Sunshine Band <[email protected]>" | htmlEscape | safeHTML }}`, + `Cathal Garvey & The Sunshine Band <[email protected]>`}, + }, + ) + + ns.AddMethodMapping(ctx.Markdownify, + []string{"markdownify"}, + [][2]string{ + {`{{ .Title | markdownify}}`, `<strong>BatMan</strong>`}, + }, + ) + + ns.AddMethodMapping(ctx.Plainify, + []string{"plainify"}, + [][2]string{ + {`{{ plainify "Hello <strong>world</strong>, gophers!" }}`, `Hello world, gophers!`}, + }, + ) + + return ns + + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/transform/init_test.go b/tpl/transform/init_test.go new file mode 100644 index 000000000..8ac20366c --- /dev/null +++ b/tpl/transform/init_test.go @@ -0,0 +1,38 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transform + +import ( + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + var found bool + var ns *internal.TemplateFuncsNamespace + + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + ns = nsf(&deps.Deps{}) + if ns.Name == name { + found = true + break + } + } + + require.True(t, found) + require.IsType(t, &Namespace{}, ns.Context()) +} diff --git a/tpl/transform/transform.go b/tpl/transform/transform.go new file mode 100644 index 000000000..b41b41b9c --- /dev/null +++ b/tpl/transform/transform.go @@ -0,0 +1,114 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transform + +import ( + "bytes" + "html" + "html/template" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" + "github.com/spf13/cast" +) + +// New returns a new instance of the transform-namespaced template functions. +func New(deps *deps.Deps) *Namespace { + return &Namespace{ + deps: deps, + } +} + +// Namespace provides template functions for the "transform" namespace. +type Namespace struct { + deps *deps.Deps +} + +// Emojify returns a copy of s with all emoji codes replaced with actual emojis. +// +// See http://www.emoji-cheat-sheet.com/ +func (ns *Namespace) Emojify(s interface{}) (template.HTML, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return template.HTML(helpers.Emojify([]byte(ss))), nil +} + +// Highlight returns a copy of s as an HTML string with syntax +// highlighting applied. +func (ns *Namespace) Highlight(s interface{}, lang, opts string) (template.HTML, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return template.HTML(helpers.Highlight(ns.deps.Cfg, html.UnescapeString(ss), lang, opts)), nil +} + +// HTMLEscape returns a copy of s with reserved HTML characters escaped. +func (ns *Namespace) HTMLEscape(s interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return html.EscapeString(ss), nil +} + +// HTMLUnescape returns a copy of with HTML escape requences converted to plain +// text. +func (ns *Namespace) HTMLUnescape(s interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return html.UnescapeString(ss), nil +} + +var markdownTrimPrefix = []byte("<p>") +var markdownTrimSuffix = []byte("</p>\n") + +// Markdownify renders a given input from Markdown to HTML. +func (ns *Namespace) Markdownify(s interface{}) (template.HTML, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + m := ns.deps.ContentSpec.RenderBytes( + &helpers.RenderingContext{ + Cfg: ns.deps.Cfg, + Content: []byte(ss), + PageFmt: "markdown", + Config: ns.deps.ContentSpec.NewBlackfriday(), + }, + ) + m = bytes.TrimPrefix(m, markdownTrimPrefix) + m = bytes.TrimSuffix(m, markdownTrimSuffix) + + return template.HTML(m), nil +} + +// Plainify returns a copy of s with all HTML tags removed. +func (ns *Namespace) Plainify(s interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return helpers.StripHTML(ss), nil +} diff --git a/tpl/transform/transform_test.go b/tpl/transform/transform_test.go new file mode 100644 index 000000000..b50d7530c --- /dev/null +++ b/tpl/transform/transform_test.go @@ -0,0 +1,206 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transform + +import ( + "fmt" + "html/template" + "testing" + + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type tstNoStringer struct{} + +func TestEmojify(t *testing.T) { + t.Parallel() + + ns := New(newDeps(viper.New())) + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {":notamoji:", template.HTML(":notamoji:")}, + {"I :heart: Hugo", template.HTML("I ❤️ Hugo")}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %s", i, test.s) + + result, err := ns.Emojify(test.s) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestHighlight(t *testing.T) { + t.Parallel() + + ns := New(newDeps(viper.New())) + + for i, test := range []struct { + s interface{} + lang string + opts string + expect interface{} + }{ + {"func boo() {}", "go", "", "boo"}, + {tstNoStringer{}, "go", "", false}, + } { + errMsg := fmt.Sprintf("[%d]", i) + + result, err := ns.Highlight(test.s, test.lang, test.opts) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Contains(t, result, "boo", errMsg) + } +} + +func TestHTMLEscape(t *testing.T) { + t.Parallel() + + ns := New(newDeps(viper.New())) + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {`"Foo & Bar's Diner" <y@z>`, `"Foo & Bar's Diner" <y@z>`}, + {"Hugo & Caddy > Wordpress & Apache", "Hugo & Caddy > Wordpress & Apache"}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %s", i, test.s) + + result, err := ns.HTMLEscape(test.s) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestHTMLUnescape(t *testing.T) { + t.Parallel() + + ns := New(newDeps(viper.New())) + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {`"Foo & Bar's Diner" <y@z>`, `"Foo & Bar's Diner" <y@z>`}, + {"Hugo & Caddy > Wordpress & Apache", "Hugo & Caddy > Wordpress & Apache"}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %s", i, test.s) + + result, err := ns.HTMLUnescape(test.s) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestMarkdownify(t *testing.T) { + t.Parallel() + + ns := New(newDeps(viper.New())) + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {"Hello **World!**", template.HTML("Hello <strong>World!</strong>")}, + {[]byte("Hello Bytes **World!**"), template.HTML("Hello Bytes <strong>World!</strong>")}, + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %s", i, test.s) + + result, err := ns.Markdownify(test.s) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestPlainify(t *testing.T) { + t.Parallel() + + ns := New(newDeps(viper.New())) + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {"<em>Note:</em> blah <b>blah</b>", "Note: blah blah"}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %s", i, test.s) + + result, err := ns.Plainify(test.s) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func newDeps(cfg config.Provider) *deps.Deps { + l := helpers.NewLanguage("en", cfg) + l.Set("i18nDir", "i18n") + return &deps.Deps{ + Cfg: cfg, + Fs: hugofs.NewMem(l), + ContentSpec: helpers.NewContentSpec(l), + } +} diff --git a/tpl/urls/init.go b/tpl/urls/init.go new file mode 100644 index 000000000..c48fe8ca1 --- /dev/null +++ b/tpl/urls/init.go @@ -0,0 +1,67 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package urls + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" +) + +const name = "urls" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx := New(d) + + ns := &internal.TemplateFuncsNamespace{ + Name: name, + Context: func(args ...interface{}) interface{} { return ctx }, + } + + ns.AddMethodMapping(ctx.AbsURL, + []string{"absURL"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.AbsLangURL, + []string{"absLangURL"}, + [][2]string{}, + ) + ns.AddMethodMapping(ctx.Ref, + []string{"ref"}, + [][2]string{}, + ) + ns.AddMethodMapping(ctx.RelURL, + []string{"relURL"}, + [][2]string{}, + ) + ns.AddMethodMapping(ctx.RelLangURL, + []string{"relLangURL"}, + [][2]string{}, + ) + ns.AddMethodMapping(ctx.RelRef, + []string{"relref"}, + [][2]string{}, + ) + ns.AddMethodMapping(ctx.URLize, + []string{"urlize"}, + [][2]string{}, + ) + + return ns + + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/urls/init_test.go b/tpl/urls/init_test.go new file mode 100644 index 000000000..6630f13d3 --- /dev/null +++ b/tpl/urls/init_test.go @@ -0,0 +1,38 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package urls + +import ( + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + var found bool + var ns *internal.TemplateFuncsNamespace + + for _, nsf := range internal.TemplateFuncsNamespaceRegistry { + ns = nsf(&deps.Deps{}) + if ns.Name == name { + found = true + break + } + } + + require.True(t, found) + require.IsType(t, &Namespace{}, ns.Context()) +} diff --git a/tpl/urls/urls.go b/tpl/urls/urls.go new file mode 100644 index 000000000..e29cabe89 --- /dev/null +++ b/tpl/urls/urls.go @@ -0,0 +1,111 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package urls + +import ( + "errors" + "html/template" + + "github.com/gohugoio/hugo/deps" + "github.com/spf13/cast" +) + +// New returns a new instance of the urls-namespaced template functions. +func New(deps *deps.Deps) *Namespace { + return &Namespace{ + deps: deps, + } +} + +// Namespace provides template functions for the "urls" namespace. +type Namespace struct { + deps *deps.Deps +} + +// AbsURL takes a given string and converts it to an absolute URL. +func (ns *Namespace) AbsURL(a interface{}) (template.HTML, error) { + s, err := cast.ToStringE(a) + if err != nil { + return "", nil + } + + return template.HTML(ns.deps.PathSpec.AbsURL(s, false)), nil +} + +// RelURL takes a given string and prepends the relative path according to a +// page's position in the project directory structure. +func (ns *Namespace) RelURL(a interface{}) (template.HTML, error) { + s, err := cast.ToStringE(a) + if err != nil { + return "", nil + } + + return template.HTML(ns.deps.PathSpec.RelURL(s, false)), nil +} + +func (ns *Namespace) URLize(a interface{}) (string, error) { + s, err := cast.ToStringE(a) + if err != nil { + return "", nil + } + return ns.deps.PathSpec.URLize(s), nil +} + +type reflinker interface { + Ref(refs ...string) (string, error) + RelRef(refs ...string) (string, error) +} + +// Ref returns the absolute URL path to a given content item. +func (ns *Namespace) Ref(in interface{}, refs ...string) (template.HTML, error) { + p, ok := in.(reflinker) + if !ok { + return "", errors.New("invalid Page received in Ref") + } + s, err := p.Ref(refs...) + return template.HTML(s), err +} + +// RelRef returns the relative URL path to a given content item. +func (ns *Namespace) RelRef(in interface{}, refs ...string) (template.HTML, error) { + p, ok := in.(reflinker) + if !ok { + return "", errors.New("invalid Page received in RelRef") + } + s, err := p.RelRef(refs...) + return template.HTML(s), err +} + +// RelLangURL takes a given string and prepends the relative path according to a +// page's position in the project directory structure and the current language. +func (ns *Namespace) RelLangURL(a interface{}) (template.HTML, error) { + s, err := cast.ToStringE(a) + if err != nil { + return "", err + } + + return template.HTML(ns.deps.PathSpec.RelURL(s, true)), nil +} + +// AbsLangURL takes a given string and converts it to an absolute URL according +// to a page's position in the project directory structure and the current +// language. +func (ns *Namespace) AbsLangURL(a interface{}) (template.HTML, error) { + s, err := cast.ToStringE(a) + if err != nil { + return "", err + } + + return template.HTML(ns.deps.PathSpec.AbsURL(s, true)), nil +} diff --git a/transform/absurl.go b/transform/absurl.go new file mode 100644 index 000000000..255ac33b6 --- /dev/null +++ b/transform/absurl.go @@ -0,0 +1,28 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transform + +var ar = newAbsURLReplacer() + +// AbsURL replaces relative URLs with absolute ones +// in HTML files, using the baseURL setting. +var AbsURL = func(ct contentTransformer) { + ar.replaceInHTML(ct) +} + +// AbsURLInXML replaces relative URLs with absolute ones +// in XML files, using the baseURL setting. +var AbsURLInXML = func(ct contentTransformer) { + ar.replaceInXML(ct) +} diff --git a/transform/absurlreplacer.go b/transform/absurlreplacer.go new file mode 100644 index 000000000..c659a94e8 --- /dev/null +++ b/transform/absurlreplacer.go @@ -0,0 +1,312 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transform + +import ( + "bytes" + "io" + "unicode/utf8" +) + +type matchState int + +const ( + matchStateNone matchState = iota + matchStateWhitespace + matchStatePartial + matchStateFull +) + +type absurllexer struct { + // the source to absurlify + content []byte + // the target for the new absurlified content + w io.Writer + + // path may be set to a "." relative path + path []byte + + pos int // input position + start int // item start position + width int // width of last element + + matchers []absURLMatcher + + ms matchState + matches [3]bool // track matches of the 3 prefixes + idx int // last index in matches checked + +} + +type stateFunc func(*absurllexer) stateFunc + +// prefix is how to identify and which func to handle the replacement. +type prefix struct { + r []rune + f func(l *absurllexer) +} + +// new prefixes can be added below, but note: +// - the matches array above must be expanded. +// - the prefix must with the current logic end with '=' +var prefixes = []*prefix{ + {r: []rune{'s', 'r', 'c', '='}, f: checkCandidateBase}, + {r: []rune{'h', 'r', 'e', 'f', '='}, f: checkCandidateBase}, + {r: []rune{'s', 'r', 'c', 's', 'e', 't', '='}, f: checkCandidateSrcset}, +} + +type absURLMatcher struct { + match []byte + quote []byte +} + +// match check rune inside word. Will be != ' '. +func (l *absurllexer) match(r rune) { + + var found bool + + // note, the prefixes can start off on the same foot, i.e. + // src and srcset. + if l.ms == matchStateWhitespace { + l.idx = 0 + for j, p := range prefixes { + if r == p.r[l.idx] { + l.matches[j] = true + found = true + // checkMatchState will only return true when r=='=', so + // we can safely ignore the return value here. + l.checkMatchState(r, j) + } + } + + if !found { + l.ms = matchStateNone + } + + return + } + + l.idx++ + for j, m := range l.matches { + // still a match? + if m { + if prefixes[j].r[l.idx] == r { + found = true + if l.checkMatchState(r, j) { + return + } + } else { + l.matches[j] = false + } + } + } + + if !found { + l.ms = matchStateNone + } +} + +func (l *absurllexer) checkMatchState(r rune, idx int) bool { + if r == '=' { + l.ms = matchStateFull + for k := range l.matches { + if k != idx { + l.matches[k] = false + } + } + return true + } + + l.ms = matchStatePartial + + return false +} + +func (l *absurllexer) emit() { + l.w.Write(l.content[l.start:l.pos]) + l.start = l.pos +} + +// handle URLs in src and href. +func checkCandidateBase(l *absurllexer) { + for _, m := range l.matchers { + if !bytes.HasPrefix(l.content[l.pos:], m.match) { + continue + } + // check for schemaless URLs + posAfter := l.pos + len(m.match) + if posAfter >= len(l.content) { + return + } + r, _ := utf8.DecodeRune(l.content[posAfter:]) + if r == '/' { + // schemaless: skip + return + } + if l.pos > l.start { + l.emit() + } + l.pos += len(m.match) + l.w.Write(m.quote) + l.w.Write(l.path) + l.start = l.pos + } +} + +// handle URLs in srcset. +func checkCandidateSrcset(l *absurllexer) { + // special case, not frequent (me think) + for _, m := range l.matchers { + if !bytes.HasPrefix(l.content[l.pos:], m.match) { + continue + } + + // check for schemaless URLs + posAfter := l.pos + len(m.match) + if posAfter >= len(l.content) { + return + } + r, _ := utf8.DecodeRune(l.content[posAfter:]) + if r == '/' { + // schemaless: skip + continue + } + + posLastQuote := bytes.Index(l.content[l.pos+1:], m.quote) + + // safe guard + if posLastQuote < 0 || posLastQuote > 2000 { + return + } + + if l.pos > l.start { + l.emit() + } + + section := l.content[l.pos+len(m.quote) : l.pos+posLastQuote+1] + + fields := bytes.Fields(section) + l.w.Write(m.quote) + for i, f := range fields { + if f[0] == '/' { + l.w.Write(l.path) + l.w.Write(f[1:]) + + } else { + l.w.Write(f) + } + + if i < len(fields)-1 { + l.w.Write([]byte(" ")) + } + } + + l.w.Write(m.quote) + l.pos += len(section) + (len(m.quote) * 2) + l.start = l.pos + } +} + +// main loop +func (l *absurllexer) replace() { + contentLength := len(l.content) + var r rune + + for { + if l.pos >= contentLength { + l.width = 0 + break + } + + var width = 1 + r = rune(l.content[l.pos]) + if r >= utf8.RuneSelf { + r, width = utf8.DecodeRune(l.content[l.pos:]) + } + l.width = width + l.pos += l.width + if r == ' ' { + l.ms = matchStateWhitespace + } else if l.ms != matchStateNone { + l.match(r) + if l.ms == matchStateFull { + var p *prefix + for i, m := range l.matches { + if m { + p = prefixes[i] + l.matches[i] = false + } + } + l.ms = matchStateNone + p.f(l) + } + } + } + + // Done! + if l.pos > l.start { + l.emit() + } +} + +func doReplace(ct contentTransformer, matchers []absURLMatcher) { + + lexer := &absurllexer{ + content: ct.Content(), + w: ct, + path: ct.Path(), + matchers: matchers} + + lexer.replace() +} + +type absURLReplacer struct { + htmlMatchers []absURLMatcher + xmlMatchers []absURLMatcher +} + +func newAbsURLReplacer() *absURLReplacer { + + // HTML + dqHTMLMatch := []byte("\"/") + sqHTMLMatch := []byte("'/") + + // XML + dqXMLMatch := []byte(""/") + sqXMLMatch := []byte("'/") + + dqHTML := []byte("\"") + sqHTML := []byte("'") + + dqXML := []byte(""") + sqXML := []byte("'") + + return &absURLReplacer{ + htmlMatchers: []absURLMatcher{ + {dqHTMLMatch, dqHTML}, + {sqHTMLMatch, sqHTML}, + }, + xmlMatchers: []absURLMatcher{ + {dqXMLMatch, dqXML}, + {sqXMLMatch, sqXML}, + }} +} + +func (au *absURLReplacer) replaceInHTML(ct contentTransformer) { + doReplace(ct, au.htmlMatchers) +} + +func (au *absURLReplacer) replaceInXML(ct contentTransformer) { + doReplace(ct, au.xmlMatchers) +} diff --git a/transform/chain.go b/transform/chain.go new file mode 100644 index 000000000..f9c99a04a --- /dev/null +++ b/transform/chain.go @@ -0,0 +1,104 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transform + +import ( + "bytes" + "io" + + bp "github.com/gohugoio/hugo/bufferpool" +) + +type trans func(rw contentTransformer) + +type link trans + +type chain []link + +// NewChain creates a chained content transformer given the provided transforms. +func NewChain(trs ...link) chain { + return trs +} + +// NewEmptyTransforms creates a new slice of transforms with a capacity of 20. +func NewEmptyTransforms() []link { + return make([]link, 0, 20) +} + +// contentTransformer is an interface that enables rotation of pooled buffers +// in the transformer chain. +type contentTransformer interface { + Path() []byte + Content() []byte + io.Writer +} + +// Implements contentTransformer +// Content is read from the from-buffer and rewritten to to the to-buffer. +type fromToBuffer struct { + path []byte + from *bytes.Buffer + to *bytes.Buffer +} + +func (ft fromToBuffer) Path() []byte { + return ft.path +} + +func (ft fromToBuffer) Write(p []byte) (n int, err error) { + return ft.to.Write(p) +} + +func (ft fromToBuffer) Content() []byte { + return ft.from.Bytes() +} + +func (c *chain) Apply(w io.Writer, r io.Reader, p []byte) error { + + b1 := bp.GetBuffer() + defer bp.PutBuffer(b1) + + if _, err := b1.ReadFrom(r); err != nil { + return err + } + + if len(*c) == 0 { + _, err := b1.WriteTo(w) + return err + } + + b2 := bp.GetBuffer() + defer bp.PutBuffer(b2) + + fb := &fromToBuffer{path: p, from: b1, to: b2} + + for i, tr := range *c { + if i > 0 { + if fb.from == b1 { + fb.from = b2 + fb.to = b1 + fb.to.Reset() + } else { + fb.from = b1 + fb.to = b2 + fb.to.Reset() + } + } + + tr(fb) + } + + _, err := fb.to.WriteTo(w) + return err +} diff --git a/transform/chain_test.go b/transform/chain_test.go new file mode 100644 index 000000000..7b770ed67 --- /dev/null +++ b/transform/chain_test.go @@ -0,0 +1,258 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transform + +import ( + "bytes" + "path/filepath" + "strings" + "testing" + + "github.com/gohugoio/hugo/helpers" + "github.com/stretchr/testify/assert" +) + +const ( + h5JsContentDoubleQuote = "<!DOCTYPE html><html><head><script src=\"foobar.js\"></script><script src=\"/barfoo.js\"></script></head><body><nav><h1>title</h1></nav><article>content <a href=\"foobar\">foobar</a>. <a href=\"/foobar\">Follow up</a></article></body></html>" + h5JsContentSingleQuote = "<!DOCTYPE html><html><head><script src='foobar.js'></script><script src='/barfoo.js'></script></head><body><nav><h1>title</h1></nav><article>content <a href='foobar'>foobar</a>. <a href='/foobar'>Follow up</a></article></body></html>" + h5JsContentAbsURL = "<!DOCTYPE html><html><head><script src=\"http://user@host:10234/foobar.js\"></script></head><body><nav><h1>title</h1></nav><article>content <a href=\"https://host/foobar\">foobar</a>. Follow up</article></body></html>" + h5JsContentAbsURLSchemaless = "<!DOCTYPE html><html><head><script src=\"//host/foobar.js\"></script><script src='//host2/barfoo.js'></head><body><nav><h1>title</h1></nav><article>content <a href=\"//host/foobar\">foobar</a>. <a href='//host2/foobar'>Follow up</a></article></body></html>" + corectOutputSrcHrefDq = "<!DOCTYPE html><html><head><script src=\"foobar.js\"></script><script src=\"http://base/barfoo.js\"></script></head><body><nav><h1>title</h1></nav><article>content <a href=\"foobar\">foobar</a>. <a href=\"http://base/foobar\">Follow up</a></article></body></html>" + corectOutputSrcHrefSq = "<!DOCTYPE html><html><head><script src='foobar.js'></script><script src='http://base/barfoo.js'></script></head><body><nav><h1>title</h1></nav><article>content <a href='foobar'>foobar</a>. <a href='http://base/foobar'>Follow up</a></article></body></html>" + + h5XMLXontentAbsURL = "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?><feed xmlns=\"http://www.w3.org/2005/Atom\"><entry><content type=\"html\"><p><a href="/foobar">foobar</a></p> <p>A video: <iframe src='/foo'></iframe></p></content></entry></feed>" + correctOutputSrcHrefInXML = "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?><feed xmlns=\"http://www.w3.org/2005/Atom\"><entry><content type=\"html\"><p><a href="http://base/foobar">foobar</a></p> <p>A video: <iframe src='http://base/foo'></iframe></p></content></entry></feed>" + h5XMLContentGuarded = "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?><feed xmlns=\"http://www.w3.org/2005/Atom\"><entry><content type=\"html\"><p><a href="//foobar">foobar</a></p> <p>A video: <iframe src='//foo'></iframe></p></content></entry></feed>" +) + +const ( + // additional sanity tests for replacements testing + replace1 = "No replacements." + replace2 = "ᚠᛇᚻ ᛒᛦᚦ ᚠᚱᚩᚠᚢᚱ\nᚠᛁᚱᚪ ᚷᛖᚻᚹᛦᛚᚳᚢᛗ" + replace3 = `End of file: src="/` + replace4 = `End of file: srcset="/` + replace5 = `Srcsett with no closing quote: srcset="/img/small.jpg do be do be do.` + + // Issue: 816, schemaless links combined with others + replaceSchemalessHTML = `Pre. src='//schemaless' src='/normal' <a href="//schemaless">Schemaless</a>. <a href="/normal">normal</a>. Post.` + replaceSchemalessHTMLCorrect = `Pre. src='//schemaless' src='http://base/normal' <a href="//schemaless">Schemaless</a>. <a href="http://base/normal">normal</a>. Post.` + replaceSchemalessXML = `Pre. src='//schemaless' src='/normal' <a href='//schemaless'>Schemaless</a>. <a href='/normal'>normal</a>. Post.` + replaceSchemalessXMLCorrect = `Pre. src='//schemaless' src='http://base/normal' <a href='//schemaless'>Schemaless</a>. <a href='http://base/normal'>normal</a>. Post.` +) + +const ( + // srcset= + srcsetBasic = `Pre. <img srcset="/img/small.jpg 200w, /img/medium.jpg 300w, /img/big.jpg 700w" alt="text" src="/img/foo.jpg">` + srcsetBasicCorrect = `Pre. <img srcset="http://base/img/small.jpg 200w, http://base/img/medium.jpg 300w, http://base/img/big.jpg 700w" alt="text" src="http://base/img/foo.jpg">` + srcsetSingleQuote = `Pre. <img srcset='/img/small.jpg 200w, /img/big.jpg 700w' alt="text" src="/img/foo.jpg"> POST.` + srcsetSingleQuoteCorrect = `Pre. <img srcset='http://base/img/small.jpg 200w, http://base/img/big.jpg 700w' alt="text" src="http://base/img/foo.jpg"> POST.` + srcsetXMLBasic = `Pre. <img srcset="/img/small.jpg 200w, /img/big.jpg 700w" alt="text" src="/img/foo.jpg">` + srcsetXMLBasicCorrect = `Pre. <img srcset="http://base/img/small.jpg 200w, http://base/img/big.jpg 700w" alt="text" src="http://base/img/foo.jpg">` + srcsetXMLSingleQuote = `Pre. <img srcset="/img/small.jpg 200w, /img/big.jpg 700w" alt="text" src="/img/foo.jpg">` + srcsetXMLSingleQuoteCorrect = `Pre. <img srcset="http://base/img/small.jpg 200w, http://base/img/big.jpg 700w" alt="text" src="http://base/img/foo.jpg">` + srcsetVariations = `Pre. +Missing start quote: <img srcset=/img/small.jpg 200w, /img/big.jpg 700w" alt="text"> src='/img/foo.jpg'> FOO. +<img srcset='/img.jpg'> +schemaless: <img srcset='//img.jpg' src='//basic.jpg'> +schemaless2: <img srcset="//img.jpg" src="//basic.jpg2> POST +` +) + +const ( + srcsetVariationsCorrect = `Pre. +Missing start quote: <img srcset=/img/small.jpg 200w, /img/big.jpg 700w" alt="text"> src='http://base/img/foo.jpg'> FOO. +<img srcset='http://base/img.jpg'> +schemaless: <img srcset='//img.jpg' src='//basic.jpg'> +schemaless2: <img srcset="//img.jpg" src="//basic.jpg2> POST +` + srcsetXMLVariations = `Pre. +Missing start quote: <img srcset=/img/small.jpg 200w /img/big.jpg 700w" alt="text"> src='/img/foo.jpg'> FOO. +<img srcset='/img.jpg'> +schemaless: <img srcset='//img.jpg' src='//basic.jpg'> +schemaless2: <img srcset="//img.jpg" src="//basic.jpg2> POST +` + srcsetXMLVariationsCorrect = `Pre. +Missing start quote: <img srcset=/img/small.jpg 200w /img/big.jpg 700w" alt="text"> src='http://base/img/foo.jpg'> FOO. +<img srcset='http://base/img.jpg'> +schemaless: <img srcset='//img.jpg' src='//basic.jpg'> +schemaless2: <img srcset="//img.jpg" src="//basic.jpg2> POST +` + + relPathVariations = `PRE. a href="/img/small.jpg" POST.` + relPathVariationsCorrect = `PRE. a href="../../img/small.jpg" POST.` + + testBaseURL = "http://base/" +) + +var ( + absURLlBenchTests = []test{ + {h5JsContentDoubleQuote, corectOutputSrcHrefDq}, + {h5JsContentSingleQuote, corectOutputSrcHrefSq}, + {h5JsContentAbsURL, h5JsContentAbsURL}, + {h5JsContentAbsURLSchemaless, h5JsContentAbsURLSchemaless}, + } + + xmlAbsURLBenchTests = []test{ + {h5XMLXontentAbsURL, correctOutputSrcHrefInXML}, + {h5XMLContentGuarded, h5XMLContentGuarded}, + } + + sanityTests = []test{{replace1, replace1}, {replace2, replace2}, {replace3, replace3}, {replace3, replace3}, {replace5, replace5}} + extraTestsHTML = []test{{replaceSchemalessHTML, replaceSchemalessHTMLCorrect}} + absURLTests = append(absURLlBenchTests, append(sanityTests, extraTestsHTML...)...) + extraTestsXML = []test{{replaceSchemalessXML, replaceSchemalessXMLCorrect}} + xmlAbsURLTests = append(xmlAbsURLBenchTests, append(sanityTests, extraTestsXML...)...) + srcsetTests = []test{{srcsetBasic, srcsetBasicCorrect}, {srcsetSingleQuote, srcsetSingleQuoteCorrect}, {srcsetVariations, srcsetVariationsCorrect}} + srcsetXMLTests = []test{ + {srcsetXMLBasic, srcsetXMLBasicCorrect}, + {srcsetXMLSingleQuote, srcsetXMLSingleQuoteCorrect}, + {srcsetXMLVariations, srcsetXMLVariationsCorrect}} + + relurlTests = []test{{relPathVariations, relPathVariationsCorrect}} +) + +func TestChainZeroTransformers(t *testing.T) { + tr := NewChain() + in := new(bytes.Buffer) + out := new(bytes.Buffer) + if err := tr.Apply(in, out, []byte("")); err != nil { + t.Errorf("A zero transformer chain returned an error.") + } +} + +func TestChaingMultipleTransformers(t *testing.T) { + f1 := func(ct contentTransformer) { + ct.Write(bytes.Replace(ct.Content(), []byte("f1"), []byte("f1r"), -1)) + } + f2 := func(ct contentTransformer) { + ct.Write(bytes.Replace(ct.Content(), []byte("f2"), []byte("f2r"), -1)) + } + f3 := func(ct contentTransformer) { + ct.Write(bytes.Replace(ct.Content(), []byte("f3"), []byte("f3r"), -1)) + } + + f4 := func(ct contentTransformer) { + ct.Write(bytes.Replace(ct.Content(), []byte("f4"), []byte("f4r"), -1)) + } + + tr := NewChain(f1, f2, f3, f4) + + out := new(bytes.Buffer) + if err := tr.Apply(out, strings.NewReader("Test: f4 f3 f1 f2 f1 The End."), []byte("")); err != nil { + t.Errorf("Multi transformer chain returned an error: %s", err) + } + + expected := "Test: f4r f3r f1r f2r f1r The End." + + if string(out.Bytes()) != expected { + t.Errorf("Expected %s got %s", expected, string(out.Bytes())) + } +} + +func BenchmarkAbsURL(b *testing.B) { + tr := NewChain(AbsURL) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + apply(b.Errorf, tr, absURLlBenchTests) + } +} + +func BenchmarkAbsURLSrcset(b *testing.B) { + tr := NewChain(AbsURL) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + apply(b.Errorf, tr, srcsetTests) + } +} + +func BenchmarkXMLAbsURLSrcset(b *testing.B) { + tr := NewChain(AbsURLInXML) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + apply(b.Errorf, tr, srcsetXMLTests) + } +} + +func TestAbsURL(t *testing.T) { + tr := NewChain(AbsURL) + + apply(t.Errorf, tr, absURLTests) + +} + +func TestRelativeURL(t *testing.T) { + tr := NewChain(AbsURL) + + applyWithPath(t.Errorf, tr, relurlTests, helpers.GetDottedRelativePath(filepath.FromSlash("/post/sub/"))) + +} + +func TestAbsURLSrcSet(t *testing.T) { + tr := NewChain(AbsURL) + + apply(t.Errorf, tr, srcsetTests) +} + +func TestAbsXMLURLSrcSet(t *testing.T) { + tr := NewChain(AbsURLInXML) + + apply(t.Errorf, tr, srcsetXMLTests) +} + +func BenchmarkXMLAbsURL(b *testing.B) { + tr := NewChain(AbsURLInXML) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + apply(b.Errorf, tr, xmlAbsURLBenchTests) + } +} + +func TestXMLAbsURL(t *testing.T) { + tr := NewChain(AbsURLInXML) + apply(t.Errorf, tr, xmlAbsURLTests) +} + +func TestNewEmptyTransforms(t *testing.T) { + transforms := NewEmptyTransforms() + assert.Equal(t, 20, cap(transforms)) +} + +type errorf func(string, ...interface{}) + +func applyWithPath(ef errorf, tr chain, tests []test, path string) { + for _, test := range tests { + out := new(bytes.Buffer) + var err error + err = tr.Apply(out, strings.NewReader(test.content), []byte(path)) + if err != nil { + ef("Unexpected error: %s", err) + } + if test.expected != string(out.Bytes()) { + ef("Expected:\n%s\nGot:\n%s", test.expected, string(out.Bytes())) + } + } +} + +func apply(ef errorf, tr chain, tests []test) { + applyWithPath(ef, tr, tests, testBaseURL) +} + +type test struct { + content string + expected string +} diff --git a/transform/hugogeneratorinject.go b/transform/hugogeneratorinject.go new file mode 100644 index 000000000..874053087 --- /dev/null +++ b/transform/hugogeneratorinject.go @@ -0,0 +1,50 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transform + +import ( + "bytes" + "fmt" + "regexp" + + "github.com/gohugoio/hugo/helpers" +) + +var metaTagsCheck = regexp.MustCompile(`(?i)<meta\s+name=['|"]?generator['|"]?`) +var hugoGeneratorTag = fmt.Sprintf(`<meta name="generator" content="Hugo %s" />`, helpers.CurrentHugoVersion) + +// HugoGeneratorInject injects a meta generator tag for Hugo if none present. +func HugoGeneratorInject(ct contentTransformer) { + if metaTagsCheck.Match(ct.Content()) { + if _, err := ct.Write(ct.Content()); err != nil { + helpers.DistinctWarnLog.Println("Failed to inject Hugo generator tag:", err) + } + return + } + + head := "<head>" + replace := []byte(fmt.Sprintf("%s\n\t%s", head, hugoGeneratorTag)) + newcontent := bytes.Replace(ct.Content(), []byte(head), replace, 1) + + if len(newcontent) == len(ct.Content()) { + head := "<HEAD>" + replace := []byte(fmt.Sprintf("%s\n\t%s", head, hugoGeneratorTag)) + newcontent = bytes.Replace(ct.Content(), []byte(head), replace, 1) + } + + if _, err := ct.Write(newcontent); err != nil { + helpers.DistinctWarnLog.Println("Failed to inject Hugo generator tag:", err) + } + +} diff --git a/transform/hugogeneratorinject_test.go b/transform/hugogeneratorinject_test.go new file mode 100644 index 000000000..d37fea24e --- /dev/null +++ b/transform/hugogeneratorinject_test.go @@ -0,0 +1,59 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transform + +import ( + "bytes" + "strings" + "testing" +) + +func TestHugoGeneratorInject(t *testing.T) { + hugoGeneratorTag = "META" + for i, this := range []struct { + in string + expect string + }{ + {`<head> + <foo /> +</head>`, `<head> + META + <foo /> +</head>`}, + {`<HEAD> + <foo /> +</HEAD>`, `<HEAD> + META + <foo /> +</HEAD>`}, + {`<head><meta name="generator" content="Jekyll" /></head>`, `<head><meta name="generator" content="Jekyll" /></head>`}, + {`<head><meta name='generator' content='Jekyll' /></head>`, `<head><meta name='generator' content='Jekyll' /></head>`}, + {`<head><meta name=generator content=Jekyll /></head>`, `<head><meta name=generator content=Jekyll /></head>`}, + {`<head><META NAME="GENERATOR" content="Jekyll" /></head>`, `<head><META NAME="GENERATOR" content="Jekyll" /></head>`}, + {"", ""}, + {"</head>", "</head>"}, + {"<head>", "<head>\n\tMETA"}, + } { + in := strings.NewReader(this.in) + out := new(bytes.Buffer) + + tr := NewChain(HugoGeneratorInject) + tr.Apply(out, in, []byte("")) + + if out.String() != this.expect { + t.Errorf("[%d] Expected \n%q got \n%q", i, this.expect, out.String()) + } + } + +} diff --git a/transform/livereloadinject.go b/transform/livereloadinject.go new file mode 100644 index 000000000..83fe7c106 --- /dev/null +++ b/transform/livereloadinject.go @@ -0,0 +1,38 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transform + +import ( + "bytes" + "fmt" +) + +func LiveReloadInject(port int) func(ct contentTransformer) { + return func(ct contentTransformer) { + endBodyTag := "</body>" + match := []byte(endBodyTag) + replaceTemplate := `<script data-no-instant>document.write('<script src="/livereload.js?port=%d&mindelay=10"></' + 'script>')</script>%s` + replace := []byte(fmt.Sprintf(replaceTemplate, port, endBodyTag)) + + newcontent := bytes.Replace(ct.Content(), match, replace, 1) + if len(newcontent) == len(ct.Content()) { + endBodyTag = "</BODY>" + replace := []byte(fmt.Sprintf(replaceTemplate, port, endBodyTag)) + match := []byte(endBodyTag) + newcontent = bytes.Replace(ct.Content(), match, replace, 1) + } + + ct.Write(newcontent) + } +} diff --git a/transform/livereloadinject_test.go b/transform/livereloadinject_test.go new file mode 100644 index 000000000..3337243bd --- /dev/null +++ b/transform/livereloadinject_test.go @@ -0,0 +1,39 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transform + +import ( + "bytes" + "fmt" + "strings" + "testing" +) + +func TestLiveReloadInject(t *testing.T) { + doTestLiveReloadInject(t, "</body>") + doTestLiveReloadInject(t, "</BODY>") +} + +func doTestLiveReloadInject(t *testing.T, bodyEndTag string) { + out := new(bytes.Buffer) + in := strings.NewReader(bodyEndTag) + + tr := NewChain(LiveReloadInject(1313)) + tr.Apply(out, in, []byte("path")) + + expected := fmt.Sprintf(`<script data-no-instant>document.write('<script src="/livereload.js?port=1313&mindelay=10"></' + 'script>')</script>%s`, bodyEndTag) + if string(out.Bytes()) != expected { + t.Errorf("Expected %s got %s", expected, string(out.Bytes())) + } +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 000000000..87357a26e --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,59 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package utils + +import ( + "os" + + jww "github.com/spf13/jwalterweatherman" +) + +// CheckErr logs the messages given and then the error. +// TODO(bep) Remove this package. +func CheckErr(logger *jww.Notepad, err error, s ...string) { + if err == nil { + return + } + if len(s) == 0 { + logger.CRITICAL.Println(err) + return + } + for _, message := range s { + logger.ERROR.Println(message) + } + logger.ERROR.Println(err) +} + +// StopOnErr exits on any error after logging it. +func StopOnErr(logger *jww.Notepad, err error, s ...string) { + if err == nil { + return + } + + defer os.Exit(-1) + + if len(s) == 0 { + newMessage := err.Error() + // Printing an empty string results in a error with + // no message, no bueno. + if newMessage != "" { + logger.CRITICAL.Println(newMessage) + } + } + for _, message := range s { + if message != "" { + logger.CRITICAL.Println(message) + } + } +} diff --git a/vendor/vendor.json b/vendor/vendor.json new file mode 100644 index 000000000..89471466c --- /dev/null +++ b/vendor/vendor.json @@ -0,0 +1,453 @@ +{ + "comment": "", + "ignore": "test", + "package": [ + { + "checksumSHA1": "fEgW0LDkuhB+99rGbe1upZnGK6I=", + "path": "github.com/BurntSushi/toml", + "revision": "8fb9fdc4f82fd3495b9086c911b86cc3d50cb7ab", + "revisionTime": "2017-06-23T11:17:45Z" + }, + { + "checksumSHA1": "NX4v3cbkXAJxFlrncqT9yEUBuoA=", + "path": "github.com/PuerkitoBio/purell", + "revision": "b938d81255b5473c57635324295cb0fe398c7a58", + "revisionTime": "2017-03-24T13:41:32Z" + }, + { + "checksumSHA1": "pvmScnaMFuAVLTxxOWjhGZBgPkg=", + "path": "github.com/PuerkitoBio/urlesc", + "revision": "bbf7a2afc14f93e1e0a5c06df524fbd75e5031e5", + "revisionTime": "2017-03-24T14:02:28Z" + }, + { + "checksumSHA1": "7yrV1Gzr1ajco1xJ1gsyqRDTY2U=", + "path": "github.com/bep/gitmap", + "revision": "de8030ebafb76c6e84d50ee6d143382637c00598", + "revisionTime": "2017-06-13T14:57:45Z" + }, + { + "checksumSHA1": "K8wTIgrK5sl+LmQs8CD/orvKsAM=", + "path": "github.com/bep/inflect", + "revision": "b896c45f5af983b1f416bdf3bb89c4f1f0926f69", + "revisionTime": "2016-04-08T19:03:23Z" + }, + { + "checksumSHA1": "NKoZRlZix5wzCfN0rTg29GtKZRU=", + "path": "github.com/chaseadamsio/goorgeous", + "revision": "677defd0e024333503d8c946dd4ba3f32ad3e5d2", + "revisionTime": "2017-04-27T12:50:10Z" + }, + { + "checksumSHA1": "CCfIpyo6sGH8gnEdP/0vGSi4ywA=", + "path": "github.com/cpuguy83/go-md2man/md2man", + "revision": "23709d0847197db6021a51fdb193e66e9222d4e7", + "revisionTime": "2017-06-03T12:52:39Z" + }, + { + "checksumSHA1": "OFu4xJEIjiI8Suu+j/gabfp+y6Q=", + "origin": "github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew", + "path": "github.com/davecgh/go-spew/spew", + "revision": "f6abca593680b2315d2075e0f5e2a9751e3f431a", + "revisionTime": "2017-06-01T20:57:54Z" + }, + { + "checksumSHA1": "Gg9/TEWjO52UKbEQ6fRB/bpcOW8=", + "path": "github.com/dchest/cssmin", + "revision": "fb8d9b44afdc258bfff6052d3667521babcb2239", + "revisionTime": "2015-12-10T17:00:30Z" + }, + { + "checksumSHA1": "mx6fo/9FWzh/jSy07p4ScYjKAFU=", + "path": "github.com/eknkc/amber", + "revision": "5fa7895500976542b0e28bb266f42ff1c7ce07f5", + "revisionTime": "2017-06-14T17:20:37Z" + }, + { + "checksumSHA1": "xN7neNcn7i1MDjmrQk/Wfav2RbM=", + "path": "github.com/eknkc/amber/parser", + "revision": "5fa7895500976542b0e28bb266f42ff1c7ce07f5", + "revisionTime": "2017-06-14T17:20:37Z" + }, + { + "checksumSHA1": "vn9aiBYikJ4xuywsOvBOZlpHvgw=", + "path": "github.com/fortytw2/leaktest", + "revision": "7dad53304f9614c1c365755c1176a8e876fee3e8", + "revisionTime": "2017-05-17T13:08:33Z" + }, + { + "checksumSHA1": "x2Km0Qy3WgJJnV19Zv25VwTJcBM=", + "path": "github.com/fsnotify/fsnotify", + "revision": "4da3e2cfbabc9f751898f250b49f2439785783a1", + "revisionTime": "2017-03-29T04:21:07Z" + }, + { + "checksumSHA1": "hEnH6sgR83Qfx7UNnphNNlelmj0=", + "path": "github.com/gorilla/websocket", + "revision": "a91eba7f97777409bc2c443f5534d41dd20c5720", + "revisionTime": "2017-03-19T17:27:27Z" + }, + { + "checksumSHA1": "zvmksNyW6g+Fd/bywd4vcn8rp+M=", + "path": "github.com/hashicorp/go-immutable-radix", + "revision": "30664b879c9a771d8d50b137ab80ee0748cb2fcc", + "revisionTime": "2017-02-14T02:52:36Z" + }, + { + "checksumSHA1": "9hffs0bAIU6CquiRhKQdzjHnKt0=", + "path": "github.com/hashicorp/golang-lru/simplelru", + "revision": "0a025b7e63adc15a622f29b0b2c4c3848243bbf6", + "revisionTime": "2016-08-13T22:13:03Z" + }, + { + "checksumSHA1": "7JBkp3EZoc0MSbiyWfzVhO4RYoY=", + "path": "github.com/hashicorp/hcl", + "revision": "392dba7d905ed5d04a5794ba89f558b27e2ba1ca", + "revisionTime": "2017-05-05T08:58:37Z" + }, + { + "checksumSHA1": "XQmjDva9JCGGkIecOgwtBEMCJhU=", + "path": "github.com/hashicorp/hcl/hcl/ast", + "revision": "392dba7d905ed5d04a5794ba89f558b27e2ba1ca", + "revisionTime": "2017-05-05T08:58:37Z" + }, + { + "checksumSHA1": "teokXoyRXEJ0vZHOWBD11l5YFNI=", + "path": "github.com/hashicorp/hcl/hcl/parser", + "revision": "392dba7d905ed5d04a5794ba89f558b27e2ba1ca", + "revisionTime": "2017-05-05T08:58:37Z" + }, + { + "checksumSHA1": "z6wdP4mRw4GVjShkNHDaOWkbxS0=", + "path": "github.com/hashicorp/hcl/hcl/scanner", + "revision": "392dba7d905ed5d04a5794ba89f558b27e2ba1ca", + "revisionTime": "2017-05-05T08:58:37Z" + }, + { + "checksumSHA1": "oS3SCN9Wd6D8/LG0Yx1fu84a7gI=", + "path": "github.com/hashicorp/hcl/hcl/strconv", + "revision": "392dba7d905ed5d04a5794ba89f558b27e2ba1ca", + "revisionTime": "2017-05-05T08:58:37Z" + }, + { + "checksumSHA1": "c6yprzj06ASwCo18TtbbNNBHljA=", + "path": "github.com/hashicorp/hcl/hcl/token", + "revision": "392dba7d905ed5d04a5794ba89f558b27e2ba1ca", + "revisionTime": "2017-05-05T08:58:37Z" + }, + { + "checksumSHA1": "PwlfXt7mFS8UYzWxOK5DOq0yxS0=", + "path": "github.com/hashicorp/hcl/json/parser", + "revision": "392dba7d905ed5d04a5794ba89f558b27e2ba1ca", + "revisionTime": "2017-05-05T08:58:37Z" + }, + { + "checksumSHA1": "YdvFsNOMSWMLnY6fcliWQa0O5Fw=", + "path": "github.com/hashicorp/hcl/json/scanner", + "revision": "392dba7d905ed5d04a5794ba89f558b27e2ba1ca", + "revisionTime": "2017-05-05T08:58:37Z" + }, + { + "checksumSHA1": "fNlXQCQEnb+B3k5UDL/r15xtSJY=", + "path": "github.com/hashicorp/hcl/json/token", + "revision": "392dba7d905ed5d04a5794ba89f558b27e2ba1ca", + "revisionTime": "2017-05-05T08:58:37Z" + }, + { + "checksumSHA1": "40vJyUB4ezQSn/NSadsKEOrudMc=", + "path": "github.com/inconshreveable/mousetrap", + "revision": "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75", + "revisionTime": "2014-10-17T20:07:13Z" + }, + { + "checksumSHA1": "gEjGS03N1eysvpQ+FCHTxPcbxXc=", + "path": "github.com/kardianos/osext", + "revision": "ae77be60afb1dcacde03767a8c37337fad28ac14", + "revisionTime": "2017-05-10T13:15:34Z" + }, + { + "checksumSHA1": "KQhA4EQp4Ldwj9nJZnEURlE6aQw=", + "path": "github.com/kr/fs", + "revision": "2788f0dbd16903de03cb8186e5c7d97b69ad387b", + "revisionTime": "2013-11-06T22:25:44Z" + }, + { + "checksumSHA1": "u6Fh5nWSW70Yi2/hq/zxPinakD4=", + "path": "github.com/kyokomi/emoji", + "revision": "ddd4753eac3f6480ca86b16cc6c98d26a0935d17", + "revisionTime": "2017-05-19T01:14:27Z" + }, + { + "checksumSHA1": "UvboqgDDSAbGORdtr5tBpkwYR0A=", + "path": "github.com/magiconair/properties", + "revision": "51463bfca2576e06c62a8504b5c0f06d61312647", + "revisionTime": "2017-03-21T09:30:39Z" + }, + { + "checksumSHA1": "49oh0/Ujwr+1qRxcj6t/eszV7kM=", + "path": "github.com/miekg/mmark", + "revision": "f809cc9d384e2f7f3985a28a899237b892f35719", + "revisionTime": "2017-05-19T09:30:52Z" + }, + { + "checksumSHA1": "EHjhpHipgm+XGccrRAms9AW3Ewk=", + "path": "github.com/mitchellh/mapstructure", + "revision": "d0303fe809921458f417bcf828397a65db30a7e4", + "revisionTime": "2017-05-23T03:00:23Z" + }, + { + "checksumSHA1": "gDe7nlx3FyCVxLkARgl0VAntDRk=", + "path": "github.com/nicksnyder/go-i18n/i18n/bundle", + "revision": "3e70a1a463008cea6726380c908b1a6a8bdf7b24", + "revisionTime": "2017-05-12T15:20:54Z" + }, + { + "checksumSHA1": "+XOg99I1zdmBRUb04ZswvzQ2WS0=", + "path": "github.com/nicksnyder/go-i18n/i18n/language", + "revision": "3e70a1a463008cea6726380c908b1a6a8bdf7b24", + "revisionTime": "2017-05-12T15:20:54Z" + }, + { + "checksumSHA1": "WZOU406In2hs8FJOHWqV8PWkJKs=", + "path": "github.com/nicksnyder/go-i18n/i18n/translation", + "revision": "3e70a1a463008cea6726380c908b1a6a8bdf7b24", + "revisionTime": "2017-05-12T15:20:54Z" + }, + { + "checksumSHA1": "TkXI1L27/2icLlcE/4wTZCfxLeM=", + "path": "github.com/opennota/urlesc", + "revision": "bbf7a2afc14f93e1e0a5c06df524fbd75e5031e5", + "revisionTime": "2017-03-24T14:02:28Z" + }, + { + "checksumSHA1": "F1IYMLBLAZaTOWnmXsgaxTGvrWI=", + "path": "github.com/pelletier/go-buffruneio", + "revision": "c37440a7cf42ac63b919c752ca73a85067e05992", + "revisionTime": "2017-02-27T22:03:11Z" + }, + { + "checksumSHA1": "vHrGGP777P2fqQHr2IYwNVVRQ/o=", + "path": "github.com/pelletier/go-toml", + "revision": "fe7536c3dee2596cdd23ee9976a17c22bdaae286", + "revisionTime": "2017-06-02T06:55:32Z" + }, + { + "checksumSHA1": "rJab1YdNhQooDiBWNnt7TLWPyBU=", + "path": "github.com/pkg/errors", + "revision": "c605e284fe17294bda444b34710735b29d1a9d90", + "revisionTime": "2017-05-05T04:36:39Z" + }, + { + "checksumSHA1": "GM8ioORTp1BH7KraT6GhmrB76t4=", + "path": "github.com/pkg/sftp", + "revision": "a5f8514e29e90a859e93871b1582e5c81f466f82", + "revisionTime": "2017-05-11T00:00:41Z" + }, + { + "checksumSHA1": "zKKp5SZ3d3ycKe4EKMNT0BqAWBw=", + "origin": "github.com/stretchr/testify/vendor/github.com/pmezard/go-difflib/difflib", + "path": "github.com/pmezard/go-difflib/difflib", + "revision": "f6abca593680b2315d2075e0f5e2a9751e3f431a", + "revisionTime": "2017-06-01T20:57:54Z" + }, + { + "checksumSHA1": "jgYnx5m5SrWrh9GE5kNZlpMWyCY=", + "path": "github.com/russross/blackfriday", + "revision": "067529f716f4c3f5e37c8c95ddd59df1007290ae", + "revisionTime": "2017-06-10T17:02:32Z" + }, + { + "checksumSHA1": "7xtI4YHA5UU4Ra/A5tBZ6rxzqCg=", + "path": "github.com/shurcooL/sanitized_anchor_name", + "revision": "541ff5ee47f1dddf6a5281af78307d921524bcb5", + "revisionTime": "2017-05-15T01:32:39Z" + }, + { + "checksumSHA1": "lBehULzb2/kIK3wZ0gz2yNmHq9s=", + "path": "github.com/spf13/afero", + "revision": "9be650865eab0c12963d8753212f4f9c66cdcf12", + "revisionTime": "2017-02-17T16:41:46Z" + }, + { + "checksumSHA1": "5KRbEQ28dDaQmKwAYTD0if/aEvg=", + "path": "github.com/spf13/afero/mem", + "revision": "9be650865eab0c12963d8753212f4f9c66cdcf12", + "revisionTime": "2017-02-17T16:41:46Z" + }, + { + "checksumSHA1": "Sq0QP4JywTr7UM4hTK1cjCi7jec=", + "path": "github.com/spf13/cast", + "revision": "acbeb36b902d72a7a4c18e8f3241075e7ab763e4", + "revisionTime": "2017-04-13T08:50:28Z" + }, + { + "checksumSHA1": "suXMIWAx0XtlQR2zippibpr3Yjg=", + "path": "github.com/spf13/cobra", + "revision": "b4dbd37a01839e0653eec12aa4bbb2a2ce7b2a37", + "revisionTime": "2017-06-12T06:36:10Z" + }, + { + "checksumSHA1": "9umiInkHtYqaxDZwVUd2U1cHp7k=", + "path": "github.com/spf13/cobra/doc", + "revision": "b4dbd37a01839e0653eec12aa4bbb2a2ce7b2a37", + "revisionTime": "2017-06-12T06:36:10Z" + }, + { + "checksumSHA1": "TmmnmNJgWmRK0eMo+43gTzVA8zI=", + "path": "github.com/spf13/fsync", + "revision": "12a01e648f05a938100a26858d2d59a120307a18", + "revisionTime": "2017-03-20T14:25:52Z" + }, + { + "checksumSHA1": "mZa8ukZwyAZ86E3oVjhnnKSlizs=", + "path": "github.com/spf13/jwalterweatherman", + "revision": "0efa5202c04663c757d84f90f5219c1250baf94f", + "revisionTime": "2017-05-23T09:39:43Z" + }, + { + "checksumSHA1": "zLJY+lsX1e5OO6gRxQd5RfKgdQY=", + "path": "github.com/spf13/nitro", + "revision": "24d7ef30a12da0bdc5e2eb370a79c659ddccf0e8", + "revisionTime": "2013-10-03T13:43:07Z" + }, + { + "checksumSHA1": "STxYqRb4gnlSr3mRpT+Igfdz/kM=", + "path": "github.com/spf13/pflag", + "revision": "e57e3eeb33f795204c1ca35f56c44f83227c6e66", + "revisionTime": "2017-05-08T18:43:26Z" + }, + { + "checksumSHA1": "KgO3wjkSOm6H7cqTrBCzLyvj6+o=", + "path": "github.com/spf13/viper", + "revision": "c1de95864d73a5465492829d7cb2dd422b19ac96", + "revisionTime": "2017-06-19T10:35:39Z" + }, + { + "checksumSHA1": "5NBHAe3S15q3L9hOLThnMZjIZRE=", + "path": "github.com/stretchr/testify/assert", + "revision": "f6abca593680b2315d2075e0f5e2a9751e3f431a", + "revisionTime": "2017-06-01T20:57:54Z" + }, + { + "checksumSHA1": "7vs6dSc1PPGBKyzb/SCIyeMJPLQ=", + "path": "github.com/stretchr/testify/require", + "revision": "f6abca593680b2315d2075e0f5e2a9751e3f431a", + "revisionTime": "2017-06-01T20:57:54Z" + }, + { + "checksumSHA1": "U4bWGZ3c9p8FHrgU5l4l5i7A6bo=", + "path": "github.com/yosssi/ace", + "revision": "ea038f4770b6746c3f8f84f14fa60d9fe1205b56", + "revisionTime": "2016-07-28T07:45:28Z" + }, + { + "checksumSHA1": "nAu0XmCeC6WnUySyI8R7w4cxAqU=", + "path": "golang.org/x/crypto/curve25519", + "revision": "850760c427c516be930bc91280636328f1a62286", + "revisionTime": "2017-06-13T19:24:08Z" + }, + { + "checksumSHA1": "wGb//LjBPNxYHqk+dcLo7BjPXK8=", + "path": "golang.org/x/crypto/ed25519", + "revision": "850760c427c516be930bc91280636328f1a62286", + "revisionTime": "2017-06-13T19:24:08Z" + }, + { + "checksumSHA1": "LXFcVx8I587SnWmKycSDEq9yvK8=", + "path": "golang.org/x/crypto/ed25519/internal/edwards25519", + "revision": "850760c427c516be930bc91280636328f1a62286", + "revisionTime": "2017-06-13T19:24:08Z" + }, + { + "checksumSHA1": "V2ffWwZh1jDCBy6NpKZnG+ruqgg=", + "path": "golang.org/x/crypto/ssh", + "revision": "850760c427c516be930bc91280636328f1a62286", + "revisionTime": "2017-06-13T19:24:08Z" + }, + { + "checksumSHA1": "VrzPJyWI6disCgYuVEQzkjqUsJk=", + "path": "golang.org/x/net/idna", + "revision": "ddf80d0970594e2e4cccf5a98861cad3d9eaa4cd", + "revisionTime": "2017-06-14T13:40:53Z" + }, + { + "checksumSHA1": "Tkb1hBdBWeO7SGjixS2Hm48F6+s=", + "path": "golang.org/x/sys/unix", + "revision": "fb4cac33e3196ff7f507ab9b2d2a44b0142f5b5a", + "revisionTime": "2017-06-14T06:48:48Z" + }, + { + "checksumSHA1": "DoQpPb6ZhqN2gP7rNXfxHaTUgEs=", + "path": "golang.org/x/text/cases", + "revision": "9e2f80a6ba7ed4ba13e0cd4b1f094bf916875735", + "revisionTime": "2017-06-09T15:53:19Z" + }, + { + "checksumSHA1": "9Px0J1fbo4RT0sxn40pPUZOEdhM=", + "path": "golang.org/x/text/internal", + "revision": "9e2f80a6ba7ed4ba13e0cd4b1f094bf916875735", + "revisionTime": "2017-06-09T15:53:19Z" + }, + { + "checksumSHA1": "hyNCcTwMQnV6/MK8uUW9E5H0J0M=", + "path": "golang.org/x/text/internal/tag", + "revision": "9e2f80a6ba7ed4ba13e0cd4b1f094bf916875735", + "revisionTime": "2017-06-09T15:53:19Z" + }, + { + "checksumSHA1": "Dhc7bHUc+d1uuYO/byxQf7AfW+o=", + "path": "golang.org/x/text/language", + "revision": "9e2f80a6ba7ed4ba13e0cd4b1f094bf916875735", + "revisionTime": "2017-06-09T15:53:19Z" + }, + { + "checksumSHA1": "IV4MN7KGBSocu/5NR3le3sxup4Y=", + "path": "golang.org/x/text/runes", + "revision": "9e2f80a6ba7ed4ba13e0cd4b1f094bf916875735", + "revisionTime": "2017-06-09T15:53:19Z" + }, + { + "checksumSHA1": "faFDXp++cLjLBlvsr+izZ+go1WU=", + "path": "golang.org/x/text/secure/bidirule", + "revision": "9e2f80a6ba7ed4ba13e0cd4b1f094bf916875735", + "revisionTime": "2017-06-09T15:53:19Z" + }, + { + "checksumSHA1": "QB2i98ahuhLf9XWZfRRO22uN2p4=", + "path": "golang.org/x/text/secure/precis", + "revision": "9e2f80a6ba7ed4ba13e0cd4b1f094bf916875735", + "revisionTime": "2017-06-09T15:53:19Z" + }, + { + "checksumSHA1": "ziMb9+ANGRJSSIuxYdRbA+cDRBQ=", + "path": "golang.org/x/text/transform", + "revision": "9e2f80a6ba7ed4ba13e0cd4b1f094bf916875735", + "revisionTime": "2017-06-09T15:53:19Z" + }, + { + "checksumSHA1": "KG+XZAbxdkpBm3Fa3bJ3Ylq8CKI=", + "path": "golang.org/x/text/unicode/bidi", + "revision": "9e2f80a6ba7ed4ba13e0cd4b1f094bf916875735", + "revisionTime": "2017-06-09T15:53:19Z" + }, + { + "checksumSHA1": "Anof4bt0AU+Sa3R8Rq0KBnlpbaQ=", + "path": "golang.org/x/text/unicode/norm", + "revision": "9e2f80a6ba7ed4ba13e0cd4b1f094bf916875735", + "revisionTime": "2017-06-09T15:53:19Z" + }, + { + "checksumSHA1": "/kqvbsH5mRknhs6xf7cJii0wQos=", + "path": "golang.org/x/text/width", + "revision": "9e2f80a6ba7ed4ba13e0cd4b1f094bf916875735", + "revisionTime": "2017-06-09T15:53:19Z" + }, + { + "checksumSHA1": "fALlQNY1fM99NesfLJ50KguWsio=", + "path": "gopkg.in/yaml.v2", + "revision": "cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b", + "revisionTime": "2017-04-07T17:21:22Z" + } + ], + "rootPath": "github.com/gohugoio/hugo" +} diff --git a/watcher/batcher.go b/watcher/batcher.go new file mode 100644 index 000000000..0b4083e81 --- /dev/null +++ b/watcher/batcher.go @@ -0,0 +1,70 @@ +// Copyright 2015 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package watcher + +import ( + "time" + + "github.com/fsnotify/fsnotify" +) + +type Batcher struct { + *fsnotify.Watcher + interval time.Duration + done chan struct{} + + Events chan []fsnotify.Event // Events are returned on this channel +} + +func New(interval time.Duration) (*Batcher, error) { + watcher, err := fsnotify.NewWatcher() + + batcher := &Batcher{} + batcher.Watcher = watcher + batcher.interval = interval + batcher.done = make(chan struct{}, 1) + batcher.Events = make(chan []fsnotify.Event, 1) + + if err == nil { + go batcher.run() + } + + return batcher, err +} + +func (b *Batcher) run() { + tick := time.Tick(b.interval) + evs := make([]fsnotify.Event, 0) +OuterLoop: + for { + select { + case ev := <-b.Watcher.Events: + evs = append(evs, ev) + case <-tick: + if len(evs) == 0 { + continue + } + b.Events <- evs + evs = make([]fsnotify.Event, 0) + case <-b.done: + break OuterLoop + } + } + close(b.done) +} + +func (b *Batcher) Close() { + b.done <- struct{}{} + b.Watcher.Close() +} |