Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/gohugoio/hugo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.circleci/config.yml51
-rw-r--r--.dockerignore9
-rw-r--r--.gitattributes8
-rw-r--r--.github/SUPPORT.md3
-rw-r--r--.github/stale.yml5
-rw-r--r--.gitignore23
-rw-r--r--.gitmodules (renamed from themes/gohugoioTheme/assets/js/filesaver.js)0
-rw-r--r--.mailmap3
-rw-r--r--.travis.yml46
-rw-r--r--CONTRIBUTING.md191
-rwxr-xr-xDockerfile33
-rw-r--r--LICENSE201
-rw-r--r--README.md194
-rwxr-xr-xbench.sh37
-rwxr-xr-xbenchSite.sh12
-rwxr-xr-xbenchbep.sh2
-rwxr-xr-xbepdock.sh1
-rw-r--r--bufferpool/bufpool.go38
-rw-r--r--bufferpool/bufpool_test.go27
-rw-r--r--cache/filecache/filecache.go353
-rw-r--r--cache/filecache/filecache_config.go202
-rw-r--r--cache/filecache/filecache_config_test.go207
-rw-r--r--cache/filecache/filecache_pruner.go80
-rw-r--r--cache/filecache/filecache_pruner_test.go118
-rw-r--r--cache/filecache/filecache_test.go257
-rw-r--r--cache/namedmemcache/named_cache.go79
-rw-r--r--cache/namedmemcache/named_cache_test.go80
-rw-r--r--cache/partitioned_lazy_cache.go99
-rw-r--r--cache/partitioned_lazy_cache_test.go138
-rw-r--r--codegen/methods.go548
-rw-r--r--codegen/methods2_test.go20
-rw-r--r--codegen/methods_test.go100
-rw-r--r--commands/check.go34
-rw-r--r--commands/check_darwin.go36
-rw-r--r--commands/commandeer.go408
-rw-r--r--commands/commands.go305
-rw-r--r--commands/commands_test.go286
-rw-r--r--commands/config.go77
-rw-r--r--commands/convert.go251
-rw-r--r--commands/deploy.go75
-rw-r--r--commands/env.go44
-rw-r--r--commands/gen.go41
-rw-r--r--commands/genautocomplete.go80
-rw-r--r--commands/genchromastyles.go74
-rw-r--r--commands/gendoc.go96
-rw-r--r--commands/gendocshelper.go74
-rw-r--r--commands/genman.go77
-rw-r--r--commands/helpers.go79
-rw-r--r--commands/hugo.go1207
-rw-r--r--commands/hugo_test.go52
-rw-r--r--commands/hugo_windows.go27
-rw-r--r--commands/import_jekyll.go651
-rw-r--r--commands/import_jekyll_test.go129
-rw-r--r--commands/limit_darwin.go84
-rw-r--r--commands/limit_others.go20
-rw-r--r--commands/list.go209
-rw-r--r--commands/list_test.go70
-rw-r--r--commands/new.go139
-rw-r--r--commands/new_content_test.go128
-rw-r--r--commands/new_site.go165
-rw-r--r--commands/new_theme.go179
-rw-r--r--commands/release.go72
-rw-r--r--commands/release_noop.go20
-rw-r--r--commands/server.go552
-rw-r--r--commands/server_errors.go95
-rw-r--r--commands/server_test.go134
-rw-r--r--commands/static_syncer.go130
-rw-r--r--commands/version.go44
-rw-r--r--common/collections/append.go112
-rw-r--r--common/collections/append_test.go78
-rw-r--r--common/collections/collections.go21
-rw-r--r--common/collections/slice.go66
-rw-r--r--common/collections/slice_test.go125
-rw-r--r--common/herrors/error_locator.go255
-rw-r--r--common/herrors/error_locator_test.go129
-rw-r--r--common/herrors/errors.go53
-rw-r--r--common/herrors/file_error.go133
-rw-r--r--common/herrors/file_error_test.go57
-rw-r--r--common/herrors/line_number_extractors.go66
-rw-r--r--common/hreflect/helpers.go91
-rw-r--r--common/hreflect/helpers_test.go42
-rw-r--r--common/hugio/readers.go54
-rw-r--r--common/hugio/writers.go76
-rw-r--r--common/hugo/hugo.go67
-rw-r--r--common/hugo/hugo_test.go35
-rw-r--r--common/hugo/vars_extended.go18
-rw-r--r--common/hugo/vars_regular.go18
-rw-r--r--common/hugo/version.go237
-rw-r--r--common/hugo/version_current.go22
-rw-r--r--common/hugo/version_test.go79
-rw-r--r--common/loggers/loggers.go173
-rw-r--r--common/loggers/loggers_test.go32
-rw-r--r--common/maps/maps.go116
-rw-r--r--common/maps/maps_test.go123
-rw-r--r--common/maps/scratch.go153
-rw-r--r--common/maps/scratch_test.go207
-rw-r--r--common/math/math.go135
-rw-r--r--common/math/math_test.go109
-rw-r--r--common/terminal/colors.go70
-rw-r--r--common/text/position.go99
-rw-r--r--common/text/position_test.go33
-rw-r--r--common/types/evictingqueue.go96
-rw-r--r--common/types/evictingqueue_test.go74
-rw-r--r--common/types/types.go80
-rw-r--r--common/types/types_test.go29
-rw-r--r--common/urls/ref.go22
-rw-r--r--compare/compare.go35
-rw-r--r--config/configLoader.go127
-rw-r--r--config/configLoader_test.go34
-rw-r--r--config/configProvider.go54
-rw-r--r--config/configProvider_test.go36
-rw-r--r--config/env.go33
-rw-r--r--config/privacy/privacyConfig.go110
-rw-r--r--config/privacy/privacyConfig_test.go104
-rw-r--r--config/services/servicesConfig.go92
-rw-r--r--config/services/servicesConfig_test.go69
-rw-r--r--config/sitemap.go44
-rw-r--r--content/en/tools/migrations.md81
-rw-r--r--create/content.go313
-rw-r--r--create/content_template_handler.go146
-rw-r--r--create/content_test.go272
-rw-r--r--deploy/cloudfront.go51
-rw-r--r--deploy/deploy.go641
-rw-r--r--deploy/deployConfig.go101
-rw-r--r--deploy/deployConfig_test.go137
-rw-r--r--deploy/deploy_test.go810
-rw-r--r--deps/deps.go354
-rw-r--r--docs/.github/stale.yml22
-rw-r--r--docs/.gitignore5
-rw-r--r--docs/LICENSE.md (renamed from LICENSE.md)0
-rw-r--r--docs/README.md47
-rw-r--r--docs/archetypes/default.md (renamed from archetypes/default.md)0
-rw-r--r--docs/archetypes/functions.md (renamed from archetypes/functions.md)0
-rw-r--r--docs/archetypes/showcase/bio.md (renamed from archetypes/showcase/bio.md)0
-rw-r--r--docs/archetypes/showcase/featured.png (renamed from archetypes/showcase/featured.png)bin41270 -> 41270 bytes
-rw-r--r--docs/archetypes/showcase/index.md (renamed from archetypes/showcase/index.md)0
-rw-r--r--docs/config.toml (renamed from config.toml)0
-rw-r--r--docs/config/_default/config.toml (renamed from config/_default/config.toml)0
-rw-r--r--docs/config/_default/languages.toml (renamed from config/_default/languages.toml)0
-rw-r--r--docs/config/_default/menus/menus.en.toml (renamed from config/_default/menus/menus.en.toml)0
-rw-r--r--docs/config/_default/menus/menus.zh.toml (renamed from config/_default/menus/menus.zh.toml)0
-rw-r--r--docs/config/_default/params.toml (renamed from config/_default/params.toml)0
-rw-r--r--docs/config/development/params.toml (renamed from config/development/params.toml)0
-rw-r--r--docs/config/production/config.toml (renamed from config/production/config.toml)0
-rw-r--r--docs/config/production/params.toml (renamed from config/production/params.toml)0
-rw-r--r--docs/content/en/_index.md (renamed from content/en/_index.md)0
-rw-r--r--docs/content/en/about/_index.md (renamed from content/en/about/_index.md)0
-rw-r--r--docs/content/en/about/benefits.md (renamed from content/en/about/benefits.md)0
-rw-r--r--docs/content/en/about/features.md (renamed from content/en/about/features.md)0
-rw-r--r--docs/content/en/about/hugo-and-gdpr.md (renamed from content/en/about/hugo-and-gdpr.md)0
-rw-r--r--docs/content/en/about/license.md (renamed from content/en/about/license.md)0
-rw-r--r--docs/content/en/about/new-in-032/index.md (renamed from content/en/about/new-in-032/index.md)0
-rw-r--r--docs/content/en/about/new-in-032/sunset.jpg (renamed from content/en/about/new-in-032/sunset.jpg)bin90587 -> 90587 bytes
-rw-r--r--docs/content/en/about/what-is-hugo.md (renamed from content/en/about/what-is-hugo.md)0
-rw-r--r--docs/content/en/commands/hugo.md (renamed from content/en/commands/hugo.md)0
-rw-r--r--docs/content/en/commands/hugo_check.md (renamed from content/en/commands/hugo_check.md)0
-rw-r--r--docs/content/en/commands/hugo_check_ulimit.md (renamed from content/en/commands/hugo_check_ulimit.md)0
-rw-r--r--docs/content/en/commands/hugo_config.md (renamed from content/en/commands/hugo_config.md)0
-rw-r--r--docs/content/en/commands/hugo_convert.md (renamed from content/en/commands/hugo_convert.md)0
-rw-r--r--docs/content/en/commands/hugo_convert_toJSON.md (renamed from content/en/commands/hugo_convert_toJSON.md)0
-rw-r--r--docs/content/en/commands/hugo_convert_toTOML.md (renamed from content/en/commands/hugo_convert_toTOML.md)0
-rw-r--r--docs/content/en/commands/hugo_convert_toYAML.md (renamed from content/en/commands/hugo_convert_toYAML.md)0
-rw-r--r--docs/content/en/commands/hugo_env.md (renamed from content/en/commands/hugo_env.md)0
-rw-r--r--docs/content/en/commands/hugo_gen.md (renamed from content/en/commands/hugo_gen.md)0
-rw-r--r--docs/content/en/commands/hugo_gen_autocomplete.md (renamed from content/en/commands/hugo_gen_autocomplete.md)0
-rw-r--r--docs/content/en/commands/hugo_gen_chromastyles.md (renamed from content/en/commands/hugo_gen_chromastyles.md)0
-rw-r--r--docs/content/en/commands/hugo_gen_doc.md (renamed from content/en/commands/hugo_gen_doc.md)0
-rw-r--r--docs/content/en/commands/hugo_gen_man.md (renamed from content/en/commands/hugo_gen_man.md)0
-rw-r--r--docs/content/en/commands/hugo_import.md (renamed from content/en/commands/hugo_import.md)0
-rw-r--r--docs/content/en/commands/hugo_import_jekyll.md (renamed from content/en/commands/hugo_import_jekyll.md)0
-rw-r--r--docs/content/en/commands/hugo_list.md (renamed from content/en/commands/hugo_list.md)0
-rw-r--r--docs/content/en/commands/hugo_list_drafts.md (renamed from content/en/commands/hugo_list_drafts.md)0
-rw-r--r--docs/content/en/commands/hugo_list_expired.md (renamed from content/en/commands/hugo_list_expired.md)0
-rw-r--r--docs/content/en/commands/hugo_list_future.md (renamed from content/en/commands/hugo_list_future.md)0
-rw-r--r--docs/content/en/commands/hugo_new.md (renamed from content/en/commands/hugo_new.md)0
-rw-r--r--docs/content/en/commands/hugo_new_site.md (renamed from content/en/commands/hugo_new_site.md)0
-rw-r--r--docs/content/en/commands/hugo_new_theme.md (renamed from content/en/commands/hugo_new_theme.md)0
-rw-r--r--docs/content/en/commands/hugo_server.md (renamed from content/en/commands/hugo_server.md)0
-rw-r--r--docs/content/en/commands/hugo_version.md (renamed from content/en/commands/hugo_version.md)0
-rw-r--r--docs/content/en/content-management/_index.md (renamed from content/en/content-management/_index.md)0
-rw-r--r--docs/content/en/content-management/archetypes.md (renamed from content/en/content-management/archetypes.md)0
-rw-r--r--docs/content/en/content-management/authors.md (renamed from content/en/content-management/authors.md)0
-rw-r--r--docs/content/en/content-management/comments.md (renamed from content/en/content-management/comments.md)0
-rw-r--r--docs/content/en/content-management/cross-references.md (renamed from content/en/content-management/cross-references.md)0
-rw-r--r--docs/content/en/content-management/formats.md (renamed from content/en/content-management/formats.md)0
-rw-r--r--docs/content/en/content-management/front-matter.md (renamed from content/en/content-management/front-matter.md)0
-rw-r--r--docs/content/en/content-management/image-processing/index.md (renamed from content/en/content-management/image-processing/index.md)0
-rw-r--r--docs/content/en/content-management/image-processing/sunset.jpg (renamed from content/en/content-management/image-processing/sunset.jpg)bin90587 -> 90587 bytes
-rw-r--r--docs/content/en/content-management/menus.md (renamed from content/en/content-management/menus.md)0
-rw-r--r--docs/content/en/content-management/multilingual.md (renamed from content/en/content-management/multilingual.md)0
-rw-r--r--docs/content/en/content-management/organization/1-featured-content-bundles.png (renamed from content/en/content-management/organization/1-featured-content-bundles.png)bin63640 -> 63640 bytes
-rw-r--r--docs/content/en/content-management/organization/index.md (renamed from content/en/content-management/organization/index.md)0
-rw-r--r--docs/content/en/content-management/page-bundles.md (renamed from content/en/content-management/page-bundles.md)0
-rw-r--r--docs/content/en/content-management/page-resources.md (renamed from content/en/content-management/page-resources.md)0
-rw-r--r--docs/content/en/content-management/related.md (renamed from content/en/content-management/related.md)0
-rw-r--r--docs/content/en/content-management/sections.md (renamed from content/en/content-management/sections.md)0
-rw-r--r--docs/content/en/content-management/shortcodes.md (renamed from content/en/content-management/shortcodes.md)0
-rw-r--r--docs/content/en/content-management/static-files.md (renamed from content/en/content-management/static-files.md)0
-rw-r--r--docs/content/en/content-management/summaries.md (renamed from content/en/content-management/summaries.md)0
-rw-r--r--docs/content/en/content-management/syntax-highlighting.md (renamed from content/en/content-management/syntax-highlighting.md)0
-rw-r--r--docs/content/en/content-management/taxonomies.md (renamed from content/en/content-management/taxonomies.md)0
-rw-r--r--docs/content/en/content-management/toc.md (renamed from content/en/content-management/toc.md)0
-rw-r--r--docs/content/en/content-management/types.md (renamed from content/en/content-management/types.md)0
-rw-r--r--docs/content/en/content-management/urls.md (renamed from content/en/content-management/urls.md)0
-rw-r--r--docs/content/en/contribute/_index.md (renamed from content/en/contribute/_index.md)0
-rw-r--r--docs/content/en/contribute/development.md (renamed from content/en/contribute/development.md)0
-rw-r--r--docs/content/en/contribute/documentation.md (renamed from content/en/contribute/documentation.md)0
-rw-r--r--docs/content/en/contribute/themes.md (renamed from content/en/contribute/themes.md)0
-rw-r--r--docs/content/en/documentation.md (renamed from content/en/documentation.md)0
-rw-r--r--docs/content/en/functions/GetPage.md (renamed from content/en/functions/GetPage.md)0
-rw-r--r--docs/content/en/functions/NumFmt.md (renamed from content/en/functions/NumFmt.md)0
-rw-r--r--docs/content/en/functions/_index.md (renamed from content/en/functions/_index.md)0
-rw-r--r--docs/content/en/functions/abslangurl.md (renamed from content/en/functions/abslangurl.md)0
-rw-r--r--docs/content/en/functions/absurl.md (renamed from content/en/functions/absurl.md)0
-rw-r--r--docs/content/en/functions/adddate.md (renamed from content/en/functions/adddate.md)0
-rw-r--r--docs/content/en/functions/after.md (renamed from content/en/functions/after.md)0
-rw-r--r--docs/content/en/functions/anchorize.md (renamed from content/en/functions/anchorize.md)0
-rw-r--r--docs/content/en/functions/append.md (renamed from content/en/functions/append.md)0
-rw-r--r--docs/content/en/functions/apply.md (renamed from content/en/functions/apply.md)0
-rw-r--r--docs/content/en/functions/base64.md (renamed from content/en/functions/base64.md)0
-rw-r--r--docs/content/en/functions/chomp.md (renamed from content/en/functions/chomp.md)0
-rw-r--r--docs/content/en/functions/complement.md (renamed from content/en/functions/complement.md)0
-rw-r--r--docs/content/en/functions/cond.md (renamed from content/en/functions/cond.md)0
-rw-r--r--docs/content/en/functions/countrunes.md (renamed from content/en/functions/countrunes.md)0
-rw-r--r--docs/content/en/functions/countwords.md (renamed from content/en/functions/countwords.md)0
-rw-r--r--docs/content/en/functions/dateformat.md (renamed from content/en/functions/dateformat.md)0
-rw-r--r--docs/content/en/functions/default.md (renamed from content/en/functions/default.md)0
-rw-r--r--docs/content/en/functions/delimit.md (renamed from content/en/functions/delimit.md)0
-rw-r--r--docs/content/en/functions/dict.md (renamed from content/en/functions/dict.md)0
-rw-r--r--docs/content/en/functions/echoparam.md (renamed from content/en/functions/echoparam.md)0
-rw-r--r--docs/content/en/functions/emojify.md (renamed from content/en/functions/emojify.md)0
-rw-r--r--docs/content/en/functions/eq.md (renamed from content/en/functions/eq.md)0
-rw-r--r--docs/content/en/functions/errorf.md (renamed from content/en/functions/errorf.md)0
-rw-r--r--docs/content/en/functions/fileExists.md (renamed from content/en/functions/fileExists.md)0
-rw-r--r--docs/content/en/functions/findRe.md (renamed from content/en/functions/findRe.md)0
-rw-r--r--docs/content/en/functions/first.md (renamed from content/en/functions/first.md)0
-rw-r--r--docs/content/en/functions/float.md (renamed from content/en/functions/float.md)0
-rw-r--r--docs/content/en/functions/format.md (renamed from content/en/functions/format.md)0
-rw-r--r--docs/content/en/functions/ge.md (renamed from content/en/functions/ge.md)0
-rw-r--r--docs/content/en/functions/get.md (renamed from content/en/functions/get.md)0
-rw-r--r--docs/content/en/functions/getenv.md (renamed from content/en/functions/getenv.md)0
-rw-r--r--docs/content/en/functions/group.md (renamed from content/en/functions/group.md)0
-rw-r--r--docs/content/en/functions/gt.md (renamed from content/en/functions/gt.md)0
-rw-r--r--docs/content/en/functions/hasPrefix.md (renamed from content/en/functions/hasPrefix.md)0
-rw-r--r--docs/content/en/functions/haschildren.md (renamed from content/en/functions/haschildren.md)0
-rw-r--r--docs/content/en/functions/hasmenucurrent.md (renamed from content/en/functions/hasmenucurrent.md)0
-rw-r--r--docs/content/en/functions/highlight.md (renamed from content/en/functions/highlight.md)0
-rw-r--r--docs/content/en/functions/htmlEscape.md (renamed from content/en/functions/htmlEscape.md)0
-rw-r--r--docs/content/en/functions/htmlUnescape.md (renamed from content/en/functions/htmlUnescape.md)0
-rw-r--r--docs/content/en/functions/humanize.md (renamed from content/en/functions/humanize.md)0
-rw-r--r--docs/content/en/functions/i18n.md (renamed from content/en/functions/i18n.md)0
-rw-r--r--docs/content/en/functions/imageConfig.md (renamed from content/en/functions/imageConfig.md)0
-rw-r--r--docs/content/en/functions/in.md (renamed from content/en/functions/in.md)0
-rw-r--r--docs/content/en/functions/index-function.md (renamed from content/en/functions/index-function.md)0
-rw-r--r--docs/content/en/functions/int.md (renamed from content/en/functions/int.md)0
-rw-r--r--docs/content/en/functions/intersect.md (renamed from content/en/functions/intersect.md)0
-rw-r--r--docs/content/en/functions/ismenucurrent.md (renamed from content/en/functions/ismenucurrent.md)0
-rw-r--r--docs/content/en/functions/isset.md (renamed from content/en/functions/isset.md)0
-rw-r--r--docs/content/en/functions/jsonify.md (renamed from content/en/functions/jsonify.md)0
-rw-r--r--docs/content/en/functions/lang.Merge.md (renamed from content/en/functions/lang.Merge.md)0
-rw-r--r--docs/content/en/functions/last.md (renamed from content/en/functions/last.md)0
-rw-r--r--docs/content/en/functions/le.md (renamed from content/en/functions/le.md)0
-rw-r--r--docs/content/en/functions/len.md (renamed from content/en/functions/len.md)0
-rw-r--r--docs/content/en/functions/lower.md (renamed from content/en/functions/lower.md)0
-rw-r--r--docs/content/en/functions/lt.md (renamed from content/en/functions/lt.md)0
-rw-r--r--docs/content/en/functions/markdownify.md (renamed from content/en/functions/markdownify.md)0
-rw-r--r--docs/content/en/functions/math.md (renamed from content/en/functions/math.md)0
-rw-r--r--docs/content/en/functions/md5.md (renamed from content/en/functions/md5.md)0
-rw-r--r--docs/content/en/functions/ne.md (renamed from content/en/functions/ne.md)0
-rw-r--r--docs/content/en/functions/now.md (renamed from content/en/functions/now.md)0
-rw-r--r--docs/content/en/functions/os.Stat.md (renamed from content/en/functions/os.Stat.md)0
-rw-r--r--docs/content/en/functions/param.md (renamed from content/en/functions/param.md)0
-rw-r--r--docs/content/en/functions/partialCached.md (renamed from content/en/functions/partialCached.md)0
-rw-r--r--docs/content/en/functions/path.Base.md (renamed from content/en/functions/path.Base.md)0
-rw-r--r--docs/content/en/functions/path.Dir.md (renamed from content/en/functions/path.Dir.md)0
-rw-r--r--docs/content/en/functions/path.Ext.md (renamed from content/en/functions/path.Ext.md)0
-rw-r--r--docs/content/en/functions/path.Join.md (renamed from content/en/functions/path.Join.md)0
-rw-r--r--docs/content/en/functions/path.Split.md (renamed from content/en/functions/path.Split.md)0
-rw-r--r--docs/content/en/functions/plainify.md (renamed from content/en/functions/plainify.md)0
-rw-r--r--docs/content/en/functions/pluralize.md (renamed from content/en/functions/pluralize.md)0
-rw-r--r--docs/content/en/functions/print.md (renamed from content/en/functions/print.md)0
-rw-r--r--docs/content/en/functions/printf.md (renamed from content/en/functions/printf.md)0
-rw-r--r--docs/content/en/functions/println.md (renamed from content/en/functions/println.md)0
-rw-r--r--docs/content/en/functions/querify.md (renamed from content/en/functions/querify.md)0
-rw-r--r--docs/content/en/functions/range.md (renamed from content/en/functions/range.md)0
-rw-r--r--docs/content/en/functions/readdir.md (renamed from content/en/functions/readdir.md)0
-rw-r--r--docs/content/en/functions/readfile.md (renamed from content/en/functions/readfile.md)0
-rw-r--r--docs/content/en/functions/ref.md (renamed from content/en/functions/ref.md)0
-rw-r--r--docs/content/en/functions/reflect.IsMap.md (renamed from content/en/functions/reflect.IsMap.md)0
-rw-r--r--docs/content/en/functions/reflect.IsSlice.md (renamed from content/en/functions/reflect.IsSlice.md)0
-rw-r--r--docs/content/en/functions/relLangURL.md (renamed from content/en/functions/relLangURL.md)0
-rw-r--r--docs/content/en/functions/relref.md (renamed from content/en/functions/relref.md)0
-rw-r--r--docs/content/en/functions/relurl.md (renamed from content/en/functions/relurl.md)0
-rw-r--r--docs/content/en/functions/render.md (renamed from content/en/functions/render.md)0
-rw-r--r--docs/content/en/functions/replace.md (renamed from content/en/functions/replace.md)0
-rw-r--r--docs/content/en/functions/replacere.md (renamed from content/en/functions/replacere.md)0
-rw-r--r--docs/content/en/functions/safeCSS.md (renamed from content/en/functions/safeCSS.md)0
-rw-r--r--docs/content/en/functions/safeHTML.md (renamed from content/en/functions/safeHTML.md)0
-rw-r--r--docs/content/en/functions/safeHTMLAttr.md (renamed from content/en/functions/safeHTMLAttr.md)0
-rw-r--r--docs/content/en/functions/safeJS.md (renamed from content/en/functions/safeJS.md)0
-rw-r--r--docs/content/en/functions/safeURL.md (renamed from content/en/functions/safeURL.md)0
-rw-r--r--docs/content/en/functions/scratch.md (renamed from content/en/functions/scratch.md)0
-rw-r--r--docs/content/en/functions/seq.md (renamed from content/en/functions/seq.md)0
-rw-r--r--docs/content/en/functions/sha.md (renamed from content/en/functions/sha.md)0
-rw-r--r--docs/content/en/functions/shuffle.md (renamed from content/en/functions/shuffle.md)0
-rw-r--r--docs/content/en/functions/singularize.md (renamed from content/en/functions/singularize.md)0
-rw-r--r--docs/content/en/functions/slice.md (renamed from content/en/functions/slice.md)0
-rw-r--r--docs/content/en/functions/slicestr.md (renamed from content/en/functions/slicestr.md)0
-rw-r--r--docs/content/en/functions/sort.md (renamed from content/en/functions/sort.md)0
-rw-r--r--docs/content/en/functions/split.md (renamed from content/en/functions/split.md)0
-rw-r--r--docs/content/en/functions/string.md (renamed from content/en/functions/string.md)0
-rw-r--r--docs/content/en/functions/strings.Repeat.md (renamed from content/en/functions/strings.Repeat.md)0
-rw-r--r--docs/content/en/functions/strings.RuneCount.md (renamed from content/en/functions/strings.RuneCount.md)0
-rw-r--r--docs/content/en/functions/strings.TrimLeft.md (renamed from content/en/functions/strings.TrimLeft.md)0
-rw-r--r--docs/content/en/functions/strings.TrimPrefix.md (renamed from content/en/functions/strings.TrimPrefix.md)0
-rw-r--r--docs/content/en/functions/strings.TrimRight.md (renamed from content/en/functions/strings.TrimRight.md)0
-rw-r--r--docs/content/en/functions/strings.TrimSuffix.md (renamed from content/en/functions/strings.TrimSuffix.md)0
-rw-r--r--docs/content/en/functions/substr.md (renamed from content/en/functions/substr.md)0
-rw-r--r--docs/content/en/functions/symdiff.md (renamed from content/en/functions/symdiff.md)0
-rw-r--r--docs/content/en/functions/templates.Exists.md (renamed from content/en/functions/templates.Exists.md)0
-rw-r--r--docs/content/en/functions/time.md (renamed from content/en/functions/time.md)0
-rw-r--r--docs/content/en/functions/title.md (renamed from content/en/functions/title.md)0
-rw-r--r--docs/content/en/functions/transform.Unmarshal.md (renamed from content/en/functions/transform.Unmarshal.md)0
-rw-r--r--docs/content/en/functions/trim.md (renamed from content/en/functions/trim.md)0
-rw-r--r--docs/content/en/functions/truncate.md (renamed from content/en/functions/truncate.md)0
-rw-r--r--docs/content/en/functions/union.md (renamed from content/en/functions/union.md)0
-rw-r--r--docs/content/en/functions/uniq.md (renamed from content/en/functions/uniq.md)0
-rw-r--r--docs/content/en/functions/unix.md (renamed from content/en/functions/unix.md)0
-rw-r--r--docs/content/en/functions/upper.md (renamed from content/en/functions/upper.md)0
-rw-r--r--docs/content/en/functions/urlize.md (renamed from content/en/functions/urlize.md)0
-rw-r--r--docs/content/en/functions/urls.Parse.md (renamed from content/en/functions/urls.Parse.md)0
-rw-r--r--docs/content/en/functions/where.md (renamed from content/en/functions/where.md)0
-rw-r--r--docs/content/en/functions/with.md (renamed from content/en/functions/with.md)0
-rw-r--r--docs/content/en/getting-started/_index.md (renamed from content/en/getting-started/_index.md)0
-rw-r--r--docs/content/en/getting-started/code-toggle.md (renamed from content/en/getting-started/code-toggle.md)0
-rw-r--r--docs/content/en/getting-started/configuration.md (renamed from content/en/getting-started/configuration.md)0
-rw-r--r--docs/content/en/getting-started/directory-structure.md (renamed from content/en/getting-started/directory-structure.md)0
-rw-r--r--docs/content/en/getting-started/installing.md (renamed from content/en/getting-started/installing.md)0
-rw-r--r--docs/content/en/getting-started/quick-start.md (renamed from content/en/getting-started/quick-start.md)0
-rw-r--r--docs/content/en/getting-started/usage.md (renamed from content/en/getting-started/usage.md)0
-rw-r--r--docs/content/en/hosting-and-deployment/_index.md (renamed from content/en/hosting-and-deployment/_index.md)0
-rw-r--r--docs/content/en/hosting-and-deployment/deployment-with-nanobox.md (renamed from content/en/hosting-and-deployment/deployment-with-nanobox.md)0
-rw-r--r--docs/content/en/hosting-and-deployment/deployment-with-rsync.md (renamed from content/en/hosting-and-deployment/deployment-with-rsync.md)0
-rw-r--r--docs/content/en/hosting-and-deployment/deployment-with-wercker.md (renamed from content/en/hosting-and-deployment/deployment-with-wercker.md)0
-rw-r--r--docs/content/en/hosting-and-deployment/hosting-on-aws-amplify.md (renamed from content/en/hosting-and-deployment/hosting-on-aws-amplify.md)0
-rw-r--r--docs/content/en/hosting-and-deployment/hosting-on-bitbucket.md (renamed from content/en/hosting-and-deployment/hosting-on-bitbucket.md)0
-rw-r--r--docs/content/en/hosting-and-deployment/hosting-on-firebase.md (renamed from content/en/hosting-and-deployment/hosting-on-firebase.md)0
-rw-r--r--docs/content/en/hosting-and-deployment/hosting-on-github.md (renamed from content/en/hosting-and-deployment/hosting-on-github.md)0
-rw-r--r--docs/content/en/hosting-and-deployment/hosting-on-gitlab.md (renamed from content/en/hosting-and-deployment/hosting-on-gitlab.md)0
-rw-r--r--docs/content/en/hosting-and-deployment/hosting-on-keycdn.md (renamed from content/en/hosting-and-deployment/hosting-on-keycdn.md)0
-rw-r--r--docs/content/en/hosting-and-deployment/hosting-on-netlify.md (renamed from content/en/hosting-and-deployment/hosting-on-netlify.md)0
-rwxr-xr-xdocs/content/en/hugo-pipes/_index.md (renamed from content/en/hugo-pipes/_index.md)0
-rwxr-xr-xdocs/content/en/hugo-pipes/bundling.md (renamed from content/en/hugo-pipes/bundling.md)0
-rwxr-xr-xdocs/content/en/hugo-pipes/fingerprint.md (renamed from content/en/hugo-pipes/fingerprint.md)0
-rwxr-xr-xdocs/content/en/hugo-pipes/introduction.md (renamed from content/en/hugo-pipes/introduction.md)0
-rwxr-xr-xdocs/content/en/hugo-pipes/minification.md (renamed from content/en/hugo-pipes/minification.md)0
-rwxr-xr-xdocs/content/en/hugo-pipes/postcss.md (renamed from content/en/hugo-pipes/postcss.md)0
-rwxr-xr-xdocs/content/en/hugo-pipes/resource-from-string.md (renamed from content/en/hugo-pipes/resource-from-string.md)0
-rwxr-xr-xdocs/content/en/hugo-pipes/resource-from-template.md (renamed from content/en/hugo-pipes/resource-from-template.md)0
-rwxr-xr-xdocs/content/en/hugo-pipes/scss-sass.md (renamed from content/en/hugo-pipes/scss-sass.md)0
-rw-r--r--docs/content/en/maintenance/_index.md (renamed from content/en/maintenance/_index.md)0
-rw-r--r--docs/content/en/news/0.10-relnotes/index.md (renamed from content/en/news/0.10-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.11-relnotes/index.md (renamed from content/en/news/0.11-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.12-relnotes/index.md (renamed from content/en/news/0.12-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.13-relnotes/index.md (renamed from content/en/news/0.13-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.14-relnotes/index.md (renamed from content/en/news/0.14-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.15-relnotes/index.md (renamed from content/en/news/0.15-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.16-relnotes/index.md (renamed from content/en/news/0.16-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.17-relnotes/index.md (renamed from content/en/news/0.17-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.18-relnotes/index.md (renamed from content/en/news/0.18-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.19-relnotes/index.md (renamed from content/en/news/0.19-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.20-relnotes/index.md (renamed from content/en/news/0.20-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.20.1-relnotes/index.md (renamed from content/en/news/0.20.1-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.20.2-relnotes/index.md (renamed from content/en/news/0.20.2-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.20.3-relnotes/index.md (renamed from content/en/news/0.20.3-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.20.4-relnotes/index.md (renamed from content/en/news/0.20.4-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.20.5-relnotes/index.md (renamed from content/en/news/0.20.5-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.20.6-relnotes/index.md (renamed from content/en/news/0.20.6-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.20.7-relnotes/index.md (renamed from content/en/news/0.20.7-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.21-relnotes/index.md (renamed from content/en/news/0.21-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.22-relnotes/index.md (renamed from content/en/news/0.22-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.22.1-relnotes/index.md (renamed from content/en/news/0.22.1-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.23-relnotes/index.md (renamed from content/en/news/0.23-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.24-relnotes/index.md (renamed from content/en/news/0.24-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.24.1-relnotes/index.md (renamed from content/en/news/0.24.1-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.25-relnotes/index.md (renamed from content/en/news/0.25-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.25.1-relnotes/index.md (renamed from content/en/news/0.25.1-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.26-relnotes/index.md (renamed from content/en/news/0.26-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.27-relnotes/index.md (renamed from content/en/news/0.27-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.27.1-relnotes/index.md (renamed from content/en/news/0.27.1-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.28-relnotes/index.md (renamed from content/en/news/0.28-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.29-relnotes/index.md (renamed from content/en/news/0.29-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.30-relnotes/index.md (renamed from content/en/news/0.30-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.30.1-relnotes/index.md (renamed from content/en/news/0.30.1-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.30.2-relnotes/index.md (renamed from content/en/news/0.30.2-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.31-relnotes/index.md (renamed from content/en/news/0.31-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.31.1-relnotes/index.md (renamed from content/en/news/0.31.1-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.32-relnotes/index.md (renamed from content/en/news/0.32-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.32.1-relnotes/index.md (renamed from content/en/news/0.32.1-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.32.2-relnotes/index.md (renamed from content/en/news/0.32.2-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.32.3-relnotes/index.md (renamed from content/en/news/0.32.3-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.32.4-relnotes/index.md (renamed from content/en/news/0.32.4-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.33-relnotes/featured-hugo-33-poster.png (renamed from content/en/news/0.33-relnotes/featured-hugo-33-poster.png)bin70230 -> 70230 bytes
-rw-r--r--docs/content/en/news/0.33-relnotes/index.md (renamed from content/en/news/0.33-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.34-relnotes/featured-34-poster.png (renamed from content/en/news/0.34-relnotes/featured-34-poster.png)bin78317 -> 78317 bytes
-rw-r--r--docs/content/en/news/0.34-relnotes/index.md (renamed from content/en/news/0.34-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.35-relnotes/featured-hugo-35-poster.png (renamed from content/en/news/0.35-relnotes/featured-hugo-35-poster.png)bin88519 -> 88519 bytes
-rw-r--r--docs/content/en/news/0.35-relnotes/index.md (renamed from content/en/news/0.35-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.36-relnotes/featured-hugo-36-poster.png (renamed from content/en/news/0.36-relnotes/featured-hugo-36-poster.png)bin67640 -> 67640 bytes
-rw-r--r--docs/content/en/news/0.36-relnotes/index.md (renamed from content/en/news/0.36-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.36.1-relnotes/index.md (renamed from content/en/news/0.36.1-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.37-relnotes/featured-hugo-37-poster.png (renamed from content/en/news/0.37-relnotes/featured-hugo-37-poster.png)bin186693 -> 186693 bytes
-rw-r--r--docs/content/en/news/0.37-relnotes/index.md (renamed from content/en/news/0.37-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.37.1-relnotes/index.md (renamed from content/en/news/0.37.1-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.38-relnotes/featured-poster.png (renamed from content/en/news/0.38-relnotes/featured-poster.png)bin69978 -> 69978 bytes
-rw-r--r--docs/content/en/news/0.38-relnotes/index.md (renamed from content/en/news/0.38-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.38.1-relnotes/index.md (renamed from content/en/news/0.38.1-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.38.2-relnotes/index.md (renamed from content/en/news/0.38.2-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.39-relnotes/featured-hugo-39-poster.png (renamed from content/en/news/0.39-relnotes/featured-hugo-39-poster.png)bin217215 -> 217215 bytes
-rw-r--r--docs/content/en/news/0.39-relnotes/index.md (renamed from content/en/news/0.39-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.40-relnotes/featured-hugo-40-poster.png (renamed from content/en/news/0.40-relnotes/featured-hugo-40-poster.png)bin69238 -> 69238 bytes
-rw-r--r--docs/content/en/news/0.40-relnotes/index.md (renamed from content/en/news/0.40-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.40.1-relnotes/index.md (renamed from content/en/news/0.40.1-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.40.2-relnotes/index.md (renamed from content/en/news/0.40.2-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.40.3-relnotes/index.md (renamed from content/en/news/0.40.3-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.41-relnotes/featured-hugo-41-poster.png (renamed from content/en/news/0.41-relnotes/featured-hugo-41-poster.png)bin67955 -> 67955 bytes
-rw-r--r--docs/content/en/news/0.41-relnotes/index.md (renamed from content/en/news/0.41-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.42-relnotes/featured-hugo-42-poster.png (renamed from content/en/news/0.42-relnotes/featured-hugo-42-poster.png)bin74852 -> 74852 bytes
-rw-r--r--docs/content/en/news/0.42-relnotes/index.md (renamed from content/en/news/0.42-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.42.1-relnotes/index.md (renamed from content/en/news/0.42.1-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.42.2-relnotes/index.md (renamed from content/en/news/0.42.2-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.43-relnotes/featured-hugo-43-poster.png (renamed from content/en/news/0.43-relnotes/featured-hugo-43-poster.png)bin78299 -> 78299 bytes
-rw-r--r--docs/content/en/news/0.43-relnotes/index.md (renamed from content/en/news/0.43-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.44-relnotes/featured-hugo-44-poster.png (renamed from content/en/news/0.44-relnotes/featured-hugo-44-poster.png)bin77631 -> 77631 bytes
-rw-r--r--docs/content/en/news/0.44-relnotes/index.md (renamed from content/en/news/0.44-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.45-relnotes/featured-hugo-45-poster.png (renamed from content/en/news/0.45-relnotes/featured-hugo-45-poster.png)bin66863 -> 66863 bytes
-rw-r--r--docs/content/en/news/0.45-relnotes/index.md (renamed from content/en/news/0.45-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.45.1-relnotes/index.md (renamed from content/en/news/0.45.1-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.46-relnotes/featured-hugo-46-poster.png (renamed from content/en/news/0.46-relnotes/featured-hugo-46-poster.png)bin68614 -> 68614 bytes
-rw-r--r--docs/content/en/news/0.46-relnotes/index.md (renamed from content/en/news/0.46-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.47-relnotes/featured-hugo-47-poster.png (renamed from content/en/news/0.47-relnotes/featured-hugo-47-poster.png)bin88288 -> 88288 bytes
-rw-r--r--docs/content/en/news/0.47-relnotes/index.md (renamed from content/en/news/0.47-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.47.1-relnotes/index.md (renamed from content/en/news/0.47.1-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.48-relnotes/featured-hugo-48-poster.png (renamed from content/en/news/0.48-relnotes/featured-hugo-48-poster.png)bin95358 -> 95358 bytes
-rw-r--r--docs/content/en/news/0.48-relnotes/index.md (renamed from content/en/news/0.48-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.49-relnotes/featured-hugo-49-poster.png (renamed from content/en/news/0.49-relnotes/featured-hugo-49-poster.png)bin66352 -> 66352 bytes
-rw-r--r--docs/content/en/news/0.49-relnotes/index.md (renamed from content/en/news/0.49-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.49.1-relnotes/index.md (renamed from content/en/news/0.49.1-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.49.2-relnotes/index.md (renamed from content/en/news/0.49.2-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.50-relnotes/featured-hugo-50-poster.png (renamed from content/en/news/0.50-relnotes/featured-hugo-50-poster.png)bin227240 -> 227240 bytes
-rw-r--r--docs/content/en/news/0.50-relnotes/index.md (renamed from content/en/news/0.50-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.51-relnotes/featured-hugo-51-poster.png (renamed from content/en/news/0.51-relnotes/featured-hugo-51-poster.png)bin117678 -> 117678 bytes
-rw-r--r--docs/content/en/news/0.51-relnotes/index.md (renamed from content/en/news/0.51-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.52-relnotes/featured-hugo-52-poster.png (renamed from content/en/news/0.52-relnotes/featured-hugo-52-poster.png)bin336810 -> 336810 bytes
-rw-r--r--docs/content/en/news/0.52-relnotes/index.md (renamed from content/en/news/0.52-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.53-relnotes/featured-hugo-53-poster.png (renamed from content/en/news/0.53-relnotes/featured-hugo-53-poster.png)bin110427 -> 110427 bytes
-rw-r--r--docs/content/en/news/0.53-relnotes/index.md (renamed from content/en/news/0.53-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.54.0-relnotes/featured-hugo-54.0-poster.png (renamed from content/en/news/0.54.0-relnotes/featured-hugo-54.0-poster.png)bin59805 -> 59805 bytes
-rw-r--r--docs/content/en/news/0.54.0-relnotes/index.md (renamed from content/en/news/0.54.0-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.55.0-relnotes/featured.png (renamed from content/en/news/0.55.0-relnotes/featured.png)bin1221797 -> 1221797 bytes
-rw-r--r--docs/content/en/news/0.55.0-relnotes/index.md (renamed from content/en/news/0.55.0-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.55.1-relnotes/index.md (renamed from content/en/news/0.55.1-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.55.2-relnotes/index.md (renamed from content/en/news/0.55.2-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.55.3-relnotes/index.md (renamed from content/en/news/0.55.3-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.55.4-relnotes/index.md (renamed from content/en/news/0.55.4-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.55.5-relnotes/index.md (renamed from content/en/news/0.55.5-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.55.6-relnotes/index.md (renamed from content/en/news/0.55.6-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.7-relnotes/index.md (renamed from content/en/news/0.7-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.8-relnotes/index.md (renamed from content/en/news/0.8-relnotes/index.md)0
-rw-r--r--docs/content/en/news/0.9-relnotes/index.md (renamed from content/en/news/0.9-relnotes/index.md)0
-rw-r--r--docs/content/en/news/_index.md (renamed from content/en/news/_index.md)0
-rw-r--r--docs/content/en/news/http2-server-push-in-hugo.md (renamed from content/en/news/http2-server-push-in-hugo.md)0
-rw-r--r--docs/content/en/news/lets-celebrate-hugos-5th-birthday/featured.png (renamed from content/en/news/lets-celebrate-hugos-5th-birthday/featured.png)bin179291 -> 179291 bytes
-rw-r--r--docs/content/en/news/lets-celebrate-hugos-5th-birthday/graph-stars.png (renamed from content/en/news/lets-celebrate-hugos-5th-birthday/graph-stars.png)bin15599 -> 15599 bytes
-rw-r--r--docs/content/en/news/lets-celebrate-hugos-5th-birthday/graph-themes.png (renamed from content/en/news/lets-celebrate-hugos-5th-birthday/graph-themes.png)bin16956 -> 16956 bytes
-rw-r--r--docs/content/en/news/lets-celebrate-hugos-5th-birthday/index.md (renamed from content/en/news/lets-celebrate-hugos-5th-birthday/index.md)0
-rw-r--r--docs/content/en/news/lets-celebrate-hugos-5th-birthday/sunset-get.png (renamed from content/en/news/lets-celebrate-hugos-5th-birthday/sunset-get.png)bin387442 -> 387442 bytes
-rw-r--r--docs/content/en/readfiles/README.md (renamed from content/en/readfiles/README.md)0
-rw-r--r--docs/content/en/readfiles/bfconfig.md (renamed from content/en/readfiles/bfconfig.md)0
-rw-r--r--docs/content/en/readfiles/dateformatting.md (renamed from content/en/readfiles/dateformatting.md)0
-rw-r--r--docs/content/en/readfiles/index.md (renamed from content/en/readfiles/index.md)0
-rw-r--r--docs/content/en/readfiles/pages-vs-site-pages.md (renamed from content/en/readfiles/pages-vs-site-pages.md)0
-rw-r--r--docs/content/en/readfiles/sectionvars.md (renamed from content/en/readfiles/sectionvars.md)0
-rw-r--r--docs/content/en/readfiles/testing.txt (renamed from content/en/readfiles/testing.txt)0
-rw-r--r--docs/content/en/showcase/1password-support/bio.md (renamed from content/en/showcase/1password-support/bio.md)0
-rw-r--r--docs/content/en/showcase/1password-support/featured.png (renamed from content/en/showcase/1password-support/featured.png)bin165718 -> 165718 bytes
-rw-r--r--docs/content/en/showcase/1password-support/index.md (renamed from content/en/showcase/1password-support/index.md)0
-rw-r--r--docs/content/en/showcase/arolla-cocoon/bio.md (renamed from content/en/showcase/arolla-cocoon/bio.md)0
-rw-r--r--docs/content/en/showcase/arolla-cocoon/featured-template.png (renamed from content/en/showcase/arolla-cocoon/featured-template.png)bin451984 -> 451984 bytes
-rw-r--r--docs/content/en/showcase/arolla-cocoon/index.md (renamed from content/en/showcase/arolla-cocoon/index.md)0
-rw-r--r--docs/content/en/showcase/fireship/bio.md (renamed from content/en/showcase/fireship/bio.md)0
-rw-r--r--docs/content/en/showcase/fireship/featured.png (renamed from content/en/showcase/fireship/featured.png)bin136959 -> 136959 bytes
-rw-r--r--docs/content/en/showcase/fireship/index.md (renamed from content/en/showcase/fireship/index.md)0
-rw-r--r--docs/content/en/showcase/flesland-flis/bio.md (renamed from content/en/showcase/flesland-flis/bio.md)0
-rw-r--r--docs/content/en/showcase/flesland-flis/featured.png (renamed from content/en/showcase/flesland-flis/featured.png)bin309284 -> 309284 bytes
-rw-r--r--docs/content/en/showcase/flesland-flis/index.md (renamed from content/en/showcase/flesland-flis/index.md)0
-rw-r--r--docs/content/en/showcase/forestry/bio.md (renamed from content/en/showcase/forestry/bio.md)0
-rw-r--r--docs/content/en/showcase/forestry/featured.png (renamed from content/en/showcase/forestry/featured.png)bin227009 -> 227009 bytes
-rw-r--r--docs/content/en/showcase/forestry/index.md (renamed from content/en/showcase/forestry/index.md)0
-rw-r--r--docs/content/en/showcase/hartwell-insurance/bio.md (renamed from content/en/showcase/hartwell-insurance/bio.md)0
-rw-r--r--docs/content/en/showcase/hartwell-insurance/featured.png (renamed from content/en/showcase/hartwell-insurance/featured.png)bin446603 -> 446603 bytes
-rw-r--r--docs/content/en/showcase/hartwell-insurance/hartwell-columns.png (renamed from content/en/showcase/hartwell-insurance/hartwell-columns.png)bin140272 -> 140272 bytes
-rw-r--r--docs/content/en/showcase/hartwell-insurance/hartwell-lighthouse.png (renamed from content/en/showcase/hartwell-insurance/hartwell-lighthouse.png)bin20649 -> 20649 bytes
-rw-r--r--docs/content/en/showcase/hartwell-insurance/hartwell-webpagetest.png (renamed from content/en/showcase/hartwell-insurance/hartwell-webpagetest.png)bin19503 -> 19503 bytes
-rw-r--r--docs/content/en/showcase/hartwell-insurance/index.md (renamed from content/en/showcase/hartwell-insurance/index.md)0
-rw-r--r--docs/content/en/showcase/letsencrypt/bio.md (renamed from content/en/showcase/letsencrypt/bio.md)0
-rw-r--r--docs/content/en/showcase/letsencrypt/featured.png (renamed from content/en/showcase/letsencrypt/featured.png)bin147459 -> 147459 bytes
-rw-r--r--docs/content/en/showcase/letsencrypt/index.md (renamed from content/en/showcase/letsencrypt/index.md)0
-rw-r--r--docs/content/en/showcase/linode/bio.md (renamed from content/en/showcase/linode/bio.md)0
-rw-r--r--docs/content/en/showcase/linode/featured.png (renamed from content/en/showcase/linode/featured.png)bin126664 -> 126664 bytes
-rw-r--r--docs/content/en/showcase/linode/index.md (renamed from content/en/showcase/linode/index.md)0
-rw-r--r--docs/content/en/showcase/over/bio.md (renamed from content/en/showcase/over/bio.md)0
-rw-r--r--docs/content/en/showcase/over/featured-over.png (renamed from content/en/showcase/over/featured-over.png)bin234973 -> 234973 bytes
-rw-r--r--docs/content/en/showcase/over/index.md (renamed from content/en/showcase/over/index.md)0
-rw-r--r--docs/content/en/showcase/pace-revenue-management/bio.md (renamed from content/en/showcase/pace-revenue-management/bio.md)0
-rw-r--r--docs/content/en/showcase/pace-revenue-management/featured.png (renamed from content/en/showcase/pace-revenue-management/featured.png)bin298908 -> 298908 bytes
-rw-r--r--docs/content/en/showcase/pace-revenue-management/index.md (renamed from content/en/showcase/pace-revenue-management/index.md)0
-rw-r--r--docs/content/en/showcase/pharmaseal/bio.md (renamed from content/en/showcase/pharmaseal/bio.md)0
-rw-r--r--docs/content/en/showcase/pharmaseal/featured-pharmaseal.png (renamed from content/en/showcase/pharmaseal/featured-pharmaseal.png)bin809599 -> 809599 bytes
-rw-r--r--docs/content/en/showcase/pharmaseal/index.md (renamed from content/en/showcase/pharmaseal/index.md)0
-rw-r--r--docs/content/en/showcase/quiply-employee-communications-app/bio.md (renamed from content/en/showcase/quiply-employee-communications-app/bio.md)0
-rw-r--r--docs/content/en/showcase/quiply-employee-communications-app/featured.png (renamed from content/en/showcase/quiply-employee-communications-app/featured.png)bin631206 -> 631206 bytes
-rw-r--r--docs/content/en/showcase/quiply-employee-communications-app/index.md (renamed from content/en/showcase/quiply-employee-communications-app/index.md)0
-rw-r--r--docs/content/en/showcase/small-multiples/bio.md (renamed from content/en/showcase/small-multiples/bio.md)0
-rw-r--r--docs/content/en/showcase/small-multiples/featured-small-multiples.png (renamed from content/en/showcase/small-multiples/featured-small-multiples.png)bin374273 -> 374273 bytes
-rw-r--r--docs/content/en/showcase/small-multiples/index.md (renamed from content/en/showcase/small-multiples/index.md)0
-rw-r--r--docs/content/en/showcase/stackimpact/bio.md (renamed from content/en/showcase/stackimpact/bio.md)0
-rw-r--r--docs/content/en/showcase/stackimpact/featured.png (renamed from content/en/showcase/stackimpact/featured.png)bin153794 -> 153794 bytes
-rw-r--r--docs/content/en/showcase/stackimpact/index.md (renamed from content/en/showcase/stackimpact/index.md)0
-rw-r--r--docs/content/en/showcase/template/bio.md (renamed from content/en/showcase/template/bio.md)0
-rw-r--r--docs/content/en/showcase/template/featured-template.png (renamed from content/en/showcase/template/featured-template.png)bin41270 -> 41270 bytes
-rw-r--r--docs/content/en/showcase/template/index.md (renamed from content/en/showcase/template/index.md)0
-rw-r--r--docs/content/en/showcase/tomango/bio.md (renamed from content/en/showcase/tomango/bio.md)0
-rw-r--r--docs/content/en/showcase/tomango/featured.png (renamed from content/en/showcase/tomango/featured.png)bin232791 -> 232791 bytes
-rw-r--r--docs/content/en/showcase/tomango/index.md (renamed from content/en/showcase/tomango/index.md)0
-rw-r--r--docs/content/en/templates/404.md (renamed from content/en/templates/404.md)0
-rw-r--r--docs/content/en/templates/_index.md (renamed from content/en/templates/_index.md)0
-rw-r--r--docs/content/en/templates/alternatives.md (renamed from content/en/templates/alternatives.md)0
-rw-r--r--docs/content/en/templates/base.md (renamed from content/en/templates/base.md)0
-rw-r--r--docs/content/en/templates/data-templates.md (renamed from content/en/templates/data-templates.md)0
-rw-r--r--docs/content/en/templates/files.md (renamed from content/en/templates/files.md)0
-rw-r--r--docs/content/en/templates/homepage.md (renamed from content/en/templates/homepage.md)0
-rw-r--r--docs/content/en/templates/internal.md (renamed from content/en/templates/internal.md)0
-rw-r--r--docs/content/en/templates/introduction.md (renamed from content/en/templates/introduction.md)0
-rw-r--r--docs/content/en/templates/lists.md (renamed from content/en/templates/lists.md)0
-rw-r--r--docs/content/en/templates/lookup-order.md (renamed from content/en/templates/lookup-order.md)0
-rw-r--r--docs/content/en/templates/menu-templates.md (renamed from content/en/templates/menu-templates.md)0
-rw-r--r--docs/content/en/templates/ordering-and-grouping.md (renamed from content/en/templates/ordering-and-grouping.md)0
-rw-r--r--docs/content/en/templates/output-formats.md (renamed from content/en/templates/output-formats.md)0
-rw-r--r--docs/content/en/templates/pagination.md (renamed from content/en/templates/pagination.md)0
-rw-r--r--docs/content/en/templates/partials.md (renamed from content/en/templates/partials.md)0
-rw-r--r--docs/content/en/templates/robots.md (renamed from content/en/templates/robots.md)0
-rw-r--r--docs/content/en/templates/rss.md (renamed from content/en/templates/rss.md)0
-rw-r--r--docs/content/en/templates/section-templates.md (renamed from content/en/templates/section-templates.md)0
-rw-r--r--docs/content/en/templates/shortcode-templates.md (renamed from content/en/templates/shortcode-templates.md)0
-rw-r--r--docs/content/en/templates/single-page-templates.md (renamed from content/en/templates/single-page-templates.md)0
-rw-r--r--docs/content/en/templates/sitemap-template.md (renamed from content/en/templates/sitemap-template.md)0
-rw-r--r--docs/content/en/templates/taxonomy-templates.md (renamed from content/en/templates/taxonomy-templates.md)0
-rw-r--r--docs/content/en/templates/template-debugging.md (renamed from content/en/templates/template-debugging.md)0
-rw-r--r--docs/content/en/templates/views.md (renamed from content/en/templates/views.md)0
-rw-r--r--docs/content/en/themes/_index.md (renamed from content/en/themes/_index.md)0
-rw-r--r--docs/content/en/themes/creating.md (renamed from content/en/themes/creating.md)0
-rw-r--r--docs/content/en/themes/installing-and-using-themes.md (renamed from content/en/themes/installing-and-using-themes.md)0
-rw-r--r--docs/content/en/themes/theme-components.md (renamed from content/en/themes/theme-components.md)0
-rw-r--r--docs/content/en/tools/_index.md (renamed from content/en/tools/_index.md)0
-rw-r--r--docs/content/en/tools/editors.md (renamed from content/en/tools/editors.md)0
-rw-r--r--docs/content/en/tools/frontends.md (renamed from content/en/tools/frontends.md)0
-rw-r--r--docs/content/en/tools/migrations.md85
-rw-r--r--docs/content/en/tools/other.md (renamed from content/en/tools/other.md)0
-rw-r--r--docs/content/en/tools/search.md (renamed from content/en/tools/search.md)0
-rw-r--r--docs/content/en/tools/starter-kits.md (renamed from content/en/tools/starter-kits.md)0
-rw-r--r--docs/content/en/troubleshooting/_index.md (renamed from content/en/troubleshooting/_index.md)0
-rw-r--r--docs/content/en/troubleshooting/build-performance.md (renamed from content/en/troubleshooting/build-performance.md)0
-rw-r--r--docs/content/en/troubleshooting/faq.md (renamed from content/en/troubleshooting/faq.md)0
-rw-r--r--docs/content/en/variables/_index.md (renamed from content/en/variables/_index.md)0
-rw-r--r--docs/content/en/variables/files.md (renamed from content/en/variables/files.md)0
-rw-r--r--docs/content/en/variables/git.md (renamed from content/en/variables/git.md)0
-rw-r--r--docs/content/en/variables/hugo.md (renamed from content/en/variables/hugo.md)0
-rw-r--r--docs/content/en/variables/menus.md (renamed from content/en/variables/menus.md)0
-rw-r--r--docs/content/en/variables/page.md (renamed from content/en/variables/page.md)0
-rw-r--r--docs/content/en/variables/shortcodes.md (renamed from content/en/variables/shortcodes.md)0
-rw-r--r--docs/content/en/variables/site.md (renamed from content/en/variables/site.md)0
-rw-r--r--docs/content/en/variables/sitemap.md (renamed from content/en/variables/sitemap.md)0
-rw-r--r--docs/content/en/variables/taxonomy.md (renamed from content/en/variables/taxonomy.md)0
-rw-r--r--docs/content/zh/_index.md (renamed from content/zh/_index.md)0
-rw-r--r--docs/content/zh/about/_index.md (renamed from content/zh/about/_index.md)0
-rw-r--r--docs/content/zh/content-management/_index.md (renamed from content/zh/content-management/_index.md)0
-rw-r--r--docs/content/zh/documentation.md (renamed from content/zh/documentation.md)0
-rw-r--r--docs/content/zh/news/_index.md (renamed from content/zh/news/_index.md)0
-rw-r--r--docs/content/zh/templates/_index.md (renamed from content/zh/templates/_index.md)0
-rw-r--r--docs/content/zh/templates/base.md (renamed from content/zh/templates/base.md)0
-rw-r--r--docs/data/articles.toml (renamed from data/articles.toml)0
-rw-r--r--docs/data/docs.json (renamed from data/docs.json)0
-rw-r--r--docs/data/homepagetweets.toml (renamed from data/homepagetweets.toml)0
-rw-r--r--docs/data/titles.toml (renamed from data/titles.toml)0
-rw-r--r--docs/layouts/index.rss.xml (renamed from layouts/index.rss.xml)0
-rw-r--r--docs/layouts/maintenance/list.html (renamed from layouts/maintenance/list.html)0
-rw-r--r--docs/layouts/partials/maintenance-pages-table.html (renamed from layouts/partials/maintenance-pages-table.html)0
-rw-r--r--docs/layouts/shortcodes/asciicast.html (renamed from layouts/shortcodes/asciicast.html)0
-rw-r--r--docs/layouts/shortcodes/chroma-lexers.html (renamed from layouts/shortcodes/chroma-lexers.html)0
-rw-r--r--docs/layouts/shortcodes/code.html (renamed from layouts/shortcodes/code.html)0
-rw-r--r--docs/layouts/shortcodes/datatable-filtered.html (renamed from layouts/shortcodes/datatable-filtered.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/docfile.html (renamed from layouts/shortcodes/docfile.html)0
-rw-r--r--docs/layouts/shortcodes/exfile.html (renamed from layouts/shortcodes/exfile.html)0
-rw-r--r--docs/layouts/shortcodes/exfm.html (renamed from layouts/shortcodes/exfm.html)0
-rw-r--r--docs/layouts/shortcodes/gh.html (renamed from layouts/shortcodes/gh.html)0
-rw-r--r--docs/layouts/shortcodes/ghrepo.html (renamed from layouts/shortcodes/ghrepo.html)0
-rw-r--r--docs/layouts/shortcodes/imgproc.html (renamed from layouts/shortcodes/imgproc.html)0
-rw-r--r--docs/layouts/shortcodes/nohighlight.html (renamed from layouts/shortcodes/nohighlight.html)0
-rw-r--r--docs/layouts/shortcodes/note.html (renamed from layouts/shortcodes/note.html)0
-rw-r--r--docs/layouts/shortcodes/output.html (renamed from layouts/shortcodes/output.html)0
-rw-r--r--docs/layouts/shortcodes/readfile.html (renamed from layouts/shortcodes/readfile.html)0
-rw-r--r--docs/layouts/shortcodes/tip.html (renamed from layouts/shortcodes/tip.html)0
-rw-r--r--docs/layouts/shortcodes/todo.html (renamed from layouts/shortcodes/todo.html)0
-rw-r--r--docs/layouts/shortcodes/warning.html (renamed from layouts/shortcodes/warning.html)0
-rw-r--r--docs/layouts/shortcodes/yt.html (renamed from layouts/shortcodes/yt.html)0
-rw-r--r--docs/netlify.toml (renamed from netlify.toml)0
-rwxr-xr-xdocs/pull-theme.sh (renamed from pull-theme.sh)0
-rw-r--r--docs/requirements.txt1
-rw-r--r--docs/resources/.gitattributes (renamed from resources/.gitattributes)0
-rw-r--r--docs/resources/_gen/assets/css/output/css/app.css_d11fe7b62c27961c87ecd0f2490357b9.content (renamed from resources/_gen/assets/css/output/css/app.css_d11fe7b62c27961c87ecd0f2490357b9.content)0
-rw-r--r--docs/resources/_gen/assets/css/output/css/app.css_d11fe7b62c27961c87ecd0f2490357b9.json (renamed from resources/_gen/assets/css/output/css/app.css_d11fe7b62c27961c87ecd0f2490357b9.json)0
-rw-r--r--docs/resources/_gen/assets/js/output/js/app.js_8848f55d07695b7ff7188138f23d69e3.content (renamed from resources/_gen/assets/js/output/js/app.js_8848f55d07695b7ff7188138f23d69e3.content)0
-rw-r--r--docs/resources/_gen/assets/js/output/js/app.js_8848f55d07695b7ff7188138f23d69e3.json (renamed from resources/_gen/assets/js/output/js/app.js_8848f55d07695b7ff7188138f23d69e3.json)0
-rw-r--r--docs/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q10_catmullrom.jpg (renamed from resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q10_catmullrom.jpg)bin1939 -> 1939 bytes
-rw-r--r--docs/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q75_catmullrom.jpg (renamed from resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q75_catmullrom.jpg)bin5109 -> 5109 bytes
-rw-r--r--docs/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_left.jpg (renamed from resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_left.jpg)bin1697 -> 1697 bytes
-rw-r--r--docs/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_right.jpg (renamed from resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_right.jpg)bin1661 -> 1661 bytes
-rw-r--r--docs/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x90_fit_q75_catmullrom.jpg (renamed from resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x90_fit_q75_catmullrom.jpg)bin1292 -> 1292 bytes
-rw-r--r--docs/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x200_fill_q75_catmullrom_smart1.jpg (renamed from resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x200_fill_q75_catmullrom_smart1.jpg)bin3570 -> 3570 bytes
-rw-r--r--docs/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q10_catmullrom.jpg (renamed from resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q10_catmullrom.jpg)bin1939 -> 1939 bytes
-rw-r--r--docs/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q75_catmullrom.jpg (renamed from resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q75_catmullrom.jpg)bin5109 -> 5109 bytes
-rw-r--r--docs/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_left.jpg (renamed from resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_left.jpg)bin1697 -> 1697 bytes
-rw-r--r--docs/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_right.jpg (renamed from resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_right.jpg)bin1661 -> 1661 bytes
-rw-r--r--docs/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x90_fit_q75_catmullrom.jpg (renamed from resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x90_fit_q75_catmullrom.jpg)bin1292 -> 1292 bytes
-rw-r--r--docs/resources/_gen/images/content-management/organization/1-featured-content-bundles_hu3e3ae7839b071119f32acaa20f204198_63640_300x0_resize_catmullrom_2.png (renamed from resources/_gen/images/content-management/organization/1-featured-content-bundles_hu3e3ae7839b071119f32acaa20f204198_63640_300x0_resize_catmullrom_2.png)bin30621 -> 30621 bytes
-rw-r--r--docs/resources/_gen/images/news/0.33-relnotes/featured-hugo-33-poster_hu45ce9da1cdea6ca61c5f4f5baccdcad4_70230_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.33-relnotes/featured-hugo-33-poster_hu45ce9da1cdea6ca61c5f4f5baccdcad4_70230_480x0_resize_catmullrom_2.png)bin31698 -> 31698 bytes
-rw-r--r--docs/resources/_gen/images/news/0.34-relnotes/featured-34-poster_hud8d73dc5df8d5a35383849a78eea35dd_78317_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.34-relnotes/featured-34-poster_hud8d73dc5df8d5a35383849a78eea35dd_78317_480x0_resize_catmullrom_2.png)bin34288 -> 34288 bytes
-rw-r--r--docs/resources/_gen/images/news/0.35-relnotes/featured-hugo-35-poster_hua42b1310dd72f60a34e02851ebf2f82e_88519_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.35-relnotes/featured-hugo-35-poster_hua42b1310dd72f60a34e02851ebf2f82e_88519_480x0_resize_catmullrom_2.png)bin37252 -> 37252 bytes
-rw-r--r--docs/resources/_gen/images/news/0.36-relnotes/featured-hugo-36-poster_huf2fee368f65c75d3878561ed4225c39a_67640_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.36-relnotes/featured-hugo-36-poster_huf2fee368f65c75d3878561ed4225c39a_67640_480x0_resize_catmullrom_2.png)bin30114 -> 30114 bytes
-rw-r--r--docs/resources/_gen/images/news/0.37-relnotes/featured-hugo-37-poster_hue9685d25c387d657b0640498bf6a10ee_186693_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.37-relnotes/featured-hugo-37-poster_hue9685d25c387d657b0640498bf6a10ee_186693_480x0_resize_catmullrom_2.png)bin60209 -> 60209 bytes
-rw-r--r--docs/resources/_gen/images/news/0.37-relnotes/featured-hugo-37-poster_hue9685d25c387d657b0640498bf6a10ee_186693_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.37-relnotes/featured-hugo-37-poster_hue9685d25c387d657b0640498bf6a10ee_186693_640x0_resize_catmullrom_2.png)bin100937 -> 100937 bytes
-rw-r--r--docs/resources/_gen/images/news/0.38-relnotes/featured-poster_hudf8012d38ef42d46a6cab1b31156bf3a_69978_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.38-relnotes/featured-poster_hudf8012d38ef42d46a6cab1b31156bf3a_69978_480x0_resize_catmullrom_2.png)bin30670 -> 30670 bytes
-rw-r--r--docs/resources/_gen/images/news/0.38-relnotes/featured-poster_hudf8012d38ef42d46a6cab1b31156bf3a_69978_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.38-relnotes/featured-poster_hudf8012d38ef42d46a6cab1b31156bf3a_69978_640x0_resize_catmullrom_2.png)bin50111 -> 50111 bytes
-rw-r--r--docs/resources/_gen/images/news/0.39-relnotes/featured-hugo-39-poster_hu3261e6e65defb4edf9f0fce20bf5f60d_217215_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.39-relnotes/featured-hugo-39-poster_hu3261e6e65defb4edf9f0fce20bf5f60d_217215_480x0_resize_catmullrom_2.png)bin76170 -> 76170 bytes
-rw-r--r--docs/resources/_gen/images/news/0.39-relnotes/featured-hugo-39-poster_hu3261e6e65defb4edf9f0fce20bf5f60d_217215_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.39-relnotes/featured-hugo-39-poster_hu3261e6e65defb4edf9f0fce20bf5f60d_217215_640x0_resize_catmullrom_2.png)bin127766 -> 127766 bytes
-rw-r--r--docs/resources/_gen/images/news/0.40-relnotes/featured-hugo-40-poster_hu20c69e2a166f65e329d8fbabe8d2cc58_69238_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.40-relnotes/featured-hugo-40-poster_hu20c69e2a166f65e329d8fbabe8d2cc58_69238_480x0_resize_catmullrom_2.png)bin32598 -> 32598 bytes
-rw-r--r--docs/resources/_gen/images/news/0.40-relnotes/featured-hugo-40-poster_hu20c69e2a166f65e329d8fbabe8d2cc58_69238_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.40-relnotes/featured-hugo-40-poster_hu20c69e2a166f65e329d8fbabe8d2cc58_69238_640x0_resize_catmullrom_2.png)bin51464 -> 51464 bytes
-rw-r--r--docs/resources/_gen/images/news/0.41-relnotes/featured-hugo-41-poster_hud81cd208cb270af61610509ee199ae20_67955_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.41-relnotes/featured-hugo-41-poster_hud81cd208cb270af61610509ee199ae20_67955_480x0_resize_catmullrom_2.png)bin29799 -> 29799 bytes
-rw-r--r--docs/resources/_gen/images/news/0.41-relnotes/featured-hugo-41-poster_hud81cd208cb270af61610509ee199ae20_67955_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.41-relnotes/featured-hugo-41-poster_hud81cd208cb270af61610509ee199ae20_67955_640x0_resize_catmullrom_2.png)bin47990 -> 47990 bytes
-rw-r--r--docs/resources/_gen/images/news/0.42-relnotes/featured-hugo-42-poster_hue0604c0846526b6d2f8ba376edd013b6_74852_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.42-relnotes/featured-hugo-42-poster_hue0604c0846526b6d2f8ba376edd013b6_74852_480x0_resize_catmullrom_2.png)bin32730 -> 32730 bytes
-rw-r--r--docs/resources/_gen/images/news/0.42-relnotes/featured-hugo-42-poster_hue0604c0846526b6d2f8ba376edd013b6_74852_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.42-relnotes/featured-hugo-42-poster_hue0604c0846526b6d2f8ba376edd013b6_74852_640x0_resize_catmullrom_2.png)bin52600 -> 52600 bytes
-rw-r--r--docs/resources/_gen/images/news/0.43-relnotes/featured-hugo-43-poster_hu3948fe44e4a966d8149b4bf077395057_78299_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.43-relnotes/featured-hugo-43-poster_hu3948fe44e4a966d8149b4bf077395057_78299_480x0_resize_catmullrom_2.png)bin36338 -> 36338 bytes
-rw-r--r--docs/resources/_gen/images/news/0.43-relnotes/featured-hugo-43-poster_hu3948fe44e4a966d8149b4bf077395057_78299_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.43-relnotes/featured-hugo-43-poster_hu3948fe44e4a966d8149b4bf077395057_78299_640x0_resize_catmullrom_2.png)bin57591 -> 57591 bytes
-rw-r--r--docs/resources/_gen/images/news/0.44-relnotes/featured-hugo-44-poster_hu6505d1982bab71bfe9c6c7adcedfd7f7_77631_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.44-relnotes/featured-hugo-44-poster_hu6505d1982bab71bfe9c6c7adcedfd7f7_77631_480x0_resize_catmullrom_2.png)bin35977 -> 35977 bytes
-rw-r--r--docs/resources/_gen/images/news/0.44-relnotes/featured-hugo-44-poster_hu6505d1982bab71bfe9c6c7adcedfd7f7_77631_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.44-relnotes/featured-hugo-44-poster_hu6505d1982bab71bfe9c6c7adcedfd7f7_77631_640x0_resize_catmullrom_2.png)bin57252 -> 57252 bytes
-rw-r--r--docs/resources/_gen/images/news/0.45-relnotes/featured-hugo-45-poster_huea79995576e3b93a3041ae824a391758_66863_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.45-relnotes/featured-hugo-45-poster_huea79995576e3b93a3041ae824a391758_66863_480x0_resize_catmullrom_2.png)bin30118 -> 30118 bytes
-rw-r--r--docs/resources/_gen/images/news/0.45-relnotes/featured-hugo-45-poster_huea79995576e3b93a3041ae824a391758_66863_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.45-relnotes/featured-hugo-45-poster_huea79995576e3b93a3041ae824a391758_66863_640x0_resize_catmullrom_2.png)bin47821 -> 47821 bytes
-rw-r--r--docs/resources/_gen/images/news/0.46-relnotes/featured-hugo-46-poster_hue04c7655caa254a1835311c9409185d8_68614_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.46-relnotes/featured-hugo-46-poster_hue04c7655caa254a1835311c9409185d8_68614_480x0_resize_catmullrom_2.png)bin30457 -> 30457 bytes
-rw-r--r--docs/resources/_gen/images/news/0.46-relnotes/featured-hugo-46-poster_hue04c7655caa254a1835311c9409185d8_68614_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.46-relnotes/featured-hugo-46-poster_hue04c7655caa254a1835311c9409185d8_68614_640x0_resize_catmullrom_2.png)bin49480 -> 49480 bytes
-rw-r--r--docs/resources/_gen/images/news/0.47-relnotes/featured-hugo-47-poster_hud3879b84908b49d38ac2cd1416f654ff_88288_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.47-relnotes/featured-hugo-47-poster_hud3879b84908b49d38ac2cd1416f654ff_88288_480x0_resize_catmullrom_2.png)bin38599 -> 38599 bytes
-rw-r--r--docs/resources/_gen/images/news/0.47-relnotes/featured-hugo-47-poster_hud3879b84908b49d38ac2cd1416f654ff_88288_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.47-relnotes/featured-hugo-47-poster_hud3879b84908b49d38ac2cd1416f654ff_88288_640x0_resize_catmullrom_2.png)bin61347 -> 61347 bytes
-rw-r--r--docs/resources/_gen/images/news/0.48-relnotes/featured-hugo-48-poster_hub95348423e80ff144dfee01d64fb9889_95358_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.48-relnotes/featured-hugo-48-poster_hub95348423e80ff144dfee01d64fb9889_95358_480x0_resize_catmullrom_2.png)bin42776 -> 42776 bytes
-rw-r--r--docs/resources/_gen/images/news/0.48-relnotes/featured-hugo-48-poster_hub95348423e80ff144dfee01d64fb9889_95358_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.48-relnotes/featured-hugo-48-poster_hub95348423e80ff144dfee01d64fb9889_95358_640x0_resize_catmullrom_2.png)bin67402 -> 67402 bytes
-rw-r--r--docs/resources/_gen/images/news/0.49-relnotes/featured-hugo-49-poster_hud9cdb0f9aa2ec95d28fc3f49c81e7940_66352_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.49-relnotes/featured-hugo-49-poster_hud9cdb0f9aa2ec95d28fc3f49c81e7940_66352_480x0_resize_catmullrom_2.png)bin31519 -> 31519 bytes
-rw-r--r--docs/resources/_gen/images/news/0.49-relnotes/featured-hugo-49-poster_hud9cdb0f9aa2ec95d28fc3f49c81e7940_66352_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.49-relnotes/featured-hugo-49-poster_hud9cdb0f9aa2ec95d28fc3f49c81e7940_66352_640x0_resize_catmullrom_2.png)bin49917 -> 49917 bytes
-rw-r--r--docs/resources/_gen/images/news/0.50-relnotes/featured-hugo-50-poster_hudcbbb9a5a0079d08447101e6cfae6e40_227240_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.50-relnotes/featured-hugo-50-poster_hudcbbb9a5a0079d08447101e6cfae6e40_227240_480x0_resize_catmullrom_2.png)bin88975 -> 88975 bytes
-rw-r--r--docs/resources/_gen/images/news/0.50-relnotes/featured-hugo-50-poster_hudcbbb9a5a0079d08447101e6cfae6e40_227240_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.50-relnotes/featured-hugo-50-poster_hudcbbb9a5a0079d08447101e6cfae6e40_227240_640x0_resize_catmullrom_2.png)bin145297 -> 145297 bytes
-rw-r--r--docs/resources/_gen/images/news/0.51-relnotes/featured-hugo-51-poster_hu25ab021d1365edeedf46d92fdb888ccf_117678_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.51-relnotes/featured-hugo-51-poster_hu25ab021d1365edeedf46d92fdb888ccf_117678_480x0_resize_catmullrom_2.png)bin48159 -> 48159 bytes
-rw-r--r--docs/resources/_gen/images/news/0.51-relnotes/featured-hugo-51-poster_hu25ab021d1365edeedf46d92fdb888ccf_117678_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.51-relnotes/featured-hugo-51-poster_hu25ab021d1365edeedf46d92fdb888ccf_117678_640x0_resize_catmullrom_2.png)bin78187 -> 78187 bytes
-rw-r--r--docs/resources/_gen/images/news/0.52-relnotes/featured-hugo-52-poster_hu7f2ed09038efabda07872a275a935ada_336810_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.52-relnotes/featured-hugo-52-poster_hu7f2ed09038efabda07872a275a935ada_336810_480x0_resize_catmullrom_2.png)bin105061 -> 105061 bytes
-rw-r--r--docs/resources/_gen/images/news/0.52-relnotes/featured-hugo-52-poster_hu7f2ed09038efabda07872a275a935ada_336810_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.52-relnotes/featured-hugo-52-poster_hu7f2ed09038efabda07872a275a935ada_336810_640x0_resize_catmullrom_2.png)bin180710 -> 180710 bytes
-rw-r--r--docs/resources/_gen/images/news/0.53-relnotes/featured-hugo-53-poster_hu3f68fc193ad172155ee35a0be89133bf_110427_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.53-relnotes/featured-hugo-53-poster_hu3f68fc193ad172155ee35a0be89133bf_110427_480x0_resize_catmullrom_2.png)bin66442 -> 66442 bytes
-rw-r--r--docs/resources/_gen/images/news/0.53-relnotes/featured-hugo-53-poster_hu3f68fc193ad172155ee35a0be89133bf_110427_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.53-relnotes/featured-hugo-53-poster_hu3f68fc193ad172155ee35a0be89133bf_110427_640x0_resize_catmullrom_2.png)bin108732 -> 108732 bytes
-rw-r--r--docs/resources/_gen/images/news/0.54.0-relnotes/featured-hugo-54.0-poster_hufa0b7b755124a76fe71c5c70a25724c2_59805_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.54.0-relnotes/featured-hugo-54.0-poster_hufa0b7b755124a76fe71c5c70a25724c2_59805_480x0_resize_catmullrom_2.png)bin28700 -> 28700 bytes
-rw-r--r--docs/resources/_gen/images/news/0.54.0-relnotes/featured-hugo-54.0-poster_hufa0b7b755124a76fe71c5c70a25724c2_59805_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.54.0-relnotes/featured-hugo-54.0-poster_hufa0b7b755124a76fe71c5c70a25724c2_59805_640x0_resize_catmullrom_2.png)bin45783 -> 45783 bytes
-rw-r--r--docs/resources/_gen/images/news/0.55.0-relnotes/featured_hu9474666a09966109e944f93e1ecf78c0_1221797_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.55.0-relnotes/featured_hu9474666a09966109e944f93e1ecf78c0_1221797_480x0_resize_catmullrom_2.png)bin173126 -> 173126 bytes
-rw-r--r--docs/resources/_gen/images/news/0.55.0-relnotes/featured_hu9474666a09966109e944f93e1ecf78c0_1221797_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/0.55.0-relnotes/featured_hu9474666a09966109e944f93e1ecf78c0_1221797_640x0_resize_catmullrom_2.png)bin307282 -> 307282 bytes
-rw-r--r--docs/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/featured_hu30cb938a182ebd06b50ed15d006d8f64_179291_480x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/featured_hu30cb938a182ebd06b50ed15d006d8f64_179291_480x0_resize_catmullrom_2.png)bin60638 -> 60638 bytes
-rw-r--r--docs/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/featured_hu30cb938a182ebd06b50ed15d006d8f64_179291_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/featured_hu30cb938a182ebd06b50ed15d006d8f64_179291_640x0_resize_catmullrom_2.png)bin88936 -> 88936 bytes
-rw-r--r--docs/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/graph-stars_hu169ba15a8bcaf4ddd6a5a1aa8505c448_15599_600x400_fit_catmullrom_2.png (renamed from resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/graph-stars_hu169ba15a8bcaf4ddd6a5a1aa8505c448_15599_600x400_fit_catmullrom_2.png)bin24246 -> 24246 bytes
-rw-r--r--docs/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/graph-themes_hu25707bee0ec3007199f71bb29226f30c_16956_600x400_fit_catmullrom_2.png (renamed from resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/graph-themes_hu25707bee0ec3007199f71bb29226f30c_16956_600x400_fit_catmullrom_2.png)bin26574 -> 26574 bytes
-rw-r--r--docs/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/sunset-get_hu69849a7cdb847c2393a7b3a7f6061c86_387442_600x300_fill_catmullrom_smart1_2.png (renamed from resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/sunset-get_hu69849a7cdb847c2393a7b3a7f6061c86_387442_600x300_fill_catmullrom_smart1_2.png)bin104931 -> 104931 bytes
-rw-r--r--docs/resources/_gen/images/showcase/1password-support/featured_hu870838c23243880857c2e418dd7ac099_165718_1024x512_fill_catmullrom_top_2.png (renamed from resources/_gen/images/showcase/1password-support/featured_hu870838c23243880857c2e418dd7ac099_165718_1024x512_fill_catmullrom_top_2.png)bin128594 -> 128594 bytes
-rw-r--r--docs/resources/_gen/images/showcase/1password-support/featured_hu870838c23243880857c2e418dd7ac099_165718_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/showcase/1password-support/featured_hu870838c23243880857c2e418dd7ac099_165718_640x0_resize_catmullrom_2.png)bin52357 -> 52357 bytes
-rw-r--r--docs/resources/_gen/images/showcase/1password-support/featured_hu870838c23243880857c2e418dd7ac099_165718_8714c8c914d32c12c7eb833a42713319.png (renamed from resources/_gen/images/showcase/1password-support/featured_hu870838c23243880857c2e418dd7ac099_165718_8714c8c914d32c12c7eb833a42713319.png)bin36323 -> 36323 bytes
-rw-r--r--docs/resources/_gen/images/showcase/arolla-cocoon/featured-template_hu22aab819ab27e4f878d1ff0b7cf78050_451984_1024x512_fill_catmullrom_top_2.png (renamed from resources/_gen/images/showcase/arolla-cocoon/featured-template_hu22aab819ab27e4f878d1ff0b7cf78050_451984_1024x512_fill_catmullrom_top_2.png)bin227795 -> 227795 bytes
-rw-r--r--docs/resources/_gen/images/showcase/arolla-cocoon/featured-template_hu22aab819ab27e4f878d1ff0b7cf78050_451984_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/showcase/arolla-cocoon/featured-template_hu22aab819ab27e4f878d1ff0b7cf78050_451984_640x0_resize_catmullrom_2.png)bin113846 -> 113846 bytes
-rw-r--r--docs/resources/_gen/images/showcase/arolla-cocoon/featured-template_hu22aab819ab27e4f878d1ff0b7cf78050_451984_ea485187288cde4b679b149346aca832.png (renamed from resources/_gen/images/showcase/arolla-cocoon/featured-template_hu22aab819ab27e4f878d1ff0b7cf78050_451984_ea485187288cde4b679b149346aca832.png)bin68265 -> 68265 bytes
-rw-r--r--docs/resources/_gen/images/showcase/fireship/featured_hu3bba74627b7e233d29d5aecb29c8d0f1_136959_1024x512_fill_catmullrom_top_2.png (renamed from resources/_gen/images/showcase/fireship/featured_hu3bba74627b7e233d29d5aecb29c8d0f1_136959_1024x512_fill_catmullrom_top_2.png)bin98052 -> 98052 bytes
-rw-r--r--docs/resources/_gen/images/showcase/fireship/featured_hu3bba74627b7e233d29d5aecb29c8d0f1_136959_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/showcase/fireship/featured_hu3bba74627b7e233d29d5aecb29c8d0f1_136959_640x0_resize_catmullrom_2.png)bin45700 -> 45700 bytes
-rw-r--r--docs/resources/_gen/images/showcase/fireship/featured_hu3bba74627b7e233d29d5aecb29c8d0f1_136959_9bf5371384e80c9f59e1f5e018440c34.png (renamed from resources/_gen/images/showcase/fireship/featured_hu3bba74627b7e233d29d5aecb29c8d0f1_136959_9bf5371384e80c9f59e1f5e018440c34.png)bin32680 -> 32680 bytes
-rw-r--r--docs/resources/_gen/images/showcase/flesland-flis/featured_hue4fd3c0e7519777bd75019750a0f5391_309284_1024x512_fill_catmullrom_top_2.png (renamed from resources/_gen/images/showcase/flesland-flis/featured_hue4fd3c0e7519777bd75019750a0f5391_309284_1024x512_fill_catmullrom_top_2.png)bin177628 -> 177628 bytes
-rw-r--r--docs/resources/_gen/images/showcase/flesland-flis/featured_hue4fd3c0e7519777bd75019750a0f5391_309284_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/showcase/flesland-flis/featured_hue4fd3c0e7519777bd75019750a0f5391_309284_640x0_resize_catmullrom_2.png)bin80505 -> 80505 bytes
-rw-r--r--docs/resources/_gen/images/showcase/flesland-flis/featured_hue4fd3c0e7519777bd75019750a0f5391_309284_f66ed2dc2e475b0cb21d76296890c5a2.png (renamed from resources/_gen/images/showcase/flesland-flis/featured_hue4fd3c0e7519777bd75019750a0f5391_309284_f66ed2dc2e475b0cb21d76296890c5a2.png)bin55651 -> 55651 bytes
-rw-r--r--docs/resources/_gen/images/showcase/forestry/featured_hu77de7d99834fa13b854b7fc62e2912a7_227009_1024x512_fill_catmullrom_top_2.png (renamed from resources/_gen/images/showcase/forestry/featured_hu77de7d99834fa13b854b7fc62e2912a7_227009_1024x512_fill_catmullrom_top_2.png)bin144909 -> 144909 bytes
-rw-r--r--docs/resources/_gen/images/showcase/forestry/featured_hu77de7d99834fa13b854b7fc62e2912a7_227009_192a300d3ccaa4371c674791fb50a62c.png (renamed from resources/_gen/images/showcase/forestry/featured_hu77de7d99834fa13b854b7fc62e2912a7_227009_192a300d3ccaa4371c674791fb50a62c.png)bin45100 -> 45100 bytes
-rw-r--r--docs/resources/_gen/images/showcase/forestry/featured_hu77de7d99834fa13b854b7fc62e2912a7_227009_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/showcase/forestry/featured_hu77de7d99834fa13b854b7fc62e2912a7_227009_640x0_resize_catmullrom_2.png)bin64714 -> 64714 bytes
-rw-r--r--docs/resources/_gen/images/showcase/hartwell-insurance/featured_hu642e730c6f819b15fc6ebbaa25b0243f_446603_1024x512_fill_catmullrom_top_2.png (renamed from resources/_gen/images/showcase/hartwell-insurance/featured_hu642e730c6f819b15fc6ebbaa25b0243f_446603_1024x512_fill_catmullrom_top_2.png)bin283187 -> 283187 bytes
-rw-r--r--docs/resources/_gen/images/showcase/hartwell-insurance/featured_hu642e730c6f819b15fc6ebbaa25b0243f_446603_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/showcase/hartwell-insurance/featured_hu642e730c6f819b15fc6ebbaa25b0243f_446603_640x0_resize_catmullrom_2.png)bin119036 -> 119036 bytes
-rw-r--r--docs/resources/_gen/images/showcase/hartwell-insurance/featured_hu642e730c6f819b15fc6ebbaa25b0243f_446603_a6f43693b7589a8d91c844654967eb51.png (renamed from resources/_gen/images/showcase/hartwell-insurance/featured_hu642e730c6f819b15fc6ebbaa25b0243f_446603_a6f43693b7589a8d91c844654967eb51.png)bin78247 -> 78247 bytes
-rw-r--r--docs/resources/_gen/images/showcase/letsencrypt/featured_hu51cfa254cfc1fb105704d2cdd6ae4737_147459_1024x512_fill_catmullrom_top_2.png (renamed from resources/_gen/images/showcase/letsencrypt/featured_hu51cfa254cfc1fb105704d2cdd6ae4737_147459_1024x512_fill_catmullrom_top_2.png)bin111430 -> 111430 bytes
-rw-r--r--docs/resources/_gen/images/showcase/letsencrypt/featured_hu51cfa254cfc1fb105704d2cdd6ae4737_147459_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/showcase/letsencrypt/featured_hu51cfa254cfc1fb105704d2cdd6ae4737_147459_640x0_resize_catmullrom_2.png)bin50540 -> 50540 bytes
-rw-r--r--docs/resources/_gen/images/showcase/letsencrypt/featured_hu51cfa254cfc1fb105704d2cdd6ae4737_147459_825bc0f79626434a7ab711238e84984a.png (renamed from resources/_gen/images/showcase/letsencrypt/featured_hu51cfa254cfc1fb105704d2cdd6ae4737_147459_825bc0f79626434a7ab711238e84984a.png)bin36458 -> 36458 bytes
-rw-r--r--docs/resources/_gen/images/showcase/linode/featured_hu6acc14b2375e47c4c764fef09fdb54c0_126664_1024x512_fill_catmullrom_top_2.png (renamed from resources/_gen/images/showcase/linode/featured_hu6acc14b2375e47c4c764fef09fdb54c0_126664_1024x512_fill_catmullrom_top_2.png)bin57772 -> 57772 bytes
-rw-r--r--docs/resources/_gen/images/showcase/linode/featured_hu6acc14b2375e47c4c764fef09fdb54c0_126664_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/showcase/linode/featured_hu6acc14b2375e47c4c764fef09fdb54c0_126664_640x0_resize_catmullrom_2.png)bin29073 -> 29073 bytes
-rw-r--r--docs/resources/_gen/images/showcase/linode/featured_hu6acc14b2375e47c4c764fef09fdb54c0_126664_97b33e8221e700cd517d4ce317c69e48.png (renamed from resources/_gen/images/showcase/linode/featured_hu6acc14b2375e47c4c764fef09fdb54c0_126664_97b33e8221e700cd517d4ce317c69e48.png)bin21333 -> 21333 bytes
-rw-r--r--docs/resources/_gen/images/showcase/over/featured-over_hu778fbd1f621ca5db45e30107849dc7c9_234973_1024x512_fill_catmullrom_top_2.png (renamed from resources/_gen/images/showcase/over/featured-over_hu778fbd1f621ca5db45e30107849dc7c9_234973_1024x512_fill_catmullrom_top_2.png)bin116441 -> 116441 bytes
-rw-r--r--docs/resources/_gen/images/showcase/over/featured-over_hu778fbd1f621ca5db45e30107849dc7c9_234973_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/showcase/over/featured-over_hu778fbd1f621ca5db45e30107849dc7c9_234973_640x0_resize_catmullrom_2.png)bin55485 -> 55485 bytes
-rw-r--r--docs/resources/_gen/images/showcase/over/featured-over_hu778fbd1f621ca5db45e30107849dc7c9_234973_fea71f0b8a2baebaf03af6e3be6229bb.png (renamed from resources/_gen/images/showcase/over/featured-over_hu778fbd1f621ca5db45e30107849dc7c9_234973_fea71f0b8a2baebaf03af6e3be6229bb.png)bin40217 -> 40217 bytes
-rw-r--r--docs/resources/_gen/images/showcase/pace-revenue-management/featured_hu143b6afebcd8780a08aa0a9f8e95dd02_298908_1024x512_fill_catmullrom_top_2.png (renamed from resources/_gen/images/showcase/pace-revenue-management/featured_hu143b6afebcd8780a08aa0a9f8e95dd02_298908_1024x512_fill_catmullrom_top_2.png)bin169210 -> 169210 bytes
-rw-r--r--docs/resources/_gen/images/showcase/pace-revenue-management/featured_hu143b6afebcd8780a08aa0a9f8e95dd02_298908_7e3f008d047fb3522bf02df4e9229522.png (renamed from resources/_gen/images/showcase/pace-revenue-management/featured_hu143b6afebcd8780a08aa0a9f8e95dd02_298908_7e3f008d047fb3522bf02df4e9229522.png)bin53399 -> 53399 bytes
-rw-r--r--docs/resources/_gen/images/showcase/pharmaseal/featured-pharmaseal_hu8b2836502d9d0176aaacb83c98b8f063_809599_1024x512_fill_catmullrom_top_2.png (renamed from resources/_gen/images/showcase/pharmaseal/featured-pharmaseal_hu8b2836502d9d0176aaacb83c98b8f063_809599_1024x512_fill_catmullrom_top_2.png)bin119985 -> 119985 bytes
-rw-r--r--docs/resources/_gen/images/showcase/pharmaseal/featured-pharmaseal_hu8b2836502d9d0176aaacb83c98b8f063_809599_5d0cd50b49fef5d99b816cd049191f5e.png (renamed from resources/_gen/images/showcase/pharmaseal/featured-pharmaseal_hu8b2836502d9d0176aaacb83c98b8f063_809599_5d0cd50b49fef5d99b816cd049191f5e.png)bin43147 -> 43147 bytes
-rw-r--r--docs/resources/_gen/images/showcase/pharmaseal/featured-pharmaseal_hu8b2836502d9d0176aaacb83c98b8f063_809599_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/showcase/pharmaseal/featured-pharmaseal_hu8b2836502d9d0176aaacb83c98b8f063_809599_640x0_resize_catmullrom_2.png)bin59087 -> 59087 bytes
-rw-r--r--docs/resources/_gen/images/showcase/quiply-employee-communications-app/featured_hua0e0d1ed0f0bc15921e78476b0c86c95_631206_1024x512_fill_catmullrom_top_2.png (renamed from resources/_gen/images/showcase/quiply-employee-communications-app/featured_hua0e0d1ed0f0bc15921e78476b0c86c95_631206_1024x512_fill_catmullrom_top_2.png)bin280168 -> 280168 bytes
-rw-r--r--docs/resources/_gen/images/showcase/quiply-employee-communications-app/featured_hua0e0d1ed0f0bc15921e78476b0c86c95_631206_3b6053b86d6afebe8262ece1955ed6cf.png (renamed from resources/_gen/images/showcase/quiply-employee-communications-app/featured_hua0e0d1ed0f0bc15921e78476b0c86c95_631206_3b6053b86d6afebe8262ece1955ed6cf.png)bin89438 -> 89438 bytes
-rw-r--r--docs/resources/_gen/images/showcase/quiply-employee-communications-app/featured_hua0e0d1ed0f0bc15921e78476b0c86c95_631206_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/showcase/quiply-employee-communications-app/featured_hua0e0d1ed0f0bc15921e78476b0c86c95_631206_640x0_resize_catmullrom_2.png)bin129787 -> 129787 bytes
-rw-r--r--docs/resources/_gen/images/showcase/stackimpact/featured_hu863cdba7b6e18bb95f64289a25912f5c_153794_1024x512_fill_catmullrom_top_2.png (renamed from resources/_gen/images/showcase/stackimpact/featured_hu863cdba7b6e18bb95f64289a25912f5c_153794_1024x512_fill_catmullrom_top_2.png)bin97618 -> 97618 bytes
-rw-r--r--docs/resources/_gen/images/showcase/stackimpact/featured_hu863cdba7b6e18bb95f64289a25912f5c_153794_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/showcase/stackimpact/featured_hu863cdba7b6e18bb95f64289a25912f5c_153794_640x0_resize_catmullrom_2.png)bin47639 -> 47639 bytes
-rw-r--r--docs/resources/_gen/images/showcase/stackimpact/featured_hu863cdba7b6e18bb95f64289a25912f5c_153794_671a5c232ffa27a2cf198d2c39f253eb.png (renamed from resources/_gen/images/showcase/stackimpact/featured_hu863cdba7b6e18bb95f64289a25912f5c_153794_671a5c232ffa27a2cf198d2c39f253eb.png)bin33218 -> 33218 bytes
-rw-r--r--docs/resources/_gen/images/showcase/template/featured-template_hu2f0d11388f944348b232a431caeb965b_41270_0be9b039f9029effab69b9239e224cf7.png (renamed from resources/_gen/images/showcase/template/featured-template_hu2f0d11388f944348b232a431caeb965b_41270_0be9b039f9029effab69b9239e224cf7.png)bin10934 -> 10934 bytes
-rw-r--r--docs/resources/_gen/images/showcase/template/featured-template_hu2f0d11388f944348b232a431caeb965b_41270_1024x512_fill_catmullrom_top_2.png (renamed from resources/_gen/images/showcase/template/featured-template_hu2f0d11388f944348b232a431caeb965b_41270_1024x512_fill_catmullrom_top_2.png)bin30468 -> 30468 bytes
-rw-r--r--docs/resources/_gen/images/showcase/tomango/featured_hube95635899674dbe563e9fac9518dc5a_232791_1024x512_fill_catmullrom_top_2.png (renamed from resources/_gen/images/showcase/tomango/featured_hube95635899674dbe563e9fac9518dc5a_232791_1024x512_fill_catmullrom_top_2.png)bin119968 -> 119968 bytes
-rw-r--r--docs/resources/_gen/images/showcase/tomango/featured_hube95635899674dbe563e9fac9518dc5a_232791_640x0_resize_catmullrom_2.png (renamed from resources/_gen/images/showcase/tomango/featured_hube95635899674dbe563e9fac9518dc5a_232791_640x0_resize_catmullrom_2.png)bin61107 -> 61107 bytes
-rw-r--r--docs/resources/_gen/images/showcase/tomango/featured_hube95635899674dbe563e9fac9518dc5a_232791_6dfd850dc877e20e2554751f779e5953.png (renamed from resources/_gen/images/showcase/tomango/featured_hube95635899674dbe563e9fac9518dc5a_232791_6dfd850dc877e20e2554751f779e5953.png)bin41947 -> 41947 bytes
-rw-r--r--docs/src/css/_chroma.css (renamed from src/css/_chroma.css)0
-rw-r--r--docs/src/package-lock.json (renamed from src/package-lock.json)0
-rw-r--r--docs/static/apple-touch-icon.png (renamed from static/apple-touch-icon.png)bin7993 -> 7993 bytes
-rw-r--r--docs/static/css/hugofont.css (renamed from static/css/hugofont.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)bin15086 -> 15086 bytes
-rw-r--r--docs/static/fonts/hugo.eot (renamed from static/fonts/hugo.eot)bin16380 -> 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)bin16228 -> 16228 bytes
-rw-r--r--docs/static/fonts/hugo.woff (renamed from static/fonts/hugo.woff)bin11728 -> 11728 bytes
-rw-r--r--docs/static/images/blog/hugo-26-poster.png (renamed from static/images/blog/hugo-26-poster.png)bin69207 -> 69207 bytes
-rw-r--r--docs/static/images/blog/hugo-27-poster.png (renamed from static/images/blog/hugo-27-poster.png)bin79893 -> 79893 bytes
-rw-r--r--docs/static/images/blog/hugo-28-poster.png (renamed from static/images/blog/hugo-28-poster.png)bin116760 -> 116760 bytes
-rw-r--r--docs/static/images/blog/hugo-29-poster.png (renamed from static/images/blog/hugo-29-poster.png)bin123034 -> 123034 bytes
-rw-r--r--docs/static/images/blog/hugo-30-poster.png (renamed from static/images/blog/hugo-30-poster.png)bin123192 -> 123192 bytes
-rw-r--r--docs/static/images/blog/hugo-31-poster.png (renamed from static/images/blog/hugo-31-poster.png)bin65077 -> 65077 bytes
-rw-r--r--docs/static/images/blog/hugo-32-poster.png (renamed from static/images/blog/hugo-32-poster.png)bin95867 -> 95867 bytes
-rw-r--r--docs/static/images/blog/hugo-bug-poster.png (renamed from static/images/blog/hugo-bug-poster.png)bin74141 -> 74141 bytes
-rw-r--r--docs/static/images/blog/hugo-http2-push.png (renamed from static/images/blog/hugo-http2-push.png)bin20544 -> 20544 bytes
-rw-r--r--docs/static/images/blog/sunset.jpg (renamed from static/images/blog/sunset.jpg)bin90587 -> 90587 bytes
-rw-r--r--docs/static/images/contribute/development/accept-cla.png (renamed from static/images/contribute/development/accept-cla.png)bin33286 -> 33286 bytes
-rw-r--r--docs/static/images/contribute/development/ci-errors.png (renamed from static/images/contribute/development/ci-errors.png)bin124801 -> 124801 bytes
-rw-r--r--docs/static/images/contribute/development/copy-remote-url.png (renamed from static/images/contribute/development/copy-remote-url.png)bin10570 -> 10570 bytes
-rw-r--r--docs/static/images/contribute/development/forking-a-repository.png (renamed from static/images/contribute/development/forking-a-repository.png)bin6759 -> 6759 bytes
-rw-r--r--docs/static/images/contribute/development/open-pull-request.png (renamed from static/images/contribute/development/open-pull-request.png)bin59990 -> 59990 bytes
-rw-r--r--docs/static/images/gohugoio-card-1.png (renamed from static/images/gohugoio-card-1.png)bin73881 -> 73881 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-nanobox/hugo-server.png (renamed from static/images/hosting-and-deployment/deployment-with-nanobox/hugo-server.png)bin123619 -> 123619 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-nanobox/hugo-with-nanobox.png (renamed from static/images/hosting-and-deployment/deployment-with-nanobox/hugo-with-nanobox.png)bin15998 -> 15998 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-nanobox/nanobox-deploy-dry-run.png (renamed from static/images/hosting-and-deployment/deployment-with-nanobox/nanobox-deploy-dry-run.png)bin67571 -> 67571 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-nanobox/nanobox-run.png (renamed from static/images/hosting-and-deployment/deployment-with-nanobox/nanobox-run.png)bin121995 -> 121995 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-wercker/adding-a-github-pages-step.png (renamed from static/images/hosting-and-deployment/deployment-with-wercker/adding-a-github-pages-step.png)bin49765 -> 49765 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-wercker/adding-the-project-to-github.png (renamed from static/images/hosting-and-deployment/deployment-with-wercker/adding-the-project-to-github.png)bin67637 -> 67637 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-wercker/and-we-ve-got-an-app.png (renamed from static/images/hosting-and-deployment/deployment-with-wercker/and-we-ve-got-an-app.png)bin45696 -> 45696 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-wercker/configure-the-deploy-step.png (renamed from static/images/hosting-and-deployment/deployment-with-wercker/configure-the-deploy-step.png)bin91874 -> 91874 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-wercker/creating-a-basic-hugo-site.png (renamed from static/images/hosting-and-deployment/deployment-with-wercker/creating-a-basic-hugo-site.png)bin34409 -> 34409 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-wercker/public-or-not.png (renamed from static/images/hosting-and-deployment/deployment-with-wercker/public-or-not.png)bin16659 -> 16659 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-wercker/using-hugo-build.png (renamed from static/images/hosting-and-deployment/deployment-with-wercker/using-hugo-build.png)bin14897 -> 14897 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-access.png (renamed from static/images/hosting-and-deployment/deployment-with-wercker/wercker-access.png)bin60815 -> 60815 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-account-settings.png (renamed from static/images/hosting-and-deployment/deployment-with-wercker/wercker-account-settings.png)bin10375 -> 10375 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-add-app.png (renamed from static/images/hosting-and-deployment/deployment-with-wercker/wercker-add-app.png)bin46966 -> 46966 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-git-connections.png (renamed from static/images/hosting-and-deployment/deployment-with-wercker/wercker-git-connections.png)bin27003 -> 27003 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-search.png (renamed from static/images/hosting-and-deployment/deployment-with-wercker/wercker-search.png)bin31555 -> 31555 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-select-owner.png (renamed from static/images/hosting-and-deployment/deployment-with-wercker/wercker-select-owner.png)bin19496 -> 19496 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-select-repository.png (renamed from static/images/hosting-and-deployment/deployment-with-wercker/wercker-select-repository.png)bin30944 -> 30944 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-sign-up-page.png (renamed from static/images/hosting-and-deployment/deployment-with-wercker/wercker-sign-up-page.png)bin23973 -> 23973 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-sign-up.png (renamed from static/images/hosting-and-deployment/deployment-with-wercker/wercker-sign-up.png)bin159610 -> 159610 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/deployment-with-wercker/werckeryml.png (renamed from static/images/hosting-and-deployment/deployment-with-wercker/werckeryml.png)bin52440 -> 52440 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/hosting-on-aws-amplify/amplify-build-settings.png (renamed from static/images/hosting-and-deployment/hosting-on-aws-amplify/amplify-build-settings.png)bin110416 -> 110416 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/hosting-on-aws-amplify/amplify-connect-repo.gif (renamed from static/images/hosting-and-deployment/hosting-on-aws-amplify/amplify-connect-repo.gif)bin2880775 -> 2880775 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/hosting-on-aws-amplify/amplify-gettingstarted.png (renamed from static/images/hosting-and-deployment/hosting-on-aws-amplify/amplify-gettingstarted.png)bin66505 -> 66505 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/hosting-on-bitbucket/bitbucket-blog-post.png (renamed from static/images/hosting-and-deployment/hosting-on-bitbucket/bitbucket-blog-post.png)bin37585 -> 37585 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/hosting-on-bitbucket/bitbucket-create-repo.png (renamed from static/images/hosting-and-deployment/hosting-on-bitbucket/bitbucket-create-repo.png)bin24689 -> 24689 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/hosting-on-keycdn/keycdn-pull-zone.png (renamed from static/images/hosting-and-deployment/hosting-on-keycdn/keycdn-pull-zone.png)bin209459 -> 209459 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/hosting-on-keycdn/secret-api-key.png (renamed from static/images/hosting-and-deployment/hosting-on-keycdn/secret-api-key.png)bin213381 -> 213381 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/hosting-on-keycdn/secret-zone-id.png (renamed from static/images/hosting-and-deployment/hosting-on-keycdn/secret-zone-id.png)bin205500 -> 205500 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-add-new-site.jpg (renamed from static/images/hosting-and-deployment/hosting-on-netlify/netlify-add-new-site.jpg)bin25643 -> 25643 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-authorize-added-permissions.jpg (renamed from static/images/hosting-and-deployment/hosting-on-netlify/netlify-authorize-added-permissions.jpg)bin46713 -> 46713 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-create-new-site-step-1.jpg (renamed from static/images/hosting-and-deployment/hosting-on-netlify/netlify-create-new-site-step-1.jpg)bin37855 -> 37855 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-create-new-site-step-2.jpg (renamed from static/images/hosting-and-deployment/hosting-on-netlify/netlify-create-new-site-step-2.jpg)bin42233 -> 42233 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-create-new-site-step-3.jpg (renamed from static/images/hosting-and-deployment/hosting-on-netlify/netlify-create-new-site-step-3.jpg)bin36939 -> 36939 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-deploy-published.jpg (renamed from static/images/hosting-and-deployment/hosting-on-netlify/netlify-deploy-published.jpg)bin18930 -> 18930 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-deploying-site.gif (renamed from static/images/hosting-and-deployment/hosting-on-netlify/netlify-deploying-site.gif)bin783315 -> 783315 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-first-authorize.jpg (renamed from static/images/hosting-and-deployment/hosting-on-netlify/netlify-first-authorize.jpg)bin44374 -> 44374 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-live-site.jpg (renamed from static/images/hosting-and-deployment/hosting-on-netlify/netlify-live-site.jpg)bin37306 -> 37306 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-push-to-deploy.jpg (renamed from static/images/hosting-and-deployment/hosting-on-netlify/netlify-push-to-deploy.jpg)bin21536 -> 21536 bytes
-rw-r--r--docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-signup.jpg (renamed from static/images/hosting-and-deployment/hosting-on-netlify/netlify-signup.jpg)bin37118 -> 37118 bytes
-rw-r--r--docs/static/images/hugo-content-bundles.png (renamed from static/images/hugo-content-bundles.png)bin63640 -> 63640 bytes
-rw-r--r--docs/static/images/icon-custom-outputs.svg (renamed from static/images/icon-custom-outputs.svg)0
-rw-r--r--docs/static/images/site-hierarchy.svg (renamed from static/images/site-hierarchy.svg)0
-rw-r--r--docs/static/img/hugo-logo-med.png (renamed from static/img/hugo-logo-med.png)bin23265 -> 23265 bytes
-rw-r--r--docs/static/img/hugo-logo.png (renamed from static/img/hugo-logo.png)bin13782 -> 13782 bytes
-rw-r--r--docs/static/img/hugo.png (renamed from static/img/hugo.png)bin18210 -> 18210 bytes
-rw-r--r--docs/static/img/hugoSM.png (renamed from static/img/hugoSM.png)bin1869 -> 1869 bytes
-rw-r--r--docs/static/share/hugo-tall.png (renamed from static/share/hugo-tall.png)bin9971 -> 9971 bytes
-rw-r--r--docs/static/share/made-with-hugo-dark.png (renamed from static/share/made-with-hugo-dark.png)bin8764 -> 8764 bytes
-rw-r--r--docs/static/share/made-with-hugo-long-dark.png (renamed from static/share/made-with-hugo-long-dark.png)bin9116 -> 9116 bytes
-rw-r--r--docs/static/share/made-with-hugo-long.png (renamed from static/share/made-with-hugo-long.png)bin9318 -> 9318 bytes
-rw-r--r--docs/static/share/made-with-hugo.png (renamed from static/share/made-with-hugo.png)bin8900 -> 8900 bytes
-rw-r--r--docs/static/share/powered-by-hugo-dark.png (renamed from static/share/powered-by-hugo-dark.png)bin3545 -> 3545 bytes
-rw-r--r--docs/static/share/powered-by-hugo-long-dark.png (renamed from static/share/powered-by-hugo-long-dark.png)bin3857 -> 3857 bytes
-rw-r--r--docs/static/share/powered-by-hugo-long.png (renamed from static/share/powered-by-hugo-long.png)bin3773 -> 3773 bytes
-rw-r--r--docs/static/share/powered-by-hugo.png (renamed from static/share/powered-by-hugo.png)bin3527 -> 3527 bytes
-rw-r--r--docs/themes/gohugoioTheme/.gitignore (renamed from themes/gohugoioTheme/.gitignore)0
-rw-r--r--docs/themes/gohugoioTheme/README.md (renamed from themes/gohugoioTheme/README.md)0
-rw-r--r--docs/themes/gohugoioTheme/archetypes/showcase.md (renamed from themes/gohugoioTheme/archetypes/showcase.md)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_algolia.css (renamed from themes/gohugoioTheme/assets/css/_algolia.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_anchorforid.css (renamed from themes/gohugoioTheme/assets/css/_anchorforid.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_animation.css (renamed from themes/gohugoioTheme/assets/css/_animation.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_carousel.css (renamed from themes/gohugoioTheme/assets/css/_carousel.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_chroma.css (renamed from themes/gohugoioTheme/assets/css/_chroma.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_code.css (renamed from themes/gohugoioTheme/assets/css/_code.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_color-scheme.css (renamed from themes/gohugoioTheme/assets/css/_color-scheme.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_columns.css (renamed from themes/gohugoioTheme/assets/css/_columns.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_content-tables.css (renamed from themes/gohugoioTheme/assets/css/_content-tables.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_content.css (renamed from themes/gohugoioTheme/assets/css/_content.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_definition-lists.css (renamed from themes/gohugoioTheme/assets/css/_definition-lists.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_documentation-styles.css (renamed from themes/gohugoioTheme/assets/css/_documentation-styles.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_fluid-type.css (renamed from themes/gohugoioTheme/assets/css/_fluid-type.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_font-family.css (renamed from themes/gohugoioTheme/assets/css/_font-family.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_hljs.css (renamed from themes/gohugoioTheme/assets/css/_hljs.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_hugo-internal-template-styling.css (renamed from themes/gohugoioTheme/assets/css/_hugo-internal-template-styling.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_no-js.css (renamed from themes/gohugoioTheme/assets/css/_no-js.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_social-icons.css (renamed from themes/gohugoioTheme/assets/css/_social-icons.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_stickyheader.css (renamed from themes/gohugoioTheme/assets/css/_stickyheader.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_svg.css (renamed from themes/gohugoioTheme/assets/css/_svg.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_tabs.css (renamed from themes/gohugoioTheme/assets/css/_tabs.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_tachyons.css (renamed from themes/gohugoioTheme/assets/css/_tachyons.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/_variables.css (renamed from themes/gohugoioTheme/assets/css/_variables.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/css/main.css (renamed from themes/gohugoioTheme/assets/css/main.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/index.js (renamed from themes/gohugoioTheme/assets/index.js)0
-rw-r--r--docs/themes/gohugoioTheme/assets/js/anchorforid.js (renamed from themes/gohugoioTheme/assets/js/anchorforid.js)0
-rw-r--r--docs/themes/gohugoioTheme/assets/js/clipboardjs.js (renamed from themes/gohugoioTheme/assets/js/clipboardjs.js)0
-rw-r--r--docs/themes/gohugoioTheme/assets/js/codeblocks.js (renamed from themes/gohugoioTheme/assets/js/codeblocks.js)0
-rw-r--r--docs/themes/gohugoioTheme/assets/js/docsearch.js (renamed from themes/gohugoioTheme/assets/js/docsearch.js)0
-rw-r--r--docs/themes/gohugoioTheme/assets/js/filesaver.js (renamed from themes/gohugoioTheme/layouts/partials/svg/exclamation.svg)0
-rw-r--r--docs/themes/gohugoioTheme/assets/js/hljs.js (renamed from themes/gohugoioTheme/assets/js/hljs.js)0
-rw-r--r--docs/themes/gohugoioTheme/assets/js/lazysizes.js (renamed from themes/gohugoioTheme/assets/js/lazysizes.js)0
-rw-r--r--docs/themes/gohugoioTheme/assets/js/main.js (renamed from themes/gohugoioTheme/assets/js/main.js)0
-rw-r--r--docs/themes/gohugoioTheme/assets/js/menutoggle.js (renamed from themes/gohugoioTheme/assets/js/menutoggle.js)0
-rw-r--r--docs/themes/gohugoioTheme/assets/js/nojs.js (renamed from themes/gohugoioTheme/assets/js/nojs.js)0
-rw-r--r--docs/themes/gohugoioTheme/assets/js/scrolldir.js (renamed from themes/gohugoioTheme/assets/js/scrolldir.js)0
-rw-r--r--docs/themes/gohugoioTheme/assets/js/smoothscroll.js (renamed from themes/gohugoioTheme/assets/js/smoothscroll.js)0
-rw-r--r--docs/themes/gohugoioTheme/assets/js/tabs.js (renamed from themes/gohugoioTheme/assets/js/tabs.js)0
-rw-r--r--docs/themes/gohugoioTheme/assets/output/css/app.css (renamed from themes/gohugoioTheme/assets/output/css/app.css)0
-rw-r--r--docs/themes/gohugoioTheme/assets/output/js/app.js (renamed from themes/gohugoioTheme/assets/output/js/app.js)0
-rw-r--r--docs/themes/gohugoioTheme/data/sponsors.toml (renamed from themes/gohugoioTheme/data/sponsors.toml)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/404.html (renamed from themes/gohugoioTheme/layouts/404.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/_default/baseof.html (renamed from themes/gohugoioTheme/layouts/_default/baseof.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/_default/list.html (renamed from themes/gohugoioTheme/layouts/_default/list.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/_default/page.html (renamed from themes/gohugoioTheme/layouts/_default/page.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/_default/single.html (renamed from themes/gohugoioTheme/layouts/_default/single.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/_default/taxonomy.html (renamed from themes/gohugoioTheme/layouts/_default/taxonomy.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/_default/terms.html (renamed from themes/gohugoioTheme/layouts/_default/terms.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/index.headers (renamed from themes/gohugoioTheme/layouts/index.headers)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/index.html (renamed from themes/gohugoioTheme/layouts/index.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/index.redir (renamed from themes/gohugoioTheme/layouts/index.redir)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/news/list.html (renamed from themes/gohugoioTheme/layouts/news/list.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/news/single.html (renamed from themes/gohugoioTheme/layouts/news/single.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/page/documentation-home.html (renamed from themes/gohugoioTheme/layouts/page/documentation-home.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/boxes-section-summaries.html (renamed from themes/gohugoioTheme/layouts/partials/boxes-section-summaries.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/boxes-small-news.html (renamed from themes/gohugoioTheme/layouts/partials/boxes-small-news.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/components/author-github-data-card.html (renamed from themes/gohugoioTheme/layouts/partials/components/author-github-data-card.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/components/author-github-data.html (renamed from themes/gohugoioTheme/layouts/partials/components/author-github-data.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/docs/functions-signature.html (renamed from themes/gohugoioTheme/layouts/partials/docs/functions-signature.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/docs/page-meta-data.html (renamed from themes/gohugoioTheme/layouts/partials/docs/page-meta-data.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/entry-summary.html (renamed from themes/gohugoioTheme/layouts/partials/entry-summary.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/gtag.html (renamed from themes/gohugoioTheme/layouts/partials/gtag.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/head-additions.html (renamed from themes/gohugoioTheme/layouts/partials/head-additions.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/hero.html (renamed from themes/gohugoioTheme/layouts/partials/hero.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/home-page-sections/features-icons.html (renamed from themes/gohugoioTheme/layouts/partials/home-page-sections/features-icons.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/home-page-sections/features-single.html (renamed from themes/gohugoioTheme/layouts/partials/home-page-sections/features-single.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/home-page-sections/installation.html (renamed from themes/gohugoioTheme/layouts/partials/home-page-sections/installation.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/home-page-sections/open-source-involvement.html (renamed from themes/gohugoioTheme/layouts/partials/home-page-sections/open-source-involvement.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/home-page-sections/showcase.html (renamed from themes/gohugoioTheme/layouts/partials/home-page-sections/showcase.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/home-page-sections/sponsors.html (renamed from themes/gohugoioTheme/layouts/partials/home-page-sections/sponsors.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/home-page-sections/tweets.html (renamed from themes/gohugoioTheme/layouts/partials/home-page-sections/tweets.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/icon-link.html (renamed from themes/gohugoioTheme/layouts/partials/icon-link.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/nav-links-docs-mobile.html (renamed from themes/gohugoioTheme/layouts/partials/nav-links-docs-mobile.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/nav-links-docs.html (renamed from themes/gohugoioTheme/layouts/partials/nav-links-docs.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/nav-links-global-mobile.html (renamed from themes/gohugoioTheme/layouts/partials/nav-links-global-mobile.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/nav-links.html (renamed from themes/gohugoioTheme/layouts/partials/nav-links.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/nav-mobile.html (renamed from themes/gohugoioTheme/layouts/partials/nav-mobile.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/nav-top.html (renamed from themes/gohugoioTheme/layouts/partials/nav-top.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/page-edit.html (renamed from themes/gohugoioTheme/layouts/partials/page-edit.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/page-header.html (renamed from themes/gohugoioTheme/layouts/partials/page-header.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/pagelayout.html (renamed from themes/gohugoioTheme/layouts/partials/pagelayout.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/previous-next-links-in-section-with-title.html (renamed from themes/gohugoioTheme/layouts/partials/previous-next-links-in-section-with-title.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/previous-next-links-in-section.html (renamed from themes/gohugoioTheme/layouts/partials/previous-next-links-in-section.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/previous-next-links.html (renamed from themes/gohugoioTheme/layouts/partials/previous-next-links.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/related.html (renamed from themes/gohugoioTheme/layouts/partials/related.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/site-footer.html (renamed from themes/gohugoioTheme/layouts/partials/site-footer.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/site-manifest.html (renamed from themes/gohugoioTheme/layouts/partials/site-manifest.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/site-nav.html (renamed from themes/gohugoioTheme/layouts/partials/site-nav.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/site-scripts.html (renamed from themes/gohugoioTheme/layouts/partials/site-scripts.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/site-search.html (renamed from themes/gohugoioTheme/layouts/partials/site-search.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/social-follow.html (renamed from themes/gohugoioTheme/layouts/partials/social-follow.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/summary.html (renamed from themes/gohugoioTheme/layouts/partials/summary.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/Twitter_Logo_Blue.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/Twitter_Logo_Blue.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/apple.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/apple.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/clipboard.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/clipboard.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/clippy.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/clippy.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/cloud.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/cloud.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/content.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/content.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/design.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/design.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/exclamation.svg0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/facebook.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/facebook.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/focus.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/focus.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/freebsd.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/freebsd.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/functions.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/functions.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/github-corner.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/github-corner.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/github-squared.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/github-squared.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/gitter.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/gitter.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/gme.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/gme.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/godoc-icon.html (renamed from themes/gohugoioTheme/layouts/partials/svg/godoc-icon.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/gopher-2.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/gopher-2.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/gopher-front.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/gopher-front.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/gopher-homepage.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/gopher-homepage.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/gopher-side_path.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/gopher-side_path.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/gopher-small.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/gopher-small.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/gopher.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/gopher.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/hugo-h-only.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/hugo-h-only.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/hugo.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/hugo.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_down.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_down.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_up.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_up.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/ic_chevron_left_black_24px.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/ic_chevron_left_black_24px.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/ic_chevron_right_black_24px.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/ic_chevron_right_black_24px.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/idea.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/idea.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/instagram.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/instagram.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/javascript.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/javascript.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/json.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/json.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/link-ext.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/link-ext.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/link-permalink.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/link-permalink.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/md.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/md.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/mdsolid.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/mdsolid.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/newlogo.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/newlogo.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/sass.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/sass.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/search.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/search.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/twitter.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/twitter.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/website.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/website.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/windows.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/windows.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/svg/yaml.svg (renamed from themes/gohugoioTheme/layouts/partials/svg/yaml.svg)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/tags.html (renamed from themes/gohugoioTheme/layouts/partials/tags.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/partials/toc.html (renamed from themes/gohugoioTheme/layouts/partials/toc.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/robots.txt (renamed from themes/gohugoioTheme/layouts/robots.txt)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/shortcodes/articlelist.html (renamed from themes/gohugoioTheme/layouts/shortcodes/articlelist.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/shortcodes/code-toggle.html (renamed from themes/gohugoioTheme/layouts/shortcodes/code-toggle.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/shortcodes/code.html (renamed from themes/gohugoioTheme/layouts/shortcodes/code.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/shortcodes/datatable.html (renamed from themes/gohugoioTheme/layouts/shortcodes/datatable.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/shortcodes/directoryindex.html (renamed from themes/gohugoioTheme/layouts/shortcodes/directoryindex.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/shortcodes/docfile.html (renamed from themes/gohugoioTheme/layouts/shortcodes/docfile.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/shortcodes/exfile.html (renamed from themes/gohugoioTheme/layouts/shortcodes/exfile.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/shortcodes/exfm.html (renamed from themes/gohugoioTheme/layouts/shortcodes/exfm.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/shortcodes/gh.html (renamed from themes/gohugoioTheme/layouts/shortcodes/gh.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/shortcodes/ghrepo.html (renamed from themes/gohugoioTheme/layouts/shortcodes/ghrepo.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/shortcodes/nohighlight.html (renamed from themes/gohugoioTheme/layouts/shortcodes/nohighlight.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/shortcodes/note.html (renamed from themes/gohugoioTheme/layouts/shortcodes/note.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/shortcodes/output.html (renamed from themes/gohugoioTheme/layouts/shortcodes/output.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/shortcodes/readfile.html (renamed from themes/gohugoioTheme/layouts/shortcodes/readfile.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/shortcodes/tip.html (renamed from themes/gohugoioTheme/layouts/shortcodes/tip.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/shortcodes/warning.html (renamed from themes/gohugoioTheme/layouts/shortcodes/warning.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/shortcodes/yt.html (renamed from themes/gohugoioTheme/layouts/shortcodes/yt.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/showcase/list.html (renamed from themes/gohugoioTheme/layouts/showcase/list.html)0
-rw-r--r--docs/themes/gohugoioTheme/layouts/showcase/single.html (renamed from themes/gohugoioTheme/layouts/showcase/single.html)0
-rw-r--r--docs/themes/gohugoioTheme/license.md (renamed from themes/gohugoioTheme/license.md)0
-rw-r--r--docs/themes/gohugoioTheme/package-lock.json (renamed from themes/gohugoioTheme/package-lock.json)0
-rw-r--r--docs/themes/gohugoioTheme/package.json (renamed from themes/gohugoioTheme/package.json)0
-rw-r--r--docs/themes/gohugoioTheme/src/package-lock.json (renamed from themes/gohugoioTheme/src/package-lock.json)0
-rw-r--r--docs/themes/gohugoioTheme/src/package.json (renamed from themes/gohugoioTheme/src/package.json)0
-rw-r--r--docs/themes/gohugoioTheme/src/readme.md (renamed from themes/gohugoioTheme/src/readme.md)0
-rw-r--r--docs/themes/gohugoioTheme/static/android-chrome-144x144.png (renamed from themes/gohugoioTheme/static/android-chrome-144x144.png)bin7612 -> 7612 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/android-chrome-192x192.png (renamed from themes/gohugoioTheme/static/android-chrome-192x192.png)bin10264 -> 10264 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/android-chrome-256x256.png (renamed from themes/gohugoioTheme/static/android-chrome-256x256.png)bin15088 -> 15088 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/android-chrome-36x36.png (renamed from themes/gohugoioTheme/static/android-chrome-36x36.png)bin1592 -> 1592 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/android-chrome-48x48.png (renamed from themes/gohugoioTheme/static/android-chrome-48x48.png)bin2038 -> 2038 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/android-chrome-72x72.png (renamed from themes/gohugoioTheme/static/android-chrome-72x72.png)bin3467 -> 3467 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/android-chrome-96x96.png (renamed from themes/gohugoioTheme/static/android-chrome-96x96.png)bin4747 -> 4747 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/apple-touch-icon.png (renamed from themes/gohugoioTheme/static/apple-touch-icon.png)bin6238 -> 6238 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/browserconfig.xml (renamed from themes/gohugoioTheme/static/browserconfig.xml)0
-rw-r--r--docs/themes/gohugoioTheme/static/dist/app.bundle.js (renamed from themes/gohugoioTheme/static/dist/app.bundle.js)0
-rw-r--r--docs/themes/gohugoioTheme/static/dist/main.css (renamed from themes/gohugoioTheme/static/dist/main.css)0
-rw-r--r--docs/themes/gohugoioTheme/static/favicon-16x16.png (renamed from themes/gohugoioTheme/static/favicon-16x16.png)bin1000 -> 1000 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/favicon-32x32.png (renamed from themes/gohugoioTheme/static/favicon-32x32.png)bin1648 -> 1648 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/favicon.ico (renamed from themes/gohugoioTheme/static/favicon.ico)bin15086 -> 15086 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-200.woff (renamed from themes/gohugoioTheme/static/fonts/muli-latin-200.woff)bin20892 -> 20892 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-200.woff2 (renamed from themes/gohugoioTheme/static/fonts/muli-latin-200.woff2)bin16936 -> 16936 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-200italic.woff (renamed from themes/gohugoioTheme/static/fonts/muli-latin-200italic.woff)bin21496 -> 21496 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-200italic.woff2 (renamed from themes/gohugoioTheme/static/fonts/muli-latin-200italic.woff2)bin17320 -> 17320 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-300.woff (renamed from themes/gohugoioTheme/static/fonts/muli-latin-300.woff)bin20932 -> 20932 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-300.woff2 (renamed from themes/gohugoioTheme/static/fonts/muli-latin-300.woff2)bin16872 -> 16872 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-300italic.woff (renamed from themes/gohugoioTheme/static/fonts/muli-latin-300italic.woff)bin21520 -> 21520 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-300italic.woff2 (renamed from themes/gohugoioTheme/static/fonts/muli-latin-300italic.woff2)bin17332 -> 17332 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-400.woff (renamed from themes/gohugoioTheme/static/fonts/muli-latin-400.woff)bin21240 -> 21240 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-400.woff2 (renamed from themes/gohugoioTheme/static/fonts/muli-latin-400.woff2)bin17172 -> 17172 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-400italic.woff (renamed from themes/gohugoioTheme/static/fonts/muli-latin-400italic.woff)bin21964 -> 21964 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-400italic.woff2 (renamed from themes/gohugoioTheme/static/fonts/muli-latin-400italic.woff2)bin17732 -> 17732 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-600.woff (renamed from themes/gohugoioTheme/static/fonts/muli-latin-600.woff)bin21208 -> 21208 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-600.woff2 (renamed from themes/gohugoioTheme/static/fonts/muli-latin-600.woff2)bin17080 -> 17080 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-600italic.woff (renamed from themes/gohugoioTheme/static/fonts/muli-latin-600italic.woff)bin21924 -> 21924 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-600italic.woff2 (renamed from themes/gohugoioTheme/static/fonts/muli-latin-600italic.woff2)bin17776 -> 17776 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-700.woff (renamed from themes/gohugoioTheme/static/fonts/muli-latin-700.woff)bin21220 -> 21220 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-700.woff2 (renamed from themes/gohugoioTheme/static/fonts/muli-latin-700.woff2)bin17128 -> 17128 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-700italic.woff (renamed from themes/gohugoioTheme/static/fonts/muli-latin-700italic.woff)bin21960 -> 21960 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-700italic.woff2 (renamed from themes/gohugoioTheme/static/fonts/muli-latin-700italic.woff2)bin17756 -> 17756 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-800.woff (renamed from themes/gohugoioTheme/static/fonts/muli-latin-800.woff)bin21244 -> 21244 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-800.woff2 (renamed from themes/gohugoioTheme/static/fonts/muli-latin-800.woff2)bin17140 -> 17140 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-800italic.woff (renamed from themes/gohugoioTheme/static/fonts/muli-latin-800italic.woff)bin21872 -> 21872 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-800italic.woff2 (renamed from themes/gohugoioTheme/static/fonts/muli-latin-800italic.woff2)bin17644 -> 17644 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-900.woff (renamed from themes/gohugoioTheme/static/fonts/muli-latin-900.woff)bin21676 -> 21676 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-900.woff2 (renamed from themes/gohugoioTheme/static/fonts/muli-latin-900.woff2)bin17436 -> 17436 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-900italic.woff (renamed from themes/gohugoioTheme/static/fonts/muli-latin-900italic.woff)bin22220 -> 22220 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/fonts/muli-latin-900italic.woff2 (renamed from themes/gohugoioTheme/static/fonts/muli-latin-900italic.woff2)bin17948 -> 17948 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/images/GitHub-Mark-64px.png (renamed from themes/gohugoioTheme/static/images/GitHub-Mark-64px.png)bin970 -> 970 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/images/gohugoio-card.png (renamed from themes/gohugoioTheme/static/images/gohugoio-card.png)bin242267 -> 242267 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/images/gopher-hero.svg (renamed from themes/gohugoioTheme/static/images/gopher-hero.svg)0
-rw-r--r--docs/themes/gohugoioTheme/static/images/gopher-side_color.svg (renamed from themes/gohugoioTheme/static/images/gopher-side_color.svg)0
-rw-r--r--docs/themes/gohugoioTheme/static/images/home-page-templating-example.png (renamed from themes/gohugoioTheme/static/images/home-page-templating-example.png)bin94053 -> 94053 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/images/homepage-screenshot-hugo-themes.jpg (renamed from themes/gohugoioTheme/static/images/homepage-screenshot-hugo-themes.jpg)bin33701 -> 33701 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/images/homepage-screenshot-hugo-themes_not-optimized-according-to-google.jpg (renamed from themes/gohugoioTheme/static/images/homepage-screenshot-hugo-themes_not-optimized-according-to-google.jpg)bin88453 -> 88453 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/images/hugo-logo-wide.svg (renamed from themes/gohugoioTheme/static/images/hugo-logo-wide.svg)0
-rw-r--r--docs/themes/gohugoioTheme/static/images/icon-built-in-templates.svg (renamed from themes/gohugoioTheme/static/images/icon-built-in-templates.svg)0
-rw-r--r--docs/themes/gohugoioTheme/static/images/icon-content-management.svg (renamed from themes/gohugoioTheme/static/images/icon-content-management.svg)0
-rw-r--r--docs/themes/gohugoioTheme/static/images/icon-fast.svg (renamed from themes/gohugoioTheme/static/images/icon-fast.svg)0
-rw-r--r--docs/themes/gohugoioTheme/static/images/icon-multilingual.svg (renamed from themes/gohugoioTheme/static/images/icon-multilingual.svg)0
-rw-r--r--docs/themes/gohugoioTheme/static/images/icon-multilingual2.svg (renamed from themes/gohugoioTheme/static/images/icon-multilingual2.svg)0
-rw-r--r--docs/themes/gohugoioTheme/static/images/icon-search.png (renamed from themes/gohugoioTheme/static/images/icon-search.png)bin337 -> 337 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/images/icon-shortcodes.svg (renamed from themes/gohugoioTheme/static/images/icon-shortcodes.svg)0
-rw-r--r--docs/themes/gohugoioTheme/static/images/site-hierarchy.svg (renamed from themes/gohugoioTheme/static/images/site-hierarchy.svg)0
-rw-r--r--docs/themes/gohugoioTheme/static/images/sponsors/esolia-logo.svg (renamed from themes/gohugoioTheme/static/images/sponsors/esolia-logo.svg)0
-rw-r--r--docs/themes/gohugoioTheme/static/images/sponsors/forestry-logotype.svg (renamed from themes/gohugoioTheme/static/images/sponsors/forestry-logotype.svg)0
-rw-r--r--docs/themes/gohugoioTheme/static/images/sponsors/linode-logo_standard_light_medium.png (renamed from themes/gohugoioTheme/static/images/sponsors/linode-logo_standard_light_medium.png)bin30887 -> 30887 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/manifest.json (renamed from themes/gohugoioTheme/static/manifest.json)0
-rw-r--r--docs/themes/gohugoioTheme/static/mstile-144x144.png (renamed from themes/gohugoioTheme/static/mstile-144x144.png)bin6225 -> 6225 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/mstile-150x150.png (renamed from themes/gohugoioTheme/static/mstile-150x150.png)bin6020 -> 6020 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/mstile-310x310.png (renamed from themes/gohugoioTheme/static/mstile-310x310.png)bin12885 -> 12885 bytes
-rw-r--r--docs/themes/gohugoioTheme/static/safari-pinned-tab.svg (renamed from themes/gohugoioTheme/static/safari-pinned-tab.svg)0
-rw-r--r--docs/themes/gohugoioTheme/theme.toml (renamed from themes/gohugoioTheme/theme.toml)0
-rw-r--r--docs/themes/gohugoioTheme/webpack.config.js (renamed from themes/gohugoioTheme/webpack.config.js)0
-rw-r--r--docshelper/docs.go37
-rw-r--r--examples/blog/.gitignore12
-rw-r--r--examples/blog/README.md42
-rw-r--r--examples/blog/config.toml4
-rw-r--r--examples/blog/content/post/another-post.md57
-rw-r--r--examples/blog/content/post/hello-hugo.md61
-rw-r--r--examples/blog/layouts/_default/single.html21
-rw-r--r--examples/blog/layouts/index.html19
-rw-r--r--examples/blog/layouts/indexes/category.html24
-rw-r--r--examples/blog/layouts/indexes/post.html24
-rw-r--r--examples/blog/layouts/indexes/tag.html24
-rw-r--r--examples/blog/layouts/partials/footer.copyright.html9
-rw-r--r--examples/blog/layouts/partials/footer.html5
-rw-r--r--examples/blog/layouts/partials/header.html10
-rw-r--r--examples/blog/layouts/partials/header.includes.html4
-rw-r--r--examples/blog/layouts/partials/menu.html15
-rw-r--r--examples/blog/layouts/partials/meta.html6
-rw-r--r--examples/blog/layouts/partials/navbar.html22
-rw-r--r--examples/blog/layouts/post/li.html4
-rw-r--r--examples/blog/layouts/post/single.html35
-rw-r--r--examples/blog/layouts/post/summary.html9
-rw-r--r--examples/blog/static/css/bootstrap.min.css11
-rw-r--r--examples/blog/static/css/custom.css7
-rw-r--r--examples/blog/static/css/font-awesome.css2086
-rw-r--r--examples/blog/static/fonts/FontAwesome.otfbin0 -> 109688 bytes
-rw-r--r--examples/blog/static/fonts/fontawesome-webfont.eotbin0 -> 70807 bytes
-rw-r--r--examples/blog/static/fonts/fontawesome-webfont.svg655
-rw-r--r--examples/blog/static/fonts/fontawesome-webfont.ttfbin0 -> 142072 bytes
-rw-r--r--examples/blog/static/fonts/fontawesome-webfont.woffbin0 -> 83588 bytes
-rw-r--r--examples/blog/static/fonts/fontawesome-webfont.woff2bin0 -> 66624 bytes
-rw-r--r--examples/blog/static/fonts/glyphicons-halflings-regular.eotbin0 -> 20127 bytes
-rw-r--r--examples/blog/static/fonts/glyphicons-halflings-regular.svg288
-rw-r--r--examples/blog/static/fonts/glyphicons-halflings-regular.ttfbin0 -> 45404 bytes
-rw-r--r--examples/blog/static/fonts/glyphicons-halflings-regular.woffbin0 -> 23424 bytes
-rw-r--r--examples/blog/static/fonts/glyphicons-halflings-regular.woff2bin0 -> 18028 bytes
-rw-r--r--examples/blog/static/js/bootstrap.js2363
-rw-r--r--examples/blog/static/js/jquery-1.11.3.min.js5
-rw-r--r--examples/multilingual/.gitignore1
-rw-r--r--examples/multilingual/README.md15
-rw-r--r--examples/multilingual/config.toml39
-rw-r--r--examples/multilingual/content/about.en.md12
-rw-r--r--examples/multilingual/content/about.et.md12
-rw-r--r--examples/multilingual/content/index.en.md10
-rw-r--r--examples/multilingual/content/index.et.md10
-rw-r--r--examples/multilingual/content/story/alpha.en.md14
-rw-r--r--examples/multilingual/content/story/beta.en.md14
-rw-r--r--examples/multilingual/content/story/index.en.md5
-rw-r--r--examples/multilingual/content/uudis/alfa.et.md15
-rw-r--r--examples/multilingual/content/uudis/beeta.et.md15
-rw-r--r--examples/multilingual/content/uudis/index.et.md5
-rw-r--r--examples/multilingual/i18n/en.toml2
-rw-r--r--examples/multilingual/i18n/et.toml2
-rw-r--r--examples/multilingual/layouts/_default/single.html4
-rw-r--r--examples/multilingual/layouts/index.html1
-rw-r--r--examples/multilingual/layouts/partials/footer.html3
-rw-r--r--examples/multilingual/layouts/partials/head.html11
-rw-r--r--examples/multilingual/layouts/partials/header.html17
-rw-r--r--examples/multilingual/layouts/story/single.html17
-rw-r--r--examples/multilingual/layouts/uudis/single.html17
-rw-r--r--examples/multilingual/static/main.css90
-rw-r--r--go.mod60
-rw-r--r--go.sum745
-rw-r--r--goreleaser-extended.yml85
-rw-r--r--goreleaser.yml66
-rw-r--r--helpers/content.go788
-rw-r--r--helpers/content_renderer.go108
-rw-r--r--helpers/content_renderer_test.go141
-rw-r--r--helpers/content_test.go518
-rw-r--r--helpers/docshelper.go59
-rw-r--r--helpers/emoji.go97
-rw-r--r--helpers/emoji_test.go147
-rw-r--r--helpers/general.go475
-rw-r--r--helpers/general_test.go330
-rw-r--r--helpers/path.go667
-rw-r--r--helpers/path_test.go808
-rw-r--r--helpers/pathspec.go88
-rw-r--r--helpers/pathspec_test.go54
-rw-r--r--helpers/processing_stats.go123
-rw-r--r--helpers/pygments.go402
-rw-r--r--helpers/pygments_test.go300
-rw-r--r--helpers/testhelpers_test.go55
-rw-r--r--helpers/url.go374
-rw-r--r--helpers/url_test.go322
-rw-r--r--htesting/test_structs.go100
-rw-r--r--htesting/testdata_builder.go59
-rw-r--r--hugofs/basepath_real_filename_fs.go91
-rw-r--r--hugofs/createcounting_fs.go99
-rw-r--r--hugofs/fs.go88
-rw-r--r--hugofs/fs_test.go60
-rw-r--r--hugofs/hashing_fs.go92
-rw-r--r--hugofs/hashing_fs_test.go53
-rw-r--r--hugofs/language_composite_fs.go51
-rw-r--r--hugofs/language_composite_fs_test.go107
-rw-r--r--hugofs/language_fs.go346
-rw-r--r--hugofs/language_fs_test.go100
-rw-r--r--hugofs/nolstat_fs.go39
-rw-r--r--hugofs/noop_fs.go82
-rw-r--r--hugofs/rootmapping_fs.go196
-rw-r--r--hugofs/rootmapping_fs_test.go94
-rw-r--r--hugofs/stacktracer_fs.go70
-rw-r--r--hugolib/404_test.go32
-rw-r--r--hugolib/alias.go193
-rw-r--r--hugolib/alias_test.go142
-rw-r--r--hugolib/case_insensitive_test.go309
-rw-r--r--hugolib/collections.go47
-rw-r--r--hugolib/collections_test.go209
-rw-r--r--hugolib/config.go639
-rw-r--r--hugolib/config_test.go399
-rw-r--r--hugolib/configdir_test.go154
-rw-r--r--hugolib/datafiles_test.go398
-rw-r--r--hugolib/disableKinds_test.go223
-rw-r--r--hugolib/embedded_shortcodes_test.go379
-rw-r--r--hugolib/embedded_templates_test.go58
-rw-r--r--hugolib/fileInfo.go136
-rw-r--r--hugolib/fileInfo_test.go30
-rw-r--r--hugolib/filesystems/basefs.go760
-rw-r--r--hugolib/filesystems/basefs_test.go363
-rw-r--r--hugolib/gitinfo.go47
-rw-r--r--hugolib/hugo_sites.go1048
-rw-r--r--hugolib/hugo_sites_build.go326
-rw-r--r--hugolib/hugo_sites_build_errors_test.go354
-rw-r--r--hugolib/hugo_sites_build_test.go1537
-rw-r--r--hugolib/hugo_sites_multihost_test.go113
-rw-r--r--hugolib/hugo_sites_rebuild_test.go83
-rw-r--r--hugolib/hugo_smoke_test.go303
-rw-r--r--hugolib/hugo_themes_test.go268
-rw-r--r--hugolib/language_content_dir_test.go305
-rw-r--r--hugolib/menu_test.go224
-rw-r--r--hugolib/minify_publisher_test.go63
-rw-r--r--hugolib/multilingual.go140
-rw-r--r--hugolib/page.go860
-rw-r--r--hugolib/page__common.go117
-rw-r--r--hugolib/page__content.go135
-rw-r--r--hugolib/page__data.go70
-rw-r--r--hugolib/page__menus.go74
-rw-r--r--hugolib/page__meta.go675
-rw-r--r--hugolib/page__new.go296
-rw-r--r--hugolib/page__output.go107
-rw-r--r--hugolib/page__paginator.go98
-rw-r--r--hugolib/page__paths.go150
-rw-r--r--hugolib/page__per_output.go453
-rw-r--r--hugolib/page__position.go76
-rw-r--r--hugolib/page__ref.go117
-rw-r--r--hugolib/page__tree.go117
-rw-r--r--hugolib/page_kinds.go40
-rw-r--r--hugolib/page_permalink_test.go150
-rw-r--r--hugolib/page_test.go1539
-rw-r--r--hugolib/page_unwrap.go50
-rw-r--r--hugolib/page_unwrap_test.go37
-rw-r--r--hugolib/pagebundler.go206
-rw-r--r--hugolib/pagebundler_capture.go773
-rw-r--r--hugolib/pagebundler_capture_test.go272
-rw-r--r--hugolib/pagebundler_handlers.go305
-rw-r--r--hugolib/pagebundler_test.go944
-rw-r--r--hugolib/pagecollections.go392
-rw-r--r--hugolib/pagecollections_test.go244
-rw-r--r--hugolib/pages_language_merge_test.go188
-rw-r--r--hugolib/paginator_test.go98
-rw-r--r--hugolib/paths/baseURL.go87
-rw-r--r--hugolib/paths/baseURL_test.go66
-rw-r--r--hugolib/paths/paths.go279
-rw-r--r--hugolib/paths/paths_test.go44
-rw-r--r--hugolib/paths/themes.go154
-rw-r--r--hugolib/permalinker.go24
-rw-r--r--hugolib/prune_resources.go19
-rw-r--r--hugolib/resource_chain_test.go478
-rw-r--r--hugolib/robotstxt_test.go42
-rw-r--r--hugolib/rss_test.go100
-rw-r--r--hugolib/shortcode.go634
-rw-r--r--hugolib/shortcode_page.go56
-rw-r--r--hugolib/shortcode_test.go1167
-rw-r--r--hugolib/site.go1902
-rw-r--r--hugolib/siteJSONEncode_test.go45
-rw-r--r--hugolib/site_benchmark_new_test.go106
-rw-r--r--hugolib/site_benchmark_test.go337
-rw-r--r--hugolib/site_output.go91
-rw-r--r--hugolib/site_output_test.go562
-rw-r--r--hugolib/site_render.go371
-rw-r--r--hugolib/site_sections.go244
-rw-r--r--hugolib/site_sections_test.go377
-rw-r--r--hugolib/site_stats_test.go101
-rw-r--r--hugolib/site_test.go959
-rw-r--r--hugolib/site_url_test.go186
-rw-r--r--hugolib/sitemap_test.go121
-rw-r--r--hugolib/taxonomy.go248
-rw-r--r--hugolib/taxonomy_test.go328
-rw-r--r--hugolib/template_engines_test.go108
-rw-r--r--hugolib/template_test.go307
-rw-r--r--hugolib/testdata/redis.cn.md697
-rw-r--r--hugolib/testdata/sunset.jpgbin0 -> 90587 bytes
-rw-r--r--hugolib/testhelpers_test.go800
-rw-r--r--hugolib/testsite/content/first-post.md4
-rw-r--r--hugolib/testsite/content_nn/first-post.md4
-rw-r--r--hugolib/translations.go53
-rw-r--r--langs/i18n/i18n.go117
-rw-r--r--langs/i18n/i18n_test.go262
-rw-r--r--langs/i18n/translationProvider.go125
-rw-r--r--langs/language.go229
-rw-r--r--langs/language_test.go48
-rw-r--r--lazy/init.go198
-rw-r--r--lazy/init_test.go226
-rw-r--r--lazy/once.go69
-rw-r--r--livereload/connection.go66
-rw-r--r--livereload/hub.go56
-rw-r--r--livereload/livereload.go188
-rw-r--r--magefile.go307
-rw-r--r--main.go33
-rw-r--r--media/docshelper.go17
-rw-r--r--media/mediaType.go371
-rw-r--r--media/mediaType_test.go214
-rw-r--r--metrics/metrics.go262
-rw-r--r--metrics/metrics_test.go59
-rw-r--r--minifiers/minifiers.go112
-rw-r--r--minifiers/minifiers_test.go93
-rw-r--r--navigation/menu.go234
-rw-r--r--navigation/pagemenus.go238
-rw-r--r--output/docshelper.go98
-rw-r--r--output/layout.go272
-rw-r--r--output/layout_base.go187
-rw-r--r--output/layout_base_test.go161
-rw-r--r--output/layout_test.go147
-rw-r--r--output/outputFormat.go380
-rw-r--r--output/outputFormat_test.go250
-rw-r--r--parser/frontmatter.go107
-rw-r--r--parser/frontmatter_test.go78
-rw-r--r--parser/metadecoders/decoder.go238
-rw-r--r--parser/metadecoders/decoder_test.go213
-rw-r--r--parser/metadecoders/format.go130
-rw-r--r--parser/metadecoders/format_test.go101
-rw-r--r--parser/pageparser/item.go134
-rw-r--r--parser/pageparser/itemtype_string.go16
-rw-r--r--parser/pageparser/pagelexer.go525
-rw-r--r--parser/pageparser/pagelexer_intro.go202
-rw-r--r--parser/pageparser/pagelexer_shortcode.go323
-rw-r--r--parser/pageparser/pagelexer_test.go29
-rw-r--r--parser/pageparser/pageparser.go139
-rw-r--r--parser/pageparser/pageparser_intro_test.go127
-rw-r--r--parser/pageparser/pageparser_main_test.go40
-rw-r--r--parser/pageparser/pageparser_shortcode_test.go191
-rw-r--r--parser/pageparser/pageparser_test.go71
-rw-r--r--publisher/publisher.go162
-rw-r--r--publisher/publisher_test.go14
-rwxr-xr-xpull-docs.sh7
-rw-r--r--related/inverted_index.go459
-rw-r--r--related/inverted_index_test.go308
-rw-r--r--releaser/git.go315
-rw-r--r--releaser/git_test.go75
-rw-r--r--releaser/github.go144
-rw-r--r--releaser/github_test.go44
-rw-r--r--releaser/releasenotes_writer.go315
-rw-r--r--releaser/releasenotes_writer_test.go44
-rw-r--r--releaser/releaser.go340
-rw-r--r--requirements.txt1
-rw-r--r--resources/image.go596
-rw-r--r--resources/image_cache.go164
-rw-r--r--resources/image_test.go400
-rw-r--r--resources/internal/glob.go48
-rw-r--r--resources/page/page.go371
-rw-r--r--resources/page/page_author.go45
-rw-r--r--resources/page/page_data.go42
-rw-r--r--resources/page/page_data_test.go57
-rw-r--r--resources/page/page_generate/.gitignore1
-rw-r--r--resources/page/page_generate/generate_page_wrappers.go283
-rw-r--r--resources/page/page_kinds.go25
-rw-r--r--resources/page/page_kinds_test.go31
-rw-r--r--resources/page/page_marshaljson.autogen.go202
-rw-r--r--resources/page/page_nop.go467
-rw-r--r--resources/page/page_outputformat.go85
-rw-r--r--resources/page/page_paths.go339
-rw-r--r--resources/page/page_paths_test.go258
-rw-r--r--resources/page/page_wrappers.autogen.go97
-rw-r--r--resources/page/pagegroup.go408
-rw-r--r--resources/page/pagegroup_test.go409
-rw-r--r--resources/page/pagemeta/page_frontmatter.go427
-rw-r--r--resources/page/pagemeta/page_frontmatter_test.go262
-rw-r--r--resources/page/pagemeta/pagemeta.go21
-rw-r--r--resources/page/pages.go145
-rw-r--r--resources/page/pages_cache.go136
-rw-r--r--resources/page/pages_cache_test.go86
-rw-r--r--resources/page/pages_language_merge.go64
-rw-r--r--resources/page/pages_prev_next.go42
-rw-r--r--resources/page/pages_prev_next_test.go83
-rw-r--r--resources/page/pages_related.go199
-rw-r--r--resources/page/pages_related_test.go86
-rw-r--r--resources/page/pages_sort.go348
-rw-r--r--resources/page/pages_sort_test.go279
-rw-r--r--resources/page/pages_test.go55
-rw-r--r--resources/page/pagination.go404
-rw-r--r--resources/page/pagination_test.go307
-rw-r--r--resources/page/permalinks.go248
-rw-r--r--resources/page/permalinks_test.go180
-rw-r--r--resources/page/site.go53
-rw-r--r--resources/page/testhelpers_test.go558
-rw-r--r--resources/page/weighted.go145
-rw-r--r--resources/page/zero_file.autogen.go88
-rw-r--r--resources/resource.go750
-rw-r--r--resources/resource/dates.go81
-rw-r--r--resources/resource/params.go89
-rw-r--r--resources/resource/resource_helpers.go70
-rw-r--r--resources/resource/resources.go123
-rw-r--r--resources/resource/resourcetypes.go166
-rw-r--r--resources/resource_cache.go217
-rw-r--r--resources/resource_factories/bundler/bundler.go122
-rw-r--r--resources/resource_factories/create/create.go65
-rw-r--r--resources/resource_metadata.go132
-rw-r--r--resources/resource_metadata_test.go231
-rw-r--r--resources/resource_test.go277
-rw-r--r--resources/resource_transformers/integrity/integrity.go113
-rw-r--r--resources/resource_transformers/integrity/integrity_test.go48
-rw-r--r--resources/resource_transformers/minifier/minify.go59
-rw-r--r--resources/resource_transformers/postcss/postcss.go185
-rw-r--r--resources/resource_transformers/templates/execute_as_template.go76
-rw-r--r--resources/resource_transformers/tocss/scss/client.go111
-rw-r--r--resources/resource_transformers/tocss/scss/tocss.go173
-rw-r--r--resources/resource_transformers/tocss/scss/tocss_notavailable.go30
-rw-r--r--resources/smartcrop.go80
-rw-r--r--resources/testdata/1234567890qwertyuiopasdfghjklzxcvbnm5to6eeeeee7via8eleph.jpgbin0 -> 90587 bytes
-rw-r--r--resources/testdata/circle.svg5
-rw-r--r--resources/testdata/gohugoio.pngbin0 -> 73886 bytes
-rw-r--r--resources/testdata/sub/gohugoio2.pngbin0 -> 73886 bytes
-rw-r--r--resources/testdata/sunset.jpgbin0 -> 90587 bytes
-rw-r--r--resources/testhelpers_test.go191
-rw-r--r--resources/transform.go554
-rw-r--r--resources/transform_test.go36
-rw-r--r--snap/plugins/x-nodejs.yaml8
-rw-r--r--snap/plugins/x_nodejs.py332
-rw-r--r--snap/snapcraft.yaml95
-rw-r--r--source/content_directory_test.go66
-rw-r--r--source/fileInfo.go290
-rw-r--r--source/fileInfo_test.go110
-rw-r--r--source/filesystem.go130
-rw-r--r--source/filesystem_test.go84
-rw-r--r--source/sourceSpec.go142
-rw-r--r--tpl/cast/cast.go63
-rw-r--r--tpl/cast/cast_test.go120
-rw-r--r--tpl/cast/docshelper.go49
-rw-r--r--tpl/cast/init.go58
-rw-r--r--tpl/cast/init_test.go38
-rw-r--r--tpl/collections/append.go38
-rw-r--r--tpl/collections/append_test.go66
-rw-r--r--tpl/collections/apply.go162
-rw-r--r--tpl/collections/apply_test.go68
-rw-r--r--tpl/collections/collections.go699
-rw-r--r--tpl/collections/collections_test.go872
-rw-r--r--tpl/collections/complement.go58
-rw-r--r--tpl/collections/complement_test.go95
-rw-r--r--tpl/collections/index.go107
-rw-r--r--tpl/collections/index_test.go60
-rw-r--r--tpl/collections/init.go191
-rw-r--r--tpl/collections/init_test.go38
-rw-r--r--tpl/collections/reflect_helpers.go209
-rw-r--r--tpl/collections/sort.go161
-rw-r--r--tpl/collections/sort_test.go237
-rw-r--r--tpl/collections/symdiff.go71
-rw-r--r--tpl/collections/symdiff_test.go80
-rw-r--r--tpl/collections/where.go476
-rw-r--r--tpl/collections/where_test.go724
-rw-r--r--tpl/compare/compare.go254
-rw-r--r--tpl/compare/compare_test.go266
-rw-r--r--tpl/compare/init.go107
-rw-r--r--tpl/compare/init_test.go38
-rw-r--r--tpl/compare/truth.go73
-rw-r--r--tpl/compare/truth_test.go60
-rw-r--r--tpl/crypto/crypto.go65
-rw-r--r--tpl/crypto/crypto_test.go103
-rw-r--r--tpl/crypto/init.go59
-rw-r--r--tpl/crypto/init_test.go38
-rw-r--r--tpl/data/data.go136
-rw-r--r--tpl/data/data_test.go256
-rw-r--r--tpl/data/init.go45
-rw-r--r--tpl/data/init_test.go41
-rw-r--r--tpl/data/resources.go123
-rw-r--r--tpl/data/resources_test.go223
-rw-r--r--tpl/encoding/encoding.go62
-rw-r--r--tpl/encoding/encoding_test.go109
-rw-r--r--tpl/encoding/init.go59
-rw-r--r--tpl/encoding/init_test.go38
-rw-r--r--tpl/fmt/fmt.go55
-rw-r--r--tpl/fmt/init.go64
-rw-r--r--tpl/fmt/init_test.go39
-rw-r--r--tpl/hugo/init.go41
-rw-r--r--tpl/hugo/init_test.go41
-rw-r--r--tpl/images/images.go87
-rw-r--r--tpl/images/images_test.go121
-rw-r--r--tpl/images/init.go42
-rw-r--r--tpl/images/init_test.go38
-rw-r--r--tpl/inflect/inflect.go77
-rw-r--r--tpl/inflect/inflect_test.go49
-rw-r--r--tpl/inflect/init.go61
-rw-r--r--tpl/inflect/init_test.go38
-rw-r--r--tpl/internal/templatefuncRegistry_test.go38
-rw-r--r--tpl/internal/templatefuncsRegistry.go285
-rw-r--r--tpl/lang/init.go52
-rw-r--r--tpl/lang/init_test.go38
-rw-r--r--tpl/lang/lang.go163
-rw-r--r--tpl/lang/lang_test.go63
-rw-r--r--tpl/math/init.go107
-rw-r--r--tpl/math/init_test.go38
-rw-r--r--tpl/math/math.go119
-rw-r--r--tpl/math/math_test.go278
-rw-r--r--tpl/math/round.go61
-rw-r--r--tpl/os/init.go63
-rw-r--r--tpl/os/init_test.go38
-rw-r--r--tpl/os/os.go153
-rw-r--r--tpl/os/os_test.go135
-rw-r--r--tpl/partials/init.go56
-rw-r--r--tpl/partials/init_test.go42
-rw-r--r--tpl/partials/partials.go192
-rw-r--r--tpl/path/init.go61
-rw-r--r--tpl/path/init_test.go38
-rw-r--r--tpl/path/path.go146
-rw-r--r--tpl/path/path_test.go179
-rw-r--r--tpl/reflect/init.go51
-rw-r--r--tpl/reflect/init_test.go39
-rw-r--r--tpl/reflect/reflect.go36
-rw-r--r--tpl/reflect/reflect_test.go55
-rw-r--r--tpl/resources/init.go68
-rw-r--r--tpl/resources/resources.go270
-rw-r--r--tpl/safe/init.go81
-rw-r--r--tpl/safe/init_test.go38
-rw-r--r--tpl/safe/safe.go73
-rw-r--r--tpl/safe/safe_test.go214
-rw-r--r--tpl/site/init.go45
-rw-r--r--tpl/site/init_test.go40
-rw-r--r--tpl/strings/init.go192
-rw-r--r--tpl/strings/init_test.go39
-rw-r--r--tpl/strings/regexp.go109
-rw-r--r--tpl/strings/regexp_test.go86
-rw-r--r--tpl/strings/strings.go460
-rw-r--r--tpl/strings/strings_test.go773
-rw-r--r--tpl/strings/truncate.go157
-rw-r--r--tpl/strings/truncate_test.go84
-rw-r--r--tpl/template.go305
-rw-r--r--tpl/template_info.go42
-rw-r--r--tpl/template_test.go31
-rw-r--r--tpl/templates/init.go44
-rw-r--r--tpl/templates/init_test.go38
-rw-r--r--tpl/templates/templates.go40
-rw-r--r--tpl/time/init.go87
-rw-r--r--tpl/time/init_test.go38
-rw-r--r--tpl/time/time.go107
-rw-r--r--tpl/time/time_test.go100
-rw-r--r--tpl/tplimpl/ace.go68
-rw-r--r--tpl/tplimpl/amber_compiler.go44
-rw-r--r--tpl/tplimpl/embedded/.gitattributes1
-rw-r--r--tpl/tplimpl/embedded/README.md5
-rw-r--r--tpl/tplimpl/embedded/generate/generate.go100
-rw-r--r--tpl/tplimpl/embedded/templates.autogen.go532
-rw-r--r--tpl/tplimpl/embedded/templates/_default/robots.txt1
-rw-r--r--tpl/tplimpl/embedded/templates/_default/rss.xml32
-rw-r--r--tpl/tplimpl/embedded/templates/_default/sitemap.xml22
-rw-r--r--tpl/tplimpl/embedded/templates/_default/sitemapindex.xml11
-rw-r--r--tpl/tplimpl/embedded/templates/disqus.html23
-rw-r--r--tpl/tplimpl/embedded/templates/google_analytics.html39
-rw-r--r--tpl/tplimpl/embedded/templates/google_analytics_async.html28
-rw-r--r--tpl/tplimpl/embedded/templates/google_news.html3
-rw-r--r--tpl/tplimpl/embedded/templates/opengraph.html49
-rw-r--r--tpl/tplimpl/embedded/templates/pagination.html40
-rw-r--r--tpl/tplimpl/embedded/templates/schema.html15
-rw-r--r--tpl/tplimpl/embedded/templates/shortcodes/__h_simple_assets.html34
-rw-r--r--tpl/tplimpl/embedded/templates/shortcodes/figure.html28
-rw-r--r--tpl/tplimpl/embedded/templates/shortcodes/gist.html1
-rw-r--r--tpl/tplimpl/embedded/templates/shortcodes/highlight.html1
-rw-r--r--tpl/tplimpl/embedded/templates/shortcodes/instagram.html10
-rw-r--r--tpl/tplimpl/embedded/templates/shortcodes/instagram_simple.html48
-rw-r--r--tpl/tplimpl/embedded/templates/shortcodes/param.html4
-rw-r--r--tpl/tplimpl/embedded/templates/shortcodes/ref.html1
-rw-r--r--tpl/tplimpl/embedded/templates/shortcodes/relref.html1
-rw-r--r--tpl/tplimpl/embedded/templates/shortcodes/twitter.html10
-rw-r--r--tpl/tplimpl/embedded/templates/shortcodes/twitter_simple.html33
-rw-r--r--tpl/tplimpl/embedded/templates/shortcodes/vimeo.html14
-rw-r--r--tpl/tplimpl/embedded/templates/shortcodes/vimeo_simple.html18
-rw-r--r--tpl/tplimpl/embedded/templates/shortcodes/youtube.html9
-rw-r--r--tpl/tplimpl/embedded/templates/twitter_cards.html29
-rw-r--r--tpl/tplimpl/shortcodes.go160
-rw-r--r--tpl/tplimpl/shortcodes_test.go98
-rw-r--r--tpl/tplimpl/template.go1057
-rw-r--r--tpl/tplimpl/templateFuncster.go33
-rw-r--r--tpl/tplimpl/templateProvider.go63
-rw-r--r--tpl/tplimpl/template_ast_transformers.go535
-rw-r--r--tpl/tplimpl/template_ast_transformers_test.go551
-rw-r--r--tpl/tplimpl/template_errors.go46
-rw-r--r--tpl/tplimpl/template_funcs.go80
-rw-r--r--tpl/tplimpl/template_funcs_test.go222
-rw-r--r--tpl/tplimpl/template_info_test.go56
-rw-r--r--tpl/transform/init.go111
-rw-r--r--tpl/transform/init_test.go38
-rw-r--r--tpl/transform/remarshal.go62
-rw-r--r--tpl/transform/remarshal_test.go172
-rw-r--r--tpl/transform/transform.go123
-rw-r--r--tpl/transform/transform_test.go257
-rw-r--r--tpl/transform/unmarshal.go167
-rw-r--r--tpl/transform/unmarshal_test.go226
-rw-r--r--tpl/urls/init.go74
-rw-r--r--tpl/urls/init_test.go39
-rw-r--r--tpl/urls/urls.go182
-rw-r--r--tpl/urls/urls_test.go68
-rw-r--r--transform/chain.go112
-rw-r--r--transform/chain_test.go69
-rw-r--r--transform/livereloadinject/livereloadinject.go47
-rw-r--r--transform/livereloadinject/livereloadinject_test.go41
-rw-r--r--transform/metainject/hugogenerator.go55
-rw-r--r--transform/metainject/hugogenerator_test.go61
-rw-r--r--transform/urlreplacers/absurl.go36
-rw-r--r--transform/urlreplacers/absurlreplacer.go245
-rw-r--r--transform/urlreplacers/absurlreplacer_test.go237
-rw-r--r--watcher/batcher.go73
1552 files changed, 104162 insertions, 112 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 000000000..884631e76
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,51 @@
+defaults: &defaults
+ docker:
+ - image: bepsays/ci-goreleaser:1.12-3
+ environment:
+ CGO_ENABLED: "0"
+
+version: 2
+jobs:
+ build:
+ <<: *defaults
+ steps:
+ - checkout:
+ path: hugo
+ - run:
+ command: |
+ git clone git@github.com:gohugoio/hugoDocs.git
+ cd hugo
+ go mod download
+ sleep 5
+ go test -p 1 ./...
+ - persist_to_workspace:
+ root: .
+ paths: .
+ release:
+ <<: *defaults
+ steps:
+ - attach_workspace:
+ at: /root/project
+ - run:
+ command: |
+ cd hugo
+ git config --global user.email "bjorn.erik.pedersen+hugoreleaser@gmail.com"
+ git config --global user.name "hugoreleaser"
+ go run -tags release main.go release -r ${CIRCLE_BRANCH}
+
+workflows:
+ version: 2
+ release:
+ jobs:
+ - build:
+ filters:
+ branches:
+ only: /release-.*/
+ - hold:
+ type: approval
+ requires:
+ - build
+ - release:
+ context: org-global
+ requires:
+ - hold
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..a183f6fcf
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,9 @@
+*.md
+*.log
+*.txt
+.git
+.github
+.circleci
+docs
+examples
+Dockerfile
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..6994810cf
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,8 @@
+# Text files have auto line endings
+* text=auto
+
+# Go source files always have LF line endings
+*.go text eol=lf
+
+# SVG files should not be modified
+*.svg -text
diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md
new file mode 100644
index 000000000..cc9de09ff
--- /dev/null
+++ b/.github/SUPPORT.md
@@ -0,0 +1,3 @@
+### 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.
diff --git a/.github/stale.yml b/.github/stale.yml
index 389205294..692c59659 100644
--- a/.github/stale.yml
+++ b/.github/stale.yml
@@ -6,7 +6,6 @@ daysUntilClose: 30
exemptLabels:
- Keep
- Security
- - UndocumentedFeature
# Label to use when marking an issue as stale
staleLabel: Stale
# Comment to post when marking an issue as stale. Set to `false` to disable
@@ -14,7 +13,9 @@ markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. The resources of the Hugo team are limited, and so we are asking for your help.
- If you still think this is important, please tell us why.
+ If this is a **bug** and you can still reproduce this error on the <code>master</code> branch, please reply with all of the information you have about it in order to keep the issue open.
+
+ If this is a **feature request**, and you feel that it is still relevant and valuable, please tell us why.
This issue will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
diff --git a/.gitignore b/.gitignore
index b203a37cd..89244f128 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,24 @@
+/hugo
+docs/public*
/.idea
-/public
+hugo.exe
+*.test
+*.prof
nohup.out
+cover.out
+*.swp
+*.swo
.DS_Store
-trace.out \ No newline at end of file
+*~
+vendor/*/
+*.bench
+*.debug
+coverage*.out
+
+dock.sh
+
+GoBuilds
+dist
+
+
+vendor \ No newline at end of file
diff --git a/themes/gohugoioTheme/assets/js/filesaver.js b/.gitmodules
index e69de29bb..e69de29bb 100644
--- a/themes/gohugoioTheme/assets/js/filesaver.js
+++ b/.gitmodules
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 000000000..e93adabc1
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,3 @@
+spf13 <steve.francia@gmail.com> Steve Francia <steve.francia@gmail.com>
+bep <bjorn.erik.pedersen@gmail.com> Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
+
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..30d58247a
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,46 @@
+language: go
+sudo: false
+dist: xenial
+env:
+ global:
+ - GOPROXY="https://proxy.golang.org"
+ - HUGO_BUILD_TAGS="extended"
+git:
+ depth: false
+go:
+ - "1.11.10"
+ - "1.12.5"
+ - tip
+os:
+ - linux
+ - osx
+ - windows
+matrix:
+ allow_failures:
+ - go: tip
+ fast_finish: true
+ exclude:
+ - os: windows
+ go: tip
+
+install:
+ - mkdir -p $HOME/src
+ - mv $TRAVIS_BUILD_DIR $HOME/src
+ - export TRAVIS_BUILD_DIR=$HOME/src/hugo
+ - cd $HOME/src/hugo
+ - go get github.com/magefile/mage
+script:
+ - go mod download
+ - mage -v test
+ - mage -v check
+ - mage -v hugo
+ - ./hugo -s docs/
+ - ./hugo --renderToMemory -s docs/
+ - df -h
+
+before_install:
+ - df -h
+ # https://travis-ci.community/t/go-cant-find-gcc-with-go1-11-1-on-windows/293/5
+ - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then choco install mingw -y; export PATH=/c/tools/mingw64/bin:"$PATH"; fi
+ - gem install asciidoctor
+ - type asciidoctor
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..124e5b754
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,191 @@
+# 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.
+
+*Note that this repository only contains the actual source code of Hugo. For **only** documentation-related pull requests / issues please refer to the [hugoDocs](https://github.com/gohugoio/hugoDocs) repository.*
+
+*Changes to the codebase **and** related documentation, e.g. for a new feature, should still use a single pull request.*
+
+## 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)
+ * [Fetching the Sources From GitHub](#fetching-the-sources-from-github)
+ * [Building Hugo with Your Changes](#building-hugo-with-your-changes)
+
+## 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.
+
+## Code Contribution
+
+Hugo has become a fully featured static site generator, so any new functionality must:
+
+* be useful to many.
+* fit naturally into _what Hugo does best._
+* strive not to break existing sites.
+* close or update an open [Hugo issue](https://github.com/gohugoio/hugo/issues)
+
+If it is of some complexity, the contributor is expected to maintain and support the new future (answer questions on the forum, fix any bugs etc.).
+
+It is recommended to open up a discussion on the [Hugo Forum](https://discourse.gohugo.io/) to get feedback on your idea before you begin. If you are submitting a complex feature, create a small design proposal on the [Hugo issue tracker](https://github.com/gohugoio/hugo/issues) before you start.
+
+
+**Bug fixes are, of course, always welcome.**
+
+
+
+## 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.
+
+### Code Contribution Guidelines
+
+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.
+
+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 `mage check` succeeds. [Travis CI](https://travis-ci.org/gohugoio/hugo) (Windows, Linux and macOS) will fail the build if `mage 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 package name (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 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
+```
+
+### Fetching the Sources From GitHub
+
+Since Hugo 0.48, Hugo uses the Go Modules support built into Go 1.11 to build. The easiest is to clone Hugo in a directory outside of `GOPATH`, as in the following example:
+
+```bash
+mkdir $HOME/src
+cd $HOME/src
+git clone https://github.com/gohugoio/hugo.git
+cd hugo
+go install
+```
+
+>Note: Some Go tools may not be fully updated to support Go Modules yet. One example would be LiteIDE. Follow [this workaround](https://github.com/visualfc/liteide/issues/986#issuecomment-428117702) for how to continue to work with Hugo below `GOPATH`.
+
+For some convenient build and test targets, you also will want to install Mage:
+
+```bash
+go get github.com/magefile/mage
+```
+
+Now, to make a change to Hugo's source:
+
+1. Create a new branch for your changes (the branch name is arbitrary):
+
+ ```bash
+ git checkout -b iss1234
+ ```
+
+1. After making your changes, commit them to your new branch:
+
+ ```bash
+ 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):
+
+ ```bash
+ git remote add fork git://github.com/USERNAME/hugo.git
+ ```
+
+1. Push the changes to your new remote:
+
+ ```bash
+ git push --set-upstream fork iss1234
+ ```
+
+1. You're now ready to submit a PR based upon the new branch in your forked repository.
+
+### Building Hugo with Your Changes
+
+Hugo uses [mage](https://github.com/magefile/mage) to sync vendor dependencies, build Hugo, run the test suite and other things. You must run mage from the Hugo directory.
+
+```bash
+cd $HOME/go/src/github.com/gohugoio/hugo
+```
+
+To build Hugo:
+
+```bash
+mage hugo
+```
+
+To install hugo in `$HOME/go/bin`:
+
+```bash
+mage install
+```
+
+To run the tests:
+
+```bash
+mage hugoRace
+mage -v check
+```
+
+To list all available commands along with descriptions:
+
+```bash
+mage -l
+```
+
+**Note:** From Hugo 0.43 we have added a build tag, `extended` that adds **SCSS support**. This needs a C compiler installed to build. You can enable this when building by:
+
+```bash
+HUGO_BUILD_TAGS=extended mage install
+````
diff --git a/Dockerfile b/Dockerfile
new file mode 100755
index 000000000..4728a0f2e
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,33 @@
+# GitHub: https://github.com/gohugoio
+# Twitter: https://twitter.com/gohugoio
+# Website: https://gohugo.io/
+
+FROM golang:1.11-stretch AS build
+
+
+WORKDIR /go/src/github.com/gohugoio/hugo
+RUN apt-get install \
+ git gcc g++ binutils
+COPY . /go/src/github.com/gohugoio/hugo/
+ENV GO111MODULE=on
+RUN go get -d .
+
+ARG CGO=0
+ENV CGO_ENABLED=${CGO}
+ENV GOOS=linux
+
+# default non-existent build tag so -tags always has an arg
+ARG BUILD_TAGS="99notag"
+RUN go install -ldflags '-w -extldflags "-static"' -tags ${BUILD_TAGS}
+
+# ---
+
+FROM alpine:3.9
+RUN apk add --no-cache ca-certificates
+COPY --from=build /go/bin/hugo /hugo
+ARG WORKDIR="/site"
+WORKDIR ${WORKDIR}
+VOLUME ${WORKDIR}
+EXPOSE 1313
+ENTRYPOINT [ "/hugo" ]
+CMD [ "--help" ]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000..261eeb9e9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ 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/README.md b/README.md
index 70908ef12..f054a46e7 100644
--- a/README.md
+++ b/README.md
@@ -1,47 +1,187 @@
-[![Netlify Status](https://api.netlify.com/api/v1/badges/e0dbbfc7-34f1-4393-a679-c16e80162705/deploy-status)](https://app.netlify.com/sites/gohugoio/deploys)
+![Hugo](https://raw.githubusercontent.com/gohugoio/hugoDocs/master/static/img/hugo-logo.png)
-# Hugo Docs
+A Fast and Flexible Static Site Generator built with love by [bep](https://github.com/bep), [spf13](http://spf13.com/) and [friends](https://github.com/gohugoio/hugo/graphs/contributors) in [Go][].
-Documentation site for [Hugo](https://github.com/gohugoio/hugo), the very fast and flexible static site generator built with love in Go.
+[Website](https://gohugo.io) |
+[Forum](https://discourse.gohugo.io) |
+[Documentation](https://gohugo.io/getting-started/) |
+[Installation Guide](https://gohugo.io/getting-started/installing/) |
+[Contribution Guide](CONTRIBUTING.md) |
+[Twitter](https://twitter.com/gohugoio)
-## Contributing
+[![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=Windows+and+Linux+and+macOS+build "Windows, Linux and macOS Build Status")](https://travis-ci.org/gohugoio/hugo)
+[![Go Report Card](https://goreportcard.com/badge/github.com/gohugoio/hugo)](https://goreportcard.com/report/github.com/gohugoio/hugo)
-We welcome contributions to Hugo of any kind including documentation, suggestions, bug reports, pull requests etc. Also check out our [contribution guide](https://gohugo.io/contribute/documentation/). We would love to hear from you.
+## Overview
-Note that this repository contains solely the documentation for Hugo. For contributions that aren't documentation-related please refer to the [hugo](https://github.com/gohugoio/hugo) repository.
+Hugo is a static HTML and CSS website generator written in [Go][].
+It is optimized for speed, ease of use, and configurability.
+Hugo takes a directory with content and templates and renders them into a full HTML website.
-*Pull requests shall **only** contain changes to the actual documentation. However, changes on the code base of Hugo **and** the documentation shall be a single, atomic pull request in the [hugo](https://github.com/gohugoio/hugo) repository.*
+Hugo relies on Markdown files with front matter for metadata, 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.
-Spelling fixes are most welcomed, and if you want to contribute longer sections to the documentation, it would be great if you had these in mind when writing:
+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.
-* Short is good. People go to the library to read novels. If there is more than one way to _do a thing_ in Hugo, describe the current _best practice_ (avoid "… but you can also do …" and "… in older versions of Hugo you had to …".
-* For examples, try to find short snippets that teaches people about the concept. If the example is also useful as-is (copy and paste), then great, but don't list long and similar examples just so people can use them on their sites.
-* Hugo has users from all over the world, so an easy to understand and [simple English](https://simple.wikipedia.org/wiki/Basic_English) is good.
+Hugo is designed to work well for any kind of website including blogs, tumbles, and docs.
-## Branches
+#### Supported Architectures
-* The `master` branch is where the site is automatically built from, and is the place to put changes relevant to the current Hugo version.
-* The `next` branch is where we store changes that is related to the next Hugo release. This can be previewed here: https://next--gohugoio.netlify.com/
+Currently, we provide pre-built Hugo binaries for Windows, Linux, FreeBSD, NetBSD, macOS (Darwin), and [Android](https://gist.github.com/bep/a0d8a26cf6b4f8bc992729b8e50b480b) for x64, i386 and ARM architectures.
-## Build
+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.
-To view the documentation site locally, you need to clone this repository:
+**Complete documentation is available at [Hugo Documentation](https://gohugo.io/getting-started/).**
+
+## 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/getting-started/installing/).
+
+### Build and Install the Binaries from Source (Advanced Install)
+
+#### Prerequisite Tools
+
+* [Git](https://git-scm.com/)
+* [Go (at least Go 1.11)](https://golang.org/dl/)
+
+#### Fetch from GitHub
+
+Since Hugo 0.48, Hugo uses the Go Modules support built into Go 1.11 to build. The easiest is to clone Hugo in a directory outside of `GOPATH`, as in the following example:
```bash
-git clone https://github.com/gohugoio/hugoDocs.git
+mkdir $HOME/src
+cd $HOME/src
+git clone https://github.com/gohugoio/hugo.git
+cd hugo
+go install
```
-Also note that the documentation version for a given version of Hugo can also be found in the `/docs` sub-folder of the [Hugo source repository](https://github.com/gohugoio/hugo).
+**If you are a Windows user, substitute the `$HOME` environment variable above with `%USERPROFILE%`.**
+
+## The Hugo Documentation
-Then to view the docs in your browser, run Hugo and open up the link:
+The Hugo documentation now lives in its own repository, see https://github.com/gohugoio/hugoDocs. But we do keep a version of that documentation as a `git subtree` in this repository. To build the sub folder `/docs` as a Hugo site, you need to clone this repo:
```bash
-▶ hugo server
-
-Started building sites ...
-.
-.
-Serving pages from memory
-Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
-Press Ctrl+C to stop
+git clone git@github.com:gohugoio/hugo.git
```
+## 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/
+
+## Dependencies
+
+Hugo stands on the shoulder of many great open source libraries, in lexical order:
+
+ | Dependency | License |
+ | :------------- | :------------- |
+ | [github.com/BurntSushi/locker](https://github.com/BurntSushi/locker) | The Unlicense |
+ | [github.com/BurntSushi/toml](https://github.com/BurntSushi/toml) | MIT License |
+ | [github.com/PuerkitoBio/purell](https://github.com/PuerkitoBio/purell) | BSD 3-Clause "New" or "Revised" License |
+ | [github.com/PuerkitoBio/urlesc](https://github.com/PuerkitoBio/urlesc) | BSD 3-Clause "New" or "Revised" License |
+ | [github.com/alecthomas/chroma](https://github.com/alecthomas/chroma) | MIT License |
+ | [github.com/bep/debounce](https://github.com/bep/debounce) | MIT License |
+ | [github.com/bep/gitmap](https://github.com/bep/gitmap) | MIT License |
+ | [github.com/bep/go-tocss](https://github.com/bep/go-tocss) | MIT License |
+ | [github.com/chaseadamsio/goorgeous](https://github.com/chaseadamsio/goorgeous) | MIT License |
+ | [github.com/cpuguy83/go-md2man](https://github.com/cpuguy83/go-md2man) | MIT License |
+ | [github.com/danwakefield/fnmatch](https://github.com/danwakefield/fnmatch) | BSD 2-Clause "Simplified" License |
+ | [github.com/disintegration/imaging](https://github.com/disintegration/imaging) | MIT License |
+ | [github.com/dlclark/regexp2](https://github.com/dlclark/regexp2) | MIT License |
+ | [github.com/eknkc/amber](https://github.com/eknkc/amber) | MIT License |
+ | [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify) | BSD 3-Clause "New" or "Revised" License |
+ | [github.com/gobwas/glob](https://github.com/gobwas/glob) | MIT License |
+ | [github.com/gorilla/websocket](https://github.com/gorilla/websocket) | BSD 2-Clause "Simplified" License |
+ | [github.com/hashicorp/go-immutable-radix](https://github.com/hashicorp/go-immutable-radix) | Mozilla Public License 2.0 |
+ | [github.com/hashicorp/golang-lru](https://github.com/hashicorp/golang-lru) | Mozilla Public License 2.0 |
+ | [github.com/hashicorp/hcl](https://github.com/hashicorp/hcl) | Mozilla Public License 2.0 |
+ | [github.com/jdkato/prose](https://github.com/jdkato/prose) | MIT License |
+ | [github.com/kyokomi/emoji](https://github.com/kyokomi/emoji) | MIT License |
+ | [github.com/magiconair/properties](https://github.com/magiconair/properties) | BSD 2-Clause "Simplified" License |
+ | [github.com/markbates/inflect](https://github.com/markbates/inflect) | MIT License |
+ | [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) | MIT License |
+ | [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) | MIT License |
+ | [github.com/miekg/mmark](https://github.com/miekg/mmark) | Simplified BSD License |
+ | [github.com/mitchellh/hashstructure](https://github.com/mitchellh/hashstructure) | MIT License |
+ | [github.com/mitchellh/mapstructure](https://github.com/mitchellh/mapstructure) | MIT License |
+ | [github.com/muesli/smartcrop](https://github.com/muesli/smartcrop) | MIT License |
+ | [github.com/nicksnyder/go-i18n](https://github.com/nicksnyder/go-i18n) | MIT License |
+ | [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) | MIT License |
+ | [github.com/pelletier/go-toml](https://github.com/pelletier/go-toml) | MIT License |
+ | [github.com/pkg/errors](https://github.com/pkg/errors) | BSD 2-Clause "Simplified" License |
+ | [github.com/russross/blackfriday](https://github.com/russross/blackfriday) | Simplified BSD License |
+ | [github.com/shurcooL/sanitized_anchor_name](https://github.com/shurcooL/sanitized_anchor_name) | MIT License |
+ | [github.com/spf13/afero](https://github.com/spf13/afero) | Apache License 2.0 |
+ | [github.com/spf13/cast](https://github.com/spf13/cast) | MIT License |
+ | [github.com/spf13/cobra](https://github.com/spf13/cobra) | Apache License 2.0 |
+ | [github.com/spf13/fsync](https://github.com/spf13/fsync) | MIT License |
+ | [github.com/spf13/jwalterweatherman](https://github.com/spf13/jwalterweatherman) | MIT License |
+ | [github.com/spf13/nitro](https://github.com/spf13/nitro) | Apache License 2.0 |
+ | [github.com/spf13/pflag](https://github.com/spf13/pflag) | BSD 3-Clause "New" or "Revised" License |
+ | [github.com/spf13/viper](https://github.com/spf13/viper) | MIT License |
+ | [github.com/tdewolff/minify](https://github.com/tdewolff/minify) | MIT License |
+ | [github.com/tdewolff/parse](https://github.com/tdewolff/parse) | MIT License |
+ | [github.com/wellington/go-libsass](https://github.com/wellington/go-libsass) | Apache License 2.0 |
+ | [github.com/yosssi/ace](https://github.com/yosssi/ace) | MIT License |
+ | [golang.org/x/image](https://golang.org/x/image) | BSD 3-Clause "New" or "Revised" License |
+ | [golang.org/x/net](https://golang.org/x/net) | BSD 3-Clause "New" or "Revised" License |
+ | [golang.org/x/sync](https://golang.org/x/sync) | BSD 3-Clause "New" or "Revised" License |
+ | [golang.org/x/sys](https://golang.org/x/sys) | BSD 3-Clause "New" or "Revised" License |
+ | [golang.org/x/text](https://golang.org/x/text) | BSD 3-Clause "New" or "Revised" License
+ | [gopkg.in/yaml.v2](https://gopkg.in/yaml.v2) | Apache License 2.0 |
+
+
+
+
+
+
+
diff --git a/bench.sh b/bench.sh
new file mode 100755
index 000000000..c6a20a7e3
--- /dev/null
+++ b/bench.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+
+# allow user to override go executable by running as GOEXE=xxx make ...
+GOEXE="${GOEXE-go}"
+
+# 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
+"${GOEXE}" test -test.run=NONE -bench="$benchFilter" -test.benchmem=true ./$PACKAGE > /tmp/bench-$PACKAGE-$BRANCH.txt
+
+git checkout master
+"${GOEXE}" 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
diff --git a/benchSite.sh b/benchSite.sh
new file mode 100755
index 000000000..623086708
--- /dev/null
+++ b/benchSite.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+# allow user to override go executable by running as GOEXE=xxx make ...
+GOEXE="${GOEXE-go}"
+
+# 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}"
+
+"${GOEXE}" test -run="NONE" -bench="BenchmarkSiteBuilding/${1}" -test.benchmem=true ./hugolib -memprofile mem.prof -count 3 -cpuprofile cpu.prof
diff --git a/benchbep.sh b/benchbep.sh
new file mode 100755
index 000000000..fabd30c18
--- /dev/null
+++ b/benchbep.sh
@@ -0,0 +1,2 @@
+gobench -package=./hugolib -bench="BenchmarkSiteBuilding/YAML,num_langs=3,num_pages=5000,tags_per_page=5,shortcodes,render" -count=3 > 1.bench
+benchcmp -best 0.bench 1.bench \ No newline at end of file
diff --git a/bepdock.sh b/bepdock.sh
new file mode 100755
index 000000000..a7ac0c639
--- /dev/null
+++ b/bepdock.sh
@@ -0,0 +1 @@
+docker run --rm --mount type=bind,source="$(pwd)",target=/hugo -w /hugo -i -t bepsays/ci-goreleaser:1.11-2 /bin/bash \ 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/filecache/filecache.go b/cache/filecache/filecache.go
new file mode 100644
index 000000000..6ad417117
--- /dev/null
+++ b/cache/filecache/filecache.go
@@ -0,0 +1,353 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package filecache
+
+import (
+ "bytes"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/gohugoio/hugo/common/hugio"
+
+ "github.com/gohugoio/hugo/helpers"
+
+ "github.com/BurntSushi/locker"
+ "github.com/spf13/afero"
+)
+
+const (
+ filecacheRootDirname = "filecache"
+)
+
+// Cache caches a set of files in a directory. This is usually a file on
+// disk, but since this is backed by an Afero file system, it can be anything.
+type Cache struct {
+ Fs afero.Fs
+
+ // Max age for items in this cache. Negative duration means forever,
+ // 0 is effectively turning this cache off.
+ maxAge time.Duration
+
+ nlocker *lockTracker
+}
+
+type lockTracker struct {
+ seenMu sync.RWMutex
+ seen map[string]struct{}
+
+ *locker.Locker
+}
+
+// Lock tracks the ids in use. We use this information to do garbage collection
+// after a Hugo build.
+func (l *lockTracker) Lock(id string) {
+ l.seenMu.RLock()
+ if _, seen := l.seen[id]; !seen {
+ l.seenMu.RUnlock()
+ l.seenMu.Lock()
+ l.seen[id] = struct{}{}
+ l.seenMu.Unlock()
+ } else {
+ l.seenMu.RUnlock()
+ }
+
+ l.Locker.Lock(id)
+}
+
+// ItemInfo contains info about a cached file.
+type ItemInfo struct {
+ // This is the file's name relative to the cache's filesystem.
+ Name string
+}
+
+// NewCache creates a new file cache with the given filesystem and max age.
+func NewCache(fs afero.Fs, maxAge time.Duration) *Cache {
+ return &Cache{
+ Fs: fs,
+ nlocker: &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})},
+ maxAge: maxAge,
+ }
+}
+
+// lockedFile is a file with a lock that is released on Close.
+type lockedFile struct {
+ afero.File
+ unlock func()
+}
+
+func (l *lockedFile) Close() error {
+ defer l.unlock()
+ return l.File.Close()
+}
+
+// WriteCloser returns a transactional writer into the cache.
+// It's important that it's closed when done.
+func (c *Cache) WriteCloser(id string) (ItemInfo, io.WriteCloser, error) {
+ id = cleanID(id)
+ c.nlocker.Lock(id)
+
+ info := ItemInfo{Name: id}
+
+ f, err := helpers.OpenFileForWriting(c.Fs, id)
+ if err != nil {
+ c.nlocker.Unlock(id)
+ return info, nil, err
+ }
+
+ return info, &lockedFile{
+ File: f,
+ unlock: func() { c.nlocker.Unlock(id) },
+ }, nil
+}
+
+// ReadOrCreate tries to lookup the file in cache.
+// If found, it is passed to read and then closed.
+// If not found a new file is created and passed to create, which should close
+// it when done.
+func (c *Cache) ReadOrCreate(id string,
+ read func(info ItemInfo, r io.Reader) error,
+ create func(info ItemInfo, w io.WriteCloser) error) (info ItemInfo, err error) {
+ id = cleanID(id)
+
+ c.nlocker.Lock(id)
+ defer c.nlocker.Unlock(id)
+
+ info = ItemInfo{Name: id}
+
+ if r := c.getOrRemove(id); r != nil {
+ err = read(info, r)
+ defer r.Close()
+ return
+ }
+
+ f, err := helpers.OpenFileForWriting(c.Fs, id)
+ if err != nil {
+ return
+ }
+
+ err = create(info, f)
+
+ return
+
+}
+
+// GetOrCreate tries to get the file with the given id from cache. If not found or expired, create will
+// be invoked and the result cached.
+// This method is protected by a named lock using the given id as identifier.
+func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (ItemInfo, io.ReadCloser, error) {
+ id = cleanID(id)
+
+ c.nlocker.Lock(id)
+ defer c.nlocker.Unlock(id)
+
+ info := ItemInfo{Name: id}
+
+ if r := c.getOrRemove(id); r != nil {
+ return info, r, nil
+ }
+
+ r, err := create()
+ if err != nil {
+ return info, nil, err
+ }
+
+ if c.maxAge == 0 {
+ // No caching.
+ return info, hugio.ToReadCloser(r), nil
+ }
+
+ var buff bytes.Buffer
+ return info,
+ hugio.ToReadCloser(&buff),
+ afero.WriteReader(c.Fs, id, io.TeeReader(r, &buff))
+}
+
+// GetOrCreateBytes is the same as GetOrCreate, but produces a byte slice.
+func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (ItemInfo, []byte, error) {
+ id = cleanID(id)
+
+ c.nlocker.Lock(id)
+ defer c.nlocker.Unlock(id)
+
+ info := ItemInfo{Name: id}
+
+ if r := c.getOrRemove(id); r != nil {
+ defer r.Close()
+ b, err := ioutil.ReadAll(r)
+ return info, b, err
+ }
+
+ b, err := create()
+ if err != nil {
+ return info, nil, err
+ }
+
+ if c.maxAge == 0 {
+ return info, b, nil
+ }
+
+ if err := afero.WriteReader(c.Fs, id, bytes.NewReader(b)); err != nil {
+ return info, nil, err
+ }
+ return info, b, nil
+
+}
+
+// GetBytes gets the file content with the given id from the cahce, nil if none found.
+func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) {
+ id = cleanID(id)
+
+ c.nlocker.Lock(id)
+ defer c.nlocker.Unlock(id)
+
+ info := ItemInfo{Name: id}
+
+ if r := c.getOrRemove(id); r != nil {
+ defer r.Close()
+ b, err := ioutil.ReadAll(r)
+ return info, b, err
+ }
+
+ return info, nil, nil
+}
+
+// Get gets the file with the given id from the cahce, nil if none found.
+func (c *Cache) Get(id string) (ItemInfo, io.ReadCloser, error) {
+ id = cleanID(id)
+
+ c.nlocker.Lock(id)
+ defer c.nlocker.Unlock(id)
+
+ info := ItemInfo{Name: id}
+
+ r := c.getOrRemove(id)
+
+ return info, r, nil
+}
+
+// getOrRemove gets the file with the given id. If it's expired, it will
+// be removed.
+func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser {
+ if c.maxAge == 0 {
+ // No caching.
+ return nil
+ }
+
+ if c.maxAge > 0 {
+ fi, err := c.Fs.Stat(id)
+ if err != nil {
+ return nil
+ }
+
+ if c.isExpired(fi.ModTime()) {
+ c.Fs.Remove(id)
+ return nil
+ }
+ }
+
+ f, err := c.Fs.Open(id)
+
+ if err != nil {
+ return nil
+ }
+
+ return f
+}
+
+func (c *Cache) isExpired(modTime time.Time) bool {
+ if c.maxAge < 0 {
+ return false
+ }
+ return c.maxAge == 0 || time.Since(modTime) > c.maxAge
+}
+
+// For testing
+func (c *Cache) getString(id string) string {
+ id = cleanID(id)
+
+ c.nlocker.Lock(id)
+ defer c.nlocker.Unlock(id)
+
+ f, err := c.Fs.Open(id)
+
+ if err != nil {
+ return ""
+ }
+ defer f.Close()
+
+ b, _ := ioutil.ReadAll(f)
+ return string(b)
+
+}
+
+// Caches is a named set of caches.
+type Caches map[string]*Cache
+
+// Get gets a named cache, nil if none found.
+func (f Caches) Get(name string) *Cache {
+ return f[strings.ToLower(name)]
+}
+
+// NewCaches creates a new set of file caches from the given
+// configuration.
+func NewCaches(p *helpers.PathSpec) (Caches, error) {
+ dcfg, err := decodeConfig(p)
+ if err != nil {
+ return nil, err
+ }
+
+ fs := p.Fs.Source
+
+ m := make(Caches)
+ for k, v := range dcfg {
+ var cfs afero.Fs
+
+ if v.isResourceDir {
+ cfs = p.BaseFs.Resources.Fs
+ } else {
+ cfs = fs
+ }
+
+ var baseDir string
+ if !strings.HasPrefix(v.Dir, "_gen") {
+ // We do cache eviction (file removes) and since the user can set
+ // his/hers own cache directory, we really want to make sure
+ // we do not delete any files that do not belong to this cache.
+ // We do add the cache name as the root, but this is an extra safe
+ // guard. We skip the files inside /resources/_gen/ because
+ // that would be breaking.
+ baseDir = filepath.Join(v.Dir, filecacheRootDirname, k)
+ } else {
+ baseDir = filepath.Join(v.Dir, k)
+ }
+ if err = cfs.MkdirAll(baseDir, 0777); err != nil && !os.IsExist(err) {
+ return nil, err
+ }
+
+ bfs := afero.NewBasePathFs(cfs, baseDir)
+
+ m[k] = NewCache(bfs, v.MaxAge)
+ }
+
+ return m, nil
+}
+
+func cleanID(name string) string {
+ return strings.TrimPrefix(filepath.Clean(name), helpers.FilePathSeparator)
+}
diff --git a/cache/filecache/filecache_config.go b/cache/filecache/filecache_config.go
new file mode 100644
index 000000000..a6a0252b2
--- /dev/null
+++ b/cache/filecache/filecache_config.go
@@ -0,0 +1,202 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package filecache
+
+import (
+ "path"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/gohugoio/hugo/helpers"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/pkg/errors"
+ "github.com/spf13/afero"
+)
+
+const (
+ cachesConfigKey = "caches"
+
+ resourcesGenDir = ":resourceDir/_gen"
+)
+
+var defaultCacheConfig = cacheConfig{
+ MaxAge: -1, // Never expire
+ Dir: ":cacheDir/:project",
+}
+
+const (
+ cacheKeyGetJSON = "getjson"
+ cacheKeyGetCSV = "getcsv"
+ cacheKeyImages = "images"
+ cacheKeyAssets = "assets"
+)
+
+var defaultCacheConfigs = map[string]cacheConfig{
+ cacheKeyGetJSON: defaultCacheConfig,
+ cacheKeyGetCSV: defaultCacheConfig,
+ cacheKeyImages: {
+ MaxAge: -1,
+ Dir: resourcesGenDir,
+ },
+ cacheKeyAssets: {
+ MaxAge: -1,
+ Dir: resourcesGenDir,
+ },
+}
+
+type cachesConfig map[string]cacheConfig
+
+type cacheConfig struct {
+ // Max age of cache entries in this cache. Any items older than this will
+ // be removed and not returned from the cache.
+ // a negative value means forever, 0 means cache is disabled.
+ MaxAge time.Duration
+
+ // The directory where files are stored.
+ Dir string
+
+ // Will resources/_gen will get its own composite filesystem that
+ // also checks any theme.
+ isResourceDir bool
+}
+
+// GetJSONCache gets the file cache for getJSON.
+func (f Caches) GetJSONCache() *Cache {
+ return f[cacheKeyGetJSON]
+}
+
+// GetCSVCache gets the file cache for getCSV.
+func (f Caches) GetCSVCache() *Cache {
+ return f[cacheKeyGetCSV]
+}
+
+// ImageCache gets the file cache for processed images.
+func (f Caches) ImageCache() *Cache {
+ return f[cacheKeyImages]
+}
+
+// AssetsCache gets the file cache for assets (processed resources, SCSS etc.).
+func (f Caches) AssetsCache() *Cache {
+ return f[cacheKeyAssets]
+}
+
+func decodeConfig(p *helpers.PathSpec) (cachesConfig, error) {
+ c := make(cachesConfig)
+ valid := make(map[string]bool)
+ // Add defaults
+ for k, v := range defaultCacheConfigs {
+ c[k] = v
+ valid[k] = true
+ }
+
+ cfg := p.Cfg
+
+ m := cfg.GetStringMap(cachesConfigKey)
+
+ _, isOsFs := p.Fs.Source.(*afero.OsFs)
+
+ for k, v := range m {
+ cc := defaultCacheConfig
+
+ dc := &mapstructure.DecoderConfig{
+ Result: &cc,
+ DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
+ WeaklyTypedInput: true,
+ }
+
+ decoder, err := mapstructure.NewDecoder(dc)
+ if err != nil {
+ return c, err
+ }
+
+ if err := decoder.Decode(v); err != nil {
+ return nil, err
+ }
+
+ if cc.Dir == "" {
+ return c, errors.New("must provide cache Dir")
+ }
+
+ name := strings.ToLower(k)
+ if !valid[name] {
+ return nil, errors.Errorf("%q is not a valid cache name", name)
+ }
+
+ c[name] = cc
+ }
+
+ // This is a very old flag in Hugo, but we need to respect it.
+ disabled := cfg.GetBool("ignoreCache")
+
+ for k, v := range c {
+ dir := filepath.ToSlash(filepath.Clean(v.Dir))
+ hadSlash := strings.HasPrefix(dir, "/")
+ parts := strings.Split(dir, "/")
+
+ for i, part := range parts {
+ if strings.HasPrefix(part, ":") {
+ resolved, isResource, err := resolveDirPlaceholder(p, part)
+ if err != nil {
+ return c, err
+ }
+ if isResource {
+ v.isResourceDir = true
+ }
+ parts[i] = resolved
+ }
+ }
+
+ dir = path.Join(parts...)
+ if hadSlash {
+ dir = "/" + dir
+ }
+ v.Dir = filepath.Clean(filepath.FromSlash(dir))
+
+ if !v.isResourceDir {
+ if isOsFs && !filepath.IsAbs(v.Dir) {
+ return c, errors.Errorf("%q must resolve to an absolute directory", v.Dir)
+ }
+
+ // Avoid cache in root, e.g. / (Unix) or c:\ (Windows)
+ if len(strings.TrimPrefix(v.Dir, filepath.VolumeName(v.Dir))) == 1 {
+ return c, errors.Errorf("%q is a root folder and not allowed as cache dir", v.Dir)
+ }
+ }
+
+ if disabled {
+ v.MaxAge = 0
+ }
+
+ c[k] = v
+ }
+
+ return c, nil
+}
+
+// Resolves :resourceDir => /myproject/resources etc., :cacheDir => ...
+func resolveDirPlaceholder(p *helpers.PathSpec, placeholder string) (cacheDir string, isResource bool, err error) {
+ switch strings.ToLower(placeholder) {
+ case ":resourcedir":
+ return "", true, nil
+ case ":cachedir":
+ d, err := helpers.GetCacheDir(p.Fs.Source, p.Cfg)
+ return d, false, err
+ case ":project":
+ return filepath.Base(p.WorkingDir), false, nil
+ }
+
+ return "", false, errors.Errorf("%q is not a valid placeholder (valid values are :cacheDir or :resourceDir)", placeholder)
+}
diff --git a/cache/filecache/filecache_config_test.go b/cache/filecache/filecache_config_test.go
new file mode 100644
index 000000000..b0f5d2dc0
--- /dev/null
+++ b/cache/filecache/filecache_config_test.go
@@ -0,0 +1,207 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package filecache
+
+import (
+ "path/filepath"
+ "runtime"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/gohugoio/hugo/helpers"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/hugofs"
+
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/require"
+)
+
+func TestDecodeConfig(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ configStr := `
+resourceDir = "myresources"
+contentDir = "content"
+dataDir = "data"
+i18nDir = "i18n"
+layoutDir = "layouts"
+assetDir = "assets"
+archetypeDir = "archetypes"
+
+[caches]
+[caches.getJSON]
+maxAge = "10m"
+dir = "/path/to/c1"
+[caches.getCSV]
+maxAge = "11h"
+dir = "/path/to/c2"
+[caches.images]
+dir = "/path/to/c3"
+
+`
+
+ cfg, err := config.FromConfigString(configStr, "toml")
+ assert.NoError(err)
+ fs := hugofs.NewMem(cfg)
+ p, err := helpers.NewPathSpec(fs, cfg)
+ assert.NoError(err)
+
+ decoded, err := decodeConfig(p)
+ assert.NoError(err)
+
+ assert.Equal(4, len(decoded))
+
+ c2 := decoded["getcsv"]
+ assert.Equal("11h0m0s", c2.MaxAge.String())
+ assert.Equal(filepath.FromSlash("/path/to/c2"), c2.Dir)
+
+ c3 := decoded["images"]
+ assert.Equal(time.Duration(-1), c3.MaxAge)
+ assert.Equal(filepath.FromSlash("/path/to/c3"), c3.Dir)
+
+}
+
+func TestDecodeConfigIgnoreCache(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ configStr := `
+resourceDir = "myresources"
+contentDir = "content"
+dataDir = "data"
+i18nDir = "i18n"
+layoutDir = "layouts"
+assetDir = "assets"
+archeTypedir = "archetypes"
+
+ignoreCache = true
+[caches]
+[caches.getJSON]
+maxAge = 1234
+dir = "/path/to/c1"
+[caches.getCSV]
+maxAge = 3456
+dir = "/path/to/c2"
+[caches.images]
+dir = "/path/to/c3"
+
+`
+
+ cfg, err := config.FromConfigString(configStr, "toml")
+ assert.NoError(err)
+ fs := hugofs.NewMem(cfg)
+ p, err := helpers.NewPathSpec(fs, cfg)
+ assert.NoError(err)
+
+ decoded, err := decodeConfig(p)
+ assert.NoError(err)
+
+ assert.Equal(4, len(decoded))
+
+ for _, v := range decoded {
+ assert.Equal(time.Duration(0), v.MaxAge)
+ }
+
+}
+
+func TestDecodeConfigDefault(t *testing.T) {
+ assert := require.New(t)
+ cfg := newTestConfig()
+
+ if runtime.GOOS == "windows" {
+ cfg.Set("resourceDir", "c:\\cache\\resources")
+ cfg.Set("cacheDir", "c:\\cache\\thecache")
+
+ } else {
+ cfg.Set("resourceDir", "/cache/resources")
+ cfg.Set("cacheDir", "/cache/thecache")
+ }
+
+ fs := hugofs.NewMem(cfg)
+ p, err := helpers.NewPathSpec(fs, cfg)
+ assert.NoError(err)
+
+ decoded, err := decodeConfig(p)
+
+ assert.NoError(err)
+
+ assert.Equal(4, len(decoded))
+
+ imgConfig := decoded[cacheKeyImages]
+ jsonConfig := decoded[cacheKeyGetJSON]
+
+ if runtime.GOOS == "windows" {
+ assert.Equal("_gen", imgConfig.Dir)
+ } else {
+ assert.Equal("_gen", imgConfig.Dir)
+ assert.Equal("/cache/thecache/hugoproject", jsonConfig.Dir)
+ }
+
+ assert.True(imgConfig.isResourceDir)
+ assert.False(jsonConfig.isResourceDir)
+}
+
+func TestDecodeConfigInvalidDir(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ configStr := `
+resourceDir = "myresources"
+contentDir = "content"
+dataDir = "data"
+i18nDir = "i18n"
+layoutDir = "layouts"
+assetDir = "assets"
+archeTypedir = "archetypes"
+
+[caches]
+[caches.getJSON]
+maxAge = "10m"
+dir = "/"
+
+`
+ if runtime.GOOS == "windows" {
+ configStr = strings.Replace(configStr, "/", "c:\\\\", 1)
+ }
+
+ cfg, err := config.FromConfigString(configStr, "toml")
+ assert.NoError(err)
+ fs := hugofs.NewMem(cfg)
+ p, err := helpers.NewPathSpec(fs, cfg)
+ assert.NoError(err)
+
+ _, err = decodeConfig(p)
+ assert.Error(err)
+
+}
+
+func newTestConfig() *viper.Viper {
+ cfg := viper.New()
+ cfg.Set("workingDir", filepath.FromSlash("/my/cool/hugoproject"))
+ cfg.Set("contentDir", "content")
+ cfg.Set("dataDir", "data")
+ cfg.Set("resourceDir", "resources")
+ cfg.Set("i18nDir", "i18n")
+ cfg.Set("layoutDir", "layouts")
+ cfg.Set("archetypeDir", "archetypes")
+ cfg.Set("assetDir", "assets")
+
+ return cfg
+}
diff --git a/cache/filecache/filecache_pruner.go b/cache/filecache/filecache_pruner.go
new file mode 100644
index 000000000..322eabf92
--- /dev/null
+++ b/cache/filecache/filecache_pruner.go
@@ -0,0 +1,80 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package filecache
+
+import (
+ "io"
+ "os"
+
+ "github.com/pkg/errors"
+ "github.com/spf13/afero"
+)
+
+// Prune removes expired and unused items from this cache.
+// The last one requires a full build so the cache usage can be tracked.
+// Note that we operate directly on the filesystem here, so this is not
+// thread safe.
+func (c Caches) Prune() (int, error) {
+ counter := 0
+ for k, cache := range c {
+ err := afero.Walk(cache.Fs, "", func(name string, info os.FileInfo, err error) error {
+ if info == nil {
+ return nil
+ }
+
+ name = cleanID(name)
+
+ if info.IsDir() {
+ f, err := cache.Fs.Open(name)
+ if err != nil {
+ // This cache dir may not exist.
+ return nil
+ }
+ defer f.Close()
+ _, err = f.Readdirnames(1)
+ if err == io.EOF {
+ // Empty dir.
+ return cache.Fs.Remove(name)
+ }
+
+ return nil
+ }
+
+ shouldRemove := cache.isExpired(info.ModTime())
+
+ if !shouldRemove && len(cache.nlocker.seen) > 0 {
+ // Remove it if it's not been touched/used in the last build.
+ _, seen := cache.nlocker.seen[name]
+ shouldRemove = !seen
+ }
+
+ if shouldRemove {
+ err := cache.Fs.Remove(name)
+ if err == nil {
+ counter++
+ }
+ return err
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return counter, errors.Wrapf(err, "failed to prune cache %q", k)
+ }
+
+ }
+
+ return counter, nil
+}
diff --git a/cache/filecache/filecache_pruner_test.go b/cache/filecache/filecache_pruner_test.go
new file mode 100644
index 000000000..e62a6315a
--- /dev/null
+++ b/cache/filecache/filecache_pruner_test.go
@@ -0,0 +1,118 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package filecache
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugofs"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestPrune(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ configStr := `
+resourceDir = "myresources"
+contentDir = "content"
+dataDir = "data"
+i18nDir = "i18n"
+layoutDir = "layouts"
+assetDir = "assets"
+archeTypedir = "archetypes"
+
+[caches]
+[caches.getjson]
+maxAge = "200ms"
+dir = "/cache/c"
+[caches.getcsv]
+maxAge = "200ms"
+dir = "/cache/d"
+[caches.assets]
+maxAge = "200ms"
+dir = ":resourceDir/_gen"
+[caches.images]
+maxAge = "200ms"
+dir = ":resourceDir/_gen"
+`
+
+ cfg, err := config.FromConfigString(configStr, "toml")
+ assert.NoError(err)
+
+ for _, name := range []string{cacheKeyGetCSV, cacheKeyGetJSON, cacheKeyAssets, cacheKeyImages} {
+ msg := fmt.Sprintf("cache: %s", name)
+ fs := hugofs.NewMem(cfg)
+ p, err := helpers.NewPathSpec(fs, cfg)
+ assert.NoError(err)
+ caches, err := NewCaches(p)
+ assert.NoError(err)
+ cache := caches[name]
+ for i := 0; i < 10; i++ {
+ id := fmt.Sprintf("i%d", i)
+ cache.GetOrCreateBytes(id, func() ([]byte, error) {
+ return []byte("abc"), nil
+ })
+ if i == 4 {
+ // This will expire the first 5
+ time.Sleep(201 * time.Millisecond)
+ }
+ }
+
+ count, err := caches.Prune()
+ assert.NoError(err)
+ assert.Equal(5, count, msg)
+
+ for i := 0; i < 10; i++ {
+ id := fmt.Sprintf("i%d", i)
+ v := cache.getString(id)
+ if i < 5 {
+ assert.Equal("", v, id)
+ } else {
+ assert.Equal("abc", v, id)
+ }
+ }
+
+ caches, err = NewCaches(p)
+ assert.NoError(err)
+ cache = caches[name]
+ // Touch one and then prune.
+ cache.GetOrCreateBytes("i5", func() ([]byte, error) {
+ return []byte("abc"), nil
+ })
+
+ count, err = caches.Prune()
+ assert.NoError(err)
+ assert.Equal(4, count)
+
+ // Now only the i5 should be left.
+ for i := 0; i < 10; i++ {
+ id := fmt.Sprintf("i%d", i)
+ v := cache.getString(id)
+ if i != 5 {
+ assert.Equal("", v, id)
+ } else {
+ assert.Equal("abc", v, id)
+ }
+ }
+
+ }
+
+}
diff --git a/cache/filecache/filecache_test.go b/cache/filecache/filecache_test.go
new file mode 100644
index 000000000..5ac2e9beb
--- /dev/null
+++ b/cache/filecache/filecache_test.go
@@ -0,0 +1,257 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package filecache
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/gohugoio/hugo/common/hugio"
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/helpers"
+
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/spf13/afero"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestFileCache(t *testing.T) {
+ t.Parallel()
+ assert := require.New(t)
+
+ tempWorkingDir, err := ioutil.TempDir("", "hugo_filecache_test_work")
+ assert.NoError(err)
+ defer os.Remove(tempWorkingDir)
+
+ tempCacheDir, err := ioutil.TempDir("", "hugo_filecache_test_cache")
+ assert.NoError(err)
+ defer os.Remove(tempCacheDir)
+
+ osfs := afero.NewOsFs()
+
+ for _, test := range []struct {
+ cacheDir string
+ workingDir string
+ }{
+ // Run with same dirs twice to make sure that works.
+ {tempCacheDir, tempWorkingDir},
+ {tempCacheDir, tempWorkingDir},
+ } {
+
+ configStr := `
+workingDir = "WORKING_DIR"
+resourceDir = "resources"
+cacheDir = "CACHEDIR"
+contentDir = "content"
+dataDir = "data"
+i18nDir = "i18n"
+layoutDir = "layouts"
+assetDir = "assets"
+archeTypedir = "archetypes"
+
+[caches]
+[caches.getJSON]
+maxAge = "10h"
+dir = ":cacheDir/c"
+
+`
+
+ winPathSep := "\\\\"
+
+ replacer := strings.NewReplacer("CACHEDIR", test.cacheDir, "WORKING_DIR", test.workingDir)
+
+ configStr = replacer.Replace(configStr)
+ configStr = strings.Replace(configStr, "\\", winPathSep, -1)
+
+ cfg, err := config.FromConfigString(configStr, "toml")
+ assert.NoError(err)
+
+ fs := hugofs.NewFrom(osfs, cfg)
+ p, err := helpers.NewPathSpec(fs, cfg)
+ assert.NoError(err)
+
+ caches, err := NewCaches(p)
+ assert.NoError(err)
+
+ cache := caches.Get("GetJSON")
+ assert.NotNil(cache)
+ assert.Equal("10h0m0s", cache.maxAge.String())
+
+ bfs, ok := cache.Fs.(*afero.BasePathFs)
+ assert.True(ok)
+ filename, err := bfs.RealPath("key")
+ assert.NoError(err)
+ if test.cacheDir != "" {
+ assert.Equal(filepath.Join(test.cacheDir, "c/"+filecacheRootDirname+"/getjson/key"), filename)
+ } else {
+ // Temp dir.
+ assert.Regexp(regexp.MustCompile(".*hugo_cache.*"+filecacheRootDirname+".*key"), filename)
+ }
+
+ cache = caches.Get("Images")
+ assert.NotNil(cache)
+ assert.Equal(time.Duration(-1), cache.maxAge)
+ bfs, ok = cache.Fs.(*afero.BasePathFs)
+ assert.True(ok)
+ filename, _ = bfs.RealPath("key")
+ assert.Equal(filepath.FromSlash("_gen/images/key"), filename)
+
+ rf := func(s string) func() (io.ReadCloser, error) {
+ return func() (io.ReadCloser, error) {
+ return struct {
+ io.ReadSeeker
+ io.Closer
+ }{
+ strings.NewReader(s),
+ ioutil.NopCloser(nil),
+ }, nil
+ }
+ }
+
+ bf := func() ([]byte, error) {
+ return []byte("bcd"), nil
+ }
+
+ for _, c := range []*Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} {
+ for i := 0; i < 2; i++ {
+ info, r, err := c.GetOrCreate("a", rf("abc"))
+ assert.NoError(err)
+ assert.NotNil(r)
+ assert.Equal("a", info.Name)
+ b, _ := ioutil.ReadAll(r)
+ r.Close()
+ assert.Equal("abc", string(b))
+
+ info, b, err = c.GetOrCreateBytes("b", bf)
+ assert.NoError(err)
+ assert.NotNil(r)
+ assert.Equal("b", info.Name)
+ assert.Equal("bcd", string(b))
+
+ _, b, err = c.GetOrCreateBytes("a", bf)
+ assert.NoError(err)
+ assert.Equal("abc", string(b))
+
+ _, r, err = c.GetOrCreate("a", rf("bcd"))
+ assert.NoError(err)
+ b, _ = ioutil.ReadAll(r)
+ r.Close()
+ assert.Equal("abc", string(b))
+ }
+ }
+
+ assert.NotNil(caches.Get("getJSON"))
+
+ info, w, err := caches.ImageCache().WriteCloser("mykey")
+ assert.NoError(err)
+ assert.Equal("mykey", info.Name)
+ io.WriteString(w, "Hugo is great!")
+ w.Close()
+ assert.Equal("Hugo is great!", caches.ImageCache().getString("mykey"))
+
+ info, r, err := caches.ImageCache().Get("mykey")
+ assert.NoError(err)
+ assert.NotNil(r)
+ assert.Equal("mykey", info.Name)
+ b, _ := ioutil.ReadAll(r)
+ r.Close()
+ assert.Equal("Hugo is great!", string(b))
+
+ info, b, err = caches.ImageCache().GetBytes("mykey")
+ assert.NoError(err)
+ assert.Equal("mykey", info.Name)
+ assert.Equal("Hugo is great!", string(b))
+
+ }
+
+}
+
+func TestFileCacheConcurrent(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ configStr := `
+resourceDir = "myresources"
+contentDir = "content"
+dataDir = "data"
+i18nDir = "i18n"
+layoutDir = "layouts"
+assetDir = "assets"
+archeTypedir = "archetypes"
+
+[caches]
+[caches.getjson]
+maxAge = "1s"
+dir = "/cache/c"
+
+`
+
+ cfg, err := config.FromConfigString(configStr, "toml")
+ assert.NoError(err)
+ fs := hugofs.NewMem(cfg)
+ p, err := helpers.NewPathSpec(fs, cfg)
+ assert.NoError(err)
+
+ caches, err := NewCaches(p)
+ assert.NoError(err)
+
+ const cacheName = "getjson"
+
+ filenameData := func(i int) (string, string) {
+ data := fmt.Sprintf("data: %d", i)
+ filename := fmt.Sprintf("file%d", i)
+ return filename, data
+ }
+
+ var wg sync.WaitGroup
+
+ for i := 0; i < 50; i++ {
+ wg.Add(1)
+ go func(i int) {
+ defer wg.Done()
+ for j := 0; j < 20; j++ {
+ c := caches.Get(cacheName)
+ assert.NotNil(c)
+ filename, data := filenameData(i)
+ _, r, err := c.GetOrCreate(filename, func() (io.ReadCloser, error) {
+ return hugio.ToReadCloser(strings.NewReader(data)), nil
+ })
+ assert.NoError(err)
+ b, _ := ioutil.ReadAll(r)
+ r.Close()
+ assert.Equal(data, string(b))
+ // Trigger some expiration.
+ time.Sleep(50 * time.Millisecond)
+ }
+ }(i)
+
+ }
+ wg.Wait()
+}
+
+func TestCleanID(t *testing.T) {
+ assert := require.New(t)
+ assert.Equal(filepath.FromSlash("a/b/c.txt"), cleanID(filepath.FromSlash("/a/b//c.txt")))
+ assert.Equal(filepath.FromSlash("a/b/c.txt"), cleanID(filepath.FromSlash("a/b//c.txt")))
+}
diff --git a/cache/namedmemcache/named_cache.go b/cache/namedmemcache/named_cache.go
new file mode 100644
index 000000000..d8c229a01
--- /dev/null
+++ b/cache/namedmemcache/named_cache.go
@@ -0,0 +1,79 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package namedmemcache provides a memory cache with a named lock. This is suitable
+// for situations where creating the cached resource can be time consuming or otherwise
+// resource hungry, or in situations where a "once only per key" is a requirement.
+package namedmemcache
+
+import (
+ "sync"
+
+ "github.com/BurntSushi/locker"
+)
+
+// Cache holds the cached values.
+type Cache struct {
+ nlocker *locker.Locker
+ cache map[string]cacheEntry
+ mu sync.RWMutex
+}
+
+type cacheEntry struct {
+ value interface{}
+ err error
+}
+
+// New creates a new cache.
+func New() *Cache {
+ return &Cache{
+ nlocker: locker.NewLocker(),
+ cache: make(map[string]cacheEntry),
+ }
+}
+
+// Clear clears the cache state.
+func (c *Cache) Clear() {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ c.cache = make(map[string]cacheEntry)
+ c.nlocker = locker.NewLocker()
+
+}
+
+// GetOrCreate tries to get the value with the given cache key, if not found
+// create will be called and cached.
+// This method is thread safe. It also guarantees that the create func for a given
+// key is invoced only once for this cache.
+func (c *Cache) GetOrCreate(key string, create func() (interface{}, error)) (interface{}, error) {
+ c.mu.RLock()
+ entry, found := c.cache[key]
+ c.mu.RUnlock()
+
+ if found {
+ return entry.value, entry.err
+ }
+
+ c.nlocker.Lock(key)
+ defer c.nlocker.Unlock(key)
+
+ // Create it.
+ value, err := create()
+
+ c.mu.Lock()
+ c.cache[key] = cacheEntry{value: value, err: err}
+ c.mu.Unlock()
+
+ return value, err
+}
diff --git a/cache/namedmemcache/named_cache_test.go b/cache/namedmemcache/named_cache_test.go
new file mode 100644
index 000000000..cf64aa210
--- /dev/null
+++ b/cache/namedmemcache/named_cache_test.go
@@ -0,0 +1,80 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package namedmemcache
+
+import (
+ "fmt"
+ "sync"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestNamedCache(t *testing.T) {
+ t.Parallel()
+ assert := require.New(t)
+
+ cache := New()
+
+ counter := 0
+ create := func() (interface{}, error) {
+ counter++
+ return counter, nil
+ }
+
+ for i := 0; i < 5; i++ {
+ v1, err := cache.GetOrCreate("a1", create)
+ assert.NoError(err)
+ assert.Equal(1, v1)
+ v2, err := cache.GetOrCreate("a2", create)
+ assert.NoError(err)
+ assert.Equal(2, v2)
+ }
+
+ cache.Clear()
+
+ v3, err := cache.GetOrCreate("a2", create)
+ assert.NoError(err)
+ assert.Equal(3, v3)
+}
+
+func TestNamedCacheConcurrent(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ var wg sync.WaitGroup
+
+ cache := New()
+
+ create := func(i int) func() (interface{}, error) {
+ return func() (interface{}, error) {
+ return i, nil
+ }
+ }
+
+ for i := 0; i < 10; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for j := 0; j < 100; j++ {
+ id := fmt.Sprintf("id%d", j)
+ v, err := cache.GetOrCreate(id, create(j))
+ assert.NoError(err)
+ assert.Equal(j, v)
+ }
+ }()
+ }
+ wg.Wait()
+}
diff --git a/cache/partitioned_lazy_cache.go b/cache/partitioned_lazy_cache.go
new file mode 100644
index 000000000..31e66e127
--- /dev/null
+++ b/cache/partitioned_lazy_cache.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 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)
+}
+
+// Lazy represents a lazily loaded cache.
+type Lazy struct {
+ initSync sync.Once
+ initErr error
+ cache map[string]interface{}
+ load func() (map[string]interface{}, error)
+}
+
+// NewLazy creates a lazy cache with the given load func.
+func NewLazy(load func() (map[string]interface{}, error)) *Lazy {
+ return &Lazy{load: load}
+}
+
+func (l *Lazy) init() error {
+ l.initSync.Do(func() {
+ c, err := l.load()
+ l.cache = c
+ l.initErr = err
+
+ })
+
+ return l.initErr
+}
+
+// Get initializes the cache if not already initialized, then looks up the
+// given key.
+func (l *Lazy) Get(key string) (interface{}, bool, error) {
+ l.init()
+ if l.initErr != nil {
+ return nil, false, l.initErr
+ }
+ v, found := l.cache[key]
+ return v, found, nil
+}
+
+// PartitionedLazyCache is a lazily loaded cache paritioned by a supplied string key.
+type PartitionedLazyCache struct {
+ partitions map[string]*Lazy
+}
+
+// NewPartitionedLazyCache creates a new NewPartitionedLazyCache with the supplied
+// partitions.
+func NewPartitionedLazyCache(partitions ...Partition) *PartitionedLazyCache {
+ lazyPartitions := make(map[string]*Lazy, len(partitions))
+ for _, partition := range partitions {
+ lazyPartitions[partition.Key] = NewLazy(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
+ }
+
+ v, found, err := p.Get(key)
+ if err != nil {
+ return nil, err
+ }
+
+ if 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..ba8b6a454
--- /dev/null
+++ b/cache/partitioned_lazy_cache_test.go
@@ -0,0 +1,138 @@
+// 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"
+ "sync"
+ "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)
+
+}
+
+func TestConcurrentPartitionedLazyCache(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ var wg sync.WaitGroup
+
+ 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)
+
+ for i := 0; i < 100; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for j := 0; j < 10; j++ {
+ v, err := cache.Get("p1", "p1_1")
+ assert.NoError(err)
+ assert.Equal("p1v1", v)
+ }
+ }()
+ }
+ wg.Wait()
+}
diff --git a/codegen/methods.go b/codegen/methods.go
new file mode 100644
index 000000000..ed8dba923
--- /dev/null
+++ b/codegen/methods.go
@@ -0,0 +1,548 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+// Some functions in this file (see comments) is based on the Go source code,
+// copyright The Go Authors and governed by a BSD-style license.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package codegen contains helpers for code generation.
+package codegen
+
+import (
+ "fmt"
+ "go/ast"
+ "go/parser"
+ "go/token"
+ "os"
+ "path"
+ "path/filepath"
+ "reflect"
+ "regexp"
+ "sort"
+ "strings"
+ "sync"
+)
+
+// Make room for insertions
+const weightWidth = 1000
+
+// NewInspector creates a new Inspector given a source root.
+func NewInspector(root string) *Inspector {
+ return &Inspector{ProjectRootDir: root}
+}
+
+// Inspector provides methods to help code generation. It uses a combination
+// of reflection and source code AST to do the heavy lifting.
+type Inspector struct {
+ ProjectRootDir string
+
+ init sync.Once
+
+ // Determines method order. Go's reflect sorts lexicographically, so
+ // we must parse the source to preserve this order.
+ methodWeight map[string]map[string]int
+}
+
+// MethodsFromTypes create a method set from the include slice, excluding any
+// method in exclude.
+func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.Type) Methods {
+ c.parseSource()
+
+ var methods Methods
+
+ var excludes = make(map[string]bool)
+
+ if len(exclude) > 0 {
+ for _, m := range c.MethodsFromTypes(exclude, nil) {
+ excludes[m.Name] = true
+ }
+ }
+
+ // There may be overlapping interfaces in types. Do a simple check for now.
+ seen := make(map[string]bool)
+
+ nameAndPackage := func(t reflect.Type) (string, string) {
+ var name, pkg string
+
+ isPointer := t.Kind() == reflect.Ptr
+
+ if isPointer {
+ t = t.Elem()
+ }
+
+ pkgPrefix := ""
+ if pkgPath := t.PkgPath(); pkgPath != "" {
+ pkgPath = strings.TrimSuffix(pkgPath, "/")
+ _, shortPath := path.Split(pkgPath)
+ pkgPrefix = shortPath + "."
+ pkg = pkgPath
+ }
+
+ name = t.Name()
+ if name == "" {
+ // interface{}
+ name = t.String()
+ }
+
+ if isPointer {
+ pkgPrefix = "*" + pkgPrefix
+ }
+
+ name = pkgPrefix + name
+
+ return name, pkg
+
+ }
+
+ for _, t := range include {
+
+ for i := 0; i < t.NumMethod(); i++ {
+
+ m := t.Method(i)
+ if excludes[m.Name] || seen[m.Name] {
+ continue
+ }
+
+ seen[m.Name] = true
+
+ if m.PkgPath != "" {
+ // Not exported
+ continue
+ }
+
+ numIn := m.Type.NumIn()
+
+ ownerName, _ := nameAndPackage(t)
+
+ method := Method{Owner: t, OwnerName: ownerName, Name: m.Name}
+
+ for i := 0; i < numIn; i++ {
+ in := m.Type.In(i)
+
+ name, pkg := nameAndPackage(in)
+
+ if pkg != "" {
+ method.Imports = append(method.Imports, pkg)
+ }
+
+ method.In = append(method.In, name)
+ }
+
+ numOut := m.Type.NumOut()
+
+ if numOut > 0 {
+ for i := 0; i < numOut; i++ {
+ out := m.Type.Out(i)
+ name, pkg := nameAndPackage(out)
+
+ if pkg != "" {
+ method.Imports = append(method.Imports, pkg)
+ }
+
+ method.Out = append(method.Out, name)
+ }
+ }
+
+ methods = append(methods, method)
+ }
+
+ }
+
+ sort.SliceStable(methods, func(i, j int) bool {
+ mi, mj := methods[i], methods[j]
+
+ wi := c.methodWeight[mi.OwnerName][mi.Name]
+ wj := c.methodWeight[mj.OwnerName][mj.Name]
+
+ if wi == wj {
+ return mi.Name < mj.Name
+ }
+
+ return wi < wj
+
+ })
+
+ return methods
+
+}
+
+func (c *Inspector) parseSource() {
+ c.init.Do(func() {
+
+ if !strings.Contains(c.ProjectRootDir, "hugo") {
+ panic("dir must be set to the Hugo root")
+ }
+
+ c.methodWeight = make(map[string]map[string]int)
+ dirExcludes := regexp.MustCompile("docs|examples")
+ fileExcludes := regexp.MustCompile("autogen")
+ var filenames []string
+
+ filepath.Walk(c.ProjectRootDir, func(path string, info os.FileInfo, err error) error {
+ if info.IsDir() {
+ if dirExcludes.MatchString(info.Name()) {
+ return filepath.SkipDir
+ }
+ }
+
+ if !strings.HasSuffix(path, ".go") || fileExcludes.MatchString(path) {
+ return nil
+ }
+
+ filenames = append(filenames, path)
+
+ return nil
+
+ })
+
+ for _, filename := range filenames {
+
+ pkg := c.packageFromPath(filename)
+
+ fset := token.NewFileSet()
+ node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
+ if err != nil {
+ panic(err)
+ }
+
+ ast.Inspect(node, func(n ast.Node) bool {
+ switch t := n.(type) {
+ case *ast.TypeSpec:
+ if t.Name.IsExported() {
+ switch it := t.Type.(type) {
+ case *ast.InterfaceType:
+ iface := pkg + "." + t.Name.Name
+ methodNames := collectMethodsRecursive(pkg, it.Methods.List)
+ weights := make(map[string]int)
+ weight := weightWidth
+ for _, name := range methodNames {
+ weights[name] = weight
+ weight += weightWidth
+ }
+ c.methodWeight[iface] = weights
+ }
+ }
+
+ }
+ return true
+ })
+
+ }
+
+ // Complement
+ for _, v1 := range c.methodWeight {
+ for k2, w := range v1 {
+ if v, found := c.methodWeight[k2]; found {
+ for k3, v3 := range v {
+ v1[k3] = (v3 / weightWidth) + w
+ }
+ }
+ }
+ }
+
+ })
+}
+
+func (c *Inspector) packageFromPath(p string) string {
+ p = filepath.ToSlash(p)
+ base := path.Base(p)
+ if !strings.Contains(base, ".") {
+ return base
+ }
+ return path.Base(strings.TrimSuffix(p, base))
+}
+
+// Method holds enough information about it to recreate it.
+type Method struct {
+ // The interface we extracted this method from.
+ Owner reflect.Type
+
+ // String version of the above, on the form PACKAGE.NAME, e.g.
+ // page.Page
+ OwnerName string
+
+ // Method name.
+ Name string
+
+ // Imports needed to satisfy the method signature.
+ Imports []string
+
+ // Argument types, including any package prefix, e.g. string, int, interface{},
+ // net.Url
+ In []string
+
+ // Return types.
+ Out []string
+}
+
+// Declaration creates a method declaration (without any body) for the given receiver.
+func (m Method) Declaration(receiver string) string {
+ return fmt.Sprintf("func (%s %s) %s%s %s", receiverShort(receiver), receiver, m.Name, m.inStr(), m.outStr())
+}
+
+// DeclarationNamed creates a method declaration (without any body) for the given receiver
+// with named return values.
+func (m Method) DeclarationNamed(receiver string) string {
+ return fmt.Sprintf("func (%s %s) %s%s %s", receiverShort(receiver), receiver, m.Name, m.inStr(), m.outStrNamed())
+}
+
+// Delegate creates a delegate call string.
+func (m Method) Delegate(receiver, delegate string) string {
+ ret := ""
+ if len(m.Out) > 0 {
+ ret = "return "
+ }
+ return fmt.Sprintf("%s%s.%s.%s%s", ret, receiverShort(receiver), delegate, m.Name, m.inOutStr())
+}
+
+func (m Method) String() string {
+ return m.Name + m.inStr() + " " + m.outStr() + "\n"
+}
+
+func (m Method) inOutStr() string {
+ if len(m.In) == 0 {
+ return "()"
+ }
+
+ args := make([]string, len(m.In))
+ for i := 0; i < len(args); i++ {
+ args[i] = fmt.Sprintf("arg%d", i)
+ }
+ return "(" + strings.Join(args, ", ") + ")"
+}
+
+func (m Method) inStr() string {
+ if len(m.In) == 0 {
+ return "()"
+ }
+
+ args := make([]string, len(m.In))
+ for i := 0; i < len(args); i++ {
+ args[i] = fmt.Sprintf("arg%d %s", i, m.In[i])
+ }
+ return "(" + strings.Join(args, ", ") + ")"
+}
+
+func (m Method) outStr() string {
+ if len(m.Out) == 0 {
+ return ""
+ }
+ if len(m.Out) == 1 {
+ return m.Out[0]
+ }
+
+ return "(" + strings.Join(m.Out, ", ") + ")"
+}
+
+func (m Method) outStrNamed() string {
+ if len(m.Out) == 0 {
+ return ""
+ }
+
+ outs := make([]string, len(m.Out))
+ for i := 0; i < len(outs); i++ {
+ outs[i] = fmt.Sprintf("o%d %s", i, m.Out[i])
+ }
+
+ return "(" + strings.Join(outs, ", ") + ")"
+}
+
+// Methods represents a list of methods for one or more interfaces.
+// The order matches the defined order in their source file(s).
+type Methods []Method
+
+// Imports returns a sorted list of package imports needed to satisfy the
+// signatures of all methods.
+func (m Methods) Imports() []string {
+ var pkgImports []string
+ for _, method := range m {
+ pkgImports = append(pkgImports, method.Imports...)
+ }
+ if len(pkgImports) > 0 {
+ pkgImports = uniqueNonEmptyStrings(pkgImports)
+ sort.Strings(pkgImports)
+ }
+ return pkgImports
+}
+
+// ToMarshalJSON creates a MarshalJSON method for these methods. Any method name
+// matchin any of the regexps in excludes will be ignored.
+func (m Methods) ToMarshalJSON(receiver, pkgPath string, excludes ...string) (string, []string) {
+ var sb strings.Builder
+
+ r := receiverShort(receiver)
+ what := firstToUpper(trimAsterisk(receiver))
+ pgkName := path.Base(pkgPath)
+
+ fmt.Fprintf(&sb, "func Marshal%sToJSON(%s %s) ([]byte, error) {\n", what, r, receiver)
+
+ var methods Methods
+ var excludeRes = make([]*regexp.Regexp, len(excludes))
+
+ for i, exclude := range excludes {
+ excludeRes[i] = regexp.MustCompile(exclude)
+ }
+
+ for _, method := range m {
+ // Exclude methods with arguments and incompatible return values
+ if len(method.In) > 0 || len(method.Out) == 0 || len(method.Out) > 2 {
+ continue
+ }
+
+ if len(method.Out) == 2 {
+ if method.Out[1] != "error" {
+ continue
+ }
+ }
+
+ for _, re := range excludeRes {
+ if re.MatchString(method.Name) {
+ continue
+ }
+ }
+
+ methods = append(methods, method)
+ }
+
+ for _, method := range methods {
+ varn := varName(method.Name)
+ if len(method.Out) == 1 {
+ fmt.Fprintf(&sb, "\t%s := %s.%s()\n", varn, r, method.Name)
+ } else {
+ fmt.Fprintf(&sb, "\t%s, err := %s.%s()\n", varn, r, method.Name)
+ fmt.Fprint(&sb, "\tif err != nil {\n\t\treturn nil, err\n\t}\n")
+ }
+ }
+
+ fmt.Fprint(&sb, "\n\ts := struct {\n")
+
+ for _, method := range methods {
+ fmt.Fprintf(&sb, "\t\t%s %s\n", method.Name, typeName(method.Out[0], pgkName))
+ }
+
+ fmt.Fprint(&sb, "\n\t}{\n")
+
+ for _, method := range methods {
+ varn := varName(method.Name)
+ fmt.Fprintf(&sb, "\t\t%s: %s,\n", method.Name, varn)
+ }
+
+ fmt.Fprint(&sb, "\n\t}\n\n")
+ fmt.Fprint(&sb, "\treturn json.Marshal(&s)\n}")
+
+ pkgImports := append(methods.Imports(), "encoding/json")
+
+ if pkgPath != "" {
+ // Exclude self
+ for i, pkgImp := range pkgImports {
+ if pkgImp == pkgPath {
+ pkgImports = append(pkgImports[:i], pkgImports[i+1:]...)
+ }
+ }
+ }
+
+ return sb.String(), pkgImports
+
+}
+
+func collectMethodsRecursive(pkg string, f []*ast.Field) []string {
+ var methodNames []string
+ for _, m := range f {
+ if m.Names != nil {
+ methodNames = append(methodNames, m.Names[0].Name)
+ continue
+ }
+
+ if ident, ok := m.Type.(*ast.Ident); ok && ident.Obj != nil {
+ // Embedded interface
+ methodNames = append(
+ methodNames,
+ collectMethodsRecursive(
+ pkg,
+ ident.Obj.Decl.(*ast.TypeSpec).Type.(*ast.InterfaceType).Methods.List)...)
+ } else {
+ // Embedded, but in a different file/package. Return the
+ // package.Name and deal with that later.
+ name := packageName(m.Type)
+ if !strings.Contains(name, ".") {
+ // Assume current package
+ name = pkg + "." + name
+ }
+ methodNames = append(methodNames, name)
+ }
+ }
+
+ return methodNames
+
+}
+
+func firstToLower(name string) string {
+ return strings.ToLower(name[:1]) + name[1:]
+}
+
+func firstToUpper(name string) string {
+ return strings.ToUpper(name[:1]) + name[1:]
+}
+
+func packageName(e ast.Expr) string {
+ switch tp := e.(type) {
+ case *ast.Ident:
+ return tp.Name
+ case *ast.SelectorExpr:
+ return fmt.Sprintf("%s.%s", packageName(tp.X), packageName(tp.Sel))
+ }
+ return ""
+}
+
+func receiverShort(receiver string) string {
+ return strings.ToLower(trimAsterisk(receiver))[:1]
+}
+
+func trimAsterisk(name string) string {
+ return strings.TrimPrefix(name, "*")
+}
+
+func typeName(name, pkg string) string {
+ return strings.TrimPrefix(name, pkg+".")
+}
+
+func uniqueNonEmptyStrings(s []string) []string {
+ var unique []string
+ set := map[string]interface{}{}
+ for _, val := range s {
+ if val == "" {
+ continue
+ }
+ if _, ok := set[val]; !ok {
+ unique = append(unique, val)
+ set[val] = val
+ }
+ }
+ return unique
+}
+
+func varName(name string) string {
+ name = firstToLower(name)
+
+ // Adjust some reserved keywords, see https://golang.org/ref/spec#Keywords
+ switch name {
+ case "type":
+ name = "typ"
+ case "package":
+ name = "pkg"
+ // Not reserved, but syntax highlighters has it as a keyword.
+ case "len":
+ name = "length"
+ }
+
+ return name
+
+}
diff --git a/codegen/methods2_test.go b/codegen/methods2_test.go
new file mode 100644
index 000000000..bd36b5e80
--- /dev/null
+++ b/codegen/methods2_test.go
@@ -0,0 +1,20 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package codegen
+
+type IEmbed interface {
+ MethodEmbed3(s string) string
+ MethodEmbed1() string
+ MethodEmbed2()
+}
diff --git a/codegen/methods_test.go b/codegen/methods_test.go
new file mode 100644
index 000000000..fad6da078
--- /dev/null
+++ b/codegen/methods_test.go
@@ -0,0 +1,100 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package codegen
+
+import (
+ "fmt"
+ "net"
+ "os"
+ "reflect"
+ "testing"
+
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/stretchr/testify/require"
+)
+
+func TestMethods(t *testing.T) {
+
+ var (
+ zeroIE = reflect.TypeOf((*IEmbed)(nil)).Elem()
+ zeroIEOnly = reflect.TypeOf((*IEOnly)(nil)).Elem()
+ zeroI = reflect.TypeOf((*I)(nil)).Elem()
+ )
+
+ dir, _ := os.Getwd()
+ c := NewInspector(dir)
+
+ t.Run("MethodsFromTypes", func(t *testing.T) {
+ assert := require.New(t)
+
+ methods := c.MethodsFromTypes([]reflect.Type{zeroI}, nil)
+
+ methodsStr := fmt.Sprint(methods)
+
+ assert.Contains(methodsStr, "Method1(arg0 herrors.ErrorContext)")
+ assert.Contains(methodsStr, "Method7() interface {}")
+ assert.Contains(methodsStr, "Method0() string\n Method4() string")
+ assert.Contains(methodsStr, "MethodEmbed3(arg0 string) string\n MethodEmbed1() string")
+
+ assert.Contains(methods.Imports(), "github.com/gohugoio/hugo/common/herrors")
+ })
+
+ t.Run("EmbedOnly", func(t *testing.T) {
+ assert := require.New(t)
+
+ methods := c.MethodsFromTypes([]reflect.Type{zeroIEOnly}, nil)
+
+ methodsStr := fmt.Sprint(methods)
+
+ assert.Contains(methodsStr, "MethodEmbed3(arg0 string) string")
+
+ })
+
+ t.Run("ToMarshalJSON", func(t *testing.T) {
+ assert := require.New(t)
+
+ m, pkg := c.MethodsFromTypes(
+ []reflect.Type{zeroI},
+ []reflect.Type{zeroIE}).ToMarshalJSON("*page", "page")
+
+ assert.Contains(m, "method6 := p.Method6()")
+ assert.Contains(m, "Method0: method0,")
+ assert.Contains(m, "return json.Marshal(&s)")
+
+ assert.Contains(pkg, "github.com/gohugoio/hugo/common/herrors")
+ assert.Contains(pkg, "encoding/json")
+
+ fmt.Println(pkg)
+
+ })
+
+}
+
+type I interface {
+ IEmbed
+ Method0() string
+ Method4() string
+ Method1(myerr herrors.ErrorContext)
+ Method3(myint int, mystring string)
+ Method5() (string, error)
+ Method6() *net.IP
+ Method7() interface{}
+ Method8() herrors.ErrorContext
+ method2()
+ method9() os.FileInfo
+}
+
+type IEOnly interface {
+ IEmbed
+}
diff --git a/commands/check.go b/commands/check.go
new file mode 100644
index 000000000..f36f23969
--- /dev/null
+++ b/commands/check.go
@@ -0,0 +1,34 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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
+
+package commands
+
+import (
+ "github.com/spf13/cobra"
+)
+
+var _ cmder = (*checkCmd)(nil)
+
+type checkCmd struct {
+ *baseCmd
+}
+
+func newCheckCmd() *checkCmd {
+ return &checkCmd{baseCmd: &baseCmd{cmd: &cobra.Command{
+ Use: "check",
+ Short: "Contains some verification checks",
+ },
+ }}
+}
diff --git a/commands/check_darwin.go b/commands/check_darwin.go
new file mode 100644
index 000000000..9291be84c
--- /dev/null
+++ b/commands/check_darwin.go
@@ -0,0 +1,36 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 _ cmder = (*checkCmd)(nil)
+
+type checkCmd struct {
+ *baseCmd
+}
+
+func newCheckCmd() *checkCmd {
+ cc := &checkCmd{baseCmd: &baseCmd{cmd: &cobra.Command{
+ Use: "check",
+ Short: "Contains some verification checks",
+ },
+ }}
+
+ cc.cmd.AddCommand(newLimitCmd().getCommand())
+
+ return cc
+}
diff --git a/commands/commandeer.go b/commands/commandeer.go
new file mode 100644
index 000000000..8c9da53b9
--- /dev/null
+++ b/commands/commandeer.go
@@ -0,0 +1,408 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/ioutil"
+
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/common/hugo"
+
+ jww "github.com/spf13/jwalterweatherman"
+
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/config"
+
+ "github.com/spf13/cobra"
+
+ "github.com/gohugoio/hugo/hugolib"
+ "github.com/spf13/afero"
+
+ "github.com/bep/debounce"
+ "github.com/gohugoio/hugo/common/types"
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/langs"
+)
+
+type commandeerHugoState struct {
+ *deps.DepsCfg
+ hugo *hugolib.HugoSites
+ fsCreate sync.Once
+}
+
+type commandeer struct {
+ *commandeerHugoState
+
+ logger *loggers.Logger
+
+ // Currently only set when in "fast render mode". But it seems to
+ // be fast enough that we could maybe just add it for all server modes.
+ changeDetector *fileChangeDetector
+
+ // We need to reuse this on server rebuilds.
+ destinationFs afero.Fs
+
+ h *hugoBuilderCommon
+ ftch flagsToConfigHandler
+
+ visitedURLs *types.EvictingStringQueue
+
+ doWithCommandeer func(c *commandeer) error
+
+ // We watch these for changes.
+ configFiles []string
+
+ // Used in cases where we get flooded with events in server mode.
+ debounce func(f func())
+
+ serverPorts []int
+ languagesConfigured bool
+ languages langs.Languages
+ doLiveReload bool
+ fastRenderMode bool
+ showErrorInBrowser bool
+
+ configured bool
+ paused bool
+
+ // Any error from the last build.
+ buildErr error
+}
+
+func (c *commandeer) errCount() int {
+ return int(c.logger.ErrorCounter.Count())
+}
+
+func (c *commandeer) getErrorWithContext() interface{} {
+ errCount := c.errCount()
+
+ if errCount == 0 {
+ return nil
+ }
+
+ m := make(map[string]interface{})
+
+ m["Error"] = errors.New(removeErrorPrefixFromLog(c.logger.Errors()))
+ m["Version"] = hugo.BuildVersionString()
+
+ fe := herrors.UnwrapErrorWithFileContext(c.buildErr)
+ if fe != nil {
+ m["File"] = fe
+ }
+
+ if c.h.verbose {
+ var b bytes.Buffer
+ herrors.FprintStackTrace(&b, c.buildErr)
+ m["StackTrace"] = b.String()
+ }
+
+ return m
+}
+
+func (c *commandeer) Set(key string, value interface{}) {
+ if c.configured {
+ panic("commandeer cannot be changed")
+ }
+ c.Cfg.Set(key, value)
+}
+
+func (c *commandeer) initFs(fs *hugofs.Fs) error {
+ c.destinationFs = fs.Destination
+ c.DepsCfg.Fs = fs
+
+ return nil
+}
+
+func newCommandeer(mustHaveConfigFile, running bool, h *hugoBuilderCommon, f flagsToConfigHandler, doWithCommandeer func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) {
+
+ var rebuildDebouncer func(f func())
+ if running {
+ // The time value used is tested with mass content replacements in a fairly big Hugo site.
+ // It is better to wait for some seconds in those cases rather than get flooded
+ // with rebuilds.
+ rebuildDebouncer = debounce.New(4 * time.Second)
+ }
+
+ c := &commandeer{
+ h: h,
+ ftch: f,
+ commandeerHugoState: &commandeerHugoState{},
+ doWithCommandeer: doWithCommandeer,
+ visitedURLs: types.NewEvictingStringQueue(10),
+ debounce: rebuildDebouncer,
+ // This will be replaced later, but we need something to log to before the configuration is read.
+ logger: loggers.NewLogger(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, running),
+ }
+
+ return c, c.loadConfig(mustHaveConfigFile, running)
+}
+
+type fileChangeDetector struct {
+ sync.Mutex
+ current map[string]string
+ prev map[string]string
+
+ irrelevantRe *regexp.Regexp
+}
+
+func (f *fileChangeDetector) OnFileClose(name, md5sum string) {
+ f.Lock()
+ defer f.Unlock()
+ f.current[name] = md5sum
+}
+
+func (f *fileChangeDetector) changed() []string {
+ if f == nil {
+ return nil
+ }
+ f.Lock()
+ defer f.Unlock()
+ var c []string
+ for k, v := range f.current {
+ vv, found := f.prev[k]
+ if !found || v != vv {
+ c = append(c, k)
+ }
+ }
+
+ return f.filterIrrelevant(c)
+}
+
+func (f *fileChangeDetector) filterIrrelevant(in []string) []string {
+ var filtered []string
+ for _, v := range in {
+ if !f.irrelevantRe.MatchString(v) {
+ filtered = append(filtered, v)
+ }
+ }
+ return filtered
+}
+
+func (f *fileChangeDetector) PrepareNew() {
+ if f == nil {
+ return
+ }
+
+ f.Lock()
+ defer f.Unlock()
+
+ if f.current == nil {
+ f.current = make(map[string]string)
+ f.prev = make(map[string]string)
+ return
+ }
+
+ f.prev = make(map[string]string)
+ for k, v := range f.current {
+ f.prev[k] = v
+ }
+ f.current = make(map[string]string)
+}
+
+func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
+
+ if c.DepsCfg == nil {
+ c.DepsCfg = &deps.DepsCfg{}
+ }
+
+ if c.logger != nil {
+ // Truncate the error log if this is a reload.
+ c.logger.Reset()
+ }
+
+ cfg := c.DepsCfg
+ c.configured = false
+ cfg.Running = running
+
+ var dir string
+ if c.h.source != "" {
+ dir, _ = filepath.Abs(c.h.source)
+ } else {
+ dir, _ = os.Getwd()
+ }
+
+ var sourceFs afero.Fs = hugofs.Os
+ if c.DepsCfg.Fs != nil {
+ sourceFs = c.DepsCfg.Fs.Source
+ }
+
+ environment := c.h.getEnvironment(running)
+
+ doWithConfig := func(cfg config.Provider) error {
+
+ if c.ftch != nil {
+ c.ftch.flagsToConfig(cfg)
+ }
+
+ cfg.Set("workingDir", dir)
+ cfg.Set("environment", environment)
+ return nil
+ }
+
+ doWithCommandeer := func(cfg config.Provider) error {
+ c.Cfg = cfg
+ if c.doWithCommandeer == nil {
+ return nil
+ }
+ err := c.doWithCommandeer(c)
+ return err
+ }
+
+ configPath := c.h.source
+ if configPath == "" {
+ configPath = dir
+ }
+ config, configFiles, err := hugolib.LoadConfig(
+ hugolib.ConfigSourceDescriptor{
+ Fs: sourceFs,
+ Path: configPath,
+ WorkingDir: dir,
+ Filename: c.h.cfgFile,
+ AbsConfigDir: c.h.getConfigDir(dir),
+ Environment: environment},
+ doWithCommandeer,
+ doWithConfig)
+
+ if err != nil {
+ if mustHaveConfigFile {
+ return err
+ }
+ if err != hugolib.ErrNoConfigFile {
+ return err
+ }
+
+ }
+
+ c.configFiles = configFiles
+
+ if l, ok := c.Cfg.Get("languagesSorted").(langs.Languages); ok {
+ c.languagesConfigured = true
+ c.languages = l
+ }
+
+ // Set some commonly used flags
+ c.doLiveReload = running && !c.Cfg.GetBool("disableLiveReload")
+ c.fastRenderMode = c.doLiveReload && !c.Cfg.GetBool("disableFastRender")
+ c.showErrorInBrowser = c.doLiveReload && !c.Cfg.GetBool("disableBrowserError")
+
+ // This is potentially double work, but we need to do this one more time now
+ // that all the languages have been configured.
+ if c.doWithCommandeer != nil {
+ if err := c.doWithCommandeer(c); err != nil {
+ return err
+ }
+ }
+
+ logger, err := c.createLogger(config, running)
+ if err != nil {
+ return err
+ }
+
+ cfg.Logger = logger
+ c.logger = logger
+
+ createMemFs := config.GetBool("renderToMemory")
+
+ if createMemFs {
+ // Rendering to memoryFS, publish to Root regardless of publishDir.
+ config.Set("publishDir", "/")
+ }
+
+ c.fsCreate.Do(func() {
+ fs := hugofs.NewFrom(sourceFs, config)
+
+ if c.destinationFs != nil {
+ // Need to reuse the destination on server rebuilds.
+ fs.Destination = c.destinationFs
+ } else if createMemFs {
+ // Hugo writes the output to memory instead of the disk.
+ fs.Destination = new(afero.MemMapFs)
+ }
+
+ if c.fastRenderMode {
+ // For now, fast render mode only. It should, however, be fast enough
+ // for the full variant, too.
+ changeDetector := &fileChangeDetector{
+ // We use this detector to decide to do a Hot reload of a single path or not.
+ // We need to filter out source maps and possibly some other to be able
+ // to make that decision.
+ irrelevantRe: regexp.MustCompile(`\.map$`),
+ }
+
+ changeDetector.PrepareNew()
+ fs.Destination = hugofs.NewHashingFs(fs.Destination, changeDetector)
+ c.changeDetector = changeDetector
+ }
+
+ if c.Cfg.GetBool("logPathWarnings") {
+ fs.Destination = hugofs.NewCreateCountingFs(fs.Destination)
+ }
+
+ // To debug hard-to-find path issues.
+ //fs.Destination = hugofs.NewStacktracerFs(fs.Destination, `fr/fr`)
+
+ err = c.initFs(fs)
+ if err != nil {
+ return
+ }
+
+ var h *hugolib.HugoSites
+
+ h, err = hugolib.NewHugoSites(*c.DepsCfg)
+ c.hugo = h
+
+ })
+
+ if err != nil {
+ return err
+ }
+
+ cacheDir, err := helpers.GetCacheDir(sourceFs, config)
+ if err != nil {
+ return err
+ }
+ config.Set("cacheDir", cacheDir)
+
+ cfg.Logger.INFO.Println("Using config file:", config.ConfigFileUsed())
+
+ themeDir := c.hugo.PathSpec.GetFirstThemeDir()
+ if themeDir != "" {
+ if _, err := sourceFs.Stat(themeDir); os.IsNotExist(err) {
+ return newSystemError("Unable to find theme Directory:", themeDir)
+ }
+ }
+
+ dir, themeVersionMismatch, minVersion := c.isThemeVsHugoVersionMismatch(sourceFs)
+
+ if themeVersionMismatch {
+ name := filepath.Base(dir)
+ cfg.Logger.ERROR.Printf("%s theme does not support Hugo version %s. Minimum version required is %s\n",
+ strings.ToUpper(name), hugo.CurrentVersion.ReleaseVersion(), minVersion)
+ }
+
+ return nil
+
+}
diff --git a/commands/commands.go b/commands/commands.go
new file mode 100644
index 000000000..51bfb4763
--- /dev/null
+++ b/commands/commands.go
@@ -0,0 +1,305 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+
+ "github.com/gohugoio/hugo/hugolib/paths"
+
+ "github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/spf13/cobra"
+)
+
+type commandsBuilder struct {
+ hugoBuilderCommon
+
+ commands []cmder
+}
+
+func newCommandsBuilder() *commandsBuilder {
+ return &commandsBuilder{}
+}
+
+func (b *commandsBuilder) addCommands(commands ...cmder) *commandsBuilder {
+ b.commands = append(b.commands, commands...)
+ return b
+}
+
+func (b *commandsBuilder) addAll() *commandsBuilder {
+ b.addCommands(
+ b.newServerCmd(),
+ newVersionCmd(),
+ newEnvCmd(),
+ newConfigCmd(),
+ newCheckCmd(),
+ newDeployCmd(),
+ newConvertCmd(),
+ b.newNewCmd(),
+ newListCmd(),
+ newImportCmd(),
+ newGenCmd(),
+ createReleaser(),
+ )
+
+ return b
+}
+
+func (b *commandsBuilder) build() *hugoCmd {
+ h := b.newHugoCmd()
+ addCommands(h.getCommand(), b.commands...)
+ return h
+}
+
+func addCommands(root *cobra.Command, commands ...cmder) {
+ for _, command := range commands {
+ cmd := command.getCommand()
+ if cmd == nil {
+ continue
+ }
+ root.AddCommand(cmd)
+ }
+}
+
+type baseCmd struct {
+ cmd *cobra.Command
+}
+
+var _ commandsBuilderGetter = (*baseBuilderCmd)(nil)
+
+// Used in tests.
+type commandsBuilderGetter interface {
+ getCommandsBuilder() *commandsBuilder
+}
+type baseBuilderCmd struct {
+ *baseCmd
+ *commandsBuilder
+}
+
+func (b *baseBuilderCmd) getCommandsBuilder() *commandsBuilder {
+ return b.commandsBuilder
+}
+
+func (c *baseCmd) getCommand() *cobra.Command {
+ return c.cmd
+}
+
+func newBaseCmd(cmd *cobra.Command) *baseCmd {
+ return &baseCmd{cmd: cmd}
+}
+
+func (b *commandsBuilder) newBuilderCmd(cmd *cobra.Command) *baseBuilderCmd {
+ bcmd := &baseBuilderCmd{commandsBuilder: b, baseCmd: &baseCmd{cmd: cmd}}
+ bcmd.hugoBuilderCommon.handleFlags(cmd)
+ return bcmd
+}
+
+func (c *baseCmd) flagsToConfig(cfg config.Provider) {
+ initializeFlags(c.cmd, cfg)
+}
+
+type hugoCmd struct {
+ *baseBuilderCmd
+
+ // Need to get the sites once built.
+ c *commandeer
+}
+
+var _ cmder = (*nilCommand)(nil)
+
+type nilCommand struct {
+}
+
+func (c *nilCommand) getCommand() *cobra.Command {
+ return nil
+}
+
+func (c *nilCommand) flagsToConfig(cfg config.Provider) {
+
+}
+
+func (b *commandsBuilder) newHugoCmd() *hugoCmd {
+ cc := &hugoCmd{}
+
+ cc.baseBuilderCmd = b.newBuilderCmd(&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 {
+ cfgInit := func(c *commandeer) error {
+ if cc.buildWatch {
+ c.Set("disableLiveReload", true)
+ }
+ return nil
+ }
+
+ c, err := initializeConfig(true, cc.buildWatch, &cc.hugoBuilderCommon, cc, cfgInit)
+ if err != nil {
+ return err
+ }
+ cc.c = c
+
+ return c.build()
+ },
+ })
+
+ cc.cmd.PersistentFlags().StringVar(&cc.cfgFile, "config", "", "config file (default is path/config.yaml|json|toml)")
+ cc.cmd.PersistentFlags().StringVar(&cc.cfgDir, "configDir", "config", "config dir")
+ cc.cmd.PersistentFlags().BoolVar(&cc.quiet, "quiet", false, "build in quiet mode")
+
+ // Set bash-completion
+ _ = cc.cmd.PersistentFlags().SetAnnotation("config", cobra.BashCompFilenameExt, config.ValidConfigFileExtensions)
+
+ cc.cmd.PersistentFlags().BoolVarP(&cc.verbose, "verbose", "v", false, "verbose output")
+ cc.cmd.PersistentFlags().BoolVarP(&cc.debug, "debug", "", false, "debug output")
+ cc.cmd.PersistentFlags().BoolVar(&cc.logging, "log", false, "enable Logging")
+ cc.cmd.PersistentFlags().StringVar(&cc.logFile, "logFile", "", "log File path (if set, logging enabled automatically)")
+ cc.cmd.PersistentFlags().BoolVar(&cc.verboseLog, "verboseLog", false, "verbose logging")
+
+ cc.cmd.Flags().BoolVarP(&cc.buildWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed")
+
+ cc.cmd.Flags().Bool("renderToMemory", false, "render to memory (only useful for benchmark testing)")
+
+ // Set bash-completion
+ _ = cc.cmd.PersistentFlags().SetAnnotation("logFile", cobra.BashCompFilenameExt, []string{})
+
+ cc.cmd.SetGlobalNormalizationFunc(helpers.NormalizeHugoFlags)
+ cc.cmd.SilenceUsage = true
+
+ return cc
+}
+
+type hugoBuilderCommon struct {
+ source string
+ baseURL string
+ environment string
+
+ buildWatch bool
+
+ gc bool
+
+ // Profile flags (for debugging of performance problems)
+ cpuprofile string
+ memprofile string
+ mutexprofile string
+ traceprofile string
+
+ // TODO(bep) var vs string
+ logging bool
+ verbose bool
+ verboseLog bool
+ debug bool
+ quiet bool
+
+ cfgFile string
+ cfgDir string
+ logFile string
+}
+
+func (cc *hugoBuilderCommon) getConfigDir(baseDir string) string {
+ if cc.cfgDir != "" {
+ return paths.AbsPathify(baseDir, cc.cfgDir)
+ }
+
+ if v, found := os.LookupEnv("HUGO_CONFIGDIR"); found {
+ return paths.AbsPathify(baseDir, v)
+ }
+
+ return paths.AbsPathify(baseDir, "config")
+}
+
+func (cc *hugoBuilderCommon) getEnvironment(isServer bool) string {
+ if cc.environment != "" {
+ return cc.environment
+ }
+
+ if v, found := os.LookupEnv("HUGO_ENVIRONMENT"); found {
+ return v
+ }
+
+ if isServer {
+ return hugo.EnvironmentDevelopment
+ }
+
+ return hugo.EnvironmentProduction
+}
+
+func (cc *hugoBuilderCommon) handleFlags(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().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from")
+ cmd.Flags().StringVarP(&cc.environment, "environment", "e", "", "build environment")
+ cmd.Flags().StringP("contentDir", "c", "", "filesystem path to content directory")
+ cmd.Flags().StringP("layoutDir", "l", "", "filesystem path to layout directory")
+ cmd.Flags().StringP("cacheDir", "", "", "filesystem path to cache directory. Defaults: $TMPDIR/hugo_cache/")
+ cmd.Flags().BoolP("ignoreCache", "", false, "ignores the cache directory")
+ cmd.Flags().StringP("destination", "d", "", "filesystem path to write files to")
+ cmd.Flags().StringSliceP("theme", "t", []string{}, "themes to use (located in /themes/THEMENAME/)")
+ cmd.Flags().StringP("themesDir", "", "", "filesystem path to themes directory")
+ cmd.Flags().StringVarP(&cc.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(&cc.gc, "gc", false, "enable to run some cleanup tasks (remove unused cache files) after the build")
+
+ cmd.Flags().Bool("templateMetrics", false, "display metrics about template executions")
+ cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics")
+ 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().BoolP("i18n-warnings", "", false, "print missing translations")
+ cmd.Flags().BoolP("path-warnings", "", false, "print warnings on duplicate target paths etc.")
+ cmd.Flags().StringVarP(&cc.cpuprofile, "profile-cpu", "", "", "write cpu profile to `file`")
+ cmd.Flags().StringVarP(&cc.memprofile, "profile-mem", "", "", "write memory profile to `file`")
+ cmd.Flags().StringVarP(&cc.mutexprofile, "profile-mutex", "", "", "write Mutex profile to `file`")
+ cmd.Flags().StringVarP(&cc.traceprofile, "trace", "", "", "write trace to `file` (not useful in general)")
+
+ // Hide these for now.
+ cmd.Flags().MarkHidden("profile-cpu")
+ cmd.Flags().MarkHidden("profile-mem")
+ cmd.Flags().MarkHidden("profile-mutex")
+
+ cmd.Flags().StringSlice("disableKinds", []string{}, "disable different kind of pages (home, RSS etc.)")
+
+ cmd.Flags().Bool("minify", false, "minify any supported output format (HTML, XML 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 checkErr(logger *loggers.Logger, 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)
+}
diff --git a/commands/commands_test.go b/commands/commands_test.go
new file mode 100644
index 000000000..a1c6cdd76
--- /dev/null
+++ b/commands/commands_test.go
@@ -0,0 +1,286 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/gohugoio/hugo/common/types"
+
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestExecute(t *testing.T) {
+
+ assert := require.New(t)
+
+ dir, err := createSimpleTestSite(t, testSiteConfig{})
+ assert.NoError(err)
+
+ defer func() {
+ os.RemoveAll(dir)
+ }()
+
+ resp := Execute([]string{"-s=" + dir})
+ assert.NoError(resp.Err)
+ result := resp.Result
+ assert.True(len(result.Sites) == 1)
+ assert.True(len(result.Sites[0].RegularPages()) == 1)
+}
+
+func TestCommandsPersistentFlags(t *testing.T) {
+ assert := require.New(t)
+
+ noOpRunE := func(cmd *cobra.Command, args []string) error {
+ return nil
+ }
+
+ tests := []struct {
+ args []string
+ check func(command []cmder)
+ }{{[]string{"server",
+ "--config=myconfig.toml",
+ "--configDir=myconfigdir",
+ "--contentDir=mycontent",
+ "--disableKinds=page,home",
+ "--environment=testing",
+ "--configDir=myconfigdir",
+ "--layoutDir=mylayouts",
+ "--theme=mytheme",
+ "--gc",
+ "--themesDir=mythemes",
+ "--cleanDestinationDir",
+ "--navigateToChanged",
+ "--disableLiveReload",
+ "--noHTTPCache",
+ "--i18n-warnings",
+ "--destination=/tmp/mydestination",
+ "-b=https://example.com/b/",
+ "--port=1366",
+ "--renderToDisk",
+ "--source=mysource",
+ "--path-warnings",
+ }, func(commands []cmder) {
+ var sc *serverCmd
+ for _, command := range commands {
+ if b, ok := command.(commandsBuilderGetter); ok {
+ v := b.getCommandsBuilder().hugoBuilderCommon
+ assert.Equal("myconfig.toml", v.cfgFile)
+ assert.Equal("myconfigdir", v.cfgDir)
+ assert.Equal("mysource", v.source)
+ assert.Equal("https://example.com/b/", v.baseURL)
+ }
+
+ if srvCmd, ok := command.(*serverCmd); ok {
+ sc = srvCmd
+ }
+ }
+
+ assert.NotNil(sc)
+ assert.True(sc.navigateToChanged)
+ assert.True(sc.disableLiveReload)
+ assert.True(sc.noHTTPCache)
+ assert.True(sc.renderToDisk)
+ assert.Equal(1366, sc.serverPort)
+ assert.Equal("testing", sc.environment)
+
+ cfg := viper.New()
+ sc.flagsToConfig(cfg)
+ assert.Equal("/tmp/mydestination", cfg.GetString("publishDir"))
+ assert.Equal("mycontent", cfg.GetString("contentDir"))
+ assert.Equal("mylayouts", cfg.GetString("layoutDir"))
+ assert.Equal([]string{"mytheme"}, cfg.GetStringSlice("theme"))
+ assert.Equal("mythemes", cfg.GetString("themesDir"))
+ assert.Equal("https://example.com/b/", cfg.GetString("baseURL"))
+
+ assert.Equal([]string{"page", "home"}, cfg.Get("disableKinds"))
+
+ assert.True(cfg.GetBool("gc"))
+
+ // The flag is named path-warnings
+ assert.True(cfg.GetBool("logPathWarnings"))
+
+ // The flag is named i18n-warnings
+ assert.True(cfg.GetBool("logI18nWarnings"))
+
+ }}}
+
+ for _, test := range tests {
+ b := newCommandsBuilder()
+ root := b.addAll().build()
+
+ for _, c := range b.commands {
+ if c.getCommand() == nil {
+ continue
+ }
+ // We are only intereseted in the flag handling here.
+ c.getCommand().RunE = noOpRunE
+ }
+ rootCmd := root.getCommand()
+ rootCmd.SetArgs(test.args)
+ assert.NoError(rootCmd.Execute())
+ test.check(b.commands)
+ }
+
+}
+
+func TestCommandsExecute(t *testing.T) {
+
+ assert := require.New(t)
+
+ dir, err := createSimpleTestSite(t, testSiteConfig{})
+ assert.NoError(err)
+
+ dirOut, err := ioutil.TempDir("", "hugo-cli-out")
+ assert.NoError(err)
+
+ defer func() {
+ os.RemoveAll(dir)
+ os.RemoveAll(dirOut)
+ }()
+
+ sourceFlag := fmt.Sprintf("-s=%s", dir)
+
+ tests := []struct {
+ commands []string
+ flags []string
+ expectErrToContain string
+ }{
+ // TODO(bep) permission issue on my OSX? "operation not permitted" {[]string{"check", "ulimit"}, nil, false},
+ {[]string{"env"}, nil, ""},
+ {[]string{"version"}, nil, ""},
+ // no args = hugo build
+ {nil, []string{sourceFlag}, ""},
+ {nil, []string{sourceFlag, "--renderToMemory"}, ""},
+ {[]string{"config"}, []string{sourceFlag}, ""},
+ {[]string{"convert", "toTOML"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "toml")}, ""},
+ {[]string{"convert", "toYAML"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "yaml")}, ""},
+ {[]string{"convert", "toJSON"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "json")}, ""},
+ {[]string{"gen", "autocomplete"}, []string{"--completionfile=" + filepath.Join(dirOut, "autocomplete.txt")}, ""},
+ {[]string{"gen", "chromastyles"}, []string{"--style=manni"}, ""},
+ {[]string{"gen", "doc"}, []string{"--dir=" + filepath.Join(dirOut, "doc")}, ""},
+ {[]string{"gen", "man"}, []string{"--dir=" + filepath.Join(dirOut, "man")}, ""},
+ {[]string{"list", "drafts"}, []string{sourceFlag}, ""},
+ {[]string{"list", "expired"}, []string{sourceFlag}, ""},
+ {[]string{"list", "future"}, []string{sourceFlag}, ""},
+ {[]string{"new", "new-page.md"}, []string{sourceFlag}, ""},
+ {[]string{"new", "site", filepath.Join(dirOut, "new-site")}, nil, ""},
+ {[]string{"unknowncommand"}, nil, "unknown command"},
+ // TODO(bep) cli refactor fix https://github.com/gohugoio/hugo/issues/4450
+ //{[]string{"new", "theme", filepath.Join(dirOut, "new-theme")}, nil,false},
+ }
+
+ for _, test := range tests {
+ b := newCommandsBuilder().addAll().build()
+ hugoCmd := b.getCommand()
+ test.flags = append(test.flags, "--quiet")
+ hugoCmd.SetArgs(append(test.commands, test.flags...))
+
+ // TODO(bep) capture output and add some simple asserts
+ // TODO(bep) misspelled subcommands does not return an error. We should investigate this
+ // but before that, check for "Error: unknown command".
+
+ _, err := hugoCmd.ExecuteC()
+ if test.expectErrToContain != "" {
+ assert.Error(err, fmt.Sprintf("%v", test.commands))
+ assert.Contains(err.Error(), test.expectErrToContain)
+ } else {
+ assert.NoError(err, fmt.Sprintf("%v", test.commands))
+ }
+
+ // Assert that we have not left any development debug artifacts in
+ // the code.
+ if b.c != nil {
+ _, ok := b.c.destinationFs.(types.DevMarker)
+ assert.False(ok)
+ }
+
+ }
+
+}
+
+type testSiteConfig struct {
+ configTOML string
+ contentDir string
+}
+
+func createSimpleTestSite(t *testing.T, cfg testSiteConfig) (string, error) {
+ d, e := ioutil.TempDir("", "hugo-cli")
+ if e != nil {
+ return "", e
+ }
+
+ cfgStr := `
+
+baseURL = "https://example.org"
+title = "Hugo Commands"
+
+`
+
+ contentDir := "content"
+
+ if cfg.configTOML != "" {
+ cfgStr = cfg.configTOML
+ }
+ if cfg.contentDir != "" {
+ contentDir = cfg.contentDir
+ }
+
+ // Just the basic. These are for CLI tests, not site testing.
+ writeFile(t, filepath.Join(d, "config.toml"), cfgStr)
+
+ writeFile(t, filepath.Join(d, contentDir, "p1.md"), `
+---
+title: "P1"
+weight: 1
+---
+
+Content
+
+`)
+
+ writeFile(t, filepath.Join(d, "layouts", "_default", "single.html"), `
+
+Single: {{ .Title }}
+
+`)
+
+ writeFile(t, filepath.Join(d, "layouts", "_default", "list.html"), `
+
+List: {{ .Title }}
+Environment: {{ hugo.Environment }}
+
+`)
+
+ return d, nil
+
+}
+
+func writeFile(t *testing.T, filename, content string) {
+ must(t, os.MkdirAll(filepath.Dir(filename), os.FileMode(0755)))
+ must(t, ioutil.WriteFile(filename, []byte(content), os.FileMode(0755)))
+}
+
+func must(t *testing.T, err error) {
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/commands/config.go b/commands/config.go
new file mode 100644
index 000000000..33a61733d
--- /dev/null
+++ b/commands/config.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.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 _ cmder = (*configCmd)(nil)
+
+type configCmd struct {
+ hugoBuilderCommon
+ *baseCmd
+}
+
+func newConfigCmd() *configCmd {
+ cc := &configCmd{}
+ cc.baseCmd = newBaseCmd(&cobra.Command{
+ Use: "config",
+ Short: "Print the site configuration",
+ Long: `Print the site configuration, both default and custom settings.`,
+ RunE: cc.printConfig,
+ })
+
+ cc.cmd.Flags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from")
+
+ return cc
+}
+
+func (c *configCmd) printConfig(cmd *cobra.Command, args []string) error {
+ cfg, err := initializeConfig(true, false, &c.hugoBuilderCommon, c, nil)
+
+ 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/convert.go b/commands/convert.go
new file mode 100644
index 000000000..d0a46a641
--- /dev/null
+++ b/commands/convert.go
@@ -0,0 +1,251 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "fmt"
+ "io"
+ "strings"
+ "time"
+
+ "github.com/gohugoio/hugo/resources/page"
+
+ "github.com/gohugoio/hugo/hugofs"
+
+ "github.com/gohugoio/hugo/helpers"
+
+ "github.com/gohugoio/hugo/parser"
+ "github.com/gohugoio/hugo/parser/metadecoders"
+ "github.com/gohugoio/hugo/parser/pageparser"
+
+ src "github.com/gohugoio/hugo/source"
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/hugolib"
+
+ "path/filepath"
+
+ "github.com/spf13/cobra"
+)
+
+var (
+ _ cmder = (*convertCmd)(nil)
+)
+
+type convertCmd struct {
+ hugoBuilderCommon
+
+ outputDir string
+ unsafe bool
+
+ *baseCmd
+}
+
+func newConvertCmd() *convertCmd {
+ cc := &convertCmd{}
+
+ cc.baseCmd = newBaseCmd(&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,
+ })
+
+ cc.cmd.AddCommand(
+ &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 cc.convertContents(metadecoders.JSON)
+ },
+ },
+ &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 cc.convertContents(metadecoders.TOML)
+ },
+ },
+ &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 cc.convertContents(metadecoders.YAML)
+ },
+ },
+ )
+
+ cc.cmd.PersistentFlags().StringVarP(&cc.outputDir, "output", "o", "", "filesystem path to write files to")
+ cc.cmd.PersistentFlags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from")
+ cc.cmd.PersistentFlags().BoolVar(&cc.unsafe, "unsafe", false, "enable less safe operations, please backup first")
+ cc.cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
+
+ return cc
+}
+
+func (cc *convertCmd) convertContents(format metadecoders.Format) error {
+ if cc.outputDir == "" && !cc.unsafe {
+ return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path")
+ }
+
+ c, err := initializeConfig(true, false, &cc.hugoBuilderCommon, cc, nil)
+ if err != nil {
+ return err
+ }
+
+ c.Cfg.Set("buildDrafts", true)
+
+ h, err := hugolib.NewHugoSites(*c.DepsCfg)
+ if err != nil {
+ return err
+ }
+
+ if err := h.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
+ return err
+ }
+
+ site := h.Sites[0]
+
+ site.Log.FEEDBACK.Println("processing", len(site.AllPages()), "content files")
+ for _, p := range site.AllPages() {
+ if err := cc.convertAndSavePage(p, site, format); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (cc *convertCmd) convertAndSavePage(p page.Page, site *hugolib.Site, targetFormat metadecoders.Format) error {
+ // The resources are not in .Site.AllPages.
+ for _, r := range p.Resources().ByType("page") {
+ if err := cc.convertAndSavePage(r.(page.Page), site, targetFormat); err != nil {
+ return err
+ }
+ }
+
+ if p.File().IsZero() {
+ // No content file.
+ return nil
+ }
+
+ errMsg := fmt.Errorf("Error processing file %q", p.Path())
+
+ site.Log.INFO.Println("Attempting to convert", p.File().Filename())
+
+ f, _ := p.File().(src.ReadableFile)
+ file, err := f.Open()
+ if err != nil {
+ site.Log.ERROR.Println(errMsg)
+ file.Close()
+ return nil
+ }
+
+ pf, err := parseContentFile(file)
+ if err != nil {
+ site.Log.ERROR.Println(errMsg)
+ file.Close()
+ return err
+ }
+
+ file.Close()
+
+ // better handling of dates in formats that don't have support for them
+ if pf.frontMatterFormat == metadecoders.JSON || pf.frontMatterFormat == metadecoders.YAML || pf.frontMatterFormat == metadecoders.TOML {
+ for k, v := range pf.frontMatter {
+ switch vv := v.(type) {
+ case time.Time:
+ pf.frontMatter[k] = vv.Format(time.RFC3339)
+ }
+ }
+ }
+
+ var newContent bytes.Buffer
+ err = parser.InterfaceToFrontMatter(pf.frontMatter, targetFormat, &newContent)
+ if err != nil {
+ site.Log.ERROR.Println(errMsg)
+ return err
+ }
+
+ newContent.Write(pf.content)
+
+ newFilename := p.File().Filename()
+
+ if cc.outputDir != "" {
+ contentDir := strings.TrimSuffix(newFilename, p.Path())
+ contentDir = filepath.Base(contentDir)
+
+ newFilename = filepath.Join(cc.outputDir, contentDir, p.Path())
+ }
+
+ fs := hugofs.Os
+ if err := helpers.WriteToDisk(newFilename, &newContent, fs); err != nil {
+ return errors.Wrapf(err, "Failed to save file %q:", newFilename)
+ }
+
+ return nil
+}
+
+type parsedFile struct {
+ frontMatterFormat metadecoders.Format
+ frontMatterSource []byte
+ frontMatter map[string]interface{}
+
+ // Everything after Front Matter
+ content []byte
+}
+
+func parseContentFile(r io.Reader) (parsedFile, error) {
+ var pf parsedFile
+
+ psr, err := pageparser.Parse(r, pageparser.Config{})
+ if err != nil {
+ return pf, err
+ }
+
+ iter := psr.Iterator()
+
+ walkFn := func(item pageparser.Item) bool {
+ if pf.frontMatterSource != nil {
+ // The rest is content.
+ pf.content = psr.Input()[item.Pos:]
+ // Done
+ return false
+ } else if item.IsFrontMatter() {
+ pf.frontMatterFormat = metadecoders.FormatFromFrontMatterType(item.Type)
+ pf.frontMatterSource = item.Val
+ }
+ return true
+
+ }
+
+ iter.PeekWalk(walkFn)
+
+ metadata, err := metadecoders.Default.UnmarshalToMap(pf.frontMatterSource, pf.frontMatterFormat)
+ if err != nil {
+ return pf, err
+ }
+ pf.frontMatter = metadata
+
+ return pf, nil
+
+}
diff --git a/commands/deploy.go b/commands/deploy.go
new file mode 100644
index 000000000..6f8eac357
--- /dev/null
+++ b/commands/deploy.go
@@ -0,0 +1,75 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "context"
+
+ "github.com/gohugoio/hugo/deploy"
+ "github.com/spf13/cobra"
+)
+
+var _ cmder = (*deployCmd)(nil)
+
+// deployCmd supports deploying sites to Cloud providers.
+type deployCmd struct {
+ hugoBuilderCommon
+ *baseCmd
+}
+
+// TODO: In addition to the "deploy" command, consider adding a "--deploy"
+// flag for the default command; this would build the site and then deploy it.
+// It's not obvious how to do this; would all of the deploy-specific flags
+// have to exist at the top level as well?
+
+// TODO: The output files change every time "hugo" is executed, it looks
+// like because of map order randomization. This means that you can
+// run "hugo && hugo deploy" again and again and upload new stuff every time. Is
+// this intended?
+
+func newDeployCmd() *deployCmd {
+ cc := &deployCmd{}
+
+ cc.baseCmd = newBaseCmd(&cobra.Command{
+ Use: "deploy",
+ Short: "Deploy your site to a Cloud provider.",
+ // TODO: improve Long docstring.
+ // TODO: update real documentation. Is it in ../docs/ or in hugoDocs?
+ Long: `Deploy your site to a Cloud provider.`,
+
+ RunE: func(cmd *cobra.Command, args []string) error {
+ cfgInit := func(c *commandeer) error {
+ return nil
+ }
+ comm, err := initializeConfig(true, false, &cc.hugoBuilderCommon, cc, cfgInit)
+ if err != nil {
+ return err
+ }
+ deployer, err := deploy.New(comm.Cfg, comm.hugo.PathSpec.PublishFs)
+ if err != nil {
+ return err
+ }
+ return deployer.Deploy(context.Background())
+ },
+ })
+
+ cc.cmd.Flags().String("target", "default", "target deployment from deployments section in config file")
+ cc.cmd.Flags().Bool("confirm", false, "ask for confirmation before making changes to the target")
+ cc.cmd.Flags().Bool("dryRun", false, "dry run")
+ cc.cmd.Flags().Bool("force", false, "force upload of all files")
+ cc.cmd.Flags().Bool("invalidateCDN", true, "invalidate the CDN cache via the CloudFrontDistributionID listed in the deployment target")
+ cc.cmd.Flags().Int("maxDeletes", 256, "maximum # of files to delete, or -1 to disable")
+
+ return cc
+}
diff --git a/commands/env.go b/commands/env.go
new file mode 100644
index 000000000..76c16b93b
--- /dev/null
+++ b/commands/env.go
@@ -0,0 +1,44 @@
+// 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 _ cmder = (*envCmd)(nil)
+
+type envCmd struct {
+ *baseCmd
+}
+
+func newEnvCmd() *envCmd {
+ return &envCmd{baseCmd: newBaseCmd(&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..6878cfe70
--- /dev/null
+++ b/commands/gen.go
@@ -0,0 +1,41 @@
+// 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 _ cmder = (*genCmd)(nil)
+
+type genCmd struct {
+ *baseCmd
+}
+
+func newGenCmd() *genCmd {
+ cc := &genCmd{}
+ cc.baseCmd = newBaseCmd(&cobra.Command{
+ Use: "gen",
+ Short: "A collection of several useful generators.",
+ })
+
+ cc.cmd.AddCommand(
+ newGenautocompleteCmd().getCommand(),
+ newGenDocCmd().getCommand(),
+ newGenManCmd().getCommand(),
+ createGenDocsHelper().getCommand(),
+ createGenChromaStyles().getCommand())
+
+ return cc
+}
diff --git a/commands/genautocomplete.go b/commands/genautocomplete.go
new file mode 100644
index 000000000..b0b98abb4
--- /dev/null
+++ b/commands/genautocomplete.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 (
+ "github.com/spf13/cobra"
+ jww "github.com/spf13/jwalterweatherman"
+)
+
+var _ cmder = (*genautocompleteCmd)(nil)
+
+type genautocompleteCmd struct {
+ autocompleteTarget string
+
+ // bash for now (zsh and others will come)
+ autocompleteType string
+
+ *baseCmd
+}
+
+func newGenautocompleteCmd() *genautocompleteCmd {
+ cc := &genautocompleteCmd{}
+
+ cc.baseCmd = newBaseCmd(&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 cc.autocompleteType != "bash" {
+ return newUserError("Only Bash is supported for now")
+ }
+
+ err := cmd.Root().GenBashCompletionFile(cc.autocompleteTarget)
+
+ if err != nil {
+ return err
+ }
+
+ jww.FEEDBACK.Println("Bash completion file for Hugo saved to", cc.autocompleteTarget)
+
+ return nil
+ },
+ })
+
+ cc.cmd.PersistentFlags().StringVarP(&cc.autocompleteTarget, "completionfile", "", "/etc/bash_completion.d/hugo.sh", "autocompletion file")
+ cc.cmd.PersistentFlags().StringVarP(&cc.autocompleteType, "type", "", "bash", "autocompletion type (currently only bash supported)")
+
+ // For bash-completion
+ cc.cmd.PersistentFlags().SetAnnotation("completionfile", cobra.BashCompFilenameExt, []string{})
+
+ return cc
+}
diff --git a/commands/genchromastyles.go b/commands/genchromastyles.go
new file mode 100644
index 000000000..a2231e56e
--- /dev/null
+++ b/commands/genchromastyles.go
@@ -0,0 +1,74 @@
+// 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 (
+ "os"
+
+ "github.com/alecthomas/chroma"
+ "github.com/alecthomas/chroma/formatters/html"
+ "github.com/alecthomas/chroma/styles"
+ "github.com/spf13/cobra"
+)
+
+var (
+ _ cmder = (*genChromaStyles)(nil)
+)
+
+type genChromaStyles struct {
+ style string
+ highlightStyle string
+ linesStyle string
+ *baseCmd
+}
+
+// TODO(bep) highlight
+func createGenChromaStyles() *genChromaStyles {
+ g := &genChromaStyles{
+ baseCmd: newBaseCmd(&cobra.Command{
+ Use: "chromastyles",
+ Short: "Generate CSS stylesheet for the Chroma code highlighter",
+ Long: `Generate CSS stylesheet for the Chroma code highlighter for a given style. This stylesheet is needed if pygmentsUseClasses is enabled in config.
+
+See https://help.farbox.com/pygments.html for preview of available styles`,
+ }),
+ }
+
+ g.cmd.RunE = func(cmd *cobra.Command, args []string) error {
+ return g.generate()
+ }
+
+ g.cmd.PersistentFlags().StringVar(&g.style, "style", "friendly", "highlighter style (see https://help.farbox.com/pygments.html)")
+ g.cmd.PersistentFlags().StringVar(&g.highlightStyle, "highlightStyle", "bg:#ffffcc", "style used for highlighting lines (see https://github.com/alecthomas/chroma)")
+ g.cmd.PersistentFlags().StringVar(&g.linesStyle, "linesStyle", "", "style used for line numbers (see https://github.com/alecthomas/chroma)")
+
+ return g
+}
+
+func (g *genChromaStyles) generate() error {
+ builder := styles.Get(g.style).Builder()
+ if g.highlightStyle != "" {
+ builder.Add(chroma.LineHighlight, g.highlightStyle)
+ }
+ if g.linesStyle != "" {
+ builder.Add(chroma.LineNumbers, g.linesStyle)
+ }
+ style, err := builder.Build()
+ if err != nil {
+ return err
+ }
+ formatter := html.New(html.WithClasses())
+ formatter.WriteCSS(os.Stdout, style)
+ return nil
+}
diff --git a/commands/gendoc.go b/commands/gendoc.go
new file mode 100644
index 000000000..8312191f2
--- /dev/null
+++ b/commands/gendoc.go
@@ -0,0 +1,96 @@
+// 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"
+)
+
+var _ cmder = (*genDocCmd)(nil)
+
+type genDocCmd struct {
+ gendocdir string
+ *baseCmd
+}
+
+func newGenDocCmd() *genDocCmd {
+ const gendocFrontmatterTemplate = `---
+date: %s
+title: "%s"
+slug: %s
+url: %s
+---
+`
+
+ cc := &genDocCmd{}
+
+ cc.baseCmd = newBaseCmd(&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(cc.gendocdir, helpers.FilePathSeparator) {
+ cc.gendocdir += helpers.FilePathSeparator
+ }
+ if found, _ := helpers.Exists(cc.gendocdir, hugofs.Os); !found {
+ jww.FEEDBACK.Println("Directory", cc.gendocdir, "does not exist, creating...")
+ if err := hugofs.Os.MkdirAll(cc.gendocdir, 0777); err != nil {
+ return err
+ }
+ }
+ now := time.Now().Format("2006-01-02")
+ 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", cc.gendocdir, "...")
+ doc.GenMarkdownTreeCustom(cmd.Root(), cc.gendocdir, prepender, linkHandler)
+ jww.FEEDBACK.Println("Done.")
+
+ return nil
+ },
+ })
+
+ cc.cmd.PersistentFlags().StringVar(&cc.gendocdir, "dir", "/tmp/hugodoc/", "the directory to write the doc.")
+
+ // For bash-completion
+ cc.cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
+
+ return cc
+}
diff --git a/commands/gendocshelper.go b/commands/gendocshelper.go
new file mode 100644
index 000000000..c243581f6
--- /dev/null
+++ b/commands/gendocshelper.go
@@ -0,0 +1,74 @@
+// 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"
+)
+
+var (
+ _ cmder = (*genDocsHelper)(nil)
+)
+
+type genDocsHelper struct {
+ target string
+ *baseCmd
+}
+
+func createGenDocsHelper() *genDocsHelper {
+ g := &genDocsHelper{
+ baseCmd: newBaseCmd(&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()
+ }
+
+ g.cmd.PersistentFlags().StringVarP(&g.target, "dir", "", "docs/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..720046289
--- /dev/null
+++ b/commands/genman.go
@@ -0,0 +1,77 @@
+// 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/common/hugo"
+ "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 _ cmder = (*genManCmd)(nil)
+
+type genManCmd struct {
+ genmandir string
+ *baseCmd
+}
+
+func newGenManCmd() *genManCmd {
+ cc := &genManCmd{}
+
+ cc.baseCmd = newBaseCmd(&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", hugo.CurrentVersion),
+ }
+ if !strings.HasSuffix(cc.genmandir, helpers.FilePathSeparator) {
+ cc.genmandir += helpers.FilePathSeparator
+ }
+ if found, _ := helpers.Exists(cc.genmandir, hugofs.Os); !found {
+ jww.FEEDBACK.Println("Directory", cc.genmandir, "does not exist, creating...")
+ if err := hugofs.Os.MkdirAll(cc.genmandir, 0777); err != nil {
+ return err
+ }
+ }
+ cmd.Root().DisableAutoGenTag = true
+
+ jww.FEEDBACK.Println("Generating Hugo man pages in", cc.genmandir, "...")
+ doc.GenManTree(cmd.Root(), header, cc.genmandir)
+
+ jww.FEEDBACK.Println("Done.")
+
+ return nil
+ },
+ })
+
+ cc.cmd.PersistentFlags().StringVar(&cc.genmandir, "dir", "man/", "the directory to write the man pages.")
+
+ // For bash-completion
+ cc.cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
+
+ return cc
+}
diff --git a/commands/helpers.go b/commands/helpers.go
new file mode 100644
index 000000000..1386e425f
--- /dev/null
+++ b/commands/helpers.go
@@ -0,0 +1,79 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "regexp"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/spf13/cobra"
+)
+
+const (
+ ansiEsc = "\u001B"
+ clearLine = "\r\033[K"
+ hideCursor = ansiEsc + "[?25l"
+ showCursor = ansiEsc + "[?25h"
+)
+
+type flagsToConfigHandler interface {
+ flagsToConfig(cfg config.Provider)
+}
+
+type cmder interface {
+ flagsToConfigHandler
+ getCommand() *cobra.Command
+}
+
+// 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())
+}
diff --git a/commands/hugo.go b/commands/hugo.go
new file mode 100644
index 000000000..07f2b95a2
--- /dev/null
+++ b/commands/hugo.go
@@ -0,0 +1,1207 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "os/signal"
+ "runtime/pprof"
+ "runtime/trace"
+ "sort"
+ "sync/atomic"
+
+ "github.com/gohugoio/hugo/hugofs"
+
+ "github.com/gohugoio/hugo/resources/page"
+
+ "github.com/gohugoio/hugo/common/hugo"
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/common/terminal"
+
+ "syscall"
+
+ "github.com/gohugoio/hugo/hugolib/filesystems"
+
+ "golang.org/x/sync/errgroup"
+
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "time"
+
+ "github.com/gohugoio/hugo/config"
+
+ "github.com/gohugoio/hugo/parser/metadecoders"
+ flag "github.com/spf13/pflag"
+
+ "github.com/fsnotify/fsnotify"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugolib"
+ "github.com/gohugoio/hugo/livereload"
+ "github.com/gohugoio/hugo/watcher"
+ "github.com/spf13/afero"
+ "github.com/spf13/cobra"
+ "github.com/spf13/fsync"
+ jww "github.com/spf13/jwalterweatherman"
+)
+
+// The Response value from Execute.
+type Response struct {
+ // The build Result will only be set in the hugo build command.
+ Result *hugolib.HugoSites
+
+ // Err is set when the command failed to execute.
+ Err error
+
+ // The command that was executed.
+ Cmd *cobra.Command
+}
+
+// IsUserError returns true is the Response error is a user error rather than a
+// system error.
+func (r Response) IsUserError() bool {
+ return r.Err != nil && isUserError(r.Err)
+}
+
+// Execute adds all child commands to the root command HugoCmd and sets flags appropriately.
+// The args are usually filled with os.Args[1:].
+func Execute(args []string) Response {
+ hugoCmd := newCommandsBuilder().addAll().build()
+ cmd := hugoCmd.getCommand()
+ cmd.SetArgs(args)
+
+ c, err := cmd.ExecuteC()
+
+ var resp Response
+
+ if c == cmd && hugoCmd.c != nil {
+ // Root command executed
+ resp.Result = hugoCmd.c.hugo
+ }
+
+ if err == nil {
+ errCount := int(loggers.GlobalErrorCounter.Count())
+ if errCount > 0 {
+ err = fmt.Errorf("logged %d errors", errCount)
+ } else if resp.Result != nil {
+ errCount = resp.Result.NumLogErrors()
+ if errCount > 0 {
+ err = fmt.Errorf("logged %d errors", errCount)
+ }
+ }
+
+ }
+
+ resp.Err = err
+ resp.Cmd = c
+
+ return resp
+}
+
+// InitializeConfig initializes a config file with sensible default configuration flags.
+func initializeConfig(mustHaveConfigFile, running bool,
+ h *hugoBuilderCommon,
+ f flagsToConfigHandler,
+ doWithCommandeer func(c *commandeer) error) (*commandeer, error) {
+
+ c, err := newCommandeer(mustHaveConfigFile, running, h, f, doWithCommandeer)
+ if err != nil {
+ return nil, err
+ }
+
+ return c, nil
+
+}
+
+func (c *commandeer) createLogger(cfg config.Provider, running bool) (*loggers.Logger, error) {
+ var (
+ logHandle = ioutil.Discard
+ logThreshold = jww.LevelWarn
+ logFile = cfg.GetString("logFile")
+ outHandle = os.Stdout
+ stdoutThreshold = jww.LevelWarn
+ )
+
+ if c.h.verboseLog || c.h.logging || (c.h.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 !c.h.quiet && cfg.GetBool("verbose") {
+ stdoutThreshold = jww.LevelInfo
+ }
+
+ if cfg.GetBool("debug") {
+ stdoutThreshold = jww.LevelDebug
+ }
+
+ if c.h.verboseLog {
+ logThreshold = jww.LevelInfo
+ if cfg.GetBool("debug") {
+ logThreshold = jww.LevelDebug
+ }
+ }
+
+ loggers.InitGlobalLogger(stdoutThreshold, logThreshold, outHandle, logHandle)
+ helpers.InitLoggers()
+
+ return loggers.NewLogger(stdoutThreshold, logThreshold, outHandle, logHandle, running), nil
+}
+
+func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
+ persFlagKeys := []string{
+ "debug",
+ "verbose",
+ "logFile",
+ // Moved from vars
+ }
+ flagKeys := []string{
+ "cleanDestinationDir",
+ "buildDrafts",
+ "buildFuture",
+ "buildExpired",
+ "uglyURLs",
+ "canonifyURLs",
+ "enableRobotsTXT",
+ "enableGitInfo",
+ "pluralizeListTitles",
+ "preserveTaxonomyNames",
+ "ignoreCache",
+ "forceSyncStatic",
+ "noTimes",
+ "noChmod",
+ "templateMetrics",
+ "templateMetricsHints",
+
+ // Moved from vars.
+ "baseURL",
+ "buildWatch",
+ "cacheDir",
+ "cfgFile",
+ "confirm",
+ "contentDir",
+ "debug",
+ "destination",
+ "disableKinds",
+ "dryRun",
+ "force",
+ "gc",
+ "i18n-warnings",
+ "invalidateCDN",
+ "layoutDir",
+ "logFile",
+ "maxDeletes",
+ "quiet",
+ "renderToMemory",
+ "source",
+ "target",
+ "theme",
+ "themesDir",
+ "verbose",
+ "verboseLog",
+ "duplicateTargetPaths",
+ }
+
+ // Will set a value even if it is the default.
+ flagKeysForced := []string{
+ "minify",
+ }
+
+ for _, key := range persFlagKeys {
+ setValueFromFlag(cmd.PersistentFlags(), key, cfg, "", false)
+ }
+ for _, key := range flagKeys {
+ setValueFromFlag(cmd.Flags(), key, cfg, "", false)
+ }
+
+ for _, key := range flagKeysForced {
+ setValueFromFlag(cmd.Flags(), key, cfg, "", true)
+ }
+
+ // Set some "config aliases"
+ setValueFromFlag(cmd.Flags(), "destination", cfg, "publishDir", false)
+ setValueFromFlag(cmd.Flags(), "i18n-warnings", cfg, "logI18nWarnings", false)
+ setValueFromFlag(cmd.Flags(), "path-warnings", cfg, "logPathWarnings", false)
+
+}
+
+func setValueFromFlag(flags *flag.FlagSet, key string, cfg config.Provider, targetKey string, force bool) {
+ key = strings.TrimSpace(key)
+ if (force && flags.Lookup(key) != nil) || flags.Changed(key) {
+ f := flags.Lookup(key)
+ configKey := key
+ if targetKey != "" {
+ configKey = targetKey
+ }
+ // Gotta love this API.
+ switch f.Value.Type() {
+ case "bool":
+ bv, _ := flags.GetBool(key)
+ cfg.Set(configKey, bv)
+ case "string":
+ cfg.Set(configKey, f.Value.String())
+ case "stringSlice":
+ bv, _ := flags.GetStringSlice(key)
+ cfg.Set(configKey, bv)
+ case "int":
+ iv, _ := flags.GetInt(key)
+ cfg.Set(configKey, iv)
+ default:
+ panic(fmt.Sprintf("update switch with %s", f.Value.Type()))
+ }
+
+ }
+}
+
+func isTerminal() bool {
+ return terminal.IsTerminal(os.Stdout)
+
+}
+func ifTerminal(s string) string {
+ if !isTerminal() {
+ return ""
+ }
+ return s
+}
+
+func (c *commandeer) fullBuild() error {
+ var (
+ g errgroup.Group
+ langCount map[string]uint64
+ )
+
+ if !c.h.quiet {
+ fmt.Print(ifTerminal(hideCursor) + "Building sites … ")
+ if isTerminal() {
+ defer func() {
+ fmt.Print(showCursor + clearLine)
+ }()
+ }
+ }
+
+ copyStaticFunc := func() error {
+
+ cnt, err := c.copyStatic()
+ if err != nil {
+ if !os.IsNotExist(err) {
+ return errors.Wrap(err, "Error copying static files")
+ }
+ c.logger.INFO.Println("No Static directory found")
+ }
+ langCount = cnt
+ langCount = cnt
+ return nil
+ }
+ buildSitesFunc := func() error {
+ if err := c.buildSites(); err != nil {
+ return errors.Wrap(err, "Error building site")
+ }
+ return nil
+ }
+ // Do not copy static files and build sites in parallel if cleanDestinationDir is enabled.
+ // This flag deletes all static resources in /public folder that are missing in /static,
+ // and it does so at the end of copyStatic() call.
+ if c.Cfg.GetBool("cleanDestinationDir") {
+ if err := copyStaticFunc(); err != nil {
+ return err
+ }
+ if err := buildSitesFunc(); err != nil {
+ return err
+ }
+ } else {
+ g.Go(copyStaticFunc)
+ g.Go(buildSitesFunc)
+ if err := g.Wait(); err != nil {
+ return err
+ }
+ }
+
+ for _, s := range c.hugo.Sites {
+ s.ProcessingStats.Static = langCount[s.Language().Lang]
+ }
+
+ if c.h.gc {
+ count, err := c.hugo.GC()
+ if err != nil {
+ return err
+ }
+ for _, s := range c.hugo.Sites {
+ // We have no way of knowing what site the garbage belonged to.
+ s.ProcessingStats.Cleaned = uint64(count)
+ }
+ }
+
+ return nil
+
+}
+
+func (c *commandeer) initCPUProfile() (func(), error) {
+ if c.h.cpuprofile == "" {
+ return nil, nil
+ }
+
+ f, err := os.Create(c.h.cpuprofile)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to create CPU profile")
+ }
+ if err := pprof.StartCPUProfile(f); err != nil {
+ return nil, errors.Wrap(err, "failed to start CPU profile")
+ }
+ return func() {
+ pprof.StopCPUProfile()
+ f.Close()
+ }, nil
+}
+
+func (c *commandeer) initMemProfile() {
+ if c.h.memprofile == "" {
+ return
+ }
+
+ f, err := os.Create(c.h.memprofile)
+ if err != nil {
+ c.logger.ERROR.Println("could not create memory profile: ", err)
+ }
+ defer f.Close()
+ runtime.GC() // get up-to-date statistics
+ if err := pprof.WriteHeapProfile(f); err != nil {
+ c.logger.ERROR.Println("could not write memory profile: ", err)
+ }
+}
+
+func (c *commandeer) initTraceProfile() (func(), error) {
+ if c.h.traceprofile == "" {
+ return nil, nil
+ }
+
+ f, err := os.Create(c.h.traceprofile)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to create trace file")
+ }
+
+ if err := trace.Start(f); err != nil {
+ return nil, errors.Wrap(err, "failed to start trace")
+ }
+
+ return func() {
+ trace.Stop()
+ f.Close()
+ }, nil
+}
+
+func (c *commandeer) initMutexProfile() (func(), error) {
+ if c.h.mutexprofile == "" {
+ return nil, nil
+ }
+
+ f, err := os.Create(c.h.mutexprofile)
+ if err != nil {
+ return nil, err
+ }
+
+ runtime.SetMutexProfileFraction(1)
+
+ return func() {
+ pprof.Lookup("mutex").WriteTo(f, 0)
+ f.Close()
+ }, nil
+
+}
+
+func (c *commandeer) initProfiling() (func(), error) {
+ stopCPUProf, err := c.initCPUProfile()
+ if err != nil {
+ return nil, err
+ }
+
+ stopMutexProf, err := c.initMutexProfile()
+ if err != nil {
+ return nil, err
+ }
+
+ stopTraceProf, err := c.initTraceProfile()
+ if err != nil {
+ return nil, err
+ }
+
+ return func() {
+ c.initMemProfile()
+
+ if stopCPUProf != nil {
+ stopCPUProf()
+ }
+ if stopMutexProf != nil {
+ stopMutexProf()
+ }
+
+ if stopTraceProf != nil {
+ stopTraceProf()
+ }
+ }, nil
+}
+
+func (c *commandeer) build() error {
+ defer c.timeTrack(time.Now(), "Total")
+
+ stopProfiling, err := c.initProfiling()
+ if err != nil {
+ return err
+ }
+
+ defer func() {
+ if stopProfiling != nil {
+ stopProfiling()
+ }
+ }()
+
+ if err := c.fullBuild(); err != nil {
+ return err
+ }
+
+ // TODO(bep) Feedback?
+ if !c.h.quiet {
+ fmt.Println()
+ c.hugo.PrintProcessingStats(os.Stdout)
+ fmt.Println()
+
+ if createCounter, ok := c.destinationFs.(hugofs.DuplicatesReporter); ok {
+ dupes := createCounter.ReportDuplicates()
+ if dupes != "" {
+ c.logger.WARN.Println("Duplicate target paths:", dupes)
+ }
+ }
+ }
+
+ if c.h.buildWatch {
+ watchDirs, err := c.getDirList()
+ if err != nil {
+ return err
+ }
+ c.logger.FEEDBACK.Println("Watching for changes in", c.hugo.PathSpec.AbsPathify(c.Cfg.GetString("contentDir")))
+ c.logger.FEEDBACK.Println("Press Ctrl+C to stop")
+ watcher, err := c.newWatcher(watchDirs...)
+ checkErr(c.Logger, err)
+ defer watcher.Close()
+
+ var sigs = make(chan os.Signal, 1)
+ signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+
+ <-sigs
+ }
+
+ return nil
+}
+
+func (c *commandeer) serverBuild() error {
+ defer c.timeTrack(time.Now(), "Total")
+
+ stopProfiling, err := c.initProfiling()
+ if err != nil {
+ return err
+ }
+
+ defer func() {
+ if stopProfiling != nil {
+ stopProfiling()
+ }
+ }()
+
+ if err := c.fullBuild(); err != nil {
+ return err
+ }
+
+ // TODO(bep) Feedback?
+ if !c.h.quiet {
+ fmt.Println()
+ c.hugo.PrintProcessingStats(os.Stdout)
+ fmt.Println()
+ }
+
+ return nil
+}
+
+func (c *commandeer) copyStatic() (map[string]uint64, error) {
+ return c.doWithPublishDirs(c.copyStaticTo)
+}
+
+func (c *commandeer) doWithPublishDirs(f func(sourceFs *filesystems.SourceFilesystem) (uint64, error)) (map[string]uint64, error) {
+
+ langCount := make(map[string]uint64)
+
+ staticFilesystems := c.hugo.BaseFs.SourceFilesystems.Static
+
+ if len(staticFilesystems) == 0 {
+ c.logger.INFO.Println("No static directories found to sync")
+ return langCount, nil
+ }
+
+ for lang, fs := range staticFilesystems {
+ cnt, err := f(fs)
+ if err != nil {
+ return langCount, err
+ }
+ if lang == "" {
+ // Not multihost
+ for _, l := range c.languages {
+ langCount[l.Lang] = cnt
+ }
+ } else {
+ langCount[lang] = cnt
+ }
+ }
+
+ return langCount, nil
+}
+
+type countingStatFs struct {
+ afero.Fs
+ statCounter uint64
+}
+
+func (fs *countingStatFs) Stat(name string) (os.FileInfo, error) {
+ f, err := fs.Fs.Stat(name)
+ if err == nil {
+ if !f.IsDir() {
+ atomic.AddUint64(&fs.statCounter, 1)
+ }
+ }
+ return f, err
+}
+
+func (c *commandeer) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
+ publishDir := c.hugo.PathSpec.PublishDir
+ // If root, remove the second '/'
+ if publishDir == "//" {
+ publishDir = helpers.FilePathSeparator
+ }
+
+ if sourceFs.PublishFolder != "" {
+ publishDir = filepath.Join(publishDir, sourceFs.PublishFolder)
+ }
+
+ fs := &countingStatFs{Fs: sourceFs.Fs}
+
+ syncer := fsync.NewSyncer()
+ syncer.NoTimes = c.Cfg.GetBool("noTimes")
+ syncer.NoChmod = c.Cfg.GetBool("noChmod")
+ syncer.SrcFs = fs
+ 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
+ err := syncer.Sync(publishDir, helpers.FilePathSeparator)
+ if err != nil {
+ return 0, err
+ }
+
+ // Sync runs Stat 3 times for every source file (which sounds much)
+ numFiles := fs.statCounter / 3
+
+ return numFiles, err
+}
+
+func (c *commandeer) firstPathSpec() *helpers.PathSpec {
+ return c.hugo.Sites[0].PathSpec
+}
+
+func (c *commandeer) timeTrack(start time.Time, name string) {
+ if c.h.quiet {
+ return
+ }
+ elapsed := time.Since(start)
+ c.logger.FEEDBACK.Printf("%s in %v ms", name, int(1000*elapsed.Seconds()))
+}
+
+// getDirList provides NewWatcher() with a list of directories to watch for changes.
+func (c *commandeer) getDirList() ([]string, error) {
+ var a []string
+
+ // To handle nested symlinked content dirs
+ var seen = make(map[string]bool)
+ var nested []string
+
+ newWalker := func(allowSymbolicDirs bool) func(path string, fi os.FileInfo, err error) error {
+ return func(path string, fi os.FileInfo, err error) error {
+ if err != nil {
+ if os.IsNotExist(err) {
+ 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 := helpers.LstatIfPossible(c.Fs.Source, link)
+ if err != nil {
+ c.logger.ERROR.Printf("Cannot stat %q: %s", link, err)
+ return nil
+ }
+ if !allowSymbolicDirs && !linkfi.Mode().IsRegular() {
+ c.logger.ERROR.Printf("Symbolic links for directories not supported, skipping %q", path)
+ return nil
+ }
+
+ if allowSymbolicDirs && linkfi.IsDir() {
+ // afero.Walk will not walk symbolic links, so wee need to do it.
+ if !seen[path] {
+ seen[path] = true
+ nested = append(nested, path)
+ }
+ return nil
+ }
+
+ fi = linkfi
+ }
+
+ if fi.IsDir() {
+ if fi.Name() == ".git" ||
+ fi.Name() == "node_modules" || fi.Name() == "bower_components" {
+ return filepath.SkipDir
+ }
+ a = append(a, path)
+ }
+ return nil
+ }
+ }
+
+ symLinkWalker := newWalker(true)
+ regularWalker := newWalker(false)
+
+ // SymbolicWalk will log anny ERRORs
+ // Also note that the Dirnames fetched below will contain any relevant theme
+ // directories.
+ for _, contentDir := range c.hugo.PathSpec.BaseFs.Content.Dirnames {
+ _ = helpers.SymbolicWalk(c.Fs.Source, contentDir, symLinkWalker)
+ }
+
+ for _, staticDir := range c.hugo.PathSpec.BaseFs.Data.Dirnames {
+ _ = helpers.SymbolicWalk(c.Fs.Source, staticDir, regularWalker)
+ }
+
+ for _, staticDir := range c.hugo.PathSpec.BaseFs.I18n.Dirnames {
+ _ = helpers.SymbolicWalk(c.Fs.Source, staticDir, regularWalker)
+ }
+
+ for _, staticDir := range c.hugo.PathSpec.BaseFs.Layouts.Dirnames {
+ _ = helpers.SymbolicWalk(c.Fs.Source, staticDir, regularWalker)
+ }
+
+ for _, staticFilesystem := range c.hugo.PathSpec.BaseFs.Static {
+ for _, staticDir := range staticFilesystem.Dirnames {
+ _ = helpers.SymbolicWalk(c.Fs.Source, staticDir, regularWalker)
+ }
+ }
+
+ for _, assetDir := range c.hugo.PathSpec.BaseFs.Assets.Dirnames {
+ _ = helpers.SymbolicWalk(c.Fs.Source, assetDir, regularWalker)
+ }
+
+ if len(nested) > 0 {
+ for {
+
+ toWalk := nested
+ nested = nested[:0]
+
+ for _, d := range toWalk {
+ _ = helpers.SymbolicWalk(c.Fs.Source, d, symLinkWalker)
+ }
+
+ if len(nested) == 0 {
+ break
+ }
+ }
+ }
+
+ a = helpers.UniqueStrings(a)
+ sort.Strings(a)
+
+ return a, nil
+}
+
+func (c *commandeer) buildSites() (err error) {
+ return c.hugo.Build(hugolib.BuildCfg{})
+}
+
+func (c *commandeer) handleBuildErr(err error, msg string) {
+ c.buildErr = err
+
+ c.logger.ERROR.Print(msg + ":\n\n")
+ c.logger.ERROR.Println(helpers.FirstUpper(err.Error()))
+ if !c.h.quiet && c.h.verbose {
+ herrors.PrintStackTrace(err)
+ }
+}
+
+func (c *commandeer) rebuildSites(events []fsnotify.Event) error {
+ defer c.timeTrack(time.Now(), "Total")
+
+ c.buildErr = nil
+ visited := c.visitedURLs.PeekAllSet()
+ if c.fastRenderMode {
+
+ // Make sure we always render the home pages
+ for _, l := range c.languages {
+ langPath := c.hugo.PathSpec.GetLangSubDir(l.Lang)
+ if langPath != "" {
+ langPath = langPath + "/"
+ }
+ home := c.hugo.PathSpec.PrependBasePath("/"+langPath, false)
+ visited[home] = true
+ }
+
+ }
+ return c.hugo.Build(hugolib.BuildCfg{RecentlyVisited: visited}, events...)
+}
+
+func (c *commandeer) partialReRender(urls ...string) error {
+ c.buildErr = nil
+ visited := make(map[string]bool)
+ for _, url := range urls {
+ visited[url] = true
+ }
+ return c.hugo.Build(hugolib.BuildCfg{RecentlyVisited: visited, PartialReRender: true})
+}
+
+func (c *commandeer) fullRebuild() {
+ c.commandeerHugoState = &commandeerHugoState{}
+ err := c.loadConfig(true, true)
+ if err != nil {
+ // Set the processing on pause until the state is recovered.
+ c.paused = true
+ c.handleBuildErr(err, "Failed to reload config")
+
+ } else {
+ c.paused = false
+ }
+
+ if !c.paused {
+ err := c.buildSites()
+ if err != nil {
+ c.logger.ERROR.Println(err)
+ } else if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") {
+ livereload.ForceRefresh()
+ }
+ }
+}
+
+// newWatcher creates a new watcher to watch filesystem events.
+func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
+ if runtime.GOOS == "darwin" {
+ tweakLimit()
+ }
+
+ staticSyncer, err := newStaticSyncer(c)
+ if err != nil {
+ return nil, err
+ }
+
+ watcher, err := watcher.New(1 * time.Second)
+
+ if err != nil {
+ return nil, err
+ }
+
+ for _, d := range dirList {
+ if d != "" {
+ _ = watcher.Add(d)
+ }
+ }
+
+ // Identifies changes to config (config.toml) files.
+ configSet := make(map[string]bool)
+
+ c.logger.FEEDBACK.Println("Watching for config changes in", strings.Join(c.configFiles, ", "))
+ for _, configFile := range c.configFiles {
+ watcher.Add(configFile)
+ configSet[configFile] = true
+ }
+
+ go func() {
+ for {
+ select {
+ case evs := <-watcher.Events:
+ c.handleEvents(watcher, staticSyncer, evs, configSet)
+ if c.showErrorInBrowser && c.errCount() > 0 {
+ // Need to reload browser to show the error
+ livereload.ForceRefresh()
+ }
+ case err := <-watcher.Errors:
+ if err != nil {
+ c.logger.ERROR.Println("Error while watching:", err)
+ }
+ }
+ }
+ }()
+
+ return watcher, nil
+}
+
+func (c *commandeer) handleEvents(watcher *watcher.Batcher,
+ staticSyncer *staticSyncer,
+ evs []fsnotify.Event,
+ configSet map[string]bool) {
+
+ for _, ev := range evs {
+ isConfig := configSet[ev.Name]
+ if !isConfig {
+ // It may be one of the /config folders
+ dirname := filepath.Dir(ev.Name)
+ if dirname != "." && configSet[dirname] {
+ isConfig = true
+ }
+
+ }
+
+ if isConfig {
+ if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
+ continue
+ }
+ if ev.Op&fsnotify.Remove == fsnotify.Remove || ev.Op&fsnotify.Rename == fsnotify.Rename {
+ for _, configFile := range c.configFiles {
+ counter := 0
+ for watcher.Add(configFile) != nil {
+ counter++
+ if counter >= 100 {
+ break
+ }
+ time.Sleep(100 * time.Millisecond)
+ }
+ }
+ }
+ // Config file(s) changed. Need full rebuild.
+ c.fullRebuild()
+ break
+ }
+ }
+
+ if c.paused {
+ // Wait for the server to get into a consistent state before
+ // we continue with processing.
+ return
+ }
+
+ if len(evs) > 50 {
+ // This is probably a mass edit of the content dir.
+ // Schedule a full rebuild for when it slows down.
+ c.debounce(c.fullRebuild)
+ return
+ }
+
+ c.logger.INFO.Println("Received System Events:", evs)
+
+ staticEvents := []fsnotify.Event{}
+ dynamicEvents := []fsnotify.Event{}
+
+ // Special handling for symbolic links inside /content.
+ filtered := []fsnotify.Event{}
+ for _, ev := range evs {
+ // Check the most specific first, i.e. files.
+ contentMapped := c.hugo.ContentChanges.GetSymbolicLinkMappings(ev.Name)
+ if len(contentMapped) > 0 {
+ for _, mapped := range contentMapped {
+ filtered = append(filtered, fsnotify.Event{Name: mapped, Op: ev.Op})
+ }
+ continue
+ }
+
+ // Check for any symbolic directory mapping.
+
+ dir, name := filepath.Split(ev.Name)
+
+ contentMapped = c.hugo.ContentChanges.GetSymbolicLinkMappings(dir)
+
+ if len(contentMapped) == 0 {
+ filtered = append(filtered, ev)
+ continue
+ }
+
+ for _, mapped := range contentMapped {
+ mappedFilename := filepath.Join(mapped, name)
+ filtered = append(filtered, fsnotify.Event{Name: mappedFilename, Op: ev.Op})
+ }
+ }
+
+ evs = filtered
+
+ 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
+ }
+ if c.hugo.Deps.SourceSpec.IgnoreFile(ev.Name) {
+ 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 !staticSyncer.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 staticSyncer.isStatic(ev.Name) {
+ staticEvents = append(staticEvents, ev)
+ } else {
+ dynamicEvents = append(dynamicEvents, ev)
+ }
+ }
+
+ if len(staticEvents) > 0 {
+ c.logger.FEEDBACK.Println("\nStatic file changes detected")
+ const layout = "2006-01-02 15:04:05.000 -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 {
+ c.logger.ERROR.Println("Error copying static files to publish dir:", err)
+ return
+ }
+ } else {
+ if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil {
+ c.logger.ERROR.Println("Error syncing static files to publish dir:", err)
+ return
+ }
+ }
+
+ if !c.h.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) == 1 {
+ ev := staticEvents[0]
+ path := c.hugo.BaseFs.SourceFilesystems.MakeStaticPathRelative(ev.Name)
+ path = c.firstPathSpec().RelURL(helpers.ToSlashTrimLeading(path), false)
+ livereload.RefreshPath(path)
+ } else {
+ livereload.ForceRefresh()
+ }
+ }
+ }
+
+ if len(dynamicEvents) > 0 {
+ partitionedEvents := partitionDynamicEvents(
+ c.firstPathSpec().BaseFs.SourceFilesystems,
+ dynamicEvents)
+
+ doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload")
+ onePageName := pickOneWriteOrCreatePath(partitionedEvents.ContentEvents)
+
+ c.logger.FEEDBACK.Println("\nChange detected, rebuilding site")
+ const layout = "2006-01-02 15:04:05.000 -0700"
+ c.logger.FEEDBACK.Println(time.Now().Format(layout))
+
+ c.changeDetector.PrepareNew()
+ if err := c.rebuildSites(dynamicEvents); err != nil {
+ c.handleBuildErr(err, "Rebuild failed")
+ }
+
+ if doLiveReload {
+ if len(partitionedEvents.ContentEvents) == 0 && len(partitionedEvents.AssetEvents) > 0 {
+ changed := c.changeDetector.changed()
+ if c.changeDetector != nil && len(changed) == 0 {
+ // Nothing has changed.
+ return
+ } else if len(changed) == 1 {
+ pathToRefresh := c.firstPathSpec().RelURL(helpers.ToSlashTrimLeading(changed[0]), false)
+ livereload.RefreshPath(pathToRefresh)
+ } else {
+ livereload.ForceRefresh()
+ }
+ }
+
+ if len(partitionedEvents.ContentEvents) > 0 {
+
+ navigate := c.Cfg.GetBool("navigateToChanged")
+ // We have fetched the same page above, but it may have
+ // changed.
+ var p page.Page
+
+ if navigate {
+ if onePageName != "" {
+ p = c.hugo.GetContentPage(onePageName)
+ }
+ }
+
+ if p != nil {
+ livereload.NavigateToPathForPort(p.RelPermalink(), p.Site().ServerPort())
+ } else {
+ livereload.ForceRefresh()
+ }
+ }
+ }
+ }
+}
+
+// dynamicEvents contains events that is considered dynamic, as in "not static".
+// Both of these categories will trigger a new build, but the asset events
+// does not fit into the "navigate to changed" logic.
+type dynamicEvents struct {
+ ContentEvents []fsnotify.Event
+ AssetEvents []fsnotify.Event
+}
+
+func partitionDynamicEvents(sourceFs *filesystems.SourceFilesystems, events []fsnotify.Event) (de dynamicEvents) {
+ for _, e := range events {
+ if sourceFs.IsAsset(e.Name) {
+ de.AssetEvents = append(de.AssetEvents, e)
+ } else {
+ de.ContentEvents = append(de.ContentEvents, e)
+ }
+ }
+ return
+
+}
+
+func pickOneWriteOrCreatePath(events []fsnotify.Event) string {
+ name := ""
+
+ // Some editors (for example notepad.exe on Windows) triggers a change
+ // both for directory and file. So we pick the longest path, which should
+ // be the file itself.
+ for _, ev := range events {
+ if (ev.Op&fsnotify.Write == fsnotify.Write || ev.Op&fsnotify.Create == fsnotify.Create) && len(ev.Name) > len(name) {
+ name = ev.Name
+ }
+ }
+
+ return name
+}
+
+// isThemeVsHugoVersionMismatch returns whether the current Hugo version is
+// less than any of the themes' min_version.
+func (c *commandeer) isThemeVsHugoVersionMismatch(fs afero.Fs) (dir string, mismatch bool, requiredMinVersion string) {
+ if !c.hugo.PathSpec.ThemeSet() {
+ return
+ }
+
+ for _, absThemeDir := range c.hugo.BaseFs.AbsThemeDirs {
+
+ path := filepath.Join(absThemeDir, "theme.toml")
+
+ exists, err := helpers.Exists(path, fs)
+
+ if err != nil || !exists {
+ continue
+ }
+
+ b, err := afero.ReadFile(fs, path)
+ if err != nil {
+ continue
+ }
+
+ tomlMeta, err := metadecoders.Default.UnmarshalToMap(b, metadecoders.TOML)
+ if err != nil {
+ continue
+ }
+
+ if minVersion, ok := tomlMeta["min_version"]; ok {
+ if hugo.CompareVersion(minVersion) > 0 {
+ return absThemeDir, true, fmt.Sprint(minVersion)
+ }
+ }
+
+ }
+
+ return
+}
diff --git a/commands/hugo_test.go b/commands/hugo_test.go
new file mode 100644
index 000000000..db6961b66
--- /dev/null
+++ b/commands/hugo_test.go
@@ -0,0 +1,52 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+// Issue #5662
+func TestHugoWithContentDirOverride(t *testing.T) {
+ assert := require.New(t)
+
+ hugoCmd := newCommandsBuilder().addAll().build()
+ cmd := hugoCmd.getCommand()
+
+ contentDir := "contentOverride"
+
+ cfgStr := `
+
+baseURL = "https://example.org"
+title = "Hugo Commands"
+
+contentDir = "thisdoesnotexist"
+
+`
+ dir, err := createSimpleTestSite(t, testSiteConfig{configTOML: cfgStr, contentDir: contentDir})
+ assert.NoError(err)
+
+ defer func() {
+ os.RemoveAll(dir)
+ }()
+
+ cmd.SetArgs([]string{"-s=" + dir, "-c=" + contentDir})
+
+ _, err = cmd.ExecuteC()
+ assert.NoError(err)
+
+}
diff --git a/commands/hugo_windows.go b/commands/hugo_windows.go
new file mode 100644
index 000000000..106c0cc71
--- /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 https://gohugo.io/ for more information.`
+}
diff --git a/commands/import_jekyll.go b/commands/import_jekyll.go
new file mode 100644
index 000000000..1d37cfd9d
--- /dev/null
+++ b/commands/import_jekyll.go
@@ -0,0 +1,651 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+ "unicode"
+
+ "github.com/gohugoio/hugo/parser/metadecoders"
+
+ "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"
+)
+
+var _ cmder = (*importCmd)(nil)
+
+type importCmd struct {
+ *baseCmd
+}
+
+func newImportCmd() *importCmd {
+ cc := &importCmd{}
+
+ cc.baseCmd = newBaseCmd(&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,
+ })
+
+ 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: cc.importFromJekyll,
+ }
+
+ importJekyllCmd.Flags().Bool("force", false, "allow import into non-empty target directory")
+
+ cc.cmd.AddCommand(importJekyllCmd)
+
+ return cc
+
+}
+
+func (i *importCmd) 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("abort: target path should not be inside the Jekyll root")
+ }
+
+ forceImport, _ := cmd.Flags().GetBool("force")
+
+ fs := afero.NewOsFs()
+ jekyllPostDirs, hasAnyPost := i.getJekyllDirInfo(fs, jekyllRoot)
+ if !hasAnyPost {
+ return errors.New("abort: jekyll root contains neither posts nor drafts")
+ }
+
+ site, err := i.createSiteFromJekyll(jekyllRoot, targetDir, jekyllPostDirs, forceImport)
+
+ if err != nil {
+ return newUserError(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.Contains(relPath, "_posts/"):
+ relPath = filepath.Join("content/post", strings.Replace(relPath, "_posts/", "", -1))
+ case strings.Contains(relPath, "_drafts/"):
+ relPath = filepath.Join("content/draft", strings.Replace(relPath, "_drafts/", "", -1))
+ draft = true
+ default:
+ return nil
+ }
+
+ fileCount++
+ return convertJekyllPost(site, path, relPath, targetDir, draft)
+ }
+
+ for jekyllPostDir, hasAnyPostInDir := range jekyllPostDirs {
+ if hasAnyPostInDir {
+ if err = helpers.SymbolicWalk(hugofs.Os, filepath.Join(jekyllRoot, jekyllPostDir), callback); 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
+}
+
+func (i *importCmd) getJekyllDirInfo(fs afero.Fs, jekyllRoot string) (map[string]bool, bool) {
+ postDirs := make(map[string]bool)
+ hasAnyPost := false
+ if entries, err := ioutil.ReadDir(jekyllRoot); err == nil {
+ for _, entry := range entries {
+ if entry.IsDir() {
+ subDir := filepath.Join(jekyllRoot, entry.Name())
+ if isPostDir, hasAnyPostInDir := i.retrieveJekyllPostDir(fs, subDir); isPostDir {
+ postDirs[entry.Name()] = hasAnyPostInDir
+ if hasAnyPostInDir {
+ hasAnyPost = true
+ }
+ }
+ }
+ }
+ }
+ return postDirs, hasAnyPost
+}
+
+func (i *importCmd) retrieveJekyllPostDir(fs afero.Fs, dir string) (bool, bool) {
+ if strings.HasSuffix(dir, "_posts") || strings.HasSuffix(dir, "_drafts") {
+ isEmpty, _ := helpers.IsEmpty(dir, fs)
+ return true, !isEmpty
+ }
+
+ if entries, err := ioutil.ReadDir(dir); err == nil {
+ for _, entry := range entries {
+ if entry.IsDir() {
+ subDir := filepath.Join(dir, entry.Name())
+ if isPostDir, hasAnyPost := i.retrieveJekyllPostDir(fs, subDir); isPostDir {
+ return isPostDir, hasAnyPost
+ }
+ }
+ }
+ }
+
+ return false, true
+}
+
+func (i *importCmd) createSiteFromJekyll(jekyllRoot, targetDir string, jekyllPostDirs map[string]bool, 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 + "\" exists but is not a directory")
+ }
+
+ isEmpty, _ := helpers.IsEmpty(targetDir, fs)
+
+ if !isEmpty && !force {
+ return nil, errors.New("target path \"" + targetDir + "\" exists and is not empty")
+ }
+ }
+
+ jekyllConfig := i.loadJekyllConfig(fs, jekyllRoot)
+
+ mkdir(targetDir, "layouts")
+ mkdir(targetDir, "content")
+ mkdir(targetDir, "archetypes")
+ mkdir(targetDir, "static")
+ mkdir(targetDir, "data")
+ mkdir(targetDir, "themes")
+
+ i.createConfigFromJekyll(fs, targetDir, "yaml", jekyllConfig)
+
+ i.copyJekyllFilesAndFolders(jekyllRoot, filepath.Join(targetDir, "static"), jekyllPostDirs)
+
+ return s, nil
+}
+
+func (i *importCmd) 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 := metadecoders.Default.UnmarshalToMap(b, metadecoders.YAML)
+
+ if err != nil {
+ return nil
+ }
+
+ return c
+}
+
+func (i *importCmd) createConfigFromJekyll(fs afero.Fs, inpath string, kind metadecoders.Format, 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,
+ }
+
+ var buf bytes.Buffer
+ err = parser.InterfaceToConfig(in, kind, &buf)
+ if err != nil {
+ return err
+ }
+
+ return helpers.WriteToDisk(filepath.Join(inpath, "config."+string(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, _ := 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 (i *importCmd) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyllPostDirs map[string]bool) (err 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)
+ if err != nil {
+ return err
+ }
+
+ 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] != '.' {
+ if _, ok := jekyllPostDirs[entry.Name()]; !ok {
+ 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
+ }
+
+ pf, err := parseContentFile(bytes.NewReader(contentBytes))
+ if err != nil {
+ jww.ERROR.Println("Parse file error:", path)
+ return err
+ }
+
+ newmetadata, err := convertJekyllMetaData(pf.frontMatter, postName, postDate, draft)
+ if err != nil {
+ jww.ERROR.Println("Convert metadata error:", path)
+ return err
+ }
+
+ content := convertJekyllContent(newmetadata, string(pf.content))
+
+ fs := hugofs.Os
+ if err := helpers.WriteToDisk(targetFile, strings.NewReader(content), fs); err != nil {
+ return fmt.Errorf("failed to save file %q: %s", filename, err)
+ }
+
+ return nil
+}
+
+func convertJekyllMetaData(m interface{}, postName string, postDate time.Time, draft bool) (interface{}, error) {
+ 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 {
+ metadata["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["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*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},
+ {regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`), replaceHighlightTag},
+ }
+
+ for _, replace := range replaceListFunc {
+ content = replace.re.ReplaceAllStringFunc(content, replace.replace)
+ }
+
+ return content
+}
+
+func replaceHighlightTag(match string) string {
+ r := regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`)
+ parts := r.FindStringSubmatch(match)
+ lastQuote := rune(0)
+ f := func(c rune) bool {
+ switch {
+ case c == lastQuote:
+ lastQuote = rune(0)
+ return false
+ case lastQuote != rune(0):
+ return false
+ case unicode.In(c, unicode.Quotation_Mark):
+ lastQuote = c
+ return false
+ default:
+ return unicode.IsSpace(c)
+ }
+ }
+ // splitting string by space but considering quoted section
+ items := strings.FieldsFunc(parts[1], f)
+
+ result := bytes.NewBufferString("{{< highlight ")
+ result.WriteString(items[0]) // language
+ options := items[1:]
+ for i, opt := range options {
+ opt = strings.Replace(opt, "\"", "", -1)
+ if opt == "linenos" {
+ opt = "linenos=table"
+ }
+ if i == 0 {
+ opt = " \"" + opt
+ }
+ if i < len(options)-1 {
+ opt += ","
+ } else if i == len(options)-1 {
+ opt += "\""
+ }
+ result.WriteString(opt)
+ }
+
+ result.WriteString(" >}}")
+ return result.String()
+}
+
+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..e0402a7a6
--- /dev/null
+++ b/commands/import_jekyll_test.go
@@ -0,0 +1,129 @@
+// 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"}`},
+ {map[interface{}]interface{}{}, "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), true,
+ `{"date":"2015-10-01T00:00:00Z","draft":true}`},
+ {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"}`},
+ {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"}`},
+ {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"}`},
+ }
+
+ 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 >}}"},
+ {map[interface{}]interface{}{},
+ "{% highlight go linenos hl_lines=\"1 2\" %}\nvar s string\nvar i int\n{% endhighlight %}",
+ "{{< highlight go \"linenos=table,hl_lines=1 2\" >}}\nvar s string\nvar i 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..6799f37b1
--- /dev/null
+++ b/commands/limit_darwin.go
@@ -0,0 +1,84 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+)
+
+var _ cmder = (*limitCmd)(nil)
+
+type limitCmd struct {
+ *baseCmd
+}
+
+func newLimitCmd() *limitCmd {
+ ccmd := &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)
+
+ if rLimit.Cur >= newRlimit {
+ return nil
+ }
+
+ jww.FEEDBACK.Println("Attempting to increase limit")
+ rLimit.Cur = newRlimit
+ 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
+ },
+ }
+
+ return &limitCmd{baseCmd: newBaseCmd(ccmd)}
+}
+
+const newRlimit = 10240
+
+func tweakLimit() {
+ var rLimit syscall.Rlimit
+ err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
+ if err != nil {
+ jww.WARN.Println("Unable to get rlimit:", err)
+ return
+ }
+ if rLimit.Cur < newRlimit {
+ rLimit.Cur = newRlimit
+ err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
+ if err != nil {
+ // This may not succeed, see https://github.com/golang/go/issues/30401
+ jww.INFO.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..8d3e6ad70
--- /dev/null
+++ b/commands/limit_others.go
@@ -0,0 +1,20 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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
+
+package commands
+
+func tweakLimit() {
+ // nothing to do
+}
diff --git a/commands/list.go b/commands/list.go
new file mode 100644
index 000000000..f233ce62c
--- /dev/null
+++ b/commands/list.go
@@ -0,0 +1,209 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/csv"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/gohugoio/hugo/hugolib"
+ "github.com/gohugoio/hugo/resources/resource"
+ "github.com/spf13/cobra"
+ jww "github.com/spf13/jwalterweatherman"
+)
+
+var _ cmder = (*listCmd)(nil)
+
+type listCmd struct {
+ hugoBuilderCommon
+ *baseCmd
+}
+
+func (lc *listCmd) buildSites(config map[string]interface{}) (*hugolib.HugoSites, error) {
+ cfgInit := func(c *commandeer) error {
+ for key, value := range config {
+ c.Set(key, value)
+ }
+ return nil
+ }
+
+ c, err := initializeConfig(true, false, &lc.hugoBuilderCommon, lc, cfgInit)
+ if err != nil {
+ return nil, err
+ }
+
+ sites, err := hugolib.NewHugoSites(*c.DepsCfg)
+
+ if err != nil {
+ return nil, newSystemError("Error creating sites", err)
+ }
+
+ if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
+ return nil, newSystemError("Error Processing Source Content", err)
+ }
+
+ return sites, nil
+}
+
+func newListCmd() *listCmd {
+ cc := &listCmd{}
+
+ cc.baseCmd = newBaseCmd(&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,
+ })
+
+ cc.cmd.AddCommand(
+ &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 {
+ sites, err := cc.buildSites(map[string]interface{}{"buildDrafts": true})
+
+ if err != nil {
+ return newSystemError("Error building sites", err)
+ }
+
+ for _, p := range sites.Pages() {
+ if p.Draft() {
+ jww.FEEDBACK.Println(strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)))
+ }
+ }
+
+ return nil
+ },
+ },
+ &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 {
+ sites, err := cc.buildSites(map[string]interface{}{"buildFuture": true})
+
+ if err != nil {
+ return newSystemError("Error building sites", err)
+ }
+
+ writer := csv.NewWriter(os.Stdout)
+ defer writer.Flush()
+
+ for _, p := range sites.Pages() {
+ if resource.IsFuture(p) {
+ err := writer.Write([]string{
+ strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)),
+ p.PublishDate().Format(time.RFC3339),
+ })
+ if err != nil {
+ return newSystemError("Error writing future posts to stdout", err)
+ }
+ }
+ }
+
+ return nil
+ },
+ },
+ &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 {
+ sites, err := cc.buildSites(map[string]interface{}{"buildExpired": true})
+
+ if err != nil {
+ return newSystemError("Error building sites", err)
+ }
+
+ writer := csv.NewWriter(os.Stdout)
+ defer writer.Flush()
+
+ for _, p := range sites.Pages() {
+ if resource.IsExpired(p) {
+ err := writer.Write([]string{
+ strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)),
+ p.ExpiryDate().Format(time.RFC3339),
+ })
+ if err != nil {
+ return newSystemError("Error writing expired posts to stdout", err)
+ }
+ }
+ }
+
+ return nil
+ },
+ },
+ &cobra.Command{
+ Use: "all",
+ Short: "List all posts",
+ Long: `List all of the posts in your content directory, include drafts, future and expired pages.`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ sites, err := cc.buildSites(map[string]interface{}{
+ "buildExpired": true,
+ "buildDrafts": true,
+ "buildFuture": true,
+ })
+
+ if err != nil {
+ return newSystemError("Error building sites", err)
+ }
+
+ writer := csv.NewWriter(os.Stdout)
+ defer writer.Flush()
+
+ writer.Write([]string{
+ "path",
+ "slug",
+ "title",
+ "date",
+ "expiryDate",
+ "publishDate",
+ "draft",
+ "permalink",
+ })
+ for _, p := range sites.Pages() {
+ if !p.IsPage() {
+ continue
+ }
+ err := writer.Write([]string{
+ strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)),
+ p.Slug(),
+ p.Title(),
+ p.Date().Format(time.RFC3339),
+ p.ExpiryDate().Format(time.RFC3339),
+ p.PublishDate().Format(time.RFC3339),
+ strconv.FormatBool(p.Draft()),
+ p.Permalink(),
+ })
+ if err != nil {
+ return newSystemError("Error writing posts to stdout", err)
+ }
+ }
+
+ return nil
+ },
+ },
+ )
+
+ cc.cmd.PersistentFlags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from")
+ cc.cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
+
+ return cc
+}
diff --git a/commands/list_test.go b/commands/list_test.go
new file mode 100644
index 000000000..f2ce70010
--- /dev/null
+++ b/commands/list_test.go
@@ -0,0 +1,70 @@
+package commands
+
+import (
+ "bytes"
+ "encoding/csv"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/spf13/cobra"
+ "github.com/stretchr/testify/require"
+)
+
+func captureStdout(f func() (*cobra.Command, error)) (string, error) {
+ old := os.Stdout
+ r, w, _ := os.Pipe()
+ os.Stdout = w
+
+ _, err := f()
+
+ if err != nil {
+ return "", err
+ }
+
+ w.Close()
+ os.Stdout = old
+
+ var buf bytes.Buffer
+ io.Copy(&buf, r)
+ return buf.String(), nil
+}
+
+func TestListAll(t *testing.T) {
+ assert := require.New(t)
+ dir, err := createSimpleTestSite(t, testSiteConfig{})
+
+ assert.NoError(err)
+
+ hugoCmd := newCommandsBuilder().addAll().build()
+ cmd := hugoCmd.getCommand()
+
+ defer func() {
+ os.RemoveAll(dir)
+ }()
+
+ cmd.SetArgs([]string{"-s=" + dir, "list", "all"})
+
+ out, err := captureStdout(cmd.ExecuteC)
+ assert.NoError(err)
+
+ r := csv.NewReader(strings.NewReader(out))
+
+ header, err := r.Read()
+ assert.NoError(err)
+
+ assert.Equal([]string{
+ "path", "slug", "title",
+ "date", "expiryDate", "publishDate",
+ "draft", "permalink",
+ }, header)
+
+ record, err := r.Read()
+ assert.Equal([]string{
+ filepath.Join("content", "p1.md"), "", "P1",
+ "0001-01-01T00:00:00Z", "0001-01-01T00:00:00Z", "0001-01-01T00:00:00Z",
+ "false", "https://example.org/p1/",
+ }, record)
+}
diff --git a/commands/new.go b/commands/new.go
new file mode 100644
index 000000000..f10369837
--- /dev/null
+++ b/commands/new.go
@@ -0,0 +1,139 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/gohugoio/hugo/create"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugolib"
+ "github.com/spf13/afero"
+ "github.com/spf13/cobra"
+ jww "github.com/spf13/jwalterweatherman"
+)
+
+var _ cmder = (*newCmd)(nil)
+
+type newCmd struct {
+ contentEditor string
+ contentType string
+
+ *baseBuilderCmd
+}
+
+func (b *commandsBuilder) newNewCmd() *newCmd {
+ cmd := &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.
+
+Ensure you run this within the root directory of your site.`,
+ }
+
+ cc := &newCmd{baseBuilderCmd: b.newBuilderCmd(cmd)}
+
+ cmd.Flags().StringVarP(&cc.contentType, "kind", "k", "", "content type to create")
+ cmd.PersistentFlags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from")
+ cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
+ cmd.Flags().StringVar(&cc.contentEditor, "editor", "", "edit new content with this editor, if provided")
+
+ cmd.AddCommand(newNewSiteCmd().getCommand())
+ cmd.AddCommand(newNewThemeCmd().getCommand())
+
+ cmd.RunE = cc.newContent
+
+ return cc
+}
+
+func (n *newCmd) newContent(cmd *cobra.Command, args []string) error {
+ cfgInit := func(c *commandeer) error {
+ if cmd.Flags().Changed("editor") {
+ c.Set("newContentEditor", n.contentEditor)
+ }
+ return nil
+ }
+
+ c, err := initializeConfig(true, false, &n.hugoBuilderCommon, n, cfgInit)
+
+ if err != nil {
+ return err
+ }
+
+ if len(args) < 1 {
+ return newUserError("path needs to be provided")
+ }
+
+ createPath := args[0]
+
+ var kind string
+
+ createPath, kind = newContentPathSection(c.hugo, createPath)
+
+ if n.contentType != "" {
+ kind = n.contentType
+ }
+
+ return create.NewContent(c.hugo, kind, createPath)
+}
+
+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 newContentPathSection(h *hugolib.HugoSites, path string) (string, string) {
+ // Forward slashes is used in all examples. Convert if needed.
+ // Issue #1133
+ createpath := filepath.FromSlash(path)
+
+ if h != nil {
+ for _, s := range h.Sites {
+ createpath = strings.TrimPrefix(createpath, s.PathSpec.ContentDir)
+ }
+ }
+
+ var section string
+ // assume the first directory is the section (kind)
+ if strings.Contains(createpath[1:], helpers.FilePathSeparator) {
+ parts := strings.Split(strings.TrimPrefix(createpath, helpers.FilePathSeparator), helpers.FilePathSeparator)
+ if len(parts) > 0 {
+ section = parts[0]
+ }
+
+ }
+
+ return createpath, section
+}
diff --git a/commands/new_content_test.go b/commands/new_content_test.go
new file mode 100644
index 000000000..5a55094d6
--- /dev/null
+++ b/commands/new_content_test.go
@@ -0,0 +1,128 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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(nil, "/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) {
+ n := newNewSiteCmd()
+ basepath := filepath.Join("base", "blog")
+ _, fs := newTestCfg()
+
+ require.NoError(t, n.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()
+ n := newNewSiteCmd()
+
+ require.NoError(t, fs.Source.MkdirAll(basepath, 0777))
+
+ require.NoError(t, n.doNewSite(fs, basepath, false))
+}
+
+func TestDoNewSite_error_base_exists(t *testing.T) {
+ basepath := filepath.Join("base", "blog")
+ _, fs := newTestCfg()
+ n := newNewSiteCmd()
+
+ require.NoError(t, fs.Source.MkdirAll(basepath, 0777))
+ _, 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, n.doNewSite(fs, basepath, false))
+
+}
+
+func TestDoNewSite_force_empty_dir(t *testing.T) {
+ basepath := filepath.Join("base", "blog")
+ _, fs := newTestCfg()
+ n := newNewSiteCmd()
+
+ require.NoError(t, fs.Source.MkdirAll(basepath, 0777))
+
+ require.NoError(t, n.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()
+ n := newNewSiteCmd()
+
+ contentPath := filepath.Join(basepath, "content")
+
+ require.NoError(t, fs.Source.MkdirAll(contentPath, 0777))
+ require.Error(t, n.doNewSite(fs, basepath, true))
+}
+
+func TestDoNewSite_error_force_config_inside_exists(t *testing.T) {
+ basepath := filepath.Join("base", "blog")
+ _, fs := newTestCfg()
+ n := newNewSiteCmd()
+
+ configPath := filepath.Join(basepath, "config.toml")
+ require.NoError(t, fs.Source.MkdirAll(basepath, 0777))
+ _, err := fs.Source.Create(configPath)
+ require.NoError(t, err)
+
+ require.Error(t, n.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/new_site.go b/commands/new_site.go
new file mode 100644
index 000000000..114ee82f6
--- /dev/null
+++ b/commands/new_site.go
@@ -0,0 +1,165 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "path/filepath"
+ "strings"
+
+ "github.com/gohugoio/hugo/parser/metadecoders"
+
+ _errors "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/create"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/parser"
+ "github.com/spf13/cobra"
+ jww "github.com/spf13/jwalterweatherman"
+ "github.com/spf13/viper"
+)
+
+var _ cmder = (*newSiteCmd)(nil)
+
+type newSiteCmd struct {
+ configFormat string
+
+ *baseCmd
+}
+
+func newNewSiteCmd() *newSiteCmd {
+ ccmd := &newSiteCmd{}
+
+ cmd := &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: ccmd.newSite,
+ }
+
+ cmd.Flags().StringVarP(&ccmd.configFormat, "format", "f", "toml", "config & frontmatter format")
+ cmd.Flags().Bool("force", false, "init inside non-empty directory")
+
+ ccmd.baseCmd = newBaseCmd(cmd)
+
+ return ccmd
+
+}
+
+func (n *newSiteCmd) 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."+n.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 _errors.Wrap(err, "Failed to create dir")
+ }
+ }
+
+ createConfig(fs, basepath, n.configFormat)
+
+ // Create a default 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
+}
+
+// newSite creates a new Hugo site and initializes a structured Hugo directory.
+func (n *newSiteCmd) 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 n.doNewSite(hugofs.NewDefault(viper.New()), createpath, forceNew)
+}
+
+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",
+ }
+
+ var buf bytes.Buffer
+ err = parser.InterfaceToConfig(in, metadecoders.FormatFromString(kind), &buf)
+ if err != nil {
+ return err
+ }
+
+ return helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), &buf, fs.Source)
+}
+
+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()
+}
diff --git a/commands/new_theme.go b/commands/new_theme.go
new file mode 100644
index 000000000..936f67e99
--- /dev/null
+++ b/commands/new_theme.go
@@ -0,0 +1,179 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/spf13/cobra"
+ jww "github.com/spf13/jwalterweatherman"
+)
+
+var _ cmder = (*newThemeCmd)(nil)
+
+type newThemeCmd struct {
+ *baseCmd
+ hugoBuilderCommon
+}
+
+func newNewThemeCmd() *newThemeCmd {
+ ccmd := &newThemeCmd{baseCmd: newBaseCmd(nil)}
+
+ cmd := &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: ccmd.newTheme,
+ }
+
+ ccmd.cmd = cmd
+
+ return ccmd
+}
+
+// newTheme creates a new Hugo theme template
+func (n *newThemeCmd) newTheme(cmd *cobra.Command, args []string) error {
+ c, err := initializeConfig(false, false, &n.hugoBuilderCommon, n, nil)
+
+ if err != nil {
+ return err
+ }
+
+ if len(args) < 1 {
+ return newUserError("theme name needs to be provided")
+ }
+
+ createpath := c.hugo.PathSpec.AbsPathify(filepath.Join(c.Cfg.GetString("themesDir"), args[0]))
+ jww.FEEDBACK.Println("Creating theme at", createpath)
+
+ cfg := c.DepsCfg
+
+ if x, _ := helpers.Exists(createpath, cfg.Fs.Source); x {
+ return errors.New(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")
+
+ baseofDefault := []byte(`<!DOCTYPE html>
+<html>
+ {{- partial "head.html" . -}}
+ <body>
+ {{- partial "header.html" . -}}
+ <div id="content">
+ {{- block "main" . }}{{- end }}
+ </div>
+ {{- partial "footer.html" . -}}
+ </body>
+</html>
+`)
+ err = helpers.WriteToDisk(filepath.Join(createpath, "layouts", "_default", "baseof.html"), bytes.NewReader(baseofDefault), cfg.Fs.Source)
+ if err != nil {
+ return err
+ }
+
+ touchFile(cfg.Fs.Source, createpath, "layouts", "partials", "head.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"), bytes.NewReader(by), cfg.Fs.Source)
+ if err != nil {
+ return err
+ }
+
+ n.createThemeMD(cfg.Fs, createpath)
+
+ return nil
+}
+
+func (n *newThemeCmd) 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"
+description = ""
+homepage = "http://example.com/"
+tags = []
+features = []
+min_version = "0.41"
+
+[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
+}
diff --git a/commands/release.go b/commands/release.go
new file mode 100644
index 000000000..4de165f35
--- /dev/null
+++ b/commands/release.go
@@ -0,0 +1,72 @@
+// +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 (
+ "errors"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/releaser"
+ "github.com/spf13/cobra"
+)
+
+var _ cmder = (*releaseCommandeer)(nil)
+
+type releaseCommandeer struct {
+ cmd *cobra.Command
+
+ version string
+
+ skipPublish bool
+ try bool
+}
+
+func createReleaser() cmder {
+ // 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().StringVarP(&r.version, "rel", "r", "", "new release version, i.e. 0.25.1")
+ r.cmd.PersistentFlags().BoolVarP(&r.skipPublish, "skip-publish", "", false, "skip all publishing pipes of the release")
+ r.cmd.PersistentFlags().BoolVarP(&r.try, "try", "", false, "simulate a release, i.e. no changes")
+
+ return r
+}
+
+func (c *releaseCommandeer) getCommand() *cobra.Command {
+ return c.cmd
+}
+
+func (c *releaseCommandeer) flagsToConfig(cfg config.Provider) {
+
+}
+
+func (r *releaseCommandeer) release() error {
+ if r.version == "" {
+ return errors.New("must set the --rel flag to the relevant version number")
+ }
+ return releaser.New(r.version, r.skipPublish, r.try).Run()
+}
diff --git a/commands/release_noop.go b/commands/release_noop.go
new file mode 100644
index 000000000..ccf34b68e
--- /dev/null
+++ b/commands/release_noop.go
@@ -0,0 +1,20 @@
+// +build !release
+
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 createReleaser() cmder {
+ return &nilCommand{}
+}
diff --git a/commands/server.go b/commands/server.go
new file mode 100644
index 000000000..5d50ebe2c
--- /dev/null
+++ b/commands/server.go
@@ -0,0 +1,552 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "fmt"
+ "net"
+ "net/http"
+ "net/url"
+ "os"
+ "os/signal"
+ "path/filepath"
+ "regexp"
+ "runtime"
+ "strconv"
+ "strings"
+ "sync"
+ "syscall"
+ "time"
+
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/livereload"
+ "github.com/gohugoio/hugo/tpl"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/spf13/afero"
+ "github.com/spf13/cobra"
+ jww "github.com/spf13/jwalterweatherman"
+)
+
+type serverCmd struct {
+ // Can be used to stop the server. Useful in tests
+ stop <-chan bool
+
+ disableLiveReload bool
+ navigateToChanged bool
+ renderToDisk bool
+ serverAppend bool
+ serverInterface string
+ serverPort int
+ liveReloadPort int
+ serverWatch bool
+ noHTTPCache bool
+
+ disableFastRender bool
+ disableBrowserError bool
+
+ *baseBuilderCmd
+}
+
+func (b *commandsBuilder) newServerCmd() *serverCmd {
+ return b.newServerCmdSignaled(nil)
+}
+
+func (b *commandsBuilder) newServerCmdSignaled(stop <-chan bool) *serverCmd {
+ cc := &serverCmd{stop: stop}
+
+ cc.baseBuilderCmd = b.newBuilderCmd(&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: cc.server,
+ })
+
+ cc.cmd.Flags().IntVarP(&cc.serverPort, "port", "p", 1313, "port on which the server will listen")
+ cc.cmd.Flags().IntVar(&cc.liveReloadPort, "liveReloadPort", -1, "port for live reloading (i.e. 443 in HTTPS proxy situations)")
+ cc.cmd.Flags().StringVarP(&cc.serverInterface, "bind", "", "127.0.0.1", "interface to which the server will bind")
+ cc.cmd.Flags().BoolVarP(&cc.serverWatch, "watch", "w", true, "watch filesystem for changes and recreate as needed")
+ cc.cmd.Flags().BoolVar(&cc.noHTTPCache, "noHTTPCache", false, "prevent HTTP caching")
+ cc.cmd.Flags().BoolVarP(&cc.serverAppend, "appendPort", "", true, "append port to baseURL")
+ cc.cmd.Flags().BoolVar(&cc.disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild")
+ cc.cmd.Flags().BoolVar(&cc.navigateToChanged, "navigateToChanged", false, "navigate to changed content file on live browser reload")
+ cc.cmd.Flags().BoolVar(&cc.renderToDisk, "renderToDisk", false, "render to Destination path (default is render to memory & serve from there)")
+ cc.cmd.Flags().BoolVar(&cc.disableFastRender, "disableFastRender", false, "enables full re-renders on changes")
+ cc.cmd.Flags().BoolVar(&cc.disableBrowserError, "disableBrowserError", false, "do not show build errors in the browser")
+
+ cc.cmd.Flags().String("memstats", "", "log memory usage to this file")
+ cc.cmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".")
+
+ return cc
+}
+
+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
+}
+
+var serverPorts []int
+
+func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
+ // If a Destination is provided via flag write to disk
+ destination, _ := cmd.Flags().GetString("destination")
+ if destination != "" {
+ sc.renderToDisk = true
+ }
+
+ var serverCfgInit sync.Once
+
+ cfgInit := func(c *commandeer) error {
+ c.Set("renderToMemory", !sc.renderToDisk)
+ if cmd.Flags().Changed("navigateToChanged") {
+ c.Set("navigateToChanged", sc.navigateToChanged)
+ }
+ if cmd.Flags().Changed("disableLiveReload") {
+ c.Set("disableLiveReload", sc.disableLiveReload)
+ }
+ if cmd.Flags().Changed("disableFastRender") {
+ c.Set("disableFastRender", sc.disableFastRender)
+ }
+ if cmd.Flags().Changed("disableBrowserError") {
+ c.Set("disableBrowserError", sc.disableBrowserError)
+ }
+ if sc.serverWatch {
+ c.Set("watch", true)
+ }
+
+ // TODO(bep) yes, we should fix.
+ if !c.languagesConfigured {
+ return nil
+ }
+
+ var err error
+
+ // We can only do this once.
+ serverCfgInit.Do(func() {
+ serverPorts = make([]int, 1)
+
+ if c.languages.IsMultihost() {
+ if !sc.serverAppend {
+ err = newSystemError("--appendPort=false not supported when in multihost mode")
+ }
+ serverPorts = make([]int, len(c.languages))
+ }
+
+ currentServerPort := sc.serverPort
+
+ for i := 0; i < len(serverPorts); i++ {
+ l, err := net.Listen("tcp", net.JoinHostPort(sc.serverInterface, strconv.Itoa(currentServerPort)))
+ if err == nil {
+ l.Close()
+ serverPorts[i] = currentServerPort
+ } else {
+ if i == 0 && sc.cmd.Flags().Changed("port") {
+ // port set explicitly by user -- he/she probably meant it!
+ err = newSystemErrorF("Server startup failed: %s", err)
+ }
+ c.logger.FEEDBACK.Println("port", sc.serverPort, "already in use, attempting to use an available port")
+ sp, err := helpers.FindAvailablePort()
+ if err != nil {
+ err = newSystemError("Unable to find alternative port to use:", err)
+ }
+ serverPorts[i] = sp.Port
+ }
+
+ currentServerPort = serverPorts[i] + 1
+ }
+ })
+
+ c.serverPorts = serverPorts
+
+ c.Set("port", sc.serverPort)
+ if sc.liveReloadPort != -1 {
+ c.Set("liveReloadPort", sc.liveReloadPort)
+ } else {
+ c.Set("liveReloadPort", serverPorts[0])
+ }
+
+ isMultiHost := c.languages.IsMultihost()
+ for i, language := range c.languages {
+ var serverPort int
+ if isMultiHost {
+ serverPort = serverPorts[i]
+ } else {
+ serverPort = serverPorts[0]
+ }
+
+ baseURL, err := sc.fixURL(language, sc.baseURL, serverPort)
+ if err != nil {
+ return nil
+ }
+ if isMultiHost {
+ language.Set("baseURL", baseURL)
+ }
+ if i == 0 {
+ c.Set("baseURL", baseURL)
+ }
+ }
+
+ return err
+
+ }
+
+ if err := memStats(); err != nil {
+ jww.WARN.Println("memstats error:", err)
+ }
+
+ c, err := initializeConfig(true, true, &sc.hugoBuilderCommon, sc, cfgInit)
+ if err != nil {
+ return err
+ }
+
+ if err := c.serverBuild(); err != nil {
+ return err
+ }
+
+ for _, s := range c.hugo.Sites {
+ s.RegisterMediaTypes()
+ }
+
+ // Watch runs its own server as part of the routine
+ if sc.serverWatch {
+
+ watchDirs, err := c.getDirList()
+ if err != nil {
+ return err
+ }
+
+ baseWatchDir := c.Cfg.GetString("workingDir")
+ relWatchDirs := make([]string, len(watchDirs))
+ for i, dir := range watchDirs {
+ relWatchDirs[i], _ = helpers.GetRelativePath(dir, baseWatchDir)
+ }
+
+ rootWatchDirs := strings.Join(helpers.UniqueStrings(helpers.ExtractRootPaths(relWatchDirs)), ",")
+
+ jww.FEEDBACK.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs)
+ watcher, err := c.newWatcher(watchDirs...)
+
+ if err != nil {
+ return err
+ }
+
+ defer watcher.Close()
+
+ }
+
+ return c.serve(sc)
+
+}
+
+type fileServer struct {
+ baseURLs []string
+ roots []string
+ errorTemplate tpl.Template
+ c *commandeer
+ s *serverCmd
+}
+
+func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, error) {
+ baseURL := f.baseURLs[i]
+ root := f.roots[i]
+ port := f.c.serverPorts[i]
+
+ publishDir := f.c.Cfg.GetString("publishDir")
+
+ if root != "" {
+ publishDir = filepath.Join(publishDir, root)
+ }
+
+ absPublishDir := f.c.hugo.PathSpec.AbsPathify(publishDir)
+
+ jww.FEEDBACK.Printf("Environment: %q", f.c.hugo.Deps.Site.Hugo().Environment)
+
+ if i == 0 {
+ if f.s.renderToDisk {
+ jww.FEEDBACK.Println("Serving pages from " + absPublishDir)
+ } else {
+ jww.FEEDBACK.Println("Serving pages from memory")
+ }
+ }
+
+ httpFs := afero.NewHttpFs(f.c.destinationFs)
+ fs := filesOnlyFs{httpFs.Dir(absPublishDir)}
+
+ if i == 0 && f.c.fastRenderMode {
+ jww.FEEDBACK.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender")
+ }
+
+ // We're only interested in the path
+ u, err := url.Parse(baseURL)
+ if err != nil {
+ return nil, "", "", errors.Wrap(err, "Invalid baseURL")
+ }
+
+ decorate := func(h http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if f.c.showErrorInBrowser {
+ // First check the error state
+ err := f.c.getErrorWithContext()
+ if err != nil {
+ w.WriteHeader(500)
+ var b bytes.Buffer
+ err := f.errorTemplate.Execute(&b, err)
+ if err != nil {
+ f.c.logger.ERROR.Println(err)
+ }
+ port = 1313
+ if !f.c.paused {
+ port = f.c.Cfg.GetInt("liveReloadPort")
+ }
+ fmt.Fprint(w, injectLiveReloadScript(&b, port))
+
+ return
+ }
+ }
+
+ if f.s.noHTTPCache {
+ w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
+ w.Header().Set("Pragma", "no-cache")
+ }
+
+ if f.c.fastRenderMode && f.c.buildErr == nil {
+ p := r.RequestURI
+ if strings.HasSuffix(p, "/") || strings.HasSuffix(p, "html") || strings.HasSuffix(p, "htm") {
+ if !f.c.visitedURLs.Contains(p) {
+ // If not already on stack, re-render that single page.
+ if err := f.c.partialReRender(p); err != nil {
+ f.c.handleBuildErr(err, fmt.Sprintf("Failed to render %q", p))
+ if f.c.showErrorInBrowser {
+ http.Redirect(w, r, p, http.StatusMovedPermanently)
+ return
+ }
+ }
+ }
+
+ f.c.visitedURLs.Add(p)
+
+ }
+ }
+ h.ServeHTTP(w, r)
+ })
+ }
+
+ fileserver := decorate(http.FileServer(fs))
+ mu := http.NewServeMux()
+
+ if u.Path == "" || u.Path == "/" {
+ mu.Handle("/", fileserver)
+ } else {
+ mu.Handle(u.Path, http.StripPrefix(u.Path, fileserver))
+ }
+
+ endpoint := net.JoinHostPort(f.s.serverInterface, strconv.Itoa(port))
+
+ return mu, u.String(), endpoint, nil
+}
+
+var logErrorRe = regexp.MustCompile(`(?s)ERROR \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `)
+
+func removeErrorPrefixFromLog(content string) string {
+ return logErrorRe.ReplaceAllLiteralString(content, "")
+}
+func (c *commandeer) serve(s *serverCmd) error {
+
+ isMultiHost := c.hugo.IsMultihost()
+
+ var (
+ baseURLs []string
+ roots []string
+ )
+
+ if isMultiHost {
+ for _, s := range c.hugo.Sites {
+ baseURLs = append(baseURLs, s.BaseURL.String())
+ roots = append(roots, s.Language().Lang)
+ }
+ } else {
+ s := c.hugo.Sites[0]
+ baseURLs = []string{s.BaseURL.String()}
+ roots = []string{""}
+ }
+
+ templ, err := c.hugo.TextTmpl.Parse("__default_server_error", buildErrorTemplate)
+ if err != nil {
+ return err
+ }
+
+ srv := &fileServer{
+ baseURLs: baseURLs,
+ roots: roots,
+ c: c,
+ s: s,
+ errorTemplate: templ,
+ }
+
+ doLiveReload := !c.Cfg.GetBool("disableLiveReload")
+
+ if doLiveReload {
+ livereload.Initialize()
+ }
+
+ var sigs = make(chan os.Signal, 1)
+ signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+
+ for i := range baseURLs {
+ mu, serverURL, endpoint, err := srv.createEndpoint(i)
+
+ if doLiveReload {
+ mu.HandleFunc("/livereload.js", livereload.ServeJS)
+ mu.HandleFunc("/livereload", livereload.Handler)
+ }
+ jww.FEEDBACK.Printf("Web Server is available at %s (bind address %s)\n", serverURL, s.serverInterface)
+ go func() {
+ err = http.ListenAndServe(endpoint, mu)
+ if err != nil {
+ c.logger.ERROR.Printf("Error: %s\n", err.Error())
+ os.Exit(1)
+ }
+ }()
+ }
+
+ jww.FEEDBACK.Println("Press Ctrl+C to stop")
+
+ if s.stop != nil {
+ select {
+ case <-sigs:
+ case <-s.stop:
+ }
+ } else {
+ <-sigs
+ }
+
+ return nil
+}
+
+// fixURL massages the baseURL into a form needed for serving
+// all pages correctly.
+func (sc *serverCmd) fixURL(cfg config.Provider, s string, port int) (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 sc.serverAppend {
+ if strings.Contains(u.Host, ":") {
+ u.Host, _, err = net.SplitHostPort(u.Host)
+ if err != nil {
+ return "", errors.Wrap(err, "Failed to split baseURL hostpost")
+ }
+ }
+ u.Host += fmt.Sprintf(":%d", port)
+ }
+
+ return u.String(), nil
+}
+
+func memStats() error {
+ b := newCommandsBuilder()
+ sc := b.newServerCmd().getCommand()
+ memstats := sc.Flags().Lookup("memstats").Value.String()
+ if memstats != "" {
+ interval, err := time.ParseDuration(sc.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_errors.go b/commands/server_errors.go
new file mode 100644
index 000000000..9f13c9d8c
--- /dev/null
+++ b/commands/server_errors.go
@@ -0,0 +1,95 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "io"
+
+ "github.com/gohugoio/hugo/transform"
+ "github.com/gohugoio/hugo/transform/livereloadinject"
+)
+
+var buildErrorTemplate = `<!doctype html>
+<html class="no-js" lang="">
+ <head>
+ <meta charset="utf-8">
+ <title>Hugo Server: Error</title>
+ <style type="text/css">
+ body {
+ font-family: "Muli",avenir, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+ font-size: 16px;
+ background-color: black;
+ color: rgba(255, 255, 255, 0.9);
+ }
+ main {
+ margin: auto;
+ width: 95%;
+ padding: 1rem;
+ }
+ .version {
+ color: #ccc;
+ padding: 1rem 0;
+ }
+ .stack {
+ margin-top: 6rem;
+ }
+ pre {
+ white-space: pre-wrap;
+ white-space: -moz-pre-wrap;
+ white-space: -pre-wrap;
+ white-space: -o-pre-wrap;
+ word-wrap: break-word;
+ }
+ .highlight {
+ overflow-x: auto;
+ padding: 0.75rem;
+ margin-bottom: 1rem;
+ background-color: #272822;
+ border: 1px solid black;
+ }
+ a {
+ color: #0594cb;
+ text-decoration: none;
+ }
+ a:hover {
+ color: #ccc;
+ }
+ </style>
+ </head>
+ <body>
+ <main>
+ {{ highlight .Error "apl" "noclasses=true,style=monokai" }}
+ {{ with .File }}
+ {{ $params := printf "noclasses=true,style=monokai,linenos=table,hl_lines=%d,linenostart=%d" (add .LinesPos 1) (sub .Position.LineNumber .LinesPos) }}
+ {{ $lexer := .ChromaLexer | default "go-html-template" }}
+ {{ highlight (delimit .Lines "\n") $lexer $params }}
+ {{ end }}
+ {{ with .StackTrace }}
+ {{ highlight . "apl" "noclasses=true,style=monokai" }}
+ {{ end }}
+ <p class="version">{{ .Version }}</p>
+ <a href="">Reload Page</a>
+ </main>
+</body>
+</html>
+`
+
+func injectLiveReloadScript(src io.Reader, port int) string {
+ var b bytes.Buffer
+ chain := transform.Chain{livereloadinject.New(port)}
+ chain.Apply(&b, src)
+
+ return b.String()
+}
diff --git a/commands/server_test.go b/commands/server_test.go
new file mode 100644
index 000000000..acee19cb8
--- /dev/null
+++ b/commands/server_test.go
@@ -0,0 +1,134 @@
+// 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 (
+ "fmt"
+ "net/http"
+ "os"
+ "runtime"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/gohugoio/hugo/helpers"
+
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/require"
+)
+
+func TestServer(t *testing.T) {
+ if isWindowsCI() {
+ // TODO(bep) not sure why server tests have started to fail on the Windows CI server.
+ t.Skip("Skip server test on appveyor")
+ }
+ assert := require.New(t)
+ dir, err := createSimpleTestSite(t, testSiteConfig{})
+ assert.NoError(err)
+
+ // Let us hope that this port is available on all systems ...
+ port := 1331
+
+ defer func() {
+ os.RemoveAll(dir)
+ }()
+
+ stop := make(chan bool)
+
+ b := newCommandsBuilder()
+ scmd := b.newServerCmdSignaled(stop)
+
+ cmd := scmd.getCommand()
+ cmd.SetArgs([]string{"-s=" + dir, fmt.Sprintf("-p=%d", port)})
+
+ go func() {
+ _, err = cmd.ExecuteC()
+ assert.NoError(err)
+ }()
+
+ // There is no way to know exactly when the server is ready for connections.
+ // We could improve by something like https://golang.org/pkg/net/http/httptest/#Server
+ // But for now, let us sleep and pray!
+ time.Sleep(2 * time.Second)
+
+ resp, err := http.Get("http://localhost:1331/")
+ assert.NoError(err)
+ defer resp.Body.Close()
+ homeContent := helpers.ReaderToString(resp.Body)
+
+ assert.Contains(homeContent, "List: Hugo Commands")
+ assert.Contains(homeContent, "Environment: development")
+
+ // Stop the server.
+ stop <- true
+
+}
+
+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 _, test := range tests {
+ t.Run(test.TestName, func(t *testing.T) {
+ b := newCommandsBuilder()
+ s := b.newServerCmd()
+ v := viper.New()
+ baseURL := test.CLIBaseURL
+ v.Set("baseURL", test.CfgBaseURL)
+ s.serverAppend = test.AppendPort
+ s.serverPort = test.Port
+ result, err := s.fixURL(v, baseURL, s.serverPort)
+ if err != nil {
+ t.Errorf("Unexpected error %s", err)
+ }
+ if result != test.Result {
+ t.Errorf("Expected %q, got %q", test.Result, result)
+ }
+ })
+ }
+}
+
+func TestRemoveErrorPrefixFromLog(t *testing.T) {
+ assert := require.New(t)
+ content := `ERROR 2018/10/07 13:11:12 Error while rendering "home": template: _default/baseof.html:4:3: executing "main" at <partial "logo" .>: error calling partial: template: partials/logo.html:5:84: executing "partials/logo.html" at <$resized.AHeight>: can't evaluate field AHeight in type *resource.Image
+ERROR 2018/10/07 13:11:12 Rebuild failed: logged 1 error(s)
+`
+
+ withoutError := removeErrorPrefixFromLog(content)
+
+ assert.False(strings.Contains(withoutError, "ERROR"), withoutError)
+
+}
+
+func isWindowsCI() bool {
+ return runtime.GOOS == "windows" && os.Getenv("CI") != ""
+}
diff --git a/commands/static_syncer.go b/commands/static_syncer.go
new file mode 100644
index 000000000..237453868
--- /dev/null
+++ b/commands/static_syncer.go
@@ -0,0 +1,130 @@
+// 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 (
+ "os"
+ "path/filepath"
+
+ "github.com/gohugoio/hugo/hugolib/filesystems"
+
+ "github.com/fsnotify/fsnotify"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/spf13/fsync"
+)
+
+type staticSyncer struct {
+ c *commandeer
+}
+
+func newStaticSyncer(c *commandeer) (*staticSyncer, error) {
+ return &staticSyncer{c: c}, nil
+}
+
+func (s *staticSyncer) isStatic(filename string) bool {
+ return s.c.hugo.BaseFs.SourceFilesystems.IsStatic(filename)
+}
+
+func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
+ c := s.c
+
+ syncFn := func(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
+ publishDir := c.hugo.PathSpec.PublishDir
+ // If root, remove the second '/'
+ if publishDir == "//" {
+ publishDir = helpers.FilePathSeparator
+ }
+
+ if sourceFs.PublishFolder != "" {
+ publishDir = filepath.Join(publishDir, sourceFs.PublishFolder)
+ }
+
+ syncer := fsync.NewSyncer()
+ syncer.NoTimes = c.Cfg.GetBool("noTimes")
+ syncer.NoChmod = c.Cfg.GetBool("noChmod")
+ syncer.SrcFs = sourceFs.Fs
+ 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
+
+ relPath := sourceFs.MakePathRelative(fromPath)
+ if relPath == "" {
+ // Not member of this virtual host.
+ 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 := sourceFs.Fs.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)
+ }
+ }
+
+ return 0, nil
+ }
+
+ _, err := c.doWithPublishDirs(syncFn)
+ return err
+
+}
diff --git a/commands/version.go b/commands/version.go
new file mode 100644
index 000000000..287950a2d
--- /dev/null
+++ b/commands/version.go
@@ -0,0 +1,44 @@
+// 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/gohugoio/hugo/common/hugo"
+ "github.com/spf13/cobra"
+ jww "github.com/spf13/jwalterweatherman"
+)
+
+var _ cmder = (*versionCmd)(nil)
+
+type versionCmd struct {
+ *baseCmd
+}
+
+func newVersionCmd() *versionCmd {
+ return &versionCmd{
+ newBaseCmd(&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() {
+ jww.FEEDBACK.Println(hugo.BuildVersionString())
+}
diff --git a/common/collections/append.go b/common/collections/append.go
new file mode 100644
index 000000000..ee15fef7d
--- /dev/null
+++ b/common/collections/append.go
@@ -0,0 +1,112 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+)
+
+// Append appends from to a slice to and returns the resulting slice.
+// If length of from is one and the only element is a slice of same type as to,
+// it will be appended.
+func Append(to interface{}, from ...interface{}) (interface{}, error) {
+ tov, toIsNil := indirect(reflect.ValueOf(to))
+
+ toIsNil = toIsNil || to == nil
+ var tot reflect.Type
+
+ if !toIsNil {
+ if tov.Kind() != reflect.Slice {
+ return nil, fmt.Errorf("expected a slice, got %T", to)
+ }
+
+ tot = tov.Type().Elem()
+ toIsNil = tov.Len() == 0
+
+ if len(from) == 1 {
+ fromv := reflect.ValueOf(from[0])
+ if fromv.Kind() == reflect.Slice {
+ if toIsNil {
+ // If we get nil []string, we just return the []string
+ return from[0], nil
+ }
+
+ fromt := reflect.TypeOf(from[0]).Elem()
+
+ // If we get []string []string, we append the from slice to to
+ if tot == fromt {
+ return reflect.AppendSlice(tov, fromv).Interface(), nil
+ } else if !fromt.AssignableTo(tot) {
+ // Fall back to a []interface{} slice.
+ return appendToInterfaceSliceFromValues(tov, fromv)
+
+ }
+ }
+ }
+ }
+
+ if toIsNil {
+ return Slice(from...), nil
+ }
+
+ for _, f := range from {
+ fv := reflect.ValueOf(f)
+ if !fv.Type().AssignableTo(tot) {
+ // Fall back to a []interface{} slice.
+ return appendToInterfaceSlice(tov, from...)
+ }
+ tov = reflect.Append(tov, fv)
+ }
+
+ return tov.Interface(), nil
+}
+
+func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]interface{}, error) {
+ var tos []interface{}
+
+ for _, slice := range []reflect.Value{slice1, slice2} {
+ for i := 0; i < slice.Len(); i++ {
+ tos = append(tos, slice.Index(i).Interface())
+ }
+ }
+
+ return tos, nil
+}
+
+func appendToInterfaceSlice(tov reflect.Value, from ...interface{}) ([]interface{}, error) {
+ var tos []interface{}
+
+ for i := 0; i < tov.Len(); i++ {
+ tos = append(tos, tov.Index(i).Interface())
+ }
+
+ tos = append(tos, from...)
+
+ return tos, nil
+}
+
+// indirect is borrowed from the Go stdlib: 'text/template/exec.go'
+// TODO(bep) consolidate
+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/common/collections/append_test.go b/common/collections/append_test.go
new file mode 100644
index 000000000..c08a69c0d
--- /dev/null
+++ b/common/collections/append_test.go
@@ -0,0 +1,78 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestAppend(t *testing.T) {
+ t.Parallel()
+
+ for i, test := range []struct {
+ start interface{}
+ addend []interface{}
+ expected interface{}
+ }{
+ {[]string{"a", "b"}, []interface{}{"c"}, []string{"a", "b", "c"}},
+ {[]string{"a", "b"}, []interface{}{"c", "d", "e"}, []string{"a", "b", "c", "d", "e"}},
+ {[]string{"a", "b"}, []interface{}{[]string{"c", "d", "e"}}, []string{"a", "b", "c", "d", "e"}},
+ {nil, []interface{}{"a", "b"}, []string{"a", "b"}},
+ {nil, []interface{}{nil}, []interface{}{nil}},
+ {[]interface{}{}, []interface{}{[]string{"c", "d", "e"}}, []string{"c", "d", "e"}},
+ {tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}},
+ []interface{}{&tstSlicer{"c"}},
+ tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}, &tstSlicer{"c"}}},
+ {&tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}},
+ []interface{}{&tstSlicer{"c"}},
+ tstSlicers{&tstSlicer{"a"},
+ &tstSlicer{"b"},
+ &tstSlicer{"c"}}},
+ {testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}},
+ []interface{}{&tstSlicerIn1{"c"}},
+ testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}, &tstSlicerIn1{"c"}}},
+ //https://github.com/gohugoio/hugo/issues/5361
+ {[]string{"a", "b"}, []interface{}{tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}},
+ []interface{}{"a", "b", &tstSlicer{"a"}, &tstSlicer{"b"}}},
+ {[]string{"a", "b"}, []interface{}{&tstSlicer{"a"}},
+ []interface{}{"a", "b", &tstSlicer{"a"}}},
+ // Errors
+ {"", []interface{}{[]string{"a", "b"}}, false},
+ // No string concatenation.
+ {"ab",
+ []interface{}{"c"},
+ false},
+ } {
+
+ errMsg := fmt.Sprintf("[%d]", i)
+
+ result, err := Append(test.start, test.addend...)
+
+ if b, ok := test.expected.(bool); ok && !b {
+ require.Error(t, err, errMsg)
+ continue
+ }
+
+ require.NoError(t, err, errMsg)
+
+ if !reflect.DeepEqual(test.expected, result) {
+ t.Fatalf("%s got\n%T: %v\nexpected\n%T: %v", errMsg, result, result, test.expected, test.expected)
+ }
+ }
+
+}
diff --git a/common/collections/collections.go b/common/collections/collections.go
new file mode 100644
index 000000000..bb47c8acc
--- /dev/null
+++ b/common/collections/collections.go
@@ -0,0 +1,21 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 contains common Hugo functionality related to collection
+// handling.
+package collections
+
+// Grouper defines a very generic way to group items by a given key.
+type Grouper interface {
+ Group(key interface{}, items interface{}) (interface{}, error)
+}
diff --git a/common/collections/slice.go b/common/collections/slice.go
new file mode 100644
index 000000000..38ca86b08
--- /dev/null
+++ b/common/collections/slice.go
@@ -0,0 +1,66 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+)
+
+// Slicer defines a very generic way to create a typed slice. This is used
+// in collections.Slice template func to get types such as Pages, PageGroups etc.
+// instead of the less useful []interface{}.
+type Slicer interface {
+ Slice(items interface{}) (interface{}, error)
+}
+
+// Slice returns a slice of all passed arguments.
+func Slice(args ...interface{}) interface{} {
+ if len(args) == 0 {
+ return args
+ }
+
+ first := args[0]
+ firstType := reflect.TypeOf(first)
+
+ if firstType == nil {
+ return args
+ }
+
+ if g, ok := first.(Slicer); ok {
+ v, err := g.Slice(args)
+ if err == nil {
+ return v
+ }
+
+ // If Slice fails, the items are not of the same type and
+ // []interface{} is the best we can do.
+ return args
+ }
+
+ if len(args) > 1 {
+ // This can be a mix of types.
+ for i := 1; i < len(args); i++ {
+ if firstType != reflect.TypeOf(args[i]) {
+ // []interface{} is the best we can do
+ return args
+ }
+ }
+ }
+
+ slice := reflect.MakeSlice(reflect.SliceOf(firstType), len(args), len(args))
+ for i, arg := range args {
+ slice.Index(i).Set(reflect.ValueOf(arg))
+ }
+ return slice.Interface()
+}
diff --git a/common/collections/slice_test.go b/common/collections/slice_test.go
new file mode 100644
index 000000000..fd8eb24f1
--- /dev/null
+++ b/common/collections/slice_test.go
@@ -0,0 +1,125 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "testing"
+
+ "github.com/alecthomas/assert"
+)
+
+var _ Slicer = (*tstSlicer)(nil)
+var _ Slicer = (*tstSlicerIn1)(nil)
+var _ Slicer = (*tstSlicerIn2)(nil)
+var _ testSlicerInterface = (*tstSlicerIn1)(nil)
+var _ testSlicerInterface = (*tstSlicerIn1)(nil)
+
+type testSlicerInterface interface {
+ Name() string
+}
+
+type testSlicerInterfaces []testSlicerInterface
+
+type tstSlicerIn1 struct {
+ name string
+}
+
+type tstSlicerIn2 struct {
+ name string
+}
+
+type tstSlicer struct {
+ name string
+}
+
+func (p *tstSlicerIn1) Slice(in interface{}) (interface{}, error) {
+ items := in.([]interface{})
+ result := make(testSlicerInterfaces, len(items))
+ for i, v := range items {
+ switch vv := v.(type) {
+ case testSlicerInterface:
+ result[i] = vv
+ default:
+ return nil, errors.New("invalid type")
+ }
+
+ }
+ return result, nil
+}
+
+func (p *tstSlicerIn2) Slice(in interface{}) (interface{}, error) {
+ items := in.([]interface{})
+ result := make(testSlicerInterfaces, len(items))
+ for i, v := range items {
+ switch vv := v.(type) {
+ case testSlicerInterface:
+ result[i] = vv
+ default:
+ return nil, errors.New("invalid type")
+ }
+ }
+ return result, nil
+}
+
+func (p *tstSlicerIn1) Name() string {
+ return p.name
+}
+
+func (p *tstSlicerIn2) Name() string {
+ return p.name
+}
+
+func (p *tstSlicer) Slice(in interface{}) (interface{}, error) {
+ items := in.([]interface{})
+ result := make(tstSlicers, len(items))
+ for i, v := range items {
+ switch vv := v.(type) {
+ case *tstSlicer:
+ result[i] = vv
+ default:
+ return nil, errors.New("invalid type")
+ }
+ }
+ return result, nil
+}
+
+type tstSlicers []*tstSlicer
+
+func TestSlice(t *testing.T) {
+ t.Parallel()
+
+ for i, test := range []struct {
+ args []interface{}
+ expected interface{}
+ }{
+ {[]interface{}{"a", "b"}, []string{"a", "b"}},
+ {[]interface{}{&tstSlicer{"a"}, &tstSlicer{"b"}}, tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}},
+ {[]interface{}{&tstSlicer{"a"}, "b"}, []interface{}{&tstSlicer{"a"}, "b"}},
+ {[]interface{}{}, []interface{}{}},
+ {[]interface{}{nil}, []interface{}{nil}},
+ {[]interface{}{5, "b"}, []interface{}{5, "b"}},
+ {[]interface{}{&tstSlicerIn1{"a"}, &tstSlicerIn2{"b"}}, testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn2{"b"}}},
+ {[]interface{}{&tstSlicerIn1{"a"}, &tstSlicer{"b"}}, []interface{}{&tstSlicerIn1{"a"}, &tstSlicer{"b"}}},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test.args)
+
+ result := Slice(test.args...)
+
+ assert.Equal(t, test.expected, result, errMsg)
+ }
+
+ assert.Len(t, Slice(), 0)
+}
diff --git a/common/herrors/error_locator.go b/common/herrors/error_locator.go
new file mode 100644
index 000000000..15de6d318
--- /dev/null
+++ b/common/herrors/error_locator.go
@@ -0,0 +1,255 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package errors contains common Hugo errors and error related utilities.
+package herrors
+
+import (
+ "io"
+ "io/ioutil"
+ "path/filepath"
+ "strings"
+
+ "github.com/gohugoio/hugo/common/text"
+
+ "github.com/spf13/afero"
+)
+
+// LineMatcher contains the elements used to match an error to a line
+type LineMatcher struct {
+ Position text.Position
+ Error error
+
+ LineNumber int
+ Offset int
+ Line string
+}
+
+// LineMatcherFn is used to match a line with an error.
+type LineMatcherFn func(m LineMatcher) bool
+
+// SimpleLineMatcher simply matches by line number.
+var SimpleLineMatcher = func(m LineMatcher) bool {
+ return m.Position.LineNumber == m.LineNumber
+}
+
+var _ text.Positioner = ErrorContext{}
+
+// ErrorContext contains contextual information about an error. This will
+// typically be the lines surrounding some problem in a file.
+type ErrorContext struct {
+
+ // If a match will contain the matched line and up to 2 lines before and after.
+ // Will be empty if no match.
+ Lines []string
+
+ // The position of the error in the Lines above. 0 based.
+ LinesPos int
+
+ position text.Position
+
+ // The lexer to use for syntax highlighting.
+ // https://gohugo.io/content-management/syntax-highlighting/#list-of-chroma-highlighting-languages
+ ChromaLexer string
+}
+
+// Position returns the text position of this error.
+func (e ErrorContext) Position() text.Position {
+ return e.position
+}
+
+var _ causer = (*ErrorWithFileContext)(nil)
+
+// ErrorWithFileContext is an error with some additional file context related
+// to that error.
+type ErrorWithFileContext struct {
+ cause error
+ ErrorContext
+}
+
+func (e *ErrorWithFileContext) Error() string {
+ pos := e.Position()
+ if pos.IsValid() {
+ return pos.String() + ": " + e.cause.Error()
+ }
+ return e.cause.Error()
+}
+
+func (e *ErrorWithFileContext) Cause() error {
+ return e.cause
+}
+
+// WithFileContextForFile will try to add a file context with lines matching the given matcher.
+// If no match could be found, the original error is returned with false as the second return value.
+func WithFileContextForFile(e error, realFilename, filename string, fs afero.Fs, matcher LineMatcherFn) (error, bool) {
+ f, err := fs.Open(filename)
+ if err != nil {
+ return e, false
+ }
+ defer f.Close()
+ return WithFileContext(e, realFilename, f, matcher)
+}
+
+// WithFileContextForFile will try to add a file context with lines matching the given matcher.
+// If no match could be found, the original error is returned with false as the second return value.
+func WithFileContext(e error, realFilename string, r io.Reader, matcher LineMatcherFn) (error, bool) {
+ if e == nil {
+ panic("error missing")
+ }
+ le := UnwrapFileError(e)
+
+ if le == nil {
+ var ok bool
+ if le, ok = ToFileError("", e).(FileError); !ok {
+ return e, false
+ }
+ }
+
+ var errCtx ErrorContext
+
+ posle := le.Position()
+
+ if posle.Offset != -1 {
+ errCtx = locateError(r, le, func(m LineMatcher) bool {
+ if posle.Offset >= m.Offset && posle.Offset < m.Offset+len(m.Line) {
+ lno := posle.LineNumber - m.Position.LineNumber + m.LineNumber
+ m.Position = text.Position{LineNumber: lno}
+ }
+ return matcher(m)
+ })
+ } else {
+ errCtx = locateError(r, le, matcher)
+ }
+
+ pos := &errCtx.position
+
+ if pos.LineNumber == -1 {
+ return e, false
+ }
+
+ pos.Filename = realFilename
+
+ if le.Type() != "" {
+ errCtx.ChromaLexer = chromaLexerFromType(le.Type())
+ } else {
+ errCtx.ChromaLexer = chromaLexerFromFilename(realFilename)
+ }
+
+ return &ErrorWithFileContext{cause: e, ErrorContext: errCtx}, true
+}
+
+// UnwrapErrorWithFileContext tries to unwrap an ErrorWithFileContext from err.
+// It returns nil if this is not possible.
+func UnwrapErrorWithFileContext(err error) *ErrorWithFileContext {
+ for err != nil {
+ switch v := err.(type) {
+ case *ErrorWithFileContext:
+ return v
+ case causer:
+ err = v.Cause()
+ default:
+ return nil
+ }
+ }
+ return nil
+}
+
+func chromaLexerFromType(fileType string) string {
+ switch fileType {
+ case "html", "htm":
+ return "go-html-template"
+ }
+ return fileType
+}
+
+func extNoDelimiter(filename string) string {
+ return strings.TrimPrefix(filepath.Ext(filename), ".")
+}
+
+func chromaLexerFromFilename(filename string) string {
+ if strings.Contains(filename, "layouts") {
+ return "go-html-template"
+ }
+
+ ext := extNoDelimiter(filename)
+ return chromaLexerFromType(ext)
+}
+
+func locateErrorInString(src string, matcher LineMatcherFn) ErrorContext {
+ return locateError(strings.NewReader(src), &fileError{}, matcher)
+}
+
+func locateError(r io.Reader, le FileError, matches LineMatcherFn) ErrorContext {
+ if le == nil {
+ panic("must provide an error")
+ }
+
+ errCtx := ErrorContext{position: text.Position{LineNumber: -1, ColumnNumber: 1, Offset: -1}, LinesPos: -1}
+
+ b, err := ioutil.ReadAll(r)
+ if err != nil {
+ return errCtx
+ }
+
+ pos := &errCtx.position
+ lepos := le.Position()
+
+ lines := strings.Split(string(b), "\n")
+
+ if le != nil && lepos.ColumnNumber >= 0 {
+ pos.ColumnNumber = lepos.ColumnNumber
+ }
+
+ lineNo := 0
+ posBytes := 0
+
+ for li, line := range lines {
+ lineNo = li + 1
+ m := LineMatcher{
+ Position: le.Position(),
+ Error: le,
+ LineNumber: lineNo,
+ Offset: posBytes,
+ Line: line,
+ }
+ if errCtx.LinesPos == -1 && matches(m) {
+ pos.LineNumber = lineNo
+ break
+ }
+
+ posBytes += len(line)
+ }
+
+ if pos.LineNumber != -1 {
+ low := pos.LineNumber - 3
+ if low < 0 {
+ low = 0
+ }
+
+ if pos.LineNumber > 2 {
+ errCtx.LinesPos = 2
+ } else {
+ errCtx.LinesPos = pos.LineNumber - 1
+ }
+
+ high := pos.LineNumber + 2
+ if high > len(lines) {
+ high = len(lines)
+ }
+
+ errCtx.Lines = lines[low:high]
+
+ }
+
+ return errCtx
+}
diff --git a/common/herrors/error_locator_test.go b/common/herrors/error_locator_test.go
new file mode 100644
index 000000000..2d007016d
--- /dev/null
+++ b/common/herrors/error_locator_test.go
@@ -0,0 +1,129 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package errors contains common Hugo errors and error related utilities.
+package herrors
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestErrorLocator(t *testing.T) {
+ assert := require.New(t)
+
+ lineMatcher := func(m LineMatcher) bool {
+ return strings.Contains(m.Line, "THEONE")
+ }
+
+ lines := `LINE 1
+LINE 2
+LINE 3
+LINE 4
+This is THEONE
+LINE 6
+LINE 7
+LINE 8
+`
+
+ location := locateErrorInString(lines, lineMatcher)
+ assert.Equal([]string{"LINE 3", "LINE 4", "This is THEONE", "LINE 6", "LINE 7"}, location.Lines)
+
+ pos := location.Position()
+ assert.Equal(5, pos.LineNumber)
+ assert.Equal(2, location.LinesPos)
+
+ assert.Equal([]string{"This is THEONE"}, locateErrorInString(`This is THEONE`, lineMatcher).Lines)
+
+ location = locateErrorInString(`L1
+This is THEONE
+L2
+`, lineMatcher)
+ assert.Equal(2, location.Position().LineNumber)
+ assert.Equal(1, location.LinesPos)
+ assert.Equal([]string{"L1", "This is THEONE", "L2", ""}, location.Lines)
+
+ location = locateErrorInString(`This is THEONE
+L2
+`, lineMatcher)
+ assert.Equal(0, location.LinesPos)
+ assert.Equal([]string{"This is THEONE", "L2", ""}, location.Lines)
+
+ location = locateErrorInString(`L1
+This THEONE
+`, lineMatcher)
+ assert.Equal([]string{"L1", "This THEONE", ""}, location.Lines)
+ assert.Equal(1, location.LinesPos)
+
+ location = locateErrorInString(`L1
+L2
+This THEONE
+`, lineMatcher)
+ assert.Equal([]string{"L1", "L2", "This THEONE", ""}, location.Lines)
+ assert.Equal(2, location.LinesPos)
+
+ location = locateErrorInString("NO MATCH", lineMatcher)
+ assert.Equal(-1, location.Position().LineNumber)
+ assert.Equal(-1, location.LinesPos)
+ assert.Equal(0, len(location.Lines))
+
+ lineMatcher = func(m LineMatcher) bool {
+ return m.LineNumber == 6
+ }
+
+ location = locateErrorInString(`A
+B
+C
+D
+E
+F
+G
+H
+I
+J`, lineMatcher)
+
+ assert.Equal([]string{"D", "E", "F", "G", "H"}, location.Lines)
+ assert.Equal(6, location.Position().LineNumber)
+ assert.Equal(2, location.LinesPos)
+
+ // Test match EOF
+ lineMatcher = func(m LineMatcher) bool {
+ return m.LineNumber == 4
+ }
+
+ location = locateErrorInString(`A
+B
+C
+`, lineMatcher)
+
+ assert.Equal([]string{"B", "C", ""}, location.Lines)
+ assert.Equal(4, location.Position().LineNumber)
+ assert.Equal(2, location.LinesPos)
+
+ offsetMatcher := func(m LineMatcher) bool {
+ return m.Offset == 1
+ }
+
+ location = locateErrorInString(`A
+B
+C
+D
+E`, offsetMatcher)
+
+ assert.Equal([]string{"A", "B", "C", "D"}, location.Lines)
+ assert.Equal(2, location.Position().LineNumber)
+ assert.Equal(1, location.LinesPos)
+
+}
diff --git a/common/herrors/errors.go b/common/herrors/errors.go
new file mode 100644
index 000000000..be98ceb39
--- /dev/null
+++ b/common/herrors/errors.go
@@ -0,0 +1,53 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package herrors contains common Hugo errors and error related utilities.
+package herrors
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "os"
+
+ _errors "github.com/pkg/errors"
+)
+
+// As defined in https://godoc.org/github.com/pkg/errors
+type causer interface {
+ Cause() error
+}
+
+type stackTracer interface {
+ StackTrace() _errors.StackTrace
+}
+
+// PrintStackTrace prints the error's stack trace to stdoud.
+func PrintStackTrace(err error) {
+ FprintStackTrace(os.Stdout, err)
+}
+
+// FprintStackTrace prints the error's stack trace to w.
+func FprintStackTrace(w io.Writer, err error) {
+ if err, ok := err.(stackTracer); ok {
+ for _, f := range err.StackTrace() {
+ fmt.Fprintf(w, "%+s:%d\n", f, f)
+ }
+ }
+}
+
+// ErrFeatureNotAvailable denotes that a feature is unavailable.
+//
+// We will, at least to begin with, make some Hugo features (SCSS with libsass) optional,
+// and this error is used to signal those situations.
+var ErrFeatureNotAvailable = errors.New("this feature is not available in your current Hugo version, see https://goo.gl/YMrWcn for more information")
diff --git a/common/herrors/file_error.go b/common/herrors/file_error.go
new file mode 100644
index 000000000..5af84adf5
--- /dev/null
+++ b/common/herrors/file_error.go
@@ -0,0 +1,133 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitatio ns under the License.
+
+package herrors
+
+import (
+ "encoding/json"
+
+ "github.com/gohugoio/hugo/common/text"
+
+ "github.com/pkg/errors"
+)
+
+var (
+ _ causer = (*fileError)(nil)
+)
+
+// FileError represents an error when handling a file: Parsing a config file,
+// execute a template etc.
+type FileError interface {
+ error
+
+ text.Positioner
+
+ // A string identifying the type of file, e.g. JSON, TOML, markdown etc.
+ Type() string
+}
+
+var _ FileError = (*fileError)(nil)
+
+type fileError struct {
+ position text.Position
+
+ fileType string
+
+ cause error
+}
+
+// Position returns the text position of this error.
+func (e fileError) Position() text.Position {
+ return e.position
+}
+
+func (e *fileError) Type() string {
+ return e.fileType
+}
+
+func (e *fileError) Error() string {
+ if e.cause == nil {
+ return ""
+ }
+ return e.cause.Error()
+}
+
+func (f *fileError) Cause() error {
+ return f.cause
+}
+
+// NewFileError creates a new FileError.
+func NewFileError(fileType string, offset, lineNumber, columnNumber int, err error) FileError {
+ pos := text.Position{Offset: offset, LineNumber: lineNumber, ColumnNumber: columnNumber}
+ return &fileError{cause: err, fileType: fileType, position: pos}
+}
+
+// UnwrapFileError tries to unwrap a FileError from err.
+// It returns nil if this is not possible.
+func UnwrapFileError(err error) FileError {
+ for err != nil {
+ switch v := err.(type) {
+ case FileError:
+ return v
+ case causer:
+ err = v.Cause()
+ default:
+ return nil
+ }
+ }
+ return nil
+}
+
+// ToFileErrorWithOffset will return a new FileError with a line number
+// with the given offset from the original.
+func ToFileErrorWithOffset(fe FileError, offset int) FileError {
+ pos := fe.Position()
+ return ToFileErrorWithLineNumber(fe, pos.LineNumber+offset)
+}
+
+// ToFileErrorWithOffset will return a new FileError with the given line number.
+func ToFileErrorWithLineNumber(fe FileError, lineNumber int) FileError {
+ pos := fe.Position()
+ pos.LineNumber = lineNumber
+ return &fileError{cause: fe, fileType: fe.Type(), position: pos}
+}
+
+// ToFileError will convert the given error to an error supporting
+// the FileError interface.
+func ToFileError(fileType string, err error) FileError {
+ for _, handle := range lineNumberExtractors {
+ lno, col := handle(err)
+ offset, typ := extractOffsetAndType(err)
+ if fileType == "" {
+ fileType = typ
+ }
+
+ if lno > 0 || offset != -1 {
+ return NewFileError(fileType, offset, lno, col, err)
+ }
+ }
+ // Fall back to the pointing to line number 1.
+ return NewFileError(fileType, -1, 1, 1, err)
+}
+
+func extractOffsetAndType(e error) (int, string) {
+ e = errors.Cause(e)
+ switch v := e.(type) {
+ case *json.UnmarshalTypeError:
+ return int(v.Offset), "json"
+ case *json.SyntaxError:
+ return int(v.Offset), "json"
+ default:
+ return -1, ""
+ }
+}
diff --git a/common/herrors/file_error_test.go b/common/herrors/file_error_test.go
new file mode 100644
index 000000000..4108983d3
--- /dev/null
+++ b/common/herrors/file_error_test.go
@@ -0,0 +1,57 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package herrors
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/pkg/errors"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestToLineNumberError(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ for i, test := range []struct {
+ in error
+ offset int
+ lineNumber int
+ columnNumber int
+ }{
+ {errors.New("no line number for you"), 0, 1, 1},
+ {errors.New(`template: _default/single.html:4:15: executing "_default/single.html" at <.Titles>: can't evaluate field Titles in type *hugolib.PageOutput`), 0, 4, 15},
+ {errors.New("parse failed: template: _default/bundle-resource-meta.html:11: unexpected in operand"), 0, 11, 1},
+ {errors.New(`failed:: template: _default/bundle-resource-meta.html:2:7: executing "main" at <.Titles>`), 0, 2, 7},
+ {errors.New("error in front matter: Near line 32 (last key parsed 'title')"), 0, 32, 1},
+ {errors.New(`failed to load translations: (6, 7): was expecting token =, but got "g" instead`), 0, 6, 7},
+ } {
+
+ got := ToFileError("template", test.in)
+
+ errMsg := fmt.Sprintf("[%d][%T]", i, got)
+ le, ok := got.(FileError)
+ assert.True(ok)
+
+ assert.True(ok, errMsg)
+ pos := le.Position()
+ assert.Equal(test.lineNumber, pos.LineNumber, errMsg)
+ assert.Equal(test.columnNumber, pos.ColumnNumber, errMsg)
+ assert.Error(errors.Cause(got))
+ }
+
+}
diff --git a/common/herrors/line_number_extractors.go b/common/herrors/line_number_extractors.go
new file mode 100644
index 000000000..93969b967
--- /dev/null
+++ b/common/herrors/line_number_extractors.go
@@ -0,0 +1,66 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitatio ns under the License.
+
+package herrors
+
+import (
+ "regexp"
+ "strconv"
+)
+
+var lineNumberExtractors = []lineNumberExtractor{
+ // Template/shortcode parse errors
+ newLineNumberErrHandlerFromRegexp(".*:(\\d+):(\\d*):"),
+ newLineNumberErrHandlerFromRegexp(".*:(\\d+):"),
+
+ // TOML parse errors
+ newLineNumberErrHandlerFromRegexp(".*Near line (\\d+)(\\s.*)"),
+
+ // YAML parse errors
+ newLineNumberErrHandlerFromRegexp("line (\\d+):"),
+
+ // i18n bundle errors
+ newLineNumberErrHandlerFromRegexp("\\((\\d+),\\s(\\d*)"),
+}
+
+type lineNumberExtractor func(e error) (int, int)
+
+func newLineNumberErrHandlerFromRegexp(expression string) lineNumberExtractor {
+ re := regexp.MustCompile(expression)
+ return extractLineNo(re)
+}
+
+func extractLineNo(re *regexp.Regexp) lineNumberExtractor {
+ return func(e error) (int, int) {
+ if e == nil {
+ panic("no error")
+ }
+ col := 1
+ s := e.Error()
+ m := re.FindStringSubmatch(s)
+ if len(m) >= 2 {
+ lno, _ := strconv.Atoi(m[1])
+ if len(m) > 2 {
+ col, _ = strconv.Atoi(m[2])
+ }
+
+ if col <= 0 {
+ col = 1
+ }
+
+ return lno, col
+ }
+
+ return -1, col
+ }
+}
diff --git a/common/hreflect/helpers.go b/common/hreflect/helpers.go
new file mode 100644
index 000000000..db7b208b5
--- /dev/null
+++ b/common/hreflect/helpers.go
@@ -0,0 +1,91 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+// Some functions in this file (see comments) is based on the Go source code,
+// copyright The Go Authors and governed by a BSD-style license.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package hreflect contains reflect helpers.
+package hreflect
+
+import (
+ "reflect"
+
+ "github.com/gohugoio/hugo/common/types"
+)
+
+// IsTruthful returns whether in represents a truthful value.
+// See IsTruthfulValue
+func IsTruthful(in interface{}) bool {
+ switch v := in.(type) {
+ case reflect.Value:
+ return IsTruthfulValue(v)
+ default:
+ return IsTruthfulValue(reflect.ValueOf(in))
+ }
+
+}
+
+var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem()
+
+// IsTruthfulValue returns whether the given value has a meaningful truth value.
+// This is based on template.IsTrue in Go's stdlib, but also considers
+// IsZero and any interface value will be unwrapped before it's considered
+// for truthfulness.
+//
+// Based on:
+// https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L306
+func IsTruthfulValue(val reflect.Value) (truth bool) {
+ val = indirectInterface(val)
+
+ if !val.IsValid() {
+ // Something like var x interface{}, never set. It's a form of nil.
+ return
+ }
+
+ if val.Type().Implements(zeroType) {
+ return !val.Interface().(types.Zeroer).IsZero()
+ }
+
+ switch val.Kind() {
+ case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
+ truth = val.Len() > 0
+ case reflect.Bool:
+ truth = val.Bool()
+ case reflect.Complex64, reflect.Complex128:
+ truth = val.Complex() != 0
+ case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface:
+ truth = !val.IsNil()
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ truth = val.Int() != 0
+ case reflect.Float32, reflect.Float64:
+ truth = val.Float() != 0
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ truth = val.Uint() != 0
+ case reflect.Struct:
+ truth = true // Struct values are always true.
+ default:
+ return
+ }
+
+ return
+}
+
+// Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931
+func indirectInterface(v reflect.Value) reflect.Value {
+ if v.Kind() != reflect.Interface {
+ return v
+ }
+ if v.IsNil() {
+ return reflect.Value{}
+ }
+ return v.Elem()
+}
diff --git a/common/hreflect/helpers_test.go b/common/hreflect/helpers_test.go
new file mode 100644
index 000000000..3c9179394
--- /dev/null
+++ b/common/hreflect/helpers_test.go
@@ -0,0 +1,42 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hreflect
+
+import (
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestIsTruthful(t *testing.T) {
+ assert := require.New(t)
+
+ assert.True(IsTruthful(true))
+ assert.False(IsTruthful(false))
+ assert.True(IsTruthful(time.Now()))
+ assert.False(IsTruthful(time.Time{}))
+}
+
+func BenchmarkIsTruthFul(b *testing.B) {
+ v := reflect.ValueOf("Hugo")
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ if !IsTruthfulValue(v) {
+ b.Fatal("not truthful")
+ }
+ }
+}
diff --git a/common/hugio/readers.go b/common/hugio/readers.go
new file mode 100644
index 000000000..8c901dd24
--- /dev/null
+++ b/common/hugio/readers.go
@@ -0,0 +1,54 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hugio
+
+import (
+ "io"
+ "strings"
+)
+
+// ReadSeeker wraps io.Reader and io.Seeker.
+type ReadSeeker interface {
+ io.Reader
+ io.Seeker
+}
+
+// ReadSeekCloser is implemented by afero.File. We use this as the common type for
+// content in Resource objects, even for strings.
+type ReadSeekCloser interface {
+ ReadSeeker
+ io.Closer
+}
+
+// ReadSeekerNoOpCloser implements ReadSeekCloser by doing nothing in Close.
+// TODO(bep) rename this and simila to ReadSeekerNopCloser, naming used in stdlib, which kind of makes sense.
+type ReadSeekerNoOpCloser struct {
+ ReadSeeker
+}
+
+// Close does nothing.
+func (r ReadSeekerNoOpCloser) Close() error {
+ return nil
+}
+
+// NewReadSeekerNoOpCloser creates a new ReadSeekerNoOpCloser with the given ReadSeeker.
+func NewReadSeekerNoOpCloser(r ReadSeeker) ReadSeekerNoOpCloser {
+ return ReadSeekerNoOpCloser{r}
+}
+
+// NewReadSeekerNoOpCloserFromString uses strings.NewReader to create a new ReadSeekerNoOpCloser
+// from the given string.
+func NewReadSeekerNoOpCloserFromString(content string) ReadSeekerNoOpCloser {
+ return ReadSeekerNoOpCloser{strings.NewReader(content)}
+}
diff --git a/common/hugio/writers.go b/common/hugio/writers.go
new file mode 100644
index 000000000..82c4dca52
--- /dev/null
+++ b/common/hugio/writers.go
@@ -0,0 +1,76 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hugio
+
+import (
+ "io"
+ "io/ioutil"
+)
+
+type multiWriteCloser struct {
+ io.Writer
+ closers []io.WriteCloser
+}
+
+func (m multiWriteCloser) Close() error {
+ var err error
+ for _, c := range m.closers {
+ if closeErr := c.Close(); err != nil {
+ err = closeErr
+ }
+ }
+ return err
+}
+
+// NewMultiWriteCloser creates a new io.WriteCloser that duplicates its writes to all the
+// provided writers.
+func NewMultiWriteCloser(writeClosers ...io.WriteCloser) io.WriteCloser {
+ writers := make([]io.Writer, len(writeClosers))
+ for i, w := range writeClosers {
+ writers[i] = w
+ }
+ return multiWriteCloser{Writer: io.MultiWriter(writers...), closers: writeClosers}
+}
+
+// ToWriteCloser creates an io.WriteCloser from the given io.Writer.
+// If it's not already, one will be created with a Close method that does nothing.
+func ToWriteCloser(w io.Writer) io.WriteCloser {
+ if rw, ok := w.(io.WriteCloser); ok {
+ return rw
+ }
+
+ return struct {
+ io.Writer
+ io.Closer
+ }{
+ w,
+ ioutil.NopCloser(nil),
+ }
+}
+
+// ToReadCloser creates an io.ReadCloser from the given io.Reader.
+// If it's not already, one will be created with a Close method that does nothing.
+func ToReadCloser(r io.Reader) io.ReadCloser {
+ if rc, ok := r.(io.ReadCloser); ok {
+ return rc
+ }
+
+ return struct {
+ io.Reader
+ io.Closer
+ }{
+ r,
+ ioutil.NopCloser(nil),
+ }
+}
diff --git a/common/hugo/hugo.go b/common/hugo/hugo.go
new file mode 100644
index 000000000..62d923bf0
--- /dev/null
+++ b/common/hugo/hugo.go
@@ -0,0 +1,67 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hugo
+
+import (
+ "fmt"
+ "html/template"
+)
+
+const (
+ EnvironmentDevelopment = "development"
+ EnvironmentProduction = "production"
+)
+
+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
+)
+
+// Info contains information about the current Hugo environment
+type Info struct {
+ CommitHash string
+ BuildDate string
+
+ // The build environment.
+ // Defaults are "production" (hugo) and "development" (hugo server).
+ // This can also be set by the user.
+ // It can be any string, but it will be all lower case.
+ Environment string
+}
+
+// Version returns the current version as a comparable version string.
+func (i Info) Version() VersionString {
+ return CurrentVersion.Version()
+}
+
+// Generator a Hugo meta generator HTML tag.
+func (i Info) Generator() template.HTML {
+ return template.HTML(fmt.Sprintf(`<meta name="generator" content="Hugo %s" />`, CurrentVersion.String()))
+}
+
+// NewInfo creates a new Hugo Info object.
+func NewInfo(environment string) Info {
+ if environment == "" {
+ environment = EnvironmentProduction
+ }
+ return Info{
+ CommitHash: commitHash,
+ BuildDate: buildDate,
+ Environment: environment,
+ }
+}
diff --git a/common/hugo/hugo_test.go b/common/hugo/hugo_test.go
new file mode 100644
index 000000000..1769db587
--- /dev/null
+++ b/common/hugo/hugo_test.go
@@ -0,0 +1,35 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hugo
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestHugoInfo(t *testing.T) {
+ assert := require.New(t)
+
+ hugoInfo := NewInfo("")
+
+ assert.Equal(CurrentVersion.Version(), hugoInfo.Version())
+ assert.IsType(VersionString(""), hugoInfo.Version())
+ assert.Equal(commitHash, hugoInfo.CommitHash)
+ assert.Equal(buildDate, hugoInfo.BuildDate)
+ assert.Equal("production", hugoInfo.Environment)
+ assert.Contains(hugoInfo.Generator(), fmt.Sprintf("Hugo %s", hugoInfo.Version()))
+
+}
diff --git a/common/hugo/vars_extended.go b/common/hugo/vars_extended.go
new file mode 100644
index 000000000..20683b804
--- /dev/null
+++ b/common/hugo/vars_extended.go
@@ -0,0 +1,18 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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 extended
+
+package hugo
+
+var isExtended = true
diff --git a/common/hugo/vars_regular.go b/common/hugo/vars_regular.go
new file mode 100644
index 000000000..e1ece83fb
--- /dev/null
+++ b/common/hugo/vars_regular.go
@@ -0,0 +1,18 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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 !extended
+
+package hugo
+
+var isExtended = false
diff --git a/common/hugo/version.go b/common/hugo/version.go
new file mode 100644
index 000000000..47641f10c
--- /dev/null
+++ b/common/hugo/version.go
@@ -0,0 +1,237 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hugo
+
+import (
+ "fmt"
+
+ "runtime"
+ "strings"
+
+ "github.com/gohugoio/hugo/compare"
+ "github.com/spf13/cast"
+)
+
+// Version represents the Hugo build version.
+type Version 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
+}
+
+var (
+ _ compare.Eqer = (*VersionString)(nil)
+ _ compare.Comparer = (*VersionString)(nil)
+)
+
+func (v Version) String() string {
+ return version(v.Number, v.PatchLevel, v.Suffix)
+}
+
+// Version returns the Hugo version.
+func (v Version) Version() VersionString {
+ return VersionString(v.String())
+}
+
+// VersionString represents a Hugo version string.
+type VersionString string
+
+func (h VersionString) String() string {
+ return string(h)
+}
+
+// Compare implements the compare.Comparer interface.
+func (h VersionString) Compare(other interface{}) int {
+ v := MustParseVersion(h.String())
+ return compareVersionsWithSuffix(v.Number, v.PatchLevel, v.Suffix, other)
+}
+
+// Eq implements the compare.Eqer interface.
+func (h VersionString) Eq(other interface{}) bool {
+ s, err := cast.ToStringE(other)
+ if err != nil {
+ return false
+ }
+ return s == h.String()
+}
+
+var versionSuffixes = []string{"-test", "-DEV"}
+
+// ParseVersion parses a version string.
+func ParseVersion(s string) (Version, error) {
+ var vv Version
+ for _, suffix := range versionSuffixes {
+ if strings.HasSuffix(s, suffix) {
+ vv.Suffix = suffix
+ s = strings.TrimSuffix(s, suffix)
+ }
+ }
+
+ v, p := parseVersion(s)
+
+ vv.Number = v
+ vv.PatchLevel = p
+
+ return vv, nil
+}
+
+// MustParseVersion parses a version string
+// and panics if any error occurs.
+func MustParseVersion(s string) Version {
+ vv, err := ParseVersion(s)
+ if err != nil {
+ panic(err)
+ }
+ return vv
+}
+
+// ReleaseVersion represents the release version.
+func (v Version) ReleaseVersion() Version {
+ v.Suffix = ""
+ return v
+}
+
+// Next returns the next Hugo release version.
+func (v Version) Next() Version {
+ return Version{Number: v.Number + 0.01}
+}
+
+// Prev returns the previous Hugo release version.
+func (v Version) Prev() Version {
+ return Version{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 Version) NextPatchLevel(level int) Version {
+ return Version{Number: v.Number - 0.01, PatchLevel: level}
+}
+
+// BuildVersionString creates a version string. This is what you see when
+// running "hugo version".
+func BuildVersionString() string {
+ program := "Hugo Static Site Generator"
+
+ version := "v" + CurrentVersion.String()
+ if commitHash != "" {
+ version += "-" + strings.ToUpper(commitHash)
+ }
+ if isExtended {
+ version += "/extended"
+ }
+
+ osArch := runtime.GOOS + "/" + runtime.GOARCH
+
+ date := buildDate
+ if date == "" {
+ date = "unknown"
+ }
+
+ return fmt.Sprintf("%s %s %s BuildDate: %s", program, version, osArch, date)
+
+}
+
+func version(version float32, patchVersion int, suffix string) string {
+ if patchVersion > 0 || version > 0.53 {
+ 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 compareVersionsWithSuffix(CurrentVersion.Number, CurrentVersion.PatchLevel, CurrentVersion.Suffix, version)
+}
+
+func compareVersions(inVersion float32, inPatchVersion int, in interface{}) int {
+ return compareVersionsWithSuffix(inVersion, inPatchVersion, "", in)
+}
+
+func compareVersionsWithSuffix(inVersion float32, inPatchVersion int, suffix string, in interface{}) int {
+ var c int
+ switch d := in.(type) {
+ case float64:
+ c = compareFloatVersions(inVersion, float32(d))
+ case float32:
+ c = compareFloatVersions(inVersion, d)
+ case int:
+ c = compareFloatVersions(inVersion, float32(d))
+ case int32:
+ c = compareFloatVersions(inVersion, float32(d))
+ case int64:
+ c = compareFloatVersions(inVersion, float32(d))
+ default:
+ s, err := cast.ToStringE(in)
+ if err != nil {
+ return -1
+ }
+
+ v, err := ParseVersion(s)
+ if err != nil {
+ return -1
+ }
+
+ if v.Number == inVersion && v.PatchLevel == inPatchVersion {
+ return strings.Compare(suffix, v.Suffix)
+ }
+
+ if v.Number < inVersion || (v.Number == inVersion && v.PatchLevel < inPatchVersion) {
+ return -1
+ }
+
+ return 1
+ }
+
+ if c == 0 && suffix != "" {
+ return 1
+ }
+
+ return c
+}
+
+func parseVersion(s string) (float32, int) {
+ 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))
+
+ return v, p
+}
+
+func compareFloatVersions(version float32, v float32) int {
+ if v == version {
+ return 0
+ }
+ if v < version {
+ return -1
+ }
+ return 1
+}
diff --git a/common/hugo/version_current.go b/common/hugo/version_current.go
new file mode 100644
index 000000000..d296efcc8
--- /dev/null
+++ b/common/hugo/version_current.go
@@ -0,0 +1,22 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hugo
+
+// CurrentVersion represents the current build version.
+// This should be the only one.
+var CurrentVersion = Version{
+ Number: 0.56,
+ PatchLevel: 0,
+ Suffix: "-DEV",
+}
diff --git a/common/hugo/version_test.go b/common/hugo/version_test.go
new file mode 100644
index 000000000..08059189e
--- /dev/null
+++ b/common/hugo/version_test.go
@@ -0,0 +1,79 @@
+// 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 hugo
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestHugoVersion(t *testing.T) {
+ assert.Equal(t, "0.15-DEV", version(0.15, 0, "-DEV"))
+ assert.Equal(t, "0.15.2-DEV", version(0.15, 2, "-DEV"))
+
+ v := Version{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())
+ nextVersionString := v.Next().Version()
+ require.Equal(t, "0.22", nextVersionString.String())
+ require.True(t, nextVersionString.Eq("0.22"))
+ require.False(t, nextVersionString.Eq("0.21"))
+ require.True(t, nextVersionString.Eq(nextVersionString))
+ require.Equal(t, "0.20.3", v.NextPatchLevel(3).String())
+
+ // We started to use full semver versions even for main
+ // releases in v0.54.0
+ v = Version{Number: 0.53, PatchLevel: 0}
+ require.Equal(t, "0.53", v.String())
+ require.Equal(t, "0.54.0", v.Next().String())
+ require.Equal(t, "0.55.0", v.Next().Next().String())
+ v = Version{Number: 0.54, PatchLevel: 0, Suffix: "-DEV"}
+ require.Equal(t, "0.54.0-DEV", v.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"))
+ require.Equal(t, -1, compareVersions(0.22, 0, "0.22-DEV"))
+ require.Equal(t, 1, compareVersions(0.22, 0, "0.22.1-DEV"))
+ require.Equal(t, 1, compareVersionsWithSuffix(0.22, 0, "-DEV", "0.22"))
+ require.Equal(t, -1, compareVersionsWithSuffix(0.22, 1, "-DEV", "0.22"))
+ require.Equal(t, 0, compareVersionsWithSuffix(0.22, 1, "-DEV", "0.22.1-DEV"))
+
+}
+
+func TestParseHugoVersion(t *testing.T) {
+ require.Equal(t, "0.25", MustParseVersion("0.25").String())
+ require.Equal(t, "0.25.2", MustParseVersion("0.25.2").String())
+ require.Equal(t, "0.25-test", MustParseVersion("0.25-test").String())
+ require.Equal(t, "0.25-DEV", MustParseVersion("0.25-DEV").String())
+
+}
diff --git a/common/loggers/loggers.go b/common/loggers/loggers.go
new file mode 100644
index 000000000..e711de468
--- /dev/null
+++ b/common/loggers/loggers.go
@@ -0,0 +1,173 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package loggers
+
+import (
+ "bytes"
+ "io"
+ "io/ioutil"
+ "log"
+ "os"
+ "regexp"
+
+ "github.com/gohugoio/hugo/common/terminal"
+
+ jww "github.com/spf13/jwalterweatherman"
+)
+
+var (
+ // Counts ERROR logs to the global jww logger.
+ GlobalErrorCounter *jww.Counter
+)
+
+func init() {
+ GlobalErrorCounter = &jww.Counter{}
+ jww.SetLogListeners(jww.LogCounter(GlobalErrorCounter, jww.LevelError))
+}
+
+// Logger wraps a *loggers.Logger and some other related logging state.
+type Logger struct {
+ *jww.Notepad
+ ErrorCounter *jww.Counter
+
+ // This is only set in server mode.
+ errors *bytes.Buffer
+}
+
+func (l *Logger) Errors() string {
+ if l.errors == nil {
+ return ""
+ }
+ return ansiColorRe.ReplaceAllString(l.errors.String(), "")
+}
+
+// Reset resets the logger's internal state.
+func (l *Logger) Reset() {
+ l.ErrorCounter.Reset()
+ if l.errors != nil {
+ l.errors.Reset()
+ }
+}
+
+// NewLogger creates a new Logger for the given thresholds
+func NewLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) *Logger {
+ return newLogger(stdoutThreshold, logThreshold, outHandle, logHandle, saveErrors)
+}
+
+// NewDebugLogger is a convenience function to create a debug logger.
+func NewDebugLogger() *Logger {
+ return newBasicLogger(jww.LevelDebug)
+}
+
+// NewWarningLogger is a convenience function to create a warning logger.
+func NewWarningLogger() *Logger {
+ return newBasicLogger(jww.LevelWarn)
+}
+
+// NewErrorLogger is a convenience function to create an error logger.
+func NewErrorLogger() *Logger {
+ return newBasicLogger(jww.LevelError)
+}
+
+var (
+ ansiColorRe = regexp.MustCompile("(?s)\\033\\[\\d*(;\\d*)*m")
+ errorRe = regexp.MustCompile("^(ERROR|FATAL|WARN)")
+)
+
+type ansiCleaner struct {
+ w io.Writer
+}
+
+func (a ansiCleaner) Write(p []byte) (n int, err error) {
+ return a.w.Write(ansiColorRe.ReplaceAll(p, []byte("")))
+}
+
+type labelColorizer struct {
+ w io.Writer
+}
+
+func (a labelColorizer) Write(p []byte) (n int, err error) {
+ replaced := errorRe.ReplaceAllStringFunc(string(p), func(m string) string {
+ switch m {
+ case "ERROR", "FATAL":
+ return terminal.Error(m)
+ case "WARN":
+ return terminal.Warning(m)
+ default:
+ return m
+ }
+ })
+ // io.MultiWriter will abort if we return a bigger write count than input
+ // bytes, so we lie a little.
+ _, err = a.w.Write([]byte(replaced))
+ return len(p), err
+
+}
+
+// InitGlobalLogger initializes the global logger, used in some rare cases.
+func InitGlobalLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer) {
+ outHandle, logHandle = getLogWriters(outHandle, logHandle)
+
+ jww.SetStdoutOutput(outHandle)
+ jww.SetLogOutput(logHandle)
+ jww.SetLogThreshold(logThreshold)
+ jww.SetStdoutThreshold(stdoutThreshold)
+
+}
+
+func getLogWriters(outHandle, logHandle io.Writer) (io.Writer, io.Writer) {
+ isTerm := terminal.IsTerminal(os.Stdout)
+ if logHandle != ioutil.Discard && isTerm {
+ // Remove any Ansi coloring from log output
+ logHandle = ansiCleaner{w: logHandle}
+ }
+
+ if isTerm {
+ outHandle = labelColorizer{w: outHandle}
+ }
+
+ return outHandle, logHandle
+
+}
+
+func newLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) *Logger {
+ errorCounter := &jww.Counter{}
+ outHandle, logHandle = getLogWriters(outHandle, logHandle)
+
+ listeners := []jww.LogListener{jww.LogCounter(errorCounter, jww.LevelError)}
+ var errorBuff *bytes.Buffer
+ if saveErrors {
+ errorBuff = new(bytes.Buffer)
+ errorCapture := func(t jww.Threshold) io.Writer {
+ if t != jww.LevelError {
+ // Only interested in ERROR
+ return nil
+ }
+
+ return errorBuff
+ }
+
+ listeners = append(listeners, errorCapture)
+ }
+
+ return &Logger{
+ Notepad: jww.NewNotepad(stdoutThreshold, logThreshold, outHandle, logHandle, "", log.Ldate|log.Ltime, listeners...),
+ ErrorCounter: errorCounter,
+ errors: errorBuff,
+ }
+}
+
+func newBasicLogger(t jww.Threshold) *Logger {
+ return newLogger(t, jww.LevelError, os.Stdout, ioutil.Discard, false)
+}
diff --git a/common/loggers/loggers_test.go b/common/loggers/loggers_test.go
new file mode 100644
index 000000000..3737ddc68
--- /dev/null
+++ b/common/loggers/loggers_test.go
@@ -0,0 +1,32 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package loggers
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestLogger(t *testing.T) {
+ assert := require.New(t)
+ l := NewWarningLogger()
+
+ l.ERROR.Println("One error")
+ l.ERROR.Println("Two error")
+ l.WARN.Println("A warning")
+
+ assert.Equal(uint64(2), l.ErrorCounter.Count())
+
+}
diff --git a/common/maps/maps.go b/common/maps/maps.go
new file mode 100644
index 000000000..e0d4f964d
--- /dev/null
+++ b/common/maps/maps.go
@@ -0,0 +1,116 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package maps
+
+import (
+ "strings"
+
+ "github.com/gobwas/glob"
+
+ "github.com/spf13/cast"
+)
+
+// ToLower 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 ToLower(m map[string]interface{}) {
+ for k, v := range m {
+ switch v.(type) {
+ case map[interface{}]interface{}:
+ v = cast.ToStringMap(v)
+ ToLower(v.(map[string]interface{}))
+ case map[string]interface{}:
+ ToLower(v.(map[string]interface{}))
+ }
+
+ lKey := strings.ToLower(k)
+ if k != lKey {
+ delete(m, k)
+ m[lKey] = v
+ }
+
+ }
+}
+
+type keyRename struct {
+ pattern glob.Glob
+ newKey string
+}
+
+// KeyRenamer supports renaming of keys in a map.
+type KeyRenamer struct {
+ renames []keyRename
+}
+
+// NewKeyRenamer creates a new KeyRenamer given a list of pattern and new key
+// value pairs.
+func NewKeyRenamer(patternKeys ...string) (KeyRenamer, error) {
+ var renames []keyRename
+ for i := 0; i < len(patternKeys); i += 2 {
+ g, err := glob.Compile(strings.ToLower(patternKeys[i]), '/')
+ if err != nil {
+ return KeyRenamer{}, err
+ }
+ renames = append(renames, keyRename{pattern: g, newKey: patternKeys[i+1]})
+ }
+
+ return KeyRenamer{renames: renames}, nil
+}
+
+func (r KeyRenamer) getNewKey(keyPath string) string {
+ for _, matcher := range r.renames {
+ if matcher.pattern.Match(keyPath) {
+ return matcher.newKey
+ }
+ }
+
+ return ""
+}
+
+// Rename renames the keys in the given map according
+// to the patterns in the current KeyRenamer.
+func (r KeyRenamer) Rename(m map[string]interface{}) {
+ r.renamePath("", m)
+}
+
+func (KeyRenamer) keyPath(k1, k2 string) string {
+ k1, k2 = strings.ToLower(k1), strings.ToLower(k2)
+ if k1 == "" {
+ return k2
+ } else {
+ return k1 + "/" + k2
+ }
+}
+
+func (r KeyRenamer) renamePath(parentKeyPath string, m map[string]interface{}) {
+ for key, val := range m {
+ keyPath := r.keyPath(parentKeyPath, key)
+ switch val.(type) {
+ case map[interface{}]interface{}:
+ val = cast.ToStringMap(val)
+ r.renamePath(keyPath, val.(map[string]interface{}))
+ case map[string]interface{}:
+ r.renamePath(keyPath, val.(map[string]interface{}))
+ }
+
+ newKey := r.getNewKey(keyPath)
+
+ if newKey != "" {
+ delete(m, key)
+ m[newKey] = val
+ }
+ }
+}
diff --git a/common/maps/maps_test.go b/common/maps/maps_test.go
new file mode 100644
index 000000000..29bffa6bc
--- /dev/null
+++ b/common/maps/maps_test.go
@@ -0,0 +1,123 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package maps
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestToLower(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 {
+ // ToLower modifies input.
+ ToLower(test.input)
+ if !reflect.DeepEqual(test.expected, test.input) {
+ t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, test.input)
+ }
+ }
+}
+
+func TestRenameKeys(t *testing.T) {
+ assert := require.New(t)
+
+ m := map[string]interface{}{
+ "a": 32,
+ "ren1": "m1",
+ "ren2": "m1_2",
+ "sub": map[string]interface{}{
+ "subsub": map[string]interface{}{
+ "REN1": "m2",
+ "ren2": "m2_2",
+ },
+ },
+ "no": map[string]interface{}{
+ "ren1": "m2",
+ "ren2": "m2_2",
+ },
+ }
+
+ expected := map[string]interface{}{
+ "a": 32,
+ "new1": "m1",
+ "new2": "m1_2",
+ "sub": map[string]interface{}{
+ "subsub": map[string]interface{}{
+ "new1": "m2",
+ "ren2": "m2_2",
+ },
+ },
+ "no": map[string]interface{}{
+ "ren1": "m2",
+ "ren2": "m2_2",
+ },
+ }
+
+ renamer, err := NewKeyRenamer(
+ "{ren1,sub/*/ren1}", "new1",
+ "{Ren2,sub/ren2}", "new2",
+ )
+ assert.NoError(err)
+
+ renamer.Rename(m)
+
+ if !reflect.DeepEqual(expected, m) {
+ t.Errorf("Expected\n%#v, got\n%#v\n", expected, m)
+ }
+
+}
diff --git a/common/maps/scratch.go b/common/maps/scratch.go
new file mode 100644
index 000000000..4acd10c6c
--- /dev/null
+++ b/common/maps/scratch.go
@@ -0,0 +1,153 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package maps
+
+import (
+ "reflect"
+ "sort"
+ "sync"
+
+ "github.com/gohugoio/hugo/common/collections"
+ "github.com/gohugoio/hugo/common/math"
+)
+
+// Scratch is a writable context used for stateful operations in Page/Node rendering.
+type Scratch struct {
+ values map[string]interface{}
+ mu sync.RWMutex
+}
+
+// Scratcher provides a scratching service.
+type Scratcher interface {
+ Scratch() *Scratch
+}
+
+type scratcher struct {
+ s *Scratch
+}
+
+func (s scratcher) Scratch() *Scratch {
+ return s.s
+}
+
+// NewScratcher creates a new Scratcher.
+func NewScratcher() Scratcher {
+ return scratcher{s: NewScratch()}
+}
+
+// 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.TypeOf(existingAddend)
+
+ if addendV.Kind() == reflect.Slice || addendV.Kind() == reflect.Array {
+ newVal, err = collections.Append(existingAddend, newAddend)
+ if err != nil {
+ return "", err
+ }
+ } 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 ""
+}
+
+// Delete deletes the given key.
+func (c *Scratch) Delete(key string) string {
+ c.mu.Lock()
+ delete(c.values, key)
+ 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
+}
+
+// NewScratch returns a new instance Scratch.
+func NewScratch() *Scratch {
+ return &Scratch{values: make(map[string]interface{})}
+}
diff --git a/common/maps/scratch_test.go b/common/maps/scratch_test.go
new file mode 100644
index 000000000..4550a22c5
--- /dev/null
+++ b/common/maps/scratch_test.go
@@ -0,0 +1,207 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package maps
+
+import (
+ "reflect"
+ "sync"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestScratchAdd(t *testing.T) {
+ t.Parallel()
+ assert := require.New(t)
+
+ scratch := NewScratch()
+ scratch.Add("int1", 10)
+ scratch.Add("int1", 20)
+ scratch.Add("int2", 20)
+
+ assert.Equal(int64(30), scratch.Get("int1"))
+ assert.Equal(20, scratch.Get("int2"))
+
+ scratch.Add("float1", float64(10.5))
+ scratch.Add("float1", float64(20.1))
+
+ assert.Equal(float64(30.6), scratch.Get("float1"))
+
+ scratch.Add("string1", "Hello ")
+ scratch.Add("string1", "big ")
+ scratch.Add("string1", "World!")
+
+ assert.Equal("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()
+ assert := require.New(t)
+
+ scratch := NewScratch()
+
+ _, err := scratch.Add("intSlice", []int{1, 2})
+ assert.NoError(err)
+ _, err = scratch.Add("intSlice", 3)
+ assert.NoError(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.NoError(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)
+ }
+}
+
+// https://github.com/gohugoio/hugo/issues/5275
+func TestScratchAddTypedSliceToInterfaceSlice(t *testing.T) {
+ t.Parallel()
+ assert := require.New(t)
+
+ scratch := NewScratch()
+ scratch.Set("slice", []interface{}{})
+
+ _, err := scratch.Add("slice", []int{1, 2})
+ assert.NoError(err)
+ assert.Equal([]int{1, 2}, scratch.Get("slice"))
+
+}
+
+// https://github.com/gohugoio/hugo/issues/5361
+func TestScratchAddDifferentTypedSliceToInterfaceSlice(t *testing.T) {
+ t.Parallel()
+ assert := require.New(t)
+
+ scratch := NewScratch()
+ scratch.Set("slice", []string{"foo"})
+
+ _, err := scratch.Add("slice", []int{1, 2})
+ assert.NoError(err)
+ assert.Equal([]interface{}{"foo", 1, 2}, scratch.Get("slice"))
+
+}
+
+func TestScratchSet(t *testing.T) {
+ t.Parallel()
+ assert := require.New(t)
+
+ scratch := NewScratch()
+ scratch.Set("key", "val")
+ assert.Equal("val", scratch.Get("key"))
+}
+
+func TestScratchDelete(t *testing.T) {
+ t.Parallel()
+ assert := require.New(t)
+
+ scratch := NewScratch()
+ scratch.Set("key", "val")
+ scratch.Delete("key")
+ scratch.Add("key", "Lucy Parsons")
+ assert.Equal("Lucy Parsons", 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()
+ assert := require.New(t)
+
+ 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([]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/common/math/math.go b/common/math/math.go
new file mode 100644
index 000000000..cd06379aa
--- /dev/null
+++ b/common/math/math.go
@@ -0,0 +1,135 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+)
+
+// 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/common/math/math_test.go b/common/math/math_test.go
new file mode 100644
index 000000000..613ac3073
--- /dev/null
+++ b/common/math/math_test.go
@@ -0,0 +1,109 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/alecthomas/assert"
+ "github.com/stretchr/testify/require"
+)
+
+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)
+ }
+}
diff --git a/common/terminal/colors.go b/common/terminal/colors.go
new file mode 100644
index 000000000..334b82fae
--- /dev/null
+++ b/common/terminal/colors.go
@@ -0,0 +1,70 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package terminal contains helper for the terminal, such as coloring output.
+package terminal
+
+import (
+ "fmt"
+ "os"
+ "runtime"
+ "strings"
+
+ isatty "github.com/mattn/go-isatty"
+)
+
+const (
+ errorColor = "\033[1;31m%s\033[0m"
+ warningColor = "\033[0;33m%s\033[0m"
+ noticeColor = "\033[1;36m%s\033[0m"
+)
+
+// IsTerminal return true if the file descriptor is terminal and the TERM
+// environment variable isn't a dumb one.
+func IsTerminal(f *os.File) bool {
+ if runtime.GOOS == "windows" {
+ return false
+ }
+
+ fd := f.Fd()
+ return os.Getenv("TERM") != "dumb" && (isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd))
+}
+
+// Notice colorizes the string in a noticeable color.
+func Notice(s string) string {
+ return colorize(s, noticeColor)
+}
+
+// Error colorizes the string in a colour that grabs attention.
+func Error(s string) string {
+ return colorize(s, errorColor)
+}
+
+// Warning colorizes the string in a colour that warns.
+func Warning(s string) string {
+ return colorize(s, warningColor)
+}
+
+// colorize s in color.
+func colorize(s, color string) string {
+ s = fmt.Sprintf(color, doublePercent(s))
+ return singlePercent(s)
+}
+
+func doublePercent(str string) string {
+ return strings.Replace(str, "%", "%%", -1)
+}
+
+func singlePercent(str string) string {
+ return strings.Replace(str, "%%", "%", -1)
+}
diff --git a/common/text/position.go b/common/text/position.go
new file mode 100644
index 000000000..0c43c5ae7
--- /dev/null
+++ b/common/text/position.go
@@ -0,0 +1,99 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package text
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/gohugoio/hugo/common/terminal"
+)
+
+// Positioner represents a thing that knows its position in a text file or stream,
+// typically an error.
+type Positioner interface {
+ Position() Position
+}
+
+// Position holds a source position in a text file or stream.
+type Position struct {
+ Filename string // filename, if any
+ Offset int // byte offset, starting at 0. It's set to -1 if not provided.
+ LineNumber int // line number, starting at 1
+ ColumnNumber int // column number, starting at 1 (character count per line)
+}
+
+func (pos Position) String() string {
+ if pos.Filename == "" {
+ pos.Filename = "<stream>"
+ }
+ return positionStringFormatfunc(pos)
+}
+
+// IsValid returns true if line number is > 0.
+func (pos Position) IsValid() bool {
+ return pos.LineNumber > 0
+}
+
+var positionStringFormatfunc func(p Position) string
+
+func createPositionStringFormatter(formatStr string) func(p Position) string {
+
+ if formatStr == "" {
+ formatStr = "\":file::line::col\""
+ }
+
+ var identifiers = []string{":file", ":line", ":col"}
+ var identifiersFound []string
+
+ for i := range formatStr {
+ for _, id := range identifiers {
+ if strings.HasPrefix(formatStr[i:], id) {
+ identifiersFound = append(identifiersFound, id)
+ }
+ }
+ }
+
+ replacer := strings.NewReplacer(":file", "%s", ":line", "%d", ":col", "%d")
+ format := replacer.Replace(formatStr)
+
+ f := func(pos Position) string {
+ args := make([]interface{}, len(identifiersFound))
+ for i, id := range identifiersFound {
+ switch id {
+ case ":file":
+ args[i] = pos.Filename
+ case ":line":
+ args[i] = pos.LineNumber
+ case ":col":
+ args[i] = pos.ColumnNumber
+ }
+ }
+
+ msg := fmt.Sprintf(format, args...)
+
+ if terminal.IsTerminal(os.Stdout) {
+ return terminal.Notice(msg)
+ }
+
+ return msg
+ }
+
+ return f
+}
+
+func init() {
+ positionStringFormatfunc = createPositionStringFormatter(os.Getenv("HUGO_FILE_LOG_FORMAT"))
+}
diff --git a/common/text/position_test.go b/common/text/position_test.go
new file mode 100644
index 000000000..a25a3edbd
--- /dev/null
+++ b/common/text/position_test.go
@@ -0,0 +1,33 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package text
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestPositionStringFormatter(t *testing.T) {
+ assert := require.New(t)
+
+ pos := Position{Filename: "/my/file.txt", LineNumber: 12, ColumnNumber: 13, Offset: 14}
+
+ assert.Equal("/my/file.txt|13|12", createPositionStringFormatter(":file|:col|:line")(pos))
+ assert.Equal("13|/my/file.txt|12", createPositionStringFormatter(":col|:file|:line")(pos))
+ assert.Equal("好:13", createPositionStringFormatter("好::col")(pos))
+ assert.Equal("\"/my/file.txt:12:13\"", createPositionStringFormatter("")(pos))
+ assert.Equal("\"/my/file.txt:12:13\"", pos.String())
+
+}
diff --git a/common/types/evictingqueue.go b/common/types/evictingqueue.go
new file mode 100644
index 000000000..884762426
--- /dev/null
+++ b/common/types/evictingqueue.go
@@ -0,0 +1,96 @@
+// 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 types contains types shared between packages in Hugo.
+package types
+
+import (
+ "sync"
+)
+
+// EvictingStringQueue is a queue which automatically evicts elements from the head of
+// the queue when attempting to add new elements onto the queue and it is full.
+// This queue orders elements LIFO (last-in-first-out). It throws away duplicates.
+// Note: This queue currently does not contain any remove (poll etc.) methods.
+type EvictingStringQueue struct {
+ size int
+ vals []string
+ set map[string]bool
+ mu sync.Mutex
+}
+
+// NewEvictingStringQueue creates a new queue with the given size.
+func NewEvictingStringQueue(size int) *EvictingStringQueue {
+ return &EvictingStringQueue{size: size, set: make(map[string]bool)}
+}
+
+// Add adds a new string to the tail of the queue if it's not already there.
+func (q *EvictingStringQueue) Add(v string) {
+ q.mu.Lock()
+ if q.set[v] {
+ q.mu.Unlock()
+ return
+ }
+
+ if len(q.set) == q.size {
+ // Full
+ delete(q.set, q.vals[0])
+ q.vals = append(q.vals[:0], q.vals[1:]...)
+ }
+ q.set[v] = true
+ q.vals = append(q.vals, v)
+ q.mu.Unlock()
+}
+
+// Contains returns whether the queue contains v.
+func (q *EvictingStringQueue) Contains(v string) bool {
+ q.mu.Lock()
+ defer q.mu.Unlock()
+ return q.set[v]
+}
+
+// Peek looks at the last element added to the queue.
+func (q *EvictingStringQueue) Peek() string {
+ q.mu.Lock()
+ l := len(q.vals)
+ if l == 0 {
+ q.mu.Unlock()
+ return ""
+ }
+ elem := q.vals[l-1]
+ q.mu.Unlock()
+ return elem
+}
+
+// PeekAll looks at all the elements in the queue, with the newest first.
+func (q *EvictingStringQueue) PeekAll() []string {
+ q.mu.Lock()
+ vals := make([]string, len(q.vals))
+ copy(vals, q.vals)
+ q.mu.Unlock()
+ for i, j := 0, len(vals)-1; i < j; i, j = i+1, j-1 {
+ vals[i], vals[j] = vals[j], vals[i]
+ }
+ return vals
+}
+
+// PeekAllSet returns PeekAll as a set.
+func (q *EvictingStringQueue) PeekAllSet() map[string]bool {
+ all := q.PeekAll()
+ set := make(map[string]bool)
+ for _, v := range all {
+ set[v] = true
+ }
+
+ return set
+}
diff --git a/common/types/evictingqueue_test.go b/common/types/evictingqueue_test.go
new file mode 100644
index 000000000..a7b1e1d54
--- /dev/null
+++ b/common/types/evictingqueue_test.go
@@ -0,0 +1,74 @@
+// 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 types
+
+import (
+ "sync"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestEvictingStringQueue(t *testing.T) {
+ assert := require.New(t)
+
+ queue := NewEvictingStringQueue(3)
+
+ assert.Equal("", queue.Peek())
+ queue.Add("a")
+ queue.Add("b")
+ queue.Add("a")
+ assert.Equal("b", queue.Peek())
+ queue.Add("b")
+ assert.Equal("b", queue.Peek())
+
+ queue.Add("a")
+ queue.Add("b")
+
+ assert.True(queue.Contains("a"))
+ assert.False(queue.Contains("foo"))
+
+ assert.Equal([]string{"b", "a"}, queue.PeekAll())
+ assert.Equal("b", queue.Peek())
+ queue.Add("c")
+ queue.Add("d")
+ // Overflowed, a should now be removed.
+ assert.Equal([]string{"d", "c", "b"}, queue.PeekAll())
+ assert.Len(queue.PeekAllSet(), 3)
+ assert.True(queue.PeekAllSet()["c"])
+}
+
+func TestEvictingStringQueueConcurrent(t *testing.T) {
+ var wg sync.WaitGroup
+ val := "someval"
+
+ queue := NewEvictingStringQueue(3)
+
+ for j := 0; j < 100; j++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ queue.Add(val)
+ v := queue.Peek()
+ if v != val {
+ t.Error("wrong val")
+ }
+ vals := queue.PeekAll()
+ if len(vals) != 1 || vals[0] != val {
+ t.Error("wrong val")
+ }
+ }()
+ }
+ wg.Wait()
+}
diff --git a/common/types/types.go b/common/types/types.go
new file mode 100644
index 000000000..f03031439
--- /dev/null
+++ b/common/types/types.go
@@ -0,0 +1,80 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package types contains types shared between packages in Hugo.
+package types
+
+import (
+ "fmt"
+ "reflect"
+
+ "github.com/spf13/cast"
+)
+
+// KeyValueStr is a string tuple.
+type KeyValueStr struct {
+ Key string
+ Value string
+}
+
+// KeyValues holds an key and a slice of values.
+type KeyValues struct {
+ Key interface{}
+ Values []interface{}
+}
+
+// KeyString returns the key as a string, an empty string if conversion fails.
+func (k KeyValues) KeyString() string {
+ return cast.ToString(k.Key)
+}
+
+func (k KeyValues) String() string {
+ return fmt.Sprintf("%v: %v", k.Key, k.Values)
+}
+
+// NewKeyValuesStrings takes a given key and slice of values and returns a new
+// KeyValues struct.
+func NewKeyValuesStrings(key string, values ...string) KeyValues {
+ iv := make([]interface{}, len(values))
+ for i := 0; i < len(values); i++ {
+ iv[i] = values[i]
+ }
+ return KeyValues{Key: key, Values: iv}
+}
+
+// Zeroer, as implemented by time.Time, will be used by the truth template
+// funcs in Hugo (if, with, not, and, or).
+type Zeroer interface {
+ IsZero() bool
+}
+
+// IsNil reports whether v is nil.
+func IsNil(v interface{}) bool {
+ if v == nil {
+ return true
+ }
+
+ value := reflect.ValueOf(v)
+ switch value.Kind() {
+ case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
+ return value.IsNil()
+ }
+
+ return false
+}
+
+// DevMarker is a marker interface for types that should only be used during
+// development.
+type DevMarker interface {
+ DevOnly()
+}
diff --git a/common/types/types_test.go b/common/types/types_test.go
new file mode 100644
index 000000000..7cec8c0c0
--- /dev/null
+++ b/common/types/types_test.go
@@ -0,0 +1,29 @@
+// 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 types
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestKeyValues(t *testing.T) {
+ assert := require.New(t)
+
+ kv := NewKeyValuesStrings("key", "a1", "a2")
+
+ assert.Equal("key", kv.KeyString())
+ assert.Equal([]interface{}{"a1", "a2"}, kv.Values)
+}
diff --git a/common/urls/ref.go b/common/urls/ref.go
new file mode 100644
index 000000000..71b00b71d
--- /dev/null
+++ b/common/urls/ref.go
@@ -0,0 +1,22 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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
+
+// RefLinker is implemented by those who support reference linking.
+// args must contain a path, but can also point to the target
+// language or output format.
+type RefLinker interface {
+ Ref(args map[string]interface{}) (string, error)
+ RelRef(args map[string]interface{}) (string, error)
+}
diff --git a/compare/compare.go b/compare/compare.go
new file mode 100644
index 000000000..18c0de777
--- /dev/null
+++ b/compare/compare.go
@@ -0,0 +1,35 @@
+// 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 compare
+
+// Eqer can be used to determine if this value is equal to the other.
+// The semantics of equals is that the two value are interchangeable
+// in the Hugo templates.
+type Eqer interface {
+ Eq(other interface{}) bool
+}
+
+// ProbablyEq is an equal check that may return false positives, but never
+// a false negative.
+type ProbablyEqer interface {
+ ProbablyEq(other interface{}) bool
+}
+
+// Comparer can be used to compare two values.
+// This will be used when using the le, ge etc. operators in the templates.
+// Compare returns -1 if the given version is less than, 0 if equal and 1 if greater than
+// the running version.
+type Comparer interface {
+ Compare(other interface{}) int
+}
diff --git a/config/configLoader.go b/config/configLoader.go
new file mode 100644
index 000000000..b8aa3fda3
--- /dev/null
+++ b/config/configLoader.go
@@ -0,0 +1,127 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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
+
+import (
+ "path/filepath"
+ "strings"
+
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/parser/metadecoders"
+ "github.com/spf13/afero"
+ "github.com/spf13/viper"
+)
+
+var (
+ ValidConfigFileExtensions = []string{"toml", "yaml", "yml", "json"}
+ validConfigFileExtensionsMap map[string]bool = make(map[string]bool)
+)
+
+func init() {
+ for _, ext := range ValidConfigFileExtensions {
+ validConfigFileExtensionsMap[ext] = true
+ }
+}
+
+// IsValidConfigFilename returns whether filename is one of the supported
+// config formats in Hugo.
+func IsValidConfigFilename(filename string) bool {
+ ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
+ return validConfigFileExtensionsMap[ext]
+}
+
+// FromConfigString creates a config from the given YAML, JSON or TOML config. This is useful in tests.
+func FromConfigString(config, configType string) (Provider, error) {
+ v := newViper()
+ m, err := readConfig(metadecoders.FormatFromString(configType), []byte(config))
+ if err != nil {
+ return nil, err
+ }
+
+ v.MergeConfigMap(m)
+
+ return v, nil
+}
+
+// FromFile loads the configuration from the given filename.
+func FromFile(fs afero.Fs, filename string) (Provider, error) {
+ m, err := loadConfigFromFile(fs, filename)
+ if err != nil {
+ return nil, err
+ }
+
+ v := newViper()
+
+ err = v.MergeConfigMap(m)
+ if err != nil {
+ return nil, err
+ }
+
+ return v, nil
+}
+
+// FromFileToMap is the same as FromFile, but it returns the config values
+// as a simple map.
+func FromFileToMap(fs afero.Fs, filename string) (map[string]interface{}, error) {
+ return loadConfigFromFile(fs, filename)
+}
+
+func readConfig(format metadecoders.Format, data []byte) (map[string]interface{}, error) {
+ m, err := metadecoders.Default.UnmarshalToMap(data, format)
+ if err != nil {
+ return nil, err
+ }
+
+ RenameKeys(m)
+
+ return m, nil
+
+}
+
+func loadConfigFromFile(fs afero.Fs, filename string) (map[string]interface{}, error) {
+ m, err := metadecoders.Default.UnmarshalFileToMap(fs, filename)
+ if err != nil {
+ return nil, err
+ }
+ RenameKeys(m)
+ return m, nil
+}
+
+var keyAliases maps.KeyRenamer
+
+func init() {
+ var err error
+ keyAliases, err = maps.NewKeyRenamer(
+ // Before 0.53 we used singular for "menu".
+ "{menu,languages/*/menu}", "menus",
+ )
+
+ if err != nil {
+ panic(err)
+ }
+}
+
+// RenameKeys renames config keys in m recursively according to a global Hugo
+// alias definition.
+func RenameKeys(m map[string]interface{}) {
+ keyAliases.Rename(m)
+}
+
+func newViper() *viper.Viper {
+ v := viper.New()
+ v.AutomaticEnv()
+ v.SetEnvPrefix("hugo")
+
+ return v
+}
diff --git a/config/configLoader_test.go b/config/configLoader_test.go
new file mode 100644
index 000000000..06a00df3b
--- /dev/null
+++ b/config/configLoader_test.go
@@ -0,0 +1,34 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestIsValidConfigFileName(t *testing.T) {
+ assert := require.New(t)
+
+ for _, ext := range ValidConfigFileExtensions {
+ filename := "config." + ext
+ assert.True(IsValidConfigFilename(filename), ext)
+ assert.True(IsValidConfigFilename(strings.ToUpper(filename)))
+ }
+
+ assert.False(IsValidConfigFilename(""))
+ assert.False(IsValidConfigFilename("config.toml.swp"))
+}
diff --git a/config/configProvider.go b/config/configProvider.go
new file mode 100644
index 000000000..31914c38b
--- /dev/null
+++ b/config/configProvider.go
@@ -0,0 +1,54 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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
+
+import (
+ "github.com/spf13/cast"
+)
+
+// 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
+ GetStringSlice(key string) []string
+ Get(key string) interface{}
+ Set(key string, value interface{})
+ IsSet(key string) bool
+}
+
+// GetStringSlicePreserveString returns a string slice from the given config and key.
+// It differs from the GetStringSlice method in that if the config value is a string,
+// we do not attempt to split it into fields.
+func GetStringSlicePreserveString(cfg Provider, key string) []string {
+ sd := cfg.Get(key)
+ if sds, ok := sd.(string); ok {
+ return []string{sds}
+ }
+ return cast.ToStringSlice(sd)
+}
+
+// SetBaseTestDefaults provides some common config defaults used in tests.
+func SetBaseTestDefaults(cfg Provider) {
+ cfg.Set("resourceDir", "resources")
+ cfg.Set("contentDir", "content")
+ cfg.Set("dataDir", "data")
+ cfg.Set("i18nDir", "i18n")
+ cfg.Set("layoutDir", "layouts")
+ cfg.Set("assetDir", "assets")
+ cfg.Set("archetypeDir", "archetypes")
+ cfg.Set("publishDir", "public")
+}
diff --git a/config/configProvider_test.go b/config/configProvider_test.go
new file mode 100644
index 000000000..7e9c2223b
--- /dev/null
+++ b/config/configProvider_test.go
@@ -0,0 +1,36 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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
+
+import (
+ "testing"
+
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/require"
+)
+
+func TestGetStringSlicePreserveString(t *testing.T) {
+ assert := require.New(t)
+ cfg := viper.New()
+
+ s := "This is a string"
+ sSlice := []string{"This", "is", "a", "slice"}
+
+ cfg.Set("s1", s)
+ cfg.Set("s2", sSlice)
+
+ assert.Equal([]string{s}, GetStringSlicePreserveString(cfg, "s1"))
+ assert.Equal(sSlice, GetStringSlicePreserveString(cfg, "s2"))
+ assert.Nil(GetStringSlicePreserveString(cfg, "s3"))
+}
diff --git a/config/env.go b/config/env.go
new file mode 100644
index 000000000..adf6f9b68
--- /dev/null
+++ b/config/env.go
@@ -0,0 +1,33 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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
+
+import (
+ "os"
+ "runtime"
+ "strconv"
+)
+
+// GetNumWorkerMultiplier returns the base value used to calculate the number
+// of workers to use for Hugo's parallel execution.
+// It returns the value in HUGO_NUMWORKERMULTIPLIER OS env variable if set to a
+// positive integer, else the number of logical CPUs.
+func GetNumWorkerMultiplier() int {
+ if gmp := os.Getenv("HUGO_NUMWORKERMULTIPLIER"); gmp != "" {
+ if p, err := strconv.Atoi(gmp); err == nil && p > 0 {
+ return p
+ }
+ }
+ return runtime.NumCPU()
+}
diff --git a/config/privacy/privacyConfig.go b/config/privacy/privacyConfig.go
new file mode 100644
index 000000000..ea34563eb
--- /dev/null
+++ b/config/privacy/privacyConfig.go
@@ -0,0 +1,110 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package privacy
+
+import (
+ "github.com/gohugoio/hugo/config"
+ "github.com/mitchellh/mapstructure"
+)
+
+const privacyConfigKey = "privacy"
+
+// Service is the common values for a service in a policy definition.
+type Service struct {
+ Disable bool
+}
+
+// Config is a privacy configuration for all the relevant services in Hugo.
+type Config struct {
+ Disqus Disqus
+ GoogleAnalytics GoogleAnalytics
+ Instagram Instagram
+ Twitter Twitter
+ Vimeo Vimeo
+ YouTube YouTube
+}
+
+// Disqus holds the privacy configuration settings related to the Disqus template.
+type Disqus struct {
+ Service `mapstructure:",squash"`
+}
+
+// GoogleAnalytics holds the privacy configuration settings related to the Google Analytics template.
+type GoogleAnalytics struct {
+ Service `mapstructure:",squash"`
+
+ // Enabling this will disable the use of Cookies and use Session Storage to Store the GA Client ID.
+ UseSessionStorage bool
+
+ // Enabling this will make the GA templates respect the
+ // "Do Not Track" HTTP header. See https://www.paulfurley.com/google-analytics-dnt/.
+ RespectDoNotTrack bool
+
+ // Enabling this will make it so the users' IP addresses are anonymized within Google Analytics.
+ AnonymizeIP bool
+}
+
+// Instagram holds the privacy configuration settings related to the Instagram shortcode.
+type Instagram struct {
+ Service `mapstructure:",squash"`
+
+ // If simple mode is enabled, a static and no-JS version of the Instagram
+ // image card will be built.
+ Simple bool
+}
+
+// Twitter holds the privacy configuration settingsrelated to the Twitter shortcode.
+type Twitter struct {
+ Service `mapstructure:",squash"`
+
+ // When set to true, the Tweet and its embedded page on your site are not used
+ // for purposes that include personalized suggestions and personalized ads.
+ EnableDNT bool
+
+ // If simple mode is enabled, a static and no-JS version of the Tweet will be built.
+ Simple bool
+}
+
+// Vimeo holds the privacy configuration settingsrelated to the Vimeo shortcode.
+type Vimeo struct {
+ Service `mapstructure:",squash"`
+
+ // If simple mode is enabled, only a thumbnail is fetched from i.vimeocdn.com and
+ // shown with a play button overlaid. If a user clicks the button, he/she will
+ // be taken to the video page on vimeo.com in a new browser tab.
+ Simple bool
+}
+
+// YouTube holds the privacy configuration settingsrelated to the YouTube shortcode.
+type YouTube struct {
+ Service `mapstructure:",squash"`
+
+ // When you turn on privacy-enhanced mode,
+ // YouTube won’t store information about visitors on your website
+ // unless the user plays the embedded video.
+ PrivacyEnhanced bool
+}
+
+// DecodeConfig creates a privacy Config from a given Hugo configuration.
+func DecodeConfig(cfg config.Provider) (pc Config, err error) {
+ if !cfg.IsSet(privacyConfigKey) {
+ return
+ }
+
+ m := cfg.GetStringMap(privacyConfigKey)
+
+ err = mapstructure.WeakDecode(m, &pc)
+
+ return
+}
diff --git a/config/privacy/privacyConfig_test.go b/config/privacy/privacyConfig_test.go
new file mode 100644
index 000000000..5ced6d9d9
--- /dev/null
+++ b/config/privacy/privacyConfig_test.go
@@ -0,0 +1,104 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package privacy
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/require"
+)
+
+func TestDecodeConfigFromTOML(t *testing.T) {
+ assert := require.New(t)
+
+ tomlConfig := `
+
+someOtherValue = "foo"
+
+[privacy]
+[privacy.disqus]
+disable = true
+[privacy.googleAnalytics]
+disable = true
+respectDoNotTrack = true
+anonymizeIP = true
+useSessionStorage = true
+[privacy.instagram]
+disable = true
+simple = true
+[privacy.twitter]
+disable = true
+enableDNT = true
+simple = true
+[privacy.vimeo]
+disable = true
+simple = true
+[privacy.youtube]
+disable = true
+privacyEnhanced = true
+simple = true
+`
+ cfg, err := config.FromConfigString(tomlConfig, "toml")
+ assert.NoError(err)
+
+ pc, err := DecodeConfig(cfg)
+ assert.NoError(err)
+ assert.NotNil(pc)
+
+ assert.True(pc.Disqus.Disable)
+ assert.True(pc.GoogleAnalytics.Disable)
+ assert.True(pc.GoogleAnalytics.RespectDoNotTrack)
+ assert.True(pc.GoogleAnalytics.AnonymizeIP)
+ assert.True(pc.GoogleAnalytics.UseSessionStorage)
+ assert.True(pc.Instagram.Disable)
+ assert.True(pc.Instagram.Simple)
+ assert.True(pc.Twitter.Disable)
+ assert.True(pc.Twitter.EnableDNT)
+ assert.True(pc.Twitter.Simple)
+ assert.True(pc.Vimeo.Disable)
+ assert.True(pc.Vimeo.Simple)
+ assert.True(pc.YouTube.PrivacyEnhanced)
+ assert.True(pc.YouTube.Disable)
+}
+
+func TestDecodeConfigFromTOMLCaseInsensitive(t *testing.T) {
+ assert := require.New(t)
+
+ tomlConfig := `
+
+someOtherValue = "foo"
+
+[Privacy]
+[Privacy.YouTube]
+PrivacyENhanced = true
+`
+ cfg, err := config.FromConfigString(tomlConfig, "toml")
+ assert.NoError(err)
+
+ pc, err := DecodeConfig(cfg)
+ assert.NoError(err)
+ assert.NotNil(pc)
+ assert.True(pc.YouTube.PrivacyEnhanced)
+}
+
+func TestDecodeConfigDefault(t *testing.T) {
+ assert := require.New(t)
+
+ pc, err := DecodeConfig(viper.New())
+ assert.NoError(err)
+ assert.NotNil(pc)
+ assert.False(pc.YouTube.PrivacyEnhanced)
+}
diff --git a/config/services/servicesConfig.go b/config/services/servicesConfig.go
new file mode 100644
index 000000000..559848f5c
--- /dev/null
+++ b/config/services/servicesConfig.go
@@ -0,0 +1,92 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package services
+
+import (
+ "github.com/gohugoio/hugo/config"
+ "github.com/mitchellh/mapstructure"
+)
+
+const (
+ servicesConfigKey = "services"
+
+ disqusShortnameKey = "disqusshortname"
+ googleAnalyticsKey = "googleanalytics"
+ rssLimitKey = "rssLimit"
+)
+
+// Config is a privacy configuration for all the relevant services in Hugo.
+type Config struct {
+ Disqus Disqus
+ GoogleAnalytics GoogleAnalytics
+ Instagram Instagram
+ Twitter Twitter
+ RSS RSS
+}
+
+// Disqus holds the functional configuration settings related to the Disqus template.
+type Disqus struct {
+ // A Shortname is the unique identifier assigned to a Disqus site.
+ Shortname string
+}
+
+// GoogleAnalytics holds the functional configuration settings related to the Google Analytics template.
+type GoogleAnalytics struct {
+ // The GA tracking ID.
+ ID string
+}
+
+// Instagram holds the functional configuration settings related to the Instagram shortcodes.
+type Instagram struct {
+ // The Simple variant of the Instagram is decorated with Bootstrap 4 card classes.
+ // This means that if you use Bootstrap 4 or want to provide your own CSS, you want
+ // to disable the inline CSS provided by Hugo.
+ DisableInlineCSS bool
+}
+
+// Twitter holds the functional configuration settings related to the Twitter shortcodes.
+type Twitter struct {
+ // The Simple variant of Twitter is decorated with a basic set of inline styles.
+ // This means that if you want to provide your own CSS, you want
+ // to disable the inline CSS provided by Hugo.
+ DisableInlineCSS bool
+}
+
+// RSS holds the functional configuration settings related to the RSS feeds.
+type RSS struct {
+ // Limit the number of pages.
+ Limit int
+}
+
+// DecodeConfig creates a services Config from a given Hugo configuration.
+func DecodeConfig(cfg config.Provider) (c Config, err error) {
+ m := cfg.GetStringMap(servicesConfigKey)
+
+ err = mapstructure.WeakDecode(m, &c)
+
+ // Keep backwards compatibility.
+ if c.GoogleAnalytics.ID == "" {
+ // Try the global config
+ c.GoogleAnalytics.ID = cfg.GetString(googleAnalyticsKey)
+ }
+ if c.Disqus.Shortname == "" {
+ c.Disqus.Shortname = cfg.GetString(disqusShortnameKey)
+ }
+
+ if c.RSS.Limit == 0 {
+ c.RSS.Limit = cfg.GetInt(rssLimitKey)
+ }
+
+ return
+}
diff --git a/config/services/servicesConfig_test.go b/config/services/servicesConfig_test.go
new file mode 100644
index 000000000..367b40153
--- /dev/null
+++ b/config/services/servicesConfig_test.go
@@ -0,0 +1,69 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package services
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/require"
+)
+
+func TestDecodeConfigFromTOML(t *testing.T) {
+ assert := require.New(t)
+
+ tomlConfig := `
+
+someOtherValue = "foo"
+
+[services]
+[services.disqus]
+shortname = "DS"
+[services.googleAnalytics]
+id = "ga_id"
+[services.instagram]
+disableInlineCSS = true
+[services.twitter]
+disableInlineCSS = true
+`
+ cfg, err := config.FromConfigString(tomlConfig, "toml")
+ assert.NoError(err)
+
+ config, err := DecodeConfig(cfg)
+ assert.NoError(err)
+ assert.NotNil(config)
+
+ assert.Equal("DS", config.Disqus.Shortname)
+ assert.Equal("ga_id", config.GoogleAnalytics.ID)
+
+ assert.True(config.Instagram.DisableInlineCSS)
+}
+
+// Support old root-level GA settings etc.
+func TestUseSettingsFromRootIfSet(t *testing.T) {
+ assert := require.New(t)
+
+ cfg := viper.New()
+ cfg.Set("disqusShortname", "root_short")
+ cfg.Set("googleAnalytics", "ga_root")
+
+ config, err := DecodeConfig(cfg)
+ assert.NoError(err)
+ assert.NotNil(config)
+
+ assert.Equal("root_short", config.Disqus.Shortname)
+ assert.Equal("ga_root", config.GoogleAnalytics.ID)
+
+}
diff --git a/config/sitemap.go b/config/sitemap.go
new file mode 100644
index 000000000..4031b7ec1
--- /dev/null
+++ b/config/sitemap.go
@@ -0,0 +1,44 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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
+
+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 DecodeSitemap(prototype Sitemap, input map[string]interface{}) Sitemap {
+
+ for key, value := range input {
+ switch key {
+ case "changefreq":
+ prototype.ChangeFreq = cast.ToString(value)
+ case "priority":
+ prototype.Priority = cast.ToFloat64(value)
+ case "filename":
+ prototype.Filename = cast.ToString(value)
+ default:
+ jww.WARN.Printf("Unknown Sitemap field: %s\n", key)
+ }
+ }
+
+ return prototype
+}
diff --git a/content/en/tools/migrations.md b/content/en/tools/migrations.md
deleted file mode 100644
index 7369b76da..000000000
--- a/content/en/tools/migrations.md
+++ /dev/null
@@ -1,81 +0,0 @@
----
-title: Migrate to Hugo
-linktitle: Migrations
-description: A list of community-developed tools for migrating from your existing static site generator or content management system to Hugo.
-date: 2017-02-01
-publishdate: 2017-02-01
-lastmod: 2017-02-01
-keywords: [migrations,jekyll,wordpress,drupal,ghost,contentful]
-menu:
- docs:
- parent: "tools"
- weight: 10
-weight: 10
-sections_weight: 10
-draft: false
-aliases: [/developer-tools/migrations/,/developer-tools/migrated/]
-toc: true
----
-
-This section highlights some projects around Hugo that are independently developed. These tools try to extend the functionality of our static site generator or help you to get started.
-
-{{% note %}}
-Do you know or maintain a similar project around Hugo? Feel free to open a [pull request](https://github.com/gohugoio/hugo/pulls) on GitHub if you think it should be added.
-{{% /note %}}
-
-Take a look at this list of migration tools if you currently use other blogging tools like Jekyll or WordPress but intend to switch to Hugo instead. They'll take care to export your content into Hugo-friendly formats.
-
-## Jekyll
-
-Alternatively, you can use the new [Jekyll import command](/commands/hugo_import_jekyll/).
-
-- [JekyllToHugo](https://github.com/SenjinDarashiva/JekyllToHugo) - A Small script for converting Jekyll blog posts to a Hugo site.
-- [ConvertToHugo](https://github.com/coderzh/ConvertToHugo) - Convert your blog from Jekyll to Hugo.
-
-## Ghost
-
-- [ghostToHugo](https://github.com/jbarone/ghostToHugo) - Convert Ghost blog posts and export them to Hugo.
-
-## Octopress
-
-- [octohug](https://github.com/codebrane/octohug) - Octopress to Hugo migrator.
-
-## DokuWiki
-
-- [dokuwiki-to-hugo](https://github.com/wgroeneveld/dokuwiki-to-hugo) - Migrates your dokuwiki source pages from [DokuWiki syntax](https://www.dokuwiki.org/wiki:syntax) to Hugo Markdown syntax. Includes extra's like the TODO plugin. Written with extensibility in mind using python 3. Also generates a TOML header for each page. Designed to copypaste the wiki directory into your /content directory.
-
-## WordPress
-
-- [wordpress-to-hugo-exporter](https://github.com/SchumacherFM/wordpress-to-hugo-exporter) - A one-click WordPress plugin that converts all posts, pages, taxonomies, metadata, and settings to Markdown and YAML which can be dropped into Hugo. (Note: If you have trouble using this plugin, you can [export your site for Jekyll](https://wordpress.org/plugins/jekyll-exporter/) and use Hugo's built in Jekyll converter listed above.)
-- [exitwp-for-hugo](https://github.com/wooni005/exitwp-for-hugo) - A python script which works with the xml export from Wordpress and converts Wordpress pages and posts to Markdown and YAML for hugo.
-- [blog2md](https://github.com/palaniraja/blog2md) - Works with [exported xml](https://en.support.wordpress.com/export/) file of your free YOUR-TLD.wordpress.com website. It also saves approved comments to `YOUR-POST-NAME-comments.md` file along with posts.
-
-## Tumblr
-
-- [tumblr-importr](https://github.com/carlmjohnson/tumblr-importr) - An importer that uses the Tumblr API to create a Hugo static site.
-- [tumblr2hugomarkdown](https://github.com/Wysie/tumblr2hugomarkdown) - Export all your Tumblr content to Hugo Markdown files with preserved original formatting.
-- [Tumblr to Hugo](https://github.com/jipiboily/tumblr-to-hugo) - A migration tool that converts each of your Tumblr posts to a content file with a proper title and path. Furthermore, "Tumblr to Hugo" creates a CSV file with the original URL and the new path on Hugo, to help you setup the redirections.
-
-## Drupal
-
-- [drupal2hugo](https://github.com/danapsimer/drupal2hugo) - Convert a Drupal site to Hugo.
-
-## Joomla
-
-- [hugojoomla](https://github.com/davetcc/hugojoomla) - This utility written in Java takes a Joomla database and converts all the content into Markdown files. It changes any URLs that are in Joomla's internal format and converts them to a suitable form.
-
-## Blogger
-
-- [blogimport](https://github.com/natefinch/blogimport) - A tool to import from Blogger posts to Hugo.
-- [blogger-to-hugo](https://bitbucket.org/petraszd/blogger-to-hugo) - Another tool to import Blogger posts to Hugo. It also downloads embedded images so they will be stored locally.
-- [blog2md](https://github.com/palaniraja/blog2md) - Works with [exported xml](https://support.google.com/blogger/answer/41387?hl=en) file of your YOUR-TLD.blogspot.com website. It also saves comments to `YOUR-POST-NAME-comments.md` file along with posts.
-- [BloggerToHugo](https://github.com/huanlin/blogger-to-hugo) - Yet another tool to import Blogger posts to Hugo. For Windows platform only, and .NET Framework 4.5 is required. See README.md before using this tool.
-
-## Contentful
-
-- [contentful2hugo](https://github.com/ArnoNuyts/contentful2hugo) - A tool to create content-files for Hugo from content on [Contentful](https://www.contentful.com/).
-
-
-## BlogML
-
-- [BlogML2Hugo](https://github.com/jijiechen/BlogML2Hugo) - A tool that helps you convert BlogML xml file to Hugo markdown files. Users need to take care of links to attachments and images by themselves. This helps the blogs that export BlogML files (e.g. BlogEngine.NET) tramsform to hugo sites easily.
diff --git a/create/content.go b/create/content.go
new file mode 100644
index 000000000..e48dfc078
--- /dev/null
+++ b/create/content.go
@@ -0,0 +1,313 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+
+ "github.com/pkg/errors"
+
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "github.com/gohugoio/hugo/hugofs"
+
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugolib"
+ "github.com/spf13/afero"
+ 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(
+ sites *hugolib.HugoSites, kind, targetPath string) error {
+ targetPath = filepath.Clean(targetPath)
+ ext := helpers.Ext(targetPath)
+ ps := sites.PathSpec
+ archetypeFs := ps.BaseFs.SourceFilesystems.Archetypes.Fs
+ sourceFs := ps.Fs.Source
+
+ jww.INFO.Printf("attempting to create %q of %q of ext %q", targetPath, kind, ext)
+
+ archetypeFilename, isDir := findArchetype(ps, kind, ext)
+ contentPath, s := resolveContentPath(sites, sourceFs, targetPath)
+
+ if isDir {
+
+ langFs := hugofs.NewLanguageFs(s.Language().Lang, sites.LanguageSet(), archetypeFs)
+
+ cm, err := mapArcheTypeDir(ps, langFs, archetypeFilename)
+ if err != nil {
+ return err
+ }
+
+ if cm.siteUsed {
+ if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
+ return err
+ }
+ }
+
+ name := filepath.Base(targetPath)
+ return newContentFromDir(archetypeFilename, sites, archetypeFs, sourceFs, cm, name, contentPath)
+ }
+
+ // Building the sites can be expensive, so only do it if really needed.
+ siteUsed := false
+
+ if archetypeFilename != "" {
+
+ var err error
+ siteUsed, err = usesSiteVar(archetypeFs, archetypeFilename)
+ if err != nil {
+ return err
+ }
+ }
+
+ if siteUsed {
+ if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
+ return err
+ }
+ }
+
+ content, err := executeArcheTypeAsTemplate(s, "", kind, targetPath, archetypeFilename)
+ if err != nil {
+ return err
+ }
+
+ 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
+}
+
+func targetSite(sites *hugolib.HugoSites, fi *hugofs.LanguageFileInfo) *hugolib.Site {
+ for _, s := range sites.Sites {
+ if fi.Lang() == s.Language().Lang {
+ return s
+ }
+ }
+ return sites.Sites[0]
+}
+
+func newContentFromDir(
+ archetypeDir string,
+ sites *hugolib.HugoSites,
+ sourceFs, targetFs afero.Fs,
+ cm archetypeMap, name, targetPath string) error {
+
+ for _, f := range cm.otherFiles {
+ filename := f.Filename()
+ // Just copy the file to destination.
+ in, err := sourceFs.Open(filename)
+ if err != nil {
+ return errors.Wrap(err, "failed to open non-content file")
+ }
+
+ targetFilename := filepath.Join(targetPath, strings.TrimPrefix(filename, archetypeDir))
+
+ targetDir := filepath.Dir(targetFilename)
+ if err := targetFs.MkdirAll(targetDir, 0777); err != nil && !os.IsExist(err) {
+ return errors.Wrapf(err, "failed to create target directory for %s:", targetDir)
+ }
+
+ out, err := targetFs.Create(targetFilename)
+ if err != nil {
+ return err
+ }
+
+ _, err = io.Copy(out, in)
+ if err != nil {
+ return err
+ }
+
+ in.Close()
+ out.Close()
+ }
+
+ for _, f := range cm.contentFiles {
+ filename := f.Filename()
+ s := targetSite(sites, f)
+ targetFilename := filepath.Join(targetPath, strings.TrimPrefix(filename, archetypeDir))
+
+ content, err := executeArcheTypeAsTemplate(s, name, archetypeDir, targetFilename, filename)
+ if err != nil {
+ return errors.Wrap(err, "failed to execute archetype template")
+ }
+
+ if err := helpers.SafeWriteToDisk(targetFilename, bytes.NewReader(content), targetFs); err != nil {
+ return errors.Wrap(err, "failed to save results")
+ }
+ }
+
+ jww.FEEDBACK.Println(targetPath, "created")
+
+ return nil
+}
+
+type archetypeMap struct {
+ // These needs to be parsed and executed as Go templates.
+ contentFiles []*hugofs.LanguageFileInfo
+ // These are just copied to destination.
+ otherFiles []*hugofs.LanguageFileInfo
+ // If the templates needs a fully built site. This can potentially be
+ // expensive, so only do when needed.
+ siteUsed bool
+}
+
+func mapArcheTypeDir(
+ ps *helpers.PathSpec,
+ fs afero.Fs,
+ archetypeDir string) (archetypeMap, error) {
+
+ var m archetypeMap
+
+ walkFn := func(filename string, fi os.FileInfo, err error) error {
+
+ if err != nil {
+ return err
+ }
+
+ if fi.IsDir() {
+ return nil
+ }
+
+ fil := fi.(*hugofs.LanguageFileInfo)
+
+ if hugolib.IsContentFile(filename) {
+ m.contentFiles = append(m.contentFiles, fil)
+ if !m.siteUsed {
+ m.siteUsed, err = usesSiteVar(fs, filename)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+ }
+
+ m.otherFiles = append(m.otherFiles, fil)
+
+ return nil
+ }
+
+ if err := helpers.SymbolicWalk(fs, archetypeDir, walkFn); err != nil {
+ return m, errors.Wrapf(err, "failed to walk archetype dir %q", archetypeDir)
+ }
+
+ return m, nil
+}
+
+func usesSiteVar(fs afero.Fs, filename string) (bool, error) {
+ f, err := fs.Open(filename)
+ if err != nil {
+ return false, errors.Wrap(err, "failed to open archetype file")
+ }
+ defer f.Close()
+ return helpers.ReaderContains(f, []byte(".Site")), nil
+}
+
+// Resolve the target content path.
+func resolveContentPath(sites *hugolib.HugoSites, fs afero.Fs, targetPath string) (string, *hugolib.Site) {
+ targetDir := filepath.Dir(targetPath)
+ first := sites.Sites[0]
+
+ var (
+ s *hugolib.Site
+ siteContentDir string
+ )
+
+ // Try the filename: my-post.en.md
+ for _, ss := range sites.Sites {
+ if strings.Contains(targetPath, "."+ss.Language().Lang+".") {
+ s = ss
+ break
+ }
+ }
+
+ for _, ss := range sites.Sites {
+ contentDir := ss.PathSpec.ContentDir
+ if !strings.HasSuffix(contentDir, helpers.FilePathSeparator) {
+ contentDir += helpers.FilePathSeparator
+ }
+ if strings.HasPrefix(targetPath, contentDir) {
+ siteContentDir = ss.PathSpec.ContentDir
+ if s == nil {
+ s = ss
+ }
+ break
+ }
+ }
+
+ if s == nil {
+ s = first
+ }
+
+ if targetDir != "" && targetDir != "." {
+ exists, _ := helpers.Exists(targetDir, fs)
+
+ if exists {
+ return targetPath, s
+ }
+ }
+
+ if siteContentDir != "" {
+ pp := filepath.Join(siteContentDir, strings.TrimPrefix(targetPath, siteContentDir))
+ return s.PathSpec.AbsPathify(pp), s
+
+ } else {
+ return s.PathSpec.AbsPathify(filepath.Join(first.PathSpec.ContentDir, targetPath)), s
+ }
+
+}
+
+// FindArchetype takes a given kind/archetype of content and returns the path
+// to the archetype in the archetype filesystem, blank if none found.
+func findArchetype(ps *helpers.PathSpec, kind, ext string) (outpath string, isDir bool) {
+ fs := ps.BaseFs.Archetypes.Fs
+
+ var pathsToCheck []string
+
+ if kind != "" {
+ pathsToCheck = append(pathsToCheck, kind+ext)
+ }
+ pathsToCheck = append(pathsToCheck, "default"+ext, "default")
+
+ for _, p := range pathsToCheck {
+ fi, err := fs.Stat(p)
+ if err == nil {
+ return p, fi.IsDir()
+ }
+ }
+
+ return "", false
+}
diff --git a/create/content_template_handler.go b/create/content_template_handler.go
new file mode 100644
index 000000000..5a8b4f63c
--- /dev/null
+++ b/create/content_template_handler.go
@@ -0,0 +1,146 @@
+// 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"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/pkg/errors"
+
+ "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.SiteInfo
+
+ // Name will in most cases be the same as TranslationBaseName, e.g. "my-post".
+ // But if that value is "index" (bundles), the Name is instead the owning folder.
+ // This is the value you in most cases would want to use to construct the title in your
+ // archetype template.
+ Name string
+
+ // The target content file. Note that the .Content will be empty, as that
+ // has not been created yet.
+ source.File
+}
+
+const (
+ // ArchetypeTemplateTemplate is used as initial template when adding an archetype template.
+ ArchetypeTemplateTemplate = `---
+title: "{{ replace .Name "-" " " | 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, name, kind, targetPath, archetypeFilename string) ([]byte, error) {
+
+ var (
+ archetypeContent []byte
+ archetypeTemplate []byte
+ err error
+ )
+
+ f := s.SourceSpec.NewFileInfo("", targetPath, false, nil)
+
+ if name == "" {
+ name = f.TranslationBaseName()
+
+ if name == "index" || name == "_index" {
+ // Page bundles; the directory name will hopefully have a better name.
+ dir := strings.TrimSuffix(f.Dir(), helpers.FilePathSeparator)
+ _, name = filepath.Split(dir)
+ }
+ }
+
+ data := ArchetypeFileData{
+ Type: kind,
+ Date: time.Now().Format(time.RFC3339),
+ Name: name,
+ File: f,
+ Site: &s.Info,
+ }
+
+ if archetypeFilename == "" {
+ // TODO(bep) archetype revive the issue about wrong tpl funcs arg order
+ archetypeTemplate = []byte(ArchetypeTemplateTemplate)
+ } else {
+ archetypeTemplate, err = afero.ReadFile(s.BaseFs.Archetypes.Fs, archetypeFilename)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read archetype file %s", 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, errors.Wrapf(err, "Failed to parse archetype file %q:", archetypeFilename)
+ }
+
+ templ, _ := templateHandler.Lookup(templateName)
+
+ var buff bytes.Buffer
+ if err := templ.Execute(&buff, data); err != nil {
+ return nil, errors.Wrapf(err, "Failed to process archetype file %q:", archetypeFilename)
+ }
+
+ archetypeContent = []byte(archetypeShortcodeReplacementsPost.Replace(buff.String()))
+
+ return archetypeContent, nil
+
+}
diff --git a/create/content_test.go b/create/content_test.go
new file mode 100644
index 000000000..e321900bc
--- /dev/null
+++ b/create/content_test.go
@@ -0,0 +1,272 @@
+// 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) {
+ assert := require.New(t)
+
+ 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
+ {"lang", "post/lang-1.md", []string{`Site Lang: en|Name: Lang 1|i18n: Hugo Rocks!`}},
+ {"lang", "post/lang-2.en.md", []string{`Site Lang: en|Name: Lang 2|i18n: Hugo Rocks!`}},
+ {"lang", "post/lang-3.nn.md", []string{`Site Lang: nn|Name: Lang 3|i18n: Hugo Rokkar!`}},
+ {"lang", "content_nn/post/lang-4.md", []string{`Site Lang: nn|Name: Lang 4|i18n: Hugo Rokkar!`}},
+ {"lang", "content_nn/post/lang-5.en.md", []string{`Site Lang: en|Name: Lang 5|i18n: Hugo Rocks!`}},
+ {"lang", "post/my-bundle/index.md", []string{`Site Lang: en|Name: My Bundle|i18n: Hugo Rocks!`}},
+ {"lang", "post/my-bundle/index.en.md", []string{`Site Lang: en|Name: My Bundle|i18n: Hugo Rocks!`}},
+ {"lang", "post/my-bundle/index.nn.md", []string{`Site Lang: nn|Name: My Bundle|i18n: Hugo Rokkar!`}},
+ {"shortcodes", "shortcodes/go.md", []string{
+ `title = "GO"`,
+ "{{< myshortcode >}}",
+ "{{% myshortcode %}}",
+ "{{</* comment */>}}\n{{%/* comment */%}}"}}, // shortcodes
+ }
+
+ for i, c := range cases {
+ cfg, fs := newTestCfg(assert)
+ assert.NoError(initFs(fs))
+ h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs})
+ assert.NoError(err)
+
+ assert.NoError(create.NewContent(h, c.kind, c.path))
+
+ fname := filepath.FromSlash(c.path)
+ if !strings.HasPrefix(fname, "content") {
+ fname = filepath.Join("content", fname)
+ }
+ content := readFileFromFs(t, fs.Source, fname)
+ for _, v := range c.expected {
+ found := strings.Contains(content, v)
+ if !found {
+ t.Fatalf("[%d] %q missing from output:\n%q", i, v, content)
+ }
+ }
+ }
+}
+
+func TestNewContentFromDir(t *testing.T) {
+ assert := require.New(t)
+ cfg, fs := newTestCfg(assert)
+ assert.NoError(initFs(fs))
+
+ archetypeDir := filepath.Join("archetypes", "my-bundle")
+ assert.NoError(fs.Source.Mkdir(archetypeDir, 0755))
+
+ archetypeThemeDir := filepath.Join("themes", "mytheme", "archetypes", "my-theme-bundle")
+ assert.NoError(fs.Source.Mkdir(archetypeThemeDir, 0755))
+
+ contentFile := `
+File: %s
+Site Lang: {{ .Site.Language.Lang }}
+Name: {{ replace .Name "-" " " | title }}
+i18n: {{ T "hugo" }}
+`
+
+ assert.NoError(afero.WriteFile(fs.Source, filepath.Join(archetypeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0755))
+ assert.NoError(afero.WriteFile(fs.Source, filepath.Join(archetypeDir, "index.nn.md"), []byte(fmt.Sprintf(contentFile, "index.nn.md")), 0755))
+
+ assert.NoError(afero.WriteFile(fs.Source, filepath.Join(archetypeDir, "pages", "bio.md"), []byte(fmt.Sprintf(contentFile, "bio.md")), 0755))
+ assert.NoError(afero.WriteFile(fs.Source, filepath.Join(archetypeDir, "resources", "hugo1.json"), []byte(`hugo1: {{ printf "no template handling in here" }}`), 0755))
+ assert.NoError(afero.WriteFile(fs.Source, filepath.Join(archetypeDir, "resources", "hugo2.xml"), []byte(`hugo2: {{ printf "no template handling in here" }}`), 0755))
+
+ assert.NoError(afero.WriteFile(fs.Source, filepath.Join(archetypeThemeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0755))
+ assert.NoError(afero.WriteFile(fs.Source, filepath.Join(archetypeThemeDir, "resources", "hugo1.json"), []byte(`hugo1: {{ printf "no template handling in here" }}`), 0755))
+
+ h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs})
+ assert.NoError(err)
+ assert.Equal(2, len(h.Sites))
+
+ assert.NoError(create.NewContent(h, "my-bundle", "post/my-post"))
+
+ assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/resources/hugo1.json")), `hugo1: {{ printf "no template handling in here" }}`)
+ assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/resources/hugo2.xml")), `hugo2: {{ printf "no template handling in here" }}`)
+
+ // Content files should get the correct site context.
+ // TODO(bep) archetype check i18n
+ assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/index.md")), `File: index.md`, `Site Lang: en`, `Name: My Post`, `i18n: Hugo Rocks!`)
+ assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/index.nn.md")), `File: index.nn.md`, `Site Lang: nn`, `Name: My Post`, `i18n: Hugo Rokkar!`)
+
+ assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/pages/bio.md")), `File: bio.md`, `Site Lang: en`, `Name: My Post`)
+
+ assert.NoError(create.NewContent(h, "my-theme-bundle", "post/my-theme-post"))
+ assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-theme-post/index.md")), `File: index.md`, `Site Lang: en`, `Name: My Theme Post`, `i18n: Hugo Rocks!`)
+ assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-theme-post/resources/hugo1.json")), `hugo1: {{ printf "no template handling in here" }}`)
+
+}
+
+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",
+ },
+ {
+ path: filepath.Join("archetypes", "lang.md"),
+ content: `Site Lang: {{ .Site.Language.Lang }}|Name: {{ replace .Name "-" " " | title }}|i18n: {{ T "hugo" }}`,
+ },
+ // #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
+}
+
+func assertContains(assert *require.Assertions, v interface{}, matches ...string) {
+ for _, m := range matches {
+ assert.Contains(v, m)
+ }
+}
+
+// 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(assert *require.Assertions) (*viper.Viper, *hugofs.Fs) {
+
+ cfg := `
+
+theme = "mytheme"
+
+[languages]
+[languages.en]
+weight = 1
+languageName = "English"
+[languages.nn]
+weight = 2
+languageName = "Nynorsk"
+contentDir = "content_nn"
+
+`
+
+ mm := afero.NewMemMapFs()
+
+ assert.NoError(afero.WriteFile(mm, filepath.Join("i18n", "en.toml"), []byte(`[hugo]
+other = "Hugo Rocks!"`), 0755))
+ assert.NoError(afero.WriteFile(mm, filepath.Join("i18n", "nn.toml"), []byte(`[hugo]
+other = "Hugo Rokkar!"`), 0755))
+
+ assert.NoError(afero.WriteFile(mm, "config.toml", []byte(cfg), 0755))
+
+ v, _, err := hugolib.LoadConfig(hugolib.ConfigSourceDescriptor{Fs: mm, Filename: "config.toml"})
+ assert.NoError(err)
+
+ return v, hugofs.NewFrom(mm, v)
+
+}
diff --git a/deploy/cloudfront.go b/deploy/cloudfront.go
new file mode 100644
index 000000000..dbdf9baf4
--- /dev/null
+++ b/deploy/cloudfront.go
@@ -0,0 +1,51 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package deploy
+
+import (
+ "context"
+ "time"
+
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/session"
+ "github.com/aws/aws-sdk-go/service/cloudfront"
+)
+
+// InvalidateCloudFront invalidates the CloudFront cache for distributionID.
+// It uses the default AWS credentials from the environment.
+func InvalidateCloudFront(ctx context.Context, distributionID string) error {
+ // SharedConfigEnable enables loading "shared config (~/.aws/config) and
+ // shared credentials (~/.aws/credentials) files".
+ // See https://docs.aws.amazon.com/sdk-for-go/api/aws/session/ for more
+ // details.
+ // This is the same codepath used by Go CDK when creating an s3 URL.
+ // TODO: Update this to a Go CDK helper once available
+ // (https://github.com/google/go-cloud/issues/2003).
+ sess, err := session.NewSessionWithOptions(session.Options{SharedConfigState: session.SharedConfigEnable})
+ if err != nil {
+ return err
+ }
+ req := &cloudfront.CreateInvalidationInput{
+ DistributionId: aws.String(distributionID),
+ InvalidationBatch: &cloudfront.InvalidationBatch{
+ CallerReference: aws.String(time.Now().Format("20060102150405")),
+ Paths: &cloudfront.Paths{
+ Items: []*string{aws.String("/*")},
+ Quantity: aws.Int64(1),
+ },
+ },
+ }
+ _, err = cloudfront.New(sess).CreateInvalidationWithContext(ctx, req)
+ return err
+}
diff --git a/deploy/deploy.go b/deploy/deploy.go
new file mode 100644
index 000000000..40c49c2e5
--- /dev/null
+++ b/deploy/deploy.go
@@ -0,0 +1,641 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package deploy
+
+import (
+ "bytes"
+ "compress/gzip"
+ "context"
+ "crypto/md5"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "mime"
+ "os"
+ "path/filepath"
+ "regexp"
+ "runtime"
+ "sort"
+ "strings"
+ "sync"
+
+ "github.com/dustin/go-humanize"
+ "github.com/gohugoio/hugo/config"
+ "github.com/pkg/errors"
+ "github.com/spf13/afero"
+ jww "github.com/spf13/jwalterweatherman"
+ "golang.org/x/text/unicode/norm"
+
+ "gocloud.dev/blob"
+ _ "gocloud.dev/blob/azureblob" // import
+ _ "gocloud.dev/blob/fileblob" // import
+ _ "gocloud.dev/blob/gcsblob" // import
+ _ "gocloud.dev/blob/s3blob" // import
+)
+
+// Deployer supports deploying the site to target cloud providers.
+type Deployer struct {
+ localFs afero.Fs
+ bucket *blob.Bucket
+
+ target *target // the target to deploy to
+ matchers []*matcher // matchers to apply to uploaded files
+ ordering []*regexp.Regexp // orders uploads
+ quiet bool // true reduces STDOUT
+ confirm bool // true enables confirmation before making changes
+ dryRun bool // true skips conformations and prints changes instead of applying them
+ force bool // true forces upload of all files
+ invalidateCDN bool // true enables invalidate CDN cache (if possible)
+ maxDeletes int // caps the # of files to delete; -1 to disable
+
+ // For tests...
+ summary deploySummary // summary of latest Deploy results
+}
+
+type deploySummary struct {
+ NumLocal, NumRemote, NumUploads, NumDeletes int
+}
+
+// New constructs a new *Deployer.
+func New(cfg config.Provider, localFs afero.Fs) (*Deployer, error) {
+ targetName := cfg.GetString("target")
+
+ // Load the [deployment] section of the config.
+ dcfg, err := decodeConfig(cfg)
+ if err != nil {
+ return nil, err
+ }
+
+ // Find the target to deploy to.
+ var tgt *target
+ for _, t := range dcfg.Targets {
+ if t.Name == targetName {
+ tgt = t
+ }
+ }
+ if tgt == nil {
+ return nil, fmt.Errorf("deployment target %q not found", targetName)
+ }
+ return &Deployer{
+ localFs: localFs,
+ target: tgt,
+ matchers: dcfg.Matchers,
+ ordering: dcfg.ordering,
+ quiet: cfg.GetBool("quiet"),
+ confirm: cfg.GetBool("confirm"),
+ dryRun: cfg.GetBool("dryRun"),
+ force: cfg.GetBool("force"),
+ invalidateCDN: cfg.GetBool("invalidateCDN"),
+ maxDeletes: cfg.GetInt("maxDeletes"),
+ }, nil
+}
+
+func (d *Deployer) openBucket(ctx context.Context) (*blob.Bucket, error) {
+ if d.bucket != nil {
+ return d.bucket, nil
+ }
+ return blob.OpenBucket(ctx, d.target.URL)
+}
+
+// Deploy deploys the site to a target.
+func (d *Deployer) Deploy(ctx context.Context) error {
+ // TODO: This opens the root path in the bucket/container.
+ // Consider adding support for targeting a subdirectory.
+ bucket, err := d.openBucket(ctx)
+ if err != nil {
+ return err
+ }
+
+ // Load local files from the source directory.
+ local, err := walkLocal(d.localFs, d.matchers)
+ if err != nil {
+ return err
+ }
+ jww.INFO.Printf("Found %d local files.\n", len(local))
+ d.summary.NumLocal = len(local)
+
+ // Load remote files from the target.
+ remote, err := walkRemote(ctx, bucket)
+ if err != nil {
+ return err
+ }
+ jww.INFO.Printf("Found %d remote files.\n", len(remote))
+ d.summary.NumRemote = len(remote)
+
+ // Diff local vs remote to see what changes need to be applied.
+ uploads, deletes := findDiffs(local, remote, d.force)
+ if err != nil {
+ return err
+ }
+ d.summary.NumUploads = len(uploads)
+ d.summary.NumDeletes = len(deletes)
+ if len(uploads)+len(deletes) == 0 {
+ if !d.quiet {
+ jww.FEEDBACK.Println("No changes required.")
+ }
+ return nil
+ }
+ if !d.quiet {
+ jww.FEEDBACK.Println(summarizeChanges(uploads, deletes))
+ }
+
+ // Ask for confirmation before proceeding.
+ if d.confirm && !d.dryRun {
+ fmt.Printf("Continue? (Y/n) ")
+ var confirm string
+ if _, err := fmt.Scanln(&confirm); err != nil {
+ return err
+ }
+ if confirm != "" && confirm[0] != 'y' && confirm[0] != 'Y' {
+ return errors.New("aborted")
+ }
+ }
+
+ // Order the uploads. They are organized in groups; all uploads in a group
+ // must be complete before moving on to the next group.
+ uploadGroups := applyOrdering(d.ordering, uploads)
+
+ // Apply the changes in parallel, using an inverted worker
+ // pool (https://www.youtube.com/watch?v=5zXAHh5tJqQ&t=26m58s).
+ // sem prevents more than nParallel concurrent goroutines.
+ const nParallel = 10
+ var errs []error
+ var errMu sync.Mutex // protects errs
+
+ for _, uploads := range uploadGroups {
+ // Short-circuit for an empty group.
+ if len(uploads) == 0 {
+ continue
+ }
+
+ // Within the group, apply uploads in parallel.
+ sem := make(chan struct{}, nParallel)
+ for _, upload := range uploads {
+ if d.dryRun {
+ if !d.quiet {
+ jww.FEEDBACK.Printf("[DRY RUN] Would upload: %v\n", upload)
+ }
+ continue
+ }
+
+ sem <- struct{}{}
+ go func(upload *fileToUpload) {
+ if err := doSingleUpload(ctx, bucket, upload); err != nil {
+ errMu.Lock()
+ defer errMu.Unlock()
+ errs = append(errs, err)
+ }
+ <-sem
+ }(upload)
+ }
+ // Wait for all uploads in the group to finish.
+ for n := nParallel; n > 0; n-- {
+ sem <- struct{}{}
+ }
+ }
+
+ if d.maxDeletes != -1 && len(deletes) > d.maxDeletes {
+ jww.WARN.Printf("Skipping %d deletes because it is more than --maxDeletes (%d). If this is expected, set --maxDeletes to a larger number, or -1 to disable this check.\n", len(deletes), d.maxDeletes)
+ d.summary.NumDeletes = 0
+ } else {
+ // Apply deletes in parallel.
+ sort.Slice(deletes, func(i, j int) bool { return deletes[i] < deletes[j] })
+ sem := make(chan struct{}, nParallel)
+ for _, del := range deletes {
+ if d.dryRun {
+ if !d.quiet {
+ jww.FEEDBACK.Printf("[DRY RUN] Would delete %s\n", del)
+ }
+ continue
+ }
+ sem <- struct{}{}
+ go func(del string) {
+ jww.INFO.Printf("Deleting %s...\n", del)
+ if err := bucket.Delete(ctx, del); err != nil {
+ errMu.Lock()
+ defer errMu.Unlock()
+ errs = append(errs, err)
+ }
+ <-sem
+ }(del)
+ }
+ // Wait for all deletes to finish.
+ for n := nParallel; n > 0; n-- {
+ sem <- struct{}{}
+ }
+ }
+ if len(errs) > 0 {
+ if !d.quiet {
+ jww.FEEDBACK.Printf("Encountered %d errors.\n", len(errs))
+ }
+ return errs[0]
+ }
+ if !d.quiet {
+ jww.FEEDBACK.Println("Success!")
+ }
+
+ if d.invalidateCDN && d.target.CloudFrontDistributionID != "" {
+ jww.FEEDBACK.Println("Invalidating CloudFront CDN...")
+ if err := InvalidateCloudFront(ctx, d.target.CloudFrontDistributionID); err != nil {
+ jww.FEEDBACK.Printf("Failed to invalidate CloudFront CDN: %v\n", err)
+ return err
+ }
+ jww.FEEDBACK.Println("Success!")
+ }
+ return nil
+}
+
+// summarizeChanges creates a text description of the proposed changes.
+func summarizeChanges(uploads []*fileToUpload, deletes []string) string {
+ uploadSize := int64(0)
+ for _, u := range uploads {
+ uploadSize += u.Local.UploadSize
+ }
+ return fmt.Sprintf("Identified %d file(s) to upload, totaling %s, and %d file(s) to delete.", len(uploads), humanize.Bytes(uint64(uploadSize)), len(deletes))
+}
+
+// doSingleUpload executes a single file upload.
+func doSingleUpload(ctx context.Context, bucket *blob.Bucket, upload *fileToUpload) error {
+ jww.INFO.Printf("Uploading %v...\n", upload)
+ opts := &blob.WriterOptions{
+ CacheControl: upload.Local.CacheControl(),
+ ContentEncoding: upload.Local.ContentEncoding(),
+ ContentType: upload.Local.ContentType(),
+ }
+ w, err := bucket.NewWriter(ctx, upload.Local.SlashPath, opts)
+ if err != nil {
+ return err
+ }
+ r, err := upload.Local.Reader()
+ if err != nil {
+ return err
+ }
+ defer r.Close()
+ _, err = io.Copy(w, r)
+ if err != nil {
+ return err
+ }
+ if err := w.Close(); err != nil {
+ return err
+ }
+ return nil
+}
+
+// localFile represents a local file from the source. Use newLocalFile to
+// construct one.
+type localFile struct {
+ // NativePath is the native path to the file (using file.Separator).
+ NativePath string
+ // SlashPath is NativePath converted to use /.
+ SlashPath string
+ // UploadSize is the size of the content to be uploaded. It may not
+ // be the same as the local file size if the content will be
+ // gzipped before upload.
+ UploadSize int64
+
+ fs afero.Fs
+ matcher *matcher
+ md5 []byte // cache
+ gzipped bytes.Buffer // cached of gzipped contents if gzipping
+}
+
+// newLocalFile initializes a *localFile.
+func newLocalFile(fs afero.Fs, nativePath, slashpath string, m *matcher) (*localFile, error) {
+ f, err := fs.Open(nativePath)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ lf := &localFile{
+ NativePath: nativePath,
+ SlashPath: slashpath,
+ fs: fs,
+ matcher: m,
+ }
+ if m != nil && m.Gzip {
+ // We're going to gzip the content. Do it once now, and cache the result
+ // in gzipped. The UploadSize is the size of the gzipped content.
+ gz := gzip.NewWriter(&lf.gzipped)
+ if _, err := io.Copy(gz, f); err != nil {
+ return nil, err
+ }
+ if err := gz.Close(); err != nil {
+ return nil, err
+ }
+ lf.UploadSize = int64(lf.gzipped.Len())
+ } else {
+ // Raw content. Just get the UploadSize.
+ info, err := f.Stat()
+ if err != nil {
+ return nil, err
+ }
+ lf.UploadSize = info.Size()
+ }
+ return lf, nil
+}
+
+// Reader returns an io.ReadCloser for reading the content to be uploaded.
+// The caller must call Close on the returned ReaderCloser.
+// The reader content may not be the same as the local file content due to
+// gzipping.
+func (lf *localFile) Reader() (io.ReadCloser, error) {
+ if lf.matcher != nil && lf.matcher.Gzip {
+ // We've got the gzipped contents cached in gzipped.
+ // Note: we can't use lf.gzipped directly as a Reader, since we it discards
+ // data after it is read, and we may read it more than once.
+ return ioutil.NopCloser(bytes.NewReader(lf.gzipped.Bytes())), nil
+ }
+ // Not expected to fail since we did it successfully earlier in newLocalFile,
+ // but could happen due to changes in the underlying filesystem.
+ return lf.fs.Open(lf.NativePath)
+}
+
+// CacheControl returns the Cache-Control header to use for lf, based on the
+// first matching matcher (if any).
+func (lf *localFile) CacheControl() string {
+ if lf.matcher == nil {
+ return ""
+ }
+ return lf.matcher.CacheControl
+}
+
+// ContentEncoding returns the Content-Encoding header to use for lf, based
+// on the matcher's Content-Encoding and Gzip fields.
+func (lf *localFile) ContentEncoding() string {
+ if lf.matcher == nil {
+ return ""
+ }
+ if lf.matcher.Gzip {
+ return "gzip"
+ }
+ return lf.matcher.ContentEncoding
+}
+
+// ContentType returns the Content-Type header to use for lf.
+// It first checks if there's a Content-Type header configured via a matching
+// matcher; if not, it tries to generate one based on the filename extension.
+// If this fails, the Content-Type will be the empty string. In this case, Go
+// Cloud will automatically try to infer a Content-Type based on the file
+// content.
+func (lf *localFile) ContentType() string {
+ if lf.matcher != nil && lf.matcher.ContentType != "" {
+ return lf.matcher.ContentType
+ }
+ // TODO: Hugo has a MediaType and a MediaTypes list and also a concept
+ // of custom MIME types.
+ // Use 1) The matcher 2) Hugo's MIME types 3) TypeByExtension.
+ return mime.TypeByExtension(filepath.Ext(lf.NativePath))
+}
+
+// Force returns true if the file should be forced to re-upload based on the
+// matching matcher.
+func (lf *localFile) Force() bool {
+ return lf.matcher != nil && lf.matcher.Force
+}
+
+// MD5 returns an MD5 hash of the content to be uploaded.
+func (lf *localFile) MD5() []byte {
+ if len(lf.md5) > 0 {
+ return lf.md5
+ }
+ h := md5.New()
+ r, err := lf.Reader()
+ if err != nil {
+ return nil
+ }
+ defer r.Close()
+ if _, err := io.Copy(h, r); err != nil {
+ return nil
+ }
+ lf.md5 = h.Sum(nil)
+ return lf.md5
+}
+
+// walkLocal walks the source directory and returns a flat list of files,
+// using localFile.SlashPath as the map keys.
+func walkLocal(fs afero.Fs, matchers []*matcher) (map[string]*localFile, error) {
+ retval := map[string]*localFile{}
+ err := afero.Walk(fs, "", func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if info.IsDir() {
+ // Skip hidden directories.
+ if path != "" && strings.HasPrefix(info.Name(), ".") {
+ return filepath.SkipDir
+ }
+ return nil
+ }
+
+ // .DS_Store is an internal MacOS attribute file; skip it.
+ if info.Name() == ".DS_Store" {
+ return nil
+ }
+
+ // When a file system is HFS+, its filepath is in NFD form.
+ if runtime.GOOS == "darwin" {
+ path = norm.NFC.String(path)
+ }
+
+ // Find the first matching matcher (if any).
+ slashpath := filepath.ToSlash(path)
+ var m *matcher
+ for _, cur := range matchers {
+ if cur.Matches(slashpath) {
+ m = cur
+ break
+ }
+ }
+ lf, err := newLocalFile(fs, path, slashpath, m)
+ if err != nil {
+ return err
+ }
+ retval[lf.SlashPath] = lf
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ return retval, nil
+}
+
+// walkRemote walks the target bucket and returns a flat list.
+func walkRemote(ctx context.Context, bucket *blob.Bucket) (map[string]*blob.ListObject, error) {
+ retval := map[string]*blob.ListObject{}
+ iter := bucket.List(nil)
+ for {
+ obj, err := iter.Next(ctx)
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+ // If the remote didn't give us an MD5, compute one.
+ // This can happen for some providers (e.g., fileblob, which uses the
+ // local filesystem), but not for the most common Cloud providers
+ // (S3, GCS, Azure). Although, it can happen for S3 if the blob was uploaded
+ // via a multi-part upload.
+ // Although it's unfortunate to have to read the file, it's likely better
+ // than assuming a delta and re-uploading it.
+ if len(obj.MD5) == 0 {
+ r, err := bucket.NewReader(ctx, obj.Key, nil)
+ if err == nil {
+ h := md5.New()
+ if _, err := io.Copy(h, r); err == nil {
+ obj.MD5 = h.Sum(nil)
+ }
+ r.Close()
+ }
+ }
+ retval[obj.Key] = obj
+ }
+ return retval, nil
+}
+
+// uploadReason is an enum of reasons why a file must be uploaded.
+type uploadReason string
+
+const (
+ reasonUnknown uploadReason = "unknown"
+ reasonNotFound uploadReason = "not found at target"
+ reasonForce uploadReason = "--force"
+ reasonSize uploadReason = "size differs"
+ reasonMD5Differs uploadReason = "md5 differs"
+ reasonMD5Missing uploadReason = "remote md5 missing"
+)
+
+// fileToUpload represents a single local file that should be uploaded to
+// the target.
+type fileToUpload struct {
+ Local *localFile
+ Reason uploadReason
+}
+
+func (u *fileToUpload) String() string {
+ details := []string{humanize.Bytes(uint64(u.Local.UploadSize))}
+ if s := u.Local.CacheControl(); s != "" {
+ details = append(details, fmt.Sprintf("Cache-Control: %q", s))
+ }
+ if s := u.Local.ContentEncoding(); s != "" {
+ details = append(details, fmt.Sprintf("Content-Encoding: %q", s))
+ }
+ if s := u.Local.ContentType(); s != "" {
+ details = append(details, fmt.Sprintf("Content-Type: %q", s))
+ }
+ return fmt.Sprintf("%s (%s): %v", u.Local.SlashPath, strings.Join(details, ", "), u.Reason)
+}
+
+// findDiffs diffs localFiles vs remoteFiles to see what changes should be
+// applied to the remote target. It returns a slice of *fileToUpload and a
+// slice of paths for files to delete.
+func findDiffs(localFiles map[string]*localFile, remoteFiles map[string]*blob.ListObject, force bool) ([]*fileToUpload, []string) {
+ var uploads []*fileToUpload
+ var deletes []string
+
+ found := map[string]bool{}
+ for path, lf := range localFiles {
+ upload := false
+ reason := reasonUnknown
+
+ if remoteFile, ok := remoteFiles[path]; ok {
+ // The file exists in remote. Let's see if we need to upload it anyway.
+
+ // TODO: We don't register a diff if the metadata (e.g., Content-Type
+ // header) has changed. This would be difficult/expensive to detect; some
+ // providers return metadata along with their "List" result, but others
+ // (notably AWS S3) do not, so gocloud.dev's blob.Bucket doesn't expose
+ // it in the list result. It would require a separate request per blob
+ // to fetch. At least for now, we work around this by documenting it and
+ // providing a "force" flag (to re-upload everything) and a "force" bool
+ // per matcher (to re-upload all files in a matcher whose headers may have
+ // changed).
+ // Idea: extract a sample set of 1 file per extension + 1 file per matcher
+ // and check those files?
+ if force {
+ upload = true
+ reason = reasonForce
+ } else if lf.Force() {
+ upload = true
+ reason = reasonForce
+ } else if lf.UploadSize != remoteFile.Size {
+ upload = true
+ reason = reasonSize
+ } else if len(remoteFile.MD5) == 0 {
+ // This shouldn't happen unless the remote didn't give us an MD5 hash
+ // from List, AND we failed to compute one by reading the remote file.
+ // Default to considering the files different.
+ upload = true
+ reason = reasonMD5Missing
+ } else if !bytes.Equal(lf.MD5(), remoteFile.MD5) {
+ upload = true
+ reason = reasonMD5Differs
+ } else {
+ // Nope! Leave uploaded = false.
+ }
+ found[path] = true
+ } else {
+ // The file doesn't exist in remote.
+ upload = true
+ reason = reasonNotFound
+ }
+ if upload {
+ jww.DEBUG.Printf("%s needs to be uploaded: %v\n", path, reason)
+ uploads = append(uploads, &fileToUpload{lf, reason})
+ } else {
+ jww.DEBUG.Printf("%s exists at target and does not need to be uploaded", path)
+ }
+ }
+
+ // Remote files that weren't found locally should be deleted.
+ for path := range remoteFiles {
+ if !found[path] {
+ deletes = append(deletes, path)
+ }
+ }
+ return uploads, deletes
+}
+
+// applyOrdering returns an ordered slice of slices of uploads.
+//
+// The returned slice will have length len(ordering)+1.
+//
+// The subslice at index i, for i = 0 ... len(ordering)-1, will have all of the
+// uploads whose Local.SlashPath matched the regex at ordering[i] (but not any
+// previous ordering regex).
+// The subslice at index len(ordering) will have the remaining uploads that
+// didn't match any ordering regex.
+//
+// The subslices are sorted by Local.SlashPath.
+func applyOrdering(ordering []*regexp.Regexp, uploads []*fileToUpload) [][]*fileToUpload {
+
+ // Sort the whole slice by Local.SlashPath first.
+ sort.Slice(uploads, func(i, j int) bool { return uploads[i].Local.SlashPath < uploads[j].Local.SlashPath })
+
+ retval := make([][]*fileToUpload, len(ordering)+1)
+ for _, u := range uploads {
+ matched := false
+ for i, re := range ordering {
+ if re.MatchString(u.Local.SlashPath) {
+ retval[i] = append(retval[i], u)
+ matched = true
+ break
+ }
+ }
+ if !matched {
+ retval[len(ordering)] = append(retval[len(ordering)], u)
+ }
+ }
+ return retval
+}
diff --git a/deploy/deployConfig.go b/deploy/deployConfig.go
new file mode 100644
index 000000000..b4fa325b7
--- /dev/null
+++ b/deploy/deployConfig.go
@@ -0,0 +1,101 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package deploy
+
+import (
+ "fmt"
+ "regexp"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/mitchellh/mapstructure"
+)
+
+const deploymentConfigKey = "deployment"
+
+// deployConfig is the complete configuration for deployment.
+type deployConfig struct {
+ Targets []*target
+ Matchers []*matcher
+ Order []string
+
+ ordering []*regexp.Regexp // compiled Order
+}
+
+type target struct {
+ Name string
+ URL string
+
+ CloudFrontDistributionID string
+}
+
+// matcher represents configuration to be applied to files whose paths match
+// a specified pattern.
+type matcher struct {
+ // Pattern is the string pattern to match against paths.
+ // Matching is done against paths converted to use / as the path separator.
+ Pattern string
+
+ // CacheControl specifies caching attributes to use when serving the blob.
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
+ CacheControl string `mapstructure:"Cache-Control"`
+
+ // ContentEncoding specifies the encoding used for the blob's content, if any.
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
+ ContentEncoding string `mapstructure:"Content-Encoding"`
+
+ // ContentType specifies the MIME type of the blob being written.
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
+ ContentType string `mapstructure:"Content-Type"`
+
+ // Gzip determines whether the file should be gzipped before upload.
+ // If so, the ContentEncoding field will automatically be set to "gzip".
+ Gzip bool
+
+ // Force indicates that matching files should be re-uploaded. Useful when
+ // other route-determined metadata (e.g., ContentType) has changed.
+ Force bool
+
+ // re is Pattern compiled.
+ re *regexp.Regexp
+}
+
+func (m *matcher) Matches(path string) bool {
+ return m.re.MatchString(path)
+}
+
+// decode creates a config from a given Hugo configuration.
+func decodeConfig(cfg config.Provider) (deployConfig, error) {
+ var dcfg deployConfig
+ if !cfg.IsSet(deploymentConfigKey) {
+ return dcfg, nil
+ }
+ if err := mapstructure.WeakDecode(cfg.GetStringMap(deploymentConfigKey), &dcfg); err != nil {
+ return dcfg, err
+ }
+ var err error
+ for _, m := range dcfg.Matchers {
+ m.re, err = regexp.Compile(m.Pattern)
+ if err != nil {
+ return dcfg, fmt.Errorf("invalid deployment.matchers.pattern: %v", err)
+ }
+ }
+ for _, o := range dcfg.Order {
+ re, err := regexp.Compile(o)
+ if err != nil {
+ return dcfg, fmt.Errorf("invalid deployment.orderings.pattern: %v", err)
+ }
+ dcfg.ordering = append(dcfg.ordering, re)
+ }
+ return dcfg, nil
+}
diff --git a/deploy/deployConfig_test.go b/deploy/deployConfig_test.go
new file mode 100644
index 000000000..3f849d89c
--- /dev/null
+++ b/deploy/deployConfig_test.go
@@ -0,0 +1,137 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package deploy
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/require"
+)
+
+func TestDecodeConfigFromTOML(t *testing.T) {
+ assert := require.New(t)
+
+ tomlConfig := `
+
+someOtherValue = "foo"
+
+[deployment]
+
+order = ["o1", "o2"]
+
+[[deployment.targets]]
+Name = "name1"
+URL = "url1"
+CloudFrontDistributionID = "cdn1"
+
+[[deployment.targets]]
+name = "name2"
+url = "url2"
+cloudfrontdistributionid = "cdn2"
+
+[[deployment.matchers]]
+Pattern = "^pattern1$"
+Cache-Control = "cachecontrol1"
+Content-Encoding = "contentencoding1"
+Content-Type = "contenttype1"
+Gzip = true
+Force = true
+
+[[deployment.matchers]]
+pattern = "^pattern2$"
+cache-control = "cachecontrol2"
+content-encoding = "contentencoding2"
+content-type = "contenttype2"
+`
+ cfg, err := config.FromConfigString(tomlConfig, "toml")
+ assert.NoError(err)
+
+ dcfg, err := decodeConfig(cfg)
+ assert.NoError(err)
+
+ assert.Equal(2, len(dcfg.Order))
+ assert.Equal("o1", dcfg.Order[0])
+ assert.Equal("o2", dcfg.Order[1])
+ assert.Equal(2, len(dcfg.ordering))
+
+ assert.Equal(2, len(dcfg.Targets))
+ assert.Equal("name1", dcfg.Targets[0].Name)
+ assert.Equal("url1", dcfg.Targets[0].URL)
+ assert.Equal("cdn1", dcfg.Targets[0].CloudFrontDistributionID)
+ assert.Equal("name2", dcfg.Targets[1].Name)
+ assert.Equal("url2", dcfg.Targets[1].URL)
+ assert.Equal("cdn2", dcfg.Targets[1].CloudFrontDistributionID)
+
+ assert.Equal(2, len(dcfg.Matchers))
+ assert.Equal("^pattern1$", dcfg.Matchers[0].Pattern)
+ assert.NotNil(dcfg.Matchers[0].re)
+ assert.Equal("cachecontrol1", dcfg.Matchers[0].CacheControl)
+ assert.Equal("contentencoding1", dcfg.Matchers[0].ContentEncoding)
+ assert.Equal("contenttype1", dcfg.Matchers[0].ContentType)
+ assert.True(dcfg.Matchers[0].Gzip)
+ assert.True(dcfg.Matchers[0].Force)
+ assert.Equal("^pattern2$", dcfg.Matchers[1].Pattern)
+ assert.NotNil(dcfg.Matchers[1].re)
+ assert.Equal("cachecontrol2", dcfg.Matchers[1].CacheControl)
+ assert.Equal("contentencoding2", dcfg.Matchers[1].ContentEncoding)
+ assert.Equal("contenttype2", dcfg.Matchers[1].ContentType)
+ assert.False(dcfg.Matchers[1].Gzip)
+ assert.False(dcfg.Matchers[1].Force)
+}
+
+func TestInvalidOrderingPattern(t *testing.T) {
+ assert := require.New(t)
+
+ tomlConfig := `
+
+someOtherValue = "foo"
+
+[deployment]
+order = ["["] # invalid regular expression
+`
+ cfg, err := config.FromConfigString(tomlConfig, "toml")
+ assert.NoError(err)
+
+ _, err = decodeConfig(cfg)
+ assert.Error(err)
+}
+
+func TestInvalidMatcherPattern(t *testing.T) {
+ assert := require.New(t)
+
+ tomlConfig := `
+
+someOtherValue = "foo"
+
+[deployment]
+[[deployment.matchers]]
+Pattern = "[" # invalid regular expression
+`
+ cfg, err := config.FromConfigString(tomlConfig, "toml")
+ assert.NoError(err)
+
+ _, err = decodeConfig(cfg)
+ assert.Error(err)
+}
+
+func TestDecodeConfigDefault(t *testing.T) {
+ assert := require.New(t)
+
+ dcfg, err := decodeConfig(viper.New())
+ assert.NoError(err)
+ assert.Equal(0, len(dcfg.Targets))
+ assert.Equal(0, len(dcfg.Matchers))
+}
diff --git a/deploy/deploy_test.go b/deploy/deploy_test.go
new file mode 100644
index 000000000..ed20daef4
--- /dev/null
+++ b/deploy/deploy_test.go
@@ -0,0 +1,810 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package deploy
+
+import (
+ "bytes"
+ "compress/gzip"
+ "context"
+ "crypto/md5"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/spf13/afero"
+ "gocloud.dev/blob"
+ "gocloud.dev/blob/fileblob"
+ "gocloud.dev/blob/memblob"
+)
+
+func TestFindDiffs(t *testing.T) {
+ hash1 := []byte("hash 1")
+ hash2 := []byte("hash 2")
+ makeLocal := func(path string, size int64, hash []byte) *localFile {
+ return &localFile{NativePath: path, SlashPath: filepath.ToSlash(path), UploadSize: size, md5: hash}
+ }
+ makeRemote := func(path string, size int64, hash []byte) *blob.ListObject {
+ return &blob.ListObject{Key: path, Size: size, MD5: hash}
+ }
+
+ tests := []struct {
+ Description string
+ Local []*localFile
+ Remote []*blob.ListObject
+ Force bool
+ WantUpdates []*fileToUpload
+ WantDeletes []string
+ }{
+ {
+ Description: "empty -> no diffs",
+ },
+ {
+ Description: "local == remote -> no diffs",
+ Local: []*localFile{
+ makeLocal("aaa", 1, hash1),
+ makeLocal("bbb", 2, hash1),
+ makeLocal("ccc", 3, hash2),
+ },
+ Remote: []*blob.ListObject{
+ makeRemote("aaa", 1, hash1),
+ makeRemote("bbb", 2, hash1),
+ makeRemote("ccc", 3, hash2),
+ },
+ },
+ {
+ Description: "local w/ separators == remote -> no diffs",
+ Local: []*localFile{
+ makeLocal(filepath.Join("aaa", "aaa"), 1, hash1),
+ makeLocal(filepath.Join("bbb", "bbb"), 2, hash1),
+ makeLocal(filepath.Join("ccc", "ccc"), 3, hash2),
+ },
+ Remote: []*blob.ListObject{
+ makeRemote("aaa/aaa", 1, hash1),
+ makeRemote("bbb/bbb", 2, hash1),
+ makeRemote("ccc/ccc", 3, hash2),
+ },
+ },
+ {
+ Description: "local == remote with force flag true -> diffs",
+ Local: []*localFile{
+ makeLocal("aaa", 1, hash1),
+ makeLocal("bbb", 2, hash1),
+ makeLocal("ccc", 3, hash2),
+ },
+ Remote: []*blob.ListObject{
+ makeRemote("aaa", 1, hash1),
+ makeRemote("bbb", 2, hash1),
+ makeRemote("ccc", 3, hash2),
+ },
+ Force: true,
+ WantUpdates: []*fileToUpload{
+ {makeLocal("aaa", 1, nil), reasonForce},
+ {makeLocal("bbb", 2, nil), reasonForce},
+ {makeLocal("ccc", 3, nil), reasonForce},
+ },
+ },
+ {
+ Description: "local == remote with route.Force true -> diffs",
+ Local: []*localFile{
+ {NativePath: "aaa", SlashPath: "aaa", UploadSize: 1, matcher: &matcher{Force: true}, md5: hash1},
+ makeLocal("bbb", 2, hash1),
+ },
+ Remote: []*blob.ListObject{
+ makeRemote("aaa", 1, hash1),
+ makeRemote("bbb", 2, hash1),
+ },
+ WantUpdates: []*fileToUpload{
+ {makeLocal("aaa", 1, nil), reasonForce},
+ },
+ },
+ {
+ Description: "extra local file -> upload",
+ Local: []*localFile{
+ makeLocal("aaa", 1, hash1),
+ makeLocal("bbb", 2, hash2),
+ },
+ Remote: []*blob.ListObject{
+ makeRemote("aaa", 1, hash1),
+ },
+ WantUpdates: []*fileToUpload{
+ {makeLocal("bbb", 2, nil), reasonNotFound},
+ },
+ },
+ {
+ Description: "extra remote file -> delete",
+ Local: []*localFile{
+ makeLocal("aaa", 1, hash1),
+ },
+ Remote: []*blob.ListObject{
+ makeRemote("aaa", 1, hash1),
+ makeRemote("bbb", 2, hash2),
+ },
+ WantDeletes: []string{"bbb"},
+ },
+ {
+ Description: "diffs in size or md5 -> upload",
+ Local: []*localFile{
+ makeLocal("aaa", 1, hash1),
+ makeLocal("bbb", 2, hash1),
+ makeLocal("ccc", 1, hash2),
+ },
+ Remote: []*blob.ListObject{
+ makeRemote("aaa", 1, nil),
+ makeRemote("bbb", 1, hash1),
+ makeRemote("ccc", 1, hash1),
+ },
+ WantUpdates: []*fileToUpload{
+ {makeLocal("aaa", 1, nil), reasonMD5Missing},
+ {makeLocal("bbb", 2, nil), reasonSize},
+ {makeLocal("ccc", 1, nil), reasonMD5Differs},
+ },
+ },
+ {
+ Description: "mix of updates and deletes",
+ Local: []*localFile{
+ makeLocal("same", 1, hash1),
+ makeLocal("updated", 2, hash1),
+ makeLocal("updated2", 1, hash2),
+ makeLocal("new", 1, hash1),
+ makeLocal("new2", 2, hash2),
+ },
+ Remote: []*blob.ListObject{
+ makeRemote("same", 1, hash1),
+ makeRemote("updated", 1, hash1),
+ makeRemote("updated2", 1, hash1),
+ makeRemote("stale", 1, hash1),
+ makeRemote("stale2", 1, hash1),
+ },
+ WantUpdates: []*fileToUpload{
+ {makeLocal("new", 1, nil), reasonNotFound},
+ {makeLocal("new2", 2, nil), reasonNotFound},
+ {makeLocal("updated", 2, nil), reasonSize},
+ {makeLocal("updated2", 1, nil), reasonMD5Differs},
+ },
+ WantDeletes: []string{"stale", "stale2"},
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.Description, func(t *testing.T) {
+ local := map[string]*localFile{}
+ for _, l := range tc.Local {
+ local[l.SlashPath] = l
+ }
+ remote := map[string]*blob.ListObject{}
+ for _, r := range tc.Remote {
+ remote[r.Key] = r
+ }
+ gotUpdates, gotDeletes := findDiffs(local, remote, tc.Force)
+ gotUpdates = applyOrdering(nil, gotUpdates)[0]
+ sort.Slice(gotDeletes, func(i, j int) bool { return gotDeletes[i] < gotDeletes[j] })
+ if diff := cmp.Diff(gotUpdates, tc.WantUpdates, cmpopts.IgnoreUnexported(localFile{})); diff != "" {
+ t.Errorf("updates differ:\n%s", diff)
+ }
+ if diff := cmp.Diff(gotDeletes, tc.WantDeletes); diff != "" {
+ t.Errorf("deletes differ:\n%s", diff)
+ }
+ })
+ }
+}
+
+func TestLocalFile(t *testing.T) {
+ const (
+ content = "hello world!"
+ )
+ contentBytes := []byte(content)
+ contentLen := int64(len(contentBytes))
+ contentMD5 := md5.Sum(contentBytes)
+ var buf bytes.Buffer
+ gz := gzip.NewWriter(&buf)
+ if _, err := gz.Write(contentBytes); err != nil {
+ t.Fatal(err)
+ }
+ gz.Close()
+ gzBytes := buf.Bytes()
+ gzLen := int64(len(gzBytes))
+ gzMD5 := md5.Sum(gzBytes)
+
+ tests := []struct {
+ Description string
+ Path string
+ Matcher *matcher
+ WantContent []byte
+ WantSize int64
+ WantMD5 []byte
+ WantContentType string // empty string is always OK, since content type detection is OS-specific
+ WantCacheControl string
+ WantContentEncoding string
+ }{
+ {
+ Description: "file with no suffix",
+ Path: "foo",
+ WantContent: contentBytes,
+ WantSize: contentLen,
+ WantMD5: contentMD5[:],
+ },
+ {
+ Description: "file with .txt suffix",
+ Path: "foo.txt",
+ WantContent: contentBytes,
+ WantSize: contentLen,
+ WantMD5: contentMD5[:],
+ },
+ {
+ Description: "CacheControl from matcher",
+ Path: "foo.txt",
+ Matcher: &matcher{CacheControl: "max-age=630720000"},
+ WantContent: contentBytes,
+ WantSize: contentLen,
+ WantMD5: contentMD5[:],
+ WantCacheControl: "max-age=630720000",
+ },
+ {
+ Description: "ContentEncoding from matcher",
+ Path: "foo.txt",
+ Matcher: &matcher{ContentEncoding: "foobar"},
+ WantContent: contentBytes,
+ WantSize: contentLen,
+ WantMD5: contentMD5[:],
+ WantContentEncoding: "foobar",
+ },
+ {
+ Description: "ContentType from matcher",
+ Path: "foo.txt",
+ Matcher: &matcher{ContentType: "foo/bar"},
+ WantContent: contentBytes,
+ WantSize: contentLen,
+ WantMD5: contentMD5[:],
+ WantContentType: "foo/bar",
+ },
+ {
+ Description: "gzipped content",
+ Path: "foo.txt",
+ Matcher: &matcher{Gzip: true},
+ WantContent: gzBytes,
+ WantSize: gzLen,
+ WantMD5: gzMD5[:],
+ WantContentEncoding: "gzip",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.Description, func(t *testing.T) {
+ fs := new(afero.MemMapFs)
+ if err := afero.WriteFile(fs, tc.Path, []byte(content), os.ModePerm); err != nil {
+ t.Fatal(err)
+ }
+ lf, err := newLocalFile(fs, tc.Path, filepath.ToSlash(tc.Path), tc.Matcher)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got := lf.UploadSize; got != tc.WantSize {
+ t.Errorf("got size %d want %d", got, tc.WantSize)
+ }
+ if got := lf.MD5(); !bytes.Equal(got, tc.WantMD5) {
+ t.Errorf("got MD5 %x want %x", got, tc.WantMD5)
+ }
+ if got := lf.CacheControl(); got != tc.WantCacheControl {
+ t.Errorf("got CacheControl %q want %q", got, tc.WantCacheControl)
+ }
+ if got := lf.ContentEncoding(); got != tc.WantContentEncoding {
+ t.Errorf("got ContentEncoding %q want %q", got, tc.WantContentEncoding)
+ }
+ if tc.WantContentType != "" {
+ if got := lf.ContentType(); got != tc.WantContentType {
+ t.Errorf("got ContentType %q want %q", got, tc.WantContentType)
+ }
+ }
+ // Verify the reader last to ensure the previous operations don't
+ // interfere with it.
+ r, err := lf.Reader()
+ if err != nil {
+ t.Fatal(err)
+ }
+ gotContent, err := ioutil.ReadAll(r)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(gotContent, tc.WantContent) {
+ t.Errorf("got content %q want %q", string(gotContent), string(tc.WantContent))
+ }
+ r.Close()
+ // Verify we can read again.
+ r, err = lf.Reader()
+ if err != nil {
+ t.Fatal(err)
+ }
+ gotContent, err = ioutil.ReadAll(r)
+ if err != nil {
+ t.Fatal(err)
+ }
+ r.Close()
+ if !bytes.Equal(gotContent, tc.WantContent) {
+ t.Errorf("got content %q want %q", string(gotContent), string(tc.WantContent))
+ }
+ })
+ }
+}
+
+func TestOrdering(t *testing.T) {
+ tests := []struct {
+ Description string
+ Uploads []string
+ Ordering []*regexp.Regexp
+ Want [][]string
+ }{
+ {
+ Description: "empty",
+ Want: [][]string{nil},
+ },
+ {
+ Description: "no ordering",
+ Uploads: []string{"c", "b", "a", "d"},
+ Want: [][]string{{"a", "b", "c", "d"}},
+ },
+ {
+ Description: "one ordering",
+ Uploads: []string{"db", "c", "b", "a", "da"},
+ Ordering: []*regexp.Regexp{regexp.MustCompile("^d")},
+ Want: [][]string{{"da", "db"}, {"a", "b", "c"}},
+ },
+ {
+ Description: "two orderings",
+ Uploads: []string{"db", "c", "b", "a", "da"},
+ Ordering: []*regexp.Regexp{
+ regexp.MustCompile("^d"),
+ regexp.MustCompile("^b"),
+ },
+ Want: [][]string{{"da", "db"}, {"b"}, {"a", "c"}},
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.Description, func(t *testing.T) {
+ uploads := make([]*fileToUpload, len(tc.Uploads))
+ for i, u := range tc.Uploads {
+ uploads[i] = &fileToUpload{Local: &localFile{SlashPath: u}}
+ }
+ gotUploads := applyOrdering(tc.Ordering, uploads)
+ var got [][]string
+ for _, subslice := range gotUploads {
+ var gotsubslice []string
+ for _, u := range subslice {
+ gotsubslice = append(gotsubslice, u.Local.SlashPath)
+ }
+ got = append(got, gotsubslice)
+ }
+ if diff := cmp.Diff(got, tc.Want); diff != "" {
+ t.Error(diff)
+ }
+ })
+ }
+}
+
+type fileData struct {
+ Name string // name of the file
+ Contents string // contents of the file
+}
+
+// initLocalFs initializes fs with some test files.
+func initLocalFs(ctx context.Context, fs afero.Fs) ([]*fileData, error) {
+ // The initial local filesystem.
+ local := []*fileData{
+ {"aaa", "aaa"},
+ {"bbb", "bbb"},
+ {"subdir/aaa", "subdir-aaa"},
+ {"subdir/nested/aaa", "subdir-nested-aaa"},
+ {"subdir2/bbb", "subdir2-bbb"},
+ }
+ if err := writeFiles(fs, local); err != nil {
+ return nil, err
+ }
+ return local, nil
+}
+
+// fsTest represents an (afero.FS, Go CDK blob.Bucket) against which end-to-end
+// tests can be run.
+type fsTest struct {
+ name string
+ fs afero.Fs
+ bucket *blob.Bucket
+}
+
+// initFsTests initializes a pair of tests for end-to-end test:
+// 1. An in-memory afero.Fs paired with an in-memory Go CDK bucket.
+// 2. A filesystem-based afero.Fs paired with an filesystem-based Go CDK bucket.
+// It returns the pair of tests and a cleanup function.
+func initFsTests() ([]*fsTest, func(), error) {
+ tmpfsdir, err := ioutil.TempDir("", "fs")
+ if err != nil {
+ return nil, nil, err
+ }
+ tmpbucketdir, err := ioutil.TempDir("", "bucket")
+ if err != nil {
+ return nil, nil, err
+ }
+
+ memfs := afero.NewMemMapFs()
+ membucket := memblob.OpenBucket(nil)
+
+ filefs := afero.NewBasePathFs(afero.NewOsFs(), tmpfsdir)
+ filebucket, err := fileblob.OpenBucket(tmpbucketdir, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ tests := []*fsTest{
+ {"mem", memfs, membucket},
+ {"file", filefs, filebucket},
+ }
+ cleanup := func() {
+ membucket.Close()
+ filebucket.Close()
+ os.RemoveAll(tmpfsdir)
+ os.RemoveAll(tmpbucketdir)
+ }
+ return tests, cleanup, nil
+}
+
+// TestEndToEndSync verifies that basic adds, updates, and deletes are working
+// correctly.
+func TestEndToEndSync(t *testing.T) {
+ ctx := context.Background()
+ tests, cleanup, err := initFsTests()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer cleanup()
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ local, err := initLocalFs(ctx, test.fs)
+ if err != nil {
+ t.Fatal(err)
+ }
+ deployer := &Deployer{
+ localFs: test.fs,
+ maxDeletes: -1,
+ bucket: test.bucket,
+ }
+
+ // Initial deployment should sync remote with local.
+ if err := deployer.Deploy(ctx); err != nil {
+ t.Errorf("initial deploy: failed: %v", err)
+ }
+ wantSummary := deploySummary{NumLocal: 5, NumRemote: 0, NumUploads: 5, NumDeletes: 0}
+ if !cmp.Equal(deployer.summary, wantSummary) {
+ t.Errorf("initial deploy: got %v, want %v", deployer.summary, wantSummary)
+ }
+ if diff, err := verifyRemote(ctx, deployer.bucket, local); err != nil {
+ t.Errorf("initial deploy: failed to verify remote: %v", err)
+ } else if diff != "" {
+ t.Errorf("initial deploy: remote snapshot doesn't match expected:\n%v", diff)
+ }
+
+ // A repeat deployment shouldn't change anything.
+ if err := deployer.Deploy(ctx); err != nil {
+ t.Errorf("no-op deploy: %v", err)
+ }
+ wantSummary = deploySummary{NumLocal: 5, NumRemote: 5, NumUploads: 0, NumDeletes: 0}
+ if !cmp.Equal(deployer.summary, wantSummary) {
+ t.Errorf("no-op deploy: got %v, want %v", deployer.summary, wantSummary)
+ }
+
+ // Make some changes to the local filesystem:
+ // 1. Modify file [0].
+ // 2. Delete file [1].
+ // 3. Add a new file (sorted last).
+ updatefd := local[0]
+ updatefd.Contents = "new contents"
+ deletefd := local[1]
+ local = append(local[:1], local[2:]...) // removing deleted [1]
+ newfd := &fileData{"zzz", "zzz"}
+ local = append(local, newfd)
+ if err := writeFiles(test.fs, []*fileData{updatefd, newfd}); err != nil {
+ t.Fatal(err)
+ }
+ if err := test.fs.Remove(deletefd.Name); err != nil {
+ t.Fatal(err)
+ }
+
+ // A deployment should apply those 3 changes.
+ if err := deployer.Deploy(ctx); err != nil {
+ t.Errorf("deploy after changes: failed: %v", err)
+ }
+ wantSummary = deploySummary{NumLocal: 5, NumRemote: 5, NumUploads: 2, NumDeletes: 1}
+ if !cmp.Equal(deployer.summary, wantSummary) {
+ t.Errorf("deploy after changes: got %v, want %v", deployer.summary, wantSummary)
+ }
+ if diff, err := verifyRemote(ctx, deployer.bucket, local); err != nil {
+ t.Errorf("deploy after changes: failed to verify remote: %v", err)
+ } else if diff != "" {
+ t.Errorf("deploy after changes: remote snapshot doesn't match expected:\n%v", diff)
+ }
+
+ // Again, a repeat deployment shouldn't change anything.
+ if err := deployer.Deploy(ctx); err != nil {
+ t.Errorf("no-op deploy: %v", err)
+ }
+ wantSummary = deploySummary{NumLocal: 5, NumRemote: 5, NumUploads: 0, NumDeletes: 0}
+ if !cmp.Equal(deployer.summary, wantSummary) {
+ t.Errorf("no-op deploy: got %v, want %v", deployer.summary, wantSummary)
+ }
+ })
+ }
+}
+
+// TestMaxDeletes verifies that the "maxDeletes" flag is working correctly.
+func TestMaxDeletes(t *testing.T) {
+ ctx := context.Background()
+ tests, cleanup, err := initFsTests()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer cleanup()
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ local, err := initLocalFs(ctx, test.fs)
+ if err != nil {
+ t.Fatal(err)
+ }
+ deployer := &Deployer{
+ localFs: test.fs,
+ maxDeletes: -1,
+ bucket: test.bucket,
+ }
+
+ // Sync remote with local.
+ if err := deployer.Deploy(ctx); err != nil {
+ t.Errorf("initial deploy: failed: %v", err)
+ }
+ wantSummary := deploySummary{NumLocal: 5, NumRemote: 0, NumUploads: 5, NumDeletes: 0}
+ if !cmp.Equal(deployer.summary, wantSummary) {
+ t.Errorf("initial deploy: got %v, want %v", deployer.summary, wantSummary)
+ }
+
+ // Delete two files, [1] and [2].
+ if err := test.fs.Remove(local[1].Name); err != nil {
+ t.Fatal(err)
+ }
+ if err := test.fs.Remove(local[2].Name); err != nil {
+ t.Fatal(err)
+ }
+
+ // A deployment with maxDeletes=0 shouldn't change anything.
+ deployer.maxDeletes = 0
+ if err := deployer.Deploy(ctx); err != nil {
+ t.Errorf("deploy failed: %v", err)
+ }
+ wantSummary = deploySummary{NumLocal: 3, NumRemote: 5, NumUploads: 0, NumDeletes: 0}
+ if !cmp.Equal(deployer.summary, wantSummary) {
+ t.Errorf("deploy: got %v, want %v", deployer.summary, wantSummary)
+ }
+
+ // A deployment with maxDeletes=1 shouldn't change anything either.
+ deployer.maxDeletes = 1
+ if err := deployer.Deploy(ctx); err != nil {
+ t.Errorf("deploy failed: %v", err)
+ }
+ wantSummary = deploySummary{NumLocal: 3, NumRemote: 5, NumUploads: 0, NumDeletes: 0}
+ if !cmp.Equal(deployer.summary, wantSummary) {
+ t.Errorf("deploy: got %v, want %v", deployer.summary, wantSummary)
+ }
+
+ // A deployment with maxDeletes=2 should make the changes.
+ deployer.maxDeletes = 2
+ if err := deployer.Deploy(ctx); err != nil {
+ t.Errorf("deploy failed: %v", err)
+ }
+ wantSummary = deploySummary{NumLocal: 3, NumRemote: 5, NumUploads: 0, NumDeletes: 2}
+ if !cmp.Equal(deployer.summary, wantSummary) {
+ t.Errorf("deploy: got %v, want %v", deployer.summary, wantSummary)
+ }
+
+ // Delete two more files, [0] and [3].
+ if err := test.fs.Remove(local[0].Name); err != nil {
+ t.Fatal(err)
+ }
+ if err := test.fs.Remove(local[3].Name); err != nil {
+ t.Fatal(err)
+ }
+
+ // A deployment with maxDeletes=-1 should make the changes.
+ deployer.maxDeletes = -1
+ if err := deployer.Deploy(ctx); err != nil {
+ t.Errorf("deploy failed: %v", err)
+ }
+ wantSummary = deploySummary{NumLocal: 1, NumRemote: 3, NumUploads: 0, NumDeletes: 2}
+ if !cmp.Equal(deployer.summary, wantSummary) {
+ t.Errorf("deploy: got %v, want %v", deployer.summary, wantSummary)
+ }
+ })
+ }
+}
+
+// TestCompression verifies that gzip compression works correctly.
+// In particular, MD5 hashes must be of the compressed content.
+func TestCompression(t *testing.T) {
+ ctx := context.Background()
+ tests, cleanup, err := initFsTests()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer cleanup()
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ local, err := initLocalFs(ctx, test.fs)
+ if err != nil {
+ t.Fatal(err)
+ }
+ deployer := &Deployer{
+ localFs: test.fs,
+ bucket: test.bucket,
+ matchers: []*matcher{{Pattern: ".*", Gzip: true, re: regexp.MustCompile(".*")}},
+ }
+
+ // Initial deployment should sync remote with local.
+ if err := deployer.Deploy(ctx); err != nil {
+ t.Errorf("initial deploy: failed: %v", err)
+ }
+ wantSummary := deploySummary{NumLocal: 5, NumRemote: 0, NumUploads: 5, NumDeletes: 0}
+ if !cmp.Equal(deployer.summary, wantSummary) {
+ t.Errorf("initial deploy: got %v, want %v", deployer.summary, wantSummary)
+ }
+
+ // A repeat deployment shouldn't change anything.
+ if err := deployer.Deploy(ctx); err != nil {
+ t.Errorf("no-op deploy: %v", err)
+ }
+ wantSummary = deploySummary{NumLocal: 5, NumRemote: 5, NumUploads: 0, NumDeletes: 0}
+ if !cmp.Equal(deployer.summary, wantSummary) {
+ t.Errorf("no-op deploy: got %v, want %v", deployer.summary, wantSummary)
+ }
+
+ // Make an update to the local filesystem, on [1].
+ updatefd := local[1]
+ updatefd.Contents = "new contents"
+ if err := writeFiles(test.fs, []*fileData{updatefd}); err != nil {
+ t.Fatal(err)
+ }
+
+ // A deployment should apply the changes.
+ if err := deployer.Deploy(ctx); err != nil {
+ t.Errorf("deploy after changes: failed: %v", err)
+ }
+ wantSummary = deploySummary{NumLocal: 5, NumRemote: 5, NumUploads: 1, NumDeletes: 0}
+ if !cmp.Equal(deployer.summary, wantSummary) {
+ t.Errorf("deploy after changes: got %v, want %v", deployer.summary, wantSummary)
+ }
+ })
+ }
+}
+
+// TestMatching verifies that matchers match correctly, and that the Force
+// attribute for matcher works.
+func TestMatching(t *testing.T) {
+ ctx := context.Background()
+ tests, cleanup, err := initFsTests()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer cleanup()
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ _, err := initLocalFs(ctx, test.fs)
+ if err != nil {
+ t.Fatal(err)
+ }
+ deployer := &Deployer{
+ localFs: test.fs,
+ bucket: test.bucket,
+ matchers: []*matcher{{Pattern: "^subdir/aaa$", Force: true, re: regexp.MustCompile("^subdir/aaa$")}},
+ }
+
+ // Initial deployment to sync remote with local.
+ if err := deployer.Deploy(ctx); err != nil {
+ t.Errorf("initial deploy: failed: %v", err)
+ }
+ wantSummary := deploySummary{NumLocal: 5, NumRemote: 0, NumUploads: 5, NumDeletes: 0}
+ if !cmp.Equal(deployer.summary, wantSummary) {
+ t.Errorf("initial deploy: got %v, want %v", deployer.summary, wantSummary)
+ }
+
+ // A repeat deployment should upload a single file, the one that matched the Force matcher.
+ // Note that matching happens based on the ToSlash form, so this matches
+ // even on Windows.
+ if err := deployer.Deploy(ctx); err != nil {
+ t.Errorf("no-op deploy with single force matcher: %v", err)
+ }
+ wantSummary = deploySummary{NumLocal: 5, NumRemote: 5, NumUploads: 1, NumDeletes: 0}
+ if !cmp.Equal(deployer.summary, wantSummary) {
+ t.Errorf("no-op deploy with single force matcher: got %v, want %v", deployer.summary, wantSummary)
+ }
+
+ // Repeat with a matcher that should now match 3 files.
+ deployer.matchers = []*matcher{{Pattern: "aaa", Force: true, re: regexp.MustCompile("aaa")}}
+ if err := deployer.Deploy(ctx); err != nil {
+ t.Errorf("no-op deploy with triple force matcher: %v", err)
+ }
+ wantSummary = deploySummary{NumLocal: 5, NumRemote: 5, NumUploads: 3, NumDeletes: 0}
+ if !cmp.Equal(deployer.summary, wantSummary) {
+ t.Errorf("no-op deploy with triple force matcher: got %v, want %v", deployer.summary, wantSummary)
+ }
+ })
+ }
+}
+
+// writeFiles writes the files in fds to fd.
+func writeFiles(fs afero.Fs, fds []*fileData) error {
+ for _, fd := range fds {
+ dir := path.Dir(fd.Name)
+ if dir != "." {
+ err := fs.MkdirAll(dir, os.ModePerm)
+ if err != nil {
+ return err
+ }
+ }
+ f, err := fs.Create(fd.Name)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ _, err = f.WriteString(fd.Contents)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// verifyRemote that the current contents of bucket matches local.
+// It returns an empty string if the contents matched, and a non-empty string
+// capturing the diff if they didn't.
+func verifyRemote(ctx context.Context, bucket *blob.Bucket, local []*fileData) (string, error) {
+ var cur []*fileData
+ iter := bucket.List(nil)
+ for {
+ obj, err := iter.Next(ctx)
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return "", err
+ }
+ contents, err := bucket.ReadAll(ctx, obj.Key)
+ if err != nil {
+ return "", err
+ }
+ cur = append(cur, &fileData{obj.Key, string(contents)})
+ }
+ if cmp.Equal(cur, local) {
+ return "", nil
+ }
+ diff := "got: \n"
+ for _, f := range cur {
+ diff += fmt.Sprintf(" %s: %s\n", f.Name, f.Contents)
+ }
+ diff += "want: \n"
+ for _, f := range local {
+ diff += fmt.Sprintf(" %s: %s\n", f.Name, f.Contents)
+ }
+ return diff, nil
+}
diff --git a/deps/deps.go b/deps/deps.go
new file mode 100644
index 000000000..fa62fe5ae
--- /dev/null
+++ b/deps/deps.go
@@ -0,0 +1,354 @@
+package deps
+
+import (
+ "sync"
+ "time"
+
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/cache/filecache"
+ "github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/langs"
+ "github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/resources/page"
+
+ "github.com/gohugoio/hugo/metrics"
+ "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/resources"
+ "github.com/gohugoio/hugo/source"
+ "github.com/gohugoio/hugo/tpl"
+ jww "github.com/spf13/jwalterweatherman"
+)
+
+// Deps holds dependencies used by many.
+// There will be normally 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 *loggers.Logger `json:"-"`
+
+ // Used to log errors that may repeat itself many times.
+ DistinctErrorLog *helpers.DistinctLogger
+
+ // Used to log warnings that may repeat itself many times.
+ DistinctWarningLog *helpers.DistinctLogger
+
+ // The templates to use. This will usually implement the full tpl.TemplateHandler.
+ Tmpl tpl.TemplateFinder `json:"-"`
+
+ // We use this to parse and execute ad-hoc text templates.
+ TextTmpl tpl.TemplateParseFinder `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 SourceSpec to use
+ SourceSpec *source.SourceSpec `json:"-"`
+
+ // The Resource Spec to use
+ ResourceSpec *resources.Spec
+
+ // The configuration to use
+ Cfg config.Provider `json:"-"`
+
+ // The file cache to use.
+ FileCaches filecache.Caches
+
+ // The translation func to use
+ Translate func(translationID string, args ...interface{}) string `json:"-"`
+
+ // The language in use. TODO(bep) consolidate with site
+ Language *langs.Language
+
+ // The site building.
+ Site page.Site
+
+ // All the output formats available for the current site.
+ OutputFormatsConfig output.Formats
+
+ templateProvider ResourceProvider
+ WithTemplate func(templ tpl.TemplateHandler) error `json:"-"`
+
+ translationProvider ResourceProvider
+
+ Metrics metrics.Provider
+
+ // Timeout is configurable in site config.
+ Timeout time.Duration
+
+ // BuildStartListeners will be notified before a build starts.
+ BuildStartListeners *Listeners
+
+ *globalErrHandler
+}
+
+type globalErrHandler struct {
+ // Channel for some "hard to get to" build errors
+ buildErrors chan error
+}
+
+// SendErr sends the error on a channel to be handled later.
+// This can be used in situations where returning and aborting the current
+// operation isn't practical.
+func (e *globalErrHandler) SendError(err error) {
+ if e.buildErrors != nil {
+ select {
+ case e.buildErrors <- err:
+ default:
+ }
+ return
+ }
+
+ jww.ERROR.Println(err)
+}
+
+func (e *globalErrHandler) StartErrorCollector() chan error {
+ e.buildErrors = make(chan error, 10)
+ return e.buildErrors
+}
+
+// Listeners represents an event listener.
+type Listeners struct {
+ sync.Mutex
+
+ // A list of funcs to be notified about an event.
+ listeners []func()
+}
+
+// Add adds a function to a Listeners instance.
+func (b *Listeners) Add(f func()) {
+ if b == nil {
+ return
+ }
+ b.Lock()
+ defer b.Unlock()
+ b.listeners = append(b.listeners, f)
+}
+
+// Notify executes all listener functions.
+func (b *Listeners) Notify() {
+ b.Lock()
+ defer b.Unlock()
+ for _, notify := range b.listeners {
+ notify()
+ }
+}
+
+// ResourceProvider is used to create and refresh, and clone resources needed.
+type ResourceProvider interface {
+ Update(deps *Deps) error
+ Clone(deps *Deps) error
+}
+
+// TemplateHandler returns the used tpl.TemplateFinder as tpl.TemplateHandler.
+func (d *Deps) TemplateHandler() tpl.TemplateHandler {
+ return d.Tmpl.(tpl.TemplateHandler)
+}
+
+// LoadResources loads translations and templates.
+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
+ }
+
+ return nil
+}
+
+// New initializes a Dep struct.
+// Defaults are set for nil values,
+// but TemplateProvider, TranslationProvider and Language are always required.
+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 = loggers.NewErrorLogger()
+ }
+
+ if fs == nil {
+ // Default to the production file system.
+ fs = hugofs.NewDefault(cfg.Language)
+ }
+
+ if cfg.MediaTypes == nil {
+ cfg.MediaTypes = media.DefaultTypes
+ }
+
+ if cfg.OutputFormats == nil {
+ cfg.OutputFormats = output.DefaultFormats
+ }
+
+ ps, err := helpers.NewPathSpec(fs, cfg.Language)
+
+ if err != nil {
+ return nil, err
+ }
+
+ fileCaches, err := filecache.NewCaches(ps)
+ if err != nil {
+ return nil, errors.WithMessage(err, "failed to create file caches from configuration")
+ }
+
+ resourceSpec, err := resources.NewSpec(ps, fileCaches, logger, cfg.OutputFormats, cfg.MediaTypes)
+ if err != nil {
+ return nil, err
+ }
+
+ contentSpec, err := helpers.NewContentSpec(cfg.Language)
+ if err != nil {
+ return nil, err
+ }
+
+ sp := source.NewSourceSpec(ps, fs.Source)
+
+ timeoutms := cfg.Language.GetInt("timeout")
+ if timeoutms <= 0 {
+ timeoutms = 3000
+ }
+
+ distinctErrorLogger := helpers.NewDistinctLogger(logger.ERROR)
+ distinctWarnLogger := helpers.NewDistinctLogger(logger.WARN)
+
+ d := &Deps{
+ Fs: fs,
+ Log: logger,
+ DistinctErrorLog: distinctErrorLogger,
+ DistinctWarningLog: distinctWarnLogger,
+ templateProvider: cfg.TemplateProvider,
+ translationProvider: cfg.TranslationProvider,
+ WithTemplate: cfg.WithTemplate,
+ PathSpec: ps,
+ ContentSpec: contentSpec,
+ SourceSpec: sp,
+ ResourceSpec: resourceSpec,
+ Cfg: cfg.Language,
+ Language: cfg.Language,
+ Site: cfg.Site,
+ FileCaches: fileCaches,
+ BuildStartListeners: &Listeners{},
+ Timeout: time.Duration(timeoutms) * time.Millisecond,
+ globalErrHandler: &globalErrHandler{},
+ }
+
+ if cfg.Cfg.GetBool("templateMetrics") {
+ d.Metrics = metrics.NewProvider(cfg.Cfg.GetBool("templateMetricsHints"))
+ }
+
+ return d, nil
+}
+
+// ForLanguage creates a copy of the Deps with the language dependent
+// parts switched out.
+func (d Deps) ForLanguage(cfg DepsCfg, onCreated func(d *Deps) error) (*Deps, error) {
+ l := cfg.Language
+ var err error
+
+ d.PathSpec, err = helpers.NewPathSpecWithBaseBaseFsProvided(d.Fs, l, d.BaseFs)
+ if err != nil {
+ return nil, err
+ }
+
+ d.ContentSpec, err = helpers.NewContentSpec(l)
+ if err != nil {
+ return nil, err
+ }
+
+ d.Site = cfg.Site
+
+ // The resource cache is global so reuse.
+ // TODO(bep) clean up these inits.
+ resourceCache := d.ResourceSpec.ResourceCache
+ d.ResourceSpec, err = resources.NewSpec(d.PathSpec, d.ResourceSpec.FileCaches, d.Log, cfg.OutputFormats, cfg.MediaTypes)
+ if err != nil {
+ return nil, err
+ }
+ d.ResourceSpec.ResourceCache = resourceCache
+
+ d.Cfg = l
+ d.Language = l
+
+ if onCreated != nil {
+ if err = onCreated(&d); err != nil {
+ return nil, err
+ }
+ }
+
+ if err := d.translationProvider.Clone(&d); err != nil {
+ return nil, err
+ }
+
+ if err := d.templateProvider.Clone(&d); err != nil {
+ return nil, err
+ }
+
+ d.BuildStartListeners = &Listeners{}
+
+ 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 *loggers.Logger
+
+ // The file systems to use
+ Fs *hugofs.Fs
+
+ // The language to use.
+ Language *langs.Language
+
+ // The Site in use
+ Site page.Site
+
+ // The configuration to use.
+ Cfg config.Provider
+
+ // The media types configured.
+ MediaTypes media.Types
+
+ // The output formats configured.
+ OutputFormats output.Formats
+
+ // Template handling.
+ TemplateProvider ResourceProvider
+ WithTemplate func(templ tpl.TemplateHandler) error
+
+ // i18n handling.
+ TranslationProvider ResourceProvider
+
+ // Whether we are in running (server) mode
+ Running bool
+}
diff --git a/docs/.github/stale.yml b/docs/.github/stale.yml
new file mode 100644
index 000000000..389205294
--- /dev/null
+++ b/docs/.github/stale.yml
@@ -0,0 +1,22 @@
+# Number of days of inactivity before an issue becomes stale
+daysUntilStale: 120
+# Number of days of inactivity before a stale issue is closed
+daysUntilClose: 30
+# Issues with these labels will never be considered stale
+exemptLabels:
+ - Keep
+ - Security
+ - UndocumentedFeature
+# Label to use when marking an issue as stale
+staleLabel: Stale
+# Comment to post when marking an issue as stale. Set to `false` to disable
+markComment: >
+ This issue has been automatically marked as stale because it has not had
+ recent activity. The resources of the Hugo team are limited, and so we are asking for your help.
+
+ If you still think this is important, please tell us why.
+
+ This issue will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
+
+# Comment to post when closing a stale issue. Set to `false` to disable
+closeComment: false
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 000000000..b203a37cd
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,5 @@
+/.idea
+/public
+nohup.out
+.DS_Store
+trace.out \ No newline at end of file
diff --git a/LICENSE.md b/docs/LICENSE.md
index b62a9b5ff..b62a9b5ff 100644
--- a/LICENSE.md
+++ b/docs/LICENSE.md
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 000000000..70908ef12
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,47 @@
+[![Netlify Status](https://api.netlify.com/api/v1/badges/e0dbbfc7-34f1-4393-a679-c16e80162705/deploy-status)](https://app.netlify.com/sites/gohugoio/deploys)
+
+# Hugo Docs
+
+Documentation site for [Hugo](https://github.com/gohugoio/hugo), the very fast and flexible static site generator built with love in Go.
+
+## Contributing
+
+We welcome contributions to Hugo of any kind including documentation, suggestions, bug reports, pull requests etc. Also check out our [contribution guide](https://gohugo.io/contribute/documentation/). We would love to hear from you.
+
+Note that this repository contains solely the documentation for Hugo. For contributions that aren't documentation-related please refer to the [hugo](https://github.com/gohugoio/hugo) repository.
+
+*Pull requests shall **only** contain changes to the actual documentation. However, changes on the code base of Hugo **and** the documentation shall be a single, atomic pull request in the [hugo](https://github.com/gohugoio/hugo) repository.*
+
+Spelling fixes are most welcomed, and if you want to contribute longer sections to the documentation, it would be great if you had these in mind when writing:
+
+* Short is good. People go to the library to read novels. If there is more than one way to _do a thing_ in Hugo, describe the current _best practice_ (avoid "… but you can also do …" and "… in older versions of Hugo you had to …".
+* For examples, try to find short snippets that teaches people about the concept. If the example is also useful as-is (copy and paste), then great, but don't list long and similar examples just so people can use them on their sites.
+* Hugo has users from all over the world, so an easy to understand and [simple English](https://simple.wikipedia.org/wiki/Basic_English) is good.
+
+## Branches
+
+* The `master` branch is where the site is automatically built from, and is the place to put changes relevant to the current Hugo version.
+* The `next` branch is where we store changes that is related to the next Hugo release. This can be previewed here: https://next--gohugoio.netlify.com/
+
+## Build
+
+To view the documentation site locally, you need to clone this repository:
+
+```bash
+git clone https://github.com/gohugoio/hugoDocs.git
+```
+
+Also note that the documentation version for a given version of Hugo can also be found in the `/docs` sub-folder of the [Hugo source repository](https://github.com/gohugoio/hugo).
+
+Then to view the docs in your browser, run Hugo and open up the link:
+
+```bash
+▶ hugo server
+
+Started building sites ...
+.
+.
+Serving pages from memory
+Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
+Press Ctrl+C to stop
+```
diff --git a/archetypes/default.md b/docs/archetypes/default.md
index f30f01f74..f30f01f74 100644
--- a/archetypes/default.md
+++ b/docs/archetypes/default.md
diff --git a/archetypes/functions.md b/docs/archetypes/functions.md
index 0a5dd344f..0a5dd344f 100644
--- a/archetypes/functions.md
+++ b/docs/archetypes/functions.md
diff --git a/archetypes/showcase/bio.md b/docs/archetypes/showcase/bio.md
index 2443c2f35..2443c2f35 100644
--- a/archetypes/showcase/bio.md
+++ b/docs/archetypes/showcase/bio.md
diff --git a/archetypes/showcase/featured.png b/docs/archetypes/showcase/featured.png
index 4f390132e..4f390132e 100644
--- a/archetypes/showcase/featured.png
+++ b/docs/archetypes/showcase/featured.png
Binary files differ
diff --git a/archetypes/showcase/index.md b/docs/archetypes/showcase/index.md
index a21bb9726..a21bb9726 100644
--- a/archetypes/showcase/index.md
+++ b/docs/archetypes/showcase/index.md
diff --git a/config.toml b/docs/config.toml
index a7eb54cf8..a7eb54cf8 100644
--- a/config.toml
+++ b/docs/config.toml
diff --git a/config/_default/config.toml b/docs/config/_default/config.toml
index 30764b5f9..30764b5f9 100644
--- a/config/_default/config.toml
+++ b/docs/config/_default/config.toml
diff --git a/config/_default/languages.toml b/docs/config/_default/languages.toml
index c9914d84d..c9914d84d 100644
--- a/config/_default/languages.toml
+++ b/docs/config/_default/languages.toml
diff --git a/config/_default/menus/menus.en.toml b/docs/config/_default/menus/menus.en.toml
index 041f31888..041f31888 100644
--- a/config/_default/menus/menus.en.toml
+++ b/docs/config/_default/menus/menus.en.toml
diff --git a/config/_default/menus/menus.zh.toml b/docs/config/_default/menus/menus.zh.toml
index 2f68be67b..2f68be67b 100644
--- a/config/_default/menus/menus.zh.toml
+++ b/docs/config/_default/menus/menus.zh.toml
diff --git a/config/_default/params.toml b/docs/config/_default/params.toml
index 6ddf97e56..6ddf97e56 100644
--- a/config/_default/params.toml
+++ b/docs/config/_default/params.toml
diff --git a/config/development/params.toml b/docs/config/development/params.toml
index 4cd7314ab..4cd7314ab 100644
--- a/config/development/params.toml
+++ b/docs/config/development/params.toml
diff --git a/config/production/config.toml b/docs/config/production/config.toml
index 961f04d35..961f04d35 100644
--- a/config/production/config.toml
+++ b/docs/config/production/config.toml
diff --git a/config/production/params.toml b/docs/config/production/params.toml
index d0071fe65..d0071fe65 100644
--- a/config/production/params.toml
+++ b/docs/config/production/params.toml
diff --git a/content/en/_index.md b/docs/content/en/_index.md
index 334704833..334704833 100644
--- a/content/en/_index.md
+++ b/docs/content/en/_index.md
diff --git a/content/en/about/_index.md b/docs/content/en/about/_index.md
index 8ed441b61..8ed441b61 100644
--- a/content/en/about/_index.md
+++ b/docs/content/en/about/_index.md
diff --git a/content/en/about/benefits.md b/docs/content/en/about/benefits.md
index 0ba28c5cc..0ba28c5cc 100644
--- a/content/en/about/benefits.md
+++ b/docs/content/en/about/benefits.md
diff --git a/content/en/about/features.md b/docs/content/en/about/features.md
index 4176c60df..4176c60df 100644
--- a/content/en/about/features.md
+++ b/docs/content/en/about/features.md
diff --git a/content/en/about/hugo-and-gdpr.md b/docs/content/en/about/hugo-and-gdpr.md
index e193e1838..e193e1838 100644
--- a/content/en/about/hugo-and-gdpr.md
+++ b/docs/content/en/about/hugo-and-gdpr.md
diff --git a/content/en/about/license.md b/docs/content/en/about/license.md
index a8e7c4abd..a8e7c4abd 100644
--- a/content/en/about/license.md
+++ b/docs/content/en/about/license.md
diff --git a/content/en/about/new-in-032/index.md b/docs/content/en/about/new-in-032/index.md
index f3e56dc6b..f3e56dc6b 100644
--- a/content/en/about/new-in-032/index.md
+++ b/docs/content/en/about/new-in-032/index.md
diff --git a/content/en/about/new-in-032/sunset.jpg b/docs/content/en/about/new-in-032/sunset.jpg
index 7d7307bed..7d7307bed 100644
--- a/content/en/about/new-in-032/sunset.jpg
+++ b/docs/content/en/about/new-in-032/sunset.jpg
Binary files differ
diff --git a/content/en/about/what-is-hugo.md b/docs/content/en/about/what-is-hugo.md
index 257c7e82d..257c7e82d 100644
--- a/content/en/about/what-is-hugo.md
+++ b/docs/content/en/about/what-is-hugo.md
diff --git a/content/en/commands/hugo.md b/docs/content/en/commands/hugo.md
index 9ef8b3a04..9ef8b3a04 100644
--- a/content/en/commands/hugo.md
+++ b/docs/content/en/commands/hugo.md
diff --git a/content/en/commands/hugo_check.md b/docs/content/en/commands/hugo_check.md
index d4b1d56e2..d4b1d56e2 100644
--- a/content/en/commands/hugo_check.md
+++ b/docs/content/en/commands/hugo_check.md
diff --git a/content/en/commands/hugo_check_ulimit.md b/docs/content/en/commands/hugo_check_ulimit.md
index c7be29efa..c7be29efa 100644
--- a/content/en/commands/hugo_check_ulimit.md
+++ b/docs/content/en/commands/hugo_check_ulimit.md
diff --git a/content/en/commands/hugo_config.md b/docs/content/en/commands/hugo_config.md
index 5236b5fac..5236b5fac 100644
--- a/content/en/commands/hugo_config.md
+++ b/docs/content/en/commands/hugo_config.md
diff --git a/content/en/commands/hugo_convert.md b/docs/content/en/commands/hugo_convert.md
index 41d45dbb2..41d45dbb2 100644
--- a/content/en/commands/hugo_convert.md
+++ b/docs/content/en/commands/hugo_convert.md
diff --git a/content/en/commands/hugo_convert_toJSON.md b/docs/content/en/commands/hugo_convert_toJSON.md
index 7f613c822..7f613c822 100644
--- a/content/en/commands/hugo_convert_toJSON.md
+++ b/docs/content/en/commands/hugo_convert_toJSON.md
diff --git a/content/en/commands/hugo_convert_toTOML.md b/docs/content/en/commands/hugo_convert_toTOML.md
index 491c269f7..491c269f7 100644
--- a/content/en/commands/hugo_convert_toTOML.md
+++ b/docs/content/en/commands/hugo_convert_toTOML.md
diff --git a/content/en/commands/hugo_convert_toYAML.md b/docs/content/en/commands/hugo_convert_toYAML.md
index e5807853e..e5807853e 100644
--- a/content/en/commands/hugo_convert_toYAML.md
+++ b/docs/content/en/commands/hugo_convert_toYAML.md
diff --git a/content/en/commands/hugo_env.md b/docs/content/en/commands/hugo_env.md
index 87ad8c4e7..87ad8c4e7 100644
--- a/content/en/commands/hugo_env.md
+++ b/docs/content/en/commands/hugo_env.md
diff --git a/content/en/commands/hugo_gen.md b/docs/content/en/commands/hugo_gen.md
index 511bb7b28..511bb7b28 100644
--- a/content/en/commands/hugo_gen.md
+++ b/docs/content/en/commands/hugo_gen.md
diff --git a/content/en/commands/hugo_gen_autocomplete.md b/docs/content/en/commands/hugo_gen_autocomplete.md
index 641d0238b..641d0238b 100644
--- a/content/en/commands/hugo_gen_autocomplete.md
+++ b/docs/content/en/commands/hugo_gen_autocomplete.md
diff --git a/content/en/commands/hugo_gen_chromastyles.md b/docs/content/en/commands/hugo_gen_chromastyles.md
index 461860ac3..461860ac3 100644
--- a/content/en/commands/hugo_gen_chromastyles.md
+++ b/docs/content/en/commands/hugo_gen_chromastyles.md
diff --git a/content/en/commands/hugo_gen_doc.md b/docs/content/en/commands/hugo_gen_doc.md
index 7158ce9fd..7158ce9fd 100644
--- a/content/en/commands/hugo_gen_doc.md
+++ b/docs/content/en/commands/hugo_gen_doc.md
diff --git a/content/en/commands/hugo_gen_man.md b/docs/content/en/commands/hugo_gen_man.md
index 45b1fcefe..45b1fcefe 100644
--- a/content/en/commands/hugo_gen_man.md
+++ b/docs/content/en/commands/hugo_gen_man.md
diff --git a/content/en/commands/hugo_import.md b/docs/content/en/commands/hugo_import.md
index b12b7f714..b12b7f714 100644
--- a/content/en/commands/hugo_import.md
+++ b/docs/content/en/commands/hugo_import.md
diff --git a/content/en/commands/hugo_import_jekyll.md b/docs/content/en/commands/hugo_import_jekyll.md
index d794d119d..d794d119d 100644
--- a/content/en/commands/hugo_import_jekyll.md
+++ b/docs/content/en/commands/hugo_import_jekyll.md
diff --git a/content/en/commands/hugo_list.md b/docs/content/en/commands/hugo_list.md
index 99caee1e8..99caee1e8 100644
--- a/content/en/commands/hugo_list.md
+++ b/docs/content/en/commands/hugo_list.md
diff --git a/content/en/commands/hugo_list_drafts.md b/docs/content/en/commands/hugo_list_drafts.md
index 271a3ee5c..271a3ee5c 100644
--- a/content/en/commands/hugo_list_drafts.md
+++ b/docs/content/en/commands/hugo_list_drafts.md
diff --git a/content/en/commands/hugo_list_expired.md b/docs/content/en/commands/hugo_list_expired.md
index df273ff73..df273ff73 100644
--- a/content/en/commands/hugo_list_expired.md
+++ b/docs/content/en/commands/hugo_list_expired.md
diff --git a/content/en/commands/hugo_list_future.md b/docs/content/en/commands/hugo_list_future.md
index 4c559f2ba..4c559f2ba 100644
--- a/content/en/commands/hugo_list_future.md
+++ b/docs/content/en/commands/hugo_list_future.md
diff --git a/content/en/commands/hugo_new.md b/docs/content/en/commands/hugo_new.md
index 8b31e42fb..8b31e42fb 100644
--- a/content/en/commands/hugo_new.md
+++ b/docs/content/en/commands/hugo_new.md
diff --git a/content/en/commands/hugo_new_site.md b/docs/content/en/commands/hugo_new_site.md
index babe7127a..babe7127a 100644
--- a/content/en/commands/hugo_new_site.md
+++ b/docs/content/en/commands/hugo_new_site.md
diff --git a/content/en/commands/hugo_new_theme.md b/docs/content/en/commands/hugo_new_theme.md
index 284b5cd3e..284b5cd3e 100644
--- a/content/en/commands/hugo_new_theme.md
+++ b/docs/content/en/commands/hugo_new_theme.md
diff --git a/content/en/commands/hugo_server.md b/docs/content/en/commands/hugo_server.md
index 447482102..447482102 100644
--- a/content/en/commands/hugo_server.md
+++ b/docs/content/en/commands/hugo_server.md
diff --git a/content/en/commands/hugo_version.md b/docs/content/en/commands/hugo_version.md
index cdc014610..cdc014610 100644
--- a/content/en/commands/hugo_version.md
+++ b/docs/content/en/commands/hugo_version.md
diff --git a/content/en/content-management/_index.md b/docs/content/en/content-management/_index.md
index 28f2ecf82..28f2ecf82 100644
--- a/content/en/content-management/_index.md
+++ b/docs/content/en/content-management/_index.md
diff --git a/content/en/content-management/archetypes.md b/docs/content/en/content-management/archetypes.md
index 354ef0fef..354ef0fef 100644
--- a/content/en/content-management/archetypes.md
+++ b/docs/content/en/content-management/archetypes.md
diff --git a/content/en/content-management/authors.md b/docs/content/en/content-management/authors.md
index 530557ac0..530557ac0 100644
--- a/content/en/content-management/authors.md
+++ b/docs/content/en/content-management/authors.md
diff --git a/content/en/content-management/comments.md b/docs/content/en/content-management/comments.md
index dad5d0786..dad5d0786 100644
--- a/content/en/content-management/comments.md
+++ b/docs/content/en/content-management/comments.md
diff --git a/content/en/content-management/cross-references.md b/docs/content/en/content-management/cross-references.md
index f51271306..f51271306 100644
--- a/content/en/content-management/cross-references.md
+++ b/docs/content/en/content-management/cross-references.md
diff --git a/content/en/content-management/formats.md b/docs/content/en/content-management/formats.md
index b65d9e604..b65d9e604 100644
--- a/content/en/content-management/formats.md
+++ b/docs/content/en/content-management/formats.md
diff --git a/content/en/content-management/front-matter.md b/docs/content/en/content-management/front-matter.md
index 4ff374000..4ff374000 100644
--- a/content/en/content-management/front-matter.md
+++ b/docs/content/en/content-management/front-matter.md
diff --git a/content/en/content-management/image-processing/index.md b/docs/content/en/content-management/image-processing/index.md
index b83a6c103..b83a6c103 100644
--- a/content/en/content-management/image-processing/index.md
+++ b/docs/content/en/content-management/image-processing/index.md
diff --git a/content/en/content-management/image-processing/sunset.jpg b/docs/content/en/content-management/image-processing/sunset.jpg
index 7d7307bed..7d7307bed 100644
--- a/content/en/content-management/image-processing/sunset.jpg
+++ b/docs/content/en/content-management/image-processing/sunset.jpg
Binary files differ
diff --git a/content/en/content-management/menus.md b/docs/content/en/content-management/menus.md
index 9ac6f8bff..9ac6f8bff 100644
--- a/content/en/content-management/menus.md
+++ b/docs/content/en/content-management/menus.md
diff --git a/content/en/content-management/multilingual.md b/docs/content/en/content-management/multilingual.md
index bd9bd97d7..bd9bd97d7 100644
--- a/content/en/content-management/multilingual.md
+++ b/docs/content/en/content-management/multilingual.md
diff --git a/content/en/content-management/organization/1-featured-content-bundles.png b/docs/content/en/content-management/organization/1-featured-content-bundles.png
index 1706a29d6..1706a29d6 100644
--- a/content/en/content-management/organization/1-featured-content-bundles.png
+++ b/docs/content/en/content-management/organization/1-featured-content-bundles.png
Binary files differ
diff --git a/content/en/content-management/organization/index.md b/docs/content/en/content-management/organization/index.md
index e9ae91b57..e9ae91b57 100644
--- a/content/en/content-management/organization/index.md
+++ b/docs/content/en/content-management/organization/index.md
diff --git a/content/en/content-management/page-bundles.md b/docs/content/en/content-management/page-bundles.md
index 0d665759c..0d665759c 100644
--- a/content/en/content-management/page-bundles.md
+++ b/docs/content/en/content-management/page-bundles.md
diff --git a/content/en/content-management/page-resources.md b/docs/content/en/content-management/page-resources.md
index dcd19e42f..dcd19e42f 100644
--- a/content/en/content-management/page-resources.md
+++ b/docs/content/en/content-management/page-resources.md
diff --git a/content/en/content-management/related.md b/docs/content/en/content-management/related.md
index e87aecca4..e87aecca4 100644
--- a/content/en/content-management/related.md
+++ b/docs/content/en/content-management/related.md
diff --git a/content/en/content-management/sections.md b/docs/content/en/content-management/sections.md
index 79ae201d4..79ae201d4 100644
--- a/content/en/content-management/sections.md
+++ b/docs/content/en/content-management/sections.md
diff --git a/content/en/content-management/shortcodes.md b/docs/content/en/content-management/shortcodes.md
index 3be1c6f9e..3be1c6f9e 100644
--- a/content/en/content-management/shortcodes.md
+++ b/docs/content/en/content-management/shortcodes.md
diff --git a/content/en/content-management/static-files.md b/docs/content/en/content-management/static-files.md
index e42ee9088..e42ee9088 100644
--- a/content/en/content-management/static-files.md
+++ b/docs/content/en/content-management/static-files.md
diff --git a/content/en/content-management/summaries.md b/docs/content/en/content-management/summaries.md
index 3c67a67dc..3c67a67dc 100644
--- a/content/en/content-management/summaries.md
+++ b/docs/content/en/content-management/summaries.md
diff --git a/content/en/content-management/syntax-highlighting.md b/docs/content/en/content-management/syntax-highlighting.md
index ea2db4650..ea2db4650 100644
--- a/content/en/content-management/syntax-highlighting.md
+++ b/docs/content/en/content-management/syntax-highlighting.md
diff --git a/content/en/content-management/taxonomies.md b/docs/content/en/content-management/taxonomies.md
index 03747e72b..03747e72b 100644
--- a/content/en/content-management/taxonomies.md
+++ b/docs/content/en/content-management/taxonomies.md
diff --git a/content/en/content-management/toc.md b/docs/content/en/content-management/toc.md
index 31326746c..31326746c 100644
--- a/content/en/content-management/toc.md
+++ b/docs/content/en/content-management/toc.md
diff --git a/content/en/content-management/types.md b/docs/content/en/content-management/types.md
index dc412af19..dc412af19 100644
--- a/content/en/content-management/types.md
+++ b/docs/content/en/content-management/types.md
diff --git a/content/en/content-management/urls.md b/docs/content/en/content-management/urls.md
index 9cb16f52d..9cb16f52d 100644
--- a/content/en/content-management/urls.md
+++ b/docs/content/en/content-management/urls.md
diff --git a/content/en/contribute/_index.md b/docs/content/en/contribute/_index.md
index 5e46ae287..5e46ae287 100644
--- a/content/en/contribute/_index.md
+++ b/docs/content/en/contribute/_index.md
diff --git a/content/en/contribute/development.md b/docs/content/en/contribute/development.md
index 2ac0d0ddb..2ac0d0ddb 100644
--- a/content/en/contribute/development.md
+++ b/docs/content/en/contribute/development.md
diff --git a/content/en/contribute/documentation.md b/docs/content/en/contribute/documentation.md
index 0e97aba1a..0e97aba1a 100644
--- a/content/en/contribute/documentation.md
+++ b/docs/content/en/contribute/documentation.md
diff --git a/content/en/contribute/themes.md b/docs/content/en/contribute/themes.md
index 5ec244fe7..5ec244fe7 100644
--- a/content/en/contribute/themes.md
+++ b/docs/content/en/contribute/themes.md
diff --git a/content/en/documentation.md b/docs/content/en/documentation.md
index 9371dbdf0..9371dbdf0 100644
--- a/content/en/documentation.md
+++ b/docs/content/en/documentation.md
diff --git a/content/en/functions/GetPage.md b/docs/content/en/functions/GetPage.md
index 366d1f093..366d1f093 100644
--- a/content/en/functions/GetPage.md
+++ b/docs/content/en/functions/GetPage.md
diff --git a/content/en/functions/NumFmt.md b/docs/content/en/functions/NumFmt.md
index 1bc07abd5..1bc07abd5 100644
--- a/content/en/functions/NumFmt.md
+++ b/docs/content/en/functions/NumFmt.md
diff --git a/content/en/functions/_index.md b/docs/content/en/functions/_index.md
index df10f2ae5..df10f2ae5 100644
--- a/content/en/functions/_index.md
+++ b/docs/content/en/functions/_index.md
diff --git a/content/en/functions/abslangurl.md b/docs/content/en/functions/abslangurl.md
index 418ff50fd..418ff50fd 100644
--- a/content/en/functions/abslangurl.md
+++ b/docs/content/en/functions/abslangurl.md
diff --git a/content/en/functions/absurl.md b/docs/content/en/functions/absurl.md
index a31dbb0b4..a31dbb0b4 100644
--- a/content/en/functions/absurl.md
+++ b/docs/content/en/functions/absurl.md
diff --git a/content/en/functions/adddate.md b/docs/content/en/functions/adddate.md
index 19eabff7f..19eabff7f 100644
--- a/content/en/functions/adddate.md
+++ b/docs/content/en/functions/adddate.md
diff --git a/content/en/functions/after.md b/docs/content/en/functions/after.md
index d627f792a..d627f792a 100644
--- a/content/en/functions/after.md
+++ b/docs/content/en/functions/after.md
diff --git a/content/en/functions/anchorize.md b/docs/content/en/functions/anchorize.md
index a0745edaf..a0745edaf 100644
--- a/content/en/functions/anchorize.md
+++ b/docs/content/en/functions/anchorize.md
diff --git a/content/en/functions/append.md b/docs/content/en/functions/append.md
index 732ffeadd..732ffeadd 100644
--- a/content/en/functions/append.md
+++ b/docs/content/en/functions/append.md
diff --git a/content/en/functions/apply.md b/docs/content/en/functions/apply.md
index df22732a0..df22732a0 100644
--- a/content/en/functions/apply.md
+++ b/docs/content/en/functions/apply.md
diff --git a/content/en/functions/base64.md b/docs/content/en/functions/base64.md
index 2f0729b85..2f0729b85 100644
--- a/content/en/functions/base64.md
+++ b/docs/content/en/functions/base64.md
diff --git a/content/en/functions/chomp.md b/docs/content/en/functions/chomp.md
index 04fd5e478..04fd5e478 100644
--- a/content/en/functions/chomp.md
+++ b/docs/content/en/functions/chomp.md
diff --git a/content/en/functions/complement.md b/docs/content/en/functions/complement.md
index 461227789..461227789 100644
--- a/content/en/functions/complement.md
+++ b/docs/content/en/functions/complement.md
diff --git a/content/en/functions/cond.md b/docs/content/en/functions/cond.md
index a5e534426..a5e534426 100644
--- a/content/en/functions/cond.md
+++ b/docs/content/en/functions/cond.md
diff --git a/content/en/functions/countrunes.md b/docs/content/en/functions/countrunes.md
index a52829a1c..a52829a1c 100644
--- a/content/en/functions/countrunes.md
+++ b/docs/content/en/functions/countrunes.md
diff --git a/content/en/functions/countwords.md b/docs/content/en/functions/countwords.md
index 40a7b39e5..40a7b39e5 100644
--- a/content/en/functions/countwords.md
+++ b/docs/content/en/functions/countwords.md
diff --git a/content/en/functions/dateformat.md b/docs/content/en/functions/dateformat.md
index 5a7afed97..5a7afed97 100644
--- a/content/en/functions/dateformat.md
+++ b/docs/content/en/functions/dateformat.md
diff --git a/content/en/functions/default.md b/docs/content/en/functions/default.md
index 18f7b7d33..18f7b7d33 100644
--- a/content/en/functions/default.md
+++ b/docs/content/en/functions/default.md
diff --git a/content/en/functions/delimit.md b/docs/content/en/functions/delimit.md
index 083e8baf8..083e8baf8 100644
--- a/content/en/functions/delimit.md
+++ b/docs/content/en/functions/delimit.md
diff --git a/content/en/functions/dict.md b/docs/content/en/functions/dict.md
index 007cc30c5..007cc30c5 100644
--- a/content/en/functions/dict.md
+++ b/docs/content/en/functions/dict.md
diff --git a/content/en/functions/echoparam.md b/docs/content/en/functions/echoparam.md
index 47e35f5c7..47e35f5c7 100644
--- a/content/en/functions/echoparam.md
+++ b/docs/content/en/functions/echoparam.md
diff --git a/content/en/functions/emojify.md b/docs/content/en/functions/emojify.md
index 2e82d8a08..2e82d8a08 100644
--- a/content/en/functions/emojify.md
+++ b/docs/content/en/functions/emojify.md
diff --git a/content/en/functions/eq.md b/docs/content/en/functions/eq.md
index 77f75db37..77f75db37 100644
--- a/content/en/functions/eq.md
+++ b/docs/content/en/functions/eq.md
diff --git a/content/en/functions/errorf.md b/docs/content/en/functions/errorf.md
index 73c13da09..73c13da09 100644
--- a/content/en/functions/errorf.md
+++ b/docs/content/en/functions/errorf.md
diff --git a/content/en/functions/fileExists.md b/docs/content/en/functions/fileExists.md
index 3d535aaca..3d535aaca 100644
--- a/content/en/functions/fileExists.md
+++ b/docs/content/en/functions/fileExists.md
diff --git a/content/en/functions/findRe.md b/docs/content/en/functions/findRe.md
index 743c8542b..743c8542b 100644
--- a/content/en/functions/findRe.md
+++ b/docs/content/en/functions/findRe.md
diff --git a/content/en/functions/first.md b/docs/content/en/functions/first.md
index a0c7ca146..a0c7ca146 100644
--- a/content/en/functions/first.md
+++ b/docs/content/en/functions/first.md
diff --git a/content/en/functions/float.md b/docs/content/en/functions/float.md
index 2a5f7579c..2a5f7579c 100644
--- a/content/en/functions/float.md
+++ b/docs/content/en/functions/float.md
diff --git a/content/en/functions/format.md b/docs/content/en/functions/format.md
index fef4d85da..fef4d85da 100644
--- a/content/en/functions/format.md
+++ b/docs/content/en/functions/format.md
diff --git a/content/en/functions/ge.md b/docs/content/en/functions/ge.md
index ecc2a0223..ecc2a0223 100644
--- a/content/en/functions/ge.md
+++ b/docs/content/en/functions/ge.md
diff --git a/content/en/functions/get.md b/docs/content/en/functions/get.md
index f6d6a6e31..f6d6a6e31 100644
--- a/content/en/functions/get.md
+++ b/docs/content/en/functions/get.md
diff --git a/content/en/functions/getenv.md b/docs/content/en/functions/getenv.md
index 8153cc07c..8153cc07c 100644
--- a/content/en/functions/getenv.md
+++ b/docs/content/en/functions/getenv.md
diff --git a/content/en/functions/group.md b/docs/content/en/functions/group.md
index e1a22ef5d..e1a22ef5d 100644
--- a/content/en/functions/group.md
+++ b/docs/content/en/functions/group.md
diff --git a/content/en/functions/gt.md b/docs/content/en/functions/gt.md
index 75b5fff0f..75b5fff0f 100644
--- a/content/en/functions/gt.md
+++ b/docs/content/en/functions/gt.md
diff --git a/content/en/functions/hasPrefix.md b/docs/content/en/functions/hasPrefix.md
index 3deac60c3..3deac60c3 100644
--- a/content/en/functions/hasPrefix.md
+++ b/docs/content/en/functions/hasPrefix.md
diff --git a/content/en/functions/haschildren.md b/docs/content/en/functions/haschildren.md
index ff1b796cc..ff1b796cc 100644
--- a/content/en/functions/haschildren.md
+++ b/docs/content/en/functions/haschildren.md
diff --git a/content/en/functions/hasmenucurrent.md b/docs/content/en/functions/hasmenucurrent.md
index c7b8eb7a9..c7b8eb7a9 100644
--- a/content/en/functions/hasmenucurrent.md
+++ b/docs/content/en/functions/hasmenucurrent.md
diff --git a/content/en/functions/highlight.md b/docs/content/en/functions/highlight.md
index f0845227d..f0845227d 100644
--- a/content/en/functions/highlight.md
+++ b/docs/content/en/functions/highlight.md
diff --git a/content/en/functions/htmlEscape.md b/docs/content/en/functions/htmlEscape.md
index a1a2d6d55..a1a2d6d55 100644
--- a/content/en/functions/htmlEscape.md
+++ b/docs/content/en/functions/htmlEscape.md
diff --git a/content/en/functions/htmlUnescape.md b/docs/content/en/functions/htmlUnescape.md
index d0ef7540c..d0ef7540c 100644
--- a/content/en/functions/htmlUnescape.md
+++ b/docs/content/en/functions/htmlUnescape.md
diff --git a/content/en/functions/humanize.md b/docs/content/en/functions/humanize.md
index fe06de3a7..fe06de3a7 100644
--- a/content/en/functions/humanize.md
+++ b/docs/content/en/functions/humanize.md
diff --git a/content/en/functions/i18n.md b/docs/content/en/functions/i18n.md
index c4b89c322..c4b89c322 100644
--- a/content/en/functions/i18n.md
+++ b/docs/content/en/functions/i18n.md
diff --git a/content/en/functions/imageConfig.md b/docs/content/en/functions/imageConfig.md
index 3952448c6..3952448c6 100644
--- a/content/en/functions/imageConfig.md
+++ b/docs/content/en/functions/imageConfig.md
diff --git a/content/en/functions/in.md b/docs/content/en/functions/in.md
index d3a27bc87..d3a27bc87 100644
--- a/content/en/functions/in.md
+++ b/docs/content/en/functions/in.md
diff --git a/content/en/functions/index-function.md b/docs/content/en/functions/index-function.md
index e5f039caf..e5f039caf 100644
--- a/content/en/functions/index-function.md
+++ b/docs/content/en/functions/index-function.md
diff --git a/content/en/functions/int.md b/docs/content/en/functions/int.md
index f5416c1dc..f5416c1dc 100644
--- a/content/en/functions/int.md
+++ b/docs/content/en/functions/int.md
diff --git a/content/en/functions/intersect.md b/docs/content/en/functions/intersect.md
index 9ab7f3c3a..9ab7f3c3a 100644
--- a/content/en/functions/intersect.md
+++ b/docs/content/en/functions/intersect.md
diff --git a/content/en/functions/ismenucurrent.md b/docs/content/en/functions/ismenucurrent.md
index 66c7197a2..66c7197a2 100644
--- a/content/en/functions/ismenucurrent.md
+++ b/docs/content/en/functions/ismenucurrent.md
diff --git a/content/en/functions/isset.md b/docs/content/en/functions/isset.md
index 8f2c3f7ad..8f2c3f7ad 100644
--- a/content/en/functions/isset.md
+++ b/docs/content/en/functions/isset.md
diff --git a/content/en/functions/jsonify.md b/docs/content/en/functions/jsonify.md
index 5c670244f..5c670244f 100644
--- a/content/en/functions/jsonify.md
+++ b/docs/content/en/functions/jsonify.md
diff --git a/content/en/functions/lang.Merge.md b/docs/content/en/functions/lang.Merge.md
index ecdab3c42..ecdab3c42 100644
--- a/content/en/functions/lang.Merge.md
+++ b/docs/content/en/functions/lang.Merge.md
diff --git a/content/en/functions/last.md b/docs/content/en/functions/last.md
index f992b980a..f992b980a 100644
--- a/content/en/functions/last.md
+++ b/docs/content/en/functions/last.md
diff --git a/content/en/functions/le.md b/docs/content/en/functions/le.md
index 054937f08..054937f08 100644
--- a/content/en/functions/le.md
+++ b/docs/content/en/functions/le.md
diff --git a/content/en/functions/len.md b/docs/content/en/functions/len.md
index e95d49c4a..e95d49c4a 100644
--- a/content/en/functions/len.md
+++ b/docs/content/en/functions/len.md
diff --git a/content/en/functions/lower.md b/docs/content/en/functions/lower.md
index a42081b68..a42081b68 100644
--- a/content/en/functions/lower.md
+++ b/docs/content/en/functions/lower.md
diff --git a/content/en/functions/lt.md b/docs/content/en/functions/lt.md
index 288d59446..288d59446 100644
--- a/content/en/functions/lt.md
+++ b/docs/content/en/functions/lt.md
diff --git a/content/en/functions/markdownify.md b/docs/content/en/functions/markdownify.md
index 50cf3e120..50cf3e120 100644
--- a/content/en/functions/markdownify.md
+++ b/docs/content/en/functions/markdownify.md
diff --git a/content/en/functions/math.md b/docs/content/en/functions/math.md
index eb38fdd0b..eb38fdd0b 100644
--- a/content/en/functions/math.md
+++ b/docs/content/en/functions/math.md
diff --git a/content/en/functions/md5.md b/docs/content/en/functions/md5.md
index db5442166..db5442166 100644
--- a/content/en/functions/md5.md
+++ b/docs/content/en/functions/md5.md
diff --git a/content/en/functions/ne.md b/docs/content/en/functions/ne.md
index b672d730c..b672d730c 100644
--- a/content/en/functions/ne.md
+++ b/docs/content/en/functions/ne.md
diff --git a/content/en/functions/now.md b/docs/content/en/functions/now.md
index ae8213d05..ae8213d05 100644
--- a/content/en/functions/now.md
+++ b/docs/content/en/functions/now.md
diff --git a/content/en/functions/os.Stat.md b/docs/content/en/functions/os.Stat.md
index 1e878d896..1e878d896 100644
--- a/content/en/functions/os.Stat.md
+++ b/docs/content/en/functions/os.Stat.md
diff --git a/content/en/functions/param.md b/docs/content/en/functions/param.md
index 6e81bb025..6e81bb025 100644
--- a/content/en/functions/param.md
+++ b/docs/content/en/functions/param.md
diff --git a/content/en/functions/partialCached.md b/docs/content/en/functions/partialCached.md
index 7becea24b..7becea24b 100644
--- a/content/en/functions/partialCached.md
+++ b/docs/content/en/functions/partialCached.md
diff --git a/content/en/functions/path.Base.md b/docs/content/en/functions/path.Base.md
index 87eb67355..87eb67355 100644
--- a/content/en/functions/path.Base.md
+++ b/docs/content/en/functions/path.Base.md
diff --git a/content/en/functions/path.Dir.md b/docs/content/en/functions/path.Dir.md
index 54a3fb8be..54a3fb8be 100644
--- a/content/en/functions/path.Dir.md
+++ b/docs/content/en/functions/path.Dir.md
diff --git a/content/en/functions/path.Ext.md b/docs/content/en/functions/path.Ext.md
index a36b006f3..a36b006f3 100644
--- a/content/en/functions/path.Ext.md
+++ b/docs/content/en/functions/path.Ext.md
diff --git a/content/en/functions/path.Join.md b/docs/content/en/functions/path.Join.md
index 06a8121f0..06a8121f0 100644
--- a/content/en/functions/path.Join.md
+++ b/docs/content/en/functions/path.Join.md
diff --git a/content/en/functions/path.Split.md b/docs/content/en/functions/path.Split.md
index d6bc15ce9..d6bc15ce9 100644
--- a/content/en/functions/path.Split.md
+++ b/docs/content/en/functions/path.Split.md
diff --git a/content/en/functions/plainify.md b/docs/content/en/functions/plainify.md
index 89e7880cd..89e7880cd 100644
--- a/content/en/functions/plainify.md
+++ b/docs/content/en/functions/plainify.md
diff --git a/content/en/functions/pluralize.md b/docs/content/en/functions/pluralize.md
index 49ce39344..49ce39344 100644
--- a/content/en/functions/pluralize.md
+++ b/docs/content/en/functions/pluralize.md
diff --git a/content/en/functions/print.md b/docs/content/en/functions/print.md
index fffbb79dc..fffbb79dc 100644
--- a/content/en/functions/print.md
+++ b/docs/content/en/functions/print.md
diff --git a/content/en/functions/printf.md b/docs/content/en/functions/printf.md
index dabb97c05..dabb97c05 100644
--- a/content/en/functions/printf.md
+++ b/docs/content/en/functions/printf.md
diff --git a/content/en/functions/println.md b/docs/content/en/functions/println.md
index 36dbfaed6..36dbfaed6 100644
--- a/content/en/functions/println.md
+++ b/docs/content/en/functions/println.md
diff --git a/content/en/functions/querify.md b/docs/content/en/functions/querify.md
index e90e07450..e90e07450 100644
--- a/content/en/functions/querify.md
+++ b/docs/content/en/functions/querify.md
diff --git a/content/en/functions/range.md b/docs/content/en/functions/range.md
index f80967c41..f80967c41 100644
--- a/content/en/functions/range.md
+++ b/docs/content/en/functions/range.md
diff --git a/content/en/functions/readdir.md b/docs/content/en/functions/readdir.md
index 21f626692..21f626692 100644
--- a/content/en/functions/readdir.md
+++ b/docs/content/en/functions/readdir.md
diff --git a/content/en/functions/readfile.md b/docs/content/en/functions/readfile.md
index bcd845c96..bcd845c96 100644
--- a/content/en/functions/readfile.md
+++ b/docs/content/en/functions/readfile.md
diff --git a/content/en/functions/ref.md b/docs/content/en/functions/ref.md
index d63c0a89d..d63c0a89d 100644
--- a/content/en/functions/ref.md
+++ b/docs/content/en/functions/ref.md
diff --git a/content/en/functions/reflect.IsMap.md b/docs/content/en/functions/reflect.IsMap.md
index d75b842b4..d75b842b4 100644
--- a/content/en/functions/reflect.IsMap.md
+++ b/docs/content/en/functions/reflect.IsMap.md
diff --git a/content/en/functions/reflect.IsSlice.md b/docs/content/en/functions/reflect.IsSlice.md
index 27d6aea21..27d6aea21 100644
--- a/content/en/functions/reflect.IsSlice.md
+++ b/docs/content/en/functions/reflect.IsSlice.md
diff --git a/content/en/functions/relLangURL.md b/docs/content/en/functions/relLangURL.md
index 7b70c1117..7b70c1117 100644
--- a/content/en/functions/relLangURL.md
+++ b/docs/content/en/functions/relLangURL.md
diff --git a/content/en/functions/relref.md b/docs/content/en/functions/relref.md
index ea992af2f..ea992af2f 100644
--- a/content/en/functions/relref.md
+++ b/docs/content/en/functions/relref.md
diff --git a/content/en/functions/relurl.md b/docs/content/en/functions/relurl.md
index aa1536544..aa1536544 100644
--- a/content/en/functions/relurl.md
+++ b/docs/content/en/functions/relurl.md
diff --git a/content/en/functions/render.md b/docs/content/en/functions/render.md
index e3909bde3..e3909bde3 100644
--- a/content/en/functions/render.md
+++ b/docs/content/en/functions/render.md
diff --git a/content/en/functions/replace.md b/docs/content/en/functions/replace.md
index aeb19f296..aeb19f296 100644
--- a/content/en/functions/replace.md
+++ b/docs/content/en/functions/replace.md
diff --git a/content/en/functions/replacere.md b/docs/content/en/functions/replacere.md
index 9c2778b5f..9c2778b5f 100644
--- a/content/en/functions/replacere.md
+++ b/docs/content/en/functions/replacere.md
diff --git a/content/en/functions/safeCSS.md b/docs/content/en/functions/safeCSS.md
index 11c10923b..11c10923b 100644
--- a/content/en/functions/safeCSS.md
+++ b/docs/content/en/functions/safeCSS.md
diff --git a/content/en/functions/safeHTML.md b/docs/content/en/functions/safeHTML.md
index 5a59fc2cb..5a59fc2cb 100644
--- a/content/en/functions/safeHTML.md
+++ b/docs/content/en/functions/safeHTML.md
diff --git a/content/en/functions/safeHTMLAttr.md b/docs/content/en/functions/safeHTMLAttr.md
index a5ecaa68b..a5ecaa68b 100644
--- a/content/en/functions/safeHTMLAttr.md
+++ b/docs/content/en/functions/safeHTMLAttr.md
diff --git a/content/en/functions/safeJS.md b/docs/content/en/functions/safeJS.md
index e614e48bf..e614e48bf 100644
--- a/content/en/functions/safeJS.md
+++ b/docs/content/en/functions/safeJS.md
diff --git a/content/en/functions/safeURL.md b/docs/content/en/functions/safeURL.md
index cb9979cd3..cb9979cd3 100644
--- a/content/en/functions/safeURL.md
+++ b/docs/content/en/functions/safeURL.md
diff --git a/content/en/functions/scratch.md b/docs/content/en/functions/scratch.md
index 1a64bb2e3..1a64bb2e3 100644
--- a/content/en/functions/scratch.md
+++ b/docs/content/en/functions/scratch.md
diff --git a/content/en/functions/seq.md b/docs/content/en/functions/seq.md
index 8bef589c5..8bef589c5 100644
--- a/content/en/functions/seq.md
+++ b/docs/content/en/functions/seq.md
diff --git a/content/en/functions/sha.md b/docs/content/en/functions/sha.md
index d10da3446..d10da3446 100644
--- a/content/en/functions/sha.md
+++ b/docs/content/en/functions/sha.md
diff --git a/content/en/functions/shuffle.md b/docs/content/en/functions/shuffle.md
index 9945ba752..9945ba752 100644
--- a/content/en/functions/shuffle.md
+++ b/docs/content/en/functions/shuffle.md
diff --git a/content/en/functions/singularize.md b/docs/content/en/functions/singularize.md
index 885eae23d..885eae23d 100644
--- a/content/en/functions/singularize.md
+++ b/docs/content/en/functions/singularize.md
diff --git a/content/en/functions/slice.md b/docs/content/en/functions/slice.md
index c8847c0c2..c8847c0c2 100644
--- a/content/en/functions/slice.md
+++ b/docs/content/en/functions/slice.md
diff --git a/content/en/functions/slicestr.md b/docs/content/en/functions/slicestr.md
index 3d245de3d..3d245de3d 100644
--- a/content/en/functions/slicestr.md
+++ b/docs/content/en/functions/slicestr.md
diff --git a/content/en/functions/sort.md b/docs/content/en/functions/sort.md
index b83e6b0bd..b83e6b0bd 100644
--- a/content/en/functions/sort.md
+++ b/docs/content/en/functions/sort.md
diff --git a/content/en/functions/split.md b/docs/content/en/functions/split.md
index c42f8eb9d..c42f8eb9d 100644
--- a/content/en/functions/split.md
+++ b/docs/content/en/functions/split.md
diff --git a/content/en/functions/string.md b/docs/content/en/functions/string.md
index d1e1962de..d1e1962de 100644
--- a/content/en/functions/string.md
+++ b/docs/content/en/functions/string.md
diff --git a/content/en/functions/strings.Repeat.md b/docs/content/en/functions/strings.Repeat.md
index 8dcb8eaa2..8dcb8eaa2 100644
--- a/content/en/functions/strings.Repeat.md
+++ b/docs/content/en/functions/strings.Repeat.md
diff --git a/content/en/functions/strings.RuneCount.md b/docs/content/en/functions/strings.RuneCount.md
index 63012ab39..63012ab39 100644
--- a/content/en/functions/strings.RuneCount.md
+++ b/docs/content/en/functions/strings.RuneCount.md
diff --git a/content/en/functions/strings.TrimLeft.md b/docs/content/en/functions/strings.TrimLeft.md
index 6bbd62cf5..6bbd62cf5 100644
--- a/content/en/functions/strings.TrimLeft.md
+++ b/docs/content/en/functions/strings.TrimLeft.md
diff --git a/content/en/functions/strings.TrimPrefix.md b/docs/content/en/functions/strings.TrimPrefix.md
index eeeecf76e..eeeecf76e 100644
--- a/content/en/functions/strings.TrimPrefix.md
+++ b/docs/content/en/functions/strings.TrimPrefix.md
diff --git a/content/en/functions/strings.TrimRight.md b/docs/content/en/functions/strings.TrimRight.md
index 2c6040218..2c6040218 100644
--- a/content/en/functions/strings.TrimRight.md
+++ b/docs/content/en/functions/strings.TrimRight.md
diff --git a/content/en/functions/strings.TrimSuffix.md b/docs/content/en/functions/strings.TrimSuffix.md
index 208e0968d..208e0968d 100644
--- a/content/en/functions/strings.TrimSuffix.md
+++ b/docs/content/en/functions/strings.TrimSuffix.md
diff --git a/content/en/functions/substr.md b/docs/content/en/functions/substr.md
index 9dde05ec9..9dde05ec9 100644
--- a/content/en/functions/substr.md
+++ b/docs/content/en/functions/substr.md
diff --git a/content/en/functions/symdiff.md b/docs/content/en/functions/symdiff.md
index b47bd26c0..b47bd26c0 100644
--- a/content/en/functions/symdiff.md
+++ b/docs/content/en/functions/symdiff.md
diff --git a/content/en/functions/templates.Exists.md b/docs/content/en/functions/templates.Exists.md
index 919a9c3b7..919a9c3b7 100644
--- a/content/en/functions/templates.Exists.md
+++ b/docs/content/en/functions/templates.Exists.md
diff --git a/content/en/functions/time.md b/docs/content/en/functions/time.md
index 306d32649..306d32649 100644
--- a/content/en/functions/time.md
+++ b/docs/content/en/functions/time.md
diff --git a/content/en/functions/title.md b/docs/content/en/functions/title.md
index 63a34835f..63a34835f 100644
--- a/content/en/functions/title.md
+++ b/docs/content/en/functions/title.md
diff --git a/content/en/functions/transform.Unmarshal.md b/docs/content/en/functions/transform.Unmarshal.md
index 571117090..571117090 100644
--- a/content/en/functions/transform.Unmarshal.md
+++ b/docs/content/en/functions/transform.Unmarshal.md
diff --git a/content/en/functions/trim.md b/docs/content/en/functions/trim.md
index 81ed05c60..81ed05c60 100644
--- a/content/en/functions/trim.md
+++ b/docs/content/en/functions/trim.md
diff --git a/content/en/functions/truncate.md b/docs/content/en/functions/truncate.md
index 0336853c1..0336853c1 100644
--- a/content/en/functions/truncate.md
+++ b/docs/content/en/functions/truncate.md
diff --git a/content/en/functions/union.md b/docs/content/en/functions/union.md
index db3c14283..db3c14283 100644
--- a/content/en/functions/union.md
+++ b/docs/content/en/functions/union.md
diff --git a/content/en/functions/uniq.md b/docs/content/en/functions/uniq.md
index 9692b247e..9692b247e 100644
--- a/content/en/functions/uniq.md
+++ b/docs/content/en/functions/uniq.md
diff --git a/content/en/functions/unix.md b/docs/content/en/functions/unix.md
index a373475f6..a373475f6 100644
--- a/content/en/functions/unix.md
+++ b/docs/content/en/functions/unix.md
diff --git a/content/en/functions/upper.md b/docs/content/en/functions/upper.md
index 2d75b37bd..2d75b37bd 100644
--- a/content/en/functions/upper.md
+++ b/docs/content/en/functions/upper.md
diff --git a/content/en/functions/urlize.md b/docs/content/en/functions/urlize.md
index 0fd7c2295..0fd7c2295 100644
--- a/content/en/functions/urlize.md
+++ b/docs/content/en/functions/urlize.md
diff --git a/content/en/functions/urls.Parse.md b/docs/content/en/functions/urls.Parse.md
index 6d0ade0f8..6d0ade0f8 100644
--- a/content/en/functions/urls.Parse.md
+++ b/docs/content/en/functions/urls.Parse.md
diff --git a/content/en/functions/where.md b/docs/content/en/functions/where.md
index ece4acb21..ece4acb21 100644
--- a/content/en/functions/where.md
+++ b/docs/content/en/functions/where.md
diff --git a/content/en/functions/with.md b/docs/content/en/functions/with.md
index 3fad8bd9c..3fad8bd9c 100644
--- a/content/en/functions/with.md
+++ b/docs/content/en/functions/with.md
diff --git a/content/en/getting-started/_index.md b/docs/content/en/getting-started/_index.md
index 478d1eaa6..478d1eaa6 100644
--- a/content/en/getting-started/_index.md
+++ b/docs/content/en/getting-started/_index.md
diff --git a/content/en/getting-started/code-toggle.md b/docs/content/en/getting-started/code-toggle.md
index c15391b04..c15391b04 100644
--- a/content/en/getting-started/code-toggle.md
+++ b/docs/content/en/getting-started/code-toggle.md
diff --git a/content/en/getting-started/configuration.md b/docs/content/en/getting-started/configuration.md
index 6fbfd3779..6fbfd3779 100644
--- a/content/en/getting-started/configuration.md
+++ b/docs/content/en/getting-started/configuration.md
diff --git a/content/en/getting-started/directory-structure.md b/docs/content/en/getting-started/directory-structure.md
index 4842409d2..4842409d2 100644
--- a/content/en/getting-started/directory-structure.md
+++ b/docs/content/en/getting-started/directory-structure.md
diff --git a/content/en/getting-started/installing.md b/docs/content/en/getting-started/installing.md
index e73511c5b..e73511c5b 100644
--- a/content/en/getting-started/installing.md
+++ b/docs/content/en/getting-started/installing.md
diff --git a/content/en/getting-started/quick-start.md b/docs/content/en/getting-started/quick-start.md
index b1b5fec31..b1b5fec31 100644
--- a/content/en/getting-started/quick-start.md
+++ b/docs/content/en/getting-started/quick-start.md
diff --git a/content/en/getting-started/usage.md b/docs/content/en/getting-started/usage.md
index 1471b51fc..1471b51fc 100644
--- a/content/en/getting-started/usage.md
+++ b/docs/content/en/getting-started/usage.md
diff --git a/content/en/hosting-and-deployment/_index.md b/docs/content/en/hosting-and-deployment/_index.md
index ea9f60f17..ea9f60f17 100644
--- a/content/en/hosting-and-deployment/_index.md
+++ b/docs/content/en/hosting-and-deployment/_index.md
diff --git a/content/en/hosting-and-deployment/deployment-with-nanobox.md b/docs/content/en/hosting-and-deployment/deployment-with-nanobox.md
index 1ab77c401..1ab77c401 100644
--- a/content/en/hosting-and-deployment/deployment-with-nanobox.md
+++ b/docs/content/en/hosting-and-deployment/deployment-with-nanobox.md
diff --git a/content/en/hosting-and-deployment/deployment-with-rsync.md b/docs/content/en/hosting-and-deployment/deployment-with-rsync.md
index 8d0137ad7..8d0137ad7 100644
--- a/content/en/hosting-and-deployment/deployment-with-rsync.md
+++ b/docs/content/en/hosting-and-deployment/deployment-with-rsync.md
diff --git a/content/en/hosting-and-deployment/deployment-with-wercker.md b/docs/content/en/hosting-and-deployment/deployment-with-wercker.md
index 1fed46430..1fed46430 100644
--- a/content/en/hosting-and-deployment/deployment-with-wercker.md
+++ b/docs/content/en/hosting-and-deployment/deployment-with-wercker.md
diff --git a/content/en/hosting-and-deployment/hosting-on-aws-amplify.md b/docs/content/en/hosting-and-deployment/hosting-on-aws-amplify.md
index d310cf732..d310cf732 100644
--- a/content/en/hosting-and-deployment/hosting-on-aws-amplify.md
+++ b/docs/content/en/hosting-and-deployment/hosting-on-aws-amplify.md
diff --git a/content/en/hosting-and-deployment/hosting-on-bitbucket.md b/docs/content/en/hosting-and-deployment/hosting-on-bitbucket.md
index 03710690e..03710690e 100644
--- a/content/en/hosting-and-deployment/hosting-on-bitbucket.md
+++ b/docs/content/en/hosting-and-deployment/hosting-on-bitbucket.md
diff --git a/content/en/hosting-and-deployment/hosting-on-firebase.md b/docs/content/en/hosting-and-deployment/hosting-on-firebase.md
index ef387cdce..ef387cdce 100644
--- a/content/en/hosting-and-deployment/hosting-on-firebase.md
+++ b/docs/content/en/hosting-and-deployment/hosting-on-firebase.md
diff --git a/content/en/hosting-and-deployment/hosting-on-github.md b/docs/content/en/hosting-and-deployment/hosting-on-github.md
index 37b82ab2b..37b82ab2b 100644
--- a/content/en/hosting-and-deployment/hosting-on-github.md
+++ b/docs/content/en/hosting-and-deployment/hosting-on-github.md
diff --git a/content/en/hosting-and-deployment/hosting-on-gitlab.md b/docs/content/en/hosting-and-deployment/hosting-on-gitlab.md
index eda592d43..eda592d43 100644
--- a/content/en/hosting-and-deployment/hosting-on-gitlab.md
+++ b/docs/content/en/hosting-and-deployment/hosting-on-gitlab.md
diff --git a/content/en/hosting-and-deployment/hosting-on-keycdn.md b/docs/content/en/hosting-and-deployment/hosting-on-keycdn.md
index 05bac2ff4..05bac2ff4 100644
--- a/content/en/hosting-and-deployment/hosting-on-keycdn.md
+++ b/docs/content/en/hosting-and-deployment/hosting-on-keycdn.md
diff --git a/content/en/hosting-and-deployment/hosting-on-netlify.md b/docs/content/en/hosting-and-deployment/hosting-on-netlify.md
index a04333d89..a04333d89 100644
--- a/content/en/hosting-and-deployment/hosting-on-netlify.md
+++ b/docs/content/en/hosting-and-deployment/hosting-on-netlify.md
diff --git a/content/en/hugo-pipes/_index.md b/docs/content/en/hugo-pipes/_index.md
index 47411072a..47411072a 100755
--- a/content/en/hugo-pipes/_index.md
+++ b/docs/content/en/hugo-pipes/_index.md
diff --git a/content/en/hugo-pipes/bundling.md b/docs/content/en/hugo-pipes/bundling.md
index 32f0a7881..32f0a7881 100755
--- a/content/en/hugo-pipes/bundling.md
+++ b/docs/content/en/hugo-pipes/bundling.md
diff --git a/content/en/hugo-pipes/fingerprint.md b/docs/content/en/hugo-pipes/fingerprint.md
index 7aa3f100a..7aa3f100a 100755
--- a/content/en/hugo-pipes/fingerprint.md
+++ b/docs/content/en/hugo-pipes/fingerprint.md
diff --git a/content/en/hugo-pipes/introduction.md b/docs/content/en/hugo-pipes/introduction.md
index 3c42dc68f..3c42dc68f 100755
--- a/content/en/hugo-pipes/introduction.md
+++ b/docs/content/en/hugo-pipes/introduction.md
diff --git a/content/en/hugo-pipes/minification.md b/docs/content/en/hugo-pipes/minification.md
index ce090752f..ce090752f 100755
--- a/content/en/hugo-pipes/minification.md
+++ b/docs/content/en/hugo-pipes/minification.md
diff --git a/content/en/hugo-pipes/postcss.md b/docs/content/en/hugo-pipes/postcss.md
index a0a673798..a0a673798 100755
--- a/content/en/hugo-pipes/postcss.md
+++ b/docs/content/en/hugo-pipes/postcss.md
diff --git a/content/en/hugo-pipes/resource-from-string.md b/docs/content/en/hugo-pipes/resource-from-string.md
index 862fcd930..862fcd930 100755
--- a/content/en/hugo-pipes/resource-from-string.md
+++ b/docs/content/en/hugo-pipes/resource-from-string.md
diff --git a/content/en/hugo-pipes/resource-from-template.md b/docs/content/en/hugo-pipes/resource-from-template.md
index 317630b40..317630b40 100755
--- a/content/en/hugo-pipes/resource-from-template.md
+++ b/docs/content/en/hugo-pipes/resource-from-template.md
diff --git a/content/en/hugo-pipes/scss-sass.md b/docs/content/en/hugo-pipes/scss-sass.md
index 489d16e77..489d16e77 100755
--- a/content/en/hugo-pipes/scss-sass.md
+++ b/docs/content/en/hugo-pipes/scss-sass.md
diff --git a/content/en/maintenance/_index.md b/docs/content/en/maintenance/_index.md
index 691a5d47c..691a5d47c 100644
--- a/content/en/maintenance/_index.md
+++ b/docs/content/en/maintenance/_index.md
diff --git a/content/en/news/0.10-relnotes/index.md b/docs/content/en/news/0.10-relnotes/index.md
index 060998ba0..060998ba0 100644
--- a/content/en/news/0.10-relnotes/index.md
+++ b/docs/content/en/news/0.10-relnotes/index.md
diff --git a/content/en/news/0.11-relnotes/index.md b/docs/content/en/news/0.11-relnotes/index.md
index dc4115fe0..dc4115fe0 100644
--- a/content/en/news/0.11-relnotes/index.md
+++ b/docs/content/en/news/0.11-relnotes/index.md
diff --git a/content/en/news/0.12-relnotes/index.md b/docs/content/en/news/0.12-relnotes/index.md
index 4401b5efc..4401b5efc 100644
--- a/content/en/news/0.12-relnotes/index.md
+++ b/docs/content/en/news/0.12-relnotes/index.md
diff --git a/content/en/news/0.13-relnotes/index.md b/docs/content/en/news/0.13-relnotes/index.md
index 198f5fe7b..198f5fe7b 100644
--- a/content/en/news/0.13-relnotes/index.md
+++ b/docs/content/en/news/0.13-relnotes/index.md
diff --git a/content/en/news/0.14-relnotes/index.md b/docs/content/en/news/0.14-relnotes/index.md
index 532538284..532538284 100644
--- a/content/en/news/0.14-relnotes/index.md
+++ b/docs/content/en/news/0.14-relnotes/index.md
diff --git a/content/en/news/0.15-relnotes/index.md b/docs/content/en/news/0.15-relnotes/index.md
index 5053e3fb2..5053e3fb2 100644
--- a/content/en/news/0.15-relnotes/index.md
+++ b/docs/content/en/news/0.15-relnotes/index.md
diff --git a/content/en/news/0.16-relnotes/index.md b/docs/content/en/news/0.16-relnotes/index.md
index 92f6e54a0..92f6e54a0 100644
--- a/content/en/news/0.16-relnotes/index.md
+++ b/docs/content/en/news/0.16-relnotes/index.md
diff --git a/content/en/news/0.17-relnotes/index.md b/docs/content/en/news/0.17-relnotes/index.md
index 034e8e891..034e8e891 100644
--- a/content/en/news/0.17-relnotes/index.md
+++ b/docs/content/en/news/0.17-relnotes/index.md
diff --git a/content/en/news/0.18-relnotes/index.md b/docs/content/en/news/0.18-relnotes/index.md
index 5aaab9ffe..5aaab9ffe 100644
--- a/content/en/news/0.18-relnotes/index.md
+++ b/docs/content/en/news/0.18-relnotes/index.md
diff --git a/content/en/news/0.19-relnotes/index.md b/docs/content/en/news/0.19-relnotes/index.md
index 5c53b057d..5c53b057d 100644
--- a/content/en/news/0.19-relnotes/index.md
+++ b/docs/content/en/news/0.19-relnotes/index.md
diff --git a/content/en/news/0.20-relnotes/index.md b/docs/content/en/news/0.20-relnotes/index.md
index 75e944a6c..75e944a6c 100644
--- a/content/en/news/0.20-relnotes/index.md
+++ b/docs/content/en/news/0.20-relnotes/index.md
diff --git a/content/en/news/0.20.1-relnotes/index.md b/docs/content/en/news/0.20.1-relnotes/index.md
index 109737bb3..109737bb3 100644
--- a/content/en/news/0.20.1-relnotes/index.md
+++ b/docs/content/en/news/0.20.1-relnotes/index.md
diff --git a/content/en/news/0.20.2-relnotes/index.md b/docs/content/en/news/0.20.2-relnotes/index.md
index 3ee08411d..3ee08411d 100644
--- a/content/en/news/0.20.2-relnotes/index.md
+++ b/docs/content/en/news/0.20.2-relnotes/index.md
diff --git a/content/en/news/0.20.3-relnotes/index.md b/docs/content/en/news/0.20.3-relnotes/index.md
index c79d9b202..c79d9b202 100644
--- a/content/en/news/0.20.3-relnotes/index.md
+++ b/docs/content/en/news/0.20.3-relnotes/index.md
diff --git a/content/en/news/0.20.4-relnotes/index.md b/docs/content/en/news/0.20.4-relnotes/index.md
index 2fde30e14..2fde30e14 100644
--- a/content/en/news/0.20.4-relnotes/index.md
+++ b/docs/content/en/news/0.20.4-relnotes/index.md
diff --git a/content/en/news/0.20.5-relnotes/index.md b/docs/content/en/news/0.20.5-relnotes/index.md
index eaed27832..eaed27832 100644
--- a/content/en/news/0.20.5-relnotes/index.md
+++ b/docs/content/en/news/0.20.5-relnotes/index.md
diff --git a/content/en/news/0.20.6-relnotes/index.md b/docs/content/en/news/0.20.6-relnotes/index.md
index 52189092a..52189092a 100644
--- a/content/en/news/0.20.6-relnotes/index.md
+++ b/docs/content/en/news/0.20.6-relnotes/index.md
diff --git a/content/en/news/0.20.7-relnotes/index.md b/docs/content/en/news/0.20.7-relnotes/index.md
index 50ac365d5..50ac365d5 100644
--- a/content/en/news/0.20.7-relnotes/index.md
+++ b/docs/content/en/news/0.20.7-relnotes/index.md
diff --git a/content/en/news/0.21-relnotes/index.md b/docs/content/en/news/0.21-relnotes/index.md
index aae1fd0b4..aae1fd0b4 100644
--- a/content/en/news/0.21-relnotes/index.md
+++ b/docs/content/en/news/0.21-relnotes/index.md
diff --git a/content/en/news/0.22-relnotes/index.md b/docs/content/en/news/0.22-relnotes/index.md
index 8a9e8d5f5..8a9e8d5f5 100644
--- a/content/en/news/0.22-relnotes/index.md
+++ b/docs/content/en/news/0.22-relnotes/index.md
diff --git a/content/en/news/0.22.1-relnotes/index.md b/docs/content/en/news/0.22.1-relnotes/index.md
index ceb207d70..ceb207d70 100644
--- a/content/en/news/0.22.1-relnotes/index.md
+++ b/docs/content/en/news/0.22.1-relnotes/index.md
diff --git a/content/en/news/0.23-relnotes/index.md b/docs/content/en/news/0.23-relnotes/index.md
index fdf6e9e73..fdf6e9e73 100644
--- a/content/en/news/0.23-relnotes/index.md
+++ b/docs/content/en/news/0.23-relnotes/index.md
diff --git a/content/en/news/0.24-relnotes/index.md b/docs/content/en/news/0.24-relnotes/index.md
index ec71e246f..ec71e246f 100644
--- a/content/en/news/0.24-relnotes/index.md
+++ b/docs/content/en/news/0.24-relnotes/index.md
diff --git a/content/en/news/0.24.1-relnotes/index.md b/docs/content/en/news/0.24.1-relnotes/index.md
index 2ec2cef55..2ec2cef55 100644
--- a/content/en/news/0.24.1-relnotes/index.md
+++ b/docs/content/en/news/0.24.1-relnotes/index.md
diff --git a/content/en/news/0.25-relnotes/index.md b/docs/content/en/news/0.25-relnotes/index.md
index 9527c911a..9527c911a 100644
--- a/content/en/news/0.25-relnotes/index.md
+++ b/docs/content/en/news/0.25-relnotes/index.md
diff --git a/content/en/news/0.25.1-relnotes/index.md b/docs/content/en/news/0.25.1-relnotes/index.md
index 7d70d87a5..7d70d87a5 100644
--- a/content/en/news/0.25.1-relnotes/index.md
+++ b/docs/content/en/news/0.25.1-relnotes/index.md
diff --git a/content/en/news/0.26-relnotes/index.md b/docs/content/en/news/0.26-relnotes/index.md
index 92f90de99..92f90de99 100644
--- a/content/en/news/0.26-relnotes/index.md
+++ b/docs/content/en/news/0.26-relnotes/index.md
diff --git a/content/en/news/0.27-relnotes/index.md b/docs/content/en/news/0.27-relnotes/index.md
index 92fc3a7b0..92fc3a7b0 100644
--- a/content/en/news/0.27-relnotes/index.md
+++ b/docs/content/en/news/0.27-relnotes/index.md
diff --git a/content/en/news/0.27.1-relnotes/index.md b/docs/content/en/news/0.27.1-relnotes/index.md
index 1184cc175..1184cc175 100644
--- a/content/en/news/0.27.1-relnotes/index.md
+++ b/docs/content/en/news/0.27.1-relnotes/index.md
diff --git a/content/en/news/0.28-relnotes/index.md b/docs/content/en/news/0.28-relnotes/index.md
index 91128e48e..91128e48e 100644
--- a/content/en/news/0.28-relnotes/index.md
+++ b/docs/content/en/news/0.28-relnotes/index.md
diff --git a/content/en/news/0.29-relnotes/index.md b/docs/content/en/news/0.29-relnotes/index.md
index 810781dda..810781dda 100644
--- a/content/en/news/0.29-relnotes/index.md
+++ b/docs/content/en/news/0.29-relnotes/index.md
diff --git a/content/en/news/0.30-relnotes/index.md b/docs/content/en/news/0.30-relnotes/index.md
index 9281a5c20..9281a5c20 100644
--- a/content/en/news/0.30-relnotes/index.md
+++ b/docs/content/en/news/0.30-relnotes/index.md
diff --git a/content/en/news/0.30.1-relnotes/index.md b/docs/content/en/news/0.30.1-relnotes/index.md
index 68165e877..68165e877 100644
--- a/content/en/news/0.30.1-relnotes/index.md
+++ b/docs/content/en/news/0.30.1-relnotes/index.md
diff --git a/content/en/news/0.30.2-relnotes/index.md b/docs/content/en/news/0.30.2-relnotes/index.md
index 1d4bcd946..1d4bcd946 100644
--- a/content/en/news/0.30.2-relnotes/index.md
+++ b/docs/content/en/news/0.30.2-relnotes/index.md
diff --git a/content/en/news/0.31-relnotes/index.md b/docs/content/en/news/0.31-relnotes/index.md
index ba16dfacb..ba16dfacb 100644
--- a/content/en/news/0.31-relnotes/index.md
+++ b/docs/content/en/news/0.31-relnotes/index.md
diff --git a/content/en/news/0.31.1-relnotes/index.md b/docs/content/en/news/0.31.1-relnotes/index.md
index a74470d64..a74470d64 100644
--- a/content/en/news/0.31.1-relnotes/index.md
+++ b/docs/content/en/news/0.31.1-relnotes/index.md
diff --git a/content/en/news/0.32-relnotes/index.md b/docs/content/en/news/0.32-relnotes/index.md
index c3f36fe64..c3f36fe64 100644
--- a/content/en/news/0.32-relnotes/index.md
+++ b/docs/content/en/news/0.32-relnotes/index.md
diff --git a/content/en/news/0.32.1-relnotes/index.md b/docs/content/en/news/0.32.1-relnotes/index.md
index 867e3413e..867e3413e 100644
--- a/content/en/news/0.32.1-relnotes/index.md
+++ b/docs/content/en/news/0.32.1-relnotes/index.md
diff --git a/content/en/news/0.32.2-relnotes/index.md b/docs/content/en/news/0.32.2-relnotes/index.md
index 7453a2678..7453a2678 100644
--- a/content/en/news/0.32.2-relnotes/index.md
+++ b/docs/content/en/news/0.32.2-relnotes/index.md
diff --git a/content/en/news/0.32.3-relnotes/index.md b/docs/content/en/news/0.32.3-relnotes/index.md
index ad795a183..ad795a183 100644
--- a/content/en/news/0.32.3-relnotes/index.md
+++ b/docs/content/en/news/0.32.3-relnotes/index.md
diff --git a/content/en/news/0.32.4-relnotes/index.md b/docs/content/en/news/0.32.4-relnotes/index.md
index bd8163e0d..bd8163e0d 100644
--- a/content/en/news/0.32.4-relnotes/index.md
+++ b/docs/content/en/news/0.32.4-relnotes/index.md
diff --git a/content/en/news/0.33-relnotes/featured-hugo-33-poster.png b/docs/content/en/news/0.33-relnotes/featured-hugo-33-poster.png
index c30caafcc..c30caafcc 100644
--- a/content/en/news/0.33-relnotes/featured-hugo-33-poster.png
+++ b/docs/content/en/news/0.33-relnotes/featured-hugo-33-poster.png
Binary files differ
diff --git a/content/en/news/0.33-relnotes/index.md b/docs/content/en/news/0.33-relnotes/index.md
index 74cd50dc4..74cd50dc4 100644
--- a/content/en/news/0.33-relnotes/index.md
+++ b/docs/content/en/news/0.33-relnotes/index.md
diff --git a/content/en/news/0.34-relnotes/featured-34-poster.png b/docs/content/en/news/0.34-relnotes/featured-34-poster.png
index a5c81b8c8..a5c81b8c8 100644
--- a/content/en/news/0.34-relnotes/featured-34-poster.png
+++ b/docs/content/en/news/0.34-relnotes/featured-34-poster.png
Binary files differ
diff --git a/content/en/news/0.34-relnotes/index.md b/docs/content/en/news/0.34-relnotes/index.md
index dd5418a77..dd5418a77 100644
--- a/content/en/news/0.34-relnotes/index.md
+++ b/docs/content/en/news/0.34-relnotes/index.md
diff --git a/content/en/news/0.35-relnotes/featured-hugo-35-poster.png b/docs/content/en/news/0.35-relnotes/featured-hugo-35-poster.png
index a97e3b901..a97e3b901 100644
--- a/content/en/news/0.35-relnotes/featured-hugo-35-poster.png
+++ b/docs/content/en/news/0.35-relnotes/featured-hugo-35-poster.png
Binary files differ
diff --git a/content/en/news/0.35-relnotes/index.md b/docs/content/en/news/0.35-relnotes/index.md
index 104cbd222..104cbd222 100644
--- a/content/en/news/0.35-relnotes/index.md
+++ b/docs/content/en/news/0.35-relnotes/index.md
diff --git a/content/en/news/0.36-relnotes/featured-hugo-36-poster.png b/docs/content/en/news/0.36-relnotes/featured-hugo-36-poster.png
index 12dec42fc..12dec42fc 100644
--- a/content/en/news/0.36-relnotes/featured-hugo-36-poster.png
+++ b/docs/content/en/news/0.36-relnotes/featured-hugo-36-poster.png
Binary files differ
diff --git a/content/en/news/0.36-relnotes/index.md b/docs/content/en/news/0.36-relnotes/index.md
index 4e6323287..4e6323287 100644
--- a/content/en/news/0.36-relnotes/index.md
+++ b/docs/content/en/news/0.36-relnotes/index.md
diff --git a/content/en/news/0.36.1-relnotes/index.md b/docs/content/en/news/0.36.1-relnotes/index.md
index 00a5b346c..00a5b346c 100644
--- a/content/en/news/0.36.1-relnotes/index.md
+++ b/docs/content/en/news/0.36.1-relnotes/index.md
diff --git a/content/en/news/0.37-relnotes/featured-hugo-37-poster.png b/docs/content/en/news/0.37-relnotes/featured-hugo-37-poster.png
index 9f369ba25..9f369ba25 100644
--- a/content/en/news/0.37-relnotes/featured-hugo-37-poster.png
+++ b/docs/content/en/news/0.37-relnotes/featured-hugo-37-poster.png
Binary files differ
diff --git a/content/en/news/0.37-relnotes/index.md b/docs/content/en/news/0.37-relnotes/index.md
index a9b6b4cef..a9b6b4cef 100644
--- a/content/en/news/0.37-relnotes/index.md
+++ b/docs/content/en/news/0.37-relnotes/index.md
diff --git a/content/en/news/0.37.1-relnotes/index.md b/docs/content/en/news/0.37.1-relnotes/index.md
index 754ed4240..754ed4240 100644
--- a/content/en/news/0.37.1-relnotes/index.md
+++ b/docs/content/en/news/0.37.1-relnotes/index.md
diff --git a/content/en/news/0.38-relnotes/featured-poster.png b/docs/content/en/news/0.38-relnotes/featured-poster.png
index 1e7988c8f..1e7988c8f 100644
--- a/content/en/news/0.38-relnotes/featured-poster.png
+++ b/docs/content/en/news/0.38-relnotes/featured-poster.png
Binary files differ
diff --git a/content/en/news/0.38-relnotes/index.md b/docs/content/en/news/0.38-relnotes/index.md
index 71d167cd5..71d167cd5 100644
--- a/content/en/news/0.38-relnotes/index.md
+++ b/docs/content/en/news/0.38-relnotes/index.md
diff --git a/content/en/news/0.38.1-relnotes/index.md b/docs/content/en/news/0.38.1-relnotes/index.md
index a025b5415..a025b5415 100644
--- a/content/en/news/0.38.1-relnotes/index.md
+++ b/docs/content/en/news/0.38.1-relnotes/index.md
diff --git a/content/en/news/0.38.2-relnotes/index.md b/docs/content/en/news/0.38.2-relnotes/index.md
index 0a045eee8..0a045eee8 100644
--- a/content/en/news/0.38.2-relnotes/index.md
+++ b/docs/content/en/news/0.38.2-relnotes/index.md
diff --git a/content/en/news/0.39-relnotes/featured-hugo-39-poster.png b/docs/content/en/news/0.39-relnotes/featured-hugo-39-poster.png
index e3fa6400a..e3fa6400a 100644
--- a/content/en/news/0.39-relnotes/featured-hugo-39-poster.png
+++ b/docs/content/en/news/0.39-relnotes/featured-hugo-39-poster.png
Binary files differ
diff --git a/content/en/news/0.39-relnotes/index.md b/docs/content/en/news/0.39-relnotes/index.md
index d1c28252a..d1c28252a 100644
--- a/content/en/news/0.39-relnotes/index.md
+++ b/docs/content/en/news/0.39-relnotes/index.md
diff --git a/content/en/news/0.40-relnotes/featured-hugo-40-poster.png b/docs/content/en/news/0.40-relnotes/featured-hugo-40-poster.png
index 9a7f36d1f..9a7f36d1f 100644
--- a/content/en/news/0.40-relnotes/featured-hugo-40-poster.png
+++ b/docs/content/en/news/0.40-relnotes/featured-hugo-40-poster.png
Binary files differ
diff --git a/content/en/news/0.40-relnotes/index.md b/docs/content/en/news/0.40-relnotes/index.md
index 9a45c1c09..9a45c1c09 100644
--- a/content/en/news/0.40-relnotes/index.md
+++ b/docs/content/en/news/0.40-relnotes/index.md
diff --git a/content/en/news/0.40.1-relnotes/index.md b/docs/content/en/news/0.40.1-relnotes/index.md
index 3352f164b..3352f164b 100644
--- a/content/en/news/0.40.1-relnotes/index.md
+++ b/docs/content/en/news/0.40.1-relnotes/index.md
diff --git a/content/en/news/0.40.2-relnotes/index.md b/docs/content/en/news/0.40.2-relnotes/index.md
index 50b9c3842..50b9c3842 100644
--- a/content/en/news/0.40.2-relnotes/index.md
+++ b/docs/content/en/news/0.40.2-relnotes/index.md
diff --git a/content/en/news/0.40.3-relnotes/index.md b/docs/content/en/news/0.40.3-relnotes/index.md
index 6f822809d..6f822809d 100644
--- a/content/en/news/0.40.3-relnotes/index.md
+++ b/docs/content/en/news/0.40.3-relnotes/index.md
diff --git a/content/en/news/0.41-relnotes/featured-hugo-41-poster.png b/docs/content/en/news/0.41-relnotes/featured-hugo-41-poster.png
index 8f752f665..8f752f665 100644
--- a/content/en/news/0.41-relnotes/featured-hugo-41-poster.png
+++ b/docs/content/en/news/0.41-relnotes/featured-hugo-41-poster.png
Binary files differ
diff --git a/content/en/news/0.41-relnotes/index.md b/docs/content/en/news/0.41-relnotes/index.md
index 411e373e5..411e373e5 100644
--- a/content/en/news/0.41-relnotes/index.md
+++ b/docs/content/en/news/0.41-relnotes/index.md
diff --git a/content/en/news/0.42-relnotes/featured-hugo-42-poster.png b/docs/content/en/news/0.42-relnotes/featured-hugo-42-poster.png
index 1f1cab1f1..1f1cab1f1 100644
--- a/content/en/news/0.42-relnotes/featured-hugo-42-poster.png
+++ b/docs/content/en/news/0.42-relnotes/featured-hugo-42-poster.png
Binary files differ
diff --git a/content/en/news/0.42-relnotes/index.md b/docs/content/en/news/0.42-relnotes/index.md
index c3f99d313..c3f99d313 100644
--- a/content/en/news/0.42-relnotes/index.md
+++ b/docs/content/en/news/0.42-relnotes/index.md
diff --git a/content/en/news/0.42.1-relnotes/index.md b/docs/content/en/news/0.42.1-relnotes/index.md
index 6b5b3c775..6b5b3c775 100644
--- a/content/en/news/0.42.1-relnotes/index.md
+++ b/docs/content/en/news/0.42.1-relnotes/index.md
diff --git a/content/en/news/0.42.2-relnotes/index.md b/docs/content/en/news/0.42.2-relnotes/index.md
index c9bf6c469..c9bf6c469 100644
--- a/content/en/news/0.42.2-relnotes/index.md
+++ b/docs/content/en/news/0.42.2-relnotes/index.md
diff --git a/content/en/news/0.43-relnotes/featured-hugo-43-poster.png b/docs/content/en/news/0.43-relnotes/featured-hugo-43-poster.png
index b221ca7f1..b221ca7f1 100644
--- a/content/en/news/0.43-relnotes/featured-hugo-43-poster.png
+++ b/docs/content/en/news/0.43-relnotes/featured-hugo-43-poster.png
Binary files differ
diff --git a/content/en/news/0.43-relnotes/index.md b/docs/content/en/news/0.43-relnotes/index.md
index cd8515995..cd8515995 100644
--- a/content/en/news/0.43-relnotes/index.md
+++ b/docs/content/en/news/0.43-relnotes/index.md
diff --git a/content/en/news/0.44-relnotes/featured-hugo-44-poster.png b/docs/content/en/news/0.44-relnotes/featured-hugo-44-poster.png
index 330b235fb..330b235fb 100644
--- a/content/en/news/0.44-relnotes/featured-hugo-44-poster.png
+++ b/docs/content/en/news/0.44-relnotes/featured-hugo-44-poster.png
Binary files differ
diff --git a/content/en/news/0.44-relnotes/index.md b/docs/content/en/news/0.44-relnotes/index.md
index aa8396898..aa8396898 100644
--- a/content/en/news/0.44-relnotes/index.md
+++ b/docs/content/en/news/0.44-relnotes/index.md
diff --git a/content/en/news/0.45-relnotes/featured-hugo-45-poster.png b/docs/content/en/news/0.45-relnotes/featured-hugo-45-poster.png
index 40f71daca..40f71daca 100644
--- a/content/en/news/0.45-relnotes/featured-hugo-45-poster.png
+++ b/docs/content/en/news/0.45-relnotes/featured-hugo-45-poster.png
Binary files differ
diff --git a/content/en/news/0.45-relnotes/index.md b/docs/content/en/news/0.45-relnotes/index.md
index 83051c058..83051c058 100644
--- a/content/en/news/0.45-relnotes/index.md
+++ b/docs/content/en/news/0.45-relnotes/index.md
diff --git a/content/en/news/0.45.1-relnotes/index.md b/docs/content/en/news/0.45.1-relnotes/index.md
index 84e0416c7..84e0416c7 100644
--- a/content/en/news/0.45.1-relnotes/index.md
+++ b/docs/content/en/news/0.45.1-relnotes/index.md
diff --git a/content/en/news/0.46-relnotes/featured-hugo-46-poster.png b/docs/content/en/news/0.46-relnotes/featured-hugo-46-poster.png
index c00622e04..c00622e04 100644
--- a/content/en/news/0.46-relnotes/featured-hugo-46-poster.png
+++ b/docs/content/en/news/0.46-relnotes/featured-hugo-46-poster.png
Binary files differ
diff --git a/content/en/news/0.46-relnotes/index.md b/docs/content/en/news/0.46-relnotes/index.md
index 8b065c2ef..8b065c2ef 100644
--- a/content/en/news/0.46-relnotes/index.md
+++ b/docs/content/en/news/0.46-relnotes/index.md
diff --git a/content/en/news/0.47-relnotes/featured-hugo-47-poster.png b/docs/content/en/news/0.47-relnotes/featured-hugo-47-poster.png
index 601922961..601922961 100644
--- a/content/en/news/0.47-relnotes/featured-hugo-47-poster.png
+++ b/docs/content/en/news/0.47-relnotes/featured-hugo-47-poster.png
Binary files differ
diff --git a/content/en/news/0.47-relnotes/index.md b/docs/content/en/news/0.47-relnotes/index.md
index 79d15ec62..79d15ec62 100644
--- a/content/en/news/0.47-relnotes/index.md
+++ b/docs/content/en/news/0.47-relnotes/index.md
diff --git a/content/en/news/0.47.1-relnotes/index.md b/docs/content/en/news/0.47.1-relnotes/index.md
index d35b0fad2..d35b0fad2 100644
--- a/content/en/news/0.47.1-relnotes/index.md
+++ b/docs/content/en/news/0.47.1-relnotes/index.md
diff --git a/content/en/news/0.48-relnotes/featured-hugo-48-poster.png b/docs/content/en/news/0.48-relnotes/featured-hugo-48-poster.png
index 7adb0d22e..7adb0d22e 100644
--- a/content/en/news/0.48-relnotes/featured-hugo-48-poster.png
+++ b/docs/content/en/news/0.48-relnotes/featured-hugo-48-poster.png
Binary files differ
diff --git a/content/en/news/0.48-relnotes/index.md b/docs/content/en/news/0.48-relnotes/index.md
index 92c765f23..92c765f23 100644
--- a/content/en/news/0.48-relnotes/index.md
+++ b/docs/content/en/news/0.48-relnotes/index.md
diff --git a/content/en/news/0.49-relnotes/featured-hugo-49-poster.png b/docs/content/en/news/0.49-relnotes/featured-hugo-49-poster.png
index 6f0f42ed4..6f0f42ed4 100644
--- a/content/en/news/0.49-relnotes/featured-hugo-49-poster.png
+++ b/docs/content/en/news/0.49-relnotes/featured-hugo-49-poster.png
Binary files differ
diff --git a/content/en/news/0.49-relnotes/index.md b/docs/content/en/news/0.49-relnotes/index.md
index 6bb272c33..6bb272c33 100644
--- a/content/en/news/0.49-relnotes/index.md
+++ b/docs/content/en/news/0.49-relnotes/index.md
diff --git a/content/en/news/0.49.1-relnotes/index.md b/docs/content/en/news/0.49.1-relnotes/index.md
index a3858a9e1..a3858a9e1 100644
--- a/content/en/news/0.49.1-relnotes/index.md
+++ b/docs/content/en/news/0.49.1-relnotes/index.md
diff --git a/content/en/news/0.49.2-relnotes/index.md b/docs/content/en/news/0.49.2-relnotes/index.md
index 1d24cd624..1d24cd624 100644
--- a/content/en/news/0.49.2-relnotes/index.md
+++ b/docs/content/en/news/0.49.2-relnotes/index.md
diff --git a/content/en/news/0.50-relnotes/featured-hugo-50-poster.png b/docs/content/en/news/0.50-relnotes/featured-hugo-50-poster.png
index de5b76d79..de5b76d79 100644
--- a/content/en/news/0.50-relnotes/featured-hugo-50-poster.png
+++ b/docs/content/en/news/0.50-relnotes/featured-hugo-50-poster.png
Binary files differ
diff --git a/content/en/news/0.50-relnotes/index.md b/docs/content/en/news/0.50-relnotes/index.md
index 46ab61cd0..46ab61cd0 100644
--- a/content/en/news/0.50-relnotes/index.md
+++ b/docs/content/en/news/0.50-relnotes/index.md
diff --git a/content/en/news/0.51-relnotes/featured-hugo-51-poster.png b/docs/content/en/news/0.51-relnotes/featured-hugo-51-poster.png
index 07755a1ab..07755a1ab 100644
--- a/content/en/news/0.51-relnotes/featured-hugo-51-poster.png
+++ b/docs/content/en/news/0.51-relnotes/featured-hugo-51-poster.png
Binary files differ
diff --git a/content/en/news/0.51-relnotes/index.md b/docs/content/en/news/0.51-relnotes/index.md
index 8590a422c..8590a422c 100644
--- a/content/en/news/0.51-relnotes/index.md
+++ b/docs/content/en/news/0.51-relnotes/index.md
diff --git a/content/en/news/0.52-relnotes/featured-hugo-52-poster.png b/docs/content/en/news/0.52-relnotes/featured-hugo-52-poster.png
index 190f5758a..190f5758a 100644
--- a/content/en/news/0.52-relnotes/featured-hugo-52-poster.png
+++ b/docs/content/en/news/0.52-relnotes/featured-hugo-52-poster.png
Binary files differ
diff --git a/content/en/news/0.52-relnotes/index.md b/docs/content/en/news/0.52-relnotes/index.md
index 7fc19e637..7fc19e637 100644
--- a/content/en/news/0.52-relnotes/index.md
+++ b/docs/content/en/news/0.52-relnotes/index.md
diff --git a/content/en/news/0.53-relnotes/featured-hugo-53-poster.png b/docs/content/en/news/0.53-relnotes/featured-hugo-53-poster.png
index c3cee3adc..c3cee3adc 100644
--- a/content/en/news/0.53-relnotes/featured-hugo-53-poster.png
+++ b/docs/content/en/news/0.53-relnotes/featured-hugo-53-poster.png
Binary files differ
diff --git a/content/en/news/0.53-relnotes/index.md b/docs/content/en/news/0.53-relnotes/index.md
index b61ab9074..b61ab9074 100644
--- a/content/en/news/0.53-relnotes/index.md
+++ b/docs/content/en/news/0.53-relnotes/index.md
diff --git a/content/en/news/0.54.0-relnotes/featured-hugo-54.0-poster.png b/docs/content/en/news/0.54.0-relnotes/featured-hugo-54.0-poster.png
index 10fe563c3..10fe563c3 100644
--- a/content/en/news/0.54.0-relnotes/featured-hugo-54.0-poster.png
+++ b/docs/content/en/news/0.54.0-relnotes/featured-hugo-54.0-poster.png
Binary files differ
diff --git a/content/en/news/0.54.0-relnotes/index.md b/docs/content/en/news/0.54.0-relnotes/index.md
index 8fc56620b..8fc56620b 100644
--- a/content/en/news/0.54.0-relnotes/index.md
+++ b/docs/content/en/news/0.54.0-relnotes/index.md
diff --git a/content/en/news/0.55.0-relnotes/featured.png b/docs/content/en/news/0.55.0-relnotes/featured.png
index 0d3180579..0d3180579 100644
--- a/content/en/news/0.55.0-relnotes/featured.png
+++ b/docs/content/en/news/0.55.0-relnotes/featured.png
Binary files differ
diff --git a/content/en/news/0.55.0-relnotes/index.md b/docs/content/en/news/0.55.0-relnotes/index.md
index c22eaf366..c22eaf366 100644
--- a/content/en/news/0.55.0-relnotes/index.md
+++ b/docs/content/en/news/0.55.0-relnotes/index.md
diff --git a/content/en/news/0.55.1-relnotes/index.md b/docs/content/en/news/0.55.1-relnotes/index.md
index 4e9880dc5..4e9880dc5 100644
--- a/content/en/news/0.55.1-relnotes/index.md
+++ b/docs/content/en/news/0.55.1-relnotes/index.md
diff --git a/content/en/news/0.55.2-relnotes/index.md b/docs/content/en/news/0.55.2-relnotes/index.md
index 0b6f49b11..0b6f49b11 100644
--- a/content/en/news/0.55.2-relnotes/index.md
+++ b/docs/content/en/news/0.55.2-relnotes/index.md
diff --git a/content/en/news/0.55.3-relnotes/index.md b/docs/content/en/news/0.55.3-relnotes/index.md
index d00c47d54..d00c47d54 100644
--- a/content/en/news/0.55.3-relnotes/index.md
+++ b/docs/content/en/news/0.55.3-relnotes/index.md
diff --git a/content/en/news/0.55.4-relnotes/index.md b/docs/content/en/news/0.55.4-relnotes/index.md
index 292b39244..292b39244 100644
--- a/content/en/news/0.55.4-relnotes/index.md
+++ b/docs/content/en/news/0.55.4-relnotes/index.md
diff --git a/content/en/news/0.55.5-relnotes/index.md b/docs/content/en/news/0.55.5-relnotes/index.md
index 45a3eda54..45a3eda54 100644
--- a/content/en/news/0.55.5-relnotes/index.md
+++ b/docs/content/en/news/0.55.5-relnotes/index.md
diff --git a/content/en/news/0.55.6-relnotes/index.md b/docs/content/en/news/0.55.6-relnotes/index.md
index c447aa061..c447aa061 100644
--- a/content/en/news/0.55.6-relnotes/index.md
+++ b/docs/content/en/news/0.55.6-relnotes/index.md
diff --git a/content/en/news/0.7-relnotes/index.md b/docs/content/en/news/0.7-relnotes/index.md
index e140304c0..e140304c0 100644
--- a/content/en/news/0.7-relnotes/index.md
+++ b/docs/content/en/news/0.7-relnotes/index.md
diff --git a/content/en/news/0.8-relnotes/index.md b/docs/content/en/news/0.8-relnotes/index.md
index 6da6b9671..6da6b9671 100644
--- a/content/en/news/0.8-relnotes/index.md
+++ b/docs/content/en/news/0.8-relnotes/index.md
diff --git a/content/en/news/0.9-relnotes/index.md b/docs/content/en/news/0.9-relnotes/index.md
index 5b9bf2c0d..5b9bf2c0d 100644
--- a/content/en/news/0.9-relnotes/index.md
+++ b/docs/content/en/news/0.9-relnotes/index.md
diff --git a/content/en/news/_index.md b/docs/content/en/news/_index.md
index 353accc3d..353accc3d 100644
--- a/content/en/news/_index.md
+++ b/docs/content/en/news/_index.md
diff --git a/content/en/news/http2-server-push-in-hugo.md b/docs/content/en/news/http2-server-push-in-hugo.md
index 5ffb8843f..5ffb8843f 100644
--- a/content/en/news/http2-server-push-in-hugo.md
+++ b/docs/content/en/news/http2-server-push-in-hugo.md
diff --git a/content/en/news/lets-celebrate-hugos-5th-birthday/featured.png b/docs/content/en/news/lets-celebrate-hugos-5th-birthday/featured.png
index 4c31412fd..4c31412fd 100644
--- a/content/en/news/lets-celebrate-hugos-5th-birthday/featured.png
+++ b/docs/content/en/news/lets-celebrate-hugos-5th-birthday/featured.png
Binary files differ
diff --git a/content/en/news/lets-celebrate-hugos-5th-birthday/graph-stars.png b/docs/content/en/news/lets-celebrate-hugos-5th-birthday/graph-stars.png
index 00848fcf0..00848fcf0 100644
--- a/content/en/news/lets-celebrate-hugos-5th-birthday/graph-stars.png
+++ b/docs/content/en/news/lets-celebrate-hugos-5th-birthday/graph-stars.png
Binary files differ
diff --git a/content/en/news/lets-celebrate-hugos-5th-birthday/graph-themes.png b/docs/content/en/news/lets-celebrate-hugos-5th-birthday/graph-themes.png
index 0d4dfd599..0d4dfd599 100644
--- a/content/en/news/lets-celebrate-hugos-5th-birthday/graph-themes.png
+++ b/docs/content/en/news/lets-celebrate-hugos-5th-birthday/graph-themes.png
Binary files differ
diff --git a/content/en/news/lets-celebrate-hugos-5th-birthday/index.md b/docs/content/en/news/lets-celebrate-hugos-5th-birthday/index.md
index 9912027b5..9912027b5 100644
--- a/content/en/news/lets-celebrate-hugos-5th-birthday/index.md
+++ b/docs/content/en/news/lets-celebrate-hugos-5th-birthday/index.md
diff --git a/content/en/news/lets-celebrate-hugos-5th-birthday/sunset-get.png b/docs/content/en/news/lets-celebrate-hugos-5th-birthday/sunset-get.png
index db3373c0b..db3373c0b 100644
--- a/content/en/news/lets-celebrate-hugos-5th-birthday/sunset-get.png
+++ b/docs/content/en/news/lets-celebrate-hugos-5th-birthday/sunset-get.png
Binary files differ
diff --git a/content/en/readfiles/README.md b/docs/content/en/readfiles/README.md
index 4b10f0e47..4b10f0e47 100644
--- a/content/en/readfiles/README.md
+++ b/docs/content/en/readfiles/README.md
diff --git a/content/en/readfiles/bfconfig.md b/docs/content/en/readfiles/bfconfig.md
index 216fc64f6..216fc64f6 100644
--- a/content/en/readfiles/bfconfig.md
+++ b/docs/content/en/readfiles/bfconfig.md
diff --git a/content/en/readfiles/dateformatting.md b/docs/content/en/readfiles/dateformatting.md
index 42138dd8a..42138dd8a 100644
--- a/content/en/readfiles/dateformatting.md
+++ b/docs/content/en/readfiles/dateformatting.md
diff --git a/content/en/readfiles/index.md b/docs/content/en/readfiles/index.md
index 3d65eaa0f..3d65eaa0f 100644
--- a/content/en/readfiles/index.md
+++ b/docs/content/en/readfiles/index.md
diff --git a/content/en/readfiles/pages-vs-site-pages.md b/docs/content/en/readfiles/pages-vs-site-pages.md
index 77f829819..77f829819 100644
--- a/content/en/readfiles/pages-vs-site-pages.md
+++ b/docs/content/en/readfiles/pages-vs-site-pages.md
diff --git a/content/en/readfiles/sectionvars.md b/docs/content/en/readfiles/sectionvars.md
index 698955c02..698955c02 100644
--- a/content/en/readfiles/sectionvars.md
+++ b/docs/content/en/readfiles/sectionvars.md
diff --git a/content/en/readfiles/testing.txt b/docs/content/en/readfiles/testing.txt
index 6428710e3..6428710e3 100644
--- a/content/en/readfiles/testing.txt
+++ b/docs/content/en/readfiles/testing.txt
diff --git a/content/en/showcase/1password-support/bio.md b/docs/content/en/showcase/1password-support/bio.md
index 9187908d9..9187908d9 100644
--- a/content/en/showcase/1password-support/bio.md
+++ b/docs/content/en/showcase/1password-support/bio.md
diff --git a/content/en/showcase/1password-support/featured.png b/docs/content/en/showcase/1password-support/featured.png
index 8e46495e6..8e46495e6 100644
--- a/content/en/showcase/1password-support/featured.png
+++ b/docs/content/en/showcase/1password-support/featured.png
Binary files differ
diff --git a/content/en/showcase/1password-support/index.md b/docs/content/en/showcase/1password-support/index.md
index 55038bc7e..55038bc7e 100644
--- a/content/en/showcase/1password-support/index.md
+++ b/docs/content/en/showcase/1password-support/index.md
diff --git a/content/en/showcase/arolla-cocoon/bio.md b/docs/content/en/showcase/arolla-cocoon/bio.md
index f01228828..f01228828 100644
--- a/content/en/showcase/arolla-cocoon/bio.md
+++ b/docs/content/en/showcase/arolla-cocoon/bio.md
diff --git a/content/en/showcase/arolla-cocoon/featured-template.png b/docs/content/en/showcase/arolla-cocoon/featured-template.png
index d95bc5c83..d95bc5c83 100644
--- a/content/en/showcase/arolla-cocoon/featured-template.png
+++ b/docs/content/en/showcase/arolla-cocoon/featured-template.png
Binary files differ
diff --git a/content/en/showcase/arolla-cocoon/index.md b/docs/content/en/showcase/arolla-cocoon/index.md
index 730b9fda2..730b9fda2 100644
--- a/content/en/showcase/arolla-cocoon/index.md
+++ b/docs/content/en/showcase/arolla-cocoon/index.md
diff --git a/content/en/showcase/fireship/bio.md b/docs/content/en/showcase/fireship/bio.md
index faf739bfa..faf739bfa 100644
--- a/content/en/showcase/fireship/bio.md
+++ b/docs/content/en/showcase/fireship/bio.md
diff --git a/content/en/showcase/fireship/featured.png b/docs/content/en/showcase/fireship/featured.png
index 33d1a47c5..33d1a47c5 100644
--- a/content/en/showcase/fireship/featured.png
+++ b/docs/content/en/showcase/fireship/featured.png
Binary files differ
diff --git a/content/en/showcase/fireship/index.md b/docs/content/en/showcase/fireship/index.md
index bab68a547..bab68a547 100644
--- a/content/en/showcase/fireship/index.md
+++ b/docs/content/en/showcase/fireship/index.md
diff --git a/content/en/showcase/flesland-flis/bio.md b/docs/content/en/showcase/flesland-flis/bio.md
index c1a4e8187..c1a4e8187 100644
--- a/content/en/showcase/flesland-flis/bio.md
+++ b/docs/content/en/showcase/flesland-flis/bio.md
diff --git a/content/en/showcase/flesland-flis/featured.png b/docs/content/en/showcase/flesland-flis/featured.png
index a6dae684e..a6dae684e 100644
--- a/content/en/showcase/flesland-flis/featured.png
+++ b/docs/content/en/showcase/flesland-flis/featured.png
Binary files differ
diff --git a/content/en/showcase/flesland-flis/index.md b/docs/content/en/showcase/flesland-flis/index.md
index 5e18f040c..5e18f040c 100644
--- a/content/en/showcase/flesland-flis/index.md
+++ b/docs/content/en/showcase/flesland-flis/index.md
diff --git a/content/en/showcase/forestry/bio.md b/docs/content/en/showcase/forestry/bio.md
index 767365cc0..767365cc0 100644
--- a/content/en/showcase/forestry/bio.md
+++ b/docs/content/en/showcase/forestry/bio.md
diff --git a/content/en/showcase/forestry/featured.png b/docs/content/en/showcase/forestry/featured.png
index 1ee315e78..1ee315e78 100644
--- a/content/en/showcase/forestry/featured.png
+++ b/docs/content/en/showcase/forestry/featured.png
Binary files differ
diff --git a/content/en/showcase/forestry/index.md b/docs/content/en/showcase/forestry/index.md
index 1a9c0faaa..1a9c0faaa 100644
--- a/content/en/showcase/forestry/index.md
+++ b/docs/content/en/showcase/forestry/index.md
diff --git a/content/en/showcase/hartwell-insurance/bio.md b/docs/content/en/showcase/hartwell-insurance/bio.md
index 8bfdad49d..8bfdad49d 100644
--- a/content/en/showcase/hartwell-insurance/bio.md
+++ b/docs/content/en/showcase/hartwell-insurance/bio.md
diff --git a/content/en/showcase/hartwell-insurance/featured.png b/docs/content/en/showcase/hartwell-insurance/featured.png
index ced251f98..ced251f98 100644
--- a/content/en/showcase/hartwell-insurance/featured.png
+++ b/docs/content/en/showcase/hartwell-insurance/featured.png
Binary files differ
diff --git a/content/en/showcase/hartwell-insurance/hartwell-columns.png b/docs/content/en/showcase/hartwell-insurance/hartwell-columns.png
index eb669b5a3..eb669b5a3 100644
--- a/content/en/showcase/hartwell-insurance/hartwell-columns.png
+++ b/docs/content/en/showcase/hartwell-insurance/hartwell-columns.png
Binary files differ
diff --git a/content/en/showcase/hartwell-insurance/hartwell-lighthouse.png b/docs/content/en/showcase/hartwell-insurance/hartwell-lighthouse.png
index 672a8c1c8..672a8c1c8 100644
--- a/content/en/showcase/hartwell-insurance/hartwell-lighthouse.png
+++ b/docs/content/en/showcase/hartwell-insurance/hartwell-lighthouse.png
Binary files differ
diff --git a/content/en/showcase/hartwell-insurance/hartwell-webpagetest.png b/docs/content/en/showcase/hartwell-insurance/hartwell-webpagetest.png
index 8dc035f3e..8dc035f3e 100644
--- a/content/en/showcase/hartwell-insurance/hartwell-webpagetest.png
+++ b/docs/content/en/showcase/hartwell-insurance/hartwell-webpagetest.png
Binary files differ
diff --git a/content/en/showcase/hartwell-insurance/index.md b/docs/content/en/showcase/hartwell-insurance/index.md
index 3e9c224c8..3e9c224c8 100644
--- a/content/en/showcase/hartwell-insurance/index.md
+++ b/docs/content/en/showcase/hartwell-insurance/index.md
diff --git a/content/en/showcase/letsencrypt/bio.md b/docs/content/en/showcase/letsencrypt/bio.md
index 24d7b96ee..24d7b96ee 100644
--- a/content/en/showcase/letsencrypt/bio.md
+++ b/docs/content/en/showcase/letsencrypt/bio.md
diff --git a/content/en/showcase/letsencrypt/featured.png b/docs/content/en/showcase/letsencrypt/featured.png
index 9535d91bd..9535d91bd 100644
--- a/content/en/showcase/letsencrypt/featured.png
+++ b/docs/content/en/showcase/letsencrypt/featured.png
Binary files differ
diff --git a/content/en/showcase/letsencrypt/index.md b/docs/content/en/showcase/letsencrypt/index.md
index 8487a3c77..8487a3c77 100644
--- a/content/en/showcase/letsencrypt/index.md
+++ b/docs/content/en/showcase/letsencrypt/index.md
diff --git a/content/en/showcase/linode/bio.md b/docs/content/en/showcase/linode/bio.md
index 42fa92229..42fa92229 100644
--- a/content/en/showcase/linode/bio.md
+++ b/docs/content/en/showcase/linode/bio.md
diff --git a/content/en/showcase/linode/featured.png b/docs/content/en/showcase/linode/featured.png
index 5d4c3e36b..5d4c3e36b 100644
--- a/content/en/showcase/linode/featured.png
+++ b/docs/content/en/showcase/linode/featured.png
Binary files differ
diff --git a/content/en/showcase/linode/index.md b/docs/content/en/showcase/linode/index.md
index b0590a8cf..b0590a8cf 100644
--- a/content/en/showcase/linode/index.md
+++ b/docs/content/en/showcase/linode/index.md
diff --git a/content/en/showcase/over/bio.md b/docs/content/en/showcase/over/bio.md
index 415668f9e..415668f9e 100644
--- a/content/en/showcase/over/bio.md
+++ b/docs/content/en/showcase/over/bio.md
diff --git a/content/en/showcase/over/featured-over.png b/docs/content/en/showcase/over/featured-over.png
index 726d98731..726d98731 100644
--- a/content/en/showcase/over/featured-over.png
+++ b/docs/content/en/showcase/over/featured-over.png
Binary files differ
diff --git a/content/en/showcase/over/index.md b/docs/content/en/showcase/over/index.md
index 9640198db..9640198db 100644
--- a/content/en/showcase/over/index.md
+++ b/docs/content/en/showcase/over/index.md
diff --git a/content/en/showcase/pace-revenue-management/bio.md b/docs/content/en/showcase/pace-revenue-management/bio.md
index 7c7cc9c1c..7c7cc9c1c 100644
--- a/content/en/showcase/pace-revenue-management/bio.md
+++ b/docs/content/en/showcase/pace-revenue-management/bio.md
diff --git a/content/en/showcase/pace-revenue-management/featured.png b/docs/content/en/showcase/pace-revenue-management/featured.png
index fa0948e5f..fa0948e5f 100644
--- a/content/en/showcase/pace-revenue-management/featured.png
+++ b/docs/content/en/showcase/pace-revenue-management/featured.png
Binary files differ
diff --git a/content/en/showcase/pace-revenue-management/index.md b/docs/content/en/showcase/pace-revenue-management/index.md
index cb520acb2..cb520acb2 100644
--- a/content/en/showcase/pace-revenue-management/index.md
+++ b/docs/content/en/showcase/pace-revenue-management/index.md
diff --git a/content/en/showcase/pharmaseal/bio.md b/docs/content/en/showcase/pharmaseal/bio.md
index 933227e13..933227e13 100644
--- a/content/en/showcase/pharmaseal/bio.md
+++ b/docs/content/en/showcase/pharmaseal/bio.md
diff --git a/content/en/showcase/pharmaseal/featured-pharmaseal.png b/docs/content/en/showcase/pharmaseal/featured-pharmaseal.png
index bd92b375b..bd92b375b 100644
--- a/content/en/showcase/pharmaseal/featured-pharmaseal.png
+++ b/docs/content/en/showcase/pharmaseal/featured-pharmaseal.png
Binary files differ
diff --git a/content/en/showcase/pharmaseal/index.md b/docs/content/en/showcase/pharmaseal/index.md
index 64e9960a3..64e9960a3 100644
--- a/content/en/showcase/pharmaseal/index.md
+++ b/docs/content/en/showcase/pharmaseal/index.md
diff --git a/content/en/showcase/quiply-employee-communications-app/bio.md b/docs/content/en/showcase/quiply-employee-communications-app/bio.md
index f72a62554..f72a62554 100644
--- a/content/en/showcase/quiply-employee-communications-app/bio.md
+++ b/docs/content/en/showcase/quiply-employee-communications-app/bio.md
diff --git a/content/en/showcase/quiply-employee-communications-app/featured.png b/docs/content/en/showcase/quiply-employee-communications-app/featured.png
index a4e9f046e..a4e9f046e 100644
--- a/content/en/showcase/quiply-employee-communications-app/featured.png
+++ b/docs/content/en/showcase/quiply-employee-communications-app/featured.png
Binary files differ
diff --git a/content/en/showcase/quiply-employee-communications-app/index.md b/docs/content/en/showcase/quiply-employee-communications-app/index.md
index 9f80850ee..9f80850ee 100644
--- a/content/en/showcase/quiply-employee-communications-app/index.md
+++ b/docs/content/en/showcase/quiply-employee-communications-app/index.md
diff --git a/content/en/showcase/small-multiples/bio.md b/docs/content/en/showcase/small-multiples/bio.md
index 3e0c1f14a..3e0c1f14a 100644
--- a/content/en/showcase/small-multiples/bio.md
+++ b/docs/content/en/showcase/small-multiples/bio.md
diff --git a/content/en/showcase/small-multiples/featured-small-multiples.png b/docs/content/en/showcase/small-multiples/featured-small-multiples.png
index a278f464d..a278f464d 100644
--- a/content/en/showcase/small-multiples/featured-small-multiples.png
+++ b/docs/content/en/showcase/small-multiples/featured-small-multiples.png
Binary files differ
diff --git a/content/en/showcase/small-multiples/index.md b/docs/content/en/showcase/small-multiples/index.md
index e2b80ea9a..e2b80ea9a 100644
--- a/content/en/showcase/small-multiples/index.md
+++ b/docs/content/en/showcase/small-multiples/index.md
diff --git a/content/en/showcase/stackimpact/bio.md b/docs/content/en/showcase/stackimpact/bio.md
index e6206dd03..e6206dd03 100644
--- a/content/en/showcase/stackimpact/bio.md
+++ b/docs/content/en/showcase/stackimpact/bio.md
diff --git a/content/en/showcase/stackimpact/featured.png b/docs/content/en/showcase/stackimpact/featured.png
index 49a3bc500..49a3bc500 100644
--- a/content/en/showcase/stackimpact/featured.png
+++ b/docs/content/en/showcase/stackimpact/featured.png
Binary files differ
diff --git a/content/en/showcase/stackimpact/index.md b/docs/content/en/showcase/stackimpact/index.md
index bc3d3fd29..bc3d3fd29 100644
--- a/content/en/showcase/stackimpact/index.md
+++ b/docs/content/en/showcase/stackimpact/index.md
diff --git a/content/en/showcase/template/bio.md b/docs/content/en/showcase/template/bio.md
index 597163340..597163340 100644
--- a/content/en/showcase/template/bio.md
+++ b/docs/content/en/showcase/template/bio.md
diff --git a/content/en/showcase/template/featured-template.png b/docs/content/en/showcase/template/featured-template.png
index 4f390132e..4f390132e 100644
--- a/content/en/showcase/template/featured-template.png
+++ b/docs/content/en/showcase/template/featured-template.png
Binary files differ
diff --git a/content/en/showcase/template/index.md b/docs/content/en/showcase/template/index.md
index a0d83955e..a0d83955e 100644
--- a/content/en/showcase/template/index.md
+++ b/docs/content/en/showcase/template/index.md
diff --git a/content/en/showcase/tomango/bio.md b/docs/content/en/showcase/tomango/bio.md
index 052bd93cd..052bd93cd 100644
--- a/content/en/showcase/tomango/bio.md
+++ b/docs/content/en/showcase/tomango/bio.md
diff --git a/content/en/showcase/tomango/featured.png b/docs/content/en/showcase/tomango/featured.png
index e495c16b7..e495c16b7 100644
--- a/content/en/showcase/tomango/featured.png
+++ b/docs/content/en/showcase/tomango/featured.png
Binary files differ
diff --git a/content/en/showcase/tomango/index.md b/docs/content/en/showcase/tomango/index.md
index 5252c02a8..5252c02a8 100644
--- a/content/en/showcase/tomango/index.md
+++ b/docs/content/en/showcase/tomango/index.md
diff --git a/content/en/templates/404.md b/docs/content/en/templates/404.md
index bb1d5e424..bb1d5e424 100644
--- a/content/en/templates/404.md
+++ b/docs/content/en/templates/404.md
diff --git a/content/en/templates/_index.md b/docs/content/en/templates/_index.md
index 18ae40eac..18ae40eac 100644
--- a/content/en/templates/_index.md
+++ b/docs/content/en/templates/_index.md
diff --git a/content/en/templates/alternatives.md b/docs/content/en/templates/alternatives.md
index 91de38488..91de38488 100644
--- a/content/en/templates/alternatives.md
+++ b/docs/content/en/templates/alternatives.md
diff --git a/content/en/templates/base.md b/docs/content/en/templates/base.md
index 5643f8d4b..5643f8d4b 100644
--- a/content/en/templates/base.md
+++ b/docs/content/en/templates/base.md
diff --git a/content/en/templates/data-templates.md b/docs/content/en/templates/data-templates.md
index e70840b7d..e70840b7d 100644
--- a/content/en/templates/data-templates.md
+++ b/docs/content/en/templates/data-templates.md
diff --git a/content/en/templates/files.md b/docs/content/en/templates/files.md
index 6b898fe73..6b898fe73 100644
--- a/content/en/templates/files.md
+++ b/docs/content/en/templates/files.md
diff --git a/content/en/templates/homepage.md b/docs/content/en/templates/homepage.md
index 48130c39b..48130c39b 100644
--- a/content/en/templates/homepage.md
+++ b/docs/content/en/templates/homepage.md
diff --git a/content/en/templates/internal.md b/docs/content/en/templates/internal.md
index fdec63c57..fdec63c57 100644
--- a/content/en/templates/internal.md
+++ b/docs/content/en/templates/internal.md
diff --git a/content/en/templates/introduction.md b/docs/content/en/templates/introduction.md
index 02a580e79..02a580e79 100644
--- a/content/en/templates/introduction.md
+++ b/docs/content/en/templates/introduction.md
diff --git a/content/en/templates/lists.md b/docs/content/en/templates/lists.md
index 34c9e6d5a..34c9e6d5a 100644
--- a/content/en/templates/lists.md
+++ b/docs/content/en/templates/lists.md
diff --git a/content/en/templates/lookup-order.md b/docs/content/en/templates/lookup-order.md
index 61ce35ef2..61ce35ef2 100644
--- a/content/en/templates/lookup-order.md
+++ b/docs/content/en/templates/lookup-order.md
diff --git a/content/en/templates/menu-templates.md b/docs/content/en/templates/menu-templates.md
index 5efb080d8..5efb080d8 100644
--- a/content/en/templates/menu-templates.md
+++ b/docs/content/en/templates/menu-templates.md
diff --git a/content/en/templates/ordering-and-grouping.md b/docs/content/en/templates/ordering-and-grouping.md
index 078adab2c..078adab2c 100644
--- a/content/en/templates/ordering-and-grouping.md
+++ b/docs/content/en/templates/ordering-and-grouping.md
diff --git a/content/en/templates/output-formats.md b/docs/content/en/templates/output-formats.md
index 1ab45e4c7..1ab45e4c7 100644
--- a/content/en/templates/output-formats.md
+++ b/docs/content/en/templates/output-formats.md
diff --git a/content/en/templates/pagination.md b/docs/content/en/templates/pagination.md
index bd4176761..bd4176761 100644
--- a/content/en/templates/pagination.md
+++ b/docs/content/en/templates/pagination.md
diff --git a/content/en/templates/partials.md b/docs/content/en/templates/partials.md
index 725e946fb..725e946fb 100644
--- a/content/en/templates/partials.md
+++ b/docs/content/en/templates/partials.md
diff --git a/content/en/templates/robots.md b/docs/content/en/templates/robots.md
index cfb077e30..cfb077e30 100644
--- a/content/en/templates/robots.md
+++ b/docs/content/en/templates/robots.md
diff --git a/content/en/templates/rss.md b/docs/content/en/templates/rss.md
index 715bc2a48..715bc2a48 100644
--- a/content/en/templates/rss.md
+++ b/docs/content/en/templates/rss.md
diff --git a/content/en/templates/section-templates.md b/docs/content/en/templates/section-templates.md
index 577529e3f..577529e3f 100644
--- a/content/en/templates/section-templates.md
+++ b/docs/content/en/templates/section-templates.md
diff --git a/content/en/templates/shortcode-templates.md b/docs/content/en/templates/shortcode-templates.md
index cfe0d316b..cfe0d316b 100644
--- a/content/en/templates/shortcode-templates.md
+++ b/docs/content/en/templates/shortcode-templates.md
diff --git a/content/en/templates/single-page-templates.md b/docs/content/en/templates/single-page-templates.md
index e8b72e598..e8b72e598 100644
--- a/content/en/templates/single-page-templates.md
+++ b/docs/content/en/templates/single-page-templates.md
diff --git a/content/en/templates/sitemap-template.md b/docs/content/en/templates/sitemap-template.md
index 7cbc7cefb..7cbc7cefb 100644
--- a/content/en/templates/sitemap-template.md
+++ b/docs/content/en/templates/sitemap-template.md
diff --git a/content/en/templates/taxonomy-templates.md b/docs/content/en/templates/taxonomy-templates.md
index b82a5175c..b82a5175c 100644
--- a/content/en/templates/taxonomy-templates.md
+++ b/docs/content/en/templates/taxonomy-templates.md
diff --git a/content/en/templates/template-debugging.md b/docs/content/en/templates/template-debugging.md
index bba84b9fe..bba84b9fe 100644
--- a/content/en/templates/template-debugging.md
+++ b/docs/content/en/templates/template-debugging.md
diff --git a/content/en/templates/views.md b/docs/content/en/templates/views.md
index eb158eed0..eb158eed0 100644
--- a/content/en/templates/views.md
+++ b/docs/content/en/templates/views.md
diff --git a/content/en/themes/_index.md b/docs/content/en/themes/_index.md
index 6a135dd39..6a135dd39 100644
--- a/content/en/themes/_index.md
+++ b/docs/content/en/themes/_index.md
diff --git a/content/en/themes/creating.md b/docs/content/en/themes/creating.md
index d96942a58..d96942a58 100644
--- a/content/en/themes/creating.md
+++ b/docs/content/en/themes/creating.md
diff --git a/content/en/themes/installing-and-using-themes.md b/docs/content/en/themes/installing-and-using-themes.md
index 93d814231..93d814231 100644
--- a/content/en/themes/installing-and-using-themes.md
+++ b/docs/content/en/themes/installing-and-using-themes.md
diff --git a/content/en/themes/theme-components.md b/docs/content/en/themes/theme-components.md
index 98072c533..98072c533 100644
--- a/content/en/themes/theme-components.md
+++ b/docs/content/en/themes/theme-components.md
diff --git a/content/en/tools/_index.md b/docs/content/en/tools/_index.md
index 5781f5c0c..5781f5c0c 100644
--- a/content/en/tools/_index.md
+++ b/docs/content/en/tools/_index.md
diff --git a/content/en/tools/editors.md b/docs/content/en/tools/editors.md
index f0d82d65d..f0d82d65d 100644
--- a/content/en/tools/editors.md
+++ b/docs/content/en/tools/editors.md
diff --git a/content/en/tools/frontends.md b/docs/content/en/tools/frontends.md
index 0eaf95ba8..0eaf95ba8 100644
--- a/content/en/tools/frontends.md
+++ b/docs/content/en/tools/frontends.md
diff --git a/docs/content/en/tools/migrations.md b/docs/content/en/tools/migrations.md
new file mode 100644
index 000000000..9a48d1df0
--- /dev/null
+++ b/docs/content/en/tools/migrations.md
@@ -0,0 +1,85 @@
+---
+title: Migrate to Hugo
+linktitle: Migrations
+description: A list of community-developed tools for migrating from your existing static site generator or content management system to Hugo.
+date: 2017-02-01
+publishdate: 2017-02-01
+lastmod: 2017-02-01
+keywords: [migrations,jekyll,wordpress,drupal,ghost,contentful]
+menu:
+ docs:
+ parent: "tools"
+ weight: 10
+weight: 10
+sections_weight: 10
+draft: false
+aliases: [/developer-tools/migrations/,/developer-tools/migrated/]
+toc: true
+---
+
+This section highlights some projects around Hugo that are independently developed. These tools try to extend the functionality of our static site generator or help you to get started.
+
+{{% note %}}
+Do you know or maintain a similar project around Hugo? Feel free to open a [pull request](https://github.com/gohugoio/hugo/pulls) on GitHub if you think it should be added.
+{{% /note %}}
+
+Take a look at this list of migration tools if you currently use other blogging tools like Jekyll or WordPress but intend to switch to Hugo instead. They'll take care to export your content into Hugo-friendly formats.
+
+## Jekyll
+
+Alternatively, you can use the new [Jekyll import command](/commands/hugo_import_jekyll/).
+
+- [JekyllToHugo](https://github.com/SenjinDarashiva/JekyllToHugo) - A Small script for converting Jekyll blog posts to a Hugo site.
+- [ConvertToHugo](https://github.com/coderzh/ConvertToHugo) - Convert your blog from Jekyll to Hugo.
+
+## Ghost
+
+- [ghostToHugo](https://github.com/jbarone/ghostToHugo) - Convert Ghost blog posts and export them to Hugo.
+
+## Octopress
+
+- [octohug](https://github.com/codebrane/octohug) - Octopress to Hugo migrator.
+
+## DokuWiki
+
+- [dokuwiki-to-hugo](https://github.com/wgroeneveld/dokuwiki-to-hugo) - Migrates your dokuwiki source pages from [DokuWiki syntax](https://www.dokuwiki.org/wiki:syntax) to Hugo Markdown syntax. Includes extra's like the TODO plugin. Written with extensibility in mind using python 3. Also generates a TOML header for each page. Designed to copypaste the wiki directory into your /content directory.
+
+## WordPress
+
+- [wordpress-to-hugo-exporter](https://github.com/SchumacherFM/wordpress-to-hugo-exporter) - A one-click WordPress plugin that converts all posts, pages, taxonomies, metadata, and settings to Markdown and YAML which can be dropped into Hugo. (Note: If you have trouble using this plugin, you can [export your site for Jekyll](https://wordpress.org/plugins/jekyll-exporter/) and use Hugo's built in Jekyll converter listed above.)
+- [exitwp-for-hugo](https://github.com/wooni005/exitwp-for-hugo) - A python script which works with the xml export from Wordpress and converts Wordpress pages and posts to Markdown and YAML for hugo.
+- [blog2md](https://github.com/palaniraja/blog2md) - Works with [exported xml](https://en.support.wordpress.com/export/) file of your free YOUR-TLD.wordpress.com website. It also saves approved comments to `YOUR-POST-NAME-comments.md` file along with posts.
+
+## Medium
+
+- [medium2md](https://github.com/gautamdhameja/medium-2-md) - A simple Medium to Hugo exporter able to import stories in one command, including Front Matter.
+
+## Tumblr
+
+- [tumblr-importr](https://github.com/carlmjohnson/tumblr-importr) - An importer that uses the Tumblr API to create a Hugo static site.
+- [tumblr2hugomarkdown](https://github.com/Wysie/tumblr2hugomarkdown) - Export all your Tumblr content to Hugo Markdown files with preserved original formatting.
+- [Tumblr to Hugo](https://github.com/jipiboily/tumblr-to-hugo) - A migration tool that converts each of your Tumblr posts to a content file with a proper title and path. Furthermore, "Tumblr to Hugo" creates a CSV file with the original URL and the new path on Hugo, to help you setup the redirections.
+
+## Drupal
+
+- [drupal2hugo](https://github.com/danapsimer/drupal2hugo) - Convert a Drupal site to Hugo.
+
+## Joomla
+
+- [hugojoomla](https://github.com/davetcc/hugojoomla) - This utility written in Java takes a Joomla database and converts all the content into Markdown files. It changes any URLs that are in Joomla's internal format and converts them to a suitable form.
+
+## Blogger
+
+- [blogimport](https://github.com/natefinch/blogimport) - A tool to import from Blogger posts to Hugo.
+- [blogger-to-hugo](https://bitbucket.org/petraszd/blogger-to-hugo) - Another tool to import Blogger posts to Hugo. It also downloads embedded images so they will be stored locally.
+- [blog2md](https://github.com/palaniraja/blog2md) - Works with [exported xml](https://support.google.com/blogger/answer/41387?hl=en) file of your YOUR-TLD.blogspot.com website. It also saves comments to `YOUR-POST-NAME-comments.md` file along with posts.
+- [BloggerToHugo](https://github.com/huanlin/blogger-to-hugo) - Yet another tool to import Blogger posts to Hugo. For Windows platform only, and .NET Framework 4.5 is required. See README.md before using this tool.
+
+## Contentful
+
+- [contentful2hugo](https://github.com/ArnoNuyts/contentful2hugo) - A tool to create content-files for Hugo from content on [Contentful](https://www.contentful.com/).
+
+
+## BlogML
+
+- [BlogML2Hugo](https://github.com/jijiechen/BlogML2Hugo) - A tool that helps you convert BlogML xml file to Hugo markdown files. Users need to take care of links to attachments and images by themselves. This helps the blogs that export BlogML files (e.g. BlogEngine.NET) tramsform to hugo sites easily.
diff --git a/content/en/tools/other.md b/docs/content/en/tools/other.md
index 0502e1cdf..0502e1cdf 100644
--- a/content/en/tools/other.md
+++ b/docs/content/en/tools/other.md
diff --git a/content/en/tools/search.md b/docs/content/en/tools/search.md
index 2a6c0296a..2a6c0296a 100644
--- a/content/en/tools/search.md
+++ b/docs/content/en/tools/search.md
diff --git a/content/en/tools/starter-kits.md b/docs/content/en/tools/starter-kits.md
index e30de33d9..e30de33d9 100644
--- a/content/en/tools/starter-kits.md
+++ b/docs/content/en/tools/starter-kits.md
diff --git a/content/en/troubleshooting/_index.md b/docs/content/en/troubleshooting/_index.md
index 3b0e93725..3b0e93725 100644
--- a/content/en/troubleshooting/_index.md
+++ b/docs/content/en/troubleshooting/_index.md
diff --git a/content/en/troubleshooting/build-performance.md b/docs/content/en/troubleshooting/build-performance.md
index e0700f381..e0700f381 100644
--- a/content/en/troubleshooting/build-performance.md
+++ b/docs/content/en/troubleshooting/build-performance.md
diff --git a/content/en/troubleshooting/faq.md b/docs/content/en/troubleshooting/faq.md
index c68473a84..c68473a84 100644
--- a/content/en/troubleshooting/faq.md
+++ b/docs/content/en/troubleshooting/faq.md
diff --git a/content/en/variables/_index.md b/docs/content/en/variables/_index.md
index 382ee25d4..382ee25d4 100644
--- a/content/en/variables/_index.md
+++ b/docs/content/en/variables/_index.md
diff --git a/content/en/variables/files.md b/docs/content/en/variables/files.md
index 7eaaa6440..7eaaa6440 100644
--- a/content/en/variables/files.md
+++ b/docs/content/en/variables/files.md
diff --git a/content/en/variables/git.md b/docs/content/en/variables/git.md
index 59ee9ac88..59ee9ac88 100644
--- a/content/en/variables/git.md
+++ b/docs/content/en/variables/git.md
diff --git a/content/en/variables/hugo.md b/docs/content/en/variables/hugo.md
index a563831ff..a563831ff 100644
--- a/content/en/variables/hugo.md
+++ b/docs/content/en/variables/hugo.md
diff --git a/content/en/variables/menus.md b/docs/content/en/variables/menus.md
index 69d46ca2b..69d46ca2b 100644
--- a/content/en/variables/menus.md
+++ b/docs/content/en/variables/menus.md
diff --git a/content/en/variables/page.md b/docs/content/en/variables/page.md
index 2e1beb6cb..2e1beb6cb 100644
--- a/content/en/variables/page.md
+++ b/docs/content/en/variables/page.md
diff --git a/content/en/variables/shortcodes.md b/docs/content/en/variables/shortcodes.md
index 7462deec7..7462deec7 100644
--- a/content/en/variables/shortcodes.md
+++ b/docs/content/en/variables/shortcodes.md
diff --git a/content/en/variables/site.md b/docs/content/en/variables/site.md
index 94e67fa1a..94e67fa1a 100644
--- a/content/en/variables/site.md
+++ b/docs/content/en/variables/site.md
diff --git a/content/en/variables/sitemap.md b/docs/content/en/variables/sitemap.md
index dd926f2b3..dd926f2b3 100644
--- a/content/en/variables/sitemap.md
+++ b/docs/content/en/variables/sitemap.md
diff --git a/content/en/variables/taxonomy.md b/docs/content/en/variables/taxonomy.md
index 5bcdffee5..5bcdffee5 100644
--- a/content/en/variables/taxonomy.md
+++ b/docs/content/en/variables/taxonomy.md
diff --git a/content/zh/_index.md b/docs/content/zh/_index.md
index 78f9ef15f..78f9ef15f 100644
--- a/content/zh/_index.md
+++ b/docs/content/zh/_index.md
diff --git a/content/zh/about/_index.md b/docs/content/zh/about/_index.md
index bf19807d9..bf19807d9 100644
--- a/content/zh/about/_index.md
+++ b/docs/content/zh/about/_index.md
diff --git a/content/zh/content-management/_index.md b/docs/content/zh/content-management/_index.md
index 8c088dc57..8c088dc57 100644
--- a/content/zh/content-management/_index.md
+++ b/docs/content/zh/content-management/_index.md
diff --git a/content/zh/documentation.md b/docs/content/zh/documentation.md
index 1639bbcd2..1639bbcd2 100644
--- a/content/zh/documentation.md
+++ b/docs/content/zh/documentation.md
diff --git a/content/zh/news/_index.md b/docs/content/zh/news/_index.md
index 286d32e19..286d32e19 100644
--- a/content/zh/news/_index.md
+++ b/docs/content/zh/news/_index.md
diff --git a/content/zh/templates/_index.md b/docs/content/zh/templates/_index.md
index 3cd8df436..3cd8df436 100644
--- a/content/zh/templates/_index.md
+++ b/docs/content/zh/templates/_index.md
diff --git a/content/zh/templates/base.md b/docs/content/zh/templates/base.md
index 689a54408..689a54408 100644
--- a/content/zh/templates/base.md
+++ b/docs/content/zh/templates/base.md
diff --git a/data/articles.toml b/docs/data/articles.toml
index 109810803..109810803 100644
--- a/data/articles.toml
+++ b/docs/data/articles.toml
diff --git a/data/docs.json b/docs/data/docs.json
index dbed37f77..dbed37f77 100644
--- a/data/docs.json
+++ b/docs/data/docs.json
diff --git a/data/homepagetweets.toml b/docs/data/homepagetweets.toml
index 01f8c2fc2..01f8c2fc2 100644
--- a/data/homepagetweets.toml
+++ b/docs/data/homepagetweets.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/index.rss.xml b/docs/layouts/index.rss.xml
index 1d3498a1e..1d3498a1e 100644
--- a/layouts/index.rss.xml
+++ b/docs/layouts/index.rss.xml
diff --git a/layouts/maintenance/list.html b/docs/layouts/maintenance/list.html
index 50059ad9e..50059ad9e 100644
--- a/layouts/maintenance/list.html
+++ b/docs/layouts/maintenance/list.html
diff --git a/layouts/partials/maintenance-pages-table.html b/docs/layouts/partials/maintenance-pages-table.html
index 8538e2104..8538e2104 100644
--- a/layouts/partials/maintenance-pages-table.html
+++ b/docs/layouts/partials/maintenance-pages-table.html
diff --git a/layouts/shortcodes/asciicast.html b/docs/layouts/shortcodes/asciicast.html
index ee23adc2d..ee23adc2d 100644
--- a/layouts/shortcodes/asciicast.html
+++ b/docs/layouts/shortcodes/asciicast.html
diff --git a/layouts/shortcodes/chroma-lexers.html b/docs/layouts/shortcodes/chroma-lexers.html
index 0df2b868f..0df2b868f 100644
--- a/layouts/shortcodes/chroma-lexers.html
+++ b/docs/layouts/shortcodes/chroma-lexers.html
diff --git a/layouts/shortcodes/code.html b/docs/layouts/shortcodes/code.html
index eafc02e6b..eafc02e6b 100644
--- a/layouts/shortcodes/code.html
+++ b/docs/layouts/shortcodes/code.html
diff --git a/layouts/shortcodes/datatable-filtered.html b/docs/layouts/shortcodes/datatable-filtered.html
index 576ddab6f..576ddab6f 100644
--- a/layouts/shortcodes/datatable-filtered.html
+++ b/docs/layouts/shortcodes/datatable-filtered.html
diff --git a/layouts/shortcodes/datatable.html b/docs/layouts/shortcodes/datatable.html
index 4e2814f5a..4e2814f5a 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 37e7d3ad1..37e7d3ad1 100644
--- a/layouts/shortcodes/directoryindex.html
+++ b/docs/layouts/shortcodes/directoryindex.html
diff --git a/layouts/shortcodes/docfile.html b/docs/layouts/shortcodes/docfile.html
index 2f982aae8..2f982aae8 100644
--- a/layouts/shortcodes/docfile.html
+++ b/docs/layouts/shortcodes/docfile.html
diff --git a/layouts/shortcodes/exfile.html b/docs/layouts/shortcodes/exfile.html
index 226782957..226782957 100644
--- a/layouts/shortcodes/exfile.html
+++ b/docs/layouts/shortcodes/exfile.html
diff --git a/layouts/shortcodes/exfm.html b/docs/layouts/shortcodes/exfm.html
index c0429bbe1..c0429bbe1 100644
--- a/layouts/shortcodes/exfm.html
+++ b/docs/layouts/shortcodes/exfm.html
diff --git a/layouts/shortcodes/gh.html b/docs/layouts/shortcodes/gh.html
index 981f4b838..981f4b838 100644
--- a/layouts/shortcodes/gh.html
+++ b/docs/layouts/shortcodes/gh.html
diff --git a/layouts/shortcodes/ghrepo.html b/docs/layouts/shortcodes/ghrepo.html
index e9df40d6a..e9df40d6a 100644
--- a/layouts/shortcodes/ghrepo.html
+++ b/docs/layouts/shortcodes/ghrepo.html
diff --git a/layouts/shortcodes/imgproc.html b/docs/layouts/shortcodes/imgproc.html
index f44b509c2..f44b509c2 100644
--- a/layouts/shortcodes/imgproc.html
+++ b/docs/layouts/shortcodes/imgproc.html
diff --git a/layouts/shortcodes/nohighlight.html b/docs/layouts/shortcodes/nohighlight.html
index 238234f17..238234f17 100644
--- a/layouts/shortcodes/nohighlight.html
+++ b/docs/layouts/shortcodes/nohighlight.html
diff --git a/layouts/shortcodes/note.html b/docs/layouts/shortcodes/note.html
index 24d2cd0b2..24d2cd0b2 100644
--- a/layouts/shortcodes/note.html
+++ b/docs/layouts/shortcodes/note.html
diff --git a/layouts/shortcodes/output.html b/docs/layouts/shortcodes/output.html
index e51d284bb..e51d284bb 100644
--- a/layouts/shortcodes/output.html
+++ b/docs/layouts/shortcodes/output.html
diff --git a/layouts/shortcodes/readfile.html b/docs/layouts/shortcodes/readfile.html
index 36400ac55..36400ac55 100644
--- a/layouts/shortcodes/readfile.html
+++ b/docs/layouts/shortcodes/readfile.html
diff --git a/layouts/shortcodes/tip.html b/docs/layouts/shortcodes/tip.html
index 139e3376b..139e3376b 100644
--- a/layouts/shortcodes/tip.html
+++ b/docs/layouts/shortcodes/tip.html
diff --git a/layouts/shortcodes/todo.html b/docs/layouts/shortcodes/todo.html
index 50a099267..50a099267 100644
--- a/layouts/shortcodes/todo.html
+++ b/docs/layouts/shortcodes/todo.html
diff --git a/layouts/shortcodes/warning.html b/docs/layouts/shortcodes/warning.html
index c9147be64..c9147be64 100644
--- a/layouts/shortcodes/warning.html
+++ b/docs/layouts/shortcodes/warning.html
diff --git a/layouts/shortcodes/yt.html b/docs/layouts/shortcodes/yt.html
index 6915cec5f..6915cec5f 100644
--- a/layouts/shortcodes/yt.html
+++ b/docs/layouts/shortcodes/yt.html
diff --git a/netlify.toml b/docs/netlify.toml
index 667456275..667456275 100644
--- a/netlify.toml
+++ b/docs/netlify.toml
diff --git a/pull-theme.sh b/docs/pull-theme.sh
index 828b6cfb4..828b6cfb4 100755
--- a/pull-theme.sh
+++ b/docs/pull-theme.sh
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 000000000..e0f2f62df
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1 @@
+Pygments==2.1.3
diff --git a/resources/.gitattributes b/docs/resources/.gitattributes
index a205a8e9d..a205a8e9d 100644
--- a/resources/.gitattributes
+++ b/docs/resources/.gitattributes
diff --git a/resources/_gen/assets/css/output/css/app.css_d11fe7b62c27961c87ecd0f2490357b9.content b/docs/resources/_gen/assets/css/output/css/app.css_d11fe7b62c27961c87ecd0f2490357b9.content
index 42d7140c5..42d7140c5 100644
--- a/resources/_gen/assets/css/output/css/app.css_d11fe7b62c27961c87ecd0f2490357b9.content
+++ b/docs/resources/_gen/assets/css/output/css/app.css_d11fe7b62c27961c87ecd0f2490357b9.content
diff --git a/resources/_gen/assets/css/output/css/app.css_d11fe7b62c27961c87ecd0f2490357b9.json b/docs/resources/_gen/assets/css/output/css/app.css_d11fe7b62c27961c87ecd0f2490357b9.json
index 91f089a79..91f089a79 100644
--- a/resources/_gen/assets/css/output/css/app.css_d11fe7b62c27961c87ecd0f2490357b9.json
+++ b/docs/resources/_gen/assets/css/output/css/app.css_d11fe7b62c27961c87ecd0f2490357b9.json
diff --git a/resources/_gen/assets/js/output/js/app.js_8848f55d07695b7ff7188138f23d69e3.content b/docs/resources/_gen/assets/js/output/js/app.js_8848f55d07695b7ff7188138f23d69e3.content
index 3097ec5a6..3097ec5a6 100644
--- a/resources/_gen/assets/js/output/js/app.js_8848f55d07695b7ff7188138f23d69e3.content
+++ b/docs/resources/_gen/assets/js/output/js/app.js_8848f55d07695b7ff7188138f23d69e3.content
diff --git a/resources/_gen/assets/js/output/js/app.js_8848f55d07695b7ff7188138f23d69e3.json b/docs/resources/_gen/assets/js/output/js/app.js_8848f55d07695b7ff7188138f23d69e3.json
index 06787c13f..06787c13f 100644
--- a/resources/_gen/assets/js/output/js/app.js_8848f55d07695b7ff7188138f23d69e3.json
+++ b/docs/resources/_gen/assets/js/output/js/app.js_8848f55d07695b7ff7188138f23d69e3.json
diff --git a/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q10_catmullrom.jpg b/docs/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q10_catmullrom.jpg
index 6499e8341..6499e8341 100644
--- a/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q10_catmullrom.jpg
+++ b/docs/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q10_catmullrom.jpg
Binary files differ
diff --git a/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q75_catmullrom.jpg b/docs/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q75_catmullrom.jpg
index 8caa58798..8caa58798 100644
--- a/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q75_catmullrom.jpg
+++ b/docs/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q75_catmullrom.jpg
Binary files differ
diff --git a/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_left.jpg b/docs/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_left.jpg
index 69549b2df..69549b2df 100644
--- a/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_left.jpg
+++ b/docs/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_left.jpg
Binary files differ
diff --git a/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_right.jpg b/docs/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_right.jpg
index 249708765..249708765 100644
--- a/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_right.jpg
+++ b/docs/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_right.jpg
Binary files differ
diff --git a/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x90_fit_q75_catmullrom.jpg b/docs/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x90_fit_q75_catmullrom.jpg
index 634fb0ce1..634fb0ce1 100644
--- a/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x90_fit_q75_catmullrom.jpg
+++ b/docs/resources/_gen/images/about/new-in-032/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x90_fit_q75_catmullrom.jpg
Binary files differ
diff --git a/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x200_fill_q75_catmullrom_smart1.jpg b/docs/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x200_fill_q75_catmullrom_smart1.jpg
index 094f2a4f1..094f2a4f1 100644
--- a/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x200_fill_q75_catmullrom_smart1.jpg
+++ b/docs/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x200_fill_q75_catmullrom_smart1.jpg
Binary files differ
diff --git a/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q10_catmullrom.jpg b/docs/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q10_catmullrom.jpg
index 6499e8341..6499e8341 100644
--- a/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q10_catmullrom.jpg
+++ b/docs/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q10_catmullrom.jpg
Binary files differ
diff --git a/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q75_catmullrom.jpg b/docs/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q75_catmullrom.jpg
index 8caa58798..8caa58798 100644
--- a/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q75_catmullrom.jpg
+++ b/docs/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x0_resize_q75_catmullrom.jpg
Binary files differ
diff --git a/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_left.jpg b/docs/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_left.jpg
index 69549b2df..69549b2df 100644
--- a/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_left.jpg
+++ b/docs/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_left.jpg
Binary files differ
diff --git a/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_right.jpg b/docs/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_right.jpg
index 249708765..249708765 100644
--- a/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_right.jpg
+++ b/docs/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x120_fill_q75_catmullrom_right.jpg
Binary files differ
diff --git a/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x90_fit_q75_catmullrom.jpg b/docs/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x90_fit_q75_catmullrom.jpg
index 634fb0ce1..634fb0ce1 100644
--- a/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x90_fit_q75_catmullrom.jpg
+++ b/docs/resources/_gen/images/content-management/image-processing/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_90x90_fit_q75_catmullrom.jpg
Binary files differ
diff --git a/resources/_gen/images/content-management/organization/1-featured-content-bundles_hu3e3ae7839b071119f32acaa20f204198_63640_300x0_resize_catmullrom_2.png b/docs/resources/_gen/images/content-management/organization/1-featured-content-bundles_hu3e3ae7839b071119f32acaa20f204198_63640_300x0_resize_catmullrom_2.png
index bc604e562..bc604e562 100644
--- a/resources/_gen/images/content-management/organization/1-featured-content-bundles_hu3e3ae7839b071119f32acaa20f204198_63640_300x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/content-management/organization/1-featured-content-bundles_hu3e3ae7839b071119f32acaa20f204198_63640_300x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.33-relnotes/featured-hugo-33-poster_hu45ce9da1cdea6ca61c5f4f5baccdcad4_70230_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.33-relnotes/featured-hugo-33-poster_hu45ce9da1cdea6ca61c5f4f5baccdcad4_70230_480x0_resize_catmullrom_2.png
index 69284a4d8..69284a4d8 100644
--- a/resources/_gen/images/news/0.33-relnotes/featured-hugo-33-poster_hu45ce9da1cdea6ca61c5f4f5baccdcad4_70230_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.33-relnotes/featured-hugo-33-poster_hu45ce9da1cdea6ca61c5f4f5baccdcad4_70230_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.34-relnotes/featured-34-poster_hud8d73dc5df8d5a35383849a78eea35dd_78317_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.34-relnotes/featured-34-poster_hud8d73dc5df8d5a35383849a78eea35dd_78317_480x0_resize_catmullrom_2.png
index 21fce414d..21fce414d 100644
--- a/resources/_gen/images/news/0.34-relnotes/featured-34-poster_hud8d73dc5df8d5a35383849a78eea35dd_78317_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.34-relnotes/featured-34-poster_hud8d73dc5df8d5a35383849a78eea35dd_78317_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.35-relnotes/featured-hugo-35-poster_hua42b1310dd72f60a34e02851ebf2f82e_88519_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.35-relnotes/featured-hugo-35-poster_hua42b1310dd72f60a34e02851ebf2f82e_88519_480x0_resize_catmullrom_2.png
index 370628aec..370628aec 100644
--- a/resources/_gen/images/news/0.35-relnotes/featured-hugo-35-poster_hua42b1310dd72f60a34e02851ebf2f82e_88519_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.35-relnotes/featured-hugo-35-poster_hua42b1310dd72f60a34e02851ebf2f82e_88519_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.36-relnotes/featured-hugo-36-poster_huf2fee368f65c75d3878561ed4225c39a_67640_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.36-relnotes/featured-hugo-36-poster_huf2fee368f65c75d3878561ed4225c39a_67640_480x0_resize_catmullrom_2.png
index f57f33902..f57f33902 100644
--- a/resources/_gen/images/news/0.36-relnotes/featured-hugo-36-poster_huf2fee368f65c75d3878561ed4225c39a_67640_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.36-relnotes/featured-hugo-36-poster_huf2fee368f65c75d3878561ed4225c39a_67640_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.37-relnotes/featured-hugo-37-poster_hue9685d25c387d657b0640498bf6a10ee_186693_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.37-relnotes/featured-hugo-37-poster_hue9685d25c387d657b0640498bf6a10ee_186693_480x0_resize_catmullrom_2.png
index d0f3670b2..d0f3670b2 100644
--- a/resources/_gen/images/news/0.37-relnotes/featured-hugo-37-poster_hue9685d25c387d657b0640498bf6a10ee_186693_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.37-relnotes/featured-hugo-37-poster_hue9685d25c387d657b0640498bf6a10ee_186693_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.37-relnotes/featured-hugo-37-poster_hue9685d25c387d657b0640498bf6a10ee_186693_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.37-relnotes/featured-hugo-37-poster_hue9685d25c387d657b0640498bf6a10ee_186693_640x0_resize_catmullrom_2.png
index a91566c1e..a91566c1e 100644
--- a/resources/_gen/images/news/0.37-relnotes/featured-hugo-37-poster_hue9685d25c387d657b0640498bf6a10ee_186693_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.37-relnotes/featured-hugo-37-poster_hue9685d25c387d657b0640498bf6a10ee_186693_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.38-relnotes/featured-poster_hudf8012d38ef42d46a6cab1b31156bf3a_69978_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.38-relnotes/featured-poster_hudf8012d38ef42d46a6cab1b31156bf3a_69978_480x0_resize_catmullrom_2.png
index ec2bf453c..ec2bf453c 100644
--- a/resources/_gen/images/news/0.38-relnotes/featured-poster_hudf8012d38ef42d46a6cab1b31156bf3a_69978_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.38-relnotes/featured-poster_hudf8012d38ef42d46a6cab1b31156bf3a_69978_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.38-relnotes/featured-poster_hudf8012d38ef42d46a6cab1b31156bf3a_69978_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.38-relnotes/featured-poster_hudf8012d38ef42d46a6cab1b31156bf3a_69978_640x0_resize_catmullrom_2.png
index b97d27f32..b97d27f32 100644
--- a/resources/_gen/images/news/0.38-relnotes/featured-poster_hudf8012d38ef42d46a6cab1b31156bf3a_69978_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.38-relnotes/featured-poster_hudf8012d38ef42d46a6cab1b31156bf3a_69978_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.39-relnotes/featured-hugo-39-poster_hu3261e6e65defb4edf9f0fce20bf5f60d_217215_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.39-relnotes/featured-hugo-39-poster_hu3261e6e65defb4edf9f0fce20bf5f60d_217215_480x0_resize_catmullrom_2.png
index e9fe82ca4..e9fe82ca4 100644
--- a/resources/_gen/images/news/0.39-relnotes/featured-hugo-39-poster_hu3261e6e65defb4edf9f0fce20bf5f60d_217215_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.39-relnotes/featured-hugo-39-poster_hu3261e6e65defb4edf9f0fce20bf5f60d_217215_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.39-relnotes/featured-hugo-39-poster_hu3261e6e65defb4edf9f0fce20bf5f60d_217215_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.39-relnotes/featured-hugo-39-poster_hu3261e6e65defb4edf9f0fce20bf5f60d_217215_640x0_resize_catmullrom_2.png
index 18d992dfe..18d992dfe 100644
--- a/resources/_gen/images/news/0.39-relnotes/featured-hugo-39-poster_hu3261e6e65defb4edf9f0fce20bf5f60d_217215_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.39-relnotes/featured-hugo-39-poster_hu3261e6e65defb4edf9f0fce20bf5f60d_217215_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.40-relnotes/featured-hugo-40-poster_hu20c69e2a166f65e329d8fbabe8d2cc58_69238_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.40-relnotes/featured-hugo-40-poster_hu20c69e2a166f65e329d8fbabe8d2cc58_69238_480x0_resize_catmullrom_2.png
index 656e02b34..656e02b34 100644
--- a/resources/_gen/images/news/0.40-relnotes/featured-hugo-40-poster_hu20c69e2a166f65e329d8fbabe8d2cc58_69238_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.40-relnotes/featured-hugo-40-poster_hu20c69e2a166f65e329d8fbabe8d2cc58_69238_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.40-relnotes/featured-hugo-40-poster_hu20c69e2a166f65e329d8fbabe8d2cc58_69238_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.40-relnotes/featured-hugo-40-poster_hu20c69e2a166f65e329d8fbabe8d2cc58_69238_640x0_resize_catmullrom_2.png
index 0939f8d0f..0939f8d0f 100644
--- a/resources/_gen/images/news/0.40-relnotes/featured-hugo-40-poster_hu20c69e2a166f65e329d8fbabe8d2cc58_69238_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.40-relnotes/featured-hugo-40-poster_hu20c69e2a166f65e329d8fbabe8d2cc58_69238_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.41-relnotes/featured-hugo-41-poster_hud81cd208cb270af61610509ee199ae20_67955_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.41-relnotes/featured-hugo-41-poster_hud81cd208cb270af61610509ee199ae20_67955_480x0_resize_catmullrom_2.png
index 1a7686877..1a7686877 100644
--- a/resources/_gen/images/news/0.41-relnotes/featured-hugo-41-poster_hud81cd208cb270af61610509ee199ae20_67955_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.41-relnotes/featured-hugo-41-poster_hud81cd208cb270af61610509ee199ae20_67955_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.41-relnotes/featured-hugo-41-poster_hud81cd208cb270af61610509ee199ae20_67955_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.41-relnotes/featured-hugo-41-poster_hud81cd208cb270af61610509ee199ae20_67955_640x0_resize_catmullrom_2.png
index 49455ba6b..49455ba6b 100644
--- a/resources/_gen/images/news/0.41-relnotes/featured-hugo-41-poster_hud81cd208cb270af61610509ee199ae20_67955_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.41-relnotes/featured-hugo-41-poster_hud81cd208cb270af61610509ee199ae20_67955_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.42-relnotes/featured-hugo-42-poster_hue0604c0846526b6d2f8ba376edd013b6_74852_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.42-relnotes/featured-hugo-42-poster_hue0604c0846526b6d2f8ba376edd013b6_74852_480x0_resize_catmullrom_2.png
index 7db1012b3..7db1012b3 100644
--- a/resources/_gen/images/news/0.42-relnotes/featured-hugo-42-poster_hue0604c0846526b6d2f8ba376edd013b6_74852_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.42-relnotes/featured-hugo-42-poster_hue0604c0846526b6d2f8ba376edd013b6_74852_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.42-relnotes/featured-hugo-42-poster_hue0604c0846526b6d2f8ba376edd013b6_74852_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.42-relnotes/featured-hugo-42-poster_hue0604c0846526b6d2f8ba376edd013b6_74852_640x0_resize_catmullrom_2.png
index adb5103fe..adb5103fe 100644
--- a/resources/_gen/images/news/0.42-relnotes/featured-hugo-42-poster_hue0604c0846526b6d2f8ba376edd013b6_74852_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.42-relnotes/featured-hugo-42-poster_hue0604c0846526b6d2f8ba376edd013b6_74852_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.43-relnotes/featured-hugo-43-poster_hu3948fe44e4a966d8149b4bf077395057_78299_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.43-relnotes/featured-hugo-43-poster_hu3948fe44e4a966d8149b4bf077395057_78299_480x0_resize_catmullrom_2.png
index 7a9eea7ac..7a9eea7ac 100644
--- a/resources/_gen/images/news/0.43-relnotes/featured-hugo-43-poster_hu3948fe44e4a966d8149b4bf077395057_78299_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.43-relnotes/featured-hugo-43-poster_hu3948fe44e4a966d8149b4bf077395057_78299_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.43-relnotes/featured-hugo-43-poster_hu3948fe44e4a966d8149b4bf077395057_78299_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.43-relnotes/featured-hugo-43-poster_hu3948fe44e4a966d8149b4bf077395057_78299_640x0_resize_catmullrom_2.png
index 02b8217bf..02b8217bf 100644
--- a/resources/_gen/images/news/0.43-relnotes/featured-hugo-43-poster_hu3948fe44e4a966d8149b4bf077395057_78299_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.43-relnotes/featured-hugo-43-poster_hu3948fe44e4a966d8149b4bf077395057_78299_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.44-relnotes/featured-hugo-44-poster_hu6505d1982bab71bfe9c6c7adcedfd7f7_77631_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.44-relnotes/featured-hugo-44-poster_hu6505d1982bab71bfe9c6c7adcedfd7f7_77631_480x0_resize_catmullrom_2.png
index 6691fdc17..6691fdc17 100644
--- a/resources/_gen/images/news/0.44-relnotes/featured-hugo-44-poster_hu6505d1982bab71bfe9c6c7adcedfd7f7_77631_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.44-relnotes/featured-hugo-44-poster_hu6505d1982bab71bfe9c6c7adcedfd7f7_77631_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.44-relnotes/featured-hugo-44-poster_hu6505d1982bab71bfe9c6c7adcedfd7f7_77631_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.44-relnotes/featured-hugo-44-poster_hu6505d1982bab71bfe9c6c7adcedfd7f7_77631_640x0_resize_catmullrom_2.png
index 841552fa2..841552fa2 100644
--- a/resources/_gen/images/news/0.44-relnotes/featured-hugo-44-poster_hu6505d1982bab71bfe9c6c7adcedfd7f7_77631_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.44-relnotes/featured-hugo-44-poster_hu6505d1982bab71bfe9c6c7adcedfd7f7_77631_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.45-relnotes/featured-hugo-45-poster_huea79995576e3b93a3041ae824a391758_66863_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.45-relnotes/featured-hugo-45-poster_huea79995576e3b93a3041ae824a391758_66863_480x0_resize_catmullrom_2.png
index 9d884f32f..9d884f32f 100644
--- a/resources/_gen/images/news/0.45-relnotes/featured-hugo-45-poster_huea79995576e3b93a3041ae824a391758_66863_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.45-relnotes/featured-hugo-45-poster_huea79995576e3b93a3041ae824a391758_66863_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.45-relnotes/featured-hugo-45-poster_huea79995576e3b93a3041ae824a391758_66863_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.45-relnotes/featured-hugo-45-poster_huea79995576e3b93a3041ae824a391758_66863_640x0_resize_catmullrom_2.png
index b259f357d..b259f357d 100644
--- a/resources/_gen/images/news/0.45-relnotes/featured-hugo-45-poster_huea79995576e3b93a3041ae824a391758_66863_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.45-relnotes/featured-hugo-45-poster_huea79995576e3b93a3041ae824a391758_66863_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.46-relnotes/featured-hugo-46-poster_hue04c7655caa254a1835311c9409185d8_68614_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.46-relnotes/featured-hugo-46-poster_hue04c7655caa254a1835311c9409185d8_68614_480x0_resize_catmullrom_2.png
index a33f11e3d..a33f11e3d 100644
--- a/resources/_gen/images/news/0.46-relnotes/featured-hugo-46-poster_hue04c7655caa254a1835311c9409185d8_68614_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.46-relnotes/featured-hugo-46-poster_hue04c7655caa254a1835311c9409185d8_68614_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.46-relnotes/featured-hugo-46-poster_hue04c7655caa254a1835311c9409185d8_68614_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.46-relnotes/featured-hugo-46-poster_hue04c7655caa254a1835311c9409185d8_68614_640x0_resize_catmullrom_2.png
index 5c039e4b8..5c039e4b8 100644
--- a/resources/_gen/images/news/0.46-relnotes/featured-hugo-46-poster_hue04c7655caa254a1835311c9409185d8_68614_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.46-relnotes/featured-hugo-46-poster_hue04c7655caa254a1835311c9409185d8_68614_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.47-relnotes/featured-hugo-47-poster_hud3879b84908b49d38ac2cd1416f654ff_88288_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.47-relnotes/featured-hugo-47-poster_hud3879b84908b49d38ac2cd1416f654ff_88288_480x0_resize_catmullrom_2.png
index aaa0c7b87..aaa0c7b87 100644
--- a/resources/_gen/images/news/0.47-relnotes/featured-hugo-47-poster_hud3879b84908b49d38ac2cd1416f654ff_88288_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.47-relnotes/featured-hugo-47-poster_hud3879b84908b49d38ac2cd1416f654ff_88288_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.47-relnotes/featured-hugo-47-poster_hud3879b84908b49d38ac2cd1416f654ff_88288_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.47-relnotes/featured-hugo-47-poster_hud3879b84908b49d38ac2cd1416f654ff_88288_640x0_resize_catmullrom_2.png
index c87495ebf..c87495ebf 100644
--- a/resources/_gen/images/news/0.47-relnotes/featured-hugo-47-poster_hud3879b84908b49d38ac2cd1416f654ff_88288_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.47-relnotes/featured-hugo-47-poster_hud3879b84908b49d38ac2cd1416f654ff_88288_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.48-relnotes/featured-hugo-48-poster_hub95348423e80ff144dfee01d64fb9889_95358_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.48-relnotes/featured-hugo-48-poster_hub95348423e80ff144dfee01d64fb9889_95358_480x0_resize_catmullrom_2.png
index af1b061f6..af1b061f6 100644
--- a/resources/_gen/images/news/0.48-relnotes/featured-hugo-48-poster_hub95348423e80ff144dfee01d64fb9889_95358_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.48-relnotes/featured-hugo-48-poster_hub95348423e80ff144dfee01d64fb9889_95358_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.48-relnotes/featured-hugo-48-poster_hub95348423e80ff144dfee01d64fb9889_95358_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.48-relnotes/featured-hugo-48-poster_hub95348423e80ff144dfee01d64fb9889_95358_640x0_resize_catmullrom_2.png
index 7ae07ef02..7ae07ef02 100644
--- a/resources/_gen/images/news/0.48-relnotes/featured-hugo-48-poster_hub95348423e80ff144dfee01d64fb9889_95358_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.48-relnotes/featured-hugo-48-poster_hub95348423e80ff144dfee01d64fb9889_95358_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.49-relnotes/featured-hugo-49-poster_hud9cdb0f9aa2ec95d28fc3f49c81e7940_66352_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.49-relnotes/featured-hugo-49-poster_hud9cdb0f9aa2ec95d28fc3f49c81e7940_66352_480x0_resize_catmullrom_2.png
index 2d9179ad9..2d9179ad9 100644
--- a/resources/_gen/images/news/0.49-relnotes/featured-hugo-49-poster_hud9cdb0f9aa2ec95d28fc3f49c81e7940_66352_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.49-relnotes/featured-hugo-49-poster_hud9cdb0f9aa2ec95d28fc3f49c81e7940_66352_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.49-relnotes/featured-hugo-49-poster_hud9cdb0f9aa2ec95d28fc3f49c81e7940_66352_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.49-relnotes/featured-hugo-49-poster_hud9cdb0f9aa2ec95d28fc3f49c81e7940_66352_640x0_resize_catmullrom_2.png
index e4f918db7..e4f918db7 100644
--- a/resources/_gen/images/news/0.49-relnotes/featured-hugo-49-poster_hud9cdb0f9aa2ec95d28fc3f49c81e7940_66352_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.49-relnotes/featured-hugo-49-poster_hud9cdb0f9aa2ec95d28fc3f49c81e7940_66352_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.50-relnotes/featured-hugo-50-poster_hudcbbb9a5a0079d08447101e6cfae6e40_227240_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.50-relnotes/featured-hugo-50-poster_hudcbbb9a5a0079d08447101e6cfae6e40_227240_480x0_resize_catmullrom_2.png
index 1a1caba80..1a1caba80 100644
--- a/resources/_gen/images/news/0.50-relnotes/featured-hugo-50-poster_hudcbbb9a5a0079d08447101e6cfae6e40_227240_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.50-relnotes/featured-hugo-50-poster_hudcbbb9a5a0079d08447101e6cfae6e40_227240_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.50-relnotes/featured-hugo-50-poster_hudcbbb9a5a0079d08447101e6cfae6e40_227240_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.50-relnotes/featured-hugo-50-poster_hudcbbb9a5a0079d08447101e6cfae6e40_227240_640x0_resize_catmullrom_2.png
index 338531b40..338531b40 100644
--- a/resources/_gen/images/news/0.50-relnotes/featured-hugo-50-poster_hudcbbb9a5a0079d08447101e6cfae6e40_227240_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.50-relnotes/featured-hugo-50-poster_hudcbbb9a5a0079d08447101e6cfae6e40_227240_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.51-relnotes/featured-hugo-51-poster_hu25ab021d1365edeedf46d92fdb888ccf_117678_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.51-relnotes/featured-hugo-51-poster_hu25ab021d1365edeedf46d92fdb888ccf_117678_480x0_resize_catmullrom_2.png
index a9a806089..a9a806089 100644
--- a/resources/_gen/images/news/0.51-relnotes/featured-hugo-51-poster_hu25ab021d1365edeedf46d92fdb888ccf_117678_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.51-relnotes/featured-hugo-51-poster_hu25ab021d1365edeedf46d92fdb888ccf_117678_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.51-relnotes/featured-hugo-51-poster_hu25ab021d1365edeedf46d92fdb888ccf_117678_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.51-relnotes/featured-hugo-51-poster_hu25ab021d1365edeedf46d92fdb888ccf_117678_640x0_resize_catmullrom_2.png
index 1ef2c1d5b..1ef2c1d5b 100644
--- a/resources/_gen/images/news/0.51-relnotes/featured-hugo-51-poster_hu25ab021d1365edeedf46d92fdb888ccf_117678_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.51-relnotes/featured-hugo-51-poster_hu25ab021d1365edeedf46d92fdb888ccf_117678_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.52-relnotes/featured-hugo-52-poster_hu7f2ed09038efabda07872a275a935ada_336810_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.52-relnotes/featured-hugo-52-poster_hu7f2ed09038efabda07872a275a935ada_336810_480x0_resize_catmullrom_2.png
index fffdde498..fffdde498 100644
--- a/resources/_gen/images/news/0.52-relnotes/featured-hugo-52-poster_hu7f2ed09038efabda07872a275a935ada_336810_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.52-relnotes/featured-hugo-52-poster_hu7f2ed09038efabda07872a275a935ada_336810_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.52-relnotes/featured-hugo-52-poster_hu7f2ed09038efabda07872a275a935ada_336810_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.52-relnotes/featured-hugo-52-poster_hu7f2ed09038efabda07872a275a935ada_336810_640x0_resize_catmullrom_2.png
index a8927153e..a8927153e 100644
--- a/resources/_gen/images/news/0.52-relnotes/featured-hugo-52-poster_hu7f2ed09038efabda07872a275a935ada_336810_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.52-relnotes/featured-hugo-52-poster_hu7f2ed09038efabda07872a275a935ada_336810_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.53-relnotes/featured-hugo-53-poster_hu3f68fc193ad172155ee35a0be89133bf_110427_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.53-relnotes/featured-hugo-53-poster_hu3f68fc193ad172155ee35a0be89133bf_110427_480x0_resize_catmullrom_2.png
index 9b203290b..9b203290b 100644
--- a/resources/_gen/images/news/0.53-relnotes/featured-hugo-53-poster_hu3f68fc193ad172155ee35a0be89133bf_110427_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.53-relnotes/featured-hugo-53-poster_hu3f68fc193ad172155ee35a0be89133bf_110427_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.53-relnotes/featured-hugo-53-poster_hu3f68fc193ad172155ee35a0be89133bf_110427_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.53-relnotes/featured-hugo-53-poster_hu3f68fc193ad172155ee35a0be89133bf_110427_640x0_resize_catmullrom_2.png
index 978d0dcba..978d0dcba 100644
--- a/resources/_gen/images/news/0.53-relnotes/featured-hugo-53-poster_hu3f68fc193ad172155ee35a0be89133bf_110427_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.53-relnotes/featured-hugo-53-poster_hu3f68fc193ad172155ee35a0be89133bf_110427_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.54.0-relnotes/featured-hugo-54.0-poster_hufa0b7b755124a76fe71c5c70a25724c2_59805_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.54.0-relnotes/featured-hugo-54.0-poster_hufa0b7b755124a76fe71c5c70a25724c2_59805_480x0_resize_catmullrom_2.png
index e7e16d4b7..e7e16d4b7 100644
--- a/resources/_gen/images/news/0.54.0-relnotes/featured-hugo-54.0-poster_hufa0b7b755124a76fe71c5c70a25724c2_59805_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.54.0-relnotes/featured-hugo-54.0-poster_hufa0b7b755124a76fe71c5c70a25724c2_59805_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.54.0-relnotes/featured-hugo-54.0-poster_hufa0b7b755124a76fe71c5c70a25724c2_59805_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.54.0-relnotes/featured-hugo-54.0-poster_hufa0b7b755124a76fe71c5c70a25724c2_59805_640x0_resize_catmullrom_2.png
index 5a92ee618..5a92ee618 100644
--- a/resources/_gen/images/news/0.54.0-relnotes/featured-hugo-54.0-poster_hufa0b7b755124a76fe71c5c70a25724c2_59805_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.54.0-relnotes/featured-hugo-54.0-poster_hufa0b7b755124a76fe71c5c70a25724c2_59805_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.55.0-relnotes/featured_hu9474666a09966109e944f93e1ecf78c0_1221797_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.55.0-relnotes/featured_hu9474666a09966109e944f93e1ecf78c0_1221797_480x0_resize_catmullrom_2.png
index 72eb0d6bc..72eb0d6bc 100644
--- a/resources/_gen/images/news/0.55.0-relnotes/featured_hu9474666a09966109e944f93e1ecf78c0_1221797_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.55.0-relnotes/featured_hu9474666a09966109e944f93e1ecf78c0_1221797_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/0.55.0-relnotes/featured_hu9474666a09966109e944f93e1ecf78c0_1221797_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/0.55.0-relnotes/featured_hu9474666a09966109e944f93e1ecf78c0_1221797_640x0_resize_catmullrom_2.png
index 65ef73fdf..65ef73fdf 100644
--- a/resources/_gen/images/news/0.55.0-relnotes/featured_hu9474666a09966109e944f93e1ecf78c0_1221797_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/0.55.0-relnotes/featured_hu9474666a09966109e944f93e1ecf78c0_1221797_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/featured_hu30cb938a182ebd06b50ed15d006d8f64_179291_480x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/featured_hu30cb938a182ebd06b50ed15d006d8f64_179291_480x0_resize_catmullrom_2.png
index 9f054a3cf..9f054a3cf 100644
--- a/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/featured_hu30cb938a182ebd06b50ed15d006d8f64_179291_480x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/featured_hu30cb938a182ebd06b50ed15d006d8f64_179291_480x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/featured_hu30cb938a182ebd06b50ed15d006d8f64_179291_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/featured_hu30cb938a182ebd06b50ed15d006d8f64_179291_640x0_resize_catmullrom_2.png
index 2adcc4c3d..2adcc4c3d 100644
--- a/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/featured_hu30cb938a182ebd06b50ed15d006d8f64_179291_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/featured_hu30cb938a182ebd06b50ed15d006d8f64_179291_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/graph-stars_hu169ba15a8bcaf4ddd6a5a1aa8505c448_15599_600x400_fit_catmullrom_2.png b/docs/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/graph-stars_hu169ba15a8bcaf4ddd6a5a1aa8505c448_15599_600x400_fit_catmullrom_2.png
index 31f5bea34..31f5bea34 100644
--- a/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/graph-stars_hu169ba15a8bcaf4ddd6a5a1aa8505c448_15599_600x400_fit_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/graph-stars_hu169ba15a8bcaf4ddd6a5a1aa8505c448_15599_600x400_fit_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/graph-themes_hu25707bee0ec3007199f71bb29226f30c_16956_600x400_fit_catmullrom_2.png b/docs/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/graph-themes_hu25707bee0ec3007199f71bb29226f30c_16956_600x400_fit_catmullrom_2.png
index 177d34fa9..177d34fa9 100644
--- a/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/graph-themes_hu25707bee0ec3007199f71bb29226f30c_16956_600x400_fit_catmullrom_2.png
+++ b/docs/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/graph-themes_hu25707bee0ec3007199f71bb29226f30c_16956_600x400_fit_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/sunset-get_hu69849a7cdb847c2393a7b3a7f6061c86_387442_600x300_fill_catmullrom_smart1_2.png b/docs/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/sunset-get_hu69849a7cdb847c2393a7b3a7f6061c86_387442_600x300_fill_catmullrom_smart1_2.png
index a4bf4bd92..a4bf4bd92 100644
--- a/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/sunset-get_hu69849a7cdb847c2393a7b3a7f6061c86_387442_600x300_fill_catmullrom_smart1_2.png
+++ b/docs/resources/_gen/images/news/lets-celebrate-hugos-5th-birthday/sunset-get_hu69849a7cdb847c2393a7b3a7f6061c86_387442_600x300_fill_catmullrom_smart1_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/1password-support/featured_hu870838c23243880857c2e418dd7ac099_165718_1024x512_fill_catmullrom_top_2.png b/docs/resources/_gen/images/showcase/1password-support/featured_hu870838c23243880857c2e418dd7ac099_165718_1024x512_fill_catmullrom_top_2.png
index b7399ff8e..b7399ff8e 100644
--- a/resources/_gen/images/showcase/1password-support/featured_hu870838c23243880857c2e418dd7ac099_165718_1024x512_fill_catmullrom_top_2.png
+++ b/docs/resources/_gen/images/showcase/1password-support/featured_hu870838c23243880857c2e418dd7ac099_165718_1024x512_fill_catmullrom_top_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/1password-support/featured_hu870838c23243880857c2e418dd7ac099_165718_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/showcase/1password-support/featured_hu870838c23243880857c2e418dd7ac099_165718_640x0_resize_catmullrom_2.png
index e953c86df..e953c86df 100644
--- a/resources/_gen/images/showcase/1password-support/featured_hu870838c23243880857c2e418dd7ac099_165718_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/showcase/1password-support/featured_hu870838c23243880857c2e418dd7ac099_165718_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/1password-support/featured_hu870838c23243880857c2e418dd7ac099_165718_8714c8c914d32c12c7eb833a42713319.png b/docs/resources/_gen/images/showcase/1password-support/featured_hu870838c23243880857c2e418dd7ac099_165718_8714c8c914d32c12c7eb833a42713319.png
index d28d99662..d28d99662 100644
--- a/resources/_gen/images/showcase/1password-support/featured_hu870838c23243880857c2e418dd7ac099_165718_8714c8c914d32c12c7eb833a42713319.png
+++ b/docs/resources/_gen/images/showcase/1password-support/featured_hu870838c23243880857c2e418dd7ac099_165718_8714c8c914d32c12c7eb833a42713319.png
Binary files differ
diff --git a/resources/_gen/images/showcase/arolla-cocoon/featured-template_hu22aab819ab27e4f878d1ff0b7cf78050_451984_1024x512_fill_catmullrom_top_2.png b/docs/resources/_gen/images/showcase/arolla-cocoon/featured-template_hu22aab819ab27e4f878d1ff0b7cf78050_451984_1024x512_fill_catmullrom_top_2.png
index 750a1100b..750a1100b 100644
--- a/resources/_gen/images/showcase/arolla-cocoon/featured-template_hu22aab819ab27e4f878d1ff0b7cf78050_451984_1024x512_fill_catmullrom_top_2.png
+++ b/docs/resources/_gen/images/showcase/arolla-cocoon/featured-template_hu22aab819ab27e4f878d1ff0b7cf78050_451984_1024x512_fill_catmullrom_top_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/arolla-cocoon/featured-template_hu22aab819ab27e4f878d1ff0b7cf78050_451984_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/showcase/arolla-cocoon/featured-template_hu22aab819ab27e4f878d1ff0b7cf78050_451984_640x0_resize_catmullrom_2.png
index b653310b1..b653310b1 100644
--- a/resources/_gen/images/showcase/arolla-cocoon/featured-template_hu22aab819ab27e4f878d1ff0b7cf78050_451984_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/showcase/arolla-cocoon/featured-template_hu22aab819ab27e4f878d1ff0b7cf78050_451984_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/arolla-cocoon/featured-template_hu22aab819ab27e4f878d1ff0b7cf78050_451984_ea485187288cde4b679b149346aca832.png b/docs/resources/_gen/images/showcase/arolla-cocoon/featured-template_hu22aab819ab27e4f878d1ff0b7cf78050_451984_ea485187288cde4b679b149346aca832.png
index e36362747..e36362747 100644
--- a/resources/_gen/images/showcase/arolla-cocoon/featured-template_hu22aab819ab27e4f878d1ff0b7cf78050_451984_ea485187288cde4b679b149346aca832.png
+++ b/docs/resources/_gen/images/showcase/arolla-cocoon/featured-template_hu22aab819ab27e4f878d1ff0b7cf78050_451984_ea485187288cde4b679b149346aca832.png
Binary files differ
diff --git a/resources/_gen/images/showcase/fireship/featured_hu3bba74627b7e233d29d5aecb29c8d0f1_136959_1024x512_fill_catmullrom_top_2.png b/docs/resources/_gen/images/showcase/fireship/featured_hu3bba74627b7e233d29d5aecb29c8d0f1_136959_1024x512_fill_catmullrom_top_2.png
index 12d4beaad..12d4beaad 100644
--- a/resources/_gen/images/showcase/fireship/featured_hu3bba74627b7e233d29d5aecb29c8d0f1_136959_1024x512_fill_catmullrom_top_2.png
+++ b/docs/resources/_gen/images/showcase/fireship/featured_hu3bba74627b7e233d29d5aecb29c8d0f1_136959_1024x512_fill_catmullrom_top_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/fireship/featured_hu3bba74627b7e233d29d5aecb29c8d0f1_136959_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/showcase/fireship/featured_hu3bba74627b7e233d29d5aecb29c8d0f1_136959_640x0_resize_catmullrom_2.png
index 83f846867..83f846867 100644
--- a/resources/_gen/images/showcase/fireship/featured_hu3bba74627b7e233d29d5aecb29c8d0f1_136959_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/showcase/fireship/featured_hu3bba74627b7e233d29d5aecb29c8d0f1_136959_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/fireship/featured_hu3bba74627b7e233d29d5aecb29c8d0f1_136959_9bf5371384e80c9f59e1f5e018440c34.png b/docs/resources/_gen/images/showcase/fireship/featured_hu3bba74627b7e233d29d5aecb29c8d0f1_136959_9bf5371384e80c9f59e1f5e018440c34.png
index d3a17e58c..d3a17e58c 100644
--- a/resources/_gen/images/showcase/fireship/featured_hu3bba74627b7e233d29d5aecb29c8d0f1_136959_9bf5371384e80c9f59e1f5e018440c34.png
+++ b/docs/resources/_gen/images/showcase/fireship/featured_hu3bba74627b7e233d29d5aecb29c8d0f1_136959_9bf5371384e80c9f59e1f5e018440c34.png
Binary files differ
diff --git a/resources/_gen/images/showcase/flesland-flis/featured_hue4fd3c0e7519777bd75019750a0f5391_309284_1024x512_fill_catmullrom_top_2.png b/docs/resources/_gen/images/showcase/flesland-flis/featured_hue4fd3c0e7519777bd75019750a0f5391_309284_1024x512_fill_catmullrom_top_2.png
index 755f765ac..755f765ac 100644
--- a/resources/_gen/images/showcase/flesland-flis/featured_hue4fd3c0e7519777bd75019750a0f5391_309284_1024x512_fill_catmullrom_top_2.png
+++ b/docs/resources/_gen/images/showcase/flesland-flis/featured_hue4fd3c0e7519777bd75019750a0f5391_309284_1024x512_fill_catmullrom_top_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/flesland-flis/featured_hue4fd3c0e7519777bd75019750a0f5391_309284_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/showcase/flesland-flis/featured_hue4fd3c0e7519777bd75019750a0f5391_309284_640x0_resize_catmullrom_2.png
index 950b2e08b..950b2e08b 100644
--- a/resources/_gen/images/showcase/flesland-flis/featured_hue4fd3c0e7519777bd75019750a0f5391_309284_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/showcase/flesland-flis/featured_hue4fd3c0e7519777bd75019750a0f5391_309284_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/flesland-flis/featured_hue4fd3c0e7519777bd75019750a0f5391_309284_f66ed2dc2e475b0cb21d76296890c5a2.png b/docs/resources/_gen/images/showcase/flesland-flis/featured_hue4fd3c0e7519777bd75019750a0f5391_309284_f66ed2dc2e475b0cb21d76296890c5a2.png
index 3056bc376..3056bc376 100644
--- a/resources/_gen/images/showcase/flesland-flis/featured_hue4fd3c0e7519777bd75019750a0f5391_309284_f66ed2dc2e475b0cb21d76296890c5a2.png
+++ b/docs/resources/_gen/images/showcase/flesland-flis/featured_hue4fd3c0e7519777bd75019750a0f5391_309284_f66ed2dc2e475b0cb21d76296890c5a2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/forestry/featured_hu77de7d99834fa13b854b7fc62e2912a7_227009_1024x512_fill_catmullrom_top_2.png b/docs/resources/_gen/images/showcase/forestry/featured_hu77de7d99834fa13b854b7fc62e2912a7_227009_1024x512_fill_catmullrom_top_2.png
index 9a88db50a..9a88db50a 100644
--- a/resources/_gen/images/showcase/forestry/featured_hu77de7d99834fa13b854b7fc62e2912a7_227009_1024x512_fill_catmullrom_top_2.png
+++ b/docs/resources/_gen/images/showcase/forestry/featured_hu77de7d99834fa13b854b7fc62e2912a7_227009_1024x512_fill_catmullrom_top_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/forestry/featured_hu77de7d99834fa13b854b7fc62e2912a7_227009_192a300d3ccaa4371c674791fb50a62c.png b/docs/resources/_gen/images/showcase/forestry/featured_hu77de7d99834fa13b854b7fc62e2912a7_227009_192a300d3ccaa4371c674791fb50a62c.png
index a25b83ef2..a25b83ef2 100644
--- a/resources/_gen/images/showcase/forestry/featured_hu77de7d99834fa13b854b7fc62e2912a7_227009_192a300d3ccaa4371c674791fb50a62c.png
+++ b/docs/resources/_gen/images/showcase/forestry/featured_hu77de7d99834fa13b854b7fc62e2912a7_227009_192a300d3ccaa4371c674791fb50a62c.png
Binary files differ
diff --git a/resources/_gen/images/showcase/forestry/featured_hu77de7d99834fa13b854b7fc62e2912a7_227009_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/showcase/forestry/featured_hu77de7d99834fa13b854b7fc62e2912a7_227009_640x0_resize_catmullrom_2.png
index 7c98b0459..7c98b0459 100644
--- a/resources/_gen/images/showcase/forestry/featured_hu77de7d99834fa13b854b7fc62e2912a7_227009_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/showcase/forestry/featured_hu77de7d99834fa13b854b7fc62e2912a7_227009_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/hartwell-insurance/featured_hu642e730c6f819b15fc6ebbaa25b0243f_446603_1024x512_fill_catmullrom_top_2.png b/docs/resources/_gen/images/showcase/hartwell-insurance/featured_hu642e730c6f819b15fc6ebbaa25b0243f_446603_1024x512_fill_catmullrom_top_2.png
index 020a3f7fa..020a3f7fa 100644
--- a/resources/_gen/images/showcase/hartwell-insurance/featured_hu642e730c6f819b15fc6ebbaa25b0243f_446603_1024x512_fill_catmullrom_top_2.png
+++ b/docs/resources/_gen/images/showcase/hartwell-insurance/featured_hu642e730c6f819b15fc6ebbaa25b0243f_446603_1024x512_fill_catmullrom_top_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/hartwell-insurance/featured_hu642e730c6f819b15fc6ebbaa25b0243f_446603_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/showcase/hartwell-insurance/featured_hu642e730c6f819b15fc6ebbaa25b0243f_446603_640x0_resize_catmullrom_2.png
index d18131eb5..d18131eb5 100644
--- a/resources/_gen/images/showcase/hartwell-insurance/featured_hu642e730c6f819b15fc6ebbaa25b0243f_446603_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/showcase/hartwell-insurance/featured_hu642e730c6f819b15fc6ebbaa25b0243f_446603_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/hartwell-insurance/featured_hu642e730c6f819b15fc6ebbaa25b0243f_446603_a6f43693b7589a8d91c844654967eb51.png b/docs/resources/_gen/images/showcase/hartwell-insurance/featured_hu642e730c6f819b15fc6ebbaa25b0243f_446603_a6f43693b7589a8d91c844654967eb51.png
index 319316d00..319316d00 100644
--- a/resources/_gen/images/showcase/hartwell-insurance/featured_hu642e730c6f819b15fc6ebbaa25b0243f_446603_a6f43693b7589a8d91c844654967eb51.png
+++ b/docs/resources/_gen/images/showcase/hartwell-insurance/featured_hu642e730c6f819b15fc6ebbaa25b0243f_446603_a6f43693b7589a8d91c844654967eb51.png
Binary files differ
diff --git a/resources/_gen/images/showcase/letsencrypt/featured_hu51cfa254cfc1fb105704d2cdd6ae4737_147459_1024x512_fill_catmullrom_top_2.png b/docs/resources/_gen/images/showcase/letsencrypt/featured_hu51cfa254cfc1fb105704d2cdd6ae4737_147459_1024x512_fill_catmullrom_top_2.png
index 5df68ea1f..5df68ea1f 100644
--- a/resources/_gen/images/showcase/letsencrypt/featured_hu51cfa254cfc1fb105704d2cdd6ae4737_147459_1024x512_fill_catmullrom_top_2.png
+++ b/docs/resources/_gen/images/showcase/letsencrypt/featured_hu51cfa254cfc1fb105704d2cdd6ae4737_147459_1024x512_fill_catmullrom_top_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/letsencrypt/featured_hu51cfa254cfc1fb105704d2cdd6ae4737_147459_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/showcase/letsencrypt/featured_hu51cfa254cfc1fb105704d2cdd6ae4737_147459_640x0_resize_catmullrom_2.png
index 7589afd5b..7589afd5b 100644
--- a/resources/_gen/images/showcase/letsencrypt/featured_hu51cfa254cfc1fb105704d2cdd6ae4737_147459_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/showcase/letsencrypt/featured_hu51cfa254cfc1fb105704d2cdd6ae4737_147459_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/letsencrypt/featured_hu51cfa254cfc1fb105704d2cdd6ae4737_147459_825bc0f79626434a7ab711238e84984a.png b/docs/resources/_gen/images/showcase/letsencrypt/featured_hu51cfa254cfc1fb105704d2cdd6ae4737_147459_825bc0f79626434a7ab711238e84984a.png
index 9e531de4b..9e531de4b 100644
--- a/resources/_gen/images/showcase/letsencrypt/featured_hu51cfa254cfc1fb105704d2cdd6ae4737_147459_825bc0f79626434a7ab711238e84984a.png
+++ b/docs/resources/_gen/images/showcase/letsencrypt/featured_hu51cfa254cfc1fb105704d2cdd6ae4737_147459_825bc0f79626434a7ab711238e84984a.png
Binary files differ
diff --git a/resources/_gen/images/showcase/linode/featured_hu6acc14b2375e47c4c764fef09fdb54c0_126664_1024x512_fill_catmullrom_top_2.png b/docs/resources/_gen/images/showcase/linode/featured_hu6acc14b2375e47c4c764fef09fdb54c0_126664_1024x512_fill_catmullrom_top_2.png
index 3f5b94403..3f5b94403 100644
--- a/resources/_gen/images/showcase/linode/featured_hu6acc14b2375e47c4c764fef09fdb54c0_126664_1024x512_fill_catmullrom_top_2.png
+++ b/docs/resources/_gen/images/showcase/linode/featured_hu6acc14b2375e47c4c764fef09fdb54c0_126664_1024x512_fill_catmullrom_top_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/linode/featured_hu6acc14b2375e47c4c764fef09fdb54c0_126664_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/showcase/linode/featured_hu6acc14b2375e47c4c764fef09fdb54c0_126664_640x0_resize_catmullrom_2.png
index b2d501efb..b2d501efb 100644
--- a/resources/_gen/images/showcase/linode/featured_hu6acc14b2375e47c4c764fef09fdb54c0_126664_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/showcase/linode/featured_hu6acc14b2375e47c4c764fef09fdb54c0_126664_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/linode/featured_hu6acc14b2375e47c4c764fef09fdb54c0_126664_97b33e8221e700cd517d4ce317c69e48.png b/docs/resources/_gen/images/showcase/linode/featured_hu6acc14b2375e47c4c764fef09fdb54c0_126664_97b33e8221e700cd517d4ce317c69e48.png
index 9f8b5a918..9f8b5a918 100644
--- a/resources/_gen/images/showcase/linode/featured_hu6acc14b2375e47c4c764fef09fdb54c0_126664_97b33e8221e700cd517d4ce317c69e48.png
+++ b/docs/resources/_gen/images/showcase/linode/featured_hu6acc14b2375e47c4c764fef09fdb54c0_126664_97b33e8221e700cd517d4ce317c69e48.png
Binary files differ
diff --git a/resources/_gen/images/showcase/over/featured-over_hu778fbd1f621ca5db45e30107849dc7c9_234973_1024x512_fill_catmullrom_top_2.png b/docs/resources/_gen/images/showcase/over/featured-over_hu778fbd1f621ca5db45e30107849dc7c9_234973_1024x512_fill_catmullrom_top_2.png
index 170449589..170449589 100644
--- a/resources/_gen/images/showcase/over/featured-over_hu778fbd1f621ca5db45e30107849dc7c9_234973_1024x512_fill_catmullrom_top_2.png
+++ b/docs/resources/_gen/images/showcase/over/featured-over_hu778fbd1f621ca5db45e30107849dc7c9_234973_1024x512_fill_catmullrom_top_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/over/featured-over_hu778fbd1f621ca5db45e30107849dc7c9_234973_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/showcase/over/featured-over_hu778fbd1f621ca5db45e30107849dc7c9_234973_640x0_resize_catmullrom_2.png
index 018a382ce..018a382ce 100644
--- a/resources/_gen/images/showcase/over/featured-over_hu778fbd1f621ca5db45e30107849dc7c9_234973_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/showcase/over/featured-over_hu778fbd1f621ca5db45e30107849dc7c9_234973_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/over/featured-over_hu778fbd1f621ca5db45e30107849dc7c9_234973_fea71f0b8a2baebaf03af6e3be6229bb.png b/docs/resources/_gen/images/showcase/over/featured-over_hu778fbd1f621ca5db45e30107849dc7c9_234973_fea71f0b8a2baebaf03af6e3be6229bb.png
index 7a6cd71b3..7a6cd71b3 100644
--- a/resources/_gen/images/showcase/over/featured-over_hu778fbd1f621ca5db45e30107849dc7c9_234973_fea71f0b8a2baebaf03af6e3be6229bb.png
+++ b/docs/resources/_gen/images/showcase/over/featured-over_hu778fbd1f621ca5db45e30107849dc7c9_234973_fea71f0b8a2baebaf03af6e3be6229bb.png
Binary files differ
diff --git a/resources/_gen/images/showcase/pace-revenue-management/featured_hu143b6afebcd8780a08aa0a9f8e95dd02_298908_1024x512_fill_catmullrom_top_2.png b/docs/resources/_gen/images/showcase/pace-revenue-management/featured_hu143b6afebcd8780a08aa0a9f8e95dd02_298908_1024x512_fill_catmullrom_top_2.png
index c295aafad..c295aafad 100644
--- a/resources/_gen/images/showcase/pace-revenue-management/featured_hu143b6afebcd8780a08aa0a9f8e95dd02_298908_1024x512_fill_catmullrom_top_2.png
+++ b/docs/resources/_gen/images/showcase/pace-revenue-management/featured_hu143b6afebcd8780a08aa0a9f8e95dd02_298908_1024x512_fill_catmullrom_top_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/pace-revenue-management/featured_hu143b6afebcd8780a08aa0a9f8e95dd02_298908_7e3f008d047fb3522bf02df4e9229522.png b/docs/resources/_gen/images/showcase/pace-revenue-management/featured_hu143b6afebcd8780a08aa0a9f8e95dd02_298908_7e3f008d047fb3522bf02df4e9229522.png
index 8d1c41943..8d1c41943 100644
--- a/resources/_gen/images/showcase/pace-revenue-management/featured_hu143b6afebcd8780a08aa0a9f8e95dd02_298908_7e3f008d047fb3522bf02df4e9229522.png
+++ b/docs/resources/_gen/images/showcase/pace-revenue-management/featured_hu143b6afebcd8780a08aa0a9f8e95dd02_298908_7e3f008d047fb3522bf02df4e9229522.png
Binary files differ
diff --git a/resources/_gen/images/showcase/pharmaseal/featured-pharmaseal_hu8b2836502d9d0176aaacb83c98b8f063_809599_1024x512_fill_catmullrom_top_2.png b/docs/resources/_gen/images/showcase/pharmaseal/featured-pharmaseal_hu8b2836502d9d0176aaacb83c98b8f063_809599_1024x512_fill_catmullrom_top_2.png
index 6aea1bbf7..6aea1bbf7 100644
--- a/resources/_gen/images/showcase/pharmaseal/featured-pharmaseal_hu8b2836502d9d0176aaacb83c98b8f063_809599_1024x512_fill_catmullrom_top_2.png
+++ b/docs/resources/_gen/images/showcase/pharmaseal/featured-pharmaseal_hu8b2836502d9d0176aaacb83c98b8f063_809599_1024x512_fill_catmullrom_top_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/pharmaseal/featured-pharmaseal_hu8b2836502d9d0176aaacb83c98b8f063_809599_5d0cd50b49fef5d99b816cd049191f5e.png b/docs/resources/_gen/images/showcase/pharmaseal/featured-pharmaseal_hu8b2836502d9d0176aaacb83c98b8f063_809599_5d0cd50b49fef5d99b816cd049191f5e.png
index 68e18af37..68e18af37 100644
--- a/resources/_gen/images/showcase/pharmaseal/featured-pharmaseal_hu8b2836502d9d0176aaacb83c98b8f063_809599_5d0cd50b49fef5d99b816cd049191f5e.png
+++ b/docs/resources/_gen/images/showcase/pharmaseal/featured-pharmaseal_hu8b2836502d9d0176aaacb83c98b8f063_809599_5d0cd50b49fef5d99b816cd049191f5e.png
Binary files differ
diff --git a/resources/_gen/images/showcase/pharmaseal/featured-pharmaseal_hu8b2836502d9d0176aaacb83c98b8f063_809599_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/showcase/pharmaseal/featured-pharmaseal_hu8b2836502d9d0176aaacb83c98b8f063_809599_640x0_resize_catmullrom_2.png
index 41f3e92e8..41f3e92e8 100644
--- a/resources/_gen/images/showcase/pharmaseal/featured-pharmaseal_hu8b2836502d9d0176aaacb83c98b8f063_809599_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/showcase/pharmaseal/featured-pharmaseal_hu8b2836502d9d0176aaacb83c98b8f063_809599_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/quiply-employee-communications-app/featured_hua0e0d1ed0f0bc15921e78476b0c86c95_631206_1024x512_fill_catmullrom_top_2.png b/docs/resources/_gen/images/showcase/quiply-employee-communications-app/featured_hua0e0d1ed0f0bc15921e78476b0c86c95_631206_1024x512_fill_catmullrom_top_2.png
index 4afe5049c..4afe5049c 100644
--- a/resources/_gen/images/showcase/quiply-employee-communications-app/featured_hua0e0d1ed0f0bc15921e78476b0c86c95_631206_1024x512_fill_catmullrom_top_2.png
+++ b/docs/resources/_gen/images/showcase/quiply-employee-communications-app/featured_hua0e0d1ed0f0bc15921e78476b0c86c95_631206_1024x512_fill_catmullrom_top_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/quiply-employee-communications-app/featured_hua0e0d1ed0f0bc15921e78476b0c86c95_631206_3b6053b86d6afebe8262ece1955ed6cf.png b/docs/resources/_gen/images/showcase/quiply-employee-communications-app/featured_hua0e0d1ed0f0bc15921e78476b0c86c95_631206_3b6053b86d6afebe8262ece1955ed6cf.png
index e9e149400..e9e149400 100644
--- a/resources/_gen/images/showcase/quiply-employee-communications-app/featured_hua0e0d1ed0f0bc15921e78476b0c86c95_631206_3b6053b86d6afebe8262ece1955ed6cf.png
+++ b/docs/resources/_gen/images/showcase/quiply-employee-communications-app/featured_hua0e0d1ed0f0bc15921e78476b0c86c95_631206_3b6053b86d6afebe8262ece1955ed6cf.png
Binary files differ
diff --git a/resources/_gen/images/showcase/quiply-employee-communications-app/featured_hua0e0d1ed0f0bc15921e78476b0c86c95_631206_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/showcase/quiply-employee-communications-app/featured_hua0e0d1ed0f0bc15921e78476b0c86c95_631206_640x0_resize_catmullrom_2.png
index d8c6222d1..d8c6222d1 100644
--- a/resources/_gen/images/showcase/quiply-employee-communications-app/featured_hua0e0d1ed0f0bc15921e78476b0c86c95_631206_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/showcase/quiply-employee-communications-app/featured_hua0e0d1ed0f0bc15921e78476b0c86c95_631206_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/stackimpact/featured_hu863cdba7b6e18bb95f64289a25912f5c_153794_1024x512_fill_catmullrom_top_2.png b/docs/resources/_gen/images/showcase/stackimpact/featured_hu863cdba7b6e18bb95f64289a25912f5c_153794_1024x512_fill_catmullrom_top_2.png
index 4041d28df..4041d28df 100644
--- a/resources/_gen/images/showcase/stackimpact/featured_hu863cdba7b6e18bb95f64289a25912f5c_153794_1024x512_fill_catmullrom_top_2.png
+++ b/docs/resources/_gen/images/showcase/stackimpact/featured_hu863cdba7b6e18bb95f64289a25912f5c_153794_1024x512_fill_catmullrom_top_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/stackimpact/featured_hu863cdba7b6e18bb95f64289a25912f5c_153794_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/showcase/stackimpact/featured_hu863cdba7b6e18bb95f64289a25912f5c_153794_640x0_resize_catmullrom_2.png
index 7dbd463bb..7dbd463bb 100644
--- a/resources/_gen/images/showcase/stackimpact/featured_hu863cdba7b6e18bb95f64289a25912f5c_153794_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/showcase/stackimpact/featured_hu863cdba7b6e18bb95f64289a25912f5c_153794_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/stackimpact/featured_hu863cdba7b6e18bb95f64289a25912f5c_153794_671a5c232ffa27a2cf198d2c39f253eb.png b/docs/resources/_gen/images/showcase/stackimpact/featured_hu863cdba7b6e18bb95f64289a25912f5c_153794_671a5c232ffa27a2cf198d2c39f253eb.png
index d27a44e98..d27a44e98 100644
--- a/resources/_gen/images/showcase/stackimpact/featured_hu863cdba7b6e18bb95f64289a25912f5c_153794_671a5c232ffa27a2cf198d2c39f253eb.png
+++ b/docs/resources/_gen/images/showcase/stackimpact/featured_hu863cdba7b6e18bb95f64289a25912f5c_153794_671a5c232ffa27a2cf198d2c39f253eb.png
Binary files differ
diff --git a/resources/_gen/images/showcase/template/featured-template_hu2f0d11388f944348b232a431caeb965b_41270_0be9b039f9029effab69b9239e224cf7.png b/docs/resources/_gen/images/showcase/template/featured-template_hu2f0d11388f944348b232a431caeb965b_41270_0be9b039f9029effab69b9239e224cf7.png
index 0026f811e..0026f811e 100644
--- a/resources/_gen/images/showcase/template/featured-template_hu2f0d11388f944348b232a431caeb965b_41270_0be9b039f9029effab69b9239e224cf7.png
+++ b/docs/resources/_gen/images/showcase/template/featured-template_hu2f0d11388f944348b232a431caeb965b_41270_0be9b039f9029effab69b9239e224cf7.png
Binary files differ
diff --git a/resources/_gen/images/showcase/template/featured-template_hu2f0d11388f944348b232a431caeb965b_41270_1024x512_fill_catmullrom_top_2.png b/docs/resources/_gen/images/showcase/template/featured-template_hu2f0d11388f944348b232a431caeb965b_41270_1024x512_fill_catmullrom_top_2.png
index 10265e45e..10265e45e 100644
--- a/resources/_gen/images/showcase/template/featured-template_hu2f0d11388f944348b232a431caeb965b_41270_1024x512_fill_catmullrom_top_2.png
+++ b/docs/resources/_gen/images/showcase/template/featured-template_hu2f0d11388f944348b232a431caeb965b_41270_1024x512_fill_catmullrom_top_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/tomango/featured_hube95635899674dbe563e9fac9518dc5a_232791_1024x512_fill_catmullrom_top_2.png b/docs/resources/_gen/images/showcase/tomango/featured_hube95635899674dbe563e9fac9518dc5a_232791_1024x512_fill_catmullrom_top_2.png
index 1d64236b6..1d64236b6 100644
--- a/resources/_gen/images/showcase/tomango/featured_hube95635899674dbe563e9fac9518dc5a_232791_1024x512_fill_catmullrom_top_2.png
+++ b/docs/resources/_gen/images/showcase/tomango/featured_hube95635899674dbe563e9fac9518dc5a_232791_1024x512_fill_catmullrom_top_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/tomango/featured_hube95635899674dbe563e9fac9518dc5a_232791_640x0_resize_catmullrom_2.png b/docs/resources/_gen/images/showcase/tomango/featured_hube95635899674dbe563e9fac9518dc5a_232791_640x0_resize_catmullrom_2.png
index cd918da6a..cd918da6a 100644
--- a/resources/_gen/images/showcase/tomango/featured_hube95635899674dbe563e9fac9518dc5a_232791_640x0_resize_catmullrom_2.png
+++ b/docs/resources/_gen/images/showcase/tomango/featured_hube95635899674dbe563e9fac9518dc5a_232791_640x0_resize_catmullrom_2.png
Binary files differ
diff --git a/resources/_gen/images/showcase/tomango/featured_hube95635899674dbe563e9fac9518dc5a_232791_6dfd850dc877e20e2554751f779e5953.png b/docs/resources/_gen/images/showcase/tomango/featured_hube95635899674dbe563e9fac9518dc5a_232791_6dfd850dc877e20e2554751f779e5953.png
index 996b81750..996b81750 100644
--- a/resources/_gen/images/showcase/tomango/featured_hube95635899674dbe563e9fac9518dc5a_232791_6dfd850dc877e20e2554751f779e5953.png
+++ b/docs/resources/_gen/images/showcase/tomango/featured_hube95635899674dbe563e9fac9518dc5a_232791_6dfd850dc877e20e2554751f779e5953.png
Binary files differ
diff --git a/src/css/_chroma.css b/docs/src/css/_chroma.css
index 1ad06604b..1ad06604b 100644
--- a/src/css/_chroma.css
+++ b/docs/src/css/_chroma.css
diff --git a/src/package-lock.json b/docs/src/package-lock.json
index 48e341a09..48e341a09 100644
--- a/src/package-lock.json
+++ b/docs/src/package-lock.json
diff --git a/static/apple-touch-icon.png b/docs/static/apple-touch-icon.png
index 50e23ce1d..50e23ce1d 100644
--- a/static/apple-touch-icon.png
+++ b/docs/static/apple-touch-icon.png
Binary files differ
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.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
index 36693330b..36693330b 100644
--- a/static/favicon.ico
+++ b/docs/static/favicon.ico
Binary files differ
diff --git a/static/fonts/hugo.eot b/docs/static/fonts/hugo.eot
index b92f00f93..b92f00f93 100644
--- a/static/fonts/hugo.eot
+++ b/docs/static/fonts/hugo.eot
Binary files differ
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
index 962914d33..962914d33 100644
--- a/static/fonts/hugo.ttf
+++ b/docs/static/fonts/hugo.ttf
Binary files differ
diff --git a/static/fonts/hugo.woff b/docs/static/fonts/hugo.woff
index 4693fbe7f..4693fbe7f 100644
--- a/static/fonts/hugo.woff
+++ b/docs/static/fonts/hugo.woff
Binary files differ
diff --git a/static/images/blog/hugo-26-poster.png b/docs/static/images/blog/hugo-26-poster.png
index 827f1f7bb..827f1f7bb 100644
--- a/static/images/blog/hugo-26-poster.png
+++ b/docs/static/images/blog/hugo-26-poster.png
Binary files differ
diff --git a/static/images/blog/hugo-27-poster.png b/docs/static/images/blog/hugo-27-poster.png
index 69efa36bc..69efa36bc 100644
--- a/static/images/blog/hugo-27-poster.png
+++ b/docs/static/images/blog/hugo-27-poster.png
Binary files differ
diff --git a/static/images/blog/hugo-28-poster.png b/docs/static/images/blog/hugo-28-poster.png
index ae3d6ac16..ae3d6ac16 100644
--- a/static/images/blog/hugo-28-poster.png
+++ b/docs/static/images/blog/hugo-28-poster.png
Binary files differ
diff --git a/static/images/blog/hugo-29-poster.png b/docs/static/images/blog/hugo-29-poster.png
index dbe2d434f..dbe2d434f 100644
--- a/static/images/blog/hugo-29-poster.png
+++ b/docs/static/images/blog/hugo-29-poster.png
Binary files differ
diff --git a/static/images/blog/hugo-30-poster.png b/docs/static/images/blog/hugo-30-poster.png
index 214369e89..214369e89 100644
--- a/static/images/blog/hugo-30-poster.png
+++ b/docs/static/images/blog/hugo-30-poster.png
Binary files differ
diff --git a/static/images/blog/hugo-31-poster.png b/docs/static/images/blog/hugo-31-poster.png
index e11e53aa7..e11e53aa7 100644
--- a/static/images/blog/hugo-31-poster.png
+++ b/docs/static/images/blog/hugo-31-poster.png
Binary files differ
diff --git a/static/images/blog/hugo-32-poster.png b/docs/static/images/blog/hugo-32-poster.png
index f915247ad..f915247ad 100644
--- a/static/images/blog/hugo-32-poster.png
+++ b/docs/static/images/blog/hugo-32-poster.png
Binary files differ
diff --git a/static/images/blog/hugo-bug-poster.png b/docs/static/images/blog/hugo-bug-poster.png
index cd236682d..cd236682d 100644
--- a/static/images/blog/hugo-bug-poster.png
+++ b/docs/static/images/blog/hugo-bug-poster.png
Binary files differ
diff --git a/static/images/blog/hugo-http2-push.png b/docs/static/images/blog/hugo-http2-push.png
index 1ddfd4653..1ddfd4653 100644
--- a/static/images/blog/hugo-http2-push.png
+++ b/docs/static/images/blog/hugo-http2-push.png
Binary files differ
diff --git a/static/images/blog/sunset.jpg b/docs/static/images/blog/sunset.jpg
index 7d7307bed..7d7307bed 100644
--- a/static/images/blog/sunset.jpg
+++ b/docs/static/images/blog/sunset.jpg
Binary files differ
diff --git a/static/images/contribute/development/accept-cla.png b/docs/static/images/contribute/development/accept-cla.png
index 929fda6ab..929fda6ab 100644
--- a/static/images/contribute/development/accept-cla.png
+++ b/docs/static/images/contribute/development/accept-cla.png
Binary files differ
diff --git a/static/images/contribute/development/ci-errors.png b/docs/static/images/contribute/development/ci-errors.png
index 95cd290b6..95cd290b6 100644
--- a/static/images/contribute/development/ci-errors.png
+++ b/docs/static/images/contribute/development/ci-errors.png
Binary files differ
diff --git a/static/images/contribute/development/copy-remote-url.png b/docs/static/images/contribute/development/copy-remote-url.png
index 9006f4a48..9006f4a48 100644
--- a/static/images/contribute/development/copy-remote-url.png
+++ b/docs/static/images/contribute/development/copy-remote-url.png
Binary files differ
diff --git a/static/images/contribute/development/forking-a-repository.png b/docs/static/images/contribute/development/forking-a-repository.png
index ea132cab3..ea132cab3 100644
--- a/static/images/contribute/development/forking-a-repository.png
+++ b/docs/static/images/contribute/development/forking-a-repository.png
Binary files differ
diff --git a/static/images/contribute/development/open-pull-request.png b/docs/static/images/contribute/development/open-pull-request.png
index 63b504fb2..63b504fb2 100644
--- a/static/images/contribute/development/open-pull-request.png
+++ b/docs/static/images/contribute/development/open-pull-request.png
Binary files differ
diff --git a/static/images/gohugoio-card-1.png b/docs/static/images/gohugoio-card-1.png
index 09953aed9..09953aed9 100644
--- a/static/images/gohugoio-card-1.png
+++ b/docs/static/images/gohugoio-card-1.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-nanobox/hugo-server.png b/docs/static/images/hosting-and-deployment/deployment-with-nanobox/hugo-server.png
index b37b2c375..b37b2c375 100644
--- a/static/images/hosting-and-deployment/deployment-with-nanobox/hugo-server.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-nanobox/hugo-server.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-nanobox/hugo-with-nanobox.png b/docs/static/images/hosting-and-deployment/deployment-with-nanobox/hugo-with-nanobox.png
index 8b889b34b..8b889b34b 100644
--- a/static/images/hosting-and-deployment/deployment-with-nanobox/hugo-with-nanobox.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-nanobox/hugo-with-nanobox.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-nanobox/nanobox-deploy-dry-run.png b/docs/static/images/hosting-and-deployment/deployment-with-nanobox/nanobox-deploy-dry-run.png
index 55c438308..55c438308 100644
--- a/static/images/hosting-and-deployment/deployment-with-nanobox/nanobox-deploy-dry-run.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-nanobox/nanobox-deploy-dry-run.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-nanobox/nanobox-run.png b/docs/static/images/hosting-and-deployment/deployment-with-nanobox/nanobox-run.png
index 3432df8c9..3432df8c9 100644
--- a/static/images/hosting-and-deployment/deployment-with-nanobox/nanobox-run.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-nanobox/nanobox-run.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-wercker/adding-a-github-pages-step.png b/docs/static/images/hosting-and-deployment/deployment-with-wercker/adding-a-github-pages-step.png
index ff28a0661..ff28a0661 100644
--- a/static/images/hosting-and-deployment/deployment-with-wercker/adding-a-github-pages-step.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-wercker/adding-a-github-pages-step.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-wercker/adding-the-project-to-github.png b/docs/static/images/hosting-and-deployment/deployment-with-wercker/adding-the-project-to-github.png
index e1065bb00..e1065bb00 100644
--- a/static/images/hosting-and-deployment/deployment-with-wercker/adding-the-project-to-github.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-wercker/adding-the-project-to-github.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-wercker/and-we-ve-got-an-app.png b/docs/static/images/hosting-and-deployment/deployment-with-wercker/and-we-ve-got-an-app.png
index 7f8e10e70..7f8e10e70 100644
--- a/static/images/hosting-and-deployment/deployment-with-wercker/and-we-ve-got-an-app.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-wercker/and-we-ve-got-an-app.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-wercker/configure-the-deploy-step.png b/docs/static/images/hosting-and-deployment/deployment-with-wercker/configure-the-deploy-step.png
index 550ea1bf2..550ea1bf2 100644
--- a/static/images/hosting-and-deployment/deployment-with-wercker/configure-the-deploy-step.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-wercker/configure-the-deploy-step.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-wercker/creating-a-basic-hugo-site.png b/docs/static/images/hosting-and-deployment/deployment-with-wercker/creating-a-basic-hugo-site.png
index 78d238f88..78d238f88 100644
--- a/static/images/hosting-and-deployment/deployment-with-wercker/creating-a-basic-hugo-site.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-wercker/creating-a-basic-hugo-site.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-wercker/public-or-not.png b/docs/static/images/hosting-and-deployment/deployment-with-wercker/public-or-not.png
index 9d81a8ba4..9d81a8ba4 100644
--- a/static/images/hosting-and-deployment/deployment-with-wercker/public-or-not.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-wercker/public-or-not.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-wercker/using-hugo-build.png b/docs/static/images/hosting-and-deployment/deployment-with-wercker/using-hugo-build.png
index b0dbec94c..b0dbec94c 100644
--- a/static/images/hosting-and-deployment/deployment-with-wercker/using-hugo-build.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-wercker/using-hugo-build.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-wercker/wercker-access.png b/docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-access.png
index 6e89c0ef3..6e89c0ef3 100644
--- a/static/images/hosting-and-deployment/deployment-with-wercker/wercker-access.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-access.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-wercker/wercker-account-settings.png b/docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-account-settings.png
index 993a1d9e9..993a1d9e9 100644
--- a/static/images/hosting-and-deployment/deployment-with-wercker/wercker-account-settings.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-account-settings.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-wercker/wercker-add-app.png b/docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-add-app.png
index 94ccef518..94ccef518 100644
--- a/static/images/hosting-and-deployment/deployment-with-wercker/wercker-add-app.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-add-app.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-wercker/wercker-git-connections.png b/docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-git-connections.png
index d89c0cd8b..d89c0cd8b 100644
--- a/static/images/hosting-and-deployment/deployment-with-wercker/wercker-git-connections.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-git-connections.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-wercker/wercker-search.png b/docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-search.png
index d099cfd5c..d099cfd5c 100644
--- a/static/images/hosting-and-deployment/deployment-with-wercker/wercker-search.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-search.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-wercker/wercker-select-owner.png b/docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-select-owner.png
index 111308508..111308508 100644
--- a/static/images/hosting-and-deployment/deployment-with-wercker/wercker-select-owner.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-select-owner.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-wercker/wercker-select-repository.png b/docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-select-repository.png
index e8835f21a..e8835f21a 100644
--- a/static/images/hosting-and-deployment/deployment-with-wercker/wercker-select-repository.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-select-repository.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-wercker/wercker-sign-up-page.png b/docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-sign-up-page.png
index 28f469649..28f469649 100644
--- a/static/images/hosting-and-deployment/deployment-with-wercker/wercker-sign-up-page.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-sign-up-page.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-wercker/wercker-sign-up.png b/docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-sign-up.png
index f24996889..f24996889 100644
--- a/static/images/hosting-and-deployment/deployment-with-wercker/wercker-sign-up.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-wercker/wercker-sign-up.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/deployment-with-wercker/werckeryml.png b/docs/static/images/hosting-and-deployment/deployment-with-wercker/werckeryml.png
index be46e6136..be46e6136 100644
--- a/static/images/hosting-and-deployment/deployment-with-wercker/werckeryml.png
+++ b/docs/static/images/hosting-and-deployment/deployment-with-wercker/werckeryml.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/hosting-on-aws-amplify/amplify-build-settings.png b/docs/static/images/hosting-and-deployment/hosting-on-aws-amplify/amplify-build-settings.png
index a9a31ec11..a9a31ec11 100644
--- a/static/images/hosting-and-deployment/hosting-on-aws-amplify/amplify-build-settings.png
+++ b/docs/static/images/hosting-and-deployment/hosting-on-aws-amplify/amplify-build-settings.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/hosting-on-aws-amplify/amplify-connect-repo.gif b/docs/static/images/hosting-and-deployment/hosting-on-aws-amplify/amplify-connect-repo.gif
index 6c57cf3b2..6c57cf3b2 100644
--- a/static/images/hosting-and-deployment/hosting-on-aws-amplify/amplify-connect-repo.gif
+++ b/docs/static/images/hosting-and-deployment/hosting-on-aws-amplify/amplify-connect-repo.gif
Binary files differ
diff --git a/static/images/hosting-and-deployment/hosting-on-aws-amplify/amplify-gettingstarted.png b/docs/static/images/hosting-and-deployment/hosting-on-aws-amplify/amplify-gettingstarted.png
index 92505df52..92505df52 100644
--- a/static/images/hosting-and-deployment/hosting-on-aws-amplify/amplify-gettingstarted.png
+++ b/docs/static/images/hosting-and-deployment/hosting-on-aws-amplify/amplify-gettingstarted.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/hosting-on-bitbucket/bitbucket-blog-post.png b/docs/static/images/hosting-and-deployment/hosting-on-bitbucket/bitbucket-blog-post.png
index b78f6fd15..b78f6fd15 100644
--- a/static/images/hosting-and-deployment/hosting-on-bitbucket/bitbucket-blog-post.png
+++ b/docs/static/images/hosting-and-deployment/hosting-on-bitbucket/bitbucket-blog-post.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/hosting-on-bitbucket/bitbucket-create-repo.png b/docs/static/images/hosting-and-deployment/hosting-on-bitbucket/bitbucket-create-repo.png
index e97f13465..e97f13465 100644
--- a/static/images/hosting-and-deployment/hosting-on-bitbucket/bitbucket-create-repo.png
+++ b/docs/static/images/hosting-and-deployment/hosting-on-bitbucket/bitbucket-create-repo.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/hosting-on-keycdn/keycdn-pull-zone.png b/docs/static/images/hosting-and-deployment/hosting-on-keycdn/keycdn-pull-zone.png
index 3cfc61138..3cfc61138 100644
--- a/static/images/hosting-and-deployment/hosting-on-keycdn/keycdn-pull-zone.png
+++ b/docs/static/images/hosting-and-deployment/hosting-on-keycdn/keycdn-pull-zone.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/hosting-on-keycdn/secret-api-key.png b/docs/static/images/hosting-and-deployment/hosting-on-keycdn/secret-api-key.png
index 26ac44857..26ac44857 100644
--- a/static/images/hosting-and-deployment/hosting-on-keycdn/secret-api-key.png
+++ b/docs/static/images/hosting-and-deployment/hosting-on-keycdn/secret-api-key.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/hosting-on-keycdn/secret-zone-id.png b/docs/static/images/hosting-and-deployment/hosting-on-keycdn/secret-zone-id.png
index c0ef6c571..c0ef6c571 100644
--- a/static/images/hosting-and-deployment/hosting-on-keycdn/secret-zone-id.png
+++ b/docs/static/images/hosting-and-deployment/hosting-on-keycdn/secret-zone-id.png
Binary files differ
diff --git a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-add-new-site.jpg b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-add-new-site.jpg
index 17698d34a..17698d34a 100644
--- a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-add-new-site.jpg
+++ b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-add-new-site.jpg
Binary files differ
diff --git a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-authorize-added-permissions.jpg b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-authorize-added-permissions.jpg
index eaae924e4..eaae924e4 100644
--- a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-authorize-added-permissions.jpg
+++ b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-authorize-added-permissions.jpg
Binary files differ
diff --git a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-create-new-site-step-1.jpg b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-create-new-site-step-1.jpg
index 347477dd2..347477dd2 100644
--- a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-create-new-site-step-1.jpg
+++ b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-create-new-site-step-1.jpg
Binary files differ
diff --git a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-create-new-site-step-2.jpg b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-create-new-site-step-2.jpg
index 18bfd6fed..18bfd6fed 100644
--- a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-create-new-site-step-2.jpg
+++ b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-create-new-site-step-2.jpg
Binary files differ
diff --git a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-create-new-site-step-3.jpg b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-create-new-site-step-3.jpg
index 6f9b6477c..6f9b6477c 100644
--- a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-create-new-site-step-3.jpg
+++ b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-create-new-site-step-3.jpg
Binary files differ
diff --git a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-deploy-published.jpg b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-deploy-published.jpg
index ed5eaf3c8..ed5eaf3c8 100644
--- a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-deploy-published.jpg
+++ b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-deploy-published.jpg
Binary files differ
diff --git a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-deploying-site.gif b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-deploying-site.gif
index c1f27c236..c1f27c236 100644
--- a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-deploying-site.gif
+++ b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-deploying-site.gif
Binary files differ
diff --git a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-first-authorize.jpg b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-first-authorize.jpg
index 748122e89..748122e89 100644
--- a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-first-authorize.jpg
+++ b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-first-authorize.jpg
Binary files differ
diff --git a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-live-site.jpg b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-live-site.jpg
index 3edc49c43..3edc49c43 100644
--- a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-live-site.jpg
+++ b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-live-site.jpg
Binary files differ
diff --git a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-push-to-deploy.jpg b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-push-to-deploy.jpg
index f23626218..f23626218 100644
--- a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-push-to-deploy.jpg
+++ b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-push-to-deploy.jpg
Binary files differ
diff --git a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-signup.jpg b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-signup.jpg
index cd9a218b4..cd9a218b4 100644
--- a/static/images/hosting-and-deployment/hosting-on-netlify/netlify-signup.jpg
+++ b/docs/static/images/hosting-and-deployment/hosting-on-netlify/netlify-signup.jpg
Binary files differ
diff --git a/static/images/hugo-content-bundles.png b/docs/static/images/hugo-content-bundles.png
index 1706a29d6..1706a29d6 100644
--- a/static/images/hugo-content-bundles.png
+++ b/docs/static/images/hugo-content-bundles.png
Binary files differ
diff --git a/static/images/icon-custom-outputs.svg b/docs/static/images/icon-custom-outputs.svg
index ccf581f31..ccf581f31 100644
--- a/static/images/icon-custom-outputs.svg
+++ b/docs/static/images/icon-custom-outputs.svg
diff --git a/static/images/site-hierarchy.svg b/docs/static/images/site-hierarchy.svg
index 7d1a043e8..7d1a043e8 100644
--- a/static/images/site-hierarchy.svg
+++ b/docs/static/images/site-hierarchy.svg
diff --git a/static/img/hugo-logo-med.png b/docs/static/img/hugo-logo-med.png
index dcc141690..dcc141690 100644
--- a/static/img/hugo-logo-med.png
+++ b/docs/static/img/hugo-logo-med.png
Binary files differ
diff --git a/static/img/hugo-logo.png b/docs/static/img/hugo-logo.png
index a4f1321b0..a4f1321b0 100644
--- a/static/img/hugo-logo.png
+++ b/docs/static/img/hugo-logo.png
Binary files differ
diff --git a/static/img/hugo.png b/docs/static/img/hugo.png
index 48acf346c..48acf346c 100644
--- a/static/img/hugo.png
+++ b/docs/static/img/hugo.png
Binary files differ
diff --git a/static/img/hugoSM.png b/docs/static/img/hugoSM.png
index f64f43088..f64f43088 100644
--- a/static/img/hugoSM.png
+++ b/docs/static/img/hugoSM.png
Binary files differ
diff --git a/static/share/hugo-tall.png b/docs/static/share/hugo-tall.png
index 001ce5eb3..001ce5eb3 100644
--- a/static/share/hugo-tall.png
+++ b/docs/static/share/hugo-tall.png
Binary files differ
diff --git a/static/share/made-with-hugo-dark.png b/docs/static/share/made-with-hugo-dark.png
index c6cadf283..c6cadf283 100644
--- a/static/share/made-with-hugo-dark.png
+++ b/docs/static/share/made-with-hugo-dark.png
Binary files differ
diff --git a/static/share/made-with-hugo-long-dark.png b/docs/static/share/made-with-hugo-long-dark.png
index 1e49995fb..1e49995fb 100644
--- a/static/share/made-with-hugo-long-dark.png
+++ b/docs/static/share/made-with-hugo-long-dark.png
Binary files differ
diff --git a/static/share/made-with-hugo-long.png b/docs/static/share/made-with-hugo-long.png
index c5df534cf..c5df534cf 100644
--- a/static/share/made-with-hugo-long.png
+++ b/docs/static/share/made-with-hugo-long.png
Binary files differ
diff --git a/static/share/made-with-hugo.png b/docs/static/share/made-with-hugo.png
index 52dfd19e5..52dfd19e5 100644
--- a/static/share/made-with-hugo.png
+++ b/docs/static/share/made-with-hugo.png
Binary files differ
diff --git a/static/share/powered-by-hugo-dark.png b/docs/static/share/powered-by-hugo-dark.png
index a8e2ebc80..a8e2ebc80 100644
--- a/static/share/powered-by-hugo-dark.png
+++ b/docs/static/share/powered-by-hugo-dark.png
Binary files differ
diff --git a/static/share/powered-by-hugo-long-dark.png b/docs/static/share/powered-by-hugo-long-dark.png
index 1b760b1bf..1b760b1bf 100644
--- a/static/share/powered-by-hugo-long-dark.png
+++ b/docs/static/share/powered-by-hugo-long-dark.png
Binary files differ
diff --git a/static/share/powered-by-hugo-long.png b/docs/static/share/powered-by-hugo-long.png
index 37131359d..37131359d 100644
--- a/static/share/powered-by-hugo-long.png
+++ b/docs/static/share/powered-by-hugo-long.png
Binary files differ
diff --git a/static/share/powered-by-hugo.png b/docs/static/share/powered-by-hugo.png
index 27ff099d5..27ff099d5 100644
--- a/static/share/powered-by-hugo.png
+++ b/docs/static/share/powered-by-hugo.png
Binary files differ
diff --git a/themes/gohugoioTheme/.gitignore b/docs/themes/gohugoioTheme/.gitignore
index 384da9681..384da9681 100644
--- a/themes/gohugoioTheme/.gitignore
+++ b/docs/themes/gohugoioTheme/.gitignore
diff --git a/themes/gohugoioTheme/README.md b/docs/themes/gohugoioTheme/README.md
index ac8e26927..ac8e26927 100644
--- a/themes/gohugoioTheme/README.md
+++ b/docs/themes/gohugoioTheme/README.md
diff --git a/themes/gohugoioTheme/archetypes/showcase.md b/docs/themes/gohugoioTheme/archetypes/showcase.md
index d852bb1ca..d852bb1ca 100644
--- a/themes/gohugoioTheme/archetypes/showcase.md
+++ b/docs/themes/gohugoioTheme/archetypes/showcase.md
diff --git a/themes/gohugoioTheme/assets/css/_algolia.css b/docs/themes/gohugoioTheme/assets/css/_algolia.css
index 0122f9758..0122f9758 100644
--- a/themes/gohugoioTheme/assets/css/_algolia.css
+++ b/docs/themes/gohugoioTheme/assets/css/_algolia.css
diff --git a/themes/gohugoioTheme/assets/css/_anchorforid.css b/docs/themes/gohugoioTheme/assets/css/_anchorforid.css
index ab5942854..ab5942854 100644
--- a/themes/gohugoioTheme/assets/css/_anchorforid.css
+++ b/docs/themes/gohugoioTheme/assets/css/_anchorforid.css
diff --git a/themes/gohugoioTheme/assets/css/_animation.css b/docs/themes/gohugoioTheme/assets/css/_animation.css
index 997931ac4..997931ac4 100644
--- a/themes/gohugoioTheme/assets/css/_animation.css
+++ b/docs/themes/gohugoioTheme/assets/css/_animation.css
diff --git a/themes/gohugoioTheme/assets/css/_carousel.css b/docs/themes/gohugoioTheme/assets/css/_carousel.css
index 11fae8702..11fae8702 100644
--- a/themes/gohugoioTheme/assets/css/_carousel.css
+++ b/docs/themes/gohugoioTheme/assets/css/_carousel.css
diff --git a/themes/gohugoioTheme/assets/css/_chroma.css b/docs/themes/gohugoioTheme/assets/css/_chroma.css
index d00ea65e6..d00ea65e6 100644
--- a/themes/gohugoioTheme/assets/css/_chroma.css
+++ b/docs/themes/gohugoioTheme/assets/css/_chroma.css
diff --git a/themes/gohugoioTheme/assets/css/_code.css b/docs/themes/gohugoioTheme/assets/css/_code.css
index 2fb402fcf..2fb402fcf 100644
--- a/themes/gohugoioTheme/assets/css/_code.css
+++ b/docs/themes/gohugoioTheme/assets/css/_code.css
diff --git a/themes/gohugoioTheme/assets/css/_color-scheme.css b/docs/themes/gohugoioTheme/assets/css/_color-scheme.css
index 1d61a7725..1d61a7725 100644
--- a/themes/gohugoioTheme/assets/css/_color-scheme.css
+++ b/docs/themes/gohugoioTheme/assets/css/_color-scheme.css
diff --git a/themes/gohugoioTheme/assets/css/_columns.css b/docs/themes/gohugoioTheme/assets/css/_columns.css
index e1e938c74..e1e938c74 100644
--- a/themes/gohugoioTheme/assets/css/_columns.css
+++ b/docs/themes/gohugoioTheme/assets/css/_columns.css
diff --git a/themes/gohugoioTheme/assets/css/_content-tables.css b/docs/themes/gohugoioTheme/assets/css/_content-tables.css
index 4e092e8bf..4e092e8bf 100644
--- a/themes/gohugoioTheme/assets/css/_content-tables.css
+++ b/docs/themes/gohugoioTheme/assets/css/_content-tables.css
diff --git a/themes/gohugoioTheme/assets/css/_content.css b/docs/themes/gohugoioTheme/assets/css/_content.css
index 9c8a8a14d..9c8a8a14d 100644
--- a/themes/gohugoioTheme/assets/css/_content.css
+++ b/docs/themes/gohugoioTheme/assets/css/_content.css
diff --git a/themes/gohugoioTheme/assets/css/_definition-lists.css b/docs/themes/gohugoioTheme/assets/css/_definition-lists.css
index e28f67d4b..e28f67d4b 100644
--- a/themes/gohugoioTheme/assets/css/_definition-lists.css
+++ b/docs/themes/gohugoioTheme/assets/css/_definition-lists.css
diff --git a/themes/gohugoioTheme/assets/css/_documentation-styles.css b/docs/themes/gohugoioTheme/assets/css/_documentation-styles.css
index 0ea8e9b72..0ea8e9b72 100644
--- a/themes/gohugoioTheme/assets/css/_documentation-styles.css
+++ b/docs/themes/gohugoioTheme/assets/css/_documentation-styles.css
diff --git a/themes/gohugoioTheme/assets/css/_fluid-type.css b/docs/themes/gohugoioTheme/assets/css/_fluid-type.css
index da9f04c81..da9f04c81 100644
--- a/themes/gohugoioTheme/assets/css/_fluid-type.css
+++ b/docs/themes/gohugoioTheme/assets/css/_fluid-type.css
diff --git a/themes/gohugoioTheme/assets/css/_font-family.css b/docs/themes/gohugoioTheme/assets/css/_font-family.css
index 9b451cf1c..9b451cf1c 100644
--- a/themes/gohugoioTheme/assets/css/_font-family.css
+++ b/docs/themes/gohugoioTheme/assets/css/_font-family.css
diff --git a/themes/gohugoioTheme/assets/css/_hljs.css b/docs/themes/gohugoioTheme/assets/css/_hljs.css
index c49107655..c49107655 100644
--- a/themes/gohugoioTheme/assets/css/_hljs.css
+++ b/docs/themes/gohugoioTheme/assets/css/_hljs.css
diff --git a/themes/gohugoioTheme/assets/css/_hugo-internal-template-styling.css b/docs/themes/gohugoioTheme/assets/css/_hugo-internal-template-styling.css
index 0b1df9610..0b1df9610 100644
--- a/themes/gohugoioTheme/assets/css/_hugo-internal-template-styling.css
+++ b/docs/themes/gohugoioTheme/assets/css/_hugo-internal-template-styling.css
diff --git a/themes/gohugoioTheme/assets/css/_no-js.css b/docs/themes/gohugoioTheme/assets/css/_no-js.css
index 7991450fe..7991450fe 100644
--- a/themes/gohugoioTheme/assets/css/_no-js.css
+++ b/docs/themes/gohugoioTheme/assets/css/_no-js.css
diff --git a/themes/gohugoioTheme/assets/css/_social-icons.css b/docs/themes/gohugoioTheme/assets/css/_social-icons.css
index 04ea11ec5..04ea11ec5 100644
--- a/themes/gohugoioTheme/assets/css/_social-icons.css
+++ b/docs/themes/gohugoioTheme/assets/css/_social-icons.css
diff --git a/themes/gohugoioTheme/assets/css/_stickyheader.css b/docs/themes/gohugoioTheme/assets/css/_stickyheader.css
index 7759bed96..7759bed96 100644
--- a/themes/gohugoioTheme/assets/css/_stickyheader.css
+++ b/docs/themes/gohugoioTheme/assets/css/_stickyheader.css
diff --git a/themes/gohugoioTheme/assets/css/_svg.css b/docs/themes/gohugoioTheme/assets/css/_svg.css
index 299a4a963..299a4a963 100644
--- a/themes/gohugoioTheme/assets/css/_svg.css
+++ b/docs/themes/gohugoioTheme/assets/css/_svg.css
diff --git a/themes/gohugoioTheme/assets/css/_tabs.css b/docs/themes/gohugoioTheme/assets/css/_tabs.css
index 6e0022cc9..6e0022cc9 100644
--- a/themes/gohugoioTheme/assets/css/_tabs.css
+++ b/docs/themes/gohugoioTheme/assets/css/_tabs.css
diff --git a/themes/gohugoioTheme/assets/css/_tachyons.css b/docs/themes/gohugoioTheme/assets/css/_tachyons.css
index d697c4d85..d697c4d85 100644
--- a/themes/gohugoioTheme/assets/css/_tachyons.css
+++ b/docs/themes/gohugoioTheme/assets/css/_tachyons.css
diff --git a/themes/gohugoioTheme/assets/css/_variables.css b/docs/themes/gohugoioTheme/assets/css/_variables.css
index 8701b1530..8701b1530 100644
--- a/themes/gohugoioTheme/assets/css/_variables.css
+++ b/docs/themes/gohugoioTheme/assets/css/_variables.css
diff --git a/themes/gohugoioTheme/assets/css/main.css b/docs/themes/gohugoioTheme/assets/css/main.css
index 1cd15fb10..1cd15fb10 100644
--- a/themes/gohugoioTheme/assets/css/main.css
+++ b/docs/themes/gohugoioTheme/assets/css/main.css
diff --git a/themes/gohugoioTheme/assets/index.js b/docs/themes/gohugoioTheme/assets/index.js
index 5a3dbc8c1..5a3dbc8c1 100644
--- a/themes/gohugoioTheme/assets/index.js
+++ b/docs/themes/gohugoioTheme/assets/index.js
diff --git a/themes/gohugoioTheme/assets/js/anchorforid.js b/docs/themes/gohugoioTheme/assets/js/anchorforid.js
index cb0855d52..cb0855d52 100644
--- a/themes/gohugoioTheme/assets/js/anchorforid.js
+++ b/docs/themes/gohugoioTheme/assets/js/anchorforid.js
diff --git a/themes/gohugoioTheme/assets/js/clipboardjs.js b/docs/themes/gohugoioTheme/assets/js/clipboardjs.js
index ffae31c7f..ffae31c7f 100644
--- a/themes/gohugoioTheme/assets/js/clipboardjs.js
+++ b/docs/themes/gohugoioTheme/assets/js/clipboardjs.js
diff --git a/themes/gohugoioTheme/assets/js/codeblocks.js b/docs/themes/gohugoioTheme/assets/js/codeblocks.js
index d8039c5d6..d8039c5d6 100644
--- a/themes/gohugoioTheme/assets/js/codeblocks.js
+++ b/docs/themes/gohugoioTheme/assets/js/codeblocks.js
diff --git a/themes/gohugoioTheme/assets/js/docsearch.js b/docs/themes/gohugoioTheme/assets/js/docsearch.js
index 0074da8cd..0074da8cd 100644
--- a/themes/gohugoioTheme/assets/js/docsearch.js
+++ b/docs/themes/gohugoioTheme/assets/js/docsearch.js
diff --git a/themes/gohugoioTheme/layouts/partials/svg/exclamation.svg b/docs/themes/gohugoioTheme/assets/js/filesaver.js
index e69de29bb..e69de29bb 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/exclamation.svg
+++ b/docs/themes/gohugoioTheme/assets/js/filesaver.js
diff --git a/themes/gohugoioTheme/assets/js/hljs.js b/docs/themes/gohugoioTheme/assets/js/hljs.js
index c2252e783..c2252e783 100644
--- a/themes/gohugoioTheme/assets/js/hljs.js
+++ b/docs/themes/gohugoioTheme/assets/js/hljs.js
diff --git a/themes/gohugoioTheme/assets/js/lazysizes.js b/docs/themes/gohugoioTheme/assets/js/lazysizes.js
index 4eb3950af..4eb3950af 100644
--- a/themes/gohugoioTheme/assets/js/lazysizes.js
+++ b/docs/themes/gohugoioTheme/assets/js/lazysizes.js
diff --git a/themes/gohugoioTheme/assets/js/main.js b/docs/themes/gohugoioTheme/assets/js/main.js
index d5b163a2b..d5b163a2b 100644
--- a/themes/gohugoioTheme/assets/js/main.js
+++ b/docs/themes/gohugoioTheme/assets/js/main.js
diff --git a/themes/gohugoioTheme/assets/js/menutoggle.js b/docs/themes/gohugoioTheme/assets/js/menutoggle.js
index d0e645385..d0e645385 100644
--- a/themes/gohugoioTheme/assets/js/menutoggle.js
+++ b/docs/themes/gohugoioTheme/assets/js/menutoggle.js
diff --git a/themes/gohugoioTheme/assets/js/nojs.js b/docs/themes/gohugoioTheme/assets/js/nojs.js
index 50b5126a9..50b5126a9 100644
--- a/themes/gohugoioTheme/assets/js/nojs.js
+++ b/docs/themes/gohugoioTheme/assets/js/nojs.js
diff --git a/themes/gohugoioTheme/assets/js/scrolldir.js b/docs/themes/gohugoioTheme/assets/js/scrolldir.js
index 0b69978cd..0b69978cd 100644
--- a/themes/gohugoioTheme/assets/js/scrolldir.js
+++ b/docs/themes/gohugoioTheme/assets/js/scrolldir.js
diff --git a/themes/gohugoioTheme/assets/js/smoothscroll.js b/docs/themes/gohugoioTheme/assets/js/smoothscroll.js
index 4bb2d99b8..4bb2d99b8 100644
--- a/themes/gohugoioTheme/assets/js/smoothscroll.js
+++ b/docs/themes/gohugoioTheme/assets/js/smoothscroll.js
diff --git a/themes/gohugoioTheme/assets/js/tabs.js b/docs/themes/gohugoioTheme/assets/js/tabs.js
index a689d474e..a689d474e 100644
--- a/themes/gohugoioTheme/assets/js/tabs.js
+++ b/docs/themes/gohugoioTheme/assets/js/tabs.js
diff --git a/themes/gohugoioTheme/assets/output/css/app.css b/docs/themes/gohugoioTheme/assets/output/css/app.css
index 93c008e6e..93c008e6e 100644
--- a/themes/gohugoioTheme/assets/output/css/app.css
+++ b/docs/themes/gohugoioTheme/assets/output/css/app.css
diff --git a/themes/gohugoioTheme/assets/output/js/app.js b/docs/themes/gohugoioTheme/assets/output/js/app.js
index 3097ec5a6..3097ec5a6 100644
--- a/themes/gohugoioTheme/assets/output/js/app.js
+++ b/docs/themes/gohugoioTheme/assets/output/js/app.js
diff --git a/themes/gohugoioTheme/data/sponsors.toml b/docs/themes/gohugoioTheme/data/sponsors.toml
index 9261ffc78..9261ffc78 100644
--- a/themes/gohugoioTheme/data/sponsors.toml
+++ b/docs/themes/gohugoioTheme/data/sponsors.toml
diff --git a/themes/gohugoioTheme/layouts/404.html b/docs/themes/gohugoioTheme/layouts/404.html
index 9b0866d18..9b0866d18 100644
--- a/themes/gohugoioTheme/layouts/404.html
+++ b/docs/themes/gohugoioTheme/layouts/404.html
diff --git a/themes/gohugoioTheme/layouts/_default/baseof.html b/docs/themes/gohugoioTheme/layouts/_default/baseof.html
index 2411b1348..2411b1348 100644
--- a/themes/gohugoioTheme/layouts/_default/baseof.html
+++ b/docs/themes/gohugoioTheme/layouts/_default/baseof.html
diff --git a/themes/gohugoioTheme/layouts/_default/list.html b/docs/themes/gohugoioTheme/layouts/_default/list.html
index 3b7a2307e..3b7a2307e 100644
--- a/themes/gohugoioTheme/layouts/_default/list.html
+++ b/docs/themes/gohugoioTheme/layouts/_default/list.html
diff --git a/themes/gohugoioTheme/layouts/_default/page.html b/docs/themes/gohugoioTheme/layouts/_default/page.html
index 4d4394d1b..4d4394d1b 100644
--- a/themes/gohugoioTheme/layouts/_default/page.html
+++ b/docs/themes/gohugoioTheme/layouts/_default/page.html
diff --git a/themes/gohugoioTheme/layouts/_default/single.html b/docs/themes/gohugoioTheme/layouts/_default/single.html
index 8cd289624..8cd289624 100644
--- a/themes/gohugoioTheme/layouts/_default/single.html
+++ b/docs/themes/gohugoioTheme/layouts/_default/single.html
diff --git a/themes/gohugoioTheme/layouts/_default/taxonomy.html b/docs/themes/gohugoioTheme/layouts/_default/taxonomy.html
index 77d1812d9..77d1812d9 100644
--- a/themes/gohugoioTheme/layouts/_default/taxonomy.html
+++ b/docs/themes/gohugoioTheme/layouts/_default/taxonomy.html
diff --git a/themes/gohugoioTheme/layouts/_default/terms.html b/docs/themes/gohugoioTheme/layouts/_default/terms.html
index f6b566656..f6b566656 100644
--- a/themes/gohugoioTheme/layouts/_default/terms.html
+++ b/docs/themes/gohugoioTheme/layouts/_default/terms.html
diff --git a/themes/gohugoioTheme/layouts/index.headers b/docs/themes/gohugoioTheme/layouts/index.headers
index fedd73525..fedd73525 100644
--- a/themes/gohugoioTheme/layouts/index.headers
+++ b/docs/themes/gohugoioTheme/layouts/index.headers
diff --git a/themes/gohugoioTheme/layouts/index.html b/docs/themes/gohugoioTheme/layouts/index.html
index 93dfdd6c6..93dfdd6c6 100644
--- a/themes/gohugoioTheme/layouts/index.html
+++ b/docs/themes/gohugoioTheme/layouts/index.html
diff --git a/themes/gohugoioTheme/layouts/index.redir b/docs/themes/gohugoioTheme/layouts/index.redir
index 2dfd2bc0f..2dfd2bc0f 100644
--- a/themes/gohugoioTheme/layouts/index.redir
+++ b/docs/themes/gohugoioTheme/layouts/index.redir
diff --git a/themes/gohugoioTheme/layouts/news/list.html b/docs/themes/gohugoioTheme/layouts/news/list.html
index 5a5284658..5a5284658 100644
--- a/themes/gohugoioTheme/layouts/news/list.html
+++ b/docs/themes/gohugoioTheme/layouts/news/list.html
diff --git a/themes/gohugoioTheme/layouts/news/single.html b/docs/themes/gohugoioTheme/layouts/news/single.html
index 200daa70a..200daa70a 100644
--- a/themes/gohugoioTheme/layouts/news/single.html
+++ b/docs/themes/gohugoioTheme/layouts/news/single.html
diff --git a/themes/gohugoioTheme/layouts/page/documentation-home.html b/docs/themes/gohugoioTheme/layouts/page/documentation-home.html
index 91f744c30..91f744c30 100644
--- a/themes/gohugoioTheme/layouts/page/documentation-home.html
+++ b/docs/themes/gohugoioTheme/layouts/page/documentation-home.html
diff --git a/themes/gohugoioTheme/layouts/partials/boxes-section-summaries.html b/docs/themes/gohugoioTheme/layouts/partials/boxes-section-summaries.html
index b7e37c47c..b7e37c47c 100644
--- a/themes/gohugoioTheme/layouts/partials/boxes-section-summaries.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/boxes-section-summaries.html
diff --git a/themes/gohugoioTheme/layouts/partials/boxes-small-news.html b/docs/themes/gohugoioTheme/layouts/partials/boxes-small-news.html
index 39b0dcf6d..39b0dcf6d 100644
--- a/themes/gohugoioTheme/layouts/partials/boxes-small-news.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/boxes-small-news.html
diff --git a/themes/gohugoioTheme/layouts/partials/components/author-github-data-card.html b/docs/themes/gohugoioTheme/layouts/partials/components/author-github-data-card.html
index 622df7953..622df7953 100644
--- a/themes/gohugoioTheme/layouts/partials/components/author-github-data-card.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/components/author-github-data-card.html
diff --git a/themes/gohugoioTheme/layouts/partials/components/author-github-data.html b/docs/themes/gohugoioTheme/layouts/partials/components/author-github-data.html
index 25baea80a..25baea80a 100644
--- a/themes/gohugoioTheme/layouts/partials/components/author-github-data.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/components/author-github-data.html
diff --git a/themes/gohugoioTheme/layouts/partials/docs/functions-signature.html b/docs/themes/gohugoioTheme/layouts/partials/docs/functions-signature.html
index 090b9243b..090b9243b 100644
--- a/themes/gohugoioTheme/layouts/partials/docs/functions-signature.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/docs/functions-signature.html
diff --git a/themes/gohugoioTheme/layouts/partials/docs/page-meta-data.html b/docs/themes/gohugoioTheme/layouts/partials/docs/page-meta-data.html
index 8b3fbbafc..8b3fbbafc 100644
--- a/themes/gohugoioTheme/layouts/partials/docs/page-meta-data.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/docs/page-meta-data.html
diff --git a/themes/gohugoioTheme/layouts/partials/entry-summary.html b/docs/themes/gohugoioTheme/layouts/partials/entry-summary.html
index d9cd9c68f..d9cd9c68f 100644
--- a/themes/gohugoioTheme/layouts/partials/entry-summary.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/entry-summary.html
diff --git a/themes/gohugoioTheme/layouts/partials/gtag.html b/docs/themes/gohugoioTheme/layouts/partials/gtag.html
index c78926503..c78926503 100644
--- a/themes/gohugoioTheme/layouts/partials/gtag.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/gtag.html
diff --git a/themes/gohugoioTheme/layouts/partials/head-additions.html b/docs/themes/gohugoioTheme/layouts/partials/head-additions.html
index af615ee7c..af615ee7c 100644
--- a/themes/gohugoioTheme/layouts/partials/head-additions.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/head-additions.html
diff --git a/themes/gohugoioTheme/layouts/partials/hero.html b/docs/themes/gohugoioTheme/layouts/partials/hero.html
index 9e7240433..9e7240433 100644
--- a/themes/gohugoioTheme/layouts/partials/hero.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/hero.html
diff --git a/themes/gohugoioTheme/layouts/partials/home-page-sections/features-icons.html b/docs/themes/gohugoioTheme/layouts/partials/home-page-sections/features-icons.html
index a7733acdc..a7733acdc 100644
--- a/themes/gohugoioTheme/layouts/partials/home-page-sections/features-icons.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/home-page-sections/features-icons.html
diff --git a/themes/gohugoioTheme/layouts/partials/home-page-sections/features-single.html b/docs/themes/gohugoioTheme/layouts/partials/home-page-sections/features-single.html
index f36b3d674..f36b3d674 100644
--- a/themes/gohugoioTheme/layouts/partials/home-page-sections/features-single.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/home-page-sections/features-single.html
diff --git a/themes/gohugoioTheme/layouts/partials/home-page-sections/installation.html b/docs/themes/gohugoioTheme/layouts/partials/home-page-sections/installation.html
index 4bea1a54a..4bea1a54a 100644
--- a/themes/gohugoioTheme/layouts/partials/home-page-sections/installation.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/home-page-sections/installation.html
diff --git a/themes/gohugoioTheme/layouts/partials/home-page-sections/open-source-involvement.html b/docs/themes/gohugoioTheme/layouts/partials/home-page-sections/open-source-involvement.html
index 5300fb7a8..5300fb7a8 100644
--- a/themes/gohugoioTheme/layouts/partials/home-page-sections/open-source-involvement.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/home-page-sections/open-source-involvement.html
diff --git a/themes/gohugoioTheme/layouts/partials/home-page-sections/showcase.html b/docs/themes/gohugoioTheme/layouts/partials/home-page-sections/showcase.html
index c73cfa5e9..c73cfa5e9 100644
--- a/themes/gohugoioTheme/layouts/partials/home-page-sections/showcase.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/home-page-sections/showcase.html
diff --git a/themes/gohugoioTheme/layouts/partials/home-page-sections/sponsors.html b/docs/themes/gohugoioTheme/layouts/partials/home-page-sections/sponsors.html
index fa179f7f4..fa179f7f4 100644
--- a/themes/gohugoioTheme/layouts/partials/home-page-sections/sponsors.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/home-page-sections/sponsors.html
diff --git a/themes/gohugoioTheme/layouts/partials/home-page-sections/tweets.html b/docs/themes/gohugoioTheme/layouts/partials/home-page-sections/tweets.html
index 5aebf6737..5aebf6737 100644
--- a/themes/gohugoioTheme/layouts/partials/home-page-sections/tweets.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/home-page-sections/tweets.html
diff --git a/themes/gohugoioTheme/layouts/partials/icon-link.html b/docs/themes/gohugoioTheme/layouts/partials/icon-link.html
index dec9ae48b..dec9ae48b 100644
--- a/themes/gohugoioTheme/layouts/partials/icon-link.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/icon-link.html
diff --git a/themes/gohugoioTheme/layouts/partials/nav-links-docs-mobile.html b/docs/themes/gohugoioTheme/layouts/partials/nav-links-docs-mobile.html
index ad9d535b4..ad9d535b4 100644
--- a/themes/gohugoioTheme/layouts/partials/nav-links-docs-mobile.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/nav-links-docs-mobile.html
diff --git a/themes/gohugoioTheme/layouts/partials/nav-links-docs.html b/docs/themes/gohugoioTheme/layouts/partials/nav-links-docs.html
index 61aa11dde..61aa11dde 100644
--- a/themes/gohugoioTheme/layouts/partials/nav-links-docs.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/nav-links-docs.html
diff --git a/themes/gohugoioTheme/layouts/partials/nav-links-global-mobile.html b/docs/themes/gohugoioTheme/layouts/partials/nav-links-global-mobile.html
index 4a1631d96..4a1631d96 100644
--- a/themes/gohugoioTheme/layouts/partials/nav-links-global-mobile.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/nav-links-global-mobile.html
diff --git a/themes/gohugoioTheme/layouts/partials/nav-links.html b/docs/themes/gohugoioTheme/layouts/partials/nav-links.html
index af3790b16..af3790b16 100644
--- a/themes/gohugoioTheme/layouts/partials/nav-links.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/nav-links.html
diff --git a/themes/gohugoioTheme/layouts/partials/nav-mobile.html b/docs/themes/gohugoioTheme/layouts/partials/nav-mobile.html
index 00b1a691c..00b1a691c 100644
--- a/themes/gohugoioTheme/layouts/partials/nav-mobile.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/nav-mobile.html
diff --git a/themes/gohugoioTheme/layouts/partials/nav-top.html b/docs/themes/gohugoioTheme/layouts/partials/nav-top.html
index d8e87eb63..d8e87eb63 100644
--- a/themes/gohugoioTheme/layouts/partials/nav-top.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/nav-top.html
diff --git a/themes/gohugoioTheme/layouts/partials/page-edit.html b/docs/themes/gohugoioTheme/layouts/partials/page-edit.html
index edf84669e..edf84669e 100644
--- a/themes/gohugoioTheme/layouts/partials/page-edit.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/page-edit.html
diff --git a/themes/gohugoioTheme/layouts/partials/page-header.html b/docs/themes/gohugoioTheme/layouts/partials/page-header.html
index dcc96242f..dcc96242f 100644
--- a/themes/gohugoioTheme/layouts/partials/page-header.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/page-header.html
diff --git a/themes/gohugoioTheme/layouts/partials/pagelayout.html b/docs/themes/gohugoioTheme/layouts/partials/pagelayout.html
index dd048223e..dd048223e 100644
--- a/themes/gohugoioTheme/layouts/partials/pagelayout.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/pagelayout.html
diff --git a/themes/gohugoioTheme/layouts/partials/previous-next-links-in-section-with-title.html b/docs/themes/gohugoioTheme/layouts/partials/previous-next-links-in-section-with-title.html
index 71a14c0ef..71a14c0ef 100644
--- a/themes/gohugoioTheme/layouts/partials/previous-next-links-in-section-with-title.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/previous-next-links-in-section-with-title.html
diff --git a/themes/gohugoioTheme/layouts/partials/previous-next-links-in-section.html b/docs/themes/gohugoioTheme/layouts/partials/previous-next-links-in-section.html
index af9f4aac1..af9f4aac1 100644
--- a/themes/gohugoioTheme/layouts/partials/previous-next-links-in-section.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/previous-next-links-in-section.html
diff --git a/themes/gohugoioTheme/layouts/partials/previous-next-links.html b/docs/themes/gohugoioTheme/layouts/partials/previous-next-links.html
index cd43dd840..cd43dd840 100644
--- a/themes/gohugoioTheme/layouts/partials/previous-next-links.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/previous-next-links.html
diff --git a/themes/gohugoioTheme/layouts/partials/related.html b/docs/themes/gohugoioTheme/layouts/partials/related.html
index fb11699af..fb11699af 100644
--- a/themes/gohugoioTheme/layouts/partials/related.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/related.html
diff --git a/themes/gohugoioTheme/layouts/partials/site-footer.html b/docs/themes/gohugoioTheme/layouts/partials/site-footer.html
index ec932d887..ec932d887 100644
--- a/themes/gohugoioTheme/layouts/partials/site-footer.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/site-footer.html
diff --git a/themes/gohugoioTheme/layouts/partials/site-manifest.html b/docs/themes/gohugoioTheme/layouts/partials/site-manifest.html
index 54472ba16..54472ba16 100644
--- a/themes/gohugoioTheme/layouts/partials/site-manifest.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/site-manifest.html
diff --git a/themes/gohugoioTheme/layouts/partials/site-nav.html b/docs/themes/gohugoioTheme/layouts/partials/site-nav.html
index 222b29f3b..222b29f3b 100644
--- a/themes/gohugoioTheme/layouts/partials/site-nav.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/site-nav.html
diff --git a/themes/gohugoioTheme/layouts/partials/site-scripts.html b/docs/themes/gohugoioTheme/layouts/partials/site-scripts.html
index b8d9ff043..b8d9ff043 100644
--- a/themes/gohugoioTheme/layouts/partials/site-scripts.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/site-scripts.html
diff --git a/themes/gohugoioTheme/layouts/partials/site-search.html b/docs/themes/gohugoioTheme/layouts/partials/site-search.html
index d8c4b97bf..d8c4b97bf 100644
--- a/themes/gohugoioTheme/layouts/partials/site-search.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/site-search.html
diff --git a/themes/gohugoioTheme/layouts/partials/social-follow.html b/docs/themes/gohugoioTheme/layouts/partials/social-follow.html
index 7b517dbb4..7b517dbb4 100644
--- a/themes/gohugoioTheme/layouts/partials/social-follow.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/social-follow.html
diff --git a/themes/gohugoioTheme/layouts/partials/summary.html b/docs/themes/gohugoioTheme/layouts/partials/summary.html
index 0f140cf70..0f140cf70 100644
--- a/themes/gohugoioTheme/layouts/partials/summary.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/summary.html
diff --git a/themes/gohugoioTheme/layouts/partials/svg/Twitter_Logo_Blue.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/Twitter_Logo_Blue.svg
index da9438414..da9438414 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/Twitter_Logo_Blue.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/Twitter_Logo_Blue.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/apple.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/apple.svg
index 6f3c20f76..6f3c20f76 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/apple.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/apple.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/clipboard.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/clipboard.svg
index e1b170359..e1b170359 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/clipboard.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/clipboard.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/clippy.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/clippy.svg
index e1b170359..e1b170359 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/clippy.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/clippy.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/cloud.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/cloud.svg
index 2ea15de87..2ea15de87 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/cloud.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/cloud.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/content.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/content.svg
index bc696b90b..bc696b90b 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/content.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/content.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/design.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/design.svg
index 9f9d71769..9f9d71769 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/design.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/design.svg
diff --git a/docs/themes/gohugoioTheme/layouts/partials/svg/exclamation.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/exclamation.svg
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/exclamation.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/facebook.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/facebook.svg
index 6e6af44a2..6e6af44a2 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/facebook.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/facebook.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/focus.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/focus.svg
index ed2c929b4..ed2c929b4 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/focus.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/focus.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/freebsd.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/freebsd.svg
index 842be09a1..842be09a1 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/freebsd.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/freebsd.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/functions.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/functions.svg
index 717a35686..717a35686 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/functions.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/functions.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/github-corner.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/github-corner.svg
index 29bc57ad3..29bc57ad3 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/github-corner.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/github-corner.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/github-squared.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/github-squared.svg
index dabc741e0..dabc741e0 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/github-squared.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/github-squared.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/gitter.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/gitter.svg
index 9c2de7da2..9c2de7da2 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/gitter.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/gitter.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/gme.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/gme.svg
index 9ab114aa3..9ab114aa3 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/gme.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/gme.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/godoc-icon.html b/docs/themes/gohugoioTheme/layouts/partials/svg/godoc-icon.html
index 1a6b82159..1a6b82159 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/godoc-icon.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/godoc-icon.html
diff --git a/themes/gohugoioTheme/layouts/partials/svg/gopher-2.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/gopher-2.svg
index 961221f18..961221f18 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/gopher-2.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/gopher-2.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/gopher-front.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/gopher-front.svg
index 0f8fbe0d9..0f8fbe0d9 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/gopher-front.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/gopher-front.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/gopher-homepage.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/gopher-homepage.svg
index 36d9f1c41..36d9f1c41 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/gopher-homepage.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/gopher-homepage.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/gopher-side_path.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/gopher-side_path.svg
index 05cfb84d1..05cfb84d1 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/gopher-side_path.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/gopher-side_path.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/gopher-small.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/gopher-small.svg
index bc1e5010c..bc1e5010c 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/gopher-small.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/gopher-small.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/gopher.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/gopher.svg
index 7f6ec255c..7f6ec255c 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/gopher.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/gopher.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/hugo-h-only.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/hugo-h-only.svg
index ea72a6f51..ea72a6f51 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/hugo-h-only.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/hugo-h-only.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/hugo.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/hugo.svg
index 58d025596..58d025596 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/hugo.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/hugo.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_down.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_down.svg
index 3ba28c3f5..3ba28c3f5 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_down.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_down.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_up.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_up.svg
index 8ec2eb766..8ec2eb766 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_up.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_up.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/ic_chevron_left_black_24px.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/ic_chevron_left_black_24px.svg
index da37757cf..da37757cf 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/ic_chevron_left_black_24px.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/ic_chevron_left_black_24px.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/ic_chevron_right_black_24px.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/ic_chevron_right_black_24px.svg
index 47689a91e..47689a91e 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/ic_chevron_right_black_24px.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/ic_chevron_right_black_24px.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/idea.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/idea.svg
index 5c2ccc2f4..5c2ccc2f4 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/idea.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/idea.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/instagram.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/instagram.svg
index ae915113b..ae915113b 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/instagram.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/instagram.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/javascript.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/javascript.svg
index b0e2f5b0d..b0e2f5b0d 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/javascript.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/javascript.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/json.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/json.svg
index d2ba6d0fc..d2ba6d0fc 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/json.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/json.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/link-ext.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/link-ext.svg
index ba9400b7f..ba9400b7f 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/link-ext.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/link-ext.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/link-permalink.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/link-permalink.svg
index f5de52d02..f5de52d02 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/link-permalink.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/link-permalink.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/md.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/md.svg
index f1a794565..f1a794565 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/md.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/md.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/mdsolid.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/mdsolid.svg
index d0d9ae938..d0d9ae938 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/mdsolid.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/mdsolid.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/newlogo.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/newlogo.svg
index 83b706383..83b706383 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/newlogo.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/newlogo.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/sass.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/sass.svg
index da3d9cfcf..da3d9cfcf 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/sass.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/sass.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/search.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/search.svg
index 181789b54..181789b54 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/search.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/search.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/twitter.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/twitter.svg
index 247ca9062..247ca9062 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/twitter.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/twitter.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/website.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/website.svg
index 2bdcf5f94..2bdcf5f94 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/website.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/website.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/windows.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/windows.svg
index fe3bf0296..fe3bf0296 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/windows.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/windows.svg
diff --git a/themes/gohugoioTheme/layouts/partials/svg/yaml.svg b/docs/themes/gohugoioTheme/layouts/partials/svg/yaml.svg
index 59eeb71c2..59eeb71c2 100644
--- a/themes/gohugoioTheme/layouts/partials/svg/yaml.svg
+++ b/docs/themes/gohugoioTheme/layouts/partials/svg/yaml.svg
diff --git a/themes/gohugoioTheme/layouts/partials/tags.html b/docs/themes/gohugoioTheme/layouts/partials/tags.html
index 59e3e51a0..59e3e51a0 100644
--- a/themes/gohugoioTheme/layouts/partials/tags.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/tags.html
diff --git a/themes/gohugoioTheme/layouts/partials/toc.html b/docs/themes/gohugoioTheme/layouts/partials/toc.html
index 583feec4f..583feec4f 100644
--- a/themes/gohugoioTheme/layouts/partials/toc.html
+++ b/docs/themes/gohugoioTheme/layouts/partials/toc.html
diff --git a/themes/gohugoioTheme/layouts/robots.txt b/docs/themes/gohugoioTheme/layouts/robots.txt
index 25b9e9a0d..25b9e9a0d 100644
--- a/themes/gohugoioTheme/layouts/robots.txt
+++ b/docs/themes/gohugoioTheme/layouts/robots.txt
diff --git a/themes/gohugoioTheme/layouts/shortcodes/articlelist.html b/docs/themes/gohugoioTheme/layouts/shortcodes/articlelist.html
index 2755b1e2d..2755b1e2d 100644
--- a/themes/gohugoioTheme/layouts/shortcodes/articlelist.html
+++ b/docs/themes/gohugoioTheme/layouts/shortcodes/articlelist.html
diff --git a/themes/gohugoioTheme/layouts/shortcodes/code-toggle.html b/docs/themes/gohugoioTheme/layouts/shortcodes/code-toggle.html
index c695a7aae..c695a7aae 100644
--- a/themes/gohugoioTheme/layouts/shortcodes/code-toggle.html
+++ b/docs/themes/gohugoioTheme/layouts/shortcodes/code-toggle.html
diff --git a/themes/gohugoioTheme/layouts/shortcodes/code.html b/docs/themes/gohugoioTheme/layouts/shortcodes/code.html
index 6df49956a..6df49956a 100644
--- a/themes/gohugoioTheme/layouts/shortcodes/code.html
+++ b/docs/themes/gohugoioTheme/layouts/shortcodes/code.html
diff --git a/themes/gohugoioTheme/layouts/shortcodes/datatable.html b/docs/themes/gohugoioTheme/layouts/shortcodes/datatable.html
index 7ddda86d0..7ddda86d0 100644
--- a/themes/gohugoioTheme/layouts/shortcodes/datatable.html
+++ b/docs/themes/gohugoioTheme/layouts/shortcodes/datatable.html
diff --git a/themes/gohugoioTheme/layouts/shortcodes/directoryindex.html b/docs/themes/gohugoioTheme/layouts/shortcodes/directoryindex.html
index 37e7d3ad1..37e7d3ad1 100644
--- a/themes/gohugoioTheme/layouts/shortcodes/directoryindex.html
+++ b/docs/themes/gohugoioTheme/layouts/shortcodes/directoryindex.html
diff --git a/themes/gohugoioTheme/layouts/shortcodes/docfile.html b/docs/themes/gohugoioTheme/layouts/shortcodes/docfile.html
index 2f982aae8..2f982aae8 100644
--- a/themes/gohugoioTheme/layouts/shortcodes/docfile.html
+++ b/docs/themes/gohugoioTheme/layouts/shortcodes/docfile.html
diff --git a/themes/gohugoioTheme/layouts/shortcodes/exfile.html b/docs/themes/gohugoioTheme/layouts/shortcodes/exfile.html
index 226782957..226782957 100644
--- a/themes/gohugoioTheme/layouts/shortcodes/exfile.html
+++ b/docs/themes/gohugoioTheme/layouts/shortcodes/exfile.html
diff --git a/themes/gohugoioTheme/layouts/shortcodes/exfm.html b/docs/themes/gohugoioTheme/layouts/shortcodes/exfm.html
index c0429bbe1..c0429bbe1 100644
--- a/themes/gohugoioTheme/layouts/shortcodes/exfm.html
+++ b/docs/themes/gohugoioTheme/layouts/shortcodes/exfm.html
diff --git a/themes/gohugoioTheme/layouts/shortcodes/gh.html b/docs/themes/gohugoioTheme/layouts/shortcodes/gh.html
index e027dc0f0..e027dc0f0 100644
--- a/themes/gohugoioTheme/layouts/shortcodes/gh.html
+++ b/docs/themes/gohugoioTheme/layouts/shortcodes/gh.html
diff --git a/themes/gohugoioTheme/layouts/shortcodes/ghrepo.html b/docs/themes/gohugoioTheme/layouts/shortcodes/ghrepo.html
index e9df40d6a..e9df40d6a 100644
--- a/themes/gohugoioTheme/layouts/shortcodes/ghrepo.html
+++ b/docs/themes/gohugoioTheme/layouts/shortcodes/ghrepo.html
diff --git a/themes/gohugoioTheme/layouts/shortcodes/nohighlight.html b/docs/themes/gohugoioTheme/layouts/shortcodes/nohighlight.html
index 0f254b4ca..0f254b4ca 100644
--- a/themes/gohugoioTheme/layouts/shortcodes/nohighlight.html
+++ b/docs/themes/gohugoioTheme/layouts/shortcodes/nohighlight.html
diff --git a/themes/gohugoioTheme/layouts/shortcodes/note.html b/docs/themes/gohugoioTheme/layouts/shortcodes/note.html
index 24d2cd0b2..24d2cd0b2 100644
--- a/themes/gohugoioTheme/layouts/shortcodes/note.html
+++ b/docs/themes/gohugoioTheme/layouts/shortcodes/note.html
diff --git a/themes/gohugoioTheme/layouts/shortcodes/output.html b/docs/themes/gohugoioTheme/layouts/shortcodes/output.html
index df1a8ae89..df1a8ae89 100644
--- a/themes/gohugoioTheme/layouts/shortcodes/output.html
+++ b/docs/themes/gohugoioTheme/layouts/shortcodes/output.html
diff --git a/themes/gohugoioTheme/layouts/shortcodes/readfile.html b/docs/themes/gohugoioTheme/layouts/shortcodes/readfile.html
index f777abe26..f777abe26 100644
--- a/themes/gohugoioTheme/layouts/shortcodes/readfile.html
+++ b/docs/themes/gohugoioTheme/layouts/shortcodes/readfile.html
diff --git a/themes/gohugoioTheme/layouts/shortcodes/tip.html b/docs/themes/gohugoioTheme/layouts/shortcodes/tip.html
index 139e3376b..139e3376b 100644
--- a/themes/gohugoioTheme/layouts/shortcodes/tip.html
+++ b/docs/themes/gohugoioTheme/layouts/shortcodes/tip.html
diff --git a/themes/gohugoioTheme/layouts/shortcodes/warning.html b/docs/themes/gohugoioTheme/layouts/shortcodes/warning.html
index c9147be64..c9147be64 100644
--- a/themes/gohugoioTheme/layouts/shortcodes/warning.html
+++ b/docs/themes/gohugoioTheme/layouts/shortcodes/warning.html
diff --git a/themes/gohugoioTheme/layouts/shortcodes/yt.html b/docs/themes/gohugoioTheme/layouts/shortcodes/yt.html
index 6915cec5f..6915cec5f 100644
--- a/themes/gohugoioTheme/layouts/shortcodes/yt.html
+++ b/docs/themes/gohugoioTheme/layouts/shortcodes/yt.html
diff --git a/themes/gohugoioTheme/layouts/showcase/list.html b/docs/themes/gohugoioTheme/layouts/showcase/list.html
index b0083fc0f..b0083fc0f 100644
--- a/themes/gohugoioTheme/layouts/showcase/list.html
+++ b/docs/themes/gohugoioTheme/layouts/showcase/list.html
diff --git a/themes/gohugoioTheme/layouts/showcase/single.html b/docs/themes/gohugoioTheme/layouts/showcase/single.html
index a7cf439cb..a7cf439cb 100644
--- a/themes/gohugoioTheme/layouts/showcase/single.html
+++ b/docs/themes/gohugoioTheme/layouts/showcase/single.html
diff --git a/themes/gohugoioTheme/license.md b/docs/themes/gohugoioTheme/license.md
index c0522a374..c0522a374 100644
--- a/themes/gohugoioTheme/license.md
+++ b/docs/themes/gohugoioTheme/license.md
diff --git a/themes/gohugoioTheme/package-lock.json b/docs/themes/gohugoioTheme/package-lock.json
index 1d6eea0fb..1d6eea0fb 100644
--- a/themes/gohugoioTheme/package-lock.json
+++ b/docs/themes/gohugoioTheme/package-lock.json
diff --git a/themes/gohugoioTheme/package.json b/docs/themes/gohugoioTheme/package.json
index 85a197097..85a197097 100644
--- a/themes/gohugoioTheme/package.json
+++ b/docs/themes/gohugoioTheme/package.json
diff --git a/themes/gohugoioTheme/src/package-lock.json b/docs/themes/gohugoioTheme/src/package-lock.json
index be0857aa6..be0857aa6 100644
--- a/themes/gohugoioTheme/src/package-lock.json
+++ b/docs/themes/gohugoioTheme/src/package-lock.json
diff --git a/themes/gohugoioTheme/src/package.json b/docs/themes/gohugoioTheme/src/package.json
index fc8341e05..fc8341e05 100644
--- a/themes/gohugoioTheme/src/package.json
+++ b/docs/themes/gohugoioTheme/src/package.json
diff --git a/themes/gohugoioTheme/src/readme.md b/docs/themes/gohugoioTheme/src/readme.md
index 0ca486d97..0ca486d97 100644
--- a/themes/gohugoioTheme/src/readme.md
+++ b/docs/themes/gohugoioTheme/src/readme.md
diff --git a/themes/gohugoioTheme/static/android-chrome-144x144.png b/docs/themes/gohugoioTheme/static/android-chrome-144x144.png
index 975cb33ba..975cb33ba 100644
--- a/themes/gohugoioTheme/static/android-chrome-144x144.png
+++ b/docs/themes/gohugoioTheme/static/android-chrome-144x144.png
Binary files differ
diff --git a/themes/gohugoioTheme/static/android-chrome-192x192.png b/docs/themes/gohugoioTheme/static/android-chrome-192x192.png
index 7ab6c3849..7ab6c3849 100644
--- a/themes/gohugoioTheme/static/android-chrome-192x192.png
+++ b/docs/themes/gohugoioTheme/static/android-chrome-192x192.png
Binary files differ
diff --git a/themes/gohugoioTheme/static/android-chrome-256x256.png b/docs/themes/gohugoioTheme/static/android-chrome-256x256.png
index ed88a2224..ed88a2224 100644
--- a/themes/gohugoioTheme/static/android-chrome-256x256.png
+++ b/docs/themes/gohugoioTheme/static/android-chrome-256x256.png
Binary files differ
diff --git a/themes/gohugoioTheme/static/android-chrome-36x36.png b/docs/themes/gohugoioTheme/static/android-chrome-36x36.png
index 3695eb088..3695eb088 100644
--- a/themes/gohugoioTheme/static/android-chrome-36x36.png
+++ b/docs/themes/gohugoioTheme/static/android-chrome-36x36.png
Binary files differ
diff --git a/themes/gohugoioTheme/static/android-chrome-48x48.png b/docs/themes/gohugoioTheme/static/android-chrome-48x48.png
index ca275dad6..ca275dad6 100644
--- a/themes/gohugoioTheme/static/android-chrome-48x48.png
+++ b/docs/themes/gohugoioTheme/static/android-chrome-48x48.png
Binary files differ
diff --git a/themes/gohugoioTheme/static/android-chrome-72x72.png b/docs/themes/gohugoioTheme/static/android-chrome-72x72.png
index 966891f25..966891f25 100644
--- a/themes/gohugoioTheme/static/android-chrome-72x72.png
+++ b/docs/themes/gohugoioTheme/static/android-chrome-72x72.png
Binary files differ
diff --git a/themes/gohugoioTheme/static/android-chrome-96x96.png b/docs/themes/gohugoioTheme/static/android-chrome-96x96.png
index feb1d3ebf..feb1d3ebf 100644
--- a/themes/gohugoioTheme/static/android-chrome-96x96.png
+++ b/docs/themes/gohugoioTheme/static/android-chrome-96x96.png
Binary files differ
diff --git a/themes/gohugoioTheme/static/apple-touch-icon.png b/docs/themes/gohugoioTheme/static/apple-touch-icon.png
index ecf1fc020..ecf1fc020 100644
--- a/themes/gohugoioTheme/static/apple-touch-icon.png
+++ b/docs/themes/gohugoioTheme/static/apple-touch-icon.png
Binary files differ
diff --git a/themes/gohugoioTheme/static/browserconfig.xml b/docs/themes/gohugoioTheme/static/browserconfig.xml
index 62400c5f2..62400c5f2 100644
--- a/themes/gohugoioTheme/static/browserconfig.xml
+++ b/docs/themes/gohugoioTheme/static/browserconfig.xml
diff --git a/themes/gohugoioTheme/static/dist/app.bundle.js b/docs/themes/gohugoioTheme/static/dist/app.bundle.js
index 6391e71e9..6391e71e9 100644
--- a/themes/gohugoioTheme/static/dist/app.bundle.js
+++ b/docs/themes/gohugoioTheme/static/dist/app.bundle.js
diff --git a/themes/gohugoioTheme/static/dist/main.css b/docs/themes/gohugoioTheme/static/dist/main.css
index 51107f438..51107f438 100644
--- a/themes/gohugoioTheme/static/dist/main.css
+++ b/docs/themes/gohugoioTheme/static/dist/main.css
diff --git a/themes/gohugoioTheme/static/favicon-16x16.png b/docs/themes/gohugoioTheme/static/favicon-16x16.png
index c62ce6fb2..c62ce6fb2 100644
--- a/themes/gohugoioTheme/static/favicon-16x16.png
+++ b/docs/themes/gohugoioTheme/static/favicon-16x16.png
Binary files differ
diff --git a/themes/gohugoioTheme/static/favicon-32x32.png b/docs/themes/gohugoioTheme/static/favicon-32x32.png
index 57a018e35..57a018e35 100644
--- a/themes/gohugoioTheme/static/favicon-32x32.png
+++ b/docs/themes/gohugoioTheme/static/favicon-32x32.png
Binary files differ
diff --git a/themes/gohugoioTheme/static/favicon.ico b/docs/themes/gohugoioTheme/static/favicon.ico
index dc007a99e..dc007a99e 100644
--- a/themes/gohugoioTheme/static/favicon.ico
+++ b/docs/themes/gohugoioTheme/static/favicon.ico
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-200.woff b/docs/themes/gohugoioTheme/static/fonts/muli-latin-200.woff
index 97602c761..97602c761 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-200.woff
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-200.woff
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-200.woff2 b/docs/themes/gohugoioTheme/static/fonts/muli-latin-200.woff2
index 858a4e9af..858a4e9af 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-200.woff2
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-200.woff2
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-200italic.woff b/docs/themes/gohugoioTheme/static/fonts/muli-latin-200italic.woff
index 472e5740a..472e5740a 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-200italic.woff
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-200italic.woff
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-200italic.woff2 b/docs/themes/gohugoioTheme/static/fonts/muli-latin-200italic.woff2
index 449772391..449772391 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-200italic.woff2
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-200italic.woff2
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-300.woff b/docs/themes/gohugoioTheme/static/fonts/muli-latin-300.woff
index 4579c75d7..4579c75d7 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-300.woff
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-300.woff
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-300.woff2 b/docs/themes/gohugoioTheme/static/fonts/muli-latin-300.woff2
index 6c211a7ed..6c211a7ed 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-300.woff2
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-300.woff2
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-300italic.woff b/docs/themes/gohugoioTheme/static/fonts/muli-latin-300italic.woff
index c739550ce..c739550ce 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-300italic.woff
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-300italic.woff
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-300italic.woff2 b/docs/themes/gohugoioTheme/static/fonts/muli-latin-300italic.woff2
index db9e434c5..db9e434c5 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-300italic.woff2
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-300italic.woff2
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-400.woff b/docs/themes/gohugoioTheme/static/fonts/muli-latin-400.woff
index 342b3aad2..342b3aad2 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-400.woff
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-400.woff
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-400.woff2 b/docs/themes/gohugoioTheme/static/fonts/muli-latin-400.woff2
index f3e9d31af..f3e9d31af 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-400.woff2
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-400.woff2
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-400italic.woff b/docs/themes/gohugoioTheme/static/fonts/muli-latin-400italic.woff
index 89bdcbd90..89bdcbd90 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-400italic.woff
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-400italic.woff
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-400italic.woff2 b/docs/themes/gohugoioTheme/static/fonts/muli-latin-400italic.woff2
index b78e3bd39..b78e3bd39 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-400italic.woff2
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-400italic.woff2
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-600.woff b/docs/themes/gohugoioTheme/static/fonts/muli-latin-600.woff
index e31fd2c52..e31fd2c52 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-600.woff
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-600.woff
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-600.woff2 b/docs/themes/gohugoioTheme/static/fonts/muli-latin-600.woff2
index 6f1f8026b..6f1f8026b 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-600.woff2
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-600.woff2
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-600italic.woff b/docs/themes/gohugoioTheme/static/fonts/muli-latin-600italic.woff
index e2b4a0154..e2b4a0154 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-600italic.woff
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-600italic.woff
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-600italic.woff2 b/docs/themes/gohugoioTheme/static/fonts/muli-latin-600italic.woff2
index fafd8076a..fafd8076a 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-600italic.woff2
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-600italic.woff2
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-700.woff b/docs/themes/gohugoioTheme/static/fonts/muli-latin-700.woff
index d2152c4ea..d2152c4ea 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-700.woff
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-700.woff
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-700.woff2 b/docs/themes/gohugoioTheme/static/fonts/muli-latin-700.woff2
index 1cedfcd14..1cedfcd14 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-700.woff2
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-700.woff2
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-700italic.woff b/docs/themes/gohugoioTheme/static/fonts/muli-latin-700italic.woff
index 016fa059c..016fa059c 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-700italic.woff
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-700italic.woff
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-700italic.woff2 b/docs/themes/gohugoioTheme/static/fonts/muli-latin-700italic.woff2
index fa9697232..fa9697232 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-700italic.woff2
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-700italic.woff2
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-800.woff b/docs/themes/gohugoioTheme/static/fonts/muli-latin-800.woff
index 9fd9939dc..9fd9939dc 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-800.woff
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-800.woff
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-800.woff2 b/docs/themes/gohugoioTheme/static/fonts/muli-latin-800.woff2
index 4cdf7bc78..4cdf7bc78 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-800.woff2
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-800.woff2
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-800italic.woff b/docs/themes/gohugoioTheme/static/fonts/muli-latin-800italic.woff
index 2d0c0d2ff..2d0c0d2ff 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-800italic.woff
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-800italic.woff
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-800italic.woff2 b/docs/themes/gohugoioTheme/static/fonts/muli-latin-800italic.woff2
index ee51dd38a..ee51dd38a 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-800italic.woff2
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-800italic.woff2
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-900.woff b/docs/themes/gohugoioTheme/static/fonts/muli-latin-900.woff
index 1b343ad2c..1b343ad2c 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-900.woff
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-900.woff
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-900.woff2 b/docs/themes/gohugoioTheme/static/fonts/muli-latin-900.woff2
index 1252216a0..1252216a0 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-900.woff2
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-900.woff2
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-900italic.woff b/docs/themes/gohugoioTheme/static/fonts/muli-latin-900italic.woff
index 0aad09765..0aad09765 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-900italic.woff
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-900italic.woff
Binary files differ
diff --git a/themes/gohugoioTheme/static/fonts/muli-latin-900italic.woff2 b/docs/themes/gohugoioTheme/static/fonts/muli-latin-900italic.woff2
index fd4e66bfb..fd4e66bfb 100644
--- a/themes/gohugoioTheme/static/fonts/muli-latin-900italic.woff2
+++ b/docs/themes/gohugoioTheme/static/fonts/muli-latin-900italic.woff2
Binary files differ
diff --git a/themes/gohugoioTheme/static/images/GitHub-Mark-64px.png b/docs/themes/gohugoioTheme/static/images/GitHub-Mark-64px.png
index ff84298cb..ff84298cb 100644
--- a/themes/gohugoioTheme/static/images/GitHub-Mark-64px.png
+++ b/docs/themes/gohugoioTheme/static/images/GitHub-Mark-64px.png
Binary files differ
diff --git a/themes/gohugoioTheme/static/images/gohugoio-card.png b/docs/themes/gohugoioTheme/static/images/gohugoio-card.png
index 5f89cb7b4..5f89cb7b4 100644
--- a/themes/gohugoioTheme/static/images/gohugoio-card.png
+++ b/docs/themes/gohugoioTheme/static/images/gohugoio-card.png
Binary files differ
diff --git a/themes/gohugoioTheme/static/images/gopher-hero.svg b/docs/themes/gohugoioTheme/static/images/gopher-hero.svg
index 36d9f1c41..36d9f1c41 100644
--- a/themes/gohugoioTheme/static/images/gopher-hero.svg
+++ b/docs/themes/gohugoioTheme/static/images/gopher-hero.svg
diff --git a/themes/gohugoioTheme/static/images/gopher-side_color.svg b/docs/themes/gohugoioTheme/static/images/gopher-side_color.svg
index 85f949783..85f949783 100644
--- a/themes/gohugoioTheme/static/images/gopher-side_color.svg
+++ b/docs/themes/gohugoioTheme/static/images/gopher-side_color.svg
diff --git a/themes/gohugoioTheme/static/images/home-page-templating-example.png b/docs/themes/gohugoioTheme/static/images/home-page-templating-example.png
index c97691442..c97691442 100644
--- a/themes/gohugoioTheme/static/images/home-page-templating-example.png
+++ b/docs/themes/gohugoioTheme/static/images/home-page-templating-example.png
Binary files differ
diff --git a/themes/gohugoioTheme/static/images/homepage-screenshot-hugo-themes.jpg b/docs/themes/gohugoioTheme/static/images/homepage-screenshot-hugo-themes.jpg
index 0f60f6a4e..0f60f6a4e 100644
--- a/themes/gohugoioTheme/static/images/homepage-screenshot-hugo-themes.jpg
+++ b/docs/themes/gohugoioTheme/static/images/homepage-screenshot-hugo-themes.jpg
Binary files differ
diff --git a/themes/gohugoioTheme/static/images/homepage-screenshot-hugo-themes_not-optimized-according-to-google.jpg b/docs/themes/gohugoioTheme/static/images/homepage-screenshot-hugo-themes_not-optimized-according-to-google.jpg
index 64424b24e..64424b24e 100644
--- a/themes/gohugoioTheme/static/images/homepage-screenshot-hugo-themes_not-optimized-according-to-google.jpg
+++ b/docs/themes/gohugoioTheme/static/images/homepage-screenshot-hugo-themes_not-optimized-according-to-google.jpg
Binary files differ
diff --git a/themes/gohugoioTheme/static/images/hugo-logo-wide.svg b/docs/themes/gohugoioTheme/static/images/hugo-logo-wide.svg
index 1678b8458..1678b8458 100644
--- a/themes/gohugoioTheme/static/images/hugo-logo-wide.svg
+++ b/docs/themes/gohugoioTheme/static/images/hugo-logo-wide.svg
diff --git a/themes/gohugoioTheme/static/images/icon-built-in-templates.svg b/docs/themes/gohugoioTheme/static/images/icon-built-in-templates.svg
index 40cb249c6..40cb249c6 100644
--- a/themes/gohugoioTheme/static/images/icon-built-in-templates.svg
+++ b/docs/themes/gohugoioTheme/static/images/icon-built-in-templates.svg
diff --git a/themes/gohugoioTheme/static/images/icon-content-management.svg b/docs/themes/gohugoioTheme/static/images/icon-content-management.svg
index e6df93b9d..e6df93b9d 100644
--- a/themes/gohugoioTheme/static/images/icon-content-management.svg
+++ b/docs/themes/gohugoioTheme/static/images/icon-content-management.svg
diff --git a/themes/gohugoioTheme/static/images/icon-fast.svg b/docs/themes/gohugoioTheme/static/images/icon-fast.svg
index 0db21fce1..0db21fce1 100644
--- a/themes/gohugoioTheme/static/images/icon-fast.svg
+++ b/docs/themes/gohugoioTheme/static/images/icon-fast.svg
diff --git a/themes/gohugoioTheme/static/images/icon-multilingual.svg b/docs/themes/gohugoioTheme/static/images/icon-multilingual.svg
index 2ac859285..2ac859285 100644
--- a/themes/gohugoioTheme/static/images/icon-multilingual.svg
+++ b/docs/themes/gohugoioTheme/static/images/icon-multilingual.svg
diff --git a/themes/gohugoioTheme/static/images/icon-multilingual2.svg b/docs/themes/gohugoioTheme/static/images/icon-multilingual2.svg
index a65c77208..a65c77208 100644
--- a/themes/gohugoioTheme/static/images/icon-multilingual2.svg
+++ b/docs/themes/gohugoioTheme/static/images/icon-multilingual2.svg
diff --git a/themes/gohugoioTheme/static/images/icon-search.png b/docs/themes/gohugoioTheme/static/images/icon-search.png
index 2eb9c504e..2eb9c504e 100644
--- a/themes/gohugoioTheme/static/images/icon-search.png
+++ b/docs/themes/gohugoioTheme/static/images/icon-search.png
Binary files differ
diff --git a/themes/gohugoioTheme/static/images/icon-shortcodes.svg b/docs/themes/gohugoioTheme/static/images/icon-shortcodes.svg
index b5cc252d6..b5cc252d6 100644
--- a/themes/gohugoioTheme/static/images/icon-shortcodes.svg
+++ b/docs/themes/gohugoioTheme/static/images/icon-shortcodes.svg
diff --git a/themes/gohugoioTheme/static/images/site-hierarchy.svg b/docs/themes/gohugoioTheme/static/images/site-hierarchy.svg
index 7d1a043e8..7d1a043e8 100644
--- a/themes/gohugoioTheme/static/images/site-hierarchy.svg
+++ b/docs/themes/gohugoioTheme/static/images/site-hierarchy.svg
diff --git a/themes/gohugoioTheme/static/images/sponsors/esolia-logo.svg b/docs/themes/gohugoioTheme/static/images/sponsors/esolia-logo.svg
index 3f5344c61..3f5344c61 100644
--- a/themes/gohugoioTheme/static/images/sponsors/esolia-logo.svg
+++ b/docs/themes/gohugoioTheme/static/images/sponsors/esolia-logo.svg
diff --git a/themes/gohugoioTheme/static/images/sponsors/forestry-logotype.svg b/docs/themes/gohugoioTheme/static/images/sponsors/forestry-logotype.svg
index ac95cd444..ac95cd444 100644
--- a/themes/gohugoioTheme/static/images/sponsors/forestry-logotype.svg
+++ b/docs/themes/gohugoioTheme/static/images/sponsors/forestry-logotype.svg
diff --git a/themes/gohugoioTheme/static/images/sponsors/linode-logo_standard_light_medium.png b/docs/themes/gohugoioTheme/static/images/sponsors/linode-logo_standard_light_medium.png
index 22daa6612..22daa6612 100644
--- a/themes/gohugoioTheme/static/images/sponsors/linode-logo_standard_light_medium.png
+++ b/docs/themes/gohugoioTheme/static/images/sponsors/linode-logo_standard_light_medium.png
Binary files differ
diff --git a/themes/gohugoioTheme/static/manifest.json b/docs/themes/gohugoioTheme/static/manifest.json
index e671ac45a..e671ac45a 100644
--- a/themes/gohugoioTheme/static/manifest.json
+++ b/docs/themes/gohugoioTheme/static/manifest.json
diff --git a/themes/gohugoioTheme/static/mstile-144x144.png b/docs/themes/gohugoioTheme/static/mstile-144x144.png
index e54b4bd75..e54b4bd75 100644
--- a/themes/gohugoioTheme/static/mstile-144x144.png
+++ b/docs/themes/gohugoioTheme/static/mstile-144x144.png
Binary files differ
diff --git a/themes/gohugoioTheme/static/mstile-150x150.png b/docs/themes/gohugoioTheme/static/mstile-150x150.png
index c7b84c690..c7b84c690 100644
--- a/themes/gohugoioTheme/static/mstile-150x150.png
+++ b/docs/themes/gohugoioTheme/static/mstile-150x150.png
Binary files differ
diff --git a/themes/gohugoioTheme/static/mstile-310x310.png b/docs/themes/gohugoioTheme/static/mstile-310x310.png
index 2cde5c08c..2cde5c08c 100644
--- a/themes/gohugoioTheme/static/mstile-310x310.png
+++ b/docs/themes/gohugoioTheme/static/mstile-310x310.png
Binary files differ
diff --git a/themes/gohugoioTheme/static/safari-pinned-tab.svg b/docs/themes/gohugoioTheme/static/safari-pinned-tab.svg
index 80ff2dae3..80ff2dae3 100644
--- a/themes/gohugoioTheme/static/safari-pinned-tab.svg
+++ b/docs/themes/gohugoioTheme/static/safari-pinned-tab.svg
diff --git a/themes/gohugoioTheme/theme.toml b/docs/themes/gohugoioTheme/theme.toml
index 8d678e7b8..8d678e7b8 100644
--- a/themes/gohugoioTheme/theme.toml
+++ b/docs/themes/gohugoioTheme/theme.toml
diff --git a/themes/gohugoioTheme/webpack.config.js b/docs/themes/gohugoioTheme/webpack.config.js
index 7014be089..7014be089 100644
--- a/themes/gohugoioTheme/webpack.config.js
+++ b/docs/themes/gohugoioTheme/webpack.config.js
diff --git a/docshelper/docs.go b/docshelper/docs.go
new file mode 100644
index 000000000..94cb70dec
--- /dev/null
+++ b/docshelper/docs.go
@@ -0,0 +1,37 @@
+// 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"
+)
+
+// DocProviders contains all DocProviders added to the system.
+var DocProviders = make(map[string]DocProvider)
+
+// AddDocProvider adds or updates the DocProvider for a given name.
+func AddDocProvider(name string, provider DocProvider) {
+ DocProviders[name] = provider
+}
+
+// DocProvider is used to save arbitrary JSON data
+// used for the generation of the documentation.
+type DocProvider func() map[string]interface{}
+
+// MarshalJSON returns a JSON representation of the DocProvider.
+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..7f7b5fc7b
--- /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>&copy; Enthusiastic Hugo User {{ now.Format "2006" }} &middot;
+ Built with <a href="https://github.com/gohugoio/hugo">Hugo</a></p>
+ </div>
+ </div>
+ </footer>
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">&times;</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
new file mode 100644
index 000000000..3ed7f8b48
--- /dev/null
+++ b/examples/blog/static/fonts/FontAwesome.otf
Binary files differ
diff --git a/examples/blog/static/fonts/fontawesome-webfont.eot b/examples/blog/static/fonts/fontawesome-webfont.eot
new file mode 100644
index 000000000..9b6afaedc
--- /dev/null
+++ b/examples/blog/static/fonts/fontawesome-webfont.eot
Binary files differ
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="&#x09;" horiz-adv-x="448" />
+<glyph unicode="&#xa0;" horiz-adv-x="448" />
+<glyph unicode="&#xa8;" horiz-adv-x="1792" />
+<glyph unicode="&#xa9;" horiz-adv-x="1792" />
+<glyph unicode="&#xae;" horiz-adv-x="1792" />
+<glyph unicode="&#xb4;" horiz-adv-x="1792" />
+<glyph unicode="&#xc6;" horiz-adv-x="1792" />
+<glyph unicode="&#xd8;" horiz-adv-x="1792" />
+<glyph unicode="&#x2000;" horiz-adv-x="768" />
+<glyph unicode="&#x2001;" horiz-adv-x="1537" />
+<glyph unicode="&#x2002;" horiz-adv-x="768" />
+<glyph unicode="&#x2003;" horiz-adv-x="1537" />
+<glyph unicode="&#x2004;" horiz-adv-x="512" />
+<glyph unicode="&#x2005;" horiz-adv-x="384" />
+<glyph unicode="&#x2006;" horiz-adv-x="256" />
+<glyph unicode="&#x2007;" horiz-adv-x="256" />
+<glyph unicode="&#x2008;" horiz-adv-x="192" />
+<glyph unicode="&#x2009;" horiz-adv-x="307" />
+<glyph unicode="&#x200a;" horiz-adv-x="85" />
+<glyph unicode="&#x202f;" horiz-adv-x="307" />
+<glyph unicode="&#x205f;" horiz-adv-x="384" />
+<glyph unicode="&#x2122;" horiz-adv-x="1792" />
+<glyph unicode="&#x221e;" horiz-adv-x="1792" />
+<glyph unicode="&#x2260;" horiz-adv-x="1792" />
+<glyph unicode="&#x25fc;" horiz-adv-x="500" d="M0 0z" />
+<glyph unicode="&#xf000;" 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="&#xf001;" 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="&#xf002;" 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="&#xf003;" 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="&#xf004;" 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="&#xf005;" 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="&#xf006;" 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="&#xf007;" 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="&#xf008;" 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="&#xf009;" 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="&#xf00a;" 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="&#xf00b;" 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="&#xf00c;" 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="&#xf00d;" 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="&#xf00e;" 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="&#xf010;" 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="&#xf011;" 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="&#xf012;" 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="&#xf013;" 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="&#xf014;" 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="&#xf015;" 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="&#xf016;" 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="&#xf017;" 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="&#xf018;" 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="&#xf019;" 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="&#xf01a;" 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="&#xf01b;" 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="&#xf01c;" 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="&#xf01d;" 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="&#xf01e;" 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="&#xf021;" 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="&#xf022;" 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="&#xf023;" 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="&#xf024;" 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="&#xf025;" 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="&#xf026;" 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="&#xf027;" 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="&#xf028;" 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="&#xf029;" 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="&#xf02a;" 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="&#xf02b;" 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="&#xf02c;" 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="&#xf02d;" 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="&#xf02e;" 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="&#xf02f;" 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="&#xf030;" 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="&#xf031;" 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="&#xf032;" 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="&#xf033;" 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="&#xf034;" 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="&#xf035;" 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="&#xf036;" 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="&#xf037;" 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="&#xf038;" 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="&#xf039;" 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="&#xf03a;" 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="&#xf03b;" 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="&#xf03c;" 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="&#xf03d;" 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="&#xf03e;" 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="&#xf040;" 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="&#xf041;" 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="&#xf042;" 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="&#xf043;" 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="&#xf044;" 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="&#xf045;" 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="&#xf046;" 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="&#xf047;" 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="&#xf048;" 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="&#xf049;" 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="&#xf04a;" 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="&#xf04b;" 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="&#xf04c;" 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="&#xf04d;" 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="&#xf04e;" 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="&#xf050;" 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="&#xf051;" 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="&#xf052;" 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="&#xf053;" 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="&#xf054;" 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="&#xf055;" 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="&#xf056;" 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="&#xf057;" 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="&#xf058;" 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="&#xf059;" 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="&#xf05a;" 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="&#xf05b;" 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="&#xf05c;" 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="&#xf05d;" 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="&#xf05e;" 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="&#xf060;" 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="&#xf061;" 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="&#xf062;" 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="&#xf063;" 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="&#xf064;" 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="&#xf065;" 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="&#xf066;" 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="&#xf067;" 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="&#xf068;" 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="&#xf069;" 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="&#xf06a;" 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="&#xf06b;" 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="&#xf06c;" 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="&#xf06d;" 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="&#xf06e;" 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="&#xf070;" 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="&#xf071;" 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="&#xf072;" 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="&#xf073;" 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="&#xf074;" 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="&#xf075;" 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="&#xf076;" 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="&#xf077;" 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="&#xf078;" 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="&#xf079;" 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="&#xf07a;" 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="&#xf07b;" 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="&#xf07c;" 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="&#xf07d;" 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="&#xf07e;" 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="&#xf080;" horiz-adv-x="2048" d="M640 640v-512h-256v512h256zM1024 1152v-1024h-256v1024h256zM2048 0v-128h-2048v1536h128v-1408h1920zM1408 896v-768h-256v768h256zM1792 1280v-1152h-256v1152h256z" />
+<glyph unicode="&#xf081;" 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="&#xf082;" 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="&#xf083;" 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="&#xf084;" 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="&#xf085;" 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="&#xf086;" 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="&#xf087;" 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="&#xf088;" 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="&#xf089;" 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="&#xf08a;" 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="&#xf08b;" 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="&#xf08c;" 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="&#xf08d;" 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="&#xf08e;" 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="&#xf090;" 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="&#xf091;" 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="&#xf092;" 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="&#xf093;" 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="&#xf094;" 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="&#xf095;" 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="&#xf096;" 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="&#xf097;" 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="&#xf098;" 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="&#xf099;" 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="&#xf09a;" 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="&#xf09b;" 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="&#xf09c;" 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="&#xf09d;" 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="&#xf09e;" 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="&#xf0a0;" 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="&#xf0a1;" 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="&#xf0a2;" 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="&#xf0a3;" 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="&#xf0a4;" 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="&#xf0a5;" 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="&#xf0a6;" 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="&#xf0a7;" 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="&#xf0a8;" 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="&#xf0a9;" 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="&#xf0aa;" 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="&#xf0ab;" 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="&#xf0ac;" 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="&#xf0ad;" 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="&#xf0ae;" 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="&#xf0b0;" 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="&#xf0b1;" 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="&#xf0b2;" 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="&#xf0c0;" 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="&#xf0c1;" 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="&#xf0c2;" 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="&#xf0c3;" 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="&#xf0c4;" 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="&#xf0c5;" 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="&#xf0c6;" 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="&#xf0c7;" 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="&#xf0c8;" 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="&#xf0c9;" 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="&#xf0ca;" 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="&#xf0cb;" 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="&#xf0cc;" 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="&#xf0cd;" 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="&#xf0ce;" 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="&#xf0d0;" 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="&#xf0d1;" 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="&#xf0d2;" 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="&#xf0d3;" 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="&#xf0d4;" 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="&#xf0d5;" 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="&#xf0d6;" 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="&#xf0d7;" 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="&#xf0d8;" 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="&#xf0d9;" 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="&#xf0da;" 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="&#xf0db;" 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="&#xf0dc;" 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="&#xf0dd;" 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="&#xf0de;" 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="&#xf0e0;" 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="&#xf0e1;" 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="&#xf0e2;" 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="&#xf0e3;" 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="&#xf0e4;" 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="&#xf0e5;" 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="&#xf0e6;" 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="&#xf0e7;" 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="&#xf0e8;" 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="&#xf0e9;" 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="&#xf0ea;" 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="&#xf0eb;" 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="&#xf0ec;" 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="&#xf0ed;" 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="&#xf0ee;" 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="&#xf0f0;" 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="&#xf0f1;" 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="&#xf0f2;" 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="&#xf0f3;" 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="&#xf0f4;" 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="&#xf0f5;" 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="&#xf0f6;" 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="&#xf0f7;" 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="&#xf0f8;" 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="&#xf0f9;" 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="&#xf0fa;" 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="&#xf0fb;" 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="&#xf0fc;" 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="&#xf0fd;" 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="&#xf0fe;" 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="&#xf100;" 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="&#xf101;" 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="&#xf102;" 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="&#xf103;" 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="&#xf104;" 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="&#xf105;" 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="&#xf106;" 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="&#xf107;" 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="&#xf108;" 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="&#xf109;" 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="&#xf10a;" 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="&#xf10b;" 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="&#xf10c;" 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="&#xf10d;" 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="&#xf10e;" 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="&#xf110;" 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="&#xf111;" 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="&#xf112;" 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="&#xf113;" 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="&#xf114;" 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="&#xf115;" 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="&#xf116;" horiz-adv-x="1792" />
+<glyph unicode="&#xf117;" horiz-adv-x="1792" />
+<glyph unicode="&#xf118;" 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="&#xf119;" 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="&#xf11a;" 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="&#xf11b;" 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="&#xf11c;" 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="&#xf11d;" 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="&#xf11e;" 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="&#xf120;" 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="&#xf121;" 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="&#xf122;" 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="&#xf123;" 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="&#xf124;" 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="&#xf125;" 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="&#xf126;" 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="&#xf127;" 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="&#xf128;" 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="&#xf129;" 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="&#xf12a;" 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="&#xf12b;" 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="&#xf12c;" 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="&#xf12d;" 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="&#xf12e;" 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="&#xf130;" 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="&#xf131;" 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="&#xf132;" 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="&#xf133;" 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="&#xf134;" 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="&#xf135;" 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="&#xf136;" 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="&#xf137;" 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="&#xf138;" 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="&#xf139;" 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="&#xf13a;" 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="&#xf13b;" 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="&#xf13c;" 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="&#xf13d;" 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="&#xf13e;" 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="&#xf140;" 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="&#xf141;" 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="&#xf142;" 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="&#xf143;" 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="&#xf144;" 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="&#xf145;" 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="&#xf146;" 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="&#xf147;" 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="&#xf148;" 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="&#xf149;" 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="&#xf14a;" 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="&#xf14b;" 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="&#xf14c;" 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="&#xf14d;" 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="&#xf14e;" 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="&#xf150;" 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="&#xf151;" 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="&#xf152;" 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="&#xf153;" 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="&#xf154;" 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="&#xf155;" 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="&#xf156;" 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="&#xf157;" 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="&#xf158;" 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="&#xf159;" 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="&#xf15a;" 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="&#xf15b;" 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="&#xf15c;" 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="&#xf15d;" 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="&#xf15e;" 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="&#xf160;" 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="&#xf161;" 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="&#xf162;" 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="&#xf163;" 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="&#xf164;" 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="&#xf165;" 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="&#xf166;" 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="&#xf167;" 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="&#xf168;" 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="&#xf169;" 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="&#xf16a;" 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="&#xf16b;" 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="&#xf16c;" 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="&#xf16d;" 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="&#xf16e;" 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="&#xf170;" 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="&#xf171;" 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="&#xf172;" 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="&#xf173;" 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="&#xf174;" 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="&#xf175;" 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="&#xf176;" 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="&#xf177;" 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="&#xf178;" 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="&#xf179;" 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="&#xf17a;" horiz-adv-x="1664" d="M682 530v-651l-682 94v557h682zM682 1273v-659h-682v565zM1664 530v-786l-907 125v661h907zM1664 1408v-794h-907v669z" />
+<glyph unicode="&#xf17b;" 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="&#xf17c;" 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="&#xf17d;" 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="&#xf17e;" 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="&#xf180;" 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="&#xf181;" 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="&#xf182;" 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="&#xf183;" 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="&#xf184;" 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="&#xf185;" 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="&#xf186;" 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="&#xf187;" 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="&#xf188;" 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="&#xf189;" 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="&#xf18a;" 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="&#xf18b;" 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="&#xf18c;" 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="&#xf18d;" 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="&#xf18e;" 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="&#xf190;" 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="&#xf191;" 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="&#xf192;" 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="&#xf193;" 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="&#xf194;" 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="&#xf195;" 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="&#xf196;" 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="&#xf197;" 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="&#xf198;" 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="&#xf199;" 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="&#xf19a;" 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="&#xf19b;" 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="&#xf19c;" 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="&#xf19d;" 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="&#xf19e;" 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="&#xf1a0;" 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="&#xf1a1;" 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="&#xf1a2;" 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="&#xf1a3;" 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="&#xf1a4;" 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="&#xf1a5;" 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="&#xf1a6;" 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="&#xf1a7;" 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="&#xf1a8;" 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="&#xf1a9;" 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="&#xf1aa;" 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="&#xf1ab;" 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="&#xf1ac;" 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="&#xf1ad;" 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="&#xf1ae;" 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="&#xf1b0;" 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="&#xf1b1;" 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="&#xf1b2;" 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="&#xf1b3;" 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="&#xf1b4;" 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="&#xf1b5;" 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="&#xf1b6;" 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="&#xf1b7;" 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="&#xf1b8;" 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="&#xf1b9;" 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="&#xf1ba;" 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="&#xf1bb;" 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="&#xf1bc;" 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="&#xf1bd;" 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="&#xf1be;" 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="&#xf1c0;" 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="&#xf1c1;" 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="&#xf1c2;" 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="&#xf1c3;" 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="&#xf1c4;" 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="&#xf1c5;" 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="&#xf1c6;" 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="&#xf1c7;" 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="&#xf1c8;" 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="&#xf1c9;" 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="&#xf1ca;" 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="&#xf1cb;" 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="&#xf1cc;" 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="&#xf1cd;" 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="&#xf1ce;" 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="&#xf1d0;" 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="&#xf1d1;" 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="&#xf1d2;" 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="&#xf1d3;" 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="&#xf1d4;" 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="&#xf1d5;" 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="&#xf1d6;" 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="&#xf1d7;" 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="&#xf1d8;" 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="&#xf1d9;" 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="&#xf1da;" 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="&#xf1db;" 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="&#xf1dc;" 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="&#xf1dd;" 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="&#xf1de;" 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="&#xf1e0;" 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="&#xf1e1;" 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="&#xf1e2;" 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="&#xf1e3;" 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="&#xf1e4;" 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="&#xf1e5;" 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="&#xf1e6;" 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="&#xf1e7;" 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="&#xf1e8;" 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="&#xf1e9;" 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="&#xf1ea;" 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="&#xf1eb;" 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="&#xf1ec;" 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="&#xf1ed;" 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="&#xf1ee;" 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="&#xf1f0;" 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="&#xf1f1;" 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="&#xf1f2;" 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="&#xf1f3;" 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="&#xf1f4;" 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="&#xf1f5;" 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="&#xf1f6;" 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="&#xf1f7;" 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="&#xf1f8;" 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="&#xf1f9;" 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="&#xf1fa;" 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="&#xf1fb;" 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="&#xf1fc;" 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="&#xf1fd;" 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="&#xf1fe;" horiz-adv-x="2048" d="M2048 0v-128h-2048v1536h128v-1408h1920zM1664 1024l256 -896h-1664v576l448 576l576 -576z" />
+<glyph unicode="&#xf200;" 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="&#xf201;" 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="&#xf202;" 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="&#xf203;" 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="&#xf204;" 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="&#xf205;" 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="&#xf206;" 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="&#xf207;" 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="&#xf208;" 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="&#xf209;" 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="&#xf20a;" 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="&#xf20b;" 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="&#xf20c;" 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="&#xf20d;" 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="&#xf20e;" 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="&#xf210;" 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="&#xf211;" 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="&#xf212;" 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="&#xf213;" 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="&#xf214;" 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="&#xf215;" 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="&#xf216;" 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="&#xf217;" 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="&#xf218;" 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="&#xf219;" 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="&#xf21a;" 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="&#xf21b;" 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="&#xf21c;" 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="&#xf21d;" 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="&#xf21e;" 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="&#xf221;" 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="&#xf222;" 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="&#xf223;" 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="&#xf224;" 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="&#xf225;" 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="&#xf226;" 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="&#xf227;" 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="&#xf228;" 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="&#xf229;" 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="&#xf22a;" 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="&#xf22b;" 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="&#xf22c;" 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="&#xf22d;" 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="&#xf22e;" horiz-adv-x="1792" />
+<glyph unicode="&#xf22f;" horiz-adv-x="1792" />
+<glyph unicode="&#xf230;" 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="&#xf231;" 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="&#xf232;" 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="&#xf233;" 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="&#xf234;" 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="&#xf235;" 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="&#xf236;" 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="&#xf237;" 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="&#xf238;" 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="&#xf239;" 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="&#xf23a;" 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="&#xf23b;" d="M809 532l266 499h-112l-157 -312q-24 -48 -44 -92l-42 92l-155 312h-120l263 -493v-324h101v318zM1536 1408v-1536h-1536v1536h1536z" />
+<glyph unicode="&#xf23c;" 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="&#xf23d;" 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="&#xf23e;" 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="&#xf240;" 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="&#xf241;" 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="&#xf242;" 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="&#xf243;" 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="&#xf244;" 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="&#xf245;" 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="&#xf246;" 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="&#xf247;" 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="&#xf248;" 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="&#xf249;" 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="&#xf24a;" 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="&#xf24b;" 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="&#xf24c;" 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="&#xf24d;" 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="&#xf24e;" 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="&#xf250;" 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="&#xf251;" 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="&#xf252;" 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="&#xf253;" 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="&#xf254;" 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="&#xf255;" 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="&#xf256;" 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="&#xf257;" 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="&#xf258;" 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="&#xf259;" 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="&#xf25a;" 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="&#xf25b;" 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="&#xf25c;" 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="&#xf25d;" 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="&#xf25e;" 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="&#xf260;" 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="&#xf261;" 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="&#xf262;" 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="&#xf263;" 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="&#xf264;" 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="&#xf265;" 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="&#xf266;" 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="&#xf267;" 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="&#xf268;" 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="&#xf269;" 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="&#xf26a;" 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="&#xf26b;" 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="&#xf26c;" 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="&#xf26d;" 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="&#xf26e;" 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="&#xf270;" 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="&#xf271;" 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="&#xf272;" 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="&#xf273;" 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="&#xf274;" 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="&#xf275;" 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="&#xf276;" 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="&#xf277;" 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="&#xf278;" 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="&#xf279;" 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="&#xf27a;" 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="&#xf27b;" 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="&#xf27c;" horiz-adv-x="1024" d="M512 345l512 295v-591l-512 -296v592zM0 640v-591l512 296zM512 1527v-591l-512 -296v591zM512 936l512 295v-591z" />
+<glyph unicode="&#xf27d;" 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="&#xf27e;" d="M0 1408h1536v-1536h-1536v1536zM1085 293l-221 631l221 297h-634l221 -297l-221 -631l317 -304z" />
+<glyph unicode="&#xf280;" 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="&#xf281;" 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="&#xf282;" 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="&#xf283;" 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="&#xf284;" 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="&#xf285;" 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="&#xf286;" 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="&#xf287;" 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="&#xf288;" 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="&#xf289;" 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="&#xf28a;" 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="&#xf28b;" 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="&#xf28c;" 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="&#xf28d;" 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="&#xf28e;" 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="&#xf290;" 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="&#xf291;" 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="&#xf292;" 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="&#xf293;" 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="&#xf294;" 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="&#xf295;" 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="&#xf296;" horiz-adv-x="1792" />
+<glyph unicode="&#xf297;" horiz-adv-x="1792" />
+<glyph unicode="&#xf298;" horiz-adv-x="1792" />
+<glyph unicode="&#xf299;" horiz-adv-x="1792" />
+<glyph unicode="&#xf29a;" horiz-adv-x="1792" />
+<glyph unicode="&#xf29b;" horiz-adv-x="1792" />
+<glyph unicode="&#xf29c;" horiz-adv-x="1792" />
+<glyph unicode="&#xf29d;" horiz-adv-x="1792" />
+<glyph unicode="&#xf29e;" horiz-adv-x="1792" />
+<glyph unicode="&#xf500;" 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
new file mode 100644
index 000000000..26dea7951
--- /dev/null
+++ b/examples/blog/static/fonts/fontawesome-webfont.ttf
Binary files differ
diff --git a/examples/blog/static/fonts/fontawesome-webfont.woff b/examples/blog/static/fonts/fontawesome-webfont.woff
new file mode 100644
index 000000000..dc35ce3c2
--- /dev/null
+++ b/examples/blog/static/fonts/fontawesome-webfont.woff
Binary files differ
diff --git a/examples/blog/static/fonts/fontawesome-webfont.woff2 b/examples/blog/static/fonts/fontawesome-webfont.woff2
new file mode 100644
index 000000000..500e51725
--- /dev/null
+++ b/examples/blog/static/fonts/fontawesome-webfont.woff2
Binary files differ
diff --git a/examples/blog/static/fonts/glyphicons-halflings-regular.eot b/examples/blog/static/fonts/glyphicons-halflings-regular.eot
new file mode 100644
index 000000000..b93a4953f
--- /dev/null
+++ b/examples/blog/static/fonts/glyphicons-halflings-regular.eot
Binary files differ
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="&#xa0;" />
+<glyph unicode="&#xa5;" 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="&#x2000;" horiz-adv-x="650" />
+<glyph unicode="&#x2001;" horiz-adv-x="1300" />
+<glyph unicode="&#x2002;" horiz-adv-x="650" />
+<glyph unicode="&#x2003;" horiz-adv-x="1300" />
+<glyph unicode="&#x2004;" horiz-adv-x="433" />
+<glyph unicode="&#x2005;" horiz-adv-x="325" />
+<glyph unicode="&#x2006;" horiz-adv-x="216" />
+<glyph unicode="&#x2007;" horiz-adv-x="216" />
+<glyph unicode="&#x2008;" horiz-adv-x="162" />
+<glyph unicode="&#x2009;" horiz-adv-x="260" />
+<glyph unicode="&#x200a;" horiz-adv-x="72" />
+<glyph unicode="&#x202f;" horiz-adv-x="260" />
+<glyph unicode="&#x205f;" horiz-adv-x="325" />
+<glyph unicode="&#x20ac;" 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="&#x20bd;" 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="&#x2212;" 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="&#x231b;" 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="&#x25fc;" horiz-adv-x="500" d="M0 0z" />
+<glyph unicode="&#x2601;" 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="&#x26fa;" 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="&#x2709;" 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="&#x270f;" 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="&#xe001;" 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="&#xe002;" 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="&#xe003;" 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="&#xe005;" 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="&#xe006;" 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="&#xe007;" 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="&#xe008;" 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="&#xe009;" 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="&#xe010;" 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="&#xe011;" 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="&#xe012;" 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="&#xe013;" 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="&#xe014;" 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="&#xe015;" 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="&#xe016;" 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="&#xe017;" 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="&#xe018;" 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="&#xe019;" 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="&#xe020;" 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="&#xe021;" 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="&#xe022;" 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="&#xe023;" 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="&#xe024;" d="M1300 0h-538l-41 400h-242l-41 -400h-538l431 1200h209l-21 -300h162l-20 300h208zM515 800l-27 -300h224l-27 300h-170z" />
+<glyph unicode="&#xe025;" 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="&#xe026;" 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="&#xe027;" 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="&#xe028;" 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="&#xe029;" 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="&#xe030;" 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="&#xe031;" 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="&#xe032;" 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="&#xe033;" 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="&#xe034;" 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="&#xe035;" 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="&#xe036;" 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="&#xe037;" 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="&#xe038;" 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="&#xe039;" 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="&#xe040;" 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="&#xe041;" 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="&#xe042;" 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="&#xe043;" 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="&#xe044;" 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="&#xe045;" 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="&#xe046;" 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="&#xe047;" 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="&#xe048;" 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="&#xe049;" 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="&#xe050;" 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="&#xe051;" 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="&#xe052;" 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="&#xe053;" 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="&#xe054;" 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="&#xe055;" 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="&#xe056;" 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="&#xe057;" 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="&#xe058;" 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="&#xe059;" 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="&#xe060;" 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="&#xe062;" 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="&#xe063;" 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="&#xe064;" 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="&#xe065;" 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="&#xe066;" 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="&#xe067;" 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="&#xe068;" 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="&#xe069;" 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="&#xe070;" 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="&#xe071;" 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="&#xe072;" 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="&#xe073;" 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="&#xe074;" 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="&#xe075;" 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="&#xe076;" 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="&#xe077;" 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="&#xe078;" 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="&#xe079;" d="M885 900l-352 -353l352 -353l-197 -198l-552 552l552 550z" />
+<glyph unicode="&#xe080;" d="M1064 547l-551 -551l-198 198l353 353l-353 353l198 198z" />
+<glyph unicode="&#xe081;" 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="&#xe082;" 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="&#xe083;" 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="&#xe084;" 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="&#xe085;" 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="&#xe086;" 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="&#xe087;" 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="&#xe088;" 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="&#xe089;" 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="&#xe090;" 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="&#xe091;" 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="&#xe092;" 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="&#xe093;" 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="&#xe094;" 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="&#xe095;" 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="&#xe096;" 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="&#xe097;" 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="&#xe101;" 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="&#xe102;" 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="&#xe103;" 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="&#xe104;" 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="&#xe105;" 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="&#xe106;" 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="&#xe107;" 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="&#xe108;" 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="&#xe109;" 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="&#xe110;" 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="&#xe111;" 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="&#xe112;" 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="&#xe113;" d="M1100 411l-198 -199l-353 353l-353 -353l-197 199l551 551z" />
+<glyph unicode="&#xe114;" d="M1101 789l-550 -551l-551 551l198 199l353 -353l353 353z" />
+<glyph unicode="&#xe115;" 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="&#xe116;" 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="&#xe117;" 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="&#xe118;" 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="&#xe119;" 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="&#xe120;" 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="&#xe121;" 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="&#xe122;" 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="&#xe123;" 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="&#xe124;" 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="&#xe125;" 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="&#xe126;" 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="&#xe127;" 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="&#xe128;" 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="&#xe129;" 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="&#xe130;" 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="&#xe131;" 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="&#xe132;" 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="&#xe133;" 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="&#xe134;" 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="&#xe135;" 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="&#xe136;" 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="&#xe137;" 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="&#xe138;" 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="&#xe139;" 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="&#xe140;" 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="&#xe141;" 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="&#xe142;" 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="&#xe143;" 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="&#xe144;" 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="&#xe145;" 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="&#xe146;" 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="&#xe148;" 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="&#xe149;" 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="&#xe150;" 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="&#xe151;" 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="&#xe152;" 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="&#xe153;" 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="&#xe154;" 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="&#xe155;" 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="&#xe156;" 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="&#xe157;" 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="&#xe158;" 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="&#xe159;" 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="&#xe160;" 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="&#xe161;" 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="&#xe162;" 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="&#xe163;" 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="&#xe164;" 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="&#xe165;" 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="&#xe166;" 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="&#xe167;" 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="&#xe168;" 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="&#xe169;" 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="&#xe170;" 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="&#xe171;" d="M1200 103l-483 276l-314 -399v423h-399l1196 796v-1096zM483 424v-230l683 953z" />
+<glyph unicode="&#xe172;" 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="&#xe173;" 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="&#xe174;" 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="&#xe175;" 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="&#xe176;" 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="&#xe177;" 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="&#xe178;" 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="&#xe179;" 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="&#xe180;" 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="&#xe181;" 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="&#xe182;" 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="&#xe183;" 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="&#xe184;" 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="&#xe185;" 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="&#xe186;" 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="&#xe187;" 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="&#xe188;" 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="&#xe189;" 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="&#xe190;" 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="&#xe191;" 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="&#xe192;" 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="&#xe193;" 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="&#xe194;" 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="&#xe195;" 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="&#xe197;" 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="&#xe198;" 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="&#xe199;" 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="&#xe200;" 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="&#xe201;" 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="&#xe202;" 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="&#xe203;" 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="&#xe204;" 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="&#xe205;" 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="&#xe206;" 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="&#xe209;" 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="&#xe210;" 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="&#xe211;" 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="&#xe212;" 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="&#xe213;" 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="&#xe214;" 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="&#xe215;" 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="&#xe216;" 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="&#xe218;" 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="&#xe219;" 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="&#xe221;" 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="&#xe223;" 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="&#xe224;" 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="&#xe225;" 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="&#xe226;" 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="&#xe227;" 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="&#xe230;" 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="&#xe231;" 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="&#xe232;" 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="&#xe233;" 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="&#xe234;" 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="&#xe235;" 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="&#xe236;" 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="&#xe237;" 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="&#xe238;" 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="&#xe239;" 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="&#xe240;" 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="&#xe241;" 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="&#xe242;" 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="&#xe243;" 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="&#xe244;" 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="&#xe245;" 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="&#xe246;" 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="&#xe247;" 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="&#xe248;" 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="&#xe249;" 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="&#xe250;" 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="&#xe251;" 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="&#xe252;" 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="&#xe253;" 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="&#xe254;" d="M700 741v-182l-692 -323v221l413 193l-413 193v221zM1200 0h-800v200h800v-200z" />
+<glyph unicode="&#xe255;" 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="&#xe256;" 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="&#xe257;" 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="&#xe258;" 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="&#xe259;" 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="&#xe260;" 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="&#xf8ff;" 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="&#x1f511;" 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="&#x1f6aa;" 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
new file mode 100644
index 000000000..1413fc609
--- /dev/null
+++ b/examples/blog/static/fonts/glyphicons-halflings-regular.ttf
Binary files differ
diff --git a/examples/blog/static/fonts/glyphicons-halflings-regular.woff b/examples/blog/static/fonts/glyphicons-halflings-regular.woff
new file mode 100644
index 000000000..9e612858f
--- /dev/null
+++ b/examples/blog/static/fonts/glyphicons-halflings-regular.woff
Binary files differ
diff --git a/examples/blog/static/fonts/glyphicons-halflings-regular.woff2 b/examples/blog/static/fonts/glyphicons-halflings-regular.woff2
new file mode 100644
index 000000000..64539b54c
--- /dev/null
+++ b/examples/blog/static/fonts/glyphicons-halflings-regular.woff2
Binary files differ
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">&copy;</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/go.mod b/go.mod
new file mode 100644
index 000000000..5189e9b7a
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,60 @@
+module github.com/gohugoio/hugo
+
+require (
+ github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69
+ github.com/BurntSushi/toml v0.3.1
+ github.com/PuerkitoBio/purell v1.1.0
+ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
+ github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38
+ github.com/alecthomas/chroma v0.6.3
+ github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 // indirect
+ github.com/aws/aws-sdk-go v1.16.23
+ github.com/bep/debounce v1.2.0
+ github.com/bep/gitmap v1.0.0
+ github.com/bep/go-tocss v0.6.0
+ github.com/chaseadamsio/goorgeous v1.1.0
+ github.com/cpuguy83/go-md2man v1.0.8 // indirect
+ github.com/disintegration/imaging v1.6.0
+ github.com/dustin/go-humanize v1.0.0
+ github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385
+ github.com/fortytw2/leaktest v1.2.0
+ github.com/fsnotify/fsnotify v1.4.7
+ github.com/gobwas/glob v0.2.3
+ github.com/google/go-cmp v0.2.0
+ github.com/gorilla/websocket v1.4.0
+ github.com/hashicorp/go-immutable-radix v1.0.0
+ github.com/jdkato/prose v1.1.0
+ github.com/kyokomi/emoji v1.5.1
+ github.com/magefile/mage v1.4.0
+ github.com/markbates/inflect v1.0.0
+ github.com/mattn/go-isatty v0.0.7
+ github.com/miekg/mmark v1.3.6
+ github.com/mitchellh/hashstructure v1.0.0
+ github.com/mitchellh/mapstructure v1.1.2
+ github.com/muesli/smartcrop v0.0.0-20180228075044-f6ebaa786a12
+ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
+ github.com/nicksnyder/go-i18n v1.10.0
+ github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84
+ github.com/pkg/errors v0.8.1
+ github.com/russross/blackfriday v1.5.2
+ github.com/sanity-io/litter v1.1.0
+ github.com/spf13/afero v1.2.2
+ github.com/spf13/cast v1.3.0
+ github.com/spf13/cobra v0.0.3
+ github.com/spf13/fsync v0.0.0-20170320142552-12a01e648f05
+ github.com/spf13/jwalterweatherman v1.1.0
+ github.com/spf13/pflag v1.0.3
+ github.com/spf13/viper v1.3.2
+ github.com/stretchr/testify v1.3.0
+ github.com/tdewolff/minify/v2 v2.3.7
+ github.com/yosssi/ace v0.0.5
+ gocloud.dev v0.13.0
+ golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f
+ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6
+ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2
+ gopkg.in/yaml.v2 v2.2.2
+)
+
+exclude github.com/chaseadamsio/goorgeous v2.0.0+incompatible
+
+replace github.com/markbates/inflect => github.com/markbates/inflect v0.0.0-20171215194931-a12c3aec81a6
diff --git a/go.sum b/go.sum
new file mode 100644
index 000000000..36cfdbece
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,745 @@
+cloud.google.com/go v0.23.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.37.0 h1:69FNAINiZfsEuwH3fKq8QrAAnHz+2m4XL4kVYi5BX0Q=
+cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
+contrib.go.opencensus.io/exporter/aws v0.0.0-20181029163544-2befc13012d0/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA=
+contrib.go.opencensus.io/exporter/ocagent v0.4.2/go.mod h1:YuG83h+XWwqWjvCqn7vK4KSyLKhThY3+gNGQ37iS2V0=
+contrib.go.opencensus.io/exporter/stackdriver v0.9.1/go.mod h1:hNe5qQofPbg6bLQY5wHCvQ7o+2E5P8PkegEuQ+MyRw0=
+contrib.go.opencensus.io/integrations/ocsql v0.1.3/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE=
+dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
+dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
+dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
+dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
+git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
+git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
+github.com/Azure/azure-amqp-common-go v1.1.3/go.mod h1:FhZtXirFANw40UXI2ntweO+VOkfaw8s6vZxUiRhLYW8=
+github.com/Azure/azure-amqp-common-go v1.1.4/go.mod h1:FhZtXirFANw40UXI2ntweO+VOkfaw8s6vZxUiRhLYW8=
+github.com/Azure/azure-pipeline-go v0.1.8 h1:KmVRa8oFMaargVesEuuEoiLCQ4zCCwQ8QX/xg++KS20=
+github.com/Azure/azure-pipeline-go v0.1.8/go.mod h1:XA1kFWRVhSK+KNFiOhfv83Fv8L9achrP7OxIzeTn1Yg=
+github.com/Azure/azure-sdk-for-go v21.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/azure-sdk-for-go v26.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/azure-service-bus-go v0.2.0/go.mod h1:auph+otjChRM1T34fA7a3JDAcUeMEPZXs3F21VHJwqI=
+github.com/Azure/azure-storage-blob-go v0.0.0-20190104215108-45d0c5e3638e h1:/xcR+zx0sJ4viF6JHLbEObtCw8edFtY6cCEqZqV+vA0=
+github.com/Azure/azure-storage-blob-go v0.0.0-20190104215108-45d0c5e3638e/go.mod h1:oGfmITT1V6x//CswqY2gtAHND+xIP64/qL7a5QJix0Y=
+github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
+github.com/Azure/go-autorest v11.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+github.com/Azure/go-autorest v11.1.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+github.com/Azure/go-autorest v11.3.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU=
+github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg=
+github.com/BurntSushi/toml v0.0.0-20170626110600-a368813c5e64 h1:BuYewlQyh/jroxY8qx41SrzD8Go17GkyCyAeVmprvQI=
+github.com/BurntSushi/toml v0.0.0-20170626110600-a368813c5e64/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/DataDog/datadog-go v0.0.0-20180822151419-281ae9f2d895/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
+github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20181215173202-6f1ecdcf9588/go.mod h1:aJ4qN3TfrelA6NZ6AXsXRfmEVaYin3EDbSPJrKS8OXo=
+github.com/Jeffail/gabs v1.1.1/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc=
+github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
+github.com/Masterminds/sprig v2.16.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
+github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
+github.com/NYTimes/gziphandler v1.0.1/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
+github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
+github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/RoaringBitmap/roaring v0.4.16/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
+github.com/SAP/go-hdb v0.13.1/go.mod h1:etBT+FAi1t5k3K3tf5vQTnosgYmhDkRi8jEnQqCnxF0=
+github.com/SAP/go-hdb v0.13.2/go.mod h1:etBT+FAi1t5k3K3tf5vQTnosgYmhDkRi8jEnQqCnxF0=
+github.com/SermoDigital/jose v0.9.1/go.mod h1:ARgCUhI1MHQH+ONky/PAtmVHQrP5JlGY0F3poXOp/fA=
+github.com/SermoDigital/jose v0.9.2-0.20161205224733-f6df55f235c2/go.mod h1:ARgCUhI1MHQH+ONky/PAtmVHQrP5JlGY0F3poXOp/fA=
+github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
+github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
+github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
+github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
+github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
+github.com/alecthomas/chroma v0.6.3 h1:8H1D0yddf0mvgvO4JDBKnzLd9ERmzzAijBxnZXGV/FA=
+github.com/alecthomas/chroma v0.6.3/go.mod h1:quT2EpvJNqkuPi6DmBHB+E33FXBgBBPzyH5++Dn1LPc=
+github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
+github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
+github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
+github.com/alecthomas/kong v0.1.15/go.mod h1:0m2VYms8rH0qbCqVB2gvGHk74bqLIq0HXjCs5bNbNQU=
+github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
+github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
+github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 h1:GDQdwm/gAcJcLAKQQZGOJ4knlw+7rfEQQcmwTbt4p5E=
+github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190122153857-e0ace9b64d22/go.mod h1:T9M45xf79ahXVelWoOBmH0y4aC1t5kXO5BxwyakgIGA=
+github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
+github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
+github.com/apache/arrow/go/arrow v0.0.0-20181031164735-a56c009257a7/go.mod h1:GjvccvtI06FGFvRU1In/maF7tKp3h7GBV9Sexo5rNPM=
+github.com/apache/arrow/go/arrow v0.0.0-20181217213538-e9ed591db9cb/go.mod h1:GjvccvtI06FGFvRU1In/maF7tKp3h7GBV9Sexo5rNPM=
+github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
+github.com/apex/log v1.1.0/go.mod h1:yA770aXIDQrhVOIGurT/pVdfCpSq1GQV/auzMN5fzvY=
+github.com/araddon/gou v0.0.0-20190110011759-c797efecbb61/go.mod h1:ikc1XA58M+Rx7SEbf0bLJCfBkwayZ8T5jBo5FXK8Uz8=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
+github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
+github.com/aws/aws-sdk-go v1.15.31/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
+github.com/aws/aws-sdk-go v1.15.59/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM=
+github.com/aws/aws-sdk-go v1.15.64/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM=
+github.com/aws/aws-sdk-go v1.16.23 h1:MwBOBeez0XEFVh6DCc888X+nHVBCjUDLnnWXSGGWUgM=
+github.com/aws/aws-sdk-go v1.16.23/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/benbjohnson/tmpl v1.0.0/go.mod h1:igT620JFIi44B6awvU9IsDhR77IXWtFigTLil/RPdps=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/bep/debounce v1.2.0 h1:wXds8Kq8qRfwAOpAxHrJDbCXgC5aHSzgQb/0gKsHQqo=
+github.com/bep/debounce v1.2.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
+github.com/bep/gitmap v1.0.0 h1:cTTZwq7vpGuhwefKCBDV9UrHnZAPVJTvoWobimrqkUc=
+github.com/bep/gitmap v1.0.0/go.mod h1:g9VRETxFUXNWzMiuxOwcudo6DfZkW9jOsOW0Ft4kYaY=
+github.com/bep/go-tocss v0.6.0 h1:lJf+nIjsQDpifUr+NgHi9QMBnrr9cFvMvEBT+uV9Q9E=
+github.com/bep/go-tocss v0.6.0/go.mod h1:d9d3crzlTl+PUZLFzBUjfFCpp68K+ku10mzTlnqU/+A=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
+github.com/blakesmith/ar v0.0.0-20150311145944-8bd4349a67f2/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
+github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
+github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
+github.com/bouk/httprouter v0.0.0-20160817010721-ee8b3818a7f5/go.mod h1:CDReaxg1cmLrtcasZy43l4EYPAknXLiQSrb7tLw5zXM=
+github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
+github.com/briankassouf/jose v0.9.1/go.mod h1:HQhVmdUf7dBNwIIdBTivnCDxcf6IZY3/zrb+uKSJz6Y=
+github.com/c-bata/go-prompt v0.2.2/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34=
+github.com/caarlos0/ctrlc v1.0.0/go.mod h1:CdXpj4rmq0q/1Eb44M9zi2nKB0QraNKuRGYGrrHhcQw=
+github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e/go.mod h1:9IOqJGCPMSc6E5ydlp5NIonxObaeu/Iub/X03EKPVYo=
+github.com/cenkalti/backoff v2.0.0+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
+github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
+github.com/census-instrumentation/opencensus-proto v0.1.0-0.20181214143942-ba49f56771b8/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/census-instrumentation/opencensus-proto v0.1.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/centrify/cloud-golang-sdk v0.0.0-20180119173102-7c97cc6fde16/go.mod h1:C0rtzmGXgN78pYR0tGJFhtHgkbAs0lIbHwkB81VxDQE=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/chaseadamsio/goorgeous v1.1.0 h1:J9UrYDhzucUMHXsCKG+kICvpR5dT1cqZdVFTYvSlUBk=
+github.com/chaseadamsio/goorgeous v1.1.0/go.mod h1:6QaC0vFoKWYDth94dHFNgRT2YkT5FHdQp/Yx15aAAi0=
+github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 h1:SKI1/fuSdodxmNNyVBR8d7X/HuLnRpvvFO0AgyQk764=
+github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=
+github.com/chrismalek/oktasdk-go v0.0.0-20181212195951-3430665dfaa0/go.mod h1:5d8DqS60xkj9k3aXfL3+mXBH0DPYO0FQjcKosxl+b/Q=
+github.com/circonus-labs/circonus-gometrics v2.2.5+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
+github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
+github.com/containerd/continuity v0.0.0-20181027224239-bea7585dbfac/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
+github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
+github.com/coreos/bbolt v1.3.1-coreos.6/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/etcd v3.3.11+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
+github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man v1.0.8 h1:DwoNytLphI8hzS2Af4D0dfaEaiSq2bN05mEm4R6vf8M=
+github.com/cpuguy83/go-md2man v1.0.8/go.mod h1:N6JayAiVKtlHSnuTCeuLSQVs75hb8q+dYQLjr7cDsKY=
+github.com/dancannon/gorethink v4.0.0+incompatible/go.mod h1:BLvkat9KmZc1efyYwhz3WnybhRZtgF1K929FD8z1avU=
+github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
+github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
+github.com/denisenkom/go-mssqldb v0.0.0-20190121005146-b04fd42d9952/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-bitstream v0.0.0-20180413035011-3522498ce2c8/go.mod h1:VMaSuZ+SZcx/wljOQKvp5srsbCiKDEb6K2wC4+PiBmQ=
+github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
+github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA=
+github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
+github.com/dlclark/regexp2 v1.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg=
+github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
+github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
+github.com/docker/distribution v2.6.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v0.0.0-20180422163414-57142e89befe/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/duosecurity/duo_api_golang v0.0.0-20181024123116-92fea9203dbc/go.mod h1:UqXY1lYT/ERa4OEAywUqdok1T4RCRdArkhic1Opuavo=
+github.com/duosecurity/duo_api_golang v0.0.0-20190107154727-539434bf0d45/go.mod h1:UqXY1lYT/ERa4OEAywUqdok1T4RCRdArkhic1Opuavo=
+github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
+github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
+github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
+github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o=
+github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
+github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
+github.com/emirpasic/gods v1.9.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
+github.com/fortytw2/leaktest v1.2.0 h1:cj6GCiwJDH7l3tMHLjZDo0QqPtrXJiWSI9JgpeQKw+Q=
+github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
+github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fullsailor/pkcs7 v0.0.0-20180613152042-8306686428a5/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
+github.com/gammazero/deque v0.0.0-20180920172122-f6adf94963e4/go.mod h1:GeIq9qoE43YdGnDXURnmKTnGg15pQz4mYkXSTChbneI=
+github.com/gammazero/workerpool v0.0.0-20181230203049-86a96b5d5d92/go.mod h1:w9RqFVO2BM3xwWEcAB8Fwp0OviTBBEiRmSBDfbXnd3w=
+github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
+github.com/getkin/kin-openapi v0.1.0/go.mod h1:+0ZtELZf+SlWH8ZdA/IeFb3L/PKOKJx8eGxAlUZ/sOU=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
+github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
+github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
+github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-ldap/ldap v2.5.1+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
+github.com/go-ldap/ldap v3.0.0+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-stomp/stomp v2.0.2+incompatible/go.mod h1:VqCtqNZv1226A1/79yh+rMiFUcfY3R109np+7ke4n0c=
+github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
+github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/gocql/gocql v0.0.0-20181117210152-33c0e89ca93a/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0=
+github.com/gocql/gocql v0.0.0-20190122205811-30de9a1866a8/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/golang/gddo v0.0.0-20181116215533-9bd4a3295021/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
+github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
+github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/subcommands v0.0.0-20181012225330-46f0354f6315/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.0 h1:Jf4mxPC/ziBnoPIdpQdPJ9OeiomAUHLvxmPRSPH9m4s=
+github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/wire v0.2.1 h1:TYj4Z2qjqxa2ufb34UJqVeO9aznL+i0fLO6TqThKZ7Y=
+github.com/google/wire v0.2.1/go.mod h1:ptBl5bWD3nzmJHVNwYHV3v4wdtKzBMlU2YbtKQCG9GI=
+github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
+github.com/googleapis/gax-go v2.0.2+incompatible h1:silFMLAnr330+NRuag/VjIGF7TLp/LBrV2CJKFLWEww=
+github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
+github.com/googleapis/gax-go/v2 v2.0.3 h1:siORttZ36U2R/WjiJuDz8znElWBiAlO9rVt+mqJt0Cc=
+github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/goreleaser/goreleaser v0.94.0/go.mod h1:OjbYR2NhOI6AEUWCowMSBzo9nP1aRif3sYtx+rhp+Zo=
+github.com/goreleaser/nfpm v0.9.7/go.mod h1:F2yzin6cBAL9gb+mSiReuXdsfTrOQwDMsuSpULof+y4=
+github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75/go.mod h1:g2644b03hfBX9Ov0ZBDgXXens4rxSxmqFBbhvKv2yVA=
+github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
+github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
+github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
+github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
+github.com/hashicorp/consul v1.4.0/go.mod h1:mFrjN1mfidgJfYP1xrJCF+AfRhr6Eaqhb2+sfyn/OOI=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-gcp-common v0.0.0-20180425173946-763e39302965/go.mod h1:LNbios2fdMAuLA1dsYUvUcoCYIfywcCEK8/ooaWjoOA=
+github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
+github.com/hashicorp/go-hclog v0.0.0-20181001195459-61d530d6c27f/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
+github.com/hashicorp/go-hclog v0.0.0-20190109152822-4783caec6f2e/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
+github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
+github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-memdb v0.0.0-20181108192425-032f93b25bec/go.mod h1:kbfItVoBJwCfKXDXN4YoAXjxcFVZ7MRrJzyTX6H4giE=
+github.com/hashicorp/go-msgpack v0.0.0-20150518234257-fa3f63826f7c/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
+github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-plugin v0.0.0-20181030172320-54b6ff97d818/go.mod h1:Ft7ju2vWzhO0ETMKUVo12XmXmII6eSUS4rsPTkY/siA=
+github.com/hashicorp/go-plugin v0.0.0-20181212150838-f444068e8f5a/go.mod h1:Ft7ju2vWzhO0ETMKUVo12XmXmII6eSUS4rsPTkY/siA=
+github.com/hashicorp/go-retryablehttp v0.5.0/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
+github.com/hashicorp/go-retryablehttp v0.5.1/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
+github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90/go.mod h1:o4zcYY1e0GEZI6eSEr+43QDYmuGglw1qSO6qdHUHCgg=
+github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
+github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
+github.com/hashicorp/go-sockaddr v1.0.1/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
+github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
+github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hashicorp/memberlist v0.1.0/go.mod h1:ncdBp14cuox2iFOq3kDiquKU6fqsTBc3W6JvZwjxxsE=
+github.com/hashicorp/nomad v0.8.7/go.mod h1:WRaKjdO1G2iqi86TvTjIYtKTyxg4pl7NLr9InxtWaI0=
+github.com/hashicorp/raft v1.0.0/go.mod h1:DVSAWItjLjTOkVbSpWQ0j0kUADIvDaCtBxIcbNAQLkI=
+github.com/hashicorp/serf v0.8.1/go.mod h1:h/Ru6tmZazX7WO/GDmwdpS975F019L4t5ng5IgwbNrE=
+github.com/hashicorp/vault v0.11.5/go.mod h1:KfSyffbKxoVyspOdlaGVjIuwLobi07qD1bAbosPMpP0=
+github.com/hashicorp/vault v1.0.2/go.mod h1:KfSyffbKxoVyspOdlaGVjIuwLobi07qD1bAbosPMpP0=
+github.com/hashicorp/vault-plugin-auth-alicloud v0.0.0-20181109180636-f278a59ca3e8/go.mod h1:o3i5QQWgV5+SYouIn++L9D0kbhLYB3FjxNRHNf6KS+Q=
+github.com/hashicorp/vault-plugin-auth-azure v0.0.0-20181207232528-4c0b46069a22/go.mod h1:f+VmjSQIxxO+YTeO3FbPWRPCPbd3f3lwpP6jaO/YduQ=
+github.com/hashicorp/vault-plugin-auth-centrify v0.0.0-20180816201131-66b0a34a58bf/go.mod h1:IIz+CMBKBEFyjeBeFUlpoUuMOyFb7mybOUNP6GX1xuk=
+github.com/hashicorp/vault-plugin-auth-gcp v0.0.0-20181210200133-4d63bbfe6fcf/go.mod h1:E/E+5CuQCjOn/YGCmZ/tA7GwLey/lN1PwwJOOa9Iqy0=
+github.com/hashicorp/vault-plugin-auth-jwt v0.0.0-20190117220024-3e8048f1026f/go.mod h1:j6Xmkj3dzuC63mivquwVVTlxjwDndwNxi4cJUku40J8=
+github.com/hashicorp/vault-plugin-auth-kubernetes v0.0.0-20181130162533-091d9e5d5fab/go.mod h1:PqRUU5TaQ6FwVTsHPLrJs1F+T5IjbSzlfTy9cTyGeHM=
+github.com/hashicorp/vault-plugin-secrets-ad v0.0.0-20181109182834-540c0b6f1f11/go.mod h1:4vRQzvp3JI+g4oUqzcklIEj2UKyhQnpIo+BDbh2uzYM=
+github.com/hashicorp/vault-plugin-secrets-alicloud v0.0.0-20181109181453-2aee79cc5cbf/go.mod h1:rl8WzY7++fZMLXd6Z/k/o9wUmMbOqpTLhbtKs1loMU0=
+github.com/hashicorp/vault-plugin-secrets-azure v0.0.0-20181207232500-0087bdef705a/go.mod h1:/DhLpYuRP2o00gkj6S0Gy7NvKk5AaAtP6p3f+OmxDUI=
+github.com/hashicorp/vault-plugin-secrets-gcp v0.0.0-20180921173200-d6445459e80c/go.mod h1:IV2OZZZ9FCtSYeKDLsnO5JipMdjwachVISz9pNuQjhs=
+github.com/hashicorp/vault-plugin-secrets-gcpkms v0.0.0-20190116164938-d6b25b0b4a39/go.mod h1:2n62quNV4DvfMY5Lxx82NJmx+9pYtv4RltLIFKxEO4E=
+github.com/hashicorp/vault-plugin-secrets-kv v0.0.0-20181106190520-2236f141171e/go.mod h1:VJHHT2SC1tAPrfENQeBhLlb5FbZoKZM+oC/ROmEftz0=
+github.com/hashicorp/vault-plugin-secrets-kv v0.0.0-20190115203747-edbfe287c5d9/go.mod h1:VJHHT2SC1tAPrfENQeBhLlb5FbZoKZM+oC/ROmEftz0=
+github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
+github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
+github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/influxdata/flux v0.13.0/go.mod h1:81jeDcHVn1rN5uj9aQ81S72Q8ol8If7N0zM0G8TnxTE=
+github.com/influxdata/influxdb v1.7.3/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=
+github.com/influxdata/influxql v0.0.0-20180925231337-1cbfca8e56b6/go.mod h1:KpVI7okXjK6PRi3Z5B+mtKZli+R1DnZgb3N+tzevNgo=
+github.com/influxdata/line-protocol v0.0.0-20180522152040-32c6aa80de5e/go.mod h1:4kt73NQhadE3daL3WhR5EJ/J2ocX0PZzwxQ0gXJ7oFE=
+github.com/influxdata/platform v0.0.0-20190117200541-d500d3cf5589/go.mod h1:YVhys+JOY4wmXtJvdtkzLhS2K/r/px/vPc+EAddK+pg=
+github.com/influxdata/tdigest v0.0.0-20181121200506-bf2b5ad3c0a9/go.mod h1:Js0mqiSBE6Ffsg94weZZ2c+v/ciT8QRHFOap7EKDrR0=
+github.com/influxdata/usage-client v0.0.0-20160829180054-6d3895376368/go.mod h1:Wbbw6tYNvwa5dlB6304Sd+82Z3f7PmVZHVKU637d4po=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/jdkato/prose v1.1.0 h1:LpvmDGwbKGTgdCH3a8VJL56sr7p/wOFPw/R4lM4PfFg=
+github.com/jdkato/prose v1.1.0/go.mod h1:jkF0lkxaX5PFSlk9l4Gh9Y+T57TqUZziWT7uZbW5ADg=
+github.com/jeffchao/backoff v0.0.0-20140404060208-9d7fd7aa17f2/go.mod h1:xkfESuHriIekR+4RoV+fu91j/CfnYM29Zi2tMFw5iD4=
+github.com/jefferai/jsonx v0.0.0-20160721235117-9cc31c3135ee/go.mod h1:N0t2vlmpe8nyZB5ouIbJQPDSR+mH6oe7xHB9VZHSUzM=
+github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
+github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jsternberg/zap-logfmt v1.2.0/go.mod h1:kz+1CUmCutPWABnNkOu9hOHKdT2q3TDYCcsFy9hpqb0=
+github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0=
+github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
+github.com/kevinburke/go-bindata v3.11.0+incompatible/go.mod h1:/pEEZ72flUW2p0yi30bslSp9YqD9pysLxunQDdb2CPM=
+github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/keybase/go-crypto v0.0.0-20181031135447-f919bfda4fc1/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M=
+github.com/keybase/go-crypto v0.0.0-20181127160227-255a5089e85a/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kyokomi/emoji v1.5.1 h1:qp9dub1mW7C4MlvoRENH6EAENb9skEFOvIEbp1Waj38=
+github.com/kyokomi/emoji v1.5.1/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
+github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/magefile/mage v1.4.0 h1:RI7B1CgnPAuu2O9lWszwya61RLmfL0KCdo+QyyI/Bhk=
+github.com/magefile/mage v1.4.0/go.mod h1:IUDi13rsHje59lecXokTfGX0QIzO45uVPlXnJYsXepA=
+github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/markbates/inflect v0.0.0-20171215194931-a12c3aec81a6 h1:LZhVjIISSbj8qLf2qDPP0D8z0uvOWAW5C85ly5mJW6c=
+github.com/markbates/inflect v0.0.0-20171215194931-a12c3aec81a6/go.mod h1:oTeZL2KHA7CUX6X+fovmK9OvIOFuqu0TwdQrZjLTh88=
+github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
+github.com/mattbaird/elastigo v0.0.0-20170123220020-2fe47fd29e4b/go.mod h1:5MWrJXKRQyhQdUCF+vu6U5c4nQpg70vW3eHaU0/AYbU=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
+github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
+github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
+github.com/mattn/go-zglob v0.0.0-20171230104132-4959821b4817/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
+github.com/mattn/go-zglob v0.0.0-20180803001819-2ea3427bfa53/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/michaelklishin/rabbit-hole v1.4.0/go.mod h1:vvI1uOitYZi0O5HEGXhaWC1XT80Gy+HvFheJ+5Krlhk=
+github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
+github.com/miekg/dns v1.1.1/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/miekg/mmark v1.3.6 h1:t47x5vThdwgLJzofNsbsAl7gmIiJ7kbDQN5BxwBmwvY=
+github.com/miekg/mmark v1.3.6/go.mod h1:w7r9mkTvpS55jlfyn22qJ618itLryxXBhA7Jp3FIlkw=
+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
+github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
+github.com/mitchellh/hashstructure v1.0.0 h1:ZkRJX1CyOoTkar7p/mLS5TZU4nJ1Rn/F8u9dGS02Q3Y=
+github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ=
+github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/mna/pigeon v1.0.1-0.20180808201053-bb0192cfc2ae/go.mod h1:Iym28+kJVnC1hfQvv5MUtI6AiFFzvQjHcvI4RFTG/04=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
+github.com/muesli/smartcrop v0.0.0-20180228075044-f6ebaa786a12 h1:l0X/8IDy2UoK+oXcQFMRSIOcyuYb5iEPytPGplnM41Y=
+github.com/muesli/smartcrop v0.0.0-20180228075044-f6ebaa786a12/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/nats-io/gnatsd v1.3.0/go.mod h1:nqco77VO78hLCJpIcVfygDP2rPGfsEHkGTUk94uh5DQ=
+github.com/nats-io/gnatsd v1.4.1/go.mod h1:nqco77VO78hLCJpIcVfygDP2rPGfsEHkGTUk94uh5DQ=
+github.com/nats-io/go-nats v1.6.0/go.mod h1:+t7RHT5ApZebkrQdnn6AhQJmhJJiKAvJUio1PiiCtj0=
+github.com/nats-io/go-nats v1.7.2/go.mod h1:+t7RHT5ApZebkrQdnn6AhQJmhJJiKAvJUio1PiiCtj0=
+github.com/nats-io/go-nats-streaming v0.4.0/go.mod h1:gfq4R3c9sKAINOpelo0gn/b9QDMBZnmrttcsNF+lqyo=
+github.com/nats-io/nats-streaming-server v0.11.2/go.mod h1:RyqtDJZvMZO66YmyjIYdIvS69zu/wDAkyNWa8PIUa5c=
+github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4=
+github.com/nats-io/nuid v1.0.0/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
+github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
+github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+github.com/nicksnyder/go-i18n v1.10.0 h1:5AzlPKvXBH4qBzmZ09Ua9Gipyruv6uApMcrNZdo96+Q=
+github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
+github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
+github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84 h1:fiKJgB4JDUd43CApkmCeTSQlWjtTtABrU2qsgbuP0BI=
+github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
+github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
+github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
+github.com/ory-am/common v0.4.0/go.mod h1:oCYGuwwM8FyYMKqh9vrhBaeUoyz/edx0bgJN6uS6/+k=
+github.com/ory/dockertest v3.3.2+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs=
+github.com/ory/dockertest v3.3.4+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs=
+github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
+github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
+github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
+github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
+github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
+github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/term v0.0.0-20180730021639-bffc007b7fd5/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
+github.com/pquerna/otp v1.1.0/go.mod h1:Zad1CMQfSQZI5KLpahDiSUX4tMMREnXw98IvL1nhgMk=
+github.com/prometheus/client_golang v0.0.0-20171201122222-661e31bf844d/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/russross/blackfriday v0.0.0-20180804101149-46c73eb196ba h1:8Vzt8HxRjy7hp1eqPKVoAEPK9npQFW2510qlobGzvi0=
+github.com/russross/blackfriday v0.0.0-20180804101149-46c73eb196ba/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
+github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
+github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
+github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
+github.com/samuel/go-zookeeper v0.0.0-20180130194729-c4fab1ac1bec/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
+github.com/sanity-io/litter v1.1.0 h1:BllcKWa3VbZmOZbDCoszYLk7zCsKHz5Beossi8SUcTc=
+github.com/sanity-io/litter v1.1.0/go.mod h1:CJ0VCw2q4qKU7LaQr3n7UOSHzgEMgcGco7N/SkZQPjw=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/segmentio/kafka-go v0.1.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo=
+github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
+github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
+github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
+github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
+github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
+github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
+github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
+github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
+github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
+github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
+github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
+github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
+github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
+github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
+github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
+github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
+github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
+github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
+github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
+github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY=
+github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
+github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
+github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
+github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
+github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
+github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
+github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
+github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/fsync v0.0.0-20170320142552-12a01e648f05 h1:pQHm7pxjSgC54M1rtLSLmju25phy6RgYf3p4O6XanYE=
+github.com/spf13/fsync v0.0.0-20170320142552-12a01e648f05/go.mod h1:jdsEoy1w+v0NpuwXZEaRAH6ADTDmzfRnE2eVwshwFrM=
+github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
+github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.2.1/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI=
+github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
+github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
+github.com/stevvooe/resumable v0.0.0-20180830230917-22b14a53ba50/go.mod h1:1pdIZTAHUz+HDKDVZ++5xg/duPlhKAIzw9qy42CWYp4=
+github.com/streadway/amqp v0.0.0-20181205114330-a314942b2fd9/go.mod h1:1WNBiOZtZQLpVAyu0iTduoJL9hEsMloAK5XWrtW0xdY=
+github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
+github.com/tcnksm/go-input v0.0.0-20180404061846-548a7d7a8ee8/go.mod h1:IlWNj9v/13q7xFbaK4mbyzMNwrZLaWSHx/aibKIZuIg=
+github.com/tdewolff/minify/v2 v2.3.7 h1:nhk7MKYRdTDwTxqEQZKLDkLe04tDHht8mBI+VJrsYvk=
+github.com/tdewolff/minify/v2 v2.3.7/go.mod h1:DD1stRlSx6JsHfl1+E/HVMQeXiec9rD1UQ0epklIZLc=
+github.com/tdewolff/parse/v2 v2.3.5 h1:/uS8JfhwVJsNkEh769GM5ENv6L9LOh2Z9uW3tCdlhs0=
+github.com/tdewolff/parse/v2 v2.3.5/go.mod h1:HansaqmN4I/U7L6/tUp0NcwT2tFO0F4EAWYGSDzkYNk=
+github.com/tdewolff/test v1.0.0 h1:jOwzqCXr5ePXEPGJaq2ivoR6HOCi+D5TPfpoyg8yvmU=
+github.com/tdewolff/test v1.0.0/go.mod h1:DiQUlutnqlEvdvhSn2LPGy4TFwRauAaYDsL+683RNX4=
+github.com/testcontainers/testcontainer-go v0.0.0-20181115231424-8e868ca12c0f/go.mod h1:SrG3IY071gtmZJjGbKO+POJ57a/MMESerYNWt6ZRtKs=
+github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
+github.com/tylerb/graceful v1.2.15/go.mod h1:LPYTbOYmUTdabwRt0TGhLllQ0MUNbs0Y5q1WXJOI9II=
+github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
+github.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
+github.com/uber/jaeger-lib v1.5.0/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
+github.com/ugorji/go/codec v0.0.0-20181012064053-8333dd449516/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/wellington/go-libsass v0.9.3-0.20181113175235-c63644206701 h1:9vG9vvVNVupO4Y7uwFkRgIMNe9rdaJMCINDe8vhAhLo=
+github.com/wellington/go-libsass v0.9.3-0.20181113175235-c63644206701/go.mod h1:mxgxgam0N0E+NAUMHLcu20Ccfc3mVpDkyrLDayqfiTs=
+github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
+github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8=
+github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
+github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA=
+github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0=
+github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
+github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
+github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
+go.etcd.io/etcd v3.3.11+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI=
+go.mongodb.org/mongo-driver v1.0.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
+go.opencensus.io v0.15.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0=
+go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
+go.opencensus.io v0.18.1-0.20181204023538-aab39bd6a98b/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
+go.opencensus.io v0.19.1/go.mod h1:gug0GbSHa8Pafr0d2urOSgoXHZ6x/RUlaiT0d9pqb4A=
+go.opencensus.io v0.19.2 h1:ZZpq6xI6kv/LuE/5s5UQvBU5vMjvRnPb8PvJrIntAnc=
+go.opencensus.io v0.19.2/go.mod h1:NO/8qkisMZLZ1FCsKNqtJPwc8/TaclWyY0B6wcYNg9M=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
+gocloud.dev v0.13.0 h1:q3j7dq8MH48LxQ5tu1e05eqTtjthFXRN7TB5/VIObtc=
+gocloud.dev v0.13.0/go.mod h1:WbTmzqihM0aICJgj1Z702dckfPW1XcCV6RQnoi3OpQA=
+golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
+golang.org/x/crypto v0.0.0-20180505025534-4ec37c66abab/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20181112044915-a3060d491354/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 h1:00VmoueYNlNz/aHIilyyQz/MHSqGoWJzpFv/HW8xpzI=
+golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
+golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f h1:FO4MZ3N56GnxbqxGKqh+YTzUWQ2sDwtFQEZgLOxh9Jc=
+golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190328230028-74de082e2cca h1:hyA6yiAgbUwuWqtscNvWAI7U1CtlaD1KilQ6iudt1aI=
+golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/oauth2 v0.0.0-20180603041954-1e0a3fa8ba9a/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180828065106-d99a578cf41b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181030150119-7e31e0c00fa0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc h1:SdCq5U4J+PpbSDIl9bM0V1e1Ug1jsnBkAFvTs1htn7U=
+golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc h1:4gbWbmmPFp4ySWICouJl6emP0MyS31yy9SrTlAGFT+g=
+golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190402142545-baf5eb976a8c h1:3xiKTkef8QqBJ8q+4fVUDMRoxnI0H/MVNFswa+aExbo=
+golang.org/x/sys v0.0.0-20190402142545-baf5eb976a8c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181017214349-06f26fdaaa28/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181219222714-6e267b5cc78e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181221154417-3ad2d988d5e2/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/xerrors v0.0.0-20190129162528-20feca13ea86 h1:kMgZCSynBSIN3PHpvuFeMExQwPWtUZ/xfnt2Yr2cp20=
+golang.org/x/xerrors v0.0.0-20190129162528-20feca13ea86/go.mod h1:/lyp46tcDBI65C0XC8F4d0/XVb7MT7RScVRech7dX/4=
+gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
+gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
+google.golang.org/api v0.0.0-20180603000442-8e296ef26005/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.0.0-20181021000519-a2651947f503/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
+google.golang.org/api v0.2.0 h1:B5VXkdjt7K2Gm6fGBC9C9a1OAKJDT95cTqwet+2zib0=
+google.golang.org/api v0.2.0/go.mod h1:IfRCZScioGtypHNTlz3gFk67J8uePVW7uDTBzXuIkhU=
+google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180601223552-81158efcc9f2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20181016170114-94acd270e44e/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
+google.golang.org/genproto v0.0.0-20181219182458-5a97ab628bfb/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
+google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 h1:Lj2SnHtxkRGJDqnGaSjo+CCdIieEnwVazbOXILwQemk=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
+google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
+google.golang.org/grpc v1.15.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
+google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
+google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
+google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/asn1-ber.v1 v1.0.0-20170511165959-379148ca0225/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
+gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fatih/pool.v2 v2.0.0/go.mod h1:8xVGeu1/2jr2wm5V9SPuMht2H5AEmf5aFMGSQixtjTY=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/gorethink/gorethink.v4 v4.1.0/go.mod h1:M7JgwrUAmshJ3iUbEK0Pt049MPyPK+CYDGGaEjdZb/c=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/ldap.v2 v2.5.1/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk=
+gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
+gopkg.in/ory-am/dockertest.v2 v2.2.3/go.mod h1:kDHEsan1UcKFYH1c28sDmqnmeqIpB4Nj682gSNhYDYM=
+gopkg.in/pipe.v2 v2.0.0-20140414041502-3c2ca4d52544/go.mod h1:UhTeH/yXCK/KY7TX24mqPkaQ7gZeqmWd/8SSS8B3aHw=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/robfig/cron.v2 v2.0.0-20150107220207-be2e0b0deed5/go.mod h1:hiOFpYm0ZJbusNj2ywpbrXowU3G8U6GIQzqn2mw1UIE=
+gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/src-d/go-billy.v4 v4.2.1/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk=
+gopkg.in/src-d/go-git-fixtures.v3 v3.1.1/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
+gopkg.in/src-d/go-git.v4 v4.8.1/go.mod h1:Vtut8izDyrM8BUVQnzJ+YvmNcem2J89EmfZYCkLokZk=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/vmihailenco/msgpack.v2 v2.9.1/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
+grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
+honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20181108184350-ae8f1f9103cc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+k8s.io/api v0.0.0-20181221193117-173ce66c1e39/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
+k8s.io/apimachinery v0.0.0-20190119020841-d41becfba9ee/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
+k8s.io/klog v0.1.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
+layeh.com/radius v0.0.0-20190118135028-0f678f039617/go.mod h1:fywZKyu//X7iRzaxLgPWsvc0L26IUpVvE/aeIL2JtIQ=
+pack.ag/amqp v0.8.0/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4=
+pack.ag/amqp v0.10.2/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4=
+sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
+sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
diff --git a/goreleaser-extended.yml b/goreleaser-extended.yml
new file mode 100644
index 000000000..8be278880
--- /dev/null
+++ b/goreleaser-extended.yml
@@ -0,0 +1,85 @@
+project_name: hugo_extended
+builds:
+- binary: hugo
+ ldflags:
+ - -s -w -X github.com/gohugoio/hugo/common/hugo.buildDate={{.Date}} -X github.com/gohugoio/hugo/common/hugo.commitHash={{ .ShortCommit }}
+ - "-extldflags '-static'"
+ env:
+ - CGO_ENABLED=1
+ - CC=x86_64-w64-mingw32-gcc
+ - CXX=x86_64-w64-mingw32-g++
+ flags:
+ - -tags
+ - extended
+ goos:
+ - windows
+ goarch:
+ - amd64
+- binary: hugo
+ ldflags: -s -w -X github.com/gohugoio/hugo/common/hugo.buildDate={{.Date}} -X github.com/gohugoio/hugo/common/hugo.commitHash={{ .ShortCommit }}
+ env:
+ - CGO_ENABLED=1
+ - CC=o64-clang
+ - CXX=o64-clang++
+ flags:
+ - -tags
+ - extended
+ goos:
+ - darwin
+ goarch:
+ - amd64
+- binary: hugo
+ ldflags: -s -w -X github.com/gohugoio/hugo/common/hugo.buildDate={{.Date}} -X github.com/gohugoio/hugo/common/hugo.commitHash={{ .ShortCommit }}
+ env:
+ - CGO_ENABLED=1
+ flags:
+ - -tags
+ - extended
+ goos:
+ - linux
+ goarch:
+ - amd64
+nfpm:
+ formats:
+ - deb
+ vendor: "gohugo.io"
+ homepage: "https://gohugo.io/"
+ maintainer: "Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>"
+ description: "A Fast and Flexible Static Site Generator built with love in GoLang."
+ license: "Apache 2.0"
+ name_template: "{{.ProjectName}}_{{.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
+archive:
+ format: tar.gz
+ format_overrides:
+ - goos: windows
+ format: zip
+ name_template: "{{.ProjectName}}_{{.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
+release:
+ draft: true
diff --git a/goreleaser.yml b/goreleaser.yml
new file mode 100644
index 000000000..5f3e444cc
--- /dev/null
+++ b/goreleaser.yml
@@ -0,0 +1,66 @@
+project_name: hugo
+build:
+ main: main.go
+ binary: hugo
+ ldflags: -s -w -X github.com/gohugoio/hugo/common/hugo.buildDate={{.Date}} -X github.com/gohugoio/hugo/common/hugo.commitHash={{ .ShortCommit }}
+ env:
+ - CGO_ENABLED=0
+ goos:
+ - darwin
+ - linux
+ - windows
+ - freebsd
+ - netbsd
+ - openbsd
+ - dragonfly
+ goarch:
+ - amd64
+ - 386
+ - arm
+ - arm64
+ goarm:
+ - 7
+nfpm:
+ formats:
+ - deb
+ vendor: "gohugo.io"
+ homepage: "https://gohugo.io/"
+ maintainer: "Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>"
+ description: "A Fast and Flexible Static Site Generator built with love in GoLang."
+ license: "Apache 2.0"
+ name_template: "{{.ProjectName}}_{{.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
+archive:
+ format: tar.gz
+ format_overrides:
+ - goos: windows
+ format: zip
+ name_template: "{{.ProjectName}}_{{.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
+release:
+ draft: true
diff --git a/helpers/content.go b/helpers/content.go
new file mode 100644
index 000000000..3892647bb
--- /dev/null
+++ b/helpers/content.go
@@ -0,0 +1,788 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "runtime"
+ "unicode"
+ "unicode/utf8"
+
+ "github.com/gohugoio/hugo/common/maps"
+
+ "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"
+)
+
+// SummaryDivider denotes where content summarization should end. The default is "<!--more-->".
+var SummaryDivider = []byte("<!--more-->")
+
+var (
+ openingPTag = []byte("<p>")
+ closingPTag = []byte("</p>")
+ paragraphIndicator = []byte("<p")
+)
+
+// ContentSpec provides functionality to render markdown content.
+type ContentSpec struct {
+ BlackFriday *BlackFriday
+ footnoteAnchorPrefix string
+ footnoteReturnLinkContents string
+ // SummaryLength is the length of the summary that Hugo extracts from a content.
+ summaryLength int
+
+ BuildFuture bool
+ BuildExpired bool
+ BuildDrafts bool
+
+ Highlight func(code, lang, optsStr string) (string, error)
+ defatultPygmentsOpts map[string]string
+
+ Cfg config.Provider
+}
+
+// NewContentSpec returns a ContentSpec initialized
+// with the appropriate fields from the given config.Provider.
+func NewContentSpec(cfg config.Provider) (*ContentSpec, error) {
+ bf := newBlackfriday(cfg.GetStringMap("blackfriday"))
+ spec := &ContentSpec{
+ BlackFriday: bf,
+ footnoteAnchorPrefix: cfg.GetString("footnoteAnchorPrefix"),
+ footnoteReturnLinkContents: cfg.GetString("footnoteReturnLinkContents"),
+ summaryLength: cfg.GetInt("summaryLength"),
+ BuildFuture: cfg.GetBool("buildFuture"),
+ BuildExpired: cfg.GetBool("buildExpired"),
+ BuildDrafts: cfg.GetBool("buildDrafts"),
+
+ Cfg: cfg,
+ }
+
+ // Highlighting setup
+ options, err := parseDefaultPygmentsOpts(cfg)
+ if err != nil {
+ return nil, err
+ }
+ spec.defatultPygmentsOpts = options
+
+ // Use the Pygmentize on path if present
+ useClassic := false
+ h := newHiglighters(spec)
+
+ if cfg.GetBool("pygmentsUseClassic") {
+ if !hasPygments() {
+ jww.WARN.Println("Highlighting with pygmentsUseClassic set requires Pygments to be installed and in the path")
+ } else {
+ useClassic = true
+ }
+ }
+
+ if useClassic {
+ spec.Highlight = h.pygmentsHighlight
+ } else {
+ spec.Highlight = h.chromaHighlight
+ }
+
+ return spec, nil
+}
+
+// BlackFriday holds configuration values for BlackFriday rendering.
+type BlackFriday struct {
+ Smartypants bool
+ SmartypantsQuotesNBSP bool
+ AngledQuotes bool
+ Fractions bool
+ HrefTargetBlank bool
+ NofollowLinks bool
+ NoreferrerLinks bool
+ SmartDashes bool
+ LatexDashes bool
+ TaskLists bool
+ PlainIDAnchors bool
+ Extensions []string
+ ExtensionsMask []string
+ SkipHTML bool
+}
+
+// NewBlackfriday creates a new Blackfriday filled with site config or some sane defaults.
+func newBlackfriday(config map[string]interface{}) *BlackFriday {
+ defaultParam := map[string]interface{}{
+ "smartypants": true,
+ "angledQuotes": false,
+ "smartypantsQuotesNBSP": false,
+ "fractions": true,
+ "hrefTargetBlank": false,
+ "nofollowLinks": false,
+ "noreferrerLinks": false,
+ "smartDashes": true,
+ "latexDashes": true,
+ "plainIDAnchors": true,
+ "taskLists": true,
+ "skipHTML": false,
+ }
+
+ maps.ToLower(defaultParam)
+
+ siteConfig := make(map[string]interface{})
+
+ for k, v := range defaultParam {
+ siteConfig[k] = v
+ }
+
+ for k, v := range config {
+ 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())
+ }
+
+ 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,
+ "joinLines": blackfriday.EXTENSION_JOIN_LINES,
+}
+
+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.SmartypantsQuotesNBSP {
+ htmlFlags |= blackfriday.HTML_SMARTYPANTS_QUOTES_NBSP
+ }
+
+ 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.NofollowLinks {
+ htmlFlags |= blackfriday.HTML_NOFOLLOW_LINKS
+ }
+
+ if ctx.Config.NoreferrerLinks {
+ htmlFlags |= blackfriday.HTML_NOREFERRER_LINKS
+ }
+
+ if ctx.Config.SmartDashes {
+ htmlFlags |= blackfriday.HTML_SMARTYPANTS_DASHES
+ }
+
+ if ctx.Config.LatexDashes {
+ htmlFlags |= blackfriday.HTML_SMARTYPANTS_LATEX_DASHES
+ }
+
+ if ctx.Config.SkipHTML {
+ htmlFlags |= blackfriday.HTML_SKIP_HTML
+ }
+
+ return &HugoHTMLRenderer{
+ cs: c,
+ 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{
+ cs: c,
+ Renderer: mmark.HtmlRendererWithParameters(htmlFlags, "", "", renderParameters),
+ Cfg: 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) {
+ if !bytes.Contains(content, []byte("<nav>")) {
+ return content, nil
+ }
+ 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
+ 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)
+ case "pandoc":
+ return getPandocContent(ctx)
+ }
+}
+
+// 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 (c *ContentSpec) TruncateWordsByRune(in []string) (string, bool) {
+ words := make([]string, len(in))
+ copy(words, in)
+
+ count := 0
+ for index, word := range words {
+ if count >= c.summaryLength {
+ return strings.Join(words[:index], " "), true
+ }
+ runeCount := utf8.RuneCountInString(word)
+ if len(word) == runeCount {
+ count++
+ } else if count+runeCount < c.summaryLength {
+ count += runeCount
+ } else {
+ for ri := range word {
+ if count >= c.summaryLength {
+ 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 (c *ContentSpec) TruncateWordsToWholeSentence(s string) (string, bool) {
+ var (
+ wordCount = 0
+ lastWordIndex = -1
+ )
+
+ for i, r := range s {
+ if unicode.IsSpace(r) {
+ wordCount++
+ lastWordIndex = i
+
+ if wordCount >= c.summaryLength {
+ 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)
+}
+
+// TrimShortHTML removes the <p>/</p> tags from HTML input in the situation
+// where said tags are the only <p> tags in the input and enclose the content
+// of the input (whitespace excluded).
+func (c *ContentSpec) TrimShortHTML(input []byte) []byte {
+ first := bytes.Index(input, paragraphIndicator)
+ last := bytes.LastIndex(input, paragraphIndicator)
+ if first == last {
+ input = bytes.TrimSpace(input)
+ input = bytes.TrimPrefix(input, openingPTag)
+ input = bytes.TrimSuffix(input, closingPTag)
+ input = bytes.TrimSpace(input)
+ }
+ return input
+}
+
+func isEndOfSentence(r rune) bool {
+ return r == '.' || r == '?' || r == '!' || r == '"' || r == '\n'
+}
+
+// Kept only for benchmark.
+func (c *ContentSpec) truncateWordsToWholeSentenceOld(content string) (string, bool) {
+ words := strings.Fields(content)
+
+ if c.summaryLength >= len(words) {
+ return strings.Join(words, " "), false
+ }
+
+ for counter, word := range words[c.summaryLength:] {
+ if strings.HasSuffix(word, ".") ||
+ strings.HasSuffix(word, "?") ||
+ strings.HasSuffix(word, ".\"") ||
+ strings.HasSuffix(word, "!") {
+ upper := c.summaryLength + counter + 1
+ return strings.Join(words[:upper], " "), (upper < len(words))
+ }
+ }
+
+ return strings.Join(words[:c.summaryLength], " "), true
+}
+
+func getAsciidocExecPath() string {
+ path, err := exec.LookPath("asciidoc")
+ if err != nil {
+ return ""
+ }
+ return path
+}
+
+func getAsciidoctorExecPath() string {
+ path, err := exec.LookPath("asciidoctor")
+ if err != nil {
+ return ""
+ }
+ return path
+}
+
+// HasAsciidoc returns whether Asciidoc or Asciidoctor is installed on this computer.
+func HasAsciidoc() bool {
+ return (getAsciidoctorExecPath() != "" ||
+ getAsciidocExecPath() != "")
+}
+
+// getAsciidocContent calls asciidoctor or asciidoc as an external helper
+// to convert AsciiDoc content to HTML.
+func getAsciidocContent(ctx *RenderingContext) []byte {
+ var isAsciidoctor bool
+ path := getAsciidoctorExecPath()
+ if path == "" {
+ path = getAsciidocExecPath()
+ if path == "" {
+ jww.ERROR.Println("asciidoctor / asciidoc not found in $PATH: Please install.\n",
+ " Leaving AsciiDoc content unrendered.")
+ return ctx.Content
+ }
+ } else {
+ isAsciidoctor = true
+ }
+
+ jww.INFO.Println("Rendering", ctx.DocumentName, "with", path, "...")
+ args := []string{"--no-header-footer", "--safe"}
+ if isAsciidoctor {
+ // asciidoctor-specific arg to show stack traces on errors
+ args = append(args, "--trace")
+ }
+ args = append(args, "-")
+ return externallyRenderContent(ctx, path, args)
+}
+
+// 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 {
+ path := getRstExecPath()
+
+ if path == "" {
+ jww.ERROR.Println("rst2html / rst2html.py not found in $PATH: Please install.\n",
+ " Leaving reStructuredText content unrendered.")
+ return ctx.Content
+
+ }
+ jww.INFO.Println("Rendering", ctx.DocumentName, "with", path, "...")
+ var result []byte
+ // certain *nix based OSs wrap executables in scripted launchers
+ // invoking binaries on these OSs via python interpreter causes SyntaxError
+ // invoke directly so that shebangs work as expected
+ // handle Windows manually because it doesn't do shebangs
+ if runtime.GOOS == "windows" {
+ python := getPythonExecPath()
+ args := []string{path, "--leave-comments", "--initial-header-level=2"}
+ result = externallyRenderContent(ctx, python, args)
+ } else {
+ args := []string{"--leave-comments", "--initial-header-level=2"}
+ result = externallyRenderContent(ctx, path, args)
+ }
+ // 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]
+}
+
+// getPandocContent calls pandoc as an external helper to convert pandoc markdown to HTML.
+func getPandocContent(ctx *RenderingContext) []byte {
+ path, err := exec.LookPath("pandoc")
+ if err != nil {
+ jww.ERROR.Println("pandoc not found in $PATH: Please install.\n",
+ " Leaving pandoc content unrendered.")
+ return ctx.Content
+ }
+ args := []string{"--mathjax"}
+ return externallyRenderContent(ctx, path, args)
+}
+
+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))
+}
+
+func externallyRenderContent(ctx *RenderingContext, path string, args []string) []byte {
+ content := ctx.Content
+ cleanContent := bytes.Replace(content, SummaryDivider, []byte(""), 1)
+
+ cmd := exec.Command(path, args...)
+ cmd.Stdin = bytes.NewReader(cleanContent)
+ var out, cmderr bytes.Buffer
+ cmd.Stdout = &out
+ cmd.Stderr = &cmderr
+ err := cmd.Run()
+ // Most external helpers exit 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(cmderr.String(), "\n") {
+ item := strings.TrimSpace(item)
+ if item != "" {
+ jww.ERROR.Printf("%s: %s", ctx.DocumentName, item)
+ }
+ }
+ if err != nil {
+ jww.ERROR.Printf("%s rendering %s: %v", path, ctx.DocumentName, err)
+ }
+
+ return normalizeExternalHelperLineFeeds(out.Bytes())
+}
diff --git a/helpers/content_renderer.go b/helpers/content_renderer.go
new file mode 100644
index 000000000..dc22cb6f4
--- /dev/null
+++ b/helpers/content_renderer.go
@@ -0,0 +1,108 @@
+// 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"
+ "strings"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/miekg/mmark"
+ "github.com/russross/blackfriday"
+)
+
+// HugoHTMLRenderer wraps a blackfriday.Renderer, typically a blackfriday.Html
+// Enabling Hugo to customise the rendering experience
+type HugoHTMLRenderer struct {
+ cs *ContentSpec
+ *RenderingContext
+ blackfriday.Renderer
+}
+
+// BlockCode renders a given text as a block of code.
+// Pygments is used if it is setup to handle code fences.
+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 := strings.Trim(string(text), "\n\r")
+ highlighted, _ := r.cs.Highlight(str, lang, opts)
+ out.WriteString(highlighted)
+ } else {
+ r.Renderer.BlockCode(out, text, lang)
+ }
+}
+
+// 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(`<label><input type="checkbox" disabled class="task-list-item">`), text[3:]...)
+ text = append(text, []byte(`</label>`)...)
+
+ case bytes.HasPrefix(text, []byte("[x] ")) || bytes.HasPrefix(text, []byte("[X] ")):
+ text = append([]byte(`<label><input type="checkbox" checked disabled class="task-list-item">`), text[3:]...)
+ text = append(text, []byte(`</label>`)...)
+ }
+
+ 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")) {
+ // Find the index of the first >, it might be 3 or 4 depending on whether
+ // there is a new line at the start, but this is safer than just hardcoding it.
+ closingBracketIndex := bytes.Index(list, []byte(">"))
+ // Rewrite the buffer from the marker
+ out.Truncate(marker)
+ // Safely assuming closingBracketIndex won't be -1 since there is a list
+ // May be either dl, ul or ol
+ list := append(list[:closingBracketIndex], append([]byte(` class="task-list"`), list[closingBracketIndex:]...)...)
+ out.Write(list)
+ }
+ }
+}
+
+// HugoMmarkHTMLRenderer wraps a mmark.Renderer, typically a mmark.html,
+// enabling Hugo to customise the rendering experience.
+type HugoMmarkHTMLRenderer struct {
+ cs *ContentSpec
+ mmark.Renderer
+ Cfg config.Provider
+}
+
+// BlockCode renders a given text as a block of code.
+// Pygments is used if it is setup to handle code fences.
+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 := strings.Trim(string(text), "\n\r")
+ highlighted, _ := r.cs.Highlight(str, lang, "")
+ out.WriteString(highlighted)
+ } 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..f542d5d54
--- /dev/null
+++ b/helpers/content_renderer_test.go
@@ -0,0 +1,141 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "github.com/stretchr/testify/require"
+)
+
+// Renders a codeblock using Blackfriday
+func (c ContentSpec) render(input string) string {
+ ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
+ 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.BlackFriday}
+ 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) {
+ assert := require.New(t)
+
+ 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">\n?<pre.*><code class="language-html" data-lang="html">.*?</code></pre>\n?</div>\n?$`},
+ {false, "<html></html>", `(?s)^<pre.*><code class="language-html">.*?</code></pre>\n$`},
+ }
+
+ for _, useClassic := range []bool{false, true} {
+ for i, d := range data {
+ v := viper.New()
+ v.Set("pygmentsStyle", "monokai")
+ v.Set("pygmentsUseClasses", true)
+ v.Set("pygmentsCodeFences", d.enabled)
+ v.Set("pygmentsUseClassic", useClassic)
+
+ c, err := NewContentSpec(v)
+ assert.NoError(err)
+
+ 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><label><input type="checkbox" checked disabled class="task-list-item"> On1</label></li>
+<li><label><input type="checkbox" checked disabled class="task-list-item"> On2</label></li>
+<li><label><input type="checkbox" disabled class="task-list-item"> Off</label></li>
+</ul>
+
+<p>END</p>
+`},
+ {`- [x] On1`, false, `<ul>
+<li>[x] On1</li>
+</ul>
+`},
+ {`* [ ] Off
+
+END`, true, `<ul class="task-list">
+<li><label><input type="checkbox" disabled class="task-list-item"> Off</label></li>
+</ul>
+
+<p>END</p>
+`},
+ } {
+ blackFridayConfig := c.BlackFriday
+ 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..709c81142
--- /dev/null
+++ b/helpers/content_test.go
@@ -0,0 +1,518 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/spf13/viper"
+
+ "github.com/miekg/mmark"
+ "github.com/russross/blackfriday"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+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 TestTrimShortHTML(t *testing.T) {
+ tests := []struct {
+ input, output []byte
+ }{
+ {[]byte(""), []byte("")},
+ {[]byte("Plain text"), []byte("Plain text")},
+ {[]byte(" \t\n Whitespace text\n\n"), []byte("Whitespace text")},
+ {[]byte("<p>Simple paragraph</p>"), []byte("Simple paragraph")},
+ {[]byte("\n \n \t <p> \t Whitespace\nHTML \n\t </p>\n\t"), []byte("Whitespace\nHTML")},
+ {[]byte("<p>Multiple</p><p>paragraphs</p>"), []byte("<p>Multiple</p><p>paragraphs</p>")},
+ {[]byte("<p>Nested<p>paragraphs</p></p>"), []byte("<p>Nested<p>paragraphs</p></p>")},
+ }
+
+ c := newTestContentSpec()
+ for i, test := range tests {
+ output := c.TrimShortHTML(test.input)
+ if bytes.Compare(test.output, output) != 0 {
+ t.Errorf("Test %d failed. Expected %q got %q", i, test.output, output)
+ }
+ }
+}
+
+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")))
+}
+
+func TestNewContentSpec(t *testing.T) {
+ cfg := viper.New()
+ assert := require.New(t)
+
+ cfg.Set("summaryLength", 32)
+ cfg.Set("buildFuture", true)
+ cfg.Set("buildExpired", true)
+ cfg.Set("buildDrafts", true)
+
+ spec, err := NewContentSpec(cfg)
+
+ assert.NoError(err)
+ assert.Equal(32, spec.summaryLength)
+ assert.True(spec.BuildFuture)
+ assert.True(spec.BuildExpired)
+ assert.True(spec.BuildDrafts)
+
+}
+
+var benchmarkTruncateString = strings.Repeat("This is a sentence about nothing.", 20)
+
+func BenchmarkTestTruncateWordsToWholeSentence(b *testing.B) {
+ c := newTestContentSpec()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ c.TruncateWordsToWholeSentence(benchmarkTruncateString)
+ }
+}
+
+func BenchmarkTestTruncateWordsToWholeSentenceOld(b *testing.B) {
+ c := newTestContentSpec()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ c.truncateWordsToWholeSentenceOld(benchmarkTruncateString)
+ }
+}
+
+func TestTruncateWordsToWholeSentence(t *testing.T) {
+ c := newTestContentSpec()
+ 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},
+ {"This... is a more difficult test?", "This... is a more difficult test?", 1, false},
+ }
+ for i, d := range data {
+ c.summaryLength = d.max
+ output, truncated := c.TruncateWordsToWholeSentence(d.input)
+ 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) {
+ c := newTestContentSpec()
+ 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 {
+ c.summaryLength = d.max
+ output, truncated := c.TruncateWordsByRune(strings.Fields(d.input))
+ 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.BlackFriday}
+ 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_QUOTES_NBSP},
+ {blackfriday.HTML_SMARTYPANTS_ANGLED_QUOTES},
+ {blackfriday.HTML_SMARTYPANTS_FRACTIONS},
+ {blackfriday.HTML_HREF_TARGET_BLANK},
+ {blackfriday.HTML_NOFOLLOW_LINKS},
+ {blackfriday.HTML_NOREFERRER_LINKS},
+ {blackfriday.HTML_SMARTYPANTS_DASHES},
+ {blackfriday.HTML_SMARTYPANTS_LATEX_DASHES},
+ }
+ defaultFlags := blackfriday.HTML_USE_XHTML
+ ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
+ ctx.Config.AngledQuotes = true
+ ctx.Config.Fractions = true
+ ctx.Config.HrefTargetBlank = true
+ ctx.Config.NofollowLinks = true
+ ctx.Config.NoreferrerLinks = true
+ ctx.Config.LatexDashes = true
+ ctx.Config.PlainIDAnchors = true
+ ctx.Config.SmartDashes = true
+ ctx.Config.Smartypants = true
+ ctx.Config.SmartypantsQuotesNBSP = 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.BlackFriday}
+ 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.BlackFriday}
+ 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.BlackFriday}
+ 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.BlackFriday}
+ 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.BlackFriday}
+ 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.BlackFriday}
+ 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.BlackFriday}
+ 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.BlackFriday}
+ 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.BlackFriday}
+ 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/docshelper.go b/helpers/docshelper.go
new file mode 100644
index 000000000..8ad817d12
--- /dev/null
+++ b/helpers/docshelper.go
@@ -0,0 +1,59 @@
+package helpers
+
+import (
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "github.com/alecthomas/chroma/lexers"
+ "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{})
+
+ var chromaLexers []interface{}
+
+ sort.Sort(lexers.Registry.Lexers)
+
+ for _, l := range lexers.Registry.Lexers {
+
+ config := l.Config()
+
+ var filenames []string
+ filenames = append(filenames, config.Filenames...)
+ filenames = append(filenames, config.AliasFilenames...)
+
+ aliases := config.Aliases
+
+ for _, filename := range filenames {
+ alias := strings.TrimSpace(strings.TrimPrefix(filepath.Ext(filename), "."))
+ if alias != "" {
+ aliases = append(aliases, alias)
+ }
+ }
+
+ sort.Strings(aliases)
+ aliases = UniqueStrings(aliases)
+
+ lexerEntry := struct {
+ Name string
+ Aliases []string
+ }{
+ config.Name,
+ aliases,
+ }
+
+ chromaLexers = append(chromaLexers, lexerEntry)
+
+ docs["lexers"] = chromaLexers
+ }
+ return docs
+
+ }
+
+ docshelper.AddDocProvider("chroma", docsProvider)
+}
diff --git a/helpers/emoji.go b/helpers/emoji.go
new file mode 100644
index 000000000..a6786c005
--- /dev/null
+++ b/helpers/emoji.go
@@ -0,0 +1,97 @@
+// 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
+)
+
+// Emoji returns the emojy given a key, e.g. ":smile:", nil if not found.
+func Emoji(key string) []byte {
+ emojiInit.Do(initEmoji)
+ return emojis[key]
+}
+
+// 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..89f9df5fa
--- /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())
+ 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..3cf7ba8af
--- /dev/null
+++ b/helpers/general.go
@@ -0,0 +1,475 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "unicode"
+ "unicode/utf8"
+
+ "github.com/gohugoio/hugo/common/hugo"
+
+ "github.com/gohugoio/hugo/hugofs"
+
+ "github.com/spf13/afero"
+
+ "github.com/jdkato/prose/transform"
+
+ bp "github.com/gohugoio/hugo/bufferpool"
+ 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 "pandoc", "pdc":
+ return "pandoc"
+ case "html", "htm":
+ return "html"
+ case "org":
+ return "org"
+ }
+
+ return ""
+}
+
+// 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())
+ copy(bc, b.Bytes())
+ return bc
+}
+
+// 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
+}
+
+// GetTitleFunc returns a func that can be used to transform a string to
+// title case.
+//
+// The supported styles are
+//
+// - "Go" (strings.Title)
+// - "AP" (see https://www.apstylebook.com/)
+// - "Chicago" (see http://www.chicagomanualofstyle.org/home.html)
+//
+// If an unknown or empty style is provided, AP style is what you get.
+func GetTitleFunc(style string) func(s string) string {
+ switch strings.ToLower(style) {
+ case "go":
+ return strings.Title
+ case "chicago":
+ tc := transform.NewTitleConverter(transform.ChicagoStyle)
+ return tc.Title
+ default:
+ tc := transform.NewTitleConverter(transform.APStyle)
+ return tc.Title
+ }
+}
+
+// HasStringsPrefix tests whether the string slice s begins with prefix slice s.
+func HasStringsPrefix(s, prefix []string) bool {
+ return len(s) >= len(prefix) && compareStringSlices(s[0:len(prefix)], prefix)
+}
+
+// HasStringsSuffix tests whether the string slice s ends with suffix slice s.
+func HasStringsSuffix(s, suffix []string) bool {
+ return len(s) >= len(suffix) && compareStringSlices(s[len(s)-len(suffix):], suffix)
+}
+
+func compareStringSlices(a, b []string) bool {
+ if a == nil && b == nil {
+ return true
+ }
+
+ if a == nil || b == nil {
+ return false
+ }
+
+ if len(a) != len(b) {
+ return false
+ }
+
+ for i := range a {
+ if a[i] != b[i] {
+ return false
+ }
+ }
+
+ return true
+}
+
+// LogPrinter is the common interface of the JWWs loggers.
+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}
+}
+
+// NewDistinctLogger creates a new DistinctLogger that logs to the provided logger.
+func NewDistinctLogger(logger LogPrinter) *DistinctLogger {
+ return &DistinctLogger{m: make(map[string]bool), logger: logger}
+}
+
+// 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 !strings.HasSuffix(alternative, ".") {
+ alternative += "."
+ }
+
+ if err {
+ DistinctErrorLog.Printf("%s's %s is deprecated and will be removed in Hugo %s. %s", object, item, hugo.CurrentVersion.Next().ReleaseVersion(), alternative)
+
+ } else {
+ DistinctWarnLog.Printf("%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{}))
+}
+
+// MD5FromFileFast creates a MD5 hash from the given file. It only reads parts of
+// the file for speed, so don't use it if the files are very subtly different.
+// It will not close the file.
+func MD5FromFileFast(r io.ReadSeeker) (string, error) {
+ const (
+ // Do not change once set in stone!
+ maxChunks = 8
+ peekSize = 64
+ seek = 2048
+ )
+
+ h := md5.New()
+ buff := make([]byte, peekSize)
+
+ for i := 0; i < maxChunks; i++ {
+ if i > 0 {
+ _, err := r.Seek(seek, 0)
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ return "", err
+ }
+ }
+
+ _, err := io.ReadAtLeast(r, buff, peekSize)
+ if err != nil {
+ if err == io.EOF || err == io.ErrUnexpectedEOF {
+ h.Write(buff)
+ break
+ }
+ return "", err
+ }
+ h.Write(buff)
+ }
+
+ return hex.EncodeToString(h.Sum(nil)), nil
+}
+
+// MD5FromReader creates a MD5 hash from the given reader.
+func MD5FromReader(r io.Reader) (string, error) {
+ h := md5.New()
+ if _, err := io.Copy(h, r); err != nil {
+ return "", nil
+ }
+ return hex.EncodeToString(h.Sum(nil)), nil
+}
+
+// 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"
+ case "uglyUrls":
+ name = "uglyURLs"
+ }
+ 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
+}
+
+// DiffStrings splits the strings into fields and runs it into DiffStringSlices.
+// Useful for tests.
+func DiffStrings(s1, s2 string) []string {
+ return DiffStringSlices(strings.Fields(s1), strings.Fields(s2))
+}
+
+// PrintFs prints the given filesystem to the given writer starting from the given path.
+// This is useful for debugging.
+func PrintFs(fs afero.Fs, path string, w io.Writer) {
+ if fs == nil {
+ return
+ }
+ afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error {
+ if info != nil && !info.IsDir() {
+ s := path
+ if lang, ok := info.(hugofs.LanguageAnnouncer); ok {
+ s = s + "\tLANG: " + lang.Lang()
+ }
+ if fp, ok := info.(hugofs.FilePather); ok {
+ s = s + "\tRF: " + fp.Filename() + "\tBP: " + fp.BaseDir()
+ }
+ fmt.Fprintln(w, " ", s)
+ }
+ return nil
+ })
+}
diff --git a/helpers/general_test.go b/helpers/general_test.go
new file mode 100644
index 000000000..ed4c3d2c2
--- /dev/null
+++ b/helpers/general_test.go
@@ -0,0 +1,330 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "reflect"
+ "strings"
+ "testing"
+
+ "github.com/spf13/afero"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+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"},
+ {"pandoc", "pandoc"},
+ {"pdc", "pandoc"},
+ {"mmark", "mmark"},
+ {"html", "html"},
+ {"htm", "html"},
+ {"org", "org"},
+ {"excel", ""},
+ } {
+ 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)
+ }
+ }
+}
+
+func TestHasStringsPrefix(t *testing.T) {
+ for i, this := range []struct {
+ s []string
+ prefix []string
+ expect bool
+ }{
+ {[]string{"a"}, []string{"a"}, true},
+ {[]string{}, []string{}, true},
+ {[]string{"a", "b", "c"}, []string{"a", "b"}, true},
+ {[]string{"d", "a", "b", "c"}, []string{"a", "b"}, false},
+ {[]string{"abra", "ca", "dabra"}, []string{"abra", "ca"}, true},
+ {[]string{"abra", "ca"}, []string{"abra", "ca", "dabra"}, false},
+ } {
+ result := HasStringsPrefix(this.s, this.prefix)
+ if result != this.expect {
+ t.Fatalf("[%d] got %t but expected %t", i, result, this.expect)
+ }
+ }
+}
+
+func TestHasStringsSuffix(t *testing.T) {
+ for i, this := range []struct {
+ s []string
+ suffix []string
+ expect bool
+ }{
+ {[]string{"a"}, []string{"a"}, true},
+ {[]string{}, []string{}, true},
+ {[]string{"a", "b", "c"}, []string{"b", "c"}, true},
+ {[]string{"abra", "ca", "dabra"}, []string{"abra", "ca"}, false},
+ {[]string{"abra", "ca", "dabra"}, []string{"ca", "dabra"}, true},
+ } {
+ result := HasStringsSuffix(this.s, this.suffix)
+ if result != this.expect {
+ t.Fatalf("[%d] got %t but expected %t", 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 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 := 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 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 TestGetTitleFunc(t *testing.T) {
+ title := "somewhere over the rainbow"
+ assert := require.New(t)
+
+ assert.Equal("Somewhere Over The Rainbow", GetTitleFunc("go")(title))
+ assert.Equal("Somewhere over the Rainbow", GetTitleFunc("chicago")(title), "Chicago style")
+ assert.Equal("Somewhere over the Rainbow", GetTitleFunc("Chicago")(title), "Chicago style")
+ assert.Equal("Somewhere Over the Rainbow", GetTitleFunc("ap")(title), "AP style")
+ assert.Equal("Somewhere Over the Rainbow", GetTitleFunc("ap")(title), "AP style")
+ assert.Equal("Somewhere Over the Rainbow", GetTitleFunc("")(title), "AP style")
+ assert.Equal("Somewhere Over the Rainbow", GetTitleFunc("unknown")(title), "AP style")
+
+}
+
+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 TestFastMD5FromFile(t *testing.T) {
+ fs := afero.NewMemMapFs()
+
+ if err := afero.WriteFile(fs, "small.txt", []byte("abc"), 0777); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := afero.WriteFile(fs, "small2.txt", []byte("abd"), 0777); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := afero.WriteFile(fs, "bigger.txt", []byte(strings.Repeat("a bc d e", 100)), 0777); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := afero.WriteFile(fs, "bigger2.txt", []byte(strings.Repeat("c d e f g", 100)), 0777); err != nil {
+ t.Fatal(err)
+ }
+
+ req := require.New(t)
+
+ sf1, err := fs.Open("small.txt")
+ req.NoError(err)
+ sf2, err := fs.Open("small2.txt")
+ req.NoError(err)
+
+ bf1, err := fs.Open("bigger.txt")
+ req.NoError(err)
+ bf2, err := fs.Open("bigger2.txt")
+ req.NoError(err)
+
+ defer sf1.Close()
+ defer sf2.Close()
+ defer bf1.Close()
+ defer bf2.Close()
+
+ m1, err := MD5FromFileFast(sf1)
+ req.NoError(err)
+ req.Equal("e9c8989b64b71a88b4efb66ad05eea96", m1)
+
+ m2, err := MD5FromFileFast(sf2)
+ req.NoError(err)
+ req.NotEqual(m1, m2)
+
+ m3, err := MD5FromFileFast(bf1)
+ req.NoError(err)
+ req.NotEqual(m2, m3)
+
+ m4, err := MD5FromFileFast(bf2)
+ req.NoError(err)
+ req.NotEqual(m3, m4)
+
+ m5, err := MD5FromReader(bf2)
+ req.NoError(err)
+ req.NotEqual(m4, m5)
+}
+
+func BenchmarkMD5FromFileFast(b *testing.B) {
+ fs := afero.NewMemMapFs()
+
+ for _, full := range []bool{false, true} {
+ b.Run(fmt.Sprintf("full=%t", full), func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ b.StopTimer()
+ if err := afero.WriteFile(fs, "file.txt", []byte(strings.Repeat("1234567890", 2000)), 0777); err != nil {
+ b.Fatal(err)
+ }
+ f, err := fs.Open("file.txt")
+ if err != nil {
+ b.Fatal(err)
+ }
+ b.StartTimer()
+ if full {
+ if _, err := MD5FromReader(f); err != nil {
+ b.Fatal(err)
+ }
+ } else {
+ if _, err := MD5FromFileFast(f); err != nil {
+ b.Fatal(err)
+ }
+ }
+ f.Close()
+ }
+ })
+ }
+
+}
diff --git a/helpers/path.go b/helpers/path.go
new file mode 100644
index 000000000..36bd3269b
--- /dev/null
+++ b/helpers/path.go
@@ -0,0 +1,667 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "sort"
+ "strings"
+ "unicode"
+
+ "github.com/gohugoio/hugo/config"
+
+ "github.com/gohugoio/hugo/common/hugio"
+ _errors "github.com/pkg/errors"
+ "github.com/spf13/afero"
+ "golang.org/x/text/runes"
+ "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")
+)
+
+// 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(s)
+}
+
+// MakePathsSanitized applies MakePathSanitized on every item in the slice
+func (p *PathSpec) MakePathsSanitized(paths []string) {
+ for i, path := range paths {
+ paths[i] = p.MakePathSanitized(path)
+ }
+}
+
+// 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))
+}
+
+// ToSlashTrimLeading is just a filepath.ToSlaas with an added / prefix trimmer.
+func ToSlashTrimLeading(s string) string {
+ return strings.TrimPrefix(filepath.ToSlash(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.
+// Spaces will be replaced with a single hyphen, and sequential hyphens will be reduced to one.
+func (p *PathSpec) UnicodeSanitize(s string) string {
+ source := []rune(s)
+ target := make([]rune, 0, len(source))
+ var prependHyphen bool
+
+ for i, r := range source {
+ isAllowed := r == '.' || r == '/' || r == '\\' || r == '_' || r == '#' || r == '+' || r == '~'
+ isAllowed = isAllowed || unicode.IsLetter(r) || unicode.IsDigit(r) || unicode.IsMark(r)
+ isAllowed = isAllowed || (r == '%' && i+2 < len(source) && ishex(source[i+1]) && ishex(source[i+2]))
+
+ if isAllowed {
+ if prependHyphen {
+ target = append(target, '-')
+ prependHyphen = false
+ }
+ target = append(target, r)
+ } else if len(target) > 0 && (r == '-' || unicode.IsSpace(r)) {
+ prependHyphen = true
+ }
+ }
+
+ var result string
+
+ if p.RemovePathAccents {
+ // remove accents - see https://blog.golang.org/normalization
+ t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
+ result, _, _ = transform.String(t, string(target))
+ } else {
+ result = string(target)
+ }
+
+ return result
+}
+
+// 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
+}
+
+// GetFirstThemeDir gets the root directory of the first theme, if there is one.
+// If there is no theme, returns the empty string.
+func (p *PathSpec) GetFirstThemeDir() string {
+ if p.ThemeSet() {
+ return p.AbsPathify(filepath.Join(p.ThemesDir, p.Themes()[0]))
+ }
+ return ""
+}
+
+// GetThemesDir gets the absolute root theme dir path.
+func (p *PathSpec) GetThemesDir() string {
+ if p.ThemeSet() {
+ return p.AbsPathify(p.ThemesDir)
+ }
+ 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.Themes()[0]), FilePathSeparator)
+ }
+ return ""
+}
+
+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
+}
+
+// ExtNoDelimiter takes a path and returns the extension, excluding the delmiter, i.e. "md".
+func ExtNoDelimiter(in string) string {
+ return strings.TrimPrefix(Ext(in), ".")
+}
+
+// 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
+}
+
+// PathAndExt is the same as FileAndExt, but it uses the path package.
+func PathAndExt(in string) (string, string) {
+ return fileAndExt(in, pb)
+}
+
+// FileAndExt takes a path and returns the file and extension separated,
+// the extension including the delmiter, i.e. ".md".
+func FileAndExt(in string) (string, string) {
+ return fileAndExt(in, fpb)
+}
+
+// FileAndExtNoDelimiter takes a path and returns the file and extension separated,
+// the extension excluding the delmiter, e.g "md".
+func FileAndExtNoDelimiter(in string) (string, string) {
+ file, ext := fileAndExt(in, fpb)
+ return file, strings.TrimPrefix(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
+}
+
+// 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 root != "" && len(root) < 4 {
+ return errors.New("path is too short")
+ }
+
+ // 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
+ }
+
+ // Some of Hugo's filesystems represents an ordered root folder, i.e. project first, then theme folders.
+ // Make sure that order is preserved. afero.Walk will sort the directories down in the file tree,
+ // but we don't care about that.
+ rootContent, err := readDir(fs, root, false)
+
+ 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 readDir(fs afero.Fs, dirname string, doSort bool) ([]os.FileInfo, error) {
+ f, err := fs.Open(dirname)
+ if err != nil {
+ return nil, err
+ }
+ list, err := f.Readdir(-1)
+ f.Close()
+ if err != nil {
+ return nil, err
+ }
+ if doSort {
+ sort.Slice(list, func(i, j int) bool { return list[i].Name() < list[j].Name() })
+ }
+ return list, nil
+}
+
+func getRealFileInfo(fs afero.Fs, path string) (os.FileInfo, string, error) {
+ fileInfo, err := LstatIfPossible(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, "", _errors.Wrapf(err, "Cannot read symbolic link %q", path)
+ }
+ fileInfo, err = LstatIfPossible(fs, link)
+ if err != nil {
+ return nil, "", _errors.Wrapf(err, "Cannot stat %q", link)
+ }
+ 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
+}
+
+// LstatIfPossible can be used to call Lstat if possible, else Stat.
+func LstatIfPossible(fs afero.Fs, path string) (os.FileInfo, error) {
+ if lstater, ok := fs.(afero.Lstater); ok {
+ fi, _, err := lstater.LstatIfPossible(path)
+ return fi, err
+ }
+
+ return fs.Stat(path)
+}
+
+// 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)
+}
+
+// OpenFilesForWriting opens all the given filenames for writing.
+func OpenFilesForWriting(fs afero.Fs, filenames ...string) (io.WriteCloser, error) {
+ var writeClosers []io.WriteCloser
+ for _, filename := range filenames {
+ f, err := OpenFileForWriting(fs, filename)
+ if err != nil {
+ for _, wc := range writeClosers {
+ wc.Close()
+ }
+ return nil, err
+ }
+ writeClosers = append(writeClosers, f)
+ }
+
+ return hugio.NewMultiWriteCloser(writeClosers...), nil
+
+}
+
+// OpenFileForWriting opens or creates the given file. If the target directory
+// does not exist, it gets created.
+func OpenFileForWriting(fs afero.Fs, filename string) (afero.File, error) {
+ filename = filepath.Clean(filename)
+ // Create will truncate if file already exists.
+ // os.Create will create any new files with mode 0666 (before umask).
+ f, err := fs.Create(filename)
+ if err != nil {
+ if !os.IsNotExist(err) {
+ return nil, err
+ }
+ if err = fs.MkdirAll(filepath.Dir(filename), 0777); err != nil { // before umask
+ return nil, err
+ }
+ f, err = fs.Create(filename)
+ }
+
+ return f, err
+}
+
+// GetCacheDir returns a cache dir from the given filesystem and config.
+// The dir will be created if it does not exist.
+func GetCacheDir(fs afero.Fs, cfg config.Provider) (string, error) {
+ cacheDir := getCacheDir(cfg)
+ if cacheDir != "" {
+ exists, err := DirExists(cacheDir, fs)
+ if err != nil {
+ return "", err
+ }
+ if !exists {
+ err := fs.MkdirAll(cacheDir, 0777) // Before umask
+ if err != nil {
+ return "", _errors.Wrap(err, "failed to create cache dir")
+ }
+ }
+ return cacheDir, nil
+ }
+
+ // Fall back to a cache in /tmp.
+ return GetTempDir("hugo_cache", fs), nil
+
+}
+
+func getCacheDir(cfg config.Provider) string {
+ // Always use the cacheDir config if set.
+ cacheDir := cfg.GetString("cacheDir")
+ if len(cacheDir) > 1 {
+ return addTrailingFileSeparator(cacheDir)
+ }
+
+ // Both of these are fairly distinctive OS env keys used by Netlify.
+ if os.Getenv("DEPLOY_PRIME_URL") != "" && os.Getenv("PULL_REQUEST") != "" {
+ // Netlify's cache behaviour is not documented, the currently best example
+ // is this project:
+ // https://github.com/philhawksworth/content-shards/blob/master/gulpfile.js
+ return "/opt/build/cache/hugo_cache/"
+
+ }
+
+ // This will fall back to an hugo_cache folder in the tmp dir, which should work fine for most CI
+ // providers. See this for a working CircleCI setup:
+ // https://github.com/bep/hugo-sass-test/blob/6c3960a8f4b90e8938228688bc49bdcdd6b2d99e/.circleci/config.yml
+ // If not, they can set the HUGO_CACHEDIR environment variable or cacheDir config key.
+ return ""
+}
+
+func addTrailingFileSeparator(s string) string {
+ if !strings.HasSuffix(s, FilePathSeparator) {
+ s = s + FilePathSeparator
+ }
+ return s
+}
+
+// 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..98291936c
--- /dev/null
+++ b/helpers/path_test.go
@@ -0,0 +1,808 @@
+// 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/gohugoio/hugo/langs"
+
+ "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 := newTestCfg()
+ v.Set("removePathAccents", test.removeAccents)
+
+ l := langs.NewDefaultLanguage(v)
+ p, err := NewPathSpec(hugofs.NewMem(v), l)
+ require.NoError(t, err)
+
+ 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()
+ v.Set("contentDir", "content")
+ v.Set("dataDir", "data")
+ v.Set("i18nDir", "i18n")
+ v.Set("layoutDir", "layouts")
+ v.Set("assetDir", "assets")
+ v.Set("resourceDir", "resources")
+ v.Set("publishDir", "public")
+ v.Set("archetypeDir", "archetypes")
+
+ l := langs.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 := newTestCfg()
+
+ v.Set("disablePathToLower", true)
+
+ l := langs.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, _ := 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 ??
+ return nil, err
+ }
+ 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) {
+ _ = os.Remove(f.Name())
+}
+
+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 {
+ return "", dirErr
+ }
+ 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 {
+ return "", dirErr
+ }
+ 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) {
+ _ = os.RemoveAll(d)
+}
+
+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 TestExtNoDelimiter(t *testing.T) {
+ assert := require.New(t)
+ assert.Equal("json", ExtNoDelimiter(filepath.FromSlash("/my/data.json")))
+}
+
+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 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..b82ebd992
--- /dev/null
+++ b/helpers/pathspec.go
@@ -0,0 +1,88 @@
+// 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 (
+ "strings"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/hugolib/filesystems"
+ "github.com/gohugoio/hugo/hugolib/paths"
+)
+
+// PathSpec holds methods that decides how paths in URLs and files in Hugo should look like.
+type PathSpec struct {
+ *paths.Paths
+ *filesystems.BaseFs
+
+ ProcessingStats *ProcessingStats
+
+ // The file systems to use
+ Fs *hugofs.Fs
+
+ // The config provider to use
+ Cfg config.Provider
+}
+
+// NewPathSpec creats a new PathSpec from the given filesystems and language.
+func NewPathSpec(fs *hugofs.Fs, cfg config.Provider) (*PathSpec, error) {
+ return NewPathSpecWithBaseBaseFsProvided(fs, cfg, nil)
+}
+
+// NewPathSpecWithBaseBaseFsProvided creats a new PathSpec from the given filesystems and language.
+// If an existing BaseFs is provided, parts of that is reused.
+func NewPathSpecWithBaseBaseFsProvided(fs *hugofs.Fs, cfg config.Provider, baseBaseFs *filesystems.BaseFs) (*PathSpec, error) {
+
+ p, err := paths.New(fs, cfg)
+ if err != nil {
+ return nil, err
+ }
+
+ var options []func(*filesystems.BaseFs) error
+ if baseBaseFs != nil {
+ options = []func(*filesystems.BaseFs) error{
+ filesystems.WithBaseFs(baseBaseFs),
+ }
+ }
+ bfs, err := filesystems.NewBase(p, options...)
+ if err != nil {
+ return nil, err
+ }
+
+ ps := &PathSpec{
+ Paths: p,
+ BaseFs: bfs,
+ Fs: fs,
+ Cfg: cfg,
+ ProcessingStats: NewProcessingStats(p.Lang()),
+ }
+
+ basePath := ps.BaseURL.Path()
+ if basePath != "" && basePath != "/" {
+ ps.BasePath = basePath
+ }
+
+ return ps, nil
+}
+
+// PermalinkForBaseURL creates a permalink from the given link and baseURL.
+func (p *PathSpec) PermalinkForBaseURL(link, baseURL string) string {
+ link = strings.TrimPrefix(link, "/")
+ if !strings.HasSuffix(baseURL, "/") {
+ baseURL += "/"
+ }
+ return baseURL + link
+
+}
diff --git a/helpers/pathspec_test.go b/helpers/pathspec_test.go
new file mode 100644
index 000000000..00dd9cd7b
--- /dev/null
+++ b/helpers/pathspec_test.go
@@ -0,0 +1,54 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/gohugoio/hugo/langs"
+ "github.com/stretchr/testify/require"
+)
+
+func TestNewPathSpecFromConfig(t *testing.T) {
+ v := newTestCfg()
+ l := langs.NewLanguage("no", v)
+ v.Set("disablePathToLower", true)
+ v.Set("removePathAccents", true)
+ v.Set("uglyURLs", true)
+ 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.DisablePathToLower)
+ require.True(t, p.RemovePathAccents)
+ require.True(t, p.UglyURLs)
+ 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, "thework", p.WorkingDir)
+ require.Equal(t, []string{"thetheme"}, p.Themes())
+}
diff --git a/helpers/processing_stats.go b/helpers/processing_stats.go
new file mode 100644
index 000000000..4382d5fa5
--- /dev/null
+++ b/helpers/processing_stats.go
@@ -0,0 +1,123 @@
+// 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 helpers
+
+import (
+ "io"
+ "strconv"
+ "sync/atomic"
+
+ "github.com/olekukonko/tablewriter"
+)
+
+// ProcessingStats represents statistics about a site build.
+type ProcessingStats struct {
+ Name string
+
+ Pages uint64
+ PaginatorPages uint64
+ Static uint64
+ ProcessedImages uint64
+ Files uint64
+ Aliases uint64
+ Sitemaps uint64
+ Cleaned uint64
+}
+
+type processingStatsTitleVal struct {
+ name string
+ val uint64
+}
+
+func (s *ProcessingStats) toVals() []processingStatsTitleVal {
+ return []processingStatsTitleVal{
+ {"Pages", s.Pages},
+ {"Paginator pages", s.PaginatorPages},
+ {"Non-page files", s.Files},
+ {"Static files", s.Static},
+ {"Processed images", s.ProcessedImages},
+ {"Aliases", s.Aliases},
+ {"Sitemaps", s.Sitemaps},
+ {"Cleaned", s.Cleaned},
+ }
+}
+
+// NewProcessingStats returns a new ProcessingStats instance.
+func NewProcessingStats(name string) *ProcessingStats {
+ return &ProcessingStats{Name: name}
+}
+
+// Incr increments a given counter.
+func (s *ProcessingStats) Incr(counter *uint64) {
+ atomic.AddUint64(counter, 1)
+}
+
+// Add adds an amount to a given counter.
+func (s *ProcessingStats) Add(counter *uint64, amount int) {
+ atomic.AddUint64(counter, uint64(amount))
+}
+
+// Table writes a table-formatted representation of the stats in a
+// ProcessingStats instance to w.
+func (s *ProcessingStats) Table(w io.Writer) {
+ titleVals := s.toVals()
+ data := make([][]string, len(titleVals))
+ for i, tv := range titleVals {
+ data[i] = []string{tv.name, strconv.Itoa(int(tv.val))}
+ }
+
+ table := tablewriter.NewWriter(w)
+
+ table.AppendBulk(data)
+ table.SetHeader([]string{"", s.Name})
+ table.SetBorder(false)
+ table.Render()
+
+}
+
+// ProcessingStatsTable writes a table-formatted representation of stats to w.
+func ProcessingStatsTable(w io.Writer, stats ...*ProcessingStats) {
+ names := make([]string, len(stats)+1)
+
+ var data [][]string
+
+ for i := 0; i < len(stats); i++ {
+ stat := stats[i]
+ names[i+1] = stat.Name
+
+ titleVals := stat.toVals()
+
+ if i == 0 {
+ data = make([][]string, len(titleVals))
+ }
+
+ for j, tv := range titleVals {
+ if i == 0 {
+ data[j] = []string{tv.name, strconv.Itoa(int(tv.val))}
+ } else {
+ data[j] = append(data[j], strconv.Itoa(int(tv.val)))
+ }
+
+ }
+
+ }
+
+ table := tablewriter.NewWriter(w)
+
+ table.AppendBulk(data)
+ table.SetHeader(names)
+ table.SetBorder(false)
+ table.Render()
+
+}
diff --git a/helpers/pygments.go b/helpers/pygments.go
new file mode 100644
index 000000000..0fe1e7592
--- /dev/null
+++ b/helpers/pygments.go
@@ -0,0 +1,402 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+
+ "github.com/alecthomas/chroma"
+ "github.com/alecthomas/chroma/formatters"
+ "github.com/alecthomas/chroma/formatters/html"
+ "github.com/alecthomas/chroma/lexers"
+ "github.com/alecthomas/chroma/styles"
+ bp "github.com/gohugoio/hugo/bufferpool"
+
+ "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
+}
+
+type highlighters struct {
+ cs *ContentSpec
+ ignoreCache bool
+ cacheDir string
+}
+
+func newHiglighters(cs *ContentSpec) highlighters {
+ return highlighters{cs: cs, ignoreCache: cs.Cfg.GetBool("ignoreCache"), cacheDir: cs.Cfg.GetString("cacheDir")}
+}
+
+func (h highlighters) chromaHighlight(code, lang, optsStr string) (string, error) {
+ opts, err := h.cs.parsePygmentsOpts(optsStr)
+ if err != nil {
+ jww.ERROR.Print(err.Error())
+ return code, err
+ }
+
+ style, found := opts["style"]
+ if !found || style == "" {
+ style = "friendly"
+ }
+
+ f, err := h.cs.chromaFormatterFromOptions(opts)
+ if err != nil {
+ jww.ERROR.Print(err.Error())
+ return code, err
+ }
+
+ b := bp.GetBuffer()
+ defer bp.PutBuffer(b)
+
+ err = chromaHighlight(b, code, lang, style, f)
+ if err != nil {
+ jww.ERROR.Print(err.Error())
+ return code, err
+ }
+
+ return h.injectCodeTag(`<div class="highlight">`+b.String()+"</div>", lang), nil
+}
+
+func (h highlighters) pygmentsHighlight(code, lang, optsStr string) (string, error) {
+ options, err := h.cs.createPygmentsOptionsString(optsStr)
+
+ if err != nil {
+ jww.ERROR.Print(err.Error())
+ return code, nil
+ }
+
+ // Try to read from cache first
+ hash := sha1.New()
+ io.WriteString(hash, code)
+ io.WriteString(hash, lang)
+ io.WriteString(hash, options)
+
+ fs := hugofs.Os
+
+ var cachefile string
+
+ if !h.ignoreCache && h.cacheDir != "" {
+ cachefile = filepath.Join(h.cacheDir, fmt.Sprintf("pygments-%x", hash.Sum(nil)))
+
+ exists, err := Exists(cachefile, fs)
+ if err != nil {
+ jww.ERROR.Print(err.Error())
+ return code, nil
+ }
+ if exists {
+ f, err := fs.Open(cachefile)
+ if err != nil {
+ jww.ERROR.Print(err.Error())
+ return code, nil
+ }
+
+ s, err := ioutil.ReadAll(f)
+ if err != nil {
+ jww.ERROR.Print(err.Error())
+ return code, nil
+ }
+
+ return string(s), nil
+ }
+ }
+
+ // 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, err
+ }
+
+ str := string(normalizeExternalHelperLineFeeds(out.Bytes()))
+
+ str = h.injectCodeTag(str, lang)
+
+ if !h.ignoreCache && cachefile != "" {
+ // Write cache file
+ if err := WriteToDisk(cachefile, strings.NewReader(str), fs); err != nil {
+ jww.ERROR.Print(stderr.String())
+ }
+ }
+
+ return str, nil
+}
+
+var preRe = regexp.MustCompile(`(?s)(.*?<pre.*?>)(.*?)(</pre>)`)
+
+func (h highlighters) injectCodeTag(code, lang string) string {
+ if lang == "" {
+ return code
+ }
+ codeTag := fmt.Sprintf(`<code class="language-%s" data-lang="%s">`, lang, lang)
+ return preRe.ReplaceAllString(code, fmt.Sprintf("$1%s$2</code>$3", codeTag))
+}
+
+func chromaHighlight(w io.Writer, source, lexer, style string, f chroma.Formatter) error {
+ l := lexers.Get(lexer)
+ if l == nil {
+ l = lexers.Analyse(source)
+ }
+ if l == nil {
+ l = lexers.Fallback
+ }
+ l = chroma.Coalesce(l)
+
+ if f == nil {
+ f = formatters.Fallback
+ }
+
+ s := styles.Get(style)
+ if s == nil {
+ s = styles.Fallback
+ }
+
+ it, err := l.Tokenise(nil, source)
+ if err != nil {
+ return err
+ }
+
+ return f.Format(w, s, it)
+}
+
+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(defaults map[string]string, in string) (map[string]string, error) {
+ in = strings.Trim(in, " ")
+ opts := make(map[string]string)
+
+ for k, v := range defaults {
+ opts[k] = v
+ }
+
+ if in == "" {
+ return opts, 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 opts, fmt.Errorf("invalid Pygments option: %s", key)
+ }
+ opts[key] = keyVal[1]
+ }
+
+ return opts, 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, err := parseOptions(nil, 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 (cs *ContentSpec) chromaFormatterFromOptions(pygmentsOpts map[string]string) (chroma.Formatter, error) {
+ var options = []html.Option{html.TabWidth(4)}
+
+ if pygmentsOpts["noclasses"] == "false" {
+ options = append(options, html.WithClasses())
+ }
+
+ lineNumbers := pygmentsOpts["linenos"]
+ if lineNumbers != "" {
+ options = append(options, html.WithLineNumbers())
+ if lineNumbers != "inline" {
+ options = append(options, html.LineNumbersInTable())
+ }
+ }
+
+ startLineStr := pygmentsOpts["linenostart"]
+ var startLine = 1
+ if startLineStr != "" {
+
+ line, err := strconv.Atoi(strings.TrimSpace(startLineStr))
+ if err == nil {
+ startLine = line
+ options = append(options, html.BaseLineNumber(startLine))
+ }
+ }
+
+ hlLines := pygmentsOpts["hl_lines"]
+
+ if hlLines != "" {
+ ranges, err := hlLinesToRanges(startLine, hlLines)
+
+ if err == nil {
+ options = append(options, html.HighlightLines(ranges))
+ }
+ }
+
+ return html.New(options...), nil
+}
+
+func (cs *ContentSpec) parsePygmentsOpts(in string) (map[string]string, error) {
+ opts, err := parseOptions(cs.defatultPygmentsOpts, in)
+ if err != nil {
+ return nil, err
+ }
+ return opts, nil
+
+}
+
+func (cs *ContentSpec) createPygmentsOptionsString(in string) (string, error) {
+ opts, err := cs.parsePygmentsOpts(in)
+ if err != nil {
+ return "", err
+ }
+ return createOptionsString(opts), nil
+}
+
+// startLine compansates for https://github.com/alecthomas/chroma/issues/30
+func hlLinesToRanges(startLine int, s string) ([][2]int, error) {
+ var ranges [][2]int
+ s = strings.TrimSpace(s)
+
+ if s == "" {
+ return ranges, nil
+ }
+
+ // Variants:
+ // 1 2 3 4
+ // 1-2 3-4
+ // 1-2 3
+ // 1 3-4
+ // 1 3-4
+ fields := strings.Split(s, " ")
+ for _, field := range fields {
+ field = strings.TrimSpace(field)
+ if field == "" {
+ continue
+ }
+ numbers := strings.Split(field, "-")
+ var r [2]int
+ first, err := strconv.Atoi(numbers[0])
+ if err != nil {
+ return ranges, err
+ }
+ first = first + startLine - 1
+ r[0] = first
+ if len(numbers) > 1 {
+ second, err := strconv.Atoi(numbers[1])
+ if err != nil {
+ return ranges, err
+ }
+ second = second + startLine - 1
+ r[1] = second
+ } else {
+ r[1] = first
+ }
+
+ ranges = append(ranges, r)
+ }
+ return ranges, nil
+
+}
diff --git a/helpers/pygments_test.go b/helpers/pygments_test.go
new file mode 100644
index 000000000..860f317d8
--- /dev/null
+++ b/helpers/pygments_test.go
@@ -0,0 +1,300 @@
+// 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"
+ "reflect"
+ "testing"
+
+ "github.com/alecthomas/chroma/formatters/html"
+
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/require"
+)
+
+func TestParsePygmentsArgs(t *testing.T) {
+ assert := require.New(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)
+ spec, err := NewContentSpec(v)
+ assert.NoError(err)
+
+ result1, err := spec.createPygmentsOptionsString(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) {
+ assert := require.New(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)
+ }
+
+ spec, err := NewContentSpec(v)
+ assert.NoError(err)
+
+ result, err := spec.createPygmentsOptionsString(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)
+ }
+ }
+}
+
+type chromaInfo struct {
+ classes bool
+ lineNumbers bool
+ lineNumbersInTable bool
+ highlightRangesLen int
+ highlightRangesStr string
+ baseLineNumber int
+}
+
+func formatterChromaInfo(f *html.Formatter) chromaInfo {
+ v := reflect.ValueOf(f).Elem()
+ c := chromaInfo{}
+ // Hack:
+
+ c.classes = f.Classes
+ c.lineNumbers = v.FieldByName("lineNumbers").Bool()
+ c.lineNumbersInTable = v.FieldByName("lineNumbersInTable").Bool()
+ c.baseLineNumber = int(v.FieldByName("baseLineNumber").Int())
+ vv := v.FieldByName("highlightRanges")
+ c.highlightRangesLen = vv.Len()
+ c.highlightRangesStr = fmt.Sprint(vv)
+
+ return c
+}
+
+func TestChromaHTMLHighlight(t *testing.T) {
+ assert := require.New(t)
+
+ v := viper.New()
+ v.Set("pygmentsUseClasses", true)
+ spec, err := NewContentSpec(v)
+ assert.NoError(err)
+
+ result, err := spec.Highlight(`echo "Hello"`, "bash", "")
+ assert.NoError(err)
+
+ assert.Contains(result, `<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash"><span class="nb">echo</span> <span class="s2">&#34;Hello&#34;</span></code></pre></div>`)
+
+}
+
+func TestChromaHTMLFormatterFromOptions(t *testing.T) {
+ assert := require.New(t)
+
+ for i, this := range []struct {
+ in string
+ pygmentsStyle interface{}
+ pygmentsUseClasses interface{}
+ pygmentsOptions string
+ assert func(c chromaInfo)
+ }{
+ {"", "monokai", true, "style=manni,noclasses=true", func(c chromaInfo) {
+ assert.True(c.classes)
+ assert.False(c.lineNumbers)
+ assert.Equal(0, c.highlightRangesLen)
+
+ }},
+ {"", nil, nil, "style=monokai,noclasses=false", func(c chromaInfo) {
+ assert.True(c.classes)
+ }},
+ {"linenos=sure,hl_lines=1 2 3", nil, nil, "style=monokai,noclasses=false", func(c chromaInfo) {
+ assert.True(c.classes)
+ assert.True(c.lineNumbers)
+ assert.Equal(3, c.highlightRangesLen)
+ assert.Equal("[[1 1] [2 2] [3 3]]", c.highlightRangesStr)
+ assert.Equal(1, c.baseLineNumber)
+ }},
+ {"linenos=inline,hl_lines=1,linenostart=4", nil, nil, "style=monokai,noclasses=false", func(c chromaInfo) {
+ assert.True(c.classes)
+ assert.True(c.lineNumbers)
+ assert.False(c.lineNumbersInTable)
+ assert.Equal(1, c.highlightRangesLen)
+ // This compansates for https://github.com/alecthomas/chroma/issues/30
+ assert.Equal("[[4 4]]", c.highlightRangesStr)
+ assert.Equal(4, c.baseLineNumber)
+ }},
+ {"linenos=table", nil, nil, "style=monokai", func(c chromaInfo) {
+ assert.True(c.lineNumbers)
+ assert.True(c.lineNumbersInTable)
+ }},
+ {"style=monokai,noclasses=false", nil, nil, "style=manni,noclasses=true", func(c chromaInfo) {
+ assert.True(c.classes)
+ }},
+ {"style=monokai,noclasses=true", "friendly", false, "style=manni,noclasses=false", func(c chromaInfo) {
+ assert.False(c.classes)
+ }},
+ } {
+ 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)
+ }
+
+ spec, err := NewContentSpec(v)
+ assert.NoError(err)
+
+ opts, err := spec.parsePygmentsOpts(this.in)
+ if err != nil {
+ t.Fatalf("[%d] parsePygmentsOpts failed: %s", i, err)
+ }
+
+ chromaFormatter, err := spec.chromaFormatterFromOptions(opts)
+ if err != nil {
+ t.Fatalf("[%d] chromaFormatterFromOptions failed: %s", i, err)
+ }
+
+ this.assert(formatterChromaInfo(chromaFormatter.(*html.Formatter)))
+ }
+}
+
+func TestHlLinesToRanges(t *testing.T) {
+ var zero [][2]int
+
+ for _, this := range []struct {
+ in string
+ startLine int
+ expected interface{}
+ }{
+ {"", 1, zero},
+ {"1 4", 1, [][2]int{{1, 1}, {4, 4}}},
+ {"1 4", 2, [][2]int{{2, 2}, {5, 5}}},
+ {"1-4 5-8", 1, [][2]int{{1, 4}, {5, 8}}},
+ {" 1 4 ", 1, [][2]int{{1, 1}, {4, 4}}},
+ {"1-4 5-8 ", 1, [][2]int{{1, 4}, {5, 8}}},
+ {"1-4 5", 1, [][2]int{{1, 4}, {5, 5}}},
+ {"4 5-9", 1, [][2]int{{4, 4}, {5, 9}}},
+ {" 1 -4 5 - 8 ", 1, true},
+ {"a b", 1, true},
+ } {
+ got, err := hlLinesToRanges(this.startLine, this.in)
+
+ if expectErr, ok := this.expected.(bool); ok && expectErr {
+ if err == nil {
+ t.Fatal("No error")
+ }
+ } else if err != nil {
+ t.Fatalf("Got error: %s", err)
+ } else if !reflect.DeepEqual(this.expected, got) {
+ t.Fatalf("Expected\n%v but got\n%v", this.expected, got)
+ }
+ }
+}
+
+func BenchmarkChromaHighlight(b *testing.B) {
+ assert := require.New(b)
+ v := viper.New()
+
+ v.Set("pygmentsstyle", "trac")
+ v.Set("pygmentsuseclasses", false)
+ v.Set("pygmentsuseclassic", false)
+
+ code := `// GetTitleFunc returns a func that can be used to transform a string to
+// title case.
+//
+// The supported styles are
+//
+// - "Go" (strings.Title)
+// - "AP" (see https://www.apstylebook.com/)
+// - "Chicago" (see http://www.chicagomanualofstyle.org/home.html)
+//
+// If an unknown or empty style is provided, AP style is what you get.
+func GetTitleFunc(style string) func(s string) string {
+ switch strings.ToLower(style) {
+ case "go":
+ return strings.Title
+ case "chicago":
+ tc := transform.NewTitleConverter(transform.ChicagoStyle)
+ return tc.Title
+ default:
+ tc := transform.NewTitleConverter(transform.APStyle)
+ return tc.Title
+ }
+}
+`
+
+ spec, err := NewContentSpec(v)
+ assert.NoError(err)
+
+ for i := 0; i < b.N; i++ {
+ _, err := spec.Highlight(code, "go", "linenos=inline,hl_lines=8 15-17")
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
diff --git a/helpers/testhelpers_test.go b/helpers/testhelpers_test.go
new file mode 100644
index 000000000..c9da4f129
--- /dev/null
+++ b/helpers/testhelpers_test.go
@@ -0,0 +1,55 @@
+package helpers
+
+import (
+ "github.com/spf13/viper"
+
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/langs"
+)
+
+func newTestPathSpec(fs *hugofs.Fs, v *viper.Viper) *PathSpec {
+ l := langs.NewDefaultLanguage(v)
+ ps, _ := NewPathSpec(fs, l)
+ return ps
+}
+
+func newTestDefaultPathSpec(configKeyValues ...interface{}) *PathSpec {
+ v := viper.New()
+ fs := hugofs.NewMem(v)
+ cfg := newTestCfgFor(fs)
+
+ for i := 0; i < len(configKeyValues); i += 2 {
+ cfg.Set(configKeyValues[i].(string), configKeyValues[i+1])
+ }
+ return newTestPathSpec(fs, cfg)
+}
+
+func newTestCfgFor(fs *hugofs.Fs) *viper.Viper {
+ v := newTestCfg()
+ v.SetFs(fs.Source)
+
+ return v
+
+}
+
+func newTestCfg() *viper.Viper {
+ v := viper.New()
+ v.Set("contentDir", "content")
+ v.Set("dataDir", "data")
+ v.Set("i18nDir", "i18n")
+ v.Set("layoutDir", "layouts")
+ v.Set("assetDir", "assets")
+ v.Set("resourceDir", "resources")
+ v.Set("publishDir", "public")
+ v.Set("archetypeDir", "archetypes")
+ return v
+}
+
+func newTestContentSpec() *ContentSpec {
+ v := viper.New()
+ spec, err := NewContentSpec(v)
+ if err != nil {
+ panic(err)
+ }
+ return spec
+}
diff --git a/helpers/url.go b/helpers/url.go
new file mode 100644
index 000000000..6dbdea299
--- /dev/null
+++ b/helpers/url.go
@@ -0,0 +1,374 @@
+// 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()
+}
+
+// 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
+func (p *PathSpec) PrependBasePath(rel string, isAbs bool) string {
+ basePath := p.GetBasePath(!isAbs)
+ if 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..a2c945dfe
--- /dev/null
+++ b/helpers/url_test.go
@@ -0,0 +1,322 @@
+// 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/gohugoio/hugo/langs"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestURLize(t *testing.T) {
+
+ v := newTestCfg()
+ l := langs.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 := newTestCfg()
+ 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)
+ v.Set("contentDir", "content")
+ l := langs.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 := newTestCfg()
+ 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 := langs.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 := newTestCfg()
+ v.Set("uglyURLs", d.ugly)
+ l := langs.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/htesting/test_structs.go b/htesting/test_structs.go
new file mode 100644
index 000000000..72dc7f3fc
--- /dev/null
+++ b/htesting/test_structs.go
@@ -0,0 +1,100 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package htesting
+
+import (
+ "html/template"
+ "time"
+
+ "github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/langs"
+ "github.com/gohugoio/hugo/navigation"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/spf13/viper"
+)
+
+type testSite struct {
+ h hugo.Info
+ l *langs.Language
+}
+
+func (t testSite) Hugo() hugo.Info {
+ return t.h
+}
+
+func (t testSite) ServerPort() int {
+ return 1313
+}
+
+func (testSite) LastChange() (t time.Time) {
+ return
+}
+
+func (t testSite) Title() string {
+ return "foo"
+}
+
+func (t testSite) Sites() page.Sites {
+ return nil
+}
+
+func (t testSite) IsServer() bool {
+ return false
+}
+
+func (t testSite) Language() *langs.Language {
+ return t.l
+}
+
+func (t testSite) Pages() page.Pages {
+ return nil
+}
+
+func (t testSite) RegularPages() page.Pages {
+ return nil
+}
+
+func (t testSite) Menus() navigation.Menus {
+ return nil
+}
+
+func (t testSite) Taxonomies() interface{} {
+ return nil
+}
+
+func (t testSite) BaseURL() template.URL {
+ return ""
+}
+
+func (t testSite) Params() map[string]interface{} {
+ return nil
+}
+
+func (t testSite) Data() map[string]interface{} {
+ return nil
+}
+
+// NewTestHugoSite creates a new minimal test site.
+func NewTestHugoSite() page.Site {
+ return testSite{
+ h: hugo.NewInfo(hugo.EnvironmentProduction),
+ l: langs.NewLanguage("en", newTestConfig()),
+ }
+}
+
+func newTestConfig() *viper.Viper {
+ v := viper.New()
+ v.Set("contentDir", "content")
+ return v
+}
diff --git a/htesting/testdata_builder.go b/htesting/testdata_builder.go
new file mode 100644
index 000000000..d7ba18521
--- /dev/null
+++ b/htesting/testdata_builder.go
@@ -0,0 +1,59 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package htesting
+
+import (
+ "path/filepath"
+ "testing"
+
+ "github.com/spf13/afero"
+)
+
+type testFile struct {
+ name string
+ content string
+}
+
+type testdataBuilder struct {
+ t testing.TB
+ fs afero.Fs
+ workingDir string
+
+ files []testFile
+}
+
+func NewTestdataBuilder(fs afero.Fs, workingDir string, t testing.TB) *testdataBuilder {
+ workingDir = filepath.Clean(workingDir)
+ return &testdataBuilder{fs: fs, workingDir: workingDir, t: t}
+}
+
+func (b *testdataBuilder) Add(filename, content string) *testdataBuilder {
+ b.files = append(b.files, testFile{name: filename, content: content})
+ return b
+}
+
+func (b *testdataBuilder) Build() *testdataBuilder {
+ for _, f := range b.files {
+ if err := afero.WriteFile(b.fs, filepath.Join(b.workingDir, f.name), []byte(f.content), 0666); err != nil {
+ b.t.Fatalf("failed to add %q: %s", f.name, err)
+ }
+ }
+ return b
+}
+
+func (b testdataBuilder) WithWorkingDir(dir string) *testdataBuilder {
+ b.workingDir = filepath.Clean(dir)
+ b.files = make([]testFile, 0)
+ return &b
+}
diff --git a/hugofs/basepath_real_filename_fs.go b/hugofs/basepath_real_filename_fs.go
new file mode 100644
index 000000000..1024c4d30
--- /dev/null
+++ b/hugofs/basepath_real_filename_fs.go
@@ -0,0 +1,91 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "os"
+
+ "github.com/spf13/afero"
+)
+
+// RealFilenameInfo is a thin wrapper around os.FileInfo adding the real filename.
+type RealFilenameInfo interface {
+ os.FileInfo
+
+ // This is the real filename to the file in the underlying filesystem.
+ RealFilename() string
+}
+
+type realFilenameInfo struct {
+ os.FileInfo
+ realFilename string
+}
+
+func (f *realFilenameInfo) RealFilename() string {
+ return f.realFilename
+}
+
+// NewBasePathRealFilenameFs returns a new BasePathRealFilenameFs instance
+// using base.
+func NewBasePathRealFilenameFs(base *afero.BasePathFs) *BasePathRealFilenameFs {
+ return &BasePathRealFilenameFs{BasePathFs: base}
+}
+
+// BasePathRealFilenameFs is a thin wrapper around afero.BasePathFs that
+// provides the real filename in Stat and LstatIfPossible.
+type BasePathRealFilenameFs struct {
+ *afero.BasePathFs
+}
+
+// Stat returns the os.FileInfo structure describing a given file. If there is
+// an error, it will be of type *os.PathError.
+func (b *BasePathRealFilenameFs) Stat(name string) (os.FileInfo, error) {
+ fi, err := b.BasePathFs.Stat(name)
+ if err != nil {
+ return nil, err
+ }
+
+ if _, ok := fi.(RealFilenameInfo); ok {
+ return fi, nil
+ }
+
+ filename, err := b.RealPath(name)
+ if err != nil {
+ return nil, &os.PathError{Op: "stat", Path: name, Err: err}
+ }
+
+ return &realFilenameInfo{FileInfo: fi, realFilename: filename}, nil
+}
+
+// LstatIfPossible returns the os.FileInfo structure describing a given file.
+// It attempts to use Lstat if supported or defers to the os. In addition to
+// the FileInfo, a boolean is returned telling whether Lstat was called.
+func (b *BasePathRealFilenameFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
+
+ fi, ok, err := b.BasePathFs.LstatIfPossible(name)
+ if err != nil {
+ return nil, false, err
+ }
+
+ if _, ok := fi.(RealFilenameInfo); ok {
+ return fi, ok, nil
+ }
+
+ filename, err := b.RealPath(name)
+ if err != nil {
+ return nil, false, &os.PathError{Op: "lstat", Path: name, Err: err}
+ }
+
+ return &realFilenameInfo{FileInfo: fi, realFilename: filename}, ok, nil
+}
diff --git a/hugofs/createcounting_fs.go b/hugofs/createcounting_fs.go
new file mode 100644
index 000000000..802806b7a
--- /dev/null
+++ b/hugofs/createcounting_fs.go
@@ -0,0 +1,99 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "fmt"
+ "os"
+ "sort"
+ "strings"
+ "sync"
+
+ "github.com/spf13/afero"
+)
+
+// Reseter is implemented by some of the stateful filesystems.
+type Reseter interface {
+ Reset()
+}
+
+// DuplicatesReporter reports about duplicate filenames.
+type DuplicatesReporter interface {
+ ReportDuplicates() string
+}
+
+func NewCreateCountingFs(fs afero.Fs) afero.Fs {
+ return &createCountingFs{Fs: fs, fileCount: make(map[string]int)}
+}
+
+// ReportDuplicates reports filenames written more than once.
+func (c *createCountingFs) ReportDuplicates() string {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ var dupes []string
+
+ for k, v := range c.fileCount {
+ if v > 1 {
+ dupes = append(dupes, fmt.Sprintf("%s (%d)", k, v))
+ }
+ }
+
+ if len(dupes) == 0 {
+ return ""
+ }
+
+ sort.Strings(dupes)
+
+ return strings.Join(dupes, ", ")
+}
+
+// createCountingFs counts filenames of created files or files opened
+// for writing.
+type createCountingFs struct {
+ afero.Fs
+
+ mu sync.Mutex
+ fileCount map[string]int
+}
+
+func (c *createCountingFs) Reset() {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ c.fileCount = make(map[string]int)
+}
+
+func (fs *createCountingFs) onCreate(filename string) {
+ fs.mu.Lock()
+ defer fs.mu.Unlock()
+
+ fs.fileCount[filename] = fs.fileCount[filename] + 1
+}
+
+func (fs *createCountingFs) Create(name string) (afero.File, error) {
+ f, err := fs.Fs.Create(name)
+ if err == nil {
+ fs.onCreate(name)
+ }
+ return f, err
+}
+
+func (fs *createCountingFs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
+ f, err := fs.Fs.OpenFile(name, flag, perm)
+ if err == nil && isWrite(flag) {
+ fs.onCreate(name)
+ }
+ return f, err
+}
diff --git a/hugofs/fs.go b/hugofs/fs.go
new file mode 100644
index 000000000..38590a64e
--- /dev/null
+++ b/hugofs/fs.go
@@ -0,0 +1,88 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "os"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/spf13/afero"
+)
+
+// Os points to an Os Afero file system.
+var Os = &afero.OsFs{}
+
+// Fs abstracts the file system to separate source and destination file systems
+// and allows both to be mocked for testing.
+type Fs struct {
+ // Source is Hugo's source file system.
+ Source afero.Fs
+
+ // Destination is Hugo's destination file system.
+ Destination afero.Fs
+
+ // Os is an OS file system.
+ // NOTE: Field is currently unused.
+ 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
+}
+
+func isWrite(flag int) bool {
+ return flag&os.O_RDWR != 0 || flag&os.O_WRONLY != 0
+}
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/hugofs/hashing_fs.go b/hugofs/hashing_fs.go
new file mode 100644
index 000000000..94a50b960
--- /dev/null
+++ b/hugofs/hashing_fs.go
@@ -0,0 +1,92 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "crypto/md5"
+ "encoding/hex"
+ "hash"
+ "os"
+
+ "github.com/spf13/afero"
+)
+
+var (
+ _ afero.Fs = (*md5HashingFs)(nil)
+)
+
+// FileHashReceiver will receive the filename an the content's MD5 sum on file close.
+type FileHashReceiver interface {
+ OnFileClose(name, md5sum string)
+}
+
+type md5HashingFs struct {
+ afero.Fs
+ hashReceiver FileHashReceiver
+}
+
+// NewHashingFs creates a new filesystem that will receive MD5 checksums of
+// any written file content on Close. Note that this is probably not a good
+// idea for "full build" situations, but when doing fast render mode, the amount
+// of files published is low, and it would be really nice to know exactly which
+// of these files where actually changed.
+// Note that this will only work for file operations that use the io.Writer
+// to write content to file, but that is fine for the "publish content" use case.
+func NewHashingFs(delegate afero.Fs, hashReceiver FileHashReceiver) afero.Fs {
+ return &md5HashingFs{Fs: delegate, hashReceiver: hashReceiver}
+}
+
+func (fs *md5HashingFs) Create(name string) (afero.File, error) {
+ f, err := fs.Fs.Create(name)
+ if err == nil {
+ f = fs.wrapFile(f)
+ }
+ return f, err
+}
+
+func (fs *md5HashingFs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
+ f, err := fs.Fs.OpenFile(name, flag, perm)
+ if err == nil && isWrite(flag) {
+ f = fs.wrapFile(f)
+ }
+ return f, err
+}
+
+func (fs *md5HashingFs) wrapFile(f afero.File) afero.File {
+ return &hashingFile{File: f, h: md5.New(), hashReceiver: fs.hashReceiver}
+}
+
+func (fs *md5HashingFs) Name() string {
+ return "md5HashingFs"
+}
+
+type hashingFile struct {
+ hashReceiver FileHashReceiver
+ h hash.Hash
+ afero.File
+}
+
+func (h *hashingFile) Write(p []byte) (n int, err error) {
+ n, err = h.File.Write(p)
+ if err != nil {
+ return
+ }
+ return h.h.Write(p)
+}
+
+func (h *hashingFile) Close() error {
+ sum := hex.EncodeToString(h.h.Sum(nil))
+ h.hashReceiver.OnFileClose(h.Name(), sum)
+ return h.File.Close()
+}
diff --git a/hugofs/hashing_fs_test.go b/hugofs/hashing_fs_test.go
new file mode 100644
index 000000000..b690630ed
--- /dev/null
+++ b/hugofs/hashing_fs_test.go
@@ -0,0 +1,53 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/stretchr/testify/require"
+)
+
+type testHashReceiver struct {
+ sum string
+ name string
+}
+
+func (t *testHashReceiver) OnFileClose(name, md5hash string) {
+ t.name = name
+ t.sum = md5hash
+}
+
+func TestHashingFs(t *testing.T) {
+ assert := require.New(t)
+
+ fs := afero.NewMemMapFs()
+ observer := &testHashReceiver{}
+ ofs := NewHashingFs(fs, observer)
+
+ f, err := ofs.Create("hashme")
+ assert.NoError(err)
+ _, err = f.Write([]byte("content"))
+ assert.NoError(err)
+ assert.NoError(f.Close())
+ assert.Equal("9a0364b9e99bb480dd25e1f0284c8555", observer.sum)
+ assert.Equal("hashme", observer.name)
+
+ f, err = ofs.Create("nowrites")
+ assert.NoError(err)
+ assert.NoError(f.Close())
+ assert.Equal("d41d8cd98f00b204e9800998ecf8427e", observer.sum)
+
+}
diff --git a/hugofs/language_composite_fs.go b/hugofs/language_composite_fs.go
new file mode 100644
index 000000000..2889f8a00
--- /dev/null
+++ b/hugofs/language_composite_fs.go
@@ -0,0 +1,51 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "github.com/spf13/afero"
+)
+
+var (
+ _ afero.Fs = (*languageCompositeFs)(nil)
+ _ afero.Lstater = (*languageCompositeFs)(nil)
+)
+
+type languageCompositeFs struct {
+ *afero.CopyOnWriteFs
+}
+
+// NewLanguageCompositeFs creates a composite and language aware filesystem.
+// This is a hybrid filesystem. To get a specific file in Open, Stat etc., use the full filename
+// to the target filesystem. This information is available in Readdir, Stat etc. via the
+// special LanguageFileInfo FileInfo implementation.
+func NewLanguageCompositeFs(base afero.Fs, overlay *LanguageFs) afero.Fs {
+ return afero.NewReadOnlyFs(&languageCompositeFs{afero.NewCopyOnWriteFs(base, overlay).(*afero.CopyOnWriteFs)})
+}
+
+// Open takes the full path to the file in the target filesystem. If it is a directory, it gets merged
+// using the language as a weight.
+func (fs *languageCompositeFs) Open(name string) (afero.File, error) {
+ f, err := fs.CopyOnWriteFs.Open(name)
+ if err != nil {
+ return nil, err
+ }
+
+ fu, ok := f.(*afero.UnionFile)
+ if ok {
+ // This is a directory: Merge it.
+ fu.Merger = LanguageDirsMerger
+ }
+ return f, nil
+}
diff --git a/hugofs/language_composite_fs_test.go b/hugofs/language_composite_fs_test.go
new file mode 100644
index 000000000..ab4e25fc0
--- /dev/null
+++ b/hugofs/language_composite_fs_test.go
@@ -0,0 +1,107 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "path/filepath"
+
+ "strings"
+
+ "testing"
+
+ "github.com/spf13/afero"
+ "github.com/stretchr/testify/require"
+)
+
+func TestCompositeLanguagFsTest(t *testing.T) {
+ assert := require.New(t)
+
+ languages := map[string]bool{
+ "sv": true,
+ "en": true,
+ "nn": true,
+ }
+ msv := afero.NewMemMapFs()
+ baseSv := "/content/sv"
+ lfssv := NewLanguageFs("sv", languages, afero.NewBasePathFs(msv, baseSv))
+ mnn := afero.NewMemMapFs()
+ baseNn := "/content/nn"
+ lfsnn := NewLanguageFs("nn", languages, afero.NewBasePathFs(mnn, baseNn))
+ men := afero.NewMemMapFs()
+ baseEn := "/content/en"
+ lfsen := NewLanguageFs("en", languages, afero.NewBasePathFs(men, baseEn))
+
+ // The order will be sv, en, nn
+ composite := NewLanguageCompositeFs(lfsnn, lfsen)
+ composite = NewLanguageCompositeFs(composite, lfssv)
+
+ afero.WriteFile(msv, filepath.Join(baseSv, "f1.txt"), []byte("some sv"), 0755)
+ afero.WriteFile(mnn, filepath.Join(baseNn, "f1.txt"), []byte("some nn"), 0755)
+ afero.WriteFile(men, filepath.Join(baseEn, "f1.txt"), []byte("some en"), 0755)
+
+ // Swedish is the top layer.
+ assertLangFile(t, composite, "f1.txt", "sv")
+
+ afero.WriteFile(msv, filepath.Join(baseSv, "f2.en.txt"), []byte("some sv"), 0755)
+ afero.WriteFile(mnn, filepath.Join(baseNn, "f2.en.txt"), []byte("some nn"), 0755)
+ afero.WriteFile(men, filepath.Join(baseEn, "f2.en.txt"), []byte("some en"), 0755)
+
+ // English is in the middle, but the most specific language match wins.
+ //assertLangFile(t, composite, "f2.en.txt", "en")
+
+ // Fetch some specific language versions
+ assertLangFile(t, composite, filepath.Join(baseNn, "f2.en.txt"), "nn")
+ assertLangFile(t, composite, filepath.Join(baseEn, "f2.en.txt"), "en")
+ assertLangFile(t, composite, filepath.Join(baseSv, "f2.en.txt"), "sv")
+
+ // Read the root
+ f, err := composite.Open("/")
+ assert.NoError(err)
+ defer f.Close()
+ files, err := f.Readdir(-1)
+ assert.NoError(err)
+ assert.Equal(4, len(files))
+ expected := map[string]bool{
+ filepath.FromSlash("/content/en/f1.txt"): true,
+ filepath.FromSlash("/content/nn/f1.txt"): true,
+ filepath.FromSlash("/content/sv/f1.txt"): true,
+ filepath.FromSlash("/content/en/f2.en.txt"): true,
+ }
+ got := make(map[string]bool)
+
+ for _, fi := range files {
+ fil, ok := fi.(*LanguageFileInfo)
+ assert.True(ok)
+ got[fil.Filename()] = true
+ }
+ assert.Equal(expected, got)
+}
+
+func assertLangFile(t testing.TB, fs afero.Fs, filename, match string) {
+ f, err := fs.Open(filename)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer f.Close()
+ b, err := afero.ReadAll(f)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ s := string(b)
+ if !strings.Contains(s, match) {
+ t.Fatalf("got %q expected it to contain %q", s, match)
+
+ }
+}
diff --git a/hugofs/language_fs.go b/hugofs/language_fs.go
new file mode 100644
index 000000000..db77c1fab
--- /dev/null
+++ b/hugofs/language_fs.go
@@ -0,0 +1,346 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/spf13/afero"
+)
+
+const hugoFsMarker = "__hugofs"
+
+var (
+ _ LanguageAnnouncer = (*LanguageFileInfo)(nil)
+ _ FilePather = (*LanguageFileInfo)(nil)
+ _ afero.Lstater = (*LanguageFs)(nil)
+)
+
+// LanguageAnnouncer is aware of its language.
+type LanguageAnnouncer interface {
+ Lang() string
+ TranslationBaseName() string
+}
+
+// FilePather is aware of its file's location.
+type FilePather interface {
+ // Filename gets the full path and filename to the file.
+ Filename() string
+
+ // Path gets the content relative path including file name and extension.
+ // The directory is relative to the content root where "content" is a broad term.
+ Path() string
+
+ // RealName is FileInfo.Name in its original form.
+ RealName() string
+
+ BaseDir() string
+}
+
+// LanguageDirsMerger implements the afero.DirsMerger interface, which is used
+// to merge two directories.
+var LanguageDirsMerger = func(lofi, bofi []os.FileInfo) ([]os.FileInfo, error) {
+ m := make(map[string]*LanguageFileInfo)
+
+ for _, fi := range lofi {
+ fil, ok := fi.(*LanguageFileInfo)
+ if !ok {
+ return nil, fmt.Errorf("received %T, expected *LanguageFileInfo", fi)
+ }
+ m[fil.virtualName] = fil
+ }
+
+ for _, fi := range bofi {
+ fil, ok := fi.(*LanguageFileInfo)
+ if !ok {
+ return nil, fmt.Errorf("received %T, expected *LanguageFileInfo", fi)
+ }
+ existing, found := m[fil.virtualName]
+
+ if !found || existing.weight < fil.weight {
+ m[fil.virtualName] = fil
+ }
+ }
+
+ merged := make([]os.FileInfo, len(m))
+ i := 0
+ for _, v := range m {
+ merged[i] = v
+ i++
+ }
+
+ return merged, nil
+}
+
+// LanguageFileInfo is a super-set of os.FileInfo with additional information
+// about the file in relation to its Hugo language.
+type LanguageFileInfo struct {
+ os.FileInfo
+ lang string
+ baseDir string
+ realFilename string
+ relFilename string
+ name string
+ realName string
+ virtualName string
+ translationBaseName string
+
+ // We add some weight to the files in their own language's content directory.
+ weight int
+}
+
+// Filename returns a file's real filename including the base (ie.
+// "/my/base/sect/page.md").
+func (fi *LanguageFileInfo) Filename() string {
+ return fi.realFilename
+}
+
+// Path returns a file's filename relative to the base (ie. "sect/page.md").
+func (fi *LanguageFileInfo) Path() string {
+ return fi.relFilename
+}
+
+// RealName returns a file's real base name (ie. "page.md").
+func (fi *LanguageFileInfo) RealName() string {
+ return fi.realName
+}
+
+// BaseDir returns a file's base directory (ie. "/my/base").
+func (fi *LanguageFileInfo) BaseDir() string {
+ return fi.baseDir
+}
+
+// Lang returns a file's language (ie. "sv").
+func (fi *LanguageFileInfo) Lang() string {
+ return fi.lang
+}
+
+// TranslationBaseName returns the base filename without any extension or language
+// identifiers (ie. "page").
+func (fi *LanguageFileInfo) TranslationBaseName() string {
+ return fi.translationBaseName
+}
+
+// Name is the name of the file within this filesystem without any path info.
+// It will be marked with language information so we can identify it as ours
+// (ie. "__hugofs_sv_page.md").
+func (fi *LanguageFileInfo) Name() string {
+ return fi.name
+}
+
+type languageFile struct {
+ afero.File
+ fs *LanguageFs
+}
+
+// Readdir creates FileInfo entries by calling Lstat if possible.
+func (l *languageFile) Readdir(c int) (ofi []os.FileInfo, err error) {
+ names, err := l.File.Readdirnames(c)
+ if err != nil {
+ return nil, err
+ }
+
+ fis := make([]os.FileInfo, len(names))
+
+ for i, name := range names {
+ fi, _, err := l.fs.LstatIfPossible(filepath.Join(l.Name(), name))
+
+ if err != nil {
+ return nil, err
+ }
+ fis[i] = fi
+ }
+
+ return fis, err
+}
+
+// LanguageFs represents a language filesystem.
+type LanguageFs struct {
+ // This Fs is usually created with a BasePathFs
+ basePath string
+ lang string
+ nameMarker string
+ languages map[string]bool
+ afero.Fs
+}
+
+// NewLanguageFs creates a new language filesystem.
+func NewLanguageFs(lang string, languages map[string]bool, fs afero.Fs) *LanguageFs {
+ if lang == "" {
+ panic("no lang set for the language fs")
+ }
+ var basePath string
+
+ if bfs, ok := fs.(*afero.BasePathFs); ok {
+ basePath, _ = bfs.RealPath("")
+ }
+
+ marker := hugoFsMarker + "_" + lang + "_"
+
+ return &LanguageFs{lang: lang, languages: languages, basePath: basePath, Fs: fs, nameMarker: marker}
+}
+
+// Lang returns a language filesystem's language (ie. "sv").
+func (fs *LanguageFs) Lang() string {
+ return fs.lang
+}
+
+// Stat returns the os.FileInfo of a given file.
+func (fs *LanguageFs) Stat(name string) (os.FileInfo, error) {
+ name, err := fs.realName(name)
+ if err != nil {
+ return nil, err
+ }
+
+ fi, err := fs.Fs.Stat(name)
+ if err != nil {
+ return nil, err
+ }
+
+ return fs.newLanguageFileInfo(name, fi)
+}
+
+// Open opens the named file for reading.
+func (fs *LanguageFs) Open(name string) (afero.File, error) {
+ name, err := fs.realName(name)
+ if err != nil {
+ return nil, err
+ }
+ f, err := fs.Fs.Open(name)
+
+ if err != nil {
+ return nil, err
+ }
+ return &languageFile{File: f, fs: fs}, nil
+}
+
+// LstatIfPossible returns the os.FileInfo structure describing a given file.
+// It attempts to use Lstat if supported or defers to the os. In addition to
+// the FileInfo, a boolean is returned telling whether Lstat was called.
+func (fs *LanguageFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
+ name, err := fs.realName(name)
+ if err != nil {
+ return nil, false, err
+ }
+
+ var fi os.FileInfo
+ var b bool
+
+ if lif, ok := fs.Fs.(afero.Lstater); ok {
+ fi, b, err = lif.LstatIfPossible(name)
+ } else {
+ fi, err = fs.Fs.Stat(name)
+ }
+
+ if err != nil {
+ return nil, b, err
+ }
+
+ lfi, err := fs.newLanguageFileInfo(name, fi)
+
+ return lfi, b, err
+}
+
+func (fs *LanguageFs) realPath(name string) (string, error) {
+ if baseFs, ok := fs.Fs.(*afero.BasePathFs); ok {
+ return baseFs.RealPath(name)
+ }
+ return name, nil
+}
+
+func (fs *LanguageFs) realName(name string) (string, error) {
+ if strings.Contains(name, hugoFsMarker) {
+ if !strings.Contains(name, fs.nameMarker) {
+ return "", os.ErrNotExist
+ }
+ return strings.Replace(name, fs.nameMarker, "", 1), nil
+ }
+
+ if fs.basePath == "" {
+ return name, nil
+ }
+
+ return strings.TrimPrefix(name, fs.basePath), nil
+}
+
+func (fs *LanguageFs) newLanguageFileInfo(filename string, fi os.FileInfo) (*LanguageFileInfo, error) {
+ filename = filepath.Clean(filename)
+ _, name := filepath.Split(filename)
+
+ realName := name
+ virtualName := name
+
+ realPath, err := fs.realPath(filename)
+ if err != nil {
+ return nil, err
+ }
+
+ lang := fs.Lang()
+
+ baseNameNoExt := ""
+
+ if !fi.IsDir() {
+
+ // Try to extract the language from the file name.
+ // Any valid language identificator in the name will win over the
+ // language set on the file system, e.g. "mypost.en.md".
+ baseName := filepath.Base(name)
+ ext := filepath.Ext(baseName)
+ baseNameNoExt = baseName
+
+ if ext != "" {
+ baseNameNoExt = strings.TrimSuffix(baseNameNoExt, ext)
+ }
+
+ fileLangExt := filepath.Ext(baseNameNoExt)
+ fileLang := strings.TrimPrefix(fileLangExt, ".")
+
+ if fs.languages[fileLang] {
+ lang = fileLang
+ baseNameNoExt = strings.TrimSuffix(baseNameNoExt, fileLangExt)
+ }
+
+ // This connects the filename to the filesystem, not the language.
+ virtualName = baseNameNoExt + "." + lang + ext
+
+ name = fs.nameMarker + name
+ }
+
+ weight := 1
+ // If this file's language belongs in this directory, add some weight to it
+ // to make it more important.
+ if lang == fs.Lang() {
+ weight = 2
+ }
+
+ if fi.IsDir() {
+ // For directories we always want to start from the union view.
+ realPath = strings.TrimPrefix(realPath, fs.basePath)
+ }
+
+ return &LanguageFileInfo{
+ lang: lang,
+ weight: weight,
+ realFilename: realPath,
+ realName: realName,
+ relFilename: strings.TrimPrefix(strings.TrimPrefix(realPath, fs.basePath), string(os.PathSeparator)),
+ name: name,
+ virtualName: virtualName,
+ translationBaseName: baseNameNoExt,
+ baseDir: fs.basePath,
+ FileInfo: fi}, nil
+}
diff --git a/hugofs/language_fs_test.go b/hugofs/language_fs_test.go
new file mode 100644
index 000000000..52e8dc9de
--- /dev/null
+++ b/hugofs/language_fs_test.go
@@ -0,0 +1,100 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "path/filepath"
+ "testing"
+
+ "github.com/spf13/afero"
+ "github.com/stretchr/testify/require"
+)
+
+func TestLanguagFs(t *testing.T) {
+ languages := map[string]bool{
+ "sv": true,
+ }
+ base := filepath.FromSlash("/my/base")
+ assert := require.New(t)
+ m := afero.NewMemMapFs()
+ bfs := afero.NewBasePathFs(m, base)
+ lfs := NewLanguageFs("sv", languages, bfs)
+ assert.NotNil(lfs)
+ assert.Equal("sv", lfs.Lang())
+ err := afero.WriteFile(lfs, filepath.FromSlash("sect/page.md"), []byte("abc"), 0777)
+ assert.NoError(err)
+ fi, err := lfs.Stat(filepath.FromSlash("sect/page.md"))
+ assert.NoError(err)
+ assert.Equal("__hugofs_sv_page.md", fi.Name())
+
+ languager, ok := fi.(LanguageAnnouncer)
+ assert.True(ok)
+
+ assert.Equal("sv", languager.Lang())
+
+ lfi, ok := fi.(*LanguageFileInfo)
+ assert.True(ok)
+ assert.Equal(filepath.FromSlash("/my/base/sect/page.md"), lfi.Filename())
+ assert.Equal(filepath.FromSlash("sect/page.md"), lfi.Path())
+ assert.Equal("page.sv.md", lfi.virtualName)
+ assert.Equal("__hugofs_sv_page.md", lfi.Name())
+ assert.Equal("page.md", lfi.RealName())
+ assert.Equal(filepath.FromSlash("/my/base"), lfi.BaseDir())
+ assert.Equal("sv", lfi.Lang())
+ assert.Equal("page", lfi.TranslationBaseName())
+}
+
+// Issue 4559
+func TestFilenamesHandling(t *testing.T) {
+ languages := map[string]bool{
+ "sv": true,
+ }
+ base := filepath.FromSlash("/my/base")
+ assert := require.New(t)
+ m := afero.NewMemMapFs()
+ bfs := afero.NewBasePathFs(m, base)
+ lfs := NewLanguageFs("sv", languages, bfs)
+ assert.NotNil(lfs)
+ assert.Equal("sv", lfs.Lang())
+
+ for _, test := range []struct {
+ filename string
+ check func(fi *LanguageFileInfo)
+ }{
+ {"tc-lib-color/class-Com.Tecnick.Color.Css", func(fi *LanguageFileInfo) {
+ assert.Equal("class-Com.Tecnick.Color", fi.TranslationBaseName())
+ assert.Equal(filepath.FromSlash("/my/base"), fi.BaseDir())
+ assert.Equal(filepath.FromSlash("tc-lib-color/class-Com.Tecnick.Color.Css"), fi.Path())
+ assert.Equal("class-Com.Tecnick.Color.Css", fi.RealName())
+ assert.Equal(filepath.FromSlash("/my/base/tc-lib-color/class-Com.Tecnick.Color.Css"), fi.Filename())
+ }},
+ {"tc-lib-color/class-Com.Tecnick.Color.sv.Css", func(fi *LanguageFileInfo) {
+ assert.Equal("class-Com.Tecnick.Color", fi.TranslationBaseName())
+ assert.Equal("class-Com.Tecnick.Color.sv.Css", fi.RealName())
+ assert.Equal(filepath.FromSlash("/my/base/tc-lib-color/class-Com.Tecnick.Color.sv.Css"), fi.Filename())
+ }},
+ } {
+ err := afero.WriteFile(lfs, filepath.FromSlash(test.filename), []byte("abc"), 0777)
+ assert.NoError(err)
+ fi, err := lfs.Stat(filepath.FromSlash(test.filename))
+ assert.NoError(err)
+
+ lfi, ok := fi.(*LanguageFileInfo)
+ assert.True(ok)
+ assert.Equal("sv", lfi.Lang())
+ test.check(lfi)
+
+ }
+
+}
diff --git a/hugofs/nolstat_fs.go b/hugofs/nolstat_fs.go
new file mode 100644
index 000000000..6b27e8e1f
--- /dev/null
+++ b/hugofs/nolstat_fs.go
@@ -0,0 +1,39 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "os"
+
+ "github.com/spf13/afero"
+)
+
+var (
+ _ afero.Fs = (*noLstatFs)(nil)
+)
+
+type noLstatFs struct {
+ afero.Fs
+}
+
+// NewNoLstatFs creates a new filesystem with no Lstat support.
+func NewNoLstatFs(fs afero.Fs) afero.Fs {
+ return &noLstatFs{Fs: fs}
+}
+
+// LstatIfPossible always delegates to Stat.
+func (fs *noLstatFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
+ fi, err := fs.Stat(name)
+ return fi, false, err
+}
diff --git a/hugofs/noop_fs.go b/hugofs/noop_fs.go
new file mode 100644
index 000000000..c3d2f2da5
--- /dev/null
+++ b/hugofs/noop_fs.go
@@ -0,0 +1,82 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "errors"
+ "os"
+ "time"
+
+ "github.com/spf13/afero"
+)
+
+var (
+ errNoOp = errors.New("this is a filesystem that does nothing and this operation is not supported")
+ _ afero.Fs = (*noOpFs)(nil)
+
+ // NoOpFs provides a no-op filesystem that implements the afero.Fs
+ // interface.
+ NoOpFs = &noOpFs{}
+)
+
+type noOpFs struct {
+}
+
+func (fs noOpFs) Create(name string) (afero.File, error) {
+ return nil, errNoOp
+}
+
+func (fs noOpFs) Mkdir(name string, perm os.FileMode) error {
+ return errNoOp
+}
+
+func (fs noOpFs) MkdirAll(path string, perm os.FileMode) error {
+ return errNoOp
+}
+
+func (fs noOpFs) Open(name string) (afero.File, error) {
+ return nil, os.ErrNotExist
+}
+
+func (fs noOpFs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
+ return nil, os.ErrNotExist
+}
+
+func (fs noOpFs) Remove(name string) error {
+ return errNoOp
+}
+
+func (fs noOpFs) RemoveAll(path string) error {
+ return errNoOp
+}
+
+func (fs noOpFs) Rename(oldname string, newname string) error {
+ return errNoOp
+}
+
+func (fs noOpFs) Stat(name string) (os.FileInfo, error) {
+ return nil, os.ErrNotExist
+}
+
+func (fs noOpFs) Name() string {
+ return "noOpFs"
+}
+
+func (fs noOpFs) Chmod(name string, mode os.FileMode) error {
+ return errNoOp
+}
+
+func (fs noOpFs) Chtimes(name string, atime time.Time, mtime time.Time) error {
+ return errNoOp
+}
diff --git a/hugofs/rootmapping_fs.go b/hugofs/rootmapping_fs.go
new file mode 100644
index 000000000..2b8b8d2c0
--- /dev/null
+++ b/hugofs/rootmapping_fs.go
@@ -0,0 +1,196 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ radix "github.com/hashicorp/go-immutable-radix"
+ "github.com/spf13/afero"
+)
+
+var filepathSeparator = string(filepath.Separator)
+
+// A RootMappingFs maps several roots into one. Note that the root of this filesystem
+// is directories only, and they will be returned in Readdir and Readdirnames
+// in the order given.
+type RootMappingFs struct {
+ afero.Fs
+ rootMapToReal *radix.Node
+ virtualRoots []string
+}
+
+type rootMappingFile struct {
+ afero.File
+ fs *RootMappingFs
+ name string
+}
+
+type rootMappingFileInfo struct {
+ name string
+}
+
+func (fi *rootMappingFileInfo) Name() string {
+ return fi.name
+}
+
+func (fi *rootMappingFileInfo) Size() int64 {
+ panic("not implemented")
+}
+
+func (fi *rootMappingFileInfo) Mode() os.FileMode {
+ return os.ModeDir
+}
+
+func (fi *rootMappingFileInfo) ModTime() time.Time {
+ panic("not implemented")
+}
+
+func (fi *rootMappingFileInfo) IsDir() bool {
+ return true
+}
+
+func (fi *rootMappingFileInfo) Sys() interface{} {
+ return nil
+}
+
+func newRootMappingDirFileInfo(name string) *rootMappingFileInfo {
+ return &rootMappingFileInfo{name: name}
+}
+
+// NewRootMappingFs creates a new RootMappingFs on top of the provided with
+// a list of from, to string pairs of root mappings.
+// Note that 'from' represents a virtual root that maps to the actual filename in 'to'.
+func NewRootMappingFs(fs afero.Fs, fromTo ...string) (*RootMappingFs, error) {
+ rootMapToReal := radix.New().Txn()
+ var virtualRoots []string
+
+ for i := 0; i < len(fromTo); i += 2 {
+ vr := filepath.Clean(fromTo[i])
+ rr := filepath.Clean(fromTo[i+1])
+
+ // We need to preserve the original order for Readdir
+ virtualRoots = append(virtualRoots, vr)
+
+ rootMapToReal.Insert([]byte(vr), rr)
+ }
+
+ return &RootMappingFs{Fs: fs,
+ virtualRoots: virtualRoots,
+ rootMapToReal: rootMapToReal.Commit().Root()}, nil
+}
+
+// Stat returns the os.FileInfo structure describing a given file. If there is
+// an error, it will be of type *os.PathError.
+func (fs *RootMappingFs) Stat(name string) (os.FileInfo, error) {
+ if fs.isRoot(name) {
+ return newRootMappingDirFileInfo(name), nil
+ }
+ realName := fs.realName(name)
+
+ fi, err := fs.Fs.Stat(realName)
+ if rfi, ok := fi.(RealFilenameInfo); ok {
+ return rfi, err
+ }
+
+ return &realFilenameInfo{FileInfo: fi, realFilename: realName}, err
+
+}
+
+func (fs *RootMappingFs) isRoot(name string) bool {
+ return name == "" || name == filepathSeparator
+
+}
+
+// Open opens the named file for reading.
+func (fs *RootMappingFs) Open(name string) (afero.File, error) {
+ if fs.isRoot(name) {
+ return &rootMappingFile{name: name, fs: fs}, nil
+ }
+ realName := fs.realName(name)
+ f, err := fs.Fs.Open(realName)
+ if err != nil {
+ return nil, err
+ }
+ return &rootMappingFile{File: f, name: name, fs: fs}, nil
+}
+
+// LstatIfPossible returns the os.FileInfo structure describing a given file.
+// It attempts to use Lstat if supported or defers to the os. In addition to
+// the FileInfo, a boolean is returned telling whether Lstat was called.
+func (fs *RootMappingFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
+
+ if fs.isRoot(name) {
+ return newRootMappingDirFileInfo(name), false, nil
+ }
+ name = fs.realName(name)
+
+ if ls, ok := fs.Fs.(afero.Lstater); ok {
+ fi, b, err := ls.LstatIfPossible(name)
+ return &realFilenameInfo{FileInfo: fi, realFilename: name}, b, err
+ }
+ fi, err := fs.Stat(name)
+ return fi, false, err
+}
+
+func (fs *RootMappingFs) realName(name string) string {
+ key, val, found := fs.rootMapToReal.LongestPrefix([]byte(filepath.Clean(name)))
+ if !found {
+ return name
+ }
+ keystr := string(key)
+
+ return filepath.Join(val.(string), strings.TrimPrefix(name, keystr))
+}
+
+func (f *rootMappingFile) Readdir(count int) ([]os.FileInfo, error) {
+ if f.File == nil {
+ dirsn := make([]os.FileInfo, 0)
+ for i := 0; i < len(f.fs.virtualRoots); i++ {
+ if count != -1 && i >= count {
+ break
+ }
+ dirsn = append(dirsn, newRootMappingDirFileInfo(f.fs.virtualRoots[i]))
+ }
+ return dirsn, nil
+ }
+ return f.File.Readdir(count)
+
+}
+
+func (f *rootMappingFile) Readdirnames(count int) ([]string, error) {
+ dirs, err := f.Readdir(count)
+ if err != nil {
+ return nil, err
+ }
+ dirss := make([]string, len(dirs))
+ for i, d := range dirs {
+ dirss[i] = d.Name()
+ }
+ return dirss, nil
+}
+
+func (f *rootMappingFile) Name() string {
+ return f.name
+}
+
+func (f *rootMappingFile) Close() error {
+ if f.File == nil {
+ return nil
+ }
+ return f.File.Close()
+}
diff --git a/hugofs/rootmapping_fs_test.go b/hugofs/rootmapping_fs_test.go
new file mode 100644
index 000000000..e6a0301c9
--- /dev/null
+++ b/hugofs/rootmapping_fs_test.go
@@ -0,0 +1,94 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/spf13/afero"
+ "github.com/stretchr/testify/require"
+)
+
+func TestRootMappingFsRealName(t *testing.T) {
+ assert := require.New(t)
+ fs := afero.NewMemMapFs()
+
+ rfs, err := NewRootMappingFs(fs, "f1", "f1t", "f2", "f2t")
+ assert.NoError(err)
+
+ assert.Equal(filepath.FromSlash("f1t/foo/file.txt"), rfs.realName(filepath.Join("f1", "foo", "file.txt")))
+
+}
+
+func TestRootMappingFsDirnames(t *testing.T) {
+ assert := require.New(t)
+ fs := afero.NewMemMapFs()
+
+ testfile := "myfile.txt"
+ assert.NoError(fs.Mkdir("f1t", 0755))
+ assert.NoError(fs.Mkdir("f2t", 0755))
+ assert.NoError(fs.Mkdir("f3t", 0755))
+ assert.NoError(afero.WriteFile(fs, filepath.Join("f2t", testfile), []byte("some content"), 0755))
+
+ rfs, err := NewRootMappingFs(fs, "bf1", "f1t", "cf2", "f2t", "af3", "f3t")
+ assert.NoError(err)
+
+ fif, err := rfs.Stat(filepath.Join("cf2", testfile))
+ assert.NoError(err)
+ assert.Equal("myfile.txt", fif.Name())
+ assert.Equal(filepath.FromSlash("f2t/myfile.txt"), fif.(RealFilenameInfo).RealFilename())
+
+ root, err := rfs.Open(filepathSeparator)
+ assert.NoError(err)
+
+ dirnames, err := root.Readdirnames(-1)
+ assert.NoError(err)
+ assert.Equal([]string{"bf1", "cf2", "af3"}, dirnames)
+
+}
+
+func TestRootMappingFsOs(t *testing.T) {
+ assert := require.New(t)
+ fs := afero.NewOsFs()
+
+ d, err := ioutil.TempDir("", "hugo-root-mapping")
+ assert.NoError(err)
+ defer func() {
+ os.RemoveAll(d)
+ }()
+
+ testfile := "myfile.txt"
+ assert.NoError(fs.Mkdir(filepath.Join(d, "f1t"), 0755))
+ assert.NoError(fs.Mkdir(filepath.Join(d, "f2t"), 0755))
+ assert.NoError(fs.Mkdir(filepath.Join(d, "f3t"), 0755))
+ assert.NoError(afero.WriteFile(fs, filepath.Join(d, "f2t", testfile), []byte("some content"), 0755))
+
+ rfs, err := NewRootMappingFs(fs, "bf1", filepath.Join(d, "f1t"), "cf2", filepath.Join(d, "f2t"), "af3", filepath.Join(d, "f3t"))
+ assert.NoError(err)
+
+ fif, err := rfs.Stat(filepath.Join("cf2", testfile))
+ assert.NoError(err)
+ assert.Equal("myfile.txt", fif.Name())
+
+ root, err := rfs.Open(filepathSeparator)
+ assert.NoError(err)
+
+ dirnames, err := root.Readdirnames(-1)
+ assert.NoError(err)
+ assert.Equal([]string{"bf1", "cf2", "af3"}, dirnames)
+
+}
diff --git a/hugofs/stacktracer_fs.go b/hugofs/stacktracer_fs.go
new file mode 100644
index 000000000..d4db164ca
--- /dev/null
+++ b/hugofs/stacktracer_fs.go
@@ -0,0 +1,70 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "fmt"
+ "os"
+ "regexp"
+ "runtime"
+
+ "github.com/gohugoio/hugo/common/types"
+
+ "github.com/spf13/afero"
+)
+
+// Make sure we don't accidently use this in the real Hugo.
+var _ types.DevMarker = (*stacktracerFs)(nil)
+
+// NewStacktracerFs wraps the given fs printing stack traces for file creates
+// matching the given regexp pattern.
+func NewStacktracerFs(fs afero.Fs, pattern string) afero.Fs {
+ return &stacktracerFs{Fs: fs, re: regexp.MustCompile(pattern)}
+}
+
+// stacktracerFs can be used in hard-to-debug development situations where
+// you get some input you don't understand where comes from.
+type stacktracerFs struct {
+ afero.Fs
+
+ // Will print stacktrace for every file creates matching this pattern.
+ re *regexp.Regexp
+}
+
+func (fs *stacktracerFs) DevOnly() {
+}
+
+func (fs *stacktracerFs) onCreate(filename string) {
+ if fs.re.MatchString(filename) {
+ trace := make([]byte, 1500)
+ runtime.Stack(trace, true)
+ fmt.Printf("\n===========\n%q:\n%s\n", filename, trace)
+ }
+}
+
+func (fs *stacktracerFs) Create(name string) (afero.File, error) {
+ f, err := fs.Fs.Create(name)
+ if err == nil {
+ fs.onCreate(name)
+ }
+ return f, err
+}
+
+func (fs *stacktracerFs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
+ f, err := fs.Fs.OpenFile(name, flag, perm)
+ if err == nil && isWrite(flag) {
+ fs.onCreate(name)
+ }
+ return f, err
+}
diff --git a/hugolib/404_test.go b/hugolib/404_test.go
new file mode 100644
index 000000000..5ea98be62
--- /dev/null
+++ b/hugolib/404_test.go
@@ -0,0 +1,32 @@
+// 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"
+)
+
+func Test404(t *testing.T) {
+ t.Parallel()
+
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile().WithTemplatesAdded("404.html", "<html><body>Not Found!</body></html>")
+ b.Build(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 would work
+ // right now.
+ b.AssertFileContent("public/404.html", "Not Found")
+
+}
diff --git a/hugolib/alias.go b/hugolib/alias.go
new file mode 100644
index 000000000..972f7b01c
--- /dev/null
+++ b/hugolib/alias.go
@@ -0,0 +1,193 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "path/filepath"
+ "runtime"
+ "strings"
+
+ "github.com/gohugoio/hugo/common/loggers"
+
+ "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/publisher"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/tpl"
+)
+
+const (
+ alias = "<!DOCTYPE html><html><head><title>{{ .Permalink }}</title><link rel=\"canonical\" href=\"{{ .Permalink }}\"/><meta name=\"robots\" content=\"noindex\"><meta 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 *loggers.Logger
+ allowRoot bool
+}
+
+func newAliasHandler(t tpl.TemplateFinder, l *loggers.Logger, allowRoot bool) aliasHandler {
+ return aliasHandler{t, l, allowRoot}
+}
+
+type aliasPage struct {
+ Permalink string
+ page.Page
+}
+
+func (a aliasHandler) renderAlias(isXHTML bool, permalink string, p page.Page) (io.Reader, error) {
+ t := "alias"
+ if isXHTML {
+ t = "alias-xhtml"
+ }
+
+ var templ tpl.Template
+ var found bool
+
+ if a.t != nil {
+ templ, found = a.t.Lookup("alias.html")
+ }
+
+ if !found {
+ def := defaultAliasTemplates.Lookup(t)
+ if def != nil {
+ templ = &tpl.TemplateAdapter{Template: def}
+ }
+
+ }
+ data := aliasPage{
+ permalink,
+ p,
+ }
+
+ 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, outputFormat output.Format, p page.Page) (err error) {
+ return s.publishDestAlias(false, path, permalink, outputFormat, p)
+}
+
+func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFormat output.Format, p page.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
+ }
+
+ pd := publisher.Descriptor{
+ Src: aliasContent,
+ TargetPath: targetPath,
+ StatCounter: &s.PathSpec.ProcessingStats.Aliases,
+ OutputFormat: outputFormat,
+ }
+
+ return s.publisher.Publish(pd)
+
+}
+
+func (a aliasHandler) targetPathAlias(src string) (string, error) {
+ originalAlias := src
+ if len(src) <= 0 {
+ return "", fmt.Errorf("alias \"\" is an empty string")
+ }
+
+ alias := path.Clean(filepath.ToSlash(src))
+
+ if !a.allowRoot && alias == "/" {
+ return "", fmt.Errorf("alias \"%s\" resolves to website root directory", originalAlias)
+ }
+
+ components := strings.Split(alias, "/")
+
+ // 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.INFO.Println(m)
+ }
+ }
+
+ // Add the final touch
+ alias = strings.TrimPrefix(alias, "/")
+ if strings.HasSuffix(alias, "/") {
+ alias = alias + "index.html"
+ } else if !strings.HasSuffix(alias, ".html") {
+ alias = alias + "/" + "index.html"
+ }
+
+ return filepath.FromSlash(alias), nil
+}
diff --git a/hugolib/alias_test.go b/hugolib/alias_test.go
new file mode 100644
index 000000000..095ae1be2
--- /dev/null
+++ b/hugolib/alias_test.go
@@ -0,0 +1,142 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/common/loggers"
+
+ "github.com/stretchr/testify/require"
+)
+
+const pageWithAlias = `---
+title: Has Alias
+aliases: ["/foo/bar/", "rel"]
+---
+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()
+ assert := require.New(t)
+
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile().WithContent("blog/page.md", pageWithAlias)
+ b.CreateSites().Build(BuildCfg{})
+
+ assert.Equal(1, len(b.H.Sites))
+ require.Len(t, b.H.Sites[0].RegularPages(), 1)
+
+ // the real page
+ b.AssertFileContent("public/blog/page/index.html", "For some moments the old man")
+ // the alias redirectors
+ b.AssertFileContent("public/foo/bar/index.html", "<meta http-equiv=\"refresh\" content=\"0; ")
+ b.AssertFileContent("public/blog/rel/index.html", "<meta http-equiv=\"refresh\" content=\"0; ")
+}
+
+func TestAliasMultipleOutputFormats(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile().WithContent("blog/page.md", pageWithAliasMultipleOutputs)
+
+ b.WithTemplates(
+ "_default/single.html", basicTemplate,
+ "_default/single.amp.html", basicTemplate,
+ "_default/single.json", basicTemplate)
+
+ b.CreateSites().Build(BuildCfg{})
+
+ // the real pages
+ b.AssertFileContent("public/blog/page/index.html", "For some moments the old man")
+ b.AssertFileContent("public/amp/blog/page/index.html", "For some moments the old man")
+ b.AssertFileContent("public/blog/page/index.json", "For some moments the old man")
+
+ // the alias redirectors
+ b.AssertFileContent("public/foo/bar/index.html", "<meta http-equiv=\"refresh\" content=\"0; ")
+ b.AssertFileContent("public/amp/foo/bar/index.html", "<meta http-equiv=\"refresh\" content=\"0; ")
+ assert.False(b.CheckExists("public/foo/bar/index.json"))
+}
+
+func TestAliasTemplate(t *testing.T) {
+ t.Parallel()
+
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile().WithContent("page.md", pageWithAlias).WithTemplatesAdded("alias.html", aliasTemplate)
+
+ b.CreateSites().Build(BuildCfg{})
+
+ // the real page
+ b.AssertFileContent("public/page/index.html", "For some moments the old man")
+ // the alias redirector
+ b.AssertFileContent("public/foo/bar/index.html", "ALIASTEMPLATE")
+}
+
+func TestTargetPathHTMLRedirectAlias(t *testing.T) {
+ h := newAliasHandler(nil, loggers.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: %q, got: %q", test.expected, path)
+ }
+ }
+}
diff --git a/hugolib/case_insensitive_test.go b/hugolib/case_insensitive_test.go
new file mode 100644
index 000000000..8c94bf5db
--- /dev/null
+++ b/hugolib/case_insensitive_test.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 hugolib
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/gohugoio/hugo/hugofs"
+
+ "github.com/gohugoio/hugo/deps"
+ "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 }}
+Partial Site Global: {{ 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(ConfigSourceDescriptor{Fs: mm, Filename: "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",
+ "Partial Site Global: green|yellow",
+ "Page Title: Side 1",
+ "Site Title: Nynorsk title",
+ "&laquo;Hi&raquo;", // 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",
+ "&ldquo;Hi&rdquo;",
+ )
+
+ 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 := LoadConfigDefault(mm)
+ 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/collections.go b/hugolib/collections.go
new file mode 100644
index 000000000..a794a9866
--- /dev/null
+++ b/hugolib/collections.go
@@ -0,0 +1,47 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/common/collections"
+ "github.com/gohugoio/hugo/resources/page"
+)
+
+var (
+ _ collections.Grouper = (*pageState)(nil)
+ _ collections.Slicer = (*pageState)(nil)
+)
+
+// collections.Slicer implementations below. We keep these bridge implementations
+// here as it makes it easier to get an idea of "type coverage". These
+// implementations have no value on their own.
+
+// Slice is not meant to be used externally. It's a bridge function
+// for the template functions. See collections.Slice.
+func (p *pageState) Slice(items interface{}) (interface{}, error) {
+ return page.ToPages(items)
+}
+
+// collections.Grouper implementations below
+
+// Group creates a PageGroup from a key and a Pages object
+// This method is not meant for external use. It got its non-typed arguments to satisfy
+// a very generic interface in the tpl package.
+func (p *pageState) Group(key interface{}, in interface{}) (interface{}, error) {
+ pages, err := page.ToPages(in)
+ if err != nil {
+ return nil, err
+ }
+ return page.PageGroup{Key: key, Pages: pages}, nil
+}
diff --git a/hugolib/collections_test.go b/hugolib/collections_test.go
new file mode 100644
index 000000000..41681bc73
--- /dev/null
+++ b/hugolib/collections_test.go
@@ -0,0 +1,209 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestGroupFunc(t *testing.T) {
+ assert := require.New(t)
+
+ pageContent := `
+---
+title: "Page"
+---
+
+`
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile().
+ WithContent("page1.md", pageContent, "page2.md", pageContent).
+ WithTemplatesAdded("index.html", `
+{{ $cool := .Site.RegularPages | group "cool" }}
+{{ $cool.Key }}: {{ len $cool.Pages }}
+
+`)
+ b.CreateSites().Build(BuildCfg{})
+
+ assert.Equal(1, len(b.H.Sites))
+ require.Len(t, b.H.Sites[0].RegularPages(), 2)
+
+ b.AssertFileContent("public/index.html", "cool: 2")
+}
+
+func TestSliceFunc(t *testing.T) {
+ assert := require.New(t)
+
+ pageContent := `
+---
+title: "Page"
+tags: ["blue", "green"]
+tags_weight: %d
+---
+
+`
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile().
+ WithContent("page1.md", fmt.Sprintf(pageContent, 10), "page2.md", fmt.Sprintf(pageContent, 20)).
+ WithTemplatesAdded("index.html", `
+{{ $cool := first 1 .Site.RegularPages | group "cool" }}
+{{ $blue := after 1 .Site.RegularPages | group "blue" }}
+{{ $weightedPages := index (index .Site.Taxonomies "tags") "blue" }}
+
+{{ $p1 := index .Site.RegularPages 0 }}{{ $p2 := index .Site.RegularPages 1 }}
+{{ $wp1 := index $weightedPages 0 }}{{ $wp2 := index $weightedPages 1 }}
+
+{{ $pages := slice $p1 $p2 }}
+{{ $pageGroups := slice $cool $blue }}
+{{ $weighted := slice $wp1 $wp2 }}
+
+{{ printf "pages:%d:%T:%v/%v" (len $pages) $pages (index $pages 0) (index $pages 1) }}
+{{ printf "pageGroups:%d:%T:%v/%v" (len $pageGroups) $pageGroups (index (index $pageGroups 0).Pages 0) (index (index $pageGroups 1).Pages 0)}}
+{{ printf "weightedPages:%d::%T:%v" (len $weighted) $weighted $weighted | safeHTML }}
+
+`)
+ b.CreateSites().Build(BuildCfg{})
+
+ assert.Equal(1, len(b.H.Sites))
+ require.Len(t, b.H.Sites[0].RegularPages(), 2)
+
+ b.AssertFileContent("public/index.html",
+ "pages:2:page.Pages:Page(/page1.md)/Page(/page2.md)",
+ "pageGroups:2:page.PagesGroup:Page(/page1.md)/Page(/page2.md)",
+ `weightedPages:2::page.WeightedPages:[WeightedPage(10,"Page") WeightedPage(20,"Page")]`)
+}
+
+func TestUnionFunc(t *testing.T) {
+ assert := require.New(t)
+
+ pageContent := `
+---
+title: "Page"
+tags: ["blue", "green"]
+tags_weight: %d
+---
+
+`
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile().
+ WithContent("page1.md", fmt.Sprintf(pageContent, 10), "page2.md", fmt.Sprintf(pageContent, 20),
+ "page3.md", fmt.Sprintf(pageContent, 30)).
+ WithTemplatesAdded("index.html", `
+{{ $unionPages := first 2 .Site.RegularPages | union .Site.RegularPages }}
+{{ $unionWeightedPages := .Site.Taxonomies.tags.blue | union .Site.Taxonomies.tags.green }}
+{{ printf "unionPages: %T %d" $unionPages (len $unionPages) }}
+{{ printf "unionWeightedPages: %T %d" $unionWeightedPages (len $unionWeightedPages) }}
+`)
+ b.CreateSites().Build(BuildCfg{})
+
+ assert.Equal(1, len(b.H.Sites))
+ require.Len(t, b.H.Sites[0].RegularPages(), 3)
+
+ b.AssertFileContent("public/index.html",
+ "unionPages: page.Pages 3",
+ "unionWeightedPages: page.WeightedPages 6")
+}
+
+func TestCollectionsFuncs(t *testing.T) {
+ assert := require.New(t)
+
+ pageContent := `
+---
+title: "Page"
+tags: ["blue", "green"]
+tags_weight: %d
+---
+
+`
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile().
+ WithContent("page1.md", fmt.Sprintf(pageContent, 10), "page2.md", fmt.Sprintf(pageContent, 20),
+ "page3.md", fmt.Sprintf(pageContent, 30)).
+ WithTemplatesAdded("index.html", `
+{{ $uniqPages := first 2 .Site.RegularPages | append .Site.RegularPages | uniq }}
+{{ $inTrue := in .Site.RegularPages (index .Site.RegularPages 1) }}
+{{ $inFalse := in .Site.RegularPages (.Site.Home) }}
+
+{{ printf "uniqPages: %T %d" $uniqPages (len $uniqPages) }}
+{{ printf "inTrue: %t" $inTrue }}
+{{ printf "inFalse: %t" $inFalse }}
+`)
+ b.CreateSites().Build(BuildCfg{})
+
+ assert.Equal(1, len(b.H.Sites))
+ require.Len(t, b.H.Sites[0].RegularPages(), 3)
+
+ b.AssertFileContent("public/index.html",
+ "uniqPages: page.Pages 3",
+ "inTrue: true",
+ "inFalse: false",
+ )
+}
+
+func TestAppendFunc(t *testing.T) {
+ assert := require.New(t)
+
+ pageContent := `
+---
+title: "Page"
+tags: ["blue", "green"]
+tags_weight: %d
+---
+
+`
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile().
+ WithContent("page1.md", fmt.Sprintf(pageContent, 10), "page2.md", fmt.Sprintf(pageContent, 20)).
+ WithTemplatesAdded("index.html", `
+
+{{ $p1 := index .Site.RegularPages 0 }}{{ $p2 := index .Site.RegularPages 1 }}
+
+{{ $pages := slice }}
+
+{{ if true }}
+ {{ $pages = $pages | append $p2 $p1 }}
+{{ end }}
+{{ $appendPages := .Site.Pages | append .Site.RegularPages }}
+{{ $appendStrings := slice "a" "b" | append "c" "d" "e" }}
+{{ $appendStringsSlice := slice "a" "b" "c" | append (slice "c" "d") }}
+
+{{ printf "pages:%d:%T:%v/%v" (len $pages) $pages (index $pages 0) (index $pages 1) }}
+{{ printf "appendPages:%d:%T:%v/%v" (len $appendPages) $appendPages (index $appendPages 0).Kind (index $appendPages 8).Kind }}
+{{ printf "appendStrings:%T:%v" $appendStrings $appendStrings }}
+{{ printf "appendStringsSlice:%T:%v" $appendStringsSlice $appendStringsSlice }}
+
+{{/* add some slightly related funcs to check what types we get */}}
+{{ $u := $appendStrings | union $appendStringsSlice }}
+{{ $i := $appendStrings | intersect $appendStringsSlice }}
+{{ printf "union:%T:%v" $u $u }}
+{{ printf "intersect:%T:%v" $i $i }}
+
+`)
+ b.CreateSites().Build(BuildCfg{})
+
+ assert.Equal(1, len(b.H.Sites))
+ require.Len(t, b.H.Sites[0].RegularPages(), 2)
+
+ b.AssertFileContent("public/index.html",
+ "pages:2:page.Pages:Page(/page2.md)/Page(/page1.md)",
+ "appendPages:9:page.Pages:home/page",
+ "appendStrings:[]string:[a b c d e]",
+ "appendStringsSlice:[]string:[a b c c d]",
+ "union:[]string:[a b c d e]",
+ "intersect:[]string:[a b c d]",
+ )
+}
diff --git a/hugolib/config.go b/hugolib/config.go
new file mode 100644
index 000000000..50e4ca6ec
--- /dev/null
+++ b/hugolib/config.go
@@ -0,0 +1,639 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "path/filepath"
+ "strings"
+
+ "github.com/gohugoio/hugo/parser/metadecoders"
+
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/hugolib/paths"
+ "github.com/pkg/errors"
+ _errors "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/langs"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/config/privacy"
+ "github.com/gohugoio/hugo/config/services"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/spf13/afero"
+ "github.com/spf13/viper"
+)
+
+// SiteConfig represents the config in .Site.Config.
+type SiteConfig struct {
+ // This contains all privacy related settings that can be used to
+ // make the YouTube template etc. GDPR compliant.
+ Privacy privacy.Config
+
+ // Services contains config for services such as Google Analytics etc.
+ Services services.Config
+}
+
+func loadSiteConfig(cfg config.Provider) (scfg SiteConfig, err error) {
+ privacyConfig, err := privacy.DecodeConfig(cfg)
+ if err != nil {
+ return
+ }
+
+ servicesConfig, err := services.DecodeConfig(cfg)
+ if err != nil {
+ return
+ }
+
+ scfg.Privacy = privacyConfig
+ scfg.Services = servicesConfig
+
+ return
+}
+
+// ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.).
+type ConfigSourceDescriptor struct {
+ Fs afero.Fs
+
+ // Path to the config file to use, e.g. /my/project/config.toml
+ Filename string
+
+ // The path to the directory to look for configuration. Is used if Filename is not
+ // set or if it is set to a relative filename.
+ Path string
+
+ // The project's working dir. Is used to look for additional theme config.
+ WorkingDir string
+
+ // The (optional) directory for additional configuration files.
+ AbsConfigDir string
+
+ // production, development
+ Environment string
+}
+
+func (d ConfigSourceDescriptor) configFilenames() []string {
+ if d.Filename == "" {
+ return []string{"config"}
+ }
+ return strings.Split(d.Filename, ",")
+}
+
+func (d ConfigSourceDescriptor) configFileDir() string {
+ if d.Path != "" {
+ return d.Path
+ }
+ return d.WorkingDir
+}
+
+// LoadConfigDefault is a convenience method to load the default "config.toml" config.
+func LoadConfigDefault(fs afero.Fs) (*viper.Viper, error) {
+ v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs, Filename: "config.toml"})
+ return v, err
+}
+
+var ErrNoConfigFile = errors.New("Unable to locate config file or config directory. Perhaps you need to create a new site.\n Run `hugo help new` for details.\n")
+
+// LoadConfig loads Hugo configuration into a new Viper and then adds
+// a set of defaults.
+func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provider) error) (*viper.Viper, []string, error) {
+ if d.Environment == "" {
+ d.Environment = hugo.EnvironmentProduction
+ }
+
+ var configFiles []string
+
+ v := viper.New()
+ l := configLoader{ConfigSourceDescriptor: d}
+
+ v.AutomaticEnv()
+ v.SetEnvPrefix("hugo")
+
+ var cerr error
+
+ for _, name := range d.configFilenames() {
+ var filename string
+ if filename, cerr = l.loadConfig(name, v); cerr != nil && cerr != ErrNoConfigFile {
+ return nil, nil, cerr
+ }
+ configFiles = append(configFiles, filename)
+ }
+
+ if d.AbsConfigDir != "" {
+ dirnames, err := l.loadConfigFromConfigDir(v)
+ if err == nil {
+ configFiles = append(configFiles, dirnames...)
+ }
+ cerr = err
+ }
+
+ if err := loadDefaultSettingsFor(v); err != nil {
+ return v, configFiles, err
+ }
+
+ if cerr == nil {
+ themeConfigFiles, err := l.loadThemeConfig(v)
+ if err != nil {
+ return v, configFiles, err
+ }
+
+ if len(themeConfigFiles) > 0 {
+ configFiles = append(configFiles, themeConfigFiles...)
+ }
+ }
+
+ // We create languages based on the settings, so we need to make sure that
+ // all configuration is loaded/set before doing that.
+ for _, d := range doWithConfig {
+ if err := d(v); err != nil {
+ return v, configFiles, err
+ }
+ }
+
+ if err := loadLanguageSettings(v, nil); err != nil {
+ return v, configFiles, err
+ }
+
+ return v, configFiles, cerr
+
+}
+
+type configLoader struct {
+ ConfigSourceDescriptor
+}
+
+func (l configLoader) loadConfig(configName string, v *viper.Viper) (string, error) {
+ baseDir := l.configFileDir()
+ var baseFilename string
+ if filepath.IsAbs(configName) {
+ baseFilename = configName
+ } else {
+ baseFilename = filepath.Join(baseDir, configName)
+ }
+
+ var filename string
+ fileExt := helpers.ExtNoDelimiter(configName)
+ if fileExt != "" {
+ exists, _ := helpers.Exists(baseFilename, l.Fs)
+ if exists {
+ filename = baseFilename
+ }
+ } else {
+ for _, ext := range config.ValidConfigFileExtensions {
+ filenameToCheck := baseFilename + "." + ext
+ exists, _ := helpers.Exists(filenameToCheck, l.Fs)
+ if exists {
+ filename = filenameToCheck
+ fileExt = ext
+ break
+ }
+ }
+ }
+
+ if filename == "" {
+ return "", ErrNoConfigFile
+ }
+
+ m, err := config.FromFileToMap(l.Fs, filename)
+ if err != nil {
+ return "", l.wrapFileError(err, filename)
+ }
+
+ if err = v.MergeConfigMap(m); err != nil {
+ return "", l.wrapFileError(err, filename)
+ }
+
+ return filename, nil
+
+}
+
+func (l configLoader) wrapFileError(err error, filename string) error {
+ err, _ = herrors.WithFileContextForFile(
+ err,
+ filename,
+ filename,
+ l.Fs,
+ herrors.SimpleLineMatcher)
+ return err
+}
+
+func (l configLoader) loadConfigFromConfigDir(v *viper.Viper) ([]string, error) {
+ sourceFs := l.Fs
+ configDir := l.AbsConfigDir
+
+ if _, err := sourceFs.Stat(configDir); err != nil {
+ // Config dir does not exist.
+ return nil, nil
+ }
+
+ defaultConfigDir := filepath.Join(configDir, "_default")
+ environmentConfigDir := filepath.Join(configDir, l.Environment)
+
+ var configDirs []string
+ // Merge from least to most specific.
+ for _, dir := range []string{defaultConfigDir, environmentConfigDir} {
+ if _, err := sourceFs.Stat(dir); err == nil {
+ configDirs = append(configDirs, dir)
+ }
+ }
+
+ if len(configDirs) == 0 {
+ return nil, nil
+ }
+
+ // Keep track of these so we can watch them for changes.
+ var dirnames []string
+
+ for _, configDir := range configDirs {
+ err := afero.Walk(sourceFs, configDir, func(path string, fi os.FileInfo, err error) error {
+ if fi == nil || err != nil {
+ return nil
+ }
+
+ if fi.IsDir() {
+ dirnames = append(dirnames, path)
+ return nil
+ }
+
+ if !config.IsValidConfigFilename(path) {
+ return nil
+ }
+
+ name := helpers.Filename(filepath.Base(path))
+
+ item, err := metadecoders.Default.UnmarshalFileToMap(sourceFs, path)
+ if err != nil {
+ return l.wrapFileError(err, path)
+ }
+
+ var keyPath []string
+
+ if name != "config" {
+ // Can be params.jp, menus.en etc.
+ name, lang := helpers.FileAndExtNoDelimiter(name)
+
+ keyPath = []string{name}
+
+ if lang != "" {
+ keyPath = []string{"languages", lang}
+ switch name {
+ case "menu", "menus":
+ keyPath = append(keyPath, "menus")
+ case "params":
+ keyPath = append(keyPath, "params")
+ }
+ }
+ }
+
+ root := item
+ if len(keyPath) > 0 {
+ root = make(map[string]interface{})
+ m := root
+ for i, key := range keyPath {
+ if i >= len(keyPath)-1 {
+ m[key] = item
+ } else {
+ nm := make(map[string]interface{})
+ m[key] = nm
+ m = nm
+ }
+ }
+ }
+
+ // Migrate menu => menus etc.
+ config.RenameKeys(root)
+
+ if err := v.MergeConfigMap(root); err != nil {
+ return l.wrapFileError(err, path)
+ }
+
+ return nil
+
+ })
+
+ if err != nil {
+ return nil, err
+ }
+
+ }
+
+ return dirnames, nil
+}
+
+func loadLanguageSettings(cfg config.Provider, oldLangs langs.Languages) error {
+
+ defaultLang := cfg.GetString("defaultContentLanguage")
+
+ var languages map[string]interface{}
+
+ languagesFromConfig := cfg.GetStringMap("languages")
+ disableLanguages := cfg.GetStringSlice("disableLanguages")
+
+ if len(disableLanguages) == 0 {
+ languages = languagesFromConfig
+ } else {
+ languages = make(map[string]interface{})
+ for k, v := range languagesFromConfig {
+ for _, disabled := range disableLanguages {
+ if disabled == defaultLang {
+ return fmt.Errorf("cannot disable default language %q", defaultLang)
+ }
+
+ if strings.EqualFold(k, disabled) {
+ v.(map[string]interface{})["disabled"] = true
+ break
+ }
+ }
+ languages[k] = v
+ }
+ }
+
+ var (
+ languages2 langs.Languages
+ err error
+ )
+
+ if len(languages) == 0 {
+ languages2 = append(languages2, langs.NewDefaultLanguage(cfg))
+ } else {
+ languages2, err = toSortedLanguages(cfg, languages)
+ if err != nil {
+ return _errors.Wrap(err, "Failed to parse multilingual config")
+ }
+ }
+
+ if oldLangs != nil {
+ // When in multihost mode, the languages are mapped to a server, so
+ // some structural language changes will need a restart of the dev server.
+ // The validation below isn't complete, but should cover the most
+ // important cases.
+ var invalid bool
+ if languages2.IsMultihost() != oldLangs.IsMultihost() {
+ invalid = true
+ } else {
+ if languages2.IsMultihost() && len(languages2) != len(oldLangs) {
+ invalid = true
+ }
+ }
+
+ if invalid {
+ return errors.New("language change needing a server restart detected")
+ }
+
+ if languages2.IsMultihost() {
+ // We need to transfer any server baseURL to the new language
+ for i, ol := range oldLangs {
+ nl := languages2[i]
+ nl.Set("baseURL", ol.GetString("baseURL"))
+ }
+ }
+ }
+
+ // The defaultContentLanguage is something the user has to decide, but it needs
+ // to match a language in the language definition list.
+ langExists := false
+ for _, lang := range languages2 {
+ if lang.Lang == defaultLang {
+ langExists = true
+ break
+ }
+ }
+
+ if !langExists {
+ return fmt.Errorf("site config value %q for defaultContentLanguage does not match any language definition", defaultLang)
+ }
+
+ cfg.Set("languagesSorted", languages2)
+ cfg.Set("multilingual", len(languages2) > 1)
+
+ multihost := languages2.IsMultihost()
+
+ if multihost {
+ cfg.Set("defaultContentLanguageInSubdir", true)
+ cfg.Set("multihost", true)
+ }
+
+ if multihost {
+ // The baseURL may be provided at the language level. If that is true,
+ // then every language must have a baseURL. In this case we always render
+ // to a language sub folder, which is then stripped from all the Permalink URLs etc.
+ for _, l := range languages2 {
+ burl := l.GetLocal("baseURL")
+ if burl == nil {
+ return errors.New("baseURL must be set on all or none of the languages")
+ }
+ }
+
+ }
+
+ return nil
+}
+
+func (l configLoader) loadThemeConfig(v1 *viper.Viper) ([]string, error) {
+ themesDir := paths.AbsPathify(l.WorkingDir, v1.GetString("themesDir"))
+ themes := config.GetStringSlicePreserveString(v1, "theme")
+
+ themeConfigs, err := paths.CollectThemes(l.Fs, themesDir, themes)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(themeConfigs) == 0 {
+ return nil, nil
+ }
+
+ v1.Set("allThemes", themeConfigs)
+
+ var configFilenames []string
+ for _, tc := range themeConfigs {
+ if tc.ConfigFilename != "" {
+ configFilenames = append(configFilenames, tc.ConfigFilename)
+ if err := l.applyThemeConfig(v1, tc); err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ return configFilenames, nil
+
+}
+
+func (l configLoader) applyThemeConfig(v1 *viper.Viper, theme paths.ThemeConfig) error {
+
+ const (
+ paramsKey = "params"
+ languagesKey = "languages"
+ menuKey = "menus"
+ )
+
+ v2 := theme.Cfg
+
+ for _, key := range []string{paramsKey, "outputformats", "mediatypes"} {
+ l.mergeStringMapKeepLeft("", key, v1, v2)
+ }
+
+ themeLower := strings.ToLower(theme.Name)
+ themeParamsNamespace := paramsKey + "." + themeLower
+
+ // Set namespaced params
+ if v2.IsSet(paramsKey) && !v1.IsSet(themeParamsNamespace) {
+ // Set it in the default store to make sure it gets in the same or
+ // behind the others.
+ v1.SetDefault(themeParamsNamespace, v2.Get(paramsKey))
+ }
+
+ // Only add params and new menu entries, we do not add language definitions.
+ if v1.IsSet(languagesKey) && v2.IsSet(languagesKey) {
+ v1Langs := v1.GetStringMap(languagesKey)
+ for k := range v1Langs {
+ langParamsKey := languagesKey + "." + k + "." + paramsKey
+ l.mergeStringMapKeepLeft(paramsKey, langParamsKey, v1, v2)
+ }
+ v2Langs := v2.GetStringMap(languagesKey)
+ for k := range v2Langs {
+ if k == "" {
+ continue
+ }
+ langParamsKey := languagesKey + "." + k + "." + paramsKey
+ langParamsThemeNamespace := langParamsKey + "." + themeLower
+ // Set namespaced params
+ if v2.IsSet(langParamsKey) && !v1.IsSet(langParamsThemeNamespace) {
+ v1.SetDefault(langParamsThemeNamespace, v2.Get(langParamsKey))
+ }
+
+ langMenuKey := languagesKey + "." + k + "." + menuKey
+ if v2.IsSet(langMenuKey) {
+ // Only add if not in the main config.
+ v2menus := v2.GetStringMap(langMenuKey)
+ for k, v := range v2menus {
+ menuEntry := menuKey + "." + k
+ menuLangEntry := langMenuKey + "." + k
+ if !v1.IsSet(menuEntry) && !v1.IsSet(menuLangEntry) {
+ v1.Set(menuLangEntry, v)
+ }
+ }
+ }
+ }
+ }
+
+ // Add menu definitions from theme not found in project
+ if v2.IsSet(menuKey) {
+ v2menus := v2.GetStringMap(menuKey)
+ for k, v := range v2menus {
+ menuEntry := menuKey + "." + k
+ if !v1.IsSet(menuEntry) {
+ v1.SetDefault(menuEntry, v)
+ }
+ }
+ }
+
+ return nil
+
+}
+
+func (configLoader) mergeStringMapKeepLeft(rootKey, key string, v1, v2 config.Provider) {
+ if !v2.IsSet(key) {
+ return
+ }
+
+ if !v1.IsSet(key) && !(rootKey != "" && rootKey != key && v1.IsSet(rootKey)) {
+ v1.Set(key, v2.Get(key))
+ return
+ }
+
+ m1 := v1.GetStringMap(key)
+ m2 := v2.GetStringMap(key)
+
+ for k, v := range m2 {
+ if _, found := m1[k]; !found {
+ if rootKey != "" && v1.IsSet(rootKey+"."+k) {
+ continue
+ }
+ m1[k] = v
+ }
+ }
+}
+
+func loadDefaultSettingsFor(v *viper.Viper) error {
+
+ c, err := helpers.NewContentSpec(v)
+ if err != nil {
+ return err
+ }
+
+ v.RegisterAlias("indexes", "taxonomies")
+
+ v.SetDefault("cleanDestinationDir", false)
+ v.SetDefault("watch", false)
+ v.SetDefault("metaDataFormat", "toml")
+ v.SetDefault("contentDir", "content")
+ v.SetDefault("layoutDir", "layouts")
+ v.SetDefault("assetDir", "assets")
+ v.SetDefault("staticDir", "static")
+ v.SetDefault("resourceDir", "resources")
+ v.SetDefault("archetypeDir", "archetypes")
+ v.SetDefault("publishDir", "public")
+ v.SetDefault("dataDir", "data")
+ v.SetDefault("i18nDir", "i18n")
+ v.SetDefault("themesDir", "themes")
+ v.SetDefault("buildDrafts", false)
+ v.SetDefault("buildFuture", false)
+ v.SetDefault("buildExpired", false)
+ v.SetDefault("environment", hugo.EnvironmentProduction)
+ 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("titleCaseStyle", "AP")
+ v.SetDefault("taxonomies", map[string]string{"tag": "tags", "category": "categories"})
+ v.SetDefault("permalinks", make(map[string]string))
+ v.SetDefault("sitemap", config.Sitemap{Priority: -1, Filename: "sitemap.xml"})
+ v.SetDefault("pygmentsStyle", "monokai")
+ v.SetDefault("pygmentsUseClasses", false)
+ v.SetDefault("pygmentsCodeFences", false)
+ v.SetDefault("pygmentsUseClassic", false)
+ v.SetDefault("pygmentsOptions", "")
+ v.SetDefault("disableLiveReload", false)
+ v.SetDefault("pluralizeListTitles", true)
+ v.SetDefault("forceSyncStatic", false)
+ v.SetDefault("footnoteAnchorPrefix", "")
+ v.SetDefault("footnoteReturnLinkContents", "")
+ v.SetDefault("newContentEditor", "")
+ v.SetDefault("paginate", 10)
+ v.SetDefault("paginatePath", "page")
+ v.SetDefault("summaryLength", 70)
+ v.SetDefault("blackfriday", c.BlackFriday)
+ 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("defaultContentLanguage", "en")
+ v.SetDefault("defaultContentLanguageInSubdir", false)
+ v.SetDefault("enableMissingTranslationPlaceholders", false)
+ v.SetDefault("enableGitInfo", false)
+ v.SetDefault("ignoreFiles", make([]string, 0))
+ v.SetDefault("disableAliases", false)
+ v.SetDefault("debug", false)
+ v.SetDefault("disableFastRender", false)
+ v.SetDefault("timeout", 10000) // 10 seconds
+ v.SetDefault("enableInlineShortcodes", false)
+ return nil
+}
diff --git a/hugolib/config_test.go b/hugolib/config_test.go
new file mode 100644
index 000000000..885a07ee9
--- /dev/null
+++ b/hugolib/config_test.go
@@ -0,0 +1,399 @@
+// 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/spf13/viper"
+ "github.com/stretchr/testify/require"
+)
+
+func TestLoadConfig(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ // 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(ConfigSourceDescriptor{Fs: mm, Filename: "hugo.toml"})
+ require.NoError(t, err)
+
+ assert.Equal("side", cfg.GetString("paginatePath"))
+ // default
+ assert.Equal("layouts", cfg.GetString("layoutDir"))
+ // no themes
+ assert.False(cfg.IsSet("allThemes"))
+}
+
+func TestLoadMultiConfig(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ // Add a random config variable for testing.
+ // side = page in Norwegian.
+ configContentBase := `
+ DontChange = "same"
+ PaginatePath = "side"
+ `
+ configContentSub := `
+ PaginatePath = "top"
+ `
+ mm := afero.NewMemMapFs()
+
+ writeToFs(t, mm, "base.toml", configContentBase)
+
+ writeToFs(t, mm, "override.toml", configContentSub)
+
+ cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Filename: "base.toml,override.toml"})
+ require.NoError(t, err)
+
+ assert.Equal("top", cfg.GetString("paginatePath"))
+ assert.Equal("same", cfg.GetString("DontChange"))
+}
+
+func TestLoadConfigFromTheme(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ mainConfigBasic := `
+theme = "test-theme"
+baseURL = "https://example.com/"
+
+`
+ mainConfig := `
+theme = "test-theme"
+baseURL = "https://example.com/"
+
+[frontmatter]
+date = ["date","publishDate"]
+
+[params]
+p1 = "p1 main"
+p2 = "p2 main"
+top = "top"
+
+[mediaTypes]
+[mediaTypes."text/m1"]
+suffixes = ["m1main"]
+
+[outputFormats.o1]
+mediaType = "text/m1"
+baseName = "o1main"
+
+[languages]
+[languages.en]
+languageName = "English"
+[languages.en.params]
+pl1 = "p1-en-main"
+[languages.nb]
+languageName = "Norsk"
+[languages.nb.params]
+pl1 = "p1-nb-main"
+
+[[menu.main]]
+name = "menu-main-main"
+
+[[menu.top]]
+name = "menu-top-main"
+
+`
+
+ themeConfig := `
+baseURL = "http://bep.is/"
+
+# Can not be set in theme.
+[frontmatter]
+expiryDate = ["date"]
+
+[params]
+p1 = "p1 theme"
+p2 = "p2 theme"
+p3 = "p3 theme"
+
+[mediaTypes]
+[mediaTypes."text/m1"]
+suffixes = ["m1theme"]
+[mediaTypes."text/m2"]
+suffixes = ["m2theme"]
+
+[outputFormats.o1]
+mediaType = "text/m1"
+baseName = "o1theme"
+[outputFormats.o2]
+mediaType = "text/m2"
+baseName = "o2theme"
+
+[languages]
+[languages.en]
+languageName = "English2"
+[languages.en.params]
+pl1 = "p1-en-theme"
+pl2 = "p2-en-theme"
+[[languages.en.menu.main]]
+name = "menu-lang-en-main"
+[[languages.en.menu.theme]]
+name = "menu-lang-en-theme"
+[languages.nb]
+languageName = "Norsk2"
+[languages.nb.params]
+pl1 = "p1-nb-theme"
+pl2 = "p2-nb-theme"
+top = "top-nb-theme"
+[[languages.nb.menu.main]]
+name = "menu-lang-nb-main"
+[[languages.nb.menu.theme]]
+name = "menu-lang-nb-theme"
+[[languages.nb.menu.top]]
+name = "menu-lang-nb-top"
+
+[[menu.main]]
+name = "menu-main-theme"
+
+[[menu.thememenu]]
+name = "menu-theme"
+
+`
+
+ b := newTestSitesBuilder(t)
+ b.WithConfigFile("toml", mainConfig).WithThemeConfigFile("toml", themeConfig)
+ b.CreateSites().Build(BuildCfg{})
+
+ got := b.Cfg.(*viper.Viper).AllSettings()
+
+ b.AssertObject(`
+map[string]interface {}{
+ "p1": "p1 main",
+ "p2": "p2 main",
+ "p3": "p3 theme",
+ "test-theme": map[string]interface {}{
+ "p1": "p1 theme",
+ "p2": "p2 theme",
+ "p3": "p3 theme",
+ },
+ "top": "top",
+}`, got["params"])
+
+ b.AssertObject(`
+map[string]interface {}{
+ "date": []interface {}{
+ "date",
+ "publishDate",
+ },
+}`, got["frontmatter"])
+
+ b.AssertObject(`
+map[string]interface {}{
+ "text/m1": map[string]interface {}{
+ "suffixes": []interface {}{
+ "m1main",
+ },
+ },
+ "text/m2": map[string]interface {}{
+ "suffixes": []interface {}{
+ "m2theme",
+ },
+ },
+}`, got["mediatypes"])
+
+ b.AssertObject(`
+map[string]interface {}{
+ "o1": map[string]interface {}{
+ "basename": "o1main",
+ "mediatype": Type{
+ MainType: "text",
+ SubType: "m1",
+ Delimiter: ".",
+ Suffixes: []string{
+ "m1main",
+ },
+ },
+ },
+ "o2": map[string]interface {}{
+ "basename": "o2theme",
+ "mediatype": Type{
+ MainType: "text",
+ SubType: "m2",
+ Delimiter: ".",
+ Suffixes: []string{
+ "m2theme",
+ },
+ },
+ },
+}`, got["outputformats"])
+
+ b.AssertObject(`map[string]interface {}{
+ "en": map[string]interface {}{
+ "languagename": "English",
+ "menus": map[string]interface {}{
+ "theme": []map[string]interface {}{
+ map[string]interface {}{
+ "name": "menu-lang-en-theme",
+ },
+ },
+ },
+ "params": map[string]interface {}{
+ "pl1": "p1-en-main",
+ "pl2": "p2-en-theme",
+ "test-theme": map[string]interface {}{
+ "pl1": "p1-en-theme",
+ "pl2": "p2-en-theme",
+ },
+ },
+ },
+ "nb": map[string]interface {}{
+ "languagename": "Norsk",
+ "menus": map[string]interface {}{
+ "theme": []map[string]interface {}{
+ map[string]interface {}{
+ "name": "menu-lang-nb-theme",
+ },
+ },
+ },
+ "params": map[string]interface {}{
+ "pl1": "p1-nb-main",
+ "pl2": "p2-nb-theme",
+ "test-theme": map[string]interface {}{
+ "pl1": "p1-nb-theme",
+ "pl2": "p2-nb-theme",
+ "top": "top-nb-theme",
+ },
+ },
+ },
+}
+`, got["languages"])
+
+ b.AssertObject(`
+map[string]interface {}{
+ "main": []map[string]interface {}{
+ map[string]interface {}{
+ "name": "menu-main-main",
+ },
+ },
+ "thememenu": []map[string]interface {}{
+ map[string]interface {}{
+ "name": "menu-theme",
+ },
+ },
+ "top": []map[string]interface {}{
+ map[string]interface {}{
+ "name": "menu-top-main",
+ },
+ },
+}
+`, got["menus"])
+
+ assert.Equal("https://example.com/", got["baseurl"])
+
+ if true {
+ return
+ }
+ // Test variants with only values from theme
+ b = newTestSitesBuilder(t)
+ b.WithConfigFile("toml", mainConfigBasic).WithThemeConfigFile("toml", themeConfig)
+ b.CreateSites().Build(BuildCfg{})
+
+ got = b.Cfg.(*viper.Viper).AllSettings()
+
+ b.AssertObject(`map[string]interface {}{
+ "p1": "p1 theme",
+ "p2": "p2 theme",
+ "p3": "p3 theme",
+ "test-theme": map[string]interface {}{
+ "p1": "p1 theme",
+ "p2": "p2 theme",
+ "p3": "p3 theme",
+ },
+}`, got["params"])
+
+ assert.Nil(got["languages"])
+ b.AssertObject(`
+map[string]interface {}{
+ "text/m1": map[string]interface {}{
+ "suffix": "m1theme",
+ },
+ "text/m2": map[string]interface {}{
+ "suffix": "m2theme",
+ },
+}`, got["mediatypes"])
+
+ b.AssertObject(`
+map[string]interface {}{
+ "o1": map[string]interface {}{
+ "basename": "o1theme",
+ "mediatype": Type{
+ MainType: "text",
+ SubType: "m1",
+ Suffix: "m1theme",
+ Delimiter: ".",
+ },
+ },
+ "o2": map[string]interface {}{
+ "basename": "o2theme",
+ "mediatype": Type{
+ MainType: "text",
+ SubType: "m2",
+ Suffix: "m2theme",
+ Delimiter: ".",
+ },
+ },
+}`, got["outputformats"])
+ b.AssertObject(`
+map[string]interface {}{
+ "main": []interface {}{
+ map[string]interface {}{
+ "name": "menu-main-theme",
+ },
+ },
+ "thememenu": []interface {}{
+ map[string]interface {}{
+ "name": "menu-theme",
+ },
+ },
+}`, got["menu"])
+
+}
+
+func TestPrivacyConfig(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ tomlConfig := `
+
+someOtherValue = "foo"
+
+[privacy]
+[privacy.youtube]
+privacyEnhanced = true
+`
+
+ b := newTestSitesBuilder(t)
+ b.WithConfigFile("toml", tomlConfig)
+ b.Build(BuildCfg{SkipRender: true})
+
+ assert.True(b.H.Sites[0].Info.Config().Privacy.YouTube.PrivacyEnhanced)
+
+}
diff --git a/hugolib/configdir_test.go b/hugolib/configdir_test.go
new file mode 100644
index 000000000..c1afbb14e
--- /dev/null
+++ b/hugolib/configdir_test.go
@@ -0,0 +1,154 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/common/herrors"
+
+ "github.com/gohugoio/hugo/htesting"
+ "github.com/spf13/afero"
+ "github.com/stretchr/testify/require"
+)
+
+func TestLoadConfigDir(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ configContent := `
+baseURL = "https://example.org"
+paginagePath = "pag_root"
+
+[languages.en]
+weight = 0
+languageName = "English"
+
+[languages.no]
+weight = 10
+languageName = "FOO"
+
+[params]
+p1 = "p1_base"
+
+`
+
+ mm := afero.NewMemMapFs()
+
+ writeToFs(t, mm, "hugo.toml", configContent)
+
+ fb := htesting.NewTestdataBuilder(mm, "config/_default", t)
+
+ fb.Add("config.toml", `paginatePath = "pag_default"`)
+
+ fb.Add("params.yaml", `
+p2: "p2params_default"
+p3: "p3params_default"
+p4: "p4params_default"
+`)
+ fb.Add("menus.toml", `
+[[docs]]
+name = "About Hugo"
+weight = 1
+[[docs]]
+name = "Home"
+weight = 2
+ `)
+
+ fb.Add("menus.no.toml", `
+ [[docs]]
+ name = "Om Hugo"
+ weight = 1
+ `)
+
+ fb.Add("params.no.toml",
+ `
+p3 = "p3params_no_default"
+p4 = "p4params_no_default"`,
+ )
+ fb.Add("languages.no.toml", `languageName = "Norsk_no_default"`)
+
+ fb.Build()
+
+ fb = fb.WithWorkingDir("config/production")
+
+ fb.Add("config.toml", `paginatePath = "pag_production"`)
+
+ fb.Add("params.no.toml", `
+p2 = "p2params_no_production"
+p3 = "p3params_no_production"
+`)
+
+ fb.Build()
+
+ fb = fb.WithWorkingDir("config/development")
+
+ // This is set in all the config.toml variants above, but this will win.
+ fb.Add("config.TOML", `paginatePath = "pag_development"`)
+ // Issue #5646
+ fb.Add("config.toml.swp", `p3 = "paginatePath = "nono"`)
+
+ fb.Add("params.no.toml", `p3 = "p3params_no_development"`)
+ fb.Add("params.toml", `p3 = "p3params_development"`)
+
+ fb.Build()
+
+ cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Environment: "development", Filename: "hugo.toml", AbsConfigDir: "config"})
+ assert.NoError(err)
+
+ assert.Equal("pag_development", cfg.GetString("paginatePath")) // /config/development/config.toml
+
+ assert.Equal(10, cfg.GetInt("languages.no.weight")) // /config.toml
+ assert.Equal("Norsk_no_default", cfg.GetString("languages.no.languageName")) // /config/_default/languages.no.toml
+
+ assert.Equal("p1_base", cfg.GetString("params.p1"))
+ assert.Equal("p2params_default", cfg.GetString("params.p2")) // Is in both _default and production
+ assert.Equal("p3params_development", cfg.GetString("params.p3"))
+ assert.Equal("p3params_no_development", cfg.GetString("languages.no.params.p3"))
+
+ assert.Equal(2, len(cfg.Get("menus.docs").(([]map[string]interface{}))))
+ noMenus := cfg.Get("languages.no.menus.docs")
+ assert.NotNil(noMenus)
+ assert.Equal(1, len(noMenus.(([]map[string]interface{}))))
+
+}
+
+func TestLoadConfigDirError(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ configContent := `
+baseURL = "https://example.org"
+
+`
+
+ mm := afero.NewMemMapFs()
+
+ writeToFs(t, mm, "hugo.toml", configContent)
+
+ fb := htesting.NewTestdataBuilder(mm, "config/development", t)
+
+ fb.Add("config.toml", `invalid & syntax`).Build()
+
+ _, _, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Environment: "development", Filename: "hugo.toml", AbsConfigDir: "config"})
+ assert.Error(err)
+
+ fe := herrors.UnwrapErrorWithFileContext(err)
+ assert.NotNil(fe)
+ assert.Equal(filepath.FromSlash("config/development/config.toml"), fe.Position().Filename)
+
+}
diff --git a/hugolib/datafiles_test.go b/hugolib/datafiles_test.go
new file mode 100644
index 000000000..b65183a8a
--- /dev/null
+++ b/hugolib/datafiles_test.go
@@ -0,0 +1,398 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+
+ "github.com/gohugoio/hugo/common/loggers"
+
+ "github.com/gohugoio/hugo/deps"
+
+ "fmt"
+ "runtime"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestDataDir(t *testing.T) {
+ t.Parallel()
+ equivDataDirs := make([]dataDir, 3)
+ equivDataDirs[0].addSource("data/test/a.json", `{ "b" : { "c1": "red" , "c2": "blue" } }`)
+ equivDataDirs[1].addSource("data/test/a.yaml", "b:\n c1: red\n c2: blue")
+ equivDataDirs[2].addSource("data/test/a.toml", "[b]\nc1 = \"red\"\nc2 = \"blue\"\n")
+ expected := map[string]interface{}{
+ "test": map[string]interface{}{
+ "a": map[string]interface{}{
+ "b": map[string]interface{}{
+ "c1": "red",
+ "c2": "blue",
+ },
+ },
+ },
+ }
+ doTestEquivalentDataDirs(t, equivDataDirs, expected)
+}
+
+// Unable to enforce equivalency for int values as
+// the JSON, YAML and TOML parsers return
+// float64, int, int64 respectively. They all return
+// float64 for float values though:
+func TestDataDirNumeric(t *testing.T) {
+ t.Parallel()
+ equivDataDirs := make([]dataDir, 3)
+ equivDataDirs[0].addSource("data/test/a.json", `{ "b" : { "c1": 1.7 , "c2": 2.9 } }`)
+ equivDataDirs[1].addSource("data/test/a.yaml", "b:\n c1: 1.7\n c2: 2.9")
+ equivDataDirs[2].addSource("data/test/a.toml", "[b]\nc1 = 1.7\nc2 = 2.9\n")
+ expected := map[string]interface{}{
+ "test": map[string]interface{}{
+ "a": map[string]interface{}{
+ "b": map[string]interface{}{
+ "c1": 1.7,
+ "c2": 2.9,
+ },
+ },
+ },
+ }
+ doTestEquivalentDataDirs(t, equivDataDirs, expected)
+}
+
+func TestDataDirBoolean(t *testing.T) {
+ t.Parallel()
+ equivDataDirs := make([]dataDir, 3)
+ equivDataDirs[0].addSource("data/test/a.json", `{ "b" : { "c1": true , "c2": false } }`)
+ equivDataDirs[1].addSource("data/test/a.yaml", "b:\n c1: true\n c2: false")
+ equivDataDirs[2].addSource("data/test/a.toml", "[b]\nc1 = true\nc2 = false\n")
+ expected := map[string]interface{}{
+ "test": map[string]interface{}{
+ "a": map[string]interface{}{
+ "b": map[string]interface{}{
+ "c1": true,
+ "c2": false,
+ },
+ },
+ },
+ }
+ doTestEquivalentDataDirs(t, equivDataDirs, expected)
+}
+
+func TestDataDirTwoFiles(t *testing.T) {
+ t.Parallel()
+ equivDataDirs := make([]dataDir, 3)
+
+ equivDataDirs[0].addSource("data/test/foo.json", `{ "bar": "foofoo" }`)
+ equivDataDirs[0].addSource("data/test.json", `{ "hello": [ "world", "foo" ] }`)
+
+ equivDataDirs[1].addSource("data/test/foo.yaml", "bar: foofoo")
+ equivDataDirs[1].addSource("data/test.yaml", "hello:\n- world\n- foo")
+
+ equivDataDirs[2].addSource("data/test/foo.toml", "bar = \"foofoo\"")
+ equivDataDirs[2].addSource("data/test.toml", "hello = [\"world\", \"foo\"]")
+
+ expected :=
+ map[string]interface{}{
+ "test": map[string]interface{}{
+ "hello": []interface{}{
+ "world",
+ "foo",
+ },
+ "foo": map[string]interface{}{
+ "bar": "foofoo",
+ },
+ },
+ }
+
+ doTestEquivalentDataDirs(t, equivDataDirs, expected)
+}
+
+func TestDataDirOverriddenValue(t *testing.T) {
+ t.Parallel()
+ equivDataDirs := make([]dataDir, 3)
+
+ // filepath.Walk walks the files in lexical order, '/' comes before '.'. Simulate this:
+ equivDataDirs[0].addSource("data/a.json", `{"a": "1"}`)
+ equivDataDirs[0].addSource("data/test/v1.json", `{"v1-2": "2"}`)
+ equivDataDirs[0].addSource("data/test/v2.json", `{"v2": ["2", "3"]}`)
+ equivDataDirs[0].addSource("data/test.json", `{"v1": "1"}`)
+
+ equivDataDirs[1].addSource("data/a.yaml", "a: \"1\"")
+ equivDataDirs[1].addSource("data/test/v1.yaml", "v1-2: \"2\"")
+ equivDataDirs[1].addSource("data/test/v2.yaml", "v2:\n- \"2\"\n- \"3\"")
+ equivDataDirs[1].addSource("data/test.yaml", "v1: \"1\"")
+
+ equivDataDirs[2].addSource("data/a.toml", "a = \"1\"")
+ equivDataDirs[2].addSource("data/test/v1.toml", "v1-2 = \"2\"")
+ equivDataDirs[2].addSource("data/test/v2.toml", "v2 = [\"2\", \"3\"]")
+ equivDataDirs[2].addSource("data/test.toml", "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"}},
+ },
+ }
+
+ doTestEquivalentDataDirs(t, equivDataDirs, expected)
+}
+
+// Issue #4361, #3890
+func TestDataDirArrayAtTopLevelOfFile(t *testing.T) {
+ t.Parallel()
+ equivDataDirs := make([]dataDir, 2)
+
+ equivDataDirs[0].addSource("data/test.json", `[ { "hello": "world" }, { "what": "time" }, { "is": "lunch?" } ]`)
+ equivDataDirs[1].addSource("data/test.yaml", `
+- hello: world
+- what: time
+- is: lunch?
+`)
+
+ expected :=
+ map[string]interface{}{
+ "test": []interface{}{
+ map[string]interface{}{"hello": "world"},
+ map[string]interface{}{"what": "time"},
+ map[string]interface{}{"is": "lunch?"},
+ },
+ }
+
+ doTestEquivalentDataDirs(t, equivDataDirs, expected)
+}
+
+// Issue #892
+func TestDataDirMultipleSources(t *testing.T) {
+ t.Parallel()
+
+ var dd dataDir
+ dd.addSource("data/test/first.yaml", "bar: 1")
+ dd.addSource("themes/mytheme/data/test/first.yaml", "bar: 2")
+ dd.addSource("data/test/second.yaml", "tender: 2")
+
+ expected :=
+ map[string]interface{}{
+ "test": map[string]interface{}{
+ "first": map[string]interface{}{
+ "bar": 1,
+ },
+ "second": map[string]interface{}{
+ "tender": 2,
+ },
+ },
+ }
+
+ doTestDataDir(t, dd, expected,
+ "theme", "mytheme")
+
+}
+
+// test (and show) the way values from four different sources,
+// including theme data, commingle and override
+func TestDataDirMultipleSourcesCommingled(t *testing.T) {
+ t.Parallel()
+
+ var dd dataDir
+ dd.addSource("data/a.json", `{ "b1" : { "c1": "data/a" }, "b2": "data/a", "b3": ["x", "y", "z"] }`)
+ dd.addSource("themes/mytheme/data/a.json", `{ "b1": "mytheme/data/a", "b2": "mytheme/data/a", "b3": "mytheme/data/a" }`)
+ dd.addSource("themes/mytheme/data/a/b1.json", `{ "c1": "mytheme/data/a/b1", "c2": "mytheme/data/a/b1" }`)
+ dd.addSource("data/a/b1.json", `{ "c1": "data/a/b1" }`)
+
+ // Per handleDataFile() comment:
+ // 1. A theme uses the same key; the main data folder wins
+ // 2. A sub folder uses the same key: the sub folder wins
+ expected :=
+ map[string]interface{}{
+ "a": map[string]interface{}{
+ "b1": map[string]interface{}{
+ "c1": "data/a/b1",
+ "c2": "mytheme/data/a/b1",
+ },
+ "b2": "data/a",
+ "b3": []interface{}{"x", "y", "z"},
+ },
+ }
+
+ doTestDataDir(t, dd, expected, "theme", "mytheme")
+}
+
+func TestDataDirCollidingChildArrays(t *testing.T) {
+ t.Parallel()
+
+ var dd dataDir
+ dd.addSource("themes/mytheme/data/a/b2.json", `["Q", "R", "S"]`)
+ dd.addSource("data/a.json", `{ "b1" : "data/a", "b2" : ["x", "y", "z"] }`)
+ dd.addSource("data/a/b2.json", `["1", "2", "3"]`)
+
+ // Per handleDataFile() comment:
+ // 1. A theme uses the same key; the main data folder wins
+ // 2. A sub folder uses the same key: the sub folder wins
+ expected :=
+ map[string]interface{}{
+ "a": map[string]interface{}{
+ "b1": "data/a",
+ "b2": []interface{}{"1", "2", "3"},
+ },
+ }
+
+ doTestDataDir(t, dd, expected, "theme", "mytheme")
+}
+
+func TestDataDirCollidingTopLevelArrays(t *testing.T) {
+ t.Parallel()
+
+ var dd dataDir
+ dd.addSource("themes/mytheme/data/a/b1.json", `["x", "y", "z"]`)
+ dd.addSource("data/a/b1.json", `["1", "2", "3"]`)
+
+ expected :=
+ map[string]interface{}{
+ "a": map[string]interface{}{
+ "b1": []interface{}{"1", "2", "3"},
+ },
+ }
+
+ doTestDataDir(t, dd, expected, "theme", "mytheme")
+}
+
+func TestDataDirCollidingMapsAndArrays(t *testing.T) {
+ t.Parallel()
+
+ var dd dataDir
+ // on
+ dd.addSource("themes/mytheme/data/a.json", `["1", "2", "3"]`)
+ dd.addSource("themes/mytheme/data/b.json", `{ "film" : "Logan Lucky" }`)
+ dd.addSource("data/a.json", `{ "music" : "Queen's Rebuke" }`)
+ dd.addSource("data/b.json", `["x", "y", "z"]`)
+
+ expected :=
+ map[string]interface{}{
+ "a": map[string]interface{}{
+ "music": "Queen's Rebuke",
+ },
+ "b": []interface{}{"x", "y", "z"},
+ }
+
+ doTestDataDir(t, dd, expected, "theme", "mytheme")
+}
+
+type dataDir struct {
+ sources [][2]string
+}
+
+func (d *dataDir) addSource(path, content string) {
+ d.sources = append(d.sources, [2]string{path, content})
+}
+
+func doTestEquivalentDataDirs(t *testing.T, equivDataDirs []dataDir, expected interface{}, configKeyValues ...interface{}) {
+ for i, dd := range equivDataDirs {
+ err := doTestDataDirImpl(t, dd, expected, configKeyValues...)
+ if err != "" {
+ t.Errorf("equivDataDirs[%d]: %s", i, err)
+ }
+ }
+}
+
+func doTestDataDir(t *testing.T, dd dataDir, expected interface{}, configKeyValues ...interface{}) {
+ err := doTestDataDirImpl(t, dd, expected, configKeyValues...)
+ if err != "" {
+ t.Error(err)
+ }
+}
+
+func doTestDataDirImpl(t *testing.T, dd dataDir, expected interface{}, configKeyValues ...interface{}) (err string) {
+ var (
+ cfg, fs = newTestCfg()
+ )
+
+ for i := 0; i < len(configKeyValues); i += 2 {
+ cfg.Set(configKeyValues[i].(string), configKeyValues[i+1])
+ }
+
+ var (
+ logger = loggers.NewErrorLogger()
+ depsCfg = deps.DepsCfg{Fs: fs, Cfg: cfg, Logger: logger}
+ )
+
+ writeSource(t, fs, filepath.Join("content", "dummy.md"), "content")
+ writeSourcesToSource(t, "", fs, dd.sources...)
+
+ expectBuildError := false
+
+ if ok, shouldFail := expected.(bool); ok && shouldFail {
+ expectBuildError = true
+ }
+
+ // trap and report panics as unmarshaling errors so that test suit can complete
+ defer func() {
+ if r := recover(); r != nil {
+ // Capture the stack trace
+ buf := make([]byte, 10000)
+ runtime.Stack(buf, false)
+ t.Errorf("PANIC: %s\n\nStack Trace : %s", r, string(buf))
+ }
+ }()
+
+ s := buildSingleSiteExpected(t, false, expectBuildError, depsCfg, BuildCfg{SkipRender: true})
+
+ if !expectBuildError && !reflect.DeepEqual(expected, s.h.Data()) {
+ // This disabled code detects the situation described in the WARNING message below.
+ // The situation seems to only occur for TOML data with integer values.
+ // Perhaps the TOML parser returns ints in another type.
+ // Re-enable temporarily to debug fails that should be passing.
+ // Re-enable permanently if reflect.DeepEqual is simply too strict.
+ /*
+ exp := fmt.Sprintf("%#v", expected)
+ got := fmt.Sprintf("%#v", s.Data)
+ if exp == got {
+ t.Logf("WARNING: reflect.DeepEqual returned FALSE for values that appear equal.\n"+
+ "Treating as equal for the purpose of the test, but this maybe should be investigated.\n"+
+ "Expected data:\n%v got\n%v\n\nExpected type structure:\n%#[1]v got\n%#[2]v", expected, s.Data)
+ return
+ }
+ */
+
+ return fmt.Sprintf("Expected data:\n%v got\n%v\n\nExpected type structure:\n%#[1]v got\n%#[2]v", expected, s.h.Data())
+ }
+
+ return
+}
+
+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..f5c093646
--- /dev/null
+++ b/hugolib/disableKinds_test.go
@@ -0,0 +1,223 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/resources/page"
+
+ "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, page.KindSection, kind404)
+}
+
+func TestDisableKindsOneDisabled(t *testing.T) {
+ t.Parallel()
+ for _, kind := range allKinds {
+ if kind == page.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 := LoadConfigDefault(mf)
+ 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.Source, "Category Terms", "2017-01-01", "content/categories/_index.md", 10)
+ writeNewContentFile(t, fs.Source, "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, page.KindPage, "public/sect/p1/index.html", "Single|P1")
+ assertDisabledKind(th,
+ func(isDisabled bool) bool {
+ p := s.getPage(page.KindHome)
+ if isDisabled {
+ return p == nil
+ }
+ return p != nil
+ }, disabled, page.KindHome, "public/index.html", "Home")
+ assertDisabledKind(th,
+ func(isDisabled bool) bool {
+ p := s.getPage(page.KindSection, "sect")
+ if isDisabled {
+ return p == nil
+ }
+ return p != nil
+ }, disabled, page.KindSection, "public/sect/index.html", "Sects")
+ assertDisabledKind(th,
+ func(isDisabled bool) bool {
+ p := s.getPage(page.KindTaxonomy, "tags", "tag1")
+
+ if isDisabled {
+ return p == nil
+ }
+ return p != nil
+
+ }, disabled, page.KindTaxonomy, "public/tags/tag1/index.html", "Tag1")
+ assertDisabledKind(th,
+ func(isDisabled bool) bool {
+ p := s.getPage(page.KindTaxonomyTerm, "tags")
+ if isDisabled {
+ return p == nil
+ }
+ return p != nil
+
+ }, disabled, page.KindTaxonomyTerm, "public/tags/index.html", "Tags")
+ assertDisabledKind(th,
+ func(isDisabled bool) bool {
+ p := s.getPage(page.KindTaxonomyTerm, "categories")
+
+ if isDisabled {
+ return p == nil
+ }
+ return p != nil
+
+ }, disabled, page.KindTaxonomyTerm, "public/categories/index.html", "Category Terms")
+ assertDisabledKind(th,
+ func(isDisabled bool) bool {
+ p := s.getPage(page.KindTaxonomy, "categories", "hugo")
+ if isDisabled {
+ return p == nil
+ }
+ return p != nil
+
+ }, disabled, page.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(page.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..c70380a4b
--- /dev/null
+++ b/hugolib/embedded_shortcodes_test.go
@@ -0,0 +1,379 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+
+ "github.com/spf13/cast"
+
+ "path/filepath"
+
+ "github.com/gohugoio/hugo/deps"
+
+ "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)
+
+ content, err := s.RegularPages()[0].Content()
+ require.NoError(t, err)
+ output := cast.ToString(content)
+
+ if !strings.Contains(output, expected) {
+ t.Errorf("Got\n%q\nExpected\n%q", output, expected)
+ }
+}
+
+func TestShortcodeHighlight(t *testing.T) {
+ t.Parallel()
+
+ for _, this := range []struct {
+ in, expected string
+ }{
+ {`{{< highlight java >}}
+void do();
+{{< /highlight >}}`,
+ `(?s)<div class="highlight"><pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java"`,
+ },
+ {`{{< highlight java "style=friendly" >}}
+void do();
+{{< /highlight >}}`,
+ `(?s)<div class="highlight"><pre style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-java" data-lang="java">`,
+ },
+ } {
+
+ 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)<figure>.*?<img src=\"/img/hugo-logo.png\"/>.*?</figure>",
+ },
+ {
+ // set alt
+ `{{< figure src="/img/hugo-logo.png" alt="Hugo logo" >}}`,
+ "(?s)<figure>.*?<img src=\"/img/hugo-logo.png\".+?alt=\"Hugo logo\"/>.*?</figure>",
+ },
+ // set title
+ {
+ `{{< figure src="/img/hugo-logo.png" title="Hugo logo" >}}`,
+ "(?s)<figure>.*?<img src=\"/img/hugo-logo.png\"/>.*?<figcaption>.*?<h4>Hugo logo</h4>.*?</figcaption>.*?</figure>",
+ },
+ // set attr and attrlink
+ {
+ `{{< figure src="/img/hugo-logo.png" attr="Hugo logo" attrlink="/img/hugo-logo.png" >}}`,
+ "(?s)<figure>.*?<img src=\"/img/hugo-logo.png\"/>.*?<figcaption>.*?<p>.*?<a href=\"/img/hugo-logo.png\">.*?Hugo logo.*?</a>.*?</p>.*?</figcaption>.*?</figure>",
+ },
+ } {
+
+ 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 title=\"YouTube Video\">.*?</iframe>.*?</div>\n",
+ },
+ // set class
+ {
+ `{{< youtube w7Ft2ymGmfc video>}}`,
+ "(?s)\n<div class=\"video\">.*?<iframe src=\"//www.youtube.com/embed/w7Ft2ymGmfc\" allowfullscreen title=\"YouTube Video\">.*?</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 title=\"YouTube Video\">.*?</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 type=\"application/javascript\" src=\"//gist.github.com/spf13/7896402.js\"></script>",
+ },
+ {
+ `{{< gist spf13 7896402 "img.html" >}}`,
+ "(?s)^<script type=\"application/javascript\" 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&mdash; 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>&mdash; 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/embedded_templates_test.go b/hugolib/embedded_templates_test.go
new file mode 100644
index 000000000..2a1e2d3b2
--- /dev/null
+++ b/hugolib/embedded_templates_test.go
@@ -0,0 +1,58 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/stretchr/testify/require"
+)
+
+// Just some simple test of the embedded templates to avoid
+// https://github.com/gohugoio/hugo/issues/4757 and similar.
+// TODO(bep) fix me https://github.com/gohugoio/hugo/issues/5926
+func _TestEmbeddedTemplates(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+ assert.True(true)
+
+ home := []string{"index.html", `
+GA:
+{{ template "_internal/google_analytics.html" . }}
+
+GA async:
+
+{{ template "_internal/google_analytics_async.html" . }}
+
+Disqus:
+
+{{ template "_internal/disqus.html" . }}
+
+`}
+
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile().WithTemplatesAdded(home...)
+
+ b.Build(BuildCfg{})
+
+ // Gheck GA regular and async
+ b.AssertFileContent("public/index.html",
+ "'anonymizeIp', true",
+ "'script','https://www.google-analytics.com/analytics.js','ga');\n\tga('create', 'ga_id', 'auto')",
+ "<script async src='https://www.google-analytics.com/analytics.js'>")
+
+ // Disqus
+ b.AssertFileContent("public/index.html", "\"disqus_shortname\" + '.disqus.com/embed.js';")
+}
diff --git a/hugolib/fileInfo.go b/hugolib/fileInfo.go
new file mode 100644
index 000000000..ea3b15ef3
--- /dev/null
+++ b/hugolib/fileInfo.go
@@ -0,0 +1,136 @@
+// 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 (
+ "strings"
+
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/source"
+)
+
+// fileInfo implements the File and ReadableFile interface.
+var (
+ _ source.File = (*fileInfo)(nil)
+ _ source.ReadableFile = (*fileInfo)(nil)
+ _ pathLangFile = (*fileInfo)(nil)
+)
+
+// A partial interface to prevent ambigous compiler error.
+type basePather interface {
+ Filename() string
+ RealName() string
+ BaseDir() string
+}
+
+type fileInfo struct {
+ bundleTp bundleDirType
+
+ source.ReadableFile
+ basePather
+
+ overriddenLang string
+
+ // Set if the content language for this file is disabled.
+ disabled bool
+}
+
+func (fi *fileInfo) Lang() string {
+ if fi.overriddenLang != "" {
+ return fi.overriddenLang
+ }
+ return fi.ReadableFile.Lang()
+}
+
+func (fi *fileInfo) Filename() string {
+ if fi == nil || fi.basePather == nil {
+ return ""
+ }
+ return fi.basePather.Filename()
+}
+
+func (fi *fileInfo) String() string {
+ if fi == nil || fi.ReadableFile == nil {
+ return ""
+ }
+ return fi.Path()
+}
+
+func (fi *fileInfo) isOwner() bool {
+ return fi.bundleTp > bundleNot
+}
+
+func IsContentFile(filename string) bool {
+ return contentFileExtensionsSet[strings.TrimPrefix(helpers.Ext(filename), ".")]
+}
+
+func (fi *fileInfo) isContentFile() bool {
+ return contentFileExtensionsSet[fi.Ext()]
+}
+
+func newFileInfo(sp *source.SourceSpec, baseDir, filename string, fi pathLangFileFi, tp bundleDirType) *fileInfo {
+
+ baseFi := sp.NewFileInfo(baseDir, filename, tp == bundleLeaf, fi)
+ f := &fileInfo{
+ bundleTp: tp,
+ ReadableFile: baseFi,
+ basePather: fi,
+ }
+
+ lang := f.Lang()
+ f.disabled = lang != "" && sp.DisabledLanguages[lang]
+
+ return f
+
+}
+
+type bundleDirType int
+
+const (
+ bundleNot bundleDirType = iota
+
+ // All from here are bundles in one form or another.
+ bundleLeaf
+ bundleBranch
+)
+
+// Returns the given file's name's bundle type and whether it is a content
+// file or not.
+func classifyBundledFile(name string) (bundleDirType, bool) {
+ if !IsContentFile(name) {
+ return bundleNot, false
+ }
+ if strings.HasPrefix(name, "_index.") {
+ return bundleBranch, true
+ }
+
+ if strings.HasPrefix(name, "index.") {
+ return bundleLeaf, true
+ }
+
+ return bundleNot, true
+}
+
+func (b bundleDirType) String() string {
+ switch b {
+ case bundleNot:
+ return "Not a bundle"
+ case bundleLeaf:
+ return "Regular bundle"
+ case bundleBranch:
+ return "Branch bundle"
+ }
+
+ return ""
+}
diff --git a/hugolib/fileInfo_test.go b/hugolib/fileInfo_test.go
new file mode 100644
index 000000000..10f5f0517
--- /dev/null
+++ b/hugolib/fileInfo_test.go
@@ -0,0 +1,30 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/require"
+)
+
+func TestFileInfo(t *testing.T) {
+ t.Run("String", func(t *testing.T) {
+ assert := require.New(t)
+ fi := &fileInfo{}
+ _, err := cast.ToStringE(fi)
+ assert.NoError(err)
+ })
+}
diff --git a/hugolib/filesystems/basefs.go b/hugolib/filesystems/basefs.go
new file mode 100644
index 000000000..d88141efd
--- /dev/null
+++ b/hugolib/filesystems/basefs.go
@@ -0,0 +1,760 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package filesystems provides the fine grained file systems used by Hugo. These
+// are typically virtual filesystems that are composites of project and theme content.
+package filesystems
+
+import (
+ "errors"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/gohugoio/hugo/config"
+
+ "github.com/gohugoio/hugo/hugofs"
+
+ "fmt"
+
+ "github.com/gohugoio/hugo/hugolib/paths"
+ "github.com/gohugoio/hugo/langs"
+ "github.com/spf13/afero"
+)
+
+// When we create a virtual filesystem with data and i18n bundles for the project and the themes,
+// this is the name of the project's virtual root. It got it's funky name to make sure
+// (or very unlikely) that it collides with a theme name.
+const projectVirtualFolder = "__h__project"
+
+var filePathSeparator = string(filepath.Separator)
+
+// BaseFs contains the core base filesystems used by Hugo. The name "base" is used
+// to underline that even if they can be composites, they all have a base path set to a specific
+// resource folder, e.g "/my-project/content". So, no absolute filenames needed.
+type BaseFs struct {
+
+ // SourceFilesystems contains the different source file systems.
+ *SourceFilesystems
+
+ // The filesystem used to publish the rendered site.
+ // This usually maps to /my-project/public.
+ PublishFs afero.Fs
+
+ themeFs afero.Fs
+
+ // TODO(bep) improve the "theme interaction"
+ AbsThemeDirs []string
+}
+
+// RelContentDir tries to create a path relative to the content root from
+// the given filename. The return value is the path and language code.
+func (b *BaseFs) RelContentDir(filename string) string {
+ for _, dirname := range b.SourceFilesystems.Content.Dirnames {
+ if strings.HasPrefix(filename, dirname) {
+ rel := strings.TrimPrefix(filename, dirname)
+ return strings.TrimPrefix(rel, filePathSeparator)
+ }
+ }
+ // Either not a content dir or already relative.
+ return filename
+}
+
+// SourceFilesystems contains the different source file systems. These can be
+// composite file systems (theme and project etc.), and they have all root
+// set to the source type the provides: data, i18n, static, layouts.
+type SourceFilesystems struct {
+ Content *SourceFilesystem
+ Data *SourceFilesystem
+ I18n *SourceFilesystem
+ Layouts *SourceFilesystem
+ Archetypes *SourceFilesystem
+ Assets *SourceFilesystem
+ Resources *SourceFilesystem
+
+ // This is a unified read-only view of the project's and themes' workdir.
+ Work *SourceFilesystem
+
+ // When in multihost we have one static filesystem per language. The sync
+ // static files is currently done outside of the Hugo build (where there is
+ // a concept of a site per language).
+ // When in non-multihost mode there will be one entry in this map with a blank key.
+ Static map[string]*SourceFilesystem
+}
+
+// A SourceFilesystem holds the filesystem for a given source type in Hugo (data,
+// i18n, layouts, static) and additional metadata to be able to use that filesystem
+// in server mode.
+type SourceFilesystem struct {
+ // This is a virtual composite filesystem. It expects path relative to a context.
+ Fs afero.Fs
+
+ // This is the base source filesystem. In real Hugo, this will be the OS filesystem.
+ // Use this if you need to resolve items in Dirnames below.
+ SourceFs afero.Fs
+
+ // Dirnames is absolute filenames to the directories in this filesystem.
+ Dirnames []string
+
+ // When syncing a source folder to the target (e.g. /public), this may
+ // be set to publish into a subfolder. This is used for static syncing
+ // in multihost mode.
+ PublishFolder string
+}
+
+// ContentStaticAssetFs will create a new composite filesystem from the content,
+// static, and asset filesystems. The site language is needed to pick the correct static filesystem.
+// The order is content, static and then assets.
+// TODO(bep) check usage
+func (s SourceFilesystems) ContentStaticAssetFs(lang string) afero.Fs {
+ staticFs := s.StaticFs(lang)
+
+ base := afero.NewCopyOnWriteFs(s.Assets.Fs, staticFs)
+ return afero.NewCopyOnWriteFs(base, s.Content.Fs)
+
+}
+
+// StaticFs returns the static filesystem for the given language.
+// This can be a composite filesystem.
+func (s SourceFilesystems) StaticFs(lang string) afero.Fs {
+ var staticFs afero.Fs = hugofs.NoOpFs
+
+ if fs, ok := s.Static[lang]; ok {
+ staticFs = fs.Fs
+ } else if fs, ok := s.Static[""]; ok {
+ staticFs = fs.Fs
+ }
+
+ return staticFs
+}
+
+// StatResource looks for a resource in these filesystems in order: static, assets and finally content.
+// If found in any of them, it returns FileInfo and the relevant filesystem.
+// Any non os.IsNotExist error will be returned.
+// An os.IsNotExist error wil be returned only if all filesystems return such an error.
+// Note that if we only wanted to find the file, we could create a composite Afero fs,
+// but we also need to know which filesystem root it lives in.
+func (s SourceFilesystems) StatResource(lang, filename string) (fi os.FileInfo, fs afero.Fs, err error) {
+ for _, fsToCheck := range []afero.Fs{s.StaticFs(lang), s.Assets.Fs, s.Content.Fs} {
+ fs = fsToCheck
+ fi, err = fs.Stat(filename)
+ if err == nil || !os.IsNotExist(err) {
+ return
+ }
+ }
+ // Not found.
+ return
+}
+
+// IsStatic returns true if the given filename is a member of one of the static
+// filesystems.
+func (s SourceFilesystems) IsStatic(filename string) bool {
+ for _, staticFs := range s.Static {
+ if staticFs.Contains(filename) {
+ return true
+ }
+ }
+ return false
+}
+
+// IsContent returns true if the given filename is a member of the content filesystem.
+func (s SourceFilesystems) IsContent(filename string) bool {
+ return s.Content.Contains(filename)
+}
+
+// IsLayout returns true if the given filename is a member of the layouts filesystem.
+func (s SourceFilesystems) IsLayout(filename string) bool {
+ return s.Layouts.Contains(filename)
+}
+
+// IsData returns true if the given filename is a member of the data filesystem.
+func (s SourceFilesystems) IsData(filename string) bool {
+ return s.Data.Contains(filename)
+}
+
+// IsAsset returns true if the given filename is a member of the asset filesystem.
+func (s SourceFilesystems) IsAsset(filename string) bool {
+ return s.Assets.Contains(filename)
+}
+
+// IsI18n returns true if the given filename is a member of the i18n filesystem.
+func (s SourceFilesystems) IsI18n(filename string) bool {
+ return s.I18n.Contains(filename)
+}
+
+// MakeStaticPathRelative makes an absolute static filename into a relative one.
+// It will return an empty string if the filename is not a member of a static filesystem.
+func (s SourceFilesystems) MakeStaticPathRelative(filename string) string {
+ for _, staticFs := range s.Static {
+ rel := staticFs.MakePathRelative(filename)
+ if rel != "" {
+ return rel
+ }
+ }
+ return ""
+}
+
+// MakePathRelative creates a relative path from the given filename.
+// It will return an empty string if the filename is not a member of this filesystem.
+func (d *SourceFilesystem) MakePathRelative(filename string) string {
+ for _, currentPath := range d.Dirnames {
+ if strings.HasPrefix(filename, currentPath) {
+ return strings.TrimPrefix(filename, currentPath)
+ }
+ }
+ return ""
+}
+
+func (d *SourceFilesystem) RealFilename(rel string) string {
+ fi, err := d.Fs.Stat(rel)
+ if err != nil {
+ return rel
+ }
+ if realfi, ok := fi.(hugofs.RealFilenameInfo); ok {
+ return realfi.RealFilename()
+ }
+
+ return rel
+}
+
+// Contains returns whether the given filename is a member of the current filesystem.
+func (d *SourceFilesystem) Contains(filename string) bool {
+ for _, dir := range d.Dirnames {
+ if strings.HasPrefix(filename, dir) {
+ return true
+ }
+ }
+ return false
+}
+
+// RealDirs gets a list of absolute paths to directories starting from the given
+// path.
+func (d *SourceFilesystem) RealDirs(from string) []string {
+ var dirnames []string
+ for _, dir := range d.Dirnames {
+ dirname := filepath.Join(dir, from)
+ if _, err := d.SourceFs.Stat(dirname); err == nil {
+ dirnames = append(dirnames, dirname)
+ }
+ }
+ return dirnames
+}
+
+// WithBaseFs allows reuse of some potentially expensive to create parts that remain
+// the same across sites/languages.
+func WithBaseFs(b *BaseFs) func(*BaseFs) error {
+ return func(bb *BaseFs) error {
+ bb.themeFs = b.themeFs
+ bb.AbsThemeDirs = b.AbsThemeDirs
+ return nil
+ }
+}
+
+func newRealBase(base afero.Fs) afero.Fs {
+ return hugofs.NewBasePathRealFilenameFs(base.(*afero.BasePathFs))
+
+}
+
+// NewBase builds the filesystems used by Hugo given the paths and options provided.NewBase
+func NewBase(p *paths.Paths, options ...func(*BaseFs) error) (*BaseFs, error) {
+ fs := p.Fs
+
+ publishFs := afero.NewBasePathFs(fs.Destination, p.AbsPublishDir)
+
+ contentFs, absContentDirs, err := createContentFs(fs.Source, p.WorkingDir, p.DefaultContentLanguage, p.Languages)
+ if err != nil {
+ return nil, err
+ }
+
+ // Make sure we don't have any overlapping content dirs. That will never work.
+ for i, d1 := range absContentDirs {
+ for j, d2 := range absContentDirs {
+ if i == j {
+ continue
+ }
+ if strings.HasPrefix(d1, d2) || strings.HasPrefix(d2, d1) {
+ return nil, fmt.Errorf("found overlapping content dirs (%q and %q)", d1, d2)
+ }
+ }
+ }
+
+ b := &BaseFs{
+ PublishFs: publishFs,
+ }
+
+ for _, opt := range options {
+ if err := opt(b); err != nil {
+ return nil, err
+ }
+ }
+
+ builder := newSourceFilesystemsBuilder(p, b)
+ sourceFilesystems, err := builder.Build()
+ if err != nil {
+ return nil, err
+ }
+
+ sourceFilesystems.Content = &SourceFilesystem{
+ SourceFs: fs.Source,
+ Fs: contentFs,
+ Dirnames: absContentDirs,
+ }
+
+ b.SourceFilesystems = sourceFilesystems
+ b.themeFs = builder.themeFs
+ b.AbsThemeDirs = builder.absThemeDirs
+
+ return b, nil
+}
+
+type sourceFilesystemsBuilder struct {
+ p *paths.Paths
+ result *SourceFilesystems
+ themeFs afero.Fs
+ hasTheme bool
+ absThemeDirs []string
+}
+
+func newSourceFilesystemsBuilder(p *paths.Paths, b *BaseFs) *sourceFilesystemsBuilder {
+ return &sourceFilesystemsBuilder{p: p, themeFs: b.themeFs, absThemeDirs: b.AbsThemeDirs, result: &SourceFilesystems{}}
+}
+
+func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
+ if b.themeFs == nil && b.p.ThemeSet() {
+ themeFs, absThemeDirs, err := createThemesOverlayFs(b.p)
+ if err != nil {
+ return nil, err
+ }
+ if themeFs == nil {
+ panic("createThemesFs returned nil")
+ }
+ b.themeFs = themeFs
+ b.absThemeDirs = absThemeDirs
+
+ }
+
+ b.hasTheme = len(b.absThemeDirs) > 0
+
+ sfs, err := b.createRootMappingFs("dataDir", "data")
+ if err != nil {
+ return nil, err
+ }
+ b.result.Data = sfs
+
+ sfs, err = b.createRootMappingFs("i18nDir", "i18n")
+ if err != nil {
+ return nil, err
+ }
+ b.result.I18n = sfs
+
+ sfs, err = b.createFs(false, true, "layoutDir", "layouts")
+ if err != nil {
+ return nil, err
+ }
+ b.result.Layouts = sfs
+
+ sfs, err = b.createFs(false, true, "archetypeDir", "archetypes")
+ if err != nil {
+ return nil, err
+ }
+ b.result.Archetypes = sfs
+
+ sfs, err = b.createFs(false, true, "assetDir", "assets")
+ if err != nil {
+ return nil, err
+ }
+ b.result.Assets = sfs
+
+ sfs, err = b.createFs(true, false, "resourceDir", "resources")
+ if err != nil {
+ return nil, err
+ }
+
+ b.result.Resources = sfs
+
+ sfs, err = b.createFs(false, true, "", "")
+ if err != nil {
+ return nil, err
+ }
+ b.result.Work = sfs
+
+ err = b.createStaticFs()
+ if err != nil {
+ return nil, err
+ }
+
+ return b.result, nil
+}
+
+func (b *sourceFilesystemsBuilder) createFs(
+ mkdir bool,
+ readOnly bool,
+ dirKey, themeFolder string) (*SourceFilesystem, error) {
+ s := &SourceFilesystem{
+ SourceFs: b.p.Fs.Source,
+ }
+
+ if themeFolder == "" {
+ themeFolder = filePathSeparator
+ }
+
+ var dir string
+ if dirKey != "" {
+ dir = b.p.Cfg.GetString(dirKey)
+ if dir == "" {
+ return s, fmt.Errorf("config %q not set", dirKey)
+ }
+ }
+
+ var fs afero.Fs
+
+ absDir := b.p.AbsPathify(dir)
+ existsInSource := b.existsInSource(absDir)
+ if !existsInSource && mkdir {
+ // We really need this directory. Make it.
+ if err := b.p.Fs.Source.MkdirAll(absDir, 0777); err == nil {
+ existsInSource = true
+ }
+ }
+ if existsInSource {
+ fs = newRealBase(afero.NewBasePathFs(b.p.Fs.Source, absDir))
+ s.Dirnames = []string{absDir}
+ }
+
+ if b.hasTheme {
+ if !strings.HasPrefix(themeFolder, filePathSeparator) {
+ themeFolder = filePathSeparator + themeFolder
+ }
+ themeFolderFs := newRealBase(afero.NewBasePathFs(b.themeFs, themeFolder))
+ if fs == nil {
+ fs = themeFolderFs
+ } else {
+ fs = afero.NewCopyOnWriteFs(themeFolderFs, fs)
+ }
+
+ for _, absThemeDir := range b.absThemeDirs {
+ absThemeFolderDir := filepath.Join(absThemeDir, themeFolder)
+ if b.existsInSource(absThemeFolderDir) {
+ s.Dirnames = append(s.Dirnames, absThemeFolderDir)
+ }
+ }
+ }
+
+ if fs == nil {
+ s.Fs = hugofs.NoOpFs
+ } else if readOnly {
+ s.Fs = afero.NewReadOnlyFs(fs)
+ } else {
+ s.Fs = fs
+ }
+
+ return s, nil
+}
+
+// Used for data, i18n -- we cannot use overlay filsesystems for those, but we need
+// to keep a strict order.
+func (b *sourceFilesystemsBuilder) createRootMappingFs(dirKey, themeFolder string) (*SourceFilesystem, error) {
+ s := &SourceFilesystem{
+ SourceFs: b.p.Fs.Source,
+ }
+
+ projectDir := b.p.Cfg.GetString(dirKey)
+ if projectDir == "" {
+ return nil, fmt.Errorf("config %q not set", dirKey)
+ }
+
+ var fromTo []string
+ to := b.p.AbsPathify(projectDir)
+
+ if b.existsInSource(to) {
+ s.Dirnames = []string{to}
+ fromTo = []string{projectVirtualFolder, to}
+ }
+
+ for _, theme := range b.p.AllThemes {
+ to := b.p.AbsPathify(filepath.Join(b.p.ThemesDir, theme.Name, themeFolder))
+ if b.existsInSource(to) {
+ s.Dirnames = append(s.Dirnames, to)
+ from := theme
+ fromTo = append(fromTo, from.Name, to)
+ }
+ }
+
+ if len(fromTo) == 0 {
+ s.Fs = hugofs.NoOpFs
+ return s, nil
+ }
+
+ fs, err := hugofs.NewRootMappingFs(b.p.Fs.Source, fromTo...)
+ if err != nil {
+ return nil, err
+ }
+
+ s.Fs = afero.NewReadOnlyFs(fs)
+
+ return s, nil
+}
+
+func (b *sourceFilesystemsBuilder) existsInSource(abspath string) bool {
+ exists, _ := afero.Exists(b.p.Fs.Source, abspath)
+ return exists
+}
+
+func (b *sourceFilesystemsBuilder) createStaticFs() error {
+ isMultihost := b.p.Cfg.GetBool("multihost")
+ ms := make(map[string]*SourceFilesystem)
+ b.result.Static = ms
+
+ if isMultihost {
+ for _, l := range b.p.Languages {
+ s := &SourceFilesystem{
+ SourceFs: b.p.Fs.Source,
+ PublishFolder: l.Lang}
+ staticDirs := removeDuplicatesKeepRight(getStaticDirs(l))
+ if len(staticDirs) == 0 {
+ continue
+ }
+
+ for _, dir := range staticDirs {
+ absDir := b.p.AbsPathify(dir)
+ if !b.existsInSource(absDir) {
+ continue
+ }
+
+ s.Dirnames = append(s.Dirnames, absDir)
+ }
+
+ fs, err := createOverlayFs(b.p.Fs.Source, s.Dirnames)
+ if err != nil {
+ return err
+ }
+
+ if b.hasTheme {
+ themeFolder := "static"
+ fs = afero.NewCopyOnWriteFs(newRealBase(afero.NewBasePathFs(b.themeFs, themeFolder)), fs)
+ for _, absThemeDir := range b.absThemeDirs {
+ s.Dirnames = append(s.Dirnames, filepath.Join(absThemeDir, themeFolder))
+ }
+ }
+
+ s.Fs = fs
+ ms[l.Lang] = s
+
+ }
+
+ return nil
+ }
+
+ s := &SourceFilesystem{
+ SourceFs: b.p.Fs.Source,
+ }
+
+ var staticDirs []string
+
+ for _, l := range b.p.Languages {
+ staticDirs = append(staticDirs, getStaticDirs(l)...)
+ }
+
+ staticDirs = removeDuplicatesKeepRight(staticDirs)
+ if len(staticDirs) == 0 {
+ return nil
+ }
+
+ for _, dir := range staticDirs {
+ absDir := b.p.AbsPathify(dir)
+ if !b.existsInSource(absDir) {
+ continue
+ }
+ s.Dirnames = append(s.Dirnames, absDir)
+ }
+
+ fs, err := createOverlayFs(b.p.Fs.Source, s.Dirnames)
+ if err != nil {
+ return err
+ }
+
+ if b.hasTheme {
+ themeFolder := "static"
+ fs = afero.NewCopyOnWriteFs(newRealBase(afero.NewBasePathFs(b.themeFs, themeFolder)), fs)
+ for _, absThemeDir := range b.absThemeDirs {
+ s.Dirnames = append(s.Dirnames, filepath.Join(absThemeDir, themeFolder))
+ }
+ }
+
+ s.Fs = fs
+ ms[""] = s
+
+ return nil
+}
+
+func getStaticDirs(cfg config.Provider) []string {
+ var staticDirs []string
+ for i := -1; i <= 10; i++ {
+ staticDirs = append(staticDirs, getStringOrStringSlice(cfg, "staticDir", i)...)
+ }
+ return staticDirs
+}
+
+func getStringOrStringSlice(cfg config.Provider, key string, id int) []string {
+
+ if id >= 0 {
+ key = fmt.Sprintf("%s%d", key, id)
+ }
+
+ return config.GetStringSlicePreserveString(cfg, key)
+
+}
+
+func createContentFs(fs afero.Fs,
+ workingDir,
+ defaultContentLanguage string,
+ languages langs.Languages) (afero.Fs, []string, error) {
+
+ var contentLanguages langs.Languages
+ var contentDirSeen = make(map[string]bool)
+ languageSet := make(map[string]bool)
+
+ // The default content language needs to be first.
+ for _, language := range languages {
+ if language.Lang == defaultContentLanguage {
+ contentLanguages = append(contentLanguages, language)
+ contentDirSeen[language.ContentDir] = true
+ }
+ languageSet[language.Lang] = true
+ }
+
+ for _, language := range languages {
+ if contentDirSeen[language.ContentDir] {
+ continue
+ }
+ if language.ContentDir == "" {
+ language.ContentDir = defaultContentLanguage
+ }
+ contentDirSeen[language.ContentDir] = true
+ contentLanguages = append(contentLanguages, language)
+
+ }
+
+ var absContentDirs []string
+
+ fs, err := createContentOverlayFs(fs, workingDir, contentLanguages, languageSet, &absContentDirs)
+ return fs, absContentDirs, err
+
+}
+
+func createContentOverlayFs(source afero.Fs,
+ workingDir string,
+ languages langs.Languages,
+ languageSet map[string]bool,
+ absContentDirs *[]string) (afero.Fs, error) {
+ if len(languages) == 0 {
+ return source, nil
+ }
+
+ language := languages[0]
+
+ contentDir := language.ContentDir
+ if contentDir == "" {
+ panic("missing contentDir")
+ }
+
+ absContentDir := paths.AbsPathify(workingDir, language.ContentDir)
+ if !strings.HasSuffix(absContentDir, paths.FilePathSeparator) {
+ absContentDir += paths.FilePathSeparator
+ }
+
+ // If root, remove the second '/'
+ if absContentDir == "//" {
+ absContentDir = paths.FilePathSeparator
+ }
+
+ if len(absContentDir) < 6 {
+ return nil, fmt.Errorf("invalid content dir %q: Path is too short", absContentDir)
+ }
+
+ *absContentDirs = append(*absContentDirs, absContentDir)
+
+ overlay := hugofs.NewLanguageFs(language.Lang, languageSet, afero.NewBasePathFs(source, absContentDir))
+ if len(languages) == 1 {
+ return overlay, nil
+ }
+
+ base, err := createContentOverlayFs(source, workingDir, languages[1:], languageSet, absContentDirs)
+ if err != nil {
+ return nil, err
+ }
+
+ return hugofs.NewLanguageCompositeFs(base, overlay), nil
+
+}
+
+func createThemesOverlayFs(p *paths.Paths) (afero.Fs, []string, error) {
+
+ themes := p.AllThemes
+
+ if len(themes) == 0 {
+ panic("AllThemes not set")
+ }
+
+ themesDir := p.AbsPathify(p.ThemesDir)
+ if themesDir == "" {
+ return nil, nil, errors.New("no themes dir set")
+ }
+
+ absPaths := make([]string, len(themes))
+
+ // The themes are ordered from left to right. We need to revert it to get the
+ // overlay logic below working as expected.
+ for i := 0; i < len(themes); i++ {
+ absPaths[i] = filepath.Join(themesDir, themes[len(themes)-1-i].Name)
+ }
+
+ fs, err := createOverlayFs(p.Fs.Source, absPaths)
+ fs = hugofs.NewNoLstatFs(fs)
+
+ return fs, absPaths, err
+
+}
+
+func createOverlayFs(source afero.Fs, absPaths []string) (afero.Fs, error) {
+ if len(absPaths) == 0 {
+ return hugofs.NoOpFs, nil
+ }
+
+ if len(absPaths) == 1 {
+ return afero.NewReadOnlyFs(newRealBase(afero.NewBasePathFs(source, absPaths[0]))), nil
+ }
+
+ base := afero.NewReadOnlyFs(newRealBase(afero.NewBasePathFs(source, absPaths[0])))
+ overlay, err := createOverlayFs(source, absPaths[1:])
+ if err != nil {
+ return nil, err
+ }
+
+ return afero.NewCopyOnWriteFs(base, overlay), nil
+}
+
+func removeDuplicatesKeepRight(in []string) []string {
+ seen := make(map[string]bool)
+ var out []string
+ for i := len(in) - 1; i >= 0; i-- {
+ v := in[i]
+ if seen[v] {
+ continue
+ }
+ out = append([]string{v}, out...)
+ seen[v] = true
+ }
+
+ return out
+}
diff --git a/hugolib/filesystems/basefs_test.go b/hugolib/filesystems/basefs_test.go
new file mode 100644
index 000000000..ec6ccb30c
--- /dev/null
+++ b/hugolib/filesystems/basefs_test.go
@@ -0,0 +1,363 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package filesystems
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/gohugoio/hugo/langs"
+
+ "github.com/spf13/afero"
+
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/hugolib/paths"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/require"
+)
+
+func TestNewBaseFs(t *testing.T) {
+ assert := require.New(t)
+ v := viper.New()
+
+ fs := hugofs.NewMem(v)
+
+ themes := []string{"btheme", "atheme"}
+
+ workingDir := filepath.FromSlash("/my/work")
+ v.Set("workingDir", workingDir)
+ v.Set("themesDir", "themes")
+ v.Set("theme", themes[:1])
+
+ // Write some data to the themes
+ for _, theme := range themes {
+ for _, dir := range []string{"i18n", "data", "archetypes", "layouts"} {
+ base := filepath.Join(workingDir, "themes", theme, dir)
+ filename := filepath.Join(base, fmt.Sprintf("theme-file-%s.txt", theme))
+ fs.Source.Mkdir(base, 0755)
+ afero.WriteFile(fs.Source, filename, []byte(fmt.Sprintf("content:%s:%s", theme, dir)), 0755)
+ }
+ // Write some files to the root of the theme
+ base := filepath.Join(workingDir, "themes", theme)
+ afero.WriteFile(fs.Source, filepath.Join(base, fmt.Sprintf("theme-root-%s.txt", theme)), []byte(fmt.Sprintf("content:%s", theme)), 0755)
+ afero.WriteFile(fs.Source, filepath.Join(base, "file-theme-root.txt"), []byte(fmt.Sprintf("content:%s", theme)), 0755)
+ }
+
+ afero.WriteFile(fs.Source, filepath.Join(workingDir, "file-root.txt"), []byte("content-project"), 0755)
+
+ afero.WriteFile(fs.Source, filepath.Join(workingDir, "themes", "btheme", "config.toml"), []byte(`
+theme = ["atheme"]
+`), 0755)
+
+ setConfigAndWriteSomeFilesTo(fs.Source, v, "contentDir", "mycontent", 3)
+ setConfigAndWriteSomeFilesTo(fs.Source, v, "i18nDir", "myi18n", 4)
+ setConfigAndWriteSomeFilesTo(fs.Source, v, "layoutDir", "mylayouts", 5)
+ setConfigAndWriteSomeFilesTo(fs.Source, v, "staticDir", "mystatic", 6)
+ setConfigAndWriteSomeFilesTo(fs.Source, v, "dataDir", "mydata", 7)
+ setConfigAndWriteSomeFilesTo(fs.Source, v, "archetypeDir", "myarchetypes", 8)
+ setConfigAndWriteSomeFilesTo(fs.Source, v, "assetDir", "myassets", 9)
+ setConfigAndWriteSomeFilesTo(fs.Source, v, "resourceDir", "myrsesource", 10)
+
+ v.Set("publishDir", "public")
+
+ p, err := paths.New(fs, v)
+ assert.NoError(err)
+
+ bfs, err := NewBase(p)
+ assert.NoError(err)
+ assert.NotNil(bfs)
+
+ root, err := bfs.I18n.Fs.Open("")
+ assert.NoError(err)
+ dirnames, err := root.Readdirnames(-1)
+ assert.NoError(err)
+ assert.Equal([]string{projectVirtualFolder, "btheme", "atheme"}, dirnames)
+ ff, err := bfs.I18n.Fs.Open("myi18n")
+ assert.NoError(err)
+ _, err = ff.Readdirnames(-1)
+ assert.NoError(err)
+
+ root, err = bfs.Data.Fs.Open("")
+ assert.NoError(err)
+ dirnames, err = root.Readdirnames(-1)
+ assert.NoError(err)
+ assert.Equal([]string{projectVirtualFolder, "btheme", "atheme"}, dirnames)
+ ff, err = bfs.I18n.Fs.Open("mydata")
+ assert.NoError(err)
+ _, err = ff.Readdirnames(-1)
+ assert.NoError(err)
+
+ checkFileCount(bfs.Content.Fs, "", assert, 3)
+ checkFileCount(bfs.I18n.Fs, "", assert, 6) // 4 + 2 themes
+ checkFileCount(bfs.Layouts.Fs, "", assert, 7)
+ checkFileCount(bfs.Static[""].Fs, "", assert, 6)
+ checkFileCount(bfs.Data.Fs, "", assert, 9) // 7 + 2 themes
+ checkFileCount(bfs.Archetypes.Fs, "", assert, 10) // 8 + 2 themes
+ checkFileCount(bfs.Assets.Fs, "", assert, 9)
+ checkFileCount(bfs.Resources.Fs, "", assert, 10)
+ checkFileCount(bfs.Work.Fs, "", assert, 78)
+
+ assert.Equal([]string{filepath.FromSlash("/my/work/mydata"), filepath.FromSlash("/my/work/themes/btheme/data"), filepath.FromSlash("/my/work/themes/atheme/data")}, bfs.Data.Dirnames)
+
+ assert.True(bfs.IsData(filepath.Join(workingDir, "mydata", "file1.txt")))
+ assert.True(bfs.IsI18n(filepath.Join(workingDir, "myi18n", "file1.txt")))
+ assert.True(bfs.IsLayout(filepath.Join(workingDir, "mylayouts", "file1.txt")))
+ assert.True(bfs.IsStatic(filepath.Join(workingDir, "mystatic", "file1.txt")))
+ assert.True(bfs.IsAsset(filepath.Join(workingDir, "myassets", "file1.txt")))
+
+ contentFilename := filepath.Join(workingDir, "mycontent", "file1.txt")
+ assert.True(bfs.IsContent(contentFilename))
+ rel := bfs.RelContentDir(contentFilename)
+ assert.Equal("file1.txt", rel)
+
+ // Check Work fs vs theme
+ checkFileContent(bfs.Work.Fs, "file-root.txt", assert, "content-project")
+ checkFileContent(bfs.Work.Fs, "theme-root-atheme.txt", assert, "content:atheme")
+
+ // https://github.com/gohugoio/hugo/issues/5318
+ // Check both project and theme.
+ for _, fs := range []afero.Fs{bfs.Archetypes.Fs, bfs.Layouts.Fs} {
+ for _, filename := range []string{"/file1.txt", "/theme-file-atheme.txt"} {
+ filename = filepath.FromSlash(filename)
+ f, err := fs.Open(filename)
+ assert.NoError(err)
+ name := f.Name()
+ f.Close()
+ assert.Equal(filename, name)
+ }
+ }
+}
+
+func createConfig() *viper.Viper {
+ v := viper.New()
+ v.Set("contentDir", "mycontent")
+ v.Set("i18nDir", "myi18n")
+ v.Set("staticDir", "mystatic")
+ v.Set("dataDir", "mydata")
+ v.Set("layoutDir", "mylayouts")
+ v.Set("archetypeDir", "myarchetypes")
+ v.Set("assetDir", "myassets")
+ v.Set("resourceDir", "resources")
+ v.Set("publishDir", "public")
+
+ return v
+}
+
+func TestNewBaseFsEmpty(t *testing.T) {
+ assert := require.New(t)
+ v := createConfig()
+ fs := hugofs.NewMem(v)
+ p, err := paths.New(fs, v)
+ assert.NoError(err)
+ bfs, err := NewBase(p)
+ assert.NoError(err)
+ assert.NotNil(bfs)
+ assert.Equal(hugofs.NoOpFs, bfs.Archetypes.Fs)
+ assert.Equal(hugofs.NoOpFs, bfs.Layouts.Fs)
+ assert.Equal(hugofs.NoOpFs, bfs.Data.Fs)
+ assert.Equal(hugofs.NoOpFs, bfs.Assets.Fs)
+ assert.Equal(hugofs.NoOpFs, bfs.I18n.Fs)
+ assert.NotNil(bfs.Work.Fs)
+ assert.NotNil(bfs.Content.Fs)
+ assert.NotNil(bfs.Static)
+}
+
+func TestRealDirs(t *testing.T) {
+ assert := require.New(t)
+ v := createConfig()
+ fs := hugofs.NewDefault(v)
+ sfs := fs.Source
+
+ root, err := afero.TempDir(sfs, "", "realdir")
+ assert.NoError(err)
+ themesDir, err := afero.TempDir(sfs, "", "themesDir")
+ assert.NoError(err)
+ defer func() {
+ os.RemoveAll(root)
+ os.RemoveAll(themesDir)
+ }()
+
+ v.Set("workingDir", root)
+ v.Set("themesDir", themesDir)
+ v.Set("theme", "mytheme")
+
+ assert.NoError(sfs.MkdirAll(filepath.Join(root, "myassets", "scss", "sf1"), 0755))
+ assert.NoError(sfs.MkdirAll(filepath.Join(root, "myassets", "scss", "sf2"), 0755))
+ assert.NoError(sfs.MkdirAll(filepath.Join(themesDir, "mytheme", "assets", "scss", "sf2"), 0755))
+ assert.NoError(sfs.MkdirAll(filepath.Join(themesDir, "mytheme", "assets", "scss", "sf3"), 0755))
+ assert.NoError(sfs.MkdirAll(filepath.Join(root, "resources"), 0755))
+ assert.NoError(sfs.MkdirAll(filepath.Join(themesDir, "mytheme", "resources"), 0755))
+
+ assert.NoError(sfs.MkdirAll(filepath.Join(root, "myassets", "js", "f2"), 0755))
+
+ afero.WriteFile(sfs, filepath.Join(filepath.Join(root, "myassets", "scss", "sf1", "a1.scss")), []byte("content"), 0755)
+ afero.WriteFile(sfs, filepath.Join(filepath.Join(root, "myassets", "scss", "sf2", "a3.scss")), []byte("content"), 0755)
+ afero.WriteFile(sfs, filepath.Join(filepath.Join(root, "myassets", "scss", "a2.scss")), []byte("content"), 0755)
+ afero.WriteFile(sfs, filepath.Join(filepath.Join(themesDir, "mytheme", "assets", "scss", "sf2", "a3.scss")), []byte("content"), 0755)
+ afero.WriteFile(sfs, filepath.Join(filepath.Join(themesDir, "mytheme", "assets", "scss", "sf3", "a4.scss")), []byte("content"), 0755)
+
+ afero.WriteFile(sfs, filepath.Join(filepath.Join(themesDir, "mytheme", "resources", "t1.txt")), []byte("content"), 0755)
+ afero.WriteFile(sfs, filepath.Join(filepath.Join(root, "resources", "p1.txt")), []byte("content"), 0755)
+ afero.WriteFile(sfs, filepath.Join(filepath.Join(root, "resources", "p2.txt")), []byte("content"), 0755)
+
+ afero.WriteFile(sfs, filepath.Join(filepath.Join(root, "myassets", "js", "f2", "a1.js")), []byte("content"), 0755)
+ afero.WriteFile(sfs, filepath.Join(filepath.Join(root, "myassets", "js", "a2.js")), []byte("content"), 0755)
+
+ p, err := paths.New(fs, v)
+ assert.NoError(err)
+ bfs, err := NewBase(p)
+ assert.NoError(err)
+ assert.NotNil(bfs)
+ checkFileCount(bfs.Assets.Fs, "", assert, 6)
+
+ realDirs := bfs.Assets.RealDirs("scss")
+ assert.Equal(2, len(realDirs))
+ assert.Equal(filepath.Join(root, "myassets/scss"), realDirs[0])
+ assert.Equal(filepath.Join(themesDir, "mytheme/assets/scss"), realDirs[len(realDirs)-1])
+
+ checkFileCount(bfs.Resources.Fs, "", assert, 3)
+
+ assert.NotNil(bfs.themeFs)
+ fi, b, err := bfs.themeFs.(afero.Lstater).LstatIfPossible(filepath.Join("resources", "t1.txt"))
+ assert.NoError(err)
+ assert.False(b)
+ assert.Equal("t1.txt", fi.Name())
+
+}
+
+func TestStaticFs(t *testing.T) {
+ assert := require.New(t)
+ v := createConfig()
+ workDir := "mywork"
+ v.Set("workingDir", workDir)
+ v.Set("themesDir", "themes")
+ v.Set("theme", "t1")
+
+ fs := hugofs.NewMem(v)
+
+ themeStaticDir := filepath.Join(workDir, "themes", "t1", "static")
+
+ afero.WriteFile(fs.Source, filepath.Join(workDir, "mystatic", "f1.txt"), []byte("Hugo Rocks!"), 0755)
+ afero.WriteFile(fs.Source, filepath.Join(themeStaticDir, "f1.txt"), []byte("Hugo Themes Rocks!"), 0755)
+ afero.WriteFile(fs.Source, filepath.Join(themeStaticDir, "f2.txt"), []byte("Hugo Themes Still Rocks!"), 0755)
+
+ p, err := paths.New(fs, v)
+ assert.NoError(err)
+ bfs, err := NewBase(p)
+ assert.NoError(err)
+ sfs := bfs.StaticFs("en")
+ checkFileContent(sfs, "f1.txt", assert, "Hugo Rocks!")
+ checkFileContent(sfs, "f2.txt", assert, "Hugo Themes Still Rocks!")
+
+}
+
+func TestStaticFsMultiHost(t *testing.T) {
+ assert := require.New(t)
+ v := createConfig()
+ workDir := "mywork"
+ v.Set("workingDir", workDir)
+ v.Set("themesDir", "themes")
+ v.Set("theme", "t1")
+ v.Set("multihost", true)
+
+ vn := viper.New()
+ vn.Set("staticDir", "nn_static")
+
+ en := langs.NewLanguage("en", v)
+ no := langs.NewLanguage("no", v)
+ no.Set("staticDir", "static_no")
+
+ languages := langs.Languages{
+ en,
+ no,
+ }
+
+ v.Set("languagesSorted", languages)
+
+ fs := hugofs.NewMem(v)
+
+ themeStaticDir := filepath.Join(workDir, "themes", "t1", "static")
+
+ afero.WriteFile(fs.Source, filepath.Join(workDir, "mystatic", "f1.txt"), []byte("Hugo Rocks!"), 0755)
+ afero.WriteFile(fs.Source, filepath.Join(workDir, "static_no", "f1.txt"), []byte("Hugo Rocks in Norway!"), 0755)
+
+ afero.WriteFile(fs.Source, filepath.Join(themeStaticDir, "f1.txt"), []byte("Hugo Themes Rocks!"), 0755)
+ afero.WriteFile(fs.Source, filepath.Join(themeStaticDir, "f2.txt"), []byte("Hugo Themes Still Rocks!"), 0755)
+
+ p, err := paths.New(fs, v)
+ assert.NoError(err)
+ bfs, err := NewBase(p)
+ assert.NoError(err)
+ enFs := bfs.StaticFs("en")
+ checkFileContent(enFs, "f1.txt", assert, "Hugo Rocks!")
+ checkFileContent(enFs, "f2.txt", assert, "Hugo Themes Still Rocks!")
+
+ noFs := bfs.StaticFs("no")
+ checkFileContent(noFs, "f1.txt", assert, "Hugo Rocks in Norway!")
+ checkFileContent(noFs, "f2.txt", assert, "Hugo Themes Still Rocks!")
+}
+
+func checkFileCount(fs afero.Fs, dirname string, assert *require.Assertions, expected int) {
+ count, _, err := countFileaAndGetDirs(fs, dirname)
+ assert.NoError(err)
+ assert.Equal(expected, count)
+}
+
+func checkFileContent(fs afero.Fs, filename string, assert *require.Assertions, expected ...string) {
+
+ b, err := afero.ReadFile(fs, filename)
+ assert.NoError(err)
+
+ content := string(b)
+
+ for _, e := range expected {
+ assert.Contains(content, e)
+ }
+}
+
+func countFileaAndGetDirs(fs afero.Fs, dirname string) (int, []string, error) {
+ if fs == nil {
+ return 0, nil, errors.New("no fs")
+ }
+
+ counter := 0
+ var dirs []string
+
+ afero.Walk(fs, dirname, func(path string, info os.FileInfo, err error) error {
+ if info != nil {
+ if !info.IsDir() {
+ counter++
+ } else if info.Name() != "." {
+ dirs = append(dirs, filepath.Join(path, info.Name()))
+ }
+ }
+
+ return nil
+ })
+
+ return counter, dirs, nil
+}
+
+func setConfigAndWriteSomeFilesTo(fs afero.Fs, v *viper.Viper, key, val string, num int) {
+ workingDir := v.GetString("workingDir")
+ v.Set(key, val)
+ fs.Mkdir(val, 0755)
+ for i := 0; i < num; i++ {
+ filename := filepath.Join(workingDir, val, fmt.Sprintf("file%d.txt", i+1))
+ afero.WriteFile(fs, filename, []byte(fmt.Sprintf("content:%s:%d", key, i+1)), 0755)
+ }
+}
diff --git a/hugolib/gitinfo.go b/hugolib/gitinfo.go
new file mode 100644
index 000000000..6acc47d17
--- /dev/null
+++ b/hugolib/gitinfo.go
@@ -0,0 +1,47 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+
+ "github.com/bep/gitmap"
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/resources/page"
+)
+
+type gitInfo struct {
+ contentDir string
+ repo *gitmap.GitRepo
+}
+
+func (g *gitInfo) forPage(p page.Page) *gitmap.GitInfo {
+ name := strings.TrimPrefix(filepath.ToSlash(p.File().Filename()), g.contentDir)
+ name = strings.TrimPrefix(name, "/")
+
+ return g.repo.Files[name]
+
+}
+
+func newGitInfo(cfg config.Provider) (*gitInfo, error) {
+ workingDir := cfg.GetString("workingDir")
+
+ gitRepo, err := gitmap.Map(workingDir, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return &gitInfo{contentDir: gitRepo.TopLevelAbsPath, repo: gitRepo}, nil
+}
diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go
new file mode 100644
index 000000000..e852e7f1d
--- /dev/null
+++ b/hugolib/hugo_sites.go
@@ -0,0 +1,1048 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "io"
+ "path"
+ "path/filepath"
+ "sort"
+ "strings"
+ "sync"
+
+ "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/parser/metadecoders"
+
+ "github.com/gohugoio/hugo/hugofs"
+
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/source"
+
+ "github.com/bep/gitmap"
+ "github.com/gohugoio/hugo/config"
+ "github.com/spf13/afero"
+
+ "github.com/gohugoio/hugo/publisher"
+
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/langs"
+ "github.com/gohugoio/hugo/lazy"
+
+ "github.com/gohugoio/hugo/langs/i18n"
+ "github.com/gohugoio/hugo/resources/page"
+ "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
+
+ multilingual *Multilingual
+
+ // Multihost is set if multilingual and baseURL set on the language level.
+ multihost bool
+
+ // If this is running in the dev server.
+ running bool
+
+ // Serializes rebuilds when server is running.
+ runningMu sync.Mutex
+
+ // Render output formats for all sites.
+ renderFormats output.Formats
+
+ *deps.Deps
+
+ gitInfo *gitInfo
+
+ // As loaded from the /data dirs
+ data map[string]interface{}
+
+ // Keeps track of bundle directories and symlinks to enable partial rebuilding.
+ ContentChanges *contentChangeMap
+
+ init *hugoSitesInit
+
+ *fatalErrorHandler
+}
+
+type fatalErrorHandler struct {
+ mu sync.Mutex
+
+ h *HugoSites
+
+ err error
+
+ done bool
+ donec chan bool // will be closed when done
+}
+
+// FatalError error is used in some rare situations where it does not make sense to
+// continue processing, to abort as soon as possible and log the error.
+func (f *fatalErrorHandler) FatalError(err error) {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+ if !f.done {
+ f.done = true
+ close(f.donec)
+ }
+ f.err = err
+}
+
+func (f *fatalErrorHandler) getErr() error {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+ return f.err
+}
+
+func (f *fatalErrorHandler) Done() <-chan bool {
+ return f.donec
+}
+
+type hugoSitesInit struct {
+ // Loads the data from all of the /data folders.
+ data *lazy.Init
+
+ // Loads the Git info for all the pages if enabled.
+ gitInfo *lazy.Init
+
+ // Maps page translations.
+ translations *lazy.Init
+}
+
+func (h *hugoSitesInit) Reset() {
+ h.data.Reset()
+ h.gitInfo.Reset()
+ h.translations.Reset()
+}
+
+func (h *HugoSites) Data() map[string]interface{} {
+ if _, err := h.init.data.Do(); err != nil {
+ h.SendError(errors.Wrap(err, "failed to load data"))
+ return nil
+ }
+ return h.data
+}
+
+func (h *HugoSites) gitInfoForPage(p page.Page) (*gitmap.GitInfo, error) {
+ if _, err := h.init.gitInfo.Do(); err != nil {
+ return nil, err
+ }
+
+ if h.gitInfo == nil {
+ return nil, nil
+ }
+
+ return h.gitInfo.forPage(p), nil
+}
+
+func (h *HugoSites) siteInfos() page.Sites {
+ infos := make(page.Sites, len(h.Sites))
+ for i, site := range h.Sites {
+ infos[i] = &site.Info
+ }
+ return infos
+}
+
+func (h *HugoSites) pickOneAndLogTheRest(errors []error) error {
+ if len(errors) == 0 {
+ return nil
+ }
+
+ var i int
+
+ for j, err := range errors {
+ // If this is in server mode, we want to return an error to the client
+ // with a file context, if possible.
+ if herrors.UnwrapErrorWithFileContext(err) != nil {
+ i = j
+ break
+ }
+ }
+
+ // Log the rest, but add a threshold to avoid flooding the log.
+ const errLogThreshold = 5
+
+ for j, err := range errors {
+ if j == i || err == nil {
+ continue
+ }
+
+ if j >= errLogThreshold {
+ break
+ }
+
+ h.Log.ERROR.Println(err)
+ }
+
+ return errors[i]
+}
+
+func (h *HugoSites) IsMultihost() bool {
+ return h != nil && h.multihost
+}
+
+func (h *HugoSites) LanguageSet() map[string]bool {
+ set := make(map[string]bool)
+ for _, s := range h.Sites {
+ set[s.language.Lang] = true
+ }
+ return set
+}
+
+func (h *HugoSites) NumLogErrors() int {
+ if h == nil {
+ return 0
+ }
+ return int(h.Log.ErrorCounter.Count())
+}
+
+func (h *HugoSites) PrintProcessingStats(w io.Writer) {
+ stats := make([]*helpers.ProcessingStats, len(h.Sites))
+ for i := 0; i < len(h.Sites); i++ {
+ stats[i] = h.Sites[i].PathSpec.ProcessingStats
+ }
+ helpers.ProcessingStatsTable(w, stats...)
+}
+
+func (h *HugoSites) langSite() map[string]*Site {
+ m := make(map[string]*Site)
+ for _, s := range h.Sites {
+ m[s.language.Lang] = s
+ }
+ return m
+}
+
+// GetContentPage finds a Page with content given the absolute filename.
+// Returns nil if none found.
+func (h *HugoSites) GetContentPage(filename string) page.Page {
+ for _, s := range h.Sites {
+ pos := s.rawAllPages.findPagePosByFilename(filename)
+ if pos == -1 {
+ continue
+ }
+ return s.rawAllPages[pos]
+ }
+
+ // If not found already, this may be bundled in another content file.
+ dir := filepath.Dir(filename)
+
+ for _, s := range h.Sites {
+ pos := s.rawAllPages.findPagePosByFilnamePrefix(dir)
+ if pos == -1 {
+ continue
+ }
+ return s.rawAllPages[pos]
+ }
+ return nil
+}
+
+// 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
+ }
+
+ var contentChangeTracker *contentChangeMap
+
+ h := &HugoSites{
+ running: cfg.Running,
+ multilingual: langConfig,
+ multihost: cfg.Cfg.GetBool("multihost"),
+ Sites: sites,
+ init: &hugoSitesInit{
+ data: lazy.New(),
+ gitInfo: lazy.New(),
+ translations: lazy.New(),
+ },
+ }
+
+ h.fatalErrorHandler = &fatalErrorHandler{
+ h: h,
+ donec: make(chan bool),
+ }
+
+ h.init.data.Add(func() (interface{}, error) {
+ err := h.loadData(h.PathSpec.BaseFs.Data.Fs)
+ return err, nil
+ })
+
+ h.init.translations.Add(func() (interface{}, error) {
+ if len(h.Sites) > 1 {
+ allTranslations := pagesToTranslationsMap(h.Sites)
+ assignTranslationsToPages(allTranslations, h.Sites)
+ }
+
+ return nil, nil
+ })
+
+ h.init.gitInfo.Add(func() (interface{}, error) {
+ err := h.loadGitInfo()
+ return nil, err
+ })
+
+ for _, s := range sites {
+ s.h = h
+ }
+
+ if err := applyDeps(cfg, sites...); err != nil {
+ return nil, err
+ }
+
+ h.Deps = sites[0].Deps
+
+ // Only needed in server mode.
+ // TODO(bep) clean up the running vs watching terms
+ if cfg.Running {
+ contentChangeTracker = &contentChangeMap{pathSpec: h.PathSpec, symContent: make(map[string]map[string]bool)}
+ h.ContentChanges = contentChangeTracker
+ }
+
+ return h, nil
+}
+
+func (h *HugoSites) loadGitInfo() error {
+ if h.Cfg.GetBool("enableGitInfo") {
+ gi, err := newGitInfo(h.Cfg)
+ if err != nil {
+ h.Log.ERROR.Println("Failed to read Git log:", err)
+ } else {
+ h.gitInfo = gi
+ }
+ }
+ return nil
+}
+
+func applyDeps(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
+ }
+
+ onCreated := func(d *deps.Deps) error {
+ s.Deps = d
+
+ // Set up the main publishing chain.
+ s.publisher = publisher.NewDestinationPublisher(d.PathSpec.BaseFs.PublishFs, s.outputFormatsConfig, s.mediaTypesConfig, cfg.Cfg.GetBool("minify"))
+
+ if err := s.initializeSiteInfo(); err != nil {
+ return err
+ }
+
+ d.Site = &s.Info
+
+ siteConfig, err := loadSiteConfig(s.language)
+ if err != nil {
+ return err
+ }
+ s.siteConfigConfig = siteConfig
+ s.siteRefLinker, err = newSiteRefLinker(s.language, s)
+ return err
+ }
+
+ cfg.Language = s.language
+ cfg.MediaTypes = s.mediaTypesConfig
+ cfg.OutputFormats = s.outputFormatsConfig
+
+ if d == nil {
+ cfg.WithTemplate = s.withSiteTemplates(cfg.WithTemplate)
+
+ var err error
+ d, err = deps.New(cfg)
+ if err != nil {
+ return err
+ }
+
+ d.OutputFormatsConfig = s.outputFormatsConfig
+
+ if err := onCreated(d); err != nil {
+ return err
+ }
+
+ if err = d.LoadResources(); err != nil {
+ return err
+ }
+
+ } else {
+ d, err = d.ForLanguage(cfg, onCreated)
+ if err != nil {
+ return err
+ }
+ d.OutputFormatsConfig = s.outputFormatsConfig
+ }
+
+ }
+
+ 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 {
+ if err := templ.LoadTemplates(""); err != nil {
+ return err
+ }
+
+ 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
+ )
+
+ languages := getLanguages(cfg.Cfg)
+
+ for _, lang := range languages {
+ if lang.Disabled {
+ continue
+ }
+ 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 etc., making it ready for a full rebuild.
+func (h *HugoSites) reset(config *BuildCfg) {
+ if config.ResetState {
+ for i, s := range h.Sites {
+ h.Sites[i] = s.reset()
+ if r, ok := s.Fs.Destination.(hugofs.Reseter); ok {
+ r.Reset()
+ }
+ }
+ }
+
+ h.fatalErrorHandler = &fatalErrorHandler{
+ h: h,
+ donec: make(chan bool),
+ }
+
+ h.init.Reset()
+}
+
+// resetLogs resets the log counters etc. Used to do a new build on the same sites.
+func (h *HugoSites) resetLogs() {
+ h.Log.Reset()
+ loggers.GlobalErrorCounter.Reset()
+ for _, s := range h.Sites {
+ s.Deps.DistinctErrorLog = helpers.NewDistinctLogger(h.Log.ERROR)
+ }
+}
+
+func (h *HugoSites) createSitesFromConfig(cfg config.Provider) error {
+ oldLangs, _ := h.Cfg.Get("languagesSorted").(langs.Languages)
+
+ if err := loadLanguageSettings(h.Cfg, oldLangs); err != nil {
+ return err
+ }
+
+ depsCfg := deps.DepsCfg{Fs: h.Fs, Cfg: 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.h = h
+ }
+
+ if err := applyDeps(depsCfg, sites...); err != nil {
+ return err
+ }
+
+ h.Deps = sites[0].Deps
+
+ h.multilingual = langConfig
+ h.multihost = h.Deps.Cfg.GetBool("multihost")
+
+ 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 {
+ // Reset site state before build. Use to force full rebuilds.
+ ResetState bool
+ // If set, we re-create the sites from the given configuration before a build.
+ // This is needed if new languages are added.
+ NewConfig config.Provider
+ // Skip rendering. Useful for testing.
+ SkipRender bool
+ // Use this to indicate what changed (for rebuilds).
+ whatChanged *whatChanged
+
+ // This is a partial re-render of some selected pages. This means
+ // we should skip most of the processing.
+ PartialReRender bool
+
+ // Recently visited URLs. This is used for partial re-rendering.
+ RecentlyVisited map[string]bool
+}
+
+// shouldRender is used in the Fast Render Mode to determine if we need to re-render
+// a Page: If it is recently visited (the home pages will always be in this set) or changed.
+// Note that a page does not have to have a content page / file.
+// For regular builds, this will allways return true.
+// TODO(bep) rename/work this.
+func (cfg *BuildCfg) shouldRender(p *pageState) bool {
+ if !p.render {
+ return false
+ }
+ if p.forceRender {
+ return true
+ }
+
+ if len(cfg.RecentlyVisited) == 0 {
+ return true
+ }
+
+ if cfg.RecentlyVisited[p.RelPermalink()] {
+ return true
+ }
+
+ if cfg.whatChanged != nil && !p.File().IsZero() {
+ return cfg.whatChanged.files[p.File().Filename()]
+ }
+
+ return false
+}
+
+func (h *HugoSites) renderCrossSitesArtifacts() error {
+
+ if !h.multilingual.enabled() || h.IsMultihost() {
+ return nil
+ }
+
+ sitemapEnabled := false
+ for _, s := range h.Sites {
+ if s.isEnabled(kindSitemap) {
+ sitemapEnabled = true
+ break
+ }
+ }
+
+ if !sitemapEnabled {
+ return nil
+ }
+
+ s := h.Sites[0]
+
+ smLayouts := []string{"sitemapindex.xml", "_default/sitemapindex.xml", "_internal/_default/sitemapindex.xml"}
+
+ return s.renderAndWriteXML(&s.PathSpec.ProcessingStats.Sitemaps, "sitemapindex",
+ s.siteCfg.sitemap.Filename, h.toSiteInfos(), smLayouts...)
+}
+
+// createMissingPages creates home page, taxonomies etc. that isnt't created as an
+// effect of having a content file.
+func (h *HugoSites) createMissingPages() error {
+
+ for _, s := range h.Sites {
+ if s.isEnabled(page.KindHome) {
+ // home pages
+ homes := s.findWorkPagesByKind(page.KindHome)
+ if len(homes) > 1 {
+ panic("Too many homes")
+ }
+ var home *pageState
+ if len(homes) == 0 {
+ home = s.newPage(page.KindHome)
+ s.workAllPages = append(s.workAllPages, home)
+ } else {
+ home = homes[0]
+ }
+
+ s.home = home
+ }
+
+ // Will create content-less root sections.
+ newSections := s.assembleSections()
+ s.workAllPages = append(s.workAllPages, newSections...)
+
+ taxonomyTermEnabled := s.isEnabled(page.KindTaxonomyTerm)
+ taxonomyEnabled := s.isEnabled(page.KindTaxonomy)
+
+ // taxonomy list and terms pages
+ taxonomies := s.Language().GetStringMapString("taxonomies")
+ if len(taxonomies) > 0 {
+ taxonomyPages := s.findWorkPagesByKind(page.KindTaxonomy)
+ taxonomyTermsPages := s.findWorkPagesByKind(page.KindTaxonomyTerm)
+
+ // Make them navigable from WeightedPage etc.
+ for _, p := range taxonomyPages {
+ ni := p.getTaxonomyNodeInfo()
+ if ni == nil {
+ // This can be nil for taxonomies, e.g. an author,
+ // with a content file, but no actual usage.
+ // Create one.
+ sections := p.SectionsEntries()
+ if len(sections) < 2 {
+ // Invalid state
+ panic(fmt.Sprintf("invalid taxonomy state for %q with sections %v", p.pathOrTitle(), sections))
+ }
+ ni = p.s.taxonomyNodes.GetOrAdd(sections[0], path.Join(sections[1:]...))
+ }
+ ni.TransferValues(p)
+ }
+ for _, p := range taxonomyTermsPages {
+ p.getTaxonomyNodeInfo().TransferValues(p)
+ }
+
+ for _, plural := range taxonomies {
+ if taxonomyTermEnabled {
+ foundTaxonomyTermsPage := false
+ for _, p := range taxonomyTermsPages {
+ if p.SectionsPath() == plural {
+ foundTaxonomyTermsPage = true
+ break
+ }
+ }
+
+ if !foundTaxonomyTermsPage {
+ n := s.newPage(page.KindTaxonomyTerm, plural)
+ n.getTaxonomyNodeInfo().TransferValues(n)
+ s.workAllPages = append(s.workAllPages, n)
+ }
+ }
+
+ if taxonomyEnabled {
+ for termKey := range s.Taxonomies[plural] {
+
+ foundTaxonomyPage := false
+
+ for _, p := range taxonomyPages {
+ sectionsPath := p.SectionsPath()
+
+ if !strings.HasPrefix(sectionsPath, plural) {
+ continue
+ }
+
+ singularKey := strings.TrimPrefix(sectionsPath, plural)
+ singularKey = strings.TrimPrefix(singularKey, "/")
+
+ if singularKey == termKey {
+ foundTaxonomyPage = true
+ break
+ }
+ }
+
+ if !foundTaxonomyPage {
+ info := s.taxonomyNodes.Get(plural, termKey)
+ if info == nil {
+ panic("no info found")
+ }
+
+ n := s.newTaxonomyPage(info.term, info.plural, info.termKey)
+ info.TransferValues(n)
+ s.workAllPages = append(s.workAllPages, n)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+func (h *HugoSites) removePageByFilename(filename string) {
+ for _, s := range h.Sites {
+ s.removePageFilename(filename)
+ }
+}
+
+func (h *HugoSites) createPageCollections() error {
+ for _, s := range h.Sites {
+ for _, p := range s.rawAllPages {
+ if !s.isEnabled(p.Kind()) {
+ continue
+ }
+
+ shouldBuild := s.shouldBuild(p)
+ s.buildStats.update(p)
+ if shouldBuild {
+ if p.m.headless {
+ s.headlessPages = append(s.headlessPages, p)
+ } else {
+ s.workAllPages = append(s.workAllPages, p)
+ }
+ }
+ }
+ }
+
+ allPages := newLazyPagesFactory(func() page.Pages {
+ var pages page.Pages
+ for _, s := range h.Sites {
+ pages = append(pages, s.Pages()...)
+ }
+
+ page.SortByDefault(pages)
+
+ return pages
+ })
+
+ allRegularPages := newLazyPagesFactory(func() page.Pages {
+ return h.findPagesByKindIn(page.KindPage, allPages.get())
+ })
+
+ for _, s := range h.Sites {
+ s.PageCollections.allPages = allPages
+ s.PageCollections.allRegularPages = allRegularPages
+ }
+
+ return nil
+}
+
+func (s *Site) preparePagesForRender(isRenderingSite bool, idx int) error {
+
+ for _, p := range s.workAllPages {
+ if err := p.initOutputFormat(isRenderingSite, idx); err != nil {
+ return err
+ }
+ }
+
+ for _, p := range s.headlessPages {
+ if err := p.initOutputFormat(isRenderingSite, idx); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// Pages returns all pages for all sites.
+func (h *HugoSites) Pages() page.Pages {
+ return h.Sites[0].AllPages()
+}
+
+func (h *HugoSites) loadData(fs afero.Fs) (err error) {
+ spec := source.NewSourceSpec(h.PathSpec, fs)
+ fileSystem := spec.NewFilesystem("")
+ h.data = make(map[string]interface{})
+ for _, r := range fileSystem.Files() {
+ if err := h.handleDataFile(r); err != nil {
+ return err
+ }
+ }
+
+ return
+}
+
+func (h *HugoSites) handleDataFile(r source.ReadableFile) error {
+ var current map[string]interface{}
+
+ f, err := r.Open()
+ if err != nil {
+ return errors.Wrapf(err, "Failed to open data file %q:", r.LogicalName())
+ }
+ defer f.Close()
+
+ // Crawl in data tree to insert data
+ current = h.data
+ keyParts := strings.Split(r.Dir(), helpers.FilePathSeparator)
+ // The first path element is the virtual folder (typically theme name), which is
+ // not part of the key.
+ if len(keyParts) > 1 {
+ for _, key := range keyParts[1:] {
+ if key != "" {
+ if _, ok := current[key]; !ok {
+ current[key] = make(map[string]interface{})
+ }
+ current = current[key].(map[string]interface{})
+ }
+ }
+ }
+
+ data, err := h.readData(r)
+ if err != nil {
+ return h.errWithFileContext(err, r)
+ }
+
+ if data == nil {
+ return nil
+ }
+
+ // 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
+ higherPrecedentData := current[r.BaseFileName()]
+
+ switch data.(type) {
+ case nil:
+ // hear the crickets?
+
+ case map[string]interface{}:
+
+ switch higherPrecedentData.(type) {
+ case nil:
+ current[r.BaseFileName()] = data
+ case map[string]interface{}:
+ // merge maps: insert entries from data for keys that
+ // don't already exist in higherPrecedentData
+ higherPrecedentMap := higherPrecedentData.(map[string]interface{})
+ for key, value := range data.(map[string]interface{}) {
+ if _, exists := higherPrecedentMap[key]; exists {
+ h.Log.WARN.Printf("Data for key '%s' in path '%s' is overridden by higher precedence data already in the data tree", key, r.Path())
+ } else {
+ higherPrecedentMap[key] = value
+ }
+ }
+ default:
+ // can't merge: higherPrecedentData is not a map
+ h.Log.WARN.Printf("The %T data from '%s' overridden by "+
+ "higher precedence %T data already in the data tree", data, r.Path(), higherPrecedentData)
+ }
+
+ case []interface{}:
+ if higherPrecedentData == nil {
+ current[r.BaseFileName()] = data
+ } else {
+ // we don't merge array data
+ h.Log.WARN.Printf("The %T data from '%s' overridden by "+
+ "higher precedence %T data already in the data tree", data, r.Path(), higherPrecedentData)
+ }
+
+ default:
+ h.Log.ERROR.Printf("unexpected data type %T in file %s", data, r.LogicalName())
+ }
+
+ return nil
+}
+
+func (h *HugoSites) errWithFileContext(err error, f source.File) error {
+ rfi, ok := f.FileInfo().(hugofs.RealFilenameInfo)
+ if !ok {
+ return err
+ }
+
+ realFilename := rfi.RealFilename()
+
+ err, _ = herrors.WithFileContextForFile(
+ err,
+ realFilename,
+ realFilename,
+ h.SourceSpec.Fs.Source,
+ herrors.SimpleLineMatcher)
+
+ return err
+}
+
+func (h *HugoSites) readData(f source.ReadableFile) (interface{}, error) {
+ file, err := f.Open()
+ if err != nil {
+ return nil, errors.Wrap(err, "readData: failed to open data file")
+ }
+ defer file.Close()
+ content := helpers.ReaderToBytes(file)
+
+ format := metadecoders.FormatFromString(f.Extension())
+ return metadecoders.Default.Unmarshal(content, format)
+}
+
+func (h *HugoSites) findPagesByKindIn(kind string, inPages page.Pages) page.Pages {
+ return h.Sites[0].findPagesByKindIn(kind, inPages)
+}
+
+func (h *HugoSites) findPagesByShortcode(shortcode string) page.Pages {
+ var pages page.Pages
+ for _, s := range h.Sites {
+ pages = append(pages, s.findPagesByShortcode(shortcode)...)
+ }
+ return pages
+}
+
+// Used in partial reloading to determine if the change is in a bundle.
+type contentChangeMap struct {
+ mu sync.RWMutex
+ branches []string
+ leafs []string
+
+ pathSpec *helpers.PathSpec
+
+ // Hugo supports symlinked content (both directories and files). This
+ // can lead to situations where the same file can be referenced from several
+ // locations in /content -- which is really cool, but also means we have to
+ // go an extra mile to handle changes.
+ // This map is only used in watch mode.
+ // It maps either file to files or the real dir to a set of content directories where it is in use.
+ symContent map[string]map[string]bool
+ symContentMu sync.Mutex
+}
+
+func (m *contentChangeMap) add(filename string, tp bundleDirType) {
+ m.mu.Lock()
+ dir := filepath.Dir(filename) + helpers.FilePathSeparator
+ dir = strings.TrimPrefix(dir, ".")
+ switch tp {
+ case bundleBranch:
+ m.branches = append(m.branches, dir)
+ case bundleLeaf:
+ m.leafs = append(m.leafs, dir)
+ default:
+ panic("invalid bundle type")
+ }
+ m.mu.Unlock()
+}
+
+// Track the addition of bundle dirs.
+func (m *contentChangeMap) handleBundles(b *bundleDirs) {
+ for _, bd := range b.bundles {
+ m.add(bd.fi.Path(), bd.tp)
+ }
+}
+
+// resolveAndRemove resolves the given filename to the root folder of a bundle, if relevant.
+// It also removes the entry from the map. It will be re-added again by the partial
+// build if it still is a bundle.
+func (m *contentChangeMap) resolveAndRemove(filename string) (string, string, bundleDirType) {
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+
+ // Bundles share resources, so we need to start from the virtual root.
+ relPath := m.pathSpec.RelContentDir(filename)
+ dir, name := filepath.Split(relPath)
+ if !strings.HasSuffix(dir, helpers.FilePathSeparator) {
+ dir += helpers.FilePathSeparator
+ }
+
+ fileTp, isContent := classifyBundledFile(name)
+
+ // This may be a member of a bundle. Start with branch bundles, the most specific.
+ if fileTp == bundleBranch || (fileTp == bundleNot && !isContent) {
+ for i, b := range m.branches {
+ if b == dir {
+ m.branches = append(m.branches[:i], m.branches[i+1:]...)
+ return dir, b, bundleBranch
+ }
+ }
+ }
+
+ // And finally the leaf bundles, which can contain anything.
+ for i, l := range m.leafs {
+ if strings.HasPrefix(dir, l) {
+ m.leafs = append(m.leafs[:i], m.leafs[i+1:]...)
+ return dir, l, bundleLeaf
+ }
+ }
+
+ if isContent && fileTp != bundleNot {
+ // A new bundle.
+ return dir, dir, fileTp
+ }
+
+ // Not part of any bundle
+ return dir, filename, bundleNot
+}
+
+func (m *contentChangeMap) addSymbolicLinkMapping(from, to string) {
+ m.symContentMu.Lock()
+ mm, found := m.symContent[from]
+ if !found {
+ mm = make(map[string]bool)
+ m.symContent[from] = mm
+ }
+ mm[to] = true
+ m.symContentMu.Unlock()
+}
+
+func (m *contentChangeMap) GetSymbolicLinkMappings(dir string) []string {
+ mm, found := m.symContent[dir]
+ if !found {
+ return nil
+ }
+ dirs := make([]string, len(mm))
+ i := 0
+ for dir := range mm {
+ dirs[i] = dir
+ i++
+ }
+
+ sort.Strings(dirs)
+ return dirs
+}
diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go
new file mode 100644
index 000000000..7f725def2
--- /dev/null
+++ b/hugolib/hugo_sites_build.go
@@ -0,0 +1,326 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "context"
+ "fmt"
+ "runtime/trace"
+ "sort"
+
+ "github.com/gohugoio/hugo/output"
+
+ "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 {
+ if h.running {
+ // Make sure we don't trigger rebuilds in parallel.
+ h.runningMu.Lock()
+ defer h.runningMu.Unlock()
+ }
+
+ ctx, task := trace.NewTask(context.Background(), "Build")
+ defer task.End()
+
+ errCollector := h.StartErrorCollector()
+ errs := make(chan error)
+
+ go func(from, to chan error) {
+ var errors []error
+ i := 0
+ for e := range from {
+ i++
+ if i > 50 {
+ break
+ }
+ errors = append(errors, e)
+ }
+ to <- h.pickOneAndLogTheRest(errors)
+
+ close(to)
+
+ }(errCollector, errs)
+
+ if h.Metrics != nil {
+ h.Metrics.Reset()
+ }
+
+ // 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}
+ }
+
+ var prepareErr error
+
+ if !config.PartialReRender {
+ prepare := func() error {
+ for _, s := range h.Sites {
+ s.Deps.BuildStartListeners.Notify()
+ }
+
+ if len(events) > 0 {
+ // Rebuild
+ if err := h.initRebuild(conf); err != nil {
+ return err
+ }
+ } else {
+ if err := h.initSites(conf); err != nil {
+ return err
+ }
+ }
+
+ var err error
+
+ f := func() {
+ err = h.process(conf, events...)
+ }
+ trace.WithRegion(ctx, "process", f)
+ if err != nil {
+ return err
+ }
+
+ f = func() {
+ err = h.assemble(conf)
+ }
+ trace.WithRegion(ctx, "assemble", f)
+ if err != nil {
+ return err
+ }
+
+ return nil
+ }
+
+ f := func() {
+ prepareErr = prepare()
+ }
+ trace.WithRegion(ctx, "prepare", f)
+ if prepareErr != nil {
+ h.SendError(prepareErr)
+ }
+
+ }
+
+ if prepareErr == nil {
+ var err error
+ f := func() {
+ err = h.render(conf)
+ }
+ trace.WithRegion(ctx, "render", f)
+ if err != nil {
+ h.SendError(err)
+ }
+ }
+
+ if h.Metrics != nil {
+ var b bytes.Buffer
+ h.Metrics.WriteMetrics(&b)
+
+ h.Log.FEEDBACK.Printf("\nTemplate Metrics:\n\n")
+ h.Log.FEEDBACK.Print(b.String())
+ h.Log.FEEDBACK.Println()
+ }
+
+ select {
+ // Make sure the channel always gets something.
+ case errCollector <- nil:
+ default:
+ }
+ close(errCollector)
+
+ err := <-errs
+ if err != nil {
+ return err
+ }
+
+ if err := h.fatalErrorHandler.getErr(); err != nil {
+ return err
+ }
+
+ errorCount := h.Log.ErrorCounter.Count()
+ if errorCount > 0 {
+ return fmt.Errorf("logged %d error(s)", errorCount)
+ }
+
+ return nil
+
+}
+
+// Build lifecycle methods below.
+// The order listed matches the order of execution.
+
+func (h *HugoSites) initSites(config *BuildCfg) error {
+ h.reset(config)
+
+ if config.NewConfig != nil {
+ if err := h.createSitesFromConfig(config.NewConfig); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (h *HugoSites) initRebuild(config *BuildCfg) error {
+ if config.NewConfig != nil {
+ return errors.New("rebuild does not support 'NewConfig'")
+ }
+
+ if config.ResetState {
+ return errors.New("rebuild does not support 'ResetState'")
+ }
+
+ if !h.running {
+ return errors.New("rebuild called when not in watch mode")
+ }
+
+ for _, s := range h.Sites {
+ s.resetBuildState()
+ }
+
+ h.reset(config)
+ h.resetLogs()
+ 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.processPartial(events)
+ config.whatChanged = &changed
+ return err
+ }
+
+ return firstSite.process(*config)
+
+}
+
+func (h *HugoSites) assemble(config *BuildCfg) error {
+
+ if len(h.Sites) > 1 {
+ // The first is initialized during process; initialize the rest
+ for _, site := range h.Sites[1:] {
+ if err := site.initializeSiteInfo(); err != nil {
+ return err
+ }
+ }
+ }
+
+ if err := h.createPageCollections(); err != nil {
+ return err
+ }
+
+ if config.whatChanged.source {
+ for _, s := range h.Sites {
+ if err := s.assembleTaxonomies(); err != nil {
+ return err
+ }
+ }
+ }
+
+ // Create pagexs for the section pages etc. without content file.
+ if err := h.createMissingPages(); err != nil {
+ return err
+ }
+
+ for _, s := range h.Sites {
+ s.setupSitePages()
+ sort.Stable(s.workAllPages)
+ }
+
+ return nil
+
+}
+
+func (h *HugoSites) render(config *BuildCfg) error {
+ siteRenderContext := &siteRenderContext{cfg: config, multihost: h.multihost}
+
+ if !config.PartialReRender {
+ h.renderFormats = output.Formats{}
+ for _, s := range h.Sites {
+ s.initRenderFormats()
+ h.renderFormats = append(h.renderFormats, s.renderFormats...)
+ }
+ }
+
+ i := 0
+ for _, s := range h.Sites {
+ for siteOutIdx, renderFormat := range s.renderFormats {
+ siteRenderContext.outIdx = siteOutIdx
+ siteRenderContext.sitesOutIdx = i
+ i++
+
+ select {
+ case <-h.Done():
+ return nil
+ default:
+ // For the non-renderable pages, we use the content iself as
+ // template and we may have to re-parse and execute it for
+ // each output format.
+ h.TemplateHandler().RebuildClone()
+
+ for _, s2 := range h.Sites {
+ // We render site by site, but since the content is lazily rendered
+ // and a site can "borrow" content from other sites, every site
+ // needs this set.
+ s2.rc = &siteRenderingContext{Format: renderFormat}
+
+ if err := s2.preparePagesForRender(s == s2, siteRenderContext.sitesOutIdx); err != nil {
+ return err
+ }
+ }
+
+ if !config.SkipRender {
+ if config.PartialReRender {
+ if err := s.renderPages(siteRenderContext); err != nil {
+ return err
+ }
+ } else {
+ if err := s.render(siteRenderContext); err != nil {
+ return err
+ }
+ }
+ }
+ }
+
+ }
+
+ }
+
+ if !config.SkipRender {
+ if err := h.renderCrossSitesArtifacts(); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/hugolib/hugo_sites_build_errors_test.go b/hugolib/hugo_sites_build_errors_test.go
new file mode 100644
index 000000000..6fe4901a1
--- /dev/null
+++ b/hugolib/hugo_sites_build_errors_test.go
@@ -0,0 +1,354 @@
+package hugolib
+
+import (
+ "fmt"
+ "path/filepath"
+ "regexp"
+ "runtime"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/fortytw2/leaktest"
+
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/stretchr/testify/require"
+)
+
+type testSiteBuildErrorAsserter struct {
+ name string
+ assert *require.Assertions
+}
+
+func (t testSiteBuildErrorAsserter) getFileError(err error) *herrors.ErrorWithFileContext {
+ t.assert.NotNil(err, t.name)
+ ferr := herrors.UnwrapErrorWithFileContext(err)
+ t.assert.NotNil(ferr, fmt.Sprintf("[%s] got %T: %+v\n%s", t.name, err, err, stackTrace()))
+ return ferr
+}
+
+func (t testSiteBuildErrorAsserter) assertLineNumber(lineNumber int, err error) {
+ fe := t.getFileError(err)
+ t.assert.Equal(lineNumber, fe.Position().LineNumber, fmt.Sprintf("[%s] got => %s\n%s", t.name, fe, stackTrace()))
+}
+
+func (t testSiteBuildErrorAsserter) assertErrorMessage(e1, e2 string) {
+ // The error message will contain filenames with OS slashes. Normalize before compare.
+ e1, e2 = filepath.ToSlash(e1), filepath.ToSlash(e2)
+ t.assert.Contains(e2, e1, stackTrace())
+
+}
+
+func TestSiteBuildErrors(t *testing.T) {
+ t.Parallel()
+
+ const (
+ yamlcontent = "yamlcontent"
+ tomlcontent = "tomlcontent"
+ jsoncontent = "jsoncontent"
+ shortcode = "shortcode"
+ base = "base"
+ single = "single"
+ )
+
+ // TODO(bep) add content tests after https://github.com/gohugoio/hugo/issues/5324
+ // is implemented.
+
+ tests := []struct {
+ name string
+ fileType string
+ fileFixer func(content string) string
+ assertCreateError func(a testSiteBuildErrorAsserter, err error)
+ assertBuildError func(a testSiteBuildErrorAsserter, err error)
+ }{
+
+ {
+ name: "Base template parse failed",
+ fileType: base,
+ fileFixer: func(content string) string {
+ return strings.Replace(content, ".Title }}", ".Title }", 1)
+ },
+ assertCreateError: func(a testSiteBuildErrorAsserter, err error) {
+ a.assertLineNumber(4, err)
+ },
+ },
+ {
+ name: "Base template execute failed",
+ fileType: base,
+ fileFixer: func(content string) string {
+ return strings.Replace(content, ".Title", ".Titles", 1)
+ },
+ assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
+ a.assertLineNumber(4, err)
+ },
+ },
+ {
+ name: "Single template parse failed",
+ fileType: single,
+ fileFixer: func(content string) string {
+ return strings.Replace(content, ".Title }}", ".Title }", 1)
+ },
+ assertCreateError: func(a testSiteBuildErrorAsserter, err error) {
+ fe := a.getFileError(err)
+ a.assert.Equal(5, fe.Position().LineNumber)
+ a.assert.Equal(1, fe.Position().ColumnNumber)
+ a.assert.Equal("go-html-template", fe.ChromaLexer)
+ a.assertErrorMessage("\"layouts/_default/single.html:5:1\": parse failed: template: _default/single.html:5: unexpected \"}\" in operand", fe.Error())
+
+ },
+ },
+ {
+ name: "Single template execute failed",
+ fileType: single,
+ fileFixer: func(content string) string {
+ return strings.Replace(content, ".Title", ".Titles", 1)
+ },
+ assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
+ fe := a.getFileError(err)
+ a.assert.Equal(5, fe.Position().LineNumber)
+ a.assert.Equal(14, fe.Position().ColumnNumber)
+ a.assert.Equal("go-html-template", fe.ChromaLexer)
+ a.assertErrorMessage("\"layouts/_default/single.html:5:14\": execute of template failed", fe.Error())
+
+ },
+ },
+ {
+ name: "Single template execute failed, long keyword",
+ fileType: single,
+ fileFixer: func(content string) string {
+ return strings.Replace(content, ".Title", ".ThisIsAVeryLongTitle", 1)
+ },
+ assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
+ fe := a.getFileError(err)
+ a.assert.Equal(5, fe.Position().LineNumber)
+ a.assert.Equal(14, fe.Position().ColumnNumber)
+ a.assert.Equal("go-html-template", fe.ChromaLexer)
+ a.assertErrorMessage("\"layouts/_default/single.html:5:14\": execute of template failed", fe.Error())
+
+ },
+ },
+ {
+ name: "Shortcode parse failed",
+ fileType: shortcode,
+ fileFixer: func(content string) string {
+ return strings.Replace(content, ".Title }}", ".Title }", 1)
+ },
+ assertCreateError: func(a testSiteBuildErrorAsserter, err error) {
+ a.assertLineNumber(4, err)
+ },
+ },
+ {
+ name: "Shortode execute failed",
+ fileType: shortcode,
+ fileFixer: func(content string) string {
+ return strings.Replace(content, ".Title", ".Titles", 1)
+ },
+ assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
+ fe := a.getFileError(err)
+ a.assert.Equal(7, fe.Position().LineNumber)
+ a.assert.Equal("md", fe.ChromaLexer)
+ // Make sure that it contains both the content file and template
+ a.assertErrorMessage(`content/myyaml.md:7:10": failed to render shortcode "sc"`, fe.Error())
+ a.assertErrorMessage(`shortcodes/sc.html:4:22: executing "shortcodes/sc.html" at <.Page.Titles>: can't evaluate`, fe.Error())
+ },
+ },
+ {
+ name: "Shortode does not exist",
+ fileType: yamlcontent,
+ fileFixer: func(content string) string {
+ return strings.Replace(content, "{{< sc >}}", "{{< nono >}}", 1)
+ },
+ assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
+ fe := a.getFileError(err)
+ a.assert.Equal(7, fe.Position().LineNumber)
+ a.assert.Equal(10, fe.Position().ColumnNumber)
+ a.assert.Equal("md", fe.ChromaLexer)
+ a.assertErrorMessage(`"content/myyaml.md:7:10": failed to extract shortcode: template for shortcode "nono" not found`, fe.Error())
+ },
+ },
+ {
+ name: "Invalid YAML front matter",
+ fileType: yamlcontent,
+ fileFixer: func(content string) string {
+ return strings.Replace(content, "title:", "title: %foo", 1)
+ },
+ assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
+ a.assertLineNumber(2, err)
+ },
+ },
+ {
+ name: "Invalid TOML front matter",
+ fileType: tomlcontent,
+ fileFixer: func(content string) string {
+ return strings.Replace(content, "description = ", "description &", 1)
+ },
+ assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
+ fe := a.getFileError(err)
+ a.assert.Equal(6, fe.Position().LineNumber)
+ a.assert.Equal("toml", fe.ErrorContext.ChromaLexer)
+
+ },
+ },
+ {
+ name: "Invalid JSON front matter",
+ fileType: jsoncontent,
+ fileFixer: func(content string) string {
+ return strings.Replace(content, "\"description\":", "\"description\"", 1)
+ },
+ assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
+ fe := a.getFileError(err)
+
+ a.assert.Equal(3, fe.Position().LineNumber)
+ a.assert.Equal("json", fe.ErrorContext.ChromaLexer)
+
+ },
+ },
+ {
+ // See https://github.com/gohugoio/hugo/issues/5327
+ name: "Panic in template Execute",
+ fileType: single,
+ fileFixer: func(content string) string {
+ return strings.Replace(content, ".Title", ".Parent.Parent.Parent", 1)
+ },
+
+ assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
+ a.assert.Error(err)
+ // This is fixed in latest Go source
+ if regexp.MustCompile("devel|12").MatchString(runtime.Version()) {
+ fe := a.getFileError(err)
+ a.assert.Equal(5, fe.Position().LineNumber)
+ a.assert.Equal(21, fe.Position().ColumnNumber)
+ } else {
+ a.assert.Contains(err.Error(), `execute of template failed: panic in Execute`)
+ }
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ assert := require.New(t)
+ errorAsserter := testSiteBuildErrorAsserter{
+ assert: assert,
+ name: test.name,
+ }
+
+ b := newTestSitesBuilder(t).WithSimpleConfigFile()
+
+ f := func(fileType, content string) string {
+ if fileType != test.fileType {
+ return content
+ }
+ return test.fileFixer(content)
+
+ }
+
+ b.WithTemplatesAdded("layouts/shortcodes/sc.html", f(shortcode, `SHORTCODE L1
+SHORTCODE L2
+SHORTCODE L3:
+SHORTCODE L4: {{ .Page.Title }}
+`))
+ b.WithTemplatesAdded("layouts/_default/baseof.html", f(base, `BASEOF L1
+BASEOF L2
+BASEOF L3
+BASEOF L4{{ if .Title }}{{ end }}
+{{block "main" .}}This is the main content.{{end}}
+BASEOF L6
+`))
+
+ b.WithTemplatesAdded("layouts/_default/single.html", f(single, `{{ define "main" }}
+SINGLE L2:
+SINGLE L3:
+SINGLE L4:
+SINGLE L5: {{ .Title }} {{ .Content }}
+{{ end }}
+`))
+
+ b.WithContent("myyaml.md", f(yamlcontent, `---
+title: "The YAML"
+---
+
+Some content.
+
+ {{< sc >}}
+
+Some more text.
+
+The end.
+
+`))
+
+ b.WithContent("mytoml.md", f(tomlcontent, `+++
+title = "The TOML"
+p1 = "v"
+p2 = "v"
+p3 = "v"
+description = "Descriptioon"
++++
+
+Some content.
+
+
+`))
+
+ b.WithContent("myjson.md", f(jsoncontent, `{
+ "title": "This is a title",
+ "description": "This is a description."
+}
+
+Some content.
+
+
+`))
+
+ createErr := b.CreateSitesE()
+ if test.assertCreateError != nil {
+ test.assertCreateError(errorAsserter, createErr)
+ } else {
+ assert.NoError(createErr)
+ }
+
+ if createErr == nil {
+ buildErr := b.BuildE(BuildCfg{})
+ if test.assertBuildError != nil {
+ test.assertBuildError(errorAsserter, buildErr)
+ } else {
+ assert.NoError(buildErr)
+ }
+ }
+ })
+ }
+}
+
+// https://github.com/gohugoio/hugo/issues/5375
+func TestSiteBuildTimeout(t *testing.T) {
+ if !isCI() {
+ defer leaktest.CheckTimeout(t, 10*time.Second)()
+ }
+
+ b := newTestSitesBuilder(t)
+ b.WithConfigFile("toml", `
+timeout = 5
+`)
+
+ b.WithTemplatesAdded("_default/single.html", `
+{{ .WordCount }}
+`, "shortcodes/c.html", `
+{{ range .Page.Site.RegularPages }}
+{{ .WordCount }}
+{{ end }}
+
+`)
+
+ for i := 1; i < 100; i++ {
+ b.WithContent(fmt.Sprintf("page%d.md", i), `---
+title: "A page"
+---
+
+{{< c >}}`)
+
+ }
+
+ b.CreateSites().BuildFail(BuildCfg{})
+
+}
diff --git a/hugolib/hugo_sites_build_test.go b/hugolib/hugo_sites_build_test.go
new file mode 100644
index 000000000..236fd11a6
--- /dev/null
+++ b/hugolib/hugo_sites_build_test.go
@@ -0,0 +1,1537 @@
+package hugolib
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/gohugoio/hugo/resources/page"
+
+ "github.com/fortytw2/leaktest"
+ "github.com/fsnotify/fsnotify"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/spf13/afero"
+ "github.com/stretchr/testify/require"
+)
+
+func TestMultiSitesMainLangInRoot(t *testing.T) {
+ t.Parallel()
+ for _, b := range []bool{false} {
+ doTestMultiSitesMainLangInRoot(t, b)
+ }
+}
+
+func doTestMultiSitesMainLangInRoot(t *testing.T, defaultInSubDir bool) {
+ assert := require.New(t)
+
+ siteConfig := map[string]interface{}{
+ "DefaultContentLanguage": "fr",
+ "DefaultContentLanguageInSubdir": defaultInSubDir,
+ }
+
+ b := newMultiSiteTestBuilder(t, "toml", multiSiteTOMLConfigTemplate, siteConfig)
+
+ pathMod := func(s string) string {
+ return s
+ }
+
+ if !defaultInSubDir {
+ pathMod = func(s string) string {
+ return strings.Replace(s, "/fr/", "/", -1)
+ }
+ }
+
+ b.CreateSites()
+ b.Build(BuildCfg{})
+
+ sites := b.H.Sites
+
+ require.Len(t, sites, 4)
+
+ enSite := sites[0]
+ frSite := sites[1]
+
+ assert.Equal("/en", enSite.Info.LanguagePrefix)
+
+ if defaultInSubDir {
+ assert.Equal("/fr", frSite.Info.LanguagePrefix)
+ } else {
+ assert.Equal("", frSite.Info.LanguagePrefix)
+ }
+
+ assert.Equal("/blog/en/foo", enSite.PathSpec.RelURL("foo", true))
+
+ doc1en := enSite.RegularPages()[0]
+ doc1fr := frSite.RegularPages()[0]
+
+ enPerm := doc1en.Permalink()
+ enRelPerm := doc1en.RelPermalink()
+ assert.Equal("http://example.com/blog/en/sect/doc1-slug/", enPerm)
+ assert.Equal("/blog/en/sect/doc1-slug/", enRelPerm)
+
+ frPerm := doc1fr.Permalink()
+ frRelPerm := doc1fr.RelPermalink()
+
+ b.AssertFileContent(pathMod("public/fr/sect/doc1/index.html"), "Single", "Bonjour")
+ b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Hello")
+
+ if defaultInSubDir {
+ assert.Equal("http://example.com/blog/fr/sect/doc1/", frPerm)
+ assert.Equal("/blog/fr/sect/doc1/", frRelPerm)
+
+ // should have a redirect on top level.
+ b.AssertFileContent("public/index.html", `<meta http-equiv="refresh" content="0; url=http://example.com/blog/fr" />`)
+ } else {
+ // Main language in root
+ assert.Equal("http://example.com/blog/sect/doc1/", frPerm)
+ assert.Equal("/blog/sect/doc1/", frRelPerm)
+
+ // should have redirect back to root
+ b.AssertFileContent("public/fr/index.html", `<meta http-equiv="refresh" content="0; url=http://example.com/blog" />`)
+ }
+ b.AssertFileContent(pathMod("public/fr/index.html"), "Home", "Bonjour")
+ b.AssertFileContent("public/en/index.html", "Home", "Hello")
+
+ // Check list pages
+ b.AssertFileContent(pathMod("public/fr/sect/index.html"), "List", "Bonjour")
+ b.AssertFileContent("public/en/sect/index.html", "List", "Hello")
+ b.AssertFileContent(pathMod("public/fr/plaques/FRtag1/index.html"), "Taxonomy List", "Bonjour")
+ b.AssertFileContent("public/en/tags/tag1/index.html", "Taxonomy 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.
+ b.AssertFileContent("public/sitemap.xml",
+ "<loc>http://example.com/blog/en/sitemap.xml</loc>",
+ "<loc>http://example.com/blog/fr/sitemap.xml</loc>")
+
+ if defaultInSubDir {
+ b.AssertFileContent("public/fr/sitemap.xml", "<loc>http://example.com/blog/fr/</loc>")
+ } else {
+ b.AssertFileContent("public/fr/sitemap.xml", "<loc>http://example.com/blog/</loc>")
+ }
+ b.AssertFileContent("public/en/sitemap.xml", "<loc>http://example.com/blog/en/</loc>")
+
+ // Check rss
+ b.AssertFileContent(pathMod("public/fr/index.xml"), pathMod(`<atom:link href="http://example.com/blog/fr/index.xml"`),
+ `rel="self" type="application/rss+xml"`)
+ b.AssertFileContent("public/en/index.xml", `<atom:link href="http://example.com/blog/en/index.xml"`)
+ b.AssertFileContent(
+ pathMod("public/fr/sect/index.xml"),
+ pathMod(`<atom:link href="http://example.com/blog/fr/sect/index.xml"`))
+ b.AssertFileContent("public/en/sect/index.xml", `<atom:link href="http://example.com/blog/en/sect/index.xml"`)
+ b.AssertFileContent(
+ pathMod("public/fr/plaques/FRtag1/index.xml"),
+ pathMod(`<atom:link href="http://example.com/blog/fr/plaques/FRtag1/index.xml"`))
+ b.AssertFileContent("public/en/tags/tag1/index.xml", `<atom:link href="http://example.com/blog/en/tags/tag1/index.xml"`)
+
+ // Check paginators
+ b.AssertFileContent(pathMod("public/fr/page/1/index.html"), pathMod(`refresh" content="0; url=http://example.com/blog/fr/"`))
+ b.AssertFileContent("public/en/page/1/index.html", `refresh" content="0; url=http://example.com/blog/en/"`)
+ b.AssertFileContent(pathMod("public/fr/page/2/index.html"), "Home Page 2", "Bonjour", pathMod("http://example.com/blog/fr/"))
+ b.AssertFileContent("public/en/page/2/index.html", "Home Page 2", "Hello", "http://example.com/blog/en/")
+ b.AssertFileContent(pathMod("public/fr/sect/page/1/index.html"), pathMod(`refresh" content="0; url=http://example.com/blog/fr/sect/"`))
+ b.AssertFileContent("public/en/sect/page/1/index.html", `refresh" content="0; url=http://example.com/blog/en/sect/"`)
+ b.AssertFileContent(pathMod("public/fr/sect/page/2/index.html"), "List Page 2", "Bonjour", pathMod("http://example.com/blog/fr/sect/"))
+ b.AssertFileContent("public/en/sect/page/2/index.html", "List Page 2", "Hello", "http://example.com/blog/en/sect/")
+ b.AssertFileContent(
+ pathMod("public/fr/plaques/FRtag1/page/1/index.html"),
+ pathMod(`refresh" content="0; url=http://example.com/blog/fr/plaques/FRtag1/"`))
+ b.AssertFileContent("public/en/tags/tag1/page/1/index.html", `refresh" content="0; url=http://example.com/blog/en/tags/tag1/"`)
+ b.AssertFileContent(
+ pathMod("public/fr/plaques/FRtag1/page/2/index.html"), "List Page 2", "Bonjour",
+ pathMod("http://example.com/blog/fr/plaques/FRtag1/"))
+ b.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)
+ b.AssertFileContent("public/nn/side/1/index.html", `refresh" content="0; url=http://example.com/blog/nn/"`)
+ b.AssertFileContent("public/nb/side/1/index.html", `refresh" content="0; url=http://example.com/blog/nb/"`)
+}
+
+func TestMultiSitesWithTwoLanguages(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+ b := newTestSitesBuilder(t).WithConfigFile("toml", `
+
+defaultContentLanguage = "nn"
+
+[languages]
+[languages.nn]
+languageName = "Nynorsk"
+weight = 1
+title = "Tittel på Nynorsk"
+[languages.nn.params]
+p1 = "p1nn"
+
+[languages.en]
+title = "Title in English"
+languageName = "English"
+weight = 2
+[languages.en.params]
+p1 = "p1en"
+`)
+
+ b.CreateSites()
+ b.Build(BuildCfg{SkipRender: true})
+ sites := b.H.Sites
+
+ assert.Len(sites, 2)
+
+ nnSite := sites[0]
+ nnHome := nnSite.getPage(page.KindHome)
+ assert.Len(nnHome.AllTranslations(), 2)
+ assert.Len(nnHome.Translations(), 1)
+ assert.True(nnHome.IsTranslated())
+
+ enHome := sites[1].getPage(page.KindHome)
+
+ p1, err := enHome.Param("p1")
+ assert.NoError(err)
+ assert.Equal("p1en", p1)
+
+ p1, err = nnHome.Param("p1")
+ assert.NoError(err)
+ assert.Equal("p1nn", p1)
+}
+
+func TestMultiSitesBuild(t *testing.T) {
+
+ for _, config := range []struct {
+ content string
+ suffix string
+ }{
+ {multiSiteTOMLConfigTemplate, "toml"},
+ {multiSiteYAMLConfigTemplate, "yml"},
+ {multiSiteJSONConfigTemplate, "json"},
+ } {
+
+ t.Run(config.suffix, func(t *testing.T) {
+ t.Parallel()
+ doTestMultiSitesBuild(t, config.content, config.suffix)
+ })
+ }
+}
+
+func doTestMultiSitesBuild(t *testing.T, configTemplate, configSuffix string) {
+ assert := require.New(t)
+
+ b := newMultiSiteTestBuilder(t, configSuffix, configTemplate, nil)
+ b.CreateSites()
+
+ sites := b.H.Sites
+ assert.Equal(4, len(sites))
+
+ b.Build(BuildCfg{})
+
+ // Check site config
+ for _, s := range sites {
+ require.True(t, s.Info.defaultContentLanguageInSubdir, s.Info.title)
+ require.NotNil(t, s.disabledKinds)
+ }
+
+ gp1 := b.H.GetContentPage(filepath.FromSlash("content/sect/doc1.en.md"))
+ require.NotNil(t, gp1)
+ require.Equal(t, "doc1", gp1.Title())
+ gp2 := b.H.GetContentPage(filepath.FromSlash("content/dummysect/notfound.md"))
+ require.Nil(t, gp2)
+
+ enSite := sites[0]
+ enSiteHome := enSite.getPage(page.KindHome)
+ require.True(t, enSiteHome.IsTranslated())
+
+ require.Equal(t, "en", enSite.language.Lang)
+
+ assert.Equal(5, len(enSite.RegularPages()))
+ assert.Equal(32, len(enSite.AllPages()))
+
+ // Check 404s
+ b.AssertFileContent("public/en/404.html", "404|en|404 Page not found")
+ b.AssertFileContent("public/fr/404.html", "404|fr|404 Page not found")
+
+ // Check robots.txt
+ b.AssertFileContent("public/en/robots.txt", "robots|en|")
+ b.AssertFileContent("public/nn/robots.txt", "robots|nn|")
+
+ b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Permalink: http://example.com/blog/en/sect/doc1-slug/")
+ b.AssertFileContent("public/en/sect/doc2/index.html", "Permalink: http://example.com/blog/en/sect/doc2/")
+ b.AssertFileContent("public/superbob/index.html", "Permalink: http://example.com/blog/superbob/")
+
+ doc2 := enSite.RegularPages()[1]
+ doc3 := enSite.RegularPages()[2]
+ require.Equal(t, doc2.Prev(), doc3, "doc3 should follow doc2, in .PrevPage")
+ doc1en := enSite.RegularPages()[0]
+ doc1fr := doc1en.Translations()[0]
+ b.AssertFileContent("public/fr/sect/doc1/index.html", "Permalink: http://example.com/blog/fr/sect/doc1/")
+
+ 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]
+ require.Len(t, doc4.Translations(), 0, "found translations for doc4")
+
+ // 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[1]
+
+ require.Equal(t, "fr", frSite.language.Lang)
+ require.Len(t, frSite.RegularPages(), 4, "should have 3 pages")
+ require.Len(t, frSite.AllPages(), 32, "should have 32 total pages (including translations and nodes)")
+
+ for _, frenchPage := range frSite.RegularPages() {
+ p := frenchPage
+ require.Equal(t, "fr", p.Language().Lang)
+ }
+
+ // See https://github.com/gohugoio/hugo/issues/4285
+ // Before Hugo 0.33 you had to be explicit with the content path to get the correct Page, which
+ // isn't ideal in a multilingual setup. You want a way to get the current language version if available.
+ // Now you can do lookups with translation base name to get that behaviour.
+ // Let us test all the regular page variants:
+ getPageDoc1En := enSite.getPage(page.KindPage, filepath.ToSlash(doc1en.File().Path()))
+ getPageDoc1EnBase := enSite.getPage(page.KindPage, "sect/doc1")
+ getPageDoc1Fr := frSite.getPage(page.KindPage, filepath.ToSlash(doc1fr.File().Path()))
+ getPageDoc1FrBase := frSite.getPage(page.KindPage, "sect/doc1")
+ require.Equal(t, doc1en, getPageDoc1En)
+ require.Equal(t, doc1fr, getPageDoc1Fr)
+ require.Equal(t, doc1en, getPageDoc1EnBase)
+ require.Equal(t, doc1fr, getPageDoc1FrBase)
+
+ // Check redirect to main language, French
+ b.AssertFileContent("public/index.html", "0; url=http://example.com/blog/fr")
+
+ // check home page content (including data files rendering)
+ b.AssertFileContent("public/en/index.html", "Default Home Page 1", "Hello", "Hugo Rocks!")
+ b.AssertFileContent("public/fr/index.html", "French Home Page 1", "Bonjour", "Hugo Rocks!")
+
+ // check single page content
+ b.AssertFileContent("public/fr/sect/doc1/index.html", "Single", "Shortcode: Bonjour", "LingoFrench")
+ b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Shortcode: Hello", "LingoDefault")
+
+ // Check node translations
+ homeEn := enSite.getPage(page.KindHome)
+ require.NotNil(t, homeEn)
+ require.Len(t, homeEn.Translations(), 3)
+ require.Equal(t, "fr", homeEn.Translations()[0].Language().Lang)
+ require.Equal(t, "nn", homeEn.Translations()[1].Language().Lang)
+ require.Equal(t, "På nynorsk", homeEn.Translations()[1].Title())
+ require.Equal(t, "nb", homeEn.Translations()[2].Language().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(page.KindSection, "sect")
+ require.NotNil(t, sectFr)
+
+ require.Equal(t, "fr", sectFr.Language().Lang)
+ require.Len(t, sectFr.Translations(), 1)
+ require.Equal(t, "en", sectFr.Translations()[0].Language().Lang)
+ require.Equal(t, "Sects", sectFr.Translations()[0].Title())
+
+ nnSite := sites[2]
+ require.Equal(t, "nn", nnSite.language.Lang)
+ taxNn := nnSite.getPage(page.KindTaxonomyTerm, "lag")
+ require.NotNil(t, taxNn)
+ require.Len(t, taxNn.Translations(), 1)
+ require.Equal(t, "nb", taxNn.Translations()[0].Language().Lang)
+
+ taxTermNn := nnSite.getPage(page.KindTaxonomy, "lag", "sogndal")
+ require.NotNil(t, taxTermNn)
+ require.Equal(t, taxTermNn, nnSite.getPage(page.KindTaxonomy, "LAG", "SOGNDAL"))
+ require.Len(t, taxTermNn.Translations(), 1)
+ require.Equal(t, "nb", taxTermNn.Translations()[0].Language().Lang)
+
+ // Check sitemap(s)
+ b.AssertFileContent("public/sitemap.xml",
+ "<loc>http://example.com/blog/en/sitemap.xml</loc>",
+ "<loc>http://example.com/blog/fr/sitemap.xml</loc>")
+ b.AssertFileContent("public/en/sitemap.xml", "http://example.com/blog/en/sect/doc2/")
+ b.AssertFileContent("public/fr/sitemap.xml", "http://example.com/blog/fr/sect/doc1/")
+
+ // 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"])
+ b.AssertFileContent("public/fr/plaques/FRtag1/index.html", "FRtag1|Bonjour|http://example.com/blog/fr/plaques/FRtag1/")
+ b.AssertFileContent("public/en/tags/tag1/index.html", "tag1|Hello|http://example.com/blog/en/tags/tag1/")
+
+ // Check Blackfriday config
+ require.True(t, strings.Contains(content(doc1fr), "&laquo;"), content(doc1fr))
+ require.False(t, strings.Contains(content(doc1en), "&laquo;"), content(doc1en))
+ require.True(t, strings.Contains(content(doc1en), "&ldquo;"), content(doc1en))
+
+ // 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 #3108
+ prevPage := enSite.RegularPages()[0].Prev()
+ require.NotNil(t, prevPage)
+ require.Equal(t, page.KindPage, prevPage.Kind())
+
+ for {
+ if prevPage == nil {
+ break
+ }
+ require.Equal(t, page.KindPage, prevPage.Kind())
+ prevPage = prevPage.Prev()
+ }
+
+ // Check bundles
+ b.AssertFileContent("public/fr/bundles/b1/index.html", "RelPermalink: /blog/fr/bundles/b1/|")
+ bundleFr := frSite.getPage(page.KindPage, "bundles/b1/index.md")
+ require.NotNil(t, bundleFr)
+ require.Equal(t, 1, len(bundleFr.Resources()))
+ logoFr := bundleFr.Resources().GetMatch("logo*")
+ require.NotNil(t, logoFr)
+ b.AssertFileContent("public/fr/bundles/b1/index.html", "Resources: image/png: /blog/fr/bundles/b1/logo.png")
+ b.AssertFileContent("public/fr/bundles/b1/logo.png", "PNG Data")
+
+ bundleEn := enSite.getPage(page.KindPage, "bundles/b1/index.en.md")
+ require.NotNil(t, bundleEn)
+ b.AssertFileContent("public/en/bundles/b1/index.html", "RelPermalink: /blog/en/bundles/b1/|")
+ require.Equal(t, 1, len(bundleEn.Resources()))
+ logoEn := bundleEn.Resources().GetMatch("logo*")
+ require.NotNil(t, logoEn)
+ b.AssertFileContent("public/en/bundles/b1/index.html", "Resources: image/png: /blog/en/bundles/b1/logo.png")
+ b.AssertFileContent("public/en/bundles/b1/logo.png", "PNG Data")
+
+}
+
+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, 10*time.Second)()
+ }
+
+ assert := require.New(t)
+
+ b := newMultiSiteTestDefaultBuilder(t).Running().CreateSites().Build(BuildCfg{})
+
+ sites := b.H.Sites
+ fs := b.Fs
+
+ b.AssertFileContent("public/en/sect/doc2/index.html", "Single: doc2|Hello|en|", "\n\n<h1 id=\"doc2\">doc2</h1>\n\n<p><em>some content</em>")
+
+ enSite := sites[0]
+ frSite := sites[1]
+
+ assert.Len(enSite.RegularPages(), 5)
+ assert.Len(frSite.RegularPages(), 4)
+
+ // Verify translations
+ b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Hello")
+ b.AssertFileContent("public/fr/sect/doc1/index.html", "Bonjour")
+
+ // check single page content
+ b.AssertFileContent("public/fr/sect/doc1/index.html", "Single", "Shortcode: Bonjour")
+ b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Shortcode: Hello")
+
+ homeEn := enSite.getPage(page.KindHome)
+ require.NotNil(t, homeEn)
+ assert.Len(homeEn.Translations(), 3)
+
+ contentFs := b.H.BaseFs.Content.Fs
+
+ 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
+ {
+ func(t *testing.T) {
+ fs.Source.Remove("content/sect/doc2.en.md")
+ },
+ []fsnotify.Event{{Name: filepath.FromSlash("content/sect/doc2.en.md"), Op: fsnotify.Remove}},
+ func(t *testing.T) {
+ assert.Len(enSite.RegularPages(), 4, "1 en removed")
+
+ // Check build stats
+ require.Equal(t, 1, enSite.buildStats.draftCount, "Draft")
+ require.Equal(t, 1, enSite.buildStats.futureCount, "Future")
+ require.Equal(t, 1, enSite.buildStats.expiredCount, "Expired")
+ require.Equal(t, 0, frSite.buildStats.draftCount, "Draft")
+ require.Equal(t, 1, frSite.buildStats.futureCount, "Future")
+ require.Equal(t, 1, frSite.buildStats.expiredCount, "Expired")
+ },
+ },
+ {
+ func(t *testing.T) {
+ writeNewContentFile(t, contentFs, "new_en_1", "2016-07-31", "new1.en.md", -5)
+ writeNewContentFile(t, contentFs, "new_en_2", "1989-07-30", "new2.en.md", -10)
+ writeNewContentFile(t, contentFs, "new_fr_1", "2016-07-30", "new1.fr.md", 10)
+ },
+ []fsnotify.Event{
+ {Name: filepath.FromSlash("content/new1.en.md"), Op: fsnotify.Create},
+ {Name: filepath.FromSlash("content/new2.en.md"), Op: fsnotify.Create},
+ {Name: filepath.FromSlash("content/new1.fr.md"), Op: fsnotify.Create},
+ },
+ func(t *testing.T) {
+ assert.Len(enSite.RegularPages(), 6)
+ assert.Len(enSite.AllPages(), 34)
+ assert.Len(frSite.RegularPages(), 5)
+ 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 := "sect/doc1.en.md"
+ doc1 := readFileFromFs(t, contentFs, p)
+ doc1 += "CHANGED"
+ writeToFs(t, contentFs, p, doc1)
+ },
+ []fsnotify.Event{{Name: filepath.FromSlash("content/sect/doc1.en.md"), Op: fsnotify.Write}},
+ func(t *testing.T) {
+ assert.Len(enSite.RegularPages(), 6)
+ 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 := contentFs.Rename("new1.en.md", "new1renamed.en.md"); err != nil {
+ t.Fatalf("Rename failed: %s", err)
+ }
+ },
+ []fsnotify.Event{
+ {Name: filepath.FromSlash("content/new1renamed.en.md"), Op: fsnotify.Rename},
+ {Name: filepath.FromSlash("content/new1.en.md"), Op: fsnotify.Rename},
+ },
+ func(t *testing.T) {
+ assert.Len(enSite.RegularPages(), 6, "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: filepath.FromSlash("layouts/_default/single.html"), Op: fsnotify.Write}},
+ func(t *testing.T) {
+ assert.Len(enSite.RegularPages(), 6)
+ assert.Len(enSite.AllPages(), 34)
+ assert.Len(frSite.RegularPages(), 5)
+ 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: filepath.FromSlash("i18n/fr.yaml"), Op: fsnotify.Write}},
+ func(t *testing.T) {
+ assert.Len(enSite.RegularPages(), 6)
+ assert.Len(enSite.AllPages(), 34)
+ assert.Len(frSite.RegularPages(), 5)
+ 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(page.KindHome)
+ require.NotNil(t, homeEn)
+ assert.Len(homeEn.Translations(), 3)
+ require.Equal(t, "fr", homeEn.Translations()[0].Language().Lang)
+
+ },
+ },
+ // Change a shortcode
+ {
+ func(t *testing.T) {
+ writeSource(t, fs, "layouts/shortcodes/shortcode.html", "Modified Shortcode: {{ i18n \"hello\" }}")
+ },
+ []fsnotify.Event{
+ {Name: filepath.FromSlash("layouts/shortcodes/shortcode.html"), Op: fsnotify.Write},
+ },
+ func(t *testing.T) {
+ assert.Len(enSite.RegularPages(), 6)
+ assert.Len(enSite.AllPages(), 34)
+ assert.Len(frSite.RegularPages(), 5)
+ b.AssertFileContent("public/fr/sect/doc1/index.html", "Single", "Modified Shortcode: Salut")
+ b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Modified Shortcode: Hello")
+ },
+ },
+ } {
+
+ if this.preFunc != nil {
+ this.preFunc(t)
+ }
+
+ err := b.H.Build(BuildCfg{}, this.events...)
+
+ if err != nil {
+ t.Fatalf("[%d] Failed to rebuild sites: %s", i, err)
+ }
+
+ this.assertFunc(t)
+ }
+
+}
+
+func TestAddNewLanguage(t *testing.T) {
+ t.Parallel()
+ assert := require.New(t)
+
+ b := newMultiSiteTestDefaultBuilder(t)
+ b.CreateSites().Build(BuildCfg{})
+
+ fs := b.Fs
+
+ newConfig := multiSiteTOMLConfigTemplate + `
+
+[Languages.sv]
+weight = 15
+title = "Svenska"
+`
+
+ writeNewContentFile(t, fs.Source, "Swedish Contentfile", "2016-01-01", "content/sect/doc1.sv.md", 10)
+ // replace the config
+ b.WithNewConfig(newConfig)
+
+ sites := b.H
+
+ assert.NoError(b.LoadConfig())
+ err := b.H.Build(BuildCfg{NewConfig: b.Cfg})
+
+ 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(page.KindHome)
+ require.NotNil(t, homeEn)
+ require.Len(t, homeEn.Translations(), 4)
+
+ require.Equal(t, "sv", homeEn.Translations()[0].Language().Lang)
+
+ require.Len(t, enSite.RegularPages(), 5)
+ require.Len(t, frSite.RegularPages(), 4)
+
+ // 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.Language().Lang)
+ require.Len(t, svPage.Translations(), 2)
+ require.Len(t, svPage.AllTranslations(), 3)
+ require.Equal(t, "en", svPage.Translations()[0].Language().Lang)
+
+ // Regular pages have no children
+ require.Len(t, svPage.Pages(), 0)
+ require.Len(t, svPage.Data().(page.Data).Pages(), 0)
+
+}
+
+func TestChangeDefaultLanguage(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ b := newMultiSiteTestBuilder(t, "", "", map[string]interface{}{
+ "DefaultContentLanguage": "fr",
+ "DefaultContentLanguageInSubdir": false,
+ })
+ b.CreateSites().Build(BuildCfg{})
+
+ b.AssertFileContent("public/sect/doc1/index.html", "Single", "Bonjour")
+ b.AssertFileContent("public/en/sect/doc2/index.html", "Single", "Hello")
+
+ // Switch language
+ b.WithNewConfigData(map[string]interface{}{
+ "DefaultContentLanguage": "en",
+ "DefaultContentLanguageInSubdir": false,
+ })
+
+ assert.NoError(b.LoadConfig())
+ err := b.H.Build(BuildCfg{NewConfig: b.Cfg})
+
+ if err != nil {
+ t.Fatalf("Failed to rebuild sites: %s", err)
+ }
+
+ // Default language is now en, so that should now be the "root" language
+ b.AssertFileContent("public/fr/sect/doc1/index.html", "Single", "Bonjour")
+ b.AssertFileContent("public/sect/doc2/index.html", "Single", "Hello")
+}
+
+// https://github.com/gohugoio/hugo/issues/4706
+func TestContentStressTest(t *testing.T) {
+ b := newTestSitesBuilder(t)
+
+ numPages := 500
+
+ contentTempl := `
+---
+%s
+title: %q
+weight: %d
+multioutput: %t
+---
+
+# Header
+
+CONTENT
+
+The End.
+`
+
+ contentTempl = strings.Replace(contentTempl, "CONTENT", strings.Repeat(`
+
+## Another header
+
+Some text. Some more text.
+
+`, 100), -1)
+
+ var content []string
+ defaultOutputs := `outputs: ["html", "json", "rss" ]`
+
+ for i := 1; i <= numPages; i++ {
+ outputs := defaultOutputs
+ multioutput := true
+ if i%3 == 0 {
+ outputs = `outputs: ["json"]`
+ multioutput = false
+ }
+ section := "s1"
+ if i%10 == 0 {
+ section = "s2"
+ }
+ content = append(content, []string{fmt.Sprintf("%s/page%d.md", section, i), fmt.Sprintf(contentTempl, outputs, fmt.Sprintf("Title %d", i), i, multioutput)}...)
+ }
+
+ content = append(content, []string{"_index.md", fmt.Sprintf(contentTempl, defaultOutputs, fmt.Sprintf("Home %d", 0), 0, true)}...)
+ content = append(content, []string{"s1/_index.md", fmt.Sprintf(contentTempl, defaultOutputs, fmt.Sprintf("S %d", 1), 1, true)}...)
+ content = append(content, []string{"s2/_index.md", fmt.Sprintf(contentTempl, defaultOutputs, fmt.Sprintf("S %d", 2), 2, true)}...)
+
+ b.WithSimpleConfigFile()
+ b.WithTemplates("layouts/_default/single.html", `Single: {{ .Content }}|RelPermalink: {{ .RelPermalink }}|Permalink: {{ .Permalink }}`)
+ b.WithTemplates("layouts/_default/myview.html", `View: {{ len .Content }}`)
+ b.WithTemplates("layouts/_default/single.json", `Single JSON: {{ .Content }}|RelPermalink: {{ .RelPermalink }}|Permalink: {{ .Permalink }}`)
+ b.WithTemplates("layouts/_default/list.html", `
+Page: {{ .Paginator.PageNumber }}
+P: {{ with .File }}{{ path.Join .Path }}{{ end }}
+List: {{ len .Paginator.Pages }}|List Content: {{ len .Content }}
+{{ $shuffled := where .Site.RegularPages "Params.multioutput" true | shuffle }}
+{{ $first5 := $shuffled | first 5 }}
+L1: {{ len .Site.RegularPages }} L2: {{ len $first5 }}
+{{ range $i, $e := $first5 }}
+Render {{ $i }}: {{ .Render "myview" }}
+{{ end }}
+END
+`)
+
+ b.WithContent(content...)
+
+ b.CreateSites().Build(BuildCfg{})
+
+ contentMatchers := []string{"<h2 id=\"another-header\">Another header</h2>", "<h2 id=\"another-header-99\">Another header</h2>", "<p>The End.</p>"}
+
+ for i := 1; i <= numPages; i++ {
+ if i%3 != 0 {
+ section := "s1"
+ if i%10 == 0 {
+ section = "s2"
+ }
+ checkContent(b, fmt.Sprintf("public/%s/page%d/index.html", section, i), contentMatchers...)
+ }
+ }
+
+ for i := 1; i <= numPages; i++ {
+ section := "s1"
+ if i%10 == 0 {
+ section = "s2"
+ }
+ checkContent(b, fmt.Sprintf("public/%s/page%d/index.json", section, i), contentMatchers...)
+ }
+
+ checkContent(b, "public/s1/index.html", "P: s1/_index.md\nList: 10|List Content: 8335\n\n\nL1: 500 L2: 5\n\nRender 0: View: 8335\n\nRender 1: View: 8335\n\nRender 2: View: 8335\n\nRender 3: View: 8335\n\nRender 4: View: 8335\n\nEND\n")
+ checkContent(b, "public/s2/index.html", "P: s2/_index.md\nList: 10|List Content: 8335", "Render 4: View: 8335\n\nEND")
+ checkContent(b, "public/index.html", "P: _index.md\nList: 10|List Content: 8335", "4: View: 8335\n\nEND")
+
+ // Check paginated pages
+ for i := 2; i <= 9; i++ {
+ checkContent(b, fmt.Sprintf("public/page/%d/index.html", i), fmt.Sprintf("Page: %d", i), "Content: 8335\n\n\nL1: 500 L2: 5\n\nRender 0: View: 8335", "Render 4: View: 8335\n\nEND")
+ }
+}
+
+func checkContent(s *sitesBuilder, filename string, matches ...string) {
+ content := readDestination(s.T, s.Fs, filename)
+ for _, match := range matches {
+ if !strings.Contains(content, match) {
+ s.Fatalf("No match for %q in content for %s\n%q", match, filename, content)
+ }
+ }
+
+}
+
+func TestTranslationsFromContentToNonContent(t *testing.T) {
+ b := newTestSitesBuilder(t)
+ b.WithConfigFile("toml", `
+
+baseURL = "http://example.com/"
+
+defaultContentLanguage = "en"
+
+[languages]
+[languages.en]
+weight = 10
+contentDir = "content/en"
+[languages.nn]
+weight = 20
+contentDir = "content/nn"
+
+
+`)
+
+ b.WithContent("en/mysection/_index.md", `
+---
+Title: My Section
+---
+
+`)
+
+ b.WithContent("en/_index.md", `
+---
+Title: My Home
+---
+
+`)
+
+ b.WithContent("en/categories/mycat/_index.md", `
+---
+Title: My MyCat
+---
+
+`)
+
+ b.WithContent("en/categories/_index.md", `
+---
+Title: My categories
+---
+
+`)
+
+ for _, lang := range []string{"en", "nn"} {
+
+ b.WithContent(lang+"/mysection/page.md", `
+---
+Title: My Page
+categories: ["mycat"]
+---
+
+`)
+
+ }
+
+ b.Build(BuildCfg{})
+
+ for _, path := range []string{
+ "/",
+ "/mysection",
+ "/categories",
+ "/categories/mycat",
+ } {
+
+ t.Run(path, func(t *testing.T) {
+ assert := require.New(t)
+
+ s1, _ := b.H.Sites[0].getPageNew(nil, path)
+ s2, _ := b.H.Sites[1].getPageNew(nil, path)
+
+ assert.NotNil(s1)
+ assert.NotNil(s2)
+
+ assert.Equal(1, len(s1.Translations()))
+ assert.Equal(1, len(s2.Translations()))
+ assert.Equal(s2, s1.Translations()[0])
+ assert.Equal(s1, s2.Translations()[0])
+
+ m1 := s1.Translations().MergeByLanguage(s2.Translations())
+ m2 := s2.Translations().MergeByLanguage(s1.Translations())
+
+ assert.Equal(1, len(m1))
+ assert.Equal(1, len(m2))
+ })
+
+ }
+}
+
+// https://github.com/gohugoio/hugo/issues/5777
+func TestTableOfContentsInShortcodes(t *testing.T) {
+ t.Parallel()
+
+ b := newMultiSiteTestDefaultBuilder(t)
+
+ b.WithTemplatesAdded("layouts/shortcodes/toc.html", tocShortcode)
+ b.WithTemplatesAdded("layouts/shortcodes/wrapper.html", "{{ .Inner }}")
+ b.WithContent("post/simple.en.md", tocPageSimple)
+ b.WithContent("post/variants1.en.md", tocPageVariants1)
+ b.WithContent("post/variants2.en.md", tocPageVariants2)
+
+ b.WithContent("post/withSCInHeading.en.md", tocPageWithShortcodesInHeadings)
+
+ b.CreateSites().Build(BuildCfg{})
+
+ b.AssertFileContent("public/en/post/simple/index.html",
+ tocPageSimpleExpected,
+ // Make sure it is inserted twice
+ `TOC1: <nav id="TableOfContents">`,
+ `TOC2: <nav id="TableOfContents">`,
+ )
+
+ b.AssertFileContentFn("public/en/post/variants1/index.html", func(s string) bool {
+ return strings.Count(s, "TableOfContents") == 4
+ })
+ b.AssertFileContentFn("public/en/post/variants2/index.html", func(s string) bool {
+ return strings.Count(s, "TableOfContents") == 6
+ })
+
+ b.AssertFileContent("public/en/post/withSCInHeading/index.html", tocPageWithShortcodesInHeadingsExpected)
+}
+
+var tocShortcode = `
+TOC1: {{ .Page.TableOfContents }}
+
+TOC2: {{ .Page.TableOfContents }}
+`
+
+func TestSelfReferencedContentInShortcode(t *testing.T) {
+ t.Parallel()
+
+ b := newMultiSiteTestDefaultBuilder(t)
+
+ var (
+ shortcode = `{{- .Page.Content -}}{{- .Page.Summary -}}{{- .Page.Plain -}}{{- .Page.PlainWords -}}{{- .Page.WordCount -}}{{- .Page.ReadingTime -}}`
+
+ page = `---
+title: sctest
+---
+Empty:{{< mycontent >}}:
+`
+ )
+
+ b.WithTemplatesAdded("layouts/shortcodes/mycontent.html", shortcode)
+ b.WithContent("post/simple.en.md", page)
+
+ b.CreateSites().Build(BuildCfg{})
+
+ b.AssertFileContent("public/en/post/simple/index.html", "Empty:[]00:")
+}
+
+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 tocPageVariants1 = `---
+title: tocTest
+publishdate: "2000-01-01"
+---
+Variant 1:
+{{% wrapper %}}
+{{< toc >}}
+{{% /wrapper %}}
+# Heading 1
+
+Variant 3:
+{{% toc %}}
+
+`
+
+var tocPageVariants2 = `---
+title: tocTest
+publishdate: "2000-01-01"
+---
+Variant 1:
+{{% wrapper %}}
+{{< toc >}}
+{{% /wrapper %}}
+# Heading 1
+
+Variant 2:
+{{< wrapper >}}
+{{< toc >}}
+{{< /wrapper >}}
+
+Variant 3:
+{{% toc %}}
+
+`
+
+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"
+
+paginate = 1
+disablePathToLower = true
+defaultContentLanguage = "{{ .DefaultContentLanguage }}"
+defaultContentLanguageInSubdir = {{ .DefaultContentLanguageInSubdir }}
+enableRobotsTXT = true
+
+[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"
+
+disablePathToLower: true
+paginate: 1
+defaultContentLanguage: "{{ .DefaultContentLanguage }}"
+defaultContentLanguageInSubdir: {{ .DefaultContentLanguageInSubdir }}
+enableRobotsTXT: 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"
+ 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"
+
+`
+
+// TODO(bep) clean move
+var multiSiteJSONConfigTemplate = `
+{
+ "baseURL": "http://example.com/blog",
+ "paginate": 1,
+ "disablePathToLower": true,
+ "defaultContentLanguage": "{{ .DefaultContentLanguage }}",
+ "defaultContentLanguageInSubdir": true,
+ "enableRobotsTXT": 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 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.Clean(filename)
+ b, err := afero.ReadFile(fs, filename)
+ if err != nil {
+ // Print some debug info
+ hadSlash := strings.HasPrefix(filename, helpers.FilePathSeparator)
+ start := 0
+ if hadSlash {
+ start = 1
+ }
+ end := start + 1
+
+ parts := strings.Split(filename, helpers.FilePathSeparator)
+ if parts[start] == "work" {
+ end++
+ }
+
+ root := filepath.Join(parts[start:end]...)
+ if hadSlash {
+ root = helpers.FilePathSeparator + root
+ }
+
+ helpers.PrintFs(fs, root, os.Stdout)
+ Fatalf(t, "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 afero.Fs, title, date, filename string, weight int) {
+ content := newTestPage(title, date, weight)
+ writeToFs(t, fs, filename, content)
+}
+
+type multiSiteTestBuilder struct {
+ configData interface{}
+ config string
+ configFormat string
+
+ *sitesBuilder
+}
+
+func newMultiSiteTestDefaultBuilder(t testing.TB) *multiSiteTestBuilder {
+ return newMultiSiteTestBuilder(t, "", "", nil)
+}
+
+func (b *multiSiteTestBuilder) WithNewConfig(config string) *multiSiteTestBuilder {
+ b.WithConfigTemplate(b.configData, b.configFormat, config)
+ return b
+}
+
+func (b *multiSiteTestBuilder) WithNewConfigData(data interface{}) *multiSiteTestBuilder {
+ b.WithConfigTemplate(data, b.configFormat, b.config)
+ return b
+}
+
+func newMultiSiteTestBuilder(t testing.TB, configFormat, config string, configData interface{}) *multiSiteTestBuilder {
+ if configData == nil {
+ configData = map[string]interface{}{
+ "DefaultContentLanguage": "fr",
+ "DefaultContentLanguageInSubdir": true,
+ }
+ }
+
+ if config == "" {
+ config = multiSiteTOMLConfigTemplate
+ }
+
+ if configFormat == "" {
+ configFormat = "toml"
+ }
+
+ b := newTestSitesBuilder(t).WithConfigTemplate(configData, configFormat, config)
+ b.WithContent("root.en.md", `---
+title: root
+weight: 10000
+slug: root
+publishdate: "2000-01-01"
+---
+# root
+`,
+ "sect/doc1.en.md", `---
+title: doc1
+weight: 1
+slug: doc1-slug
+tags:
+ - tag1
+publishdate: "2000-01-01"
+---
+# doc1
+*some "content"*
+
+{{< shortcode >}}
+
+{{< lingo >}}
+
+NOTE: slug should be used as URL
+`,
+ "sect/doc1.fr.md", `---
+title: doc1
+weight: 1
+plaques:
+ - FRtag1
+ - FRtag2
+publishdate: "2000-01-04"
+---
+# doc1
+*quelque "contenu"*
+
+{{< shortcode >}}
+
+{{< lingo >}}
+
+NOTE: should be in the 'en' Page's 'Translations' field.
+NOTE: date is after "doc3"
+`,
+ "sect/doc2.en.md", `---
+title: doc2
+weight: 2
+publishdate: "2000-01-02"
+---
+# doc2
+*some content*
+NOTE: without slug, "doc2" should be used, without ".en" as URL
+`,
+ "sect/doc3.en.md", `---
+title: doc3
+weight: 3
+publishdate: "2000-01-03"
+aliases: [/en/al/alias1,/al/alias2/]
+tags:
+ - tag2
+ - tag1
+url: /superbob/
+---
+# doc3
+*some content*
+NOTE: third 'en' doc, should trigger pagination on home page.
+`,
+ "sect/doc4.md", `---
+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'
+`,
+ "other/doc5.fr.md", `---
+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
+ "stats/expired.fr.md", `---
+title: expired
+publishdate: "2000-01-06"
+expiryDate: "2001-01-06"
+---
+# Expired
+`,
+ "stats/future.fr.md", `---
+title: future
+weight: 6
+publishdate: "2100-01-06"
+---
+# Future
+`,
+ "stats/expired.en.md", `---
+title: expired
+weight: 7
+publishdate: "2000-01-06"
+expiryDate: "2001-01-06"
+---
+# Expired
+`,
+ "stats/future.en.md", `---
+title: future
+weight: 6
+publishdate: "2100-01-06"
+---
+# Future
+`,
+ "stats/draft.en.md", `---
+title: expired
+publishdate: "2000-01-06"
+draft: true
+---
+# Draft
+`,
+ "stats/tax.nn.md", `---
+title: Tax NN
+weight: 8
+publishdate: "2000-01-06"
+weight: 1001
+lag:
+- Sogndal
+---
+# Tax NN
+`,
+ "stats/tax.nb.md", `---
+title: Tax NB
+weight: 8
+publishdate: "2000-01-06"
+weight: 1002
+lag:
+- Sogndal
+---
+# Tax NB
+`,
+ // Bundle
+ "bundles/b1/index.en.md", `---
+title: Bundle EN
+publishdate: "2000-01-06"
+weight: 2001
+---
+# Bundle Content EN
+`,
+ "bundles/b1/index.md", `---
+title: Bundle Default
+publishdate: "2000-01-06"
+weight: 2002
+---
+# Bundle Content Default
+`,
+ "bundles/b1/logo.png", `
+PNG Data
+`)
+
+ return &multiSiteTestBuilder{sitesBuilder: b, configFormat: configFormat, config: config, configData: configData}
+}
diff --git a/hugolib/hugo_sites_multihost_test.go b/hugolib/hugo_sites_multihost_test.go
new file mode 100644
index 000000000..999d94559
--- /dev/null
+++ b/hugolib/hugo_sites_multihost_test.go
@@ -0,0 +1,113 @@
+package hugolib
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/resources/page"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestMultihosts(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ var configTemplate = `
+paginate = 1
+disablePathToLower = true
+defaultContentLanguage = "fr"
+defaultContentLanguageInSubdir = false
+staticDir = ["s1", "s2"]
+
+[permalinks]
+other = "/somewhere/else/:filename"
+
+[Taxonomies]
+tag = "tags"
+
+[Languages]
+[Languages.en]
+staticDir2 = ["ens1", "ens2"]
+baseURL = "https://example.com/docs"
+weight = 10
+title = "In English"
+languageName = "English"
+
+[Languages.fr]
+staticDir2 = ["frs1", "frs2"]
+baseURL = "https://example.fr"
+weight = 20
+title = "Le Français"
+languageName = "Français"
+
+[Languages.nn]
+staticDir2 = ["nns1", "nns2"]
+baseURL = "https://example.no"
+weight = 30
+title = "På nynorsk"
+languageName = "Nynorsk"
+
+`
+
+ b := newMultiSiteTestDefaultBuilder(t).WithConfigFile("toml", configTemplate)
+ b.CreateSites().Build(BuildCfg{})
+
+ b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Hello")
+
+ s1 := b.H.Sites[0]
+
+ s1h := s1.getPage(page.KindHome)
+ assert.True(s1h.IsTranslated())
+ assert.Len(s1h.Translations(), 2)
+ assert.Equal("https://example.com/docs/", s1h.Permalink())
+
+ // For “regular multilingual” we kept the aliases pages with url in front matter
+ // as a literal value that we use as is.
+ // There is an ambiguity in the guessing.
+ // For multihost, we never want any content in the root.
+ //
+ // check url in front matter:
+ pageWithURLInFrontMatter := s1.getPage(page.KindPage, "sect/doc3.en.md")
+ assert.NotNil(pageWithURLInFrontMatter)
+ assert.Equal("/docs/superbob/", pageWithURLInFrontMatter.RelPermalink())
+ b.AssertFileContent("public/en/superbob/index.html", "doc3|Hello|en")
+
+ // check alias:
+ b.AssertFileContent("public/en/al/alias1/index.html", `content="0; url=https://example.com/docs/superbob/"`)
+ b.AssertFileContent("public/en/al/alias2/index.html", `content="0; url=https://example.com/docs/superbob/"`)
+
+ s2 := b.H.Sites[1]
+
+ s2h := s2.getPage(page.KindHome)
+ assert.Equal("https://example.fr/", s2h.Permalink())
+
+ b.AssertFileContent("public/fr/index.html", "French Home Page", "String Resource: /docs/text/pipes.txt")
+ b.AssertFileContent("public/fr/text/pipes.txt", "Hugo Pipes")
+ b.AssertFileContent("public/en/index.html", "Default Home Page", "String Resource: /docs/text/pipes.txt")
+ b.AssertFileContent("public/en/text/pipes.txt", "Hugo Pipes")
+
+ // Check paginators
+ b.AssertFileContent("public/en/page/1/index.html", `refresh" content="0; url=https://example.com/docs/"`)
+ b.AssertFileContent("public/nn/page/1/index.html", `refresh" content="0; url=https://example.no/"`)
+ b.AssertFileContent("public/en/sect/page/2/index.html", "List Page 2", "Hello", "https://example.com/docs/sect/", "\"/docs/sect/page/3/")
+ b.AssertFileContent("public/fr/sect/page/2/index.html", "List Page 2", "Bonjour", "https://example.fr/sect/")
+
+ // Check bundles
+
+ bundleEn := s1.getPage(page.KindPage, "bundles/b1/index.en.md")
+ require.NotNil(t, bundleEn)
+ require.Equal(t, "/docs/bundles/b1/", bundleEn.RelPermalink())
+ require.Equal(t, 1, len(bundleEn.Resources()))
+
+ b.AssertFileContent("public/en/bundles/b1/logo.png", "PNG Data")
+ b.AssertFileContent("public/en/bundles/b1/index.html", " image/png: /docs/bundles/b1/logo.png")
+
+ bundleFr := s2.getPage(page.KindPage, "bundles/b1/index.md")
+ require.NotNil(t, bundleFr)
+ require.Equal(t, "/bundles/b1/", bundleFr.RelPermalink())
+ require.Equal(t, 1, len(bundleFr.Resources()))
+ b.AssertFileContent("public/fr/bundles/b1/logo.png", "PNG Data")
+ b.AssertFileContent("public/fr/bundles/b1/index.html", " image/png: /bundles/b1/logo.png")
+
+}
diff --git a/hugolib/hugo_sites_rebuild_test.go b/hugolib/hugo_sites_rebuild_test.go
new file mode 100644
index 000000000..4a81fe950
--- /dev/null
+++ b/hugolib/hugo_sites_rebuild_test.go
@@ -0,0 +1,83 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+)
+
+func TestSitesRebuild(t *testing.T) {
+
+ configFile := `
+baseURL = "https://example.com"
+title = "Rebuild this"
+contentDir = "content"
+
+
+`
+
+ contentFilename := "content/blog/page1.md"
+
+ b := newTestSitesBuilder(t).WithConfigFile("toml", configFile)
+
+ // To simulate https://github.com/gohugoio/hugo/issues/5838, the home page
+ // needs a content page.
+ b.WithContent("content/_index.md", `---
+title: Home, Sweet Home!
+---
+
+`)
+
+ b.WithContent(contentFilename, `
+---
+title: "Page 1"
+summary: "Initial summary"
+paginate: 3
+---
+
+Content.
+
+`)
+
+ b.WithTemplatesAdded("index.html", `
+{{ range (.Paginate .Site.RegularPages).Pages }}
+* Page Paginate: {{ .Title }}|Summary: {{ .Summary }}|Content: {{ .Content }}
+{{ end }}
+{{ range .Pages }}
+* Page Pages: {{ .Title }}|Summary: {{ .Summary }}|Content: {{ .Content }}
+{{ end }}
+`)
+
+ b.Running().Build(BuildCfg{})
+
+ b.AssertFileContent("public/index.html", "* Page Paginate: Page 1|Summary: Initial summary|Content: <p>Content.</p>")
+
+ b.EditFiles(contentFilename, `
+---
+title: "Page 1 edit"
+summary: "Edited summary"
+---
+
+Edited content.
+
+`)
+
+ b.Build(BuildCfg{})
+
+ b.AssertFileContent("public/index.html", "* Page Paginate: Page 1 edit|Summary: Edited summary|Content: <p>Edited content.</p>")
+
+ // https://github.com/gohugoio/hugo/issues/5833
+ b.AssertFileContent("public/index.html", "* Page Pages: Page 1 edit|Summary: Edited summary|Content: <p>Edited content.</p>")
+
+}
diff --git a/hugolib/hugo_smoke_test.go b/hugolib/hugo_smoke_test.go
new file mode 100644
index 000000000..d5b8861ce
--- /dev/null
+++ b/hugolib/hugo_smoke_test.go
@@ -0,0 +1,303 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestSmoke(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ const configFile = `
+baseURL = "https://example.com"
+title = "Simple Site"
+rssLimit = 3
+defaultContentLanguage = "en"
+enableRobotsTXT = true
+
+[languages]
+[languages.en]
+weight = 1
+title = "In English"
+[languages.no]
+weight = 2
+title = "På norsk"
+
+[params]
+hugo = "Rules!"
+
+[outputs]
+ home = ["HTML", "JSON", "CSV", "RSS"]
+
+`
+
+ const pageContentAndSummaryDivider = `---
+title: Page with outputs
+hugo: "Rocks!"
+outputs: ["HTML", "JSON"]
+tags: [ "hugo" ]
+aliases: [ "/a/b/c" ]
+---
+
+This is summary.
+
+<!--more-->
+
+This is content with some shortcodes.
+
+Shortcode 1: {{< sc >}}.
+Shortcode 2: {{< sc >}}.
+
+`
+
+ const pageContentWithMarkdownShortcodes = `---
+title: Page with markdown shortcode
+hugo: "Rocks!"
+outputs: ["HTML", "JSON"]
+---
+
+This is summary.
+
+<!--more-->
+
+This is content[^a].
+
+# Header above
+
+{{% markdown-shortcode %}}
+# Header inside
+
+Some **markdown**.[^b]
+
+{{% /markdown-shortcode %}}
+
+# Heder below
+
+Some more content[^c].
+
+Footnotes:
+
+[^a]: Fn 1
+[^b]: Fn 2
+[^c]: Fn 3
+
+`
+
+ var pageContentAutoSummary = strings.Replace(pageContentAndSummaryDivider, "<!--more-->", "", 1)
+
+ b := newTestSitesBuilder(t).WithConfigFile("toml", configFile)
+ b.WithTemplatesAdded("shortcodes/markdown-shortcode.html", `
+Some **Markdown** in shortcode.
+
+{{ .Inner }}
+
+
+
+`)
+
+ b.WithTemplatesAdded("shortcodes/markdown-shortcode.json", `
+Some **Markdown** in JSON shortcode.
+{{ .Inner }}
+
+`)
+
+ for i := 1; i <= 11; i++ {
+ if i%2 == 0 {
+ b.WithContent(fmt.Sprintf("blog/page%d.md", i), pageContentAndSummaryDivider)
+ b.WithContent(fmt.Sprintf("blog/page%d.no.md", i), pageContentAndSummaryDivider)
+ } else {
+ b.WithContent(fmt.Sprintf("blog/page%d.md", i), pageContentAutoSummary)
+ }
+ }
+
+ for i := 1; i <= 5; i++ {
+ // Root section pages
+ b.WithContent(fmt.Sprintf("root%d.md", i), pageContentAutoSummary)
+ }
+
+ // https://github.com/gohugoio/hugo/issues/4695
+ b.WithContent("blog/markyshort.md", pageContentWithMarkdownShortcodes)
+
+ // Add one bundle
+ b.WithContent("blog/mybundle/index.md", pageContentAndSummaryDivider)
+ b.WithContent("blog/mybundle/mydata.csv", "Bundled CSV")
+
+ const (
+ commonPageTemplate = `|{{ .Kind }}|{{ .Title }}|{{ .Path }}|{{ .Summary }}|{{ .Content }}|RelPermalink: {{ .RelPermalink }}|WordCount: {{ .WordCount }}|Pages: {{ .Pages }}|Data Pages: Pages({{ len .Data.Pages }})|Resources: {{ len .Resources }}|Summary: {{ .Summary }}`
+ commonPaginatorTemplate = `|Paginator: {{ with .Paginator }}{{ .PageNumber }}{{ else }}NIL{{ end }}`
+ commonListTemplateNoPaginator = `|{{ range $i, $e := (.Pages | first 1) }}|Render {{ $i }}: {{ .Kind }}|{{ .Render "li" }}|{{ end }}|Site params: {{ $.Site.Params.hugo }}|RelPermalink: {{ .RelPermalink }}`
+ commonListTemplate = commonPaginatorTemplate + `|{{ range $i, $e := (.Pages | first 1) }}|Render {{ $i }}: {{ .Kind }}|{{ .Render "li" }}|{{ end }}|Site params: {{ $.Site.Params.hugo }}|RelPermalink: {{ .RelPermalink }}`
+ commonShortcodeTemplate = `|{{ .Name }}|{{ .Ordinal }}|{{ .Page.Summary }}|{{ .Page.Content }}|WordCount: {{ .Page.WordCount }}`
+ prevNextTemplate = `|Prev: {{ with .Prev }}{{ .RelPermalink }}{{ end }}|Next: {{ with .Next }}{{ .RelPermalink }}{{ end }}`
+ prevNextInSectionTemplate = `|PrevInSection: {{ with .PrevInSection }}{{ .RelPermalink }}{{ end }}|NextInSection: {{ with .NextInSection }}{{ .RelPermalink }}{{ end }}`
+ paramsTemplate = `|Params: {{ .Params.hugo }}`
+ treeNavTemplate = `|CurrentSection: {{ .CurrentSection }}`
+ )
+
+ b.WithTemplates(
+ "_default/list.html", "HTML: List"+commonPageTemplate+commonListTemplate+"|First Site: {{ .Sites.First.Title }}",
+ "_default/list.json", "JSON: List"+commonPageTemplate+commonListTemplateNoPaginator,
+ "_default/list.csv", "CSV: List"+commonPageTemplate+commonListTemplateNoPaginator,
+ "_default/single.html", "HTML: Single"+commonPageTemplate+prevNextTemplate+prevNextInSectionTemplate+treeNavTemplate,
+ "_default/single.json", "JSON: Single"+commonPageTemplate,
+
+ // For .Render test
+ "_default/li.html", `HTML: LI|{{ strings.Contains .Content "HTML: Shortcode: sc" }}`+paramsTemplate,
+ "_default/li.json", `JSON: LI|{{ strings.Contains .Content "JSON: Shortcode: sc" }}`+paramsTemplate,
+ "_default/li.csv", `CSV: LI|{{ strings.Contains .Content "CSV: Shortcode: sc" }}`+paramsTemplate,
+
+ "404.html", "{{ .Kind }}|{{ .Title }}|Page not found",
+
+ "shortcodes/sc.html", "HTML: Shortcode: "+commonShortcodeTemplate,
+ "shortcodes/sc.json", "JSON: Shortcode: "+commonShortcodeTemplate,
+ "shortcodes/sc.csv", "CSV: Shortcode: "+commonShortcodeTemplate,
+ )
+
+ b.CreateSites().Build(BuildCfg{})
+
+ b.AssertFileContent("public/blog/page1/index.html",
+ "This is content with some shortcodes.",
+ "Page with outputs",
+ "Pages: Pages(0)",
+ "RelPermalink: /blog/page1/|",
+ "Shortcode 1: HTML: Shortcode: |sc|0|||WordCount: 0.",
+ "Shortcode 2: HTML: Shortcode: |sc|1|||WordCount: 0.",
+ "Prev: /blog/page10/|Next: /blog/mybundle/",
+ "PrevInSection: /blog/page10/|NextInSection: /blog/mybundle/",
+ "Summary: This is summary.",
+ "CurrentSection: Page(/blog)",
+ )
+
+ b.AssertFileContent("public/blog/page1/index.json",
+ "JSON: Single|page|Page with outputs|",
+ "SON: Shortcode: |sc|0||")
+
+ b.AssertFileContent("public/index.html",
+ "home|In English",
+ "Site params: Rules",
+ "Pages: Pages(18)|Data Pages: Pages(18)",
+ "Paginator: 1",
+ "First Site: In English",
+ "RelPermalink: /",
+ )
+
+ b.AssertFileContent("public/no/index.html", "home|På norsk", "RelPermalink: /no/")
+
+ // Check RSS
+ rssHome := b.FileContent("public/index.xml")
+ assert.Contains(rssHome, `<atom:link href="https://example.com/index.xml" rel="self" type="application/rss+xml" />`)
+ assert.Equal(3, strings.Count(rssHome, "<item>")) // rssLimit = 3
+
+ // .Render should use template/content from the current output format
+ // even if that output format isn't configured for that page.
+ b.AssertFileContent(
+ "public/index.json",
+ "Render 0: page|JSON: LI|false|Params: Rocks!",
+ )
+
+ b.AssertFileContent(
+ "public/index.html",
+ "Render 0: page|HTML: LI|false|Params: Rocks!|",
+ )
+
+ b.AssertFileContent(
+ "public/index.csv",
+ "Render 0: page|CSV: LI|false|Params: Rocks!|",
+ )
+
+ // Check bundled resources
+ b.AssertFileContent(
+ "public/blog/mybundle/index.html",
+ "Resources: 1",
+ )
+
+ // Check pages in root section
+ b.AssertFileContent(
+ "public/root3/index.html",
+ "Single|page|Page with outputs|root3.md|",
+ "Prev: /root4/|Next: /root2/|PrevInSection: /root4/|NextInSection: /root2/",
+ )
+
+ b.AssertFileContent(
+ "public/root3/index.json", "Shortcode 1: JSON:")
+
+ // Paginators
+ b.AssertFileContent("public/page/1/index.html", `rel="canonical" href="https://example.com/"`)
+ b.AssertFileContent("public/page/2/index.html", "HTML: List|home|In English|", "Paginator: 2")
+
+ // 404
+ b.AssertFileContent("public/404.html", "404|404 Page not found")
+
+ // Sitemaps
+ b.AssertFileContent("public/en/sitemap.xml", "<loc>https://example.com/blog/</loc>")
+ b.AssertFileContent("public/no/sitemap.xml", `hreflang="no"`)
+
+ b.AssertFileContent("public/sitemap.xml", "<loc>https://example.com/en/sitemap.xml</loc>", "<loc>https://example.com/no/sitemap.xml</loc>")
+
+ // robots.txt
+ b.AssertFileContent("public/robots.txt", `User-agent: *`)
+
+ // Aliases
+ b.AssertFileContent("public/a/b/c/index.html", `refresh`)
+
+ // Markdown vs shortcodes
+ // Check that all footnotes are grouped (even those from inside the shortcode)
+ b.AssertFileContentRe("public/blog/markyshort/index.html", `Footnotes:.*<ol>.*Fn 1.*Fn 2.*Fn 3.*</ol>`)
+
+}
+
+// https://github.com/golang/go/issues/30286
+func TestDataRace(t *testing.T) {
+
+ const page = `
+---
+title: "The Page"
+outputs: ["HTML", "JSON"]
+---
+
+The content.
+
+
+ `
+
+ b := newTestSitesBuilder(t).WithSimpleConfigFile()
+ for i := 1; i <= 50; i++ {
+ b.WithContent(fmt.Sprintf("blog/page%d.md", i), page)
+ }
+
+ b.WithContent("_index.md", `
+---
+title: "The Home"
+outputs: ["HTML", "JSON", "CSV", "RSS"]
+---
+
+The content.
+
+
+`)
+
+ commonTemplate := `{{ .Data.Pages }}`
+
+ b.WithTemplatesAdded("_default/single.html", "HTML Single: "+commonTemplate)
+ b.WithTemplatesAdded("_default/list.html", "HTML List: "+commonTemplate)
+
+ b.CreateSites().Build(BuildCfg{})
+}
diff --git a/hugolib/hugo_themes_test.go b/hugolib/hugo_themes_test.go
new file mode 100644
index 000000000..05bfaa692
--- /dev/null
+++ b/hugolib/hugo_themes_test.go
@@ -0,0 +1,268 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "path/filepath"
+ "testing"
+
+ "github.com/gohugoio/hugo/common/loggers"
+)
+
+func TestThemesGraph(t *testing.T) {
+ t.Parallel()
+
+ const (
+ themeStandalone = `
+title = "Theme Standalone"
+[params]
+v1 = "v1s"
+v2 = "v2s"
+`
+ themeCyclic = `
+title = "Theme Cyclic"
+theme = "theme3"
+[params]
+v1 = "v1c"
+v2 = "v2c"
+`
+ theme1 = `
+title = "Theme #1"
+theme = "themeStandalone"
+[params]
+v2 = "v21"
+`
+
+ theme2 = `
+title = "Theme #2"
+theme = "theme1"
+[params]
+v1 = "v12"
+`
+
+ theme3 = `
+title = "Theme #3"
+theme = ["theme2", "themeStandalone", "themeCyclic"]
+[params]
+v1 = "v13"
+v2 = "v24"
+`
+
+ theme4 = `
+title = "Theme #4"
+theme = "theme3"
+[params]
+v1 = "v14"
+v2 = "v24"
+`
+
+ site1 = `
+ theme = "theme4"
+
+ [params]
+ v1 = "site"
+`
+ site2 = `
+ theme = ["theme2", "themeStandalone"]
+`
+ )
+
+ var (
+ testConfigs = []struct {
+ siteConfig string
+
+ // The name of theme somewhere in the middle to write custom key/files.
+ offset string
+
+ check func(b *sitesBuilder)
+ }{
+ {site1, "theme3", func(b *sitesBuilder) {
+
+ // site1: theme4 theme3 theme2 theme1 themeStandalone themeCyclic
+
+ // Check data
+ // theme3 should win the offset competition
+ b.AssertFileContent("public/index.html", "theme1o::[offset][v]theme3", "theme4o::[offset][v]theme3", "themeStandaloneo::[offset][v]theme3")
+ b.AssertFileContent("public/index.html", "nproject::[inner][other]project|[project][other]project|[theme][other]theme4|[theme1][other]theme1")
+ b.AssertFileContent("public/index.html", "ntheme::[inner][other]theme4|[theme][other]theme4|[theme1][other]theme1|[theme2][other]theme2|[theme3][other]theme3")
+ b.AssertFileContent("public/index.html", "theme1::[inner][other]project|[project][other]project|[theme][other]theme1|[theme1][other]theme1|")
+ b.AssertFileContent("public/index.html", "theme4::[inner][other]project|[project][other]project|[theme][other]theme4|[theme4][other]theme4|")
+
+ // Check layouts
+ b.AssertFileContent("public/index.html", "partial ntheme: theme4", "partial theme2o: theme3")
+
+ // Check i18n
+ b.AssertFileContent("public/index.html", "i18n: project theme4")
+
+ // Check static files
+ // TODO(bep) static files not currently part of the build b.AssertFileContent("public/nproject.txt", "TODO")
+
+ // Check site params
+ b.AssertFileContent("public/index.html", "v1::site", "v2::v24")
+ }},
+ {site2, "", func(b *sitesBuilder) {
+
+ // site2: theme2 theme1 themeStandalone
+ b.AssertFileContent("public/index.html", "nproject::[inner][other]project|[project][other]project|[theme][other]theme2|[theme1][other]theme1|[theme2][other]theme2|[themeStandalone][other]themeStandalone|")
+ b.AssertFileContent("public/index.html", "ntheme::[inner][other]theme2|[theme][other]theme2|[theme1][other]theme1|[theme2][other]theme2|[themeStandalone][other]themeStandalone|")
+ b.AssertFileContent("public/index.html", "i18n: project theme2")
+ b.AssertFileContent("public/index.html", "partial ntheme: theme2")
+
+ // Params only set in themes
+ b.AssertFileContent("public/index.html", "v1::v12", "v2::v21")
+
+ }},
+ }
+
+ themeConfigs = []struct {
+ name string
+ config string
+ }{
+ {"themeStandalone", themeStandalone},
+ {"themeCyclic", themeCyclic},
+ {"theme1", theme1},
+ {"theme2", theme2},
+ {"theme3", theme3},
+ {"theme4", theme4},
+ }
+ )
+
+ for i, testConfig := range testConfigs {
+ t.Log(fmt.Sprintf("Test %d", i))
+ b := newTestSitesBuilder(t).WithLogger(loggers.NewErrorLogger())
+ b.WithConfigFile("toml", testConfig.siteConfig)
+
+ for _, tc := range themeConfigs {
+ var variationsNameBase = []string{"nproject", "ntheme", tc.name}
+
+ themeRoot := filepath.Join("themes", tc.name)
+ b.WithSourceFile(filepath.Join(themeRoot, "config.toml"), tc.config)
+
+ b.WithSourceFile(filepath.Join("layouts", "partials", "m.html"), `{{- range $k, $v := . }}{{ $k }}::{{ template "printv" $v }}
+{{ end }}
+{{ define "printv" }}
+{{- $tp := printf "%T" . -}}
+{{- if (strings.HasSuffix $tp "map[string]interface {}") -}}
+{{- range $k, $v := . }}[{{ $k }}]{{ template "printv" $v }}{{ end -}}
+{{- else -}}
+{{- . }}|
+{{- end -}}
+{{ end }}
+`)
+
+ for _, nameVariaton := range variationsNameBase {
+ roots := []string{"", themeRoot}
+
+ for _, root := range roots {
+ name := tc.name
+ if root == "" {
+ name = "project"
+ }
+
+ if nameVariaton == "ntheme" && name == "project" {
+ continue
+ }
+
+ // static
+ b.WithSourceFile(filepath.Join(root, "static", nameVariaton+".txt"), name)
+
+ // layouts
+ if i == 1 {
+ b.WithSourceFile(filepath.Join(root, "layouts", "partials", "theme2o.html"), "Not Set")
+ }
+ b.WithSourceFile(filepath.Join(root, "layouts", "partials", nameVariaton+".html"), name)
+ if root != "" && testConfig.offset == tc.name {
+ for _, tc2 := range themeConfigs {
+ b.WithSourceFile(filepath.Join(root, "layouts", "partials", tc2.name+"o.html"), name)
+ }
+ }
+
+ // i18n + data
+
+ var dataContent string
+ if root == "" {
+ dataContent = fmt.Sprintf(`
+[%s]
+other = %q
+
+[inner]
+other = %q
+
+`, name, name, name)
+ } else {
+ dataContent = fmt.Sprintf(`
+[%s]
+other = %q
+
+[inner]
+other = %q
+
+[theme]
+other = %q
+
+`, name, name, name, name)
+ }
+
+ b.WithSourceFile(filepath.Join(root, "data", nameVariaton+".toml"), dataContent)
+ b.WithSourceFile(filepath.Join(root, "i18n", "en.toml"), dataContent)
+
+ // If an offset is set, duplicate a data key with a winner in the middle.
+ if root != "" && testConfig.offset == tc.name {
+ for _, tc2 := range themeConfigs {
+ dataContent := fmt.Sprintf(`
+[offset]
+v = %q
+`, tc.name)
+ b.WithSourceFile(filepath.Join(root, "data", tc2.name+"o.toml"), dataContent)
+ }
+ }
+ }
+
+ }
+
+ }
+
+ for _, themeConfig := range themeConfigs {
+ b.WithSourceFile(filepath.Join("themes", "config.toml"), themeConfig.config)
+ }
+
+ b.WithContent(filepath.Join("content", "page.md"), `---
+title: "Page"
+---
+
+`)
+
+ homeTpl := `
+data: {{ partial "m" .Site.Data }}
+i18n: {{ i18n "inner" }} {{ i18n "theme" }}
+partial ntheme: {{ partial "ntheme" . }}
+partial theme2o: {{ partial "theme2o" . }}
+params: {{ partial "m" .Site.Params }}
+
+`
+
+ b.WithTemplates(filepath.Join("layouts", "home.html"), homeTpl)
+
+ b.Build(BuildCfg{})
+
+ var _ = os.Stdout
+
+ // printFs(b.H.Deps.BaseFs.LayoutsFs, "", os.Stdout)
+ testConfig.check(b)
+
+ }
+
+}
diff --git a/hugolib/language_content_dir_test.go b/hugolib/language_content_dir_test.go
new file mode 100644
index 000000000..ad1e1fb53
--- /dev/null
+++ b/hugolib/language_content_dir_test.go
@@ -0,0 +1,305 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "path/filepath"
+ "testing"
+
+ "github.com/gohugoio/hugo/resources/page"
+
+ "github.com/stretchr/testify/require"
+)
+
+/*
+
+/en/p1.md
+/nn/p1.md
+
+.Readdir
+
+- Name() => p1.en.md, p1.nn.md
+
+.Stat(name)
+
+.Open() --- real file name
+
+
+*/
+
+func TestLanguageContentRoot(t *testing.T) {
+ t.Parallel()
+ assert := require.New(t)
+
+ config := `
+baseURL = "https://example.org/"
+
+defaultContentLanguage = "en"
+defaultContentLanguageInSubdir = true
+
+contentDir = "content/main"
+workingDir = "/my/project"
+
+[Languages]
+[Languages.en]
+weight = 10
+title = "In English"
+languageName = "English"
+
+[Languages.nn]
+weight = 20
+title = "På Norsk"
+languageName = "Norsk"
+# This tells Hugo that all content in this directory is in the Norwegian language.
+# It does not have to have the "my-page.nn.md" format. It can, but that is optional.
+contentDir = "content/norsk"
+
+[Languages.sv]
+weight = 30
+title = "På Svenska"
+languageName = "Svensk"
+contentDir = "content/svensk"
+`
+
+ pageTemplate := `
+---
+title: %s
+slug: %s
+weight: %d
+---
+
+Content.
+
+SVP3-REF: {{< ref path="/sect/page3.md" lang="sv" >}}
+SVP3-RELREF: {{< relref path="/sect/page3.md" lang="sv" >}}
+
+`
+
+ pageBundleTemplate := `
+---
+title: %s
+weight: %d
+---
+
+Content.
+
+`
+ var contentFiles []string
+ section := "sect"
+
+ var contentRoot = func(lang string) string {
+ switch lang {
+ case "nn":
+ return "content/norsk"
+ case "sv":
+ return "content/svensk"
+ default:
+ return "content/main"
+ }
+
+ }
+
+ var contentSectionRoot = func(lang string) string {
+ return contentRoot(lang) + "/" + section
+ }
+
+ for _, lang := range []string{"en", "nn", "sv"} {
+ for j := 1; j <= 10; j++ {
+ if (lang == "nn" || lang == "en") && j%4 == 0 {
+ // Skip 4 and 8 for nn
+ // We also skip it for en, but that is added to the Swedish directory below.
+ continue
+ }
+
+ if lang == "sv" && j%5 == 0 {
+ // Skip 5 and 10 for sv
+ continue
+ }
+
+ base := fmt.Sprintf("p-%s-%d", lang, j)
+ slug := base
+ langID := ""
+
+ if lang == "sv" && j%4 == 0 {
+ // Put an English page in the Swedish content dir.
+ langID = ".en"
+ }
+
+ if lang == "en" && j == 8 {
+ // This should win over the sv variant above.
+ langID = ".en"
+ }
+
+ slug += langID
+
+ contentRoot := contentSectionRoot(lang)
+
+ filename := filepath.Join(contentRoot, fmt.Sprintf("page%d%s.md", j, langID))
+ contentFiles = append(contentFiles, filename, fmt.Sprintf(pageTemplate, slug, slug, j))
+ }
+ }
+
+ // Put common translations in all of them
+ for i, lang := range []string{"en", "nn", "sv"} {
+ contentRoot := contentSectionRoot(lang)
+
+ slug := fmt.Sprintf("common_%s", lang)
+
+ filename := filepath.Join(contentRoot, "common.md")
+ contentFiles = append(contentFiles, filename, fmt.Sprintf(pageTemplate, slug, slug, 100+i))
+
+ for j, lang2 := range []string{"en", "nn", "sv"} {
+ filename := filepath.Join(contentRoot, fmt.Sprintf("translated_all.%s.md", lang2))
+ langSlug := slug + "_translated_all_" + lang2
+ contentFiles = append(contentFiles, filename, fmt.Sprintf(pageTemplate, langSlug, langSlug, 200+i+j))
+ }
+
+ for j, lang2 := range []string{"sv", "nn"} {
+ if lang == "en" {
+ continue
+ }
+ filename := filepath.Join(contentRoot, fmt.Sprintf("translated_some.%s.md", lang2))
+ langSlug := slug + "_translated_some_" + lang2
+ contentFiles = append(contentFiles, filename, fmt.Sprintf(pageTemplate, langSlug, langSlug, 300+i+j))
+ }
+ }
+
+ // Add a bundle with some images
+ for i, lang := range []string{"en", "nn", "sv"} {
+ contentRoot := contentSectionRoot(lang)
+ slug := fmt.Sprintf("bundle_%s", lang)
+ filename := filepath.Join(contentRoot, "mybundle", "index.md")
+ contentFiles = append(contentFiles, filename, fmt.Sprintf(pageBundleTemplate, slug, 400+i))
+ if lang == "en" {
+ imageFilename := filepath.Join(contentRoot, "mybundle", "logo.png")
+ contentFiles = append(contentFiles, imageFilename, "PNG Data")
+ }
+ imageFilename := filepath.Join(contentRoot, "mybundle", "featured.png")
+ contentFiles = append(contentFiles, imageFilename, fmt.Sprintf("PNG Data for %s", lang))
+
+ // Add some bundled pages
+ contentFiles = append(contentFiles, filepath.Join(contentRoot, "mybundle", "p1.md"), fmt.Sprintf(pageBundleTemplate, slug, 401+i))
+ contentFiles = append(contentFiles, filepath.Join(contentRoot, "mybundle", "sub", "p1.md"), fmt.Sprintf(pageBundleTemplate, slug, 402+i))
+
+ }
+
+ // Add some static files inside the content dir
+ // https://github.com/gohugoio/hugo/issues/5759
+ for _, lang := range []string{"en", "nn", "sv"} {
+ contentRoot := contentRoot(lang)
+ for i := 0; i < 2; i++ {
+ filename := filepath.Join(contentRoot, "mystatic", fmt.Sprintf("file%d.yaml", i))
+ contentFiles = append(contentFiles, filename, lang)
+ }
+ }
+
+ b := newTestSitesBuilder(t)
+ b.WithWorkingDir("/my/project").WithConfigFile("toml", config).WithContent(contentFiles...).CreateSites()
+
+ _ = os.Stdout
+
+ b.Build(BuildCfg{})
+
+ assert.Equal(3, len(b.H.Sites))
+
+ enSite := b.H.Sites[0]
+ nnSite := b.H.Sites[1]
+ svSite := b.H.Sites[2]
+
+ b.AssertFileContent("/my/project/public/en/mystatic/file1.yaml", "en")
+ b.AssertFileContent("/my/project/public/nn/mystatic/file1.yaml", "nn")
+
+ //dumpPages(nnSite.RegularPages...)
+ assert.Equal(12, len(nnSite.RegularPages()))
+ assert.Equal(13, len(enSite.RegularPages()))
+
+ assert.Equal(10, len(svSite.RegularPages()))
+
+ svP2, err := svSite.getPageNew(nil, "/sect/page2.md")
+ assert.NoError(err)
+ nnP2, err := nnSite.getPageNew(nil, "/sect/page2.md")
+ assert.NoError(err)
+
+ enP2, err := enSite.getPageNew(nil, "/sect/page2.md")
+ assert.NoError(err)
+ assert.Equal("en", enP2.Language().Lang)
+ assert.Equal("sv", svP2.Language().Lang)
+ assert.Equal("nn", nnP2.Language().Lang)
+
+ content, _ := nnP2.Content()
+ assert.Contains(content, "SVP3-REF: https://example.org/sv/sect/p-sv-3/")
+ assert.Contains(content, "SVP3-RELREF: /sv/sect/p-sv-3/")
+
+ // Test RelRef with and without language indicator.
+ nn3RefArgs := map[string]interface{}{
+ "path": "/sect/page3.md",
+ "lang": "nn",
+ }
+ nnP3RelRef, err := svP2.RelRef(
+ nn3RefArgs,
+ )
+ assert.NoError(err)
+ assert.Equal("/nn/sect/p-nn-3/", nnP3RelRef)
+ nnP3Ref, err := svP2.Ref(
+ nn3RefArgs,
+ )
+ assert.NoError(err)
+ assert.Equal("https://example.org/nn/sect/p-nn-3/", nnP3Ref)
+
+ for i, p := range enSite.RegularPages() {
+ j := i + 1
+ msg := fmt.Sprintf("Test %d", j)
+ assert.Equal("en", p.Language().Lang, msg)
+ assert.Equal("sect", p.Section())
+ if j < 9 {
+ if j%4 == 0 {
+ assert.Contains(p.Title(), fmt.Sprintf("p-sv-%d.en", i+1), msg)
+ } else {
+ assert.Contains(p.Title(), "p-en", msg)
+ }
+ }
+ }
+
+ // Check bundles
+ bundleEn := enSite.RegularPages()[len(enSite.RegularPages())-1]
+ bundleNn := nnSite.RegularPages()[len(nnSite.RegularPages())-1]
+ bundleSv := svSite.RegularPages()[len(svSite.RegularPages())-1]
+
+ assert.Equal("/en/sect/mybundle/", bundleEn.RelPermalink())
+ assert.Equal("/sv/sect/mybundle/", bundleSv.RelPermalink())
+
+ assert.Equal(4, len(bundleEn.Resources()))
+ assert.Equal(4, len(bundleNn.Resources()))
+ assert.Equal(4, len(bundleSv.Resources()))
+
+ b.AssertFileContent("/my/project/public/en/sect/mybundle/index.html", "image/png: /en/sect/mybundle/logo.png")
+ b.AssertFileContent("/my/project/public/nn/sect/mybundle/index.html", "image/png: /nn/sect/mybundle/logo.png")
+ b.AssertFileContent("/my/project/public/sv/sect/mybundle/index.html", "image/png: /sv/sect/mybundle/logo.png")
+
+ b.AssertFileContent("/my/project/public/sv/sect/mybundle/featured.png", "PNG Data for sv")
+ b.AssertFileContent("/my/project/public/nn/sect/mybundle/featured.png", "PNG Data for nn")
+ b.AssertFileContent("/my/project/public/en/sect/mybundle/featured.png", "PNG Data for en")
+ b.AssertFileContent("/my/project/public/en/sect/mybundle/logo.png", "PNG Data")
+ b.AssertFileContent("/my/project/public/sv/sect/mybundle/logo.png", "PNG Data")
+ b.AssertFileContent("/my/project/public/nn/sect/mybundle/logo.png", "PNG Data")
+
+ nnSect := nnSite.getPage(page.KindSection, "sect")
+ assert.NotNil(nnSect)
+ assert.Equal(12, len(nnSect.Pages()))
+ nnHome, _ := nnSite.Info.Home()
+ assert.Equal("/nn/", nnHome.RelPermalink())
+
+}
diff --git a/hugolib/menu_test.go b/hugolib/menu_test.go
new file mode 100644
index 000000000..f1db3cb3a
--- /dev/null
+++ b/hugolib/menu_test.go
@@ -0,0 +1,224 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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:
+ title: %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 }}|{{ .Title }}|{{ .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", "atitle1", 40))
+ writeSource(t, fs, "content/sect1/p2.md", fmt.Sprintf(menuPageTemplate, "p2", 2, "main", "atitle2", 30))
+ writeSource(t, fs, "content/sect2/p3.md", fmt.Sprintf(menuPageTemplate, "p3", 3, "main", "atitle3", 20))
+ writeSource(t, fs, "content/sect2/p4.md", fmt.Sprintf(menuPageTemplate, "p4", 4, "main", "atitle4", 10))
+ writeSource(t, fs, "content/sect3/p5.md", fmt.Sprintf(menuPageTemplate, "p5", 5, "main", "atitle5", 5))
+
+ writeNewContentFile(t, fs.Source, "Section One", "2017-01-01", "content/sect1/_index.md", 100)
+ writeNewContentFile(t, fs.Source, "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|Section Five|10|-|-|"+
+ "/sect1/|Section One|Section One|100|-|HasMenuCurrent|"+
+ "/sect2/|Sect2s|Sect2s|0|-|-|"+
+ "/sect3/|Sect3s|Sect3s|0|-|-|",
+ "Menu Main: "+
+ "/sect3/p5/|p5|atitle5|5|-|-|"+
+ "/sect2/p4/|p4|atitle4|10|-|-|"+
+ "/sect2/p3/|p3|atitle3|20|-|-|"+
+ "/sect1/p2/|p2|atitle2|30|-|-|"+
+ "/sect1/p1/|p1|atitle1|40|IsMenuCurrent|-|",
+ )
+
+ th.assertFileContent("public/sect2/p3/index.html", "Single",
+ "Menu Sect: "+
+ "/sect5/|Section Five|Section Five|10|-|-|"+
+ "/sect1/|Section One|Section One|100|-|-|"+
+ "/sect2/|Sect2s|Sect2s|0|-|HasMenuCurrent|"+
+ "/sect3/|Sect3s|Sect3s|0|-|-|")
+
+}
+
+func TestMenuFrontMatter(t *testing.T) {
+
+ b := newTestSitesBuilder(t).WithSimpleConfigFile()
+
+ b.WithTemplatesAdded("index.html", `
+Main: {{ len .Site.Menus.main }}
+Other: {{ len .Site.Menus.other }}
+{{ range .Site.Menus.main }}
+* Main|{{ .Name }}: {{ .URL }}
+{{ end }}
+{{ range .Site.Menus.other }}
+* Other|{{ .Name }}: {{ .URL }}
+{{ end }}
+`)
+
+ // Issue #5828
+ b.WithContent("blog/page1.md", `
+---
+title: "P1"
+menu: main
+---
+
+`)
+
+ b.WithContent("blog/page2.md", `
+---
+title: "P2"
+menu: [main,other]
+---
+
+`)
+
+ b.WithContent("blog/page3.md", `
+---
+title: "P3"
+menu:
+ main:
+ weight: 30
+---
+`)
+
+ b.Build(BuildCfg{})
+
+ b.AssertFileContent("public/index.html",
+ "Main: 3", "Other: 1",
+ "Main|P1: /blog/page1/",
+ "Other|P2: /blog/page2/",
+ )
+
+}
+
+// https://github.com/gohugoio/hugo/issues/5849
+func TestMenuPageMultipleOutputFormats(t *testing.T) {
+
+ config := `
+baseURL = "https://example.com"
+
+# DAMP is similar to AMP, but not permalinkable.
+[outputFormats]
+[outputFormats.damp]
+mediaType = "text/html"
+path = "damp"
+
+`
+
+ b := newTestSitesBuilder(t).WithConfigFile("toml", config)
+ b.WithContent("_index.md", `
+---
+Title: Home Sweet Home
+outputs: [ "html", "amp" ]
+menu: "main"
+---
+
+`)
+
+ b.WithContent("blog/html-amp.md", `
+---
+Title: AMP and HTML
+outputs: [ "html", "amp" ]
+menu: "main"
+---
+
+`)
+
+ b.WithContent("blog/html.md", `
+---
+Title: HTML only
+outputs: [ "html" ]
+menu: "main"
+---
+
+`)
+
+ b.WithContent("blog/amp.md", `
+---
+Title: AMP only
+outputs: [ "amp" ]
+menu: "main"
+---
+
+`)
+
+ b.WithTemplatesAdded("index.html", `{{ range .Site.Menus.main }}{{ .Title }}|{{ .URL }}|{{ end }}`)
+
+ b.Build(BuildCfg{})
+
+ b.AssertFileContent("public/index.html", "AMP and HTML|/blog/html-amp/|AMP only|/amp/blog/amp/|HTML only|/blog/html/|Home Sweet Home|/|")
+ b.AssertFileContent("public/amp/index.html", "AMP and HTML|/amp/blog/html-amp/|AMP only|/amp/blog/amp/|HTML only|/blog/html/|Home Sweet Home|/amp/|")
+}
diff --git a/hugolib/minify_publisher_test.go b/hugolib/minify_publisher_test.go
new file mode 100644
index 000000000..66e674ade
--- /dev/null
+++ b/hugolib/minify_publisher_test.go
@@ -0,0 +1,63 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/viper"
+)
+
+func TestMinifyPublisher(t *testing.T) {
+ t.Parallel()
+
+ v := viper.New()
+ v.Set("minify", true)
+ v.Set("baseURL", "https://example.org/")
+
+ htmlTemplate := `
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>HTML5 boilerplate – all you really need…</title>
+ <link rel="stylesheet" href="css/style.css">
+ <!--[if IE]>
+ <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
+ <![endif]-->
+</head>
+
+<body id="home">
+
+ <h1>{{ .Title }}</h1>
+ <p>{{ .Permalink }}</p>
+
+</body>
+</html>
+`
+
+ b := newTestSitesBuilder(t)
+ b.WithViper(v).WithTemplatesAdded("layouts/index.html", htmlTemplate)
+ b.CreateSites().Build(BuildCfg{})
+
+ // Check minification
+ // HTML
+ b.AssertFileContent("public/index.html", "<!doctype html>")
+
+ // RSS
+ b.AssertFileContent("public/index.xml", "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?><rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\"><channel><title/><link>https://example.org/</link>")
+
+ // Sitemap
+ b.AssertFileContent("public/sitemap.xml", "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?><urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:xhtml=\"http://www.w3.org/1999/xhtml\"><url><loc>h")
+}
diff --git a/hugolib/multilingual.go b/hugolib/multilingual.go
new file mode 100644
index 000000000..6f744f3a5
--- /dev/null
+++ b/hugolib/multilingual.go
@@ -0,0 +1,140 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+
+ "github.com/gohugoio/hugo/common/maps"
+
+ "sort"
+
+ "errors"
+ "fmt"
+
+ "github.com/gohugoio/hugo/langs"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/spf13/cast"
+)
+
+// Multilingual manages the all languages used in a multilingual site.
+type Multilingual struct {
+ Languages langs.Languages
+
+ DefaultLang *langs.Language
+
+ langMap map[string]*langs.Language
+ langMapInit sync.Once
+}
+
+// Language returns the Language associated with the given string.
+func (ml *Multilingual) Language(lang string) *langs.Language {
+ ml.langMapInit.Do(func() {
+ ml.langMap = make(map[string]*langs.Language)
+ for _, l := range ml.Languages {
+ ml.langMap[l.Lang] = l
+ }
+ })
+ return ml.langMap[lang]
+}
+
+func getLanguages(cfg config.Provider) langs.Languages {
+ if cfg.IsSet("languagesSorted") {
+ return cfg.Get("languagesSorted").(langs.Languages)
+ }
+
+ return langs.Languages{langs.NewDefaultLanguage(cfg)}
+}
+
+func newMultiLingualFromSites(cfg config.Provider, sites ...*Site) (*Multilingual, error) {
+ languages := make(langs.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: langs.NewLanguage(defaultLang, cfg)}, nil
+
+}
+
+func (ml *Multilingual) enabled() bool {
+ return len(ml.Languages) > 1
+}
+
+func (s *Site) multilingualEnabled() bool {
+ if s.h == nil {
+ return false
+ }
+ return s.h.multilingual != nil && s.h.multilingual.enabled()
+}
+
+func toSortedLanguages(cfg config.Provider, l map[string]interface{}) (langs.Languages, error) {
+ languages := make(langs.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 := langs.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)
+ case "contentdir":
+ language.ContentDir = cast.ToString(v)
+ case "disabled":
+ language.Disabled = cast.ToBool(v)
+ case "params":
+ m := cast.ToStringMap(v)
+ // Needed for case insensitive fetching of params values
+ maps.ToLower(m)
+ for k, vv := range m {
+ language.SetParam(k, vv)
+ }
+ }
+
+ // Put all into the Params map
+ language.SetParam(loki, v)
+
+ // Also set it in the configuration map (for baseURL etc.)
+ language.Set(loki, v)
+ }
+
+ languages[i] = language
+ i++
+ }
+
+ sort.Sort(languages)
+
+ return languages, nil
+}
diff --git a/hugolib/page.go b/hugolib/page.go
new file mode 100644
index 000000000..537482fb2
--- /dev/null
+++ b/hugolib/page.go
@@ -0,0 +1,860 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "github.com/bep/gitmap"
+
+ "github.com/gohugoio/hugo/helpers"
+
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/parser/metadecoders"
+
+ "github.com/gohugoio/hugo/parser/pageparser"
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/output"
+
+ "github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/source"
+
+ "github.com/gohugoio/hugo/common/collections"
+ "github.com/gohugoio/hugo/common/text"
+ "github.com/gohugoio/hugo/resources"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/resources/resource"
+)
+
+var (
+ _ page.Page = (*pageState)(nil)
+ _ collections.Grouper = (*pageState)(nil)
+ _ collections.Slicer = (*pageState)(nil)
+)
+
+var (
+ pageTypesProvider = resource.NewResourceTypesProvider(media.OctetType, pageResourceType)
+ nopPageOutput = &pageOutput{pagePerOutputProviders: nopPagePerOutput}
+)
+
+// pageContext provides contextual information about this page, for error
+// logging and similar.
+type pageContext interface {
+ posOffset(offset int) text.Position
+ wrapError(err error) error
+ getRenderingConfig() *helpers.BlackFriday
+}
+
+// wrapErr adds some context to the given error if possible.
+func wrapErr(err error, ctx interface{}) error {
+ if pc, ok := ctx.(pageContext); ok {
+ return pc.wrapError(err)
+ }
+ return err
+}
+
+type pageSiteAdapter struct {
+ p page.Page
+ s *Site
+}
+
+func (pa pageSiteAdapter) GetPage(ref string) (page.Page, error) {
+ p, err := pa.s.getPageNew(pa.p, ref)
+ if p == nil {
+ // The nil struct has meaning in some situations, mostly to avoid breaking
+ // existing sites doing $nilpage.IsDescendant($p), which will always return
+ // false.
+ p = page.NilPage
+ }
+ return p, err
+}
+
+type pageState struct {
+ // This slice will be of same length as the number of global slice of output
+ // formats (for all sites).
+ pageOutputs []*pageOutput
+
+ // This will be shifted out when we start to render a new output format.
+ *pageOutput
+
+ // Common for all output formats.
+ *pageCommon
+}
+
+// Eq returns whether the current page equals the given page.
+// This is what's invoked when doing `{{ if eq $page $otherPage }}`
+func (p *pageState) Eq(other interface{}) bool {
+ pp, err := unwrapPage(other)
+ if err != nil {
+ return false
+ }
+
+ return p == pp
+}
+
+func (p *pageState) GitInfo() *gitmap.GitInfo {
+ return p.gitInfo
+}
+
+func (p *pageState) MarshalJSON() ([]byte, error) {
+ return page.MarshalPageToJSON(p)
+}
+
+func (p *pageState) Pages() page.Pages {
+ p.pagesInit.Do(func() {
+ if p.pages != nil {
+ return
+ }
+
+ var pages page.Pages
+
+ switch p.Kind() {
+ case page.KindPage:
+ case page.KindHome:
+ pages = p.s.RegularPages()
+ case page.KindTaxonomy:
+ termInfo := p.getTaxonomyNodeInfo()
+ taxonomy := p.s.Taxonomies[termInfo.plural].Get(termInfo.termKey)
+ pages = taxonomy.Pages()
+ case page.KindTaxonomyTerm:
+ plural := p.getTaxonomyNodeInfo().plural
+ // A list of all page.KindTaxonomy pages with matching plural
+ for _, p := range p.s.findPagesByKind(page.KindTaxonomy) {
+ if p.SectionsEntries()[0] == plural {
+ pages = append(pages, p)
+ }
+ }
+ case kind404, kindSitemap, kindRobotsTXT:
+ pages = p.s.Pages()
+ }
+
+ p.pages = pages
+ })
+
+ return p.pages
+}
+
+// RawContent returns the un-rendered source content without
+// any leading front matter.
+func (p *pageState) RawContent() string {
+ if p.source.parsed == nil {
+ return ""
+ }
+ start := p.source.posMainContent
+ if start == -1 {
+ start = 0
+ }
+ return string(p.source.parsed.Input()[start:])
+}
+
+func (p *pageState) Resources() resource.Resources {
+ p.resourcesInit.Do(func() {
+
+ sort := func() {
+ sort.SliceStable(p.resources, func(i, j int) bool {
+ ri, rj := p.resources[i], p.resources[j]
+ if ri.ResourceType() < rj.ResourceType() {
+ return true
+ }
+
+ p1, ok1 := ri.(page.Page)
+ p2, ok2 := rj.(page.Page)
+
+ if ok1 != ok2 {
+ return ok2
+ }
+
+ if ok1 {
+ return page.DefaultPageSort(p1, p2)
+ }
+
+ return ri.RelPermalink() < rj.RelPermalink()
+ })
+ }
+
+ sort()
+
+ if len(p.m.resourcesMetadata) > 0 {
+ resources.AssignMetadata(p.m.resourcesMetadata, p.resources...)
+ sort()
+ }
+
+ })
+ return p.resources
+}
+
+func (p *pageState) HasShortcode(name string) bool {
+ if p.shortcodeState == nil {
+ return false
+ }
+
+ return p.shortcodeState.nameSet[name]
+}
+
+func (p *pageState) Site() page.Site {
+ return &p.s.Info
+}
+
+func (p *pageState) String() string {
+ if sourceRef := p.sourceRef(); sourceRef != "" {
+ return fmt.Sprintf("Page(%s)", sourceRef)
+ }
+ return fmt.Sprintf("Page(%q)", p.Title())
+}
+
+// IsTranslated returns whether this content file is translated to
+// other language(s).
+func (p *pageState) IsTranslated() bool {
+ p.s.h.init.translations.Do()
+ return len(p.translations) > 0
+}
+
+// TranslationKey returns the key used to map language translations of this page.
+// It will use the translationKey set in front matter if set, or the content path and
+// filename (excluding any language code and extension), e.g. "about/index".
+// The Page Kind is always prepended.
+func (p *pageState) TranslationKey() string {
+ p.translationKeyInit.Do(func() {
+ if p.m.translationKey != "" {
+ p.translationKey = p.Kind() + "/" + p.m.translationKey
+ } else if p.IsPage() && !p.File().IsZero() {
+ p.translationKey = path.Join(p.Kind(), filepath.ToSlash(p.File().Dir()), p.File().TranslationBaseName())
+ } else if p.IsNode() {
+ p.translationKey = path.Join(p.Kind(), p.SectionsPath())
+ }
+
+ })
+
+ return p.translationKey
+
+}
+
+// AllTranslations returns all translations, including the current Page.
+func (p *pageState) AllTranslations() page.Pages {
+ p.s.h.init.translations.Do()
+ return p.allTranslations
+}
+
+// Translations returns the translations excluding the current Page.
+func (p *pageState) Translations() page.Pages {
+ p.s.h.init.translations.Do()
+ return p.translations
+}
+
+func (p *pageState) getRenderingConfig() *helpers.BlackFriday {
+ if p.m.renderingConfig == nil {
+ return p.s.ContentSpec.BlackFriday
+ }
+ return p.m.renderingConfig
+}
+
+func (ps *pageState) initCommonProviders(pp pagePaths) error {
+ if ps.IsPage() {
+ ps.posNextPrev = &nextPrev{init: ps.s.init.prevNext}
+ ps.posNextPrevSection = &nextPrev{init: ps.s.init.prevNextInSection}
+ ps.InSectionPositioner = newPagePositionInSection(ps.posNextPrevSection)
+ ps.Positioner = newPagePosition(ps.posNextPrev)
+ }
+
+ ps.OutputFormatsProvider = pp
+ ps.targetPathDescriptor = pp.targetPathDescriptor
+ ps.RefProvider = newPageRef(ps)
+ ps.SitesProvider = &ps.s.Info
+
+ return nil
+}
+
+func (p *pageState) getLayoutDescriptor() output.LayoutDescriptor {
+ p.layoutDescriptorInit.Do(func() {
+ var section string
+ sections := p.SectionsEntries()
+
+ switch p.Kind() {
+ case page.KindSection:
+ section = sections[0]
+ case page.KindTaxonomyTerm:
+ section = p.getTaxonomyNodeInfo().singular
+ case page.KindTaxonomy:
+ section = p.getTaxonomyNodeInfo().parent.singular
+ default:
+ }
+
+ p.layoutDescriptor = output.LayoutDescriptor{
+ Kind: p.Kind(),
+ Type: p.Type(),
+ Lang: p.Language().Lang,
+ Layout: p.Layout(),
+ Section: section,
+ }
+ })
+
+ return p.layoutDescriptor
+
+}
+
+func (p *pageState) getLayouts(layouts ...string) ([]string, error) {
+ f := p.outputFormat()
+
+ if len(layouts) == 0 {
+ selfLayout := p.selfLayoutForOutput(f)
+ if selfLayout != "" {
+ return []string{selfLayout}, nil
+ }
+ }
+
+ layoutDescriptor := p.getLayoutDescriptor()
+
+ if len(layouts) > 0 {
+ layoutDescriptor.Layout = layouts[0]
+ layoutDescriptor.LayoutOverride = true
+ }
+
+ return p.s.layoutHandler.For(layoutDescriptor, f)
+}
+
+// This is serialized
+func (p *pageState) initOutputFormat(isRenderingSite bool, idx int) error {
+ if err := p.shiftToOutputFormat(isRenderingSite, idx); err != nil {
+ return err
+ }
+
+ if !p.renderable {
+ if _, err := p.Content(); err != nil {
+ return err
+ }
+ }
+
+ return nil
+
+}
+
+// Must be run after the site section tree etc. is built and ready.
+func (p *pageState) initPage() error {
+ if _, err := p.init.Do(); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (p *pageState) setPages(pages page.Pages) {
+ page.SortByDefault(pages)
+ p.pages = pages
+}
+
+func (p *pageState) renderResources() (err error) {
+ p.resourcesPublishInit.Do(func() {
+ var toBeDeleted []int
+
+ for i, r := range p.Resources() {
+ if _, ok := r.(page.Page); ok {
+ // Pages gets rendered with the owning page but we count them here.
+ p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Pages)
+ continue
+ }
+
+ src, ok := r.(resource.Source)
+ if !ok {
+ err = errors.Errorf("Resource %T does not support resource.Source", src)
+ return
+ }
+
+ if err := src.Publish(); err != nil {
+ if os.IsNotExist(err) {
+ // The resource has been deleted from the file system.
+ // This should be extremely rare, but can happen on live reload in server
+ // mode when the same resource is member of different page bundles.
+ toBeDeleted = append(toBeDeleted, i)
+ } else {
+ p.s.Log.ERROR.Printf("Failed to publish Resource for page %q: %s", p.pathOrTitle(), err)
+ }
+ } else {
+ p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Files)
+ }
+ }
+
+ for _, i := range toBeDeleted {
+ p.deleteResource(i)
+ }
+
+ })
+
+ return
+}
+
+func (p *pageState) deleteResource(i int) {
+ p.resources = append(p.resources[:i], p.resources[i+1:]...)
+}
+
+func (p *pageState) getTargetPaths() page.TargetPaths {
+ return p.targetPaths()
+}
+
+func (p *pageState) setTranslations(pages page.Pages) {
+ p.allTranslations = pages
+ page.SortByLanguage(p.allTranslations)
+ translations := make(page.Pages, 0)
+ for _, t := range p.allTranslations {
+ if !t.Eq(p) {
+ translations = append(translations, t)
+ }
+ }
+ p.translations = translations
+}
+
+func (p *pageState) AlternativeOutputFormats() page.OutputFormats {
+ f := p.outputFormat()
+ var o page.OutputFormats
+ for _, of := range p.OutputFormats() {
+ if of.Format.NotAlternative || of.Format.Name == f.Name {
+ continue
+ }
+
+ o = append(o, of)
+ }
+ return o
+}
+
+func (p *pageState) Render(layout ...string) template.HTML {
+ l, err := p.getLayouts(layout...)
+ if err != nil {
+ p.s.SendError(p.wrapError(errors.Errorf(".Render: failed to resolve layout %v", layout)))
+ return ""
+ }
+
+ for _, layout := range l {
+ templ, found := p.s.Tmpl.Lookup(layout)
+ if !found {
+ // 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 := executeToString(templ, p)
+ if err != nil {
+ p.s.SendError(p.wrapError(errors.Wrapf(err, ".Render: failed to execute template %q v", layout)))
+ return ""
+ }
+ return template.HTML(res)
+ }
+ }
+
+ return ""
+
+}
+
+// wrapError adds some more context to the given error if possible
+func (p *pageState) wrapError(err error) error {
+
+ var filename string
+ if !p.File().IsZero() {
+ filename = p.File().Filename()
+ }
+
+ err, _ = herrors.WithFileContextForFile(
+ err,
+ filename,
+ filename,
+ p.s.SourceSpec.Fs.Source,
+ herrors.SimpleLineMatcher)
+
+ return err
+}
+
+func (p *pageState) addResources(r ...resource.Resource) {
+ p.resources = append(p.resources, r...)
+}
+
+func (p *pageState) addSectionToParent() {
+ if p.parent == nil {
+ return
+ }
+ p.parent.subSections = append(p.parent.subSections, p)
+}
+
+func (p *pageState) contentMarkupType() string {
+ if p.m.markup != "" {
+ return p.m.markup
+
+ }
+ return p.File().Ext()
+}
+
+func (p *pageState) mapContent(meta *pageMeta) error {
+
+ s := p.shortcodeState
+
+ p.renderable = true
+
+ rn := &pageContentMap{
+ items: make([]interface{}, 0, 20),
+ }
+
+ iter := p.source.parsed.Iterator()
+
+ fail := func(err error, i pageparser.Item) error {
+ return p.parseError(err, iter.Input(), i.Pos)
+ }
+
+ // the parser is guaranteed to return items in proper order or fail, so …
+ // … it's safe to keep some "global" state
+ var currShortcode shortcode
+ var ordinal int
+
+Loop:
+ for {
+ it := iter.Next()
+
+ switch {
+ case it.Type == pageparser.TypeIgnore:
+ case it.Type == pageparser.TypeHTMLStart:
+ // This is HTML without front matter. It can still have shortcodes.
+ p.selfLayout = "__" + p.File().Filename()
+ p.renderable = false
+ rn.AddBytes(it)
+ case it.IsFrontMatter():
+ f := metadecoders.FormatFromFrontMatterType(it.Type)
+ m, err := metadecoders.Default.UnmarshalToMap(it.Val, f)
+ if err != nil {
+ if fe, ok := err.(herrors.FileError); ok {
+ return herrors.ToFileErrorWithOffset(fe, iter.LineNumber()-1)
+ } else {
+ return err
+ }
+ }
+
+ if err := meta.setMetadata(p, m); err != nil {
+ return err
+ }
+
+ next := iter.Peek()
+ if !next.IsDone() {
+ p.source.posMainContent = next.Pos
+ }
+
+ if !p.s.shouldBuild(p) {
+ // Nothing more to do.
+ return nil
+ }
+
+ case it.Type == pageparser.TypeLeadSummaryDivider:
+ posBody := -1
+ f := func(item pageparser.Item) bool {
+ if posBody == -1 && !item.IsDone() {
+ posBody = item.Pos
+ }
+
+ if item.IsNonWhitespace() {
+ p.truncated = true
+
+ // Done
+ return false
+ }
+ return true
+ }
+ iter.PeekWalk(f)
+
+ p.source.posSummaryEnd = it.Pos
+ p.source.posBodyStart = posBody
+ p.source.hasSummaryDivider = true
+
+ if meta.markup != "html" {
+ // The content will be rendered by Blackfriday or similar,
+ // and we need to track the summary.
+ rn.AddReplacement(internalSummaryDividerPre, it)
+ }
+
+ // Handle shortcode
+ case it.IsLeftShortcodeDelim():
+ // let extractShortcode handle left delim (will do so recursively)
+ iter.Backup()
+
+ currShortcode, err := s.extractShortcode(ordinal, 0, iter)
+ if err != nil {
+ return fail(errors.Wrap(err, "failed to extract shortcode"), it)
+ }
+
+ currShortcode.pos = it.Pos
+ currShortcode.length = iter.Current().Pos - it.Pos
+ if currShortcode.placeholder == "" {
+ currShortcode.placeholder = createShortcodePlaceholder("s", currShortcode.ordinal)
+ }
+
+ if currShortcode.name != "" {
+ s.nameSet[currShortcode.name] = true
+ }
+
+ if currShortcode.params == nil {
+ var s []string
+ currShortcode.params = s
+ }
+
+ currShortcode.placeholder = createShortcodePlaceholder("s", ordinal)
+ ordinal++
+ s.shortcodes = append(s.shortcodes, currShortcode)
+
+ rn.AddShortcode(currShortcode)
+
+ case it.Type == pageparser.TypeEmoji:
+ if emoji := helpers.Emoji(it.ValStr()); emoji != nil {
+ rn.AddReplacement(emoji, it)
+ } else {
+ rn.AddBytes(it)
+ }
+ case it.IsEOF():
+ break Loop
+ case it.IsError():
+ err := fail(errors.WithStack(errors.New(it.ValStr())), it)
+ currShortcode.err = err
+ return err
+
+ default:
+ rn.AddBytes(it)
+ }
+ }
+
+ p.cmap = rn
+
+ return nil
+}
+
+func (p *pageState) errorf(err error, format string, a ...interface{}) error {
+ if herrors.UnwrapErrorWithFileContext(err) != nil {
+ // More isn't always better.
+ return err
+ }
+ args := append([]interface{}{p.Language().Lang, p.pathOrTitle()}, a...)
+ format = "[%s] page %q: " + format
+ if err == nil {
+ errors.Errorf(format, args...)
+ return fmt.Errorf(format, args...)
+ }
+ return errors.Wrapf(err, format, args...)
+}
+
+func (p *pageState) outputFormat() (f output.Format) {
+ if p.pageOutput == nil {
+ panic("no pageOutput")
+ }
+ return p.pageOutput.f
+}
+
+func (p *pageState) parseError(err error, input []byte, offset int) error {
+ if herrors.UnwrapFileError(err) != nil {
+ // Use the most specific location.
+ return err
+ }
+ pos := p.posFromInput(input, offset)
+ return herrors.NewFileError("md", -1, pos.LineNumber, pos.ColumnNumber, err)
+
+}
+
+func (p *pageState) pathOrTitle() string {
+ if !p.File().IsZero() {
+ return p.File().Filename()
+ }
+
+ if p.Path() != "" {
+ return p.Path()
+ }
+
+ return p.Title()
+}
+
+func (p *pageState) posFromPage(offset int) text.Position {
+ return p.posFromInput(p.source.parsed.Input(), offset)
+}
+
+func (p *pageState) posFromInput(input []byte, offset int) text.Position {
+ lf := []byte("\n")
+ input = input[:offset]
+ lineNumber := bytes.Count(input, lf) + 1
+ endOfLastLine := bytes.LastIndex(input, lf)
+
+ return text.Position{
+ Filename: p.pathOrTitle(),
+ LineNumber: lineNumber,
+ ColumnNumber: offset - endOfLastLine,
+ Offset: offset,
+ }
+}
+
+func (p *pageState) posOffset(offset int) text.Position {
+ return p.posFromInput(p.source.parsed.Input(), offset)
+}
+
+// shiftToOutputFormat is serialized. The output format idx refers to the
+// full set of output formats for all sites.
+func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
+ if err := p.initPage(); err != nil {
+ return err
+ }
+
+ if idx >= len(p.pageOutputs) {
+ panic(fmt.Sprintf("invalid page state for %q: got output format index %d, have %d", p.pathOrTitle(), idx, len(p.pageOutputs)))
+ }
+
+ p.pageOutput = p.pageOutputs[idx]
+
+ if p.pageOutput == nil {
+ panic(fmt.Sprintf("pageOutput is nil for output idx %d", idx))
+ }
+
+ // Reset any built paginator. This will trigger when re-rendering pages in
+ // server mode.
+ if isRenderingSite && p.pageOutput.paginator != nil && p.pageOutput.paginator.current != nil {
+ p.pageOutput.paginator.reset()
+ }
+
+ if idx > 0 {
+ // Check if we can reuse content from one of the previous formats.
+ for i := idx - 1; i >= 0; i-- {
+ po := p.pageOutputs[i]
+ if po.cp != nil && po.cp.reuse {
+ p.pageOutput.cp = po.cp
+ break
+ }
+ }
+ }
+
+ for _, r := range p.Resources().ByType(pageResourceType) {
+ rp := r.(*pageState)
+ if err := rp.shiftToOutputFormat(isRenderingSite, idx); err != nil {
+ return errors.Wrap(err, "failed to shift outputformat in Page resource")
+ }
+ }
+
+ return nil
+}
+
+func (p *pageState) getTaxonomyNodeInfo() *taxonomyNodeInfo {
+ info := p.s.taxonomyNodes.Get(p.SectionsEntries()...)
+
+ if info == nil {
+ // There can be unused content pages for taxonomies (e.g. author that
+ // has not written anything, yet), and these will not have a taxonomy
+ // node created in the assemble taxonomies step.
+ return nil
+ }
+
+ return info
+
+}
+
+func (p *pageState) sortParentSections() {
+ if p.parent == nil {
+ return
+ }
+ page.SortByDefault(p.parent.subSections)
+}
+
+// sourceRef returns the reference used by GetPage and ref/relref shortcodes to refer to
+// this page. It is prefixed with a "/".
+//
+// For pages that have a source file, it is returns the path to this file as an
+// absolute path rooted in this site's content dir.
+// For pages that do not (sections witout content page etc.), it returns the
+// virtual path, consistent with where you would add a source file.
+func (p *pageState) sourceRef() string {
+ if !p.File().IsZero() {
+ sourcePath := p.File().Path()
+ if sourcePath != "" {
+ return "/" + filepath.ToSlash(sourcePath)
+ }
+ }
+
+ if len(p.SectionsEntries()) > 0 {
+ // no backing file, return the virtual source path
+ return "/" + p.SectionsPath()
+ }
+
+ return ""
+}
+
+type pageStatePages []*pageState
+
+// Implement sorting.
+func (ps pageStatePages) Len() int { return len(ps) }
+
+func (ps pageStatePages) Less(i, j int) bool { return page.DefaultPageSort(ps[i], ps[j]) }
+
+func (ps pageStatePages) Swap(i, j int) { ps[i], ps[j] = ps[j], ps[i] }
+
+// findPagePos Given a page, it will find the position in Pages
+// will return -1 if not found
+func (ps pageStatePages) findPagePos(page *pageState) int {
+ for i, x := range ps {
+ if x.File().Filename() == page.File().Filename() {
+ return i
+ }
+ }
+ return -1
+}
+
+func (ps pageStatePages) findPagePosByFilename(filename string) int {
+ for i, x := range ps {
+ if x.File().Filename() == filename {
+ return i
+ }
+ }
+ return -1
+}
+
+func (ps pageStatePages) findPagePosByFilnamePrefix(prefix string) int {
+ if prefix == "" {
+ return -1
+ }
+
+ lenDiff := -1
+ currPos := -1
+ prefixLen := len(prefix)
+
+ // Find the closest match
+ for i, x := range ps {
+ if strings.HasPrefix(x.File().Filename(), prefix) {
+ diff := len(x.File().Filename()) - prefixLen
+ if lenDiff == -1 || diff < lenDiff {
+ lenDiff = diff
+ currPos = i
+ }
+ }
+ }
+ return currPos
+}
+
+func (s *Site) sectionsFromFile(fi source.File) []string {
+ dirname := fi.Dir()
+ dirname = strings.Trim(dirname, helpers.FilePathSeparator)
+ if dirname == "" {
+ return nil
+ }
+ parts := strings.Split(dirname, helpers.FilePathSeparator)
+
+ if fii, ok := fi.(*fileInfo); ok {
+ if fii.bundleTp == bundleLeaf && len(parts) > 0 {
+ // my-section/mybundle/index.md => my-section
+ return parts[:len(parts)-1]
+ }
+ }
+
+ return parts
+}
diff --git a/hugolib/page__common.go b/hugolib/page__common.go
new file mode 100644
index 000000000..f9ceee8c9
--- /dev/null
+++ b/hugolib/page__common.go
@@ -0,0 +1,117 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+
+ "github.com/bep/gitmap"
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/compare"
+ "github.com/gohugoio/hugo/lazy"
+ "github.com/gohugoio/hugo/navigation"
+ "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/resources/resource"
+)
+
+type pageCommon struct {
+ s *Site
+ m *pageMeta
+
+ // Laziliy initialized dependencies.
+ init *lazy.Init
+
+ // All of these represents the common parts of a page.Page
+ maps.Scratcher
+ navigation.PageMenusProvider
+ page.AuthorProvider
+ page.PageRenderProvider
+ page.AlternativeOutputFormatsProvider
+ page.ChildCareProvider
+ page.FileProvider
+ page.GetPageProvider
+ page.GitInfoProvider
+ page.InSectionPositioner
+ page.OutputFormatsProvider
+ page.PageMetaProvider
+ page.Positioner
+ page.RawContentProvider
+ page.RelatedKeywordsProvider
+ page.RefProvider
+ page.ShortcodeInfoProvider
+ page.SitesProvider
+ page.DeprecatedWarningPageMethods
+ page.TranslationsProvider
+ page.TreeProvider
+ resource.LanguageProvider
+ resource.ResourceDataProvider
+ resource.ResourceMetaProvider
+ resource.ResourceParamsProvider
+ resource.ResourceTypesProvider
+ resource.TranslationKeyProvider
+ compare.Eqer
+
+ // Describes how paths and URLs for this page and its descendants
+ // should look like.
+ targetPathDescriptor page.TargetPathDescriptor
+
+ layoutDescriptor output.LayoutDescriptor
+ layoutDescriptorInit sync.Once
+
+ // The parsed page content.
+ pageContent
+
+ // Set if feature enabled and this is in a Git repo.
+ gitInfo *gitmap.GitInfo
+
+ // Positional navigation
+ posNextPrev *nextPrev
+ posNextPrevSection *nextPrev
+
+ // Menus
+ pageMenus *pageMenus
+
+ // Internal use
+ page.InternalDependencies
+
+ // The children. Regular pages will have none.
+ *pagePages
+
+ // Any bundled resources
+ resources resource.Resources
+ resourcesInit sync.Once
+ resourcesPublishInit sync.Once
+
+ translations page.Pages
+ allTranslations page.Pages
+
+ // Calculated an cached translation mapping key
+ translationKey string
+ translationKeyInit sync.Once
+
+ // Will only be set for sections and regular pages.
+ parent *pageState
+
+ // Will only be set for section pages and the home page.
+ subSections page.Pages
+
+ // Set in fast render mode to force render a given page.
+ forceRender bool
+}
+
+type pagePages struct {
+ pages page.Pages
+ pagesInit sync.Once
+}
diff --git a/hugolib/page__content.go b/hugolib/page__content.go
new file mode 100644
index 000000000..593c5b9a1
--- /dev/null
+++ b/hugolib/page__content.go
@@ -0,0 +1,135 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/output"
+ "github.com/gohugoio/hugo/parser/pageparser"
+)
+
+var (
+ internalSummaryDividerBase = "HUGOMORE42"
+ internalSummaryDividerBaseBytes = []byte(internalSummaryDividerBase)
+ internalSummaryDividerPre = []byte("\n\n" + internalSummaryDividerBase + "\n\n")
+)
+
+// The content related items on a Page.
+type pageContent struct {
+ renderable bool
+ selfLayout string
+
+ truncated bool
+
+ cmap *pageContentMap
+
+ shortcodeState *shortcodeHandler
+
+ source rawPageContent
+}
+
+// returns the content to be processed by Blackfriday or similar.
+func (p pageContent) contentToRender(renderedShortcodes map[string]string) []byte {
+ source := p.source.parsed.Input()
+
+ c := make([]byte, 0, len(source)+(len(source)/10))
+
+ for _, it := range p.cmap.items {
+ switch v := it.(type) {
+ case pageparser.Item:
+ c = append(c, source[v.Pos:v.Pos+len(v.Val)]...)
+ case pageContentReplacement:
+ c = append(c, v.val...)
+ case *shortcode:
+ if !p.renderable || !v.insertPlaceholder() {
+ // Insert the rendered shortcode.
+ renderedShortcode, found := renderedShortcodes[v.placeholder]
+ if !found {
+ // This should never happen.
+ panic(fmt.Sprintf("rendered shortcode %q not found", v.placeholder))
+ }
+
+ c = append(c, []byte(renderedShortcode)...)
+
+ } else {
+ // Insert the placeholder so we can insert the content after
+ // markdown processing.
+ c = append(c, []byte(v.placeholder)...)
+
+ }
+ default:
+ panic(fmt.Sprintf("unkown item type %T", it))
+ }
+ }
+
+ return c
+}
+
+func (p pageContent) selfLayoutForOutput(f output.Format) string {
+ if p.selfLayout == "" {
+ return ""
+ }
+ return p.selfLayout + f.Name
+}
+
+type rawPageContent struct {
+ hasSummaryDivider bool
+
+ // The AST of the parsed page. Contains information about:
+ // shortcodes, front matter, summary indicators.
+ parsed pageparser.Result
+
+ // Returns the position in bytes after any front matter.
+ posMainContent int
+
+ // These are set if we're able to determine this from the source.
+ posSummaryEnd int
+ posBodyStart int
+}
+
+type pageContentReplacement struct {
+ val []byte
+
+ source pageparser.Item
+}
+
+type pageContentMap struct {
+
+ // If not, we can skip any pre-rendering of shortcodes.
+ hasMarkdownShortcode bool
+
+ // Indicates whether we must do placeholder replacements.
+ hasNonMarkdownShortcode bool
+
+ // *shortcode, pageContentReplacement or pageparser.Item
+ items []interface{}
+}
+
+func (p *pageContentMap) AddBytes(item pageparser.Item) {
+ p.items = append(p.items, item)
+}
+
+func (p *pageContentMap) AddReplacement(val []byte, source pageparser.Item) {
+ p.items = append(p.items, pageContentReplacement{val: val, source: source})
+}
+
+func (p *pageContentMap) AddShortcode(s *shortcode) {
+ p.items = append(p.items, s)
+ if s.insertPlaceholder() {
+ p.hasNonMarkdownShortcode = true
+ } else {
+ p.hasMarkdownShortcode = true
+ }
+}
diff --git a/hugolib/page__data.go b/hugolib/page__data.go
new file mode 100644
index 000000000..79a64931b
--- /dev/null
+++ b/hugolib/page__data.go
@@ -0,0 +1,70 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+
+ "github.com/gohugoio/hugo/resources/page"
+)
+
+type pageData struct {
+ *pageState
+
+ dataInit sync.Once
+ data page.Data
+}
+
+func (p *pageData) Data() interface{} {
+ p.dataInit.Do(func() {
+ p.data = make(page.Data)
+
+ if p.Kind() == page.KindPage {
+ return
+ }
+
+ switch p.Kind() {
+ case page.KindTaxonomy:
+ termInfo := p.getTaxonomyNodeInfo()
+ pluralInfo := termInfo.parent
+
+ singular := pluralInfo.singular
+ plural := pluralInfo.plural
+ term := termInfo.term
+ taxonomy := p.s.Taxonomies[plural].Get(termInfo.termKey)
+
+ p.data[singular] = taxonomy
+ p.data["Singular"] = singular
+ p.data["Plural"] = plural
+ p.data["Term"] = term
+ case page.KindTaxonomyTerm:
+ info := p.getTaxonomyNodeInfo()
+ plural := info.plural
+ singular := info.singular
+
+ p.data["Singular"] = singular
+ p.data["Plural"] = plural
+ p.data["Terms"] = p.s.Taxonomies[plural]
+ // keep the following just for legacy reasons
+ p.data["OrderedIndex"] = p.data["Terms"]
+ p.data["Index"] = p.data["Terms"]
+ }
+
+ // Assign the function to the map to make sure it is lazily initialized
+ p.data["pages"] = p.Pages
+
+ })
+
+ return p.data
+}
diff --git a/hugolib/page__menus.go b/hugolib/page__menus.go
new file mode 100644
index 000000000..0c9616a6d
--- /dev/null
+++ b/hugolib/page__menus.go
@@ -0,0 +1,74 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+
+ "github.com/gohugoio/hugo/navigation"
+)
+
+type pageMenus struct {
+ p *pageState
+
+ q navigation.MenyQueryProvider
+
+ pmInit sync.Once
+ pm navigation.PageMenus
+}
+
+func (p *pageMenus) HasMenuCurrent(menuID string, me *navigation.MenuEntry) bool {
+ p.p.s.init.menus.Do()
+ p.init()
+ return p.q.HasMenuCurrent(menuID, me)
+}
+
+func (p *pageMenus) IsMenuCurrent(menuID string, inme *navigation.MenuEntry) bool {
+ p.p.s.init.menus.Do()
+ p.init()
+ return p.q.IsMenuCurrent(menuID, inme)
+}
+
+func (p *pageMenus) Menus() navigation.PageMenus {
+ // There is a reverse dependency here. initMenus will, once, build the
+ // site menus and update any relevant page.
+ p.p.s.init.menus.Do()
+
+ return p.menus()
+}
+
+func (p *pageMenus) menus() navigation.PageMenus {
+ p.init()
+ return p.pm
+
+}
+
+func (p *pageMenus) init() {
+ p.pmInit.Do(func() {
+ p.q = navigation.NewMenuQueryProvider(
+ p.p.s.Info.sectionPagesMenu,
+ p,
+ p.p.s,
+ p.p,
+ )
+
+ var err error
+ p.pm, err = navigation.PageMenusFromPage(p.p)
+ if err != nil {
+ p.p.s.Log.ERROR.Println(p.p.wrapError(err))
+ }
+
+ })
+
+}
diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go
new file mode 100644
index 000000000..d14b9d724
--- /dev/null
+++ b/hugolib/page__meta.go
@@ -0,0 +1,675 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "time"
+
+ "github.com/gohugoio/hugo/common/hugo"
+
+ "github.com/gohugoio/hugo/related"
+
+ "github.com/gohugoio/hugo/source"
+ "github.com/markbates/inflect"
+ "github.com/mitchellh/mapstructure"
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/helpers"
+
+ "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/resources/page/pagemeta"
+ "github.com/gohugoio/hugo/resources/resource"
+ "github.com/spf13/cast"
+)
+
+var cjkRe = regexp.MustCompile(`\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}`)
+
+type pageMeta struct {
+ // 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
+
+ // This is a standalone page not part of any page collection. These
+ // include sitemap, robotsTXT and similar. It will have no pageOutputs, but
+ // a fixed pageOutput.
+ standalone bool
+
+ bundleType string
+
+ // Params contains configuration defined in the params section of page frontmatter.
+ params map[string]interface{}
+
+ title string
+ linkTitle string
+
+ summary string
+
+ resourcePath string
+
+ weight int
+
+ markup string
+ contentType string
+
+ // whether the content is in a CJK language.
+ isCJKLanguage bool
+
+ layout string
+
+ aliases []string
+
+ draft bool
+
+ description string
+ keywords []string
+
+ urlPaths pagemeta.URLPath
+
+ resource.Dates
+
+ // This is enabled if it is a leaf bundle (the "index.md" type) and it is marked as headless in front matter.
+ // Being headless means that
+ // 1. The page itself is not rendered to disk
+ // 2. It is not available in .Site.Pages etc.
+ // 3. But you can get it via .Site.GetPage
+ headless bool
+
+ // Set if this page is bundled inside another.
+ bundled bool
+
+ // A key that maps to translation(s) of this page. This value is fetched
+ // from the page front matter.
+ translationKey string
+
+ // From front matter.
+ configuredOutputFormats output.Formats
+
+ // This is the raw front matter metadata that is going to be assigned to
+ // the Resources above.
+ resourcesMetadata []map[string]interface{}
+
+ f source.File
+
+ sections []string
+
+ // Sitemap overrides from front matter.
+ sitemap config.Sitemap
+
+ s *Site
+
+ renderingConfig *helpers.BlackFriday
+}
+
+func (p *pageMeta) Aliases() []string {
+ return p.aliases
+}
+
+func (p *pageMeta) Author() page.Author {
+ authors := p.Authors()
+
+ for _, author := range authors {
+ return author
+ }
+ return page.Author{}
+}
+
+func (p *pageMeta) Authors() page.AuthorList {
+ authorKeys, ok := p.params["authors"]
+ if !ok {
+ return page.AuthorList{}
+ }
+ authors := authorKeys.([]string)
+ if len(authors) < 1 || len(p.s.Info.Authors) < 1 {
+ return page.AuthorList{}
+ }
+
+ al := make(page.AuthorList)
+ for _, author := range authors {
+ a, ok := p.s.Info.Authors[author]
+ if ok {
+ al[author] = a
+ }
+ }
+ return al
+}
+
+func (p *pageMeta) BundleType() string {
+ return p.bundleType
+}
+
+func (p *pageMeta) Description() string {
+ return p.description
+}
+
+func (p *pageMeta) Lang() string {
+ return p.s.Lang()
+}
+
+func (p *pageMeta) Draft() bool {
+ return p.draft
+}
+
+func (p *pageMeta) File() source.File {
+ return p.f
+}
+
+func (p *pageMeta) IsHome() bool {
+ return p.Kind() == page.KindHome
+}
+
+func (p *pageMeta) Keywords() []string {
+ return p.keywords
+}
+
+func (p *pageMeta) Kind() string {
+ return p.kind
+}
+
+func (p *pageMeta) Layout() string {
+ return p.layout
+}
+
+func (p *pageMeta) LinkTitle() string {
+ if p.linkTitle != "" {
+ return p.linkTitle
+ }
+
+ return p.Title()
+}
+
+func (p *pageMeta) Name() string {
+ if p.resourcePath != "" {
+ return p.resourcePath
+ }
+ return p.Title()
+}
+
+func (p *pageMeta) IsNode() bool {
+ return !p.IsPage()
+}
+
+func (p *pageMeta) IsPage() bool {
+ return p.Kind() == page.KindPage
+}
+
+// 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 SiteInfo.
+// TODO(bep) interface
+func (p *pageMeta) Param(key interface{}) (interface{}, error) {
+ return resource.Param(p, p.s.Info.Params(), key)
+}
+
+func (p *pageMeta) Params() map[string]interface{} {
+ return p.params
+}
+
+func (p *pageMeta) Path() string {
+ if !p.File().IsZero() {
+ return p.File().Path()
+ }
+ return p.SectionsPath()
+}
+
+// RelatedKeywords implements the related.Document interface needed for fast page searches.
+func (p *pageMeta) RelatedKeywords(cfg related.IndexConfig) ([]related.Keyword, error) {
+
+ v, err := p.Param(cfg.Name)
+ if err != nil {
+ return nil, err
+ }
+
+ return cfg.ToKeywords(v)
+}
+
+func (p *pageMeta) IsSection() bool {
+ return p.Kind() == page.KindSection
+}
+
+func (p *pageMeta) Section() string {
+ if p.IsHome() {
+ return ""
+ }
+
+ if p.IsNode() {
+ if len(p.sections) == 0 {
+ // May be a sitemap or similar.
+ return ""
+ }
+ return p.sections[0]
+ }
+
+ if !p.File().IsZero() {
+ return p.File().Section()
+ }
+
+ panic("invalid page state")
+
+}
+
+func (p *pageMeta) SectionsEntries() []string {
+ return p.sections
+}
+
+func (p *pageMeta) SectionsPath() string {
+ return path.Join(p.SectionsEntries()...)
+}
+
+func (p *pageMeta) Sitemap() config.Sitemap {
+ return p.sitemap
+}
+
+func (p *pageMeta) Title() string {
+ return p.title
+}
+
+func (p *pageMeta) Type() string {
+ if p.contentType != "" {
+ return p.contentType
+ }
+
+ if x := p.Section(); x != "" {
+ return x
+ }
+
+ return "page"
+}
+
+func (p *pageMeta) Weight() int {
+ return p.weight
+}
+
+func (pm *pageMeta) setMetadata(p *pageState, frontmatter map[string]interface{}) error {
+ if frontmatter == nil {
+ return errors.New("missing frontmatter data")
+ }
+
+ pm.params = make(map[string]interface{})
+
+ // Needed for case insensitive fetching of params values
+ maps.ToLower(frontmatter)
+
+ var mtime time.Time
+ if p.File().FileInfo() != nil {
+ mtime = p.File().FileInfo().ModTime()
+ }
+
+ var gitAuthorDate time.Time
+ if p.gitInfo != nil {
+ gitAuthorDate = p.gitInfo.AuthorDate
+ }
+
+ descriptor := &pagemeta.FrontMatterDescriptor{
+ Frontmatter: frontmatter,
+ Params: pm.params,
+ Dates: &pm.Dates,
+ PageURLs: &pm.urlPaths,
+ BaseFilename: p.File().ContentBaseName(),
+ ModTime: mtime,
+ GitAuthorDate: gitAuthorDate,
+ }
+
+ // Handle the date separately
+ // TODO(bep) we need to "do more" in this area so this can be split up and
+ // more easily tested without the Page, but the coupling is strong.
+ err := pm.s.frontmatterHandler.HandleDates(descriptor)
+ if err != nil {
+ p.s.Log.ERROR.Printf("Failed to handle dates for page %q: %s", p.pathOrTitle(), err)
+ }
+
+ var sitemapSet bool
+
+ var draft, published, isCJKLanguage *bool
+ for k, v := range frontmatter {
+ loki := strings.ToLower(k)
+
+ if loki == "published" { // Intentionally undocumented
+ vv, err := cast.ToBoolE(v)
+ if err == nil {
+ published = &vv
+ }
+ // published may also be a date
+ continue
+ }
+
+ if pm.s.frontmatterHandler.IsDateKey(loki) {
+ continue
+ }
+
+ switch loki {
+ case "title":
+ pm.title = cast.ToString(v)
+ pm.params[loki] = pm.title
+ case "linktitle":
+ pm.linkTitle = cast.ToString(v)
+ pm.params[loki] = pm.linkTitle
+ case "summary":
+ pm.summary = cast.ToString(v)
+ pm.params[loki] = pm.summary
+ case "description":
+ pm.description = cast.ToString(v)
+ pm.params[loki] = pm.description
+ case "slug":
+ // Don't start or end with a -
+ pm.urlPaths.Slug = strings.Trim(cast.ToString(v), "-")
+ pm.params[loki] = pm.Slug()
+ case "url":
+ url := cast.ToString(v)
+ if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
+ return fmt.Errorf("URLs with protocol (http*) not supported: %q. In page %q", url, p.pathOrTitle())
+ }
+ lang := p.s.GetLanguagePrefix()
+ if lang != "" && !strings.HasPrefix(url, "/") && strings.HasPrefix(url, lang+"/") {
+ if strings.HasPrefix(hugo.CurrentVersion.String(), "0.55") {
+ // We added support for page relative URLs in Hugo 0.55 and
+ // this may get its language path added twice.
+ // TODO(bep) eventually remove this.
+ p.s.Log.WARN.Printf(`Front matter in %q with the url %q with no leading / has what looks like the language prefix added. In Hugo 0.55 we added support for page relative URLs in front matter, no language prefix needed. Check the URL and consider to either add a leading / or remove the language prefix.`, p.pathOrTitle(), url)
+
+ }
+ }
+ pm.urlPaths.URL = url
+ pm.params[loki] = url
+ case "type":
+ pm.contentType = cast.ToString(v)
+ pm.params[loki] = pm.contentType
+ case "keywords":
+ pm.keywords = cast.ToStringSlice(v)
+ pm.params[loki] = pm.keywords
+ case "headless":
+ // For now, only the leaf bundles ("index.md") can be headless (i.e. produce no output).
+ // We may expand on this in the future, but that gets more complex pretty fast.
+ if p.File().TranslationBaseName() == "index" {
+ pm.headless = cast.ToBool(v)
+ }
+ pm.params[loki] = pm.headless
+ 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 {
+ pm.configuredOutputFormats = outFormats
+ pm.params[loki] = outFormats
+ }
+
+ }
+ case "draft":
+ draft = new(bool)
+ *draft = cast.ToBool(v)
+ case "layout":
+ pm.layout = cast.ToString(v)
+ pm.params[loki] = pm.layout
+ case "markup":
+ pm.markup = cast.ToString(v)
+ pm.params[loki] = pm.markup
+ case "weight":
+ pm.weight = cast.ToInt(v)
+ pm.params[loki] = pm.weight
+ case "aliases":
+ pm.aliases = cast.ToStringSlice(v)
+ for i, alias := range pm.aliases {
+ if strings.HasPrefix(alias, "http://") || strings.HasPrefix(alias, "https://") {
+ return fmt.Errorf("http* aliases not supported: %q", alias)
+ }
+ pm.aliases[i] = filepath.ToSlash(alias)
+ }
+ pm.params[loki] = pm.aliases
+ case "sitemap":
+ p.m.sitemap = config.DecodeSitemap(p.s.siteCfg.sitemap, cast.ToStringMap(v))
+ pm.params[loki] = p.m.sitemap
+ sitemapSet = true
+ case "iscjklanguage":
+ isCJKLanguage = new(bool)
+ *isCJKLanguage = cast.ToBool(v)
+ case "translationkey":
+ pm.translationKey = cast.ToString(v)
+ pm.params[loki] = pm.translationKey
+ case "resources":
+ var resources []map[string]interface{}
+ handled := true
+
+ switch vv := v.(type) {
+ case []map[interface{}]interface{}:
+ for _, vvv := range vv {
+ resources = append(resources, cast.ToStringMap(vvv))
+ }
+ case []map[string]interface{}:
+ resources = append(resources, vv...)
+ case []interface{}:
+ for _, vvv := range vv {
+ switch vvvv := vvv.(type) {
+ case map[interface{}]interface{}:
+ resources = append(resources, cast.ToStringMap(vvvv))
+ case map[string]interface{}:
+ resources = append(resources, vvvv)
+ }
+ }
+ default:
+ handled = false
+ }
+
+ if handled {
+ pm.params[loki] = resources
+ pm.resourcesMetadata = resources
+ break
+ }
+ fallthrough
+
+ default:
+ // If not one of the explicit values, store in Params
+ switch vv := v.(type) {
+ case bool:
+ pm.params[loki] = vv
+ case string:
+ pm.params[loki] = vv
+ case int64, int32, int16, int8, int:
+ pm.params[loki] = vv
+ case float64, float32:
+ pm.params[loki] = vv
+ case time.Time:
+ pm.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
+ pm.params[loki] = vvv
+ case map[string]interface{}: // Proper parsing structured array from JSON based FrontMatter
+ pm.params[loki] = vvv
+ case []interface{}:
+ pm.params[loki] = vvv
+ default:
+ a := make([]string, len(vvv))
+ for i, u := range vvv {
+ a[i] = cast.ToString(u)
+ }
+
+ pm.params[loki] = a
+ }
+ } else {
+ pm.params[loki] = []string{}
+ }
+ default:
+ pm.params[loki] = vv
+ }
+ }
+ }
+ }
+
+ if !sitemapSet {
+ pm.sitemap = p.s.siteCfg.sitemap
+ }
+
+ pm.markup = helpers.GuessType(pm.markup)
+
+ if draft != nil && published != nil {
+ pm.draft = *draft
+ p.m.s.Log.WARN.Printf("page %q has both draft and published settings in its frontmatter. Using draft.", p.File().Filename())
+ } else if draft != nil {
+ pm.draft = *draft
+ } else if published != nil {
+ pm.draft = !*published
+ }
+ pm.params["draft"] = pm.draft
+
+ if isCJKLanguage != nil {
+ pm.isCJKLanguage = *isCJKLanguage
+ } else if p.s.siteCfg.hasCJKLanguage {
+ if cjkRe.Match(p.source.parsed.Input()) {
+ pm.isCJKLanguage = true
+ } else {
+ pm.isCJKLanguage = false
+ }
+ }
+
+ pm.params["iscjklanguage"] = p.m.isCJKLanguage
+
+ return nil
+}
+
+func (p *pageMeta) applyDefaultValues() error {
+ if p.markup == "" {
+ if !p.File().IsZero() {
+ // Fall back to file extension
+ p.markup = helpers.GuessType(p.File().Ext())
+ }
+ if p.markup == "" {
+ p.markup = "unknown"
+ }
+ }
+
+ if p.title == "" && p.f.IsZero() {
+ switch p.Kind() {
+ case page.KindHome:
+ p.title = p.s.Info.title
+ case page.KindSection:
+ sectionName := helpers.FirstUpper(p.sections[0])
+ if p.s.Cfg.GetBool("pluralizeListTitles") {
+ p.title = inflect.Pluralize(sectionName)
+ } else {
+ p.title = sectionName
+ }
+ case page.KindTaxonomy:
+ key := p.sections[len(p.sections)-1]
+ p.title = strings.Replace(p.s.titleFunc(key), "-", " ", -1)
+ case page.KindTaxonomyTerm:
+ p.title = p.s.titleFunc(p.sections[0])
+ case kind404:
+ p.title = "404 Page not found"
+
+ }
+ }
+
+ if p.IsNode() {
+ p.bundleType = "branch"
+ } else {
+ source := p.File()
+ if fi, ok := source.(*fileInfo); ok {
+ switch fi.bundleTp {
+ case bundleBranch:
+ p.bundleType = "branch"
+ case bundleLeaf:
+ p.bundleType = "leaf"
+ }
+ }
+ }
+
+ bfParam := getParamToLower(p, "blackfriday")
+ if bfParam != nil {
+ p.renderingConfig = p.s.ContentSpec.BlackFriday
+
+ // Create a copy so we can modify it.
+ bf := *p.s.ContentSpec.BlackFriday
+ p.renderingConfig = &bf
+ pageParam := cast.ToStringMap(bfParam)
+ if err := mapstructure.Decode(pageParam, &p.renderingConfig); err != nil {
+ return errors.WithMessage(err, "failed to decode rendering config")
+ }
+ }
+
+ return nil
+
+}
+
+// The output formats this page will be rendered to.
+func (m *pageMeta) outputFormats() output.Formats {
+ if len(m.configuredOutputFormats) > 0 {
+ return m.configuredOutputFormats
+ }
+
+ return m.s.outputFormats[m.Kind()]
+}
+
+func (p *pageMeta) Slug() string {
+ return p.urlPaths.Slug
+}
+
+func getParam(m resource.ResourceParamsProvider, key string, stringToLower bool) interface{} {
+ v := m.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 getParamToLower(m resource.ResourceParamsProvider, key string) interface{} {
+ return getParam(m, key, true)
+}
diff --git a/hugolib/page__new.go b/hugolib/page__new.go
new file mode 100644
index 000000000..64c84b0f8
--- /dev/null
+++ b/hugolib/page__new.go
@@ -0,0 +1,296 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "strings"
+
+ "github.com/gohugoio/hugo/common/hugo"
+
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/source"
+
+ "github.com/gohugoio/hugo/parser/pageparser"
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/output"
+
+ "github.com/gohugoio/hugo/lazy"
+
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/resources/resource"
+)
+
+func newPageBase(metaProvider *pageMeta) (*pageState, error) {
+ if metaProvider.s == nil {
+ panic("must provide a Site")
+ }
+
+ s := metaProvider.s
+
+ ps := &pageState{
+ pageOutput: nopPageOutput,
+ pageCommon: &pageCommon{
+ FileProvider: metaProvider,
+ AuthorProvider: metaProvider,
+ Scratcher: maps.NewScratcher(),
+ Positioner: page.NopPage,
+ InSectionPositioner: page.NopPage,
+ ResourceMetaProvider: metaProvider,
+ ResourceParamsProvider: metaProvider,
+ PageMetaProvider: metaProvider,
+ RelatedKeywordsProvider: metaProvider,
+ OutputFormatsProvider: page.NopPage,
+ ResourceTypesProvider: pageTypesProvider,
+ RefProvider: page.NopPage,
+ ShortcodeInfoProvider: page.NopPage,
+ LanguageProvider: s,
+ pagePages: &pagePages{},
+
+ InternalDependencies: s,
+ init: lazy.New(),
+ m: metaProvider,
+ s: s},
+ }
+
+ siteAdapter := pageSiteAdapter{s: s, p: ps}
+
+ deprecatedWarningPage := struct {
+ source.FileWithoutOverlap
+ page.DeprecatedWarningPageMethods1
+ }{
+ FileWithoutOverlap: metaProvider.File(),
+ DeprecatedWarningPageMethods1: &pageDeprecatedWarning{p: ps},
+ }
+
+ ps.DeprecatedWarningPageMethods = page.NewDeprecatedWarningPage(deprecatedWarningPage)
+ ps.pageMenus = &pageMenus{p: ps}
+ ps.PageMenusProvider = ps.pageMenus
+ ps.GetPageProvider = siteAdapter
+ ps.GitInfoProvider = ps
+ ps.TranslationsProvider = ps
+ ps.ResourceDataProvider = &pageData{pageState: ps}
+ ps.RawContentProvider = ps
+ ps.ChildCareProvider = ps
+ ps.TreeProvider = pageTree{p: ps}
+ ps.Eqer = ps
+ ps.TranslationKeyProvider = ps
+ ps.ShortcodeInfoProvider = ps
+ ps.PageRenderProvider = ps
+ ps.AlternativeOutputFormatsProvider = ps
+
+ return ps, nil
+
+}
+
+func newPageFromMeta(metaProvider *pageMeta) (*pageState, error) {
+ if metaProvider.f == nil {
+ metaProvider.f = page.NewZeroFile(metaProvider.s.DistinctWarningLog)
+ }
+
+ ps, err := newPageBase(metaProvider)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := metaProvider.applyDefaultValues(); err != nil {
+ return nil, err
+ }
+
+ ps.init.Add(func() (interface{}, error) {
+ pp, err := newPagePaths(metaProvider.s, ps, metaProvider)
+ if err != nil {
+ return nil, err
+ }
+
+ makeOut := func(f output.Format, render bool) *pageOutput {
+ return newPageOutput(nil, ps, pp, f, render)
+ }
+
+ if ps.m.standalone {
+ ps.pageOutput = makeOut(ps.m.outputFormats()[0], true)
+ } else {
+ ps.pageOutputs = make([]*pageOutput, len(ps.s.h.renderFormats))
+ created := make(map[string]*pageOutput)
+ outputFormatsForPage := ps.m.outputFormats()
+ for i, f := range ps.s.h.renderFormats {
+ po, found := created[f.Name]
+ if !found {
+ _, shouldRender := outputFormatsForPage.GetByName(f.Name)
+ po = makeOut(f, shouldRender)
+ created[f.Name] = po
+ }
+ ps.pageOutputs[i] = po
+ }
+ }
+
+ if err := ps.initCommonProviders(pp); err != nil {
+ return nil, err
+ }
+
+ return nil, nil
+
+ })
+
+ return ps, err
+
+}
+
+// Used by the legacy 404, sitemap and robots.txt rendering
+func newPageStandalone(m *pageMeta, f output.Format) (*pageState, error) {
+ m.configuredOutputFormats = output.Formats{f}
+ m.standalone = true
+ p, err := newPageFromMeta(m)
+
+ if err != nil {
+ return nil, err
+ }
+
+ if err := p.initPage(); err != nil {
+ return nil, err
+ }
+
+ return p, nil
+
+}
+
+func newPageWithContent(f *fileInfo, s *Site, bundled bool, content resource.OpenReadSeekCloser) (*pageState, error) {
+ sections := s.sectionsFromFile(f)
+ kind := s.kindFromFileInfoOrSections(f, sections)
+ if kind == page.KindTaxonomy {
+ s.PathSpec.MakePathsSanitized(sections)
+ }
+
+ metaProvider := &pageMeta{kind: kind, sections: sections, bundled: bundled, s: s, f: f}
+
+ ps, err := newPageBase(metaProvider)
+ if err != nil {
+ return nil, err
+ }
+
+ gi, err := s.h.gitInfoForPage(ps)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to load Git data")
+ }
+ ps.gitInfo = gi
+
+ r, err := content()
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+
+ parseResult, err := pageparser.Parse(
+ r,
+ pageparser.Config{EnableEmoji: s.siteCfg.enableEmoji},
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ ps.pageContent = pageContent{
+ source: rawPageContent{
+ parsed: parseResult,
+ posMainContent: -1,
+ posSummaryEnd: -1,
+ posBodyStart: -1,
+ },
+ }
+
+ ps.shortcodeState = newShortcodeHandler(ps, ps.s, nil)
+
+ if err := ps.mapContent(metaProvider); err != nil {
+ return nil, ps.wrapError(err)
+ }
+
+ if err := metaProvider.applyDefaultValues(); err != nil {
+ return nil, err
+ }
+
+ ps.init.Add(func() (interface{}, error) {
+ reuseContent := ps.renderable && !ps.shortcodeState.hasShortcodes()
+
+ // Creates what's needed for each output format.
+ contentPerOutput := newPageContentOutput(ps)
+
+ pp, err := newPagePaths(s, ps, metaProvider)
+ if err != nil {
+ return nil, err
+ }
+
+ // Prepare output formats for all sites.
+ ps.pageOutputs = make([]*pageOutput, len(ps.s.h.renderFormats))
+ created := make(map[string]*pageOutput)
+ outputFormatsForPage := ps.m.outputFormats()
+
+ for i, f := range ps.s.h.renderFormats {
+ if po, found := created[f.Name]; found {
+ ps.pageOutputs[i] = po
+ continue
+ }
+
+ _, render := outputFormatsForPage.GetByName(f.Name)
+ var contentProvider *pageContentOutput
+ if reuseContent && i > 0 {
+ contentProvider = ps.pageOutputs[0].cp
+ } else {
+ var err error
+ contentProvider, err = contentPerOutput(f)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ po := newPageOutput(contentProvider, ps, pp, f, render)
+ ps.pageOutputs[i] = po
+ created[f.Name] = po
+ }
+
+ if err := ps.initCommonProviders(pp); err != nil {
+ return nil, err
+ }
+
+ return nil, nil
+ })
+
+ return ps, nil
+}
+
+type pageDeprecatedWarning struct {
+ p *pageState
+}
+
+func (p *pageDeprecatedWarning) IsDraft() bool { return p.p.m.draft }
+func (p *pageDeprecatedWarning) Hugo() hugo.Info { return p.p.s.Info.Hugo() }
+func (p *pageDeprecatedWarning) LanguagePrefix() string { return p.p.s.Info.LanguagePrefix }
+func (p *pageDeprecatedWarning) GetParam(key string) interface{} {
+ return p.p.m.params[strings.ToLower(key)]
+}
+func (p *pageDeprecatedWarning) RSSLink() template.URL {
+ f := p.p.OutputFormats().Get("RSS")
+ if f == nil {
+ return ""
+ }
+ return template.URL(f.Permalink())
+}
+func (p *pageDeprecatedWarning) URL() string {
+ if p.p.IsPage() && p.p.m.urlPaths.URL != "" {
+ // This is the url set in front matter
+ return p.p.m.urlPaths.URL
+ }
+ // Fall back to the relative permalink.
+ return p.p.RelPermalink()
+
+}
diff --git a/hugolib/page__output.go b/hugolib/page__output.go
new file mode 100644
index 000000000..619ac0d77
--- /dev/null
+++ b/hugolib/page__output.go
@@ -0,0 +1,107 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/output"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/resources/resource"
+)
+
+func newPageOutput(
+ cp *pageContentOutput, // may be nil
+ ps *pageState,
+ pp pagePaths,
+ f output.Format,
+ render bool) *pageOutput {
+
+ var targetPathsProvider targetPathsHolder
+ var linksProvider resource.ResourceLinksProvider
+
+ ft, found := pp.targetPaths[f.Name]
+ if !found {
+ // Link to the main output format
+ ft = pp.targetPaths[pp.OutputFormats()[0].Format.Name]
+ }
+ targetPathsProvider = ft
+ linksProvider = ft
+
+ var paginatorProvider page.PaginatorProvider = page.NopPage
+ var pag *pagePaginator
+
+ if render && ps.IsNode() {
+ pag = newPagePaginator(ps)
+ paginatorProvider = pag
+ }
+
+ var contentProvider page.ContentProvider = page.NopPage
+ var tableOfContentsProvider page.TableOfContentsProvider = page.NopPage
+
+ if cp != nil {
+ contentProvider = cp
+ tableOfContentsProvider = cp
+ }
+
+ providers := struct {
+ page.ContentProvider
+ page.TableOfContentsProvider
+ page.PaginatorProvider
+ resource.ResourceLinksProvider
+ targetPather
+ }{
+ contentProvider,
+ tableOfContentsProvider,
+ paginatorProvider,
+ linksProvider,
+ targetPathsProvider,
+ }
+
+ po := &pageOutput{
+ f: f,
+ cp: cp,
+ pagePerOutputProviders: providers,
+ render: render,
+ paginator: pag,
+ }
+
+ return po
+
+}
+
+// We create a pageOutput for every output format combination, even if this
+// particular page isn't configured to be rendered to that format.
+type pageOutput struct {
+ // Set if this page isn't configured to be rendered to this format.
+ render bool
+
+ f output.Format
+
+ // Only set if render is set.
+ // Note that this will be lazily initialized, so only used if actually
+ // used in template(s).
+ paginator *pagePaginator
+
+ // This interface provides the functionality that is specific for this
+ // output format.
+ pagePerOutputProviders
+
+ // This may be nil.
+ cp *pageContentOutput
+}
+
+func (p *pageOutput) enablePlaceholders() {
+ if p.cp != nil {
+ p.cp.enablePlaceholders()
+ }
+}
diff --git a/hugolib/page__paginator.go b/hugolib/page__paginator.go
new file mode 100644
index 000000000..026546742
--- /dev/null
+++ b/hugolib/page__paginator.go
@@ -0,0 +1,98 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+
+ "github.com/gohugoio/hugo/resources/page"
+)
+
+func newPagePaginator(source *pageState) *pagePaginator {
+ return &pagePaginator{
+ source: source,
+ pagePaginatorInit: &pagePaginatorInit{},
+ }
+}
+
+type pagePaginator struct {
+ *pagePaginatorInit
+ source *pageState
+}
+
+type pagePaginatorInit struct {
+ init sync.Once
+ current *page.Pager
+}
+
+// reset resets the paginator to allow for a rebuild.
+func (p *pagePaginator) reset() {
+ p.pagePaginatorInit = &pagePaginatorInit{}
+}
+
+func (p *pagePaginator) Paginate(seq interface{}, options ...interface{}) (*page.Pager, error) {
+ var initErr error
+ p.init.Do(func() {
+ pagerSize, err := page.ResolvePagerSize(p.source.s.Cfg, options...)
+ if err != nil {
+ initErr = err
+ return
+ }
+
+ pd := p.source.targetPathDescriptor
+ pd.Type = p.source.outputFormat()
+ paginator, err := page.Paginate(pd, seq, pagerSize)
+ if err != nil {
+ initErr = err
+ return
+ }
+
+ p.current = paginator.Pagers()[0]
+
+ })
+
+ if initErr != nil {
+ return nil, initErr
+ }
+
+ return p.current, nil
+}
+
+func (p *pagePaginator) Paginator(options ...interface{}) (*page.Pager, error) {
+ var initErr error
+ p.init.Do(func() {
+ pagerSize, err := page.ResolvePagerSize(p.source.s.Cfg, options...)
+ if err != nil {
+ initErr = err
+ return
+ }
+
+ pd := p.source.targetPathDescriptor
+ pd.Type = p.source.outputFormat()
+ paginator, err := page.Paginate(pd, p.source.Pages(), pagerSize)
+ if err != nil {
+ initErr = err
+ return
+ }
+
+ p.current = paginator.Pagers()[0]
+
+ })
+
+ if initErr != nil {
+ return nil, initErr
+ }
+
+ return p.current, nil
+}
diff --git a/hugolib/page__paths.go b/hugolib/page__paths.go
new file mode 100644
index 000000000..adbdb4668
--- /dev/null
+++ b/hugolib/page__paths.go
@@ -0,0 +1,150 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "net/url"
+
+ "github.com/gohugoio/hugo/resources/page"
+)
+
+func newPagePaths(
+ s *Site,
+ p page.Page,
+ pm *pageMeta) (pagePaths, error) {
+
+ targetPathDescriptor, err := createTargetPathDescriptor(s, p, pm)
+ if err != nil {
+ return pagePaths{}, err
+ }
+
+ outputFormats := pm.outputFormats()
+ if len(outputFormats) == 0 {
+ outputFormats = pm.s.outputFormats[pm.Kind()]
+ }
+
+ if len(outputFormats) == 0 {
+ return pagePaths{}, nil
+ }
+
+ if pm.headless {
+ outputFormats = outputFormats[:1]
+ }
+
+ pageOutputFormats := make(page.OutputFormats, len(outputFormats))
+ targets := make(map[string]targetPathsHolder)
+
+ for i, f := range outputFormats {
+ desc := targetPathDescriptor
+ desc.Type = f
+ paths := page.CreateTargetPaths(desc)
+
+ var relPermalink, permalink string
+
+ // If a page is headless or bundled in another, it will not get published
+ // on its own and it will have no links.
+ if !pm.headless && !pm.bundled {
+ relPermalink = paths.RelPermalink(s.PathSpec)
+ permalink = paths.PermalinkForOutputFormat(s.PathSpec, f)
+ }
+
+ pageOutputFormats[i] = page.NewOutputFormat(relPermalink, permalink, len(outputFormats) == 1, f)
+
+ // Use the main format for permalinks, usually HTML.
+ permalinksIndex := 0
+ if f.Permalinkable {
+ // Unless it's permalinkable
+ permalinksIndex = i
+ }
+
+ targets[f.Name] = targetPathsHolder{
+ paths: paths,
+ OutputFormat: pageOutputFormats[permalinksIndex]}
+
+ }
+
+ return pagePaths{
+ outputFormats: pageOutputFormats,
+ targetPaths: targets,
+ targetPathDescriptor: targetPathDescriptor,
+ }, nil
+
+}
+
+type pagePaths struct {
+ outputFormats page.OutputFormats
+
+ targetPaths map[string]targetPathsHolder
+ targetPathDescriptor page.TargetPathDescriptor
+}
+
+func (l pagePaths) OutputFormats() page.OutputFormats {
+ return l.outputFormats
+}
+
+func createTargetPathDescriptor(s *Site, p page.Page, pm *pageMeta) (page.TargetPathDescriptor, error) {
+ var (
+ dir string
+ baseName string
+ )
+
+ d := s.Deps
+
+ if !p.File().IsZero() {
+ dir = p.File().Dir()
+ baseName = p.File().TranslationBaseName()
+ }
+
+ alwaysInSubDir := p.Kind() == kindSitemap
+
+ desc := page.TargetPathDescriptor{
+ PathSpec: d.PathSpec,
+ Kind: p.Kind(),
+ Sections: p.SectionsEntries(),
+ UglyURLs: s.Info.uglyURLs(p),
+ ForcePrefix: s.h.IsMultihost() || alwaysInSubDir,
+ Dir: dir,
+ URL: pm.urlPaths.URL,
+ }
+
+ if pm.Slug() != "" {
+ desc.BaseName = pm.Slug()
+ } else {
+ desc.BaseName = baseName
+ }
+
+ desc.PrefixFilePath = s.getLanguageTargetPathLang(alwaysInSubDir)
+ desc.PrefixLink = s.getLanguagePermalinkLang(alwaysInSubDir)
+
+ // Expand only page.KindPage and page.KindTaxonomy; don't expand other Kinds of Pages
+ // like page.KindSection or page.KindTaxonomyTerm because they are "shallower" and
+ // the permalink configuration values are likely to be redundant, e.g.
+ // naively expanding /category/:slug/ would give /category/categories/ for
+ // the "categories" page.KindTaxonomyTerm.
+ if p.Kind() == page.KindPage || p.Kind() == page.KindTaxonomy {
+ opath, err := d.ResourceSpec.Permalinks.Expand(p.Section(), p)
+ if err != nil {
+ return desc, err
+ }
+
+ if opath != "" {
+ opath, _ = url.QueryUnescape(opath)
+ desc.ExpandedPermalink = opath
+ }
+
+ }
+
+ return desc, nil
+
+}
diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go
new file mode 100644
index 000000000..177e0420a
--- /dev/null
+++ b/hugolib/page__per_output.go
@@ -0,0 +1,453 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "context"
+ "fmt"
+ "html/template"
+ "strings"
+ "sync"
+ "unicode/utf8"
+
+ "github.com/gohugoio/hugo/lazy"
+
+ bp "github.com/gohugoio/hugo/bufferpool"
+ "github.com/gohugoio/hugo/tpl"
+
+ "github.com/gohugoio/hugo/output"
+
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/resources/resource"
+)
+
+var (
+ nopTargetPath = targetPathsHolder{}
+ nopPagePerOutput = struct {
+ resource.ResourceLinksProvider
+ page.ContentProvider
+ page.PageRenderProvider
+ page.PaginatorProvider
+ page.TableOfContentsProvider
+ page.AlternativeOutputFormatsProvider
+
+ targetPather
+ }{
+ page.NopPage,
+ page.NopPage,
+ page.NopPage,
+ page.NopPage,
+ page.NopPage,
+ page.NopPage,
+ nopTargetPath,
+ }
+)
+
+func newPageContentOutput(p *pageState) func(f output.Format) (*pageContentOutput, error) {
+
+ parent := p.init
+
+ return func(f output.Format) (*pageContentOutput, error) {
+ cp := &pageContentOutput{
+ p: p,
+ f: f,
+ }
+
+ initContent := func() error {
+ var err error
+ var hasVariants bool
+
+ cp.contentPlaceholders, hasVariants, err = p.shortcodeState.renderShortcodesForPage(p, f)
+ if err != nil {
+ return err
+ }
+
+ if p.render && !hasVariants {
+ // We can reuse this for the other output formats
+ cp.enableReuse()
+ }
+
+ cp.workContent = p.contentToRender(cp.contentPlaceholders)
+
+ isHTML := cp.p.m.markup == "html"
+
+ if p.renderable {
+ if !isHTML {
+ cp.workContent = cp.renderContent(p, cp.workContent)
+ tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent)
+ cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents)
+ cp.workContent = tmpContent
+ }
+
+ if cp.placeholdersEnabled {
+ // ToC was accessed via .Page.TableOfContents in the shortcode,
+ // at a time when the ToC wasn't ready.
+ cp.contentPlaceholders[tocShortcodePlaceholder] = string(cp.tableOfContents)
+ }
+
+ if p.cmap.hasNonMarkdownShortcode || cp.placeholdersEnabled {
+ // There are one or more replacement tokens to be replaced.
+ cp.workContent, err = replaceShortcodeTokens(cp.workContent, cp.contentPlaceholders)
+ if err != nil {
+ return err
+ }
+ }
+
+ if cp.p.source.hasSummaryDivider {
+ if isHTML {
+ src := p.source.parsed.Input()
+
+ // Use the summary sections as they are provided by the user.
+ if p.source.posSummaryEnd != -1 {
+ cp.summary = helpers.BytesToHTML(src[p.source.posMainContent:p.source.posSummaryEnd])
+ }
+
+ if cp.p.source.posBodyStart != -1 {
+ cp.workContent = src[cp.p.source.posBodyStart:]
+ }
+
+ } else {
+ summary, content, err := splitUserDefinedSummaryAndContent(cp.p.m.markup, cp.workContent)
+ if err != nil {
+ cp.p.s.Log.ERROR.Printf("Failed to set user defined summary for page %q: %s", cp.p.pathOrTitle(), err)
+ } else {
+ cp.workContent = content
+ cp.summary = helpers.BytesToHTML(summary)
+ }
+ }
+ } else if cp.p.m.summary != "" {
+ html := cp.p.s.ContentSpec.RenderBytes(&helpers.RenderingContext{
+ Content: []byte(cp.p.m.summary), RenderTOC: false, PageFmt: cp.p.m.markup,
+ Cfg: p.Language(),
+ DocumentID: p.File().UniqueID(), DocumentName: p.File().Path(),
+ Config: cp.p.getRenderingConfig()})
+ html = cp.p.s.ContentSpec.TrimShortHTML(html)
+ cp.summary = helpers.BytesToHTML(html)
+ }
+ }
+
+ cp.content = helpers.BytesToHTML(cp.workContent)
+
+ if !p.renderable {
+ err := cp.addSelfTemplate()
+ return err
+ }
+
+ return nil
+
+ }
+
+ // Recursive loops can only happen in content files with template code (shortcodes etc.)
+ // Avoid creating new goroutines if we don't have to.
+ needTimeout := !p.renderable || p.shortcodeState.hasShortcodes()
+
+ if needTimeout {
+ cp.initMain = parent.BranchdWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) {
+ return nil, initContent()
+ })
+ } else {
+ cp.initMain = parent.Branch(func() (interface{}, error) {
+ return nil, initContent()
+ })
+ }
+
+ cp.initPlain = cp.initMain.Branch(func() (interface{}, error) {
+ cp.plain = helpers.StripHTML(string(cp.content))
+ cp.plainWords = strings.Fields(cp.plain)
+ cp.setWordCounts(p.m.isCJKLanguage)
+
+ if err := cp.setAutoSummary(); err != nil {
+ return err, nil
+ }
+
+ return nil, nil
+ })
+
+ return cp, nil
+
+ }
+
+}
+
+// pageContentOutput represents the Page content for a given output format.
+type pageContentOutput struct {
+ f output.Format
+
+ // If we can safely reuse this for other output formats.
+ reuse bool
+ reuseInit sync.Once
+
+ p *pageState
+
+ // Lazy load dependencies
+ initMain *lazy.Init
+ initPlain *lazy.Init
+
+ placeholdersEnabled bool
+ placeholdersEnabledInit sync.Once
+
+ // Content state
+
+ workContent []byte
+
+ // Temporary storage of placeholders mapped to their content.
+ // These are shortcodes etc. Some of these will need to be replaced
+ // after any markup is rendered, so they share a common prefix.
+ contentPlaceholders map[string]string
+
+ // Content sections
+ content template.HTML
+ summary template.HTML
+ tableOfContents template.HTML
+
+ truncated bool
+
+ plainWords []string
+ plain string
+ fuzzyWordCount int
+ wordCount int
+ readingTime int
+}
+
+func (p *pageContentOutput) Content() (interface{}, error) {
+ p.p.s.initInit(p.initMain, p.p)
+ return p.content, nil
+}
+
+func (p *pageContentOutput) FuzzyWordCount() int {
+ p.p.s.initInit(p.initPlain, p.p)
+ return p.fuzzyWordCount
+}
+
+func (p *pageContentOutput) Len() int {
+ p.p.s.initInit(p.initMain, p.p)
+ return len(p.content)
+}
+
+func (p *pageContentOutput) Plain() string {
+ p.p.s.initInit(p.initPlain, p.p)
+ return p.plain
+}
+
+func (p *pageContentOutput) PlainWords() []string {
+ p.p.s.initInit(p.initPlain, p.p)
+ return p.plainWords
+}
+
+func (p *pageContentOutput) ReadingTime() int {
+ p.p.s.initInit(p.initPlain, p.p)
+ return p.readingTime
+}
+
+func (p *pageContentOutput) Summary() template.HTML {
+ p.p.s.initInit(p.initMain, p.p)
+ if !p.p.source.hasSummaryDivider {
+ p.p.s.initInit(p.initPlain, p.p)
+ }
+ return p.summary
+}
+
+func (p *pageContentOutput) TableOfContents() template.HTML {
+ p.p.s.initInit(p.initMain, p.p)
+ return p.tableOfContents
+}
+
+func (p *pageContentOutput) Truncated() bool {
+ if p.p.truncated {
+ return true
+ }
+ p.p.s.initInit(p.initPlain, p.p)
+ return p.truncated
+}
+
+func (p *pageContentOutput) WordCount() int {
+ p.p.s.initInit(p.initPlain, p.p)
+ return p.wordCount
+}
+
+func (p *pageContentOutput) setAutoSummary() error {
+ if p.p.source.hasSummaryDivider || p.p.m.summary != "" {
+ return nil
+ }
+
+ var summary string
+ var truncated bool
+
+ if p.p.m.isCJKLanguage {
+ summary, truncated = p.p.s.ContentSpec.TruncateWordsByRune(p.plainWords)
+ } else {
+ summary, truncated = p.p.s.ContentSpec.TruncateWordsToWholeSentence(p.plain)
+ }
+ p.summary = template.HTML(summary)
+
+ p.truncated = truncated
+
+ return nil
+
+}
+
+func (cp *pageContentOutput) renderContent(p page.Page, content []byte) []byte {
+ return cp.p.s.ContentSpec.RenderBytes(&helpers.RenderingContext{
+ Content: content, RenderTOC: true, PageFmt: cp.p.m.markup,
+ Cfg: p.Language(),
+ DocumentID: p.File().UniqueID(), DocumentName: p.File().Path(),
+ Config: cp.p.getRenderingConfig()})
+}
+
+func (p *pageContentOutput) setWordCounts(isCJKLanguage bool) {
+ if 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 isCJKLanguage {
+ p.readingTime = (p.wordCount + 500) / 501
+ } else {
+ p.readingTime = (p.wordCount + 212) / 213
+ }
+}
+
+func (p *pageContentOutput) addSelfTemplate() error {
+ self := p.p.selfLayoutForOutput(p.f)
+ err := p.p.s.TemplateHandler().AddLateTemplate(self, string(p.content))
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// A callback to signal that we have inserted a placeholder into the rendered
+// content. This avoids doing extra replacement work.
+func (p *pageContentOutput) enablePlaceholders() {
+ p.placeholdersEnabledInit.Do(func() {
+ p.placeholdersEnabled = true
+ })
+}
+
+func (p *pageContentOutput) enableReuse() {
+ p.reuseInit.Do(func() {
+ p.reuse = true
+ })
+}
+
+// these will be shifted out when rendering a given output format.
+type pagePerOutputProviders interface {
+ targetPather
+ page.ContentProvider
+ page.PaginatorProvider
+ page.TableOfContentsProvider
+ resource.ResourceLinksProvider
+}
+
+type targetPather interface {
+ targetPaths() page.TargetPaths
+}
+
+type targetPathsHolder struct {
+ paths page.TargetPaths
+ page.OutputFormat
+}
+
+func (t targetPathsHolder) targetPaths() page.TargetPaths {
+ return t.paths
+}
+
+func executeToString(templ tpl.Template, data interface{}) (string, error) {
+ b := bp.GetBuffer()
+ defer bp.PutBuffer(b)
+ if err := templ.Execute(b, data); err != nil {
+ return "", err
+ }
+ return b.String(), nil
+
+}
+
+func splitUserDefinedSummaryAndContent(markup string, c []byte) (summary []byte, content []byte, err error) {
+ defer func() {
+ if r := recover(); r != nil {
+ err = fmt.Errorf("summary split failed: %s", r)
+ }
+ }()
+
+ startDivider := bytes.Index(c, internalSummaryDividerBaseBytes)
+
+ if startDivider == -1 {
+ return
+ }
+
+ startTag := "p"
+ switch markup {
+ case "asciidoc":
+ startTag = "div"
+
+ }
+
+ // Walk back and forward to the surrounding tags.
+ start := bytes.LastIndex(c[:startDivider], []byte("<"+startTag))
+ end := bytes.Index(c[startDivider:], []byte("</"+startTag))
+
+ if start == -1 {
+ start = startDivider
+ } else {
+ start = startDivider - (startDivider - start)
+ }
+
+ if end == -1 {
+ end = startDivider + len(internalSummaryDividerBase)
+ } else {
+ end = startDivider + end + len(startTag) + 3
+ }
+
+ var addDiv bool
+
+ switch markup {
+ case "rst":
+ addDiv = true
+ }
+
+ withoutDivider := append(c[:start], bytes.Trim(c[end:], "\n")...)
+
+ if len(withoutDivider) > 0 {
+ summary = bytes.TrimSpace(withoutDivider[:start])
+ }
+
+ if addDiv {
+ // For the rst
+ summary = append(append([]byte(nil), summary...), []byte("</div>")...)
+ }
+
+ if err != nil {
+ return
+ }
+
+ content = bytes.TrimSpace(withoutDivider)
+
+ return
+}
diff --git a/hugolib/page__position.go b/hugolib/page__position.go
new file mode 100644
index 000000000..458b3e423
--- /dev/null
+++ b/hugolib/page__position.go
@@ -0,0 +1,76 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/lazy"
+ "github.com/gohugoio/hugo/resources/page"
+)
+
+func newPagePosition(n *nextPrev) pagePosition {
+ return pagePosition{nextPrev: n}
+}
+
+func newPagePositionInSection(n *nextPrev) pagePositionInSection {
+ return pagePositionInSection{nextPrev: n}
+
+}
+
+type nextPrev struct {
+ init *lazy.Init
+ prevPage page.Page
+ nextPage page.Page
+}
+
+func (n *nextPrev) next() page.Page {
+ n.init.Do()
+ return n.nextPage
+}
+
+func (n *nextPrev) prev() page.Page {
+ n.init.Do()
+ return n.prevPage
+}
+
+type pagePosition struct {
+ *nextPrev
+}
+
+func (p pagePosition) Next() page.Page {
+ return p.next()
+}
+
+func (p pagePosition) NextPage() page.Page {
+ return p.Next()
+}
+
+func (p pagePosition) Prev() page.Page {
+ return p.prev()
+}
+
+func (p pagePosition) PrevPage() page.Page {
+ return p.Prev()
+}
+
+type pagePositionInSection struct {
+ *nextPrev
+}
+
+func (p pagePositionInSection) NextInSection() page.Page {
+ return p.next()
+}
+
+func (p pagePositionInSection) PrevInSection() page.Page {
+ return p.prev()
+}
diff --git a/hugolib/page__ref.go b/hugolib/page__ref.go
new file mode 100644
index 000000000..41bd527db
--- /dev/null
+++ b/hugolib/page__ref.go
@@ -0,0 +1,117 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/common/text"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/pkg/errors"
+)
+
+func newPageRef(p *pageState) pageRef {
+ return pageRef{p: p}
+}
+
+type pageRef struct {
+ p *pageState
+}
+
+func (p pageRef) Ref(argsm map[string]interface{}) (string, error) {
+ return p.ref(argsm, p.p)
+}
+
+func (p pageRef) RefFrom(argsm map[string]interface{}, source interface{}) (string, error) {
+ return p.ref(argsm, source)
+}
+
+func (p pageRef) RelRef(argsm map[string]interface{}) (string, error) {
+ return p.relRef(argsm, p.p)
+}
+
+func (p pageRef) RelRefFrom(argsm map[string]interface{}, source interface{}) (string, error) {
+ return p.relRef(argsm, source)
+}
+
+func (p pageRef) decodeRefArgs(args map[string]interface{}) (refArgs, *Site, error) {
+ var ra refArgs
+ err := mapstructure.WeakDecode(args, &ra)
+ if err != nil {
+ return ra, nil, nil
+ }
+
+ s := p.p.s
+
+ if ra.Lang != "" && ra.Lang != p.p.s.Language().Lang {
+ // Find correct site
+ found := false
+ for _, ss := range p.p.s.h.Sites {
+ if ss.Lang() == ra.Lang {
+ found = true
+ s = ss
+ }
+ }
+
+ if !found {
+ p.p.s.siteRefLinker.logNotFound(ra.Path, fmt.Sprintf("no site found with lang %q", ra.Lang), nil, text.Position{})
+ return ra, nil, nil
+ }
+ }
+
+ return ra, s, nil
+}
+
+func (p pageRef) ref(argsm map[string]interface{}, source interface{}) (string, error) {
+ args, s, err := p.decodeRefArgs(argsm)
+ if err != nil {
+ return "", errors.Wrap(err, "invalid arguments to Ref")
+ }
+
+ if s == nil {
+ return p.p.s.siteRefLinker.notFoundURL, nil
+ }
+
+ if args.Path == "" {
+ return "", nil
+ }
+
+ return s.refLink(args.Path, source, false, args.OutputFormat)
+
+}
+
+func (p pageRef) relRef(argsm map[string]interface{}, source interface{}) (string, error) {
+ args, s, err := p.decodeRefArgs(argsm)
+ if err != nil {
+ return "", errors.Wrap(err, "invalid arguments to Ref")
+ }
+
+ if s == nil {
+ return p.p.s.siteRefLinker.notFoundURL, nil
+ }
+
+ if args.Path == "" {
+ return "", nil
+ }
+
+ return s.refLink(args.Path, source, true, args.OutputFormat)
+
+}
+
+type refArgs struct {
+ Path string
+ Lang string
+ OutputFormat string
+}
diff --git a/hugolib/page__tree.go b/hugolib/page__tree.go
new file mode 100644
index 000000000..bddfde7c8
--- /dev/null
+++ b/hugolib/page__tree.go
@@ -0,0 +1,117 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/common/types"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/resources/page"
+)
+
+type pageTree struct {
+ p *pageState
+}
+
+func (pt pageTree) IsAncestor(other interface{}) (bool, error) {
+ if pt.p == nil {
+ return false, nil
+ }
+
+ pp, err := unwrapPage(other)
+ if err != nil || pp == nil {
+ return false, err
+ }
+
+ if pt.p.Kind() == page.KindPage && len(pt.p.SectionsEntries()) == len(pp.SectionsEntries()) {
+ // A regular page is never its section's ancestor.
+ return false, nil
+ }
+
+ return helpers.HasStringsPrefix(pp.SectionsEntries(), pt.p.SectionsEntries()), nil
+}
+
+func (pt pageTree) CurrentSection() page.Page {
+ p := pt.p
+
+ if p.IsHome() || p.IsSection() {
+ return p
+ }
+
+ return p.Parent()
+}
+
+func (pt pageTree) IsDescendant(other interface{}) (bool, error) {
+ if pt.p == nil {
+ return false, nil
+ }
+ pp, err := unwrapPage(other)
+ if err != nil || pp == nil {
+ return false, err
+ }
+
+ if pp.Kind() == page.KindPage && len(pt.p.SectionsEntries()) == len(pp.SectionsEntries()) {
+ // A regular page is never its section's descendant.
+ return false, nil
+ }
+ return helpers.HasStringsPrefix(pt.p.SectionsEntries(), pp.SectionsEntries()), nil
+}
+
+func (pt pageTree) FirstSection() page.Page {
+ p := pt.p
+
+ parent := p.Parent()
+
+ if types.IsNil(parent) || parent.IsHome() {
+ return p
+ }
+
+ for {
+ current := parent
+ parent = parent.Parent()
+ if types.IsNil(parent) || parent.IsHome() {
+ return current
+ }
+ }
+
+}
+
+func (pt pageTree) InSection(other interface{}) (bool, error) {
+ if pt.p == nil || types.IsNil(other) {
+ return false, nil
+ }
+
+ pp, err := unwrapPage(other)
+ if err != nil {
+ return false, err
+ }
+
+ if pp == nil {
+ return false, nil
+ }
+
+ return pp.CurrentSection().Eq(pt.p.CurrentSection()), nil
+
+}
+
+func (pt pageTree) Page() page.Page {
+ return pt.p
+}
+
+func (pt pageTree) Parent() page.Page {
+ return pt.p.parent
+}
+
+func (pt pageTree) Sections() page.Pages {
+ return pt.p.subSections
+}
diff --git a/hugolib/page_kinds.go b/hugolib/page_kinds.go
new file mode 100644
index 000000000..39de31a16
--- /dev/null
+++ b/hugolib/page_kinds.go
@@ -0,0 +1,40 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/resources/page"
+)
+
+var (
+
+ // This is all the kinds we can expect to find in .Site.Pages.
+ allKindsInPages = []string{page.KindPage, page.KindHome, page.KindSection, page.KindTaxonomy, page.KindTaxonomyTerm}
+ allKinds = append(allKindsInPages, []string{kindRSS, kindSitemap, kindRobotsTXT, kind404}...)
+)
+
+const (
+
+ // 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"
+
+ pageResourceType = "page"
+)
diff --git a/hugolib/page_permalink_test.go b/hugolib/page_permalink_test.go
new file mode 100644
index 000000000..526f9578b
--- /dev/null
+++ b/hugolib/page_permalink_test.go
@@ -0,0 +1,150 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"},
+ // Issue #4666
+ {"x/y/z/boo-makeindex.md", "http://barnew/boo", "", "", true, true, "http://barnew/boo/x/y/z/boo-makeindex.html", "/x/y/z/boo-makeindex.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 {
+ t.Run(fmt.Sprintf("%s-%d", test.file, i), func(t *testing.T) {
+
+ 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
+output: ["HTML"]
+---
+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)
+ }
+ })
+ }
+
+}
+
+func TestRelativeURLInFrontMatter(t *testing.T) {
+
+ config := `
+baseURL = "https://example.com"
+defaultContentLanguage = "en"
+defaultContentLanguageInSubdir = false
+
+[Languages]
+[Languages.en]
+weight = 10
+contentDir = "content/en"
+[Languages.nn]
+weight = 20
+contentDir = "content/nn"
+
+`
+
+ pageTempl := `---
+title: "A page"
+url: %q
+---
+
+Some content.
+`
+
+ b := newTestSitesBuilder(t).WithConfigFile("toml", config)
+ b.WithContent("content/en/blog/page1.md", fmt.Sprintf(pageTempl, "myblog/p1/"))
+ b.WithContent("content/en/blog/page2.md", fmt.Sprintf(pageTempl, "../../../../../myblog/p2/"))
+ b.WithContent("content/en/blog/page3.md", fmt.Sprintf(pageTempl, "../myblog/../myblog/p3/"))
+ b.WithContent("content/en/blog/_index.md", fmt.Sprintf(pageTempl, "this-is-my-english-blog"))
+ b.WithContent("content/nn/blog/page1.md", fmt.Sprintf(pageTempl, "myblog/p1/"))
+ b.WithContent("content/nn/blog/_index.md", fmt.Sprintf(pageTempl, "this-is-my-blog"))
+
+ b.Build(BuildCfg{})
+
+ b.AssertFileContent("public/nn/myblog/p1/index.html", "Single: A page|Hello|nn|RelPermalink: /nn/myblog/p1/|")
+ b.AssertFileContent("public/nn/this-is-my-blog/index.html", "List Page 1|A page|Hello|https://example.com/nn/this-is-my-blog/|")
+ b.AssertFileContent("public/this-is-my-english-blog/index.html", "List Page 1|A page|Hello|https://example.com/this-is-my-english-blog/|")
+ b.AssertFileContent("public/myblog/p1/index.html", "Single: A page|Hello|en|RelPermalink: /myblog/p1/|Permalink: https://example.com/myblog/p1/|")
+ b.AssertFileContent("public/myblog/p2/index.html", "Single: A page|Hello|en|RelPermalink: /myblog/p2/|Permalink: https://example.com/myblog/p2/|")
+ b.AssertFileContent("public/myblog/p3/index.html", "Single: A page|Hello|en|RelPermalink: /myblog/p3/|Permalink: https://example.com/myblog/p3/|")
+
+}
diff --git a/hugolib/page_test.go b/hugolib/page_test.go
new file mode 100644
index 000000000..91ccb0d3e
--- /dev/null
+++ b/hugolib/page_test.go
@@ -0,0 +1,1539 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "os"
+
+ "github.com/gohugoio/hugo/common/loggers"
+
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/gohugoio/hugo/hugofs"
+
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/resources/resource"
+
+ "github.com/spf13/afero"
+ "github.com/spf13/viper"
+
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+const (
+ homePage = "---\ntitle: Home\n---\nHome Page Content\n"
+ simplePage = "---\ntitle: Simple\n---\nSimple Page\n"
+
+ simplePageRFC3339Date = "---\ntitle: RFC3339 Date\ndate: \"2013-05-17T16:59:30Z\"\n---\nrfc3339 content"
+
+ simplePageWithoutSummaryDelimiter = `---
+title: SimpleWithoutSummaryDelimiter
+---
+[Lorem ipsum](https://lipsum.com/) dolor sit amet, consectetur adipiscing 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.
+
+Additional text.
+
+Further text.
+`
+
+ simplePageWithSummaryDelimiter = `---
+title: Simple
+---
+Summary Next Line
+
+<!--more-->
+Some more text
+`
+
+ simplePageWithSummaryParameter = `---
+title: SimpleWithSummaryParameter
+summary: "Page with summary parameter and [a link](http://www.example.com/)"
+---
+
+Some text.
+
+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
+`
+
+ 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`
+)
+
+func checkPageTitle(t *testing.T, page 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.Page, expected string, msg ...interface{}) {
+ a := normalizeContent(expected)
+ b := normalizeContent(content(page))
+ if a != b {
+ t.Log(stackTrace())
+ 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.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.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.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.Page, time time.Time) {
+ if page.Date() != time {
+ t.Fatalf("Page date is: %s. Expected: %s", page.Date(), time)
+ }
+}
+
+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 page.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() }},
+ {"rst", func() bool { return helpers.HasRst() }},
+ }
+
+ for _, e := range engines {
+ if !e.shouldExecute() {
+ continue
+ }
+
+ cfg, fs := newTestCfg()
+
+ 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])
+ }
+
+ // Add a content page for the home page
+ homePath := fmt.Sprintf("_index.%s", e.ext)
+ writeSource(t, fs, filepath.Join(contentDir, homePath), homePage)
+
+ 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())
+
+ home, err := s.Info.Home()
+ require.NoError(t, err)
+ require.NotNil(t, home)
+ require.Equal(t, homePath, home.File().Path())
+ require.Contains(t, content(home), "Home Page Content")
+
+ }
+
+}
+
+// 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 href=\"#fn:1\">1</a></sup></p>") {
+ t.Fatalf("Got summary:\n%q", p.Summary())
+ }
+
+ c := content(p)
+ if c != "<p>The <a href=\"http://gohugo.io/\">best static site generator</a>.<sup class=\"footnote-ref\" id=\"fnref:1\"><a href=\"#fn:1\">1</a></sup></p>\n\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", c)
+ }
+}
+
+func TestPageDatesAllKinds(t *testing.T) {
+ t.Parallel()
+ assert := assert.New(t)
+
+ pageContent := `
+---
+title: Page
+date: 2017-01-15
+tags: ["hugo"]
+categories: ["cool stuff"]
+---
+`
+
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile().WithContent("page.md", pageContent)
+ b.WithContent("blog/page.md", pageContent)
+
+ b.CreateSites().Build(BuildCfg{})
+
+ assert.Equal(1, len(b.H.Sites))
+ s := b.H.Sites[0]
+
+ checkDate := func(t time.Time, msg string) {
+ assert.Equal(2017, t.Year(), msg)
+ }
+
+ checkDated := func(d resource.Dated, msg string) {
+ checkDate(d.Date(), "date: "+msg)
+ checkDate(d.Lastmod(), "lastmod: "+msg)
+ }
+ for _, p := range s.Pages() {
+ checkDated(p, p.Kind())
+ }
+ checkDate(s.Info.LastChange(), "site")
+
+}
+
+func TestPageDatesSections(t *testing.T) {
+ t.Parallel()
+ assert := assert.New(t)
+
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile().WithContent("no-index/page.md", `
+---
+title: Page
+date: 2017-01-15
+---
+`)
+ b.WithSimpleConfigFile().WithContent("with-index-no-date/_index.md", `---
+title: No Date
+---
+
+`)
+
+ // https://github.com/gohugoio/hugo/issues/5854
+ b.WithSimpleConfigFile().WithContent("with-index-date/_index.md", `---
+title: Date
+date: 2018-01-15
+---
+
+`)
+
+ b.CreateSites().Build(BuildCfg{})
+
+ assert.Equal(1, len(b.H.Sites))
+ s := b.H.Sites[0]
+
+ assert.Equal(2017, s.getPage("/").Date().Year())
+ assert.Equal(2017, s.getPage("/no-index").Date().Year())
+ assert.True(s.getPage("/with-index-no-date").Date().IsZero())
+ assert.Equal(2018, s.getPage("/with-index-date").Date().Year())
+
+}
+
+func TestCreateNewPage(t *testing.T) {
+ t.Parallel()
+ assertFunc := func(t *testing.T, ext string, pages page.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.File().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")
+ }
+
+ settings := map[string]interface{}{
+ "contentDir": "mycontent",
+ }
+
+ testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePage)
+}
+
+func TestPageSummary(t *testing.T) {
+ t.Parallel()
+ assertFunc := func(t *testing.T, ext string, pages page.Pages) {
+ p := pages[0]
+ checkPageTitle(t, p, "SimpleWithoutSummaryDelimiter")
+ // Source is not Asciidoctor- or RST-compatibile so don't test them
+ if ext != "ad" && ext != "rst" {
+ checkPageContent(t, p, normalizeExpected(ext, "<p><a href=\"https://lipsum.com/\">Lorem ipsum</a> dolor sit amet, consectetur adipiscing 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.</p>\n\n<p>Additional text.</p>\n\n<p>Further text.</p>\n"), ext)
+ checkPageSummary(t, p, normalizeExpected(ext, "Lorem ipsum dolor sit amet, consectetur adipiscing 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. Additional text."), ext)
+ }
+ checkPageType(t, p, "page")
+ }
+
+ testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithoutSummaryDelimiter)
+}
+
+func TestPageWithDelimiter(t *testing.T) {
+ t.Parallel()
+ assertFunc := func(t *testing.T, ext string, pages page.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")
+ }
+
+ testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryDelimiter)
+}
+
+func TestPageWithSummaryParameter(t *testing.T) {
+ t.Parallel()
+ assertFunc := func(t *testing.T, ext string, pages page.Pages) {
+ p := pages[0]
+ checkPageTitle(t, p, "SimpleWithSummaryParameter")
+ checkPageContent(t, p, normalizeExpected(ext, "<p>Some text.</p>\n\n<p>Some more text.</p>\n"), ext)
+ // Summary is not Asciidoctor- or RST-compatibile so don't test them
+ if ext != "ad" && ext != "rst" {
+ checkPageSummary(t, p, normalizeExpected(ext, "Page with summary parameter and <a href=\"http://www.example.com/\">a link</a>"), ext)
+ }
+ checkPageType(t, p, "page")
+ }
+
+ testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryParameter)
+}
+
+// Issue #3854
+// Also see https://github.com/gohugoio/hugo/issues/3977
+func TestPageWithDateFields(t *testing.T) {
+ assert := require.New(t)
+ pageWithDate := `---
+title: P%d
+weight: %d
+%s: 2017-10-13
+---
+Simple Page With Some Date`
+
+ hasDate := func(p page.Page) bool {
+ return p.Date().Year() == 2017
+ }
+
+ datePage := func(field string, weight int) string {
+ return fmt.Sprintf(pageWithDate, weight, weight, field)
+ }
+
+ t.Parallel()
+ assertFunc := func(t *testing.T, ext string, pages page.Pages) {
+ assert.True(len(pages) > 0)
+ for _, p := range pages {
+ assert.True(hasDate(p))
+ }
+
+ }
+
+ fields := []string{"date", "publishdate", "pubdate", "published"}
+ pageContents := make([]string, len(fields))
+ for i, field := range fields {
+ pageContents[i] = datePage(field, i+1)
+ }
+
+ testAllMarkdownEnginesForPages(t, assertFunc, nil, pageContents...)
+}
+
+// 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.Equal(t, p.RawContent(), "**Raw**")
+
+}
+
+func TestPageWithShortCodeInSummary(t *testing.T) {
+ t.Parallel()
+ assertFunc := func(t *testing.T, ext string, pages page.Pages) {
+ p := pages[0]
+ checkPageTitle(t, p, "Simple")
+ checkPageContent(t, p, normalizeExpected(ext, "<p>Summary Next Line. <figure> <img src=\"/not/real\"/> </figure> . More text here.</p><p>Some more text</p>"))
+ 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 page.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&ndash;as swift as the passage of light&ndash;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>&ldquo;You&rsquo;re a great Granser,&rdquo; he cried delightedly, &ldquo;always making believe them little marks mean something.&rdquo;</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 page.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)
+}
+
+// #2973
+func TestSummaryWithHTMLTagsOnNextLine(t *testing.T) {
+
+ assertFunc := func(t *testing.T, ext string, pages page.Pages) {
+ p := pages[0]
+ require.Contains(t, p.Summary(), "Happy new year everyone!")
+ require.NotContains(t, p.Summary(), "User interface")
+ }
+
+ testAllMarkdownEnginesForPages(t, assertFunc, nil, `---
+title: Simple
+---
+Happy new year everyone!
+
+Here is the last report for commits in the year 2016. It covers hrev50718-hrev50829.
+
+<!--more-->
+
+<h3>User interface</h3>
+
+`)
+}
+
+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 TestPageWithLastmodFromGitInfo(t *testing.T) {
+ assrt := require.New(t)
+
+ // We need to use the OS fs for this.
+ cfg := viper.New()
+ fs := hugofs.NewFrom(hugofs.Os, cfg)
+ fs.Destination = &afero.MemMapFs{}
+
+ cfg.Set("frontmatter", map[string]interface{}{
+ "lastmod": []string{":git", "lastmod"},
+ })
+ cfg.Set("defaultContentLanguage", "en")
+
+ langConfig := map[string]interface{}{
+ "en": map[string]interface{}{
+ "weight": 1,
+ "languageName": "English",
+ "contentDir": "content",
+ },
+ "nn": map[string]interface{}{
+ "weight": 2,
+ "languageName": "Nynorsk",
+ "contentDir": "content_nn",
+ },
+ }
+
+ cfg.Set("languages", langConfig)
+ cfg.Set("enableGitInfo", true)
+
+ assrt.NoError(loadDefaultSettingsFor(cfg))
+ assrt.NoError(loadLanguageSettings(cfg, nil))
+
+ wd, err := os.Getwd()
+ assrt.NoError(err)
+ cfg.Set("workingDir", filepath.Join(wd, "testsite"))
+
+ h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg})
+
+ assrt.NoError(err)
+ assrt.Len(h.Sites, 2)
+
+ require.NoError(t, h.Build(BuildCfg{SkipRender: true}))
+
+ enSite := h.Sites[0]
+ assrt.Len(enSite.RegularPages(), 1)
+
+ // 2018-03-11 is the Git author date for testsite/content/first-post.md
+ assrt.Equal("2018-03-11", enSite.RegularPages()[0].Lastmod().Format("2006-01-02"))
+
+ nnSite := h.Sites[1]
+ assrt.Len(nnSite.RegularPages(), 1)
+
+ // 2018-08-11 is the Git author date for testsite/content_nn/first-post.md
+ assrt.Equal("2018-08-11", nnSite.RegularPages()[0].Lastmod().Format("2006-01-02"))
+
+}
+
+func TestPageWithFrontMatterConfig(t *testing.T) {
+ t.Parallel()
+
+ for _, dateHandler := range []string{":filename", ":fileModTime"} {
+ t.Run(fmt.Sprintf("dateHandler=%q", dateHandler), func(t *testing.T) {
+ assrt := require.New(t)
+ cfg, fs := newTestCfg()
+
+ pageTemplate := `
+---
+title: Page
+weight: %d
+lastMod: 2018-02-28
+%s
+---
+Content
+`
+
+ cfg.Set("frontmatter", map[string]interface{}{
+ "date": []string{dateHandler, "date"},
+ })
+
+ c1 := filepath.Join("content", "section", "2012-02-21-noslug.md")
+ c2 := filepath.Join("content", "section", "2012-02-22-slug.md")
+
+ writeSource(t, fs, c1, fmt.Sprintf(pageTemplate, 1, ""))
+ writeSource(t, fs, c2, fmt.Sprintf(pageTemplate, 2, "slug: aslug"))
+
+ c1fi, err := fs.Source.Stat(c1)
+ assrt.NoError(err)
+ c2fi, err := fs.Source.Stat(c2)
+ assrt.NoError(err)
+
+ s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
+
+ assrt.Len(s.RegularPages(), 2)
+
+ noSlug := s.RegularPages()[0]
+ slug := s.RegularPages()[1]
+
+ assrt.Equal(28, noSlug.Lastmod().Day())
+
+ switch strings.ToLower(dateHandler) {
+ case ":filename":
+ assrt.False(noSlug.Date().IsZero())
+ assrt.False(slug.Date().IsZero())
+ assrt.Equal(2012, noSlug.Date().Year())
+ assrt.Equal(2012, slug.Date().Year())
+ assrt.Equal("noslug", noSlug.Slug())
+ assrt.Equal("aslug", slug.Slug())
+ case ":filemodtime":
+ assrt.Equal(c1fi.ModTime().Year(), noSlug.Date().Year())
+ assrt.Equal(c2fi.ModTime().Year(), slug.Date().Year())
+ fallthrough
+ default:
+ assrt.Equal("", noSlug.Slug())
+ assrt.Equal("aslug", slug.Slug())
+
+ }
+ })
+ }
+
+}
+
+func TestWordCountWithAllCJKRunesWithoutHasCJKLanguage(t *testing.T) {
+ t.Parallel()
+ assertFunc := func(t *testing.T, ext string, pages page.Pages) {
+ p := pages[0]
+ if p.WordCount() != 8 {
+ t.Fatalf("[%s] incorrect word count. expected %v, got %v", ext, 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 page.Pages) {
+ p := pages[0]
+ if p.WordCount() != 15 {
+ t.Fatalf("[%s] incorrect word count, expected %v, got %v", ext, 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 page.Pages) {
+ p := pages[0]
+ if p.WordCount() != 74 {
+ t.Fatalf("[%s] incorrect word count, expected %v, got %v", ext, 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 page.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 page.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.FuzzyWordCount())
+ }
+
+ if p.ReadingTime() != 3 {
+ t.Fatalf("[%s] incorrect min read. expected %v, got %v", ext, 3, p.ReadingTime())
+ }
+
+ }
+
+ testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithLongContent)
+}
+
+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)
+
+ }
+}
+
+func TestTranslationKey(t *testing.T) {
+ t.Parallel()
+ assert := require.New(t)
+ cfg, fs := newTestCfg()
+
+ writeSource(t, fs, filepath.Join("content", filepath.FromSlash("sect/simple.no.md")), "---\ntitle: \"A1\"\ntranslationKey: \"k1\"\n---\nContent\n")
+ writeSource(t, fs, filepath.Join("content", filepath.FromSlash("sect/simple.en.md")), "---\ntitle: \"A2\"\n---\nContent\n")
+
+ s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
+
+ require.Len(t, s.RegularPages(), 2)
+
+ home, _ := s.Info.Home()
+ assert.NotNil(home)
+ assert.Equal("home", home.TranslationKey())
+ assert.Equal("page/k1", s.RegularPages()[0].TranslationKey())
+ p2 := s.RegularPages()[1]
+
+ assert.Equal("page/sect/simple", p2.TranslationKey())
+
+}
+
+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")
+}
+
+func TestPageWithEmoji(t *testing.T) {
+ for _, enableEmoji := range []bool{true, false} {
+ v := viper.New()
+ v.Set("enableEmoji", enableEmoji)
+ b := newTestSitesBuilder(t)
+ b.WithViper(v)
+
+ b.WithSimpleConfigFile()
+
+ b.WithContent("page-emoji.md", `---
+title: "Hugo Smile"
+---
+This is a :smile:.
+<!--more-->
+
+Another :smile: This is :not: :an: :emoji:.
+
+O :christmas_tree:
+
+Write me an :e-mail: or :email:?
+
+Too many colons: :: ::: :::: :?: :!: :.:
+
+If you dislike this video, you can hit that :-1: button :stuck_out_tongue_winking_eye:,
+but if you like it, hit :+1: and get subscribed!
+`)
+
+ b.CreateSites().Build(BuildCfg{})
+
+ if enableEmoji {
+ b.AssertFileContent("public/page-emoji/index.html",
+ "This is a 😄",
+ "Another 😄",
+ "This is :not: :an: :emoji:.",
+ "O 🎄",
+ "Write me an 📧 or ✉️?",
+ "Too many colons: :: ::: :::: :?: :!: :.:",
+ "you can hit that 👎 button 😜,",
+ "hit 👍 and get subscribed!",
+ )
+ } else {
+ b.AssertFileContent("public/page-emoji/index.html",
+ "This is a :smile:",
+ "Another :smile:",
+ "This is :not: :an: :emoji:.",
+ "O :christmas_tree:",
+ "Write me an :e-mail: or :email:?",
+ "Too many colons: :: ::: :::: :?: :!: :.:",
+ "you can hit that :-1: button :stuck_out_tongue_winking_eye:,",
+ "hit :+1: and get subscribed!",
+ )
+ }
+
+ }
+
+}
+
+func TestPageHTMLContent(t *testing.T) {
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile()
+
+ frontmatter := `---
+title: "HTML Content"
+---
+`
+ b.WithContent("regular.html", frontmatter+`<h1>Hugo</h1>`)
+ b.WithContent("noblackfridayforyou.html", frontmatter+`**Hugo!**`)
+ b.WithContent("manualsummary.html", frontmatter+`
+<p>This is summary</p>
+<!--more-->
+<p>This is the main content.</p>`)
+
+ b.Build(BuildCfg{})
+
+ b.AssertFileContent(
+ "public/regular/index.html",
+ "Single: HTML Content|Hello|en|RelPermalink: /regular/|",
+ "Summary: Hugo|Truncated: false")
+
+ b.AssertFileContent(
+ "public/noblackfridayforyou/index.html",
+ "Permalink: http://example.com/noblackfridayforyou/|**Hugo!**|",
+ )
+
+ // https://github.com/gohugoio/hugo/issues/5723
+ b.AssertFileContent(
+ "public/manualsummary/index.html",
+ "Single: HTML Content|Hello|en|RelPermalink: /manualsummary/|",
+ "Summary: \n<p>This is summary</p>\n|Truncated: true",
+ "|<p>This is the main content.</p>|",
+ )
+
+}
+
+// https://github.com/gohugoio/hugo/issues/5381
+func TestPageManualSummary(t *testing.T) {
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile()
+
+ b.WithContent("page-md-shortcode.md", `---
+title: "Hugo"
+---
+This is a {{< sc >}}.
+<!--more-->
+Content.
+`)
+
+ // https://github.com/gohugoio/hugo/issues/5464
+ b.WithContent("page-md-only-shortcode.md", `---
+title: "Hugo"
+---
+{{< sc >}}
+<!--more-->
+{{< sc >}}
+`)
+
+ b.WithContent("page-md-shortcode-same-line.md", `---
+title: "Hugo"
+---
+This is a {{< sc >}}<!--more-->Same line.
+`)
+
+ b.WithContent("page-md-shortcode-same-line-after.md", `---
+title: "Hugo"
+---
+Summary<!--more-->{{< sc >}}
+`)
+
+ b.WithContent("page-org-shortcode.org", `#+TITLE: T1
+#+AUTHOR: A1
+#+DESCRIPTION: D1
+This is a {{< sc >}}.
+# more
+Content.
+`)
+
+ b.WithContent("page-org-variant1.org", `#+TITLE: T1
+Summary.
+
+# more
+
+Content.
+`)
+
+ b.WithTemplatesAdded("layouts/shortcodes/sc.html", "a shortcode")
+ b.WithTemplatesAdded("layouts/_default/single.html", `
+SUMMARY:{{ .Summary }}:END
+--------------------------
+CONTENT:{{ .Content }}
+`)
+
+ b.CreateSites().Build(BuildCfg{})
+
+ b.AssertFileContent("public/page-md-shortcode/index.html",
+ "SUMMARY:<p>This is a a shortcode.</p>:END",
+ "CONTENT:<p>This is a a shortcode.</p>\n\n<p>Content.</p>\n",
+ )
+
+ b.AssertFileContent("public/page-md-shortcode-same-line/index.html",
+ "SUMMARY:<p>This is a a shortcode</p>:END",
+ "CONTENT:<p>This is a a shortcode</p>\n\n<p>Same line.</p>\n",
+ )
+
+ b.AssertFileContent("public/page-md-shortcode-same-line-after/index.html",
+ "SUMMARY:<p>Summary</p>:END",
+ "CONTENT:<p>Summary</p>\n\na shortcode",
+ )
+
+ b.AssertFileContent("public/page-org-shortcode/index.html",
+ "SUMMARY:<p>This is a a shortcode.</p>:END",
+ "CONTENT:<p>This is a a shortcode.</p>\n\n<p>Content.\t</p>\n",
+ )
+ b.AssertFileContent("public/page-org-variant1/index.html",
+ "SUMMARY:<p>Summary.</p>:END",
+ "CONTENT:<p>Summary.</p>\n\n<p>Content.\t</p>\n",
+ )
+
+ b.AssertFileContent("public/page-md-only-shortcode/index.html",
+ "SUMMARY:a shortcode:END",
+ "CONTENT:a shortcode\n\na shortcode\n",
+ )
+}
+
+// https://github.com/gohugoio/hugo/issues/5478
+func TestPageWithCommentedOutFrontMatter(t *testing.T) {
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile()
+
+ b.WithContent("page.md", `<!--
++++
+title = "hello"
++++
+-->
+This is the content.
+`)
+
+ b.WithTemplatesAdded("layouts/_default/single.html", `
+Title: {{ .Title }}
+Content:{{ .Content }}
+`)
+
+ b.CreateSites().Build(BuildCfg{})
+
+ b.AssertFileContent("public/page/index.html",
+ "Title: hello",
+ "Content:<p>This is the content.</p>",
+ )
+
+}
+
+// https://github.com/gohugoio/hugo/issues/5781
+func TestPageWithZeroFile(t *testing.T) {
+ newTestSitesBuilder(t).WithLogger(loggers.NewWarningLogger()).WithSimpleConfigFile().
+ WithTemplatesAdded("index.html", "{{ .File.Filename }}{{ with .File }}{{ .Dir }}{{ end }}").Build(BuildCfg{})
+}
+
+func TestHomePageWithNoTitle(t *testing.T) {
+ b := newTestSitesBuilder(t).WithConfigFile("toml", `
+title = "Site Title"
+`)
+ b.WithTemplatesAdded("index.html", "Title|{{ with .Title }}{{ . }}{{ end }}|")
+ b.WithContent("_index.md", `---
+description: "No title for you!"
+---
+
+Content.
+`)
+
+ b.Build(BuildCfg{})
+ b.AssertFileContent("public/index.html", "Title||")
+}
+
+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())
+ }
+
+ })
+ }
+ }
+}
+
+// https://github.com/gohugoio/hugo/issues/4675
+func TestWordCountAndSimilarVsSummary(t *testing.T) {
+
+ t.Parallel()
+ assert := require.New(t)
+
+ single := []string{"_default/single.html", `
+WordCount: {{ .WordCount }}
+FuzzyWordCount: {{ .FuzzyWordCount }}
+ReadingTime: {{ .ReadingTime }}
+Len Plain: {{ len .Plain }}
+Len PlainWords: {{ len .PlainWords }}
+Truncated: {{ .Truncated }}
+Len Summary: {{ len .Summary }}
+Len Content: {{ len .Content }}
+
+SUMMARY:{{ .Summary }}:{{ len .Summary }}:END
+`}
+
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile().WithTemplatesAdded(single...).WithContent("p1.md", fmt.Sprintf(`---
+title: p1
+---
+
+%s
+
+`, strings.Repeat("word ", 510)),
+
+ "p2.md", fmt.Sprintf(`---
+title: p2
+---
+This is a summary.
+
+<!--more-->
+
+%s
+
+`, strings.Repeat("word ", 310)),
+ "p3.md", fmt.Sprintf(`---
+title: p3
+isCJKLanguage: true
+---
+Summary: In Chinese, 好 means good.
+
+<!--more-->
+
+%s
+
+`, strings.Repeat("好", 200)),
+ "p4.md", fmt.Sprintf(`---
+title: p4
+isCJKLanguage: false
+---
+Summary: In Chinese, 好 means good.
+
+<!--more-->
+
+%s
+
+`, strings.Repeat("好", 200)),
+
+ "p5.md", fmt.Sprintf(`---
+title: p4
+isCJKLanguage: true
+---
+Summary: In Chinese, 好 means good.
+
+%s
+
+`, strings.Repeat("好", 200)),
+ "p6.md", fmt.Sprintf(`---
+title: p4
+isCJKLanguage: false
+---
+Summary: In Chinese, 好 means good.
+
+%s
+
+`, strings.Repeat("好", 200)),
+ )
+
+ b.CreateSites().Build(BuildCfg{})
+
+ assert.Equal(1, len(b.H.Sites))
+ require.Len(t, b.H.Sites[0].RegularPages(), 6)
+
+ b.AssertFileContent("public/p1/index.html", "WordCount: 510\nFuzzyWordCount: 600\nReadingTime: 3\nLen Plain: 2550\nLen PlainWords: 510\nTruncated: false\nLen Summary: 2549\nLen Content: 2557")
+
+ b.AssertFileContent("public/p2/index.html", "WordCount: 314\nFuzzyWordCount: 400\nReadingTime: 2\nLen Plain: 1569\nLen PlainWords: 314\nTruncated: true\nLen Summary: 25\nLen Content: 1583")
+
+ b.AssertFileContent("public/p3/index.html", "WordCount: 206\nFuzzyWordCount: 300\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 43\nLen Content: 652")
+ b.AssertFileContent("public/p4/index.html", "WordCount: 7\nFuzzyWordCount: 100\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 43\nLen Content: 652")
+ b.AssertFileContent("public/p5/index.html", "WordCount: 206\nFuzzyWordCount: 300\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 229\nLen Content: 653")
+ b.AssertFileContent("public/p6/index.html", "WordCount: 7\nFuzzyWordCount: 100\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: false\nLen Summary: 637\nLen Content: 653")
+
+}
+
+func TestScratchSite(t *testing.T) {
+ t.Parallel()
+
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile().WithTemplatesAdded("index.html", `
+{{ .Scratch.Set "b" "bv" }}
+B: {{ .Scratch.Get "b" }}
+`,
+ "shortcodes/scratch.html", `
+{{ .Scratch.Set "c" "cv" }}
+C: {{ .Scratch.Get "c" }}
+`,
+ )
+
+ b.WithContentAdded("scratchme.md", `
+---
+title: Scratch Me!
+---
+
+{{< scratch >}}
+`)
+ b.Build(BuildCfg{})
+
+ b.AssertFileContent("public/index.html", "B: bv")
+ b.AssertFileContent("public/scratchme/index.html", "C: cv")
+}
diff --git a/hugolib/page_unwrap.go b/hugolib/page_unwrap.go
new file mode 100644
index 000000000..eda6636d1
--- /dev/null
+++ b/hugolib/page_unwrap.go
@@ -0,0 +1,50 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/pkg/errors"
+
+ "github.com/gohugoio/hugo/resources/page"
+)
+
+// Wraps a Page.
+type pageWrapper interface {
+ page() page.Page
+}
+
+// unwrapPage is used in equality checks and similar.
+func unwrapPage(in interface{}) (page.Page, error) {
+ switch v := in.(type) {
+ case *pageState:
+ return v, nil
+ case pageWrapper:
+ return v.page(), nil
+ case page.Page:
+ return v, nil
+ case nil:
+ return nil, nil
+ default:
+ return nil, errors.Errorf("unwrapPage: %T not supported", in)
+ }
+}
+
+func mustUnwrapPage(in interface{}) page.Page {
+ p, err := unwrapPage(in)
+ if err != nil {
+ panic(err)
+ }
+
+ return p
+}
diff --git a/hugolib/page_unwrap_test.go b/hugolib/page_unwrap_test.go
new file mode 100644
index 000000000..23747dce8
--- /dev/null
+++ b/hugolib/page_unwrap_test.go
@@ -0,0 +1,37 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/gohugoio/hugo/resources/page"
+ "github.com/stretchr/testify/require"
+)
+
+func TestUnwrapPage(t *testing.T) {
+ assert := require.New(t)
+
+ p := &pageState{}
+
+ assert.Equal(p, mustUnwrap(newPageForShortcode(p)))
+}
+
+func mustUnwrap(v interface{}) page.Page {
+ p, err := unwrapPage(v)
+ if err != nil {
+ panic(err)
+ }
+ return p
+}
diff --git a/hugolib/pagebundler.go b/hugolib/pagebundler.go
new file mode 100644
index 000000000..5149968bc
--- /dev/null
+++ b/hugolib/pagebundler.go
@@ -0,0 +1,206 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "context"
+ "fmt"
+ "math"
+ "path/filepath"
+
+ "github.com/gohugoio/hugo/config"
+
+ _errors "github.com/pkg/errors"
+
+ "golang.org/x/sync/errgroup"
+)
+
+type siteContentProcessor struct {
+ site *Site
+
+ handleContent contentHandler
+
+ ctx context.Context
+
+ // The input file bundles.
+ fileBundlesChan chan *bundleDir
+
+ // The input file singles.
+ fileSinglesChan chan *fileInfo
+
+ // These assets should be just copied to destination.
+ fileAssetsChan chan pathLangFile
+
+ numWorkers int
+
+ // The output Pages
+ pagesChan chan *pageState
+
+ // Used for partial rebuilds (aka. live reload)
+ // Will signal replacement of pages in the site collection.
+ partialBuild bool
+}
+
+func (s *siteContentProcessor) processBundle(b *bundleDir) {
+ select {
+ case s.fileBundlesChan <- b:
+ case <-s.ctx.Done():
+ }
+}
+
+func (s *siteContentProcessor) processSingle(fi *fileInfo) {
+ select {
+ case s.fileSinglesChan <- fi:
+ case <-s.ctx.Done():
+ }
+}
+
+func (s *siteContentProcessor) processAsset(asset pathLangFile) {
+ select {
+ case s.fileAssetsChan <- asset:
+ case <-s.ctx.Done():
+ }
+}
+
+func newSiteContentProcessor(ctx context.Context, partialBuild bool, s *Site) *siteContentProcessor {
+ numWorkers := config.GetNumWorkerMultiplier() * 3
+
+ numWorkers = int(math.Ceil(float64(numWorkers) / float64(len(s.h.Sites))))
+
+ return &siteContentProcessor{
+ ctx: ctx,
+ partialBuild: partialBuild,
+ site: s,
+ handleContent: newHandlerChain(s),
+ fileBundlesChan: make(chan *bundleDir, numWorkers),
+ fileSinglesChan: make(chan *fileInfo, numWorkers),
+ fileAssetsChan: make(chan pathLangFile, numWorkers),
+ numWorkers: numWorkers,
+ pagesChan: make(chan *pageState, numWorkers),
+ }
+}
+
+func (s *siteContentProcessor) closeInput() {
+ close(s.fileSinglesChan)
+ close(s.fileBundlesChan)
+ close(s.fileAssetsChan)
+}
+
+func (s *siteContentProcessor) process(ctx context.Context) error {
+ g1, ctx := errgroup.WithContext(ctx)
+ g2, ctx := errgroup.WithContext(ctx)
+
+ // There can be only one of these per site.
+ g1.Go(func() error {
+ for p := range s.pagesChan {
+ if p.s != s.site {
+ panic(fmt.Sprintf("invalid page site: %v vs %v", p.s, s))
+ }
+
+ p.forceRender = s.partialBuild
+
+ if p.forceRender {
+ s.site.replacePage(p)
+ } else {
+ s.site.addPage(p)
+ }
+ }
+ return nil
+ })
+
+ for i := 0; i < s.numWorkers; i++ {
+ g2.Go(func() error {
+ for {
+ select {
+ case f, ok := <-s.fileSinglesChan:
+ if !ok {
+ return nil
+ }
+
+ err := s.readAndConvertContentFile(f)
+ if err != nil {
+ return err
+ }
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+ })
+
+ g2.Go(func() error {
+ for {
+ select {
+ case file, ok := <-s.fileAssetsChan:
+ if !ok {
+ return nil
+ }
+ f, err := s.site.BaseFs.Content.Fs.Open(file.Filename())
+ if err != nil {
+ return _errors.Wrap(err, "failed to open assets file")
+ }
+ filename := filepath.Join(s.site.GetTargetLanguageBasePath(), file.Path())
+ err = s.site.publish(&s.site.PathSpec.ProcessingStats.Files, filename, f)
+ f.Close()
+ if err != nil {
+ return err
+ }
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+ })
+
+ g2.Go(func() error {
+ for {
+ select {
+ case bundle, ok := <-s.fileBundlesChan:
+ if !ok {
+ return nil
+ }
+ err := s.readAndConvertContentBundle(bundle)
+ if err != nil {
+ return err
+ }
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+ })
+ }
+
+ err := g2.Wait()
+
+ close(s.pagesChan)
+
+ if err != nil {
+ return err
+ }
+
+ if err := g1.Wait(); err != nil {
+ return err
+ }
+
+ return nil
+
+}
+
+func (s *siteContentProcessor) readAndConvertContentFile(file *fileInfo) error {
+ ctx := &handlerContext{source: file, pages: s.pagesChan}
+ return s.handleContent(ctx).err
+}
+
+func (s *siteContentProcessor) readAndConvertContentBundle(bundle *bundleDir) error {
+ ctx := &handlerContext{bundle: bundle, pages: s.pagesChan}
+ return s.handleContent(ctx).err
+}
diff --git a/hugolib/pagebundler_capture.go b/hugolib/pagebundler_capture.go
new file mode 100644
index 000000000..7c01a751d
--- /dev/null
+++ b/hugolib/pagebundler_capture.go
@@ -0,0 +1,773 @@
+// 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 (
+ "errors"
+ "fmt"
+ "os"
+ "path"
+ "path/filepath"
+
+ "github.com/gohugoio/hugo/config"
+
+ "github.com/gohugoio/hugo/common/loggers"
+ _errors "github.com/pkg/errors"
+
+ "sort"
+ "strings"
+ "sync"
+
+ "github.com/spf13/afero"
+
+ "github.com/gohugoio/hugo/hugofs"
+
+ "github.com/gohugoio/hugo/helpers"
+
+ "golang.org/x/sync/errgroup"
+
+ "github.com/gohugoio/hugo/source"
+)
+
+var errSkipCyclicDir = errors.New("skip potential cyclic dir")
+
+type capturer struct {
+ // To prevent symbolic link cycles: Visit same folder only once.
+ seen map[string]bool
+ seenMu sync.Mutex
+
+ handler captureResultHandler
+
+ sourceSpec *source.SourceSpec
+ fs afero.Fs
+ logger *loggers.Logger
+
+ // Filenames limits the content to process to a list of filenames/directories.
+ // This is used for partial building in server mode.
+ filenames []string
+
+ // Used to determine how to handle content changes in server mode.
+ contentChanges *contentChangeMap
+
+ // Semaphore used to throttle the concurrent sub directory handling.
+ sem chan bool
+}
+
+func newCapturer(
+ logger *loggers.Logger,
+ sourceSpec *source.SourceSpec,
+ handler captureResultHandler,
+ contentChanges *contentChangeMap,
+ filenames ...string) *capturer {
+
+ numWorkers := config.GetNumWorkerMultiplier()
+
+ // TODO(bep) the "index" vs "_index" check/strings should be moved in one place.
+ isBundleHeader := func(filename string) bool {
+ base := filepath.Base(filename)
+ name := helpers.Filename(base)
+ return IsContentFile(base) && (name == "index" || name == "_index")
+ }
+
+ // Make sure that any bundle header files are processed before the others. This makes
+ // sure that any bundle head is processed before its resources.
+ sort.Slice(filenames, func(i, j int) bool {
+ a, b := filenames[i], filenames[j]
+ ac, bc := isBundleHeader(a), isBundleHeader(b)
+
+ if ac {
+ return true
+ }
+
+ if bc {
+ return false
+ }
+
+ return a < b
+ })
+
+ c := &capturer{
+ sem: make(chan bool, numWorkers),
+ handler: handler,
+ sourceSpec: sourceSpec,
+ fs: sourceSpec.SourceFs,
+ logger: logger,
+ contentChanges: contentChanges,
+ seen: make(map[string]bool),
+ filenames: filenames}
+
+ return c
+}
+
+// Captured files and bundles ready to be processed will be passed on to
+// these channels.
+type captureResultHandler interface {
+ handleSingles(fis ...*fileInfo)
+ handleCopyFile(fi pathLangFile)
+ captureBundlesHandler
+}
+
+type captureBundlesHandler interface {
+ handleBundles(b *bundleDirs)
+}
+
+type captureResultHandlerChain struct {
+ handlers []captureBundlesHandler
+}
+
+func (c *captureResultHandlerChain) handleSingles(fis ...*fileInfo) {
+ for _, h := range c.handlers {
+ if hh, ok := h.(captureResultHandler); ok {
+ hh.handleSingles(fis...)
+ }
+ }
+}
+func (c *captureResultHandlerChain) handleBundles(b *bundleDirs) {
+ for _, h := range c.handlers {
+ h.handleBundles(b)
+ }
+}
+
+func (c *captureResultHandlerChain) handleCopyFile(file pathLangFile) {
+ for _, h := range c.handlers {
+ if hh, ok := h.(captureResultHandler); ok {
+ hh.handleCopyFile(file)
+ }
+ }
+}
+
+func (c *capturer) capturePartial(filenames ...string) error {
+ handled := make(map[string]bool)
+
+ for _, filename := range filenames {
+ dir, resolvedFilename, tp := c.contentChanges.resolveAndRemove(filename)
+ if handled[resolvedFilename] {
+ continue
+ }
+
+ handled[resolvedFilename] = true
+
+ switch tp {
+ case bundleLeaf:
+ if err := c.handleDir(resolvedFilename); err != nil {
+ // Directory may have been deleted.
+ if !os.IsNotExist(err) {
+ return err
+ }
+ }
+ case bundleBranch:
+ if err := c.handleBranchDir(resolvedFilename); err != nil {
+ // Directory may have been deleted.
+ if !os.IsNotExist(err) {
+ return err
+ }
+ }
+ default:
+ fi, err := c.resolveRealPath(resolvedFilename)
+ if os.IsNotExist(err) {
+ // File has been deleted.
+ continue
+ }
+
+ // Just in case the owning dir is a new symlink -- this will
+ // create the proper mapping for it.
+ c.resolveRealPath(dir)
+
+ f, active := c.newFileInfo(fi, tp)
+ if active {
+ c.copyOrHandleSingle(f)
+ }
+ }
+ }
+
+ return nil
+}
+
+func (c *capturer) capture() error {
+ if len(c.filenames) > 0 {
+ return c.capturePartial(c.filenames...)
+ }
+
+ err := c.handleDir(helpers.FilePathSeparator)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (c *capturer) handleNestedDir(dirname string) error {
+ select {
+ case c.sem <- true:
+ var g errgroup.Group
+
+ g.Go(func() error {
+ defer func() {
+ <-c.sem
+ }()
+ return c.handleDir(dirname)
+ })
+ return g.Wait()
+ default:
+ // For deeply nested file trees, waiting for a semaphore wil deadlock.
+ return c.handleDir(dirname)
+ }
+}
+
+// This handles a bundle branch and its resources only. This is used
+// in server mode on changes. If this dir does not (anymore) represent a bundle
+// branch, the handling is upgraded to the full handleDir method.
+func (c *capturer) handleBranchDir(dirname string) error {
+ files, err := c.readDir(dirname)
+ if err != nil {
+
+ return err
+ }
+
+ var (
+ dirType bundleDirType
+ )
+
+ for _, fi := range files {
+ if !fi.IsDir() {
+ tp, _ := classifyBundledFile(fi.RealName())
+ if dirType == bundleNot {
+ dirType = tp
+ }
+
+ if dirType == bundleLeaf {
+ return c.handleDir(dirname)
+ }
+ }
+ }
+
+ if dirType != bundleBranch {
+ return c.handleDir(dirname)
+ }
+
+ dirs := newBundleDirs(bundleBranch, c)
+
+ var secondPass []*fileInfo
+
+ // Handle potential bundle headers first.
+ for _, fi := range files {
+ if fi.IsDir() {
+ continue
+ }
+
+ tp, isContent := classifyBundledFile(fi.RealName())
+
+ f, active := c.newFileInfo(fi, tp)
+
+ if !active {
+ continue
+ }
+
+ if !f.isOwner() {
+ if !isContent {
+ // This is a partial update -- we only care about the files that
+ // is in this bundle.
+ secondPass = append(secondPass, f)
+ }
+ continue
+ }
+ dirs.addBundleHeader(f)
+ }
+
+ for _, f := range secondPass {
+ dirs.addBundleFiles(f)
+ }
+
+ c.handler.handleBundles(dirs)
+
+ return nil
+
+}
+
+func (c *capturer) handleDir(dirname string) error {
+
+ files, err := c.readDir(dirname)
+ if err != nil {
+ return err
+ }
+
+ type dirState int
+
+ const (
+ dirStateDefault dirState = iota
+
+ dirStateAssetsOnly
+ dirStateSinglesOnly
+ )
+
+ var (
+ fileBundleTypes = make([]bundleDirType, len(files))
+
+ // Start with the assumption that this dir contains only non-content assets (images etc.)
+ // If that is still true after we had a first look at the list of files, we
+ // can just copy the files to destination. We will still have to look at the
+ // sub-folders for potential bundles.
+ state = dirStateAssetsOnly
+
+ // Start with the assumption that this dir is not a bundle.
+ // A directory is a bundle if it contains a index content file,
+ // e.g. index.md (a leaf bundle) or a _index.md (a branch bundle).
+ bundleType = bundleNot
+ )
+
+ /* First check for any content files.
+ - If there are none, then this is a assets folder only (images etc.)
+ and we can just plainly copy them to
+ destination.
+ - If this is a section with no image etc. or similar, we can just handle it
+ as it was a single content file.
+ */
+ var hasNonContent, isBranch bool
+
+ for i, fi := range files {
+ if !fi.IsDir() {
+ tp, isContent := classifyBundledFile(fi.RealName())
+
+ fileBundleTypes[i] = tp
+ if !isBranch {
+ isBranch = tp == bundleBranch
+ }
+
+ if isContent {
+ // This is not a assets-only folder.
+ state = dirStateDefault
+ } else {
+ hasNonContent = true
+ }
+ }
+ }
+
+ if isBranch && !hasNonContent {
+ // This is a section or similar with no need for any bundle handling.
+ state = dirStateSinglesOnly
+ }
+
+ if state > dirStateDefault {
+ return c.handleNonBundle(dirname, files, state == dirStateSinglesOnly)
+ }
+
+ var fileInfos = make([]*fileInfo, 0, len(files))
+
+ for i, fi := range files {
+
+ currentType := bundleNot
+
+ if !fi.IsDir() {
+ currentType = fileBundleTypes[i]
+ if bundleType == bundleNot && currentType != bundleNot {
+ bundleType = currentType
+ }
+ }
+
+ if bundleType == bundleNot && currentType != bundleNot {
+ bundleType = currentType
+ }
+
+ f, active := c.newFileInfo(fi, currentType)
+
+ if !active {
+ continue
+ }
+
+ fileInfos = append(fileInfos, f)
+ }
+
+ var todo []*fileInfo
+
+ if bundleType != bundleLeaf {
+ for _, fi := range fileInfos {
+ if fi.FileInfo().IsDir() {
+ // Handle potential nested bundles.
+ if err := c.handleNestedDir(fi.Path()); err != nil {
+ return err
+ }
+ } else if bundleType == bundleNot || (!fi.isOwner() && fi.isContentFile()) {
+ // Not in a bundle.
+ c.copyOrHandleSingle(fi)
+ } else {
+ // This is a section folder or similar with non-content files in it.
+ todo = append(todo, fi)
+ }
+ }
+ } else {
+ todo = fileInfos
+ }
+
+ if len(todo) == 0 {
+ return nil
+ }
+
+ dirs, err := c.createBundleDirs(todo, bundleType)
+ if err != nil {
+ return err
+ }
+
+ // Send the bundle to the next step in the processor chain.
+ c.handler.handleBundles(dirs)
+
+ return nil
+}
+
+func (c *capturer) handleNonBundle(
+ dirname string,
+ fileInfos pathLangFileFis,
+ singlesOnly bool) error {
+
+ for _, fi := range fileInfos {
+ if fi.IsDir() {
+ if err := c.handleNestedDir(fi.Filename()); err != nil {
+ return err
+ }
+ } else {
+ if singlesOnly {
+ f, active := c.newFileInfo(fi, bundleNot)
+ if !active {
+ continue
+ }
+ c.handler.handleSingles(f)
+ } else {
+ c.handler.handleCopyFile(fi)
+ }
+ }
+ }
+
+ return nil
+}
+
+func (c *capturer) copyOrHandleSingle(fi *fileInfo) {
+ if fi.isContentFile() {
+ c.handler.handleSingles(fi)
+ } else {
+ // These do not currently need any further processing.
+ c.handler.handleCopyFile(fi)
+ }
+}
+
+func (c *capturer) createBundleDirs(fileInfos []*fileInfo, bundleType bundleDirType) (*bundleDirs, error) {
+ dirs := newBundleDirs(bundleType, c)
+
+ for _, fi := range fileInfos {
+ if fi.FileInfo().IsDir() {
+ var collector func(fis ...*fileInfo)
+
+ if bundleType == bundleBranch {
+ // All files in the current directory are part of this bundle.
+ // Trying to include sub folders in these bundles are filled with ambiguity.
+ collector = func(fis ...*fileInfo) {
+ for _, fi := range fis {
+ c.copyOrHandleSingle(fi)
+ }
+ }
+ } else {
+ // All nested files and directories are part of this bundle.
+ collector = func(fis ...*fileInfo) {
+ fileInfos = append(fileInfos, fis...)
+ }
+ }
+ err := c.collectFiles(fi.Path(), collector)
+ if err != nil {
+ return nil, err
+ }
+
+ } else if fi.isOwner() {
+ // There can be more than one language, so:
+ // 1. Content files must be attached to its language's bundle.
+ // 2. Other files must be attached to all languages.
+ // 3. Every content file needs a bundle header.
+ dirs.addBundleHeader(fi)
+ }
+ }
+
+ for _, fi := range fileInfos {
+ if fi.FileInfo().IsDir() || fi.isOwner() {
+ continue
+ }
+
+ if fi.isContentFile() {
+ if bundleType != bundleBranch {
+ dirs.addBundleContentFile(fi)
+ }
+ } else {
+ dirs.addBundleFiles(fi)
+ }
+ }
+
+ return dirs, nil
+}
+
+func (c *capturer) collectFiles(dirname string, handleFiles func(fis ...*fileInfo)) error {
+
+ filesInDir, err := c.readDir(dirname)
+ if err != nil {
+ return err
+ }
+
+ for _, fi := range filesInDir {
+ if fi.IsDir() {
+ err := c.collectFiles(fi.Filename(), handleFiles)
+ if err != nil {
+ return err
+ }
+ } else {
+ f, active := c.newFileInfo(fi, bundleNot)
+ if active {
+ handleFiles(f)
+ }
+ }
+ }
+
+ return nil
+}
+
+func (c *capturer) readDir(dirname string) (pathLangFileFis, error) {
+ if c.sourceSpec.IgnoreFile(dirname) {
+ return nil, nil
+ }
+
+ dir, err := c.fs.Open(dirname)
+ if err != nil {
+ return nil, err
+ }
+ defer dir.Close()
+ fis, err := dir.Readdir(-1)
+ if err != nil {
+ return nil, err
+ }
+
+ pfis := make(pathLangFileFis, 0, len(fis))
+
+ for _, fi := range fis {
+ fip := fi.(pathLangFileFi)
+
+ if !c.sourceSpec.IgnoreFile(fip.Filename()) {
+
+ err := c.resolveRealPathIn(fip)
+
+ if err != nil {
+ // It may have been deleted in the meantime.
+ if err == errSkipCyclicDir || os.IsNotExist(err) {
+ continue
+ }
+ return nil, err
+ }
+
+ pfis = append(pfis, fip)
+ }
+ }
+
+ return pfis, nil
+}
+
+func (c *capturer) newFileInfo(fi pathLangFileFi, tp bundleDirType) (*fileInfo, bool) {
+ f := newFileInfo(c.sourceSpec, "", "", fi, tp)
+ return f, !f.disabled
+}
+
+type pathLangFile interface {
+ hugofs.LanguageAnnouncer
+ hugofs.FilePather
+}
+
+type pathLangFileFi interface {
+ os.FileInfo
+ pathLangFile
+}
+
+type pathLangFileFis []pathLangFileFi
+
+type bundleDirs struct {
+ tp bundleDirType
+ // Maps languages to bundles.
+ bundles map[string]*bundleDir
+
+ // Keeps track of language overrides for non-content files, e.g. logo.en.png.
+ langOverrides map[string]bool
+
+ c *capturer
+}
+
+func newBundleDirs(tp bundleDirType, c *capturer) *bundleDirs {
+ return &bundleDirs{tp: tp, bundles: make(map[string]*bundleDir), langOverrides: make(map[string]bool), c: c}
+}
+
+type bundleDir struct {
+ tp bundleDirType
+ fi *fileInfo
+
+ resources map[string]*fileInfo
+}
+
+func (b bundleDir) clone() *bundleDir {
+ b.resources = make(map[string]*fileInfo)
+ fic := *b.fi
+ b.fi = &fic
+ return &b
+}
+
+func newBundleDir(fi *fileInfo, bundleType bundleDirType) *bundleDir {
+ return &bundleDir{fi: fi, tp: bundleType, resources: make(map[string]*fileInfo)}
+}
+
+func (b *bundleDirs) addBundleContentFile(fi *fileInfo) {
+ dir, found := b.bundles[fi.Lang()]
+ if !found {
+ // Every bundled content file needs a bundle header.
+ // If one does not exist in its language, we pick the default
+ // language version, or a random one if that doesn't exist, either.
+ tl := b.c.sourceSpec.DefaultContentLanguage
+ ldir, found := b.bundles[tl]
+ if !found {
+ // Just pick one.
+ for _, v := range b.bundles {
+ ldir = v
+ break
+ }
+ }
+
+ if ldir == nil {
+ panic(fmt.Sprintf("bundle not found for file %q", fi.Filename()))
+ }
+
+ dir = ldir.clone()
+ dir.fi.overriddenLang = fi.Lang()
+ b.bundles[fi.Lang()] = dir
+ }
+
+ dir.resources[fi.Path()] = fi
+}
+
+func (b *bundleDirs) addBundleFiles(fi *fileInfo) {
+ dir := filepath.ToSlash(fi.Dir())
+ p := dir + fi.TranslationBaseName() + "." + fi.Ext()
+ for lang, bdir := range b.bundles {
+ key := path.Join(lang, p)
+
+ // Given mypage.de.md (German translation) and mypage.md we pick the most
+ // specific for that language.
+ if fi.Lang() == lang || !b.langOverrides[key] {
+ bdir.resources[key] = fi
+ }
+ b.langOverrides[key] = true
+ }
+}
+
+func (b *bundleDirs) addBundleHeader(fi *fileInfo) {
+ b.bundles[fi.Lang()] = newBundleDir(fi, b.tp)
+}
+
+func (c *capturer) isSeen(dirname string) bool {
+ c.seenMu.Lock()
+ defer c.seenMu.Unlock()
+ seen := c.seen[dirname]
+ c.seen[dirname] = true
+ if seen {
+ c.logger.INFO.Printf("Content dir %q already processed; skipped to avoid infinite recursion.", dirname)
+ return true
+
+ }
+ return false
+}
+
+func (c *capturer) resolveRealPath(path string) (pathLangFileFi, error) {
+ fileInfo, err := c.lstatIfPossible(path)
+ if err != nil {
+ return nil, err
+ }
+ return fileInfo, c.resolveRealPathIn(fileInfo)
+}
+
+func (c *capturer) resolveRealPathIn(fileInfo pathLangFileFi) error {
+
+ basePath := fileInfo.BaseDir()
+ path := fileInfo.Filename()
+
+ realPath := path
+
+ if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
+ link, err := filepath.EvalSymlinks(path)
+ if err != nil {
+ return _errors.Wrapf(err, "Cannot read symbolic link %q, error was:", path)
+ }
+
+ // This is a file on the outside of any base fs, so we have to use the os package.
+ sfi, err := os.Stat(link)
+ if err != nil {
+ return _errors.Wrapf(err, "Cannot stat %q, error was:", link)
+ }
+
+ // TODO(bep) improve all of this.
+ if a, ok := fileInfo.(*hugofs.LanguageFileInfo); ok {
+ a.FileInfo = sfi
+ }
+
+ realPath = link
+
+ if realPath != path && sfi.IsDir() && c.isSeen(realPath) {
+ // Avoid cyclic symlinks.
+ // Note that this may prevent some uses that isn't cyclic and also
+ // potential useful, but this implementation is both robust and simple:
+ // We stop at the first directory that we have seen before, e.g.
+ // /content/blog will only be processed once.
+ return errSkipCyclicDir
+ }
+
+ if c.contentChanges != nil {
+ // Keep track of symbolic links in watch mode.
+ var from, to string
+ if sfi.IsDir() {
+ from = realPath
+ to = path
+
+ if !strings.HasSuffix(to, helpers.FilePathSeparator) {
+ to = to + helpers.FilePathSeparator
+ }
+ if !strings.HasSuffix(from, helpers.FilePathSeparator) {
+ from = from + helpers.FilePathSeparator
+ }
+
+ if !strings.HasSuffix(basePath, helpers.FilePathSeparator) {
+ basePath = basePath + helpers.FilePathSeparator
+ }
+
+ if strings.HasPrefix(from, basePath) {
+ // With symbolic links inside /content we need to keep
+ // a reference to both. This may be confusing with --navigateToChanged
+ // but the user has chosen this him or herself.
+ c.contentChanges.addSymbolicLinkMapping(from, from)
+ }
+
+ } else {
+ from = realPath
+ to = path
+ }
+
+ c.contentChanges.addSymbolicLinkMapping(from, to)
+ }
+ }
+
+ return nil
+}
+
+func (c *capturer) lstatIfPossible(path string) (pathLangFileFi, error) {
+ fi, err := helpers.LstatIfPossible(c.fs, path)
+ if err != nil {
+ return nil, err
+ }
+ return fi.(pathLangFileFi), nil
+}
diff --git a/hugolib/pagebundler_capture_test.go b/hugolib/pagebundler_capture_test.go
new file mode 100644
index 000000000..b6d9822af
--- /dev/null
+++ b/hugolib/pagebundler_capture_test.go
@@ -0,0 +1,272 @@
+// 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"
+ "os"
+ "path"
+ "path/filepath"
+ "sort"
+
+ "github.com/gohugoio/hugo/common/loggers"
+
+ "runtime"
+ "strings"
+ "sync"
+ "testing"
+
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/source"
+ "github.com/stretchr/testify/require"
+)
+
+type storeFilenames struct {
+ sync.Mutex
+ filenames []string
+ copyNames []string
+ dirKeys []string
+}
+
+func (s *storeFilenames) handleSingles(fis ...*fileInfo) {
+ s.Lock()
+ defer s.Unlock()
+ for _, fi := range fis {
+ s.filenames = append(s.filenames, filepath.ToSlash(fi.Filename()))
+ }
+}
+
+func (s *storeFilenames) handleBundles(d *bundleDirs) {
+ s.Lock()
+ defer s.Unlock()
+ var keys []string
+ for _, b := range d.bundles {
+ res := make([]string, len(b.resources))
+ i := 0
+ for _, r := range b.resources {
+ res[i] = path.Join(r.Lang(), filepath.ToSlash(r.Filename()))
+ i++
+ }
+ sort.Strings(res)
+ keys = append(keys, path.Join("__bundle", b.fi.Lang(), filepath.ToSlash(b.fi.Filename()), "resources", strings.Join(res, "|")))
+ }
+ s.dirKeys = append(s.dirKeys, keys...)
+}
+
+func (s *storeFilenames) handleCopyFile(file pathLangFile) {
+ s.Lock()
+ defer s.Unlock()
+ s.copyNames = append(s.copyNames, filepath.ToSlash(file.Filename()))
+}
+
+func (s *storeFilenames) sortedStr() string {
+ s.Lock()
+ defer s.Unlock()
+ sort.Strings(s.filenames)
+ sort.Strings(s.dirKeys)
+ sort.Strings(s.copyNames)
+ return "\nF:\n" + strings.Join(s.filenames, "\n") + "\nD:\n" + strings.Join(s.dirKeys, "\n") +
+ "\nC:\n" + strings.Join(s.copyNames, "\n") + "\n"
+}
+
+func TestPageBundlerCaptureSymlinks(t *testing.T) {
+ if runtime.GOOS == "windows" && os.Getenv("CI") == "" {
+ t.Skip("Skip TestPageBundlerCaptureSymlinks as os.Symlink needs administrator rights on Windows")
+ }
+
+ assert := require.New(t)
+ ps, clean, workDir := newTestBundleSymbolicSources(t)
+ sourceSpec := source.NewSourceSpec(ps, ps.BaseFs.Content.Fs)
+ defer clean()
+
+ fileStore := &storeFilenames{}
+ logger := loggers.NewErrorLogger()
+ c := newCapturer(logger, sourceSpec, fileStore, nil)
+
+ assert.NoError(c.capture())
+
+ expected := `
+F:
+/base/a/page_s.md
+/base/a/regular.md
+/base/symbolic1/s1.md
+/base/symbolic1/s2.md
+/base/symbolic3/circus/a/page_s.md
+/base/symbolic3/circus/a/regular.md
+D:
+__bundle/en/base/symbolic2/a1/index.md/resources/en/base/symbolic2/a1/logo.png|en/base/symbolic2/a1/page.md
+C:
+/base/symbolic3/s1.png
+/base/symbolic3/s2.png
+`
+
+ got := strings.Replace(fileStore.sortedStr(), filepath.ToSlash(workDir), "", -1)
+ got = strings.Replace(got, "//", "/", -1)
+
+ if expected != got {
+ diff := helpers.DiffStringSlices(strings.Fields(expected), strings.Fields(got))
+ t.Log(got)
+ t.Fatalf("Failed:\n%s", diff)
+ }
+}
+
+func TestPageBundlerCaptureBasic(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+ fs, cfg := newTestBundleSources(t)
+ assert.NoError(loadDefaultSettingsFor(cfg))
+ assert.NoError(loadLanguageSettings(cfg, nil))
+ ps, err := helpers.NewPathSpec(fs, cfg)
+ assert.NoError(err)
+
+ sourceSpec := source.NewSourceSpec(ps, ps.BaseFs.Content.Fs)
+
+ fileStore := &storeFilenames{}
+
+ c := newCapturer(loggers.NewErrorLogger(), sourceSpec, fileStore, nil)
+
+ assert.NoError(c.capture())
+
+ expected := `
+F:
+/work/base/_1.md
+/work/base/a/1.md
+/work/base/a/2.md
+/work/base/assets/pages/mypage.md
+D:
+__bundle/en/work/base/_index.md/resources/en/work/base/_1.png
+__bundle/en/work/base/a/b/index.md/resources/en/work/base/a/b/ab1.md
+__bundle/en/work/base/b/my-bundle/index.md/resources/en/work/base/b/my-bundle/1.md|en/work/base/b/my-bundle/2.md|en/work/base/b/my-bundle/c/logo.png|en/work/base/b/my-bundle/custom-mime.bep|en/work/base/b/my-bundle/sunset1.jpg|en/work/base/b/my-bundle/sunset2.jpg
+__bundle/en/work/base/c/bundle/index.md/resources/en/work/base/c/bundle/logo-은행.png
+__bundle/en/work/base/root/index.md/resources/en/work/base/root/1.md|en/work/base/root/c/logo.png
+C:
+/work/base/assets/pic1.png
+/work/base/assets/pic2.png
+/work/base/images/hugo-logo.png
+`
+
+ got := fileStore.sortedStr()
+
+ if expected != got {
+ diff := helpers.DiffStringSlices(strings.Fields(expected), strings.Fields(got))
+ t.Log(got)
+ t.Fatalf("Failed:\n%s", diff)
+ }
+}
+
+func TestPageBundlerCaptureMultilingual(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+ fs, cfg := newTestBundleSourcesMultilingual(t)
+ assert.NoError(loadDefaultSettingsFor(cfg))
+ assert.NoError(loadLanguageSettings(cfg, nil))
+
+ ps, err := helpers.NewPathSpec(fs, cfg)
+ assert.NoError(err)
+
+ sourceSpec := source.NewSourceSpec(ps, ps.BaseFs.Content.Fs)
+ fileStore := &storeFilenames{}
+ c := newCapturer(loggers.NewErrorLogger(), sourceSpec, fileStore, nil)
+
+ assert.NoError(c.capture())
+
+ expected := `
+F:
+/work/base/1s/mypage.md
+/work/base/1s/mypage.nn.md
+/work/base/bb/_1.md
+/work/base/bb/_1.nn.md
+/work/base/bb/en.md
+/work/base/bc/page.md
+/work/base/bc/page.nn.md
+/work/base/be/_index.md
+/work/base/be/page.md
+/work/base/be/page.nn.md
+D:
+__bundle/en/work/base/bb/_index.md/resources/en/work/base/bb/a.png|en/work/base/bb/b.png|nn/work/base/bb/c.nn.png
+__bundle/en/work/base/bc/_index.md/resources/en/work/base/bc/logo-bc.png
+__bundle/en/work/base/bd/index.md/resources/en/work/base/bd/page.md
+__bundle/en/work/base/bf/my-bf-bundle/index.md/resources/en/work/base/bf/my-bf-bundle/page.md
+__bundle/en/work/base/lb/index.md/resources/en/work/base/lb/1.md|en/work/base/lb/2.md|en/work/base/lb/c/d/deep.png|en/work/base/lb/c/logo.png|en/work/base/lb/c/one.png|en/work/base/lb/c/page.md
+__bundle/nn/work/base/bb/_index.nn.md/resources/en/work/base/bb/a.png|nn/work/base/bb/b.nn.png|nn/work/base/bb/c.nn.png
+__bundle/nn/work/base/bd/index.md/resources/nn/work/base/bd/page.nn.md
+__bundle/nn/work/base/bf/my-bf-bundle/index.nn.md/resources
+__bundle/nn/work/base/lb/index.nn.md/resources/en/work/base/lb/c/d/deep.png|en/work/base/lb/c/one.png|nn/work/base/lb/2.nn.md|nn/work/base/lb/c/logo.nn.png
+C:
+/work/base/1s/mylogo.png
+/work/base/bb/b/d.nn.png
+`
+
+ got := fileStore.sortedStr()
+
+ if expected != got {
+ diff := helpers.DiffStringSlices(strings.Fields(expected), strings.Fields(got))
+ t.Log(got)
+ t.Fatalf("Failed:\n%s", strings.Join(diff, "\n"))
+ }
+
+}
+
+type noOpFileStore int
+
+func (noOpFileStore) handleSingles(fis ...*fileInfo) {}
+func (noOpFileStore) handleBundles(b *bundleDirs) {}
+func (noOpFileStore) handleCopyFile(file pathLangFile) {}
+
+func BenchmarkPageBundlerCapture(b *testing.B) {
+ capturers := make([]*capturer, b.N)
+
+ for i := 0; i < b.N; i++ {
+ cfg, fs := newTestCfg()
+ ps, _ := helpers.NewPathSpec(fs, cfg)
+ sourceSpec := source.NewSourceSpec(ps, fs.Source)
+
+ base := fmt.Sprintf("base%d", i)
+ for j := 1; j <= 5; j++ {
+ js := fmt.Sprintf("j%d", j)
+ writeSource(b, fs, filepath.Join(base, js, "index.md"), "content")
+ writeSource(b, fs, filepath.Join(base, js, "logo1.png"), "content")
+ writeSource(b, fs, filepath.Join(base, js, "sub", "logo2.png"), "content")
+ writeSource(b, fs, filepath.Join(base, js, "section", "_index.md"), "content")
+ writeSource(b, fs, filepath.Join(base, js, "section", "logo.png"), "content")
+ writeSource(b, fs, filepath.Join(base, js, "section", "sub", "logo.png"), "content")
+
+ for k := 1; k <= 5; k++ {
+ ks := fmt.Sprintf("k%d", k)
+ writeSource(b, fs, filepath.Join(base, js, ks, "logo1.png"), "content")
+ writeSource(b, fs, filepath.Join(base, js, "section", ks, "logo.png"), "content")
+ }
+ }
+
+ for i := 1; i <= 5; i++ {
+ writeSource(b, fs, filepath.Join(base, "assetsonly", fmt.Sprintf("image%d.png", i)), "image")
+ }
+
+ for i := 1; i <= 5; i++ {
+ writeSource(b, fs, filepath.Join(base, "contentonly", fmt.Sprintf("c%d.md", i)), "content")
+ }
+
+ capturers[i] = newCapturer(loggers.NewErrorLogger(), sourceSpec, new(noOpFileStore), nil, base)
+ }
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ err := capturers[i].capture()
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
diff --git a/hugolib/pagebundler_handlers.go b/hugolib/pagebundler_handlers.go
new file mode 100644
index 000000000..e745a04f2
--- /dev/null
+++ b/hugolib/pagebundler_handlers.go
@@ -0,0 +1,305 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/filepath"
+
+ "github.com/gohugoio/hugo/common/hugio"
+
+ "strings"
+
+ "github.com/gohugoio/hugo/resources"
+ "github.com/gohugoio/hugo/resources/resource"
+)
+
+var (
+ // This should be the only list of valid extensions for content files.
+ contentFileExtensions = []string{
+ "html", "htm",
+ "mdown", "markdown", "md",
+ "asciidoc", "adoc", "ad",
+ "rest", "rst",
+ "mmark",
+ "org",
+ "pandoc", "pdc"}
+
+ contentFileExtensionsSet map[string]bool
+)
+
+func init() {
+ contentFileExtensionsSet = make(map[string]bool)
+ for _, ext := range contentFileExtensions {
+ contentFileExtensionsSet[ext] = true
+ }
+}
+
+func newHandlerChain(s *Site) contentHandler {
+ c := &contentHandlers{s: s}
+
+ contentFlow := c.parsePage(
+ c.handlePageContent(),
+ )
+
+ c.rootHandler = c.processFirstMatch(
+ contentFlow,
+
+ // Creates a file resource (image, CSS etc.) if there is a parent
+ // page set on the current context.
+ c.createResource(),
+
+ // Everything that isn't handled above, will just be copied
+ // to destination.
+ c.copyFile(),
+ )
+
+ return c.rootHandler
+
+}
+
+type contentHandlers struct {
+ s *Site
+ rootHandler contentHandler
+}
+
+func (c *contentHandlers) processFirstMatch(handlers ...contentHandler) func(ctx *handlerContext) handlerResult {
+ return func(ctx *handlerContext) handlerResult {
+ for _, h := range handlers {
+ res := h(ctx)
+ if res.handled || res.err != nil {
+ return res
+ }
+ }
+ return handlerResult{err: errors.New("no matching handler found")}
+ }
+}
+
+type handlerContext struct {
+ // These are the pages stored in Site.
+ pages chan<- *pageState
+
+ doNotAddToSiteCollections bool
+
+ currentPage *pageState
+ parentPage *pageState
+
+ bundle *bundleDir
+
+ source *fileInfo
+
+ // Relative path to the target.
+ target string
+}
+
+func (c *handlerContext) ext() string {
+ if c.currentPage != nil {
+ return c.currentPage.contentMarkupType()
+ }
+
+ if c.bundle != nil {
+ return c.bundle.fi.Ext()
+ } else {
+ return c.source.Ext()
+ }
+}
+
+func (c *handlerContext) targetPath() string {
+ if c.target != "" {
+ return c.target
+ }
+
+ return c.source.Filename()
+}
+
+func (c *handlerContext) file() *fileInfo {
+ if c.bundle != nil {
+ return c.bundle.fi
+ }
+
+ return c.source
+}
+
+// Create a copy with the current context as its parent.
+func (c handlerContext) childCtx(fi *fileInfo) *handlerContext {
+ if c.currentPage == nil {
+ panic("Need a Page to create a child context")
+ }
+
+ c.target = strings.TrimPrefix(fi.Path(), c.bundle.fi.Dir())
+ c.source = fi
+
+ c.doNotAddToSiteCollections = c.bundle != nil && c.bundle.tp != bundleBranch
+
+ c.bundle = nil
+
+ c.parentPage = c.currentPage
+ c.currentPage = nil
+
+ return &c
+}
+
+func (c *handlerContext) supports(exts ...string) bool {
+ ext := c.ext()
+ for _, s := range exts {
+ if s == ext {
+ return true
+ }
+ }
+
+ return false
+}
+
+func (c *handlerContext) isContentFile() bool {
+ return contentFileExtensionsSet[c.ext()]
+}
+
+type (
+ handlerResult struct {
+ err error
+ handled bool
+ result interface{}
+ }
+
+ contentHandler func(ctx *handlerContext) handlerResult
+)
+
+var (
+ notHandled handlerResult
+)
+
+func (c *contentHandlers) parsePage(h contentHandler) contentHandler {
+ return func(ctx *handlerContext) handlerResult {
+ if !ctx.isContentFile() {
+ return notHandled
+ }
+
+ result := handlerResult{handled: true}
+ fi := ctx.file()
+
+ content := func() (hugio.ReadSeekCloser, error) {
+ f, err := fi.Open()
+ if err != nil {
+ return nil, fmt.Errorf("failed to open content file %q: %s", fi.Filename(), err)
+ }
+ return f, nil
+ }
+
+ ps, err := newPageWithContent(fi, c.s, ctx.parentPage != nil, content)
+ if err != nil {
+ return handlerResult{err: err}
+ }
+
+ if !c.s.shouldBuild(ps) {
+ if !ctx.doNotAddToSiteCollections {
+ ctx.pages <- ps
+ }
+ return result
+ }
+
+ ctx.currentPage = ps
+
+ if ctx.bundle != nil {
+ // Add the bundled files
+ for _, fi := range ctx.bundle.resources {
+ childCtx := ctx.childCtx(fi)
+ res := c.rootHandler(childCtx)
+ if res.err != nil {
+ return res
+ }
+ if res.result != nil {
+ switch resv := res.result.(type) {
+ case *pageState:
+ resv.m.resourcePath = filepath.ToSlash(childCtx.target)
+ resv.parent = ps
+ ps.addResources(resv)
+ case resource.Resource:
+ ps.addResources(resv)
+
+ default:
+ panic("Unknown type")
+ }
+ }
+ }
+ }
+
+ return h(ctx)
+ }
+}
+
+func (c *contentHandlers) handlePageContent() contentHandler {
+ return func(ctx *handlerContext) handlerResult {
+ p := ctx.currentPage
+
+ if !ctx.doNotAddToSiteCollections {
+ ctx.pages <- p
+ }
+
+ return handlerResult{handled: true, result: p}
+ }
+}
+
+func (c *contentHandlers) createResource() contentHandler {
+ return func(ctx *handlerContext) handlerResult {
+ if ctx.parentPage == nil {
+ return notHandled
+ }
+
+ // TODO(bep) consolidate with multihost logic + clean up
+ outputFormats := ctx.parentPage.m.outputFormats()
+ seen := make(map[string]bool)
+ var targetBasePaths []string
+ // Make sure bundled resources are published to all of the ouptput formats'
+ // sub paths.
+ for _, f := range outputFormats {
+ p := f.Path
+ if seen[p] {
+ continue
+ }
+ seen[p] = true
+ targetBasePaths = append(targetBasePaths, p)
+
+ }
+
+ resource, err := c.s.ResourceSpec.New(
+ resources.ResourceSourceDescriptor{
+ TargetPaths: ctx.parentPage.getTargetPaths,
+ SourceFile: ctx.source,
+ RelTargetFilename: ctx.target,
+ TargetBasePaths: targetBasePaths,
+ })
+
+ return handlerResult{err: err, handled: true, result: resource}
+ }
+}
+
+func (c *contentHandlers) copyFile() contentHandler {
+ return func(ctx *handlerContext) handlerResult {
+ f, err := c.s.BaseFs.Content.Fs.Open(ctx.source.Filename())
+ if err != nil {
+ err := fmt.Errorf("failed to open file in copyFile: %s", err)
+ return handlerResult{err: err}
+ }
+
+ target := ctx.targetPath()
+
+ defer f.Close()
+ if err := c.s.publish(&c.s.PathSpec.ProcessingStats.Files, target, f); err != nil {
+ return handlerResult{err: err}
+ }
+
+ return handlerResult{handled: true}
+ }
+}
diff --git a/hugolib/pagebundler_test.go b/hugolib/pagebundler_test.go
new file mode 100644
index 000000000..64c1529d8
--- /dev/null
+++ b/hugolib/pagebundler_test.go
@@ -0,0 +1,944 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "os"
+ "path"
+ "runtime"
+ "strings"
+ "testing"
+
+ "github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/resources/page"
+
+ "github.com/gohugoio/hugo/helpers"
+
+ "io"
+
+ "github.com/spf13/afero"
+
+ "github.com/gohugoio/hugo/media"
+
+ "path/filepath"
+
+ "fmt"
+
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/spf13/viper"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestPageBundlerSiteRegular(t *testing.T) {
+ t.Parallel()
+
+ baseBaseURL := "https://example.com"
+
+ for _, baseURLPath := range []string{"", "/hugo"} {
+ for _, canonify := range []bool{false, true} {
+ for _, ugly := range []bool{false, true} {
+ baseURLPathId := baseURLPath
+ if baseURLPathId == "" {
+ baseURLPathId = "NONE"
+ }
+ t.Run(fmt.Sprintf("ugly=%t,canonify=%t,path=%s", ugly, canonify, baseURLPathId),
+ func(t *testing.T) {
+ baseURL := baseBaseURL + baseURLPath
+ relURLBase := baseURLPath
+ if canonify {
+ relURLBase = ""
+ }
+ assert := require.New(t)
+ fs, cfg := newTestBundleSources(t)
+ cfg.Set("baseURL", baseURL)
+ cfg.Set("canonifyURLs", canonify)
+ assert.NoError(loadDefaultSettingsFor(cfg))
+ assert.NoError(loadLanguageSettings(cfg, nil))
+
+ cfg.Set("permalinks", map[string]string{
+ "a": ":sections/:filename",
+ "b": ":year/:slug/",
+ "c": ":sections/:slug",
+ "": ":filename/",
+ })
+
+ cfg.Set("outputFormats", map[string]interface{}{
+ "CUSTOMO": map[string]interface{}{
+ "mediaType": media.HTMLType,
+ "baseName": "cindex",
+ "path": "cpath",
+ "permalinkable": true,
+ },
+ })
+
+ cfg.Set("outputs", map[string]interface{}{
+ "home": []string{"HTML", "CUSTOMO"},
+ "page": []string{"HTML", "CUSTOMO"},
+ "section": []string{"HTML", "CUSTOMO"},
+ })
+
+ cfg.Set("uglyURLs", ugly)
+
+ s := buildSingleSite(t, deps.DepsCfg{Logger: loggers.NewErrorLogger(), Fs: fs, Cfg: cfg}, BuildCfg{})
+
+ th := testHelper{s.Cfg, s.Fs, t}
+
+ assert.Len(s.RegularPages(), 8)
+
+ singlePage := s.getPage(page.KindPage, "a/1.md")
+ assert.Equal("", singlePage.BundleType())
+
+ assert.NotNil(singlePage)
+ assert.Equal(singlePage, s.getPage("page", "a/1"))
+ assert.Equal(singlePage, s.getPage("page", "1"))
+
+ assert.Contains(content(singlePage), "TheContent")
+
+ relFilename := func(basePath, outBase string) (string, string) {
+ rel := basePath
+ if ugly {
+ rel = strings.TrimSuffix(basePath, "/") + ".html"
+ }
+
+ var filename string
+ if !ugly {
+ filename = path.Join(basePath, outBase)
+ } else {
+ filename = rel
+ }
+
+ rel = fmt.Sprintf("%s%s", relURLBase, rel)
+
+ return rel, filename
+ }
+
+ // Check both output formats
+ rel, filename := relFilename("/a/1/", "index.html")
+ th.assertFileContent(filepath.Join("/work/public", filename),
+ "TheContent",
+ "Single RelPermalink: "+rel,
+ )
+
+ rel, filename = relFilename("/cpath/a/1/", "cindex.html")
+
+ th.assertFileContent(filepath.Join("/work/public", filename),
+ "TheContent",
+ "Single RelPermalink: "+rel,
+ )
+
+ th.assertFileContent(filepath.FromSlash("/work/public/images/hugo-logo.png"), "content")
+
+ // This should be just copied to destination.
+ th.assertFileContent(filepath.FromSlash("/work/public/assets/pic1.png"), "content")
+
+ leafBundle1 := s.getPage(page.KindPage, "b/my-bundle/index.md")
+ assert.NotNil(leafBundle1)
+ assert.Equal("leaf", leafBundle1.BundleType())
+ assert.Equal("b", leafBundle1.Section())
+ sectionB := s.getPage(page.KindSection, "b")
+ assert.NotNil(sectionB)
+ home, _ := s.Info.Home()
+ assert.Equal("branch", home.BundleType())
+
+ // This is a root bundle and should live in the "home section"
+ // See https://github.com/gohugoio/hugo/issues/4332
+ rootBundle := s.getPage(page.KindPage, "root")
+ assert.NotNil(rootBundle)
+ assert.True(rootBundle.Parent().IsHome())
+ if !ugly {
+ th.assertFileContent(filepath.FromSlash("/work/public/root/index.html"), "Single RelPermalink: "+relURLBase+"/root/")
+ th.assertFileContent(filepath.FromSlash("/work/public/cpath/root/cindex.html"), "Single RelPermalink: "+relURLBase+"/cpath/root/")
+ }
+
+ leafBundle2 := s.getPage(page.KindPage, "a/b/index.md")
+ assert.NotNil(leafBundle2)
+ unicodeBundle := s.getPage(page.KindPage, "c/bundle/index.md")
+ assert.NotNil(unicodeBundle)
+
+ pageResources := leafBundle1.Resources().ByType(pageResourceType)
+ assert.Len(pageResources, 2)
+ firstPage := pageResources[0].(page.Page)
+ secondPage := pageResources[1].(page.Page)
+ assert.Equal(filepath.FromSlash("/work/base/b/my-bundle/1.md"), firstPage.File().Filename(), secondPage.File().Filename())
+ assert.Contains(content(firstPage), "TheContent")
+ assert.Equal(6, len(leafBundle1.Resources()))
+
+ // Verify shortcode in bundled page
+ assert.Contains(content(secondPage), filepath.FromSlash("MyShort in b/my-bundle/2.md"))
+
+ // https://github.com/gohugoio/hugo/issues/4582
+ assert.Equal(leafBundle1, firstPage.Parent())
+ assert.Equal(leafBundle1, secondPage.Parent())
+
+ assert.Equal(firstPage, pageResources.GetMatch("1*"))
+ assert.Equal(secondPage, pageResources.GetMatch("2*"))
+ assert.Nil(pageResources.GetMatch("doesnotexist*"))
+
+ imageResources := leafBundle1.Resources().ByType("image")
+ assert.Equal(3, len(imageResources))
+
+ assert.NotNil(leafBundle1.OutputFormats().Get("CUSTOMO"))
+
+ relPermalinker := func(s string) string {
+ return fmt.Sprintf(s, relURLBase)
+ }
+
+ permalinker := func(s string) string {
+ return fmt.Sprintf(s, baseURL)
+ }
+
+ if ugly {
+ th.assertFileContent("/work/public/2017/pageslug.html",
+ relPermalinker("Single RelPermalink: %s/2017/pageslug.html"),
+ permalinker("Single Permalink: %s/2017/pageslug.html"),
+ relPermalinker("Sunset RelPermalink: %s/2017/pageslug/sunset1.jpg"),
+ permalinker("Sunset Permalink: %s/2017/pageslug/sunset1.jpg"))
+ } else {
+ th.assertFileContent("/work/public/2017/pageslug/index.html",
+ relPermalinker("Sunset RelPermalink: %s/2017/pageslug/sunset1.jpg"),
+ permalinker("Sunset Permalink: %s/2017/pageslug/sunset1.jpg"))
+
+ th.assertFileContent("/work/public/cpath/2017/pageslug/cindex.html",
+ relPermalinker("Single RelPermalink: %s/cpath/2017/pageslug/"),
+ relPermalinker("Short Sunset RelPermalink: %s/cpath/2017/pageslug/sunset2.jpg"),
+ relPermalinker("Sunset RelPermalink: %s/cpath/2017/pageslug/sunset1.jpg"),
+ permalinker("Sunset Permalink: %s/cpath/2017/pageslug/sunset1.jpg"),
+ )
+ }
+
+ th.assertFileContent(filepath.FromSlash("/work/public/2017/pageslug/c/logo.png"), "content")
+ th.assertFileContent(filepath.FromSlash("/work/public/cpath/2017/pageslug/c/logo.png"), "content")
+ th.assertFileNotExist("/work/public/cpath/cpath/2017/pageslug/c/logo.png")
+
+ // Custom media type defined in site config.
+ assert.Len(leafBundle1.Resources().ByType("bepsays"), 1)
+
+ if ugly {
+ th.assertFileContent(filepath.FromSlash("/work/public/2017/pageslug.html"),
+ "TheContent",
+ relPermalinker("Sunset RelPermalink: %s/2017/pageslug/sunset1.jpg"),
+ permalinker("Sunset Permalink: %s/2017/pageslug/sunset1.jpg"),
+ "Thumb Width: 123",
+ "Thumb Name: my-sunset-1",
+ relPermalinker("Short Sunset RelPermalink: %s/2017/pageslug/sunset2.jpg"),
+ "Short Thumb Width: 56",
+ "1: Image Title: Sunset Galore 1",
+ "1: Image Params: map[myparam:My Sunny Param]",
+ relPermalinker("1: Image RelPermalink: %s/2017/pageslug/sunset1.jpg"),
+ "2: Image Title: Sunset Galore 2",
+ "2: Image Params: map[myparam:My Sunny Param]",
+ "1: Image myParam: Lower: My Sunny Param Caps: My Sunny Param",
+ "0: Page Title: Bundle Galore",
+ )
+
+ // https://github.com/gohugoio/hugo/issues/5882
+ th.assertFileContent(
+ filepath.FromSlash("/work/public/2017/pageslug.html"), "0: Page RelPermalink: |")
+
+ th.assertFileContent(filepath.FromSlash("/work/public/cpath/2017/pageslug.html"), "TheContent")
+
+ // 은행
+ th.assertFileContent(filepath.FromSlash("/work/public/c/은행/logo-은행.png"), "은행 PNG")
+
+ } else {
+ th.assertFileContent(filepath.FromSlash("/work/public/2017/pageslug/index.html"), "TheContent")
+ th.assertFileContent(filepath.FromSlash("/work/public/cpath/2017/pageslug/cindex.html"), "TheContent")
+ th.assertFileContent(filepath.FromSlash("/work/public/2017/pageslug/index.html"), "Single Title")
+ th.assertFileContent(filepath.FromSlash("/work/public/root/index.html"), "Single Title")
+
+ }
+
+ })
+ }
+ }
+ }
+
+}
+
+func TestPageBundlerSiteMultilingual(t *testing.T) {
+ t.Parallel()
+
+ for _, ugly := range []bool{false, true} {
+ t.Run(fmt.Sprintf("ugly=%t", ugly),
+ func(t *testing.T) {
+
+ assert := require.New(t)
+ fs, cfg := newTestBundleSourcesMultilingual(t)
+ cfg.Set("uglyURLs", ugly)
+
+ assert.NoError(loadDefaultSettingsFor(cfg))
+ assert.NoError(loadLanguageSettings(cfg, nil))
+ sites, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg})
+ assert.NoError(err)
+ assert.Equal(2, len(sites.Sites))
+
+ assert.NoError(sites.Build(BuildCfg{}))
+
+ s := sites.Sites[0]
+
+ assert.Equal(8, len(s.RegularPages()))
+ assert.Equal(16, len(s.Pages()))
+ assert.Equal(31, len(s.AllPages()))
+
+ bundleWithSubPath := s.getPage(page.KindPage, "lb/index")
+ assert.NotNil(bundleWithSubPath)
+
+ // See https://github.com/gohugoio/hugo/issues/4312
+ // Before that issue:
+ // A bundle in a/b/index.en.md
+ // a/b/index.en.md => OK
+ // a/b/index => OK
+ // index.en.md => ambigous, but OK.
+ // With bundles, the file name has little meaning, the folder it lives in does. So this should also work:
+ // a/b
+ // and probably also just b (aka "my-bundle")
+ // These may also be translated, so we also need to test that.
+ // "bf", "my-bf-bundle", "index.md + nn
+ bfBundle := s.getPage(page.KindPage, "bf/my-bf-bundle/index")
+ assert.NotNil(bfBundle)
+ assert.Equal("en", bfBundle.Language().Lang)
+ assert.Equal(bfBundle, s.getPage(page.KindPage, "bf/my-bf-bundle/index.md"))
+ assert.Equal(bfBundle, s.getPage(page.KindPage, "bf/my-bf-bundle"))
+ assert.Equal(bfBundle, s.getPage(page.KindPage, "my-bf-bundle"))
+
+ nnSite := sites.Sites[1]
+ assert.Equal(7, len(nnSite.RegularPages()))
+
+ bfBundleNN := nnSite.getPage(page.KindPage, "bf/my-bf-bundle/index")
+ assert.NotNil(bfBundleNN)
+ assert.Equal("nn", bfBundleNN.Language().Lang)
+ assert.Equal(bfBundleNN, nnSite.getPage(page.KindPage, "bf/my-bf-bundle/index.nn.md"))
+ assert.Equal(bfBundleNN, nnSite.getPage(page.KindPage, "bf/my-bf-bundle"))
+ assert.Equal(bfBundleNN, nnSite.getPage(page.KindPage, "my-bf-bundle"))
+
+ // See https://github.com/gohugoio/hugo/issues/4295
+ // Every resource should have its Name prefixed with its base folder.
+ cBundleResources := bundleWithSubPath.Resources().Match("c/**")
+ assert.Equal(4, len(cBundleResources))
+ bundlePage := bundleWithSubPath.Resources().GetMatch("c/page*")
+ assert.NotNil(bundlePage)
+ assert.IsType(&pageState{}, bundlePage)
+
+ })
+ }
+}
+
+func TestMultilingualDisableDefaultLanguage(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+ _, cfg := newTestBundleSourcesMultilingual(t)
+
+ cfg.Set("disableLanguages", []string{"en"})
+
+ err := loadDefaultSettingsFor(cfg)
+ assert.NoError(err)
+ err = loadLanguageSettings(cfg, nil)
+ assert.Error(err)
+ assert.Contains(err.Error(), "cannot disable default language")
+}
+
+func TestMultilingualDisableLanguage(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+ fs, cfg := newTestBundleSourcesMultilingual(t)
+ cfg.Set("disableLanguages", []string{"nn"})
+
+ assert.NoError(loadDefaultSettingsFor(cfg))
+ assert.NoError(loadLanguageSettings(cfg, nil))
+
+ sites, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg})
+ assert.NoError(err)
+ assert.Equal(1, len(sites.Sites))
+
+ assert.NoError(sites.Build(BuildCfg{}))
+
+ s := sites.Sites[0]
+
+ assert.Equal(8, len(s.RegularPages()))
+ assert.Equal(16, len(s.Pages()))
+ // No nn pages
+ assert.Equal(16, len(s.AllPages()))
+ for _, p := range s.rawAllPages {
+ assert.True(p.Language().Lang != "nn")
+ }
+ for _, p := range s.AllPages() {
+ assert.True(p.Language().Lang != "nn")
+ }
+
+}
+
+func TestPageBundlerSiteWitSymbolicLinksInContent(t *testing.T) {
+ if runtime.GOOS == "windows" && os.Getenv("CI") == "" {
+ t.Skip("Skip TestPageBundlerSiteWitSymbolicLinksInContent as os.Symlink needs administrator rights on Windows")
+ }
+
+ assert := require.New(t)
+ ps, clean, workDir := newTestBundleSymbolicSources(t)
+ defer clean()
+
+ cfg := ps.Cfg
+ fs := ps.Fs
+
+ s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg, Logger: loggers.NewErrorLogger()}, BuildCfg{})
+
+ th := testHelper{s.Cfg, s.Fs, t}
+
+ assert.Equal(7, len(s.RegularPages()))
+ a1Bundle := s.getPage(page.KindPage, "symbolic2/a1/index.md")
+ assert.NotNil(a1Bundle)
+ assert.Equal(2, len(a1Bundle.Resources()))
+ assert.Equal(1, len(a1Bundle.Resources().ByType(pageResourceType)))
+
+ th.assertFileContent(filepath.FromSlash(workDir+"/public/a/page/index.html"), "TheContent")
+ th.assertFileContent(filepath.FromSlash(workDir+"/public/symbolic1/s1/index.html"), "TheContent")
+ th.assertFileContent(filepath.FromSlash(workDir+"/public/symbolic2/a1/index.html"), "TheContent")
+
+}
+
+func TestPageBundlerHeadless(t *testing.T) {
+ t.Parallel()
+
+ cfg, fs := newTestCfg()
+ assert := require.New(t)
+
+ workDir := "/work"
+ cfg.Set("workingDir", workDir)
+ cfg.Set("contentDir", "base")
+ cfg.Set("baseURL", "https://example.com")
+
+ pageContent := `---
+title: "Bundle Galore"
+slug: s1
+date: 2017-01-23
+---
+
+TheContent.
+
+{{< myShort >}}
+`
+
+ writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "single.html"), "single {{ .Content }}")
+ writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "list.html"), "list")
+ writeSource(t, fs, filepath.Join(workDir, "layouts", "shortcodes", "myShort.html"), "SHORTCODE")
+
+ writeSource(t, fs, filepath.Join(workDir, "base", "a", "index.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "a", "l1.png"), "PNG image")
+ writeSource(t, fs, filepath.Join(workDir, "base", "a", "l2.png"), "PNG image")
+
+ writeSource(t, fs, filepath.Join(workDir, "base", "b", "index.md"), `---
+title: "Headless Bundle in Topless Bar"
+slug: s2
+headless: true
+date: 2017-01-23
+---
+
+TheContent.
+HEADLESS {{< myShort >}}
+`)
+ writeSource(t, fs, filepath.Join(workDir, "base", "b", "l1.png"), "PNG image")
+ writeSource(t, fs, filepath.Join(workDir, "base", "b", "l2.png"), "PNG image")
+ writeSource(t, fs, filepath.Join(workDir, "base", "b", "p1.md"), pageContent)
+
+ s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{})
+
+ assert.Equal(1, len(s.RegularPages()))
+ assert.Equal(1, len(s.headlessPages))
+
+ regular := s.getPage(page.KindPage, "a/index")
+ assert.Equal("/a/s1/", regular.RelPermalink())
+
+ headless := s.getPage(page.KindPage, "b/index")
+ assert.NotNil(headless)
+ assert.Equal("Headless Bundle in Topless Bar", headless.Title())
+ assert.Equal("", headless.RelPermalink())
+ assert.Equal("", headless.Permalink())
+ assert.Contains(content(headless), "HEADLESS SHORTCODE")
+
+ headlessResources := headless.Resources()
+ assert.Equal(3, len(headlessResources))
+ assert.Equal(2, len(headlessResources.Match("l*")))
+ pageResource := headlessResources.GetMatch("p*")
+ assert.NotNil(pageResource)
+ assert.IsType(&pageState{}, pageResource)
+ p := pageResource.(page.Page)
+ assert.Contains(content(p), "SHORTCODE")
+ assert.Equal("p1.md", p.Name())
+
+ th := testHelper{s.Cfg, s.Fs, t}
+
+ th.assertFileContent(filepath.FromSlash(workDir+"/public/a/s1/index.html"), "TheContent")
+ th.assertFileContent(filepath.FromSlash(workDir+"/public/a/s1/l1.png"), "PNG")
+
+ th.assertFileNotExist(workDir + "/public/b/s2/index.html")
+ // But the bundled resources needs to be published
+ th.assertFileContent(filepath.FromSlash(workDir+"/public/b/s2/l1.png"), "PNG")
+
+}
+
+func TestMultiSiteBundles(t *testing.T) {
+ assert := require.New(t)
+ b := newTestSitesBuilder(t)
+ b.WithConfigFile("toml", `
+
+baseURL = "http://example.com/"
+
+defaultContentLanguage = "en"
+
+[languages]
+[languages.en]
+weight = 10
+contentDir = "content/en"
+[languages.nn]
+weight = 20
+contentDir = "content/nn"
+
+
+`)
+
+ b.WithContent("en/mybundle/index.md", `
+---
+headless: true
+---
+
+`)
+
+ b.WithContent("nn/mybundle/index.md", `
+---
+headless: true
+---
+
+`)
+
+ b.WithContent("en/mybundle/data.yaml", `data en`)
+ b.WithContent("en/mybundle/forms.yaml", `forms en`)
+ b.WithContent("nn/mybundle/data.yaml", `data nn`)
+
+ b.WithContent("en/_index.md", `
+---
+Title: Home
+---
+
+Home content.
+
+`)
+
+ b.WithContent("en/section-not-bundle/_index.md", `
+---
+Title: Section Page
+---
+
+Section content.
+
+`)
+
+ b.WithContent("en/section-not-bundle/single.md", `
+---
+Title: Section Single
+Date: 2018-02-01
+---
+
+Single content.
+
+`)
+
+ b.Build(BuildCfg{})
+
+ b.AssertFileContent("public/nn/mybundle/data.yaml", "data nn")
+ b.AssertFileContent("public/nn/mybundle/forms.yaml", "forms en")
+ b.AssertFileContent("public/mybundle/data.yaml", "data en")
+ b.AssertFileContent("public/mybundle/forms.yaml", "forms en")
+
+ assert.False(b.CheckExists("public/nn/nn/mybundle/data.yaml"))
+ assert.False(b.CheckExists("public/en/mybundle/data.yaml"))
+
+ homeEn := b.H.Sites[0].home
+ assert.NotNil(homeEn)
+ assert.Equal(2018, homeEn.Date().Year())
+
+ b.AssertFileContent("public/section-not-bundle/index.html", "Section Page", "Content: <p>Section content.</p>")
+ b.AssertFileContent("public/section-not-bundle/single/index.html", "Section Single", "|<p>Single content.</p>")
+
+}
+
+func newTestBundleSources(t *testing.T) (*hugofs.Fs, *viper.Viper) {
+ cfg, fs := newTestCfg()
+ assert := require.New(t)
+
+ workDir := "/work"
+ cfg.Set("workingDir", workDir)
+ cfg.Set("contentDir", "base")
+ cfg.Set("baseURL", "https://example.com")
+ cfg.Set("mediaTypes", map[string]interface{}{
+ "text/bepsays": map[string]interface{}{
+ "suffixes": []string{"bep"},
+ },
+ })
+
+ pageContent := `---
+title: "Bundle Galore"
+slug: pageslug
+date: 2017-10-09
+---
+
+TheContent.
+`
+
+ pageContentShortcode := `---
+title: "Bundle Galore"
+slug: pageslug
+date: 2017-10-09
+---
+
+TheContent.
+
+{{< myShort >}}
+`
+
+ pageWithImageShortcodeAndResourceMetadataContent := `---
+title: "Bundle Galore"
+slug: pageslug
+date: 2017-10-09
+resources:
+- src: "*.jpg"
+ name: "my-sunset-:counter"
+ title: "Sunset Galore :counter"
+ params:
+ myParam: "My Sunny Param"
+---
+
+TheContent.
+
+{{< myShort >}}
+`
+
+ pageContentNoSlug := `---
+title: "Bundle Galore #2"
+date: 2017-10-09
+---
+
+TheContent.
+`
+
+ singleLayout := `
+Single Title: {{ .Title }}
+Single RelPermalink: {{ .RelPermalink }}
+Single Permalink: {{ .Permalink }}
+Content: {{ .Content }}
+{{ $sunset := .Resources.GetMatch "my-sunset-1*" }}
+{{ with $sunset }}
+Sunset RelPermalink: {{ .RelPermalink }}
+Sunset Permalink: {{ .Permalink }}
+{{ $thumb := .Fill "123x123" }}
+Thumb Width: {{ $thumb.Width }}
+Thumb Name: {{ $thumb.Name }}
+Thumb Title: {{ $thumb.Title }}
+Thumb RelPermalink: {{ $thumb.RelPermalink }}
+{{ end }}
+{{ $types := slice "image" "page" }}
+{{ range $types }}
+{{ $typeTitle := . | title }}
+{{ range $i, $e := $.Resources.ByType . }}
+{{ $i }}: {{ $typeTitle }} Title: {{ .Title }}
+{{ $i }}: {{ $typeTitle }} Name: {{ .Name }}
+{{ $i }}: {{ $typeTitle }} RelPermalink: {{ .RelPermalink }}|
+{{ $i }}: {{ $typeTitle }} Params: {{ printf "%v" .Params }}
+{{ $i }}: {{ $typeTitle }} myParam: Lower: {{ .Params.myparam }} Caps: {{ .Params.MYPARAM }}
+{{ end }}
+{{ end }}
+`
+
+ myShort := `
+MyShort in {{ .Page.File.Path }}:
+{{ $sunset := .Page.Resources.GetMatch "my-sunset-2*" }}
+{{ with $sunset }}
+Short Sunset RelPermalink: {{ .RelPermalink }}
+{{ $thumb := .Fill "56x56" }}
+Short Thumb Width: {{ $thumb.Width }}
+{{ end }}
+`
+
+ listLayout := `{{ .Title }}|{{ .Content }}`
+
+ writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "single.html"), singleLayout)
+ writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "list.html"), listLayout)
+ writeSource(t, fs, filepath.Join(workDir, "layouts", "shortcodes", "myShort.html"), myShort)
+
+ writeSource(t, fs, filepath.Join(workDir, "base", "_index.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "_1.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "_1.png"), pageContent)
+
+ writeSource(t, fs, filepath.Join(workDir, "base", "images", "hugo-logo.png"), "content")
+ writeSource(t, fs, filepath.Join(workDir, "base", "a", "2.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "a", "1.md"), pageContent)
+
+ writeSource(t, fs, filepath.Join(workDir, "base", "a", "b", "index.md"), pageContentNoSlug)
+ writeSource(t, fs, filepath.Join(workDir, "base", "a", "b", "ab1.md"), pageContentNoSlug)
+
+ // Mostly plain static assets in a folder with a page in a sub folder thrown in.
+ writeSource(t, fs, filepath.Join(workDir, "base", "assets", "pic1.png"), "content")
+ writeSource(t, fs, filepath.Join(workDir, "base", "assets", "pic2.png"), "content")
+ writeSource(t, fs, filepath.Join(workDir, "base", "assets", "pages", "mypage.md"), pageContent)
+
+ // Bundle
+ writeSource(t, fs, filepath.Join(workDir, "base", "b", "my-bundle", "index.md"), pageWithImageShortcodeAndResourceMetadataContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "b", "my-bundle", "1.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "b", "my-bundle", "2.md"), pageContentShortcode)
+ writeSource(t, fs, filepath.Join(workDir, "base", "b", "my-bundle", "custom-mime.bep"), "bepsays")
+ writeSource(t, fs, filepath.Join(workDir, "base", "b", "my-bundle", "c", "logo.png"), "content")
+
+ // Bundle with 은행 slug
+ // See https://github.com/gohugoio/hugo/issues/4241
+ writeSource(t, fs, filepath.Join(workDir, "base", "c", "bundle", "index.md"), `---
+title: "은행 은행"
+slug: 은행
+date: 2017-10-09
+---
+
+Content for 은행.
+`)
+
+ // Bundle in root
+ writeSource(t, fs, filepath.Join(workDir, "base", "root", "index.md"), pageWithImageShortcodeAndResourceMetadataContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "root", "1.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "root", "c", "logo.png"), "content")
+
+ writeSource(t, fs, filepath.Join(workDir, "base", "c", "bundle", "logo-은행.png"), "은행 PNG")
+
+ // Write a real image into one of the bundle above.
+ src, err := os.Open("testdata/sunset.jpg")
+ assert.NoError(err)
+
+ // We need 2 to test https://github.com/gohugoio/hugo/issues/4202
+ out, err := fs.Source.Create(filepath.Join(workDir, "base", "b", "my-bundle", "sunset1.jpg"))
+ assert.NoError(err)
+ out2, err := fs.Source.Create(filepath.Join(workDir, "base", "b", "my-bundle", "sunset2.jpg"))
+ assert.NoError(err)
+
+ _, err = io.Copy(out, src)
+ assert.NoError(err)
+ out.Close()
+ src.Seek(0, 0)
+ _, err = io.Copy(out2, src)
+ out2.Close()
+ src.Close()
+ assert.NoError(err)
+
+ return fs, cfg
+
+}
+
+func newTestBundleSourcesMultilingual(t *testing.T) (*hugofs.Fs, *viper.Viper) {
+ cfg, fs := newTestCfg()
+
+ workDir := "/work"
+ cfg.Set("workingDir", workDir)
+ cfg.Set("contentDir", "base")
+ cfg.Set("baseURL", "https://example.com")
+ cfg.Set("defaultContentLanguage", "en")
+
+ langConfig := map[string]interface{}{
+ "en": map[string]interface{}{
+ "weight": 1,
+ "languageName": "English",
+ },
+ "nn": map[string]interface{}{
+ "weight": 2,
+ "languageName": "Nynorsk",
+ },
+ }
+
+ cfg.Set("languages", langConfig)
+
+ pageContent := `---
+slug: pageslug
+date: 2017-10-09
+---
+
+TheContent.
+`
+
+ layout := `{{ .Title }}|{{ .Content }}|Lang: {{ .Site.Language.Lang }}`
+
+ writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "single.html"), layout)
+ writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "list.html"), layout)
+
+ writeSource(t, fs, filepath.Join(workDir, "base", "1s", "mypage.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "1s", "mypage.nn.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "1s", "mylogo.png"), "content")
+
+ writeSource(t, fs, filepath.Join(workDir, "base", "bb", "_index.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "bb", "_index.nn.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "bb", "en.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "bb", "_1.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "bb", "_1.nn.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "bb", "a.png"), "content")
+ writeSource(t, fs, filepath.Join(workDir, "base", "bb", "b.png"), "content")
+ writeSource(t, fs, filepath.Join(workDir, "base", "bb", "b.nn.png"), "content")
+ writeSource(t, fs, filepath.Join(workDir, "base", "bb", "c.nn.png"), "content")
+ writeSource(t, fs, filepath.Join(workDir, "base", "bb", "b", "d.nn.png"), "content")
+
+ writeSource(t, fs, filepath.Join(workDir, "base", "bc", "_index.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "bc", "page.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "bc", "logo-bc.png"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "bc", "page.nn.md"), pageContent)
+
+ writeSource(t, fs, filepath.Join(workDir, "base", "bd", "index.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "bd", "page.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "bd", "page.nn.md"), pageContent)
+
+ writeSource(t, fs, filepath.Join(workDir, "base", "be", "_index.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "be", "page.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "be", "page.nn.md"), pageContent)
+
+ // Bundle leaf, multilingual
+ writeSource(t, fs, filepath.Join(workDir, "base", "lb", "index.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "lb", "index.nn.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "lb", "1.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "lb", "2.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "lb", "2.nn.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "lb", "c", "page.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "lb", "c", "logo.png"), "content")
+ writeSource(t, fs, filepath.Join(workDir, "base", "lb", "c", "logo.nn.png"), "content")
+ writeSource(t, fs, filepath.Join(workDir, "base", "lb", "c", "one.png"), "content")
+ writeSource(t, fs, filepath.Join(workDir, "base", "lb", "c", "d", "deep.png"), "content")
+
+ //Translated bundle in some sensible sub path.
+ writeSource(t, fs, filepath.Join(workDir, "base", "bf", "my-bf-bundle", "index.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "bf", "my-bf-bundle", "index.nn.md"), pageContent)
+ writeSource(t, fs, filepath.Join(workDir, "base", "bf", "my-bf-bundle", "page.md"), pageContent)
+
+ return fs, cfg
+}
+
+func newTestBundleSymbolicSources(t *testing.T) (*helpers.PathSpec, func(), string) {
+ assert := require.New(t)
+ // We need to use the OS fs for this.
+ cfg := viper.New()
+ fs := hugofs.NewFrom(hugofs.Os, cfg)
+ fs.Destination = &afero.MemMapFs{}
+ loadDefaultSettingsFor(cfg)
+
+ workDir, clean, err := createTempDir("hugosym")
+ assert.NoError(err)
+
+ contentDir := "base"
+ cfg.Set("workingDir", workDir)
+ cfg.Set("contentDir", contentDir)
+ cfg.Set("baseURL", "https://example.com")
+
+ if err := loadLanguageSettings(cfg, nil); err != nil {
+ t.Fatal(err)
+ }
+
+ layout := `{{ .Title }}|{{ .Content }}`
+ pageContent := `---
+slug: %s
+date: 2017-10-09
+---
+
+TheContent.
+`
+
+ fs.Source.MkdirAll(filepath.Join(workDir, "layouts", "_default"), 0777)
+ fs.Source.MkdirAll(filepath.Join(workDir, contentDir), 0777)
+ fs.Source.MkdirAll(filepath.Join(workDir, contentDir, "a"), 0777)
+ for i := 1; i <= 3; i++ {
+ fs.Source.MkdirAll(filepath.Join(workDir, fmt.Sprintf("symcontent%d", i)), 0777)
+
+ }
+ fs.Source.MkdirAll(filepath.Join(workDir, "symcontent2", "a1"), 0777)
+
+ writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "single.html"), layout)
+ writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "list.html"), layout)
+
+ writeSource(t, fs, filepath.Join(workDir, contentDir, "a", "regular.md"), fmt.Sprintf(pageContent, "a1"))
+
+ // Regular files inside symlinked folder.
+ writeSource(t, fs, filepath.Join(workDir, "symcontent1", "s1.md"), fmt.Sprintf(pageContent, "s1"))
+ writeSource(t, fs, filepath.Join(workDir, "symcontent1", "s2.md"), fmt.Sprintf(pageContent, "s2"))
+
+ // A bundle
+ writeSource(t, fs, filepath.Join(workDir, "symcontent2", "a1", "index.md"), fmt.Sprintf(pageContent, ""))
+ writeSource(t, fs, filepath.Join(workDir, "symcontent2", "a1", "page.md"), fmt.Sprintf(pageContent, "page"))
+ writeSource(t, fs, filepath.Join(workDir, "symcontent2", "a1", "logo.png"), "image")
+
+ // Assets
+ writeSource(t, fs, filepath.Join(workDir, "symcontent3", "s1.png"), "image")
+ writeSource(t, fs, filepath.Join(workDir, "symcontent3", "s2.png"), "image")
+
+ wd, _ := os.Getwd()
+ defer func() {
+ os.Chdir(wd)
+ }()
+ // Symlinked sections inside content.
+ os.Chdir(filepath.Join(workDir, contentDir))
+ for i := 1; i <= 3; i++ {
+ assert.NoError(os.Symlink(filepath.FromSlash(fmt.Sprintf(("../symcontent%d"), i)), fmt.Sprintf("symbolic%d", i)))
+ }
+
+ os.Chdir(filepath.Join(workDir, contentDir, "a"))
+
+ // Create a symlink to one single content file
+ assert.NoError(os.Symlink(filepath.FromSlash("../../symcontent2/a1/page.md"), "page_s.md"))
+
+ os.Chdir(filepath.FromSlash("../../symcontent3"))
+
+ // Create a circular symlink. Will print some warnings.
+ assert.NoError(os.Symlink(filepath.Join("..", contentDir), filepath.FromSlash("circus")))
+
+ os.Chdir(workDir)
+ assert.NoError(err)
+
+ ps, _ := helpers.NewPathSpec(fs, cfg)
+
+ return ps, clean, workDir
+}
+
+// https://github.com/gohugoio/hugo/issues/5858
+func TestBundledResourcesWhenMultipleOutputFormats(t *testing.T) {
+ t.Parallel()
+
+ b := newTestSitesBuilder(t).Running().WithConfigFile("toml", `
+baseURL = "https://example.org"
+[outputs]
+ # This looks odd, but it triggers the behaviour in #5858
+ # The total output formats list gets sorted, so CSS before HTML.
+ home = [ "CSS" ]
+
+`)
+ b.WithContent("mybundle/index.md", `
+---
+title: Page
+date: 2017-01-15
+---
+`,
+ "mybundle/data.json", "MyData",
+ )
+
+ b.CreateSites().Build(BuildCfg{})
+
+ b.AssertFileContent("public/mybundle/data.json", "MyData")
+
+ // Change the bundled JSON file and make sure it gets republished.
+ b.EditFiles("content/mybundle/data.json", "My changed data")
+
+ b.Build(BuildCfg{})
+
+ b.AssertFileContent("public/mybundle/data.json", "My changed data")
+
+}
diff --git a/hugolib/pagecollections.go b/hugolib/pagecollections.go
new file mode 100644
index 000000000..f62ea0905
--- /dev/null
+++ b/hugolib/pagecollections.go
@@ -0,0 +1,392 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/cache"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/resources/page"
+)
+
+// Used in the page cache to mark more than one hit for a given key.
+var ambiguityFlag = &pageState{}
+
+// PageCollections contains the page collections for a site.
+type PageCollections struct {
+
+ // Includes absolute all pages (of all types), including drafts etc.
+ rawAllPages pageStatePages
+
+ // rawAllPages plus additional pages created during the build process.
+ workAllPages pageStatePages
+
+ // Includes headless bundles, i.e. bundles that produce no output for its content page.
+ headlessPages pageStatePages
+
+ // Lazy initialized page collections
+ pages *lazyPagesFactory
+ regularPages *lazyPagesFactory
+ allPages *lazyPagesFactory
+ allRegularPages *lazyPagesFactory
+
+ // The index for .Site.GetPage etc.
+ pageIndex *cache.Lazy
+}
+
+// Pages returns all pages.
+// This is for the current language only.
+func (c *PageCollections) Pages() page.Pages {
+ return c.pages.get()
+}
+
+// RegularPages returns all the regular pages.
+// This is for the current language only.
+func (c *PageCollections) RegularPages() page.Pages {
+ return c.regularPages.get()
+}
+
+// AllPages returns all pages for all languages.
+func (c *PageCollections) AllPages() page.Pages {
+ return c.allPages.get()
+}
+
+// AllPages returns all regular pages for all languages.
+func (c *PageCollections) AllRegularPages() page.Pages {
+ return c.allRegularPages.get()
+}
+
+// Get initializes the index if not already done so, then
+// looks up the given page ref, returns nil if no value found.
+func (c *PageCollections) getFromCache(ref string) (page.Page, error) {
+ v, found, err := c.pageIndex.Get(ref)
+ if err != nil {
+ return nil, err
+ }
+ if !found {
+ return nil, nil
+ }
+
+ p := v.(page.Page)
+
+ if p != ambiguityFlag {
+ return p, nil
+ }
+ return nil, fmt.Errorf("page reference %q is ambiguous", ref)
+}
+
+type lazyPagesFactory struct {
+ pages page.Pages
+
+ init sync.Once
+ factory page.PagesFactory
+}
+
+func (l *lazyPagesFactory) get() page.Pages {
+ l.init.Do(func() {
+ l.pages = l.factory()
+ })
+ return l.pages
+}
+
+func newLazyPagesFactory(factory page.PagesFactory) *lazyPagesFactory {
+ return &lazyPagesFactory{factory: factory}
+}
+
+func newPageCollections() *PageCollections {
+ return newPageCollectionsFromPages(nil)
+}
+
+func newPageCollectionsFromPages(pages pageStatePages) *PageCollections {
+
+ c := &PageCollections{rawAllPages: pages}
+
+ c.pages = newLazyPagesFactory(func() page.Pages {
+ pages := make(page.Pages, len(c.workAllPages))
+ for i, p := range c.workAllPages {
+ pages[i] = p
+ }
+ return pages
+ })
+
+ c.regularPages = newLazyPagesFactory(func() page.Pages {
+ return c.findPagesByKindInWorkPages(page.KindPage, c.workAllPages)
+ })
+
+ c.pageIndex = cache.NewLazy(func() (map[string]interface{}, error) {
+ index := make(map[string]interface{})
+
+ add := func(ref string, p page.Page) {
+ ref = strings.ToLower(ref)
+ existing := index[ref]
+ if existing == nil {
+ index[ref] = p
+ } else if existing != ambiguityFlag && existing != p {
+ index[ref] = ambiguityFlag
+ }
+ }
+
+ for _, pageCollection := range []pageStatePages{c.workAllPages, c.headlessPages} {
+ for _, p := range pageCollection {
+ if p.IsPage() {
+ sourceRef := p.sourceRef()
+
+ if sourceRef != "" {
+ // index the canonical ref
+ // e.g. /section/article.md
+ add(sourceRef, p)
+ }
+
+ // Ref/Relref supports this potentially ambiguous lookup.
+ add(p.File().LogicalName(), p)
+
+ translationBaseName := p.File().TranslationBaseName()
+
+ dir, _ := path.Split(sourceRef)
+ dir = strings.TrimSuffix(dir, "/")
+
+ if translationBaseName == "index" {
+ add(dir, p)
+ add(path.Base(dir), p)
+ } else {
+ add(translationBaseName, p)
+ }
+
+ // We need a way to get to the current language version.
+ pathWithNoExtensions := path.Join(dir, translationBaseName)
+ add(pathWithNoExtensions, p)
+ } else {
+ // index the canonical, unambiguous ref for any backing file
+ // e.g. /section/_index.md
+ sourceRef := p.sourceRef()
+ if sourceRef != "" {
+ add(sourceRef, p)
+ }
+
+ ref := p.SectionsPath()
+
+ // index the canonical, unambiguous virtual ref
+ // e.g. /section
+ // (this may already have been indexed above)
+ add("/"+ref, p)
+ }
+ }
+ }
+
+ return index, nil
+ })
+
+ return c
+}
+
+// This is an adapter func for the old API with Kind as first argument.
+// This is invoked when you do .Site.GetPage. We drop the Kind and fails
+// if there are more than 2 arguments, which would be ambigous.
+func (c *PageCollections) getPageOldVersion(ref ...string) (page.Page, error) {
+ var refs []string
+ for _, r := range ref {
+ // A common construct in the wild is
+ // .Site.GetPage "home" "" or
+ // .Site.GetPage "home" "/"
+ if r != "" && r != "/" {
+ refs = append(refs, r)
+ }
+ }
+
+ var key string
+
+ if len(refs) > 2 {
+ // This was allowed in Hugo <= 0.44, but we cannot support this with the
+ // new API. This should be the most unusual case.
+ return nil, fmt.Errorf(`too many arguments to .Site.GetPage: %v. Use lookups on the form {{ .Site.GetPage "/posts/mypage-md" }}`, ref)
+ }
+
+ if len(refs) == 0 || refs[0] == page.KindHome {
+ key = "/"
+ } else if len(refs) == 1 {
+ if len(ref) == 2 && refs[0] == page.KindSection {
+ // This is an old style reference to the "Home Page section".
+ // Typically fetched via {{ .Site.GetPage "section" .Section }}
+ // See https://github.com/gohugoio/hugo/issues/4989
+ key = "/"
+ } else {
+ key = refs[0]
+ }
+ } else {
+ key = refs[1]
+ }
+
+ key = filepath.ToSlash(key)
+ if !strings.HasPrefix(key, "/") {
+ key = "/" + key
+ }
+
+ return c.getPageNew(nil, key)
+}
+
+// Only used in tests.
+func (c *PageCollections) getPage(typ string, sections ...string) page.Page {
+ refs := append([]string{typ}, path.Join(sections...))
+ p, _ := c.getPageOldVersion(refs...)
+ return p
+}
+
+// Case insensitive page lookup.
+func (c *PageCollections) getPageNew(context page.Page, ref string) (page.Page, error) {
+ var anError error
+
+ ref = strings.ToLower(ref)
+
+ // Absolute (content root relative) reference.
+ if strings.HasPrefix(ref, "/") {
+ p, err := c.getFromCache(ref)
+ if err == nil && p != nil {
+ return p, nil
+ }
+ if err != nil {
+ anError = err
+ }
+
+ } else if context != nil {
+ // Try the page-relative path.
+ ppath := path.Join("/", strings.ToLower(context.SectionsPath()), ref)
+ p, err := c.getFromCache(ppath)
+ if err == nil && p != nil {
+ return p, nil
+ }
+ if err != nil {
+ anError = err
+ }
+ }
+
+ if !strings.HasPrefix(ref, "/") {
+ // Many people will have "post/foo.md" in their content files.
+ p, err := c.getFromCache("/" + ref)
+ if err == nil && p != nil {
+ if context != nil {
+ // TODO(bep) remove this case and the message below when the storm has passed
+ err := wrapErr(errors.New(`make non-relative ref/relref page reference(s) in page %q absolute, e.g. {{< ref "/blog/my-post.md" >}}`), context)
+ helpers.DistinctWarnLog.Println(err)
+ }
+ return p, nil
+ }
+ if err != nil {
+ anError = err
+ }
+ }
+
+ // Last try.
+ ref = strings.TrimPrefix(ref, "/")
+ p, err := c.getFromCache(ref)
+ if err != nil {
+ anError = err
+ }
+
+ if p == nil && anError != nil {
+ return nil, wrapErr(errors.Wrap(anError, "failed to resolve ref"), context)
+ }
+
+ return p, nil
+}
+
+func (*PageCollections) findPagesByKindIn(kind string, inPages page.Pages) page.Pages {
+ var pages page.Pages
+ for _, p := range inPages {
+ if p.Kind() == kind {
+ pages = append(pages, p)
+ }
+ }
+ return pages
+}
+
+func (c *PageCollections) findPagesByKind(kind string) page.Pages {
+ return c.findPagesByKindIn(kind, c.Pages())
+}
+
+func (c *PageCollections) findWorkPagesByKind(kind string) pageStatePages {
+ var pages pageStatePages
+ for _, p := range c.workAllPages {
+ if p.Kind() == kind {
+ pages = append(pages, p)
+ }
+ }
+ return pages
+}
+
+func (*PageCollections) findPagesByKindInWorkPages(kind string, inPages pageStatePages) page.Pages {
+ var pages page.Pages
+ for _, p := range inPages {
+ if p.Kind() == kind {
+ pages = append(pages, p)
+ }
+ }
+ return pages
+}
+
+func (c *PageCollections) findFirstWorkPageByKindIn(kind string) *pageState {
+ for _, p := range c.workAllPages {
+ if p.Kind() == kind {
+ return p
+ }
+ }
+ return nil
+}
+
+func (c *PageCollections) addPage(page *pageState) {
+ c.rawAllPages = append(c.rawAllPages, page)
+}
+
+func (c *PageCollections) removePageFilename(filename string) {
+ if i := c.rawAllPages.findPagePosByFilename(filename); i >= 0 {
+ c.clearResourceCacheForPage(c.rawAllPages[i])
+ c.rawAllPages = append(c.rawAllPages[:i], c.rawAllPages[i+1:]...)
+ }
+
+}
+
+func (c *PageCollections) removePage(page *pageState) {
+ if i := c.rawAllPages.findPagePos(page); i >= 0 {
+ c.clearResourceCacheForPage(c.rawAllPages[i])
+ c.rawAllPages = append(c.rawAllPages[:i], c.rawAllPages[i+1:]...)
+ }
+}
+
+func (c *PageCollections) findPagesByShortcode(shortcode string) page.Pages {
+ var pages page.Pages
+ for _, p := range c.rawAllPages {
+ if p.HasShortcode(shortcode) {
+ pages = append(pages, p)
+ }
+ }
+ return pages
+}
+
+func (c *PageCollections) replacePage(page *pageState) {
+ // will find existing page that matches filepath and remove it
+ c.removePage(page)
+ c.addPage(page)
+}
+
+func (c *PageCollections) clearResourceCacheForPage(page *pageState) {
+ if len(page.resources) > 0 {
+ page.s.ResourceSpec.DeleteCacheByPrefix(page.targetPaths().SubResourceBaseTarget)
+ }
+}
diff --git a/hugolib/pagecollections_test.go b/hugolib/pagecollections_test.go
new file mode 100644
index 000000000..a5a347f83
--- /dev/null
+++ b/hugolib/pagecollections_test.go
@@ -0,0 +1,244 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/resources/page"
+
+ "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.getPageNew(nil, "/")
+ if home == nil {
+ b.Fatal("Home is nil")
+ }
+
+ p, _ := s.getPageNew(nil, 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.getPageNew(nil, pagePaths[i])
+ require.NotNil(b, page)
+ }
+}
+
+type testCase struct {
+ kind string
+ context page.Page
+ path []string
+ expectedTitle string
+}
+
+func (t *testCase) check(p page.Page, err error, errorMsg string, assert *require.Assertions) {
+ switch t.kind {
+ case "Ambiguous":
+ assert.Error(err)
+ assert.Nil(p, errorMsg)
+ case "NoPage":
+ assert.NoError(err)
+ assert.Nil(p, errorMsg)
+ default:
+ assert.NoError(err, errorMsg)
+ assert.NotNil(p, errorMsg)
+ assert.Equal(t.kind, p.Kind(), errorMsg)
+ assert.Equal(t.expectedTitle, p.Title(), errorMsg)
+ }
+}
+
+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, "home page")
+ writeSource(t, fs, filepath.Join("content", "_index.md"), content)
+
+ content = fmt.Sprintf(pageCollectionsPageTemplate, "about page")
+ writeSource(t, fs, filepath.Join("content", "about.md"), content)
+
+ content = fmt.Sprintf(pageCollectionsPageTemplate, "section 3")
+ writeSource(t, fs, filepath.Join("content", "sect3", "_index.md"), content)
+
+ content = fmt.Sprintf(pageCollectionsPageTemplate, "UniqueBase")
+ writeSource(t, fs, filepath.Join("content", "sect3", "unique.md"), content)
+
+ content = fmt.Sprintf(pageCollectionsPageTemplate, "another sect7")
+ writeSource(t, fs, filepath.Join("content", "sect3", "sect7", "_index.md"), content)
+
+ content = fmt.Sprintf(pageCollectionsPageTemplate, "deep page")
+ writeSource(t, fs, filepath.Join("content", "sect3", "subsect", "deep.md"), content)
+
+ s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
+
+ sec3, err := s.getPageNew(nil, "/sect3")
+ assert.NoError(err, "error getting Page for /sec3")
+ assert.NotNil(sec3, "failed to get Page for /sec3")
+
+ tests := []testCase{
+ // legacy content root relative paths
+ {page.KindHome, nil, []string{}, "home page"},
+ {page.KindPage, nil, []string{"about.md"}, "about page"},
+ {page.KindSection, nil, []string{"sect3"}, "section 3"},
+ {page.KindPage, nil, []string{"sect3/page1.md"}, "Title3_1"},
+ {page.KindPage, nil, []string{"sect4/page2.md"}, "Title4_2"},
+ {page.KindSection, nil, []string{"sect3/sect7"}, "another sect7"},
+ {page.KindPage, nil, []string{"sect3/subsect/deep.md"}, "deep page"},
+ {page.KindPage, nil, []string{filepath.FromSlash("sect5/page3.md")}, "Title5_3"}, //test OS-specific path
+
+ // shorthand refs (potentially ambiguous)
+ {page.KindPage, nil, []string{"unique.md"}, "UniqueBase"},
+ {"Ambiguous", nil, []string{"page1.md"}, ""},
+
+ // ISSUE: This is an ambiguous ref, but because we have to support the legacy
+ // content root relative paths without a leading slash, the lookup
+ // returns /sect7. This undermines ambiguity detection, but we have no choice.
+ //{"Ambiguous", nil, []string{"sect7"}, ""},
+ {page.KindSection, nil, []string{"sect7"}, "Sect7s"},
+
+ // absolute paths
+ {page.KindHome, nil, []string{"/"}, "home page"},
+ {page.KindPage, nil, []string{"/about.md"}, "about page"},
+ {page.KindSection, nil, []string{"/sect3"}, "section 3"},
+ {page.KindPage, nil, []string{"/sect3/page1.md"}, "Title3_1"},
+ {page.KindPage, nil, []string{"/sect4/page2.md"}, "Title4_2"},
+ {page.KindSection, nil, []string{"/sect3/sect7"}, "another sect7"},
+ {page.KindPage, nil, []string{"/sect3/subsect/deep.md"}, "deep page"},
+ {page.KindPage, nil, []string{filepath.FromSlash("/sect5/page3.md")}, "Title5_3"}, //test OS-specific path
+ {page.KindPage, nil, []string{"/sect3/unique.md"}, "UniqueBase"}, //next test depends on this page existing
+ // {"NoPage", nil, []string{"/unique.md"}, ""}, // ISSUE #4969: this is resolving to /sect3/unique.md
+ {"NoPage", nil, []string{"/missing-page.md"}, ""},
+ {"NoPage", nil, []string{"/missing-section"}, ""},
+
+ // relative paths
+ {page.KindHome, sec3, []string{".."}, "home page"},
+ {page.KindHome, sec3, []string{"../"}, "home page"},
+ {page.KindPage, sec3, []string{"../about.md"}, "about page"},
+ {page.KindSection, sec3, []string{"."}, "section 3"},
+ {page.KindSection, sec3, []string{"./"}, "section 3"},
+ {page.KindPage, sec3, []string{"page1.md"}, "Title3_1"},
+ {page.KindPage, sec3, []string{"./page1.md"}, "Title3_1"},
+ {page.KindPage, sec3, []string{"../sect4/page2.md"}, "Title4_2"},
+ {page.KindSection, sec3, []string{"sect7"}, "another sect7"},
+ {page.KindSection, sec3, []string{"./sect7"}, "another sect7"},
+ {page.KindPage, sec3, []string{"./subsect/deep.md"}, "deep page"},
+ {page.KindPage, sec3, []string{"./subsect/../../sect7/page9.md"}, "Title7_9"},
+ {page.KindPage, sec3, []string{filepath.FromSlash("../sect5/page3.md")}, "Title5_3"}, //test OS-specific path
+ {page.KindPage, sec3, []string{"./unique.md"}, "UniqueBase"},
+ {"NoPage", sec3, []string{"./sect2"}, ""},
+ //{"NoPage", sec3, []string{"sect2"}, ""}, // ISSUE: /sect3 page relative query is resolving to /sect2
+
+ // absolute paths ignore context
+ {page.KindHome, sec3, []string{"/"}, "home page"},
+ {page.KindPage, sec3, []string{"/about.md"}, "about page"},
+ {page.KindPage, sec3, []string{"/sect4/page2.md"}, "Title4_2"},
+ {page.KindPage, sec3, []string{"/sect3/subsect/deep.md"}, "deep page"}, //next test depends on this page existing
+ {"NoPage", sec3, []string{"/subsect/deep.md"}, ""},
+ }
+
+ for _, test := range tests {
+ errorMsg := fmt.Sprintf("Test case %s %v -> %s", test.context, test.path, test.expectedTitle)
+
+ // test legacy public Site.GetPage (which does not support page context relative queries)
+ if test.context == nil {
+ args := append([]string{test.kind}, test.path...)
+ page, err := s.Info.GetPage(args...)
+ test.check(page, err, errorMsg, assert)
+ }
+
+ // test new internal Site.getPageNew
+ var ref string
+ if len(test.path) == 1 {
+ ref = filepath.ToSlash(test.path[0])
+ } else {
+ ref = path.Join(test.path...)
+ }
+ page2, err := s.getPageNew(test.context, ref)
+ test.check(page2, err, errorMsg, assert)
+ }
+
+}
diff --git a/hugolib/pages_language_merge_test.go b/hugolib/pages_language_merge_test.go
new file mode 100644
index 000000000..bae2ddd81
--- /dev/null
+++ b/hugolib/pages_language_merge_test.go
@@ -0,0 +1,188 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "testing"
+
+ "github.com/gohugoio/hugo/resources/resource"
+ "github.com/stretchr/testify/require"
+)
+
+// TODO(bep) move and rewrite in resource/page.
+
+func TestMergeLanguages(t *testing.T) {
+ t.Parallel()
+ assert := require.New(t)
+
+ b := newTestSiteForLanguageMerge(t, 30)
+ b.CreateSites()
+
+ b.Build(BuildCfg{SkipRender: true})
+
+ h := b.H
+
+ enSite := h.Sites[0]
+ frSite := h.Sites[1]
+ nnSite := h.Sites[2]
+
+ assert.Equal(31, len(enSite.RegularPages()))
+ assert.Equal(6, len(frSite.RegularPages()))
+ assert.Equal(12, len(nnSite.RegularPages()))
+
+ for i := 0; i < 2; i++ {
+ mergedNN := nnSite.RegularPages().MergeByLanguage(enSite.RegularPages())
+ assert.Equal(31, len(mergedNN))
+ for i := 1; i <= 31; i++ {
+ expectedLang := "en"
+ if i == 2 || i%3 == 0 || i == 31 {
+ expectedLang = "nn"
+ }
+ p := mergedNN[i-1]
+ assert.Equal(expectedLang, p.Language().Lang, fmt.Sprintf("Test %d", i))
+ }
+ }
+
+ mergedFR := frSite.RegularPages().MergeByLanguage(enSite.RegularPages())
+ assert.Equal(31, len(mergedFR))
+ for i := 1; i <= 31; i++ {
+ expectedLang := "en"
+ if i%5 == 0 {
+ expectedLang = "fr"
+ }
+ p := mergedFR[i-1]
+ assert.Equal(expectedLang, p.Language().Lang, fmt.Sprintf("Test %d", i))
+ }
+
+ firstNN := nnSite.RegularPages()[0]
+ assert.Equal(4, len(firstNN.Sites()))
+ assert.Equal("en", firstNN.Sites().First().Language().Lang)
+
+ nnBundle := nnSite.getPage("page", "bundle")
+ enBundle := enSite.getPage("page", "bundle")
+
+ assert.Equal(6, len(enBundle.Resources()))
+ assert.Equal(2, len(nnBundle.Resources()))
+
+ var ri interface{} = nnBundle.Resources()
+
+ // This looks less ugly in the templates ...
+ mergedNNResources := ri.(resource.ResourcesLanguageMerger).MergeByLanguage(enBundle.Resources())
+ assert.Equal(6, len(mergedNNResources))
+
+ unchanged, err := nnSite.RegularPages().MergeByLanguageInterface(nil)
+ assert.NoError(err)
+ assert.Equal(nnSite.RegularPages(), unchanged)
+
+}
+
+func TestMergeLanguagesTemplate(t *testing.T) {
+ t.Parallel()
+
+ b := newTestSiteForLanguageMerge(t, 15)
+ b.WithTemplates("home.html", `
+{{ $pages := .Site.RegularPages }}
+{{ .Scratch.Set "pages" $pages }}
+{{ if eq .Language.Lang "nn" }}:
+{{ $enSite := index .Sites 0 }}
+{{ $frSite := index .Sites 1 }}
+{{ $nnBundle := .Site.GetPage "page" "bundle" }}
+{{ $enBundle := $enSite.GetPage "page" "bundle" }}
+{{ .Scratch.Set "pages" ($pages | lang.Merge $frSite.RegularPages| lang.Merge $enSite.RegularPages) }}
+{{ .Scratch.Set "pages2" (sort ($nnBundle.Resources | lang.Merge $enBundle.Resources) "Title") }}
+{{ end }}
+{{ $pages := .Scratch.Get "pages" }}
+{{ $pages2 := .Scratch.Get "pages2" }}
+Pages1: {{ range $i, $p := $pages }}{{ add $i 1 }}: {{ .File.Path }} {{ .Language.Lang }} | {{ end }}
+Pages2: {{ range $i, $p := $pages2 }}{{ add $i 1 }}: {{ .Title }} {{ .Language.Lang }} | {{ end }}
+
+`,
+ "shortcodes/shortcode.html", "MyShort",
+ "shortcodes/lingo.html", "MyLingo",
+ )
+
+ b.CreateSites()
+ b.Build(BuildCfg{})
+
+ b.AssertFileContent("public/nn/index.html", "Pages1: 1: p1.md en | 2: p2.nn.md nn | 3: p3.nn.md nn | 4: p4.md en | 5: p5.fr.md fr | 6: p6.nn.md nn | 7: p7.md en | 8: p8.md en | 9: p9.nn.md nn | 10: p10.fr.md fr | 11: p11.md en | 12: p12.nn.md nn | 13: p13.md en | 14: p14.md en | 15: p15.nn.md nn")
+ b.AssertFileContent("public/nn/index.html", "Pages2: 1: doc100 en | 2: doc101 nn | 3: doc102 nn | 4: doc103 en | 5: doc104 en | 6: doc105 en")
+}
+
+func newTestSiteForLanguageMerge(t testing.TB, count int) *sitesBuilder {
+ contentTemplate := `---
+title: doc%d
+weight: %d
+date: "2018-02-28"
+---
+# doc
+*some "content"*
+
+{{< shortcode >}}
+
+{{< lingo >}}
+`
+
+ builder := newTestSitesBuilder(t).WithDefaultMultiSiteConfig()
+
+ // We need some content with some missing translations.
+ // "en" is the main language, so add some English content + some Norwegian (nn, nynorsk) content.
+ var contentPairs []string
+ for i := 1; i <= count; i++ {
+ content := fmt.Sprintf(contentTemplate, i, i)
+ contentPairs = append(contentPairs, []string{fmt.Sprintf("p%d.md", i), content}...)
+ if i == 2 || i%3 == 0 {
+ // Add page 2,3, 6, 9 ... to both languages
+ contentPairs = append(contentPairs, []string{fmt.Sprintf("p%d.nn.md", i), content}...)
+ }
+ if i%5 == 0 {
+ // Add some French content, too.
+ contentPairs = append(contentPairs, []string{fmt.Sprintf("p%d.fr.md", i), content}...)
+ }
+ }
+
+ // See https://github.com/gohugoio/hugo/issues/4644
+ // Add a bundles
+ j := 100
+ contentPairs = append(contentPairs, []string{"bundle/index.md", fmt.Sprintf(contentTemplate, j, j)}...)
+ for i := 0; i < 6; i++ {
+ contentPairs = append(contentPairs, []string{fmt.Sprintf("bundle/pb%d.md", i), fmt.Sprintf(contentTemplate, i+j, i+j)}...)
+ }
+ contentPairs = append(contentPairs, []string{"bundle/index.nn.md", fmt.Sprintf(contentTemplate, j, j)}...)
+ for i := 1; i < 3; i++ {
+ contentPairs = append(contentPairs, []string{fmt.Sprintf("bundle/pb%d.nn.md", i), fmt.Sprintf(contentTemplate, i+j, i+j)}...)
+ }
+
+ builder.WithContent(contentPairs...)
+ return builder
+}
+
+func BenchmarkMergeByLanguage(b *testing.B) {
+ const count = 100
+
+ builder := newTestSiteForLanguageMerge(b, count)
+ builder.CreateSites()
+ builder.Build(BuildCfg{SkipRender: true})
+ h := builder.H
+
+ enSite := h.Sites[0]
+ nnSite := h.Sites[2]
+
+ for i := 0; i < b.N; i++ {
+ merged := nnSite.RegularPages().MergeByLanguage(enSite.RegularPages())
+ if len(merged) != count {
+ b.Fatal("Count mismatch")
+ }
+ }
+}
diff --git a/hugolib/paginator_test.go b/hugolib/paginator_test.go
new file mode 100644
index 000000000..d98ec30e9
--- /dev/null
+++ b/hugolib/paginator_test.go
@@ -0,0 +1,98 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "testing"
+)
+
+func TestPaginator(t *testing.T) {
+ configFile := `
+baseURL = "https://example.com/foo/"
+paginate = 3
+paginatepath = "thepage"
+
+[languages.en]
+weight = 1
+contentDir = "content/en"
+
+[languages.nn]
+weight = 2
+contentDir = "content/nn"
+
+`
+ b := newTestSitesBuilder(t).WithConfigFile("toml", configFile)
+ var content []string
+ for i := 0; i < 9; i++ {
+ for _, contentDir := range []string{"content/en", "content/nn"} {
+ content = append(content, fmt.Sprintf(contentDir+"/blog/page%d.md", i), fmt.Sprintf(`---
+title: Page %d
+---
+
+Content.
+`, i))
+ }
+
+ }
+
+ b.WithContent(content...)
+
+ pagTemplate := `
+{{ $pag := $.Paginator }}
+Total: {{ $pag.TotalPages }}
+First: {{ $pag.First.URL }}
+Page Number: {{ $pag.PageNumber }}
+URL: {{ $pag.URL }}
+{{ with $pag.Next }}Next: {{ .URL }}{{ end }}
+{{ with $pag.Prev }}Prev: {{ .URL }}{{ end }}
+{{ range $i, $e := $pag.Pagers }}
+{{ printf "%d: %d/%d %t" $i $pag.PageNumber .PageNumber (eq . $pag) -}}
+{{ end }}
+`
+
+ b.WithTemplatesAdded("index.html", pagTemplate)
+ b.WithTemplatesAdded("index.xml", pagTemplate)
+
+ b.Build(BuildCfg{})
+
+ b.AssertFileContent("public/index.html",
+ "Page Number: 1",
+ "0: 1/1 true")
+
+ b.AssertFileContent("public/thepage/2/index.html",
+ "Total: 3",
+ "Page Number: 2",
+ "URL: /foo/thepage/2/",
+ "Next: /foo/thepage/3/",
+ "Prev: /foo/",
+ "1: 2/2 true",
+ )
+
+ b.AssertFileContent("public/index.xml",
+ "Page Number: 1",
+ "0: 1/1 true")
+ b.AssertFileContent("public/thepage/2/index.xml",
+ "Page Number: 2",
+ "1: 2/2 true")
+
+ b.AssertFileContent("public/nn/index.html",
+ "Page Number: 1",
+ "0: 1/1 true")
+
+ b.AssertFileContent("public/nn/index.xml",
+ "Page Number: 1",
+ "0: 1/1 true")
+
+}
diff --git a/hugolib/paths/baseURL.go b/hugolib/paths/baseURL.go
new file mode 100644
index 000000000..a3c7e9d27
--- /dev/null
+++ b/hugolib/paths/baseURL.go
@@ -0,0 +1,87 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package paths
+
+import (
+ "fmt"
+ "net/url"
+ "strings"
+)
+
+// A BaseURL in Hugo is normally on the form scheme://path, but the
+// form scheme: is also valid (mailto:hugo@rules.com).
+type BaseURL struct {
+ url *url.URL
+ urlStr string
+}
+
+func (b BaseURL) String() string {
+ if b.urlStr != "" {
+ return b.urlStr
+ }
+ return b.url.String()
+}
+
+func (b BaseURL) Path() string {
+ return b.url.Path
+}
+
+// HostURL returns the URL to the host root without any path elements.
+func (b BaseURL) HostURL() string {
+ return strings.TrimSuffix(b.String(), b.Path())
+}
+
+// WithProtocol returns the BaseURL prefixed with the given protocol.
+// The Protocol is normally of 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
+}
+
+// URL returns a copy of the internal URL.
+// The copy can be safely used and modified.
+func (b BaseURL) URL() *url.URL {
+ 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/hugolib/paths/baseURL_test.go b/hugolib/paths/baseURL_test.go
new file mode 100644
index 000000000..382a18314
--- /dev/null
+++ b/hugolib/paths/baseURL_test.go
@@ -0,0 +1,66 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package paths
+
+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:hugo@rules.com")
+ require.NoError(t, err)
+ require.Equal(t, "mailto:hugo@rules.com", b.String())
+
+ // These are pretty constructed
+ p, err = b.WithProtocol("webcal")
+ require.NoError(t, err)
+ require.Equal(t, "webcal:hugo@rules.com", p)
+
+ p, err = b.WithProtocol("webcal://")
+ require.NoError(t, err)
+ require.Equal(t, "webcal://hugo@rules.com", 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())
+
+ // BaseURL with sub path
+ b, err = newBaseURLFromString("http://example.com/sub")
+ require.NoError(t, err)
+ require.Equal(t, "http://example.com/sub", b.String())
+ require.Equal(t, "http://example.com", b.HostURL())
+}
diff --git a/hugolib/paths/paths.go b/hugolib/paths/paths.go
new file mode 100644
index 000000000..df66e2a46
--- /dev/null
+++ b/hugolib/paths/paths.go
@@ -0,0 +1,279 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package paths
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/langs"
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/hugofs"
+)
+
+var FilePathSeparator = string(filepath.Separator)
+
+type Paths struct {
+ Fs *hugofs.Fs
+ Cfg config.Provider
+
+ BaseURL
+
+ // If the baseURL contains a base path, e.g. https://example.com/docs, then "/docs" will be the BasePath.
+ BasePath string
+
+ // Directories
+ // TODO(bep) when we have trimmed down mos of the dirs usage outside of this package, make
+ // these into an interface.
+ ContentDir string
+ ThemesDir string
+ WorkingDir string
+
+ // Directories to store Resource related artifacts.
+ AbsResourcesDir string
+
+ AbsPublishDir string
+
+ // pagination path handling
+ PaginatePath string
+
+ PublishDir string
+
+ // When in multihost mode, this returns a list of base paths below PublishDir
+ // for each language.
+ MultihostTargetBasePaths []string
+
+ DisablePathToLower bool
+ RemovePathAccents bool
+ UglyURLs bool
+ CanonifyURLs bool
+
+ Language *langs.Language
+ Languages langs.Languages
+
+ // 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
+
+ themes []string
+ AllThemes []ThemeConfig
+}
+
+func New(fs *hugofs.Fs, cfg config.Provider) (*Paths, error) {
+ baseURLstr := cfg.GetString("baseURL")
+ baseURL, err := newBaseURLFromString(baseURLstr)
+
+ if err != nil {
+ return nil, errors.Wrapf(err, "Failed to create baseURL from %q:", baseURLstr)
+ }
+
+ contentDir := filepath.Clean(cfg.GetString("contentDir"))
+ workingDir := filepath.Clean(cfg.GetString("workingDir"))
+ resourceDir := filepath.Clean(cfg.GetString("resourceDir"))
+ publishDir := filepath.Clean(cfg.GetString("publishDir"))
+
+ if contentDir == "" {
+ return nil, fmt.Errorf("contentDir not set")
+ }
+ if resourceDir == "" {
+ return nil, fmt.Errorf("resourceDir not set")
+ }
+ if publishDir == "" {
+ return nil, fmt.Errorf("publishDir not set")
+ }
+
+ defaultContentLanguage := cfg.GetString("defaultContentLanguage")
+
+ var (
+ language *langs.Language
+ languages langs.Languages
+ )
+
+ if l, ok := cfg.(*langs.Language); ok {
+ language = l
+
+ }
+
+ if l, ok := cfg.Get("languagesSorted").(langs.Languages); ok {
+ languages = l
+ }
+
+ if len(languages) == 0 {
+ // We have some old tests that does not test the entire chain, hence
+ // they have no languages. So create one so we get the proper filesystem.
+ languages = langs.Languages{&langs.Language{Lang: "en", Cfg: cfg, ContentDir: contentDir}}
+ }
+
+ absPublishDir := AbsPathify(workingDir, publishDir)
+ if !strings.HasSuffix(absPublishDir, FilePathSeparator) {
+ absPublishDir += FilePathSeparator
+ }
+ // If root, remove the second '/'
+ if absPublishDir == "//" {
+ absPublishDir = FilePathSeparator
+ }
+ absResourcesDir := AbsPathify(workingDir, resourceDir)
+ if !strings.HasSuffix(absResourcesDir, FilePathSeparator) {
+ absResourcesDir += FilePathSeparator
+ }
+ if absResourcesDir == "//" {
+ absResourcesDir = FilePathSeparator
+ }
+
+ var multihostTargetBasePaths []string
+ if languages.IsMultihost() {
+ for _, l := range languages {
+ multihostTargetBasePaths = append(multihostTargetBasePaths, l.Lang)
+ }
+ }
+
+ p := &Paths{
+ Fs: fs,
+ Cfg: cfg,
+ BaseURL: baseURL,
+
+ DisablePathToLower: cfg.GetBool("disablePathToLower"),
+ RemovePathAccents: cfg.GetBool("removePathAccents"),
+ UglyURLs: cfg.GetBool("uglyURLs"),
+ CanonifyURLs: cfg.GetBool("canonifyURLs"),
+
+ ContentDir: contentDir,
+ ThemesDir: cfg.GetString("themesDir"),
+ WorkingDir: workingDir,
+
+ AbsResourcesDir: absResourcesDir,
+ AbsPublishDir: absPublishDir,
+
+ themes: config.GetStringSlicePreserveString(cfg, "theme"),
+
+ multilingual: cfg.GetBool("multilingual"),
+ defaultContentLanguageInSubdir: cfg.GetBool("defaultContentLanguageInSubdir"),
+ DefaultContentLanguage: defaultContentLanguage,
+
+ Language: language,
+ Languages: languages,
+ MultihostTargetBasePaths: multihostTargetBasePaths,
+
+ PaginatePath: cfg.GetString("paginatePath"),
+ }
+
+ if !cfg.IsSet("theme") && cfg.IsSet("allThemes") {
+ p.AllThemes = cfg.Get("allThemes").([]ThemeConfig)
+ } else {
+ p.AllThemes, err = collectThemeNames(p)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // TODO(bep) remove this, eventually
+ p.PublishDir = absPublishDir
+
+ return p, nil
+}
+
+// GetBasePath returns any path element in baseURL if needed.
+func (p *Paths) GetBasePath(isRelativeURL bool) string {
+ if isRelativeURL && p.CanonifyURLs {
+ // The baseURL will be prepended later.
+ return ""
+ }
+ return p.BasePath
+}
+
+func (p *Paths) Lang() string {
+ if p == nil || p.Language == nil {
+ return ""
+ }
+ return p.Language.Lang
+}
+
+// ThemeSet checks whether a theme is in use or not.
+func (p *Paths) ThemeSet() bool {
+ return len(p.themes) > 0
+}
+
+func (p *Paths) Themes() []string {
+ return p.themes
+}
+
+func (p *Paths) GetTargetLanguageBasePath() string {
+ if p.Languages.IsMultihost() {
+ // In a multihost configuration all assets will be published below the language code.
+ return p.Lang()
+ }
+ return p.GetLanguagePrefix()
+}
+
+func (p *Paths) GetURLLanguageBasePath() string {
+ if p.Languages.IsMultihost() {
+ return ""
+ }
+ return p.GetLanguagePrefix()
+}
+
+func (p *Paths) GetLanguagePrefix() string {
+ if !p.multilingual {
+ return ""
+ }
+
+ defaultLang := p.DefaultContentLanguage
+ defaultInSubDir := p.defaultContentLanguageInSubdir
+
+ currentLang := p.Language.Lang
+ if currentLang == "" || (currentLang == defaultLang && !defaultInSubDir) {
+ return ""
+ }
+ return currentLang
+}
+
+// GetLangSubDir returns the given language's subdir if needed.
+func (p *Paths) GetLangSubDir(lang string) string {
+ if !p.multilingual {
+ return ""
+ }
+
+ if p.Languages.IsMultihost() {
+ return ""
+ }
+
+ if lang == "" || (lang == p.DefaultContentLanguage && !p.defaultContentLanguageInSubdir) {
+ return ""
+ }
+
+ return lang
+}
+
+// AbsPathify creates an absolute path if given a relative path. If already
+// absolute, the path is just cleaned.
+func (p *Paths) AbsPathify(inPath string) string {
+ return AbsPathify(p.WorkingDir, inPath)
+}
+
+// AbsPathify creates an absolute path if given a working dir and arelative path.
+// If already absolute, the path is just cleaned.
+func AbsPathify(workingDir, inPath string) string {
+ if filepath.IsAbs(inPath) {
+ return filepath.Clean(inPath)
+ }
+ return filepath.Join(workingDir, inPath)
+}
diff --git a/hugolib/paths/paths_test.go b/hugolib/paths/paths_test.go
new file mode 100644
index 000000000..3bd445b8b
--- /dev/null
+++ b/hugolib/paths/paths_test.go
@@ -0,0 +1,44 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package paths
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/require"
+)
+
+func TestNewPaths(t *testing.T) {
+ assert := require.New(t)
+
+ v := viper.New()
+ fs := hugofs.NewMem(v)
+
+ v.Set("defaultContentLanguageInSubdir", true)
+ v.Set("defaultContentLanguage", "no")
+ v.Set("multilingual", true)
+ v.Set("contentDir", "content")
+ v.Set("workingDir", "work")
+ v.Set("resourceDir", "resources")
+ v.Set("publishDir", "public")
+
+ p, err := New(fs, v)
+ assert.NoError(err)
+
+ assert.Equal(true, p.defaultContentLanguageInSubdir)
+ assert.Equal("no", p.DefaultContentLanguage)
+ assert.Equal(true, p.multilingual)
+}
diff --git a/hugolib/paths/themes.go b/hugolib/paths/themes.go
new file mode 100644
index 000000000..a526953f1
--- /dev/null
+++ b/hugolib/paths/themes.go
@@ -0,0 +1,154 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package paths
+
+import (
+ "path/filepath"
+ "strings"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/spf13/afero"
+ "github.com/spf13/cast"
+)
+
+type ThemeConfig struct {
+ // The theme name as provided by the folder name below /themes.
+ Name string
+
+ // Optional configuration filename (e.g. "/themes/mytheme/config.json").
+ ConfigFilename string
+
+ // Optional config read from the ConfigFile above.
+ Cfg config.Provider
+}
+
+// Create file system, an ordered theme list from left to right, no duplicates.
+type themesCollector struct {
+ themesDir string
+ fs afero.Fs
+ seen map[string]bool
+ themes []ThemeConfig
+}
+
+func (c *themesCollector) isSeen(theme string) bool {
+ loki := strings.ToLower(theme)
+ if c.seen[loki] {
+ return true
+ }
+ c.seen[loki] = true
+ return false
+}
+
+func (c *themesCollector) addAndRecurse(themes ...string) error {
+ for i := 0; i < len(themes); i++ {
+ theme := themes[i]
+ configFilename := c.getConfigFileIfProvided(theme)
+ if !c.isSeen(theme) {
+ tc, err := c.add(theme, configFilename)
+ if err != nil {
+ return err
+ }
+ if err := c.addThemeNamesFromTheme(tc); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func (c *themesCollector) add(name, configFilename string) (ThemeConfig, error) {
+ var cfg config.Provider
+ var tc ThemeConfig
+
+ if configFilename != "" {
+ var err error
+ cfg, err = config.FromFile(c.fs, configFilename)
+ if err != nil {
+ return tc, err
+ }
+ }
+
+ tc = ThemeConfig{Name: name, ConfigFilename: configFilename, Cfg: cfg}
+ c.themes = append(c.themes, tc)
+ return tc, nil
+
+}
+
+func collectThemeNames(p *Paths) ([]ThemeConfig, error) {
+ return CollectThemes(p.Fs.Source, p.AbsPathify(p.ThemesDir), p.Themes())
+
+}
+
+func CollectThemes(fs afero.Fs, themesDir string, themes []string) ([]ThemeConfig, error) {
+ if len(themes) == 0 {
+ return nil, nil
+ }
+
+ c := &themesCollector{
+ fs: fs,
+ themesDir: themesDir,
+ seen: make(map[string]bool)}
+
+ for i := 0; i < len(themes); i++ {
+ theme := themes[i]
+ if err := c.addAndRecurse(theme); err != nil {
+ return nil, err
+ }
+ }
+
+ return c.themes, nil
+
+}
+
+func (c *themesCollector) getConfigFileIfProvided(theme string) string {
+ configDir := filepath.Join(c.themesDir, theme)
+
+ var (
+ configFilename string
+ exists bool
+ )
+
+ // Viper supports more, but this is the sub-set supported by Hugo.
+ for _, configFormats := range config.ValidConfigFileExtensions {
+ configFilename = filepath.Join(configDir, "config."+configFormats)
+ exists, _ = afero.Exists(c.fs, configFilename)
+ if exists {
+ break
+ }
+ }
+
+ if !exists {
+ // No theme config set.
+ return ""
+ }
+
+ return configFilename
+
+}
+
+func (c *themesCollector) addThemeNamesFromTheme(theme ThemeConfig) error {
+ if theme.Cfg != nil && theme.Cfg.IsSet("theme") {
+ v := theme.Cfg.Get("theme")
+ switch vv := v.(type) {
+ case []string:
+ return c.addAndRecurse(vv...)
+ case []interface{}:
+ return c.addAndRecurse(cast.ToStringSlice(vv)...)
+ default:
+ return c.addAndRecurse(cast.ToString(vv))
+ }
+ }
+
+ return nil
+}
diff --git a/hugolib/permalinker.go b/hugolib/permalinker.go
new file mode 100644
index 000000000..29dad6ce4
--- /dev/null
+++ b/hugolib/permalinker.go
@@ -0,0 +1,24 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 = (*pageState)(nil)
+)
+
+// Permalinker provides permalinks of both the relative and absolute kind.
+type Permalinker interface {
+ Permalink() string
+ RelPermalink() string
+}
diff --git a/hugolib/prune_resources.go b/hugolib/prune_resources.go
new file mode 100644
index 000000000..bf5a1ef2f
--- /dev/null
+++ b/hugolib/prune_resources.go
@@ -0,0 +1,19 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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
+
+// GC requires a build first and must run on it's own. It is not thread safe.
+func (h *HugoSites) GC() (int, error) {
+ return h.Deps.FileCaches.Prune()
+}
diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go
new file mode 100644
index 000000000..e22121b77
--- /dev/null
+++ b/hugolib/resource_chain_test.go
@@ -0,0 +1,478 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/spf13/viper"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/gohugoio/hugo/hugofs"
+
+ "github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss"
+)
+
+func TestSCSSWithIncludePaths(t *testing.T) {
+ if !scss.Supports() {
+ t.Skip("Skip SCSS")
+ }
+ assert := require.New(t)
+ workDir, clean, err := createTempDir("hugo-scss-include")
+ assert.NoError(err)
+ defer clean()
+
+ v := viper.New()
+ v.Set("workingDir", workDir)
+ b := newTestSitesBuilder(t).WithLogger(loggers.NewErrorLogger())
+ b.WithViper(v)
+ b.WithWorkingDir(workDir)
+ // Need to use OS fs for this.
+ b.Fs = hugofs.NewDefault(v)
+
+ fooDir := filepath.Join(workDir, "node_modules", "foo")
+ scssDir := filepath.Join(workDir, "assets", "scss")
+ assert.NoError(os.MkdirAll(fooDir, 0777))
+ assert.NoError(os.MkdirAll(filepath.Join(workDir, "content", "sect"), 0777))
+ assert.NoError(os.MkdirAll(filepath.Join(workDir, "data"), 0777))
+ assert.NoError(os.MkdirAll(filepath.Join(workDir, "i18n"), 0777))
+ assert.NoError(os.MkdirAll(filepath.Join(workDir, "layouts", "shortcodes"), 0777))
+ assert.NoError(os.MkdirAll(filepath.Join(workDir, "layouts", "_default"), 0777))
+ assert.NoError(os.MkdirAll(filepath.Join(scssDir), 0777))
+
+ b.WithSourceFile(filepath.Join(fooDir, "_moo.scss"), `
+$moolor: #fff;
+
+moo {
+ color: $moolor;
+}
+`)
+
+ b.WithSourceFile(filepath.Join(scssDir, "main.scss"), `
+@import "moo";
+
+`)
+
+ b.WithTemplatesAdded("index.html", `
+{{ $cssOpts := (dict "includePaths" (slice "node_modules/foo" ) ) }}
+{{ $r := resources.Get "scss/main.scss" | toCSS $cssOpts | minify }}
+T1: {{ $r.Content }}
+`)
+ b.Build(BuildCfg{})
+
+ b.AssertFileContent(filepath.Join(workDir, "public/index.html"), `T1: moo{color:#fff}`)
+
+}
+
+func TestSCSSWithThemeOverrides(t *testing.T) {
+ if !scss.Supports() {
+ t.Skip("Skip SCSS")
+ }
+ assert := require.New(t)
+ workDir, clean, err := createTempDir("hugo-scss-include")
+ assert.NoError(err)
+ defer clean()
+
+ theme := "mytheme"
+ themesDir := filepath.Join(workDir, "themes")
+ themeDirs := filepath.Join(themesDir, theme)
+ v := viper.New()
+ v.Set("workingDir", workDir)
+ v.Set("theme", theme)
+ b := newTestSitesBuilder(t).WithLogger(loggers.NewErrorLogger())
+ b.WithViper(v)
+ b.WithWorkingDir(workDir)
+ // Need to use OS fs for this.
+ b.Fs = hugofs.NewDefault(v)
+
+ fooDir := filepath.Join(workDir, "node_modules", "foo")
+ scssDir := filepath.Join(workDir, "assets", "scss")
+ scssThemeDir := filepath.Join(themeDirs, "assets", "scss")
+ assert.NoError(os.MkdirAll(fooDir, 0777))
+ assert.NoError(os.MkdirAll(filepath.Join(workDir, "content", "sect"), 0777))
+ assert.NoError(os.MkdirAll(filepath.Join(workDir, "data"), 0777))
+ assert.NoError(os.MkdirAll(filepath.Join(workDir, "i18n"), 0777))
+ assert.NoError(os.MkdirAll(filepath.Join(workDir, "layouts", "shortcodes"), 0777))
+ assert.NoError(os.MkdirAll(filepath.Join(workDir, "layouts", "_default"), 0777))
+ assert.NoError(os.MkdirAll(filepath.Join(scssDir, "components"), 0777))
+ assert.NoError(os.MkdirAll(filepath.Join(scssThemeDir, "components"), 0777))
+
+ b.WithSourceFile(filepath.Join(scssThemeDir, "components", "_imports.scss"), `
+@import "moo";
+@import "_boo";
+`)
+
+ b.WithSourceFile(filepath.Join(scssThemeDir, "components", "_moo.scss"), `
+$moolor: #fff;
+
+moo {
+ color: $moolor;
+}
+`)
+
+ b.WithSourceFile(filepath.Join(scssThemeDir, "components", "_boo.scss"), `
+$boolor: orange;
+
+boo {
+ color: $boolor;
+}
+`)
+
+ b.WithSourceFile(filepath.Join(scssThemeDir, "main.scss"), `
+@import "components/imports";
+
+`)
+
+ b.WithSourceFile(filepath.Join(scssDir, "components", "_moo.scss"), `
+$moolor: #ccc;
+
+moo {
+ color: $moolor;
+}
+`)
+
+ b.WithSourceFile(filepath.Join(scssDir, "components", "_boo.scss"), `
+$boolor: green;
+
+boo {
+ color: $boolor;
+}
+`)
+
+ b.WithTemplatesAdded("index.html", `
+{{ $cssOpts := (dict "includePaths" (slice "node_modules/foo" ) ) }}
+{{ $r := resources.Get "scss/main.scss" | toCSS $cssOpts | minify }}
+T1: {{ $r.Content }}
+`)
+ b.Build(BuildCfg{})
+
+ b.AssertFileContent(filepath.Join(workDir, "public/index.html"), `T1: moo{color:#ccc}boo{color:green}`)
+
+}
+
+func TestResourceChain(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ tests := []struct {
+ name string
+ shouldRun func() bool
+ prepare func(b *sitesBuilder)
+ verify func(b *sitesBuilder)
+ }{
+ {"tocss", func() bool { return scss.Supports() }, func(b *sitesBuilder) {
+ b.WithTemplates("home.html", `
+{{ $scss := resources.Get "scss/styles2.scss" | toCSS }}
+{{ $sass := resources.Get "sass/styles3.sass" | toCSS }}
+{{ $scssCustomTarget := resources.Get "scss/styles2.scss" | toCSS (dict "targetPath" "styles/main.css") }}
+{{ $scssCustomTargetString := resources.Get "scss/styles2.scss" | toCSS "styles/main.css" }}
+{{ $scssMin := resources.Get "scss/styles2.scss" | toCSS | minify }}
+{{ $scssFromTempl := ".{{ .Kind }} { color: blue; }" | resources.FromString "kindofblue.templ" | resources.ExecuteAsTemplate "kindofblue.scss" . | toCSS (dict "targetPath" "styles/templ.css") | minify }}
+{{ $bundle1 := slice $scssFromTempl $scssMin | resources.Concat "styles/bundle1.css" }}
+T1: Len Content: {{ len $scss.Content }}|RelPermalink: {{ $scss.RelPermalink }}|Permalink: {{ $scss.Permalink }}|MediaType: {{ $scss.MediaType.Type }}
+T2: Content: {{ $scssMin.Content }}|RelPermalink: {{ $scssMin.RelPermalink }}
+T3: Content: {{ len $scssCustomTarget.Content }}|RelPermalink: {{ $scssCustomTarget.RelPermalink }}|MediaType: {{ $scssCustomTarget.MediaType.Type }}
+T4: Content: {{ len $scssCustomTargetString.Content }}|RelPermalink: {{ $scssCustomTargetString.RelPermalink }}|MediaType: {{ $scssCustomTargetString.MediaType.Type }}
+T5: Content: {{ $sass.Content }}|T5 RelPermalink: {{ $sass.RelPermalink }}|
+T6: {{ $bundle1.Permalink }}
+`)
+ }, func(b *sitesBuilder) {
+ b.AssertFileContent("public/index.html", `T1: Len Content: 24|RelPermalink: /scss/styles2.css|Permalink: http://example.com/scss/styles2.css|MediaType: text/css`)
+ b.AssertFileContent("public/index.html", `T2: Content: body{color:#333}|RelPermalink: /scss/styles2.min.css`)
+ b.AssertFileContent("public/index.html", `T3: Content: 24|RelPermalink: /styles/main.css|MediaType: text/css`)
+ b.AssertFileContent("public/index.html", `T4: Content: 24|RelPermalink: /styles/main.css|MediaType: text/css`)
+ b.AssertFileContent("public/index.html", `T5: Content: .content-navigation {`)
+ b.AssertFileContent("public/index.html", `T5 RelPermalink: /sass/styles3.css|`)
+ b.AssertFileContent("public/index.html", `T6: http://example.com/styles/bundle1.css`)
+
+ assert.False(b.CheckExists("public/styles/templ.min.css"))
+ b.AssertFileContent("public/styles/bundle1.css", `.home{color:blue}body{color:#333}`)
+
+ }},
+
+ {"minify", func() bool { return true }, func(b *sitesBuilder) {
+ b.WithTemplates("home.html", `
+Min CSS: {{ ( resources.Get "css/styles1.css" | minify ).Content }}
+Min JS: {{ ( resources.Get "js/script1.js" | resources.Minify ).Content | safeJS }}
+Min JSON: {{ ( resources.Get "mydata/json1.json" | resources.Minify ).Content | safeHTML }}
+Min XML: {{ ( resources.Get "mydata/xml1.xml" | resources.Minify ).Content | safeHTML }}
+Min SVG: {{ ( resources.Get "mydata/svg1.svg" | resources.Minify ).Content | safeHTML }}
+Min SVG again: {{ ( resources.Get "mydata/svg1.svg" | resources.Minify ).Content | safeHTML }}
+Min HTML: {{ ( resources.Get "mydata/html1.html" | resources.Minify ).Content | safeHTML }}
+
+
+`)
+ }, func(b *sitesBuilder) {
+ b.AssertFileContent("public/index.html", `Min CSS: h1{font-style:bold}`)
+ b.AssertFileContent("public/index.html", `Min JS: var x;x=5;document.getElementById(&#34;demo&#34;).innerHTML=x*10;`)
+ b.AssertFileContent("public/index.html", `Min JSON: {"employees":[{"firstName":"John","lastName":"Doe"},{"firstName":"Anna","lastName":"Smith"},{"firstName":"Peter","lastName":"Jones"}]}`)
+ b.AssertFileContent("public/index.html", `Min XML: <hello><world>Hugo Rocks!</<world></hello>`)
+ b.AssertFileContent("public/index.html", `Min SVG: <svg height="100" width="100"><path d="M5 10 20 40z"/></svg>`)
+ b.AssertFileContent("public/index.html", `Min SVG again: <svg height="100" width="100"><path d="M5 10 20 40z"/></svg>`)
+ b.AssertFileContent("public/index.html", `Min HTML: <html><a href=#>Cool</a></html>`)
+ }},
+
+ {"concat", func() bool { return true }, func(b *sitesBuilder) {
+ b.WithTemplates("home.html", `
+{{ $a := "A" | resources.FromString "a.txt"}}
+{{ $b := "B" | resources.FromString "b.txt"}}
+{{ $c := "C" | resources.FromString "c.txt"}}
+{{ $textResources := .Resources.Match "*.txt" }}
+{{ $combined := slice $a $b $c | resources.Concat "bundle/concat.txt" }}
+T1: Content: {{ $combined.Content }}|RelPermalink: {{ $combined.RelPermalink }}|Permalink: {{ $combined.Permalink }}|MediaType: {{ $combined.MediaType.Type }}
+{{ with $textResources }}
+{{ $combinedText := . | resources.Concat "bundle/concattxt.txt" }}
+T2: Content: {{ $combinedText.Content }}|{{ $combinedText.RelPermalink }}
+{{ end }}
+{{/* https://github.com/gohugoio/hugo/issues/5269 */}}
+{{ $css := "body { color: blue; }" | resources.FromString "styles.css" }}
+{{ $minified := resources.Get "css/styles1.css" | minify }}
+{{ slice $css $minified | resources.Concat "bundle/mixed.css" }}
+`)
+ }, func(b *sitesBuilder) {
+ b.AssertFileContent("public/index.html", `T1: Content: ABC|RelPermalink: /bundle/concat.txt|Permalink: http://example.com/bundle/concat.txt|MediaType: text/plain`)
+ b.AssertFileContent("public/bundle/concat.txt", "ABC")
+
+ b.AssertFileContent("public/index.html", `T2: Content: t1t|t2t|`)
+ b.AssertFileContent("public/bundle/concattxt.txt", "t1t|t2t|")
+ }},
+ {"fromstring", func() bool { return true }, func(b *sitesBuilder) {
+ b.WithTemplates("home.html", `
+{{ $r := "Hugo Rocks!" | resources.FromString "rocks/hugo.txt" }}
+{{ $r.Content }}|{{ $r.RelPermalink }}|{{ $r.Permalink }}|{{ $r.MediaType.Type }}
+`)
+
+ }, func(b *sitesBuilder) {
+ b.AssertFileContent("public/index.html", `Hugo Rocks!|/rocks/hugo.txt|http://example.com/rocks/hugo.txt|text/plain`)
+ b.AssertFileContent("public/rocks/hugo.txt", "Hugo Rocks!")
+
+ }},
+ {"execute-as-template", func() bool {
+ // TODO(bep) eventually remove
+ return isGo111()
+ }, func(b *sitesBuilder) {
+ b.WithTemplates("home.html", `
+{{ $var := "Hugo Page" }}
+{{ if .IsHome }}
+{{ $var = "Hugo Home" }}
+{{ end }}
+T1: {{ $var }}
+{{ $result := "{{ .Kind | upper }}" | resources.FromString "mytpl.txt" | resources.ExecuteAsTemplate "result.txt" . }}
+T2: {{ $result.Content }}|{{ $result.RelPermalink}}|{{$result.MediaType.Type }}
+`)
+
+ }, func(b *sitesBuilder) {
+ b.AssertFileContent("public/index.html", `T2: HOME|/result.txt|text/plain`, `T1: Hugo Home`)
+
+ }},
+ {"fingerprint", func() bool { return true }, func(b *sitesBuilder) {
+ b.WithTemplates("home.html", `
+{{ $r := "ab" | resources.FromString "rocks/hugo.txt" }}
+{{ $result := $r | fingerprint }}
+{{ $result512 := $r | fingerprint "sha512" }}
+{{ $resultMD5 := $r | fingerprint "md5" }}
+T1: {{ $result.Content }}|{{ $result.RelPermalink}}|{{$result.MediaType.Type }}|{{ $result.Data.Integrity }}|
+T2: {{ $result512.Content }}|{{ $result512.RelPermalink}}|{{$result512.MediaType.Type }}|{{ $result512.Data.Integrity }}|
+T3: {{ $resultMD5.Content }}|{{ $resultMD5.RelPermalink}}|{{$resultMD5.MediaType.Type }}|{{ $resultMD5.Data.Integrity }}|
+{{ $r2 := "bc" | resources.FromString "rocks/hugo2.txt" | fingerprint }}
+{{/* https://github.com/gohugoio/hugo/issues/5296 */}}
+T4: {{ $r2.Data.Integrity }}|
+
+
+`)
+ }, func(b *sitesBuilder) {
+ b.AssertFileContent("public/index.html", `T1: ab|/rocks/hugo.fb8e20fc2e4c3f248c60c39bd652f3c1347298bb977b8b4d5903b85055620603.txt|text/plain|sha256-&#43;44g/C5MPySMYMOb1lLzwTRymLuXe4tNWQO4UFViBgM=|`)
+ b.AssertFileContent("public/index.html", `T2: ab|/rocks/hugo.2d408a0717ec188158278a796c689044361dc6fdde28d6f04973b80896e1823975cdbf12eb63f9e0591328ee235d80e9b5bf1aa6a44f4617ff3caf6400eb172d.txt|text/plain|sha512-LUCKBxfsGIFYJ4p5bGiQRDYdxv3eKNbwSXO4CJbhgjl1zb8S62P54FkTKO4jXYDptb8apqRPRhf/PK9kAOsXLQ==|`)
+ b.AssertFileContent("public/index.html", `T3: ab|/rocks/hugo.187ef4436122d1cc2f40dc2b92f0eba0.txt|text/plain|md5-GH70Q2Ei0cwvQNwrkvDroA==|`)
+ b.AssertFileContent("public/index.html", `T4: sha256-Hgu9bGhroFC46wP/7txk/cnYCUf86CGrvl1tyNJSxaw=|`)
+
+ }},
+ // https://github.com/gohugoio/hugo/issues/5226
+ {"baseurl-path", func() bool { return true }, func(b *sitesBuilder) {
+ b.WithSimpleConfigFileAndBaseURL("https://example.com/hugo/")
+ b.WithTemplates("home.html", `
+{{ $r1 := "ab" | resources.FromString "rocks/hugo.txt" }}
+T1: {{ $r1.Permalink }}|{{ $r1.RelPermalink }}
+`)
+ }, func(b *sitesBuilder) {
+ b.AssertFileContent("public/index.html", `T1: https://example.com/hugo/rocks/hugo.txt|/hugo/rocks/hugo.txt`)
+
+ }},
+
+ // https://github.com/gohugoio/hugo/issues/4944
+ {"Prevent resource publish on .Content only", func() bool { return true }, func(b *sitesBuilder) {
+ b.WithTemplates("home.html", `
+{{ $cssInline := "body { color: green; }" | resources.FromString "inline.css" | minify }}
+{{ $cssPublish1 := "body { color: blue; }" | resources.FromString "external1.css" | minify }}
+{{ $cssPublish2 := "body { color: orange; }" | resources.FromString "external2.css" | minify }}
+
+Inline: {{ $cssInline.Content }}
+Publish 1: {{ $cssPublish1.Content }} {{ $cssPublish1.RelPermalink }}
+Publish 2: {{ $cssPublish2.Permalink }}
+`)
+
+ }, func(b *sitesBuilder) {
+ b.AssertFileContent("public/index.html",
+ `Inline: body{color:green}`,
+ "Publish 1: body{color:blue} /external1.min.css",
+ "Publish 2: http://example.com/external2.min.css",
+ )
+ assert.True(b.CheckExists("public/external2.min.css"), "Referenced content should be copied to /public")
+ assert.True(b.CheckExists("public/external1.min.css"), "Referenced content should be copied to /public")
+
+ assert.False(b.CheckExists("public/inline.min.css"), "Inline content should not be copied to /public")
+ }},
+
+ {"unmarshal", func() bool { return true }, func(b *sitesBuilder) {
+ b.WithTemplates("home.html", `
+{{ $toml := "slogan = \"Hugo Rocks!\"" | resources.FromString "slogan.toml" | transform.Unmarshal }}
+{{ $csv1 := "\"Hugo Rocks\",\"Hugo is Fast!\"" | resources.FromString "slogans.csv" | transform.Unmarshal }}
+{{ $csv2 := "a;b;c" | transform.Unmarshal (dict "delimiter" ";") }}
+
+Slogan: {{ $toml.slogan }}
+CSV1: {{ $csv1 }} {{ len (index $csv1 0) }}
+CSV2: {{ $csv2 }}
+`)
+ }, func(b *sitesBuilder) {
+ b.AssertFileContent("public/index.html",
+ `Slogan: Hugo Rocks!`,
+ `[[Hugo Rocks Hugo is Fast!]] 2`,
+ `CSV2: [[a b c]]`,
+ )
+ }},
+
+ {"template", func() bool { return true }, func(b *sitesBuilder) {}, func(b *sitesBuilder) {
+ }},
+ }
+
+ for _, test := range tests {
+ if !test.shouldRun() {
+ t.Log("Skip", test.name)
+ continue
+ }
+
+ b := newTestSitesBuilder(t).WithLogger(loggers.NewErrorLogger())
+ b.WithSimpleConfigFile()
+ b.WithContent("_index.md", `
+---
+title: Home
+---
+
+Home.
+
+`,
+ "page1.md", `
+---
+title: Hello1
+---
+
+Hello1
+`,
+ "page2.md", `
+---
+title: Hello2
+---
+
+Hello2
+`,
+ "t1.txt", "t1t|",
+ "t2.txt", "t2t|",
+ )
+
+ b.WithSourceFile(filepath.Join("assets", "css", "styles1.css"), `
+h1 {
+ font-style: bold;
+}
+`)
+
+ b.WithSourceFile(filepath.Join("assets", "js", "script1.js"), `
+var x;
+x = 5;
+document.getElementById("demo").innerHTML = x * 10;
+`)
+
+ b.WithSourceFile(filepath.Join("assets", "mydata", "json1.json"), `
+{
+"employees":[
+ {"firstName":"John", "lastName":"Doe"},
+ {"firstName":"Anna", "lastName":"Smith"},
+ {"firstName":"Peter", "lastName":"Jones"}
+]
+}
+`)
+
+ b.WithSourceFile(filepath.Join("assets", "mydata", "svg1.svg"), `
+<svg height="100" width="100">
+ <line x1="5" y1="10" x2="20" y2="40"/>
+</svg>
+`)
+
+ b.WithSourceFile(filepath.Join("assets", "mydata", "xml1.xml"), `
+<hello>
+<world>Hugo Rocks!</<world>
+</hello>
+`)
+
+ b.WithSourceFile(filepath.Join("assets", "mydata", "html1.html"), `
+<html>
+<a href="#">
+Cool
+</a >
+</html>
+`)
+
+ b.WithSourceFile(filepath.Join("assets", "scss", "styles2.scss"), `
+$color: #333;
+
+body {
+ color: $color;
+}
+`)
+
+ b.WithSourceFile(filepath.Join("assets", "sass", "styles3.sass"), `
+$color: #333;
+
+.content-navigation
+ border-color: $color
+
+`)
+
+ t.Log("Test", test.name)
+ test.prepare(b)
+ b.Build(BuildCfg{})
+ test.verify(b)
+ }
+}
+
+func TestMultiSiteResource(t *testing.T) {
+ t.Parallel()
+ assert := require.New(t)
+
+ b := newMultiSiteTestDefaultBuilder(t)
+
+ b.CreateSites().Build(BuildCfg{})
+
+ // This build is multilingual, but not multihost. There should be only one pipes.txt
+ b.AssertFileContent("public/fr/index.html", "French Home Page", "String Resource: /blog/text/pipes.txt")
+ assert.False(b.CheckExists("public/fr/text/pipes.txt"))
+ assert.False(b.CheckExists("public/en/text/pipes.txt"))
+ b.AssertFileContent("public/en/index.html", "Default Home Page", "String Resource: /blog/text/pipes.txt")
+ b.AssertFileContent("public/text/pipes.txt", "Hugo Pipes")
+
+}
diff --git a/hugolib/robotstxt_test.go b/hugolib/robotstxt_test.go
new file mode 100644
index 000000000..e924cb8dc
--- /dev/null
+++ b/hugolib/robotstxt_test.go
@@ -0,0 +1,42 @@
+// 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"
+
+ "github.com/spf13/viper"
+)
+
+const robotTxtTemplate = `User-agent: Googlebot
+ {{ range .Data.Pages }}
+ Disallow: {{.RelPermalink}}
+ {{ end }}
+`
+
+func TestRobotsTXTOutput(t *testing.T) {
+ t.Parallel()
+
+ cfg := viper.New()
+ cfg.Set("baseURL", "http://auth/bub/")
+ cfg.Set("enableRobotsTXT", true)
+
+ b := newTestSitesBuilder(t).WithViper(cfg)
+ b.WithTemplatesAdded("layouts/robots.txt", robotTxtTemplate)
+
+ b.Build(BuildCfg{})
+
+ b.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..38f0f1eff
--- /dev/null
+++ b/hugolib/rss_test.go
@@ -0,0 +1,100 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 := "index.xml"
+
+ cfg.Set("baseURL", "http://auth/bub/")
+ cfg.Set("title", "RSSTest")
+ cfg.Set("rssLimit", rssLimit)
+
+ for _, src := range weightedSources {
+ writeSource(t, fs, filepath.Join("content", "sect", src[0]), src[1])
+ }
+
+ 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)
+ }
+
+ // Encoded summary
+ th.assertFileContent(filepath.Join("public", rssURI), "<?xml", "description", "A &lt;em&gt;custom&lt;/em&gt; summary")
+}
+
+// Before Hugo 0.49 we set the pseudo page kind RSS on the page when output to RSS.
+// This had some unintended side effects, esp. when the only output format for that page
+// was RSS.
+// For the page kinds that can have multiple output formats, the Kind should be one of the
+// standard home, page etc.
+// This test has this single purpose: Check that the Kind is that of the source page.
+// See https://github.com/gohugoio/hugo/issues/5138
+func TestRSSKind(t *testing.T) {
+ t.Parallel()
+
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile().WithTemplatesAdded("index.rss.xml", `RSS Kind: {{ .Kind }}`)
+
+ b.Build(BuildCfg{})
+
+ b.AssertFileContent("public/index.xml", "RSS Kind: home")
+}
+
+func TestRSSCanonifyURLs(t *testing.T) {
+ t.Parallel()
+
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile().WithTemplatesAdded("index.rss.xml", `<rss>{{ range .Pages }}<item>{{ .Content | html }}</item>{{ end }}</rss>`)
+ b.WithContent("page.md", `---
+Title: My Page
+---
+
+Figure:
+
+{{< figure src="/images/sunset.jpg" title="Sunset" >}}
+
+
+
+`)
+ b.Build(BuildCfg{})
+
+ b.AssertFileContent("public/index.xml", "img src=&#34;http://example.com/images/sunset.jpg")
+}
diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go
new file mode 100644
index 000000000..895d0dcf2
--- /dev/null
+++ b/hugolib/shortcode.go
@@ -0,0 +1,634 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "strconv"
+
+ "html/template"
+ "path"
+
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/pkg/errors"
+
+ "reflect"
+
+ "regexp"
+ "sort"
+
+ "github.com/gohugoio/hugo/parser/pageparser"
+ "github.com/gohugoio/hugo/resources/page"
+
+ _errors "github.com/pkg/errors"
+
+ "strings"
+ "sync"
+
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/common/text"
+ "github.com/gohugoio/hugo/common/urls"
+ "github.com/gohugoio/hugo/output"
+
+ bp "github.com/gohugoio/hugo/bufferpool"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/tpl"
+)
+
+var (
+ _ urls.RefLinker = (*ShortcodeWithPage)(nil)
+ _ pageWrapper = (*ShortcodeWithPage)(nil)
+ _ text.Positioner = (*ShortcodeWithPage)(nil)
+)
+
+// ShortcodeWithPage is the "." context in a shortcode template.
+type ShortcodeWithPage struct {
+ Params interface{}
+ Inner template.HTML
+ Page page.Page
+ Parent *ShortcodeWithPage
+ Name string
+ IsNamedParams bool
+
+ // Zero-based ordinal in relation to its parent. If the parent is the page itself,
+ // this ordinal will represent the position of this shortcode in the page content.
+ Ordinal int
+
+ // pos is the position in bytes in the source file. Used for error logging.
+ posInit sync.Once
+ posOffset int
+ pos text.Position
+
+ scratch *maps.Scratch
+}
+
+// Position returns this shortcode's detailed position. Note that this information
+// may be expensive to calculate, so only use this in error situations.
+func (scp *ShortcodeWithPage) Position() text.Position {
+ scp.posInit.Do(func() {
+ if p, ok := mustUnwrapPage(scp.Page).(pageContext); ok {
+ scp.pos = p.posOffset(scp.posOffset)
+ }
+ })
+ return scp.pos
+}
+
+// Site returns information about the current site.
+func (scp *ShortcodeWithPage) Site() page.Site {
+ return scp.Page.Site()
+}
+
+// Ref is a shortcut to the Ref method on Page. It passes itself as a context
+// to get better error messages.
+func (scp *ShortcodeWithPage) Ref(args map[string]interface{}) (string, error) {
+ return scp.Page.RefFrom(args, scp)
+}
+
+// RelRef is a shortcut to the RelRef method on Page. It passes itself as a context
+// to get better error messages.
+func (scp *ShortcodeWithPage) RelRef(args map[string]interface{}) (string, error) {
+ return scp.Page.RelRefFrom(args, scp)
+}
+
+// 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() *maps.Scratch {
+ if scp.scratch == nil {
+ scp.scratch = maps.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 {
+ // We treat this as a non error, so people can do similar to
+ // {{ $myParam := .Get "myParam" | default .Get 0 }}
+ // Without having to do additional checks.
+ return nil
+ } 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 {
+ return ""
+ }
+ 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 {
+ // We treat this as a non error, so people can do similar to
+ // {{ $myParam := .Get "myParam" | default .Get 0 }}
+ // Without having to do additional checks.
+ return nil
+ }
+ }
+
+ 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
+ }
+
+}
+
+func (scp *ShortcodeWithPage) page() page.Page {
+ return scp.Page
+}
+
+// Note - this value must not contain any markup syntax
+const shortcodePlaceholderPrefix = "HAHAHUGOSHORTCODE"
+
+func createShortcodePlaceholder(id string, ordinal int) string {
+ return shortcodePlaceholderPrefix + "-" + id + strconv.Itoa(ordinal) + "-HBHB"
+}
+
+type shortcode struct {
+ name string
+ isInline bool // inline shortcode. Any inner will be a Go template.
+ isClosing bool // whether a closing tag was provided
+ inner []interface{} // string or nested shortcode
+ params interface{} // map or array
+ ordinal int
+ err error
+
+ info tpl.Info
+
+ // If set, the rendered shortcode is sent as part of the surrounding content
+ // to Blackfriday and similar.
+ // Before Hug0 0.55 we didn't send any shortcode output to the markup
+ // renderer, and this flag told Hugo to process the {{ .Inner }} content
+ // separately.
+ // The old behaviour can be had by starting your shortcode template with:
+ // {{ $_hugo_config := `{ "version": 1 }`}}
+ doMarkup bool
+
+ // the placeholder in the source when passed to Blackfriday etc.
+ // This also identifies the rendered shortcode.
+ placeholder string
+
+ pos int // the position in bytes in the source file
+ length int // the length in bytes in the source file
+}
+
+func (s shortcode) insertPlaceholder() bool {
+ return !s.doMarkup || s.info.Config.Version == 1
+}
+
+func (s shortcode) innerString() string {
+ var sb strings.Builder
+
+ for _, inner := range s.inner {
+ sb.WriteString(inner.(string))
+ }
+
+ return sb.String()
+}
+
+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)
+}
+
+type shortcodeHandler struct {
+ p *pageState
+
+ s *Site
+
+ // Ordered list of shortcodes for a page.
+ shortcodes []*shortcode
+
+ // All the shortcode names in this set.
+ nameSet map[string]bool
+
+ // Configuration
+ enableInlineShortcodes bool
+}
+
+func newShortcodeHandler(p *pageState, s *Site, placeholderFunc func() string) *shortcodeHandler {
+
+ sh := &shortcodeHandler{
+ p: p,
+ s: s,
+ enableInlineShortcodes: s.enableInlineShortcodes,
+ shortcodes: make([]*shortcode, 0, 4),
+ nameSet: make(map[string]bool),
+ }
+
+ return sh
+}
+
+const (
+ innerNewlineRegexp = "\n"
+ innerCleanupRegexp = `\A<p>(.*)</p>\n\z`
+ innerCleanupExpand = "$1"
+)
+
+func renderShortcode(
+ level int,
+ s *Site,
+ tplVariants tpl.TemplateVariants,
+ sc *shortcode,
+ parent *ShortcodeWithPage,
+ p *pageState) (string, bool, error) {
+
+ var tmpl tpl.Template
+
+ // Tracks whether this shortcode or any of its children has template variations
+ // in other languages or output formats. We are currently only interested in
+ // the output formats, so we may get some false positives -- we
+ // should improve on that.
+ var hasVariants bool
+
+ if sc.isInline {
+ if !p.s.enableInlineShortcodes {
+ return "", false, nil
+ }
+ templName := path.Join("_inline_shortcode", p.File().Path(), sc.name)
+ if sc.isClosing {
+ templStr := sc.innerString()
+
+ var err error
+ tmpl, err = s.TextTmpl.Parse(templName, templStr)
+ if err != nil {
+ fe := herrors.ToFileError("html", err)
+ l1, l2 := p.posOffset(sc.pos).LineNumber, fe.Position().LineNumber
+ fe = herrors.ToFileErrorWithLineNumber(fe, l1+l2-1)
+ return "", false, p.wrapError(fe)
+ }
+
+ } else {
+ // Re-use of shortcode defined earlier in the same page.
+ var found bool
+ tmpl, found = s.TextTmpl.Lookup(templName)
+ if !found {
+ return "", false, _errors.Errorf("no earlier definition of shortcode %q found", sc.name)
+ }
+ }
+ } else {
+ var found, more bool
+ tmpl, found, more = s.Tmpl.LookupVariant(sc.name, tplVariants)
+ if !found {
+ s.Log.ERROR.Printf("Unable to locate template for shortcode %q in page %q", sc.name, p.File().Path())
+ return "", false, nil
+ }
+ hasVariants = hasVariants || more
+ }
+
+ data := &ShortcodeWithPage{Ordinal: sc.ordinal, posOffset: sc.pos, Params: sc.params, Page: newPageForShortcode(p), Parent: parent, Name: sc.name}
+ 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 := innerData.(type) {
+ case string:
+ inner += innerData
+ case *shortcode:
+ s, more, err := renderShortcode(level+1, s, tplVariants, innerData, data, p)
+ if err != nil {
+ return "", false, err
+ }
+ hasVariants = hasVariants || more
+ inner += s
+ default:
+ s.Log.ERROR.Printf("Illegal state on shortcode rendering of %q in page %q. Illegal type in inner data: %s ",
+ sc.name, p.File().Path(), reflect.TypeOf(innerData))
+ return "", false, nil
+ }
+ }
+
+ // Pre Hugo 0.55 this was the behaviour even for the outer-most
+ // shortcode.
+ if sc.doMarkup && (level > 0 || sc.info.Config.Version == 1) {
+ newInner := s.ContentSpec.RenderBytes(&helpers.RenderingContext{
+ Content: []byte(inner),
+ PageFmt: p.m.markup,
+ Cfg: p.Language(),
+ DocumentID: p.File().UniqueID(),
+ DocumentName: p.File().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.
+ switch p.m.markup {
+ case "", "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)
+ }
+
+ }
+
+ result, err := renderShortcodeWithPage(tmpl, data)
+
+ if err != nil && sc.isInline {
+ fe := herrors.ToFileError("html", err)
+ l1, l2 := p.posFromPage(sc.pos).LineNumber, fe.Position().LineNumber
+ fe = herrors.ToFileErrorWithLineNumber(fe, l1+l2-1)
+ return "", false, fe
+ }
+
+ return result, hasVariants, err
+}
+
+func (s *shortcodeHandler) hasShortcodes() bool {
+ return len(s.shortcodes) > 0
+}
+
+func (s *shortcodeHandler) renderShortcodesForPage(p *pageState, f output.Format) (map[string]string, bool, error) {
+
+ rendered := make(map[string]string)
+
+ tplVariants := tpl.TemplateVariants{
+ Language: p.Language().Lang,
+ OutputFormat: f,
+ }
+
+ var hasVariants bool
+
+ for _, v := range s.shortcodes {
+ s, more, err := renderShortcode(0, s.s, tplVariants, v, nil, p)
+ if err != nil {
+ err = p.parseError(_errors.Wrapf(err, "failed to render shortcode %q", v.name), p.source.parsed.Input(), v.pos)
+ return nil, false, err
+ }
+ hasVariants = hasVariants || more
+ rendered[v.placeholder] = s
+
+ }
+
+ return rendered, hasVariants, nil
+}
+
+var errShortCodeIllegalState = errors.New("Illegal shortcode state")
+
+func (s *shortcodeHandler) parseError(err error, input []byte, pos int) error {
+ if s.p != nil {
+ return s.p.parseError(err, input, pos)
+ }
+ return err
+}
+
+// pageTokens state:
+// - before: positioned just before the shortcode start
+// - after: shortcode(s) consumed (plural when they are nested)
+func (s *shortcodeHandler) extractShortcode(ordinal, level int, pt *pageparser.Iterator) (*shortcode, error) {
+ if s == nil {
+ panic("handler nil")
+ }
+ sc := &shortcode{ordinal: ordinal}
+
+ var cnt = 0
+ var nestedOrdinal = 0
+ var nextLevel = level + 1
+
+ fail := func(err error, i pageparser.Item) error {
+ return s.parseError(err, pt.Input(), i.Pos)
+ }
+
+Loop:
+ for {
+ currItem := pt.Next()
+ switch {
+ case currItem.IsLeftShortcodeDelim():
+ next := pt.Peek()
+ if next.IsShortcodeClose() {
+ continue
+ }
+
+ if cnt > 0 {
+ // nested shortcode; append it to inner content
+ pt.Backup()
+ nested, err := s.extractShortcode(nestedOrdinal, nextLevel, pt)
+ nestedOrdinal++
+ 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.IsShortcodeMarkupDelimiter()
+ }
+
+ cnt++
+
+ case currItem.IsRightShortcodeDelim():
+ // we trust the template on this:
+ // if there's no inner, we're done
+ if !sc.isInline && !sc.info.IsInner {
+ return sc, nil
+ }
+
+ case currItem.IsShortcodeClose():
+ next := pt.Peek()
+ if !sc.isInline && !sc.info.IsInner {
+ if next.IsError() {
+ // return that error, more specific
+ continue
+ }
+
+ return sc, fail(_errors.Errorf("shortcode %q has no .Inner, yet a closing tag was provided", next.Val), next)
+ }
+ if next.IsRightShortcodeDelim() {
+ // self-closing
+ pt.Consume(1)
+ } else {
+ sc.isClosing = true
+ pt.Consume(2)
+ }
+
+ return sc, nil
+ case currItem.IsText():
+ sc.inner = append(sc.inner, currItem.ValStr())
+ case currItem.IsShortcodeName():
+
+ sc.name = currItem.ValStr()
+
+ // Check if the template expects inner content.
+ // We pick the first template for an arbitrary output format
+ // if more than one. It is "all inner or no inner".
+ tmpl, found, _ := s.s.Tmpl.LookupVariant(sc.name, tpl.TemplateVariants{})
+ if !found {
+ return nil, _errors.Errorf("template for shortcode %q not found", sc.name)
+ }
+
+ sc.info = tmpl.(tpl.TemplateInfoProvider).TemplateInfo()
+ case currItem.IsInlineShortcodeName():
+ sc.name = currItem.ValStr()
+ sc.isInline = true
+ case currItem.IsShortcodeParam():
+ if !pt.IsValueNext() {
+ continue
+ } else if pt.Peek().IsShortcodeParamVal() {
+ // named params
+ if sc.params == nil {
+ params := make(map[string]string)
+ params[currItem.ValStr()] = pt.Next().ValStr()
+ sc.params = params
+ } else {
+ if params, ok := sc.params.(map[string]string); ok {
+ params[currItem.ValStr()] = pt.Next().ValStr()
+ } else {
+ return sc, errShortCodeIllegalState
+ }
+
+ }
+ } else {
+ // positional params
+ if sc.params == nil {
+ var params []string
+ params = append(params, currItem.ValStr())
+ sc.params = params
+ } else {
+ if params, ok := sc.params.([]string); ok {
+ params = append(params, currItem.ValStr())
+ sc.params = params
+ } else {
+ return sc, errShortCodeIllegalState
+ }
+
+ }
+ }
+ case currItem.IsDone():
+ // handled by caller
+ pt.Backup()
+ break Loop
+
+ }
+ }
+ return sc, nil
+}
+
+// Replace prefixed shortcode tokens with the real content.
+// Note: This function will rewrite the input slice.
+func replaceShortcodeTokens(source []byte, replacements map[string]string) ([]byte, error) {
+
+ if len(replacements) == 0 {
+ return source, nil
+ }
+
+ start := 0
+
+ pre := []byte(shortcodePlaceholderPrefix)
+ 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) < len(source) && 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 renderShortcodeWithPage(tmpl tpl.Template, data *ShortcodeWithPage) (string, error) {
+ buffer := bp.GetBuffer()
+ defer bp.PutBuffer(buffer)
+
+ err := tmpl.Execute(buffer, data)
+ if err != nil {
+ return "", _errors.Wrap(err, "failed to process shortcode")
+ }
+ return buffer.String(), nil
+}
diff --git a/hugolib/shortcode_page.go b/hugolib/shortcode_page.go
new file mode 100644
index 000000000..e8a3a37e1
--- /dev/null
+++ b/hugolib/shortcode_page.go
@@ -0,0 +1,56 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+
+ "github.com/gohugoio/hugo/resources/page"
+)
+
+var tocShortcodePlaceholder = createShortcodePlaceholder("TOC", 0)
+
+// This is sent to the shortcodes. They cannot access the content
+// they're a part of. It would cause an infinite regress.
+//
+// Go doesn't support virtual methods, so this careful dance is currently (I think)
+// the best we can do.
+type pageForShortcode struct {
+ page.PageWithoutContent
+ page.ContentProvider
+
+ // We need to replace it after we have rendered it, so provide a
+ // temporary placeholder.
+ toc template.HTML
+
+ p *pageState
+}
+
+func newPageForShortcode(p *pageState) page.Page {
+ return &pageForShortcode{
+ PageWithoutContent: p,
+ ContentProvider: page.NopPage,
+ toc: template.HTML(tocShortcodePlaceholder),
+ p: p,
+ }
+}
+
+func (p *pageForShortcode) page() page.Page {
+ return p.PageWithoutContent.(page.Page)
+}
+
+func (p *pageForShortcode) TableOfContents() template.HTML {
+ p.p.enablePlaceholders()
+ return p.toc
+}
diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go
new file mode 100644
index 000000000..42eef61ae
--- /dev/null
+++ b/hugolib/shortcode_test.go
@@ -0,0 +1,1167 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "regexp"
+
+ "reflect"
+
+ "github.com/gohugoio/hugo/parser/pageparser"
+ "github.com/gohugoio/hugo/resources/page"
+
+ "strings"
+ "testing"
+
+ "github.com/spf13/viper"
+
+ "github.com/spf13/afero"
+
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/tpl"
+ "github.com/spf13/cast"
+
+ "github.com/stretchr/testify/require"
+)
+
+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(content(h.Sites[0].RegularPages()[0]))
+ output = strings.TrimPrefix(output, "<p>")
+ output = strings.TrimSuffix(output, "</p>")
+
+ expected = strings.TrimSpace(expected)
+
+ if output != expected {
+ Fatalf(t, "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 {{ with .Get 1 }}{{ . }}{{ else }}Missing{{ end }}`)
+ return nil
+ }
+ CheckShortCodeMatch(t, "{{< video 47238zzb >}}", "Playing Video Missing", wt)
+}
+
+// #5071
+func TestShortcodeRelated(t *testing.T) {
+ t.Parallel()
+ wt := func(tem tpl.TemplateHandler) error {
+ tem.AddTemplate("_internal/shortcodes/a.html", `{{ len (.Site.RegularPages.Related .Page) }}`)
+ return nil
+ }
+
+ CheckShortCodeMatch(t, "{{< a >}}", "0", wt)
+}
+
+func TestShortcodeInnerMarkup(t *testing.T) {
+ t.Parallel()
+ wt := func(tem tpl.TemplateHandler) error {
+ tem.AddTemplate("shortcodes/a.html", `<div>{{ .Inner }}</div>`)
+ tem.AddTemplate("shortcodes/b.html", `**Bold**: <div>{{ .Inner }}</div>`)
+ return nil
+ }
+
+ CheckShortCodeMatch(t,
+ "{{< a >}}B: <div>{{% b %}}**Bold**{{% /b %}}</div>{{< /a >}}",
+ // This assertion looks odd, but is correct: for inner shortcodes with
+ // the {{% we treats the .Inner content as markup, but not the shortcode
+ // itself.
+ "<div>B: <div>**Bold**: <div><strong>Bold</strong></div></div></div>",
+ wt)
+
+ CheckShortCodeMatch(t,
+ "{{% b %}}This is **B**: {{< b >}}This is B{{< /b>}}{{% /b %}}",
+ "<strong>Bold</strong>: <div>This is <strong>B</strong>: <strong>Bold</strong>: <div>This is B</div></div>",
+ 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></div>", wt)
+}
+
+func TestIsNamedParamsSC(t *testing.T) {
+ t.Parallel()
+ wt := func(tem tpl.TemplateHandler) error {
+ tem.AddTemplate("_internal/shortcodes/bynameorposition.html", `{{ with .Get "id" }}Named: {{ . }}{{ else }}Pos: {{ .Get 0 }}{{ end }}`)
+ 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, `{{< bynameorposition id="name" >}}`, `Named: name`, wt)
+ CheckShortCodeMatch(t, `{{< bynameorposition position >}}`, `Pos: 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 {
+ // Note: In Hugo 0.55 we made it so any outer {{%'s inner content was rendered as part of the surrounding
+ // markup. This solved lots of problems, but it also meant that this test had to be adjusted.
+ tem.AddTemplate("_internal/shortcodes/wrapper.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`)
+ tem.AddTemplate("_internal/shortcodes/inside.html", `{{ .Inner }}`)
+ return nil
+ }
+ CheckShortCodeMatch(t, `{{< wrapper >}}{{% inside %}}
+# More Here
+
+[link](http://spf13.com) and text
+
+{{% /inside %}}{{< /wrapper >}}`, "<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 TestEmbeddedSC(t *testing.T) {
+ t.Parallel()
+ CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" %}}`, "<figure class=\"bananas orange\">\n <img src=\"/found/here\"/> \n</figure>", nil)
+ CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" caption="This is a caption" %}}`, "<figure class=\"bananas orange\">\n <img src=\"/found/here\"\n alt=\"This is a caption\"/> <figcaption>\n <p>This is a caption</p>\n </figcaption>\n</figure>", 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></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 TestFigureOnlySrc(t *testing.T) {
+ t.Parallel()
+ CheckShortCodeMatch(t, `{{< figure src="/found/here" >}}`, "<figure>\n <img src=\"/found/here\"/> \n</figure>", nil)
+}
+
+func TestFigureCaptionAttrWithMarkdown(t *testing.T) {
+ t.Parallel()
+ CheckShortCodeMatch(t, `{{< figure src="/found/here" caption="Something **bold** _italic_" >}}`, "<figure>\n <img src=\"/found/here\"\n alt=\"Something bold italic\"/> <figcaption>\n <p>Something <strong>bold</strong> <em>italic</em></p>\n </figcaption>\n</figure>", nil)
+ CheckShortCodeMatch(t, `{{< figure src="/found/here" attr="Something **bold** _italic_" >}}`, "<figure>\n <img src=\"/found/here\"/> <figcaption>\n <p>Something <strong>bold</strong> <em>italic</em></p>\n </figcaption>\n</figure>", nil)
+}
+
+func TestFigureImgWidth(t *testing.T) {
+ t.Parallel()
+ CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" alt="apple" width="100px" %}}`, "<figure class=\"bananas orange\">\n <img src=\"/found/here\"\n alt=\"apple\" width=\"100px\"/> \n</figure>", nil)
+}
+
+func TestFigureImgHeight(t *testing.T) {
+ t.Parallel()
+ CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" alt="apple" height="100px" %}}`, "<figure class=\"bananas orange\">\n <img src=\"/found/here\"\n alt=\"apple\" height=\"100px\"/> \n</figure>", nil)
+}
+
+func TestFigureImgWidthAndHeight(t *testing.T) {
+ t.Parallel()
+ CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" alt="apple" width="50" height="100" %}}`, "<figure class=\"bananas orange\">\n <img src=\"/found/here\"\n alt=\"apple\" width=\"50\" height=\"100\"/> \n</figure>", nil)
+}
+
+func TestFigureLinkNoTarget(t *testing.T) {
+ t.Parallel()
+ CheckShortCodeMatch(t, `{{< figure src="/found/here" link="/jump/here/on/clicking" >}}`, "<figure><a href=\"/jump/here/on/clicking\">\n <img src=\"/found/here\"/> </a>\n</figure>", nil)
+}
+
+func TestFigureLinkWithTarget(t *testing.T) {
+ t.Parallel()
+ CheckShortCodeMatch(t, `{{< figure src="/found/here" link="/jump/here/on/clicking" target="_self" >}}`, "<figure><a href=\"/jump/here/on/clicking\" target=\"_self\">\n <img src=\"/found/here\"/> </a>\n</figure>", nil)
+}
+
+func TestFigureLinkWithTargetAndRel(t *testing.T) {
+ t.Parallel()
+ CheckShortCodeMatch(t, `{{< figure src="/found/here" link="/jump/here/on/clicking" target="_blank" rel="noopener" >}}`, "<figure><a href=\"/jump/here/on/clicking\" target=\"_blank\" rel=\"noopener\">\n <img src=\"/found/here\"/> </a>\n</figure>", nil)
+}
+
+// #1642
+func TestShortcodeWrappedInPIssue(t *testing.T) {
+ t.Parallel()
+ wt := func(tem tpl.TemplateHandler) error {
+ tem.AddTemplate("_internal/shortcodes/bug.html", `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`)
+ return nil
+ }
+ CheckShortCodeMatch(t, `
+{{< bug >}}
+
+{{< bug >}}
+`, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", wt)
+}
+
+func TestExtractShortcodes(t *testing.T) {
+ t.Parallel()
+ b := newTestSitesBuilder(t).WithSimpleConfigFile()
+
+ b.WithTemplates(
+ "default/single.html", `EMPTY`,
+ "_internal/shortcodes/tag.html", `tag`,
+ "_internal/shortcodes/legacytag.html", `{{ $_hugo_config := "{ \"version\": 1 }" }}tag`,
+ "_internal/shortcodes/sc1.html", `sc1`,
+ "_internal/shortcodes/sc2.html", `sc2`,
+ "_internal/shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`,
+ "_internal/shortcodes/inner2.html", `{{.Inner}}`,
+ "_internal/shortcodes/inner3.html", `{{.Inner}}`,
+ ).WithContent("page.md", `---
+title: "Shortcodes Galore!"
+---
+`)
+
+ b.CreateSites().Build(BuildCfg{})
+
+ s := b.H.Sites[0]
+
+ /*errCheck := func(s string) func(name string, assert *require.Assertions, shortcode *shortcode, err error) {
+ return func(name string, assert *require.Assertions, shortcode *shortcode, err error) {
+ assert.Error(err, name)
+ assert.Equal(s, err.Error(), name)
+ }
+ }*/
+
+ // Make it more regexp friendly
+ strReplacer := strings.NewReplacer("[", "{", "]", "}")
+
+ str := func(s *shortcode) string {
+ if s == nil {
+ return "<nil>"
+ }
+ return strReplacer.Replace(fmt.Sprintf("%s;inline:%t;closing:%t;inner:%v;params:%v;ordinal:%d;markup:%t;version:%d;pos:%d",
+ s.name, s.isInline, s.isClosing, s.inner, s.params, s.ordinal, s.doMarkup, s.info.Config.Version, s.pos))
+ }
+
+ regexpCheck := func(re string) func(assert *require.Assertions, shortcode *shortcode, err error) {
+ return func(assert *require.Assertions, shortcode *shortcode, err error) {
+ assert.NoError(err)
+ got := str(shortcode)
+ assert.Regexp(regexp.MustCompile(re), got, got)
+ }
+ }
+
+ for _, test := range []struct {
+ name string
+ input string
+ check func(assert *require.Assertions, shortcode *shortcode, err error)
+ }{
+ {"one shortcode, no markup", "{{< tag >}}", regexpCheck("tag.*closing:false.*markup:false")},
+ {"one shortcode, markup", "{{% tag %}}", regexpCheck("tag.*closing:false.*markup:true;version:2")},
+ {"one shortcode, markup, legacy", "{{% legacytag %}}", regexpCheck("tag.*closing:false.*markup:true;version:1")},
+ {"outer shortcode markup", "{{% inner %}}{{< tag >}}{{% /inner %}}", regexpCheck("inner.*closing:true.*markup:true")},
+ {"inner shortcode markup", "{{< inner >}}{{% tag %}}{{< /inner >}}", regexpCheck("inner.*closing:true.*;markup:false;version:2")},
+ {"one pos param", "{{% tag param1 %}}", regexpCheck("tag.*params:{param1}")},
+ {"two pos params", "{{< tag param1 param2>}}", regexpCheck("tag.*params:{param1 param2}")},
+ {"one named param", `{{% tag param1="value" %}}`, regexpCheck("tag.*params:map{param1:value}")},
+ {"two named params", `{{< tag param1="value1" param2="value2" >}}`, regexpCheck("tag.*params:map{param\\d:value\\d param\\d:value\\d}")},
+ {"inner", `{{< inner >}}Inner Content{{< / inner >}}`, regexpCheck("inner;inline:false;closing:true;inner:{Inner Content};")},
+ // issue #934
+ {"inner self-closing", `{{< inner />}}`, regexpCheck("inner;.*inner:{}")},
+ {"nested inner", `{{< inner >}}Inner Content->{{% inner2 param1 %}}inner2txt{{% /inner2 %}}Inner close->{{< / inner >}}`,
+ regexpCheck("inner;.*inner:{Inner Content->.*Inner close->}")},
+ {"nested, nested inner", `{{< inner >}}inner2->{{% inner2 param1 %}}inner2txt->inner3{{< inner3>}}inner3txt{{</ inner3 >}}{{% /inner2 %}}final close->{{< / inner >}}`,
+ regexpCheck("inner:{inner2-> inner2.*{{inner2txt->inner3.*final close->}")},
+ {"closed without content", `{{< inner param1 >}}{{< / inner >}}`, regexpCheck("inner.*inner:{}")},
+ {"inline", `{{< my.inline >}}Hi{{< /my.inline >}}`, regexpCheck("my.inline;inline:true;closing:true;inner:{Hi};")},
+ } {
+
+ t.Run(test.name, func(t *testing.T) {
+ assert := require.New(t)
+
+ counter := 0
+ placeholderFunc := func() string {
+ counter++
+ return fmt.Sprintf("HAHA%s-%dHBHB", shortcodePlaceholderPrefix, counter)
+ }
+
+ p, err := pageparser.ParseMain(strings.NewReader(test.input), pageparser.Config{})
+ assert.NoError(err)
+ handler := newShortcodeHandler(nil, s, placeholderFunc)
+ iter := p.Iterator()
+
+ short, err := handler.extractShortcode(0, 0, iter)
+
+ test.check(assert, short, err)
+
+ })
+ }
+
+}
+
+func TestShortcodesInSite(t *testing.T) {
+ t.Parallel()
+ baseURL := "http://foo/bar"
+
+ tests := []struct {
+ contentPath string
+ content string
+ outFile string
+ expected interface{}
+ }{
+ {"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"), `-hbhb">b</h1>`},
+ // #2223 pygments
+ {"sect/doc6.md", "\n```bash\nb = {{< b >}} c = {{% c %}}\n```\n",
+ filepath.FromSlash("public/sect/doc6/index.html"),
+ `<span class="nv">b</span>`},
+ // #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"},
+ {"sect/doc12.md", `---
+title: "Foo"
+---
+
+{{% html-indented-v1 %}}`,
+ "public/sect/doc12/index.html",
+ "<h1>Hugo!</h1>"},
+ }
+
+ sources := make([][2]string, len(tests))
+
+ for i, test := range tests {
+ sources[i] = [2]string{filepath.FromSlash(test.contentPath), test.content}
+ }
+
+ addTemplates := func(templ tpl.TemplateHandler) error {
+ templ.AddTemplate("_default/single.html", "{{.Content}} Word Count: {{ .WordCount }}")
+
+ 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/html-indented-v1.html", "{{ $_hugo_config := `{ \"version\": 1 }` }}"+`
+ <h1>Hugo!</h1>
+`)
+ 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{})
+
+ for i, test := range tests {
+ t.Run(fmt.Sprintf("test=%d;contentPath=%s", i, test.contentPath), func(t *testing.T) {
+ if strings.HasSuffix(test.contentPath, ".ad") && !helpers.HasAsciidoc() {
+ t.Skip("Skip Asciidoc test case as no Asciidoc present.")
+ } else if strings.HasSuffix(test.contentPath, ".rst") && !helpers.HasRst() {
+ t.Skip("Skip Rst test case as no rst2html present.")
+ }
+
+ th := testHelper{s.Cfg, s.Fs, t}
+
+ expected := cast.ToStringSlice(test.expected)
+ th.assertFileContent(filepath.FromSlash(test.outFile), 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 >}}
+`
+
+ 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"))
+
+ err := h.Build(BuildCfg{})
+ require.NoError(t, err)
+ require.Len(t, h.Sites, 1)
+
+ s := h.Sites[0]
+ home := s.getPage(page.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",
+ )
+
+}
+
+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, 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 HAHAHUGOSHORTCODE-1HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "Hello World."},
+ {"Hello HAHAHUGOSHORTCODE-1@}@.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, false},
+ {"HAHAHUGOSHORTCODE2-1HBHB", "PREFIX2", map[string]string{"HAHAHUGOSHORTCODE2-1HBHB": "World"}, "World"},
+ {"Hello World!", "PREFIX2", map[string]string{}, "Hello World!"},
+ {"!HAHAHUGOSHORTCODE-1HBHB", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "!World"},
+ {"HAHAHUGOSHORTCODE-1HBHB!", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "World!"},
+ {"!HAHAHUGOSHORTCODE-1HBHB!", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "!World!"},
+ {"_{_PREFIX-1HBHB", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "_{_PREFIX-1HBHB"},
+ {"Hello HAHAHUGOSHORTCODE-1HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-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 HAHAHUGOSHORTCODE-1HBHB asdf HAHAHUGOSHORTCODE-2HBHB.", "A", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "v1", "HAHAHUGOSHORTCODE-2HBHB": "v2"}, "A v1 asdf v2."},
+ {"Hello HAHAHUGOSHORTCODE2-1HBHB. Go HAHAHUGOSHORTCODE2-2HBHB, Go, Go HAHAHUGOSHORTCODE2-3HBHB Go Go!.", "PREFIX2", map[string]string{"HAHAHUGOSHORTCODE2-1HBHB": "Europe", "HAHAHUGOSHORTCODE2-2HBHB": "Jonny", "HAHAHUGOSHORTCODE2-3HBHB": "Johnny"}, "Hello Europe. Go Jonny, Go, Go Johnny Go Go!."},
+ {"A HAHAHUGOSHORTCODE-2HBHB HAHAHUGOSHORTCODE-1HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B"}, "A B A."},
+ {"A HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-2", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A"}, false},
+ {"A HAHAHUGOSHORTCODE-1HBHB but not the second.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B"}, "A A but not the second."},
+ {"An HAHAHUGOSHORTCODE-1HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B"}, "An A."},
+ {"An HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-2HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B"}, "An A B."},
+ {"A HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-2HBHB HAHAHUGOSHORTCODE-3HBHB HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-3HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B", "HAHAHUGOSHORTCODE-3HBHB": "C"}, "A A B C A C."},
+ {"A HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-2HBHB HAHAHUGOSHORTCODE-3HBHB HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-3HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B", "HAHAHUGOSHORTCODE-3HBHB": "C"}, "A A B C A C."},
+ // Issue #1148 remove p-tags 10 =>
+ {"Hello <p>HAHAHUGOSHORTCODE-1HBHB</p>. END.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "Hello World. END."},
+ {"Hello <p>HAHAHUGOSHORTCODE-1HBHB</p>. <p>HAHAHUGOSHORTCODE-2HBHB</p> END.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World", "HAHAHUGOSHORTCODE-2HBHB": "THE"}, "Hello World. THE END."},
+ {"Hello <p>HAHAHUGOSHORTCODE-1HBHB. END</p>.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "Hello <p>World. END</p>."},
+ {"<p>Hello HAHAHUGOSHORTCODE-1HBHB</p>. END.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "<p>Hello World</p>. END."},
+ {"Hello <p>HAHAHUGOSHORTCODE-1HBHB12", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "Hello <p>World12"},
+ {"Hello HAHAHUGOSHORTCODE-1HBHB. HAHAHUGOSHORTCODE-1HBHB-HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-1HBHB END", "P", map[string]string{"HAHAHUGOSHORTCODE-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.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 TestShortcodeGetContent(t *testing.T) {
+ t.Parallel()
+ assert := require.New(t)
+
+ contentShortcode := `
+{{- $t := .Get 0 -}}
+{{- $p := .Get 1 -}}
+{{- $k := .Get 2 -}}
+{{- $page := $.Page.Site.GetPage "page" $p -}}
+{{ if $page }}
+{{- if eq $t "bundle" -}}
+{{- .Scratch.Set "p" ($page.Resources.GetMatch (printf "%s*" $k)) -}}
+{{- else -}}
+{{- $.Scratch.Set "p" $page -}}
+{{- end -}}P1:{{ .Page.Content }}|P2:{{ $p := ($.Scratch.Get "p") }}{{ $p.Title }}/{{ $p.Content }}|
+{{- else -}}
+{{- errorf "Page %s is nil" $p -}}
+{{- end -}}
+`
+
+ var templates []string
+ var content []string
+
+ contentWithShortcodeTemplate := `---
+title: doc%s
+weight: %d
+---
+Logo:{{< c "bundle" "b1" "logo.png" >}}:P1: {{< c "page" "section1/p1" "" >}}:BP1:{{< c "bundle" "b1" "bp1" >}}`
+
+ simpleContentTemplate := `---
+title: doc%s
+weight: %d
+---
+C-%s`
+
+ v := viper.New()
+
+ v.Set("timeout", 500)
+
+ templates = append(templates, []string{"shortcodes/c.html", contentShortcode}...)
+ templates = append(templates, []string{"_default/single.html", "Single Content: {{ .Content }}"}...)
+ templates = append(templates, []string{"_default/list.html", "List Content: {{ .Content }}"}...)
+
+ content = append(content, []string{"b1/index.md", fmt.Sprintf(contentWithShortcodeTemplate, "b1", 1)}...)
+ content = append(content, []string{"b1/logo.png", "PNG logo"}...)
+ content = append(content, []string{"b1/bp1.md", fmt.Sprintf(simpleContentTemplate, "bp1", 1, "bp1")}...)
+
+ content = append(content, []string{"section1/_index.md", fmt.Sprintf(contentWithShortcodeTemplate, "s1", 2)}...)
+ content = append(content, []string{"section1/p1.md", fmt.Sprintf(simpleContentTemplate, "s1p1", 2, "s1p1")}...)
+
+ content = append(content, []string{"section2/_index.md", fmt.Sprintf(simpleContentTemplate, "b1", 1, "b1")}...)
+ content = append(content, []string{"section2/s2p1.md", fmt.Sprintf(contentWithShortcodeTemplate, "bp1", 1)}...)
+
+ builder := newTestSitesBuilder(t).WithDefaultMultiSiteConfig()
+
+ builder.WithViper(v).WithContent(content...).WithTemplates(templates...).CreateSites().Build(BuildCfg{})
+ s := builder.H.Sites[0]
+ assert.Equal(3, len(s.RegularPages()))
+
+ builder.AssertFileContent("public/section1/index.html",
+ "List Content: <p>Logo:P1:|P2:logo.png/PNG logo|:P1: P1:|P2:docs1p1/<p>C-s1p1</p>\n|",
+ "BP1:P1:|P2:docbp1/<p>C-bp1</p>",
+ )
+
+ builder.AssertFileContent("public/b1/index.html",
+ "Single Content: <p>Logo:P1:|P2:logo.png/PNG logo|:P1: P1:|P2:docs1p1/<p>C-s1p1</p>\n|",
+ "P2:docbp1/<p>C-bp1</p>",
+ )
+
+ builder.AssertFileContent("public/section2/s2p1/index.html",
+ "Single Content: <p>Logo:P1:|P2:logo.png/PNG logo|:P1: P1:|P2:docs1p1/<p>C-s1p1</p>\n|",
+ "P2:docbp1/<p>C-bp1</p>",
+ )
+
+}
+
+// https://github.com/gohugoio/hugo/issues/5833
+func TestShortcodeParentResourcesOnRebuild(t *testing.T) {
+ t.Parallel()
+
+ b := newTestSitesBuilder(t).Running().WithSimpleConfigFile()
+ b.WithTemplatesAdded(
+ "index.html", `
+{{ $b := .Site.GetPage "b1" }}
+b1 Content: {{ $b.Content }}
+{{$p := $b.Resources.GetMatch "p1*" }}
+Content: {{ $p.Content }}
+{{ $article := .Site.GetPage "blog/article" }}
+Article Content: {{ $article.Content }}
+`,
+ "shortcodes/c.html", `
+{{ range .Page.Parent.Resources }}
+* Parent resource: {{ .Name }}: {{ .RelPermalink }}
+{{ end }}
+`)
+
+ pageContent := `
+---
+title: MyPage
+---
+
+SHORTCODE: {{< c >}}
+
+`
+
+ b.WithContent("b1/index.md", pageContent,
+ "b1/logo.png", "PNG logo",
+ "b1/p1.md", pageContent,
+ "blog/_index.md", pageContent,
+ "blog/logo-article.png", "PNG logo",
+ "blog/article.md", pageContent,
+ )
+
+ b.Build(BuildCfg{})
+
+ assert := func(matchers ...string) {
+ allMatchers := append(matchers, "Parent resource: logo.png: /b1/logo.png",
+ "Article Content: <p>SHORTCODE: \n\n* Parent resource: logo-article.png: /blog/logo-article.png",
+ )
+
+ b.AssertFileContent("public/index.html",
+ allMatchers...,
+ )
+ }
+
+ assert()
+
+ b.EditFiles("content/b1/index.md", pageContent+" Edit.")
+
+ b.Build(BuildCfg{})
+
+ assert("Edit.")
+
+}
+
+func TestShortcodePreserveOrder(t *testing.T) {
+ t.Parallel()
+ assert := require.New(t)
+
+ contentTemplate := `---
+title: doc%d
+weight: %d
+---
+# doc
+
+{{< s1 >}}{{< s2 >}}{{< s3 >}}{{< s4 >}}{{< s5 >}}
+
+{{< nested >}}
+{{< ordinal >}} {{< scratch >}}
+{{< ordinal >}} {{< scratch >}}
+{{< ordinal >}} {{< scratch >}}
+{{< /nested >}}
+
+`
+
+ ordinalShortcodeTemplate := `ordinal: {{ .Ordinal }}{{ .Page.Scratch.Set "ordinal" .Ordinal }}`
+
+ nestedShortcode := `outer ordinal: {{ .Ordinal }} inner: {{ .Inner }}`
+ scratchGetShortcode := `scratch ordinal: {{ .Ordinal }} scratch get ordinal: {{ .Page.Scratch.Get "ordinal" }}`
+ shortcodeTemplate := `v%d: {{ .Ordinal }} sgo: {{ .Page.Scratch.Get "o2" }}{{ .Page.Scratch.Set "o2" .Ordinal }}|`
+
+ var shortcodes []string
+ var content []string
+
+ shortcodes = append(shortcodes, []string{"shortcodes/nested.html", nestedShortcode}...)
+ shortcodes = append(shortcodes, []string{"shortcodes/ordinal.html", ordinalShortcodeTemplate}...)
+ shortcodes = append(shortcodes, []string{"shortcodes/scratch.html", scratchGetShortcode}...)
+
+ for i := 1; i <= 5; i++ {
+ sc := fmt.Sprintf(shortcodeTemplate, i)
+ sc = strings.Replace(sc, "%%", "%", -1)
+ shortcodes = append(shortcodes, []string{fmt.Sprintf("shortcodes/s%d.html", i), sc}...)
+ }
+
+ for i := 1; i <= 3; i++ {
+ content = append(content, []string{fmt.Sprintf("p%d.md", i), fmt.Sprintf(contentTemplate, i, i)}...)
+ }
+
+ builder := newTestSitesBuilder(t).WithDefaultMultiSiteConfig()
+
+ builder.WithContent(content...).WithTemplatesAdded(shortcodes...).CreateSites().Build(BuildCfg{})
+
+ s := builder.H.Sites[0]
+ assert.Equal(3, len(s.RegularPages()))
+
+ builder.AssertFileContent("public/en/p1/index.html", `v1: 0 sgo: |v2: 1 sgo: 0|v3: 2 sgo: 1|v4: 3 sgo: 2|v5: 4 sgo: 3`)
+ builder.AssertFileContent("public/en/p1/index.html", `outer ordinal: 5 inner:
+ordinal: 0 scratch ordinal: 1 scratch get ordinal: 0
+ordinal: 2 scratch ordinal: 3 scratch get ordinal: 2
+ordinal: 4 scratch ordinal: 5 scratch get ordinal: 4`)
+
+}
+
+func TestShortcodeVariables(t *testing.T) {
+ t.Parallel()
+ assert := require.New(t)
+
+ builder := newTestSitesBuilder(t).WithSimpleConfigFile()
+
+ builder.WithContent("page.md", `---
+title: "Hugo Rocks!"
+---
+
+# doc
+
+ {{< s1 >}}
+
+`).WithTemplatesAdded("layouts/shortcodes/s1.html", `
+Name: {{ .Name }}
+{{ with .Position }}
+File: {{ .Filename }}
+Offset: {{ .Offset }}
+Line: {{ .LineNumber }}
+Column: {{ .ColumnNumber }}
+String: {{ . | safeHTML }}
+{{ end }}
+
+`).CreateSites().Build(BuildCfg{})
+
+ s := builder.H.Sites[0]
+ assert.Equal(1, len(s.RegularPages()))
+
+ builder.AssertFileContent("public/page/index.html",
+ filepath.FromSlash("File: content/page.md"),
+ "Line: 7", "Column: 4", "Offset: 40",
+ filepath.FromSlash("String: \"content/page.md:7:4\""),
+ "Name: s1",
+ )
+
+}
+
+func TestInlineShortcodes(t *testing.T) {
+ for _, enableInlineShortcodes := range []bool{true, false} {
+ t.Run(fmt.Sprintf("enableInlineShortcodes=%t", enableInlineShortcodes),
+ func(t *testing.T) {
+ conf := fmt.Sprintf(`
+baseURL = "https://example.com"
+enableInlineShortcodes = %t
+`, enableInlineShortcodes)
+
+ b := newTestSitesBuilder(t)
+ b.WithConfigFile("toml", conf)
+
+ shortcodeContent := `FIRST:{{< myshort.inline "first" >}}
+Page: {{ .Page.Title }}
+Seq: {{ seq 3 }}
+Param: {{ .Get 0 }}
+{{< /myshort.inline >}}:END:
+
+SECOND:{{< myshort.inline "second" />}}:END
+NEW INLINE: {{< n1.inline "5" >}}W1: {{ seq (.Get 0) }}{{< /n1.inline >}}:END:
+INLINE IN INNER: {{< outer >}}{{< n2.inline >}}W2: {{ seq 4 }}{{< /n2.inline >}}{{< /outer >}}:END:
+REUSED INLINE IN INNER: {{< outer >}}{{< n1.inline "3" />}}{{< /outer >}}:END:
+`
+
+ b.WithContent("page-md-shortcode.md", `---
+title: "Hugo"
+---
+`+shortcodeContent)
+
+ b.WithContent("_index.md", `---
+title: "Hugo Home"
+---
+
+`+shortcodeContent)
+
+ b.WithTemplatesAdded("layouts/_default/single.html", `
+CONTENT:{{ .Content }}
+`)
+
+ b.WithTemplatesAdded("layouts/index.html", `
+CONTENT:{{ .Content }}
+`)
+
+ b.WithTemplatesAdded("layouts/shortcodes/outer.html", `Inner: {{ .Inner }}`)
+
+ b.CreateSites().Build(BuildCfg{})
+
+ shouldContain := []string{
+ "Seq: [1 2 3]",
+ "Param: first",
+ "Param: second",
+ "NEW INLINE: W1: [1 2 3 4 5]",
+ "INLINE IN INNER: Inner: W2: [1 2 3 4]",
+ "REUSED INLINE IN INNER: Inner: W1: [1 2 3]",
+ }
+
+ if enableInlineShortcodes {
+ b.AssertFileContent("public/page-md-shortcode/index.html",
+ shouldContain...,
+ )
+ b.AssertFileContent("public/index.html",
+ shouldContain...,
+ )
+ } else {
+ b.AssertFileContent("public/page-md-shortcode/index.html",
+ "FIRST::END",
+ "SECOND::END",
+ "NEW INLINE: :END",
+ "INLINE IN INNER: Inner: :END:",
+ "REUSED INLINE IN INNER: Inner: :END:",
+ )
+ }
+ })
+
+ }
+}
+
+// https://github.com/gohugoio/hugo/issues/5863
+func TestShortcodeNamespaced(t *testing.T) {
+ t.Parallel()
+ assert := require.New(t)
+
+ builder := newTestSitesBuilder(t).WithSimpleConfigFile()
+
+ builder.WithContent("page.md", `---
+title: "Hugo Rocks!"
+---
+
+# doc
+
+ hello: {{< hello >}}
+ test/hello: {{< test/hello >}}
+
+`).WithTemplatesAdded(
+ "layouts/shortcodes/hello.html", `hello`,
+ "layouts/shortcodes/test/hello.html", `test/hello`).CreateSites().Build(BuildCfg{})
+
+ s := builder.H.Sites[0]
+ assert.Equal(1, len(s.RegularPages()))
+
+ builder.AssertFileContent("public/page/index.html",
+ "hello: hello",
+ "test/hello: test/hello",
+ )
+}
diff --git a/hugolib/site.go b/hugolib/site.go
new file mode 100644
index 000000000..7cc80e22f
--- /dev/null
+++ b/hugolib/site.go
@@ -0,0 +1,1902 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "context"
+ "fmt"
+ "html/template"
+ "io"
+ "log"
+ "mime"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/gohugoio/hugo/common/maps"
+
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/common/text"
+
+ "github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/publisher"
+ _errors "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/langs"
+
+ "github.com/gohugoio/hugo/resources/page"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/lazy"
+ "golang.org/x/sync/errgroup"
+
+ "github.com/gohugoio/hugo/media"
+
+ "github.com/fsnotify/fsnotify"
+ bp "github.com/gohugoio/hugo/bufferpool"
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/navigation"
+ "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/related"
+ "github.com/gohugoio/hugo/resources"
+ "github.com/gohugoio/hugo/resources/page/pagemeta"
+ "github.com/gohugoio/hugo/resources/resource"
+ "github.com/gohugoio/hugo/source"
+ "github.com/gohugoio/hugo/tpl"
+
+ "github.com/spf13/afero"
+ "github.com/spf13/cast"
+ "github.com/spf13/viper"
+)
+
+// 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 {
+
+ // The owning container. When multiple languages, there will be multiple
+ // sites.
+ h *HugoSites
+
+ *PageCollections
+
+ Taxonomies TaxonomyList
+
+ taxonomyNodes *taxonomyNodeInfos
+
+ Sections Taxonomy
+ Info SiteInfo
+
+ layoutHandler *output.LayoutHandler
+
+ buildStats *buildStats
+
+ language *langs.Language
+
+ siteCfg siteConfigHolder
+
+ disabledKinds map[string]bool
+
+ enableInlineShortcodes 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
+
+ siteConfigConfig SiteConfig
+
+ // How to handle page front matter.
+ frontmatterHandler pagemeta.FrontMatterHandler
+
+ // 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:"-"`
+
+ // The func used to title case titles.
+ titleFunc func(s string) string
+
+ relatedDocsHandler *page.RelatedDocsHandler
+ siteRefLinker
+
+ publisher publisher.Publisher
+
+ menus navigation.Menus
+
+ // Shortcut to the home page. Note that this may be nil if
+ // home page, for some odd reason, is disabled.
+ home *pageState
+
+ // The last modification date of this site.
+ lastmod time.Time
+
+ // Lazily loaded site dependencies
+ init *siteInit
+}
+
+type siteConfigHolder struct {
+ sitemap config.Sitemap
+ taxonomiesConfig map[string]string
+ timeout time.Duration
+ hasCJKLanguage bool
+ enableEmoji bool
+}
+
+// Lazily loaded site dependencies.
+type siteInit struct {
+ prevNext *lazy.Init
+ prevNextInSection *lazy.Init
+ menus *lazy.Init
+}
+
+func (init *siteInit) Reset() {
+ init.prevNext.Reset()
+ init.prevNextInSection.Reset()
+ init.menus.Reset()
+}
+
+func (s *Site) initInit(init *lazy.Init, pctx pageContext) {
+ _, err := init.Do()
+ if err != nil {
+ s.h.FatalError(pctx.wrapError(err))
+ }
+}
+
+func (s *Site) prepareInits() {
+ s.init = &siteInit{}
+
+ var init lazy.Init
+
+ s.init.prevNext = init.Branch(func() (interface{}, error) {
+ regularPages := s.findWorkPagesByKind(page.KindPage)
+ for i, p := range regularPages {
+ if p.posNextPrev == nil {
+ continue
+ }
+ p.posNextPrev.nextPage = nil
+ p.posNextPrev.prevPage = nil
+
+ if i > 0 {
+ p.posNextPrev.nextPage = regularPages[i-1]
+ }
+
+ if i < len(regularPages)-1 {
+ p.posNextPrev.prevPage = regularPages[i+1]
+ }
+ }
+ return nil, nil
+ })
+
+ s.init.prevNextInSection = init.Branch(func() (interface{}, error) {
+ var rootSection []int
+ for i, p1 := range s.workAllPages {
+ if p1.IsPage() && p1.Section() == "" {
+ rootSection = append(rootSection, i)
+ }
+ if p1.IsSection() {
+ sectionPages := p1.Pages()
+ for i, p2 := range sectionPages {
+ p2s := p2.(*pageState)
+ if p2s.posNextPrevSection == nil {
+ continue
+ }
+
+ p2s.posNextPrevSection.nextPage = nil
+ p2s.posNextPrevSection.prevPage = nil
+
+ if i > 0 {
+ p2s.posNextPrevSection.nextPage = sectionPages[i-1]
+ }
+
+ if i < len(sectionPages)-1 {
+ p2s.posNextPrevSection.prevPage = sectionPages[i+1]
+ }
+ }
+ }
+ }
+
+ for i, j := range rootSection {
+ p := s.workAllPages[j]
+ if i > 0 {
+ p.posNextPrevSection.nextPage = s.workAllPages[rootSection[i-1]]
+ }
+
+ if i < len(rootSection)-1 {
+ p.posNextPrevSection.prevPage = s.workAllPages[rootSection[i+1]]
+ }
+ }
+
+ return nil, nil
+ })
+
+ s.init.menus = init.Branch(func() (interface{}, error) {
+ s.assembleMenus()
+ return nil, nil
+ })
+
+}
+
+// Build stats for a given site.
+type buildStats struct {
+ draftCount int
+ futureCount int
+ expiredCount int
+}
+
+// TODO(bep) consolidate all site stats into this
+func (b *buildStats) update(p page.Page) {
+ if p.Draft() {
+ b.draftCount++
+ }
+
+ if resource.IsFuture(p) {
+ b.futureCount++
+ }
+
+ if resource.IsExpired(p) {
+ b.expiredCount++
+ }
+}
+
+type siteRenderingContext struct {
+ output.Format
+}
+
+func (s *Site) Menus() navigation.Menus {
+ s.init.menus.Do()
+ return s.menus
+}
+
+func (s *Site) initRenderFormats() {
+ formatSet := make(map[string]bool)
+ formats := output.Formats{}
+ for _, p := range s.workAllPages {
+ for _, f := range p.m.configuredOutputFormats {
+ if !formatSet[f.Name] {
+ formats = append(formats, f)
+ formatSet[f.Name] = true
+ }
+ }
+ }
+
+ // Add the per kind configured output formats
+ for _, kind := range allKindsInPages {
+ if siteFormats, found := s.outputFormats[kind]; found {
+ for _, f := range siteFormats {
+ if !formatSet[f.Name] {
+ formats = append(formats, f)
+ formatSet[f.Name] = true
+ }
+ }
+ }
+ }
+
+ sort.Sort(formats)
+ s.renderFormats = formats
+}
+
+func (s *Site) GetRelatedDocsHandler() *page.RelatedDocsHandler {
+ return s.relatedDocsHandler
+}
+
+func (s *Site) Language() *langs.Language {
+ return s.language
+}
+
+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(),
+ disabledKinds: s.disabledKinds,
+ titleFunc: s.titleFunc,
+ relatedDocsHandler: s.relatedDocsHandler.Clone(),
+ siteRefLinker: s.siteRefLinker,
+ outputFormats: s.outputFormats,
+ rc: s.rc,
+ outputFormatsConfig: s.outputFormatsConfig,
+ frontmatterHandler: s.frontmatterHandler,
+ mediaTypesConfig: s.mediaTypesConfig,
+ language: s.language,
+ h: s.h,
+ publisher: s.publisher,
+ siteConfigConfig: s.siteConfigConfig,
+ enableInlineShortcodes: s.enableInlineShortcodes,
+ buildStats: &buildStats{},
+ init: s.init,
+ PageCollections: newPageCollections(),
+ siteCfg: s.siteCfg,
+ }
+
+}
+
+// newSite creates a new site with the given configuration.
+func newSite(cfg deps.DepsCfg) (*Site, error) {
+ c := newPageCollections()
+
+ if cfg.Language == nil {
+ cfg.Language = langs.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
+ }
+
+ taxonomies := cfg.Language.GetStringMapString("taxonomies")
+
+ var relatedContentConfig related.Config
+
+ if cfg.Language.IsSet("related") {
+ relatedContentConfig, err = related.DecodeConfig(cfg.Language.Get("related"))
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ relatedContentConfig = related.DefaultConfig
+ if _, found := taxonomies["tag"]; found {
+ relatedContentConfig.Add(related.IndexConfig{Name: "tags", Weight: 80})
+ }
+ }
+
+ titleFunc := helpers.GetTitleFunc(cfg.Language.GetString("titleCaseStyle"))
+
+ frontMatterHandler, err := pagemeta.NewFrontmatterHandler(cfg.Logger, cfg.Cfg)
+ if err != nil {
+ return nil, err
+ }
+
+ siteConfig := siteConfigHolder{
+ sitemap: config.DecodeSitemap(config.Sitemap{Priority: -1, Filename: "sitemap.xml"}, cfg.Language.GetStringMap("sitemap")),
+ taxonomiesConfig: taxonomies,
+ timeout: time.Duration(cfg.Language.GetInt("timeout")) * time.Millisecond,
+ hasCJKLanguage: cfg.Language.GetBool("hasCJKLanguage"),
+ enableEmoji: cfg.Language.Cfg.GetBool("enableEmoji"),
+ }
+
+ s := &Site{
+ PageCollections: c,
+ layoutHandler: output.NewLayoutHandler(),
+ language: cfg.Language,
+ disabledKinds: disabledKinds,
+ titleFunc: titleFunc,
+ relatedDocsHandler: page.NewRelatedDocsHandler(relatedContentConfig),
+ outputFormats: outputFormats,
+ rc: &siteRenderingContext{output.HTMLFormat},
+ outputFormatsConfig: siteOutputFormatsConfig,
+ mediaTypesConfig: siteMediaTypesConfig,
+ frontmatterHandler: frontMatterHandler,
+ buildStats: &buildStats{},
+ enableInlineShortcodes: cfg.Language.GetBool("enableInlineShortcodes"),
+ siteCfg: siteConfig,
+ }
+
+ s.prepareInits()
+
+ 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 = applyDeps(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.
+// TODO(bep) test refactor -- remove
+func NewSiteDefaultLang(withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) {
+ v := viper.New()
+ if err := loadDefaultSettingsFor(v); err != nil {
+ return nil, err
+ }
+ return newSiteForLang(langs.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.
+// TODO(bep) test refactor -- remove
+func NewEnglishSite(withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) {
+ v := viper.New()
+ if err := loadDefaultSettingsFor(v); err != nil {
+ return nil, err
+ }
+ return newSiteForLang(langs.NewLanguage("en", v), withTemplate...)
+}
+
+// newSiteForLang creates a new site in the given language.
+func newSiteForLang(lang *langs.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, 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) {
+ h, err := NewHugoSites(cfg)
+ if err != nil {
+ return nil, err
+ }
+ return h.Sites[0], nil
+
+}
+
+type SiteInfo struct {
+ Authors page.AuthorList
+ Social SiteSocial
+
+ hugoInfo hugo.Info
+ title string
+ RSSLink string
+ Author map[string]interface{}
+ LanguageCode string
+ Copyright string
+
+ permalinks map[string]string
+
+ LanguagePrefix string
+ Languages langs.Languages
+
+ BuildDrafts bool
+
+ canonifyURLs bool
+ relativeURLs bool
+ uglyURLs func(p page.Page) bool
+
+ owner *HugoSites
+ s *Site
+ language *langs.Language
+ defaultContentLanguageInSubdir bool
+ sectionPagesMenu string
+}
+
+func (s *SiteInfo) Pages() page.Pages {
+ return s.s.Pages()
+
+}
+
+func (s *SiteInfo) RegularPages() page.Pages {
+ return s.s.RegularPages()
+
+}
+
+func (s *SiteInfo) AllPages() page.Pages {
+ return s.s.AllPages()
+}
+
+func (s *SiteInfo) AllRegularPages() page.Pages {
+ return s.s.AllRegularPages()
+}
+
+func (s *SiteInfo) Permalinks() map[string]string {
+ // Remove in 0.57
+ helpers.Deprecated("Site", ".Permalinks", "", false)
+ return s.permalinks
+}
+
+func (s *SiteInfo) LastChange() time.Time {
+ return s.s.lastmod
+}
+
+func (s *SiteInfo) Title() string {
+ return s.title
+}
+
+func (s *SiteInfo) Site() page.Site {
+ return s
+}
+
+func (s *SiteInfo) Menus() navigation.Menus {
+ return s.s.Menus()
+}
+
+// TODO(bep) type
+func (s *SiteInfo) Taxonomies() interface{} {
+ return s.s.Taxonomies
+}
+
+func (s *SiteInfo) Params() map[string]interface{} {
+ return s.s.Language().Params()
+}
+
+func (s *SiteInfo) Data() map[string]interface{} {
+ return s.s.h.Data()
+}
+
+func (s *SiteInfo) Language() *langs.Language {
+ return s.language
+}
+
+func (s *SiteInfo) Config() SiteConfig {
+ return s.s.siteConfigConfig
+}
+
+func (s *SiteInfo) Hugo() hugo.Info {
+ return s.hugoInfo
+}
+
+// Sites is a convenience method to get all the Hugo sites/languages configured.
+func (s *SiteInfo) Sites() page.Sites {
+ return s.s.h.siteInfos()
+}
+
+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())
+}
+
+// ServerPort returns the port part of the BaseURL, 0 if none found.
+func (s *SiteInfo) ServerPort() int {
+ ps := s.s.PathSpec.BaseURL.URL().Port()
+ if ps == "" {
+ return 0
+ }
+ p, err := strconv.Atoi(ps)
+ if err != nil {
+ return 0
+ }
+ return p
+}
+
+// GoogleAnalytics is kept here for historic reasons.
+func (s *SiteInfo) GoogleAnalytics() string {
+ return s.Config().Services.GoogleAnalytics.ID
+
+}
+
+// DisqusShortname is kept here for historic reasons.
+func (s *SiteInfo) DisqusShortname() string {
+ return s.Config().Services.Disqus.Shortname
+}
+
+// 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) IsServer() bool {
+ return s.owner.running
+}
+
+type siteRefLinker struct {
+ s *Site
+
+ errorLogger *log.Logger
+ notFoundURL string
+}
+
+func newSiteRefLinker(cfg config.Provider, s *Site) (siteRefLinker, error) {
+ logger := s.Log.ERROR
+
+ notFoundURL := cfg.GetString("refLinksNotFoundURL")
+ errLevel := cfg.GetString("refLinksErrorLevel")
+ if strings.EqualFold(errLevel, "warning") {
+ logger = s.Log.WARN
+ }
+ return siteRefLinker{s: s, errorLogger: logger, notFoundURL: notFoundURL}, nil
+}
+
+func (s siteRefLinker) logNotFound(ref, what string, p page.Page, position text.Position) {
+ if position.IsValid() {
+ s.errorLogger.Printf("[%s] REF_NOT_FOUND: Ref %q: %s: %s", s.s.Lang(), ref, position.String(), what)
+ } else if p == nil {
+ s.errorLogger.Printf("[%s] REF_NOT_FOUND: Ref %q: %s", s.s.Lang(), ref, what)
+ } else {
+ s.errorLogger.Printf("[%s] REF_NOT_FOUND: Ref %q from page %q: %s", s.s.Lang(), ref, p.Path(), what)
+ }
+}
+
+func (s *siteRefLinker) refLink(ref string, source interface{}, relative bool, outputFormat string) (string, error) {
+
+ p, err := unwrapPage(source)
+ if err != nil {
+ return "", err
+ }
+
+ var refURL *url.URL
+
+ ref = filepath.ToSlash(ref)
+
+ refURL, err = url.Parse(ref)
+
+ if err != nil {
+ return s.notFoundURL, err
+ }
+
+ var target page.Page
+ var link string
+
+ if refURL.Path != "" {
+ target, err := s.s.getPageNew(p, refURL.Path)
+ var pos text.Position
+ if err != nil || target == nil {
+ if p, ok := source.(text.Positioner); ok {
+ pos = p.Position()
+
+ }
+ }
+
+ if err != nil {
+ s.logNotFound(refURL.Path, err.Error(), p, pos)
+ return s.notFoundURL, nil
+ }
+
+ if target == nil {
+ s.logNotFound(refURL.Path, "page not found", p, pos)
+ return s.notFoundURL, nil
+ }
+
+ var permalinker Permalinker = target
+
+ if outputFormat != "" {
+ o := target.OutputFormats().Get(outputFormat)
+
+ if o == nil {
+ s.logNotFound(refURL.Path, fmt.Sprintf("output format %q", outputFormat), p, pos)
+ return s.notFoundURL, nil
+ }
+ permalinker = o
+ }
+
+ if relative {
+ link = permalinker.RelPermalink()
+ } else {
+ link = permalinker.Permalink()
+ }
+ }
+
+ if refURL.Fragment != "" {
+ _ = target
+ link = link + "#" + refURL.Fragment
+ if pctx, ok := target.(pageContext); ok && !target.File().IsZero() && !pctx.getRenderingConfig().PlainIDAnchors {
+ if refURL.Path != "" {
+ link = link + ":" + target.File().UniqueID()
+ }
+ } else if pctx, ok := p.(pageContext); ok && !p.File().IsZero() && !pctx.getRenderingConfig().PlainIDAnchors {
+ link = link + ":" + p.File().UniqueID()
+ }
+
+ }
+ return link, nil
+}
+
+// Ref will give an absolute URL to ref in the given Page.
+func (s *SiteInfo) Ref(ref string, page page.Page, options ...string) (string, error) {
+ // Remove in Hugo 0.54
+ helpers.Deprecated("Site", ".Ref", "Use .Site.GetPage", true)
+ outputFormat := ""
+ if len(options) > 0 {
+ outputFormat = options[0]
+ }
+
+ return s.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.Page, options ...string) (string, error) {
+ // Remove in Hugo 0.54
+ helpers.Deprecated("Site", ".RelRef", "Use .Site.GetPage", true)
+ outputFormat := ""
+ if len(options) > 0 {
+ outputFormat = options[0]
+ }
+
+ return s.s.refLink(ref, page, true, outputFormat)
+}
+
+func (s *Site) running() bool {
+ return s.h != nil && s.h.running
+}
+
+func (s *Site) multilingual() *Multilingual {
+ return s.h.multilingual
+}
+
+type whatChanged struct {
+ source bool
+ other bool
+ files map[string]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 {
+ for _, suffix := range mt.Suffixes {
+ _ = mime.AddExtensionType(mt.Delimiter+suffix, mt.Type()+"; charset=utf-8")
+ }
+ }
+}
+
+func (s *Site) filterFileEvents(events []fsnotify.Event) []fsnotify.Event {
+ var filtered []fsnotify.Event
+ 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.SourceSpec.IgnoreFile(ev.Name) {
+ continue
+ }
+
+ // Throw away any directories
+ isRegular, err := s.SourceSpec.IsRegularSourceFile(ev.Name)
+ if err != nil && os.IsNotExist(err) && (ev.Op&fsnotify.Remove == fsnotify.Remove || ev.Op&fsnotify.Rename == fsnotify.Rename) {
+ // Force keep of event
+ isRegular = true
+ }
+ if !isRegular {
+ continue
+ }
+
+ filtered = append(filtered, ev)
+ }
+
+ return filtered
+}
+
+func (s *Site) translateFileEvents(events []fsnotify.Event) []fsnotify.Event {
+ var filtered []fsnotify.Event
+
+ eventMap := make(map[string][]fsnotify.Event)
+
+ // We often get a Remove etc. followed by a Create, a Create followed by a Write.
+ // Remove the superflous events to mage the update logic simpler.
+ for _, ev := range events {
+ eventMap[ev.Name] = append(eventMap[ev.Name], ev)
+ }
+
+ for _, ev := range events {
+ mapped := eventMap[ev.Name]
+
+ // Keep one
+ found := false
+ var kept fsnotify.Event
+ for i, ev2 := range mapped {
+ if i == 0 {
+ kept = ev2
+ }
+
+ if ev2.Op&fsnotify.Write == fsnotify.Write {
+ kept = ev2
+ found = true
+ }
+
+ if !found && ev2.Op&fsnotify.Create == fsnotify.Create {
+ kept = ev2
+ }
+ }
+
+ filtered = append(filtered, kept)
+ }
+
+ return filtered
+}
+
+// reBuild partially rebuilds a site given the filesystem events.
+// It returns whetever the content source was changed.
+// TODO(bep) clean up/rewrite this method.
+func (s *Site) processPartial(events []fsnotify.Event) (whatChanged, error) {
+
+ events = s.filterFileEvents(events)
+ events = s.translateFileEvents(events)
+
+ s.Log.DEBUG.Printf("Rebuild for events %q", events)
+
+ h := s.h
+
+ // First we need to determine what changed
+
+ var (
+ sourceChanged = []fsnotify.Event{}
+ sourceReallyChanged = []fsnotify.Event{}
+ contentFilesChanged []string
+ tmplChanged = []fsnotify.Event{}
+ dataChanged = []fsnotify.Event{}
+ i18nChanged = []fsnotify.Event{}
+ shortcodesChanged = make(map[string]bool)
+ sourceFilesChanged = make(map[string]bool)
+
+ // prevent spamming the log on changes
+ logger = helpers.NewDistinctFeedbackLogger()
+ )
+
+ cachePartitions := make([]string, len(events))
+
+ for i, ev := range events {
+ cachePartitions[i] = resources.ResourceKeyPartition(ev.Name)
+
+ 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") {
+ 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)
+ }
+ }
+
+ // These in memory resource caches will be rebuilt on demand.
+ for _, s := range s.h.Sites {
+ s.ResourceSpec.ResourceCache.DeletePartitions(cachePartitions...)
+ }
+
+ if len(tmplChanged) > 0 || len(i18nChanged) > 0 {
+ sites := s.h.Sites
+ first := sites[0]
+
+ s.h.init.Reset()
+
+ // TOD(bep) globals clean
+ if err := first.Deps.LoadResources(); err != nil {
+ return whatChanged{}, err
+ }
+
+ for i := 1; i < len(sites); i++ {
+ site := sites[i]
+ var err error
+ depsCfg := deps.DepsCfg{
+ Language: site.language,
+ MediaTypes: site.mediaTypesConfig,
+ OutputFormats: site.outputFormatsConfig,
+ }
+ site.Deps, err = first.Deps.ForLanguage(depsCfg, func(d *deps.Deps) error {
+ d.Site = &site.Info
+ return nil
+ })
+ if err != nil {
+ return whatChanged{}, err
+ }
+ }
+ }
+
+ if len(dataChanged) > 0 {
+ s.h.init.data.Reset()
+ }
+
+ for _, ev := range sourceChanged {
+ removed := false
+
+ if ev.Op&fsnotify.Remove == fsnotify.Remove {
+ removed = true
+ }
+
+ // 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 {
+ removed = true
+ }
+ }
+ if removed && IsContentFile(ev.Name) {
+ h.removePageByFilename(ev.Name)
+ }
+
+ sourceReallyChanged = append(sourceReallyChanged, ev)
+ sourceFilesChanged[ev.Name] = true
+ }
+
+ 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 := h.findPagesByShortcode(shortcode)
+ for _, p := range pagesWithShortcode {
+ contentFilesChanged = append(contentFilesChanged, p.File().Filename())
+ }
+ }
+
+ if len(sourceReallyChanged) > 0 || len(contentFilesChanged) > 0 {
+ var filenamesChanged []string
+ for _, e := range sourceReallyChanged {
+ filenamesChanged = append(filenamesChanged, e.Name)
+ }
+ if len(contentFilesChanged) > 0 {
+ filenamesChanged = append(filenamesChanged, contentFilesChanged...)
+ }
+
+ filenamesChanged = helpers.UniqueStrings(filenamesChanged)
+
+ if err := s.readAndProcessContent(filenamesChanged...); err != nil {
+ return whatChanged{}, err
+ }
+
+ }
+
+ changed := whatChanged{
+ source: len(sourceChanged) > 0 || len(shortcodesChanged) > 0,
+ other: len(tmplChanged) > 0 || len(i18nChanged) > 0 || len(dataChanged) > 0,
+ files: sourceFilesChanged,
+ }
+
+ return changed, nil
+
+}
+
+func (s *Site) process(config BuildCfg) (err error) {
+ if err = s.initialize(); err != nil {
+ return
+ }
+ if err := s.readAndProcessContent(); err != nil {
+ return err
+ }
+ return err
+
+}
+
+func (s *Site) setupSitePages() {
+ var homeDates *resource.Dates
+ if s.home != nil {
+ // If the home page has no dates set, we fall back to the site dates.
+ homeDates = &s.home.m.Dates
+ }
+
+ if !s.lastmod.IsZero() && (homeDates == nil || !resource.IsZeroDates(homeDates)) {
+ return
+ }
+
+ if homeDates != nil && !s.lastmod.IsZero() {
+ homeDates.FDate = s.lastmod
+ homeDates.FLastmod = s.lastmod
+ return
+
+ }
+
+ var siteLastmod time.Time
+ var siteLastDate time.Time
+
+ for _, page := range s.workAllPages {
+ if !page.IsPage() {
+ continue
+ }
+ // 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.
+ // TODO(bep) check if this can be done earlier
+ if page.Lastmod().After(siteLastmod) {
+ siteLastmod = page.Lastmod()
+ }
+ if page.Date().After(siteLastDate) {
+ siteLastDate = page.Date()
+ }
+ }
+
+ s.lastmod = siteLastmod
+
+ if homeDates != nil && resource.IsZeroDates(homeDates) {
+ homeDates.FDate = siteLastDate
+ homeDates.FLastmod = s.lastmod
+ }
+
+}
+
+func (s *Site) render(ctx *siteRenderContext) (err error) {
+
+ if err := page.Clear(); err != nil {
+ return err
+ }
+
+ if ctx.outIdx == 0 {
+ // 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
+ }
+ }
+
+ }
+
+ if err = s.renderPages(ctx); err != nil {
+ return
+ }
+
+ if ctx.outIdx == 0 {
+ if err = s.renderSitemap(); err != nil {
+ return
+ }
+
+ if err = s.renderRobotsTXT(); err != nil {
+ return
+ }
+
+ if err = s.render404(); err != nil {
+ return
+ }
+ }
+
+ if !ctx.renderSingletonPages() {
+ return
+ }
+
+ if err = s.renderMainLanguageRedirect(); err != nil {
+ return
+ }
+
+ return
+}
+
+func (s *Site) Initialise() (err error) {
+ return s.initialize()
+}
+
+func (s *Site) initialize() (err error) {
+ return s.initializeSiteInfo()
+}
+
+// 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 {
+ p := s.HomeAbsURL()
+ if !strings.HasSuffix(p, "/") {
+ p += "/"
+ }
+ p += s.s.siteCfg.sitemap.Filename
+ return p
+}
+
+func (s *Site) initializeSiteInfo() error {
+ var (
+ lang = s.language
+ languages langs.Languages
+ )
+
+ if s.h != nil && s.h.multilingual != nil {
+ languages = s.h.multilingual.Languages
+ }
+
+ permalinks := s.Cfg.GetStringMapString("permalinks")
+
+ defaultContentInSubDir := s.Cfg.GetBool("defaultContentLanguageInSubdir")
+ defaultContentLanguage := s.Cfg.GetString("defaultContentLanguage")
+
+ languagePrefix := ""
+ if s.multilingualEnabled() && (defaultContentInSubDir || lang.Lang != defaultContentLanguage) {
+ languagePrefix = "/" + lang.Lang
+ }
+
+ var uglyURLs = func(p page.Page) bool {
+ return false
+ }
+
+ v := s.Cfg.Get("uglyURLs")
+ if v != nil {
+ switch vv := v.(type) {
+ case bool:
+ uglyURLs = func(p page.Page) bool {
+ return vv
+ }
+ case string:
+ // Is what be get from CLI (--uglyURLs)
+ vvv := cast.ToBool(vv)
+ uglyURLs = func(p page.Page) bool {
+ return vvv
+ }
+ default:
+ m := cast.ToStringMapBool(v)
+ uglyURLs = func(p page.Page) bool {
+ return m[p.Section()]
+ }
+ }
+ }
+
+ s.Info = SiteInfo{
+ title: lang.GetString("title"),
+ Author: lang.GetStringMap("author"),
+ Social: lang.GetStringMapString("social"),
+ LanguageCode: lang.GetString("languageCode"),
+ Copyright: lang.GetString("copyright"),
+ language: lang,
+ LanguagePrefix: languagePrefix,
+ Languages: languages,
+ defaultContentLanguageInSubdir: defaultContentInSubDir,
+ sectionPagesMenu: lang.GetString("sectionPagesMenu"),
+ BuildDrafts: s.Cfg.GetBool("buildDrafts"),
+ canonifyURLs: s.Cfg.GetBool("canonifyURLs"),
+ relativeURLs: s.Cfg.GetBool("relativeURLs"),
+ uglyURLs: uglyURLs,
+ permalinks: permalinks,
+ owner: s.h,
+ s: s,
+ hugoInfo: hugo.NewInfo(s.Cfg.GetString("environment")),
+ }
+
+ rssOutputFormat, found := s.outputFormats[page.KindHome].GetByName(output.RSSFormat.Name)
+
+ if found {
+ s.Info.RSSLink = s.permalink(rssOutputFormat.BaseFilename())
+ }
+
+ return nil
+}
+
+func (s *Site) isI18nEvent(e fsnotify.Event) bool {
+ return s.BaseFs.SourceFilesystems.IsI18n(e.Name)
+}
+
+func (s *Site) isDataDirEvent(e fsnotify.Event) bool {
+ return s.BaseFs.SourceFilesystems.IsData(e.Name)
+}
+
+func (s *Site) isLayoutDirEvent(e fsnotify.Event) bool {
+ return s.BaseFs.SourceFilesystems.IsLayout(e.Name)
+}
+
+func (s *Site) isContentDirEvent(e fsnotify.Event) bool {
+ return s.BaseFs.IsContent(e.Name)
+}
+
+type contentCaptureResultHandler struct {
+ defaultContentProcessor *siteContentProcessor
+ contentProcessors map[string]*siteContentProcessor
+}
+
+func (c *contentCaptureResultHandler) getContentProcessor(lang string) *siteContentProcessor {
+ proc, found := c.contentProcessors[lang]
+ if found {
+ return proc
+ }
+ return c.defaultContentProcessor
+}
+
+func (c *contentCaptureResultHandler) handleSingles(fis ...*fileInfo) {
+ for _, fi := range fis {
+ proc := c.getContentProcessor(fi.Lang())
+ proc.processSingle(fi)
+ }
+}
+func (c *contentCaptureResultHandler) handleBundles(d *bundleDirs) {
+ for _, b := range d.bundles {
+ proc := c.getContentProcessor(b.fi.Lang())
+ proc.processBundle(b)
+ }
+}
+
+func (c *contentCaptureResultHandler) handleCopyFile(f pathLangFile) {
+ proc := c.getContentProcessor(f.Lang())
+ proc.processAsset(f)
+}
+
+func (s *Site) readAndProcessContent(filenames ...string) error {
+
+ ctx := context.Background()
+ g, ctx := errgroup.WithContext(ctx)
+
+ defaultContentLanguage := s.SourceSpec.DefaultContentLanguage
+
+ contentProcessors := make(map[string]*siteContentProcessor)
+ var defaultContentProcessor *siteContentProcessor
+ sites := s.h.langSite()
+ for k, v := range sites {
+ if v.language.Disabled {
+ continue
+ }
+ proc := newSiteContentProcessor(ctx, len(filenames) > 0, v)
+ contentProcessors[k] = proc
+ if k == defaultContentLanguage {
+ defaultContentProcessor = proc
+ }
+ g.Go(func() error {
+ return proc.process(ctx)
+ })
+ }
+
+ var (
+ handler captureResultHandler
+ bundleMap *contentChangeMap
+ )
+
+ mainHandler := &contentCaptureResultHandler{contentProcessors: contentProcessors, defaultContentProcessor: defaultContentProcessor}
+
+ sourceSpec := source.NewSourceSpec(s.PathSpec, s.BaseFs.Content.Fs)
+
+ if s.running() {
+ // Need to track changes.
+ bundleMap = s.h.ContentChanges
+ handler = &captureResultHandlerChain{handlers: []captureBundlesHandler{mainHandler, bundleMap}}
+
+ } else {
+ handler = mainHandler
+ }
+
+ c := newCapturer(s.Log, sourceSpec, handler, bundleMap, filenames...)
+
+ err1 := c.capture()
+
+ for _, proc := range contentProcessors {
+ proc.closeInput()
+ }
+
+ err2 := g.Wait()
+
+ if err1 != nil {
+ return err1
+ }
+ return err2
+}
+
+func (s *Site) getMenusFromConfig() navigation.Menus {
+
+ ret := navigation.Menus{}
+
+ if menus := s.language.GetStringMap("menus"); 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 := navigation.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)
+ // TODO(bep) clean up all of this
+ menuEntry.ConfiguredURL = s.Info.createNodeMenuEntryURL(menuEntry.ConfiguredURL)
+
+ if ret[name] == nil {
+ ret[name] = navigation.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 = make(navigation.Menus)
+
+ type twoD struct {
+ MenuName, EntryName string
+ }
+ flat := map[twoD]*navigation.MenuEntry{}
+ children := map[twoD]navigation.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
+
+ if sectionPagesMenu != "" {
+ for _, p := range s.workAllPages {
+ if p.Kind() == page.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 := navigation.MenuEntry{Identifier: id,
+ Name: p.LinkTitle(),
+ Weight: p.Weight(),
+ Page: p}
+ flat[twoD{sectionPagesMenu, me.KeyName()}] = &me
+ }
+ }
+ }
+
+ // Add menu entries provided by pages
+ for _, p := range s.workAllPages {
+ for name, me := range p.pageMenus.menus() {
+ if _, ok := flat[twoD{name, me.KeyName()}]; ok {
+ s.SendError(p.wrapError(errors.Errorf("duplicate menu entry with identifier %q in menu %q", me.KeyName(), name)))
+ 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}] = &navigation.MenuEntry{Name: p.EntryName}
+ }
+ 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] = navigation.Menu{}
+ }
+ s.menus[menu.MenuName] = s.menus[menu.MenuName].Add(e)
+ }
+ }
+}
+
+// get any lanaguagecode to prefix the target file path with.
+func (s *Site) getLanguageTargetPathLang(alwaysInSubDir bool) string {
+ if s.h.IsMultihost() {
+ return s.Language().Lang
+ }
+
+ return s.getLanguagePermalinkLang(alwaysInSubDir)
+}
+
+// get any lanaguagecode to prefix the relative permalink with.
+func (s *Site) getLanguagePermalinkLang(alwaysInSubDir bool) string {
+
+ if !s.Info.IsMultiLingual() || s.h.IsMultihost() {
+ return ""
+ }
+
+ if alwaysInSubDir {
+ return s.Language().Lang
+ }
+
+ isDefault := s.Language().Lang == s.multilingual().DefaultLang.Lang
+
+ if !isDefault || s.Info.defaultContentLanguageInSubdir {
+ return s.Language().Lang
+ }
+
+ return ""
+}
+
+func (s *Site) getTaxonomyKey(key string) string {
+ if s.PathSpec.DisablePathToLower {
+ return s.PathSpec.MakePath(key)
+ }
+ return strings.ToLower(s.PathSpec.MakePath(key))
+}
+
+func (s *Site) assembleTaxonomies() error {
+ s.Taxonomies = make(TaxonomyList)
+ taxonomies := s.siteCfg.taxonomiesConfig
+ for _, plural := range taxonomies {
+ s.Taxonomies[plural] = make(Taxonomy)
+ }
+
+ s.taxonomyNodes = &taxonomyNodeInfos{
+ m: make(map[string]*taxonomyNodeInfo),
+ getKey: s.getTaxonomyKey,
+ }
+
+ s.Log.INFO.Printf("found taxonomies: %#v\n", taxonomies)
+
+ for singular, plural := range taxonomies {
+ parent := s.taxonomyNodes.GetOrCreate(plural, "")
+ parent.singular = singular
+
+ addTaxonomy := func(plural, term string, weight int, p page.Page) {
+ key := s.getTaxonomyKey(term)
+
+ n := s.taxonomyNodes.GetOrCreate(plural, term)
+ n.parent = parent
+
+ w := page.NewWeightedPage(weight, p, n.owner)
+
+ s.Taxonomies[plural].add(key, w)
+
+ n.UpdateFromPage(w.Page)
+ parent.UpdateFromPage(w.Page)
+ }
+
+ for _, p := range s.workAllPages {
+ vals := getParam(p, plural, false)
+
+ w := getParamToLower(p, plural+"_weight")
+ weight, err := cast.ToIntE(w)
+ if err != nil {
+ s.Log.ERROR.Printf("Unable to convert taxonomy weight %#v to int for %q", w, p.pathOrTitle())
+ // weight will equal zero, so let the flow continue
+ }
+
+ if vals != nil {
+ if v, ok := vals.([]string); ok {
+ for _, idx := range v {
+ addTaxonomy(plural, idx, weight, p)
+ }
+ } else if v, ok := vals.(string); ok {
+ addTaxonomy(plural, v, weight, p)
+ } else {
+ s.Log.ERROR.Printf("Invalid %s in %q\n", plural, p.pathOrTitle())
+ }
+ }
+ }
+
+ for k := range s.Taxonomies[plural] {
+ s.Taxonomies[plural][k].Sort()
+ }
+ }
+
+ return nil
+}
+
+// Prepare site for a new full build.
+func (s *Site) resetBuildState() {
+ s.relatedDocsHandler = s.relatedDocsHandler.Clone()
+ s.PageCollections = newPageCollectionsFromPages(s.rawAllPages)
+ s.buildStats = &buildStats{}
+ s.init.Reset()
+
+ for _, p := range s.rawAllPages {
+ p.pagePages = &pagePages{}
+ p.subSections = page.Pages{}
+ p.parent = nil
+ p.Scratcher = maps.NewScratcher()
+ }
+}
+
+func (s *Site) errorCollator(results <-chan error, errs chan<- error) {
+ var errors []error
+ for e := range results {
+ errors = append(errors, e)
+ }
+
+ errs <- s.h.pickOneAndLogTheRest(errors)
+
+ close(errs)
+}
+
+// GetPage looks up a page of a given type for the given ref.
+// In Hugo <= 0.44 you had to add Page Kind (section, home) etc. as the first
+// argument and then either a unix styled path (with or without a leading slash))
+// or path elements separated.
+// When we now remove the Kind from this API, we need to make the transition as painless
+// as possible for existing sites. Most sites will use {{ .Site.GetPage "section" "my/section" }},
+// i.e. 2 arguments, so we test for that.
+func (s *SiteInfo) GetPage(ref ...string) (page.Page, error) {
+ p, err := s.s.getPageOldVersion(ref...)
+
+ if p == nil {
+ // The nil struct has meaning in some situations, mostly to avoid breaking
+ // existing sites doing $nilpage.IsDescendant($p), which will always return
+ // false.
+ p = page.NilPage
+ }
+
+ return p, err
+}
+
+func (s *Site) permalink(link string) string {
+ return s.PathSpec.PermalinkForBaseURL(link, s.PathSpec.BaseURL.String())
+
+}
+
+func (s *Site) renderAndWriteXML(statCounter *uint64, name string, targetPath string, d interface{}, layouts ...string) error {
+ s.Log.DEBUG.Printf("Render XML for %q to %q", name, targetPath)
+ renderBuffer := bp.GetBuffer()
+ defer bp.PutBuffer(renderBuffer)
+
+ if err := s.renderForLayouts(name, "", d, renderBuffer, layouts...); err != nil {
+ return err
+ }
+
+ var path string
+ if s.Info.relativeURLs {
+ path = helpers.GetDottedRelativePath(targetPath)
+ } else {
+ s := s.PathSpec.BaseURL.String()
+ if !strings.HasSuffix(s, "/") {
+ s += "/"
+ }
+ path = s
+ }
+
+ pd := publisher.Descriptor{
+ Src: renderBuffer,
+ TargetPath: targetPath,
+ StatCounter: statCounter,
+ // For the minification part of XML,
+ // we currently only use the MIME type.
+ OutputFormat: output.RSSFormat,
+ AbsURLPath: path,
+ }
+
+ return s.publisher.Publish(pd)
+
+}
+
+func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, layouts ...string) error {
+ renderBuffer := bp.GetBuffer()
+ defer bp.PutBuffer(renderBuffer)
+
+ of := p.outputFormat()
+
+ if err := s.renderForLayouts(p.Kind(), of.Name, p, renderBuffer, layouts...); err != nil {
+ return err
+ }
+
+ if renderBuffer.Len() == 0 {
+ return nil
+ }
+
+ isHTML := of.IsHTML
+ isRSS := of.Name == "RSS"
+
+ var path string
+
+ if s.Info.relativeURLs {
+ path = helpers.GetDottedRelativePath(targetPath)
+ } else if isRSS || s.Info.canonifyURLs {
+ url := s.PathSpec.BaseURL.String()
+ if !strings.HasSuffix(url, "/") {
+ url += "/"
+ }
+ path = url
+ }
+
+ pd := publisher.Descriptor{
+ Src: renderBuffer,
+ TargetPath: targetPath,
+ StatCounter: statCounter,
+ OutputFormat: p.outputFormat(),
+ }
+
+ if isRSS {
+ // Always canonify URLs in RSS
+ pd.AbsURLPath = path
+ } else if isHTML {
+ if s.Info.relativeURLs || s.Info.canonifyURLs {
+ pd.AbsURLPath = path
+ }
+
+ if s.running() && s.Cfg.GetBool("watch") && !s.Cfg.GetBool("disableLiveReload") {
+ pd.LiveReloadPort = s.Cfg.GetInt("liveReloadPort")
+ }
+
+ // For performance reasons we only inject the Hugo generator tag on the home page.
+ if p.IsHome() {
+ pd.AddHugoGeneratorTag = !s.Cfg.GetBool("disableHugoGeneratorInject")
+ }
+
+ }
+
+ return s.publisher.Publish(pd)
+}
+
+var infoOnMissingLayout = map[string]bool{
+ // The 404 layout is very much optional in Hugo, but we do look for it.
+ "404": true,
+}
+
+func (s *Site) renderForLayouts(name, outputFormat string, d interface{}, w io.Writer, layouts ...string) (err error) {
+ templ := s.findFirstTemplate(layouts...)
+ if templ == nil {
+ log := s.Log.WARN
+ if infoOnMissingLayout[name] {
+ log = s.Log.INFO
+ }
+
+ errMsg := "You should create a template file which matches Hugo Layouts Lookup Rules for this combination."
+ var args []interface{}
+ msg := "found no layout file for"
+ if outputFormat != "" {
+ msg += " %q"
+ args = append(args, outputFormat)
+ }
+ if name != "" {
+ msg += " for %q"
+ args = append(args, name)
+ }
+
+ msg += ": " + errMsg
+
+ log.Printf(msg, args...)
+
+ return nil
+ }
+
+ if err = templ.Execute(w, d); err != nil {
+ return _errors.Wrapf(err, "render of %q failed", name)
+ }
+ return
+}
+
+func (s *Site) findFirstTemplate(layouts ...string) tpl.Template {
+ for _, layout := range layouts {
+ if templ, found := s.Tmpl.Lookup(layout); found {
+ return templ
+ }
+ }
+ return nil
+}
+
+func (s *Site) publish(statCounter *uint64, path string, r io.Reader) (err error) {
+ s.PathSpec.ProcessingStats.Incr(statCounter)
+
+ return helpers.WriteToDisk(filepath.Clean(path), r, s.BaseFs.PublishFs)
+}
+
+func (s *Site) kindFromFileInfoOrSections(fi *fileInfo, sections []string) string {
+ if fi.TranslationBaseName() == "_index" {
+ if fi.Dir() == "" {
+ return page.KindHome
+ }
+
+ return s.kindFromSections(sections)
+
+ }
+ return page.KindPage
+}
+
+func (s *Site) kindFromSections(sections []string) string {
+ if len(sections) == 0 || len(s.siteCfg.taxonomiesConfig) == 0 {
+ return page.KindSection
+ }
+
+ sectionPath := path.Join(sections...)
+
+ for _, plural := range s.siteCfg.taxonomiesConfig {
+ if plural == sectionPath {
+ return page.KindTaxonomyTerm
+ }
+
+ if strings.HasPrefix(sectionPath, plural) {
+ return page.KindTaxonomy
+ }
+
+ }
+
+ return page.KindSection
+}
+
+func (s *Site) newTaxonomyPage(title string, sections ...string) *pageState {
+ p, err := newPageFromMeta(&pageMeta{
+ title: title,
+ s: s,
+ kind: page.KindTaxonomy,
+ sections: sections,
+ })
+
+ if err != nil {
+ panic(err)
+ }
+
+ return p
+
+}
+
+func (s *Site) newPage(kind string, sections ...string) *pageState {
+ p, err := newPageFromMeta(&pageMeta{
+ s: s,
+ kind: kind,
+ sections: sections,
+ })
+
+ if err != nil {
+ panic(err)
+ }
+
+ return p
+}
+
+func (s *Site) shouldBuild(p page.Page) bool {
+ return shouldBuild(s.BuildFuture, s.BuildExpired,
+ s.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
+}
diff --git a/hugolib/siteJSONEncode_test.go b/hugolib/siteJSONEncode_test.go
new file mode 100644
index 000000000..ac0286ce2
--- /dev/null
+++ b/hugolib/siteJSONEncode_test.go
@@ -0,0 +1,45 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+)
+
+// 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()
+
+ templ := `Page: |{{ index .Site.RegularPages 0 | jsonify }}|
+Site: {{ site | jsonify }}
+`
+
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile().WithTemplatesAdded("index.html", templ)
+ b.WithContent("page.md", `---
+title: "Page"
+date: 2019-02-28
+---
+
+Content.
+
+`)
+
+ b.Build(BuildCfg{})
+
+ b.AssertFileContent("public/index.html", `"Date":"2019-02-28T00:00:00Z"`)
+
+}
diff --git a/hugolib/site_benchmark_new_test.go b/hugolib/site_benchmark_new_test.go
new file mode 100644
index 000000000..c816dc9c3
--- /dev/null
+++ b/hugolib/site_benchmark_new_test.go
@@ -0,0 +1,106 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+)
+
+// TODO(bep) eventually remove the old (too complicated setup).
+func BenchmarkSiteNew(b *testing.B) {
+ // TODO(bep) create some common and stable data set
+
+ const pageContent = `---
+title: "My Page"
+---
+
+My page content.
+
+`
+
+ config := `
+baseURL = "https://example.com"
+
+`
+
+ benchmarks := []struct {
+ name string
+ create func(i int) *sitesBuilder
+ check func(s *sitesBuilder)
+ }{
+ {"Bundle with image", func(i int) *sitesBuilder {
+ sb := newTestSitesBuilder(b).WithConfigFile("toml", config)
+ sb.WithContent("content/blog/mybundle/index.md", pageContent)
+ sb.WithSunset("content/blog/mybundle/sunset1.jpg")
+
+ return sb
+ },
+ func(s *sitesBuilder) {
+ s.AssertFileContent("public/blog/mybundle/index.html", "/blog/mybundle/sunset1.jpg")
+ s.CheckExists("public/blog/mybundle/sunset1.jpg")
+
+ },
+ },
+ {"Bundle with JSON file", func(i int) *sitesBuilder {
+ sb := newTestSitesBuilder(b).WithConfigFile("toml", config)
+ sb.WithContent("content/blog/mybundle/index.md", pageContent)
+ sb.WithContent("content/blog/mybundle/mydata.json", `{ "hello": "world" }`)
+
+ return sb
+ },
+ func(s *sitesBuilder) {
+ s.AssertFileContent("public/blog/mybundle/index.html", "Resources: application/json: /blog/mybundle/mydata.json")
+ s.CheckExists("public/blog/mybundle/mydata.json")
+
+ },
+ },
+ {"Multiple languages", func(i int) *sitesBuilder {
+ sb := newTestSitesBuilder(b).WithConfigFile("toml", `
+baseURL = "https://example.com"
+
+[languages]
+[languages.en]
+weight=1
+[languages.fr]
+weight=2
+
+`)
+
+ return sb
+ },
+ func(s *sitesBuilder) {
+
+ },
+ },
+ }
+
+ for _, bm := range benchmarks {
+ b.Run(bm.name, func(b *testing.B) {
+ sites := make([]*sitesBuilder, b.N)
+ for i := 0; i < b.N; i++ {
+ sites[i] = bm.create(i)
+ }
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ s := sites[i]
+ err := s.BuildE(BuildCfg{})
+ if err != nil {
+ b.Fatal(err)
+ }
+ bm.check(s)
+ }
+ })
+ }
+}
diff --git a/hugolib/site_benchmark_test.go b/hugolib/site_benchmark_test.go
new file mode 100644
index 000000000..8c5e07a52
--- /dev/null
+++ b/hugolib/site_benchmark_test.go
@@ -0,0 +1,337 @@
+// 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 (
+ "flag"
+ "fmt"
+ "math/rand"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/spf13/afero"
+)
+
+type siteBuildingBenchmarkConfig struct {
+ Frontmatter string
+ NumPages int
+ NumLangs 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
+ id += fmt.Sprintf("num_langs=%d%s", s.NumLangs, 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)
+}
+
+var someLangs = []string{"en", "fr", "nn"}
+
+func BenchmarkSiteBuilding(b *testing.B) {
+ var (
+ // The below represents the full matrix of benchmarks. Big!
+ allFrontmatters = []string{"YAML", "TOML"}
+ allNumRootSections = []int{1, 5}
+ allNumTags = []int{0, 1, 10, 20, 50, 100, 500, 1000, 5000}
+ allTagsPerPage = []int{0, 1, 5, 20, 50, 80}
+ allNumPages = []int{1, 10, 100, 500, 1000, 5000, 10000}
+ allDoRender = []bool{false, true}
+ allDoShortCodes = []bool{false, true}
+ allNumLangs = []int{1, 3}
+ )
+
+ var runDefault bool
+
+ visitor := func(a *flag.Flag) {
+ if a.Name == "test.bench" && len(a.Value.String()) < 40 {
+ // The full suite is too big, so fall back to some smaller default if no
+ // restriction is set.
+ runDefault = true
+ }
+ }
+
+ flag.Visit(visitor)
+
+ if runDefault {
+ allFrontmatters = allFrontmatters[1:]
+ allNumRootSections = allNumRootSections[0:2]
+ allNumTags = allNumTags[0:2]
+ allTagsPerPage = allTagsPerPage[2:3]
+ allNumPages = allNumPages[2:5]
+ allDoRender = allDoRender[1:2]
+ allDoShortCodes = allDoShortCodes[1:2]
+ }
+
+ var conf siteBuildingBenchmarkConfig
+ for _, numLangs := range allNumLangs {
+ conf.NumLangs = numLangs
+ for _, frontmatter := range allFrontmatters {
+ conf.Frontmatter = frontmatter
+ for _, rootSections := range allNumRootSections {
+ conf.RootSections = rootSections
+ for _, numTags := range allNumTags {
+ conf.NumTags = numTags
+ for _, tagsPerPage := range allTagsPerPage {
+ conf.TagsPerPage = tagsPerPage
+ for _, numPages := range allNumPages {
+ conf.NumPages = numPages
+ for _, render := range allDoRender {
+ conf.Render = render
+ for _, shortcodes := range allDoShortCodes {
+ conf.Shortcodes = shortcodes
+ doBenchMarkSiteBuilding(conf, b)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+func doBenchMarkSiteBuilding(conf siteBuildingBenchmarkConfig, b *testing.B) {
+ b.Run(conf.String(), func(b *testing.B) {
+ b.StopTimer()
+ sites := createHugoBenchmarkSites(b, b.N, conf)
+ b.StartTimer()
+ 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:]
+ }
+ })
+}
+
+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 :smile:.
+
+> 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"
+enableEmoji = true
+
+[outputs]
+home = [ "HTML" ]
+section = [ "HTML" ]
+taxonomy = [ "HTML" ]
+taxonomyTerm = [ "HTML" ]
+page = [ "HTML" ]
+
+[languages]
+%s
+
+[Taxonomies]
+tag = "tags"
+category = "categories"
+`
+
+ langConfigTemplate := `
+[languages.%s]
+languageName = "Lang %s"
+weight = %d
+`
+
+ langConfig := ""
+
+ for i := 0; i < cfg.NumLangs; i++ {
+ langCode := someLangs[i]
+ langConfig += fmt.Sprintf(langConfigTemplate, langCode, langCode, i+1)
+ }
+
+ siteConfig = fmt.Sprintf(siteConfig, langConfig)
+
+ 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)
+ }
+
+ if cfg.Shortcodes {
+ contentPagesContent = [3]string{
+ someMarkdown,
+ 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 }}|{{ partial "myPartial" . }}`,
+ "layouts/_default/list.html", `List HTML|{{ .Title }}|{{ .Content }}|GetPage: {{ with .Site.GetPage "page" "sect3/page3.md" }}{{ .Title }}{{ end }}`,
+ "layouts/partials/myPartial.html", `Partial: {{ "Hello **world**!" | markdownify }}`,
+ "layouts/shortcodes/myShortcode.html", `<p>MyShortcode</p>`)
+
+ fs := th.Fs
+
+ pagesPerSection := cfg.NumPages / cfg.RootSections / cfg.NumLangs
+ for li := 0; li < cfg.NumLangs; li++ {
+ fileLangCodeID := ""
+ if li > 0 {
+ fileLangCodeID = "." + someLangs[li] + "."
+ }
+
+ 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]
+ }
+
+ var tagsStr string
+
+ 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)])
+
+ contentFilename := fmt.Sprintf("page%d%s.md", j, fileLangCodeID)
+
+ writeSource(b, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), contentFilename), content)
+
+ }
+
+ content := fmt.Sprintf(pageTemplate, fmt.Sprintf("Section %d", i), "[]", contentPagesContent[rand.Intn(3)])
+
+ indexContentFilename := fmt.Sprintf("_index%s.md", fileLangCodeID)
+ writeSource(b, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), indexContentFilename), content)
+ }
+ }
+
+ sites[i] = h
+ }
+
+ return sites
+}
diff --git a/hugolib/site_output.go b/hugolib/site_output.go
new file mode 100644
index 000000000..9fb236506
--- /dev/null
+++ b/hugolib/site_output.go
@@ -0,0 +1,91 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/config"
+ "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/spf13/cast"
+)
+
+func createDefaultOutputFormats(allFormats output.Formats, cfg config.Provider) map[string]output.Formats {
+ rssOut, _ := allFormats.GetByName(output.RSSFormat.Name)
+ htmlOut, _ := allFormats.GetByName(output.HTMLFormat.Name)
+ robotsOut, _ := allFormats.GetByName(output.RobotsTxtFormat.Name)
+ sitemapOut, _ := allFormats.GetByName(output.SitemapFormat.Name)
+
+ return map[string]output.Formats{
+ page.KindPage: {htmlOut},
+ page.KindHome: {htmlOut, rssOut},
+ page.KindSection: {htmlOut, rssOut},
+ page.KindTaxonomy: {htmlOut, rssOut},
+ page.KindTaxonomyTerm: {htmlOut, rssOut},
+ // Below are for conistency. They are currently not used during rendering.
+ kindRSS: {rssOut},
+ kindSitemap: {sitemapOut},
+ kindRobotsTXT: {robotsOut},
+ kind404: {htmlOut},
+ }
+
+}
+
+func createSiteOutputFormats(allFormats output.Formats, cfg config.Provider) (map[string]output.Formats, error) {
+ defaultOutputFormats := createDefaultOutputFormats(allFormats, cfg)
+
+ if !cfg.IsSet("outputs") {
+ return defaultOutputFormats, nil
+ }
+
+ outFormats := make(map[string]output.Formats)
+
+ outputs := cfg.GetStringMap("outputs")
+
+ if len(outputs) == 0 {
+ return outFormats, nil
+ }
+
+ seen := make(map[string]bool)
+
+ 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)
+ }
+
+ // This effectively prevents empty outputs entries for a given Kind.
+ // We need at least one.
+ if len(formats) > 0 {
+ seen[k] = true
+ outFormats[k] = formats
+ }
+ }
+
+ // Add defaults for the entries not provided by the user.
+ for k, v := range defaultOutputFormats {
+ if !seen[k] {
+ outFormats[k] = v
+ }
+ }
+
+ return outFormats, nil
+
+}
diff --git a/hugolib/site_output_test.go b/hugolib/site_output_test.go
new file mode 100644
index 000000000..71b87b636
--- /dev/null
+++ b/hugolib/site_output_test.go
@@ -0,0 +1,562 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+
+ "github.com/gohugoio/hugo/resources/page"
+
+ "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 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 = ["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 >}}
+
+{{< myOtherShort >}}
+
+`
+
+ 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/myOtherShort.html", `OtherShort: {{ "<h1>Hi!</h1>" | safeHTML }}`,
+ "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 }}
+Len Pages: {{ .Kind }} {{ len .Site.RegularPages }} Page Number: {{ .Paginator.PageNumber }}
+{{ end }}
+`,
+ "layouts/_default/single.html", `{{ define "main" }}{{ .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))
+
+ for i := 1; i <= 10; i++ {
+ writeSource(t, fs, fmt.Sprintf("content/p%d.md", i), fmt.Sprintf(pageTemplate, fmt.Sprintf("Page %d", i), outputsStr))
+
+ }
+
+ err := h.Build(BuildCfg{})
+
+ require.NoError(t, err)
+
+ s := h.Sites[0]
+ require.Equal(t, "en", s.language.Lang)
+
+ home := s.getPage(page.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",
+ "OtherShort: <h1>Hi!</h1>",
+ )
+
+ 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" />`,
+ "en: Elbow",
+ "ShortHTML",
+ "OtherShort: <h1>Hi!</h1>",
+ "Len Pages: home 10",
+ )
+ th.assertFileContent("public/page/2/index.html", "Page Number: 2")
+ th.assertFileNotExist("public/page/2/index.json")
+
+ 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" />`,
+ "ShortJSON",
+ "OtherShort: <h1>Hi!</h1>",
+ )
+ th.assertFileContent("public/nn/index.json",
+ "List JSON|JSON Nynorsk Heim|",
+ "nn: Olboge",
+ "ShortJSON",
+ )
+ }
+
+ of := home.OutputFormats()
+
+ json := of.Get("JSON")
+ 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())
+ }
+
+ require.True(t, home.HasShortcode("myShort"))
+ require.False(t, home.HasShortcode("doesNotExist"))
+
+}
+
+// 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"]
+delimiter = ""
+[mediaTypes."text/defaultdelim"]
+suffixes = ["defd"]
+[mediaTypes."text/nosuffix"]
+[mediaTypes."text/customdelim"]
+suffixes = ["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(page.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())
+
+}
+
+func TestCreateSiteOutputFormats(t *testing.T) {
+ assert := require.New(t)
+
+ outputsConfig := map[string]interface{}{
+ page.KindHome: []string{"HTML", "JSON"},
+ page.KindSection: []string{"JSON"},
+ }
+
+ cfg := viper.New()
+ cfg.Set("outputs", outputsConfig)
+
+ outputs, err := createSiteOutputFormats(output.DefaultFormats, cfg)
+ assert.NoError(err)
+ assert.Equal(output.Formats{output.JSONFormat}, outputs[page.KindSection])
+ assert.Equal(output.Formats{output.HTMLFormat, output.JSONFormat}, outputs[page.KindHome])
+
+ // Defaults
+ assert.Equal(output.Formats{output.HTMLFormat, output.RSSFormat}, outputs[page.KindTaxonomy])
+ assert.Equal(output.Formats{output.HTMLFormat, output.RSSFormat}, outputs[page.KindTaxonomyTerm])
+ assert.Equal(output.Formats{output.HTMLFormat}, outputs[page.KindPage])
+
+ // These aren't (currently) in use when rendering in Hugo,
+ // but the pages needs to be assigned an output format,
+ // so these should also be correct/sensible.
+ assert.Equal(output.Formats{output.RSSFormat}, outputs[kindRSS])
+ assert.Equal(output.Formats{output.SitemapFormat}, outputs[kindSitemap])
+ assert.Equal(output.Formats{output.RobotsTxtFormat}, outputs[kindRobotsTXT])
+ assert.Equal(output.Formats{output.HTMLFormat}, outputs[kind404])
+
+}
+
+func TestCreateSiteOutputFormatsInvalidConfig(t *testing.T) {
+ assert := require.New(t)
+
+ outputsConfig := map[string]interface{}{
+ page.KindHome: []string{"FOO", "JSON"},
+ }
+
+ cfg := viper.New()
+ cfg.Set("outputs", outputsConfig)
+
+ _, err := createSiteOutputFormats(output.DefaultFormats, cfg)
+ assert.Error(err)
+}
+
+func TestCreateSiteOutputFormatsEmptyConfig(t *testing.T) {
+ assert := require.New(t)
+
+ outputsConfig := map[string]interface{}{
+ page.KindHome: []string{},
+ }
+
+ cfg := viper.New()
+ cfg.Set("outputs", outputsConfig)
+
+ outputs, err := createSiteOutputFormats(output.DefaultFormats, cfg)
+ assert.NoError(err)
+ assert.Equal(output.Formats{output.HTMLFormat, output.RSSFormat}, outputs[page.KindHome])
+}
+
+func TestCreateSiteOutputFormatsCustomFormats(t *testing.T) {
+ assert := require.New(t)
+
+ outputsConfig := map[string]interface{}{
+ page.KindHome: []string{},
+ }
+
+ cfg := viper.New()
+ cfg.Set("outputs", outputsConfig)
+
+ var (
+ customRSS = output.Format{Name: "RSS", BaseName: "customRSS"}
+ customHTML = output.Format{Name: "HTML", BaseName: "customHTML"}
+ )
+
+ outputs, err := createSiteOutputFormats(output.Formats{customRSS, customHTML}, cfg)
+ assert.NoError(err)
+ assert.Equal(output.Formats{customHTML, customRSS}, outputs[page.KindHome])
+}
+
+// https://github.com/gohugoio/hugo/issues/5849
+func TestOutputFormatPermalinkable(t *testing.T) {
+
+ config := `
+baseURL = "https://example.com"
+
+
+
+# DAMP is similar to AMP, but not permalinkable.
+[outputFormats]
+[outputFormats.damp]
+mediaType = "text/html"
+path = "damp"
+[outputFormats.ramp]
+mediaType = "text/html"
+path = "ramp"
+permalinkable = true
+[outputFormats.base]
+mediaType = "text/html"
+isHTML = true
+baseName = "that"
+permalinkable = true
+[outputFormats.nobase]
+mediaType = "application/json"
+permalinkable = true
+
+`
+
+ b := newTestSitesBuilder(t).WithConfigFile("toml", config)
+ b.WithContent("_index.md", `
+---
+Title: Home Sweet Home
+outputs: [ "html", "amp", "damp", "base" ]
+---
+
+`)
+
+ b.WithContent("blog/html-amp.md", `
+---
+Title: AMP and HTML
+outputs: [ "html", "amp" ]
+---
+
+`)
+
+ b.WithContent("blog/html-damp.md", `
+---
+Title: DAMP and HTML
+outputs: [ "html", "damp" ]
+---
+
+`)
+
+ b.WithContent("blog/html-ramp.md", `
+---
+Title: RAMP and HTML
+outputs: [ "html", "ramp" ]
+---
+
+`)
+
+ b.WithContent("blog/html.md", `
+---
+Title: HTML only
+outputs: [ "html" ]
+---
+
+`)
+
+ b.WithContent("blog/amp.md", `
+---
+Title: AMP only
+outputs: [ "amp" ]
+---
+
+`)
+
+ b.WithContent("blog/html-base-nobase.md", `
+---
+Title: HTML, Base and Nobase
+outputs: [ "html", "base", "nobase" ]
+---
+
+`)
+
+ const commonTemplate = `
+This RelPermalink: {{ .RelPermalink }}
+Output Formats: {{ len .OutputFormats }};{{ range .OutputFormats }}{{ .Name }};{{ .RelPermalink }}|{{ end }}
+
+`
+
+ b.WithTemplatesAdded("index.html", commonTemplate)
+ b.WithTemplatesAdded("_default/single.html", commonTemplate)
+ b.WithTemplatesAdded("_default/single.json", commonTemplate)
+
+ b.Build(BuildCfg{})
+
+ b.AssertFileContent("public/index.html",
+ "This RelPermalink: /",
+ "Output Formats: 4;HTML;/|AMP;/amp/|damp;/damp/|base;/that.html|",
+ )
+
+ b.AssertFileContent("public/amp/index.html",
+ "This RelPermalink: /amp/",
+ "Output Formats: 4;HTML;/|AMP;/amp/|damp;/damp/|base;/that.html|",
+ )
+
+ b.AssertFileContent("public/blog/html-amp/index.html",
+ "Output Formats: 2;HTML;/blog/html-amp/|AMP;/amp/blog/html-amp/|",
+ "This RelPermalink: /blog/html-amp/")
+
+ b.AssertFileContent("public/amp/blog/html-amp/index.html",
+ "Output Formats: 2;HTML;/blog/html-amp/|AMP;/amp/blog/html-amp/|",
+ "This RelPermalink: /amp/blog/html-amp/")
+
+ // Damp is not permalinkable
+ b.AssertFileContent("public/damp/blog/html-damp/index.html",
+ "This RelPermalink: /blog/html-damp/",
+ "Output Formats: 2;HTML;/blog/html-damp/|damp;/damp/blog/html-damp/|")
+
+ b.AssertFileContent("public/blog/html-ramp/index.html",
+ "This RelPermalink: /blog/html-ramp/",
+ "Output Formats: 2;HTML;/blog/html-ramp/|ramp;/ramp/blog/html-ramp/|")
+
+ b.AssertFileContent("public/ramp/blog/html-ramp/index.html",
+ "This RelPermalink: /ramp/blog/html-ramp/",
+ "Output Formats: 2;HTML;/blog/html-ramp/|ramp;/ramp/blog/html-ramp/|")
+
+ // https://github.com/gohugoio/hugo/issues/5877
+ outputFormats := "Output Formats: 3;HTML;/blog/html-base-nobase/|base;/blog/html-base-nobase/that.html|nobase;/blog/html-base-nobase/index.json|"
+
+ b.AssertFileContent("public/blog/html-base-nobase/index.json",
+ "This RelPermalink: /blog/html-base-nobase/index.json",
+ outputFormats,
+ )
+
+ b.AssertFileContent("public/blog/html-base-nobase/that.html",
+ "This RelPermalink: /blog/html-base-nobase/that.html",
+ outputFormats,
+ )
+
+ b.AssertFileContent("public/blog/html-base-nobase/index.html",
+ "This RelPermalink: /blog/html-base-nobase/",
+ outputFormats,
+ )
+
+}
diff --git a/hugolib/site_render.go b/hugolib/site_render.go
new file mode 100644
index 000000000..34f288da2
--- /dev/null
+++ b/hugolib/site_render.go
@@ -0,0 +1,371 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "sync"
+
+ "github.com/gohugoio/hugo/config"
+
+ "github.com/gohugoio/hugo/output"
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/resources/page/pagemeta"
+)
+
+type siteRenderContext struct {
+ cfg *BuildCfg
+
+ // Zero based index for all output formats combined.
+ sitesOutIdx int
+
+ // Zero based index of the output formats configured within a Site.
+ // Note that these outputs are sorted.
+ outIdx int
+
+ multihost bool
+}
+
+// Whether to render 404.html, robotsTXT.txt which usually is rendered
+// once only in the site root.
+func (s siteRenderContext) renderSingletonPages() bool {
+ if s.multihost {
+ // 1 per site
+ return s.outIdx == 0
+ }
+
+ // 1 for all sites
+ return s.sitesOutIdx == 0
+
+}
+
+// renderPages renders pages each corresponding to a markdown file.
+// TODO(bep np doc
+func (s *Site) renderPages(ctx *siteRenderContext) error {
+
+ numWorkers := config.GetNumWorkerMultiplier()
+
+ results := make(chan error)
+ pages := make(chan *pageState, numWorkers) // buffered for performance
+ errs := make(chan error)
+
+ go s.errorCollator(results, errs)
+
+ wg := &sync.WaitGroup{}
+
+ for i := 0; i < numWorkers; i++ {
+ wg.Add(1)
+ go pageRenderer(ctx, s, pages, results, wg)
+ }
+
+ cfg := ctx.cfg
+
+ if !cfg.PartialReRender && ctx.outIdx == 0 && len(s.headlessPages) > 0 {
+ wg.Add(1)
+ go headlessPagesPublisher(s, wg)
+ }
+
+L:
+ for _, page := range s.workAllPages {
+ if cfg.shouldRender(page) {
+ select {
+ case <-s.h.Done():
+ break L
+ default:
+ pages <- page
+ }
+ }
+ }
+
+ close(pages)
+
+ wg.Wait()
+
+ close(results)
+
+ err := <-errs
+ if err != nil {
+ return errors.Wrap(err, "failed to render pages")
+ }
+ return nil
+}
+
+func headlessPagesPublisher(s *Site, wg *sync.WaitGroup) {
+ defer wg.Done()
+ for _, p := range s.headlessPages {
+ if err := p.renderResources(); err != nil {
+ s.SendError(p.errorf(err, "failed to render page resources"))
+ }
+ }
+}
+
+func pageRenderer(
+ ctx *siteRenderContext,
+ s *Site,
+ pages <-chan *pageState,
+ results chan<- error,
+ wg *sync.WaitGroup) {
+
+ defer wg.Done()
+
+ for p := range pages {
+ f := p.outputFormat()
+
+ // TODO(bep) get rid of this odd construct. RSS is an output format.
+ if f.Name == "RSS" && !s.isEnabled(kindRSS) {
+ continue
+ }
+
+ if err := p.renderResources(); err != nil {
+ s.SendError(p.errorf(err, "failed to render page resources"))
+ continue
+ }
+
+ layouts, err := p.getLayouts()
+ if err != nil {
+ s.Log.ERROR.Printf("Failed to resolve layout for output %q for page %q: %s", f.Name, p, err)
+ continue
+ }
+
+ targetPath := p.targetPaths().TargetFilename
+
+ if targetPath == "" {
+ s.Log.ERROR.Printf("Failed to create target path for output %q for page %q: %s", f.Name, p, err)
+ continue
+ }
+
+ if err := s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "page "+p.Title(), targetPath, p, layouts...); err != nil {
+ results <- err
+ }
+
+ if p.paginator != nil && p.paginator.current != nil {
+ if err := s.renderPaginator(p, layouts); err != nil {
+ results <- err
+ }
+ }
+ }
+}
+
+// renderPaginator must be run after the owning Page has been rendered.
+func (s *Site) renderPaginator(p *pageState, layouts []string) error {
+
+ paginatePath := s.Cfg.GetString("paginatePath")
+
+ d := p.targetPathDescriptor
+ f := p.s.rc.Format
+ d.Type = f
+
+ if p.paginator.current == nil || p.paginator.current != p.paginator.current.First() {
+ panic(fmt.Sprintf("invalid paginator state for %q", p.pathOrTitle()))
+ }
+
+ // Write alias for page 1
+ d.Addends = fmt.Sprintf("/%s/%d", paginatePath, 1)
+ targetPaths := page.CreateTargetPaths(d)
+
+ if err := s.writeDestAlias(targetPaths.TargetFilename, p.Permalink(), f, nil); err != nil {
+ return err
+ }
+
+ // Render pages for the rest
+ for current := p.paginator.current.Next(); current != nil; current = current.Next() {
+
+ p.paginator.current = current
+ d.Addends = fmt.Sprintf("/%s/%d", paginatePath, current.PageNumber())
+ targetPaths := page.CreateTargetPaths(d)
+
+ if err := s.renderAndWritePage(
+ &s.PathSpec.ProcessingStats.PaginatorPages,
+ p.Title(),
+ targetPaths.TargetFilename, p, layouts...); err != nil {
+ return err
+ }
+
+ }
+
+ return nil
+}
+
+func (s *Site) render404() error {
+ if !s.isEnabled(kind404) {
+ return nil
+ }
+
+ p, err := newPageStandalone(&pageMeta{
+ s: s,
+ kind: kind404,
+ urlPaths: pagemeta.URLPath{
+ URL: "404.html",
+ },
+ },
+ output.HTMLFormat,
+ )
+
+ if err != nil {
+ return err
+ }
+
+ nfLayouts := []string{"404.html"}
+
+ targetPath := p.targetPaths().TargetFilename
+
+ if targetPath == "" {
+ return errors.New("failed to create targetPath for 404 page")
+ }
+
+ return s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "404 page", targetPath, p, nfLayouts...)
+}
+
+func (s *Site) renderSitemap() error {
+ if !s.isEnabled(kindSitemap) {
+ return nil
+ }
+
+ p, err := newPageStandalone(&pageMeta{
+ s: s,
+ kind: kindSitemap,
+ urlPaths: pagemeta.URLPath{
+ URL: s.siteCfg.sitemap.Filename,
+ }},
+ output.HTMLFormat,
+ )
+
+ if err != nil {
+ return err
+ }
+
+ targetPath := p.targetPaths().TargetFilename
+
+ if targetPath == "" {
+ return errors.New("failed to create targetPath for sitemap")
+ }
+
+ smLayouts := []string{"sitemap.xml", "_default/sitemap.xml", "_internal/_default/sitemap.xml"}
+
+ return s.renderAndWriteXML(&s.PathSpec.ProcessingStats.Sitemaps, "sitemap", targetPath, p, smLayouts...)
+}
+
+func (s *Site) renderRobotsTXT() error {
+ if !s.isEnabled(kindRobotsTXT) {
+ return nil
+ }
+
+ if !s.Cfg.GetBool("enableRobotsTXT") {
+ return nil
+ }
+
+ p, err := newPageStandalone(&pageMeta{
+ s: s,
+ kind: kindRobotsTXT,
+ urlPaths: pagemeta.URLPath{
+ URL: "robots.txt",
+ },
+ },
+ output.RobotsTxtFormat)
+
+ if err != nil {
+ return err
+ }
+
+ rLayouts := []string{"robots.txt", "_default/robots.txt", "_internal/_default/robots.txt"}
+
+ return s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "Robots Txt", p.targetPaths().TargetFilename, p, rLayouts...)
+
+}
+
+// renderAliases renders shell pages that simply have a redirect in the header.
+func (s *Site) renderAliases() error {
+ for _, p := range s.workAllPages {
+
+ if len(p.Aliases()) == 0 {
+ continue
+ }
+
+ for _, of := range p.OutputFormats() {
+ if !of.Format.IsHTML {
+ continue
+ }
+
+ plink := of.Permalink()
+ f := of.Format
+
+ for _, a := range p.Aliases() {
+ isRelative := !strings.HasPrefix(a, "/")
+
+ if isRelative {
+ // Make alias relative, where "." will be on the
+ // same directory level as the current page.
+ // TODO(bep) ugly URLs doesn't seem to be supported in
+ // aliases, I'm not sure why not.
+ basePath := of.RelPermalink()
+ if strings.HasSuffix(basePath, "/") {
+ basePath = path.Join(basePath, "..")
+ }
+ a = path.Join(basePath, a)
+
+ } else if f.Path != "" {
+ // Make sure AMP and similar doesn't clash with regular aliases.
+ a = path.Join(f.Path, a)
+ }
+
+ lang := p.Language().Lang
+
+ if s.h.multihost && !strings.HasPrefix(a, "/"+lang) {
+ // These need to be in its language root.
+ a = path.Join(lang, a)
+ }
+
+ if err := s.writeDestAlias(a, plink, f, p); err != nil {
+ return err
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+// renderMainLanguageRedirect creates a redirect to the main language home,
+// depending on if it lives in sub folder (e.g. /en) or not.
+func (s *Site) renderMainLanguageRedirect() error {
+
+ if !s.h.multilingual.enabled() || s.h.IsMultihost() {
+ // No need for a redirect
+ return nil
+ }
+
+ html, found := s.outputFormatsConfig.GetByName("HTML")
+ if found {
+ mainLang := s.h.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, html, 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, html, 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..8fce43471
--- /dev/null
+++ b/hugolib/site_sections.go
@@ -0,0 +1,244 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "strconv"
+ "strings"
+
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/resources/resource"
+
+ radix "github.com/hashicorp/go-immutable-radix"
+)
+
+// Sections returns the top level sections.
+func (s *SiteInfo) Sections() page.Pages {
+ home, err := s.Home()
+ if err == nil {
+ return home.Sections()
+ }
+ return nil
+}
+
+// Home is a shortcut to the home page, equivalent to .Site.GetPage "home".
+func (s *SiteInfo) Home() (page.Page, error) {
+ return s.s.home, nil
+}
+
+func (s *Site) assembleSections() pageStatePages {
+ var newPages pageStatePages
+
+ if !s.isEnabled(page.KindSection) {
+ return newPages
+ }
+
+ // Maps section kind pages to their path, i.e. "my/section"
+ sectionPages := make(map[string]*pageState)
+
+ // The sections with content files will already have been created.
+ for _, sect := range s.findWorkPagesByKind(page.KindSection) {
+ sectionPages[sect.SectionsPath()] = sect
+ }
+
+ const (
+ sectKey = "__hs"
+ sectSectKey = "_a" + sectKey
+ sectPageKey = "_b" + sectKey
+ )
+
+ var (
+ inPages = radix.New().Txn()
+ inSections = radix.New().Txn()
+ undecided pageStatePages
+ )
+
+ home := s.findFirstWorkPageByKindIn(page.KindHome)
+
+ for i, p := range s.workAllPages {
+
+ if p.Kind() != page.KindPage {
+ continue
+ }
+
+ sections := p.SectionsEntries()
+
+ if len(sections) == 0 {
+ // Root level pages. These will have the home page as their Parent.
+ p.parent = home
+ continue
+ }
+
+ sectionKey := p.SectionsPath()
+ _, found := sectionPages[sectionKey]
+
+ if !found && len(sections) == 1 {
+
+ // We only create content-file-less sections for the root sections.
+ n := s.newPage(page.KindSection, sections[0])
+
+ sectionPages[sectionKey] = n
+ newPages = append(newPages, n)
+ found = true
+ }
+
+ if len(sections) > 1 {
+ // Create the root section if not found.
+ _, rootFound := sectionPages[sections[0]]
+ if !rootFound {
+ sect := s.newPage(page.KindSection, sections[0])
+ sectionPages[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 {
+ sections := sect.SectionsEntries()
+ for i := len(sections); i > 0; i-- {
+ sectionPath := sections[:i]
+ sectionKey := path.Join(sectionPath...)
+ _, found := sectionPages[sectionKey]
+ if !found {
+ sect = s.newPage(page.KindSection, sectionPath[len(sectionPath)-1])
+ sect.m.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 *pageState
+ children page.Pages
+ dates *resource.Dates
+ rootSections = inSections.Commit().Root()
+ )
+
+ for i, p := range undecided {
+ // Now we can decide where to put this page into the tree.
+ sectionKey := p.SectionsPath()
+
+ _, v, _ := rootSections.LongestPrefix([]byte(sectionKey))
+ sect := v.(*pageState)
+ pagePath := path.Join(path.Join(sect.SectionsEntries()...), 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.(*pageState)
+
+ if p.Kind() == page.KindSection {
+ if currentSection != nil {
+ // A new section
+ currentSection.setPages(children)
+ if dates != nil {
+ currentSection.m.Dates = *dates
+ }
+ }
+
+ currentSection = p
+ children = make(page.Pages, 0)
+ dates = nil
+ // Use section's dates from front matter if set.
+ if resource.IsZeroDates(currentSection) {
+ dates = &resource.Dates{}
+ }
+
+ return false
+
+ }
+
+ // Regular page
+ p.parent = currentSection
+ children = append(children, p)
+ if dates != nil {
+ dates.UpdateDateAndLastmodIfAfter(p)
+ }
+
+ return false
+ })
+
+ if currentSection != nil {
+ currentSection.setPages(children)
+ if dates != nil {
+ currentSection.m.Dates = *dates
+ }
+ }
+
+ // Build the sections hierarchy
+ for _, sect := range sectionPages {
+ sections := sect.SectionsEntries()
+ if len(sections) == 1 {
+ if home != nil {
+ sect.parent = home
+ }
+ } else {
+ parentSearchKey := path.Join(sect.SectionsEntries()[:len(sections)-1]...)
+ _, v, _ := rootSections.LongestPrefix([]byte(parentSearchKey))
+ p := v.(*pageState)
+ sect.parent = p
+ }
+
+ sect.addSectionToParent()
+ }
+
+ var (
+ sectionsParamId = "mainSections"
+ sectionsParamIdLower = strings.ToLower(sectionsParamId)
+ mainSections interface{}
+ mainSectionsFound bool
+ maxSectionWeight int
+ )
+
+ mainSections, mainSectionsFound = s.Info.Params()[sectionsParamIdLower]
+
+ for _, sect := range sectionPages {
+ sect.sortParentSections()
+
+ 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
+
+}
diff --git a/hugolib/site_sections_test.go b/hugolib/site_sections_test.go
new file mode 100644
index 000000000..199947c31
--- /dev/null
+++ b/hugolib/site_sections_test.go
@@ -0,0 +1,377 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/resources/page"
+ "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 "l1/l2") }}
+<html>List|{{ .Title }}|L1/l2-IsActive: {{ .InSection $sect }}
+{{ range .Paginator.Pages }}
+PAG|{{ .Title }}|{{ $sect.InSection . }}
+{{ end }}
+{{/* https://github.com/gohugoio/hugo/issues/4989 */}}
+{{ $sections := (.Site.GetPage "section" .Section).Sections.ByWeight }}
+</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(assert *require.Assertions, p page.Page)
+ }{
+ {"elsewhere", func(assert *require.Assertions, p page.Page) {
+ assert.Len(p.Pages(), 1)
+ for _, p := range p.Pages() {
+ assert.Equal("elsewhere", p.SectionsPath())
+ }
+ }},
+ {"post", func(assert *require.Assertions, p page.Page) {
+ assert.Len(p.Pages(), 2)
+ for _, p := range p.Pages() {
+ assert.Equal("post", p.Section())
+ }
+ }},
+ {"empty1", func(assert *require.Assertions, p page.Page) {
+ // > b,c
+ assert.NotNil(getPage(p, "/empty1/b"))
+ assert.NotNil(getPage(p, "/empty1/b/c"))
+
+ }},
+ {"empty2", func(assert *require.Assertions, p page.Page) {
+ // > b,c,d where b and d have content files.
+ b := getPage(p, "/empty2/b")
+ assert.NotNil(b)
+ assert.Equal("T40_-1", b.Title())
+ c := getPage(p, "/empty2/b/c")
+
+ assert.NotNil(c)
+ assert.Equal("Cs", c.Title())
+ d := getPage(p, "/empty2/b/c/d")
+
+ assert.NotNil(d)
+ assert.Equal("T41_-1", d.Title())
+
+ assert.False(c.Eq(d))
+ assert.True(c.Eq(c))
+ assert.False(c.Eq("asdf"))
+
+ }},
+ {"empty3", func(assert *require.Assertions, p page.Page) {
+ // b,c,d with regular page in b
+ b := getPage(p, "/empty3/b")
+ assert.NotNil(b)
+ assert.Len(b.Pages(), 1)
+ assert.Equal("empty3.md", b.Pages()[0].File().LogicalName())
+
+ }},
+ {"empty3", func(assert *require.Assertions, p page.Page) {
+ xxx := getPage(p, "/empty3/nil")
+ assert.Nil(xxx)
+ }},
+ {"top", func(assert *require.Assertions, p page.Page) {
+ assert.Equal("Tops", p.Title())
+ assert.Len(p.Pages(), 2)
+ assert.Equal("mypage2.md", p.Pages()[0].File().LogicalName())
+ assert.Equal("mypage3.md", p.Pages()[1].File().LogicalName())
+ home := p.Parent()
+ assert.True(home.IsHome())
+ assert.Len(p.Sections(), 0)
+ assert.Equal(home, home.CurrentSection())
+ active, err := home.InSection(home)
+ assert.NoError(err)
+ assert.True(active)
+ assert.Equal(p, p.FirstSection())
+ }},
+ {"l1", func(assert *require.Assertions, p page.Page) {
+ assert.Equal("L1s", p.Title())
+ assert.Len(p.Pages(), 2)
+ assert.True(p.Parent().IsHome())
+ assert.Len(p.Sections(), 2)
+ }},
+ {"l1,l2", func(assert *require.Assertions, p page.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.RelPermalink())
+ assert.Len(p.Sections(), 1)
+
+ for _, child := range p.Pages() {
+
+ assert.Equal(p, child.CurrentSection())
+ 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(getPage(p, "/"))
+ assert.NoError(err)
+ assert.False(active)
+
+ isAncestor, err := p.IsAncestor(child)
+ assert.NoError(err)
+ assert.True(isAncestor)
+ isAncestor, err = child.IsAncestor(p)
+ assert.NoError(err)
+ assert.False(isAncestor)
+
+ isDescendant, err := p.IsDescendant(child)
+ assert.NoError(err)
+ assert.False(isDescendant)
+ isDescendant, err = child.IsDescendant(p)
+ assert.NoError(err)
+ assert.True(isDescendant)
+ }
+
+ assert.True(p.Eq(p.CurrentSection()))
+
+ }},
+ {"l1,l2_2", func(assert *require.Assertions, p page.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].File().Path())
+ assert.Equal("L1s", p.Parent().Title())
+ assert.Len(p.Sections(), 0)
+ }},
+ {"l1,l2,l3", func(assert *require.Assertions, p page.Page) {
+ nilp, _ := p.GetPage("this/does/not/exist")
+
+ assert.Equal("T3_-1", p.Title())
+ assert.Len(p.Pages(), 2)
+ assert.Equal("T2_-1", p.Parent().Title())
+ assert.Len(p.Sections(), 0)
+
+ l1 := getPage(p, "/l1")
+ isDescendant, err := l1.IsDescendant(p)
+ assert.NoError(err)
+ assert.False(isDescendant)
+ isDescendant, err = l1.IsDescendant(nil)
+ assert.NoError(err)
+ assert.False(isDescendant)
+ isDescendant, err = nilp.IsDescendant(p)
+ assert.NoError(err)
+ assert.False(isDescendant)
+ isDescendant, err = p.IsDescendant(l1)
+ assert.NoError(err)
+ assert.True(isDescendant)
+
+ isAncestor, err := l1.IsAncestor(p)
+ assert.NoError(err)
+ assert.True(isAncestor)
+ isAncestor, err = p.IsAncestor(l1)
+ assert.NoError(err)
+ assert.False(isAncestor)
+ assert.Equal(l1, p.FirstSection())
+ isAncestor, err = p.IsAncestor(nil)
+ assert.NoError(err)
+ assert.False(isAncestor)
+ isAncestor, err = nilp.IsAncestor(l1)
+ assert.NoError(err)
+ assert.False(isAncestor)
+
+ }},
+ {"perm a,link", func(assert *require.Assertions, p page.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())
+
+ }},
+ }
+
+ home := s.getPage(page.KindHome)
+
+ for _, test := range tests {
+ t.Run(fmt.Sprintf("sections %s", test.sections), func(t *testing.T) {
+ assert := require.New(t)
+ sections := strings.Split(test.sections, ",")
+ p := s.getPage(page.KindSection, sections...)
+ assert.NotNil(p, fmt.Sprint(sections))
+
+ if p.Pages() != nil {
+ assert.Equal(p.Pages(), p.Data().(page.Data).Pages())
+ }
+ assert.NotNil(p.Parent(), fmt.Sprintf("Parent nil: %q", test.sections))
+ test.verify(assert, p)
+ })
+ }
+
+ assert.NotNil(home)
+
+ assert.Len(home.Sections(), 9)
+ assert.Equal(home.Sections(), s.Info.Sections())
+
+ rootPage := s.getPage(page.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(page.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")
+
+}
+
+func TestNextInSectionNested(t *testing.T) {
+ t.Parallel()
+
+ pageContent := `---
+title: "The Page"
+weight: %d
+---
+Some content.
+`
+ createPageContent := func(weight int) string {
+ return fmt.Sprintf(pageContent, weight)
+ }
+
+ b := newTestSitesBuilder(t)
+ b.WithSimpleConfigFile()
+ b.WithTemplates("_default/single.html", `
+Prev: {{ with .PrevInSection }}{{ .RelPermalink }}{{ end }}|
+Next: {{ with .NextInSection }}{{ .RelPermalink }}{{ end }}|
+`)
+
+ b.WithContent("blog/page1.md", createPageContent(1))
+ b.WithContent("blog/page2.md", createPageContent(2))
+ b.WithContent("blog/cool/_index.md", createPageContent(1))
+ b.WithContent("blog/cool/cool1.md", createPageContent(1))
+ b.WithContent("blog/cool/cool2.md", createPageContent(2))
+ b.WithContent("root1.md", createPageContent(1))
+ b.WithContent("root2.md", createPageContent(2))
+
+ b.Build(BuildCfg{})
+
+ b.AssertFileContent("public/root1/index.html",
+ "Prev: /root2/|", "Next: |")
+ b.AssertFileContent("public/root2/index.html",
+ "Prev: |", "Next: /root1/|")
+ b.AssertFileContent("public/blog/page1/index.html",
+ "Prev: /blog/page2/|", "Next: |")
+ b.AssertFileContent("public/blog/page2/index.html",
+ "Prev: |", "Next: /blog/page1/|")
+ b.AssertFileContent("public/blog/cool/cool1/index.html",
+ "Prev: /blog/cool/cool2/|", "Next: |")
+ b.AssertFileContent("public/blog/cool/cool2/index.html",
+ "Prev: |", "Next: /blog/cool/cool1/|")
+
+}
diff --git a/hugolib/site_stats_test.go b/hugolib/site_stats_test.go
new file mode 100644
index 000000000..c722037b4
--- /dev/null
+++ b/hugolib/site_stats_test.go
@@ -0,0 +1,101 @@
+// 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"
+ "io/ioutil"
+ "testing"
+
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/spf13/afero"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestSiteStats(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ siteConfig := `
+baseURL = "http://example.com/blog"
+
+paginate = 1
+defaultContentLanguage = "nn"
+
+[languages]
+[languages.nn]
+languageName = "Nynorsk"
+weight = 1
+title = "Hugo på norsk"
+
+[languages.en]
+languageName = "English"
+weight = 2
+title = "Hugo in English"
+
+`
+
+ pageTemplate := `---
+title: "T%d"
+tags:
+%s
+categories:
+%s
+aliases: [/Ali%d]
+---
+# Doc
+`
+
+ th, h := newTestSitesFromConfig(t, afero.NewMemMapFs(), siteConfig,
+ "layouts/_default/single.html", "Single|{{ .Title }}|{{ .Content }}",
+ "layouts/_default/list.html", `List|{{ .Title }}|Pages: {{ .Paginator.TotalPages }}|{{ .Content }}`,
+ "layouts/_default/terms.html", "Terms List|{{ .Title }}|{{ .Content }}",
+ )
+ require.Len(t, h.Sites, 2)
+
+ fs := th.Fs
+
+ for i := 0; i < 2; i++ {
+ for j := 0; j < 2; j++ {
+ pageID := i + j + 1
+ writeSource(t, fs, fmt.Sprintf("content/sect/p%d.md", pageID),
+ fmt.Sprintf(pageTemplate, pageID, fmt.Sprintf("- tag%d", j), fmt.Sprintf("- category%d", j), pageID))
+ }
+ }
+
+ for i := 0; i < 5; i++ {
+ writeSource(t, fs, fmt.Sprintf("content/assets/image%d.png", i+1), "image")
+ }
+
+ err := h.Build(BuildCfg{})
+
+ assert.NoError(err)
+
+ stats := []*helpers.ProcessingStats{
+ h.Sites[0].PathSpec.ProcessingStats,
+ h.Sites[1].PathSpec.ProcessingStats}
+
+ stats[0].Table(ioutil.Discard)
+ stats[1].Table(ioutil.Discard)
+
+ var buff bytes.Buffer
+
+ helpers.ProcessingStatsTable(&buff, stats...)
+
+ assert.Contains(buff.String(), "Pages | 19 | 6")
+
+}
diff --git a/hugolib/site_test.go b/hugolib/site_test.go
new file mode 100644
index 000000000..5912abbc9
--- /dev/null
+++ b/hugolib/site_test.go
@@ -0,0 +1,959 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/markbates/inflect"
+
+ "github.com/gohugoio/hugo/helpers"
+
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+const (
+ templateMissingFunc = "{{ .Title | funcdoesnotexists }}"
+ templateWithURLAbs = "<a href=\"/foobar.jpg\">Going</a>"
+)
+
+func TestRenderWithInvalidTemplate(t *testing.T) {
+ t.Parallel()
+ cfg, fs := newTestCfg()
+
+ writeSource(t, fs, filepath.Join("content", "foo.md"), "foo")
+
+ withTemplate := createWithTemplateFromNameValues("missing", templateMissingFunc)
+
+ buildSingleSiteExpected(t, true, false, deps.DepsCfg{Fs: fs, Cfg: cfg, WithTemplate: withTemplate}, BuildCfg{})
+
+}
+
+func TestDraftAndFutureRender(t *testing.T) {
+ t.Parallel()
+ sources := [][2]string{
+ {filepath.FromSlash("sect/doc1.md"), "---\ntitle: doc1\ndraft: true\npublishdate: \"2414-05-29\"\n---\n# doc1\n*some content*"},
+ {filepath.FromSlash("sect/doc2.md"), "---\ntitle: doc2\ndraft: true\npublishdate: \"2012-05-29\"\n---\n# doc2\n*some content*"},
+ {filepath.FromSlash("sect/doc3.md"), "---\ntitle: doc3\ndraft: false\npublishdate: \"2414-05-29\"\n---\n# doc3\n*some content*"},
+ {filepath.FromSlash("sect/doc4.md"), "---\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[0]), src[1])
+
+ }
+
+ 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 := [][2]string{
+ {filepath.FromSlash("sect/doc3.md"), "---\ntitle: doc1\nexpirydate: \"2400-05-29\"\n---\n# doc1\n*some content*"},
+ {filepath.FromSlash("sect/doc4.md"), "---\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[0]), src[1])
+
+ }
+
+ 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"
+ }
+
+ doc3Slashed := filepath.FromSlash("/sect/doc3.md")
+
+ sources := [][2]string{
+ {
+ filepath.FromSlash("sect/doc1.md"),
+ fmt.Sprintf(`Ref 2: {{< %s "sect/doc2.md" >}}`, refShortcode),
+ },
+ // Issue #1148: Make sure that no P-tags is added around shortcodes.
+ {
+ filepath.FromSlash("sect/doc2.md"),
+ fmt.Sprintf(`**Ref 1:**
+
+{{< %s "sect/doc1.md" >}}
+
+THE END.`, refShortcode),
+ },
+ // Issue #1753: Should not add a trailing newline after shortcode.
+ {
+ filepath.FromSlash("sect/doc3.md"),
+ fmt.Sprintf(`**Ref 1:**{{< %s "sect/doc3.md" >}}.`, refShortcode),
+ },
+ // Issue #3703
+ {
+ filepath.FromSlash("sect/doc4.md"),
+ fmt.Sprintf(`**Ref 1:**{{< %s "%s" >}}.`, refShortcode, doc3Slashed),
+ },
+ }
+
+ 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[0]), src[1])
+ }
+
+ s := buildSingleSite(
+ t,
+ deps.DepsCfg{
+ Fs: fs,
+ Cfg: cfg,
+ WithTemplate: createWithTemplateFromNameValues("_default/single.html", "{{.Content}}")},
+ BuildCfg{})
+
+ require.Len(t, s.RegularPages(), 4)
+
+ 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)},
+ {filepath.FromSlash(fmt.Sprintf("public/sect/doc4%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("blackfriday",
+ map[string]interface{}{
+ "plainIDAnchors": true})
+
+ cfg.Set("uglyURLs", uglyURLs)
+
+ sources := [][2]string{
+ {filepath.FromSlash("sect/doc1.md"), "---\nmarkup: markdown\n---\n# title\nsome *content*"},
+ {filepath.FromSlash("sect/doc2.md"), "---\nurl: /ugly.html\nmarkup: markdown\n---\n# title\ndoc2 *content*"},
+ }
+
+ for _, src := range sources {
+ writeSource(t, fs, filepath.Join("content", src[0]), src[1])
+ }
+
+ 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"), "<root>RSS</root>"},
+ {filepath.FromSlash("public/sitemap.xml"), "<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)
+ }
+ }
+
+}
+
+// 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} {
+ t.Run(fmt.Sprintf("canonify=%t,uglify=%t,pluralize=%t", canonify, uglify, pluralize), func(t *testing.T) {
+ 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 := [][2]string{
+ {filepath.FromSlash("sect/doc1.html"), "doc1"},
+ // Add one more page to sect to make sure sect is picked in mainSections
+ {filepath.FromSlash("sect/sect.html"), "sect"},
+ {filepath.FromSlash("Fish and Chips/doc2.html"), "doc2"},
+ {filepath.FromSlash("ラーメン/doc3.html"), "doc3"},
+ }
+
+ cfg, fs := newTestCfg()
+
+ cfg.Set("baseURL", "http://auth/sub/")
+ cfg.Set("uglyURLs", uglify)
+ cfg.Set("pluralizeListTitles", pluralize)
+ cfg.Set("canonifyURLs", canonify)
+
+ for _, src := range sources {
+ writeSource(t, fs, filepath.Join("content", src[0]), src[1])
+ }
+
+ writeSource(t, fs, filepath.Join("layouts", "_default/single.html"), "{{.Content}}")
+ writeSource(t, fs, filepath.Join("layouts", "_default/list.html"), "{{ .Kind }}|{{.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 := [][2]string{
+ {filepath.FromSlash("sect/doc1.html"), "---\nmarkup: markdown\n---\n# title\nsome *content*"},
+ {filepath.FromSlash("sect/doc2.html"), "<!doctype html><html><body>more content</body></html>"},
+ {filepath.FromSlash("sect/doc3.md"), "# doc3\n*some* content"},
+ {filepath.FromSlash("sect/doc4.md"), "---\ntitle: doc4\n---\n# doc4\n*some content*"},
+ {filepath.FromSlash("sect/doc5.html"), "<!doctype html><html>{{ template \"head\" }}<body>body5</body></html>"},
+ {filepath.FromSlash("sect/doc6.html"), "<!doctype html><html>{{ template \"head_abs\" }}<body>body5</body></html>"},
+ {filepath.FromSlash("doc7.html"), "<html><body>doc7 content</body></html>"},
+ {filepath.FromSlash("sect/doc8.html"), "---\nmarkup: md\n---\n# title\nsome *content*"},
+ // Issue #3021
+ {filepath.FromSlash("doc9.html"), "<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[0]), src[1])
+
+ }
+
+ 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 {
+ helpers.PrintFs(fs.Destination, "public", os.Stdout)
+ 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 := [][2]string{
+ {filepath.FromSlash("sect/doc1.html"), "<!doctype html><html><head></head><body><a href=\"#frag1\">link</a></body></html>"},
+ {filepath.FromSlash("blue/doc2.html"), "---\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[0]), src[1])
+
+ }
+
+ 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 = `+++
+weight = "2"
+title = "One"
+my_param = "foo"
+my_date = 1979-05-27T07:32:00Z
++++
+Front Matter with Ordered Pages`
+
+var weightedPage2 = `+++
+weight = "6"
+title = "Two"
+publishdate = "2012-03-05"
+my_param = "foo"
++++
+Front Matter with Ordered Pages 2`
+
+var weightedPage3 = `+++
+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 = `+++
+weight = "4"
+title = "Four"
+date = "2012-01-01"
+publishdate = "2012-01-01"
+my_param = "baz"
+my_date = 2010-05-27T07:32:00Z
+summary = "A _custom_ summary"
+categories = [ "hugo" ]
++++
+Front Matter with Ordered Pages 4. This is longer content`
+
+var weightedSources = [][2]string{
+ {filepath.FromSlash("sect/doc1.md"), weightedPage1},
+ {filepath.FromSlash("sect/doc2.md"), weightedPage2},
+ {filepath.FromSlash("sect/doc3.md"), weightedPage3},
+ {filepath.FromSlash("sect/doc4.md"), 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[0]), src[1])
+
+ }
+
+ s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
+
+ if s.getPage(page.KindSection, "sect").Pages()[1].Title() != "Three" || s.getPage(page.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 = [][2]string{
+ {filepath.FromSlash("sect1/doc1.md"), weightedPage1},
+ {filepath.FromSlash("sect1/doc2.md"), weightedPage2},
+ {filepath.FromSlash("sect2/doc3.md"), weightedPage3},
+ {filepath.FromSlash("sect3/doc4.md"), 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)
+ }
+
+ 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 = `+++
+tags = [ "a", "b", "c" ]
+tags_weight = 22
+categories = ["d"]
+title = "foo"
+categories_weight = 44
++++
+Front Matter with weighted tags and categories`
+
+var pageWithWeightedTaxonomies2 = `+++
+tags = "a"
+tags_weight = 33
+title = "bar"
+categories = [ "d", "e" ]
+categories_weight = 11.0
+alias = "spf13"
+date = 1979-05-27T07:32:00Z
++++
+Front Matter with weighted tags and categories`
+
+var pageWithWeightedTaxonomies3 = `+++
+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 := [][2]string{
+ {filepath.FromSlash("sect/doc1.md"), pageWithWeightedTaxonomies2},
+ {filepath.FromSlash("sect/doc2.md"), pageWithWeightedTaxonomies1},
+ {filepath.FromSlash("sect/doc3.md"), 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 setupLinkingMockSite(t *testing.T) *Site {
+ sources := [][2]string{
+ {filepath.FromSlash("level2/unique.md"), ""},
+ {filepath.FromSlash("_index.md"), ""},
+ {filepath.FromSlash("common.md"), ""},
+ {filepath.FromSlash("rootfile.md"), ""},
+ {filepath.FromSlash("root-image.png"), ""},
+
+ {filepath.FromSlash("level2/2-root.md"), ""},
+ {filepath.FromSlash("level2/common.md"), ""},
+
+ {filepath.FromSlash("level2/2-image.png"), ""},
+ {filepath.FromSlash("level2/common.png"), ""},
+
+ {filepath.FromSlash("level2/level3/start.md"), ""},
+ {filepath.FromSlash("level2/level3/_index.md"), ""},
+ {filepath.FromSlash("level2/level3/3-root.md"), ""},
+ {filepath.FromSlash("level2/level3/common.md"), ""},
+ {filepath.FromSlash("level2/level3/3-image.png"), ""},
+ {filepath.FromSlash("level2/level3/common.png"), ""},
+
+ {filepath.FromSlash("level2/level3/embedded.dot.md"), ""},
+ }
+
+ 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{}{})
+ 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 := site.getPage(page.KindPage, "level2/level3/start.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
+ }{
+ // different refs resolving to the same unique filename:
+ {"/level2/unique.md", "", true, "/level2/unique/"},
+ {"../unique.md", "", true, "/level2/unique/"},
+ {"unique.md", "", true, "/level2/unique/"},
+
+ {"level2/common.md", "", true, "/level2/common/"},
+ {"3-root.md", "", true, "/level2/level3/3-root/"},
+ {"../..", "", true, "/"},
+
+ // different refs resolving to the same ambiguous top-level filename:
+ {"../../common.md", "", true, "/common/"},
+ {"/common.md", "", true, "/common/"},
+
+ // different refs resolving to the same ambiguous level-2 filename:
+ {"/level2/common.md", "", true, "/level2/common/"},
+ {"../common.md", "", true, "/level2/common/"},
+ {"common.md", "", true, "/level2/level3/common/"},
+
+ // different refs resolving to the same section:
+ {"/level2", "", true, "/level2/"},
+ {"..", "", true, "/level2/"},
+ {"../", "", true, "/level2/"},
+
+ // different refs resolving to the same subsection:
+ {"/level2/level3", "", true, "/level2/level3/"},
+ {"/level2/level3/_index.md", "", true, "/level2/level3/"},
+ {".", "", true, "/level2/level3/"},
+ {"./", "", true, "/level2/level3/"},
+
+ // try to confuse parsing
+ {"embedded.dot.md", "", true, "/level2/level3/embedded.dot/"},
+
+ //test empty link, as well as fragment only link
+ {"", "", true, ""},
+ } {
+ checkLinkCase(site, test.link, currentPage, test.relative, test.outputFormat, test.expected, t, i)
+
+ //make sure fragment links are also handled
+ checkLinkCase(site, test.link+"#intro", currentPage, test.relative, test.outputFormat, test.expected+"#intro", t, i)
+ }
+
+ // TODO: and then the failure cases.
+}
+
+func checkLinkCase(site *Site, link string, currentPage page.Page, relative bool, outputFormat string, expected string, t *testing.T, i int) {
+ if out, err := site.refLink(link, currentPage, relative, outputFormat); err != nil || out != expected {
+ t.Fatalf("[%d] Expected %q from %q to resolve to %q, got %q - error: %s", i, link, currentPage.Path(), expected, out, err)
+ }
+}
diff --git a/hugolib/site_url_test.go b/hugolib/site_url_test.go
new file mode 100644
index 000000000..9827f994b
--- /dev/null
+++ b/hugolib/site_url_test.go
@@ -0,0 +1,186 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/resources/page"
+
+ "html/template"
+
+ "github.com/gohugoio/hugo/deps"
+ "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 = [][2]string{
+ {filepath.FromSlash("content/blue/doc1.md"), slugDoc1},
+ {filepath.FromSlash("content/blue/doc2.md"), 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)
+ require.NoError(t, 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)
+ }
+ }
+}
+
+func TestUglyURLsPerSection(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ const dt = `---
+title: Do not go gentle into that good night
+---
+
+Wild men who caught and sang the sun in flight,
+And learn, too late, they grieved it on its way,
+Do not go gentle into that good night.
+
+`
+
+ cfg, fs := newTestCfg()
+
+ cfg.Set("uglyURLs", map[string]bool{
+ "sect2": true,
+ })
+
+ writeSource(t, fs, filepath.Join("content", "sect1", "p1.md"), dt)
+ writeSource(t, fs, filepath.Join("content", "sect2", "p2.md"), dt)
+
+ s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
+
+ assert.Len(s.RegularPages(), 2)
+
+ notUgly := s.getPage(page.KindPage, "sect1/p1.md")
+ assert.NotNil(notUgly)
+ assert.Equal("sect1", notUgly.Section())
+ assert.Equal("/sect1/p1/", notUgly.RelPermalink())
+
+ ugly := s.getPage(page.KindPage, "sect2/p2.md")
+ assert.NotNil(ugly)
+ assert.Equal("sect2", ugly.Section())
+ assert.Equal("/sect2/p2.html", ugly.RelPermalink())
+}
+
+func TestSectionWithURLInFrontMatter(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ const st = `---
+title: Do not go gentle into that good night
+url: %s
+---
+
+Wild men who caught and sang the sun in flight,
+And learn, too late, they grieved it on its way,
+Do not go gentle into that good night.
+
+`
+
+ const pt = `---
+title: Wild men who caught and sang the sun in flight
+---
+
+Wild men who caught and sang the sun in flight,
+And learn, too late, they grieved it on its way,
+Do not go gentle into that good night.
+
+`
+
+ cfg, fs := newTestCfg()
+ th := testHelper{cfg, fs, t}
+
+ cfg.Set("paginate", 1)
+
+ writeSource(t, fs, filepath.Join("content", "sect1", "_index.md"), fmt.Sprintf(st, "/ss1/"))
+ writeSource(t, fs, filepath.Join("content", "sect2", "_index.md"), fmt.Sprintf(st, "/ss2/"))
+
+ for i := 0; i < 5; i++ {
+ writeSource(t, fs, filepath.Join("content", "sect1", fmt.Sprintf("p%d.md", i+1)), pt)
+ writeSource(t, fs, filepath.Join("content", "sect2", fmt.Sprintf("p%d.md", i+1)), pt)
+ }
+
+ 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>")
+
+ s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{})
+
+ assert.Len(s.RegularPages(), 10)
+
+ sect1 := s.getPage(page.KindSection, "sect1")
+ assert.NotNil(sect1)
+ assert.Equal("/ss1/", sect1.RelPermalink())
+ th.assertFileContent(filepath.Join("public", "ss1", "index.html"), "P1|URL: /ss1/|Next: /ss1/page/2/")
+ th.assertFileContent(filepath.Join("public", "ss1", "page", "2", "index.html"), "P2|URL: /ss1/page/2/|Next: /ss1/page/3/")
+
+}
diff --git a/hugolib/sitemap_test.go b/hugolib/sitemap_test.go
new file mode 100644
index 000000000..5aba6f09d
--- /dev/null
+++ b/hugolib/sitemap_test.go
@@ -0,0 +1,121 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/tpl"
+ "github.com/stretchr/testify/require"
+)
+
+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 := config.Sitemap{Priority: 3.0, Filename: "doo.xml", ChangeFreq: "3"}
+ input := map[string]interface{}{
+ "changefreq": "3",
+ "priority": 3.0,
+ "filename": "doo.xml",
+ "unknown": "ignore",
+ }
+ result := config.DecodeSitemap(config.Sitemap{}, input)
+
+ if !reflect.DeepEqual(expected, result) {
+ t.Errorf("Got \n%v expected \n%v", result, expected)
+ }
+
+}
+
+// https://github.com/gohugoio/hugo/issues/5910
+func TestSitemapOutputFormats(t *testing.T) {
+
+ b := newTestSitesBuilder(t).WithSimpleConfigFile()
+
+ b.WithContent("blog/html-amp.md", `
+---
+Title: AMP and HTML
+outputs: [ "html", "amp" ]
+---
+
+`)
+
+ b.Build(BuildCfg{})
+
+ // Should link to the HTML version.
+ b.AssertFileContent("public/sitemap.xml", " <loc>http://example.com/blog/html-amp/</loc>")
+}
diff --git a/hugolib/taxonomy.go b/hugolib/taxonomy.go
new file mode 100644
index 000000000..e6c80161a
--- /dev/null
+++ b/hugolib/taxonomy.go
@@ -0,0 +1,248 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "sort"
+
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/resources/resource"
+)
+
+// 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'] = page.WeightedPages
+// TagTaxonomy['go'] = page.WeightedPages
+type Taxonomy map[string]page.WeightedPages
+
+// 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, page.WeightedPages: TaxonomyPages}
+type OrderedTaxonomyEntry struct {
+ Name string
+ page.WeightedPages
+}
+
+// Get the weighted pages for the given key.
+func (i Taxonomy) Get(key string) page.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 page.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() page.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])
+}
+
+// taxonomyNodeInfo stores additional metadata about a taxonomy.
+type taxonomyNodeInfo struct {
+ plural string
+
+ // Maps "tags" to "tag".
+ singular string
+
+ // The term key as used in the taxonomy map, e.g "tag1".
+ // The value is normalized for paths, but may or not be lowercased
+ // depending on the disablePathToLower setting.
+ termKey string
+
+ // The original, unedited term name. Useful for titles etc.
+ term string
+
+ dates resource.Dates
+
+ parent *taxonomyNodeInfo
+
+ // Either of Kind taxonomyTerm (parent) or taxonomy
+ owner *page.PageWrapper
+}
+
+func (t *taxonomyNodeInfo) UpdateFromPage(p page.Page) {
+
+ // Select the latest dates
+ t.dates.UpdateDateAndLastmodIfAfter(p)
+}
+
+func (t *taxonomyNodeInfo) TransferValues(p *pageState) {
+ t.owner.Page = p
+ if p.Lastmod().IsZero() && p.Date().IsZero() {
+ p.m.Dates.UpdateDateAndLastmodIfAfter(t.dates)
+ }
+}
+
+// Maps either plural or plural/term to a taxonomy node.
+// TODO(bep) consolidate somehow with s.Taxonomies
+type taxonomyNodeInfos struct {
+ m map[string]*taxonomyNodeInfo
+ getKey func(string) string
+}
+
+// map[string]*taxonomyNodeInfo
+func (t taxonomyNodeInfos) key(parts ...string) string {
+ return path.Join(parts...)
+}
+
+// GetOrAdd will get or create and add a new taxonomy node to the parent identified with plural.
+// It will panic if the parent does not exist.
+func (t taxonomyNodeInfos) GetOrAdd(plural, term string) *taxonomyNodeInfo {
+ parent := t.GetOrCreate(plural, "")
+ if parent == nil {
+ panic(fmt.Sprintf("no parent found with plural %q", plural))
+ }
+ child := t.GetOrCreate(plural, term)
+ child.parent = parent
+ return child
+}
+
+func (t taxonomyNodeInfos) GetOrCreate(plural, term string) *taxonomyNodeInfo {
+ termKey := t.getKey(term)
+ key := t.key(plural, termKey)
+
+ n, found := t.m[key]
+ if found {
+ return n
+ }
+
+ n = &taxonomyNodeInfo{
+ plural: plural,
+ termKey: termKey,
+ term: term,
+ owner: &page.PageWrapper{}, // Page will be assigned later.
+ }
+
+ t.m[key] = n
+
+ return n
+}
+
+func (t taxonomyNodeInfos) Get(sections ...string) *taxonomyNodeInfo {
+ key := t.key(sections...)
+
+ n, found := t.m[key]
+ if found {
+ return n
+ }
+
+ return nil
+}
diff --git a/hugolib/taxonomy_test.go b/hugolib/taxonomy_test.go
new file mode 100644
index 000000000..f4902ae8d
--- /dev/null
+++ b/hugolib/taxonomy_test.go
@@ -0,0 +1,328 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+
+ "github.com/gohugoio/hugo/resources/page"
+
+ "reflect"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/gohugoio/hugo/deps"
+)
+
+func TestTaxonomiesCountOrder(t *testing.T) {
+ t.Parallel()
+ taxonomies := make(map[string]string)
+
+ taxonomies["tag"] = "tags"
+ taxonomies["category"] = "categories"
+
+ cfg, fs := newTestCfg()
+
+ cfg.Set("taxonomies", taxonomies)
+
+ const pageContent = `---
+tags: ['a', 'B', 'c']
+categories: 'd'
+---
+YAML frontmatter with tags and categories taxonomy.`
+
+ writeSource(t, fs, filepath.Join("content", "page.md"), pageContent)
+
+ 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.Page().Title()+":"+t.Name)
+ }
+
+ expect := []string{"a:a", "B:b", "c:c"}
+
+ if !reflect.DeepEqual(st, expect) {
+ t.Fatalf("ordered taxonomies mismatch, expected\n%v\ngot\n%q", expect, st)
+ }
+}
+
+//
+func TestTaxonomiesWithAndWithoutContentFile(t *testing.T) {
+ for _, uglyURLs := range []bool{false, true} {
+ t.Run(fmt.Sprintf("uglyURLs=%t", uglyURLs), func(t *testing.T) {
+ doTestTaxonomiesWithAndWithoutContentFile(t, uglyURLs)
+ })
+ }
+}
+
+func doTestTaxonomiesWithAndWithoutContentFile(t *testing.T, uglyURLs bool) {
+ t.Parallel()
+
+ siteConfig := `
+baseURL = "http://example.com/blog"
+uglyURLs = %t
+paginate = 1
+defaultContentLanguage = "en"
+[Taxonomies]
+tag = "tags"
+category = "categories"
+other = "others"
+empty = "empties"
+permalinked = "permalinkeds"
+[permalinks]
+permalinkeds = "/perma/:slug/"
+`
+
+ pageTemplate := `---
+title: "%s"
+tags:
+%s
+categories:
+%s
+others:
+%s
+permalinkeds:
+%s
+---
+# Doc
+`
+
+ siteConfig = fmt.Sprintf(siteConfig, uglyURLs)
+
+ th, h := newTestSitesFromConfigWithDefaultTemplates(t, siteConfig)
+ require.Len(t, h.Sites, 1)
+
+ fs := th.Fs
+
+ writeSource(t, fs, "content/p1.md", fmt.Sprintf(pageTemplate, "t1/c1", "- Tag1", "- cAt1", "- o1", "- Pl1"))
+ writeSource(t, fs, "content/p2.md", fmt.Sprintf(pageTemplate, "t2/c1", "- tag2", "- cAt1", "- o1", "- Pl1"))
+ writeSource(t, fs, "content/p3.md", fmt.Sprintf(pageTemplate, "t2/c12", "- tag2", "- cat2", "- o1", "- Pl1"))
+ writeSource(t, fs, "content/p4.md", fmt.Sprintf(pageTemplate, "Hello World", "", "", "- \"Hello Hugo world\"", "- Pl1"))
+
+ writeNewContentFile(t, fs.Source, "Category Terms", "2017-01-01", "content/categories/_index.md", 10)
+ writeNewContentFile(t, fs.Source, "Tag1 List", "2017-01-01", "content/tags/Tag1/_index.md", 10)
+
+ // https://github.com/gohugoio/hugo/issues/5847
+ writeNewContentFile(t, fs.Source, "Unused Tag List", "2018-01-01", "content/tags/not-used/_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.
+ // 4. the "permalinkeds" taxonomy with permalinks configuration.
+
+ 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")
+
+ // 4.
+ th.assertFileContent(pathFunc("public/perma/pl1/index.html"), "List", "Pl1")
+
+ // This looks kind of funky, but the taxonomy terms do not have a permalinks definition,
+ // for good reasons.
+ th.assertFileContent(pathFunc("public/permalinkeds/index.html"), "Terms List", "Permalinkeds")
+
+ s := h.Sites[0]
+
+ // Make sure that each page.KindTaxonomyTerm page has an appropriate number
+ // of page.KindTaxonomy pages in its Pages slice.
+ taxonomyTermPageCounts := map[string]int{
+ "tags": 3,
+ "categories": 2,
+ "others": 2,
+ "empties": 0,
+ "permalinkeds": 1,
+ }
+
+ for taxonomy, count := range taxonomyTermPageCounts {
+ term := s.getPage(page.KindTaxonomyTerm, taxonomy)
+ require.NotNil(t, term)
+ require.Len(t, term.Pages(), count)
+
+ for _, p := range term.Pages() {
+ require.Equal(t, page.KindTaxonomy, p.Kind())
+ }
+ }
+
+ cat1 := s.getPage(page.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())
+ }
+
+ pl1 := s.getPage(page.KindTaxonomy, "permalinkeds", "pl1")
+ permalinkeds := s.getPage(page.KindTaxonomyTerm, "permalinkeds")
+ require.NotNil(t, pl1)
+ require.NotNil(t, permalinkeds)
+ if uglyURLs {
+ require.Equal(t, "/blog/perma/pl1.html", pl1.RelPermalink())
+ require.Equal(t, "/blog/permalinkeds.html", permalinkeds.RelPermalink())
+ } else {
+ require.Equal(t, "/blog/perma/pl1/", pl1.RelPermalink())
+ require.Equal(t, "/blog/permalinkeds/", permalinkeds.RelPermalink())
+ }
+
+ helloWorld := s.getPage(page.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")
+
+}
+
+// https://github.com/gohugoio/hugo/issues/5513
+// https://github.com/gohugoio/hugo/issues/5571
+func TestTaxonomiesPathSeparation(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ config := `
+baseURL = "https://example.com"
+[taxonomies]
+"news/tag" = "news/tags"
+"news/category" = "news/categories"
+"t1/t2/t3" = "t1/t2/t3s"
+"s1/s2/s3" = "s1/s2/s3s"
+`
+
+ pageContent := `
++++
+title = "foo"
+"news/categories" = ["a", "b", "c", "d/e", "f/g/h"]
+"t1/t2/t3s" = ["t4/t5", "t4/t5/t6"]
++++
+Content.
+`
+
+ b := newTestSitesBuilder(t)
+ b.WithConfigFile("toml", config)
+ b.WithContent("page.md", pageContent)
+ b.WithContent("news/categories/b/_index.md", `
+---
+title: "This is B"
+---
+`)
+
+ b.WithContent("news/categories/f/g/h/_index.md", `
+---
+title: "This is H"
+---
+`)
+
+ b.WithContent("t1/t2/t3s/t4/t5/_index.md", `
+---
+title: "This is T5"
+---
+`)
+
+ b.WithContent("s1/s2/s3s/_index.md", `
+---
+title: "This is S3s"
+---
+`)
+
+ b.CreateSites().Build(BuildCfg{})
+
+ s := b.H.Sites[0]
+
+ ta := s.findPagesByKind(page.KindTaxonomy)
+ te := s.findPagesByKind(page.KindTaxonomyTerm)
+
+ assert.Equal(4, len(te))
+ assert.Equal(7, len(ta))
+
+ b.AssertFileContent("public/news/categories/a/index.html", "Taxonomy List Page 1|a|Hello|https://example.com/news/categories/a/|")
+ b.AssertFileContent("public/news/categories/b/index.html", "Taxonomy List Page 1|This is B|Hello|https://example.com/news/categories/b/|")
+ b.AssertFileContent("public/news/categories/d/e/index.html", "Taxonomy List Page 1|d/e|Hello|https://example.com/news/categories/d/e/|")
+ b.AssertFileContent("public/news/categories/f/g/h/index.html", "Taxonomy List Page 1|This is H|Hello|https://example.com/news/categories/f/g/h/|")
+ b.AssertFileContent("public/t1/t2/t3s/t4/t5/index.html", "Taxonomy List Page 1|This is T5|Hello|https://example.com/t1/t2/t3s/t4/t5/|")
+ b.AssertFileContent("public/t1/t2/t3s/t4/t5/t6/index.html", "Taxonomy List Page 1|t4/t5/t6|Hello|https://example.com/t1/t2/t3s/t4/t5/t6/|")
+
+ b.AssertFileContent("public/news/categories/index.html", "Taxonomy Term Page 1|News/Categories|Hello|https://example.com/news/categories/|")
+ b.AssertFileContent("public/t1/t2/t3s/index.html", "Taxonomy Term Page 1|T1/T2/T3s|Hello|https://example.com/t1/t2/t3s/|")
+ b.AssertFileContent("public/s1/s2/s3s/index.html", "Taxonomy Term Page 1|This is S3s|Hello|https://example.com/s1/s2/s3s/|")
+
+}
+
+// https://github.com/gohugoio/hugo/issues/5719
+func TestTaxonomiesNextGenLoops(t *testing.T) {
+ b := newTestSitesBuilder(t).WithSimpleConfigFile()
+
+ b.WithTemplatesAdded("index.html", `
+<h1>Tags</h1>
+<ul>
+ {{ range .Site.Taxonomies.tags }}
+ <li><a href="{{ .Page.Permalink }}">{{ .Page.Title }}</a> {{ .Count }}</li>
+ {{ end }}
+</ul>
+
+`)
+
+ b.WithTemplatesAdded("_default/terms.html", `
+<h1>Terms</h1>
+<ul>
+ {{ range .Data.Terms.Alphabetical }}
+ <li><a href="{{ .Page.Permalink }}">{{ .Page.Title }}</a> {{ .Count }}</li>
+ {{ end }}
+</ul>
+`)
+
+ for i := 0; i < 10; i++ {
+ b.WithContent(fmt.Sprintf("page%d.md", i+1), `
+---
+Title: "Taxonomy!"
+tags: ["Hugo Rocks!", "Rocks I say!" ]
+categories: ["This is Cool", "And new" ]
+---
+
+Content.
+
+ `)
+ }
+
+ b.CreateSites().Build(BuildCfg{})
+
+ b.AssertFileContent("public/index.html", `<li><a href="http://example.com/tags/hugo-rocks/">Hugo Rocks!</a> 10</li>`)
+ b.AssertFileContent("public/categories/index.html", `<li><a href="http://example.com/categories/this-is-cool/">This is Cool</a> 10</li>`)
+ b.AssertFileContent("public/tags/index.html", `<li><a href="http://example.com/tags/rocks-i-say/">Rocks I say!</a> 10</li>`)
+
+}
diff --git a/hugolib/template_engines_test.go b/hugolib/template_engines_test.go
new file mode 100644
index 000000000..6a046c9f5
--- /dev/null
+++ b/hugolib/template_engines_test.go
@@ -0,0 +1,108 @@
+// 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, ".IsNamedParams", "IsNamedParams", -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},
+ } {
+ t.Run(config.suffix,
+ func(t *testing.T) {
+ doTestTemplateEngine(t, config.suffix, config.templateFixer)
+ })
+ }
+
+}
+
+func doTestTemplateEngine(t *testing.T, suffix string, templateFixer func(s string) string) {
+
+ cfg, fs := newTestCfg()
+
+ t.Log("Testing", suffix)
+
+ templTemplate := `
+p
+ |
+ | Page Title: {{ .Title }}
+ br
+ | Page Content: {{ .Content }}
+ br
+ | {{ title "hello world" }}
+
+`
+
+ templShortcodeTemplate := `
+p
+ |
+ | Shortcode: {{ .IsNamedParams }}
+`
+
+ templ := templateFixer(templTemplate)
+ shortcodeTempl := templateFixer(templShortcodeTemplate)
+
+ writeSource(t, fs, filepath.Join("content", "p.md"), `
+---
+title: My Title
+---
+My Content
+
+Shortcode: {{< myShort >}}
+
+`)
+
+ writeSource(t, fs, filepath.Join("layouts", "_default", fmt.Sprintf("single.%s", suffix)), templ)
+ writeSource(t, fs, filepath.Join("layouts", "shortcodes", fmt.Sprintf("myShort.%s", suffix)), shortcodeTempl)
+
+ 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",
+ "Shortcode: false",
+ )
+
+}
diff --git a/hugolib/template_test.go b/hugolib/template_test.go
new file mode 100644
index 000000000..3ec81323b
--- /dev/null
+++ b/hugolib/template_test.go
@@ -0,0 +1,307 @@
+// 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 _, this := range []struct {
+ name string
+ 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 site 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 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 site 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 }}`)
+ writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "index.html"), `{{define "main"}}index{{ end }}`)
+
+ },
+ func(t *testing.T) {
+ th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "Base: list")
+ th.assertFileContent(filepath.Join("public", "index.html"), "Base: index") // Issue #3505
+ },
+ },
+ {
+ "Variant 4, theme, use themes 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")
+ },
+ },
+ {
+ // Issue #3116
+ "Test section list and single template selection",
+ 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")
+ },
+ },
+ {
+ // Issue #2995
+ "Test section list and single template selection with base template",
+ 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")
+ },
+ },
+ } {
+
+ 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(this.name)
+ this.assert(t)
+
+ }
+}
+
+// https://github.com/gohugoio/hugo/issues/4895
+func TestTemplateBOM(t *testing.T) {
+
+ b := newTestSitesBuilder(t).WithSimpleConfigFile()
+ bom := "\ufeff"
+
+ b.WithTemplatesAdded(
+ "_default/baseof.html", bom+`
+ Base: {{ block "main" . }}base main{{ end }}`,
+ "_default/single.html", bom+`{{ define "main" }}Hi!?{{ end }}`)
+
+ b.WithContent("page.md", `---
+title: "Page"
+---
+
+Page Content
+`)
+
+ b.CreateSites().Build(BuildCfg{})
+
+ b.AssertFileContent("public/page/index.html", "Base: Hi!?")
+
+}
+
+func TestTemplateFuncs(t *testing.T) {
+
+ b := newTestSitesBuilder(t).WithDefaultMultiSiteConfig()
+
+ homeTpl := `Site: {{ site.Language.Lang }} / {{ .Site.Language.Lang }} / {{ site.BaseURL }}
+Sites: {{ site.Sites.First.Home.Language.Lang }}
+Hugo: {{ hugo.Generator }}
+`
+
+ b.WithTemplatesAdded(
+ "index.html", homeTpl,
+ "index.fr.html", homeTpl,
+ )
+
+ b.CreateSites().Build(BuildCfg{})
+
+ b.AssertFileContent("public/en/index.html",
+ "Site: en / en / http://example.com/blog",
+ "Sites: en",
+ "Hugo: <meta name=\"generator\" content=\"Hugo")
+ b.AssertFileContent("public/fr/index.html",
+ "Site: fr / fr / http://example.com/blog",
+ "Sites: en",
+ "Hugo: <meta name=\"generator\" content=\"Hugo",
+ )
+
+}
+
+func TestPartialWithReturn(t *testing.T) {
+
+ b := newTestSitesBuilder(t).WithSimpleConfigFile()
+
+ b.WithTemplatesAdded(
+ "index.html", `
+Test Partials With Return Values:
+
+add42: 50: {{ partial "add42.tpl" 8 }}
+dollarContext: 60: {{ partial "dollarContext.tpl" 18 }}
+adder: 70: {{ partial "dict.tpl" (dict "adder" 28) }}
+complex: 80: {{ partial "complex.tpl" 38 }}
+`,
+ "partials/add42.tpl", `
+ {{ $v := add . 42 }}
+ {{ return $v }}
+ `,
+ "partials/dollarContext.tpl", `
+{{ $v := add $ 42 }}
+{{ return $v }}
+`,
+ "partials/dict.tpl", `
+{{ $v := add $.adder 42 }}
+{{ return $v }}
+`,
+ "partials/complex.tpl", `
+{{ return add . 42 }}
+`,
+ )
+
+ b.CreateSites().Build(BuildCfg{})
+
+ b.AssertFileContent("public/index.html",
+ "add42: 50: 50",
+ "dollarContext: 60: 60",
+ "adder: 70: 70",
+ "complex: 80: 80",
+ )
+
+}
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的轻度使用者。
+
+虽然译者已经很认真地对待这次翻译,但是限于水平有限,肯定会有不少错漏,如果发现该书的翻译有什么需要修改,可以通过他的邮箱与他联系。他的邮箱是<jasonlai256@gmail.com>。
+
+### 致谢
+
+必须特别感谢[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:leto@dune.gov "{id: 9001, email: 'leto@dune.gov', ...}"
+ set users:9001 "{id: 9001, email: 'leto@dune.gov', ...}"
+
+这种方法很糟糕,如此不但会产生两倍数量的内存,而且这将会成为数据管理的恶梦。
+
+如果Redis允许你将一个关键字链接到另一个的话,可能情况会好很多,可惜Redis并没有提供这样的功能(而且很可能永远都不会提供)。Redis发展到现在,其开发的首要目的是要保持代码和API的整洁简单,关键字链接功能的内部实现并不符合这个前提(对于关键字,我们还有很多相关方法没有谈论到)。其实,Redis已经提供了解决的方法:散列。
+
+使用散列数据结构,我们可以摆脱重复的缠绕:
+
+ set users:9001 "{id: 9001, email: leto@dune.gov, ...}"
+ hset users:lookup:email leto@dune.gov 9001
+
+我们所做的是,使用域来作为一个二级索引,然后去引用单个用户对象。要通过id来获取用户信息,我们可以使用一个普通的`get`命令:
+
+ get users:9001
+
+而如果想通过电子邮箱来获取用户信息,我们可以使用`hget`命令再配合使用`get`命令(Ruby代码):
+
+ id = redis.hget('users:lookup:email', 'leto@dune.gov')
+ 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/testdata/sunset.jpg b/hugolib/testdata/sunset.jpg
new file mode 100644
index 000000000..7d7307bed
--- /dev/null
+++ b/hugolib/testdata/sunset.jpg
Binary files differ
diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go
new file mode 100644
index 000000000..fe0f824f2
--- /dev/null
+++ b/hugolib/testhelpers_test.go
@@ -0,0 +1,800 @@
+package hugolib
+
+import (
+ "io"
+ "io/ioutil"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "testing"
+ "unicode/utf8"
+
+ "bytes"
+ "fmt"
+ "regexp"
+ "strings"
+ "text/template"
+
+ "github.com/fsnotify/fsnotify"
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/sanity-io/litter"
+ "github.com/spf13/afero"
+ "github.com/spf13/cast"
+
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/tpl"
+ "github.com/spf13/viper"
+
+ "os"
+
+ "github.com/gohugoio/hugo/resources/resource"
+
+ "github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type sitesBuilder struct {
+ Cfg config.Provider
+ Fs *hugofs.Fs
+ T testing.TB
+
+ *require.Assertions
+
+ logger *loggers.Logger
+
+ dumper litter.Options
+
+ // Used to test partial rebuilds.
+ changedFiles []string
+
+ // Aka the Hugo server mode.
+ running bool
+
+ H *HugoSites
+
+ theme string
+
+ // Default toml
+ configFormat string
+
+ // Default is empty.
+ // TODO(bep) revisit this and consider always setting it to something.
+ // Consider this in relation to using the BaseFs.PublishFs to all publishing.
+ workingDir string
+
+ // Base data/content
+ contentFilePairs []string
+ templateFilePairs []string
+ i18nFilePairs []string
+ dataFilePairs []string
+
+ // Additional data/content.
+ // As in "use the base, but add these on top".
+ contentFilePairsAdded []string
+ templateFilePairsAdded []string
+ i18nFilePairsAdded []string
+ dataFilePairsAdded []string
+}
+
+func newTestSitesBuilder(t testing.TB) *sitesBuilder {
+ v := viper.New()
+ fs := hugofs.NewMem(v)
+
+ litterOptions := litter.Options{
+ HidePrivateFields: true,
+ StripPackageNames: true,
+ Separator: " ",
+ }
+
+ return &sitesBuilder{T: t, Assertions: require.New(t), Fs: fs, configFormat: "toml", dumper: litterOptions}
+}
+
+func createTempDir(prefix string) (string, func(), error) {
+ workDir, err := ioutil.TempDir("", prefix)
+ if err != nil {
+ return "", nil, err
+ }
+
+ if runtime.GOOS == "darwin" && !strings.HasPrefix(workDir, "/private") {
+ // To get the entry folder in line with the rest. This its a little bit
+ // mysterious, but so be it.
+ workDir = "/private" + workDir
+ }
+ return workDir, func() { os.RemoveAll(workDir) }, nil
+}
+
+func (s *sitesBuilder) Running() *sitesBuilder {
+ s.running = true
+ return s
+}
+
+func (s *sitesBuilder) WithLogger(logger *loggers.Logger) *sitesBuilder {
+ s.logger = logger
+ return s
+}
+
+func (s *sitesBuilder) WithWorkingDir(dir string) *sitesBuilder {
+ s.workingDir = dir
+ return s
+}
+
+func (s *sitesBuilder) WithConfigTemplate(data interface{}, format, configTemplate string) *sitesBuilder {
+ if format == "" {
+ format = "toml"
+ }
+
+ templ, err := template.New("test").Parse(configTemplate)
+ if err != nil {
+ s.Fatalf("Template parse failed: %s", err)
+ }
+ var b bytes.Buffer
+ templ.Execute(&b, data)
+ return s.WithConfigFile(format, b.String())
+}
+
+func (s *sitesBuilder) WithViper(v *viper.Viper) *sitesBuilder {
+ loadDefaultSettingsFor(v)
+ s.Cfg = v
+
+ return s
+}
+
+func (s *sitesBuilder) WithConfigFile(format, conf string) *sitesBuilder {
+ writeSource(s.T, s.Fs, "config."+format, conf)
+ s.configFormat = format
+ return s
+}
+
+func (s *sitesBuilder) WithThemeConfigFile(format, conf string) *sitesBuilder {
+ if s.theme == "" {
+ s.theme = "test-theme"
+ }
+ filename := filepath.Join("themes", s.theme, "config."+format)
+ writeSource(s.T, s.Fs, filename, conf)
+ return s
+}
+
+func (s *sitesBuilder) WithSourceFile(filename, content string) *sitesBuilder {
+ writeSource(s.T, s.Fs, filepath.FromSlash(filename), content)
+ return s
+}
+
+const commonConfigSections = `
+
+[services]
+[services.disqus]
+shortname = "disqus_shortname"
+[services.googleAnalytics]
+id = "ga_id"
+
+[privacy]
+[privacy.disqus]
+disable = false
+[privacy.googleAnalytics]
+respectDoNotTrack = true
+anonymizeIP = true
+[privacy.instagram]
+simple = true
+[privacy.twitter]
+enableDNT = true
+[privacy.vimeo]
+disable = false
+[privacy.youtube]
+disable = false
+privacyEnhanced = true
+
+`
+
+func (s *sitesBuilder) WithSimpleConfigFile() *sitesBuilder {
+ return s.WithSimpleConfigFileAndBaseURL("http://example.com/")
+}
+
+func (s *sitesBuilder) WithSimpleConfigFileAndBaseURL(baseURL string) *sitesBuilder {
+ config := fmt.Sprintf("baseURL = %q", baseURL)
+
+ config = config + commonConfigSections
+ return s.WithConfigFile("toml", config)
+}
+
+func (s *sitesBuilder) WithDefaultMultiSiteConfig() *sitesBuilder {
+ var defaultMultiSiteConfig = `
+baseURL = "http://example.com/blog"
+
+paginate = 1
+disablePathToLower = true
+defaultContentLanguage = "en"
+defaultContentLanguageInSubdir = true
+
+[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"
+` + commonConfigSections
+
+ return s.WithConfigFile("toml", defaultMultiSiteConfig)
+
+}
+
+func (s *sitesBuilder) WithSunset(in string) {
+ // Write a real image into one of the bundle above.
+ src, err := os.Open(filepath.FromSlash("testdata/sunset.jpg"))
+ s.NoError(err)
+
+ out, err := s.Fs.Source.Create(filepath.FromSlash(in))
+ s.NoError(err)
+
+ _, err = io.Copy(out, src)
+ s.NoError(err)
+
+ out.Close()
+ src.Close()
+}
+
+func (s *sitesBuilder) WithContent(filenameContent ...string) *sitesBuilder {
+ s.contentFilePairs = append(s.contentFilePairs, filenameContent...)
+ return s
+}
+
+func (s *sitesBuilder) WithContentAdded(filenameContent ...string) *sitesBuilder {
+ s.contentFilePairsAdded = append(s.contentFilePairsAdded, filenameContent...)
+ return s
+}
+
+func (s *sitesBuilder) WithTemplates(filenameContent ...string) *sitesBuilder {
+ s.templateFilePairs = append(s.templateFilePairs, filenameContent...)
+ return s
+}
+
+func (s *sitesBuilder) WithTemplatesAdded(filenameContent ...string) *sitesBuilder {
+ s.templateFilePairsAdded = append(s.templateFilePairsAdded, filenameContent...)
+ return s
+}
+
+func (s *sitesBuilder) WithData(filenameContent ...string) *sitesBuilder {
+ s.dataFilePairs = append(s.dataFilePairs, filenameContent...)
+ return s
+}
+
+func (s *sitesBuilder) WithDataAdded(filenameContent ...string) *sitesBuilder {
+ s.dataFilePairsAdded = append(s.dataFilePairsAdded, filenameContent...)
+ return s
+}
+
+func (s *sitesBuilder) WithI18n(filenameContent ...string) *sitesBuilder {
+ s.i18nFilePairs = append(s.i18nFilePairs, filenameContent...)
+ return s
+}
+
+func (s *sitesBuilder) WithI18nAdded(filenameContent ...string) *sitesBuilder {
+ s.i18nFilePairsAdded = append(s.i18nFilePairsAdded, filenameContent...)
+ return s
+}
+
+func (s *sitesBuilder) EditFiles(filenameContent ...string) *sitesBuilder {
+ var changedFiles []string
+ for i := 0; i < len(filenameContent); i += 2 {
+ filename, content := filepath.FromSlash(filenameContent[i]), filenameContent[i+1]
+ changedFiles = append(changedFiles, filename)
+ writeSource(s.T, s.Fs, filename, content)
+
+ }
+ s.changedFiles = changedFiles
+
+ return s
+}
+
+func (s *sitesBuilder) writeFilePairs(folder string, filenameContent []string) *sitesBuilder {
+ if len(filenameContent)%2 != 0 {
+ s.Fatalf("expect filenameContent for %q in pairs (%d)", folder, len(filenameContent))
+ }
+ for i := 0; i < len(filenameContent); i += 2 {
+ filename, content := filenameContent[i], filenameContent[i+1]
+ target := folder
+ // TODO(bep) clean up this magic.
+ if strings.HasPrefix(filename, folder) {
+ target = ""
+ }
+
+ if s.workingDir != "" {
+ target = filepath.Join(s.workingDir, target)
+ }
+
+ writeSource(s.T, s.Fs, filepath.Join(target, filename), content)
+ }
+ return s
+}
+
+func (s *sitesBuilder) CreateSites() *sitesBuilder {
+ if err := s.CreateSitesE(); err != nil {
+ s.Fatalf("Failed to create sites: %s", err)
+ }
+
+ return s
+}
+
+func (s *sitesBuilder) LoadConfig() error {
+ cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: s.Fs.Source, Filename: "config." + s.configFormat})
+ if err != nil {
+ return err
+ }
+ s.Cfg = cfg
+ return nil
+}
+
+func (s *sitesBuilder) CreateSitesE() error {
+ s.addDefaults()
+ s.writeFilePairs("content", s.contentFilePairs)
+ s.writeFilePairs("content", s.contentFilePairsAdded)
+ s.writeFilePairs("layouts", s.templateFilePairs)
+ s.writeFilePairs("layouts", s.templateFilePairsAdded)
+ s.writeFilePairs("data", s.dataFilePairs)
+ s.writeFilePairs("data", s.dataFilePairsAdded)
+ s.writeFilePairs("i18n", s.i18nFilePairs)
+ s.writeFilePairs("i18n", s.i18nFilePairsAdded)
+
+ if s.Cfg == nil {
+ if err := s.LoadConfig(); err != nil {
+ return err
+ }
+ }
+
+ sites, err := NewHugoSites(deps.DepsCfg{Fs: s.Fs, Cfg: s.Cfg, Logger: s.logger, Running: s.running})
+ if err != nil {
+ return err
+ }
+ s.H = sites
+
+ return nil
+}
+
+func (s *sitesBuilder) BuildE(cfg BuildCfg) error {
+ if s.H == nil {
+ s.CreateSites()
+ }
+
+ return s.H.Build(cfg)
+}
+
+func (s *sitesBuilder) Build(cfg BuildCfg) *sitesBuilder {
+ return s.build(cfg, false)
+}
+
+func (s *sitesBuilder) BuildFail(cfg BuildCfg) *sitesBuilder {
+ return s.build(cfg, true)
+}
+
+func (s *sitesBuilder) changeEvents() []fsnotify.Event {
+ if len(s.changedFiles) == 0 {
+ return nil
+ }
+
+ events := make([]fsnotify.Event, len(s.changedFiles))
+ // TODO(bep) remove?
+ for i, v := range s.changedFiles {
+ events[i] = fsnotify.Event{
+ Name: v,
+ Op: fsnotify.Write,
+ }
+ }
+
+ return events
+}
+
+func (s *sitesBuilder) build(cfg BuildCfg, shouldFail bool) *sitesBuilder {
+ defer func() {
+ s.changedFiles = nil
+ }()
+
+ if s.H == nil {
+ s.CreateSites()
+ }
+
+ err := s.H.Build(cfg, s.changeEvents()...)
+
+ if err == nil {
+ logErrorCount := s.H.NumLogErrors()
+ if logErrorCount > 0 {
+ err = fmt.Errorf("logged %d errors", logErrorCount)
+ }
+ }
+ if err != nil && !shouldFail {
+ herrors.PrintStackTrace(err)
+ s.Fatalf("Build failed: %s", err)
+ } else if err == nil && shouldFail {
+ s.Fatalf("Expected error")
+ }
+
+ return s
+}
+
+func (s *sitesBuilder) addDefaults() {
+
+ var (
+ contentTemplate = `---
+title: doc1
+weight: 1
+tags:
+ - tag1
+date: "2018-02-28"
+---
+# doc1
+*some "content"*
+{{< shortcode >}}
+{{< lingo >}}
+`
+
+ defaultContent = []string{
+ "content/sect/doc1.en.md", contentTemplate,
+ "content/sect/doc1.fr.md", contentTemplate,
+ "content/sect/doc1.nb.md", contentTemplate,
+ "content/sect/doc1.nn.md", contentTemplate,
+ }
+
+ listTemplateCommon = "{{ $p := .Paginator }}{{ $p.PageNumber }}|{{ .Title }}|{{ i18n \"hello\" }}|{{ .Permalink }}|Pager: {{ template \"_internal/pagination.html\" . }}|Kind: {{ .Kind }}|Content: {{ .Content }}"
+
+ defaultTemplates = []string{
+ "_default/single.html", "Single: {{ .Title }}|{{ i18n \"hello\" }}|{{.Language.Lang}}|RelPermalink: {{ .RelPermalink }}|Permalink: {{ .Permalink }}|{{ .Content }}|Resources: {{ range .Resources }}{{ .MediaType }}: {{ .RelPermalink}} -- {{ end }}|Summary: {{ .Summary }}|Truncated: {{ .Truncated }}",
+ "_default/list.html", "List Page " + listTemplateCommon,
+ "index.html", "{{ $p := .Paginator }}Default Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}|String Resource: {{ ( \"Hugo Pipes\" | resources.FromString \"text/pipes.txt\").RelPermalink }}",
+ "index.fr.html", "{{ $p := .Paginator }}French Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}|String Resource: {{ ( \"Hugo Pipes\" | resources.FromString \"text/pipes.txt\").RelPermalink }}",
+ "_default/terms.html", "Taxonomy Term Page " + listTemplateCommon,
+ "_default/taxonomy.html", "Taxonomy List Page " + listTemplateCommon,
+ // Shortcodes
+ "shortcodes/shortcode.html", "Shortcode: {{ i18n \"hello\" }}",
+ // A shortcode in multiple languages
+ "shortcodes/lingo.html", "LingoDefault",
+ "shortcodes/lingo.fr.html", "LingoFrench",
+ // Special templates
+ "404.html", "404|{{ .Lang }}|{{ .Title }}",
+ "robots.txt", "robots|{{ .Lang }}|{{ .Title }}",
+ }
+
+ defaultI18n = []string{
+ "en.yaml", `
+hello:
+ other: "Hello"
+`,
+ "fr.yaml", `
+hello:
+ other: "Bonjour"
+`,
+ }
+
+ defaultData = []string{
+ "hugo.toml", "slogan = \"Hugo Rocks!\"",
+ }
+ )
+
+ if len(s.contentFilePairs) == 0 {
+ s.writeFilePairs("content", defaultContent)
+ }
+ if len(s.templateFilePairs) == 0 {
+ s.writeFilePairs("layouts", defaultTemplates)
+ }
+ if len(s.dataFilePairs) == 0 {
+ s.writeFilePairs("data", defaultData)
+ }
+ if len(s.i18nFilePairs) == 0 {
+ s.writeFilePairs("i18n", defaultI18n)
+ }
+}
+
+func (s *sitesBuilder) Fatalf(format string, args ...interface{}) {
+ Fatalf(s.T, format, args...)
+}
+
+func Fatalf(t testing.TB, format string, args ...interface{}) {
+ trace := stackTrace()
+ format = format + "\n%s"
+ args = append(args, trace)
+ t.Fatalf(format, args...)
+}
+
+func stackTrace() string {
+ return strings.Join(assert.CallerInfo(), "\n\r\t\t\t")
+}
+
+func (s *sitesBuilder) AssertFileContentFn(filename string, f func(s string) bool) {
+ content := s.FileContent(filename)
+ if !f(content) {
+ s.Fatalf("Assert failed for %q", filename)
+ }
+}
+
+func (s *sitesBuilder) AssertFileContent(filename string, matches ...string) {
+ content := s.FileContent(filename)
+ for _, match := range matches {
+ if !strings.Contains(content, match) {
+ s.Fatalf("No match for %q in content for %s\n%s\n%q", match, filename, content, content)
+ }
+ }
+}
+
+func (s *sitesBuilder) FileContent(filename string) string {
+ return readDestination(s.T, s.Fs, filename)
+}
+
+func (s *sitesBuilder) AssertObject(expected string, object interface{}) {
+ got := s.dumper.Sdump(object)
+ expected = strings.TrimSpace(expected)
+
+ if expected != got {
+ fmt.Println(got)
+ diff := helpers.DiffStrings(expected, got)
+ s.Fatalf("diff:\n%s\nexpected\n%s\ngot\n%s", diff, expected, got)
+ }
+}
+
+func (s *sitesBuilder) AssertFileContentRe(filename string, matches ...string) {
+ content := readDestination(s.T, s.Fs, filename)
+ for _, match := range matches {
+ r := regexp.MustCompile("(?s)" + match)
+ if !r.MatchString(content) {
+ s.Fatalf("No match for %q in content for %s\n%q", match, filename, content)
+ }
+ }
+}
+
+func (s *sitesBuilder) CheckExists(filename string) bool {
+ return destinationExists(s.Fs, filepath.Clean(filename))
+}
+
+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)))
+ }
+}
+
+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 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
+
+}
+
+func newTestSitesFromConfig(t testing.TB, afs afero.Fs, tomlConfig string, layoutPathContentPairs ...string) (testHelper, *HugoSites) {
+ if len(layoutPathContentPairs)%2 != 0 {
+ Fatalf(t, "Layouts must be provided in pairs")
+ }
+
+ writeToFs(t, afs, "config.toml", tomlConfig)
+
+ cfg, err := LoadConfigDefault(afs)
+ 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 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, false, depsCfg, buildCfg)
+}
+
+func buildSingleSiteExpected(t testing.TB, expectSiteInitEror, expectBuildError bool, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site {
+ h, err := NewHugoSites(depsCfg)
+
+ if expectSiteInitEror {
+ require.Error(t, err)
+ return nil
+ } else {
+ 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 ...[2]string) {
+ for _, src := range sources {
+ writeSource(t, fs, filepath.Join(base, src[0]), src[1])
+ }
+}
+
+func getPage(in page.Page, ref string) page.Page {
+ p, err := in.GetPage(ref)
+ if err != nil {
+ panic(err)
+ }
+ return p
+}
+
+func content(c resource.ContentProvider) string {
+ cc, err := c.Content()
+ if err != nil {
+ panic(err)
+ }
+
+ ccs, err := cast.ToStringE(cc)
+ if err != nil {
+ panic(err)
+ }
+ return ccs
+}
+
+func dumpPages(pages ...page.Page) {
+ fmt.Println("---------")
+ for i, p := range pages {
+ fmt.Printf("%d: Kind: %s Title: %-10s RelPermalink: %-10s Path: %-10s sections: %s\n",
+ i+1,
+ p.Kind(), p.Title(), p.RelPermalink(), p.Path(), p.SectionsPath())
+ }
+}
+
+func dumpSPages(pages ...*pageState) {
+ for i, p := range pages {
+ fmt.Printf("%d: Kind: %s Title: %-10s RelPermalink: %-10s Path: %-10s sections: %s\n",
+ i+1,
+ p.Kind(), p.Title(), p.RelPermalink(), p.Path(), p.SectionsPath())
+ }
+}
+
+func printStringIndexes(s string) {
+ lines := strings.Split(s, "\n")
+ i := 0
+
+ for _, line := range lines {
+
+ for _, r := range line {
+ fmt.Printf("%-3s", strconv.Itoa(i))
+ i += utf8.RuneLen(r)
+ }
+ i++
+ fmt.Println()
+ for _, r := range line {
+ fmt.Printf("%-3s", string(r))
+ }
+ fmt.Println()
+
+ }
+}
+
+func isCI() bool {
+ return os.Getenv("CI") != ""
+}
+
+func isGo111() bool {
+ return strings.Contains(runtime.Version(), "1.11")
+}
+
+// See https://github.com/golang/go/issues/19280
+// Not in use.
+var parallelEnabled = true
+
+func parallel(t *testing.T) {
+ if parallelEnabled {
+ t.Parallel()
+ }
+}
diff --git a/hugolib/testsite/content/first-post.md b/hugolib/testsite/content/first-post.md
new file mode 100644
index 000000000..4a8007946
--- /dev/null
+++ b/hugolib/testsite/content/first-post.md
@@ -0,0 +1,4 @@
+---
+title: "My First Post"
+lastmod: 2018-02-28
+--- \ No newline at end of file
diff --git a/hugolib/testsite/content_nn/first-post.md b/hugolib/testsite/content_nn/first-post.md
new file mode 100644
index 000000000..1c3b4e831
--- /dev/null
+++ b/hugolib/testsite/content_nn/first-post.md
@@ -0,0 +1,4 @@
+---
+title: "Min første dag"
+lastmod: 1972-02-28
+--- \ No newline at end of file
diff --git a/hugolib/translations.go b/hugolib/translations.go
new file mode 100644
index 000000000..072ce33e5
--- /dev/null
+++ b/hugolib/translations.go
@@ -0,0 +1,53 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/resources/page"
+)
+
+func pagesToTranslationsMap(sites []*Site) map[string]page.Pages {
+ out := make(map[string]page.Pages)
+
+ for _, s := range sites {
+ for _, p := range s.workAllPages {
+ // TranslationKey is implemented for all page types.
+ base := p.TranslationKey()
+
+ pageTranslations, found := out[base]
+ if !found {
+ pageTranslations = make(page.Pages, 0)
+ }
+
+ pageTranslations = append(pageTranslations, p)
+ out[base] = pageTranslations
+ }
+ }
+
+ return out
+}
+
+func assignTranslationsToPages(allTranslations map[string]page.Pages, sites []*Site) {
+ for _, s := range sites {
+ for _, p := range s.workAllPages {
+ base := p.TranslationKey()
+ translations, found := allTranslations[base]
+ if !found {
+ continue
+ }
+
+ p.setTranslations(translations)
+ }
+ }
+}
diff --git a/langs/i18n/i18n.go b/langs/i18n/i18n.go
new file mode 100644
index 000000000..5beef8683
--- /dev/null
+++ b/langs/i18n/i18n.go
@@ -0,0 +1,117 @@
+// 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/common/loggers"
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/helpers"
+
+ "github.com/nicksnyder/go-i18n/i18n/bundle"
+ "github.com/nicksnyder/go-i18n/i18n/translation"
+)
+
+var (
+ i18nWarningLogger = helpers.NewDistinctFeedbackLogger()
+)
+
+// Translator handles i18n translations.
+type Translator struct {
+ translateFuncs map[string]bundle.TranslateFunc
+ cfg config.Provider
+ logger *loggers.Logger
+}
+
+// NewTranslator creates a new Translator for the given language bundle and configuration.
+func NewTranslator(b *bundle.Bundle, cfg config.Provider, logger *loggers.Logger) 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.INFO.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.INFO.Println("i18n not initialized; if you need string translations, check that you have a bundle 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 {
+ t.logger.INFO.Printf("No translation bundle found for default language %q", defaultContentLanguage)
+ }
+
+ translations := bndl.Translations()
+
+ 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 {
+ t.logger.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(translations, currentLang, translationID) {
+ 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(translations, defaultContentLanguage, translationID) {
+ return translated
+ }
+ }
+ return ""
+ }
+ }
+}
+
+// If the translation map contains translationID for specified currentLang,
+// then the translationID is actually translated.
+func isIDTranslated(translations map[string]map[string]translation.Translation, lang, id string) bool {
+ _, contains := translations[lang][id]
+ return contains
+}
diff --git a/langs/i18n/i18n_test.go b/langs/i18n/i18n_test.go
new file mode 100644
index 000000000..b67cabc55
--- /dev/null
+++ b/langs/i18n/i18n_test.go
@@ -0,0 +1,262 @@
+// 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 (
+ "path/filepath"
+ "testing"
+
+ "github.com/gohugoio/hugo/tpl/tplimpl"
+
+ "github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/htesting"
+ "github.com/gohugoio/hugo/langs"
+ "github.com/spf13/afero"
+ "github.com/spf13/viper"
+
+ "github.com/gohugoio/hugo/deps"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/stretchr/testify/require"
+)
+
+var logger = loggers.NewErrorLogger()
+
+type i18nTest struct {
+ name string
+ data map[string][]byte
+ args interface{}
+ lang, id, expected, expectedFlag string
+}
+
+var i18nTests = []i18nTest{
+ // All translations present
+ {
+ name: "all-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
+ {
+ name: "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
+ {
+ name: "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
+ {
+ name: "missing",
+ 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
+ {
+ name: "file-missing",
+ data: map[string][]byte{
+ "en.toml": []byte(""),
+ },
+ args: nil,
+ lang: "es",
+ id: "hello",
+ expected: "",
+ expectedFlag: "[i18n] hello",
+ },
+ // Context provided
+ {
+ name: "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
+ {
+ name: "same-id-and-translation",
+ 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
+ {
+ name: "same-id-and-translation-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",
+ },
+ // Unknown language code should get its plural spec from en
+ {
+ name: "unknown-language-code",
+ data: map[string][]byte{
+ "en.toml": []byte(`[readingTime]
+one ="one minute read"
+other = "{{.Count}} minutes read"`),
+ "klingon.toml": []byte(`[readingTime]
+one = "eitt minutt med lesing"
+other = "{{ .Count }} minuttar lesing"`),
+ },
+ args: 3,
+ lang: "klingon",
+ id: "readingTime",
+ expected: "3 minuttar lesing",
+ expectedFlag: "3 minuttar lesing",
+ },
+}
+
+func doTestI18nTranslate(t testing.TB, test i18nTest, cfg config.Provider) string {
+ tp := prepareTranslationProvider(t, test, cfg)
+ f := tp.t.Func(test.lang)
+ return f(test.id, test.args)
+
+}
+
+func prepareTranslationProvider(t testing.TB, test i18nTest, cfg config.Provider) *TranslationProvider {
+ assert := require.New(t)
+ fs := hugofs.NewMem(cfg)
+
+ for file, content := range test.data {
+ err := afero.WriteFile(fs.Source, filepath.Join("i18n", file), []byte(content), 0755)
+ assert.NoError(err)
+ }
+
+ tp := NewTranslationProvider()
+ depsCfg := newDepsConfig(tp, cfg, fs)
+ d, err := deps.New(depsCfg)
+ assert.NoError(err)
+ assert.NoError(d.LoadResources())
+
+ return tp
+}
+
+func newDepsConfig(tp *TranslationProvider, cfg config.Provider, fs *hugofs.Fs) deps.DepsCfg {
+ l := langs.NewLanguage("en", cfg)
+ l.Set("i18nDir", "i18n")
+ return deps.DepsCfg{
+ Language: l,
+ Site: htesting.NewTestHugoSite(),
+ Cfg: cfg,
+ Fs: fs,
+ Logger: logger,
+ TemplateProvider: tplimpl.DefaultTemplateProvider,
+ TranslationProvider: tp,
+ }
+}
+
+func getConfig() *viper.Viper {
+ v := viper.New()
+ v.SetDefault("defaultContentLanguage", "en")
+ v.Set("contentDir", "content")
+ v.Set("dataDir", "data")
+ v.Set("i18nDir", "i18n")
+ v.Set("layoutDir", "layouts")
+ v.Set("archetypeDir", "archetypes")
+ v.Set("assetDir", "assets")
+ v.Set("resourceDir", "resources")
+ v.Set("publishDir", "public")
+ return v
+
+}
+
+func TestI18nTranslate(t *testing.T) {
+ var actual, expected string
+ v := getConfig()
+
+ // 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)
+ }
+ }
+}
+
+func BenchmarkI18nTranslate(b *testing.B) {
+ v := getConfig()
+ for _, test := range i18nTests {
+ b.Run(test.name, func(b *testing.B) {
+ tp := prepareTranslationProvider(b, test, v)
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ f := tp.t.Func(test.lang)
+ actual := f(test.id, test.args)
+ if actual != test.expected {
+ b.Fatalf("expected %v got %v", test.expected, actual)
+ }
+ }
+ })
+ }
+
+}
diff --git a/langs/i18n/translationProvider.go b/langs/i18n/translationProvider.go
new file mode 100644
index 000000000..74e144007
--- /dev/null
+++ b/langs/i18n/translationProvider.go
@@ -0,0 +1,125 @@
+// 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 (
+ "errors"
+
+ "github.com/gohugoio/hugo/common/herrors"
+
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/source"
+ "github.com/nicksnyder/go-i18n/i18n/bundle"
+ "github.com/nicksnyder/go-i18n/i18n/language"
+ _errors "github.com/pkg/errors"
+)
+
+// 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 {
+ sp := source.NewSourceSpec(d.PathSpec, d.BaseFs.SourceFilesystems.I18n.Fs)
+ src := sp.NewFilesystem("")
+
+ i18nBundle := bundle.New()
+
+ en := language.GetPluralSpec("en")
+ if en == nil {
+ return errors.New("the English language has vanished like an old oak table")
+ }
+ var newLangs []string
+
+ for _, r := range src.Files() {
+ currentSpec := language.GetPluralSpec(r.BaseFileName())
+ if currentSpec == nil {
+ // This may is a language code not supported by go-i18n, it may be
+ // Klingon or ... not even a fake language. Make sure it works.
+ newLangs = append(newLangs, r.BaseFileName())
+ }
+ }
+
+ if len(newLangs) > 0 {
+ language.RegisterPluralSpec(newLangs, en)
+ }
+
+ // The source files are ordered so the most important comes first. Since this is a
+ // last key win situation, we have to reverse the iteration order.
+ files := src.Files()
+ for i := len(files) - 1; i >= 0; i-- {
+ if err := addTranslationFile(i18nBundle, files[i]); err != nil {
+ return err
+ }
+ }
+
+ tp.t = NewTranslator(i18nBundle, d.Cfg, d.Log)
+
+ d.Translate = tp.t.Func(d.Language.Lang)
+
+ return nil
+
+}
+
+func addTranslationFile(bundle *bundle.Bundle, r source.ReadableFile) error {
+ f, err := r.Open()
+ if err != nil {
+ return _errors.Wrapf(err, "failed to open translations file %q:", r.LogicalName())
+ }
+ err = bundle.ParseTranslationFileBytes(r.LogicalName(), helpers.ReaderToBytes(f))
+ f.Close()
+ if err != nil {
+ return errWithFileContext(_errors.Wrapf(err, "failed to load translations"), r)
+ }
+ 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
+}
+
+func errWithFileContext(inerr error, r source.ReadableFile) error {
+ rfi, ok := r.FileInfo().(hugofs.RealFilenameInfo)
+ if !ok {
+ return inerr
+ }
+
+ realFilename := rfi.RealFilename()
+ f, err := r.Open()
+ if err != nil {
+ return inerr
+ }
+ defer f.Close()
+
+ err, _ = herrors.WithFileContext(
+ inerr,
+ realFilename,
+ f,
+ herrors.SimpleLineMatcher)
+
+ return err
+
+}
diff --git a/langs/language.go b/langs/language.go
new file mode 100644
index 000000000..14e3263ae
--- /dev/null
+++ b/langs/language.go
@@ -0,0 +1,229 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package langs
+
+import (
+ "sort"
+ "strings"
+
+ "github.com/gohugoio/hugo/common/maps"
+ "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,
+ strings.ToLower("assetDir"): true,
+ strings.ToLower("resourceDir"): true,
+}
+
+// Language manages specific-language configuration.
+type Language struct {
+ Lang string
+ LanguageName string
+ Title string
+ Weight int
+
+ Disabled bool
+
+ // If set per language, this tells Hugo that all content files without any
+ // language indicator (e.g. my-page.en.md) is in this language.
+ // This is usually a path relative to the working dir, but it can be an
+ // absolute directory reference. It is what we get.
+ ContentDir string
+
+ Cfg config.Provider
+
+ // These are params declared in the [params] section of the language merged with the
+ // site's params, the most specific (language) wins on duplicate keys.
+ params map[string]interface{}
+
+ // These are config values, i.e. the settings declared outside of the [params] section of the language.
+ // This is the map Hugo looks in when looking for configuration values (baseURL etc.).
+ // Values in this map can also be fetched from the params map above.
+ settings map[string]interface{}
+}
+
+func (l *Language) String() string {
+ return l.Lang
+}
+
+// NewLanguage creates a new language.
+func NewLanguage(lang string, cfg config.Provider) *Language {
+ // Note that language specific params will be overridden later.
+ // We should improve that, but we need to make a copy:
+ params := make(map[string]interface{})
+ for k, v := range cfg.GetStringMap("params") {
+ params[k] = v
+ }
+ maps.ToLower(params)
+
+ defaultContentDir := cfg.GetString("contentDir")
+ if defaultContentDir == "" {
+ panic("contentDir not set")
+ }
+
+ l := &Language{Lang: lang, ContentDir: defaultContentDir, Cfg: cfg, params: params, settings: make(map[string]interface{})}
+ return l
+}
+
+// NewDefaultLanguage creates the default language for a config.Provider.
+// If not otherwise specified the default is "en".
+func NewDefaultLanguage(cfg config.Provider) *Language {
+ defaultLang := cfg.GetString("defaultContentLanguage")
+
+ if defaultLang == "" {
+ defaultLang = "en"
+ }
+
+ return NewLanguage(defaultLang, cfg)
+}
+
+// Languages is a sortable list of languages.
+type Languages []*Language
+
+// NewLanguages creates a sorted list of languages.
+// NOTE: function is currently unused.
+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 {
+ wi, wj := l[i].Weight, l[j].Weight
+
+ if wi == wj {
+ return l[i].Lang < l[j].Lang
+ }
+
+ return wj == 0 || wi < wj
+
+}
+
+func (l Languages) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
+
+// Params retunrs language-specific params merged with the global params.
+func (l *Language) Params() map[string]interface{} {
+ return l.params
+}
+
+// IsMultihost returns whether there are more than one language and at least one of
+// the languages has baseURL specificed on the language level.
+func (l Languages) IsMultihost() bool {
+ if len(l) <= 1 {
+ return false
+ }
+
+ for _, lang := range l {
+ if lang.GetLocal("baseURL") != nil {
+ return true
+ }
+ }
+ return false
+}
+
+// SetParam sets a 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))
+}
+
+// GetStringSlice returns the value associated with the key as a slice of strings.
+func (l *Language) GetStringSlice(key string) []string {
+ return cast.ToStringSlice(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{} {
+ local := l.GetLocal(key)
+ if local != nil {
+ return local
+ }
+ return l.Cfg.Get(key)
+}
+
+// GetLocal gets a configuration value set on language level. It will
+// not fall back to any global value.
+// It will return nil if a value with the given key cannot be found.
+func (l *Language) GetLocal(key string) interface{} {
+ if l == nil {
+ panic("language not set")
+ }
+ key = strings.ToLower(key)
+ if !globalOnlySettings[key] {
+ if v, ok := l.settings[key]; ok {
+ return v
+ }
+ }
+ return nil
+}
+
+// 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.settings[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.settings[key]; ok {
+ return true
+ }
+ }
+ return l.Cfg.IsSet(key)
+
+}
diff --git a/langs/language_test.go b/langs/language_test.go
new file mode 100644
index 000000000..8783172fb
--- /dev/null
+++ b/langs/language_test.go
@@ -0,0 +1,48 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package langs
+
+import (
+ "testing"
+
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/require"
+)
+
+func TestGetGlobalOnlySetting(t *testing.T) {
+ v := viper.New()
+ v.Set("defaultContentLanguageInSubdir", true)
+ v.Set("contentDir", "content")
+ v.Set("paginatePath", "page")
+ lang := NewDefaultLanguage(v)
+ lang.Set("defaultContentLanguageInSubdir", false)
+ lang.Set("paginatePath", "side")
+
+ require.True(t, lang.GetBool("defaultContentLanguageInSubdir"))
+ require.Equal(t, "side", lang.GetString("paginatePath"))
+}
+
+func TestLanguageParams(t *testing.T) {
+ assert := require.New(t)
+
+ v := viper.New()
+ v.Set("p1", "p1cfg")
+ v.Set("contentDir", "content")
+
+ lang := NewDefaultLanguage(v)
+ lang.SetParam("p1", "p1p")
+
+ assert.Equal("p1p", lang.Params()["p1"])
+ assert.Equal("p1cfg", lang.Get("p1"))
+}
diff --git a/lazy/init.go b/lazy/init.go
new file mode 100644
index 000000000..a54fda96a
--- /dev/null
+++ b/lazy/init.go
@@ -0,0 +1,198 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package lazy
+
+import (
+ "context"
+ "sync"
+ "time"
+
+ "github.com/pkg/errors"
+)
+
+// New creates a new empty Init.
+func New() *Init {
+ return &Init{}
+}
+
+// Init holds a graph of lazily initialized dependencies.
+type Init struct {
+ mu sync.Mutex
+
+ prev *Init
+ children []*Init
+
+ init onceMore
+ out interface{}
+ err error
+ f func() (interface{}, error)
+}
+
+// Add adds a func as a new child dependency.
+func (ini *Init) Add(initFn func() (interface{}, error)) *Init {
+ if ini == nil {
+ ini = New()
+ }
+ return ini.add(false, initFn)
+}
+
+// AddWithTimeout is same as Add, but with a timeout that aborts initialization.
+func (ini *Init) AddWithTimeout(timeout time.Duration, f func(ctx context.Context) (interface{}, error)) *Init {
+ return ini.Add(func() (interface{}, error) {
+ return ini.withTimeout(timeout, f)
+ })
+}
+
+// Branch creates a new dependency branch based on an existing and adds
+// the given dependency as a child.
+func (ini *Init) Branch(initFn func() (interface{}, error)) *Init {
+ if ini == nil {
+ ini = New()
+ }
+ return ini.add(true, initFn)
+}
+
+// BranchdWithTimeout is same as Branch, but with a timeout.
+func (ini *Init) BranchdWithTimeout(timeout time.Duration, f func(ctx context.Context) (interface{}, error)) *Init {
+ return ini.Branch(func() (interface{}, error) {
+ return ini.withTimeout(timeout, f)
+ })
+}
+
+// Do initializes the entire dependency graph.
+func (ini *Init) Do() (interface{}, error) {
+ if ini == nil {
+ panic("init is nil")
+ }
+
+ ini.init.Do(func() {
+ prev := ini.prev
+ if prev != nil {
+ // A branch. Initialize the ancestors.
+ if prev.shouldInitialize() {
+ _, err := prev.Do()
+ if err != nil {
+ ini.err = err
+ return
+ }
+ } else if prev.inProgress() {
+ // Concurrent initialization. The following init func
+ // may depend on earlier state, so wait.
+ prev.wait()
+ }
+ }
+
+ if ini.f != nil {
+ ini.out, ini.err = ini.f()
+ }
+
+ for _, child := range ini.children {
+ if child.shouldInitialize() {
+ _, err := child.Do()
+ if err != nil {
+ ini.err = err
+ return
+ }
+ }
+ }
+ })
+
+ ini.wait()
+
+ return ini.out, ini.err
+
+}
+
+// TODO(bep) investigate if we can use sync.Cond for this.
+func (ini *Init) wait() {
+ var counter time.Duration
+ for !ini.init.Done() {
+ counter += 10
+ if counter > 600000000 {
+ panic("BUG: timed out in lazy init")
+ }
+ time.Sleep(counter * time.Microsecond)
+ }
+}
+
+func (ini *Init) inProgress() bool {
+ return ini != nil && ini.init.InProgress()
+}
+
+func (ini *Init) shouldInitialize() bool {
+ return !(ini == nil || ini.init.Done() || ini.init.InProgress())
+}
+
+// Reset resets the current and all its dependencies.
+func (ini *Init) Reset() {
+ mu := ini.init.ResetWithLock()
+ defer mu.Unlock()
+ for _, d := range ini.children {
+ d.Reset()
+ }
+}
+
+func (ini *Init) add(branch bool, initFn func() (interface{}, error)) *Init {
+ ini.mu.Lock()
+ defer ini.mu.Unlock()
+
+ if branch {
+ return &Init{
+ f: initFn,
+ prev: ini,
+ }
+ }
+
+ ini.checkDone()
+ ini.children = append(ini.children, &Init{
+ f: initFn,
+ })
+
+ return ini
+}
+
+func (ini *Init) checkDone() {
+ if ini.init.Done() {
+ panic("init cannot be added to after it has run")
+ }
+}
+
+func (ini *Init) withTimeout(timeout time.Duration, f func(ctx context.Context) (interface{}, error)) (interface{}, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), timeout)
+ defer cancel()
+ c := make(chan verr, 1)
+
+ go func() {
+ v, err := f(ctx)
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ c <- verr{v: v, err: err}
+ }
+ }()
+
+ select {
+ case <-ctx.Done():
+ return nil, errors.New("timed out initializing value. This is most likely a circular loop in a shortcode")
+ case ve := <-c:
+ return ve.v, ve.err
+ }
+
+}
+
+type verr struct {
+ v interface{}
+ err error
+}
diff --git a/lazy/init_test.go b/lazy/init_test.go
new file mode 100644
index 000000000..ea1b22fe9
--- /dev/null
+++ b/lazy/init_test.go
@@ -0,0 +1,226 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package lazy
+
+import (
+ "context"
+ "errors"
+ "math/rand"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+var (
+ rnd = rand.New(rand.NewSource(time.Now().UnixNano()))
+ bigOrSmall = func() int {
+ if rnd.Intn(10) < 5 {
+ return 10000 + rnd.Intn(100000)
+ }
+ return 1 + rnd.Intn(50)
+ }
+)
+
+func doWork() {
+ doWorkOfSize(bigOrSmall())
+}
+
+func doWorkOfSize(size int) {
+ _ = strings.Repeat("Hugo Rocks! ", size)
+}
+
+func TestInit(t *testing.T) {
+ assert := require.New(t)
+
+ var result string
+
+ f1 := func(name string) func() (interface{}, error) {
+ return func() (interface{}, error) {
+ result += name + "|"
+ doWork()
+ return name, nil
+ }
+ }
+
+ f2 := func() func() (interface{}, error) {
+ return func() (interface{}, error) {
+ doWork()
+ return nil, nil
+ }
+ }
+
+ root := New()
+
+ root.Add(f1("root(1)"))
+ root.Add(f1("root(2)"))
+
+ branch1 := root.Branch(f1("branch_1"))
+ branch1.Add(f1("branch_1_1"))
+ branch1_2 := branch1.Add(f1("branch_1_2"))
+ branch1_2_1 := branch1_2.Add(f1("branch_1_2_1"))
+
+ var wg sync.WaitGroup
+
+ // Add some concurrency and randomness to verify thread safety and
+ // init order.
+ for i := 0; i < 100; i++ {
+ wg.Add(1)
+ go func(i int) {
+ defer wg.Done()
+ var err error
+ if rnd.Intn(10) < 5 {
+ _, err = root.Do()
+ assert.NoError(err)
+ }
+
+ // Add a new branch on the fly.
+ if rnd.Intn(10) > 5 {
+ branch := branch1_2.Branch(f2())
+ _, err = branch.Do()
+ assert.NoError(err)
+ } else {
+ _, err = branch1_2_1.Do()
+ assert.NoError(err)
+ }
+ _, err = branch1_2.Do()
+ assert.NoError(err)
+
+ }(i)
+
+ wg.Wait()
+
+ assert.Equal("root(1)|root(2)|branch_1|branch_1_1|branch_1_2|branch_1_2_1|", result)
+
+ }
+
+}
+
+func TestInitAddWithTimeout(t *testing.T) {
+ assert := require.New(t)
+
+ init := New().AddWithTimeout(100*time.Millisecond, func(ctx context.Context) (interface{}, error) {
+ return nil, nil
+ })
+
+ _, err := init.Do()
+
+ assert.NoError(err)
+}
+
+func TestInitAddWithTimeoutTimeout(t *testing.T) {
+ assert := require.New(t)
+
+ init := New().AddWithTimeout(100*time.Millisecond, func(ctx context.Context) (interface{}, error) {
+ time.Sleep(500 * time.Millisecond)
+ select {
+ case <-ctx.Done():
+ return nil, nil
+ default:
+ }
+ t.Fatal("slept")
+ return nil, nil
+ })
+
+ _, err := init.Do()
+
+ assert.Error(err)
+
+ assert.Contains(err.Error(), "timed out")
+
+ time.Sleep(1 * time.Second)
+
+}
+
+func TestInitAddWithTimeoutError(t *testing.T) {
+ assert := require.New(t)
+
+ init := New().AddWithTimeout(100*time.Millisecond, func(ctx context.Context) (interface{}, error) {
+ return nil, errors.New("failed")
+ })
+
+ _, err := init.Do()
+
+ assert.Error(err)
+}
+
+type T struct {
+ sync.Mutex
+ V1 string
+ V2 string
+}
+
+func (t *T) Add1(v string) {
+ t.Lock()
+ t.V1 += v
+ t.Unlock()
+}
+
+func (t *T) Add2(v string) {
+ t.Lock()
+ t.V2 += v
+ t.Unlock()
+}
+
+// https://github.com/gohugoio/hugo/issues/5901
+func TestInitBranchOrder(t *testing.T) {
+ assert := require.New(t)
+
+ base := New()
+
+ work := func(size int, f func()) func() (interface{}, error) {
+ return func() (interface{}, error) {
+ doWorkOfSize(size)
+ if f != nil {
+ f()
+ }
+
+ return nil, nil
+ }
+ }
+
+ state := &T{}
+
+ base = base.Add(work(10000, func() {
+ state.Add1("A")
+ }))
+
+ inits := make([]*Init, 2)
+ for i := range inits {
+ inits[i] = base.Branch(work(i+1*100, func() {
+ // V1 is A
+ ab := state.V1 + "B"
+ state.Add2(ab)
+
+ }))
+ }
+
+ var wg sync.WaitGroup
+
+ for _, v := range inits {
+ v := v
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ _, err := v.Do()
+ assert.NoError(err)
+ }()
+ }
+
+ wg.Wait()
+
+ assert.Equal("ABAB", state.V2)
+}
diff --git a/lazy/once.go b/lazy/once.go
new file mode 100644
index 000000000..c434bfa0b
--- /dev/null
+++ b/lazy/once.go
@@ -0,0 +1,69 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package lazy
+
+import (
+ "sync"
+ "sync/atomic"
+)
+
+// onceMore is similar to sync.Once.
+//
+// Additional features are:
+// * it can be reset, so the action can be repeated if needed
+// * it has methods to check if it's done or in progress
+//
+type onceMore struct {
+ mu sync.Mutex
+ lock uint32
+ done uint32
+}
+
+func (t *onceMore) Do(f func()) {
+ if atomic.LoadUint32(&t.done) == 1 {
+ return
+ }
+
+ // f may call this Do and we would get a deadlock.
+ locked := atomic.CompareAndSwapUint32(&t.lock, 0, 1)
+ if !locked {
+ return
+ }
+ defer atomic.StoreUint32(&t.lock, 0)
+
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ // Double check
+ if t.done == 1 {
+ return
+ }
+ defer atomic.StoreUint32(&t.done, 1)
+ f()
+
+}
+
+func (t *onceMore) InProgress() bool {
+ return atomic.LoadUint32(&t.lock) == 1
+}
+
+func (t *onceMore) Done() bool {
+ return atomic.LoadUint32(&t.done) == 1
+}
+
+func (t *onceMore) ResetWithLock() *sync.Mutex {
+ t.mu.Lock()
+ defer atomic.StoreUint32(&t.done, 0)
+ return &t.mu
+}
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..2f3cee8f0
--- /dev/null
+++ b/livereload/livereload.go
@@ -0,0 +1,188 @@
+// 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 (
+ "fmt"
+ "net"
+ "net/http"
+ "net/url"
+ "path/filepath"
+
+ "github.com/gorilla/websocket"
+)
+
+// Prefix to signal to LiveReload that we need to navigate to another path.
+const hugoNavigatePrefix = "__hugo_navigate"
+
+var upgrader = &websocket.Upgrader{
+ // Hugo may potentially spin up multiple HTTP servers, so we need to exclude the
+ // port when checking the origin.
+ CheckOrigin: func(r *http.Request) bool {
+ origin := r.Header["Origin"]
+ if len(origin) == 0 {
+ return true
+ }
+ u, err := url.Parse(origin[0])
+ if err != nil {
+ return false
+ }
+
+ if u.Host == r.Host {
+ return true
+ }
+
+ h1, _, err := net.SplitHostPort(u.Host)
+ if err != nil {
+ return false
+ }
+ h2, _, err := net.SplitHostPort(r.Host)
+ if err != nil {
+ return false
+ }
+
+ return h1 == h2
+ },
+ 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")
+}
+
+// NavigateToPath tells livereload to navigate to the given path.
+// This translates to `window.location.href = path` in the client.
+func NavigateToPath(path string) {
+ RefreshPath(hugoNavigatePrefix + path)
+}
+
+// NavigateToPathForPort is similar to NavigateToPath but will also
+// set window.location.port to the given port value.
+func NavigateToPathForPort(path string, port int) {
+ refreshPathForPort(hugoNavigatePrefix+path, port)
+}
+
+// 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) {
+ refreshPathForPort(s, -1)
+}
+
+func refreshPathForPort(s string, port int) {
+ // Tell livereload a file has changed - will force a hard refresh if not CSS or an image
+ urlPath := filepath.ToSlash(s)
+ portStr := ""
+ if port > 0 {
+ portStr = fmt.Sprintf(`, "overrideURL": %d`, port)
+ }
+ msg := fmt.Sprintf(`{"command":"reload","path":%q,"originalPath":"","liveCSS":true,"liveImg":true%s}`, urlPath, portStr)
+ wsHub.broadcast <- []byte(msg)
+}
+
+// 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())
+}
+
+func liveReloadJS() []byte {
+ return []byte(livereloadJS + hugoLiveReloadPlugin)
+}
+
+var (
+ // This is temporary patched with this PR (enables sensible error messages):
+ // https://github.com/livereload/livereload-js/pull/64
+ // TODO(bep) replace with distribution once merged.
+ livereloadJS = `(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 h=n[s]={exports:{}};t[s][0].call(h.exports,function(e){var n=t[s][1][e];return i(n?n:e)},h,h.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.2";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.https?"s":"")+"://"+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,l,c={}.hasOwnProperty;t=e("./connector").Connector;l=e("./timer").Timer;i=e("./options").Options;s=e("./reloader").Reloader;r=e("./protocol").ProtocolError;n.LiveReload=o=function(){function e(e){var n,o,a;this.window=e;this.listeners={};this.plugins=[];this.pluginIdentifiers={};this.console=this.window.console&&this.window.console.log&&this.window.console.error?this.window.location.href.match(/LR-verbose/)?this.window.console:{log:function(){},error:this.window.console.error.bind(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("LiveReloadOptions"in e){this.options=new i;a=e["LiveReloadOptions"];for(n in a){if(!c.call(a,n))continue;o=a[n];this.options.set(n,o)}}else{this.options=i.extract(this.window.document);if(!this.options){this.console.error("LiveReload disabled because it could not find its own <SCRIPT> tag");return}}this.reloader=new s(this.window,this.console,l);this.connector=new t(this.options,this.WebSocket,l,{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 r){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)});this.initialized=true}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;if(!this.initialized){return}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.initialized){return}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:l,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.initialized){return}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,"./protocol":6,"./reloader":7,"./timer":9}],5:[function(e,t,n){(function(){var e;n.Options=e=function(){function e(){this.https=false;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,h,u,d,f,p;f=t.getElementsByTagName("script");for(a=0,u=f.length;a<u;a++){n=f[a];if((c=n.src)&&(i=c.match(/^[^:]+:\/\/(.*)\/z?livereload\.js(?:\?(.*))?$/))){s=new e;s.https=c.indexOf("https")===0;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(h=0,d=p.length;h<d;h++){l=p[h];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(e){s=e;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(t){n=t;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,h,u,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(h=0,p=e.length;h<p;h++){y=e[h],s=y.selector,l=y.styleNames;w=this.document.querySelectorAll("[style*="+s+"]");for(u=0,m=w.length;u<m;u++){o=w[u];this.reloadStyleImages(o.style,l,t,n)}}}if(this.document.styleSheets){R=this.document.styleSheets;_=[];for(d=0,g=R.length;d<g;d++){c=R[d];_.push(this.reloadStylesheetImages(c,t,n))}return _}};t.prototype.reloadStylesheetImages=function(t,n,o){var i,r,s,l,c,a,h,u;try{s=t!=null?t.cssRules:void 0}catch(e){i=e}if(!s){return}for(c=0,h=s.length;c<h;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,u=e.length;a<u;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,h;for(a=0,h=t.length;a<h;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,h,u,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(h=0,p=v.length;h<p;h++){l=v[h];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(u=0,m=o.length;u<m;u++){n=o[u];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(e){o=e}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]);`
+ hugoLiveReloadPlugin = fmt.Sprintf(`
+/*
+Hugo adds a specific prefix, "__hugo_navigate", to the path in certain situations to signal
+navigation to another content page.
+*/
+
+function HugoReload() {}
+
+HugoReload.identifier = 'hugoReloader';
+HugoReload.version = '0.9';
+
+HugoReload.prototype.reload = function(path, options) {
+ var prefix = %q;
+
+ if (path.lastIndexOf(prefix, 0) !== 0) {
+ return false
+ }
+
+ path = path.substring(prefix.length);
+
+ var portChanged = options.overrideURL && options.overrideURL != window.location.port
+
+ if (!portChanged && window.location.pathname === path) {
+ window.location.reload();
+ } else {
+ if (portChanged) {
+ window.location = location.protocol + "//" + location.hostname + ":" + options.overrideURL + path;
+ } else {
+ window.location.pathname = path;
+ }
+ }
+
+ return true;
+};
+
+LiveReload.addPlugin(HugoReload)
+`, hugoNavigatePrefix)
+)
diff --git a/magefile.go b/magefile.go
new file mode 100644
index 000000000..3b74a7e94
--- /dev/null
+++ b/magefile.go
@@ -0,0 +1,307 @@
+// +build mage
+
+package main
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/gohugoio/hugo/codegen"
+ "github.com/gohugoio/hugo/resources/page/page_generate"
+
+ "github.com/magefile/mage/mg"
+ "github.com/magefile/mage/sh"
+)
+
+const (
+ packageName = "github.com/gohugoio/hugo"
+ noGitLdflags = "-X $PACKAGE/common/hugo.buildDate=$BUILD_DATE"
+)
+
+var ldflags = "-X $PACKAGE/common/hugo.commitHash=$COMMIT_HASH -X $PACKAGE/common/hugo.buildDate=$BUILD_DATE"
+
+// allow user to override go executable by running as GOEXE=xxx make ... on unix-like systems
+var goexe = "go"
+
+func init() {
+ if exe := os.Getenv("GOEXE"); exe != "" {
+ goexe = exe
+ }
+
+ // We want to use Go 1.11 modules even if the source lives inside GOPATH.
+ // The default is "auto".
+ os.Setenv("GO111MODULE", "on")
+}
+
+// Build hugo binary
+func Hugo() error {
+ return sh.RunWith(flagEnv(), goexe, "build", "-ldflags", ldflags, "-tags", buildTags(), packageName)
+}
+
+// Build hugo binary with race detector enabled
+func HugoRace() error {
+ return sh.RunWith(flagEnv(), goexe, "build", "-race", "-ldflags", ldflags, "-tags", buildTags(), packageName)
+}
+
+// Install hugo binary
+func Install() error {
+ return sh.RunWith(flagEnv(), goexe, "install", "-ldflags", ldflags, "-tags", buildTags(), packageName)
+}
+
+func flagEnv() map[string]string {
+ hash, _ := sh.Output("git", "rev-parse", "--short", "HEAD")
+ return map[string]string{
+ "PACKAGE": packageName,
+ "COMMIT_HASH": hash,
+ "BUILD_DATE": time.Now().Format("2006-01-02T15:04:05Z0700"),
+ }
+}
+
+func Generate() error {
+ generatorPackages := []string{
+ "tpl/tplimpl/embedded/generate",
+ //"resources/page/generate",
+ }
+
+ for _, pkg := range generatorPackages {
+ if err := sh.RunWith(flagEnv(), goexe, "generate", path.Join(packageName, pkg)); err != nil {
+ return err
+ }
+ }
+
+ dir, _ := os.Getwd()
+ c := codegen.NewInspector(dir)
+
+ if err := page_generate.Generate(c); err != nil {
+ return err
+ }
+
+ goFmtPatterns := []string{
+ // TODO(bep) check: stat ./resources/page/*autogen*: no such file or directory
+ "./resources/page/page_marshaljson.autogen.go",
+ "./resources/page/page_wrappers.autogen.go",
+ "./resources/page/zero_file.autogen.go",
+ }
+
+ for _, pattern := range goFmtPatterns {
+ if err := sh.Run("gofmt", "-w", filepath.FromSlash(pattern)); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// Build hugo without git info
+func HugoNoGitInfo() error {
+ ldflags = noGitLdflags
+ return Hugo()
+}
+
+var docker = sh.RunCmd("docker")
+
+// Build hugo Docker container
+func Docker() error {
+ if err := docker("build", "-t", "hugo", "."); err != nil {
+ return err
+ }
+ // yes ignore errors here
+ docker("rm", "-f", "hugo-build")
+ if err := docker("run", "--name", "hugo-build", "hugo ls /go/bin"); err != nil {
+ return err
+ }
+ if err := docker("cp", "hugo-build:/go/bin/hugo", "."); err != nil {
+ return err
+ }
+ return docker("rm", "hugo-build")
+}
+
+// Run tests and linters
+func Check() {
+ if strings.Contains(runtime.Version(), "1.8") {
+ // Go 1.8 doesn't play along with go test ./... and /vendor.
+ // We could fix that, but that would take time.
+ fmt.Printf("Skip Check on %s\n", runtime.Version())
+ return
+ }
+
+ mg.Deps(Test386)
+
+ mg.Deps(Fmt, Vet)
+
+ // don't run two tests in parallel, they saturate the CPUs anyway, and running two
+ // causes memory issues in CI.
+ mg.Deps(TestRace)
+}
+
+// Run tests in 32-bit mode
+// Note that we don't run with the extended tag. Currently not supported in 32 bit.
+func Test386() error {
+ return sh.RunWith(map[string]string{"GOARCH": "386"}, goexe, "test", "./...")
+}
+
+// Run tests
+func Test() error {
+ return sh.Run(goexe, "test", "./...", "-tags", buildTags())
+}
+
+// Run tests with race detector
+func TestRace() error {
+ return sh.Run(goexe, "test", "-race", "./...", "-tags", buildTags())
+}
+
+// Run gofmt linter
+func Fmt() error {
+ if !isGoLatest() {
+ return nil
+ }
+ pkgs, err := hugoPackages()
+ if err != nil {
+ return err
+ }
+ failed := false
+ first := true
+ for _, pkg := range pkgs {
+ files, err := filepath.Glob(filepath.Join(pkg, "*.go"))
+ if err != nil {
+ return nil
+ }
+ for _, f := range files {
+ // gofmt doesn't exit with non-zero when it finds unformatted code
+ // so we have to explicitly look for output, and if we find any, we
+ // should fail this target.
+ s, err := sh.Output("gofmt", "-l", f)
+ if err != nil {
+ fmt.Printf("ERROR: running gofmt on %q: %v\n", f, err)
+ failed = true
+ }
+ if s != "" {
+ if first {
+ fmt.Println("The following files are not gofmt'ed:")
+ first = false
+ }
+ failed = true
+ fmt.Println(s)
+ }
+ }
+ }
+ if failed {
+ return errors.New("improperly formatted go files")
+ }
+ return nil
+}
+
+var (
+ pkgPrefixLen = len("github.com/gohugoio/hugo")
+ pkgs []string
+ pkgsInit sync.Once
+)
+
+func hugoPackages() ([]string, error) {
+ var err error
+ pkgsInit.Do(func() {
+ var s string
+ s, err = sh.Output(goexe, "list", "./...")
+ if err != nil {
+ return
+ }
+ pkgs = strings.Split(s, "\n")
+ for i := range pkgs {
+ pkgs[i] = "." + pkgs[i][pkgPrefixLen:]
+ }
+ })
+ return pkgs, err
+}
+
+// Run golint linter
+func Lint() error {
+ pkgs, err := hugoPackages()
+ if err != nil {
+ return err
+ }
+ failed := false
+ for _, pkg := range pkgs {
+ // We don't actually want to fail this target if we find golint errors,
+ // so we don't pass -set_exit_status, but we still print out any failures.
+ if _, err := sh.Exec(nil, os.Stderr, nil, "golint", pkg); err != nil {
+ fmt.Printf("ERROR: running go lint on %q: %v\n", pkg, err)
+ failed = true
+ }
+ }
+ if failed {
+ return errors.New("errors running golint")
+ }
+ return nil
+}
+
+// Run go vet linter
+func Vet() error {
+ if err := sh.Run(goexe, "vet", "./..."); err != nil {
+ return fmt.Errorf("error running go vet: %v", err)
+ }
+ return nil
+}
+
+// Generate test coverage report
+func TestCoverHTML() error {
+ const (
+ coverAll = "coverage-all.out"
+ cover = "coverage.out"
+ )
+ f, err := os.Create(coverAll)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ if _, err := f.Write([]byte("mode: count")); err != nil {
+ return err
+ }
+ pkgs, err := hugoPackages()
+ if err != nil {
+ return err
+ }
+ for _, pkg := range pkgs {
+ if err := sh.Run(goexe, "test", "-coverprofile="+cover, "-covermode=count", pkg); err != nil {
+ return err
+ }
+ b, err := ioutil.ReadFile(cover)
+ if err != nil {
+ if os.IsNotExist(err) {
+ continue
+ }
+ return err
+ }
+ idx := bytes.Index(b, []byte{'\n'})
+ b = b[idx+1:]
+ if _, err := f.Write(b); err != nil {
+ return err
+ }
+ }
+ if err := f.Close(); err != nil {
+ return err
+ }
+ return sh.Run(goexe, "tool", "cover", "-html="+coverAll)
+}
+
+func isGoLatest() bool {
+ return strings.Contains(runtime.Version(), "1.11")
+}
+
+func buildTags() string {
+ // To build the extended Hugo SCSS/SASS enabled version, build with
+ // HUGO_BUILD_TAGS=extended mage install etc.
+ if envtags := os.Getenv("HUGO_BUILD_TAGS"); envtags != "" {
+ return envtags
+ }
+ return "none"
+
+}
diff --git a/main.go b/main.go
new file mode 100644
index 000000000..ecb423e60
--- /dev/null
+++ b/main.go
@@ -0,0 +1,33 @@
+// 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 (
+ "os"
+
+ "github.com/gohugoio/hugo/commands"
+)
+
+func main() {
+ resp := commands.Execute(os.Args[1:])
+
+ if resp.Err != nil {
+ if resp.IsUserError() {
+ resp.Cmd.Println("")
+ resp.Cmd.Println(resp.Cmd.UsageString())
+ }
+ 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..434672c43
--- /dev/null
+++ b/media/mediaType.go
@@ -0,0 +1,371 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "errors"
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/gohugoio/hugo/common/maps"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+const (
+ defaultDelimiter = "."
+)
+
+// 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 application/svg+xml
+// If suffix is not provided, the sub type will be used.
+// See // https://en.wikipedia.org/wiki/Media_type
+type Type struct {
+ MainType string `json:"mainType"` // i.e. text
+ SubType string `json:"subType"` // i.e. html
+
+ // This is the optional suffix after the "+" in the MIME type,
+ // e.g. "xml" in "applicatiion/rss+xml".
+ mimeSuffix string
+
+ Delimiter string `json:"delimiter"` // e.g. "."
+
+ // TODO(bep) make this a string to make it hashable + method
+ Suffixes []string `json:"suffixes"`
+
+ // Set when doing lookup by suffix.
+ fileSuffix string
+}
+
+// FromStringAndExt is same as FromString, but adds the file extension to the type.
+func FromStringAndExt(t, ext string) (Type, error) {
+ tp, err := fromString(t)
+ if err != nil {
+ return tp, err
+ }
+ tp.Suffixes = []string{strings.TrimPrefix(ext, ".")}
+ return tp, nil
+}
+
+// FromString creates a new Type given a type string 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 := strings.Split(subParts[0], ";")[0]
+
+ var suffix string
+
+ if len(subParts) > 1 {
+ suffix = subParts[1]
+ }
+
+ return Type{MainType: mainType, SubType: subType, mimeSuffix: suffix}, nil
+}
+
+// Type returns a string representing the main- and sub-type of a media type, e.g. "text/css".
+// A suffix identifier will be appended after a "+" if set, e.g. "image/svg+xml".
+// 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 {
+ // Examples are
+ // image/svg+xml
+ // text/css
+ if m.mimeSuffix != "" {
+ return fmt.Sprintf("%s/%s+%s", m.MainType, m.SubType, m.mimeSuffix)
+ }
+ return fmt.Sprintf("%s/%s", m.MainType, m.SubType)
+
+}
+
+func (m Type) String() string {
+ return m.Type()
+}
+
+// FullSuffix returns the file suffix with any delimiter prepended.
+func (m Type) FullSuffix() string {
+ return m.Delimiter + m.Suffix()
+}
+
+// Suffix returns the file suffix without any delmiter prepended.
+func (m Type) Suffix() string {
+ if m.fileSuffix != "" {
+ return m.fileSuffix
+ }
+ if len(m.Suffixes) > 0 {
+ return m.Suffixes[0]
+ }
+ // There are MIME types without file suffixes.
+ return ""
+}
+
+// Definitions from https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types etc.
+// Note that from Hugo 0.44 we only set Suffix if it is part of the MIME type.
+var (
+ CalendarType = Type{MainType: "text", SubType: "calendar", Suffixes: []string{"ics"}, Delimiter: defaultDelimiter}
+ CSSType = Type{MainType: "text", SubType: "css", Suffixes: []string{"css"}, Delimiter: defaultDelimiter}
+ SCSSType = Type{MainType: "text", SubType: "x-scss", Suffixes: []string{"scss"}, Delimiter: defaultDelimiter}
+ SASSType = Type{MainType: "text", SubType: "x-sass", Suffixes: []string{"sass"}, Delimiter: defaultDelimiter}
+ CSVType = Type{MainType: "text", SubType: "csv", Suffixes: []string{"csv"}, Delimiter: defaultDelimiter}
+ HTMLType = Type{MainType: "text", SubType: "html", Suffixes: []string{"html"}, Delimiter: defaultDelimiter}
+ JavascriptType = Type{MainType: "application", SubType: "javascript", Suffixes: []string{"js"}, Delimiter: defaultDelimiter}
+ JSONType = Type{MainType: "application", SubType: "json", Suffixes: []string{"json"}, Delimiter: defaultDelimiter}
+ RSSType = Type{MainType: "application", SubType: "rss", mimeSuffix: "xml", Suffixes: []string{"xml"}, Delimiter: defaultDelimiter}
+ XMLType = Type{MainType: "application", SubType: "xml", Suffixes: []string{"xml"}, Delimiter: defaultDelimiter}
+ SVGType = Type{MainType: "image", SubType: "svg", mimeSuffix: "xml", Suffixes: []string{"svg"}, Delimiter: defaultDelimiter}
+ TextType = Type{MainType: "text", SubType: "plain", Suffixes: []string{"txt"}, Delimiter: defaultDelimiter}
+ TOMLType = Type{MainType: "application", SubType: "toml", Suffixes: []string{"toml"}, Delimiter: defaultDelimiter}
+ YAMLType = Type{MainType: "application", SubType: "yaml", Suffixes: []string{"yaml", "yml"}, Delimiter: defaultDelimiter}
+
+ // Common image types
+ PNGType = Type{MainType: "image", SubType: "png", Suffixes: []string{"png"}, Delimiter: defaultDelimiter}
+ JPGType = Type{MainType: "image", SubType: "jpg", Suffixes: []string{"jpg", "jpeg"}, Delimiter: defaultDelimiter}
+
+ OctetType = Type{MainType: "application", SubType: "octet-stream"}
+)
+
+// DefaultTypes is the default media types supported by Hugo.
+var DefaultTypes = Types{
+ CalendarType,
+ CSSType,
+ CSVType,
+ SCSSType,
+ SASSType,
+ HTMLType,
+ JavascriptType,
+ JSONType,
+ RSSType,
+ XMLType,
+ SVGType,
+ TextType,
+ OctetType,
+ YAMLType,
+ TOMLType,
+ PNGType,
+ JPGType,
+}
+
+func init() {
+ sort.Sort(DefaultTypes)
+}
+
+// Types is a slice of media types.
+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() }
+
+// GetByType returns a media type for tp.
+func (t Types) GetByType(tp string) (Type, bool) {
+ for _, tt := range t {
+ if strings.EqualFold(tt.Type(), tp) {
+ return tt, true
+ }
+ }
+
+ if !strings.Contains(tp, "+") {
+ // Try with the main and sub type
+ parts := strings.Split(tp, "/")
+ if len(parts) == 2 {
+ return t.GetByMainSubType(parts[0], parts[1])
+ }
+ }
+
+ return Type{}, false
+}
+
+// BySuffix will return all media types matching a suffix.
+func (t Types) BySuffix(suffix string) []Type {
+ var types []Type
+ for _, tt := range t {
+ if match := tt.matchSuffix(suffix); match != "" {
+ types = append(types, tt)
+ }
+ }
+ return types
+}
+
+// GetFirstBySuffix will return the first media type matching the given suffix.
+func (t Types) GetFirstBySuffix(suffix string) (Type, bool) {
+ for _, tt := range t {
+ if match := tt.matchSuffix(suffix); match != "" {
+ tt.fileSuffix = match
+ 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 match := tt.matchSuffix(suffix); match != "" {
+ if found {
+ // ambiguous
+ found = false
+ return
+ }
+ tp = tt
+ tp.fileSuffix = match
+ found = true
+ }
+ }
+ return
+}
+
+func (m Type) matchSuffix(suffix string) string {
+ for _, s := range m.Suffixes {
+ if strings.EqualFold(suffix, s) {
+ return s
+ }
+ }
+
+ return ""
+}
+
+// GetByMainSubType gets a media type given a main and a sub type e.g. "text" and "plain".
+// It will return false if no format could be found, or if the combination given
+// is ambiguous.
+// The lookup is case insensitive.
+func (t Types) GetByMainSubType(mainType, subType string) (tp Type, found bool) {
+ for _, tt := range t {
+ if strings.EqualFold(mainType, tt.MainType) && strings.EqualFold(subType, tt.SubType) {
+ if found {
+ // ambiguous
+ found = false
+ return
+ }
+
+ tp = tt
+ found = true
+ }
+ }
+ return
+}
+
+func suffixIsRemoved() error {
+ return errors.New(`MediaType.Suffix is removed. Before Hugo 0.44 this was used both to set a custom file suffix and as way
+to augment the mediatype definition (what you see after the "+", e.g. "image/svg+xml").
+
+This had its limitations. For one, it was only possible with one file extension per MIME type.
+
+Now you can specify multiple file suffixes using "suffixes", but you need to specify the full MIME type
+identifier:
+
+[mediaTypes]
+[mediaTypes."image/svg+xml"]
+suffixes = ["svg", "abc" ]
+
+In most cases, it will be enough to just change:
+
+[mediaTypes]
+[mediaTypes."my/custom-mediatype"]
+suffix = "txt"
+
+To:
+
+[mediaTypes]
+[mediaTypes."my/custom-mediatype"]
+suffixes = ["txt"]
+
+Note that you can still get the Media Type's suffix from a template: {{ $mediaType.Suffix }}. But this will now map to the MIME type filename.
+`)
+}
+
+// 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(mms ...map[string]interface{}) (Types, error) {
+ var m Types
+
+ // Maps type string to Type. Type string is the full application/svg+xml.
+ mmm := make(map[string]Type)
+ for _, dt := range DefaultTypes {
+ suffixes := make([]string, len(dt.Suffixes))
+ copy(suffixes, dt.Suffixes)
+ dt.Suffixes = suffixes
+ mmm[dt.Type()] = dt
+ }
+
+ for _, mm := range mms {
+ for k, v := range mm {
+ var mediaType Type
+
+ mediaType, found := mmm[k]
+ if !found {
+ var err error
+ mediaType, err = fromString(k)
+ if err != nil {
+ return m, err
+ }
+ }
+
+ if err := mapstructure.WeakDecode(v, &mediaType); err != nil {
+ return m, err
+ }
+
+ vm := v.(map[string]interface{})
+ maps.ToLower(vm)
+ _, delimiterSet := vm["delimiter"]
+ _, suffixSet := vm["suffix"]
+
+ if suffixSet {
+ return Types{}, suffixIsRemoved()
+ }
+
+ // The user may set the delimiter as an empty string.
+ if !delimiterSet && len(mediaType.Suffixes) != 0 {
+ mediaType.Delimiter = defaultDelimiter
+ }
+
+ mmm[k] = mediaType
+
+ }
+ }
+
+ for _, v := range mmm {
+ m = append(m, v)
+ }
+ sort.Sort(m)
+
+ return m, nil
+}
+
+// MarshalJSON returns the JSON encoding of m.
+func (m Type) MarshalJSON() ([]byte, error) {
+ type Alias Type
+ return json.Marshal(&struct {
+ Type string `json:"type"`
+ String string `json:"string"`
+ Alias
+ }{
+ Type: m.Type(),
+ String: m.String(),
+ Alias: (Alias)(m),
+ })
+}
diff --git a/media/mediaType_test.go b/media/mediaType_test.go
new file mode 100644
index 000000000..e51f29b12
--- /dev/null
+++ b/media/mediaType_test.go
@@ -0,0 +1,214 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"},
+ {CSSType, "text", "css", "css", "text/css", "text/css"},
+ {SCSSType, "text", "x-scss", "scss", "text/x-scss", "text/x-scss"},
+ {CSVType, "text", "csv", "csv", "text/csv", "text/csv"},
+ {HTMLType, "text", "html", "html", "text/html", "text/html"},
+ {JavascriptType, "application", "javascript", "js", "application/javascript", "application/javascript"},
+ {JSONType, "application", "json", "json", "application/json", "application/json"},
+ {RSSType, "application", "rss", "xml", "application/rss+xml", "application/rss+xml"},
+ {SVGType, "image", "svg", "svg", "image/svg+xml", "image/svg+xml"},
+ {TextType, "text", "plain", "txt", "text/plain", "text/plain"},
+ {XMLType, "application", "xml", "xml", "application/xml", "application/xml"},
+ {TOMLType, "application", "toml", "toml", "application/toml", "application/toml"},
+ {YAMLType, "application", "yaml", "yaml", "application/yaml", "application/yaml"},
+ } {
+ require.Equal(t, test.expectedMainType, test.tp.MainType)
+ require.Equal(t, test.expectedSubType, test.tp.SubType)
+ require.Equal(t, test.expectedSuffix, test.tp.Suffix(), test.tp.String())
+ require.Equal(t, defaultDelimiter, test.tp.Delimiter)
+
+ require.Equal(t, test.expectedType, test.tp.Type())
+ require.Equal(t, test.expectedString, test.tp.String())
+
+ }
+
+ require.Equal(t, 17, len(DefaultTypes))
+
+}
+
+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)
+
+ mt, found = types.GetByType("application/rss+xml")
+ require.True(t, found)
+ require.Equal(t, mt, RSSType)
+
+ mt, found = types.GetByType("application/rss")
+ require.True(t, found)
+ require.Equal(t, mt, RSSType)
+}
+
+func TestGetByMainSubType(t *testing.T) {
+ assert := require.New(t)
+ f, found := DefaultTypes.GetByMainSubType("text", "plain")
+ assert.True(found)
+ assert.Equal(f, TextType)
+ _, found = DefaultTypes.GetByMainSubType("foo", "plain")
+ assert.False(found)
+}
+
+func TestBySuffix(t *testing.T) {
+ assert := require.New(t)
+ formats := DefaultTypes.BySuffix("xml")
+ assert.Equal(2, len(formats))
+ assert.Equal("rss", formats[0].SubType)
+ assert.Equal("xml", formats[1].SubType)
+}
+
+func TestGetFirstBySuffix(t *testing.T) {
+ assert := require.New(t)
+ f, found := DefaultTypes.GetFirstBySuffix("xml")
+ assert.True(found)
+ assert.Equal(Type{MainType: "application", SubType: "rss", mimeSuffix: "xml", Delimiter: ".", Suffixes: []string{"xml"}, fileSuffix: "xml"}, f)
+}
+
+func TestFromTypeString(t *testing.T) {
+ f, err := fromString("text/html")
+ require.NoError(t, err)
+ require.Equal(t, HTMLType.Type(), f.Type())
+
+ f, err = fromString("application/custom")
+ require.NoError(t, err)
+ require.Equal(t, Type{MainType: "application", SubType: "custom", mimeSuffix: "", fileSuffix: ""}, f)
+
+ f, err = fromString("application/custom+sfx")
+ require.NoError(t, err)
+ require.Equal(t, Type{MainType: "application", SubType: "custom", mimeSuffix: "sfx"}, f)
+
+ _, err = fromString("noslash")
+ require.Error(t, err)
+
+ f, err = fromString("text/xml; charset=utf-8")
+ require.NoError(t, err)
+ require.Equal(t, Type{MainType: "text", SubType: "xml", mimeSuffix: ""}, f)
+ require.Equal(t, "", f.Suffix())
+}
+
+// Add a test for the SVG case
+// https://github.com/gohugoio/hugo/issues/4920
+func TestFromExtensionMultipleSuffixes(t *testing.T) {
+ assert := require.New(t)
+ tp, found := DefaultTypes.GetBySuffix("svg")
+ assert.True(found)
+ assert.Equal("image/svg+xml", tp.String())
+ assert.Equal("svg", tp.fileSuffix)
+ assert.Equal(".svg", tp.FullSuffix())
+ tp, found = DefaultTypes.GetByType("image/svg+xml")
+ assert.True(found)
+ assert.Equal("image/svg+xml", tp.String())
+ assert.True(found)
+ assert.Equal(".svg", tp.FullSuffix())
+
+}
+
+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{}{
+ {
+ "application/json": map[string]interface{}{
+ "suffixes": []string{"jasn"}}}},
+ false,
+ func(t *testing.T, name string, tt Types) {
+ require.Len(t, tt, len(DefaultTypes))
+ json, found := tt.GetBySuffix("jasn")
+ require.True(t, found)
+ require.Equal(t, "application/json", json.String(), name)
+ require.Equal(t, ".jasn", json.FullSuffix())
+ }},
+ {
+ "MIME suffix in key, multiple file suffixes, custom delimiter",
+ []map[string]interface{}{
+ {
+ "application/hugo+hg": map[string]interface{}{
+ "suffixes": []string{"hg1", "hg2"},
+ "Delimiter": "_",
+ }}},
+ false,
+ func(t *testing.T, name string, tt Types) {
+ require.Len(t, tt, len(DefaultTypes)+1)
+ hg, found := tt.GetBySuffix("hg2")
+ require.True(t, found)
+ require.Equal(t, "hg", hg.mimeSuffix)
+ require.Equal(t, "hg2", hg.Suffix())
+ require.Equal(t, "_hg2", hg.FullSuffix())
+ require.Equal(t, "application/hugo+hg", hg.String(), name)
+
+ hg, found = tt.GetByType("application/hugo+hg")
+ require.True(t, found)
+
+ }},
+ {
+ "Add custom media type",
+ []map[string]interface{}{
+ {
+ "text/hugo+hgo": map[string]interface{}{
+ "Suffixes": []string{"hgo2"}}}},
+ 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("hgo2")
+ require.True(t, found)
+ require.Equal(t, "text/hugo+hgo", hugo.String(), name)
+ }},
+ }
+
+ 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/metrics/metrics.go b/metrics/metrics.go
new file mode 100644
index 000000000..329981202
--- /dev/null
+++ b/metrics/metrics.go
@@ -0,0 +1,262 @@
+// 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 metrics provides simple metrics tracking features.
+package metrics
+
+import (
+ "fmt"
+ "io"
+ "math"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/gohugoio/hugo/compare"
+
+ "github.com/gohugoio/hugo/common/hreflect"
+)
+
+// The Provider interface defines an interface for measuring metrics.
+type Provider interface {
+ // MeasureSince adds a measurement for key to the metric store.
+ // Used with defer and time.Now().
+ MeasureSince(key string, start time.Time)
+
+ // WriteMetrics will write a summary of the metrics to w.
+ WriteMetrics(w io.Writer)
+
+ // TrackValue tracks the value for diff calculations etc.
+ TrackValue(key string, value interface{})
+
+ // Reset clears the metric store.
+ Reset()
+}
+
+type diff struct {
+ baseline interface{}
+ count int
+ simSum int
+}
+
+func (d *diff) add(v interface{}) *diff {
+ if !hreflect.IsTruthful(v) {
+ d.baseline = v
+ d.count = 1
+ d.simSum = 100 // If we get only one it is very cache friendly.
+ return d
+ }
+
+ d.simSum += howSimilar(v, d.baseline)
+ d.count++
+
+ return d
+}
+
+// Store provides storage for a set of metrics.
+type Store struct {
+ calculateHints bool
+ metrics map[string][]time.Duration
+ mu sync.Mutex
+ diffs map[string]*diff
+ diffmu sync.Mutex
+}
+
+// NewProvider returns a new instance of a metric store.
+func NewProvider(calculateHints bool) Provider {
+ return &Store{
+ calculateHints: calculateHints,
+ metrics: make(map[string][]time.Duration),
+ diffs: make(map[string]*diff),
+ }
+}
+
+// Reset clears the metrics store.
+func (s *Store) Reset() {
+ s.mu.Lock()
+ s.metrics = make(map[string][]time.Duration)
+ s.mu.Unlock()
+ s.diffmu.Lock()
+ s.diffs = make(map[string]*diff)
+ s.diffmu.Unlock()
+}
+
+// TrackValue tracks the value for diff calculations etc.
+func (s *Store) TrackValue(key string, value interface{}) {
+ if !s.calculateHints {
+ return
+ }
+
+ s.diffmu.Lock()
+ var (
+ d *diff
+ found bool
+ )
+
+ d, found = s.diffs[key]
+
+ if !found {
+ d = &diff{}
+ s.diffs[key] = d
+ }
+
+ d.add(value)
+ s.diffmu.Unlock()
+}
+
+// MeasureSince adds a measurement for key to the metric store.
+func (s *Store) MeasureSince(key string, start time.Time) {
+ s.mu.Lock()
+ s.metrics[key] = append(s.metrics[key], time.Since(start))
+ s.mu.Unlock()
+}
+
+// WriteMetrics writes a summary of the metrics to w.
+func (s *Store) WriteMetrics(w io.Writer) {
+ s.mu.Lock()
+
+ results := make([]result, len(s.metrics))
+
+ var i int
+ for k, v := range s.metrics {
+ var sum time.Duration
+ var max time.Duration
+
+ diff, found := s.diffs[k]
+ cacheFactor := 0
+ if found {
+ cacheFactor = int(math.Floor(float64(diff.simSum) / float64(diff.count)))
+ }
+
+ for _, d := range v {
+ sum += d
+ if d > max {
+ max = d
+ }
+ }
+
+ avg := time.Duration(int(sum) / len(v))
+
+ results[i] = result{key: k, count: len(v), max: max, sum: sum, avg: avg, cacheFactor: cacheFactor}
+ i++
+ }
+
+ s.mu.Unlock()
+
+ if s.calculateHints {
+ fmt.Fprintf(w, " %9s %13s %12s %12s %5s %s\n", "cache", "cumulative", "average", "maximum", "", "")
+ fmt.Fprintf(w, " %9s %13s %12s %12s %5s %s\n", "potential", "duration", "duration", "duration", "count", "template")
+ fmt.Fprintf(w, " %9s %13s %12s %12s %5s %s\n", "-----", "----------", "--------", "--------", "-----", "--------")
+ } else {
+ fmt.Fprintf(w, " %13s %12s %12s %5s %s\n", "cumulative", "average", "maximum", "", "")
+ fmt.Fprintf(w, " %13s %12s %12s %5s %s\n", "duration", "duration", "duration", "count", "template")
+ fmt.Fprintf(w, " %13s %12s %12s %5s %s\n", "----------", "--------", "--------", "-----", "--------")
+
+ }
+
+ sort.Sort(bySum(results))
+ for _, v := range results {
+ if s.calculateHints {
+ fmt.Fprintf(w, " %9d %13s %12s %12s %5d %s\n", v.cacheFactor, v.sum, v.avg, v.max, v.count, v.key)
+ } else {
+ fmt.Fprintf(w, " %13s %12s %12s %5d %s\n", v.sum, v.avg, v.max, v.count, v.key)
+ }
+ }
+
+}
+
+// A result represents the calculated results for a given metric.
+type result struct {
+ key string
+ count int
+ cacheFactor int
+ sum time.Duration
+ max time.Duration
+ avg time.Duration
+}
+
+type bySum []result
+
+func (b bySum) Len() int { return len(b) }
+func (b bySum) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
+func (b bySum) Less(i, j int) bool { return b[i].sum > b[j].sum }
+
+// howSimilar is a naive diff implementation that returns
+// a number between 0-100 indicating how similar a and b are.
+func howSimilar(a, b interface{}) int {
+ // TODO(bep) object equality fast path, but remember that
+ // we can get anytning in here.
+
+ as, ok1 := a.(string)
+ bs, ok2 := b.(string)
+
+ if ok1 && ok2 {
+ return howSimilarStrings(as, bs)
+ }
+
+ if ok1 != ok2 {
+ return 0
+ }
+
+ e1, ok1 := a.(compare.Eqer)
+ e2, ok2 := b.(compare.Eqer)
+ if ok1 && ok2 && e1.Eq(e2) {
+ return 100
+ }
+
+ pe1, pok1 := a.(compare.ProbablyEqer)
+ pe2, pok2 := b.(compare.ProbablyEqer)
+ if pok1 && pok2 && pe1.ProbablyEq(pe2) {
+ return 90
+ }
+
+ return 0
+}
+
+// howSimilar is a naive diff implementation that returns
+// a number between 0-100 indicating how similar a and b are.
+// 100 is when all words in a also exists in b.
+func howSimilarStrings(a, b string) int {
+
+ // Give some weight to the word positions.
+ const partitionSize = 4
+
+ af, bf := strings.Fields(a), strings.Fields(b)
+ if len(bf) > len(af) {
+ af, bf = bf, af
+ }
+
+ m1 := make(map[string]bool)
+ for i, x := range bf {
+ partition := partition(i, partitionSize)
+ key := x + "/" + strconv.Itoa(partition)
+ m1[key] = true
+ }
+
+ common := 0
+ for i, x := range af {
+ partition := partition(i, partitionSize)
+ key := x + "/" + strconv.Itoa(partition)
+ if m1[key] {
+ common++
+ }
+ }
+
+ return int(math.Floor((float64(common) / float64(len(af)) * 100)))
+}
+
+func partition(d, scale int) int {
+ return int(math.Floor((float64(d) / float64(scale)))) * scale
+}
diff --git a/metrics/metrics_test.go b/metrics/metrics_test.go
new file mode 100644
index 000000000..d22a51733
--- /dev/null
+++ b/metrics/metrics_test.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 metrics
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/gohugoio/hugo/resources/page"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestSimilarPercentage(t *testing.T) {
+ assert := require.New(t)
+
+ sentence := "this is some words about nothing, Hugo!"
+ words := strings.Fields(sentence)
+ for i, j := 0, len(words)-1; i < j; i, j = i+1, j-1 {
+ words[i], words[j] = words[j], words[i]
+ }
+ sentenceReversed := strings.Join(words, " ")
+
+ assert.Equal(100, howSimilar("Hugo Rules", "Hugo Rules"))
+ assert.Equal(50, howSimilar("Hugo Rules", "Hugo Rocks"))
+ assert.Equal(66, howSimilar("The Hugo Rules", "The Hugo Rocks"))
+ assert.Equal(66, howSimilar("The Hugo Rules", "The Hugo"))
+ assert.Equal(66, howSimilar("The Hugo", "The Hugo Rules"))
+ assert.Equal(0, howSimilar("Totally different", "Not Same"))
+ assert.Equal(14, howSimilar(sentence, sentenceReversed))
+
+}
+
+func TestSimilarPercentageNonString(t *testing.T) {
+ assert := require.New(t)
+ assert.Equal(100, howSimilar(page.NopPage, page.NopPage))
+ assert.Equal(90, howSimilar(page.Pages{}, page.Pages{}))
+}
+
+func BenchmarkHowSimilar(b *testing.B) {
+ s1 := "Hugo is cool and " + strings.Repeat("fun ", 10) + "!"
+ s2 := "Hugo is cool and " + strings.Repeat("cool ", 10) + "!"
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ howSimilar(s1, s2)
+ }
+}
diff --git a/minifiers/minifiers.go b/minifiers/minifiers.go
new file mode 100644
index 000000000..9533ebb69
--- /dev/null
+++ b/minifiers/minifiers.go
@@ -0,0 +1,112 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package minifiers contains minifiers mapped to MIME types. This package is used
+// in both the resource transformation, i.e. resources.Minify, and in the publishing
+// chain.
+package minifiers
+
+import (
+ "io"
+ "regexp"
+
+ "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/transform"
+
+ "github.com/gohugoio/hugo/media"
+ "github.com/tdewolff/minify/v2"
+ "github.com/tdewolff/minify/v2/css"
+ "github.com/tdewolff/minify/v2/html"
+ "github.com/tdewolff/minify/v2/js"
+ "github.com/tdewolff/minify/v2/json"
+ "github.com/tdewolff/minify/v2/svg"
+ "github.com/tdewolff/minify/v2/xml"
+)
+
+// Client wraps a minifier.
+type Client struct {
+ m *minify.M
+}
+
+// Transformer returns a func that can be used in the transformer publishing chain.
+// TODO(bep) minify config etc
+func (m Client) Transformer(mediatype media.Type) transform.Transformer {
+ _, params, min := m.m.Match(mediatype.Type())
+ if min == nil {
+ // No minifier for this MIME type
+ return nil
+ }
+
+ return func(ft transform.FromTo) error {
+ // Note that the source io.Reader will already be buffered, but it implements
+ // the Bytes() method, which is recognized by the Minify library.
+ return min.Minify(m.m, ft.To(), ft.From(), params)
+ }
+}
+
+// Minify tries to minify the src into dst given a MIME type.
+func (m Client) Minify(mediatype media.Type, dst io.Writer, src io.Reader) error {
+ return m.m.Minify(mediatype.Type(), dst, src)
+}
+
+// New creates a new Client with the provided MIME types as the mapping foundation.
+// The HTML minifier is also registered for additional HTML types (AMP etc.) in the
+// provided list of output formats.
+func New(mediaTypes media.Types, outputFormats output.Formats) Client {
+ m := minify.New()
+ htmlMin := &html.Minifier{
+ KeepDocumentTags: true,
+ KeepConditionalComments: true,
+ KeepEndTags: true,
+ KeepDefaultAttrVals: true,
+ }
+
+ cssMin := &css.Minifier{
+ Decimals: -1,
+ KeepCSS2: true,
+ }
+
+ // We use the Type definition of the media types defined in the site if found.
+ addMinifier(m, mediaTypes, "css", cssMin)
+ addMinifierFunc(m, mediaTypes, "js", js.Minify)
+ m.AddFuncRegexp(regexp.MustCompile("^(application|text)/(x-)?(java|ecma)script$"), js.Minify)
+ m.AddFuncRegexp(regexp.MustCompile(`^(application|text)/(x-|ld\+)?json$`), json.Minify)
+ addMinifierFunc(m, mediaTypes, "json", json.Minify)
+ addMinifierFunc(m, mediaTypes, "svg", svg.Minify)
+ addMinifierFunc(m, mediaTypes, "xml", xml.Minify)
+
+ // HTML
+ addMinifier(m, mediaTypes, "html", htmlMin)
+ for _, of := range outputFormats {
+ if of.IsHTML {
+ m.Add(of.MediaType.Type(), htmlMin)
+ }
+ }
+
+ return Client{m: m}
+
+}
+
+func addMinifier(m *minify.M, mt media.Types, suffix string, min minify.Minifier) {
+ types := mt.BySuffix(suffix)
+ for _, t := range types {
+ m.Add(t.Type(), min)
+ }
+}
+
+func addMinifierFunc(m *minify.M, mt media.Types, suffix string, min minify.MinifierFunc) {
+ types := mt.BySuffix(suffix)
+ for _, t := range types {
+ m.AddFunc(t.Type(), min)
+ }
+}
diff --git a/minifiers/minifiers_test.go b/minifiers/minifiers_test.go
new file mode 100644
index 000000000..acfa22d2c
--- /dev/null
+++ b/minifiers/minifiers_test.go
@@ -0,0 +1,93 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package minifiers
+
+import (
+ "bytes"
+ "strings"
+ "testing"
+
+ "github.com/gohugoio/hugo/media"
+
+ "github.com/gohugoio/hugo/output"
+ "github.com/stretchr/testify/require"
+)
+
+func TestNew(t *testing.T) {
+ assert := require.New(t)
+ m := New(media.DefaultTypes, output.DefaultFormats)
+
+ var rawJS string
+ var minJS string
+ rawJS = " var foo =1 ; foo ++ ; "
+ minJS = "var foo=1;foo++;"
+
+ var rawJSON string
+ var minJSON string
+ rawJSON = " { \"a\" : 123 , \"b\":2, \"c\": 5 } "
+ minJSON = "{\"a\":123,\"b\":2,\"c\":5}"
+
+ for _, test := range []struct {
+ tp media.Type
+ rawString string
+ expectedMinString string
+ }{
+ {media.CSSType, " body { color: blue; } ", "body{color:blue}"},
+ {media.RSSType, " <hello> Hugo! </hello> ", "<hello>Hugo!</hello>"}, // RSS should be handled as XML
+ {media.JSONType, rawJSON, minJSON},
+ {media.JavascriptType, rawJS, minJS},
+ // JS Regex minifiers
+ {media.Type{MainType: "application", SubType: "ecmascript"}, rawJS, minJS},
+ {media.Type{MainType: "application", SubType: "javascript"}, rawJS, minJS},
+ {media.Type{MainType: "application", SubType: "x-javascript"}, rawJS, minJS},
+ {media.Type{MainType: "application", SubType: "x-ecmascript"}, rawJS, minJS},
+ {media.Type{MainType: "text", SubType: "ecmascript"}, rawJS, minJS},
+ {media.Type{MainType: "text", SubType: "javascript"}, rawJS, minJS},
+ {media.Type{MainType: "text", SubType: "x-javascript"}, rawJS, minJS},
+ {media.Type{MainType: "text", SubType: "x-ecmascript"}, rawJS, minJS},
+ // JSON Regex minifiers
+ {media.Type{MainType: "application", SubType: "json"}, rawJSON, minJSON},
+ {media.Type{MainType: "application", SubType: "x-json"}, rawJSON, minJSON},
+ {media.Type{MainType: "application", SubType: "ld+json"}, rawJSON, minJSON},
+ {media.Type{MainType: "text", SubType: "json"}, rawJSON, minJSON},
+ {media.Type{MainType: "text", SubType: "x-json"}, rawJSON, minJSON},
+ {media.Type{MainType: "text", SubType: "ld+json"}, rawJSON, minJSON},
+ } {
+ var b bytes.Buffer
+
+ assert.NoError(m.Minify(test.tp, &b, strings.NewReader(test.rawString)))
+ assert.Equal(test.expectedMinString, b.String())
+ }
+
+}
+
+func TestBugs(t *testing.T) {
+ assert := require.New(t)
+ m := New(media.DefaultTypes, output.DefaultFormats)
+
+ for _, test := range []struct {
+ tp media.Type
+ rawString string
+ expectedMinString string
+ }{
+ // https://github.com/gohugoio/hugo/issues/5506
+ {media.CSSType, " body { color: rgba(000, 000, 000, 0.7); }", "body{color:rgba(0,0,0,.7)}"},
+ } {
+ var b bytes.Buffer
+
+ assert.NoError(m.Minify(test.tp, &b, strings.NewReader(test.rawString)))
+ assert.Equal(test.expectedMinString, b.String())
+ }
+
+}
diff --git a/navigation/menu.go b/navigation/menu.go
new file mode 100644
index 000000000..47d40a3c7
--- /dev/null
+++ b/navigation/menu.go
@@ -0,0 +1,234 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package navigation
+
+import (
+ "github.com/gohugoio/hugo/common/types"
+
+ "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 {
+ ConfiguredURL string // The URL value from front matter / config.
+ Page Page
+ Name string
+ Menu string
+ Identifier string
+ title string
+ Pre template.HTML
+ Post template.HTML
+ Weight int
+ Parent string
+ Children Menu
+}
+
+func (m *MenuEntry) URL() string {
+ if m.ConfiguredURL != "" {
+ return m.ConfiguredURL
+ }
+
+ if !types.IsNil(m.Page) {
+ return m.Page.RelPermalink()
+ }
+
+ return ""
+}
+
+// A narrow version of page.Page.
+type Page interface {
+ LinkTitle() string
+ RelPermalink() string
+ Section() string
+ Weight() int
+ IsPage() bool
+ Params() map[string]interface{}
+}
+
+// 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
+
+// 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 {
+ murl, inmeurl := m.URL(), inme.URL()
+ return murl != "" && inmeurl != "" && murl == inmeurl
+}
+
+func (m *MenuEntry) MarshallMap(ime map[string]interface{}) {
+ for k, v := range ime {
+ loki := strings.ToLower(k)
+ switch loki {
+ case "url":
+ m.ConfiguredURL = cast.ToString(v)
+ case "weight":
+ m.Weight = cast.ToInt(v)
+ case "name":
+ m.Name = cast.ToString(v)
+ case "title":
+ m.title = 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 {
+ m = append(m, me)
+ // TODO(bep)
+ 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
+}
+
+func (m *MenuEntry) Title() string {
+ if m.title != "" {
+ return m.title
+ }
+
+ if m.Page != nil {
+ return m.Page.LinkTitle()
+ }
+
+ return ""
+}
diff --git a/navigation/pagemenus.go b/navigation/pagemenus.go
new file mode 100644
index 000000000..4d2c45767
--- /dev/null
+++ b/navigation/pagemenus.go
@@ -0,0 +1,238 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package navigation
+
+import (
+ "github.com/pkg/errors"
+ "github.com/spf13/cast"
+)
+
+type PageMenusProvider interface {
+ PageMenusGetter
+ MenyQueryProvider
+}
+
+type PageMenusGetter interface {
+ Menus() PageMenus
+}
+
+type MenusGetter interface {
+ Menus() Menus
+}
+
+type MenyQueryProvider interface {
+ HasMenuCurrent(menuID string, me *MenuEntry) bool
+ IsMenuCurrent(menuID string, inme *MenuEntry) bool
+}
+
+func PageMenusFromPage(p Page) (PageMenus, error) {
+ params := p.Params()
+
+ ms, ok := params["menus"]
+ if !ok {
+ ms, ok = params["menu"]
+ }
+
+ pm := PageMenus{}
+
+ if !ok {
+ return nil, nil
+ }
+
+ me := MenuEntry{Page: p, Name: p.LinkTitle(), Weight: p.Weight()}
+
+ // Could be the name of the menu to attach it to
+ mname, err := cast.ToStringE(ms)
+
+ if err == nil {
+ me.Menu = mname
+ pm[mname] = &me
+ return pm, nil
+ }
+
+ // Could be a slice of strings
+ mnames, err := cast.ToStringSliceE(ms)
+
+ if err == nil {
+ for _, mname := range mnames {
+ me.Menu = mname
+ pm[mname] = &me
+ }
+ return pm, nil
+ }
+
+ // Could be a structured menu entry
+ menus, err := cast.ToStringMapE(ms)
+ if err != nil {
+ return pm, errors.Wrapf(err, "unable to process menus for %q", p.LinkTitle())
+ }
+
+ for name, menu := range menus {
+ menuEntry := MenuEntry{Page: p, Name: p.LinkTitle(), Weight: p.Weight(), Menu: name}
+ if menu != nil {
+ ime, err := cast.ToStringMapE(menu)
+ if err != nil {
+ return pm, errors.Wrapf(err, "unable to process menus for %q", p.LinkTitle())
+ }
+
+ menuEntry.MarshallMap(ime)
+ }
+ pm[name] = &menuEntry
+ }
+
+ return pm, nil
+
+}
+
+func NewMenuQueryProvider(
+ setionPagesMenu string,
+ pagem PageMenusGetter,
+ sitem MenusGetter,
+ p Page) MenyQueryProvider {
+
+ return &pageMenus{
+ p: p,
+ pagem: pagem,
+ sitem: sitem,
+ setionPagesMenu: setionPagesMenu,
+ }
+}
+
+type pageMenus struct {
+ pagem PageMenusGetter
+ sitem MenusGetter
+ setionPagesMenu string
+ p Page
+}
+
+func (pm *pageMenus) HasMenuCurrent(menuID string, me *MenuEntry) bool {
+
+ // page is labeled as "shadow-member" of the menu with the same identifier as the section
+ if pm.setionPagesMenu != "" {
+ section := pm.p.Section()
+
+ if section != "" && pm.setionPagesMenu == menuID && section == me.Identifier {
+ return true
+ }
+ }
+
+ if !me.HasChildren() {
+ return false
+ }
+
+ menus := pm.pagem.Menus()
+
+ if m, ok := menus[menuID]; ok {
+
+ for _, child := range me.Children {
+ if child.IsEqual(m) {
+ return true
+ }
+ if pm.HasMenuCurrent(menuID, child) {
+ return true
+ }
+ }
+ }
+
+ if pm.p == nil || pm.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{Page: pm.p, Name: pm.p.LinkTitle()}
+
+ for _, child := range me.Children {
+ if nme.IsSameResource(child) {
+ return true
+ }
+ if pm.HasMenuCurrent(menuID, child) {
+ return true
+ }
+ }
+
+ return false
+
+}
+
+func (pm *pageMenus) IsMenuCurrent(menuID string, inme *MenuEntry) bool {
+ menus := pm.pagem.Menus()
+
+ if me, ok := menus[menuID]; ok {
+ if me.IsEqual(inme) {
+ return true
+ }
+ }
+
+ if pm.p == nil || pm.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{Page: pm.p, Name: pm.p.LinkTitle()}
+
+ 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 := pm.sitem.Menus()[menuID]; ok {
+ for _, menuEntry := range menu {
+ if menuEntry.IsSameResource(inme) {
+ return true
+ }
+
+ descendantFound := pm.isSameAsDescendantMenu(inme, menuEntry)
+ if descendantFound {
+ return descendantFound
+ }
+
+ }
+ }
+
+ return false
+}
+
+func (pm *pageMenus) isSameAsDescendantMenu(inme *MenuEntry, parent *MenuEntry) bool {
+ if parent.HasChildren() {
+ for _, child := range parent.Children {
+ if child.IsSameResource(inme) {
+ return true
+ }
+ descendantFound := pm.isSameAsDescendantMenu(inme, child)
+ if descendantFound {
+ return descendantFound
+ }
+ }
+ }
+ return false
+}
+
+var NopPageMenus = new(nopPageMenus)
+
+type nopPageMenus int
+
+func (m nopPageMenus) Menus() PageMenus {
+ return PageMenus{}
+}
+
+func (m nopPageMenus) HasMenuCurrent(menuID string, me *MenuEntry) bool {
+ return false
+}
+
+func (m nopPageMenus) IsMenuCurrent(menuID string, inme *MenuEntry) bool {
+ return false
+}
diff --git a/output/docshelper.go b/output/docshelper.go
new file mode 100644
index 000000000..ad16d3257
--- /dev/null
+++ b/output/docshelper.go
@@ -0,0 +1,98 @@
+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
+ Kind 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
+ f Format
+ }{
+ // Taxonomy output.LayoutDescriptor={categories category taxonomy en false Type Section
+ {"Single page in \"posts\" section", LayoutDescriptor{Kind: "page", Type: "posts"}, HTMLFormat},
+ {"Single page in \"posts\" section with layout set", LayoutDescriptor{Kind: "page", Type: "posts", Layout: demoLayout}, HTMLFormat},
+ {"AMP single page", LayoutDescriptor{Kind: "page", Type: "posts"}, AMPFormat},
+ {"AMP single page, French language", LayoutDescriptor{Kind: "page", Type: "posts", Lang: "fr"}, AMPFormat},
+ // All section or typeless pages gets "page" as type
+ {"Home page", LayoutDescriptor{Kind: "home", Type: "page"}, HTMLFormat},
+ {"Home page with type set", LayoutDescriptor{Kind: "home", Type: demoType}, HTMLFormat},
+ {"Home page with layout set", LayoutDescriptor{Kind: "home", Type: "page", Layout: demoLayout}, HTMLFormat},
+ {`AMP home, French language"`, LayoutDescriptor{Kind: "home", Type: "page", Lang: "fr"}, AMPFormat},
+ {"JSON home", LayoutDescriptor{Kind: "home", Type: "page"}, JSONFormat},
+ {"RSS home", LayoutDescriptor{Kind: "home", Type: "page"}, RSSFormat},
+ {"RSS section posts", LayoutDescriptor{Kind: "section", Type: "posts"}, RSSFormat},
+ {"Taxonomy list in categories", LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category"}, RSSFormat},
+ {"Taxonomy terms in categories", LayoutDescriptor{Kind: "taxonomyTerm", Type: "categories", Section: "category"}, RSSFormat},
+ {"Section list for \"posts\" section", LayoutDescriptor{Kind: "section", Type: "posts", Section: "posts"}, HTMLFormat},
+ {"Section list for \"posts\" section with type set to \"blog\"", LayoutDescriptor{Kind: "section", Type: "blog", Section: "posts"}, HTMLFormat},
+ {"Section list for \"posts\" section with layout set to \"demoLayout\"", LayoutDescriptor{Kind: "section", Layout: demoLayout, Section: "posts"}, HTMLFormat},
+
+ {"Taxonomy list in categories", LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category"}, HTMLFormat},
+ {"Taxonomy term in categories", LayoutDescriptor{Kind: "taxonomyTerm", Type: "categories", Section: "category"}, HTMLFormat},
+ } {
+
+ l := NewLayoutHandler()
+ layouts, _ := l.For(example.d, example.f)
+
+ basicExamples = append(basicExamples, Example{
+ Example: example.name,
+ Kind: example.d.Kind,
+ 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 {
+ if strings.Contains(ll, "page/") {
+ // This is a valid lookup, but it's more confusing than useful.
+ continue
+ }
+ ll = "layouts/" + strings.TrimPrefix(ll, "_text/")
+
+ 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..5d72938af
--- /dev/null
+++ b/output/layout.go
@@ -0,0 +1,272 @@
+// 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"
+ "strings"
+ "sync"
+
+ "github.com/gohugoio/hugo/helpers"
+)
+
+// These may be used as content sections with potential conflicts. Avoid that.
+var reservedSections = map[string]bool{
+ "shortcodes": true,
+ "partials": true,
+}
+
+// LayoutDescriptor describes how a layout should be chosen. This is
+// typically built from a Page.
+type LayoutDescriptor struct {
+ Type string
+ Section string
+ Kind string
+ Lang string
+ Layout string
+ // LayoutOverride indicates what we should only look for the above layout.
+ LayoutOverride bool
+}
+
+// LayoutHandler calculates the layout template to use to render a given output type.
+type LayoutHandler struct {
+ mu sync.RWMutex
+ cache map[layoutCacheKey][]string
+}
+
+type layoutCacheKey struct {
+ d LayoutDescriptor
+ f string
+}
+
+// NewLayoutHandler creates a new LayoutHandler.
+func NewLayoutHandler() *LayoutHandler {
+ return &LayoutHandler{cache: make(map[layoutCacheKey][]string)}
+}
+
+// For returns a layout for the given LayoutDescriptor and options.
+// Layouts are rendered and cached internally.
+func (l *LayoutHandler) For(d LayoutDescriptor, f Format) ([]string, error) {
+
+ // We will get lots of requests for the same layouts, so avoid recalculations.
+ key := layoutCacheKey{d, f.Name}
+ l.mu.RLock()
+ if cacheVal, found := l.cache[key]; found {
+ l.mu.RUnlock()
+ return cacheVal, nil
+ }
+ l.mu.RUnlock()
+
+ layouts := resolvePageTemplate(d, f)
+
+ layouts = prependTextPrefixIfNeeded(f, layouts...)
+ layouts = helpers.UniqueStrings(layouts)
+
+ l.mu.Lock()
+ l.cache[key] = layouts
+ l.mu.Unlock()
+
+ return layouts, nil
+}
+
+type layoutBuilder struct {
+ layoutVariations []string
+ typeVariations []string
+ d LayoutDescriptor
+ f Format
+}
+
+func (l *layoutBuilder) addLayoutVariations(vars ...string) {
+ for _, layoutVar := range vars {
+ if l.d.LayoutOverride && layoutVar != l.d.Layout {
+ continue
+ }
+ l.layoutVariations = append(l.layoutVariations, layoutVar)
+ }
+}
+
+func (l *layoutBuilder) addTypeVariations(vars ...string) {
+ for _, typeVar := range vars {
+ if !reservedSections[typeVar] {
+ l.typeVariations = append(l.typeVariations, typeVar)
+ }
+ }
+}
+
+func (l *layoutBuilder) addSectionType() {
+ if l.d.Section != "" {
+ l.addTypeVariations(l.d.Section)
+ }
+}
+
+func (l *layoutBuilder) addKind() {
+ l.addLayoutVariations(l.d.Kind)
+ l.addTypeVariations(l.d.Kind)
+}
+
+func resolvePageTemplate(d LayoutDescriptor, f Format) []string {
+
+ b := &layoutBuilder{d: d, f: f}
+
+ if d.Layout != "" {
+ b.addLayoutVariations(d.Layout)
+ }
+
+ if d.Type != "" {
+ b.addTypeVariations(d.Type)
+ }
+
+ switch d.Kind {
+ case "page":
+ b.addLayoutVariations("single")
+ b.addSectionType()
+ case "home":
+ b.addLayoutVariations("index", "home")
+ // Also look in the root
+ b.addTypeVariations("")
+ case "section":
+ if d.Section != "" {
+ b.addLayoutVariations(d.Section)
+ }
+ b.addSectionType()
+ b.addKind()
+ case "taxonomy":
+ if d.Section != "" {
+ b.addLayoutVariations(d.Section)
+ }
+ b.addKind()
+ b.addSectionType()
+
+ case "taxonomyTerm":
+ if d.Section != "" {
+ b.addLayoutVariations(d.Section + ".terms")
+ }
+ b.addTypeVariations("taxonomy")
+ b.addSectionType()
+ b.addLayoutVariations("terms")
+
+ }
+
+ isRSS := f.Name == RSSFormat.Name
+ if isRSS {
+ // The historic and common rss.xml case
+ b.addLayoutVariations("")
+ }
+
+ // All have _default in their lookup path
+ b.addTypeVariations("_default")
+
+ if d.Kind != "page" {
+ // Add the common list type
+ b.addLayoutVariations("list")
+ }
+
+ layouts := b.resolveVariations()
+
+ if isRSS {
+ layouts = append(layouts, "_internal/_default/rss.xml")
+ }
+
+ return layouts
+
+}
+
+func (l *layoutBuilder) resolveVariations() []string {
+
+ var layouts []string
+
+ var variations []string
+ name := strings.ToLower(l.f.Name)
+
+ if l.d.Lang != "" {
+ // We prefer the most specific type before language.
+ variations = append(variations, []string{fmt.Sprintf("%s.%s", l.d.Lang, name), name, l.d.Lang}...)
+ } else {
+ variations = append(variations, name)
+ }
+
+ variations = append(variations, "")
+
+ for _, typeVar := range l.typeVariations {
+ for _, variation := range variations {
+ for _, layoutVar := range l.layoutVariations {
+ if variation == "" && layoutVar == "" {
+ continue
+ }
+ template := layoutTemplate(typeVar, layoutVar)
+ layouts = append(layouts, replaceKeyValues(template,
+ "TYPE", typeVar,
+ "LAYOUT", layoutVar,
+ "VARIATIONS", variation,
+ "EXTENSION", l.f.MediaType.Suffix(),
+ ))
+ }
+ }
+
+ }
+
+ return filterDotLess(layouts)
+}
+
+func layoutTemplate(typeVar, layoutVar string) string {
+
+ var l string
+
+ if typeVar != "" {
+ l = "TYPE/"
+ }
+
+ if layoutVar != "" {
+ l += "LAYOUT.VARIATIONS.EXTENSION"
+ } else {
+ l += "VARIATIONS.EXTENSION"
+ }
+
+ return l
+}
+
+func filterDotLess(layouts []string) []string {
+ var filteredLayouts []string
+
+ for _, l := range layouts {
+ l = strings.Replace(l, "..", ".", -1)
+ l = strings.Trim(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)
+}
diff --git a/output/layout_base.go b/output/layout_base.go
new file mode 100644
index 000000000..b8930df82
--- /dev/null
+++ b/output/layout_base.go
@@ -0,0 +1,187 @@
+// 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"), []byte("{{- define"), []byte("{{-define")}
+)
+
+// TemplateNames represents a template naming scheme.
+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
+}
+
+// TemplateLookupDescriptor describes the template lookup configuration.
+type TemplateLookupDescriptor struct {
+ // The full path to the site root.
+ WorkingDir string
+
+ // The path to the template relative the the base.
+ // I.e. shortcodes/youtube.html
+ RelPath string
+
+ // The template name prefix to look for.
+ Prefix 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 isShorthCodeOrPartial(name string) bool {
+ return strings.HasPrefix(name, "shortcodes/") || strings.HasPrefix(name, "partials/")
+}
+
+// CreateTemplateNames returns a TemplateNames object for a given template.
+func CreateTemplateNames(d TemplateLookupDescriptor) (TemplateNames, error) {
+
+ name := filepath.ToSlash(d.RelPath)
+ name = strings.TrimPrefix(name, "/")
+
+ if d.Prefix != "" {
+ name = strings.Trim(d.Prefix, "/") + "/" + name
+ }
+
+ var (
+ id TemplateNames
+ )
+
+ // 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 = d.RelPath
+ id.Name = name
+
+ if isPlainText {
+ id.Name = "_text/" + id.Name
+ }
+
+ // Ace and Go templates may have both a base and inner template.
+ if ext == "amber" || isShorthCodeOrPartial(name) {
+ // No base template support
+ return id, nil
+ }
+
+ pathDir := filepath.Dir(d.RelPath)
+
+ 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(d.RelPath, innerMarkers)
+ if err != nil {
+ return id, err
+ }
+
+ if needsBase {
+ currBaseFilename := fmt.Sprintf("%s-%s", filenameNoSuffix, baseFilename)
+
+ // 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>
+ //
+ // The filesystem it looks in a a composite of the project and potential theme(s).
+ pathsToCheck := createPathsToCheck(pathDir, baseFilename, currBaseFilename)
+
+ // We may have language code and/or "terms" in the template name. We want the most specific,
+ // but need to fall back to the baseof.html or baseof.ace if needed.
+ // E.g. list-baseof.en.html and list-baseof.terms.en.html
+ // See #3893, #3856.
+ baseBaseFilename, currBaseBaseFilename := helpers.Filename(baseFilename), helpers.Filename(currBaseFilename)
+ p1, p2 := strings.Split(baseBaseFilename, "."), strings.Split(currBaseBaseFilename, ".")
+ if len(p1) > 0 && len(p1) == len(p2) {
+ for i := len(p1); i > 0; i-- {
+ v1, v2 := strings.Join(p1[:i], ".")+"."+ext, strings.Join(p2[:i], ".")+"."+ext
+ pathsToCheck = append(pathsToCheck, createPathsToCheck(pathDir, v1, v2)...)
+
+ }
+ }
+
+ for _, p := range pathsToCheck {
+ if ok, err := d.FileExists(p); err == nil && ok {
+ id.MasterFilename = p
+ break
+ }
+ }
+ }
+
+ return id, nil
+
+}
+
+func createPathsToCheck(baseTemplatedDir, baseFilename, currBaseFilename string) []string {
+ return []string{
+ filepath.Join(baseTemplatedDir, currBaseFilename),
+ filepath.Join(baseTemplatedDir, baseFilename),
+ filepath.Join("_default", currBaseFilename),
+ filepath.Join("_default", baseFilename),
+ }
+}
diff --git a/output/layout_base_test.go b/output/layout_base_test.go
new file mode 100644
index 000000000..25294c918
--- /dev/null
+++ b/output/layout_base_test.go
@@ -0,0 +1,161 @@
+// 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/"
+ 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{WorkingDir: workingDir, RelPath: layoutPath1}, false, "",
+ TemplateNames{
+ Name: "_default/single.html",
+ OverlayFilename: "_default/single.html",
+ }},
+ {"Base", TemplateLookupDescriptor{WorkingDir: workingDir, RelPath: layoutPath1}, true, "",
+ TemplateNames{
+ Name: "_default/single.html",
+ OverlayFilename: "_default/single.html",
+ MasterFilename: "_default/single-baseof.html",
+ }},
+ // Issue #3893
+ {"Base Lang, Default Base", TemplateLookupDescriptor{WorkingDir: workingDir, RelPath: "_default/list.en.html"}, true, "_default/baseof.html",
+ TemplateNames{
+ Name: "_default/list.en.html",
+ OverlayFilename: "_default/list.en.html",
+ MasterFilename: "_default/baseof.html",
+ }},
+ {"Base Lang, Lang Base", TemplateLookupDescriptor{WorkingDir: workingDir, RelPath: "_default/list.en.html"}, true, "_default/baseof.html|_default/baseof.en.html",
+ TemplateNames{
+ Name: "_default/list.en.html",
+ OverlayFilename: "_default/list.en.html",
+ MasterFilename: "_default/baseof.en.html",
+ }},
+ // Issue #3856
+ {"Base Taxonomy Term", TemplateLookupDescriptor{WorkingDir: workingDir, RelPath: "taxonomy/tag.terms.html"}, true, "_default/baseof.html",
+ TemplateNames{
+ Name: "taxonomy/tag.terms.html",
+ OverlayFilename: "taxonomy/tag.terms.html",
+ MasterFilename: "_default/baseof.html",
+ }},
+
+ {"Partial", TemplateLookupDescriptor{WorkingDir: workingDir, RelPath: "partials/menu.html"}, true,
+ "mytheme/layouts/_default/baseof.html",
+ TemplateNames{
+ Name: "partials/menu.html",
+ OverlayFilename: "partials/menu.html",
+ }},
+ {"Partial in subfolder", TemplateLookupDescriptor{WorkingDir: workingDir, RelPath: "/partials/sub/menu.html"}, true,
+ "_default/baseof.html",
+ TemplateNames{
+ Name: "partials/sub/menu.html",
+ OverlayFilename: "/partials/sub/menu.html",
+ }},
+ {"Shortcode in subfolder", TemplateLookupDescriptor{WorkingDir: workingDir, RelPath: "shortcodes/sub/menu.html"}, true,
+ "_default/baseof.html",
+ TemplateNames{
+ Name: "shortcodes/sub/menu.html",
+ OverlayFilename: "shortcodes/sub/menu.html",
+ }},
+ {"AMP, no base", TemplateLookupDescriptor{WorkingDir: workingDir, RelPath: layoutPathAmp}, false, "",
+ TemplateNames{
+ Name: "_default/single.amp.html",
+ OverlayFilename: "_default/single.amp.html",
+ }},
+ {"JSON, no base", TemplateLookupDescriptor{WorkingDir: workingDir, RelPath: layoutPathJSON}, false, "",
+ TemplateNames{
+ Name: "_default/single.json",
+ OverlayFilename: "_default/single.json",
+ }},
+ {"AMP with base", TemplateLookupDescriptor{WorkingDir: workingDir, RelPath: layoutPathAmp}, true, "single-baseof.html|single-baseof.amp.html",
+ TemplateNames{
+ Name: "_default/single.amp.html",
+ OverlayFilename: "_default/single.amp.html",
+ MasterFilename: "_default/single-baseof.amp.html",
+ }},
+ {"AMP with no AMP base", TemplateLookupDescriptor{WorkingDir: workingDir, RelPath: layoutPathAmp}, true, "single-baseof.html",
+ TemplateNames{
+ Name: "_default/single.amp.html",
+ OverlayFilename: "_default/single.amp.html",
+ MasterFilename: "_default/single-baseof.html",
+ }},
+
+ {"JSON with base", TemplateLookupDescriptor{WorkingDir: workingDir, RelPath: layoutPathJSON}, true, "single-baseof.json",
+ TemplateNames{
+ Name: "_default/single.json",
+ OverlayFilename: "_default/single.json",
+ MasterFilename: "_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.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..e5f2b5b6f
--- /dev/null
+++ b/output/layout_test.go
@@ -0,0 +1,147 @@
+// 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"
+ "reflect"
+ "strings"
+ "testing"
+
+ "github.com/gohugoio/hugo/media"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestLayout(t *testing.T) {
+
+ noExtNoDelimMediaType := media.TextType
+ noExtNoDelimMediaType.Suffixes = nil
+ noExtNoDelimMediaType.Delimiter = ""
+
+ noExtMediaType := media.TextType
+ noExtMediaType.Suffixes = nil
+
+ var (
+ ampType = Format{
+ Name: "AMP",
+ MediaType: media.HTMLType,
+ BaseName: "index",
+ }
+
+ htmlFormat = HTMLFormat
+
+ noExtDelimFormat = Format{
+ Name: "NEM",
+ MediaType: noExtNoDelimMediaType,
+ BaseName: "_redirects",
+ }
+
+ noExt = Format{
+ Name: "NEX",
+ MediaType: noExtMediaType,
+ BaseName: "next",
+ }
+ )
+
+ for _, this := range []struct {
+ name string
+ d LayoutDescriptor
+ layoutOverride string
+ tp Format
+ expect []string
+ expectCount int
+ }{
+ {"Home", LayoutDescriptor{Kind: "home"}, "", ampType,
+ []string{"index.amp.html", "home.amp.html", "list.amp.html", "index.html", "home.html", "list.html", "_default/index.amp.html"}, 12},
+ {"Home, HTML", LayoutDescriptor{Kind: "home"}, "", htmlFormat,
+ // We will eventually get to index.html. This looks stuttery, but makes the lookup logic easy to understand.
+ []string{"index.html.html", "home.html.html"}, 12},
+ {"Home, french language", LayoutDescriptor{Kind: "home", Lang: "fr"}, "", ampType,
+ []string{"index.fr.amp.html"},
+ 24},
+ {"Home, no ext or delim", LayoutDescriptor{Kind: "home"}, "", noExtDelimFormat,
+ []string{"index.nem", "home.nem", "list.nem"}, 6},
+ {"Home, no ext", LayoutDescriptor{Kind: "home"}, "", noExt,
+ []string{"index.nex", "home.nex", "list.nex"}, 6},
+ {"Page, no ext or delim", LayoutDescriptor{Kind: "page"}, "", noExtDelimFormat,
+ []string{"_default/single.nem"}, 1},
+ {"Section", LayoutDescriptor{Kind: "section", Section: "sect1"}, "", ampType,
+ []string{"sect1/sect1.amp.html", "sect1/section.amp.html", "sect1/list.amp.html", "sect1/sect1.html", "sect1/section.html", "sect1/list.html", "section/sect1.amp.html", "section/section.amp.html"}, 18},
+ {"Section with layout", LayoutDescriptor{Kind: "section", Section: "sect1", Layout: "mylayout"}, "", ampType,
+ []string{"sect1/mylayout.amp.html", "sect1/sect1.amp.html", "sect1/section.amp.html", "sect1/list.amp.html", "sect1/mylayout.html", "sect1/sect1.html"}, 24},
+ {"Taxonomy", LayoutDescriptor{Kind: "taxonomy", Section: "tag"}, "", ampType,
+ []string{"taxonomy/tag.amp.html", "taxonomy/taxonomy.amp.html", "taxonomy/list.amp.html", "taxonomy/tag.html", "taxonomy/taxonomy.html"}, 18},
+ {"Taxonomy term", LayoutDescriptor{Kind: "taxonomyTerm", Section: "categories"}, "", ampType,
+ []string{"taxonomy/categories.terms.amp.html", "taxonomy/terms.amp.html", "taxonomy/list.amp.html", "taxonomy/categories.terms.html", "taxonomy/terms.html"}, 18},
+ {"Page", LayoutDescriptor{Kind: "page"}, "", ampType,
+ []string{"_default/single.amp.html", "_default/single.html"}, 2},
+ {"Page with layout", LayoutDescriptor{Kind: "page", Layout: "mylayout"}, "", ampType,
+ []string{"_default/mylayout.amp.html", "_default/single.amp.html", "_default/mylayout.html", "_default/single.html"}, 4},
+ {"Page with layout and type", LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype"}, "", ampType,
+ []string{"myttype/mylayout.amp.html", "myttype/single.amp.html", "myttype/mylayout.html"}, 8},
+ {"Page with layout and type with subtype", LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype/mysubtype"}, "", ampType,
+ []string{"myttype/mysubtype/mylayout.amp.html", "myttype/mysubtype/single.amp.html", "myttype/mysubtype/mylayout.html"}, 8},
+ // RSS
+ {"RSS Home", LayoutDescriptor{Kind: "home"}, "", RSSFormat,
+ []string{"index.rss.xml", "home.rss.xml", "rss.xml"}, 15},
+ {"RSS Section", LayoutDescriptor{Kind: "section", Section: "sect1"}, "", RSSFormat,
+ []string{"sect1/sect1.rss.xml", "sect1/section.rss.xml", "sect1/rss.xml", "sect1/list.rss.xml", "sect1/sect1.xml", "sect1/section.xml"}, 22},
+ {"RSS Taxonomy", LayoutDescriptor{Kind: "taxonomy", Section: "tag"}, "", RSSFormat,
+ []string{"taxonomy/tag.rss.xml", "taxonomy/taxonomy.rss.xml", "taxonomy/rss.xml", "taxonomy/list.rss.xml", "taxonomy/tag.xml", "taxonomy/taxonomy.xml"}, 22},
+ {"RSS Taxonomy term", LayoutDescriptor{Kind: "taxonomyTerm", Section: "tag"}, "", RSSFormat,
+ []string{"taxonomy/tag.terms.rss.xml", "taxonomy/terms.rss.xml", "taxonomy/rss.xml", "taxonomy/list.rss.xml", "taxonomy/tag.terms.xml"}, 22},
+ {"Home plain text", LayoutDescriptor{Kind: "home"}, "", JSONFormat,
+ []string{"_text/index.json.json", "_text/home.json.json"}, 12},
+ {"Page plain text", LayoutDescriptor{Kind: "page"}, "", JSONFormat,
+ []string{"_text/_default/single.json.json", "_text/_default/single.json"}, 2},
+ {"Reserved section, shortcodes", LayoutDescriptor{Kind: "section", Section: "shortcodes", Type: "shortcodes"}, "", ampType,
+ []string{"section/shortcodes.amp.html"}, 12},
+ {"Reserved section, partials", LayoutDescriptor{Kind: "section", Section: "partials", Type: "partials"}, "", ampType,
+ []string{"section/partials.amp.html"}, 12},
+ } {
+ t.Run(this.name, func(t *testing.T) {
+ l := NewLayoutHandler()
+
+ layouts, err := l.For(this.d, this.tp)
+
+ require.NoError(t, err)
+ require.NotNil(t, layouts)
+ require.True(t, len(layouts) >= len(this.expect), fmt.Sprint(layouts))
+ // Not checking the complete list for now ...
+ got := layouts[:len(this.expect)]
+ if len(layouts) != this.expectCount || !reflect.DeepEqual(got, this.expect) {
+ formatted := strings.Replace(fmt.Sprintf("%v", layouts), "[", "\"", 1)
+ formatted = strings.Replace(formatted, "]", "\"", 1)
+ formatted = strings.Replace(formatted, " ", "\", \"", -1)
+
+ t.Fatalf("Got %d/%d:\n%v\nExpected:\n%v\nAll:\n%v\nFormatted:\n%s", len(layouts), this.expectCount, got, this.expect, layouts, formatted)
+
+ }
+
+ })
+ }
+
+}
+
+func BenchmarkLayout(b *testing.B) {
+ descriptor := LayoutDescriptor{Kind: "taxonomyTerm", Section: "categories"}
+ l := NewLayoutHandler()
+
+ 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..c9c108ac5
--- /dev/null
+++ b/output/outputFormat.go
@@ -0,0 +1,380 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 `json:"name"`
+
+ MediaType media.Type `json:"mediaType"`
+
+ // Must be set to a value when there are two or more conflicting mediatype for the same resource.
+ Path string `json:"path"`
+
+ // The base output file name used when not using "ugly URLs", defaults to "index".
+ BaseName string `json:"baseName"`
+
+ // 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 `json:"rel"`
+
+ // The protocol to use, i.e. "webcal://". Defaults to the protocol of the baseURL.
+ Protocol string `json:"protocol"`
+
+ // IsPlainText decides whether to use text/template or html/template
+ // as template parser.
+ IsPlainText bool `json:"isPlainText"`
+
+ // 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 `json:"isHTML"`
+
+ // Enable to ignore the global uglyURLs setting.
+ NoUgly bool `json:"noUgly"`
+
+ // 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 `json:"notAlternative"`
+
+ // Setting this will make this output format control the value of
+ // .Permalink and .RelPermalink for a rendered Page.
+ // If not set, these values will point to the main (first) output format
+ // configured. That is probably the behaviour you want in most situations,
+ // as you probably don't want to link back to the RSS version of a page, as an
+ // example. AMP would, however, be a good example of an output format where this
+ // behaviour is wanted.
+ Permalinkable bool `json:"permalinkable"`
+
+ // Setting this to a non-zero value will be used as the first sort criteria.
+ Weight int `json:"weight"`
+}
+
+// An ordered list of built-in output formats.
+var (
+ AMPFormat = Format{
+ Name: "AMP",
+ MediaType: media.HTMLType,
+ BaseName: "index",
+ Path: "amp",
+ Rel: "amphtml",
+ IsHTML: true,
+ Permalinkable: true,
+ // See https://www.ampproject.org/learn/overview/
+ }
+
+ 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,
+ Permalinkable: true,
+
+ // Weight will be used as first sort criteria. HTML will, by default,
+ // be rendered first, but set it to 10 so it's easy to put one above it.
+ Weight: 10,
+ }
+
+ JSONFormat = Format{
+ Name: "JSON",
+ MediaType: media.JSONType,
+ BaseName: "index",
+ IsPlainText: true,
+ Rel: "alternate",
+ }
+
+ RobotsTxtFormat = Format{
+ Name: "ROBOTS",
+ MediaType: media.TextType,
+ BaseName: "robots",
+ IsPlainText: true,
+ Rel: "alternate",
+ }
+
+ RSSFormat = Format{
+ Name: "RSS",
+ MediaType: media.RSSType,
+ BaseName: "index",
+ NoUgly: true,
+ Rel: "alternate",
+ }
+
+ SitemapFormat = Format{
+ Name: "Sitemap",
+ MediaType: media.XMLType,
+ BaseName: "sitemap",
+ NoUgly: true,
+ Rel: "sitemap",
+ }
+)
+
+// DefaultFormats contains the default output formats supported by Hugo.
+var DefaultFormats = Formats{
+ AMPFormat,
+ CalendarFormat,
+ CSSFormat,
+ CSVFormat,
+ HTMLFormat,
+ JSONFormat,
+ RobotsTxtFormat,
+ RSSFormat,
+ SitemapFormat,
+}
+
+func init() {
+ sort.Sort(DefaultFormats)
+}
+
+// Formats is a slice of Format.
+type Formats []Format
+
+func (formats Formats) Len() int { return len(formats) }
+func (formats Formats) Swap(i, j int) { formats[i], formats[j] = formats[j], formats[i] }
+func (formats Formats) Less(i, j int) bool {
+ fi, fj := formats[i], formats[j]
+ if fi.Weight == fj.Weight {
+ return fi.Name < fj.Name
+ }
+
+ if fj.Weight == 0 {
+ return true
+ }
+
+ return fi.Weight > 0 && fi.Weight < fj.Weight
+
+}
+
+// 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)
+}
+
+// BaseFilename returns the base filename of f including an extension (ie.
+// "index.xml").
+func (f Format) BaseFilename() string {
+ return f.BaseName + f.MediaType.FullSuffix()
+}
+
+// MarshalJSON returns the JSON encoding of f.
+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..3d2fa5d17
--- /dev/null
+++ b/output/outputFormat_test.go
@@ -0,0 +1,250 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "sort"
+ "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.False(t, CSVFormat.Permalinkable)
+
+ 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.True(t, AMPFormat.Permalinkable)
+
+ 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.True(t, AMPFormat.Permalinkable)
+
+ 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.Delimiter = ""
+
+ noExtMediaType := media.TextType
+
+ 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)
+ _, 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{}{
+ {
+ "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{}{
+ {
+ "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{}{
+ {
+ "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{}{
+ {
+ "MYOTHERXMLFORMAT": map[string]interface{}{
+ "baseName": "myotherxml",
+ "mediaType": media.XMLType,
+ }},
+ {
+ "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)
+ }
+ }
+}
+
+func TestSort(t *testing.T) {
+ assert := require.New(t)
+ assert.Equal("HTML", DefaultFormats[0].Name)
+ assert.Equal("AMP", DefaultFormats[1].Name)
+
+ json := JSONFormat
+ json.Weight = 1
+
+ formats := Formats{
+ AMPFormat,
+ HTMLFormat,
+ json,
+ }
+
+ sort.Sort(formats)
+
+ assert.Equal("JSON", formats[0].Name)
+ assert.Equal("HTML", formats[1].Name)
+ assert.Equal("AMP", formats[2].Name)
+
+}
diff --git a/parser/frontmatter.go b/parser/frontmatter.go
new file mode 100644
index 000000000..4965d3fe8
--- /dev/null
+++ b/parser/frontmatter.go
@@ -0,0 +1,107 @@
+// 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 (
+ "encoding/json"
+ "errors"
+ "io"
+
+ "github.com/gohugoio/hugo/parser/metadecoders"
+
+ "github.com/BurntSushi/toml"
+
+ yaml "gopkg.in/yaml.v2"
+)
+
+const (
+ yamlDelimLf = "---\n"
+ tomlDelimLf = "+++\n"
+)
+
+func InterfaceToConfig(in interface{}, format metadecoders.Format, w io.Writer) error {
+ if in == nil {
+ return errors.New("input was nil")
+ }
+
+ switch format {
+ case metadecoders.YAML:
+ b, err := yaml.Marshal(in)
+ if err != nil {
+ return err
+ }
+
+ _, err = w.Write(b)
+ return err
+
+ case metadecoders.TOML:
+ return toml.NewEncoder(w).Encode(in)
+ case metadecoders.JSON:
+ 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")
+ }
+}
+
+func InterfaceToFrontMatter(in interface{}, format metadecoders.Format, w io.Writer) error {
+ if in == nil {
+ return errors.New("input was nil")
+ }
+
+ switch format {
+ case metadecoders.YAML:
+ _, err := w.Write([]byte(yamlDelimLf))
+ if err != nil {
+ return err
+ }
+
+ err = InterfaceToConfig(in, format, w)
+ if err != nil {
+ return err
+ }
+
+ _, err = w.Write([]byte(yamlDelimLf))
+ return err
+
+ case metadecoders.TOML:
+ _, err := w.Write([]byte(tomlDelimLf))
+ if err != nil {
+ return err
+ }
+
+ err = InterfaceToConfig(in, format, w)
+
+ if err != nil {
+ return err
+ }
+
+ _, err = w.Write([]byte("\n" + tomlDelimLf))
+ return err
+
+ default:
+ return InterfaceToConfig(in, format, w)
+ }
+}
diff --git a/parser/frontmatter_test.go b/parser/frontmatter_test.go
new file mode 100644
index 000000000..9d9b7c3b8
--- /dev/null
+++ b/parser/frontmatter_test.go
@@ -0,0 +1,78 @@
+// 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"
+ "reflect"
+ "testing"
+
+ "github.com/gohugoio/hugo/parser/metadecoders"
+)
+
+func TestInterfaceToConfig(t *testing.T) {
+ cases := []struct {
+ input interface{}
+ format metadecoders.Format
+ want []byte
+ isErr bool
+ }{
+ // TOML
+ {map[string]interface{}{}, metadecoders.TOML, nil, false},
+ {
+ map[string]interface{}{"title": "test 1"},
+ metadecoders.TOML,
+ []byte("title = \"test 1\"\n"),
+ false,
+ },
+
+ // YAML
+ {map[string]interface{}{}, metadecoders.YAML, []byte("{}\n"), false},
+ {
+ map[string]interface{}{"title": "test 1"},
+ metadecoders.YAML,
+ []byte("title: test 1\n"),
+ false,
+ },
+
+ // JSON
+ {map[string]interface{}{}, metadecoders.JSON, []byte("{}\n"), false},
+ {
+ map[string]interface{}{"title": "test 1"},
+ metadecoders.JSON,
+ []byte("{\n \"title\": \"test 1\"\n}\n"),
+ false,
+ },
+
+ // Errors
+ {nil, metadecoders.TOML, nil, true},
+ {map[string]interface{}{}, "foo", nil, true},
+ }
+
+ for i, c := range cases {
+ var buf bytes.Buffer
+
+ err := InterfaceToConfig(c.input, c.format, &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())
+ }
+ }
+}
diff --git a/parser/metadecoders/decoder.go b/parser/metadecoders/decoder.go
new file mode 100644
index 000000000..b2d8307b6
--- /dev/null
+++ b/parser/metadecoders/decoder.go
@@ -0,0 +1,238 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metadecoders
+
+import (
+ "bytes"
+ "encoding/csv"
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ "github.com/gohugoio/hugo/common/herrors"
+
+ "github.com/BurntSushi/toml"
+ "github.com/chaseadamsio/goorgeous"
+ "github.com/pkg/errors"
+ "github.com/spf13/afero"
+ "github.com/spf13/cast"
+ yaml "gopkg.in/yaml.v2"
+)
+
+// Decoder provides some configuration options for the decoders.
+type Decoder struct {
+ // Delimiter is the field delimiter used in the CSV decoder. It defaults to ','.
+ Delimiter rune
+
+ // Comment, if not 0, is the comment character ued in the CSV decoder. Lines beginning with the
+ // Comment character without preceding whitespace are ignored.
+ Comment rune
+}
+
+// OptionsKey is used in cache keys.
+func (d Decoder) OptionsKey() string {
+ var sb strings.Builder
+ sb.WriteRune(d.Delimiter)
+ sb.WriteRune(d.Comment)
+ return sb.String()
+}
+
+// Default is a Decoder in its default configuration.
+var Default = Decoder{
+ Delimiter: ',',
+}
+
+// UnmarshalToMap will unmarshall data in format f into a new map. This is
+// what's needed for Hugo's front matter decoding.
+func (d Decoder) UnmarshalToMap(data []byte, f Format) (map[string]interface{}, error) {
+ m := make(map[string]interface{})
+ if data == nil {
+ return m, nil
+ }
+
+ err := d.unmarshal(data, f, &m)
+
+ return m, err
+}
+
+// UnmarshalFileToMap is the same as UnmarshalToMap, but reads the data from
+// the given filename.
+func (d Decoder) UnmarshalFileToMap(fs afero.Fs, filename string) (map[string]interface{}, error) {
+ format := FormatFromString(filename)
+ if format == "" {
+ return nil, errors.Errorf("%q is not a valid configuration format", filename)
+ }
+
+ data, err := afero.ReadFile(fs, filename)
+ if err != nil {
+ return nil, err
+ }
+ return d.UnmarshalToMap(data, format)
+}
+
+// Unmarshal will unmarshall data in format f into an interface{}.
+// This is what's needed for Hugo's /data handling.
+func (d Decoder) Unmarshal(data []byte, f Format) (interface{}, error) {
+ if data == nil {
+ switch f {
+ case CSV:
+ return make([][]string, 0), nil
+ default:
+ return make(map[string]interface{}), nil
+ }
+
+ }
+ var v interface{}
+ err := d.unmarshal(data, f, &v)
+
+ return v, err
+}
+
+// unmarshal unmarshals data in format f into v.
+func (d Decoder) unmarshal(data []byte, f Format, v interface{}) error {
+
+ var err error
+
+ switch f {
+ case ORG:
+ vv, err := goorgeous.OrgHeaders(data)
+ if err != nil {
+ return toFileError(f, errors.Wrap(err, "failed to unmarshal ORG headers"))
+ }
+ switch v.(type) {
+ case *map[string]interface{}:
+ *v.(*map[string]interface{}) = vv
+ default:
+ *v.(*interface{}) = vv
+ }
+ case JSON:
+ err = json.Unmarshal(data, v)
+ case TOML:
+ err = toml.Unmarshal(data, v)
+ case YAML:
+ err = yaml.Unmarshal(data, v)
+ if err != nil {
+ return toFileError(f, errors.Wrap(err, "failed to unmarshal YAML"))
+ }
+
+ // To support boolean keys, the YAML package unmarshals maps to
+ // map[interface{}]interface{}. Here we recurse through the result
+ // and change all maps to map[string]interface{} like we would've
+ // gotten from `json`.
+ var ptr interface{}
+ switch v.(type) {
+ case *map[string]interface{}:
+ ptr = *v.(*map[string]interface{})
+ case *interface{}:
+ ptr = *v.(*interface{})
+ default:
+ return errors.Errorf("unknown type %T in YAML unmarshal", v)
+ }
+
+ if mm, changed := stringifyMapKeys(ptr); changed {
+ switch v.(type) {
+ case *map[string]interface{}:
+ *v.(*map[string]interface{}) = mm.(map[string]interface{})
+ case *interface{}:
+ *v.(*interface{}) = mm
+ }
+ }
+ case CSV:
+ return d.unmarshalCSV(data, v)
+
+ default:
+ return errors.Errorf("unmarshal of format %q is not supported", f)
+ }
+
+ if err == nil {
+ return nil
+ }
+
+ return toFileError(f, errors.Wrap(err, "unmarshal failed"))
+
+}
+
+func (d Decoder) unmarshalCSV(data []byte, v interface{}) error {
+ r := csv.NewReader(bytes.NewReader(data))
+ r.Comma = d.Delimiter
+ r.Comment = d.Comment
+
+ records, err := r.ReadAll()
+ if err != nil {
+ return err
+ }
+
+ switch v.(type) {
+ case *interface{}:
+ *v.(*interface{}) = records
+ default:
+ return errors.Errorf("CSV cannot be unmarshaled into %T", v)
+
+ }
+
+ return nil
+
+}
+
+func toFileError(f Format, err error) error {
+ return herrors.ToFileError(string(f), err)
+}
+
+// stringifyMapKeys recurses into in and changes all instances of
+// map[interface{}]interface{} to map[string]interface{}. This is useful to
+// work around the impedance mismatch between JSON and YAML unmarshaling that's
+// described here: https://github.com/go-yaml/yaml/issues/139
+//
+// Inspired by https://github.com/stripe/stripe-mock, MIT licensed
+func stringifyMapKeys(in interface{}) (interface{}, bool) {
+
+ switch in := in.(type) {
+ case []interface{}:
+ for i, v := range in {
+ if vv, replaced := stringifyMapKeys(v); replaced {
+ in[i] = vv
+ }
+ }
+ case map[string]interface{}:
+ for k, v := range in {
+ if vv, changed := stringifyMapKeys(v); changed {
+ in[k] = vv
+ }
+ }
+ case map[interface{}]interface{}:
+ res := make(map[string]interface{})
+ var (
+ ok bool
+ err error
+ )
+ for k, v := range in {
+ var ks string
+
+ if ks, ok = k.(string); !ok {
+ ks, err = cast.ToStringE(k)
+ if err != nil {
+ ks = fmt.Sprintf("%v", k)
+ }
+ }
+ if vv, replaced := stringifyMapKeys(v); replaced {
+ res[ks] = vv
+ } else {
+ res[ks] = v
+ }
+ }
+ return res, true
+ }
+
+ return nil, false
+}
diff --git a/parser/metadecoders/decoder_test.go b/parser/metadecoders/decoder_test.go
new file mode 100644
index 000000000..146df5069
--- /dev/null
+++ b/parser/metadecoders/decoder_test.go
@@ -0,0 +1,213 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metadecoders
+
+import (
+ "fmt"
+ "reflect"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestUnmarshalToMap(t *testing.T) {
+ assert := require.New(t)
+
+ expect := map[string]interface{}{"a": "b"}
+
+ d := Default
+
+ for i, test := range []struct {
+ data string
+ format Format
+ expect interface{}
+ }{
+ {`a = "b"`, TOML, expect},
+ {`a: "b"`, YAML, expect},
+ // Make sure we get all string keys, even for YAML
+ {"a: Easy!\nb:\n c: 2\n d: [3, 4]", YAML, map[string]interface{}{"a": "Easy!", "b": map[string]interface{}{"c": 2, "d": []interface{}{3, 4}}}},
+ {"a:\n true: 1\n false: 2", YAML, map[string]interface{}{"a": map[string]interface{}{"true": 1, "false": 2}}},
+ {`{ "a": "b" }`, JSON, expect},
+ {`#+a: b`, ORG, expect},
+ // errors
+ {`a = b`, TOML, false},
+ {`a,b,c`, CSV, false}, // Use Unmarshal for CSV
+ } {
+ msg := fmt.Sprintf("%d: %s", i, test.format)
+ m, err := d.UnmarshalToMap([]byte(test.data), test.format)
+ if b, ok := test.expect.(bool); ok && !b {
+ assert.Error(err, msg)
+ } else {
+ assert.NoError(err, msg)
+ assert.Equal(test.expect, m, msg)
+ }
+ }
+}
+
+func TestUnmarshalToInterface(t *testing.T) {
+ assert := require.New(t)
+
+ expect := map[string]interface{}{"a": "b"}
+
+ d := Default
+
+ for i, test := range []struct {
+ data string
+ format Format
+ expect interface{}
+ }{
+ {`[ "Brecker", "Blake", "Redman" ]`, JSON, []interface{}{"Brecker", "Blake", "Redman"}},
+ {`{ "a": "b" }`, JSON, expect},
+ {`#+a: b`, ORG, expect},
+ {`a = "b"`, TOML, expect},
+ {`a: "b"`, YAML, expect},
+ {`a,b,c`, CSV, [][]string{{"a", "b", "c"}}},
+ {"a: Easy!\nb:\n c: 2\n d: [3, 4]", YAML, map[string]interface{}{"a": "Easy!", "b": map[string]interface{}{"c": 2, "d": []interface{}{3, 4}}}},
+ // errors
+ {`a = "`, TOML, false},
+ } {
+ msg := fmt.Sprintf("%d: %s", i, test.format)
+ m, err := d.Unmarshal([]byte(test.data), test.format)
+ if b, ok := test.expect.(bool); ok && !b {
+ assert.Error(err, msg)
+ } else {
+ assert.NoError(err, msg)
+ assert.Equal(test.expect, m, msg)
+ }
+
+ }
+
+}
+
+func TestStringifyYAMLMapKeys(t *testing.T) {
+ cases := []struct {
+ input interface{}
+ want interface{}
+ replaced bool
+ }{
+ {
+ map[interface{}]interface{}{"a": 1, "b": 2},
+ map[string]interface{}{"a": 1, "b": 2},
+ true,
+ },
+ {
+ map[interface{}]interface{}{"a": []interface{}{1, map[interface{}]interface{}{"b": 2}}},
+ map[string]interface{}{"a": []interface{}{1, map[string]interface{}{"b": 2}}},
+ true,
+ },
+ {
+ map[interface{}]interface{}{true: 1, "b": false},
+ map[string]interface{}{"true": 1, "b": false},
+ true,
+ },
+ {
+ map[interface{}]interface{}{1: "a", 2: "b"},
+ map[string]interface{}{"1": "a", "2": "b"},
+ true,
+ },
+ {
+ map[interface{}]interface{}{"a": map[interface{}]interface{}{"b": 1}},
+ map[string]interface{}{"a": map[string]interface{}{"b": 1}},
+ true,
+ },
+ {
+ map[string]interface{}{"a": map[string]interface{}{"b": 1}},
+ map[string]interface{}{"a": map[string]interface{}{"b": 1}},
+ false,
+ },
+ {
+ []interface{}{map[interface{}]interface{}{1: "a", 2: "b"}},
+ []interface{}{map[string]interface{}{"1": "a", "2": "b"}},
+ false,
+ },
+ }
+
+ for i, c := range cases {
+ res, replaced := stringifyMapKeys(c.input)
+
+ if c.replaced != replaced {
+ t.Fatalf("[%d] Replaced mismatch: %t", i, replaced)
+ }
+ if !c.replaced {
+ res = c.input
+ }
+ if !reflect.DeepEqual(res, c.want) {
+ t.Errorf("[%d] given %q\nwant: %q\n got: %q", i, c.input, c.want, res)
+ }
+ }
+}
+
+func BenchmarkStringifyMapKeysStringsOnlyInterfaceMaps(b *testing.B) {
+ maps := make([]map[interface{}]interface{}, b.N)
+ for i := 0; i < b.N; i++ {
+ maps[i] = map[interface{}]interface{}{
+ "a": map[interface{}]interface{}{
+ "b": 32,
+ "c": 43,
+ "d": map[interface{}]interface{}{
+ "b": 32,
+ "c": 43,
+ },
+ },
+ "b": []interface{}{"a", "b"},
+ "c": "d",
+ }
+ }
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ stringifyMapKeys(maps[i])
+ }
+}
+
+func BenchmarkStringifyMapKeysStringsOnlyStringMaps(b *testing.B) {
+ m := map[string]interface{}{
+ "a": map[string]interface{}{
+ "b": 32,
+ "c": 43,
+ "d": map[string]interface{}{
+ "b": 32,
+ "c": 43,
+ },
+ },
+ "b": []interface{}{"a", "b"},
+ "c": "d",
+ }
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ stringifyMapKeys(m)
+ }
+}
+
+func BenchmarkStringifyMapKeysIntegers(b *testing.B) {
+ maps := make([]map[interface{}]interface{}, b.N)
+ for i := 0; i < b.N; i++ {
+ maps[i] = map[interface{}]interface{}{
+ 1: map[interface{}]interface{}{
+ 4: 32,
+ 5: 43,
+ 6: map[interface{}]interface{}{
+ 7: 32,
+ 8: 43,
+ },
+ },
+ 2: []interface{}{"a", "b"},
+ 3: "d",
+ }
+ }
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ stringifyMapKeys(maps[i])
+ }
+}
diff --git a/parser/metadecoders/format.go b/parser/metadecoders/format.go
new file mode 100644
index 000000000..4f81528c3
--- /dev/null
+++ b/parser/metadecoders/format.go
@@ -0,0 +1,130 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metadecoders
+
+import (
+ "path/filepath"
+ "strings"
+
+ "github.com/gohugoio/hugo/media"
+
+ "github.com/gohugoio/hugo/parser/pageparser"
+)
+
+type Format string
+
+const (
+ // These are the supported metdata formats in Hugo. Most of these are also
+ // supported as /data formats.
+ ORG Format = "org"
+ JSON Format = "json"
+ TOML Format = "toml"
+ YAML Format = "yaml"
+ CSV Format = "csv"
+)
+
+// FormatFromString turns formatStr, typically a file extension without any ".",
+// into a Format. It returns an empty string for unknown formats.
+func FormatFromString(formatStr string) Format {
+ formatStr = strings.ToLower(formatStr)
+ if strings.Contains(formatStr, ".") {
+ // Assume a filename
+ formatStr = strings.TrimPrefix(filepath.Ext(formatStr), ".")
+
+ }
+ switch formatStr {
+ case "yaml", "yml":
+ return YAML
+ case "json":
+ return JSON
+ case "toml":
+ return TOML
+ case "org":
+ return ORG
+ case "csv":
+ return CSV
+ }
+
+ return ""
+
+}
+
+// FormatFromMediaType gets the Format given a MIME type, empty string
+// if unknown.
+func FormatFromMediaType(m media.Type) Format {
+ for _, suffix := range m.Suffixes {
+ if f := FormatFromString(suffix); f != "" {
+ return f
+ }
+ }
+
+ return ""
+}
+
+// FormatFromFrontMatterType will return empty if not supported.
+func FormatFromFrontMatterType(typ pageparser.ItemType) Format {
+ switch typ {
+ case pageparser.TypeFrontMatterJSON:
+ return JSON
+ case pageparser.TypeFrontMatterORG:
+ return ORG
+ case pageparser.TypeFrontMatterTOML:
+ return TOML
+ case pageparser.TypeFrontMatterYAML:
+ return YAML
+ default:
+ return ""
+ }
+}
+
+// FormatFromContentString tries to detect the format (JSON, YAML or TOML)
+// in the given string.
+// It return an empty string if no format could be detected.
+func (d Decoder) FormatFromContentString(data string) Format {
+ csvIdx := strings.IndexRune(data, d.Delimiter)
+ jsonIdx := strings.Index(data, "{")
+ yamlIdx := strings.Index(data, ":")
+ tomlIdx := strings.Index(data, "=")
+
+ if isLowerIndexThan(csvIdx, jsonIdx, yamlIdx, tomlIdx) {
+ return CSV
+ }
+
+ if isLowerIndexThan(jsonIdx, yamlIdx, tomlIdx) {
+ return JSON
+ }
+
+ if isLowerIndexThan(yamlIdx, tomlIdx) {
+ return YAML
+ }
+
+ if tomlIdx != -1 {
+ return TOML
+ }
+
+ return ""
+}
+
+func isLowerIndexThan(first int, others ...int) bool {
+ if first == -1 {
+ return false
+ }
+ for _, other := range others {
+ if other != -1 && other < first {
+ return false
+ }
+ }
+
+ return true
+}
diff --git a/parser/metadecoders/format_test.go b/parser/metadecoders/format_test.go
new file mode 100644
index 000000000..7794843b1
--- /dev/null
+++ b/parser/metadecoders/format_test.go
@@ -0,0 +1,101 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metadecoders
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/gohugoio/hugo/media"
+
+ "github.com/gohugoio/hugo/parser/pageparser"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestFormatFromString(t *testing.T) {
+ assert := require.New(t)
+ for i, test := range []struct {
+ s string
+ expect Format
+ }{
+ {"json", JSON},
+ {"yaml", YAML},
+ {"yml", YAML},
+ {"toml", TOML},
+ {"config.toml", TOML},
+ {"tOMl", TOML},
+ {"org", ORG},
+ {"foo", ""},
+ } {
+ assert.Equal(test.expect, FormatFromString(test.s), fmt.Sprintf("t%d", i))
+ }
+}
+
+func TestFormatFromMediaType(t *testing.T) {
+ assert := require.New(t)
+ for i, test := range []struct {
+ m media.Type
+ expect Format
+ }{
+ {media.JSONType, JSON},
+ {media.YAMLType, YAML},
+ {media.TOMLType, TOML},
+ {media.CalendarType, ""},
+ } {
+ assert.Equal(test.expect, FormatFromMediaType(test.m), fmt.Sprintf("t%d", i))
+ }
+}
+
+func TestFormatFromFrontMatterType(t *testing.T) {
+ assert := require.New(t)
+ for i, test := range []struct {
+ typ pageparser.ItemType
+ expect Format
+ }{
+ {pageparser.TypeFrontMatterJSON, JSON},
+ {pageparser.TypeFrontMatterTOML, TOML},
+ {pageparser.TypeFrontMatterYAML, YAML},
+ {pageparser.TypeFrontMatterORG, ORG},
+ {pageparser.TypeIgnore, ""},
+ } {
+ assert.Equal(test.expect, FormatFromFrontMatterType(test.typ), fmt.Sprintf("t%d", i))
+ }
+}
+
+func TestFormatFromContentString(t *testing.T) {
+ t.Parallel()
+ assert := require.New(t)
+
+ for i, test := range []struct {
+ data string
+ expect interface{}
+ }{
+ {`foo = "bar"`, TOML},
+ {` foo = "bar"`, TOML},
+ {`foo="bar"`, TOML},
+ {`foo: "bar"`, YAML},
+ {`foo:"bar"`, YAML},
+ {`{ "foo": "bar"`, JSON},
+ {`a,b,c"`, CSV},
+ {`asdfasdf`, Format("")},
+ {``, Format("")},
+ } {
+ errMsg := fmt.Sprintf("[%d] %s", i, test.data)
+
+ result := Default.FormatFromContentString(test.data)
+
+ assert.Equal(test.expect, result, errMsg)
+ }
+}
diff --git a/parser/pageparser/item.go b/parser/pageparser/item.go
new file mode 100644
index 000000000..3877ee6d9
--- /dev/null
+++ b/parser/pageparser/item.go
@@ -0,0 +1,134 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pageparser
+
+import (
+ "bytes"
+ "fmt"
+)
+
+type Item struct {
+ Type ItemType
+ Pos int
+ Val []byte
+}
+
+type Items []Item
+
+func (i Item) ValStr() string {
+ return string(i.Val)
+}
+
+func (i Item) IsText() bool {
+ return i.Type == tText
+}
+
+func (i Item) IsNonWhitespace() bool {
+ return len(bytes.TrimSpace(i.Val)) > 0
+}
+
+func (i Item) IsShortcodeName() bool {
+ return i.Type == tScName
+}
+
+func (i Item) IsInlineShortcodeName() bool {
+ return i.Type == tScNameInline
+}
+
+func (i Item) IsLeftShortcodeDelim() bool {
+ return i.Type == tLeftDelimScWithMarkup || i.Type == tLeftDelimScNoMarkup
+}
+
+func (i Item) IsRightShortcodeDelim() bool {
+ return i.Type == tRightDelimScWithMarkup || i.Type == tRightDelimScNoMarkup
+}
+
+func (i Item) IsShortcodeClose() bool {
+ return i.Type == tScClose
+}
+
+func (i Item) IsShortcodeParam() bool {
+ return i.Type == tScParam
+}
+
+func (i Item) IsShortcodeParamVal() bool {
+ return i.Type == tScParamVal
+}
+
+func (i Item) IsShortcodeMarkupDelimiter() bool {
+ return i.Type == tLeftDelimScWithMarkup || i.Type == tRightDelimScWithMarkup
+}
+
+func (i Item) IsFrontMatter() bool {
+ return i.Type >= TypeFrontMatterYAML && i.Type <= TypeFrontMatterORG
+}
+
+func (i Item) IsDone() bool {
+ return i.Type == tError || i.Type == tEOF
+}
+
+func (i Item) IsEOF() bool {
+ return i.Type == tEOF
+}
+
+func (i Item) IsError() bool {
+ return i.Type == tError
+}
+
+func (i Item) String() string {
+ switch {
+ case i.Type == tEOF:
+ return "EOF"
+ case i.Type == tError:
+ return string(i.Val)
+ case i.Type > tKeywordMarker:
+ return fmt.Sprintf("<%s>", i.Val)
+ case len(i.Val) > 50:
+ return fmt.Sprintf("%v:%.20q...", i.Type, i.Val)
+ }
+ return fmt.Sprintf("%v:[%s]", i.Type, i.Val)
+}
+
+type ItemType int
+
+const (
+ tError ItemType = iota
+ tEOF
+
+ // page items
+ TypeHTMLStart // document starting with < as first non-whitespace
+ TypeLeadSummaryDivider // <!--more-->, # more
+ TypeFrontMatterYAML
+ TypeFrontMatterTOML
+ TypeFrontMatterJSON
+ TypeFrontMatterORG
+ TypeEmoji
+ TypeIgnore // // The BOM Unicode byte order marker and possibly others
+
+ // shortcode items
+ tLeftDelimScNoMarkup
+ tRightDelimScNoMarkup
+ tLeftDelimScWithMarkup
+ tRightDelimScWithMarkup
+ tScClose
+ tScName
+ tScNameInline
+ tScParam
+ tScParamVal
+
+ tText // plain text
+
+ // preserved for later - keywords come after this
+ tKeywordMarker
+)
diff --git a/parser/pageparser/itemtype_string.go b/parser/pageparser/itemtype_string.go
new file mode 100644
index 000000000..632afaecc
--- /dev/null
+++ b/parser/pageparser/itemtype_string.go
@@ -0,0 +1,16 @@
+// Code generated by "stringer -type ItemType"; DO NOT EDIT.
+
+package pageparser
+
+import "strconv"
+
+const _ItemType_name = "tErrortEOFTypeHTMLStartTypeLeadSummaryDividerTypeFrontMatterYAMLTypeFrontMatterTOMLTypeFrontMatterJSONTypeFrontMatterORGTypeEmojiTypeIgnoretLeftDelimScNoMarkuptRightDelimScNoMarkuptLeftDelimScWithMarkuptRightDelimScWithMarkuptScClosetScNametScNameInlinetScParamtScParamValtTexttKeywordMarker"
+
+var _ItemType_index = [...]uint16{0, 6, 10, 23, 45, 64, 83, 102, 120, 129, 139, 159, 180, 202, 225, 233, 240, 253, 261, 272, 277, 291}
+
+func (i ItemType) String() string {
+ if i < 0 || i >= ItemType(len(_ItemType_index)-1) {
+ return "ItemType(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _ItemType_name[_ItemType_index[i]:_ItemType_index[i+1]]
+}
diff --git a/parser/pageparser/pagelexer.go b/parser/pageparser/pagelexer.go
new file mode 100644
index 000000000..d010c8152
--- /dev/null
+++ b/parser/pageparser/pagelexer.go
@@ -0,0 +1,525 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package pageparser provides a parser for Hugo content files (Markdown, HTML etc.) in Hugo.
+// This implementation 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
+package pageparser
+
+import (
+ "bytes"
+ "fmt"
+ "unicode"
+ "unicode/utf8"
+)
+
+const eof = -1
+
+// returns the next state in scanner.
+type stateFunc func(*pageLexer) stateFunc
+
+type pageLexer struct {
+ input []byte
+ stateStart stateFunc
+ state stateFunc
+ pos int // input position
+ start int // item start position
+ width int // width of last element
+
+ // Contains lexers for shortcodes and other main section
+ // elements.
+ sectionHandlers *sectionHandlers
+
+ cfg Config
+
+ // The summary divider to look for.
+ summaryDivider []byte
+ // Set when we have parsed any summary divider
+ summaryDividerChecked bool
+ // Whether we're in a HTML comment.
+ isInHTMLComment bool
+
+ lexerShortcodeState
+
+ // items delivered to client
+ items Items
+}
+
+// Implement the Result interface
+func (l *pageLexer) Iterator() *Iterator {
+ return l.newIterator()
+}
+
+func (l *pageLexer) Input() []byte {
+ return l.input
+
+}
+
+type Config struct {
+ EnableEmoji bool
+}
+
+// note: the input position here is normally 0 (start), but
+// can be set if position of first shortcode is known
+func newPageLexer(input []byte, stateStart stateFunc, cfg Config) *pageLexer {
+ lexer := &pageLexer{
+ input: input,
+ stateStart: stateStart,
+ cfg: cfg,
+ lexerShortcodeState: lexerShortcodeState{
+ currLeftDelimItem: tLeftDelimScNoMarkup,
+ currRightDelimItem: tRightDelimScNoMarkup,
+ openShortcodes: make(map[string]bool),
+ },
+ items: make([]Item, 0, 5),
+ }
+
+ lexer.sectionHandlers = createSectionHandlers(lexer)
+
+ return lexer
+}
+
+func (l *pageLexer) newIterator() *Iterator {
+ return &Iterator{l: l, lastPos: -1}
+}
+
+// main loop
+func (l *pageLexer) run() *pageLexer {
+ for l.state = l.stateStart; l.state != nil; {
+ l.state = l.state(l)
+ }
+ return l
+}
+
+// Page syntax
+var (
+ byteOrderMark = '\ufeff'
+ summaryDivider = []byte("<!--more-->")
+ summaryDividerOrg = []byte("# more")
+ delimTOML = []byte("+++")
+ delimYAML = []byte("---")
+ delimOrg = []byte("#+")
+ htmlCommentStart = []byte("<!--")
+ htmlCommentEnd = []byte("-->")
+
+ emojiDelim = byte(':')
+)
+
+func (l *pageLexer) next() rune {
+ if int(l.pos) >= len(l.input) {
+ l.width = 0
+ return eof
+ }
+
+ runeValue, runeWidth := utf8.DecodeRune(l.input[l.pos:])
+ l.width = 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 = append(l.items, Item{t, l.start, l.input[l.start:l.pos]})
+ l.start = l.pos
+}
+
+func (l *pageLexer) isEOF() bool {
+ return l.pos >= len(l.input)
+}
+
+// special case, do not send '\\' back to client
+func (l *pageLexer) ignoreEscapesAndEmit(t ItemType) {
+ val := bytes.Map(func(r rune) rune {
+ if r == '\\' {
+ return -1
+ }
+ return r
+ }, l.input[l.start:l.pos])
+ l.items = append(l.items, Item{t, l.start, val})
+ l.start = l.pos
+}
+
+// gets the current value (for debugging and error handling)
+func (l *pageLexer) current() []byte {
+ return l.input[l.start:l.pos]
+}
+
+// ignore current element
+func (l *pageLexer) ignore() {
+ l.start = l.pos
+}
+
+var lf = []byte("\n")
+
+// nil terminates the parser
+func (l *pageLexer) errorf(format string, args ...interface{}) stateFunc {
+ l.items = append(l.items, Item{tError, l.start, []byte(fmt.Sprintf(format, args...))})
+ return nil
+}
+
+func (l *pageLexer) consumeCRLF() bool {
+ var consumed bool
+ for _, r := range crLf {
+ if l.next() != r {
+ l.backup()
+ } else {
+ consumed = true
+ }
+ }
+ return consumed
+}
+
+func (l *pageLexer) consumeToNextLine() {
+ for {
+ r := l.next()
+ if r == eof || isEndOfLine(r) {
+ return
+ }
+ }
+}
+
+func (l *pageLexer) consumeSpace() {
+ for {
+ r := l.next()
+ if r == eof || !unicode.IsSpace(r) {
+ l.backup()
+ return
+ }
+ }
+}
+
+// lex a string starting at ":"
+func lexEmoji(l *pageLexer) stateFunc {
+ pos := l.pos + 1
+ valid := false
+
+ for i := pos; i < len(l.input); i++ {
+ if i > pos && l.input[i] == emojiDelim {
+ pos = i + 1
+ valid = true
+ break
+ }
+ r, _ := utf8.DecodeRune(l.input[i:])
+ if !(isAlphaNumericOrHyphen(r) || r == '+') {
+ break
+ }
+ }
+
+ if valid {
+ l.pos = pos
+ l.emit(TypeEmoji)
+ } else {
+ l.pos++
+ l.emit(tText)
+ }
+
+ return lexMainSection
+}
+
+type sectionHandlers struct {
+ l *pageLexer
+
+ // Set when none of the sections are found so we
+ // can safely stop looking and skip to the end.
+ skipAll bool
+
+ handlers []*sectionHandler
+ skipIndexes []int
+}
+
+func (s *sectionHandlers) skip() int {
+ if s.skipAll {
+ return -1
+ }
+
+ s.skipIndexes = s.skipIndexes[:0]
+ var shouldSkip bool
+ for _, skipper := range s.handlers {
+ idx := skipper.skip()
+ if idx != -1 {
+ shouldSkip = true
+ s.skipIndexes = append(s.skipIndexes, idx)
+ }
+ }
+
+ if !shouldSkip {
+ s.skipAll = true
+ return -1
+ }
+
+ return minIndex(s.skipIndexes...)
+}
+
+func createSectionHandlers(l *pageLexer) *sectionHandlers {
+
+ shortCodeHandler := &sectionHandler{
+ l: l,
+ skipFunc: func(l *pageLexer) int {
+ return l.index(leftDelimSc)
+ },
+ lexFunc: func(origin stateFunc, l *pageLexer) (stateFunc, bool) {
+ if !l.isShortCodeStart() {
+ return origin, false
+ }
+
+ if l.isInline {
+ // If we're inside an inline shortcode, the only valid shortcode markup is
+ // the markup which closes it.
+ b := l.input[l.pos+3:]
+ end := indexNonWhiteSpace(b, '/')
+ if end != len(l.input)-1 {
+ b = bytes.TrimSpace(b[end+1:])
+ if end == -1 || !bytes.HasPrefix(b, []byte(l.currShortcodeName+" ")) {
+ return l.errorf("inline shortcodes do not support nesting"), true
+ }
+ }
+ }
+
+ if l.hasPrefix(leftDelimScWithMarkup) {
+ l.currLeftDelimItem = tLeftDelimScWithMarkup
+ l.currRightDelimItem = tRightDelimScWithMarkup
+ } else {
+ l.currLeftDelimItem = tLeftDelimScNoMarkup
+ l.currRightDelimItem = tRightDelimScNoMarkup
+ }
+
+ return lexShortcodeLeftDelim, true
+ },
+ }
+
+ summaryDividerHandler := &sectionHandler{
+ l: l,
+ skipFunc: func(l *pageLexer) int {
+ if l.summaryDividerChecked || l.summaryDivider == nil {
+ return -1
+
+ }
+ return l.index(l.summaryDivider)
+ },
+ lexFunc: func(origin stateFunc, l *pageLexer) (stateFunc, bool) {
+ if !l.hasPrefix(l.summaryDivider) {
+ return origin, false
+ }
+
+ l.summaryDividerChecked = true
+ l.pos += len(l.summaryDivider)
+ // This makes it a little easier to reason about later.
+ l.consumeSpace()
+ l.emit(TypeLeadSummaryDivider)
+
+ return origin, true
+
+ },
+ }
+
+ handlers := []*sectionHandler{shortCodeHandler, summaryDividerHandler}
+
+ if l.cfg.EnableEmoji {
+ emojiHandler := &sectionHandler{
+ l: l,
+ skipFunc: func(l *pageLexer) int {
+ return l.indexByte(emojiDelim)
+ },
+ lexFunc: func(origin stateFunc, l *pageLexer) (stateFunc, bool) {
+ return lexEmoji, true
+ },
+ }
+
+ handlers = append(handlers, emojiHandler)
+ }
+
+ return &sectionHandlers{
+ l: l,
+ handlers: handlers,
+ skipIndexes: make([]int, len(handlers)),
+ }
+}
+
+func (s *sectionHandlers) lex(origin stateFunc) stateFunc {
+ if s.skipAll {
+ return nil
+ }
+
+ if s.l.pos > s.l.start {
+ s.l.emit(tText)
+ }
+
+ for _, handler := range s.handlers {
+ if handler.skipAll {
+ continue
+ }
+
+ next, handled := handler.lexFunc(origin, handler.l)
+ if next == nil || handled {
+ return next
+ }
+ }
+
+ // Not handled by the above.
+ s.l.pos++
+
+ return origin
+}
+
+type sectionHandler struct {
+ l *pageLexer
+
+ // No more sections of this type.
+ skipAll bool
+
+ // Returns the index of the next match, -1 if none found.
+ skipFunc func(l *pageLexer) int
+
+ // Lex lexes the current section and returns the next state func and
+ // a bool telling if this section was handled.
+ // Note that returning nil as the next state will terminate the
+ // lexer.
+ lexFunc func(origin stateFunc, l *pageLexer) (stateFunc, bool)
+}
+
+func (s *sectionHandler) skip() int {
+ if s.skipAll {
+ return -1
+ }
+
+ idx := s.skipFunc(s.l)
+ if idx == -1 {
+ s.skipAll = true
+ }
+ return idx
+}
+
+func lexMainSection(l *pageLexer) stateFunc {
+
+ if l.isEOF() {
+ return lexDone
+ }
+
+ if l.isInHTMLComment {
+ return lexEndFromtMatterHTMLComment
+ }
+
+ // Fast forward as far as possible.
+ skip := l.sectionHandlers.skip()
+
+ if skip == -1 {
+ l.pos = len(l.input)
+ return lexDone
+ } else if skip > 0 {
+ l.pos += skip
+ }
+
+ next := l.sectionHandlers.lex(lexMainSection)
+ if next != nil {
+ return next
+ }
+
+ l.pos = len(l.input)
+ return lexDone
+
+}
+
+func lexDone(l *pageLexer) stateFunc {
+
+ // Done!
+ if l.pos > l.start {
+ l.emit(tText)
+ }
+ l.emit(tEOF)
+ return nil
+}
+
+func (l *pageLexer) printCurrentInput() {
+ fmt.Printf("input[%d:]: %q", l.pos, string(l.input[l.pos:]))
+}
+
+// state helpers
+
+func (l *pageLexer) index(sep []byte) int {
+ return bytes.Index(l.input[l.pos:], sep)
+}
+
+func (l *pageLexer) indexByte(sep byte) int {
+ return bytes.IndexByte(l.input[l.pos:], sep)
+}
+
+func (l *pageLexer) hasPrefix(prefix []byte) bool {
+ return bytes.HasPrefix(l.input[l.pos:], prefix)
+}
+
+// helper functions
+
+// returns the min index >= 0
+func minIndex(indices ...int) int {
+ min := -1
+
+ for _, j := range indices {
+ if j < 0 {
+ continue
+ }
+ if min == -1 {
+ min = j
+ } else if j < min {
+ min = j
+ }
+ }
+ return min
+}
+
+func indexNonWhiteSpace(s []byte, in rune) int {
+ idx := bytes.IndexFunc(s, func(r rune) bool {
+ return !unicode.IsSpace(r)
+ })
+
+ if idx == -1 {
+ return -1
+ }
+
+ r, _ := utf8.DecodeRune(s[idx:])
+ if r == in {
+ return idx
+ }
+ return -1
+}
+
+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 == '-'
+}
+
+var crLf = []rune{'\r', '\n'}
+
+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/parser/pageparser/pagelexer_intro.go b/parser/pageparser/pagelexer_intro.go
new file mode 100644
index 000000000..56dd4224d
--- /dev/null
+++ b/parser/pageparser/pagelexer_intro.go
@@ -0,0 +1,202 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package pageparser provides a parser for Hugo content files (Markdown, HTML etc.) in Hugo.
+// This implementation 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
+package pageparser
+
+func lexIntroSection(l *pageLexer) stateFunc {
+ l.summaryDivider = summaryDivider
+
+LOOP:
+ for {
+ r := l.next()
+ if r == eof {
+ break
+ }
+
+ switch {
+ case r == '+':
+ return l.lexFrontMatterSection(TypeFrontMatterTOML, r, "TOML", delimTOML)
+ case r == '-':
+ return l.lexFrontMatterSection(TypeFrontMatterYAML, r, "YAML", delimYAML)
+ case r == '{':
+ return lexFrontMatterJSON
+ case r == '#':
+ return lexFrontMatterOrgMode
+ case r == byteOrderMark:
+ l.emit(TypeIgnore)
+ case !isSpace(r) && !isEndOfLine(r):
+ if r == '<' {
+ l.backup()
+ if l.hasPrefix(htmlCommentStart) {
+ // This may be commented out front mattter, which should
+ // still be read.
+ l.consumeToNextLine()
+ l.isInHTMLComment = true
+ l.emit(TypeIgnore)
+ continue LOOP
+ } else {
+ if l.pos > l.start {
+ l.emit(tText)
+ }
+ l.next()
+ // This is the start of a plain HTML document with no
+ // front matter. I still can contain shortcodes, so we
+ // have to keep looking.
+ l.emit(TypeHTMLStart)
+ }
+ }
+ break LOOP
+ }
+ }
+
+ // Now move on to the shortcodes.
+ return lexMainSection
+}
+
+func lexEndFromtMatterHTMLComment(l *pageLexer) stateFunc {
+ l.isInHTMLComment = false
+ right := l.index(htmlCommentEnd)
+ if right == -1 {
+ return l.errorf("starting HTML comment with no end")
+ }
+ l.pos += right + len(htmlCommentEnd)
+ l.emit(TypeIgnore)
+
+ // Now move on to the shortcodes.
+ return lexMainSection
+}
+
+func lexFrontMatterJSON(l *pageLexer) stateFunc {
+ // Include the left delimiter
+ l.backup()
+
+ var (
+ inQuote bool
+ level int
+ )
+
+ for {
+
+ r := l.next()
+
+ switch {
+ case r == eof:
+ return l.errorf("unexpected EOF parsing JSON front matter")
+ case r == '{':
+ if !inQuote {
+ level++
+ }
+ case r == '}':
+ if !inQuote {
+ level--
+ }
+ case r == '"':
+ inQuote = !inQuote
+ case r == '\\':
+ // This may be an escaped quote. Make sure it's not marked as a
+ // real one.
+ l.next()
+ }
+
+ if level == 0 {
+ break
+ }
+ }
+
+ l.consumeCRLF()
+ l.emit(TypeFrontMatterJSON)
+
+ return lexMainSection
+}
+
+func lexFrontMatterOrgMode(l *pageLexer) stateFunc {
+ /*
+ #+TITLE: Test File For chaseadamsio/goorgeous
+ #+AUTHOR: Chase Adams
+ #+DESCRIPTION: Just another golang parser for org content!
+ */
+
+ l.summaryDivider = summaryDividerOrg
+
+ l.backup()
+
+ if !l.hasPrefix(delimOrg) {
+ return lexMainSection
+ }
+
+ // Read lines until we no longer see a #+ prefix
+LOOP:
+ for {
+
+ r := l.next()
+
+ switch {
+ case r == '\n':
+ if !l.hasPrefix(delimOrg) {
+ break LOOP
+ }
+ case r == eof:
+ break LOOP
+
+ }
+ }
+
+ l.emit(TypeFrontMatterORG)
+
+ return lexMainSection
+
+}
+
+// Handle YAML or TOML front matter.
+func (l *pageLexer) lexFrontMatterSection(tp ItemType, delimr rune, name string, delim []byte) stateFunc {
+
+ for i := 0; i < 2; i++ {
+ if r := l.next(); r != delimr {
+ return l.errorf("invalid %s delimiter", name)
+ }
+ }
+
+ // Let front matter start at line 1
+ wasEndOfLine := l.consumeCRLF()
+ // We don't care about the delimiters.
+ l.ignore()
+
+ var r rune
+
+ for {
+ if !wasEndOfLine {
+ r = l.next()
+ if r == eof {
+ return l.errorf("EOF looking for end %s front matter delimiter", name)
+ }
+ }
+
+ if wasEndOfLine || isEndOfLine(r) {
+ if l.hasPrefix(delim) {
+ l.emit(tp)
+ l.pos += 3
+ l.consumeCRLF()
+ l.ignore()
+ break
+ }
+ }
+
+ wasEndOfLine = false
+ }
+
+ return lexMainSection
+}
diff --git a/parser/pageparser/pagelexer_shortcode.go b/parser/pageparser/pagelexer_shortcode.go
new file mode 100644
index 000000000..d503d1797
--- /dev/null
+++ b/parser/pageparser/pagelexer_shortcode.go
@@ -0,0 +1,323 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package pageparser provides a parser for Hugo content files (Markdown, HTML etc.) in Hugo.
+// This implementation 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
+package pageparser
+
+type lexerShortcodeState struct {
+ currLeftDelimItem ItemType
+ currRightDelimItem ItemType
+ isInline bool
+ 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
+
+}
+
+// Shortcode syntax
+var (
+ leftDelimSc = []byte("{{")
+ leftDelimScNoMarkup = []byte("{{<")
+ rightDelimScNoMarkup = []byte(">}}")
+ leftDelimScWithMarkup = []byte("{{%")
+ rightDelimScWithMarkup = []byte("%}}")
+ leftComment = []byte("/*") // comments in this context us used to to mark shortcodes as "not really a shortcode"
+ rightComment = []byte("*/")
+)
+
+func (l *pageLexer) isShortCodeStart() bool {
+ return l.hasPrefix(leftDelimScWithMarkup) || l.hasPrefix(leftDelimScNoMarkup)
+}
+
+func lexShortcodeLeftDelim(l *pageLexer) stateFunc {
+ l.pos += len(l.currentLeftShortcodeDelim())
+ if l.hasPrefix(leftComment) {
+ return lexShortcodeComment
+ }
+ l.emit(l.currentLeftShortcodeDelimItem())
+ l.elementStepNum = 0
+ l.paramElements = 0
+ return lexInsideShortcode
+}
+
+func lexShortcodeComment(l *pageLexer) stateFunc {
+ posRightComment := l.index(append(rightComment, l.currentRightShortcodeDelim()...))
+ if posRightComment <= 1 {
+ return l.errorf("comment must be closed")
+ }
+ // we emit all as text, except the comment markers
+ l.emit(tText)
+ l.pos += len(leftComment)
+ l.ignore()
+ l.pos += posRightComment - len(leftComment)
+ l.emit(tText)
+ l.pos += len(rightComment)
+ l.ignore()
+ l.pos += len(l.currentRightShortcodeDelim())
+ l.emit(tText)
+ return lexMainSection
+}
+
+func lexShortcodeRightDelim(l *pageLexer) stateFunc {
+ l.closingState = 0
+ l.pos += len(l.currentRightShortcodeDelim())
+ l.emit(l.currentRightShortcodeDelimItem())
+ return lexMainSection
+}
+
+// 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
+}
+
+// Inline shortcodes has the form {{< myshortcode.inline >}}
+var inlineIdentifier = []byte("inline ")
+
+// scans an alphanumeric inside shortcode
+func lexIdentifierInShortcode(l *pageLexer) stateFunc {
+ lookForEnd := false
+Loop:
+ for {
+ switch r := l.next(); {
+ case isAlphaNumericOrHyphen(r):
+ // Allow forward slash inside names to make it possible to create namespaces.
+ case r == '/':
+ case r == '.':
+ l.isInline = l.hasPrefix(inlineIdentifier)
+ if !l.isInline {
+ return l.errorf("period in shortcode name only allowed for inline identifiers")
+ }
+ default:
+ l.backup()
+ word := string(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++
+ if l.isInline {
+ l.emit(tScNameInline)
+ } else {
+ l.emit(tScName)
+ }
+ break Loop
+ }
+ }
+
+ if lookForEnd {
+ return lexEndOfShortcode
+ }
+ return lexInsideShortcode
+}
+
+func lexEndOfShortcode(l *pageLexer) stateFunc {
+ l.isInline = false
+ if l.hasPrefix(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 l.hasPrefix(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.isInline = false
+ 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
+}
+
+func (l *pageLexer) currentLeftShortcodeDelimItem() ItemType {
+ return l.currLeftDelimItem
+}
+
+func (l *pageLexer) currentRightShortcodeDelimItem() ItemType {
+ return l.currRightDelimItem
+}
+
+func (l *pageLexer) currentLeftShortcodeDelim() []byte {
+ if l.currLeftDelimItem == tLeftDelimScWithMarkup {
+ return leftDelimScWithMarkup
+ }
+ return leftDelimScNoMarkup
+
+}
+
+func (l *pageLexer) currentRightShortcodeDelim() []byte {
+ if l.currRightDelimItem == tRightDelimScWithMarkup {
+ return rightDelimScWithMarkup
+ }
+ return rightDelimScNoMarkup
+}
diff --git a/parser/pageparser/pagelexer_test.go b/parser/pageparser/pagelexer_test.go
new file mode 100644
index 000000000..70def3091
--- /dev/null
+++ b/parser/pageparser/pagelexer_test.go
@@ -0,0 +1,29 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pageparser
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestMinIndex(t *testing.T) {
+ assert := require.New(t)
+ assert.Equal(1, minIndex(4, 1, 2, 3))
+ assert.Equal(0, minIndex(4, 0, -2, 2, 5))
+ assert.Equal(-1, minIndex())
+ assert.Equal(-1, minIndex(-2, -3))
+
+}
diff --git a/parser/pageparser/pageparser.go b/parser/pageparser/pageparser.go
new file mode 100644
index 000000000..db563d44c
--- /dev/null
+++ b/parser/pageparser/pageparser.go
@@ -0,0 +1,139 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package pageparser provides a parser for Hugo content files (Markdown, HTML etc.) in Hugo.
+// This implementation 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
+package pageparser
+
+import (
+ "bytes"
+ "io"
+ "io/ioutil"
+
+ "github.com/pkg/errors"
+)
+
+// Result holds the parse result.
+type Result interface {
+ // Iterator returns a new Iterator positioned at the beginning of the parse tree.
+ Iterator() *Iterator
+ // Input returns the input to Parse.
+ Input() []byte
+}
+
+var _ Result = (*pageLexer)(nil)
+
+// Parse parses the page in the given reader according to the given Config.
+// TODO(bep) now that we have improved the "lazy order" init, it *may* be
+// some potential saving in doing a buffered approach where the first pass does
+// the frontmatter only.
+func Parse(r io.Reader, cfg Config) (Result, error) {
+ return parseSection(r, cfg, lexIntroSection)
+}
+
+// ParseMain parses starting with the main section. Used in tests.
+func ParseMain(r io.Reader, cfg Config) (Result, error) {
+ return parseSection(r, cfg, lexMainSection)
+}
+
+func parseSection(r io.Reader, cfg Config, start stateFunc) (Result, error) {
+ b, err := ioutil.ReadAll(r)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to read page content")
+ }
+ return parseBytes(b, cfg, start)
+}
+
+func parseBytes(b []byte, cfg Config, start stateFunc) (Result, error) {
+ lexer := newPageLexer(b, start, cfg)
+ lexer.run()
+ return lexer, nil
+}
+
+// An Iterator has methods to iterate a parsed page with support going back
+// if needed.
+type Iterator struct {
+ l *pageLexer
+ lastPos int // position of the last item returned by nextItem
+}
+
+// consumes and returns the next item
+func (t *Iterator) Next() Item {
+ t.lastPos++
+ return t.Current()
+}
+
+// Input returns the input source.
+func (t *Iterator) Input() []byte {
+ return t.l.Input()
+}
+
+var errIndexOutOfBounds = Item{tError, 0, []byte("no more tokens")}
+
+// Current will repeatably return the current item.
+func (t *Iterator) Current() Item {
+ if t.lastPos >= len(t.l.items) {
+ return errIndexOutOfBounds
+ }
+ return t.l.items[t.lastPos]
+}
+
+// backs up one token.
+func (t *Iterator) Backup() {
+ if t.lastPos < 0 {
+ panic("need to go forward before going back")
+ }
+ t.lastPos--
+}
+
+// check for non-error and non-EOF types coming next
+func (t *Iterator) IsValueNext() bool {
+ i := t.Peek()
+ return i.Type != tError && i.Type != tEOF
+}
+
+// look at, but do not consume, the next item
+// repeated, sequential calls will return the same item
+func (t *Iterator) Peek() Item {
+ return t.l.items[t.lastPos+1]
+}
+
+// PeekWalk will feed the next items in the iterator to walkFn
+// until it returns false.
+func (t *Iterator) PeekWalk(walkFn func(item Item) bool) {
+ for i := t.lastPos + 1; i < len(t.l.items); i++ {
+ item := t.l.items[i]
+ if !walkFn(item) {
+ break
+ }
+ }
+}
+
+// Consume is a convencience method to consume the next n tokens,
+// but back off Errors and EOF.
+func (t *Iterator) Consume(cnt int) {
+ for i := 0; i < cnt; i++ {
+ token := t.Next()
+ if token.Type == tError || token.Type == tEOF {
+ t.Backup()
+ break
+ }
+ }
+}
+
+// LineNumber returns the current line number. Used for logging.
+func (t *Iterator) LineNumber() int {
+ return bytes.Count(t.l.input[:t.Current().Pos], lf) + 1
+}
diff --git a/parser/pageparser/pageparser_intro_test.go b/parser/pageparser/pageparser_intro_test.go
new file mode 100644
index 000000000..3e5bac872
--- /dev/null
+++ b/parser/pageparser/pageparser_intro_test.go
@@ -0,0 +1,127 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pageparser
+
+import (
+ "fmt"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+type lexerTest struct {
+ name string
+ input string
+ items []Item
+}
+
+func nti(tp ItemType, val string) Item {
+ return Item{tp, 0, []byte(val)}
+}
+
+var (
+ tstJSON = `{ "a": { "b": "\"Hugo\"}" } }`
+ tstFrontMatterTOML = nti(TypeFrontMatterTOML, "foo = \"bar\"\n")
+ tstFrontMatterYAML = nti(TypeFrontMatterYAML, "foo: \"bar\"\n")
+ tstFrontMatterYAMLCRLF = nti(TypeFrontMatterYAML, "foo: \"bar\"\r\n")
+ tstFrontMatterJSON = nti(TypeFrontMatterJSON, tstJSON+"\r\n")
+ tstSomeText = nti(tText, "\nSome text.\n")
+ tstSummaryDivider = nti(TypeLeadSummaryDivider, "<!--more-->\n")
+ tstHtmlStart = nti(TypeHTMLStart, "<")
+ tstNewline = nti(tText, "\n")
+
+ tstORG = `
+#+TITLE: T1
+#+AUTHOR: A1
+#+DESCRIPTION: D1
+`
+ tstFrontMatterORG = nti(TypeFrontMatterORG, tstORG)
+)
+
+var crLfReplacer = strings.NewReplacer("\r", "#", "\n", "$")
+
+// TODO(bep) a way to toggle ORG mode vs the rest.
+var frontMatterTests = []lexerTest{
+ {"empty", "", []Item{tstEOF}},
+ {"Byte order mark", "\ufeff\nSome text.\n", []Item{nti(TypeIgnore, "\ufeff"), tstSomeText, tstEOF}},
+ {"HTML Document", ` <html> `, []Item{nti(tText, " "), tstHtmlStart, nti(tText, "html> "), tstEOF}},
+ {"HTML Document with shortcode", `<html>{{< sc1 >}}</html>`, []Item{tstHtmlStart, nti(tText, "html>"), tstLeftNoMD, tstSC1, tstRightNoMD, nti(tText, "</html>"), tstEOF}},
+ {"No front matter", "\nSome text.\n", []Item{tstSomeText, tstEOF}},
+ {"YAML front matter", "---\nfoo: \"bar\"\n---\n\nSome text.\n", []Item{tstFrontMatterYAML, tstSomeText, tstEOF}},
+ {"YAML empty front matter", "---\n---\n\nSome text.\n", []Item{nti(TypeFrontMatterYAML, ""), tstSomeText, tstEOF}},
+ {"YAML commented out front matter", "<!--\n---\nfoo: \"bar\"\n---\n-->\nSome text.\n", []Item{nti(TypeIgnore, "<!--\n"), tstFrontMatterYAML, nti(TypeIgnore, "-->"), tstSomeText, tstEOF}},
+ {"YAML commented out front matter, no end", "<!--\n---\nfoo: \"bar\"\n---\nSome text.\n", []Item{nti(TypeIgnore, "<!--\n"), tstFrontMatterYAML, nti(tError, "starting HTML comment with no end")}},
+ // Note that we keep all bytes as they are, but we need to handle CRLF
+ {"YAML front matter CRLF", "---\r\nfoo: \"bar\"\r\n---\n\nSome text.\n", []Item{tstFrontMatterYAMLCRLF, tstSomeText, tstEOF}},
+ {"TOML front matter", "+++\nfoo = \"bar\"\n+++\n\nSome text.\n", []Item{tstFrontMatterTOML, tstSomeText, tstEOF}},
+ {"JSON front matter", tstJSON + "\r\n\nSome text.\n", []Item{tstFrontMatterJSON, tstSomeText, tstEOF}},
+ {"ORG front matter", tstORG + "\nSome text.\n", []Item{tstFrontMatterORG, tstSomeText, tstEOF}},
+ {"Summary divider ORG", tstORG + "\nSome text.\n# more\nSome text.\n", []Item{tstFrontMatterORG, tstSomeText, nti(TypeLeadSummaryDivider, "# more\n"), nti(tText, "Some text.\n"), tstEOF}},
+ {"Summary divider", "+++\nfoo = \"bar\"\n+++\n\nSome text.\n<!--more-->\nSome text.\n", []Item{tstFrontMatterTOML, tstSomeText, tstSummaryDivider, nti(tText, "Some text.\n"), tstEOF}},
+ {"Summary divider same line", "+++\nfoo = \"bar\"\n+++\n\nSome text.<!--more-->Some text.\n", []Item{tstFrontMatterTOML, nti(tText, "\nSome text."), nti(TypeLeadSummaryDivider, "<!--more-->"), nti(tText, "Some text.\n"), tstEOF}},
+ // https://github.com/gohugoio/hugo/issues/5402
+ {"Summary and shortcode, no space", "+++\nfoo = \"bar\"\n+++\n\nSome text.\n<!--more-->{{< sc1 >}}\nSome text.\n", []Item{tstFrontMatterTOML, tstSomeText, nti(TypeLeadSummaryDivider, "<!--more-->"), tstLeftNoMD, tstSC1, tstRightNoMD, tstSomeText, tstEOF}},
+ // https://github.com/gohugoio/hugo/issues/5464
+ {"Summary and shortcode only", "+++\nfoo = \"bar\"\n+++\n{{< sc1 >}}\n<!--more-->\n{{< sc2 >}}", []Item{tstFrontMatterTOML, tstLeftNoMD, tstSC1, tstRightNoMD, tstNewline, tstSummaryDivider, tstLeftNoMD, tstSC2, tstRightNoMD, tstEOF}},
+}
+
+func TestFrontMatter(t *testing.T) {
+ t.Parallel()
+ for i, test := range frontMatterTests {
+ items := collect([]byte(test.input), false, lexIntroSection)
+ if !equal(items, test.items) {
+ got := crLfReplacer.Replace(fmt.Sprint(items))
+ expected := crLfReplacer.Replace(fmt.Sprint(test.items))
+ t.Errorf("[%d] %s: got\n\t%v\nexpected\n\t%v", i, test.name, got, expected)
+ }
+ }
+}
+
+func collectWithConfig(input []byte, skipFrontMatter bool, stateStart stateFunc, cfg Config) (items []Item) {
+ l := newPageLexer(input, stateStart, cfg)
+ l.run()
+ t := l.newIterator()
+
+ for {
+ item := t.Next()
+ items = append(items, item)
+ if item.Type == tEOF || item.Type == tError {
+ break
+ }
+ }
+ return
+}
+
+func collect(input []byte, skipFrontMatter bool, stateStart stateFunc) (items []Item) {
+ var cfg Config
+
+ return collectWithConfig(input, skipFrontMatter, stateStart, cfg)
+
+}
+
+// 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].Type != i2[k].Type {
+ return false
+ }
+ if !reflect.DeepEqual(i1[k].Val, i2[k].Val) {
+ return false
+ }
+ }
+ return true
+}
diff --git a/parser/pageparser/pageparser_main_test.go b/parser/pageparser/pageparser_main_test.go
new file mode 100644
index 000000000..008c88c51
--- /dev/null
+++ b/parser/pageparser/pageparser_main_test.go
@@ -0,0 +1,40 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pageparser
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestMain(t *testing.T) {
+ t.Parallel()
+
+ var mainTests = []lexerTest{
+ {"emoji #1", "Some text with :emoji:", []Item{nti(tText, "Some text with "), nti(TypeEmoji, ":emoji:"), tstEOF}},
+ {"emoji #2", "Some text with :emoji: and some text.", []Item{nti(tText, "Some text with "), nti(TypeEmoji, ":emoji:"), nti(tText, " and some text."), tstEOF}},
+ {"looks like an emoji #1", "Some text and then :emoji", []Item{nti(tText, "Some text and then "), nti(tText, ":"), nti(tText, "emoji"), tstEOF}},
+ {"looks like an emoji #2", "Some text and then ::", []Item{nti(tText, "Some text and then "), nti(tText, ":"), nti(tText, ":"), tstEOF}},
+ {"looks like an emoji #3", ":Some :text", []Item{nti(tText, ":"), nti(tText, "Some "), nti(tText, ":"), nti(tText, "text"), tstEOF}},
+ }
+
+ for i, test := range mainTests {
+ items := collectWithConfig([]byte(test.input), false, lexMainSection, Config{EnableEmoji: true})
+ if !equal(items, test.items) {
+ got := crLfReplacer.Replace(fmt.Sprint(items))
+ expected := crLfReplacer.Replace(fmt.Sprint(test.items))
+ t.Errorf("[%d] %s: got\n\t%v\nexpected\n\t%v", i, test.name, got, expected)
+ }
+ }
+}
diff --git a/parser/pageparser/pageparser_shortcode_test.go b/parser/pageparser/pageparser_shortcode_test.go
new file mode 100644
index 000000000..75ee56090
--- /dev/null
+++ b/parser/pageparser/pageparser_shortcode_test.go
@@ -0,0 +1,191 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pageparser
+
+import "testing"
+
+var (
+ tstEOF = nti(tEOF, "")
+ tstLeftNoMD = nti(tLeftDelimScNoMarkup, "{{<")
+ tstRightNoMD = nti(tRightDelimScNoMarkup, ">}}")
+ tstLeftMD = nti(tLeftDelimScWithMarkup, "{{%")
+ tstRightMD = nti(tRightDelimScWithMarkup, "%}}")
+ tstSCClose = nti(tScClose, "/")
+ tstSC1 = nti(tScName, "sc1")
+ tstSC1Inline = nti(tScNameInline, "sc1.inline")
+ tstSC2Inline = nti(tScNameInline, "sc2.inline")
+ tstSC2 = nti(tScName, "sc2")
+ tstSC3 = nti(tScName, "sc3")
+ tstSCSlash = nti(tScName, "sc/sub")
+ tstParam1 = nti(tScParam, "param1")
+ tstParam2 = nti(tScParam, "param2")
+ tstVal = nti(tScParamVal, "Hello World")
+ tstText = nti(tText, "Hello World")
+)
+
+var shortCodeLexerTests = []lexerTest{
+ {"empty", "", []Item{tstEOF}},
+ {"spaces", " \t\n", []Item{nti(tText, " \t\n"), tstEOF}},
+ {"text", `to be or not`, []Item{nti(tText, "to be or not"), tstEOF}},
+ {"no markup", `{{< sc1 >}}`, []Item{tstLeftNoMD, tstSC1, tstRightNoMD, tstEOF}},
+ {"with EOL", "{{< sc1 \n >}}", []Item{tstLeftNoMD, tstSC1, tstRightNoMD, tstEOF}},
+
+ {"forward slash inside name", `{{< sc/sub >}}`, []Item{tstLeftNoMD, tstSCSlash, 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,
+ nti(tError, "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,
+ nti(tText, " inner "),
+ tstLeftMD,
+ tstSCClose,
+ tstSC1,
+ tstRightMD,
+ tstEOF,
+ }},
+ {"close, but no open", `{{< /sc1 >}}`, []Item{
+ tstLeftNoMD, nti(tError, "got closing shortcode, but none is open")}},
+ {"close wrong", `{{< sc1 >}}{{< /another >}}`, []Item{
+ tstLeftNoMD, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose,
+ nti(tError, "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,
+ nti(tError, "closing tag for shortcode 'another' does not match start tag")}},
+ {"close with extra keyword", `{{< sc1 >}}{{< /sc1 keyword>}}`, []Item{
+ tstLeftNoMD, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose, tstSC1,
+ nti(tError, "unclosed shortcode")}},
+ {"Youtube id", `{{< sc1 -ziL-Q_456igdO-4 >}}`, []Item{
+ tstLeftNoMD, tstSC1, nti(tScParam, "-ziL-Q_456igdO-4"), tstRightNoMD, tstEOF}},
+ {"non-alphanumerics param quoted", `{{< sc1 "-ziL-.%QigdO-4" >}}`, []Item{
+ tstLeftNoMD, tstSC1, nti(tScParam, "-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,
+ nti(tText, "ab"),
+ tstLeftMD, tstSC2, tstParam1, tstRightMD,
+ nti(tText, "cd"),
+ tstLeftNoMD, tstSC3, tstRightNoMD,
+ nti(tText, "ef"),
+ tstLeftNoMD, tstSCClose, tstSC3, tstRightNoMD,
+ nti(tText, "gh"),
+ tstLeftMD, tstSCClose, tstSC2, tstRightMD,
+ nti(tText, "ij"),
+ tstLeftNoMD, tstSCClose, tstSC1, tstRightNoMD,
+ nti(tText, "kl"), tstEOF,
+ }},
+
+ {"two quoted params", `{{< sc1 "param nr. 1" "param nr. 2" >}}`, []Item{
+ tstLeftNoMD, tstSC1, nti(tScParam, "param nr. 1"), nti(tScParam, "param nr. 2"), tstRightNoMD, tstEOF}},
+ {"two named params", `{{< sc1 param1="Hello World" param2="p2Val">}}`, []Item{
+ tstLeftNoMD, tstSC1, tstParam1, tstVal, tstParam2, nti(tScParamVal, "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,
+ nti(tScParamVal, `Hello `), nti(tError, `got positional parameter 'escaped'. Cannot mix named and positional parameters`)}},
+ {"escaped quotes inside nonescaped quotes",
+ `{{< sc1 param1="Hello \"escaped\" World" >}}`, []Item{
+ tstLeftNoMD, tstSC1, tstParam1, nti(tScParamVal, `Hello "escaped" World`), tstRightNoMD, tstEOF}},
+ {"escaped quotes inside nonescaped quotes in positional param",
+ `{{< sc1 "Hello \"escaped\" World" >}}`, []Item{
+ tstLeftNoMD, tstSC1, nti(tScParam, `Hello "escaped" World`), tstRightNoMD, tstEOF}},
+ {"unterminated quote", `{{< sc1 param2="Hello World>}}`, []Item{
+ tstLeftNoMD, tstSC1, tstParam2, nti(tError, "unterminated quoted string in shortcode parameter-argument: 'Hello World>}}'")}},
+ {"one named param, one not", `{{< sc1 param1="Hello World" p2 >}}`, []Item{
+ tstLeftNoMD, tstSC1, tstParam1, tstVal,
+ nti(tError, "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,
+ nti(tError, "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,
+ nti(tError, "got named parameter 'param2'. Cannot mix named and positional parameters")}},
+ {"ono positional param, one not", `{{< sc1 param1 param2="Hello World">}}`, []Item{
+ tstLeftNoMD, tstSC1, tstParam1,
+ nti(tError, "got named parameter 'param2'. Cannot mix named and positional parameters")}},
+ {"commented out", `{{</* sc1 */>}}`, []Item{
+ nti(tText, "{{<"), nti(tText, " sc1 "), nti(tText, ">}}"), tstEOF}},
+ {"commented out, with asterisk inside", `{{</* sc1 "**/*.pdf" */>}}`, []Item{
+ nti(tText, "{{<"), nti(tText, " sc1 \"**/*.pdf\" "), nti(tText, ">}}"), tstEOF}},
+ {"commented out, missing close", `{{</* sc1 >}}`, []Item{
+ nti(tError, "comment must be closed")}},
+ {"commented out, misplaced close", `{{</* sc1 >}}*/`, []Item{
+ nti(tError, "comment must be closed")}},
+ // Inline shortcodes
+ {"basic inline", `{{< sc1.inline >}}Hello World{{< /sc1.inline >}}`, []Item{tstLeftNoMD, tstSC1Inline, tstRightNoMD, tstText, tstLeftNoMD, tstSCClose, tstSC1Inline, tstRightNoMD, tstEOF}},
+ {"basic inline with space", `{{< sc1.inline >}}Hello World{{< / sc1.inline >}}`, []Item{tstLeftNoMD, tstSC1Inline, tstRightNoMD, tstText, tstLeftNoMD, tstSCClose, tstSC1Inline, tstRightNoMD, tstEOF}},
+ {"inline self closing", `{{< sc1.inline >}}Hello World{{< /sc1.inline >}}Hello World{{< sc1.inline />}}`, []Item{tstLeftNoMD, tstSC1Inline, tstRightNoMD, tstText, tstLeftNoMD, tstSCClose, tstSC1Inline, tstRightNoMD, tstText, tstLeftNoMD, tstSC1Inline, tstSCClose, tstRightNoMD, tstEOF}},
+ {"inline self closing, then a new inline", `{{< sc1.inline >}}Hello World{{< /sc1.inline >}}Hello World{{< sc1.inline />}}{{< sc2.inline >}}Hello World{{< /sc2.inline >}}`, []Item{
+ tstLeftNoMD, tstSC1Inline, tstRightNoMD, tstText, tstLeftNoMD, tstSCClose, tstSC1Inline, tstRightNoMD, tstText, tstLeftNoMD, tstSC1Inline, tstSCClose, tstRightNoMD,
+ tstLeftNoMD, tstSC2Inline, tstRightNoMD, tstText, tstLeftNoMD, tstSCClose, tstSC2Inline, tstRightNoMD, tstEOF}},
+ {"inline with template syntax", `{{< sc1.inline >}}{{ .Get 0 }}{{ .Get 1 }}{{< /sc1.inline >}}`, []Item{tstLeftNoMD, tstSC1Inline, tstRightNoMD, nti(tText, "{{ .Get 0 }}"), nti(tText, "{{ .Get 1 }}"), tstLeftNoMD, tstSCClose, tstSC1Inline, tstRightNoMD, tstEOF}},
+ {"inline with nested shortcode (not supported)", `{{< sc1.inline >}}Hello World{{< sc1 >}}{{< /sc1.inline >}}`, []Item{tstLeftNoMD, tstSC1Inline, tstRightNoMD, tstText, nti(tError, "inline shortcodes do not support nesting")}},
+ {"inline case mismatch", `{{< sc1.Inline >}}Hello World{{< /sc1.Inline >}}`, []Item{tstLeftNoMD, nti(tError, "period in shortcode name only allowed for inline identifiers")}},
+}
+
+func TestShortcodeLexer(t *testing.T) {
+ t.Parallel()
+ for i, test := range shortCodeLexerTests {
+ t.Run(test.name, func(t *testing.T) {
+ items := collect([]byte(test.input), true, lexMainSection)
+ 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) {
+ testInputs := make([][]byte, len(shortCodeLexerTests))
+ for i, input := range shortCodeLexerTests {
+ testInputs[i] = []byte(input.input)
+ }
+ var cfg Config
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ for _, input := range testInputs {
+ items := collectWithConfig(input, true, lexMainSection, cfg)
+ if len(items) == 0 {
+ }
+
+ }
+ }
+}
diff --git a/parser/pageparser/pageparser_test.go b/parser/pageparser/pageparser_test.go
new file mode 100644
index 000000000..f54376c33
--- /dev/null
+++ b/parser/pageparser/pageparser_test.go
@@ -0,0 +1,71 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pageparser
+
+import (
+ "strings"
+ "testing"
+)
+
+func BenchmarkParse(b *testing.B) {
+ start := `
+
+
+---
+title: "Front Matters"
+description: "It really does"
+---
+
+This is some summary. This is some summary. This is some summary. This is some summary.
+
+ <!--more-->
+
+
+`
+ input := []byte(start + strings.Repeat(strings.Repeat("this is text", 30)+"{{< myshortcode >}}This is some inner content.{{< /myshortcode >}}", 10))
+ cfg := Config{EnableEmoji: false}
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ if _, err := parseBytes(input, cfg, lexIntroSection); err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func BenchmarkParseWithEmoji(b *testing.B) {
+ start := `
+
+
+---
+title: "Front Matters"
+description: "It really does"
+---
+
+This is some summary. This is some summary. This is some summary. This is some summary.
+
+ <!--more-->
+
+
+`
+ input := []byte(start + strings.Repeat("this is not emoji: ", 50) + strings.Repeat("some text ", 70) + strings.Repeat("this is not: ", 50) + strings.Repeat("but this is a :smile: ", 3) + strings.Repeat("some text ", 70))
+ cfg := Config{EnableEmoji: true}
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ if _, err := parseBytes(input, cfg, lexIntroSection); err != nil {
+ b.Fatal(err)
+ }
+ }
+}
diff --git a/publisher/publisher.go b/publisher/publisher.go
new file mode 100644
index 000000000..119be356b
--- /dev/null
+++ b/publisher/publisher.go
@@ -0,0 +1,162 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package publisher
+
+import (
+ "errors"
+ "io"
+ "sync/atomic"
+
+ "github.com/gohugoio/hugo/media"
+
+ "github.com/gohugoio/hugo/minifiers"
+
+ bp "github.com/gohugoio/hugo/bufferpool"
+ "github.com/gohugoio/hugo/helpers"
+
+ "github.com/spf13/afero"
+
+ "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/transform"
+ "github.com/gohugoio/hugo/transform/livereloadinject"
+ "github.com/gohugoio/hugo/transform/metainject"
+ "github.com/gohugoio/hugo/transform/urlreplacers"
+)
+
+// Descriptor describes the needed publishing chain for an item.
+type Descriptor struct {
+ // The content to publish.
+ Src io.Reader
+
+ // The OutputFormat of the this content.
+ OutputFormat output.Format
+
+ // Where to publish this content. This is a filesystem-relative path.
+ TargetPath string
+
+ // Counter for the end build summary.
+ StatCounter *uint64
+
+ // Configuration that trigger pre-processing.
+ // LiveReload script will be injected if this is > 0
+ LiveReloadPort int
+
+ // Enable to inject the Hugo generated tag in the header. Is currently only
+ // injected on the home page for HTML type of output formats.
+ AddHugoGeneratorTag bool
+
+ // If set, will replace all relative URLs with this one.
+ AbsURLPath string
+
+ // Enable to minify the output using the OutputFormat defined above to
+ // pick the correct minifier configuration.
+ Minify bool
+}
+
+// DestinationPublisher is the default and currently only publisher in Hugo. This
+// publisher prepares and publishes an item to the defined destination, e.g. /public.
+type DestinationPublisher struct {
+ fs afero.Fs
+ minify bool
+ min minifiers.Client
+}
+
+// NewDestinationPublisher creates a new DestinationPublisher.
+func NewDestinationPublisher(fs afero.Fs, outputFormats output.Formats, mediaTypes media.Types, minify bool) DestinationPublisher {
+ pub := DestinationPublisher{fs: fs}
+ if minify {
+ pub.min = minifiers.New(mediaTypes, outputFormats)
+ pub.minify = true
+ }
+ return pub
+}
+
+// Publish applies any relevant transformations and writes the file
+// to its destination, e.g. /public.
+func (p DestinationPublisher) Publish(d Descriptor) error {
+ if d.TargetPath == "" {
+ return errors.New("Publish: must provide a TargetPath")
+ }
+
+ src := d.Src
+
+ transformers := p.createTransformerChain(d)
+
+ if len(transformers) != 0 {
+ b := bp.GetBuffer()
+ defer bp.PutBuffer(b)
+
+ if err := transformers.Apply(b, d.Src); err != nil {
+ return err
+ }
+
+ // This is now what we write to disk.
+ src = b
+ }
+
+ f, err := helpers.OpenFileForWriting(p.fs, d.TargetPath)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ _, err = io.Copy(f, src)
+ if err == nil && d.StatCounter != nil {
+ atomic.AddUint64(d.StatCounter, uint64(1))
+ }
+ return err
+}
+
+// Publisher publishes a result file.
+type Publisher interface {
+ Publish(d Descriptor) error
+}
+
+// XML transformer := transform.New(urlreplacers.NewAbsURLInXMLTransformer(path))
+func (p DestinationPublisher) createTransformerChain(f Descriptor) transform.Chain {
+ transformers := transform.NewEmpty()
+
+ isHTML := f.OutputFormat.IsHTML
+
+ if f.AbsURLPath != "" {
+ if isHTML {
+ transformers = append(transformers, urlreplacers.NewAbsURLTransformer(f.AbsURLPath))
+ } else {
+ // Assume XML.
+ transformers = append(transformers, urlreplacers.NewAbsURLInXMLTransformer(f.AbsURLPath))
+ }
+ }
+
+ if isHTML {
+ if f.LiveReloadPort > 0 {
+ transformers = append(transformers, livereloadinject.New(f.LiveReloadPort))
+ }
+
+ // This is only injected on the home page.
+ if f.AddHugoGeneratorTag {
+ transformers = append(transformers, metainject.HugoGenerator)
+ }
+
+ }
+
+ if p.minify {
+ minifyTransformer := p.min.Transformer(f.OutputFormat.MediaType)
+ if minifyTransformer != nil {
+ transformers = append(transformers, minifyTransformer)
+ }
+ }
+
+ return transformers
+
+}
diff --git a/publisher/publisher_test.go b/publisher/publisher_test.go
new file mode 100644
index 000000000..200accc8b
--- /dev/null
+++ b/publisher/publisher_test.go
@@ -0,0 +1,14 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package publisher
diff --git a/pull-docs.sh b/pull-docs.sh
new file mode 100755
index 000000000..b8850530a
--- /dev/null
+++ b/pull-docs.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+HUGO_DOCS_BRANCH="${HUGO_DOCS_BRANCH-master}"
+
+# We may extend this to also push changes in the other direction, but this is the most important step.
+git subtree pull --prefix=docs/ https://github.com/gohugoio/hugoDocs.git ${HUGO_DOCS_BRANCH} --squash
+
diff --git a/related/inverted_index.go b/related/inverted_index.go
new file mode 100644
index 000000000..fda6b9222
--- /dev/null
+++ b/related/inverted_index.go
@@ -0,0 +1,459 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package related holds code to help finding related content.
+package related
+
+import (
+ "errors"
+ "fmt"
+ "math"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/gohugoio/hugo/common/types"
+ "github.com/mitchellh/mapstructure"
+)
+
+var (
+ _ Keyword = (*StringKeyword)(nil)
+ zeroDate = time.Time{}
+
+ // DefaultConfig is the default related config.
+ DefaultConfig = Config{
+ Threshold: 80,
+ Indices: IndexConfigs{
+ IndexConfig{Name: "keywords", Weight: 100},
+ IndexConfig{Name: "date", Weight: 10},
+ },
+ }
+)
+
+/*
+Config is the top level configuration element used to configure how to retrieve
+related content in Hugo.
+
+An example site config.toml:
+
+ [related]
+ threshold = 1
+ [[related.indices]]
+ name = "keywords"
+ weight = 200
+ [[related.indices]]
+ name = "tags"
+ weight = 100
+ [[related.indices]]
+ name = "date"
+ weight = 1
+ pattern = "2006"
+*/
+type Config struct {
+ // Only include matches >= threshold, a normalized rank between 0 and 100.
+ Threshold int
+
+ // To get stable "See also" sections we, by default, exclude newer related pages.
+ IncludeNewer bool
+
+ // Will lower case all string values and queries to the indices.
+ // May get better results, but at a slight performance cost.
+ ToLower bool
+
+ Indices IndexConfigs
+}
+
+// Add adds a given index.
+func (c *Config) Add(index IndexConfig) {
+ if c.ToLower {
+ index.ToLower = true
+ }
+ c.Indices = append(c.Indices, index)
+}
+
+// IndexConfigs holds a set of index configurations.
+type IndexConfigs []IndexConfig
+
+// IndexConfig configures an index.
+type IndexConfig struct {
+ // The index name. This directly maps to a field or Param name.
+ Name string
+
+ // Contextual pattern used to convert the Param value into a string.
+ // Currently only used for dates. Can be used to, say, bump posts in the same
+ // time frame when searching for related documents.
+ // For dates it follows Go's time.Format patterns, i.e.
+ // "2006" for YYYY and "200601" for YYYYMM.
+ Pattern string
+
+ // This field's weight when doing multi-index searches. Higher is "better".
+ Weight int
+
+ // Will lower case all string values in and queries tothis index.
+ // May get better accurate results, but at a slight performance cost.
+ ToLower bool
+}
+
+// Document is the interface an indexable document in Hugo must fulfill.
+type Document interface {
+ // RelatedKeywords returns a list of keywords for the given index config.
+ RelatedKeywords(cfg IndexConfig) ([]Keyword, error)
+
+ // When this document was or will be published.
+ PublishDate() time.Time
+
+ // Name is used as an tiebreaker if both Weight and PublishDate are
+ // the same.
+ Name() string
+}
+
+// InvertedIndex holds an inverted index, also sometimes named posting list, which
+// lists, for every possible search term, the documents that contain that term.
+type InvertedIndex struct {
+ cfg Config
+ index map[string]map[Keyword][]Document
+
+ minWeight int
+ maxWeight int
+}
+
+func (idx *InvertedIndex) getIndexCfg(name string) (IndexConfig, bool) {
+ for _, conf := range idx.cfg.Indices {
+ if conf.Name == name {
+ return conf, true
+ }
+ }
+
+ return IndexConfig{}, false
+}
+
+// NewInvertedIndex creates a new InvertedIndex.
+// Documents to index must be added in Add.
+func NewInvertedIndex(cfg Config) *InvertedIndex {
+ idx := &InvertedIndex{index: make(map[string]map[Keyword][]Document), cfg: cfg}
+ for _, conf := range cfg.Indices {
+ idx.index[conf.Name] = make(map[Keyword][]Document)
+ if conf.Weight < idx.minWeight {
+ // By default, the weight scale starts at 0, but we allow
+ // negative weights.
+ idx.minWeight = conf.Weight
+ }
+ if conf.Weight > idx.maxWeight {
+ idx.maxWeight = conf.Weight
+ }
+ }
+ return idx
+}
+
+// Add documents to the inverted index.
+// The value must support == and !=.
+func (idx *InvertedIndex) Add(docs ...Document) error {
+ var err error
+ for _, config := range idx.cfg.Indices {
+ if config.Weight == 0 {
+ // Disabled
+ continue
+ }
+ setm := idx.index[config.Name]
+
+ for _, doc := range docs {
+ var words []Keyword
+ words, err = doc.RelatedKeywords(config)
+ if err != nil {
+ continue
+ }
+
+ for _, keyword := range words {
+ setm[keyword] = append(setm[keyword], doc)
+ }
+ }
+ }
+
+ return err
+
+}
+
+// queryElement holds the index name and keywords that can be used to compose a
+// search for related content.
+type queryElement struct {
+ Index string
+ Keywords []Keyword
+}
+
+func newQueryElement(index string, keywords ...Keyword) queryElement {
+ return queryElement{Index: index, Keywords: keywords}
+}
+
+type ranks []*rank
+
+type rank struct {
+ Doc Document
+ Weight int
+ Matches int
+}
+
+func (r *rank) addWeight(w int) {
+ r.Weight += w
+ r.Matches++
+}
+
+func newRank(doc Document, weight int) *rank {
+ return &rank{Doc: doc, Weight: weight, Matches: 1}
+}
+
+func (r ranks) Len() int { return len(r) }
+func (r ranks) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
+func (r ranks) Less(i, j int) bool {
+ if r[i].Weight == r[j].Weight {
+ if r[i].Doc.PublishDate() == r[j].Doc.PublishDate() {
+ return r[i].Doc.Name() < r[j].Doc.Name()
+ }
+ return r[i].Doc.PublishDate().After(r[j].Doc.PublishDate())
+ }
+ return r[i].Weight > r[j].Weight
+}
+
+// SearchDoc finds the documents matching any of the keywords in the given indices
+// against the given document.
+// The resulting document set will be sorted according to number of matches
+// and the index weights, and any matches with a rank below the configured
+// threshold (normalize to 0..100) will be removed.
+// If an index name is provided, only that index will be queried.
+func (idx *InvertedIndex) SearchDoc(doc Document, indices ...string) ([]Document, error) {
+ var q []queryElement
+
+ var configs IndexConfigs
+
+ if len(indices) == 0 {
+ configs = idx.cfg.Indices
+ } else {
+ configs = make(IndexConfigs, len(indices))
+ for i, indexName := range indices {
+ cfg, found := idx.getIndexCfg(indexName)
+ if !found {
+ return nil, fmt.Errorf("index %q not found", indexName)
+ }
+ configs[i] = cfg
+ }
+ }
+
+ for _, cfg := range configs {
+ keywords, err := doc.RelatedKeywords(cfg)
+ if err != nil {
+ return nil, err
+ }
+
+ q = append(q, newQueryElement(cfg.Name, keywords...))
+
+ }
+
+ return idx.searchDate(doc.PublishDate(), q...)
+}
+
+// ToKeywords returns a Keyword slice of the given input.
+func (cfg IndexConfig) ToKeywords(v interface{}) ([]Keyword, error) {
+ var (
+ keywords []Keyword
+ toLower = cfg.ToLower
+ )
+ switch vv := v.(type) {
+ case string:
+ if toLower {
+ vv = strings.ToLower(vv)
+ }
+ keywords = append(keywords, StringKeyword(vv))
+ case []string:
+ if toLower {
+ for i := 0; i < len(vv); i++ {
+ vv[i] = strings.ToLower(vv[i])
+ }
+ }
+ keywords = append(keywords, StringsToKeywords(vv...)...)
+ case time.Time:
+ layout := "2006"
+ if cfg.Pattern != "" {
+ layout = cfg.Pattern
+ }
+ keywords = append(keywords, StringKeyword(vv.Format(layout)))
+ case nil:
+ return keywords, nil
+ default:
+ return keywords, fmt.Errorf("indexing currently not supported for index %q and type %T", cfg.Name, vv)
+ }
+
+ return keywords, nil
+}
+
+// SearchKeyValues finds the documents matching any of the keywords in the given indices.
+// The resulting document set will be sorted according to number of matches
+// and the index weights, and any matches with a rank below the configured
+// threshold (normalize to 0..100) will be removed.
+func (idx *InvertedIndex) SearchKeyValues(args ...types.KeyValues) ([]Document, error) {
+ q := make([]queryElement, len(args))
+
+ for i, arg := range args {
+ var keywords []Keyword
+ key := arg.KeyString()
+ if key == "" {
+ return nil, fmt.Errorf("index %q not valid", arg.Key)
+ }
+ conf, found := idx.getIndexCfg(key)
+ if !found {
+ return nil, fmt.Errorf("index %q not found", key)
+ }
+
+ for _, val := range arg.Values {
+ k, err := conf.ToKeywords(val)
+ if err != nil {
+ return nil, err
+ }
+ keywords = append(keywords, k...)
+ }
+
+ q[i] = newQueryElement(conf.Name, keywords...)
+
+ }
+
+ return idx.search(q...)
+}
+
+func (idx *InvertedIndex) search(query ...queryElement) ([]Document, error) {
+ return idx.searchDate(zeroDate, query...)
+}
+
+func (idx *InvertedIndex) searchDate(upperDate time.Time, query ...queryElement) ([]Document, error) {
+ matchm := make(map[Document]*rank, 200)
+ applyDateFilter := !idx.cfg.IncludeNewer && !upperDate.IsZero()
+
+ for _, el := range query {
+ setm, found := idx.index[el.Index]
+ if !found {
+ return []Document{}, fmt.Errorf("index for %q not found", el.Index)
+ }
+
+ config, found := idx.getIndexCfg(el.Index)
+ if !found {
+ return []Document{}, fmt.Errorf("index config for %q not found", el.Index)
+ }
+
+ for _, kw := range el.Keywords {
+ if docs, found := setm[kw]; found {
+ for _, doc := range docs {
+ if applyDateFilter {
+ // Exclude newer than the limit given
+ if doc.PublishDate().After(upperDate) {
+ continue
+ }
+ }
+ r, found := matchm[doc]
+ if !found {
+ matchm[doc] = newRank(doc, config.Weight)
+ } else {
+ r.addWeight(config.Weight)
+ }
+ }
+ }
+ }
+ }
+
+ if len(matchm) == 0 {
+ return []Document{}, nil
+ }
+
+ matches := make(ranks, 0, 100)
+
+ for _, v := range matchm {
+ avgWeight := v.Weight / v.Matches
+ weight := norm(avgWeight, idx.minWeight, idx.maxWeight)
+ threshold := idx.cfg.Threshold / v.Matches
+
+ if weight >= threshold {
+ matches = append(matches, v)
+ }
+ }
+
+ sort.Stable(matches)
+
+ result := make([]Document, len(matches))
+
+ for i, m := range matches {
+ result[i] = m.Doc
+ }
+
+ return result, nil
+}
+
+// normalizes num to a number between 0 and 100.
+func norm(num, min, max int) int {
+ if min > max {
+ panic("min > max")
+ }
+ return int(math.Floor((float64(num-min) / float64(max-min) * 100) + 0.5))
+}
+
+// DecodeConfig decodes a slice of map into Config.
+func DecodeConfig(in interface{}) (Config, error) {
+ if in == nil {
+ return Config{}, errors.New("no related config provided")
+ }
+
+ m, ok := in.(map[string]interface{})
+ if !ok {
+ return Config{}, fmt.Errorf("expected map[string]interface {} got %T", in)
+ }
+
+ if len(m) == 0 {
+ return Config{}, errors.New("empty related config provided")
+ }
+
+ var c Config
+
+ if err := mapstructure.WeakDecode(m, &c); err != nil {
+ return c, err
+ }
+
+ if c.Threshold < 0 || c.Threshold > 100 {
+ return Config{}, errors.New("related threshold must be between 0 and 100")
+ }
+
+ if c.ToLower {
+ for i := range c.Indices {
+ c.Indices[i].ToLower = true
+ }
+ }
+
+ return c, nil
+}
+
+// StringKeyword is a string search keyword.
+type StringKeyword string
+
+func (s StringKeyword) String() string {
+ return string(s)
+}
+
+// Keyword is the interface a keyword in the search index must implement.
+type Keyword interface {
+ String() string
+}
+
+// StringsToKeywords converts the given slice of strings to a slice of Keyword.
+func StringsToKeywords(s ...string) []Keyword {
+ kw := make([]Keyword, len(s))
+
+ for i := 0; i < len(s); i++ {
+ kw[i] = StringKeyword(s[i])
+ }
+
+ return kw
+}
diff --git a/related/inverted_index_test.go b/related/inverted_index_test.go
new file mode 100644
index 000000000..4ef27875d
--- /dev/null
+++ b/related/inverted_index_test.go
@@ -0,0 +1,308 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package related
+
+import (
+ "fmt"
+ "math/rand"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+type testDoc struct {
+ keywords map[string][]Keyword
+ date time.Time
+ name string
+}
+
+func (d *testDoc) String() string {
+ s := "\n"
+ for k, v := range d.keywords {
+ s += k + ":\t\t"
+ for _, vv := range v {
+ s += " " + vv.String()
+ }
+ s += "\n"
+ }
+ return s
+}
+
+func (d *testDoc) Name() string {
+ return d.name
+}
+
+func newTestDoc(name string, keywords ...string) *testDoc {
+ time.Sleep(1 * time.Millisecond)
+ return newTestDocWithDate(name, time.Now(), keywords...)
+}
+
+func newTestDocWithDate(name string, date time.Time, keywords ...string) *testDoc {
+ km := make(map[string][]Keyword)
+
+ kw := &testDoc{keywords: km, date: date}
+
+ kw.addKeywords(name, keywords...)
+ return kw
+}
+
+func (d *testDoc) addKeywords(name string, keywords ...string) *testDoc {
+ keywordm := createTestKeywords(name, keywords...)
+
+ for k, v := range keywordm {
+ keywords := make([]Keyword, len(v))
+ for i := 0; i < len(v); i++ {
+ keywords[i] = StringKeyword(v[i])
+ }
+ d.keywords[k] = keywords
+ }
+ return d
+}
+
+func createTestKeywords(name string, keywords ...string) map[string][]string {
+ return map[string][]string{
+ name: keywords,
+ }
+}
+
+func (d *testDoc) RelatedKeywords(cfg IndexConfig) ([]Keyword, error) {
+ return d.keywords[cfg.Name], nil
+}
+
+func (d *testDoc) PublishDate() time.Time {
+ return d.date
+}
+
+func TestSearch(t *testing.T) {
+
+ config := Config{
+ Threshold: 90,
+ IncludeNewer: false,
+ Indices: IndexConfigs{
+ IndexConfig{Name: "tags", Weight: 50},
+ IndexConfig{Name: "keywords", Weight: 65},
+ },
+ }
+
+ idx := NewInvertedIndex(config)
+ //idx.debug = true
+
+ docs := []Document{
+ newTestDoc("tags", "a", "b", "c", "d"),
+ newTestDoc("tags", "b", "d", "g"),
+ newTestDoc("tags", "b", "h").addKeywords("keywords", "a"),
+ newTestDoc("tags", "g", "h").addKeywords("keywords", "a", "b"),
+ }
+
+ idx.Add(docs...)
+
+ t.Run("count", func(t *testing.T) {
+ assert := require.New(t)
+ assert.Len(idx.index, 2)
+ set1, found := idx.index["tags"]
+ assert.True(found)
+ // 6 tags
+ assert.Len(set1, 6)
+
+ set2, found := idx.index["keywords"]
+ assert.True(found)
+ assert.Len(set2, 2)
+
+ })
+
+ t.Run("search-tags", func(t *testing.T) {
+ assert := require.New(t)
+ m, err := idx.search(newQueryElement("tags", StringsToKeywords("a", "b", "d", "z")...))
+ assert.NoError(err)
+ assert.Len(m, 2)
+ assert.Equal(docs[0], m[0])
+ assert.Equal(docs[1], m[1])
+ })
+
+ t.Run("search-tags-and-keywords", func(t *testing.T) {
+ assert := require.New(t)
+ m, err := idx.search(
+ newQueryElement("tags", StringsToKeywords("a", "b", "z")...),
+ newQueryElement("keywords", StringsToKeywords("a", "b")...))
+ assert.NoError(err)
+ assert.Len(m, 3)
+ assert.Equal(docs[3], m[0])
+ assert.Equal(docs[2], m[1])
+ assert.Equal(docs[0], m[2])
+ })
+
+ t.Run("searchdoc-all", func(t *testing.T) {
+ assert := require.New(t)
+ doc := newTestDoc("tags", "a").addKeywords("keywords", "a")
+ m, err := idx.SearchDoc(doc)
+ assert.NoError(err)
+ assert.Len(m, 2)
+ assert.Equal(docs[3], m[0])
+ assert.Equal(docs[2], m[1])
+ })
+
+ t.Run("searchdoc-tags", func(t *testing.T) {
+ assert := require.New(t)
+ doc := newTestDoc("tags", "a", "b", "d", "z").addKeywords("keywords", "a", "b")
+ m, err := idx.SearchDoc(doc, "tags")
+ assert.NoError(err)
+ assert.Len(m, 2)
+ assert.Equal(docs[0], m[0])
+ assert.Equal(docs[1], m[1])
+ })
+
+ t.Run("searchdoc-keywords-date", func(t *testing.T) {
+ assert := require.New(t)
+ doc := newTestDoc("tags", "a", "b", "d", "z").addKeywords("keywords", "a", "b")
+ // This will get a date newer than the others.
+ newDoc := newTestDoc("keywords", "a", "b")
+ idx.Add(newDoc)
+
+ m, err := idx.SearchDoc(doc, "keywords")
+ assert.NoError(err)
+ assert.Len(m, 2)
+ assert.Equal(docs[3], m[0])
+ })
+
+ t.Run("searchdoc-keywords-same-date", func(t *testing.T) {
+ assert := require.New(t)
+ idx := NewInvertedIndex(config)
+
+ date := time.Now()
+
+ doc := newTestDocWithDate("keywords", date, "a", "b")
+ doc.name = "thedoc"
+
+ for i := 0; i < 10; i++ {
+ docc := *doc
+ docc.name = fmt.Sprintf("doc%d", i)
+ idx.Add(&docc)
+ }
+
+ m, err := idx.SearchDoc(doc, "keywords")
+ assert.NoError(err)
+ assert.Len(m, 10)
+ for i := 0; i < 10; i++ {
+ assert.Equal(fmt.Sprintf("doc%d", i), m[i].Name())
+ }
+ })
+
+}
+
+func BenchmarkRelatedNewIndex(b *testing.B) {
+
+ pages := make([]*testDoc, 100)
+ numkeywords := 30
+ allKeywords := make([]string, numkeywords)
+ for i := 0; i < numkeywords; i++ {
+ allKeywords[i] = fmt.Sprintf("keyword%d", i+1)
+ }
+
+ for i := 0; i < len(pages); i++ {
+ start := rand.Intn(len(allKeywords))
+ end := start + 3
+ if end >= len(allKeywords) {
+ end = start + 1
+ }
+
+ kw := newTestDoc("tags", allKeywords[start:end]...)
+ if i%5 == 0 {
+ start := rand.Intn(len(allKeywords))
+ end := start + 3
+ if end >= len(allKeywords) {
+ end = start + 1
+ }
+ kw.addKeywords("keywords", allKeywords[start:end]...)
+ }
+
+ pages[i] = kw
+ }
+
+ cfg := Config{
+ Threshold: 50,
+ Indices: IndexConfigs{
+ IndexConfig{Name: "tags", Weight: 100},
+ IndexConfig{Name: "keywords", Weight: 200},
+ },
+ }
+
+ b.Run("singles", func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ idx := NewInvertedIndex(cfg)
+ for _, doc := range pages {
+ idx.Add(doc)
+ }
+ }
+ })
+
+ b.Run("all", func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ idx := NewInvertedIndex(cfg)
+ docs := make([]Document, len(pages))
+ for i := 0; i < len(pages); i++ {
+ docs[i] = pages[i]
+ }
+ idx.Add(docs...)
+ }
+ })
+
+}
+
+func BenchmarkRelatedMatchesIn(b *testing.B) {
+
+ q1 := newQueryElement("tags", StringsToKeywords("keyword2", "keyword5", "keyword32", "asdf")...)
+ q2 := newQueryElement("keywords", StringsToKeywords("keyword3", "keyword4")...)
+
+ docs := make([]*testDoc, 1000)
+ numkeywords := 20
+ allKeywords := make([]string, numkeywords)
+ for i := 0; i < numkeywords; i++ {
+ allKeywords[i] = fmt.Sprintf("keyword%d", i+1)
+ }
+
+ cfg := Config{
+ Threshold: 20,
+ Indices: IndexConfigs{
+ IndexConfig{Name: "tags", Weight: 100},
+ IndexConfig{Name: "keywords", Weight: 200},
+ },
+ }
+
+ idx := NewInvertedIndex(cfg)
+
+ for i := 0; i < len(docs); i++ {
+ start := rand.Intn(len(allKeywords))
+ end := start + 3
+ if end >= len(allKeywords) {
+ end = start + 1
+ }
+
+ index := "tags"
+ if i%5 == 0 {
+ index = "keywords"
+ }
+
+ idx.Add(newTestDoc(index, allKeywords[start:end]...))
+ }
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ if i%10 == 0 {
+ idx.search(q2)
+ } else {
+ idx.search(q1)
+ }
+ }
+}
diff --git a/releaser/git.go b/releaser/git.go
new file mode 100644
index 000000000..9527b5113
--- /dev/null
+++ b/releaser/git.go
@@ -0,0 +1,315 @@
+// 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"
+ otherChanges = "otherChanges"
+)
+
+type changeLog struct {
+ Version string
+ Enhancements map[string]gitInfos
+ Fixes map[string]gitInfos
+ Notes gitInfos
+ All gitInfos
+ Docs gitInfos
+
+ // Overall stats
+ Repo *gitHubRepo
+ ContributorCount int
+ ThemeCount int
+}
+
+func newChangeLog(infos, docInfos gitInfos) *changeLog {
+ return &changeLog{
+ Enhancements: make(map[string]gitInfos),
+ Fixes: make(map[string]gitInfos),
+ All: infos,
+ Docs: docInfos,
+ }
+}
+
+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, docInfos gitInfos) *changeLog {
+ log := newChangeLog(infos, docInfos)
+ 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)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, repo, repoPath string, remote bool) (gitInfos, error) {
+ return getGitInfosBefore("HEAD", tag, repo, repoPath, 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, repo, repoPath string, remote bool) (gitInfos, error) {
+ client := newGitHubAPI(repo)
+ var g gitInfos
+
+ log, err := gitLogBefore(ref, tag, repoPath)
+ 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{}
+
+ if len(items) > 0 {
+ gi.Hash = items[0]
+ }
+ if len(items) > 1 {
+ gi.Author = items[1]
+ }
+ if len(items) > 2 {
+ gi.Subject = items[2]
+ }
+ if len(items) > 3 {
+ gi.Body = items[3]
+ }
+
+ if remote && gi.Hash != "" {
+ gc, err := client.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:|Merge commit|Squashed|Revert"
+
+func gitLogBefore(ref, tag, repoPath string) (string, error) {
+ var prevTag string
+ var err error
+ if tag != "" {
+ prevTag = tag
+ } else {
+ prevTag, err = gitVersionTagBefore(ref)
+ if err != nil {
+ return "", err
+ }
+ }
+
+ defaultArgs := []string{"log", "-E", fmt.Sprintf("--grep=%s", ignoredCommits), "--invert-grep", "--pretty=format:%x1e%h%x1f%aE%x1f%s%x1f%b", "--abbrev-commit", prevTag + ".." + ref}
+
+ var args []string
+
+ if repoPath != "" {
+ args = append([]string{"-C", repoPath}, defaultArgs...)
+ } else {
+ args = defaultArgs
+ }
+
+ log, err := git(args...)
+ 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..f0d6fd24b
--- /dev/null
+++ b/releaser/git_test.go
@@ -0,0 +1,75 @@
+// 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 (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestGitInfos(t *testing.T) {
+ skipIfCI(t)
+ infos, err := getGitInfos("v0.20", "hugo", "", 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 isCI() {
+ // 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..ba019ccad
--- /dev/null
+++ b/releaser/github.go
@@ -0,0 +1,144 @@
+package releaser
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "strings"
+)
+
+var (
+ gitHubCommitsAPI = "https://api.github.com/repos/gohugoio/REPO/commits/%s"
+ gitHubRepoAPI = "https://api.github.com/repos/gohugoio/REPO"
+ gitHubContributorsAPI = "https://api.github.com/repos/gohugoio/REPO/contributors"
+)
+
+type gitHubAPI struct {
+ commitsAPITemplate string
+ repoAPI string
+ contributorsAPITemplate string
+}
+
+func newGitHubAPI(repo string) *gitHubAPI {
+ return &gitHubAPI{
+ commitsAPITemplate: strings.Replace(gitHubCommitsAPI, "REPO", repo, -1),
+ repoAPI: strings.Replace(gitHubRepoAPI, "REPO", repo, -1),
+ contributorsAPITemplate: strings.Replace(gitHubContributorsAPI, "REPO", repo, -1),
+ }
+}
+
+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 (g *gitHubAPI) fetchCommit(ref string) (gitHubCommit, error) {
+ var commit gitHubCommit
+
+ u := fmt.Sprintf(g.commitsAPITemplate, ref)
+
+ req, err := http.NewRequest("GET", u, nil)
+ if err != nil {
+ return commit, err
+ }
+
+ err = doGitHubRequest(req, &commit)
+
+ return commit, err
+}
+
+func (g *gitHubAPI) fetchRepo() (gitHubRepo, error) {
+ var repo gitHubRepo
+
+ req, err := http.NewRequest("GET", g.repoAPI, 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(g.contributorsAPITemplate+"?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..1187cbb2c
--- /dev/null
+++ b/releaser/github_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 releaser
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestGitHubLookupCommit(t *testing.T) {
+ skipIfNoToken(t)
+ client := newGitHubAPI("hugo")
+ commit, err := client.fetchCommit("793554108763c0984f1a1b1a6ee5744b560d78d0")
+ require.NoError(t, err)
+ fmt.Println(commit)
+}
+
+func TestFetchRepo(t *testing.T) {
+ skipIfNoToken(t)
+ client := newGitHubAPI("hugo")
+ repo, err := client.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..15602dc44
--- /dev/null
+++ b/releaser/releasenotes_writer.go
@@ -0,0 +1,315 @@
+// 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 (
+ "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 -}}
+{{- $docsContribsPerAuthor := .Docs.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 a big thanks to [@digitalcraftsman](https://github.com/digitalcraftsman) and [@onedrawingperday](https://github.com/onedrawingperday) for their relentless work on keeping the themes site in pristine condition and to [@kaushalmodi](https://github.com/kaushalmodi) for his great work on the documentation site.
+{{ end }}
+{{- if not $patchRelease }}
+Many have also been busy writing and fixing the documentation in [hugoDocs](https://github.com/gohugoio/hugoDocs),
+which has received **{{ len .Docs }} contributions by {{ len $docsContribsPerAuthor }} contributors**.
+{{- if gt (len $docsContribsPerAuthor) 3 -}}
+{{- $u1 := index $docsContribsPerAuthor 0 -}}
+{{- $u2 := index $docsContribsPerAuthor 1 -}}
+{{- $u3 := index $docsContribsPerAuthor 2 -}}
+{{- $u4 := index $docsContribsPerAuthor 3 }} A special thanks to {{ $u1.AuthorLink }}, {{ $u2.AuthorLink }}, {{ $u3.AuthorLink }}, and {{ $u4.AuthorLink }} for their work on the documentation site.
+{{ end }}
+{{ 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 }}
+* {{ . }}+ [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" -}}
+{{- $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 $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.HasSuffix(c.Version, "0")
+ },
+ "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, infosMain, infosDocs gitInfos, to io.Writer) error {
+ client := newGitHubAPI("hugo")
+ changes := gitInfosToChangeLog(infosMain, infosDocs)
+ changes.Version = version
+ repo, err := client.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, infosMain, infosDocs gitInfos) (string, error) {
+ f, err := ioutil.TempFile("", "hugorelease")
+ if err != nil {
+ return "", err
+ }
+
+ defer f.Close()
+
+ if err := writeReleaseNotes(version, infosMain, infosDocs, f); err != nil {
+ return "", err
+ }
+
+ return f.Name(), nil
+}
+
+func getReleaseNotesDocsTempDirAndName(version string, final bool) (string, string) {
+ if final {
+ return hugoFilepath("temp"), fmt.Sprintf("%s-relnotes-ready.md", version)
+ }
+ return hugoFilepath("temp"), fmt.Sprintf("%s-relnotes.md", version)
+}
+
+func getReleaseNotesDocsTempFilename(version string, final bool) string {
+ return filepath.Join(getReleaseNotesDocsTempDirAndName(version, final))
+}
+
+func (r *ReleaseHandler) releaseNotesState(version string) (releaseNotesState, error) {
+ docsTempPath, name := getReleaseNotesDocsTempDirAndName(version, false)
+ _, err := os.Stat(filepath.Join(docsTempPath, name))
+
+ if err == nil {
+ return releaseNotesCreated, nil
+ }
+
+ docsTempPath, name = getReleaseNotesDocsTempDirAndName(version, true)
+ _, err = os.Stat(filepath.Join(docsTempPath, name))
+
+ if err == nil {
+ return releaseNotesReady, nil
+ }
+
+ if !os.IsNotExist(err) {
+ return releaseNotesNone, err
+ }
+
+ return releaseNotesNone, nil
+
+}
+
+func (r *ReleaseHandler) writeReleaseNotesToTemp(version string, infosMain, infosDocs gitInfos) (string, error) {
+
+ docsTempPath, name := getReleaseNotesDocsTempDirAndName(version, false)
+
+ var (
+ w io.WriteCloser
+ )
+
+ if !r.try {
+ os.Mkdir(docsTempPath, os.ModePerm)
+
+ f, err := os.Create(filepath.Join(docsTempPath, name))
+ if err != nil {
+ return "", err
+ }
+
+ name = f.Name()
+
+ defer f.Close()
+
+ w = f
+
+ } else {
+ w = os.Stdout
+ }
+
+ if err := writeReleaseNotes(version, infosMain, infosDocs, w); err != nil {
+ return "", err
+ }
+
+ return name, nil
+
+}
+
+func (r *ReleaseHandler) writeReleaseNotesToDocs(title, sourceFilename string) (string, error) {
+ targetFilename := "index.md"
+ bundleDir := strings.TrimSuffix(filepath.Base(sourceFilename), "-ready.md")
+ contentDir := hugoFilepath("docs/content/en/news/" + bundleDir)
+ targetFullFilename := filepath.Join(contentDir, targetFilename)
+
+ if r.try {
+ fmt.Printf("Write release notes to /docs: Bundle %q Dir: %q\n", bundleDir, contentDir)
+ return targetFullFilename, nil
+ }
+
+ if err := os.MkdirAll(contentDir, os.ModePerm); err != nil {
+ return "", nil
+ }
+
+ b, err := ioutil.ReadFile(sourceFilename)
+ if err != nil {
+ return "", err
+ }
+
+ f, err := os.Create(targetFullFilename)
+ if err != nil {
+ return "", err
+ }
+ defer f.Close()
+
+ fmTail := ""
+ if !strings.HasSuffix(title, ".0") {
+ // Bug fix release
+ fmTail = `
+images:
+- images/blog/hugo-bug-poster.png
+`
+ }
+
+ if _, err := f.WriteString(fmt.Sprintf(`
+---
+date: %s
+title: %q
+description: %q
+categories: ["Releases"]%s
+---
+
+ `, time.Now().Format("2006-01-02"), title, title, fmTail)); 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..f5b7a87d3
--- /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", "hugo", "", false)
+ require.NoError(t, err)
+
+ require.NoError(t, writeReleaseNotes("0.21", infos, infos, &b))
+
+ fmt.Println(b.String())
+
+}
diff --git a/releaser/releaser.go b/releaser/releaser.go
new file mode 100644
index 000000000..5430e1f8b
--- /dev/null
+++ b/releaser/releaser.go
@@ -0,0 +1,340 @@
+// 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 (
+ "fmt"
+ "io/ioutil"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/gohugoio/hugo/common/hugo"
+ "github.com/pkg/errors"
+)
+
+const commitPrefix = "releaser:"
+
+type releaseNotesState int
+
+const (
+ releaseNotesNone = iota
+ releaseNotesCreated
+ releaseNotesReady
+)
+
+// ReleaseHandler provides functionality to release a new version of Hugo.
+type ReleaseHandler struct {
+ cliVersion string
+
+ skipPublish bool
+
+ // Just simulate, no actual changes.
+ try bool
+
+ git func(args ...string) (string, error)
+}
+
+func (r ReleaseHandler) calculateVersions() (hugo.Version, hugo.Version) {
+ newVersion := hugo.MustParseVersion(r.cliVersion)
+ finalVersion := newVersion.Next()
+ finalVersion.PatchLevel = 0
+
+ if newVersion.Suffix != "-test" {
+ newVersion.Suffix = ""
+ }
+
+ finalVersion.Suffix = "-DEV"
+
+ return newVersion, finalVersion
+}
+
+// New initialises a ReleaseHandler.
+func New(version string, skipPublish, try bool) *ReleaseHandler {
+ // When triggered from CI release branch
+ version = strings.TrimPrefix(version, "release-")
+ version = strings.TrimPrefix(version, "v")
+ rh := &ReleaseHandler{cliVersion: version, skipPublish: skipPublish, try: try}
+
+ if try {
+ rh.git = func(args ...string) (string, error) {
+ fmt.Println("git", strings.Join(args, " "))
+ return "", nil
+ }
+ } else {
+ rh.git = git
+ }
+
+ return rh
+}
+
+// Run creates a new release.
+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()
+
+ 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 between, 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
+ gitCommitsDocs gitInfos
+ relNotesState releaseNotesState
+ )
+
+ relNotesState, err = r.releaseNotesState(version)
+ if err != nil {
+ return err
+ }
+
+ prepareRelaseNotes := relNotesState == releaseNotesNone
+ shouldRelease := relNotesState == releaseNotesReady
+
+ defer r.gitPush() // TODO(bep)
+
+ if prepareRelaseNotes || shouldRelease {
+ gitCommits, err = getGitInfos(changeLogFromTag, "hugo", "", !r.try)
+ if err != nil {
+ return err
+ }
+
+ // TODO(bep) explicit tag?
+ gitCommitsDocs, err = getGitInfos("", "hugoDocs", "../hugoDocs", !r.try)
+ if err != nil {
+ return err
+ }
+ }
+
+ if relNotesState == releaseNotesCreated {
+ fmt.Println("Release notes created, but not ready. Reneame to *-ready.md to continue ...")
+ return nil
+ }
+
+ if prepareRelaseNotes {
+ releaseNotesFile, err := r.writeReleaseNotesToTemp(version, gitCommits, gitCommitsDocs)
+ if err != nil {
+ return err
+ }
+
+ if _, err := r.git("add", releaseNotesFile); err != nil {
+ return err
+ }
+ if _, err := r.git("commit", "-m", fmt.Sprintf("%s Add release notes draft for %s\n\nRename to *-ready.md to continue. [ci skip]", commitPrefix, newVersion)); err != nil {
+ return err
+ }
+ }
+
+ if !shouldRelease {
+ fmt.Printf("Skip release ... ")
+ return nil
+ }
+
+ // For docs, for now we assume that:
+ // The /docs subtree is up to date and ready to go.
+ // The hugoDocs/dev and hugoDocs/master must be merged manually after release.
+ // TODO(bep) improve this when we see how it works.
+
+ if err := r.bumpVersions(newVersion); err != nil {
+ return err
+ }
+
+ if _, err := r.git("commit", "-a", "-m", fmt.Sprintf("%s Bump versions for release of %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil {
+ return err
+ }
+
+ releaseNotesFile := getReleaseNotesDocsTempFilename(version, true)
+
+ // Write the release notes to the docs site as well.
+ docFile, err := r.writeReleaseNotesToDocs(version, releaseNotesFile)
+ if err != nil {
+ return err
+ }
+
+ if _, err := r.git("add", docFile); err != nil {
+ return err
+ }
+ if _, err := r.git("commit", "-m", fmt.Sprintf("%s Add release notes to /docs for release of %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil {
+ return err
+ }
+
+ if _, err := r.git("tag", "-a", tag, "-m", fmt.Sprintf("%s %s [ci skip]", commitPrefix, newVersion)); err != nil {
+ return err
+ }
+
+ if !r.skipPublish {
+ if _, err := r.git("push", "origin", tag); err != nil {
+ return err
+ }
+ }
+
+ if err := r.release(releaseNotesFile); err != nil {
+ return err
+ }
+
+ if err := r.bumpVersions(finalVersion); err != nil {
+ return err
+ }
+
+ if !r.try {
+ // No longer needed.
+ if err := os.Remove(releaseNotesFile); err != nil {
+ return err
+ }
+ }
+
+ if _, err := r.git("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) gitPush() {
+ if r.skipPublish {
+ return
+ }
+ if _, err := r.git("push", "origin", "HEAD"); err != nil {
+ log.Fatal("push failed:", err)
+ }
+}
+
+func (r *ReleaseHandler) release(releaseNotesFile string) error {
+ if r.try {
+ fmt.Println("Skip goreleaser...")
+ return nil
+ }
+
+ args := []string{"--rm-dist", "--release-notes", releaseNotesFile}
+ if r.skipPublish {
+ args = append(args, "--skip-publish")
+ }
+
+ cmd := exec.Command("goreleaser", args...)
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ err := cmd.Run()
+ if err != nil {
+ return errors.Wrap(err, "goreleaser failed")
+ }
+ return nil
+}
+
+func (r *ReleaseHandler) bumpVersions(ver hugo.Version) error {
+ toDev := ""
+
+ if ver.Suffix != "" {
+ toDev = ver.Suffix
+ }
+
+ if err := r.replaceInFile("common/hugo/version_current.go",
+ `Number:(\s{4,})(.*),`, fmt.Sprintf(`Number:${1}%.2f,`, ver.Number),
+ `PatchLevel:(\s*)(.*),`, fmt.Sprintf(`PatchLevel:${1}%d,`, ver.PatchLevel),
+ `Suffix:(\s{4,})".*",`, fmt.Sprintf(`Suffix:${1}"%s",`, toDev)); err != nil {
+ return err
+ }
+
+ snapcraftGrade := "stable"
+ if ver.Suffix != "" {
+ snapcraftGrade = "devel"
+ }
+ if err := r.replaceInFile("snap/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 := r.replaceInFile("commands/new.go",
+ `min_version = "(.*)"`, fmt.Sprintf(`min_version = "%s"`, minVersion)); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *ReleaseHandler) replaceInFile(filename string, oldNew ...string) error {
+ fullFilename := hugoFilepath(filename)
+ fi, err := os.Stat(fullFilename)
+ if err != nil {
+ return err
+ }
+
+ if r.try {
+ fmt.Printf("Replace in %q: %q\n", filename, oldNew)
+ return nil
+ }
+
+ 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)
+}
+
+func isCI() bool {
+ return os.Getenv("CI") != ""
+}
diff --git a/requirements.txt b/requirements.txt
index e0f2f62df..941f32d5d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,2 @@
Pygments==2.1.3
+docutils==0.12
diff --git a/resources/image.go b/resources/image.go
new file mode 100644
index 000000000..f1aae2996
--- /dev/null
+++ b/resources/image.go
@@ -0,0 +1,596 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resources
+
+import (
+ "errors"
+ "fmt"
+ "image"
+ "image/color"
+ "image/draw"
+ "image/jpeg"
+ "io"
+ "os"
+ "strconv"
+ "strings"
+ "sync"
+
+ "github.com/gohugoio/hugo/resources/resource"
+
+ _errors "github.com/pkg/errors"
+
+ "github.com/disintegration/imaging"
+ "github.com/gohugoio/hugo/common/hugio"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/mitchellh/mapstructure"
+
+ // Blind import for image.Decode
+ _ "image/gif"
+ _ "image/png"
+
+ // Blind import for image.Decode
+ _ "golang.org/x/image/webp"
+)
+
+var (
+ _ resource.Resource = (*Image)(nil)
+ _ resource.Source = (*Image)(nil)
+ _ resource.Cloner = (*Image)(nil)
+)
+
+// Imaging contains default image processing configuration. This will be fetched
+// from site (or language) config.
+type Imaging struct {
+ // Default image quality setting (1-100). Only used for JPEG images.
+ Quality int
+
+ // Resample filter used. See https://github.com/disintegration/imaging
+ ResampleFilter string
+
+ // The anchor used in Fill. Default is "smart", i.e. Smart Crop.
+ Anchor string
+}
+
+const (
+ defaultJPEGQuality = 75
+ defaultResampleFilter = "box"
+)
+
+var (
+ imageFormats = map[string]imaging.Format{
+ ".jpg": imaging.JPEG,
+ ".jpeg": imaging.JPEG,
+ ".png": imaging.PNG,
+ ".tif": imaging.TIFF,
+ ".tiff": imaging.TIFF,
+ ".bmp": imaging.BMP,
+ ".gif": imaging.GIF,
+ }
+
+ // Add or increment if changes to an image format's processing requires
+ // re-generation.
+ imageFormatsVersions = map[imaging.Format]int{
+ imaging.PNG: 2, // Floyd Steinberg dithering
+ }
+
+ // Increment to mark all processed images as stale. Only use when absolutely needed.
+ // See the finer grained smartCropVersionNumber and imageFormatsVersions.
+ mainImageVersionNumber = 0
+)
+
+var anchorPositions = map[string]imaging.Anchor{
+ strings.ToLower("Center"): imaging.Center,
+ strings.ToLower("TopLeft"): imaging.TopLeft,
+ strings.ToLower("Top"): imaging.Top,
+ strings.ToLower("TopRight"): imaging.TopRight,
+ strings.ToLower("Left"): imaging.Left,
+ strings.ToLower("Right"): imaging.Right,
+ strings.ToLower("BottomLeft"): imaging.BottomLeft,
+ strings.ToLower("Bottom"): imaging.Bottom,
+ strings.ToLower("BottomRight"): imaging.BottomRight,
+}
+
+var imageFilters = map[string]imaging.ResampleFilter{
+ strings.ToLower("NearestNeighbor"): imaging.NearestNeighbor,
+ strings.ToLower("Box"): imaging.Box,
+ strings.ToLower("Linear"): imaging.Linear,
+ strings.ToLower("Hermite"): imaging.Hermite,
+ strings.ToLower("MitchellNetravali"): imaging.MitchellNetravali,
+ strings.ToLower("CatmullRom"): imaging.CatmullRom,
+ strings.ToLower("BSpline"): imaging.BSpline,
+ strings.ToLower("Gaussian"): imaging.Gaussian,
+ strings.ToLower("Lanczos"): imaging.Lanczos,
+ strings.ToLower("Hann"): imaging.Hann,
+ strings.ToLower("Hamming"): imaging.Hamming,
+ strings.ToLower("Blackman"): imaging.Blackman,
+ strings.ToLower("Bartlett"): imaging.Bartlett,
+ strings.ToLower("Welch"): imaging.Welch,
+ strings.ToLower("Cosine"): imaging.Cosine,
+}
+
+// Image represents an image resource.
+type Image struct {
+ config image.Config
+ configInit sync.Once
+ configLoaded bool
+
+ imaging *Imaging
+
+ format imaging.Format
+
+ *genericResource
+}
+
+// Width returns i's width.
+func (i *Image) Width() int {
+ i.initConfig()
+ return i.config.Width
+}
+
+// Height returns i's height.
+func (i *Image) Height() int {
+ i.initConfig()
+ return i.config.Height
+}
+
+// WithNewBase implements the Cloner interface.
+func (i *Image) WithNewBase(base string) resource.Resource {
+ return &Image{
+ imaging: i.imaging,
+ format: i.format,
+ genericResource: i.genericResource.WithNewBase(base).(*genericResource)}
+}
+
+// Resize resizes the image to the specified width and height using the specified resampling
+// filter and returns the transformed image. If one of width or height is 0, the image aspect
+// ratio is preserved.
+func (i *Image) Resize(spec string) (*Image, error) {
+ return i.doWithImageConfig("resize", spec, func(src image.Image, conf imageConfig) (image.Image, error) {
+ return imaging.Resize(src, conf.Width, conf.Height, conf.Filter), nil
+ })
+}
+
+// Fit scales down the image using the specified resample filter to fit the specified
+// maximum width and height.
+func (i *Image) Fit(spec string) (*Image, error) {
+ return i.doWithImageConfig("fit", spec, func(src image.Image, conf imageConfig) (image.Image, error) {
+ return imaging.Fit(src, conf.Width, conf.Height, conf.Filter), nil
+ })
+}
+
+// Fill scales the image to the smallest possible size that will cover the specified dimensions,
+// crops the resized image to the specified dimensions using the given anchor point.
+// Space delimited config: 200x300 TopLeft
+func (i *Image) Fill(spec string) (*Image, error) {
+ return i.doWithImageConfig("fill", spec, func(src image.Image, conf imageConfig) (image.Image, error) {
+ if conf.AnchorStr == smartCropIdentifier {
+ return smartCrop(src, conf.Width, conf.Height, conf.Anchor, conf.Filter)
+ }
+ return imaging.Fill(src, conf.Width, conf.Height, conf.Anchor, conf.Filter), nil
+ })
+}
+
+// Holds configuration to create a new image from an existing one, resize etc.
+type imageConfig struct {
+ Action string
+
+ // Quality ranges from 1 to 100 inclusive, higher is better.
+ // This is only relevant for JPEG images.
+ // Default is 75.
+ Quality int
+
+ // Rotate rotates an image by the given angle counter-clockwise.
+ // The rotation will be performed first.
+ Rotate int
+
+ Width int
+ Height int
+
+ Filter imaging.ResampleFilter
+ FilterStr string
+
+ Anchor imaging.Anchor
+ AnchorStr string
+}
+
+func (i *Image) isJPEG() bool {
+ name := strings.ToLower(i.relTargetDirFile.file)
+ return strings.HasSuffix(name, ".jpg") || strings.HasSuffix(name, ".jpeg")
+}
+
+// Serialize image processing. The imaging library spins up its own set of Go routines,
+// so there is not much to gain from adding more load to the mix. That
+// can even have negative effect in low resource scenarios.
+// Note that this only effects the non-cached scenario. Once the processed
+// image is written to disk, everything is fast, fast fast.
+const imageProcWorkers = 1
+
+var imageProcSem = make(chan bool, imageProcWorkers)
+
+func (i *Image) doWithImageConfig(action, spec string, f func(src image.Image, conf imageConfig) (image.Image, error)) (*Image, error) {
+ conf, err := parseImageConfig(spec)
+ if err != nil {
+ return nil, err
+ }
+ conf.Action = action
+
+ if conf.Quality <= 0 && i.isJPEG() {
+ // We need a quality setting for all JPEGs
+ conf.Quality = i.imaging.Quality
+ }
+
+ if conf.FilterStr == "" {
+ conf.FilterStr = i.imaging.ResampleFilter
+ conf.Filter = imageFilters[conf.FilterStr]
+ }
+
+ if conf.AnchorStr == "" {
+ conf.AnchorStr = i.imaging.Anchor
+ if !strings.EqualFold(conf.AnchorStr, smartCropIdentifier) {
+ conf.Anchor = anchorPositions[conf.AnchorStr]
+ }
+ }
+
+ return i.spec.imageCache.getOrCreate(i, conf, func() (*Image, image.Image, error) {
+ imageProcSem <- true
+ defer func() {
+ <-imageProcSem
+ }()
+
+ ci := i.clone()
+
+ errOp := action
+ errPath := i.sourceFilename
+
+ ci.setBasePath(conf)
+
+ src, err := i.decodeSource()
+ if err != nil {
+ return nil, nil, &os.PathError{Op: errOp, Path: errPath, Err: err}
+ }
+
+ if conf.Rotate != 0 {
+ // Rotate it before any scaling to get the dimensions correct.
+ src = imaging.Rotate(src, float64(conf.Rotate), color.Transparent)
+ }
+
+ converted, err := f(src, conf)
+ if err != nil {
+ return ci, nil, &os.PathError{Op: errOp, Path: errPath, Err: err}
+ }
+
+ if i.format == imaging.PNG {
+ // Apply the colour palette from the source
+ if paletted, ok := src.(*image.Paletted); ok {
+ tmp := image.NewPaletted(converted.Bounds(), paletted.Palette)
+ draw.FloydSteinberg.Draw(tmp, tmp.Bounds(), converted, converted.Bounds().Min)
+ converted = tmp
+ }
+ }
+
+ b := converted.Bounds()
+ ci.config = image.Config{Width: b.Max.X, Height: b.Max.Y}
+ ci.configLoaded = true
+
+ return ci, converted, nil
+ })
+
+}
+
+func (i imageConfig) key(format imaging.Format) string {
+ k := strconv.Itoa(i.Width) + "x" + strconv.Itoa(i.Height)
+ if i.Action != "" {
+ k += "_" + i.Action
+ }
+ if i.Quality > 0 {
+ k += "_q" + strconv.Itoa(i.Quality)
+ }
+ if i.Rotate != 0 {
+ k += "_r" + strconv.Itoa(i.Rotate)
+ }
+ anchor := i.AnchorStr
+ if anchor == smartCropIdentifier {
+ anchor = anchor + strconv.Itoa(smartCropVersionNumber)
+ }
+
+ k += "_" + i.FilterStr
+
+ if strings.EqualFold(i.Action, "fill") {
+ k += "_" + anchor
+ }
+
+ if v, ok := imageFormatsVersions[format]; ok {
+ k += "_" + strconv.Itoa(v)
+ }
+
+ if mainImageVersionNumber > 0 {
+ k += "_" + strconv.Itoa(mainImageVersionNumber)
+ }
+
+ return k
+}
+
+func newImageConfig(width, height, quality, rotate int, filter, anchor string) imageConfig {
+ var c imageConfig
+
+ c.Width = width
+ c.Height = height
+ c.Quality = quality
+ c.Rotate = rotate
+
+ if filter != "" {
+ filter = strings.ToLower(filter)
+ if v, ok := imageFilters[filter]; ok {
+ c.Filter = v
+ c.FilterStr = filter
+ }
+ }
+
+ if anchor != "" {
+ anchor = strings.ToLower(anchor)
+ if v, ok := anchorPositions[anchor]; ok {
+ c.Anchor = v
+ c.AnchorStr = anchor
+ }
+ }
+
+ return c
+}
+
+func parseImageConfig(config string) (imageConfig, error) {
+ var (
+ c imageConfig
+ err error
+ )
+
+ if config == "" {
+ return c, errors.New("image config cannot be empty")
+ }
+
+ parts := strings.Fields(config)
+ for _, part := range parts {
+ part = strings.ToLower(part)
+
+ if part == smartCropIdentifier {
+ c.AnchorStr = smartCropIdentifier
+ } else if pos, ok := anchorPositions[part]; ok {
+ c.Anchor = pos
+ c.AnchorStr = part
+ } else if filter, ok := imageFilters[part]; ok {
+ c.Filter = filter
+ c.FilterStr = part
+ } else if part[0] == 'q' {
+ c.Quality, err = strconv.Atoi(part[1:])
+ if err != nil {
+ return c, err
+ }
+ if c.Quality < 1 || c.Quality > 100 {
+ return c, errors.New("quality ranges from 1 to 100 inclusive")
+ }
+ } else if part[0] == 'r' {
+ c.Rotate, err = strconv.Atoi(part[1:])
+ if err != nil {
+ return c, err
+ }
+ } else if strings.Contains(part, "x") {
+ widthHeight := strings.Split(part, "x")
+ if len(widthHeight) <= 2 {
+ first := widthHeight[0]
+ if first != "" {
+ c.Width, err = strconv.Atoi(first)
+ if err != nil {
+ return c, err
+ }
+ }
+
+ if len(widthHeight) == 2 {
+ second := widthHeight[1]
+ if second != "" {
+ c.Height, err = strconv.Atoi(second)
+ if err != nil {
+ return c, err
+ }
+ }
+ }
+ } else {
+ return c, errors.New("invalid image dimensions")
+ }
+
+ }
+ }
+
+ if c.Width == 0 && c.Height == 0 {
+ return c, errors.New("must provide Width or Height")
+ }
+
+ return c, nil
+}
+
+func (i *Image) initConfig() error {
+ var err error
+ i.configInit.Do(func() {
+ if i.configLoaded {
+ return
+ }
+
+ var (
+ f hugio.ReadSeekCloser
+ config image.Config
+ )
+
+ f, err = i.ReadSeekCloser()
+ if err != nil {
+ return
+ }
+ defer f.Close()
+
+ config, _, err = image.DecodeConfig(f)
+ if err != nil {
+ return
+ }
+ i.config = config
+ })
+
+ if err != nil {
+ return _errors.Wrap(err, "failed to load image config")
+ }
+
+ return nil
+}
+
+func (i *Image) decodeSource() (image.Image, error) {
+ f, err := i.ReadSeekCloser()
+ if err != nil {
+ return nil, _errors.Wrap(err, "failed to open image for decode")
+ }
+ defer f.Close()
+ img, _, err := image.Decode(f)
+ return img, err
+}
+
+// returns an opened file or nil if nothing to write.
+func (i *Image) openDestinationsForWriting() (io.WriteCloser, error) {
+ targetFilenames := i.targetFilenames()
+ var changedFilenames []string
+
+ // Fast path:
+ // This is a processed version of the original;
+ // check if it already existis at the destination.
+ for _, targetFilename := range targetFilenames {
+ if _, err := i.spec.BaseFs.PublishFs.Stat(targetFilename); err == nil {
+ continue
+ }
+ changedFilenames = append(changedFilenames, targetFilename)
+ }
+
+ if len(changedFilenames) == 0 {
+ return nil, nil
+ }
+
+ return helpers.OpenFilesForWriting(i.spec.BaseFs.PublishFs, changedFilenames...)
+
+}
+
+func (i *Image) encodeTo(conf imageConfig, img image.Image, w io.Writer) error {
+ switch i.format {
+ case imaging.JPEG:
+
+ var rgba *image.RGBA
+ quality := conf.Quality
+
+ if nrgba, ok := img.(*image.NRGBA); ok {
+ if nrgba.Opaque() {
+ rgba = &image.RGBA{
+ Pix: nrgba.Pix,
+ Stride: nrgba.Stride,
+ Rect: nrgba.Rect,
+ }
+ }
+ }
+ if rgba != nil {
+ return jpeg.Encode(w, rgba, &jpeg.Options{Quality: quality})
+ }
+ return jpeg.Encode(w, img, &jpeg.Options{Quality: quality})
+ default:
+ return imaging.Encode(w, img, i.format)
+ }
+}
+
+func (i *Image) clone() *Image {
+ g := *i.genericResource
+ g.resourceContent = &resourceContent{}
+ if g.publishOnce != nil {
+ g.publishOnce = &publishOnce{logger: g.publishOnce.logger}
+ }
+
+ return &Image{
+ imaging: i.imaging,
+ format: i.format,
+ genericResource: &g}
+}
+
+func (i *Image) setBasePath(conf imageConfig) {
+ i.relTargetDirFile = i.relTargetPathFromConfig(conf)
+}
+
+func (i *Image) relTargetPathFromConfig(conf imageConfig) dirFile {
+ p1, p2 := helpers.FileAndExt(i.relTargetDirFile.file)
+
+ idStr := fmt.Sprintf("_hu%s_%d", i.hash, i.osFileInfo.Size())
+
+ // Do not change for no good reason.
+ const md5Threshold = 100
+
+ key := conf.key(i.format)
+
+ // It is useful to have the key in clear text, but when nesting transforms, it
+ // can easily be too long to read, and maybe even too long
+ // for the different OSes to handle.
+ if len(p1)+len(idStr)+len(p2) > md5Threshold {
+ key = helpers.MD5String(p1 + key + p2)
+ huIdx := strings.Index(p1, "_hu")
+ if huIdx != -1 {
+ p1 = p1[:huIdx]
+ } else {
+ // This started out as a very long file name. Making it even longer
+ // could melt ice in the Arctic.
+ p1 = ""
+ }
+ } else if strings.Contains(p1, idStr) {
+ // On scaling an already scaled image, we get the file info from the original.
+ // Repeating the same info in the filename makes it stuttery for no good reason.
+ idStr = ""
+ }
+
+ return dirFile{
+ dir: i.relTargetDirFile.dir,
+ file: fmt.Sprintf("%s%s_%s%s", p1, idStr, key, p2),
+ }
+
+}
+
+func decodeImaging(m map[string]interface{}) (Imaging, error) {
+ var i Imaging
+ if err := mapstructure.WeakDecode(m, &i); err != nil {
+ return i, err
+ }
+
+ if i.Quality == 0 {
+ i.Quality = defaultJPEGQuality
+ } else if i.Quality < 0 || i.Quality > 100 {
+ return i, errors.New("JPEG quality must be a number between 1 and 100")
+ }
+
+ if i.Anchor == "" || strings.EqualFold(i.Anchor, smartCropIdentifier) {
+ i.Anchor = smartCropIdentifier
+ } else {
+ i.Anchor = strings.ToLower(i.Anchor)
+ if _, found := anchorPositions[i.Anchor]; !found {
+ return i, errors.New("invalid anchor value in imaging config")
+ }
+ }
+
+ if i.ResampleFilter == "" {
+ i.ResampleFilter = defaultResampleFilter
+ } else {
+ filter := strings.ToLower(i.ResampleFilter)
+ _, found := imageFilters[filter]
+ if !found {
+ return i, fmt.Errorf("%q is not a valid resample filter", filter)
+ }
+ i.ResampleFilter = filter
+ }
+
+ return i, nil
+}
diff --git a/resources/image_cache.go b/resources/image_cache.go
new file mode 100644
index 000000000..cf1e999ba
--- /dev/null
+++ b/resources/image_cache.go
@@ -0,0 +1,164 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resources
+
+import (
+ "image"
+ "io"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "github.com/gohugoio/hugo/common/hugio"
+
+ "github.com/gohugoio/hugo/cache/filecache"
+ "github.com/gohugoio/hugo/helpers"
+)
+
+type imageCache struct {
+ pathSpec *helpers.PathSpec
+
+ fileCache *filecache.Cache
+
+ mu sync.RWMutex
+ store map[string]*Image
+}
+
+func (c *imageCache) isInCache(key string) bool {
+ c.mu.RLock()
+ _, found := c.store[c.normalizeKey(key)]
+ c.mu.RUnlock()
+ return found
+}
+
+func (c *imageCache) deleteByPrefix(prefix string) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ prefix = c.normalizeKey(prefix)
+ for k := range c.store {
+ if strings.HasPrefix(k, prefix) {
+ delete(c.store, k)
+ }
+ }
+}
+
+func (c *imageCache) normalizeKey(key string) string {
+ // It is a path with Unix style slashes and it always starts with a leading slash.
+ key = filepath.ToSlash(key)
+ if !strings.HasPrefix(key, "/") {
+ key = "/" + key
+ }
+
+ return key
+}
+
+func (c *imageCache) clear() {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ c.store = make(map[string]*Image)
+}
+
+func (c *imageCache) getOrCreate(
+ parent *Image, conf imageConfig, createImage func() (*Image, image.Image, error)) (*Image, error) {
+
+ relTarget := parent.relTargetPathFromConfig(conf)
+ key := parent.relTargetPathForRel(relTarget.path(), false, false, false)
+
+ // First check the in-memory store, then the disk.
+ c.mu.RLock()
+ img, found := c.store[key]
+ c.mu.RUnlock()
+
+ if found {
+ return img, nil
+ }
+
+ // These funcs are protected by a named lock.
+ // read clones the parent to its new name and copies
+ // the content to the destinations.
+ read := func(info filecache.ItemInfo, r io.Reader) error {
+ img = parent.clone()
+ img.relTargetDirFile.file = relTarget.file
+ img.sourceFilename = info.Name
+
+ w, err := img.openDestinationsForWriting()
+ if err != nil {
+ return err
+ }
+
+ if w == nil {
+ // Nothing to write.
+ return nil
+ }
+
+ defer w.Close()
+ _, err = io.Copy(w, r)
+ return err
+ }
+
+ // create creates the image and encodes it to w (cache) and to its destinations.
+ create := func(info filecache.ItemInfo, w io.WriteCloser) (err error) {
+ var conv image.Image
+ img, conv, err = createImage()
+ if err != nil {
+ w.Close()
+ return
+ }
+ img.relTargetDirFile.file = relTarget.file
+ img.sourceFilename = info.Name
+
+ destinations, err := img.openDestinationsForWriting()
+ if err != nil {
+ w.Close()
+ return err
+ }
+
+ if destinations != nil {
+ w = hugio.NewMultiWriteCloser(w, destinations)
+ }
+ defer w.Close()
+
+ return img.encodeTo(conf, conv, w)
+ }
+
+ // Now look in the file cache.
+
+ // The definition of this counter is not that we have processed that amount
+ // (e.g. resized etc.), it can be fetched from file cache,
+ // but the count of processed image variations for this site.
+ c.pathSpec.ProcessingStats.Incr(&c.pathSpec.ProcessingStats.ProcessedImages)
+
+ _, err := c.fileCache.ReadOrCreate(key, read, create)
+ if err != nil {
+ return nil, err
+ }
+
+ // The file is now stored in this cache.
+ img.overriddenSourceFs = c.fileCache.Fs
+
+ c.mu.Lock()
+ if img2, found := c.store[key]; found {
+ c.mu.Unlock()
+ return img2, nil
+ }
+ c.store[key] = img
+ c.mu.Unlock()
+
+ return img, nil
+
+}
+
+func newImageCache(fileCache *filecache.Cache, ps *helpers.PathSpec) *imageCache {
+ return &imageCache{fileCache: fileCache, pathSpec: ps, store: make(map[string]*Image)}
+}
diff --git a/resources/image_test.go b/resources/image_test.go
new file mode 100644
index 000000000..6639dbb24
--- /dev/null
+++ b/resources/image_test.go
@@ -0,0 +1,400 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resources
+
+import (
+ "fmt"
+ "math/rand"
+ "path/filepath"
+ "strconv"
+ "testing"
+
+ "github.com/disintegration/imaging"
+
+ "sync"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestParseImageConfig(t *testing.T) {
+ for i, this := range []struct {
+ in string
+ expect interface{}
+ }{
+ {"300x400", newImageConfig(300, 400, 0, 0, "", "")},
+ {"100x200 bottomRight", newImageConfig(100, 200, 0, 0, "", "BottomRight")},
+ {"10x20 topleft Lanczos", newImageConfig(10, 20, 0, 0, "Lanczos", "topleft")},
+ {"linear left 10x r180", newImageConfig(10, 0, 0, 180, "linear", "left")},
+ {"x20 riGht Cosine q95", newImageConfig(0, 20, 95, 0, "cosine", "right")},
+
+ {"", false},
+ {"foo", false},
+ } {
+ result, err := parseImageConfig(this.in)
+ if b, ok := this.expect.(bool); ok && !b {
+ if err == nil {
+ t.Errorf("[%d] parseImageConfig didn't return an expected error", i)
+ }
+ } else {
+ if err != nil {
+ t.Fatalf("[%d] err: %s", i, err)
+ }
+ if fmt.Sprint(result) != fmt.Sprint(this.expect) {
+ t.Fatalf("[%d] got\n%v\n but expected\n%v", i, result, this.expect)
+ }
+ }
+ }
+}
+
+func TestImageTransformBasic(t *testing.T) {
+
+ assert := require.New(t)
+
+ image := fetchSunset(assert)
+ fileCache := image.spec.FileCaches.ImageCache().Fs
+
+ assert.Equal("/a/sunset.jpg", image.RelPermalink())
+ assert.Equal("image", image.ResourceType())
+
+ resized, err := image.Resize("300x200")
+ assert.NoError(err)
+ assert.True(image != resized)
+ assert.True(image.genericResource != resized.genericResource)
+ assert.True(image.sourceFilename != resized.sourceFilename)
+
+ resized0x, err := image.Resize("x200")
+ assert.NoError(err)
+ assert.Equal(320, resized0x.Width())
+ assert.Equal(200, resized0x.Height())
+
+ assertFileCache(assert, fileCache, resized0x.RelPermalink(), 320, 200)
+
+ resizedx0, err := image.Resize("200x")
+ assert.NoError(err)
+ assert.Equal(200, resizedx0.Width())
+ assert.Equal(125, resizedx0.Height())
+ assertFileCache(assert, fileCache, resizedx0.RelPermalink(), 200, 125)
+
+ resizedAndRotated, err := image.Resize("x200 r90")
+ assert.NoError(err)
+ assert.Equal(125, resizedAndRotated.Width())
+ assert.Equal(200, resizedAndRotated.Height())
+ assertFileCache(assert, fileCache, resizedAndRotated.RelPermalink(), 125, 200)
+
+ assert.Equal("/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x200_resize_q68_linear.jpg", resized.RelPermalink())
+ assert.Equal(300, resized.Width())
+ assert.Equal(200, resized.Height())
+
+ fitted, err := resized.Fit("50x50")
+ assert.NoError(err)
+ assert.Equal("/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_625708021e2bb281c9f1002f88e4753f.jpg", fitted.RelPermalink())
+ assert.Equal(50, fitted.Width())
+ assert.Equal(33, fitted.Height())
+
+ // Check the MD5 key threshold
+ fittedAgain, _ := fitted.Fit("10x20")
+ fittedAgain, err = fittedAgain.Fit("10x20")
+ assert.NoError(err)
+ assert.Equal("/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_3f65ba24dc2b7fba0f56d7f104519157.jpg", fittedAgain.RelPermalink())
+ assert.Equal(10, fittedAgain.Width())
+ assert.Equal(6, fittedAgain.Height())
+
+ filled, err := image.Fill("200x100 bottomLeft")
+ assert.NoError(err)
+ assert.Equal("/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x100_fill_q68_linear_bottomleft.jpg", filled.RelPermalink())
+ assert.Equal(200, filled.Width())
+ assert.Equal(100, filled.Height())
+ assertFileCache(assert, fileCache, filled.RelPermalink(), 200, 100)
+
+ smart, err := image.Fill("200x100 smart")
+ assert.NoError(err)
+ assert.Equal(fmt.Sprintf("/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x100_fill_q68_linear_smart%d.jpg", smartCropVersionNumber), smart.RelPermalink())
+ assert.Equal(200, smart.Width())
+ assert.Equal(100, smart.Height())
+ assertFileCache(assert, fileCache, smart.RelPermalink(), 200, 100)
+
+ // Check cache
+ filledAgain, err := image.Fill("200x100 bottomLeft")
+ assert.NoError(err)
+ assert.True(filled == filledAgain)
+ assert.True(filled.sourceFilename == filledAgain.sourceFilename)
+ assertFileCache(assert, fileCache, filledAgain.RelPermalink(), 200, 100)
+
+}
+
+// https://github.com/gohugoio/hugo/issues/4261
+func TestImageTransformLongFilename(t *testing.T) {
+ assert := require.New(t)
+
+ image := fetchImage(assert, "1234567890qwertyuiopasdfghjklzxcvbnm5to6eeeeee7via8eleph.jpg")
+ assert.NotNil(image)
+
+ resized, err := image.Resize("200x")
+ assert.NoError(err)
+ assert.NotNil(resized)
+ assert.Equal(200, resized.Width())
+ assert.Equal("/a/_hu59e56ffff1bc1d8d122b1403d34e039f_90587_65b757a6e14debeae720fe8831f0a9bc.jpg", resized.RelPermalink())
+ resized, err = resized.Resize("100x")
+ assert.NoError(err)
+ assert.NotNil(resized)
+ assert.Equal(100, resized.Width())
+ assert.Equal("/a/_hu59e56ffff1bc1d8d122b1403d34e039f_90587_c876768085288f41211f768147ba2647.jpg", resized.RelPermalink())
+}
+
+// https://github.com/gohugoio/hugo/issues/5730
+func TestImagePermalinkPublishOrder(t *testing.T) {
+ for _, checkOriginalFirst := range []bool{true, false} {
+ name := "OriginalFirst"
+ if !checkOriginalFirst {
+ name = "ResizedFirst"
+ }
+
+ t.Run(name, func(t *testing.T) {
+
+ assert := require.New(t)
+ spec := newTestResourceOsFs(assert)
+
+ check1 := func(img *Image) {
+ resizedLink := "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_100x50_resize_q75_box.jpg"
+ assert.Equal(resizedLink, img.RelPermalink())
+ assertImageFile(assert, spec.PublishFs, resizedLink, 100, 50)
+ }
+
+ check2 := func(img *Image) {
+ assert.Equal("/a/sunset.jpg", img.RelPermalink())
+ assertImageFile(assert, spec.PublishFs, "a/sunset.jpg", 900, 562)
+ }
+
+ orignal := fetchImageForSpec(spec, assert, "sunset.jpg")
+ assert.NotNil(orignal)
+
+ if checkOriginalFirst {
+ check2(orignal)
+ }
+
+ resized, err := orignal.Resize("100x50")
+ assert.NoError(err)
+
+ check1(resized)
+
+ if !checkOriginalFirst {
+ check2(orignal)
+ }
+ })
+ }
+
+}
+
+func TestImageTransformConcurrent(t *testing.T) {
+
+ var wg sync.WaitGroup
+
+ assert := require.New(t)
+
+ spec := newTestResourceOsFs(assert)
+
+ image := fetchImageForSpec(spec, assert, "sunset.jpg")
+
+ for i := 0; i < 4; i++ {
+ wg.Add(1)
+ go func(id int) {
+ defer wg.Done()
+ for j := 0; j < 5; j++ {
+ img := image
+ for k := 0; k < 2; k++ {
+ r1, err := img.Resize(fmt.Sprintf("%dx", id-k))
+ if err != nil {
+ t.Error(err)
+ }
+
+ if r1.Width() != id-k {
+ t.Errorf("Width: %d:%d", r1.Width(), j)
+ }
+
+ r2, err := r1.Resize(fmt.Sprintf("%dx", id-k-1))
+ if err != nil {
+ t.Error(err)
+ }
+
+ _, err = r2.decodeSource()
+ if err != nil {
+ t.Error("Err decode:", err)
+ }
+
+ img = r1
+ }
+ }
+ }(i + 20)
+ }
+
+ wg.Wait()
+}
+
+func TestDecodeImaging(t *testing.T) {
+ assert := require.New(t)
+ m := map[string]interface{}{
+ "quality": 42,
+ "resampleFilter": "NearestNeighbor",
+ "anchor": "topLeft",
+ }
+
+ imaging, err := decodeImaging(m)
+
+ assert.NoError(err)
+ assert.Equal(42, imaging.Quality)
+ assert.Equal("nearestneighbor", imaging.ResampleFilter)
+ assert.Equal("topleft", imaging.Anchor)
+
+ m = map[string]interface{}{}
+
+ imaging, err = decodeImaging(m)
+ assert.NoError(err)
+ assert.Equal(defaultJPEGQuality, imaging.Quality)
+ assert.Equal("box", imaging.ResampleFilter)
+ assert.Equal("smart", imaging.Anchor)
+
+ _, err = decodeImaging(map[string]interface{}{
+ "quality": 123,
+ })
+ assert.Error(err)
+
+ _, err = decodeImaging(map[string]interface{}{
+ "resampleFilter": "asdf",
+ })
+ assert.Error(err)
+
+ _, err = decodeImaging(map[string]interface{}{
+ "anchor": "asdf",
+ })
+ assert.Error(err)
+
+ imaging, err = decodeImaging(map[string]interface{}{
+ "anchor": "Smart",
+ })
+ assert.NoError(err)
+ assert.Equal("smart", imaging.Anchor)
+
+}
+
+func TestImageWithMetadata(t *testing.T) {
+ assert := require.New(t)
+
+ image := fetchSunset(assert)
+
+ var meta = []map[string]interface{}{
+ {
+ "title": "My Sunset",
+ "name": "Sunset #:counter",
+ "src": "*.jpg",
+ },
+ }
+
+ assert.NoError(AssignMetadata(meta, image))
+ assert.Equal("Sunset #1", image.Name())
+
+ resized, err := image.Resize("200x")
+ assert.NoError(err)
+ assert.Equal("Sunset #1", resized.Name())
+
+}
+
+func TestImageResize8BitPNG(t *testing.T) {
+
+ assert := require.New(t)
+
+ image := fetchImage(assert, "gohugoio.png")
+
+ assert.Equal(imaging.PNG, image.format)
+ assert.Equal("/a/gohugoio.png", image.RelPermalink())
+ assert.Equal("image", image.ResourceType())
+
+ resized, err := image.Resize("800x")
+ assert.NoError(err)
+ assert.Equal(imaging.PNG, resized.format)
+ assert.Equal("/a/gohugoio_hu0e1b9e4a4be4d6f86c7b37b9ccce3fbc_73886_800x0_resize_linear_2.png", resized.RelPermalink())
+ assert.Equal(800, resized.Width())
+
+}
+
+func TestImageResizeInSubPath(t *testing.T) {
+
+ assert := require.New(t)
+
+ image := fetchImage(assert, "sub/gohugoio2.png")
+ fileCache := image.spec.FileCaches.ImageCache().Fs
+
+ assert.Equal(imaging.PNG, image.format)
+ assert.Equal("/a/sub/gohugoio2.png", image.RelPermalink())
+ assert.Equal("image", image.ResourceType())
+
+ resized, err := image.Resize("101x101")
+ assert.NoError(err)
+ assert.Equal(imaging.PNG, resized.format)
+ assert.Equal("/a/sub/gohugoio2_hu0e1b9e4a4be4d6f86c7b37b9ccce3fbc_73886_101x101_resize_linear_2.png", resized.RelPermalink())
+ assert.Equal(101, resized.Width())
+
+ assertFileCache(assert, fileCache, resized.RelPermalink(), 101, 101)
+ publishedImageFilename := filepath.Clean(resized.RelPermalink())
+ assertImageFile(assert, image.spec.BaseFs.PublishFs, publishedImageFilename, 101, 101)
+ assert.NoError(image.spec.BaseFs.PublishFs.Remove(publishedImageFilename))
+
+ // Cleare mem cache to simulate reading from the file cache.
+ resized.spec.imageCache.clear()
+
+ resizedAgain, err := image.Resize("101x101")
+ assert.NoError(err)
+ assert.Equal("/a/sub/gohugoio2_hu0e1b9e4a4be4d6f86c7b37b9ccce3fbc_73886_101x101_resize_linear_2.png", resizedAgain.RelPermalink())
+ assert.Equal(101, resizedAgain.Width())
+ assertFileCache(assert, fileCache, resizedAgain.RelPermalink(), 101, 101)
+ assertImageFile(assert, image.spec.BaseFs.PublishFs, publishedImageFilename, 101, 101)
+
+}
+
+func TestSVGImage(t *testing.T) {
+ assert := require.New(t)
+ spec := newTestResourceSpec(assert)
+ svg := fetchResourceForSpec(spec, assert, "circle.svg")
+ assert.NotNil(svg)
+}
+
+func TestSVGImageContent(t *testing.T) {
+ assert := require.New(t)
+ spec := newTestResourceSpec(assert)
+ svg := fetchResourceForSpec(spec, assert, "circle.svg")
+ assert.NotNil(svg)
+
+ content, err := svg.Content()
+ assert.NoError(err)
+ assert.IsType("", content)
+ assert.Contains(content.(string), `<svg height="100" width="100">`)
+}
+
+func BenchmarkResizeParallel(b *testing.B) {
+ assert := require.New(b)
+ img := fetchSunset(assert)
+
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ w := rand.Intn(10) + 10
+ resized, err := img.Resize(strconv.Itoa(w) + "x")
+ if err != nil {
+ b.Fatal(err)
+ }
+ _, err = resized.Resize(strconv.Itoa(w-1) + "x")
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+}
diff --git a/resources/internal/glob.go b/resources/internal/glob.go
new file mode 100644
index 000000000..a87a23f13
--- /dev/null
+++ b/resources/internal/glob.go
@@ -0,0 +1,48 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "strings"
+ "sync"
+
+ "github.com/gobwas/glob"
+)
+
+var (
+ globCache = make(map[string]glob.Glob)
+ globMu sync.RWMutex
+)
+
+func GetGlob(pattern string) (glob.Glob, error) {
+ var g glob.Glob
+
+ globMu.RLock()
+ g, found := globCache[pattern]
+ globMu.RUnlock()
+ if !found {
+ var err error
+ g, err = glob.Compile(strings.ToLower(pattern), '/')
+ if err != nil {
+ return nil, err
+ }
+
+ globMu.Lock()
+ globCache[pattern] = g
+ globMu.Unlock()
+ }
+
+ return g, nil
+
+}
diff --git a/resources/page/page.go b/resources/page/page.go
new file mode 100644
index 000000000..e2c883a8e
--- /dev/null
+++ b/resources/page/page.go
@@ -0,0 +1,371 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package page contains the core interfaces and types for the Page resource,
+// a core component in Hugo.
+package page
+
+import (
+ "html/template"
+
+ "github.com/bep/gitmap"
+ "github.com/gohugoio/hugo/config"
+
+ "github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/common/maps"
+
+ "github.com/gohugoio/hugo/compare"
+
+ "github.com/gohugoio/hugo/navigation"
+ "github.com/gohugoio/hugo/related"
+ "github.com/gohugoio/hugo/resources/resource"
+ "github.com/gohugoio/hugo/source"
+)
+
+// Clear clears any global package state.
+func Clear() error {
+ spc.clear()
+ return nil
+}
+
+// AlternativeOutputFormatsProvider provides alternative output formats for a
+// Page.
+type AlternativeOutputFormatsProvider interface {
+ // AlternativeOutputFormats gives the alternative output formats for the
+ // current output.
+ // 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.
+ AlternativeOutputFormats() OutputFormats
+}
+
+// AuthorProvider provides author information.
+type AuthorProvider interface {
+ Author() Author
+ Authors() AuthorList
+}
+
+// ChildCareProvider provides accessors to child resources.
+type ChildCareProvider interface {
+ Pages() Pages
+ Resources() resource.Resources
+}
+
+// ContentProvider provides the content related values for a Page.
+type ContentProvider interface {
+ Content() (interface{}, error)
+ Plain() string
+ PlainWords() []string
+ Summary() template.HTML
+ Truncated() bool
+ FuzzyWordCount() int
+ WordCount() int
+ ReadingTime() int
+ Len() int
+}
+
+// FileProvider provides the source file.
+type FileProvider interface {
+ File() source.File
+}
+
+// GetPageProvider provides the GetPage method.
+type GetPageProvider interface {
+ // GetPage looks up a page for the given ref.
+ // {{ with .GetPage "blog" }}{{ .Title }}{{ end }}
+ //
+ // This will return nil when no page could be found, and will return
+ // an error if the ref is ambiguous.
+ GetPage(ref string) (Page, error)
+}
+
+// GitInfoProvider provides Git info.
+type GitInfoProvider interface {
+ GitInfo() *gitmap.GitInfo
+}
+
+// InSectionPositioner provides section navigation.
+type InSectionPositioner interface {
+ NextInSection() Page
+ PrevInSection() Page
+}
+
+// InternalDependencies is considered an internal interface.
+type InternalDependencies interface {
+ GetRelatedDocsHandler() *RelatedDocsHandler
+}
+
+// OutputFormatsProvider provides the OutputFormats of a Page.
+type OutputFormatsProvider interface {
+ OutputFormats() OutputFormats
+}
+
+// Page is the core interface in Hugo.
+type Page interface {
+ ContentProvider
+ TableOfContentsProvider
+ PageWithoutContent
+}
+
+// PageMetaProvider provides page metadata, typically provided via front matter.
+type PageMetaProvider interface {
+ // The 4 page dates
+ resource.Dated
+
+ // Aliases forms the base for redirects generation.
+ Aliases() []string
+
+ // BundleType returns the bundle type: "leaf", "branch" or an empty string if it is none.
+ // See https://gohugo.io/content-management/page-bundles/
+ BundleType() string
+
+ // A configured description.
+ Description() string
+
+ // Whether this is a draft. Will only be true if run with the --buildDrafts (-D) flag.
+ Draft() bool
+
+ // IsHome returns whether this is the home page.
+ IsHome() bool
+
+ // Configured keywords.
+ Keywords() []string
+
+ // The Page Kind. One of page, home, section, taxonomy, taxonomyTerm.
+ Kind() string
+
+ // The configured layout to use to render this page. Typically set in front matter.
+ Layout() string
+
+ // The title used for links.
+ LinkTitle() string
+
+ // IsNode returns whether this is an item of one of the list types in Hugo,
+ // i.e. not a regular content
+ IsNode() bool
+
+ // IsPage returns whether this is a regular content
+ IsPage() bool
+
+ // Param looks for a param in Page and then in Site config.
+ Param(key interface{}) (interface{}, error)
+
+ // Path gets the relative path, including file name and extension if relevant,
+ // to the source of this Page. It will be relative to any content root.
+ Path() string
+
+ // The slug, typically defined in front matter.
+ Slug() string
+
+ // This page's language code. Will be the same as the site's.
+ Lang() string
+
+ // IsSection returns whether this is a section
+ IsSection() bool
+
+ // Section returns the first path element below the content root.
+ Section() string
+
+ // Returns a slice of sections (directories if it's a file) to this
+ // Page.
+ SectionsEntries() []string
+
+ // SectionsPath is SectionsEntries joined with a /.
+ SectionsPath() string
+
+ // Sitemap returns the sitemap configuration for this page.
+ Sitemap() config.Sitemap
+
+ // Type is a discriminator used to select layouts etc. It is typically set
+ // in front matter, but will fall back to the root section.
+ Type() string
+
+ // The configured weight, used as the first sort value in the default
+ // page sort if non-zero.
+ Weight() int
+}
+
+// PageRenderProvider provides a way for a Page to render itself.
+type PageRenderProvider interface {
+ Render(layout ...string) template.HTML
+}
+
+// PageWithoutContent is the Page without any of the content methods.
+type PageWithoutContent interface {
+ RawContentProvider
+ resource.Resource
+ PageMetaProvider
+ resource.LanguageProvider
+
+ // For pages backed by a file.
+ FileProvider
+
+ GitInfoProvider
+
+ // Output formats
+ OutputFormatsProvider
+ AlternativeOutputFormatsProvider
+
+ // Tree navigation
+ ChildCareProvider
+ TreeProvider
+
+ // Horisontal navigation
+ InSectionPositioner
+ PageRenderProvider
+ PaginatorProvider
+ Positioner
+ navigation.PageMenusProvider
+
+ // TODO(bep)
+ AuthorProvider
+
+ // Page lookups/refs
+ GetPageProvider
+ RefProvider
+
+ resource.TranslationKeyProvider
+ TranslationsProvider
+
+ SitesProvider
+
+ // Helper methods
+ ShortcodeInfoProvider
+ compare.Eqer
+ maps.Scratcher
+ RelatedKeywordsProvider
+
+ DeprecatedWarningPageMethods
+}
+
+// Positioner provides next/prev navigation.
+type Positioner interface {
+ Next() Page
+ Prev() Page
+
+ // Deprecated: Use Prev. Will be removed in Hugo 0.57
+ PrevPage() Page
+
+ // Deprecated: Use Next. Will be removed in Hugo 0.57
+ NextPage() Page
+}
+
+// RawContentProvider provides the raw, unprocessed content of the page.
+type RawContentProvider interface {
+ RawContent() string
+}
+
+// RefProvider provides the methods needed to create reflinks to pages.
+type RefProvider interface {
+ Ref(argsm map[string]interface{}) (string, error)
+ RefFrom(argsm map[string]interface{}, source interface{}) (string, error)
+ RelRef(argsm map[string]interface{}) (string, error)
+ RelRefFrom(argsm map[string]interface{}, source interface{}) (string, error)
+}
+
+// RelatedKeywordsProvider allows a Page to be indexed.
+type RelatedKeywordsProvider interface {
+ // Make it indexable as a related.Document
+ RelatedKeywords(cfg related.IndexConfig) ([]related.Keyword, error)
+}
+
+// ShortcodeInfoProvider provides info about the shortcodes in a Page.
+type ShortcodeInfoProvider interface {
+ // HasShortcode return whether the page has a shortcode with the given name.
+ // This method is mainly motivated with the Hugo Docs site's need for a list
+ // of pages with the `todo` shortcode in it.
+ HasShortcode(name string) bool
+}
+
+// SitesProvider provide accessors to get sites.
+type SitesProvider interface {
+ Site() Site
+ Sites() Sites
+}
+
+// TableOfContentsProvider provides the table of contents for a Page.
+type TableOfContentsProvider interface {
+ TableOfContents() template.HTML
+}
+
+// TranslationsProvider provides access to any translations.
+type TranslationsProvider interface {
+
+ // IsTranslated returns whether this content file is translated to
+ // other language(s).
+ IsTranslated() bool
+
+ // AllTranslations returns all translations, including the current Page.
+ AllTranslations() Pages
+
+ // Translations returns the translations excluding the current Page.
+ Translations() Pages
+}
+
+// TreeProvider provides section tree navigation.
+type TreeProvider interface {
+
+ // IsAncestor returns whether the current page is an ancestor of the given
+ // Note that this method is not relevant for taxonomy lists and taxonomy terms pages.
+ IsAncestor(other interface{}) (bool, error)
+
+ // CurrentSection returns the page's current section or the page itself if home or a section.
+ // Note that this will return nil for pages that is not regular, home or section pages.
+ CurrentSection() Page
+
+ // IsDescendant returns whether the current page is a descendant of the given
+ // Note that this method is not relevant for taxonomy lists and taxonomy terms pages.
+ IsDescendant(other interface{}) (bool, error)
+
+ // FirstSection returns the section on level 1 below home, e.g. "/docs".
+ // For the home page, this will return itself.
+ FirstSection() Page
+
+ // 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.
+ InSection(other interface{}) (bool, error)
+
+ // Parent returns a section's parent section or a page's section.
+ // To get a section's subsections, see Page's Sections method.
+ Parent() Page
+
+ // Sections returns this section's subsections, if any.
+ // Note that for non-sections, this method will always return an empty list.
+ Sections() Pages
+
+ // Page returns a reference to the Page itself, kept here mostly
+ // for legacy reasons.
+ Page() Page
+}
+
+// DeprecatedWarningPageMethods lists deprecated Page methods that will trigger
+// a WARNING if invoked.
+// This was added in Hugo 0.55.
+type DeprecatedWarningPageMethods interface {
+ source.FileWithoutOverlap
+ DeprecatedWarningPageMethods1
+}
+
+type DeprecatedWarningPageMethods1 interface {
+ IsDraft() bool
+ Hugo() hugo.Info
+ LanguagePrefix() string
+ GetParam(key string) interface{}
+ RSSLink() template.URL
+ URL() string
+}
+
+// Move here to trigger ERROR instead of WARNING.
+// TODO(bep) create wrappers and put into the Page once it has some methods.
+type DeprecatedErrorPageMethods interface {
+}
diff --git a/resources/page/page_author.go b/resources/page/page_author.go
new file mode 100644
index 000000000..9e8a95182
--- /dev/null
+++ b/resources/page/page_author.go
@@ -0,0 +1,45 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+// 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/resources/page/page_data.go b/resources/page/page_data.go
new file mode 100644
index 000000000..3345a44da
--- /dev/null
+++ b/resources/page/page_data.go
@@ -0,0 +1,42 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package page contains the core interfaces and types for the Page resource,
+// a core component in Hugo.
+package page
+
+import (
+ "fmt"
+)
+
+// Data represents the .Data element in a Page in Hugo. We make this
+// a type so we can do lazy loading of .Data.Pages
+type Data map[string]interface{}
+
+// Pages returns the pages stored with key "pages". If this is a func,
+// it will be invoked.
+func (d Data) Pages() Pages {
+ v, found := d["pages"]
+ if !found {
+ return nil
+ }
+
+ switch vv := v.(type) {
+ case Pages:
+ return vv
+ case func() Pages:
+ return vv()
+ default:
+ panic(fmt.Sprintf("%T is not Pages", v))
+ }
+}
diff --git a/resources/page/page_data_test.go b/resources/page/page_data_test.go
new file mode 100644
index 000000000..b6641bcd7
--- /dev/null
+++ b/resources/page/page_data_test.go
@@ -0,0 +1,57 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "bytes"
+ "testing"
+
+ "text/template"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestPageData(t *testing.T) {
+ assert := require.New(t)
+
+ data := make(Data)
+
+ assert.Nil(data.Pages())
+
+ pages := Pages{
+ &testPage{title: "a1"},
+ &testPage{title: "a2"},
+ }
+
+ data["pages"] = pages
+
+ assert.Equal(pages, data.Pages())
+
+ data["pages"] = func() Pages {
+ return pages
+ }
+
+ assert.Equal(pages, data.Pages())
+
+ templ, err := template.New("").Parse(`Pages: {{ .Pages }}`)
+
+ assert.NoError(err)
+
+ var buff bytes.Buffer
+
+ assert.NoError(templ.Execute(&buff, data))
+
+ assert.Contains(buff.String(), "Pages(2)")
+
+}
diff --git a/resources/page/page_generate/.gitignore b/resources/page/page_generate/.gitignore
new file mode 100644
index 000000000..84fd70a9f
--- /dev/null
+++ b/resources/page/page_generate/.gitignore
@@ -0,0 +1 @@
+generate \ No newline at end of file
diff --git a/resources/page/page_generate/generate_page_wrappers.go b/resources/page/page_generate/generate_page_wrappers.go
new file mode 100644
index 000000000..6d421c356
--- /dev/null
+++ b/resources/page/page_generate/generate_page_wrappers.go
@@ -0,0 +1,283 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page_generate
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "path/filepath"
+ "reflect"
+
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/common/maps"
+
+ "github.com/gohugoio/hugo/codegen"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/source"
+)
+
+const header = `// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This file is autogenerated.
+`
+
+var (
+ fileInterfaceDeprecated = reflect.TypeOf((*source.FileWithoutOverlap)(nil)).Elem()
+ pageInterfaceDeprecated = reflect.TypeOf((*page.DeprecatedWarningPageMethods)(nil)).Elem()
+ pageInterface = reflect.TypeOf((*page.Page)(nil)).Elem()
+
+ packageDir = filepath.FromSlash("resources/page")
+)
+
+func Generate(c *codegen.Inspector) error {
+ if err := generateMarshalJSON(c); err != nil {
+ return errors.Wrap(err, "failed to generate JSON marshaler")
+
+ }
+
+ if err := generateDeprecatedWrappers(c); err != nil {
+ return errors.Wrap(err, "failed to generate deprecate wrappers")
+ }
+
+ if err := generateFileIsZeroWrappers(c); err != nil {
+ return errors.Wrap(err, "failed to generate file wrappers")
+ }
+
+ return nil
+}
+
+func generateMarshalJSON(c *codegen.Inspector) error {
+ filename := filepath.Join(c.ProjectRootDir, packageDir, "page_marshaljson.autogen.go")
+ f, err := os.Create(filename)
+
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ includes := []reflect.Type{pageInterface}
+
+ // Exclude these methods
+ excludes := []reflect.Type{
+ // We need to eveluate the deprecated vs JSON in the future,
+ // but leave them out for now.
+ pageInterfaceDeprecated,
+
+ // Leave this out for now. We need to revisit the author issue.
+ reflect.TypeOf((*page.AuthorProvider)(nil)).Elem(),
+
+ // navigation.PageMenus
+
+ // Prevent loops.
+ reflect.TypeOf((*page.SitesProvider)(nil)).Elem(),
+ reflect.TypeOf((*page.Positioner)(nil)).Elem(),
+
+ reflect.TypeOf((*page.ChildCareProvider)(nil)).Elem(),
+ reflect.TypeOf((*page.TreeProvider)(nil)).Elem(),
+ reflect.TypeOf((*page.InSectionPositioner)(nil)).Elem(),
+ reflect.TypeOf((*page.PaginatorProvider)(nil)).Elem(),
+ reflect.TypeOf((*maps.Scratcher)(nil)).Elem(),
+ }
+
+ methods := c.MethodsFromTypes(
+ includes,
+ excludes)
+
+ if len(methods) == 0 {
+ return errors.New("no methods found")
+ }
+
+ marshalJSON, pkgImports := methods.ToMarshalJSON(
+ "Page",
+ "github.com/gohugoio/hugo/resources/page",
+ // Exclusion regexps. Matches method names.
+ `\bPage\b`,
+ )
+
+ fmt.Fprintf(f, `%s
+
+package page
+
+%s
+
+
+%s
+
+
+`, header, importsString(pkgImports), marshalJSON)
+
+ return nil
+}
+
+func generateDeprecatedWrappers(c *codegen.Inspector) error {
+ filename := filepath.Join(c.ProjectRootDir, packageDir, "page_wrappers.autogen.go")
+ f, err := os.Create(filename)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ // Generate a wrapper for deprecated page methods
+
+ reasons := map[string]string{
+ "IsDraft": "Use .Draft.",
+ "Hugo": "Use the global hugo function.",
+ "LanguagePrefix": "Use .Site.LanguagePrefix.",
+ "GetParam": "Use .Param or .Params.myParam.",
+ "RSSLink": `Use the Output Format's link, e.g. something like:
+ {{ with .OutputFormats.Get "RSS" }}{{ .RelPermalink }}{{ end }}`,
+ "URL": "Use .Permalink or .RelPermalink. If what you want is the front matter URL value, use .Params.url",
+ }
+
+ deprecated := func(name string, tp reflect.Type) string {
+ var alternative string
+ if tp == fileInterfaceDeprecated {
+ alternative = "Use .File." + name
+ } else {
+ var found bool
+ alternative, found = reasons[name]
+ if !found {
+ panic(fmt.Sprintf("no deprecated reason found for %q", name))
+ }
+ }
+
+ return fmt.Sprintf("helpers.Deprecated(%q, %q, %q, false)", "Page", "."+name, alternative)
+ }
+
+ var buff bytes.Buffer
+
+ methods := c.MethodsFromTypes([]reflect.Type{fileInterfaceDeprecated, pageInterfaceDeprecated}, nil)
+
+ for _, m := range methods {
+ fmt.Fprint(&buff, m.Declaration("*pageDeprecated"))
+ fmt.Fprintln(&buff, " {")
+ fmt.Fprintf(&buff, "\t%s\n", deprecated(m.Name, m.Owner))
+ fmt.Fprintf(&buff, "\t%s\n}\n", m.Delegate("p", "p"))
+
+ }
+
+ pkgImports := append(methods.Imports(), "github.com/gohugoio/hugo/helpers")
+
+ fmt.Fprintf(f, `%s
+
+package page
+
+%s
+// NewDeprecatedWarningPage adds deprecation warnings to the given implementation.
+func NewDeprecatedWarningPage(p DeprecatedWarningPageMethods) DeprecatedWarningPageMethods {
+ return &pageDeprecated{p: p}
+}
+
+type pageDeprecated struct {
+ p DeprecatedWarningPageMethods
+}
+
+%s
+
+`, header, importsString(pkgImports), buff.String())
+
+ return nil
+}
+
+func generateFileIsZeroWrappers(c *codegen.Inspector) error {
+ filename := filepath.Join(c.ProjectRootDir, packageDir, "zero_file.autogen.go")
+ f, err := os.Create(filename)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ // Generate warnings for zero file access
+
+ warning := func(name string, tp reflect.Type) string {
+ msg := fmt.Sprintf(".File.%s on zero object. Wrap it in if or with: {{ with .File }}{{ .%s }}{{ end }}", name, name)
+
+ return fmt.Sprintf("z.log.Println(%q)", msg)
+ }
+
+ var buff bytes.Buffer
+
+ methods := c.MethodsFromTypes([]reflect.Type{reflect.TypeOf((*source.File)(nil)).Elem()}, nil)
+
+ for _, m := range methods {
+ if m.Name == "IsZero" {
+ continue
+ }
+ fmt.Fprint(&buff, m.DeclarationNamed("zeroFile"))
+ fmt.Fprintln(&buff, " {")
+ fmt.Fprintf(&buff, "\t%s\n", warning(m.Name, m.Owner))
+ if len(m.Out) > 0 {
+ fmt.Fprintln(&buff, "\treturn")
+ }
+ fmt.Fprintln(&buff, "}")
+
+ }
+
+ pkgImports := append(methods.Imports(), "github.com/gohugoio/hugo/helpers", "github.com/gohugoio/hugo/source")
+
+ fmt.Fprintf(f, `%s
+
+package page
+
+%s
+
+// ZeroFile represents a zero value of source.File with warnings if invoked.
+type zeroFile struct {
+ log *helpers.DistinctLogger
+}
+
+func NewZeroFile(log *helpers.DistinctLogger) source.File {
+ return zeroFile{log: log}
+}
+
+func (zeroFile) IsZero() bool {
+ return true
+}
+
+%s
+
+`, header, importsString(pkgImports), buff.String())
+
+ return nil
+}
+
+func importsString(imps []string) string {
+ if len(imps) == 0 {
+ return ""
+ }
+
+ if len(imps) == 1 {
+ return fmt.Sprintf("import %q", imps[0])
+ }
+
+ impsStr := "import (\n"
+ for _, imp := range imps {
+ impsStr += fmt.Sprintf("%q\n", imp)
+ }
+
+ return impsStr + ")"
+}
diff --git a/resources/page/page_kinds.go b/resources/page/page_kinds.go
new file mode 100644
index 000000000..a2e59438e
--- /dev/null
+++ b/resources/page/page_kinds.go
@@ -0,0 +1,25 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+const (
+ KindPage = "page"
+
+ // The rest are node types; home page, sections etc.
+
+ KindHome = "home"
+ KindSection = "section"
+ KindTaxonomy = "taxonomy"
+ KindTaxonomyTerm = "taxonomyTerm"
+)
diff --git a/resources/page/page_kinds_test.go b/resources/page/page_kinds_test.go
new file mode 100644
index 000000000..8ad7343dc
--- /dev/null
+++ b/resources/page/page_kinds_test.go
@@ -0,0 +1,31 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+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)
+
+}
diff --git a/resources/page/page_marshaljson.autogen.go b/resources/page/page_marshaljson.autogen.go
new file mode 100644
index 000000000..b2a8ef79f
--- /dev/null
+++ b/resources/page/page_marshaljson.autogen.go
@@ -0,0 +1,202 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This file is autogenerated.
+
+package page
+
+import (
+ "encoding/json"
+ "github.com/bep/gitmap"
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/langs"
+ "github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/navigation"
+ "github.com/gohugoio/hugo/source"
+ "html/template"
+ "time"
+)
+
+func MarshalPageToJSON(p Page) ([]byte, error) {
+ content, err := p.Content()
+ if err != nil {
+ return nil, err
+ }
+ plain := p.Plain()
+ plainWords := p.PlainWords()
+ summary := p.Summary()
+ truncated := p.Truncated()
+ fuzzyWordCount := p.FuzzyWordCount()
+ wordCount := p.WordCount()
+ readingTime := p.ReadingTime()
+ length := p.Len()
+ tableOfContents := p.TableOfContents()
+ rawContent := p.RawContent()
+ mediaType := p.MediaType()
+ resourceType := p.ResourceType()
+ permalink := p.Permalink()
+ relPermalink := p.RelPermalink()
+ name := p.Name()
+ title := p.Title()
+ params := p.Params()
+ data := p.Data()
+ date := p.Date()
+ lastmod := p.Lastmod()
+ publishDate := p.PublishDate()
+ expiryDate := p.ExpiryDate()
+ aliases := p.Aliases()
+ bundleType := p.BundleType()
+ description := p.Description()
+ draft := p.Draft()
+ isHome := p.IsHome()
+ keywords := p.Keywords()
+ kind := p.Kind()
+ layout := p.Layout()
+ linkTitle := p.LinkTitle()
+ isNode := p.IsNode()
+ isPage := p.IsPage()
+ path := p.Path()
+ slug := p.Slug()
+ lang := p.Lang()
+ isSection := p.IsSection()
+ section := p.Section()
+ sectionsEntries := p.SectionsEntries()
+ sectionsPath := p.SectionsPath()
+ sitemap := p.Sitemap()
+ typ := p.Type()
+ weight := p.Weight()
+ language := p.Language()
+ file := p.File()
+ gitInfo := p.GitInfo()
+ outputFormats := p.OutputFormats()
+ alternativeOutputFormats := p.AlternativeOutputFormats()
+ menus := p.Menus()
+ translationKey := p.TranslationKey()
+ isTranslated := p.IsTranslated()
+ allTranslations := p.AllTranslations()
+ translations := p.Translations()
+
+ s := struct {
+ Content interface{}
+ Plain string
+ PlainWords []string
+ Summary template.HTML
+ Truncated bool
+ FuzzyWordCount int
+ WordCount int
+ ReadingTime int
+ Len int
+ TableOfContents template.HTML
+ RawContent string
+ MediaType media.Type
+ ResourceType string
+ Permalink string
+ RelPermalink string
+ Name string
+ Title string
+ Params map[string]interface{}
+ Data interface{}
+ Date time.Time
+ Lastmod time.Time
+ PublishDate time.Time
+ ExpiryDate time.Time
+ Aliases []string
+ BundleType string
+ Description string
+ Draft bool
+ IsHome bool
+ Keywords []string
+ Kind string
+ Layout string
+ LinkTitle string
+ IsNode bool
+ IsPage bool
+ Path string
+ Slug string
+ Lang string
+ IsSection bool
+ Section string
+ SectionsEntries []string
+ SectionsPath string
+ Sitemap config.Sitemap
+ Type string
+ Weight int
+ Language *langs.Language
+ File source.File
+ GitInfo *gitmap.GitInfo
+ OutputFormats OutputFormats
+ AlternativeOutputFormats OutputFormats
+ Menus navigation.PageMenus
+ TranslationKey string
+ IsTranslated bool
+ AllTranslations Pages
+ Translations Pages
+ }{
+ Content: content,
+ Plain: plain,
+ PlainWords: plainWords,
+ Summary: summary,
+ Truncated: truncated,
+ FuzzyWordCount: fuzzyWordCount,
+ WordCount: wordCount,
+ ReadingTime: readingTime,
+ Len: length,
+ TableOfContents: tableOfContents,
+ RawContent: rawContent,
+ MediaType: mediaType,
+ ResourceType: resourceType,
+ Permalink: permalink,
+ RelPermalink: relPermalink,
+ Name: name,
+ Title: title,
+ Params: params,
+ Data: data,
+ Date: date,
+ Lastmod: lastmod,
+ PublishDate: publishDate,
+ ExpiryDate: expiryDate,
+ Aliases: aliases,
+ BundleType: bundleType,
+ Description: description,
+ Draft: draft,
+ IsHome: isHome,
+ Keywords: keywords,
+ Kind: kind,
+ Layout: layout,
+ LinkTitle: linkTitle,
+ IsNode: isNode,
+ IsPage: isPage,
+ Path: path,
+ Slug: slug,
+ Lang: lang,
+ IsSection: isSection,
+ Section: section,
+ SectionsEntries: sectionsEntries,
+ SectionsPath: sectionsPath,
+ Sitemap: sitemap,
+ Type: typ,
+ Weight: weight,
+ Language: language,
+ File: file,
+ GitInfo: gitInfo,
+ OutputFormats: outputFormats,
+ AlternativeOutputFormats: alternativeOutputFormats,
+ Menus: menus,
+ TranslationKey: translationKey,
+ IsTranslated: isTranslated,
+ AllTranslations: allTranslations,
+ Translations: translations,
+ }
+
+ return json.Marshal(&s)
+}
diff --git a/resources/page/page_nop.go b/resources/page/page_nop.go
new file mode 100644
index 000000000..229bcb077
--- /dev/null
+++ b/resources/page/page_nop.go
@@ -0,0 +1,467 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package page contains the core interfaces and types for the Page resource,
+// a core component in Hugo.
+package page
+
+import (
+ "html/template"
+ "os"
+ "time"
+
+ "github.com/bep/gitmap"
+ "github.com/gohugoio/hugo/navigation"
+
+ "github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/source"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/langs"
+ "github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/related"
+ "github.com/gohugoio/hugo/resources/resource"
+)
+
+var (
+ NopPage Page = new(nopPage)
+ NilPage *nopPage
+)
+
+// PageNop implements Page, but does nothing.
+type nopPage int
+
+func (p *nopPage) Aliases() []string {
+ return nil
+}
+
+func (p *nopPage) Sitemap() config.Sitemap {
+ return config.Sitemap{}
+}
+
+func (p *nopPage) Layout() string {
+ return ""
+}
+
+func (p *nopPage) RSSLink() template.URL {
+ return ""
+}
+
+func (p *nopPage) Author() Author {
+ return Author{}
+
+}
+func (p *nopPage) Authors() AuthorList {
+ return nil
+}
+
+func (p *nopPage) AllTranslations() Pages {
+ return nil
+}
+
+func (p *nopPage) LanguagePrefix() string {
+ return ""
+}
+
+func (p *nopPage) AlternativeOutputFormats() OutputFormats {
+ return nil
+}
+
+func (p *nopPage) BaseFileName() string {
+ return ""
+}
+
+func (p *nopPage) BundleType() string {
+ return ""
+}
+
+func (p *nopPage) Content() (interface{}, error) {
+ return "", nil
+}
+
+func (p *nopPage) ContentBaseName() string {
+ return ""
+}
+
+func (p *nopPage) CurrentSection() Page {
+ return nil
+}
+
+func (p *nopPage) Data() interface{} {
+ return nil
+}
+
+func (p *nopPage) Date() (t time.Time) {
+ return
+}
+
+func (p *nopPage) Description() string {
+ return ""
+}
+
+func (p *nopPage) RefFrom(argsm map[string]interface{}, source interface{}) (string, error) {
+ return "", nil
+}
+func (p *nopPage) RelRefFrom(argsm map[string]interface{}, source interface{}) (string, error) {
+ return "", nil
+}
+
+func (p *nopPage) Dir() string {
+ return ""
+}
+
+func (p *nopPage) Draft() bool {
+ return false
+}
+
+func (p *nopPage) Eq(other interface{}) bool {
+ return p == other
+}
+
+func (p *nopPage) ExpiryDate() (t time.Time) {
+ return
+}
+
+func (p *nopPage) Ext() string {
+ return ""
+}
+
+func (p *nopPage) Extension() string {
+ return ""
+}
+
+var nilFile *source.FileInfo
+
+func (p *nopPage) File() source.File {
+ return nilFile
+}
+
+func (p *nopPage) FileInfo() os.FileInfo {
+ return nil
+}
+
+func (p *nopPage) Filename() string {
+ return ""
+}
+
+func (p *nopPage) FirstSection() Page {
+ return nil
+}
+
+func (p *nopPage) FuzzyWordCount() int {
+ return 0
+}
+
+func (p *nopPage) GetPage(ref string) (Page, error) {
+ return nil, nil
+}
+
+func (p *nopPage) GetParam(key string) interface{} {
+ return nil
+}
+
+func (p *nopPage) GitInfo() *gitmap.GitInfo {
+ return nil
+}
+
+func (p *nopPage) HasMenuCurrent(menuID string, me *navigation.MenuEntry) bool {
+ return false
+}
+
+func (p *nopPage) HasShortcode(name string) bool {
+ return false
+}
+
+func (p *nopPage) Hugo() (h hugo.Info) {
+ return
+}
+
+func (p *nopPage) InSection(other interface{}) (bool, error) {
+ return false, nil
+}
+
+func (p *nopPage) IsAncestor(other interface{}) (bool, error) {
+ return false, nil
+}
+
+func (p *nopPage) IsDescendant(other interface{}) (bool, error) {
+ return false, nil
+}
+
+func (p *nopPage) IsDraft() bool {
+ return false
+}
+
+func (p *nopPage) IsHome() bool {
+ return false
+}
+
+func (p *nopPage) IsMenuCurrent(menuID string, inme *navigation.MenuEntry) bool {
+ return false
+}
+
+func (p *nopPage) IsNode() bool {
+ return false
+}
+
+func (p *nopPage) IsPage() bool {
+ return false
+}
+
+func (p *nopPage) IsSection() bool {
+ return false
+}
+
+func (p *nopPage) IsTranslated() bool {
+ return false
+}
+
+func (p *nopPage) Keywords() []string {
+ return nil
+}
+
+func (p *nopPage) Kind() string {
+ return ""
+}
+
+func (p *nopPage) Lang() string {
+ return ""
+}
+
+func (p *nopPage) Language() *langs.Language {
+ return nil
+}
+
+func (p *nopPage) Lastmod() (t time.Time) {
+ return
+}
+
+func (p *nopPage) Len() int {
+ return 0
+}
+
+func (p *nopPage) LinkTitle() string {
+ return ""
+}
+
+func (p *nopPage) LogicalName() string {
+ return ""
+}
+
+func (p *nopPage) MediaType() (m media.Type) {
+ return
+}
+
+func (p *nopPage) Menus() (m navigation.PageMenus) {
+ return
+}
+
+func (p *nopPage) Name() string {
+ return ""
+}
+
+func (p *nopPage) Next() Page {
+ return nil
+}
+
+func (p *nopPage) OutputFormats() OutputFormats {
+ return nil
+}
+
+func (p *nopPage) Pages() Pages {
+ return nil
+}
+
+func (p *nopPage) Paginate(seq interface{}, options ...interface{}) (*Pager, error) {
+ return nil, nil
+}
+
+func (p *nopPage) Paginator(options ...interface{}) (*Pager, error) {
+ return nil, nil
+}
+
+func (p *nopPage) Param(key interface{}) (interface{}, error) {
+ return nil, nil
+}
+
+func (p *nopPage) Params() map[string]interface{} {
+ return nil
+}
+
+func (p *nopPage) Page() Page {
+ return p
+}
+
+func (p *nopPage) Parent() Page {
+ return nil
+}
+
+func (p *nopPage) Path() string {
+ return ""
+}
+
+func (p *nopPage) Permalink() string {
+ return ""
+}
+
+func (p *nopPage) Plain() string {
+ return ""
+}
+
+func (p *nopPage) PlainWords() []string {
+ return nil
+}
+
+func (p *nopPage) Prev() Page {
+ return nil
+}
+
+func (p *nopPage) PublishDate() (t time.Time) {
+ return
+}
+
+func (p *nopPage) PrevInSection() Page {
+ return nil
+}
+func (p *nopPage) NextInSection() Page {
+ return nil
+}
+
+func (p *nopPage) PrevPage() Page {
+ return nil
+}
+
+func (p *nopPage) NextPage() Page {
+ return nil
+}
+
+func (p *nopPage) RawContent() string {
+ return ""
+}
+
+func (p *nopPage) ReadingTime() int {
+ return 0
+}
+
+func (p *nopPage) Ref(argsm map[string]interface{}) (string, error) {
+ return "", nil
+}
+
+func (p *nopPage) RelPermalink() string {
+ return ""
+}
+
+func (p *nopPage) RelRef(argsm map[string]interface{}) (string, error) {
+ return "", nil
+}
+
+func (p *nopPage) Render(layout ...string) template.HTML {
+ return ""
+}
+
+func (p *nopPage) ResourceType() string {
+ return ""
+}
+
+func (p *nopPage) Resources() resource.Resources {
+ return nil
+}
+
+func (p *nopPage) Scratch() *maps.Scratch {
+ return nil
+}
+
+func (p *nopPage) RelatedKeywords(cfg related.IndexConfig) ([]related.Keyword, error) {
+ return nil, nil
+}
+
+func (p *nopPage) Section() string {
+ return ""
+}
+
+func (p *nopPage) Sections() Pages {
+ return nil
+}
+
+func (p *nopPage) SectionsEntries() []string {
+ return nil
+}
+
+func (p *nopPage) SectionsPath() string {
+ return ""
+}
+
+func (p *nopPage) Site() Site {
+ return nil
+}
+
+func (p *nopPage) Sites() Sites {
+ return nil
+}
+
+func (p *nopPage) Slug() string {
+ return ""
+}
+
+func (p *nopPage) String() string {
+ return "nopPage"
+}
+
+func (p *nopPage) Summary() template.HTML {
+ return ""
+}
+
+func (p *nopPage) TableOfContents() template.HTML {
+ return ""
+}
+
+func (p *nopPage) Title() string {
+ return ""
+}
+
+func (p *nopPage) TranslationBaseName() string {
+ return ""
+}
+
+func (p *nopPage) TranslationKey() string {
+ return ""
+}
+
+func (p *nopPage) Translations() Pages {
+ return nil
+}
+
+func (p *nopPage) Truncated() bool {
+ return false
+}
+
+func (p *nopPage) Type() string {
+ return ""
+}
+
+func (p *nopPage) URL() string {
+ return ""
+}
+
+func (p *nopPage) UniqueID() string {
+ return ""
+}
+
+func (p *nopPage) Weight() int {
+ return 0
+}
+
+func (p *nopPage) WordCount() int {
+ return 0
+}
diff --git a/resources/page/page_outputformat.go b/resources/page/page_outputformat.go
new file mode 100644
index 000000000..ff4213cc4
--- /dev/null
+++ b/resources/page/page_outputformat.go
@@ -0,0 +1,85 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package page contains the core interfaces and types for the Page resource,
+// a core component in Hugo.
+package page
+
+import (
+ "strings"
+
+ "github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/output"
+)
+
+// OutputFormats holds a list of the relevant output formats for a given page.
+type OutputFormats []OutputFormat
+
+// 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
+
+ Format output.Format
+
+ relPermalink string
+ permalink string
+}
+
+// Name returns this OutputFormat's name, i.e. HTML, AMP, JSON etc.
+func (o OutputFormat) Name() string {
+ return o.Format.Name
+}
+
+// MediaType returns this OutputFormat's MediaType (MIME type).
+func (o OutputFormat) MediaType() media.Type {
+ return o.Format.MediaType
+}
+
+// Permalink returns the absolute permalink to this output format.
+func (o OutputFormat) Permalink() string {
+ return o.permalink
+}
+
+// RelPermalink returns the relative permalink to this output format.
+func (o OutputFormat) RelPermalink() string {
+ return o.relPermalink
+}
+
+func NewOutputFormat(relPermalink, permalink string, isCanonical bool, f output.Format) OutputFormat {
+ rel := f.Rel
+ if isCanonical {
+ rel = "canonical"
+ }
+ return OutputFormat{Rel: rel, Format: f, relPermalink: relPermalink, permalink: permalink}
+}
+
+// Get gets a OutputFormat given its name, i.e. json, html etc.
+// It returns nil if none found.
+func (o OutputFormats) Get(name string) *OutputFormat {
+ for _, f := range o {
+ if strings.EqualFold(f.Format.Name, name) {
+ return &f
+ }
+ }
+ return nil
+}
diff --git a/resources/page/page_paths.go b/resources/page/page_paths.go
new file mode 100644
index 000000000..fd231e72b
--- /dev/null
+++ b/resources/page/page_paths.go
@@ -0,0 +1,339 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "path"
+ "path/filepath"
+
+ "strings"
+
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/output"
+)
+
+const slash = "/"
+
+// 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.
+//
+//
+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
+
+ // Typically a language prefix added to file paths.
+ PrefixFilePath string
+
+ // Typically a language prefix added to links.
+ PrefixLink string
+
+ // If in multihost mode etc., every link/path needs to be prefixed, even
+ // if set in URL.
+ ForcePrefix bool
+
+ // URL from front matter if set. Will override any Slug etc.
+ 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
+}
+
+// TODO(bep) move this type.
+type TargetPaths struct {
+
+ // Where to store the file on disk relative to the publish dir. OS slashes.
+ TargetFilename string
+
+ // The directory to write sub-resources of the above.
+ SubResourceBaseTarget string
+
+ // The base for creating links to sub-resources of the above.
+ SubResourceBaseLink string
+
+ // The relative permalink to this resources. Unix slashes.
+ Link string
+}
+
+func (p TargetPaths) RelPermalink(s *helpers.PathSpec) string {
+ return s.PrependBasePath(p.Link, false)
+}
+
+func (p TargetPaths) PermalinkForOutputFormat(s *helpers.PathSpec, f output.Format) string {
+ var baseURL string
+ var err error
+ if f.Protocol != "" {
+ baseURL, err = s.BaseURL.WithProtocol(f.Protocol)
+ if err != nil {
+ return ""
+ }
+ } else {
+ baseURL = s.BaseURL.String()
+ }
+
+ return s.PermalinkForBaseURL(p.Link, baseURL)
+}
+
+func isHtmlIndex(s string) bool {
+ return strings.HasSuffix(s, "/index.html")
+}
+
+func CreateTargetPaths(d TargetPathDescriptor) (tp TargetPaths) {
+
+ if d.Type.Name == "" {
+ panic("CreateTargetPath: missing type")
+ }
+
+ // Normalize all file Windows paths to simplify what's next.
+ if helpers.FilePathSeparator != slash {
+ d.Dir = filepath.ToSlash(d.Dir)
+ d.PrefixFilePath = filepath.ToSlash(d.PrefixFilePath)
+
+ }
+
+ if d.URL != "" && !strings.HasPrefix(d.URL, "/") {
+ // Treat this as a context relative URL
+ d.ForcePrefix = true
+ }
+
+ pagePath := slash
+
+ var (
+ pagePathDir string
+ link string
+ linkDir string
+ )
+
+ // 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
+ baseNameSameAsType := d.BaseName != "" && d.BaseName == d.Type.BaseName
+
+ if d.ExpandedPermalink == "" && baseNameSameAsType {
+ isUgly = true
+ }
+
+ if d.Kind != KindPage && d.URL == "" && len(d.Sections) > 0 {
+ if d.ExpandedPermalink != "" {
+ pagePath = pjoin(pagePath, d.ExpandedPermalink)
+ } else {
+ pagePath = pjoin(d.Sections...)
+ }
+ needsBase = false
+ }
+
+ if d.Type.Path != "" {
+ pagePath = pjoin(pagePath, d.Type.Path)
+ }
+
+ if d.Kind != KindHome && d.URL != "" {
+ pagePath = pjoin(pagePath, d.URL)
+
+ if d.Addends != "" {
+ pagePath = pjoin(pagePath, d.Addends)
+ }
+
+ pagePathDir = pagePath
+ link = pagePath
+ hasDot := strings.Contains(d.URL, ".")
+ hasSlash := strings.HasSuffix(d.URL, slash)
+
+ if hasSlash || !hasDot {
+ pagePath = pjoin(pagePath, d.Type.BaseName+d.Type.MediaType.FullSuffix())
+ } else if hasDot {
+ pagePathDir = path.Dir(pagePathDir)
+ }
+
+ if !isHtmlIndex(pagePath) {
+ link = pagePath
+ } else if !hasSlash {
+ link += slash
+ }
+
+ linkDir = pagePathDir
+
+ if d.ForcePrefix {
+
+ // Prepend language prefix if not already set in URL
+ if d.PrefixFilePath != "" && !strings.HasPrefix(d.URL, slash+d.PrefixFilePath) {
+ pagePath = pjoin(d.PrefixFilePath, pagePath)
+ pagePathDir = pjoin(d.PrefixFilePath, pagePathDir)
+ }
+
+ if d.PrefixLink != "" && !strings.HasPrefix(d.URL, slash+d.PrefixLink) {
+ link = pjoin(d.PrefixLink, link)
+ linkDir = pjoin(d.PrefixLink, linkDir)
+ }
+ }
+
+ } else if d.Kind == KindPage {
+
+ if d.ExpandedPermalink != "" {
+ pagePath = pjoin(pagePath, d.ExpandedPermalink)
+
+ } else {
+ if d.Dir != "" {
+ pagePath = pjoin(pagePath, d.Dir)
+ }
+ if d.BaseName != "" {
+ pagePath = pjoin(pagePath, d.BaseName)
+ }
+ }
+
+ if d.Addends != "" {
+ pagePath = pjoin(pagePath, d.Addends)
+ }
+
+ link = pagePath
+
+ if baseNameSameAsType {
+ link = strings.TrimSuffix(link, d.BaseName)
+ }
+
+ pagePathDir = link
+ link = link + slash
+ linkDir = pagePathDir
+
+ if isUgly {
+ pagePath = addSuffix(pagePath, d.Type.MediaType.FullSuffix())
+ } else {
+ pagePath = pjoin(pagePath, d.Type.BaseName+d.Type.MediaType.FullSuffix())
+ }
+
+ if !isHtmlIndex(pagePath) {
+ link = pagePath
+ }
+
+ if d.PrefixFilePath != "" {
+ pagePath = pjoin(d.PrefixFilePath, pagePath)
+ pagePathDir = pjoin(d.PrefixFilePath, pagePathDir)
+ }
+
+ if d.PrefixLink != "" {
+ link = pjoin(d.PrefixLink, link)
+ linkDir = pjoin(d.PrefixLink, linkDir)
+ }
+
+ } else {
+ if d.Addends != "" {
+ pagePath = pjoin(pagePath, d.Addends)
+ }
+
+ needsBase = needsBase && d.Addends == ""
+
+ // No permalink expansion etc. for node type pages (for now)
+ base := ""
+
+ if needsBase || !isUgly {
+ base = d.Type.BaseName
+ }
+
+ pagePathDir = pagePath
+ link = pagePath
+ linkDir = pagePathDir
+
+ if base != "" {
+ pagePath = path.Join(pagePath, addSuffix(base, d.Type.MediaType.FullSuffix()))
+ } else {
+ pagePath = addSuffix(pagePath, d.Type.MediaType.FullSuffix())
+
+ }
+
+ if !isHtmlIndex(pagePath) {
+ link = pagePath
+ } else {
+ link += slash
+ }
+
+ if d.PrefixFilePath != "" {
+ pagePath = pjoin(d.PrefixFilePath, pagePath)
+ pagePathDir = pjoin(d.PrefixFilePath, pagePathDir)
+ }
+
+ if d.PrefixLink != "" {
+ link = pjoin(d.PrefixLink, link)
+ linkDir = pjoin(d.PrefixLink, linkDir)
+ }
+ }
+
+ pagePath = pjoin(slash, pagePath)
+ pagePathDir = strings.TrimSuffix(path.Join(slash, pagePathDir), slash)
+
+ hadSlash := strings.HasSuffix(link, slash)
+ link = strings.Trim(link, slash)
+ if hadSlash {
+ link += slash
+ }
+
+ if !strings.HasPrefix(link, slash) {
+ link = slash + link
+ }
+
+ linkDir = strings.TrimSuffix(path.Join(slash, linkDir), slash)
+
+ // Note: MakePathSanitized will lower case the path if
+ // disablePathToLower isn't set.
+ pagePath = d.PathSpec.MakePathSanitized(pagePath)
+ pagePathDir = d.PathSpec.MakePathSanitized(pagePathDir)
+ link = d.PathSpec.MakePathSanitized(link)
+ linkDir = d.PathSpec.MakePathSanitized(linkDir)
+
+ tp.TargetFilename = filepath.FromSlash(pagePath)
+ tp.SubResourceBaseTarget = filepath.FromSlash(pagePathDir)
+ tp.SubResourceBaseLink = linkDir
+ tp.Link = d.PathSpec.URLizeFilename(link)
+ if tp.Link == "" {
+ tp.Link = slash
+ }
+
+ return
+}
+
+func addSuffix(s, suffix string) string {
+ return strings.Trim(s, slash) + suffix
+}
+
+// Like path.Join, but preserves one trailing slash if present.
+func pjoin(elem ...string) string {
+ hadSlash := strings.HasSuffix(elem[len(elem)-1], slash)
+ joined := path.Join(elem...)
+ if hadSlash && !strings.HasSuffix(joined, slash) {
+ return joined + slash
+ }
+ return joined
+}
diff --git a/resources/page/page_paths_test.go b/resources/page/page_paths_test.go
new file mode 100644
index 000000000..4aaa41e8a
--- /dev/null
+++ b/resources/page/page_paths_test.go
@@ -0,0 +1,258 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/gohugoio/hugo/media"
+
+ "fmt"
+
+ "github.com/gohugoio/hugo/output"
+)
+
+func TestPageTargetPath(t *testing.T) {
+
+ pathSpec := newTestPathSpec()
+
+ noExtNoDelimMediaType := media.TextType
+ noExtNoDelimMediaType.Suffixes = []string{}
+ noExtNoDelimMediaType.Delimiter = ""
+
+ // Netlify style _redirects
+ noExtDelimFormat := output.Format{
+ Name: "NER",
+ MediaType: noExtNoDelimMediaType,
+ BaseName: "_redirects",
+ }
+
+ for _, langPrefixPath := range []string{"", "no"} {
+ for _, langPrefixLink := range []string{"", "no"} {
+ for _, uglyURLs := range []bool{false, true} {
+
+ tests := []struct {
+ name string
+ d TargetPathDescriptor
+ expected TargetPaths
+ }{
+ {"JSON home", TargetPathDescriptor{Kind: KindHome, Type: output.JSONFormat}, TargetPaths{TargetFilename: "/index.json", SubResourceBaseTarget: "", Link: "/index.json"}},
+ {"AMP home", TargetPathDescriptor{Kind: KindHome, Type: output.AMPFormat}, TargetPaths{TargetFilename: "/amp/index.html", SubResourceBaseTarget: "/amp", Link: "/amp/"}},
+ {"HTML home", TargetPathDescriptor{Kind: KindHome, BaseName: "_index", Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/index.html", SubResourceBaseTarget: "", Link: "/"}},
+ {"Netlify redirects", TargetPathDescriptor{Kind: KindHome, BaseName: "_index", Type: noExtDelimFormat}, TargetPaths{TargetFilename: "/_redirects", SubResourceBaseTarget: "", Link: "/_redirects"}},
+ {"HTML section list", TargetPathDescriptor{
+ Kind: KindSection,
+ Sections: []string{"sect1"},
+ BaseName: "_index",
+ Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/sect1/index.html", SubResourceBaseTarget: "/sect1", Link: "/sect1/"}},
+ {"HTML taxonomy list", TargetPathDescriptor{
+ Kind: KindTaxonomy,
+ Sections: []string{"tags", "hugo"},
+ BaseName: "_index",
+ Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/tags/hugo/index.html", SubResourceBaseTarget: "/tags/hugo", Link: "/tags/hugo/"}},
+ {"HTML taxonomy term", TargetPathDescriptor{
+ Kind: KindTaxonomy,
+ Sections: []string{"tags"},
+ BaseName: "_index",
+ Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/tags/index.html", SubResourceBaseTarget: "/tags", Link: "/tags/"}},
+ {
+ "HTML page", TargetPathDescriptor{
+ Kind: KindPage,
+ Dir: "/a/b",
+ BaseName: "mypage",
+ Sections: []string{"a"},
+ Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/a/b/mypage/index.html", SubResourceBaseTarget: "/a/b/mypage", Link: "/a/b/mypage/"}},
+
+ {
+ "HTML page with index as base", TargetPathDescriptor{
+ Kind: KindPage,
+ Dir: "/a/b",
+ BaseName: "index",
+ Sections: []string{"a"},
+ Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/a/b/index.html", SubResourceBaseTarget: "/a/b", Link: "/a/b/"}},
+
+ {
+ "HTML page with special chars", TargetPathDescriptor{
+ Kind: KindPage,
+ Dir: "/a/b",
+ BaseName: "My Page!",
+ Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/a/b/my-page/index.html", SubResourceBaseTarget: "/a/b/my-page", Link: "/a/b/my-page/"}},
+ {"RSS home", TargetPathDescriptor{Kind: "rss", Type: output.RSSFormat}, TargetPaths{TargetFilename: "/index.xml", SubResourceBaseTarget: "", Link: "/index.xml"}},
+ {"RSS section list", TargetPathDescriptor{
+ Kind: "rss",
+ Sections: []string{"sect1"},
+ Type: output.RSSFormat}, TargetPaths{TargetFilename: "/sect1/index.xml", SubResourceBaseTarget: "/sect1", Link: "/sect1/index.xml"}},
+ {
+ "AMP page", TargetPathDescriptor{
+ Kind: KindPage,
+ Dir: "/a/b/c",
+ BaseName: "myamp",
+ Type: output.AMPFormat}, TargetPaths{TargetFilename: "/amp/a/b/c/myamp/index.html", SubResourceBaseTarget: "/amp/a/b/c/myamp", Link: "/amp/a/b/c/myamp/"}},
+ {
+ "AMP page with URL with suffix", TargetPathDescriptor{
+ Kind: KindPage,
+ Dir: "/sect/",
+ BaseName: "mypage",
+ URL: "/some/other/url.xhtml",
+ Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/some/other/url.xhtml", SubResourceBaseTarget: "/some/other", Link: "/some/other/url.xhtml"}},
+ {
+ "JSON page with URL without suffix", TargetPathDescriptor{
+ Kind: KindPage,
+ Dir: "/sect/",
+ BaseName: "mypage",
+ URL: "/some/other/path/",
+ Type: output.JSONFormat}, TargetPaths{TargetFilename: "/some/other/path/index.json", SubResourceBaseTarget: "/some/other/path", Link: "/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}, TargetPaths{TargetFilename: "/some/other/path/index.json", SubResourceBaseTarget: "/some/other/path", Link: "/some/other/path/index.json"}},
+ {
+ "HTML page with URL without suffix and no trailing slash", TargetPathDescriptor{
+ Kind: KindPage,
+ Dir: "/sect/",
+ BaseName: "mypage",
+ URL: "/some/other/path",
+ Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/some/other/path/index.html", SubResourceBaseTarget: "/some/other/path", Link: "/some/other/path/"}},
+ {
+ "HTML page with expanded permalink", TargetPathDescriptor{
+ Kind: KindPage,
+ Dir: "/a/b",
+ BaseName: "mypage",
+ ExpandedPermalink: "/2017/10/my-title/",
+ Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/2017/10/my-title/index.html", SubResourceBaseTarget: "/2017/10/my-title", Link: "/2017/10/my-title/"}},
+ {
+ "Paginated HTML home", TargetPathDescriptor{
+ Kind: KindHome,
+ BaseName: "_index",
+ Type: output.HTMLFormat,
+ Addends: "page/3"}, TargetPaths{TargetFilename: "/page/3/index.html", SubResourceBaseTarget: "/page/3", Link: "/page/3/"}},
+ {
+ "Paginated Taxonomy list", TargetPathDescriptor{
+ Kind: KindTaxonomy,
+ BaseName: "_index",
+ Sections: []string{"tags", "hugo"},
+ Type: output.HTMLFormat,
+ Addends: "page/3"}, TargetPaths{TargetFilename: "/tags/hugo/page/3/index.html", SubResourceBaseTarget: "/tags/hugo/page/3", Link: "/tags/hugo/page/3/"}},
+ {
+ "Regular page with addend", TargetPathDescriptor{
+ Kind: KindPage,
+ Dir: "/a/b",
+ BaseName: "mypage",
+ Addends: "c/d/e",
+ Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/a/b/mypage/c/d/e/index.html", SubResourceBaseTarget: "/a/b/mypage/c/d/e", Link: "/a/b/mypage/c/d/e/"}},
+ }
+
+ for i, test := range tests {
+ t.Run(fmt.Sprintf("langPrefixPath=%s,langPrefixLink=%s,uglyURLs=%t,name=%s", langPrefixPath, langPrefixLink, uglyURLs, test.name),
+ func(t *testing.T) {
+
+ test.d.ForcePrefix = true
+ test.d.PathSpec = pathSpec
+ test.d.UglyURLs = uglyURLs
+ test.d.PrefixFilePath = langPrefixPath
+ test.d.PrefixLink = langPrefixLink
+ 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 test.d.Type.MediaType.Suffix() != "" && (!strings.HasPrefix(expected.TargetFilename, "/index") || test.d.Addends != "") && test.d.URL == "" && isUgly {
+ expected.TargetFilename = strings.Replace(expected.TargetFilename,
+ "/"+test.d.Type.BaseName+"."+test.d.Type.MediaType.Suffix(),
+ "."+test.d.Type.MediaType.Suffix(), 1)
+ expected.Link = strings.TrimSuffix(expected.Link, "/") + "." + test.d.Type.MediaType.Suffix()
+
+ }
+
+ if test.d.PrefixFilePath != "" && !strings.HasPrefix(test.d.URL, "/"+test.d.PrefixFilePath) {
+ expected.TargetFilename = "/" + test.d.PrefixFilePath + expected.TargetFilename
+ expected.SubResourceBaseTarget = "/" + test.d.PrefixFilePath + expected.SubResourceBaseTarget
+ }
+
+ if test.d.PrefixLink != "" && !strings.HasPrefix(test.d.URL, "/"+test.d.PrefixLink) {
+ expected.Link = "/" + test.d.PrefixLink + expected.Link
+ }
+
+ expected.TargetFilename = filepath.FromSlash(expected.TargetFilename)
+ expected.SubResourceBaseTarget = filepath.FromSlash(expected.SubResourceBaseTarget)
+
+ pagePath := CreateTargetPaths(test.d)
+
+ if !eqTargetPaths(pagePath, expected) {
+ t.Fatalf("[%d] [%s] targetPath expected\n%#v, got:\n%#v", i, test.name, expected, pagePath)
+
+ }
+ })
+ }
+ }
+
+ }
+ }
+}
+
+func TestPageTargetPathPrefix(t *testing.T) {
+ pathSpec := newTestPathSpec()
+ tests := []struct {
+ name string
+ d TargetPathDescriptor
+ expected TargetPaths
+ }{
+ {"URL set, prefix both, no force", TargetPathDescriptor{Kind: KindPage, Type: output.JSONFormat, URL: "/mydir/my.json", ForcePrefix: false, PrefixFilePath: "pf", PrefixLink: "pl"},
+ TargetPaths{TargetFilename: "/mydir/my.json", SubResourceBaseTarget: "/mydir", SubResourceBaseLink: "/mydir", Link: "/mydir/my.json"}},
+ {"URL set, prefix both, force", TargetPathDescriptor{Kind: KindPage, Type: output.JSONFormat, URL: "/mydir/my.json", ForcePrefix: true, PrefixFilePath: "pf", PrefixLink: "pl"},
+ TargetPaths{TargetFilename: "/pf/mydir/my.json", SubResourceBaseTarget: "/pf/mydir", SubResourceBaseLink: "/pl/mydir", Link: "/pl/mydir/my.json"}},
+ }
+
+ for i, test := range tests {
+ t.Run(fmt.Sprintf(test.name),
+ func(t *testing.T) {
+ test.d.PathSpec = pathSpec
+ expected := test.expected
+ expected.TargetFilename = filepath.FromSlash(expected.TargetFilename)
+ expected.SubResourceBaseTarget = filepath.FromSlash(expected.SubResourceBaseTarget)
+
+ pagePath := CreateTargetPaths(test.d)
+
+ if pagePath != expected {
+ t.Fatalf("[%d] [%s] targetPath expected\n%#v, got:\n%#v", i, test.name, expected, pagePath)
+ }
+ })
+ }
+
+}
+
+func eqTargetPaths(p1, p2 TargetPaths) bool {
+
+ if p1.Link != p2.Link {
+ return false
+ }
+
+ if p1.SubResourceBaseTarget != p2.SubResourceBaseTarget {
+ return false
+ }
+
+ if p1.TargetFilename != p2.TargetFilename {
+ return false
+ }
+
+ return true
+}
diff --git a/resources/page/page_wrappers.autogen.go b/resources/page/page_wrappers.autogen.go
new file mode 100644
index 000000000..d7fcb5201
--- /dev/null
+++ b/resources/page/page_wrappers.autogen.go
@@ -0,0 +1,97 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This file is autogenerated.
+
+package page
+
+import (
+ "github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/helpers"
+ "html/template"
+ "os"
+)
+
+// NewDeprecatedWarningPage adds deprecation warnings to the given implementation.
+func NewDeprecatedWarningPage(p DeprecatedWarningPageMethods) DeprecatedWarningPageMethods {
+ return &pageDeprecated{p: p}
+}
+
+type pageDeprecated struct {
+ p DeprecatedWarningPageMethods
+}
+
+func (p *pageDeprecated) Filename() string {
+ helpers.Deprecated("Page", ".Filename", "Use .File.Filename", false)
+ return p.p.Filename()
+}
+func (p *pageDeprecated) Dir() string {
+ helpers.Deprecated("Page", ".Dir", "Use .File.Dir", false)
+ return p.p.Dir()
+}
+func (p *pageDeprecated) IsDraft() bool {
+ helpers.Deprecated("Page", ".IsDraft", "Use .Draft.", false)
+ return p.p.IsDraft()
+}
+func (p *pageDeprecated) Extension() string {
+ helpers.Deprecated("Page", ".Extension", "Use .File.Extension", false)
+ return p.p.Extension()
+}
+func (p *pageDeprecated) Hugo() hugo.Info {
+ helpers.Deprecated("Page", ".Hugo", "Use the global hugo function.", false)
+ return p.p.Hugo()
+}
+func (p *pageDeprecated) Ext() string {
+ helpers.Deprecated("Page", ".Ext", "Use .File.Ext", false)
+ return p.p.Ext()
+}
+func (p *pageDeprecated) LanguagePrefix() string {
+ helpers.Deprecated("Page", ".LanguagePrefix", "Use .Site.LanguagePrefix.", false)
+ return p.p.LanguagePrefix()
+}
+func (p *pageDeprecated) GetParam(arg0 string) interface{} {
+ helpers.Deprecated("Page", ".GetParam", "Use .Param or .Params.myParam.", false)
+ return p.p.GetParam(arg0)
+}
+func (p *pageDeprecated) LogicalName() string {
+ helpers.Deprecated("Page", ".LogicalName", "Use .File.LogicalName", false)
+ return p.p.LogicalName()
+}
+func (p *pageDeprecated) BaseFileName() string {
+ helpers.Deprecated("Page", ".BaseFileName", "Use .File.BaseFileName", false)
+ return p.p.BaseFileName()
+}
+func (p *pageDeprecated) RSSLink() template.URL {
+ helpers.Deprecated("Page", ".RSSLink", "Use the Output Format's link, e.g. something like: \n {{ with .OutputFormats.Get \"RSS\" }}{{ .RelPermalink }}{{ end }}", false)
+ return p.p.RSSLink()
+}
+func (p *pageDeprecated) TranslationBaseName() string {
+ helpers.Deprecated("Page", ".TranslationBaseName", "Use .File.TranslationBaseName", false)
+ return p.p.TranslationBaseName()
+}
+func (p *pageDeprecated) URL() string {
+ helpers.Deprecated("Page", ".URL", "Use .Permalink or .RelPermalink. If what you want is the front matter URL value, use .Params.url", false)
+ return p.p.URL()
+}
+func (p *pageDeprecated) ContentBaseName() string {
+ helpers.Deprecated("Page", ".ContentBaseName", "Use .File.ContentBaseName", false)
+ return p.p.ContentBaseName()
+}
+func (p *pageDeprecated) UniqueID() string {
+ helpers.Deprecated("Page", ".UniqueID", "Use .File.UniqueID", false)
+ return p.p.UniqueID()
+}
+func (p *pageDeprecated) FileInfo() os.FileInfo {
+ helpers.Deprecated("Page", ".FileInfo", "Use .File.FileInfo", false)
+ return p.p.FileInfo()
+}
diff --git a/resources/page/pagegroup.go b/resources/page/pagegroup.go
new file mode 100644
index 000000000..3d87d9014
--- /dev/null
+++ b/resources/page/pagegroup.go
@@ -0,0 +1,408 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "errors"
+ "fmt"
+ "reflect"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/gohugoio/hugo/common/collections"
+ "github.com/gohugoio/hugo/compare"
+
+ "github.com/gohugoio/hugo/resources/resource"
+)
+
+var (
+ _ collections.Slicer = PageGroup{}
+ _ compare.ProbablyEqer = PageGroup{}
+ _ compare.ProbablyEqer = PagesGroup{}
+)
+
+// PageGroup represents a group of pages, grouped by the key.
+// The key is typically a year or similar.
+type PageGroup struct {
+ Key interface{}
+ 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)).Elem()
+ pagesType = reflect.TypeOf(Pages{})
+)
+
+// 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.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, pagesType))
+ case reflect.Method:
+ tmp = reflect.MakeMap(reflect.MapOf(e.Type.Out(0), pagesType))
+ }
+
+ 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(pagesType, 0, 0))
+ }
+ tmp.SetMapIndex(fv, reflect.Append(tmp.MapIndex(fv), ppv))
+ }
+
+ sortedKeys := sortKeys(tmp.MapKeys(), direction)
+ r := make([]PageGroup, len(sortedKeys))
+ for i, k := range sortedKeys {
+ r[i] = PageGroup{Key: k.Interface(), Pages: tmp.MapIndex(k).Interface().(Pages)}
+ }
+
+ 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 := resource.GetParamToLower(e, key)
+ if param != nil {
+ if _, ok := param.([]string); !ok {
+ keyt = reflect.TypeOf(param)
+ tmp = reflect.MakeMap(reflect.MapOf(keyt, pagesType))
+ break
+ }
+ }
+ }
+ if !tmp.IsValid() {
+ return nil, errors.New("there is no such a param")
+ }
+
+ for _, e := range p {
+ param := resource.GetParam(e, key)
+
+ if param == nil || reflect.TypeOf(param) != keyt {
+ continue
+ }
+ v := reflect.ValueOf(param)
+ if !tmp.MapIndex(v).IsValid() {
+ tmp.SetMapIndex(v, reflect.MakeSlice(pagesType, 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().(Pages)})
+ }
+
+ 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].(Page))
+ 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.(Page))
+ 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 := resource.GetParamToLower(e, key)
+ if _, ok := param.(time.Time); ok {
+ r = append(r, e)
+ }
+ }
+ pdate := func(p1, p2 Page) bool {
+ p1p, p2p := p1.(Page), p2.(Page)
+ return resource.GetParamToLower(p1p, key).(time.Time).Unix() < resource.GetParamToLower(p2p, key).(time.Time).Unix()
+ }
+ pageBy(pdate).Sort(r)
+ return r
+ }
+ formatter := func(p Page) string {
+ return resource.GetParamToLower(p, key).(time.Time).Format(format)
+ }
+ return p.groupByDateField(sorter, formatter, order...)
+}
+
+// ProbablyEq wraps comare.ProbablyEqer
+func (p PageGroup) ProbablyEq(other interface{}) bool {
+ otherP, ok := other.(PageGroup)
+ if !ok {
+ return false
+ }
+
+ if p.Key != otherP.Key {
+ return false
+ }
+
+ return p.Pages.ProbablyEq(otherP.Pages)
+
+}
+
+// Slice is not meant to be used externally. It's a bridge function
+// for the template functions. See collections.Slice.
+func (p PageGroup) Slice(in interface{}) (interface{}, error) {
+ switch items := in.(type) {
+ case PageGroup:
+ return items, nil
+ case []interface{}:
+ groups := make(PagesGroup, len(items))
+ for i, v := range items {
+ g, ok := v.(PageGroup)
+ if !ok {
+ return nil, fmt.Errorf("type %T is not a PageGroup", v)
+ }
+ groups[i] = g
+ }
+ return groups, nil
+ default:
+ return nil, fmt.Errorf("invalid slice type %T", items)
+ }
+}
+
+// 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
+}
+
+// ProbablyEq wraps comare.ProbablyEqer
+func (psg PagesGroup) ProbablyEq(other interface{}) bool {
+ otherPsg, ok := other.(PagesGroup)
+ if !ok {
+ return false
+ }
+
+ if len(psg) != len(otherPsg) {
+ return false
+ }
+
+ for i := range psg {
+ if !psg[i].ProbablyEq(otherPsg[i]) {
+ return false
+ }
+ }
+
+ return true
+
+}
+
+// ToPagesGroup tries to convert seq into a PagesGroup.
+func ToPagesGroup(seq interface{}) (PagesGroup, error) {
+ switch v := seq.(type) {
+ case nil:
+ return nil, nil
+ case PagesGroup:
+ return v, nil
+ case []PageGroup:
+ return PagesGroup(v), nil
+ case []interface{}:
+ l := len(v)
+ if l == 0 {
+ break
+ }
+ switch v[0].(type) {
+ case PageGroup:
+ pagesGroup := make(PagesGroup, l)
+ for i, ipg := range v {
+ if pg, ok := ipg.(PageGroup); ok {
+ pagesGroup[i] = pg
+ } else {
+ return nil, fmt.Errorf("unsupported type in paginate from slice, got %T instead of PageGroup", ipg)
+ }
+ }
+ return pagesGroup, nil
+ }
+ }
+
+ return nil, nil
+}
diff --git a/resources/page/pagegroup_test.go b/resources/page/pagegroup_test.go
new file mode 100644
index 000000000..51ac09034
--- /dev/null
+++ b/resources/page/pagegroup_test.go
@@ -0,0 +1,409 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "reflect"
+ "strings"
+ "testing"
+
+ "github.com/spf13/cast"
+ "github.com/stretchr/testify/require"
+)
+
+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 {
+ var pages Pages
+ for _, src := range pageGroupTestSources {
+ p := newTestPage()
+ p.path = src.path
+ if p.path != "" {
+ p.section = strings.Split(strings.TrimPrefix(p.path, "/"), "/")[0]
+ }
+ p.weight = src.weight
+ p.date = cast.ToTime(src.date)
+ p.pubDate = 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\n%#v, got\n%#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 TestGroupByParamCalledWithUnavailableKey(t *testing.T) {
+ t.Parallel()
+ pages := preparePageGroupTestPages(t)
+ _, err := pages.GroupByParam("UnavailableKey")
+ 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) {
+ assert := require.New(t)
+ testStr := "TestString"
+ p := newTestPage()
+ p.params["custom_param"] = testStr
+ pages := Pages{p}
+
+ groups, err := pages.GroupByParam("custom_param")
+
+ assert.NoError(err)
+ assert.Equal(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/resources/page/pagemeta/page_frontmatter.go b/resources/page/pagemeta/page_frontmatter.go
new file mode 100644
index 000000000..1ce3fbee4
--- /dev/null
+++ b/resources/page/pagemeta/page_frontmatter.go
@@ -0,0 +1,427 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pagemeta
+
+import (
+ "strings"
+ "time"
+
+ "github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/resources/resource"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/spf13/cast"
+)
+
+// FrontMatterHandler maps front matter into Page fields and .Params.
+// Note that we currently have only extracted the date logic.
+type FrontMatterHandler struct {
+ fmConfig frontmatterConfig
+
+ dateHandler frontMatterFieldHandler
+ lastModHandler frontMatterFieldHandler
+ publishDateHandler frontMatterFieldHandler
+ expiryDateHandler frontMatterFieldHandler
+
+ // A map of all date keys configured, including any custom.
+ allDateKeys map[string]bool
+
+ logger *loggers.Logger
+}
+
+// FrontMatterDescriptor describes how to handle front matter for a given Page.
+// It has pointers to values in the receiving page which gets updated.
+type FrontMatterDescriptor struct {
+
+ // This the Page's front matter.
+ Frontmatter map[string]interface{}
+
+ // This is the Page's base filename (BaseFilename), e.g. page.md., or
+ // if page is a leaf bundle, the bundle folder name (ContentBaseName).
+ BaseFilename string
+
+ // The content file's mod time.
+ ModTime time.Time
+
+ // May be set from the author date in Git.
+ GitAuthorDate time.Time
+
+ // The below are pointers to values on Page and will be modified.
+
+ // This is the Page's params.
+ Params map[string]interface{}
+
+ // This is the Page's dates.
+ Dates *resource.Dates
+
+ // This is the Page's Slug etc.
+ PageURLs *URLPath
+}
+
+var (
+ dateFieldAliases = map[string][]string{
+ fmDate: {},
+ fmLastmod: {"modified"},
+ fmPubDate: {"pubdate", "published"},
+ fmExpiryDate: {"unpublishdate"},
+ }
+)
+
+// HandleDates updates all the dates given the current configuration and the
+// supplied front matter params. Note that this requires all lower-case keys
+// in the params map.
+func (f FrontMatterHandler) HandleDates(d *FrontMatterDescriptor) error {
+ if d.Dates == nil {
+ panic("missing dates")
+ }
+
+ if f.dateHandler == nil {
+ panic("missing date handler")
+ }
+
+ if _, err := f.dateHandler(d); err != nil {
+ return err
+ }
+
+ if _, err := f.lastModHandler(d); err != nil {
+ return err
+ }
+
+ if _, err := f.publishDateHandler(d); err != nil {
+ return err
+ }
+
+ if _, err := f.expiryDateHandler(d); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// IsDateKey returns whether the given front matter key is considered a date by the current
+// configuration.
+func (f FrontMatterHandler) IsDateKey(key string) bool {
+ return f.allDateKeys[key]
+}
+
+// A Zero date is a signal that the name can not be parsed.
+// This follows the format as outlined in Jekyll, https://jekyllrb.com/docs/posts/:
+// "Where YEAR is a four-digit number, MONTH and DAY are both two-digit numbers"
+func dateAndSlugFromBaseFilename(name string) (time.Time, string) {
+ withoutExt, _ := helpers.FileAndExt(name)
+
+ if len(withoutExt) < 10 {
+ // This can not be a date.
+ return time.Time{}, ""
+ }
+
+ // Note: Hugo currently have no custom timezone support.
+ // We will have to revisit this when that is in place.
+ d, err := time.Parse("2006-01-02", withoutExt[:10])
+ if err != nil {
+ return time.Time{}, ""
+ }
+
+ // Be a little lenient with the format here.
+ slug := strings.Trim(withoutExt[10:], " -_")
+
+ return d, slug
+}
+
+type frontMatterFieldHandler func(d *FrontMatterDescriptor) (bool, error)
+
+func (f FrontMatterHandler) newChainedFrontMatterFieldHandler(handlers ...frontMatterFieldHandler) frontMatterFieldHandler {
+ return func(d *FrontMatterDescriptor) (bool, error) {
+ for _, h := range handlers {
+ // First successful handler wins.
+ success, err := h(d)
+ if err != nil {
+ f.logger.ERROR.Println(err)
+ } else if success {
+ return true, nil
+ }
+ }
+ return false, nil
+ }
+}
+
+type frontmatterConfig struct {
+ date []string
+ lastmod []string
+ publishDate []string
+ expiryDate []string
+}
+
+const (
+ // These are all the date handler identifiers
+ // All identifiers not starting with a ":" maps to a front matter parameter.
+ fmDate = "date"
+ fmPubDate = "publishdate"
+ fmLastmod = "lastmod"
+ fmExpiryDate = "expirydate"
+
+ // Gets date from filename, e.g 218-02-22-mypage.md
+ fmFilename = ":filename"
+
+ // Gets date from file OS mod time.
+ fmModTime = ":filemodtime"
+
+ // Gets date from Git
+ fmGitAuthorDate = ":git"
+)
+
+// This is the config you get when doing nothing.
+func newDefaultFrontmatterConfig() frontmatterConfig {
+ return frontmatterConfig{
+ date: []string{fmDate, fmPubDate, fmLastmod},
+ lastmod: []string{fmGitAuthorDate, fmLastmod, fmDate, fmPubDate},
+ publishDate: []string{fmPubDate, fmDate},
+ expiryDate: []string{fmExpiryDate},
+ }
+}
+
+func newFrontmatterConfig(cfg config.Provider) (frontmatterConfig, error) {
+ c := newDefaultFrontmatterConfig()
+ defaultConfig := c
+
+ if cfg.IsSet("frontmatter") {
+ fm := cfg.GetStringMap("frontmatter")
+ for k, v := range fm {
+ loki := strings.ToLower(k)
+ switch loki {
+ case fmDate:
+ c.date = toLowerSlice(v)
+ case fmPubDate:
+ c.publishDate = toLowerSlice(v)
+ case fmLastmod:
+ c.lastmod = toLowerSlice(v)
+ case fmExpiryDate:
+ c.expiryDate = toLowerSlice(v)
+ }
+ }
+ }
+
+ expander := func(c, d []string) []string {
+ out := expandDefaultValues(c, d)
+ out = addDateFieldAliases(out)
+ return out
+ }
+
+ c.date = expander(c.date, defaultConfig.date)
+ c.publishDate = expander(c.publishDate, defaultConfig.publishDate)
+ c.lastmod = expander(c.lastmod, defaultConfig.lastmod)
+ c.expiryDate = expander(c.expiryDate, defaultConfig.expiryDate)
+
+ return c, nil
+}
+
+func addDateFieldAliases(values []string) []string {
+ var complete []string
+
+ for _, v := range values {
+ complete = append(complete, v)
+ if aliases, found := dateFieldAliases[v]; found {
+ complete = append(complete, aliases...)
+ }
+ }
+ return helpers.UniqueStrings(complete)
+}
+
+func expandDefaultValues(values []string, defaults []string) []string {
+ var out []string
+ for _, v := range values {
+ if v == ":default" {
+ out = append(out, defaults...)
+ } else {
+ out = append(out, v)
+ }
+ }
+ return out
+}
+
+func toLowerSlice(in interface{}) []string {
+ out := cast.ToStringSlice(in)
+ for i := 0; i < len(out); i++ {
+ out[i] = strings.ToLower(out[i])
+ }
+
+ return out
+}
+
+// NewFrontmatterHandler creates a new FrontMatterHandler with the given logger and configuration.
+// If no logger is provided, one will be created.
+func NewFrontmatterHandler(logger *loggers.Logger, cfg config.Provider) (FrontMatterHandler, error) {
+
+ if logger == nil {
+ logger = loggers.NewErrorLogger()
+ }
+
+ frontMatterConfig, err := newFrontmatterConfig(cfg)
+ if err != nil {
+ return FrontMatterHandler{}, err
+ }
+
+ allDateKeys := make(map[string]bool)
+ addKeys := func(vals []string) {
+ for _, k := range vals {
+ if !strings.HasPrefix(k, ":") {
+ allDateKeys[k] = true
+ }
+ }
+ }
+
+ addKeys(frontMatterConfig.date)
+ addKeys(frontMatterConfig.expiryDate)
+ addKeys(frontMatterConfig.lastmod)
+ addKeys(frontMatterConfig.publishDate)
+
+ f := FrontMatterHandler{logger: logger, fmConfig: frontMatterConfig, allDateKeys: allDateKeys}
+
+ if err := f.createHandlers(); err != nil {
+ return f, err
+ }
+
+ return f, nil
+}
+
+func (f *FrontMatterHandler) createHandlers() error {
+ var err error
+
+ if f.dateHandler, err = f.createDateHandler(f.fmConfig.date,
+ func(d *FrontMatterDescriptor, t time.Time) {
+ d.Dates.FDate = t
+ setParamIfNotSet(fmDate, t, d)
+ }); err != nil {
+ return err
+ }
+
+ if f.lastModHandler, err = f.createDateHandler(f.fmConfig.lastmod,
+ func(d *FrontMatterDescriptor, t time.Time) {
+ setParamIfNotSet(fmLastmod, t, d)
+ d.Dates.FLastmod = t
+ }); err != nil {
+ return err
+ }
+
+ if f.publishDateHandler, err = f.createDateHandler(f.fmConfig.publishDate,
+ func(d *FrontMatterDescriptor, t time.Time) {
+ setParamIfNotSet(fmPubDate, t, d)
+ d.Dates.FPublishDate = t
+ }); err != nil {
+ return err
+ }
+
+ if f.expiryDateHandler, err = f.createDateHandler(f.fmConfig.expiryDate,
+ func(d *FrontMatterDescriptor, t time.Time) {
+ setParamIfNotSet(fmExpiryDate, t, d)
+ d.Dates.FExpiryDate = t
+ }); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func setParamIfNotSet(key string, value interface{}, d *FrontMatterDescriptor) {
+ if _, found := d.Params[key]; found {
+ return
+ }
+ d.Params[key] = value
+}
+
+func (f FrontMatterHandler) createDateHandler(identifiers []string, setter func(d *FrontMatterDescriptor, t time.Time)) (frontMatterFieldHandler, error) {
+ var h *frontmatterFieldHandlers
+ var handlers []frontMatterFieldHandler
+
+ for _, identifier := range identifiers {
+ switch identifier {
+ case fmFilename:
+ handlers = append(handlers, h.newDateFilenameHandler(setter))
+ case fmModTime:
+ handlers = append(handlers, h.newDateModTimeHandler(setter))
+ case fmGitAuthorDate:
+ handlers = append(handlers, h.newDateGitAuthorDateHandler(setter))
+ default:
+ handlers = append(handlers, h.newDateFieldHandler(identifier, setter))
+ }
+ }
+
+ return f.newChainedFrontMatterFieldHandler(handlers...), nil
+
+}
+
+type frontmatterFieldHandlers int
+
+func (f *frontmatterFieldHandlers) newDateFieldHandler(key string, setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler {
+ return func(d *FrontMatterDescriptor) (bool, error) {
+ v, found := d.Frontmatter[key]
+
+ if !found {
+ return false, nil
+ }
+
+ date, err := cast.ToTimeE(v)
+ if err != nil {
+ return false, nil
+ }
+
+ // We map several date keys to one, so, for example,
+ // "expirydate", "unpublishdate" will all set .ExpiryDate (first found).
+ setter(d, date)
+
+ // This is the params key as set in front matter.
+ d.Params[key] = date
+
+ return true, nil
+ }
+}
+
+func (f *frontmatterFieldHandlers) newDateFilenameHandler(setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler {
+ return func(d *FrontMatterDescriptor) (bool, error) {
+ date, slug := dateAndSlugFromBaseFilename(d.BaseFilename)
+ if date.IsZero() {
+ return false, nil
+ }
+
+ setter(d, date)
+
+ if _, found := d.Frontmatter["slug"]; !found {
+ // Use slug from filename
+ d.PageURLs.Slug = slug
+ }
+
+ return true, nil
+ }
+}
+
+func (f *frontmatterFieldHandlers) newDateModTimeHandler(setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler {
+ return func(d *FrontMatterDescriptor) (bool, error) {
+ if d.ModTime.IsZero() {
+ return false, nil
+ }
+ setter(d, d.ModTime)
+ return true, nil
+ }
+}
+
+func (f *frontmatterFieldHandlers) newDateGitAuthorDateHandler(setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler {
+ return func(d *FrontMatterDescriptor) (bool, error) {
+ if d.GitAuthorDate.IsZero() {
+ return false, nil
+ }
+ setter(d, d.GitAuthorDate)
+ return true, nil
+ }
+}
diff --git a/resources/page/pagemeta/page_frontmatter_test.go b/resources/page/pagemeta/page_frontmatter_test.go
new file mode 100644
index 000000000..313f704d9
--- /dev/null
+++ b/resources/page/pagemeta/page_frontmatter_test.go
@@ -0,0 +1,262 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pagemeta
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/gohugoio/hugo/resources/resource"
+ "github.com/spf13/viper"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestDateAndSlugFromBaseFilename(t *testing.T) {
+
+ t.Parallel()
+
+ assert := require.New(t)
+
+ tests := []struct {
+ name string
+ date string
+ slug string
+ }{
+ {"page.md", "0001-01-01", ""},
+ {"2012-09-12-page.md", "2012-09-12", "page"},
+ {"2018-02-28-page.md", "2018-02-28", "page"},
+ {"2018-02-28_page.md", "2018-02-28", "page"},
+ {"2018-02-28 page.md", "2018-02-28", "page"},
+ {"2018-02-28page.md", "2018-02-28", "page"},
+ {"2018-02-28-.md", "2018-02-28", ""},
+ {"2018-02-28-.md", "2018-02-28", ""},
+ {"2018-02-28.md", "2018-02-28", ""},
+ {"2018-02-28-page", "2018-02-28", "page"},
+ {"2012-9-12-page.md", "0001-01-01", ""},
+ {"asdfasdf.md", "0001-01-01", ""},
+ }
+
+ for i, test := range tests {
+ expecteFDate, err := time.Parse("2006-01-02", test.date)
+ assert.NoError(err)
+
+ errMsg := fmt.Sprintf("Test %d", i)
+ gotDate, gotSlug := dateAndSlugFromBaseFilename(test.name)
+
+ assert.Equal(expecteFDate, gotDate, errMsg)
+ assert.Equal(test.slug, gotSlug, errMsg)
+
+ }
+}
+
+func newTestFd() *FrontMatterDescriptor {
+ return &FrontMatterDescriptor{
+ Frontmatter: make(map[string]interface{}),
+ Params: make(map[string]interface{}),
+ Dates: &resource.Dates{},
+ PageURLs: &URLPath{},
+ }
+}
+
+func TestFrontMatterNewConfig(t *testing.T) {
+ assert := require.New(t)
+
+ cfg := viper.New()
+
+ cfg.Set("frontmatter", map[string]interface{}{
+ "date": []string{"publishDate", "LastMod"},
+ "Lastmod": []string{"publishDate"},
+ "expiryDate": []string{"lastMod"},
+ "publishDate": []string{"date"},
+ })
+
+ fc, err := newFrontmatterConfig(cfg)
+ assert.NoError(err)
+ assert.Equal([]string{"publishdate", "pubdate", "published", "lastmod", "modified"}, fc.date)
+ assert.Equal([]string{"publishdate", "pubdate", "published"}, fc.lastmod)
+ assert.Equal([]string{"lastmod", "modified"}, fc.expiryDate)
+ assert.Equal([]string{"date"}, fc.publishDate)
+
+ // Default
+ cfg = viper.New()
+ fc, err = newFrontmatterConfig(cfg)
+ assert.NoError(err)
+ assert.Equal([]string{"date", "publishdate", "pubdate", "published", "lastmod", "modified"}, fc.date)
+ assert.Equal([]string{":git", "lastmod", "modified", "date", "publishdate", "pubdate", "published"}, fc.lastmod)
+ assert.Equal([]string{"expirydate", "unpublishdate"}, fc.expiryDate)
+ assert.Equal([]string{"publishdate", "pubdate", "published", "date"}, fc.publishDate)
+
+ // :default keyword
+ cfg.Set("frontmatter", map[string]interface{}{
+ "date": []string{"d1", ":default"},
+ "lastmod": []string{"d2", ":default"},
+ "expiryDate": []string{"d3", ":default"},
+ "publishDate": []string{"d4", ":default"},
+ })
+ fc, err = newFrontmatterConfig(cfg)
+ assert.NoError(err)
+ assert.Equal([]string{"d1", "date", "publishdate", "pubdate", "published", "lastmod", "modified"}, fc.date)
+ assert.Equal([]string{"d2", ":git", "lastmod", "modified", "date", "publishdate", "pubdate", "published"}, fc.lastmod)
+ assert.Equal([]string{"d3", "expirydate", "unpublishdate"}, fc.expiryDate)
+ assert.Equal([]string{"d4", "publishdate", "pubdate", "published", "date"}, fc.publishDate)
+
+}
+
+func TestFrontMatterDatesHandlers(t *testing.T) {
+ assert := require.New(t)
+
+ for _, handlerID := range []string{":filename", ":fileModTime", ":git"} {
+
+ cfg := viper.New()
+
+ cfg.Set("frontmatter", map[string]interface{}{
+ "date": []string{handlerID, "date"},
+ })
+
+ handler, err := NewFrontmatterHandler(nil, cfg)
+ assert.NoError(err)
+
+ d1, _ := time.Parse("2006-01-02", "2018-02-01")
+ d2, _ := time.Parse("2006-01-02", "2018-02-02")
+
+ d := newTestFd()
+ switch strings.ToLower(handlerID) {
+ case ":filename":
+ d.BaseFilename = "2018-02-01-page.md"
+ case ":filemodtime":
+ d.ModTime = d1
+ case ":git":
+ d.GitAuthorDate = d1
+ }
+ d.Frontmatter["date"] = d2
+ assert.NoError(handler.HandleDates(d))
+ assert.Equal(d1, d.Dates.FDate)
+ assert.Equal(d2, d.Params["date"])
+
+ d = newTestFd()
+ d.Frontmatter["date"] = d2
+ assert.NoError(handler.HandleDates(d))
+ assert.Equal(d2, d.Dates.FDate)
+ assert.Equal(d2, d.Params["date"])
+
+ }
+}
+
+func TestFrontMatterDatesCustomConfig(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ cfg := viper.New()
+ cfg.Set("frontmatter", map[string]interface{}{
+ "date": []string{"mydate"},
+ "lastmod": []string{"publishdate"},
+ "publishdate": []string{"publishdate"},
+ })
+
+ handler, err := NewFrontmatterHandler(nil, cfg)
+ assert.NoError(err)
+
+ testDate, err := time.Parse("2006-01-02", "2018-02-01")
+ assert.NoError(err)
+
+ d := newTestFd()
+ d.Frontmatter["mydate"] = testDate
+ testDate = testDate.Add(24 * time.Hour)
+ d.Frontmatter["date"] = testDate
+ testDate = testDate.Add(24 * time.Hour)
+ d.Frontmatter["lastmod"] = testDate
+ testDate = testDate.Add(24 * time.Hour)
+ d.Frontmatter["publishdate"] = testDate
+ testDate = testDate.Add(24 * time.Hour)
+ d.Frontmatter["expirydate"] = testDate
+
+ assert.NoError(handler.HandleDates(d))
+
+ assert.Equal(1, d.Dates.FDate.Day())
+ assert.Equal(4, d.Dates.FLastmod.Day())
+ assert.Equal(4, d.Dates.FPublishDate.Day())
+ assert.Equal(5, d.Dates.FExpiryDate.Day())
+
+ assert.Equal(d.Dates.FDate, d.Params["date"])
+ assert.Equal(d.Dates.FDate, d.Params["mydate"])
+ assert.Equal(d.Dates.FPublishDate, d.Params["publishdate"])
+ assert.Equal(d.Dates.FExpiryDate, d.Params["expirydate"])
+
+ assert.False(handler.IsDateKey("date")) // This looks odd, but is configured like this.
+ assert.True(handler.IsDateKey("mydate"))
+ assert.True(handler.IsDateKey("publishdate"))
+ assert.True(handler.IsDateKey("pubdate"))
+
+}
+
+func TestFrontMatterDatesDefaultKeyword(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ cfg := viper.New()
+
+ cfg.Set("frontmatter", map[string]interface{}{
+ "date": []string{"mydate", ":default"},
+ "publishdate": []string{":default", "mypubdate"},
+ })
+
+ handler, err := NewFrontmatterHandler(nil, cfg)
+ assert.NoError(err)
+
+ testDate, _ := time.Parse("2006-01-02", "2018-02-01")
+ d := newTestFd()
+ d.Frontmatter["mydate"] = testDate
+ d.Frontmatter["date"] = testDate.Add(1 * 24 * time.Hour)
+ d.Frontmatter["mypubdate"] = testDate.Add(2 * 24 * time.Hour)
+ d.Frontmatter["publishdate"] = testDate.Add(3 * 24 * time.Hour)
+
+ assert.NoError(handler.HandleDates(d))
+
+ assert.Equal(1, d.Dates.FDate.Day())
+ assert.Equal(2, d.Dates.FLastmod.Day())
+ assert.Equal(4, d.Dates.FPublishDate.Day())
+ assert.True(d.Dates.FExpiryDate.IsZero())
+
+}
+
+func TestExpandDefaultValues(t *testing.T) {
+ assert := require.New(t)
+ assert.Equal([]string{"a", "b", "c", "d"}, expandDefaultValues([]string{"a", ":default", "d"}, []string{"b", "c"}))
+ assert.Equal([]string{"a", "b", "c"}, expandDefaultValues([]string{"a", "b", "c"}, []string{"a", "b", "c"}))
+ assert.Equal([]string{"b", "c", "a", "b", "c", "d"}, expandDefaultValues([]string{":default", "a", ":default", "d"}, []string{"b", "c"}))
+
+}
+
+func TestFrontMatterDateFieldHandler(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ handlers := new(frontmatterFieldHandlers)
+
+ fd := newTestFd()
+ d, _ := time.Parse("2006-01-02", "2018-02-01")
+ fd.Frontmatter["date"] = d
+ h := handlers.newDateFieldHandler("date", func(d *FrontMatterDescriptor, t time.Time) { d.Dates.FDate = t })
+
+ handled, err := h(fd)
+ assert.True(handled)
+ assert.NoError(err)
+ assert.Equal(d, fd.Dates.FDate)
+}
diff --git a/resources/page/pagemeta/pagemeta.go b/resources/page/pagemeta/pagemeta.go
new file mode 100644
index 000000000..07e5c5673
--- /dev/null
+++ b/resources/page/pagemeta/pagemeta.go
@@ -0,0 +1,21 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pagemeta
+
+type URLPath struct {
+ URL string
+ Permalink string
+ Slug string
+ Section string
+}
diff --git a/resources/page/pages.go b/resources/page/pages.go
new file mode 100644
index 000000000..ccfecdf2b
--- /dev/null
+++ b/resources/page/pages.go
@@ -0,0 +1,145 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "fmt"
+ "math/rand"
+
+ "github.com/gohugoio/hugo/compare"
+
+ "github.com/gohugoio/hugo/resources/resource"
+)
+
+var (
+ _ resource.ResourcesConverter = Pages{}
+ _ compare.ProbablyEqer = Pages{}
+)
+
+// Pages is a slice of pages. This is the most common list type in Hugo.
+type Pages []Page
+
+func (ps Pages) String() string {
+ return fmt.Sprintf("Pages(%d)", len(ps))
+}
+
+// Used in tests.
+func (ps Pages) shuffle() {
+ for i := range ps {
+ j := rand.Intn(i + 1)
+ ps[i], ps[j] = ps[j], ps[i]
+ }
+}
+
+// ToResources wraps resource.ResourcesConverter
+func (pages Pages) ToResources() resource.Resources {
+ r := make(resource.Resources, len(pages))
+ for i, p := range pages {
+ r[i] = p
+ }
+ return r
+}
+
+// ToPages tries to convert seq into Pages.
+func ToPages(seq interface{}) (Pages, error) {
+ if seq == nil {
+ return Pages{}, nil
+ }
+
+ switch v := seq.(type) {
+ case Pages:
+ return v, nil
+ case *Pages:
+ return *(v), nil
+ case WeightedPages:
+ return v.Pages(), nil
+ case PageGroup:
+ return v.Pages, nil
+ case []interface{}:
+ pages := make(Pages, len(v))
+ success := true
+ for i, vv := range v {
+ p, ok := vv.(Page)
+ if !ok {
+ success = false
+ break
+ }
+ pages[i] = p
+ }
+ if success {
+ return pages, nil
+ }
+ }
+
+ return nil, fmt.Errorf("cannot convert type %T to Pages", seq)
+}
+
+func (p Pages) Group(key interface{}, in interface{}) (interface{}, error) {
+ pages, err := ToPages(in)
+ if err != nil {
+ return nil, err
+ }
+ return PageGroup{Key: key, Pages: pages}, nil
+}
+
+// Len returns the number of pages in the list.
+func (p Pages) Len() int {
+ return len(p)
+}
+
+// ProbablyEq wraps comare.ProbablyEqer
+func (pages Pages) ProbablyEq(other interface{}) bool {
+ otherPages, ok := other.(Pages)
+ if !ok {
+ return false
+ }
+
+ if len(pages) != len(otherPages) {
+ return false
+ }
+
+ step := 1
+
+ for i := 0; i < len(pages); i += step {
+ if !pages[i].Eq(otherPages[i]) {
+ return false
+ }
+
+ if i > 50 {
+ // This is most likely the same.
+ step = 50
+ }
+ }
+
+ return true
+}
+
+func (ps Pages) removeFirstIfFound(p Page) Pages {
+ ii := -1
+ for i, pp := range ps {
+ if p.Eq(pp) {
+ ii = i
+ break
+ }
+ }
+
+ if ii != -1 {
+ ps = append(ps[:ii], ps[ii+1:]...)
+ }
+ return ps
+}
+
+// PagesFactory somehow creates some Pages.
+// We do a lot of lazy Pages initialization in Hugo, so we need a type.
+type PagesFactory func() Pages
diff --git a/resources/page/pages_cache.go b/resources/page/pages_cache.go
new file mode 100644
index 000000000..e82d9a8cf
--- /dev/null
+++ b/resources/page/pages_cache.go
@@ -0,0 +1,136 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "sync"
+)
+
+type pageCacheEntry struct {
+ in []Pages
+ out Pages
+}
+
+func (entry pageCacheEntry) matches(pageLists []Pages) bool {
+ if len(entry.in) != len(pageLists) {
+ return false
+ }
+ for i, p := range pageLists {
+ if !pagesEqual(p, entry.in[i]) {
+ return false
+ }
+ }
+
+ return true
+}
+
+type pageCache struct {
+ sync.RWMutex
+ m map[string][]pageCacheEntry
+}
+
+func newPageCache() *pageCache {
+ return &pageCache{m: make(map[string][]pageCacheEntry)}
+}
+
+func (c *pageCache) clear() {
+ c.Lock()
+ defer c.Unlock()
+ c.m = make(map[string][]pageCacheEntry)
+}
+
+// get/getP gets a Pages slice from the cache matching the given key and
+// all the provided Pages slices.
+// If none found in cache, a copy of the first slice is created.
+//
+// If an apply func is provided, that func is applied to the newly created copy.
+//
+// The getP variant' apply func takes a pointer to Pages.
+//
+// The cache and the execution of the apply func is protected by a RWMutex.
+func (c *pageCache) get(key string, apply func(p Pages), pageLists ...Pages) (Pages, bool) {
+ return c.getP(key, func(p *Pages) {
+ if apply != nil {
+ apply(*p)
+ }
+ }, pageLists...)
+}
+
+func (c *pageCache) getP(key string, apply func(p *Pages), pageLists ...Pages) (Pages, bool) {
+ c.RLock()
+ if cached, ok := c.m[key]; ok {
+ for _, entry := range cached {
+ if entry.matches(pageLists) {
+ c.RUnlock()
+ return entry.out, true
+ }
+ }
+ }
+ c.RUnlock()
+
+ c.Lock()
+ defer c.Unlock()
+
+ // double-check
+ if cached, ok := c.m[key]; ok {
+ for _, entry := range cached {
+ if entry.matches(pageLists) {
+ return entry.out, true
+ }
+ }
+ }
+
+ p := pageLists[0]
+ pagesCopy := append(Pages(nil), p...)
+
+ if apply != nil {
+ apply(&pagesCopy)
+ }
+
+ entry := pageCacheEntry{in: pageLists, out: pagesCopy}
+ if v, ok := c.m[key]; ok {
+ c.m[key] = append(v, entry)
+ } else {
+ c.m[key] = []pageCacheEntry{entry}
+ }
+
+ return pagesCopy, false
+
+}
+
+// pagesEqual returns whether p1 and p2 are equal.
+func pagesEqual(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
+ }
+
+ for i := 0; i < len(p1); i++ {
+ if p1[i] != p2[i] {
+ return false
+ }
+ }
+ return true
+}
diff --git a/resources/page/pages_cache_test.go b/resources/page/pages_cache_test.go
new file mode 100644
index 000000000..b83283408
--- /dev/null
+++ b/resources/page/pages_cache_test.go
@@ -0,0 +1,86 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "strconv"
+ "sync"
+ "sync/atomic"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPageCache(t *testing.T) {
+ t.Parallel()
+ c1 := newPageCache()
+
+ changeFirst := func(p Pages) {
+ p[0].(*testPage).description = "changed"
+ }
+
+ var o1 uint64
+ var o2 uint64
+
+ var wg sync.WaitGroup
+
+ var l1 sync.Mutex
+ var l2 sync.Mutex
+
+ var testPageSets []Pages
+
+ for i := 0; i < 50; i++ {
+ testPageSets = append(testPageSets, createSortTestPages(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", nil, pages)
+ assert.Equal(t, !atomic.CompareAndSwapUint64(&o1, uint64(k), uint64(k+1)), c)
+ l1.Unlock()
+ p2, c2 := c1.get("k1", nil, p)
+ assert.True(t, c2)
+ assert.True(t, pagesEqual(p, p2))
+ assert.True(t, pagesEqual(p, pages))
+ assert.NotNil(t, p)
+
+ l2.Lock()
+ p3, c3 := c1.get("k2", changeFirst, pages)
+ assert.Equal(t, !atomic.CompareAndSwapUint64(&o2, uint64(k), uint64(k+1)), c3)
+ l2.Unlock()
+ assert.NotNil(t, p3)
+ assert.Equal(t, p3[0].(*testPage).description, "changed")
+ }
+ }()
+ }
+ wg.Wait()
+}
+
+func BenchmarkPageCache(b *testing.B) {
+ cache := newPageCache()
+ pages := make(Pages, 30)
+ for i := 0; i < 30; i++ {
+ pages[i] = &testPage{title: "p" + strconv.Itoa(i)}
+ }
+ key := "key"
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ cache.getP(key, nil, pages)
+ }
+}
diff --git a/resources/page/pages_language_merge.go b/resources/page/pages_language_merge.go
new file mode 100644
index 000000000..11393a754
--- /dev/null
+++ b/resources/page/pages_language_merge.go
@@ -0,0 +1,64 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "fmt"
+)
+
+var (
+ _ pagesLanguageMerger = (*Pages)(nil)
+)
+
+type pagesLanguageMerger interface {
+ MergeByLanguage(other Pages) Pages
+ // Needed for integration with the tpl package.
+ MergeByLanguageInterface(other interface{}) (interface{}, error)
+}
+
+// MergeByLanguage supplies missing translations in p1 with values from p2.
+// The result is sorted by the default sort order for pages.
+func (p1 Pages) MergeByLanguage(p2 Pages) Pages {
+ merge := func(pages *Pages) {
+ m := make(map[string]bool)
+ for _, p := range *pages {
+ m[p.TranslationKey()] = true
+ }
+
+ for _, p := range p2 {
+ if _, found := m[p.TranslationKey()]; !found {
+ *pages = append(*pages, p)
+ }
+ }
+
+ SortByDefault(*pages)
+ }
+
+ out, _ := spc.getP("pages.MergeByLanguage", merge, p1, p2)
+
+ return out
+}
+
+// MergeByLanguageInterface is the generic version of MergeByLanguage. It
+// is here just so it can be called from the tpl package.
+func (p1 Pages) MergeByLanguageInterface(in interface{}) (interface{}, error) {
+ if in == nil {
+ return p1, nil
+ }
+ p2, ok := in.(Pages)
+ if !ok {
+ return nil, fmt.Errorf("%T cannot be merged by language", in)
+ }
+ return p1.MergeByLanguage(p2), nil
+}
diff --git a/resources/page/pages_prev_next.go b/resources/page/pages_prev_next.go
new file mode 100644
index 000000000..9293c9874
--- /dev/null
+++ b/resources/page/pages_prev_next.go
@@ -0,0 +1,42 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+// Prev returns the previous page reletive to the given
+func (p Pages) Prev(cur Page) Page {
+ for x, c := range p {
+ if c.Eq(cur) {
+ if x == 0 {
+ // TODO(bep) consider return nil here to get it line with the other Prevs
+ return p[len(p)-1]
+ }
+ return p[x-1]
+ }
+ }
+ return nil
+}
+
+// Next returns the next page reletive to the given
+func (p Pages) Next(cur Page) Page {
+ for x, c := range p {
+ if c.Eq(cur) {
+ if x < len(p)-1 {
+ return p[x+1]
+ }
+ // TODO(bep) consider return nil here to get it line with the other Nexts
+ return p[0]
+ }
+ }
+ return nil
+}
diff --git a/resources/page/pages_prev_next_test.go b/resources/page/pages_prev_next_test.go
new file mode 100644
index 000000000..c39ad0603
--- /dev/null
+++ b/resources/page/pages_prev_next_test.go
@@ -0,0 +1,83 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+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 {
+ w := WeightedPages{}
+
+ for _, src := range pagePNTestSources {
+ p := newTestPage()
+ p.path = src.path
+ p.weight = src.weight
+ p.date = cast.ToTime(src.date)
+ p.pubDate = cast.ToTime(src.date)
+ w = append(w, WeightedPage{Weight: p.weight, Page: 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/resources/page/pages_related.go b/resources/page/pages_related.go
new file mode 100644
index 000000000..1a4386135
--- /dev/null
+++ b/resources/page/pages_related.go
@@ -0,0 +1,199 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "sync"
+
+ "github.com/gohugoio/hugo/common/types"
+ "github.com/gohugoio/hugo/related"
+ "github.com/pkg/errors"
+ "github.com/spf13/cast"
+)
+
+var (
+ // Assert that Pages and PageGroup implements the PageGenealogist interface.
+ _ PageGenealogist = (Pages)(nil)
+ _ PageGenealogist = PageGroup{}
+)
+
+// A PageGenealogist finds related pages in a page collection. This interface is implemented
+// by Pages and PageGroup, which makes it available as `{{ .RegularRelated . }}` etc.
+type PageGenealogist interface {
+
+ // Template example:
+ // {{ $related := .RegularPages.Related . }}
+ Related(doc related.Document) (Pages, error)
+
+ // Template example:
+ // {{ $related := .RegularPages.RelatedIndices . "tags" "date" }}
+ RelatedIndices(doc related.Document, indices ...interface{}) (Pages, error)
+
+ // Template example:
+ // {{ $related := .RegularPages.RelatedTo ( keyVals "tags" "hugo", "rocks") ( keyVals "date" .Date ) }}
+ RelatedTo(args ...types.KeyValues) (Pages, error)
+}
+
+// Related searches all the configured indices with the search keywords from the
+// supplied document.
+func (p Pages) Related(doc related.Document) (Pages, error) {
+ result, err := p.searchDoc(doc)
+ if err != nil {
+ return nil, err
+ }
+
+ if page, ok := doc.(Page); ok {
+ return result.removeFirstIfFound(page), nil
+ }
+
+ return result, nil
+
+}
+
+// RelatedIndices searches the given indices with the search keywords from the
+// supplied document.
+func (p Pages) RelatedIndices(doc related.Document, indices ...interface{}) (Pages, error) {
+ indicesStr, err := cast.ToStringSliceE(indices)
+ if err != nil {
+ return nil, err
+ }
+
+ result, err := p.searchDoc(doc, indicesStr...)
+ if err != nil {
+ return nil, err
+ }
+
+ if page, ok := doc.(Page); ok {
+ return result.removeFirstIfFound(page), nil
+ }
+
+ return result, nil
+
+}
+
+// RelatedTo searches the given indices with the corresponding values.
+func (p Pages) RelatedTo(args ...types.KeyValues) (Pages, error) {
+ if len(p) == 0 {
+ return nil, nil
+ }
+
+ return p.search(args...)
+
+}
+
+func (p Pages) search(args ...types.KeyValues) (Pages, error) {
+ return p.withInvertedIndex(func(idx *related.InvertedIndex) ([]related.Document, error) {
+ return idx.SearchKeyValues(args...)
+ })
+
+}
+
+func (p Pages) searchDoc(doc related.Document, indices ...string) (Pages, error) {
+ return p.withInvertedIndex(func(idx *related.InvertedIndex) ([]related.Document, error) {
+ return idx.SearchDoc(doc, indices...)
+ })
+}
+
+func (p Pages) withInvertedIndex(search func(idx *related.InvertedIndex) ([]related.Document, error)) (Pages, error) {
+ if len(p) == 0 {
+ return nil, nil
+ }
+
+ d, ok := p[0].(InternalDependencies)
+ if !ok {
+ return nil, errors.Errorf("invalid type %T in related serch", p[0])
+ }
+
+ cache := d.GetRelatedDocsHandler()
+
+ searchIndex, err := cache.getOrCreateIndex(p)
+ if err != nil {
+ return nil, err
+ }
+
+ result, err := search(searchIndex)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(result) > 0 {
+ mp := make(Pages, len(result))
+ for i, match := range result {
+ mp[i] = match.(Page)
+ }
+ return mp, nil
+ }
+
+ return nil, nil
+}
+
+type cachedPostingList struct {
+ p Pages
+
+ postingList *related.InvertedIndex
+}
+
+type RelatedDocsHandler struct {
+ cfg related.Config
+
+ postingLists []*cachedPostingList
+ mu sync.RWMutex
+}
+
+func NewRelatedDocsHandler(cfg related.Config) *RelatedDocsHandler {
+ return &RelatedDocsHandler{cfg: cfg}
+}
+
+func (s *RelatedDocsHandler) Clone() *RelatedDocsHandler {
+ return NewRelatedDocsHandler(s.cfg)
+}
+
+// This assumes that a lock has been acquired.
+func (s *RelatedDocsHandler) getIndex(p Pages) *related.InvertedIndex {
+ for _, ci := range s.postingLists {
+ if pagesEqual(p, ci.p) {
+ return ci.postingList
+ }
+ }
+ return nil
+}
+
+func (s *RelatedDocsHandler) getOrCreateIndex(p Pages) (*related.InvertedIndex, error) {
+ s.mu.RLock()
+ cachedIndex := s.getIndex(p)
+ if cachedIndex != nil {
+ s.mu.RUnlock()
+ return cachedIndex, nil
+ }
+ s.mu.RUnlock()
+
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ if cachedIndex := s.getIndex(p); cachedIndex != nil {
+ return cachedIndex, nil
+ }
+
+ searchIndex := related.NewInvertedIndex(s.cfg)
+
+ for _, page := range p {
+ if err := searchIndex.Add(page); err != nil {
+ return nil, err
+ }
+ }
+
+ s.postingLists = append(s.postingLists, &cachedPostingList{p: p, postingList: searchIndex})
+
+ return searchIndex, nil
+}
diff --git a/resources/page/pages_related_test.go b/resources/page/pages_related_test.go
new file mode 100644
index 000000000..016b492c8
--- /dev/null
+++ b/resources/page/pages_related_test.go
@@ -0,0 +1,86 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "testing"
+ "time"
+
+ "github.com/gohugoio/hugo/common/types"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestRelated(t *testing.T) {
+ assert := require.New(t)
+
+ t.Parallel()
+
+ pages := Pages{
+ &testPage{
+ title: "Page 1",
+ pubDate: mustParseDate("2017-01-03"),
+ params: map[string]interface{}{
+ "keywords": []string{"hugo", "says"},
+ },
+ },
+ &testPage{
+ title: "Page 2",
+ pubDate: mustParseDate("2017-01-02"),
+ params: map[string]interface{}{
+ "keywords": []string{"hugo", "rocks"},
+ },
+ },
+ &testPage{
+ title: "Page 3",
+ pubDate: mustParseDate("2017-01-01"),
+ params: map[string]interface{}{
+ "keywords": []string{"bep", "says"},
+ },
+ },
+ }
+
+ result, err := pages.RelatedTo(types.NewKeyValuesStrings("keywords", "hugo", "rocks"))
+
+ assert.NoError(err)
+ assert.Len(result, 2)
+ assert.Equal("Page 2", result[0].Title())
+ assert.Equal("Page 1", result[1].Title())
+
+ result, err = pages.Related(pages[0])
+ assert.NoError(err)
+ assert.Len(result, 2)
+ assert.Equal("Page 2", result[0].Title())
+ assert.Equal("Page 3", result[1].Title())
+
+ result, err = pages.RelatedIndices(pages[0], "keywords")
+ assert.NoError(err)
+ assert.Len(result, 2)
+ assert.Equal("Page 2", result[0].Title())
+ assert.Equal("Page 3", result[1].Title())
+
+ result, err = pages.RelatedTo(types.NewKeyValuesStrings("keywords", "bep", "rocks"))
+ assert.NoError(err)
+ assert.Len(result, 2)
+ assert.Equal("Page 2", result[0].Title())
+ assert.Equal("Page 3", result[1].Title())
+}
+
+func mustParseDate(s string) time.Time {
+ d, err := time.Parse("2006-01-02", s)
+ if err != nil {
+ panic(err)
+ }
+ return d
+}
diff --git a/resources/page/pages_sort.go b/resources/page/pages_sort.go
new file mode 100644
index 000000000..7b2a34a6a
--- /dev/null
+++ b/resources/page/pages_sort.go
@@ -0,0 +1,348 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "sort"
+
+ "github.com/gohugoio/hugo/resources/resource"
+
+ "github.com/spf13/cast"
+)
+
+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 func 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() {
+ if p1.File().IsZero() || p2.File().IsZero() {
+ return p1.File().IsZero()
+ }
+ return p1.File().Filename() < p2.File().Filename()
+ }
+ 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() {
+ if !p1.File().IsZero() && !p2.File().IsZero() {
+ return p1.File().Filename() < p2.File().Filename()
+ }
+ }
+ 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]) }
+
+// 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 {
+ const key = "pageSort.ByWeight"
+ pages, _ := spc.get(key, pageBy(DefaultPageSort).Sort, p)
+ return pages
+}
+
+// SortByDefault sorts pages by the default sort.
+func SortByDefault(pages Pages) {
+ pageBy(DefaultPageSort).Sort(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 {
+
+ const key = "pageSort.ByTitle"
+
+ title := func(p1, p2 Page) bool {
+ return p1.Title() < p2.Title()
+ }
+
+ pages, _ := spc.get(key, pageBy(title).Sort, p)
+ 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 {
+
+ const key = "pageSort.ByLinkTitle"
+
+ linkTitle := func(p1, p2 Page) bool {
+ return p1.LinkTitle() < p2.LinkTitle()
+ }
+
+ pages, _ := spc.get(key, pageBy(linkTitle).Sort, p)
+
+ 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 {
+
+ const key = "pageSort.ByDate"
+
+ date := func(p1, p2 Page) bool {
+ return p1.Date().Unix() < p2.Date().Unix()
+ }
+
+ pages, _ := spc.get(key, pageBy(date).Sort, p)
+
+ 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 {
+
+ const key = "pageSort.ByPublishDate"
+
+ pubDate := func(p1, p2 Page) bool {
+ return p1.PublishDate().Unix() < p2.PublishDate().Unix()
+ }
+
+ pages, _ := spc.get(key, pageBy(pubDate).Sort, p)
+
+ 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 {
+
+ const key = "pageSort.ByExpiryDate"
+
+ expDate := func(p1, p2 Page) bool {
+ return p1.ExpiryDate().Unix() < p2.ExpiryDate().Unix()
+ }
+
+ pages, _ := spc.get(key, pageBy(expDate).Sort, p)
+
+ 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 {
+
+ const key = "pageSort.ByLastmod"
+
+ date := func(p1, p2 Page) bool {
+ return p1.Lastmod().Unix() < p2.Lastmod().Unix()
+ }
+
+ pages, _ := spc.get(key, pageBy(date).Sort, p)
+
+ 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 {
+
+ const key = "pageSort.ByLength"
+
+ length := func(p1, p2 Page) bool {
+
+ p1l, ok1 := p1.(resource.LengthProvider)
+ p2l, ok2 := p2.(resource.LengthProvider)
+
+ if !ok1 {
+ return true
+ }
+
+ if !ok2 {
+ return false
+ }
+
+ return p1l.Len() < p2l.Len()
+ }
+
+ pages, _ := spc.get(key, pageBy(length).Sort, p)
+
+ 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 {
+
+ const key = "pageSort.ByLanguage"
+
+ pages, _ := spc.get(key, pageBy(languagePageSort).Sort, p)
+
+ return pages
+}
+
+// SortByLanguage sorts the pages by language.
+func SortByLanguage(pages Pages) {
+ pageBy(languagePageSort).Sort(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 {
+ const 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, reverseFunc, p)
+
+ return pages
+}
+
+// ByParam sorts the pages according to the given page Params key.
+//
+// Adjacent invocations on the same receiver with the same paramsKey will return a cached result.
+//
+// This may safely be executed in parallel.
+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)
+
+ if v1 == nil {
+ return false
+ }
+
+ if v2 == nil {
+ return true
+ }
+
+ isNumeric := func(v interface{}) bool {
+ switch v.(type) {
+ case uint8, uint16, uint32, uint64, int, int8, int16, int32, int64, float32, float64:
+ return true
+ default:
+ return false
+ }
+ }
+
+ if isNumeric(v1) && isNumeric(v2) {
+ return cast.ToFloat64(v1) < cast.ToFloat64(v2)
+ }
+
+ s1 := cast.ToString(v1)
+ s2 := cast.ToString(v2)
+
+ return s1 < s2
+ }
+
+ pages, _ := spc.get(key, pageBy(paramsKeyComparator).Sort, p)
+
+ return pages
+}
diff --git a/resources/page/pages_sort_test.go b/resources/page/pages_sort_test.go
new file mode 100644
index 000000000..c781de2f3
--- /dev/null
+++ b/resources/page/pages_sort_test.go
@@ -0,0 +1,279 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/gohugoio/hugo/resources/resource"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+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)
+
+ p := createSortTestPages(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)
+ SortByDefault(p)
+
+ 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)
+ SortByDefault(p)
+
+ 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)
+ SortByDefault(p)
+ 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)
+ SortByDefault(p)
+ assert.Equal(t, "al", p[0].LinkTitle())
+ assert.Equal(t, "bl", p[1].LinkTitle())
+ assert.Equal(t, "cl", p[2].LinkTitle())
+}
+
+// https://github.com/gohugoio/hugo/issues/4953
+func TestSortByLinkTitle(t *testing.T) {
+ t.Parallel()
+ assert := require.New(t)
+ pages := createSortTestPages(6)
+
+ for i, p := range pages {
+ pp := p.(*testPage)
+ if i < 5 {
+ pp.title = fmt.Sprintf("title%d", i)
+ }
+
+ if i > 2 {
+ pp.linkTitle = fmt.Sprintf("linkTitle%d", i)
+ }
+
+ }
+
+ pages.shuffle()
+
+ bylt := pages.ByLinkTitle()
+
+ for i, p := range bylt {
+ msg := fmt.Sprintf("test: %d", i)
+ if i < 3 {
+ assert.Equal(fmt.Sprintf("linkTitle%d", i+3), p.LinkTitle(), msg)
+ } else {
+ assert.Equal(fmt.Sprintf("title%d", i-3), p.LinkTitle(), msg)
+ }
+ }
+}
+
+func TestSortByN(t *testing.T) {
+ t.Parallel()
+ d1 := time.Now()
+ d2 := d1.Add(-2 * time.Hour)
+ d3 := d1.Add(-10 * time.Hour)
+ d4 := d1.Add(-20 * time.Hour)
+
+ p := createSortTestPages(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].(resource.LengthProvider).Len() == len(p[0].(*testPage).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()
+ p := createSortTestPages(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()
+ p1 := createSortTestPages(10)
+ assert.Equal(t, 0, p1[0].(*testPage).fuzzyWordCount)
+ assert.Equal(t, 9, p1[9].(*testPage).fuzzyWordCount)
+ p2 := p1.Reverse()
+ assert.Equal(t, 9, p2[0].(*testPage).fuzzyWordCount)
+ assert.Equal(t, 0, p2[9].(*testPage).fuzzyWordCount)
+ // cached
+ assert.True(t, pagesEqual(p2, p1.Reverse()))
+}
+
+func TestPageSortByParam(t *testing.T) {
+ t.Parallel()
+ var k interface{} = "arbitrarily.nested"
+
+ unsorted := createSortTestPages(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 TestPageSortByParamNumeric(t *testing.T) {
+ t.Parallel()
+ var k interface{} = "arbitrarily.nested"
+
+ n := 10
+ unsorted := createSortTestPages(n)
+ for i := 0; i < n; i++ {
+ v := 100 - i
+ if i%2 == 0 {
+ v = 100.0 - i
+ }
+
+ unsorted[i].(*testPage).params = map[string]interface{}{
+ "arbitrarily": map[string]interface{}{
+ "nested": v,
+ },
+ }
+ }
+ 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, 100, firstSetValue)
+ assert.Equal(t, 99, secondSetValue)
+ assert.Equal(t, 92, 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, 92, firstSetSortedValue)
+ assert.Equal(t, 93, secondSetSortedValue)
+ assert.Equal(t, 100, lastSetSortedValue)
+ assert.Equal(t, unsetValue, unsetSortedValue)
+}
+
+func BenchmarkSortByWeightAndReverse(b *testing.B) {
+ p := createSortTestPages(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 {
+ this := pages[i].(*testPage)
+ other := pages[len(dates)-1-i].(*testPage)
+
+ this.date = dates[i]
+ this.lastMod = dates[i]
+ this.weight = weights[i]
+ this.title = titles[i]
+ // make sure we compare apples and ... apples ...
+ other.linkTitle = this.Title() + "l"
+ other.pubDate = dates[i]
+ other.expiryDate = dates[i]
+ other.content = titles[i] + "_content"
+ }
+ lastLastMod := pages[2].Lastmod()
+ pages[2].(*testPage).lastMod = pages[1].Lastmod()
+ pages[1].(*testPage).lastMod = lastLastMod
+
+ for _, p := range pages {
+ p.(*testPage).content = ""
+ }
+
+}
+
+func createSortTestPages(num int) Pages {
+ pages := make(Pages, num)
+
+ for i := 0; i < num; i++ {
+ p := newTestPage()
+ p.path = 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/resources/page/pages_test.go b/resources/page/pages_test.go
new file mode 100644
index 000000000..5220a6d33
--- /dev/null
+++ b/resources/page/pages_test.go
@@ -0,0 +1,55 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestProbablyEq(t *testing.T) {
+
+ p1, p2, p3 := &testPage{title: "p1"}, &testPage{title: "p2"}, &testPage{title: "p3"}
+ pages12 := Pages{p1, p2}
+ pages21 := Pages{p2, p1}
+ pages123 := Pages{p1, p2, p3}
+
+ t.Run("Pages", func(t *testing.T) {
+ assert := require.New(t)
+
+ assert.True(pages12.ProbablyEq(pages12))
+ assert.False(pages123.ProbablyEq(pages12))
+ assert.False(pages12.ProbablyEq(pages21))
+ })
+
+ t.Run("PageGroup", func(t *testing.T) {
+ assert := require.New(t)
+
+ assert.True(PageGroup{Key: "a", Pages: pages12}.ProbablyEq(PageGroup{Key: "a", Pages: pages12}))
+ assert.False(PageGroup{Key: "a", Pages: pages12}.ProbablyEq(PageGroup{Key: "b", Pages: pages12}))
+
+ })
+
+ t.Run("PagesGroup", func(t *testing.T) {
+ assert := require.New(t)
+
+ pg1, pg2 := PageGroup{Key: "a", Pages: pages12}, PageGroup{Key: "b", Pages: pages123}
+
+ assert.True(PagesGroup{pg1, pg2}.ProbablyEq(PagesGroup{pg1, pg2}))
+ assert.False(PagesGroup{pg1, pg2}.ProbablyEq(PagesGroup{pg2, pg1}))
+
+ })
+
+}
diff --git a/resources/page/pagination.go b/resources/page/pagination.go
new file mode 100644
index 000000000..6d5da966e
--- /dev/null
+++ b/resources/page/pagination.go
@@ -0,0 +1,404 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "errors"
+ "fmt"
+ "html/template"
+ "math"
+ "reflect"
+
+ "github.com/gohugoio/hugo/config"
+
+ "github.com/spf13/cast"
+)
+
+// PaginatorProvider provides two ways to create a page paginator.
+type PaginatorProvider interface {
+ Paginator(options ...interface{}) (*Pager, error)
+ Paginate(seq interface{}, options ...interface{}) (*Pager, error)
+}
+
+// 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
+}
+
+type pagers []*Pager
+
+var (
+ paginatorEmptyPages Pages
+ paginatorEmptyPageGroups PagesGroup
+)
+
+type Paginator struct {
+ paginatedElements []paginatedElement
+ pagers
+ paginationURLFactory
+ total int
+ size int
+}
+
+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
+}
+
+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 Paginate(td TargetPathDescriptor, seq interface{}, pagerSize int) (*Paginator, error) {
+
+ if pagerSize <= 0 {
+ return nil, errors.New("'paginate' configuration setting must be positive to paginate")
+ }
+
+ urlFactory := newPaginationURLFactory(td)
+
+ var paginator *Paginator
+
+ groups, err := ToPagesGroup(seq)
+ if err != nil {
+ return nil, err
+ }
+ if groups != nil {
+ paginator, _ = newPaginatorFromPageGroups(groups, pagerSize, urlFactory)
+ } else {
+ pages, err := ToPages(seq)
+ if err != nil {
+ return nil, err
+ }
+ paginator, _ = newPaginatorFromPages(pages, pagerSize, urlFactory)
+ }
+
+ return paginator, nil
+}
+
+// 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(pageNumber int) string {
+ pathDescriptor := d
+ var rel string
+ if pageNumber > 1 {
+ rel = fmt.Sprintf("/%s/%d/", d.PathSpec.PaginatePath, pageNumber)
+ pathDescriptor.Addends = rel
+ }
+
+ return CreateTargetPaths(pathDescriptor).RelPermalink(d.PathSpec)
+
+ }
+}
diff --git a/resources/page/pagination_test.go b/resources/page/pagination_test.go
new file mode 100644
index 000000000..1308d60d1
--- /dev/null
+++ b/resources/page/pagination_test.go
@@ -0,0 +1,307 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "fmt"
+ "html/template"
+ "testing"
+
+ "github.com/spf13/viper"
+
+ "github.com/gohugoio/hugo/output"
+ "github.com/stretchr/testify/require"
+)
+
+func TestSplitPages(t *testing.T) {
+ t.Parallel()
+
+ pages := createTestPages(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()
+ pages := createTestPages(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()
+ pages := createTestPages(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()
+ pages := createTestPages(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 := viper.New()
+ cfg.Set("paginatePath", "zoo")
+
+ for _, uglyURLs := range []bool{false, true} {
+ t.Run(fmt.Sprintf("uglyURLs=%t", uglyURLs), func(t *testing.T) {
+
+ tests := []struct {
+ name string
+ d TargetPathDescriptor
+ baseURL string
+ page int
+ expected string
+ expectedUgly string
+ }{
+ {"HTML home page 32",
+ TargetPathDescriptor{Kind: KindHome, Type: output.HTMLFormat}, "http://example.com/", 32, "/zoo/32/", "/zoo/32.html"},
+ {"JSON home page 42",
+ TargetPathDescriptor{Kind: KindHome, Type: output.JSONFormat}, "http://example.com/", 42, "/zoo/42/index.json", "/zoo/42.json"},
+ }
+
+ for _, test := range tests {
+ d := test.d
+ cfg.Set("baseURL", test.baseURL)
+ cfg.Set("uglyURLs", uglyURLs)
+ d.UglyURLs = uglyURLs
+
+ pathSpec := newTestPathSpecFor(cfg)
+ d.PathSpec = pathSpec
+
+ factory := newPaginationURLFactory(d)
+
+ got := factory(test.page)
+
+ if uglyURLs {
+ require.Equal(t, test.expectedUgly, got)
+ } else {
+ require.Equal(t, test.expected, got)
+ }
+
+ }
+ })
+
+ }
+}
+
+func TestProbablyEqualPageLists(t *testing.T) {
+ t.Parallel()
+ fivePages := createTestPages(5)
+ zeroPages := createTestPages(0)
+ zeroPagesByWeight, _ := createTestPages(0).GroupBy("Weight", "asc")
+ fivePagesByWeight, _ := createTestPages(5).GroupBy("Weight", "asc")
+ ninePagesByWeight, _ := createTestPages(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(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 TestPaginationPage(t *testing.T) {
+ t.Parallel()
+ urlFactory := func(page int) string {
+ return fmt.Sprintf("page/%d/", page)
+ }
+
+ fivePages := createTestPages(7)
+ fivePagesFuzzyWordCount, _ := createTestPages(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.NotNil(t, page21)
+ require.Equal(t, 3, page21.FuzzyWordCount())
+ require.Nil(t, page2Nil)
+}
diff --git a/resources/page/permalinks.go b/resources/page/permalinks.go
new file mode 100644
index 000000000..98489231b
--- /dev/null
+++ b/resources/page/permalinks.go
@@ -0,0 +1,248 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "fmt"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/helpers"
+)
+
+// PermalinkExpander holds permalin mappings per section.
+type PermalinkExpander struct {
+ // 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.
+ knownPermalinkAttributes map[string]pageToPermaAttribute
+
+ expanders map[string]func(Page) (string, error)
+
+ ps *helpers.PathSpec
+}
+
+// NewPermalinkExpander creates a new PermalinkExpander configured by the given
+// PathSpec.
+func NewPermalinkExpander(ps *helpers.PathSpec) (PermalinkExpander, error) {
+
+ p := PermalinkExpander{ps: ps}
+
+ p.knownPermalinkAttributes = map[string]pageToPermaAttribute{
+ "year": p.pageToPermalinkDate,
+ "month": p.pageToPermalinkDate,
+ "monthname": p.pageToPermalinkDate,
+ "day": p.pageToPermalinkDate,
+ "weekday": p.pageToPermalinkDate,
+ "weekdayname": p.pageToPermalinkDate,
+ "yearday": p.pageToPermalinkDate,
+ "section": p.pageToPermalinkSection,
+ "sections": p.pageToPermalinkSections,
+ "title": p.pageToPermalinkTitle,
+ "slug": p.pageToPermalinkSlugElseTitle,
+ "filename": p.pageToPermalinkFilename,
+ }
+
+ patterns := ps.Cfg.GetStringMapString("permalinks")
+ if patterns == nil {
+ return p, nil
+ }
+
+ e, err := p.parse(patterns)
+ if err != nil {
+ return p, err
+ }
+
+ p.expanders = e
+
+ return p, nil
+}
+
+// Expand expands the path in p according to the rules defined for the given key.
+// If no rules are found for the given key, an empty string is returned.
+func (l PermalinkExpander) Expand(key string, p Page) (string, error) {
+ expand, found := l.expanders[key]
+
+ if !found {
+ return "", nil
+ }
+
+ return expand(p)
+
+}
+
+func (l PermalinkExpander) parse(patterns map[string]string) (map[string]func(Page) (string, error), error) {
+
+ expanders := make(map[string]func(Page) (string, error))
+
+ for k, pattern := range patterns {
+ if !l.validate(pattern) {
+ return nil, &permalinkExpandError{pattern: pattern, err: errPermalinkIllFormed}
+ }
+
+ pattern := pattern
+ matches := attributeRegexp.FindAllStringSubmatch(pattern, -1)
+
+ callbacks := make([]pageToPermaAttribute, len(matches))
+ replacements := make([]string, len(matches))
+ for i, m := range matches {
+ replacement := m[0]
+ attr := replacement[1:]
+ replacements[i] = replacement
+ callback, ok := l.knownPermalinkAttributes[attr]
+
+ if !ok {
+ return nil, &permalinkExpandError{pattern: pattern, err: errPermalinkAttributeUnknown}
+ }
+
+ callbacks[i] = callback
+ }
+
+ expanders[k] = func(p Page) (string, error) {
+
+ if matches == nil {
+ return pattern, nil
+ }
+
+ newField := pattern
+
+ for i, replacement := range replacements {
+ attr := replacement[1:]
+ callback := callbacks[i]
+ newAttr, err := callback(p, attr)
+
+ if err != nil {
+ return "", &permalinkExpandError{pattern: pattern, err: err}
+ }
+
+ newField = strings.Replace(newField, replacement, newAttr, 1)
+
+ }
+
+ return newField, nil
+
+ }
+
+ }
+
+ return expanders, nil
+}
+
+// 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)
+
+var attributeRegexp = regexp.MustCompile(`:\w+`)
+
+// validate determines if a PathPattern is well-formed
+func (l PermalinkExpander) validate(pp string) bool {
+ fragments := strings.Split(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 := l.knownPermalinkAttributes[k]; !ok {
+ return false
+ }
+ }
+ }
+ return true
+}
+
+type permalinkExpandError struct {
+ pattern string
+ err error
+}
+
+func (pee *permalinkExpandError) Error() string {
+ return fmt.Sprintf("error expanding %q: %s", string(pee.pattern), pee.err)
+}
+
+var (
+ errPermalinkIllFormed = errors.New("permalink ill-formed")
+ errPermalinkAttributeUnknown = errors.New("permalink attribute not recognised")
+)
+
+func (l PermalinkExpander) 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 (l PermalinkExpander) pageToPermalinkTitle(p Page, _ string) (string, error) {
+ return l.ps.URLize(p.Title()), nil
+}
+
+// pageToPermalinkFilename returns the URL-safe form of the filename
+func (l PermalinkExpander) pageToPermalinkFilename(p Page, _ string) (string, error) {
+ name := p.File().TranslationBaseName()
+ if name == "index" {
+ // Page bundles; the directory name will hopefully have a better name.
+ dir := strings.TrimSuffix(p.File().Dir(), helpers.FilePathSeparator)
+ _, name = filepath.Split(dir)
+ }
+
+ return l.ps.URLize(name), nil
+}
+
+// if the page has a slug, return the slug, else return the title
+func (l PermalinkExpander) pageToPermalinkSlugElseTitle(p Page, a string) (string, error) {
+ if p.Slug() != "" {
+ return l.ps.URLize(p.Slug()), nil
+ }
+ return l.pageToPermalinkTitle(p, a)
+}
+
+func (l PermalinkExpander) pageToPermalinkSection(p Page, _ string) (string, error) {
+ return p.Section(), nil
+}
+
+func (l PermalinkExpander) pageToPermalinkSections(p Page, _ string) (string, error) {
+ return p.CurrentSection().SectionsPath(), nil
+}
diff --git a/resources/page/permalinks_test.go b/resources/page/permalinks_test.go
new file mode 100644
index 000000000..d7af7e06d
--- /dev/null
+++ b/resources/page/permalinks_test.go
@@ -0,0 +1,180 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "fmt"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+// testdataPermalinks is used by a couple of tests; the expandsTo content is
+// subject to the data in simplePageJSON.
+var testdataPermalinks = []struct {
+ spec string
+ valid bool
+ expandsTo string
+}{
+ {":title", true, "spf13-vim-3.0-release-and-new-website"},
+ {"/:year-:month-:title", true, "/2012-04-spf13-vim-3.0-release-and-new-website"},
+ {"/:year/:yearday/:month/:monthname/:day/:weekday/:weekdayname/", true, "/2012/97/04/April/06/5/Friday/"}, // Dates
+ {"/:section/", true, "/blue/"}, // Section
+ {"/:title/", true, "/spf13-vim-3.0-release-and-new-website/"}, // Title
+ {"/:slug/", true, "/the-slug/"}, // Slug
+ {"/:filename/", true, "/test-page/"}, // Filename
+ // TODO(moorereason): need test scaffolding for this.
+ //{"/:sections/", false, "/blue/"}, // Sections
+
+ // Failures
+ {"/blog/:fred", false, ""},
+ {"/:year//:title", false, ""},
+}
+
+func TestPermalinkExpansion(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ page := newTestPageWithFile("/test-page/index.md")
+ page.title = "Spf13 Vim 3.0 Release and new website"
+ d, _ := time.Parse("2006-01-02", "2012-04-06")
+ page.date = d
+ page.section = "blue"
+ page.slug = "The Slug"
+
+ for i, item := range testdataPermalinks {
+
+ msg := fmt.Sprintf("Test %d", i)
+
+ if !item.valid {
+ continue
+ }
+
+ permalinksConfig := map[string]string{
+ "posts": item.spec,
+ }
+
+ ps := newTestPathSpec()
+ ps.Cfg.Set("permalinks", permalinksConfig)
+
+ expander, err := NewPermalinkExpander(ps)
+ assert.NoError(err)
+
+ expanded, err := expander.Expand("posts", page)
+ assert.NoError(err)
+ assert.Equal(item.expandsTo, expanded, msg)
+
+ }
+}
+
+func TestPermalinkExpansionMultiSection(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ page := newTestPage()
+ page.title = "Page Title"
+ d, _ := time.Parse("2006-01-02", "2012-04-06")
+ page.date = d
+ page.section = "blue"
+ page.slug = "The Slug"
+
+ permalinksConfig := map[string]string{
+ "posts": "/:slug",
+ "blog": "/:section/:year",
+ }
+
+ ps := newTestPathSpec()
+ ps.Cfg.Set("permalinks", permalinksConfig)
+
+ expander, err := NewPermalinkExpander(ps)
+ assert.NoError(err)
+
+ expanded, err := expander.Expand("posts", page)
+ assert.NoError(err)
+ assert.Equal("/the-slug", expanded)
+
+ expanded, err = expander.Expand("blog", page)
+ assert.NoError(err)
+ assert.Equal("/blue/2012", expanded)
+
+}
+
+func TestPermalinkExpansionConcurrent(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ permalinksConfig := map[string]string{
+ "posts": "/:slug/",
+ }
+
+ ps := newTestPathSpec()
+ ps.Cfg.Set("permalinks", permalinksConfig)
+
+ expander, err := NewPermalinkExpander(ps)
+ assert.NoError(err)
+
+ var wg sync.WaitGroup
+
+ for i := 1; i < 20; i++ {
+ wg.Add(1)
+ go func(i int) {
+ defer wg.Done()
+ page := newTestPage()
+ for j := 1; j < 20; j++ {
+ page.slug = fmt.Sprintf("slug%d", i+j)
+ expanded, err := expander.Expand("posts", page)
+ assert.NoError(err)
+ assert.Equal(fmt.Sprintf("/%s/", page.slug), expanded)
+ }
+ }(i)
+ }
+
+ wg.Wait()
+}
+
+func BenchmarkPermalinkExpand(b *testing.B) {
+ page := newTestPage()
+ page.title = "Hugo Rocks"
+ d, _ := time.Parse("2006-01-02", "2019-02-28")
+ page.date = d
+
+ permalinksConfig := map[string]string{
+ "posts": "/:year-:month-:title",
+ }
+
+ ps := newTestPathSpec()
+ ps.Cfg.Set("permalinks", permalinksConfig)
+
+ expander, err := NewPermalinkExpander(ps)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ s, err := expander.Expand("posts", page)
+ if err != nil {
+ b.Fatal(err)
+ }
+ if s != "/2019-02-hugo-rocks" {
+ b.Fatal(s)
+ }
+
+ }
+}
diff --git a/resources/page/site.go b/resources/page/site.go
new file mode 100644
index 000000000..25df063f1
--- /dev/null
+++ b/resources/page/site.go
@@ -0,0 +1,53 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "html/template"
+ "time"
+
+ "github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/langs"
+ "github.com/gohugoio/hugo/navigation"
+)
+
+// Site represents a site in the build. This is currently a very narrow interface,
+// but the actual implementation will be richer, see hugolib.SiteInfo.
+type Site interface {
+ Language() *langs.Language
+ RegularPages() Pages
+ Pages() Pages
+ IsServer() bool
+ ServerPort() int
+ Title() string
+ Sites() Sites
+ Hugo() hugo.Info
+ BaseURL() template.URL
+ Taxonomies() interface{}
+ LastChange() time.Time
+ Menus() navigation.Menus
+ Params() map[string]interface{}
+ Data() map[string]interface{}
+}
+
+// Sites represents an ordered list of sites (languages).
+type Sites []Site
+
+// First is a convenience method to get the first Site, i.e. the main language.
+func (s Sites) First() Site {
+ if len(s) == 0 {
+ return nil
+ }
+ return s[0]
+}
diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go
new file mode 100644
index 000000000..60a6c0816
--- /dev/null
+++ b/resources/page/testhelpers_test.go
@@ -0,0 +1,558 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "fmt"
+ "html/template"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/bep/gitmap"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/resources/resource"
+ "github.com/spf13/viper"
+
+ "github.com/gohugoio/hugo/navigation"
+
+ "github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/langs"
+ "github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/related"
+
+ "github.com/gohugoio/hugo/source"
+)
+
+var (
+ _ resource.LengthProvider = (*testPage)(nil)
+ _ Page = (*testPage)(nil)
+)
+
+var relatedDocsHandler = NewRelatedDocsHandler(related.DefaultConfig)
+
+func newTestPage() *testPage {
+ return newTestPageWithFile("/a/b/c.md")
+}
+
+func newTestPageWithFile(filename string) *testPage {
+ filename = filepath.FromSlash(filename)
+ file := source.NewTestFile(filename)
+ return &testPage{
+ params: make(map[string]interface{}),
+ data: make(map[string]interface{}),
+ file: file,
+ }
+}
+
+func newTestPathSpec() *helpers.PathSpec {
+ return newTestPathSpecFor(viper.New())
+}
+
+func newTestPathSpecFor(cfg config.Provider) *helpers.PathSpec {
+ config.SetBaseTestDefaults(cfg)
+ fs := hugofs.NewMem(cfg)
+ s, err := helpers.NewPathSpec(fs, cfg)
+ if err != nil {
+ panic(err)
+ }
+ return s
+}
+
+type testPage struct {
+ description string
+ title string
+ linkTitle string
+
+ section string
+
+ content string
+
+ fuzzyWordCount int
+
+ path string
+
+ slug string
+
+ // Dates
+ date time.Time
+ lastMod time.Time
+ expiryDate time.Time
+ pubDate time.Time
+
+ weight int
+
+ params map[string]interface{}
+ data map[string]interface{}
+
+ file source.File
+}
+
+func (p *testPage) Aliases() []string {
+ panic("not implemented")
+}
+
+func (p *testPage) AllTranslations() Pages {
+ panic("not implemented")
+}
+
+func (p *testPage) AlternativeOutputFormats() OutputFormats {
+ panic("not implemented")
+}
+
+func (p *testPage) Author() Author {
+ return Author{}
+
+}
+func (p *testPage) Authors() AuthorList {
+ return nil
+}
+
+func (p *testPage) BaseFileName() string {
+ panic("not implemented")
+}
+
+func (p *testPage) BundleType() string {
+ panic("not implemented")
+}
+
+func (p *testPage) Content() (interface{}, error) {
+ panic("not implemented")
+}
+
+func (p *testPage) ContentBaseName() string {
+ panic("not implemented")
+}
+
+func (p *testPage) CurrentSection() Page {
+ panic("not implemented")
+}
+
+func (p *testPage) Data() interface{} {
+ return p.data
+}
+
+func (p *testPage) Sitemap() config.Sitemap {
+ return config.Sitemap{}
+}
+
+func (p *testPage) Layout() string {
+ return ""
+}
+func (p *testPage) Date() time.Time {
+ return p.date
+}
+
+func (p *testPage) Description() string {
+ return ""
+}
+
+func (p *testPage) Dir() string {
+ panic("not implemented")
+}
+
+func (p *testPage) Draft() bool {
+ panic("not implemented")
+}
+
+func (p *testPage) Eq(other interface{}) bool {
+ return p == other
+}
+
+func (p *testPage) ExpiryDate() time.Time {
+ return p.expiryDate
+}
+
+func (p *testPage) Ext() string {
+ panic("not implemented")
+}
+
+func (p *testPage) Extension() string {
+ panic("not implemented")
+}
+
+func (p *testPage) File() source.File {
+ return p.file
+}
+
+func (p *testPage) FileInfo() os.FileInfo {
+ panic("not implemented")
+}
+
+func (p *testPage) Filename() string {
+ panic("not implemented")
+}
+
+func (p *testPage) FirstSection() Page {
+ panic("not implemented")
+}
+
+func (p *testPage) FuzzyWordCount() int {
+ return p.fuzzyWordCount
+}
+
+func (p *testPage) GetPage(ref string) (Page, error) {
+ panic("not implemented")
+}
+
+func (p *testPage) GetParam(key string) interface{} {
+ panic("not implemented")
+}
+
+func (p *testPage) GetRelatedDocsHandler() *RelatedDocsHandler {
+ return relatedDocsHandler
+}
+
+func (p *testPage) GitInfo() *gitmap.GitInfo {
+ return nil
+}
+
+func (p *testPage) HasMenuCurrent(menuID string, me *navigation.MenuEntry) bool {
+ panic("not implemented")
+}
+
+func (p *testPage) HasShortcode(name string) bool {
+ panic("not implemented")
+}
+
+func (p *testPage) Hugo() hugo.Info {
+ panic("not implemented")
+}
+
+func (p *testPage) InSection(other interface{}) (bool, error) {
+ panic("not implemented")
+}
+
+func (p *testPage) IsAncestor(other interface{}) (bool, error) {
+ panic("not implemented")
+}
+
+func (p *testPage) IsDescendant(other interface{}) (bool, error) {
+ panic("not implemented")
+}
+
+func (p *testPage) IsDraft() bool {
+ return false
+}
+
+func (p *testPage) IsHome() bool {
+ panic("not implemented")
+}
+
+func (p *testPage) IsMenuCurrent(menuID string, inme *navigation.MenuEntry) bool {
+ panic("not implemented")
+}
+
+func (p *testPage) IsNode() bool {
+ panic("not implemented")
+}
+
+func (p *testPage) IsPage() bool {
+ panic("not implemented")
+}
+
+func (p *testPage) IsSection() bool {
+ panic("not implemented")
+}
+
+func (p *testPage) IsTranslated() bool {
+ panic("not implemented")
+}
+
+func (p *testPage) Keywords() []string {
+ return nil
+}
+
+func (p *testPage) Kind() string {
+ panic("not implemented")
+}
+
+func (p *testPage) Lang() string {
+ panic("not implemented")
+}
+
+func (p *testPage) Language() *langs.Language {
+ panic("not implemented")
+}
+
+func (p *testPage) LanguagePrefix() string {
+ return ""
+}
+
+func (p *testPage) Lastmod() time.Time {
+ return p.lastMod
+}
+
+func (p *testPage) Len() int {
+ return len(p.content)
+}
+
+func (p *testPage) LinkTitle() string {
+ if p.linkTitle == "" {
+ return p.title
+ }
+ return p.linkTitle
+}
+
+func (p *testPage) LogicalName() string {
+ panic("not implemented")
+}
+
+func (p *testPage) MediaType() media.Type {
+ panic("not implemented")
+}
+
+func (p *testPage) Menus() navigation.PageMenus {
+ return navigation.PageMenus{}
+}
+
+func (p *testPage) Name() string {
+ panic("not implemented")
+}
+
+func (p *testPage) Next() Page {
+ panic("not implemented")
+}
+
+func (p *testPage) NextInSection() Page {
+ return nil
+}
+
+func (p *testPage) NextPage() Page {
+ return nil
+}
+
+func (p *testPage) OutputFormats() OutputFormats {
+ panic("not implemented")
+}
+
+func (p *testPage) Pages() Pages {
+ panic("not implemented")
+}
+
+func (p *testPage) Paginate(seq interface{}, options ...interface{}) (*Pager, error) {
+ return nil, nil
+}
+
+func (p *testPage) Paginator(options ...interface{}) (*Pager, error) {
+ return nil, nil
+}
+
+func (p *testPage) Param(key interface{}) (interface{}, error) {
+ return resource.Param(p, nil, key)
+}
+
+func (p *testPage) Params() map[string]interface{} {
+ return p.params
+}
+
+func (p *testPage) Page() Page {
+ return p
+}
+
+func (p *testPage) Parent() Page {
+ panic("not implemented")
+}
+
+func (p *testPage) Path() string {
+ return p.path
+}
+
+func (p *testPage) Permalink() string {
+ panic("not implemented")
+}
+
+func (p *testPage) Plain() string {
+ panic("not implemented")
+}
+
+func (p *testPage) PlainWords() []string {
+ panic("not implemented")
+}
+
+func (p *testPage) Prev() Page {
+ panic("not implemented")
+}
+
+func (p *testPage) PrevInSection() Page {
+ return nil
+}
+
+func (p *testPage) PrevPage() Page {
+ return nil
+}
+
+func (p *testPage) PublishDate() time.Time {
+ return p.pubDate
+}
+
+func (p *testPage) RSSLink() template.URL {
+ return ""
+}
+
+func (p *testPage) RawContent() string {
+ panic("not implemented")
+}
+
+func (p *testPage) ReadingTime() int {
+ panic("not implemented")
+}
+
+func (p *testPage) Ref(argsm map[string]interface{}) (string, error) {
+ panic("not implemented")
+}
+
+func (p *testPage) RefFrom(argsm map[string]interface{}, source interface{}) (string, error) {
+ return "", nil
+}
+
+func (p *testPage) RelPermalink() string {
+ panic("not implemented")
+}
+
+func (p *testPage) RelRef(argsm map[string]interface{}) (string, error) {
+ panic("not implemented")
+}
+
+func (p *testPage) RelRefFrom(argsm map[string]interface{}, source interface{}) (string, error) {
+ return "", nil
+}
+
+func (p *testPage) Render(layout ...string) template.HTML {
+ panic("not implemented")
+}
+
+func (p *testPage) ResourceType() string {
+ panic("not implemented")
+}
+
+func (p *testPage) Resources() resource.Resources {
+ panic("not implemented")
+}
+
+func (p *testPage) Scratch() *maps.Scratch {
+ panic("not implemented")
+}
+
+func (p *testPage) RelatedKeywords(cfg related.IndexConfig) ([]related.Keyword, error) {
+ v, err := p.Param(cfg.Name)
+ if err != nil {
+ return nil, err
+ }
+
+ return cfg.ToKeywords(v)
+}
+
+func (p *testPage) Section() string {
+ return p.section
+}
+
+func (p *testPage) Sections() Pages {
+ panic("not implemented")
+}
+
+func (p *testPage) SectionsEntries() []string {
+ panic("not implemented")
+}
+
+func (p *testPage) SectionsPath() string {
+ panic("not implemented")
+}
+
+func (p *testPage) Site() Site {
+ panic("not implemented")
+}
+
+func (p *testPage) Sites() Sites {
+ panic("not implemented")
+}
+
+func (p *testPage) Slug() string {
+ return p.slug
+}
+
+func (p *testPage) String() string {
+ return p.path
+}
+
+func (p *testPage) Summary() template.HTML {
+ panic("not implemented")
+}
+
+func (p *testPage) TableOfContents() template.HTML {
+ panic("not implemented")
+}
+
+func (p *testPage) Title() string {
+ return p.title
+}
+
+func (p *testPage) TranslationBaseName() string {
+ panic("not implemented")
+}
+
+func (p *testPage) TranslationKey() string {
+ return p.path
+}
+
+func (p *testPage) Translations() Pages {
+ panic("not implemented")
+}
+
+func (p *testPage) Truncated() bool {
+ panic("not implemented")
+}
+
+func (p *testPage) Type() string {
+ return p.section
+}
+
+func (p *testPage) URL() string {
+ return ""
+}
+
+func (p *testPage) UniqueID() string {
+ panic("not implemented")
+}
+
+func (p *testPage) Weight() int {
+ return p.weight
+}
+
+func (p *testPage) WordCount() int {
+ panic("not implemented")
+}
+
+func createTestPages(num int) Pages {
+ pages := make(Pages, num)
+
+ for i := 0; i < num; i++ {
+ m := &testPage{
+ path: fmt.Sprintf("/x/y/z/p%d.md", i),
+ weight: 5,
+ fuzzyWordCount: i + 2, // magic
+ }
+
+ if i%2 == 0 {
+ m.weight = 10
+ }
+ pages[i] = m
+
+ }
+
+ return pages
+}
diff --git a/resources/page/weighted.go b/resources/page/weighted.go
new file mode 100644
index 000000000..3f75bcc3c
--- /dev/null
+++ b/resources/page/weighted.go
@@ -0,0 +1,145 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package page
+
+import (
+ "fmt"
+ "sort"
+
+ "github.com/gohugoio/hugo/common/collections"
+)
+
+var (
+ _ collections.Slicer = WeightedPage{}
+)
+
+// WeightedPages is a list of Pages with their corresponding (and relative) weight
+// [{Weight: 30, Page: *1}, {Weight: 40, Page: *2}]
+type WeightedPages []WeightedPage
+
+// Page will return the Page (of Kind taxonomyList) that represents this set
+// of pages. This method will panic if p is empty, as that should never happen.
+func (p WeightedPages) Page() Page {
+ if len(p) == 0 {
+ panic("WeightedPages is empty")
+ }
+
+ first := p[0]
+
+ // TODO(bep) fix tests
+ if first.owner == nil {
+ return nil
+ }
+
+ return first.owner.Page
+}
+
+// A WeightedPage is a Page with a weight.
+type WeightedPage struct {
+ Weight int
+ Page
+
+ // Reference to the owning Page. This avoids having to do
+ // manual .Site.GetPage lookups. It is implemented in this roundabout way
+ // because we cannot add additional state to the WeightedPages slice
+ // without breaking lots of templates in the wild.
+ owner *PageWrapper
+}
+
+// PageWrapper wraps a Page.
+type PageWrapper struct {
+ Page
+}
+
+func NewWeightedPage(weight int, p Page, owner *PageWrapper) WeightedPage {
+ return WeightedPage{Weight: weight, Page: p, owner: owner}
+}
+
+func (w WeightedPage) String() string {
+ return fmt.Sprintf("WeightedPage(%d,%q)", w.Weight, w.Page.Title())
+}
+
+// Slice is not meant to be used externally. It's a bridge function
+// for the template functions. See collections.Slice.
+func (p WeightedPage) Slice(in interface{}) (interface{}, error) {
+ switch items := in.(type) {
+ case WeightedPages:
+ return items, nil
+ case []interface{}:
+ weighted := make(WeightedPages, len(items))
+ for i, v := range items {
+ g, ok := v.(WeightedPage)
+ if !ok {
+ return nil, fmt.Errorf("type %T is not a WeightedPage", v)
+ }
+ weighted[i] = g
+ }
+ return weighted, nil
+ default:
+ return nil, fmt.Errorf("invalid slice type %T", items)
+ }
+}
+
+// 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 == cur {
+ 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 == cur {
+ 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 {
+ return DefaultPageSort(wp[i].Page, wp[j].Page)
+ }
+ return wp[i].Weight < wp[j].Weight
+}
diff --git a/resources/page/zero_file.autogen.go b/resources/page/zero_file.autogen.go
new file mode 100644
index 000000000..eec1dd66d
--- /dev/null
+++ b/resources/page/zero_file.autogen.go
@@ -0,0 +1,88 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This file is autogenerated.
+
+package page
+
+import (
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/source"
+ "os"
+)
+
+// ZeroFile represents a zero value of source.File with warnings if invoked.
+type zeroFile struct {
+ log *helpers.DistinctLogger
+}
+
+func NewZeroFile(log *helpers.DistinctLogger) source.File {
+ return zeroFile{log: log}
+}
+
+func (zeroFile) IsZero() bool {
+ return true
+}
+
+func (z zeroFile) Path() (o0 string) {
+ z.log.Println(".File.Path on zero object. Wrap it in if or with: {{ with .File }}{{ .Path }}{{ end }}")
+ return
+}
+func (z zeroFile) Section() (o0 string) {
+ z.log.Println(".File.Section on zero object. Wrap it in if or with: {{ with .File }}{{ .Section }}{{ end }}")
+ return
+}
+func (z zeroFile) Lang() (o0 string) {
+ z.log.Println(".File.Lang on zero object. Wrap it in if or with: {{ with .File }}{{ .Lang }}{{ end }}")
+ return
+}
+func (z zeroFile) Filename() (o0 string) {
+ z.log.Println(".File.Filename on zero object. Wrap it in if or with: {{ with .File }}{{ .Filename }}{{ end }}")
+ return
+}
+func (z zeroFile) Dir() (o0 string) {
+ z.log.Println(".File.Dir on zero object. Wrap it in if or with: {{ with .File }}{{ .Dir }}{{ end }}")
+ return
+}
+func (z zeroFile) Extension() (o0 string) {
+ z.log.Println(".File.Extension on zero object. Wrap it in if or with: {{ with .File }}{{ .Extension }}{{ end }}")
+ return
+}
+func (z zeroFile) Ext() (o0 string) {
+ z.log.Println(".File.Ext on zero object. Wrap it in if or with: {{ with .File }}{{ .Ext }}{{ end }}")
+ return
+}
+func (z zeroFile) LogicalName() (o0 string) {
+ z.log.Println(".File.LogicalName on zero object. Wrap it in if or with: {{ with .File }}{{ .LogicalName }}{{ end }}")
+ return
+}
+func (z zeroFile) BaseFileName() (o0 string) {
+ z.log.Println(".File.BaseFileName on zero object. Wrap it in if or with: {{ with .File }}{{ .BaseFileName }}{{ end }}")
+ return
+}
+func (z zeroFile) TranslationBaseName() (o0 string) {
+ z.log.Println(".File.TranslationBaseName on zero object. Wrap it in if or with: {{ with .File }}{{ .TranslationBaseName }}{{ end }}")
+ return
+}
+func (z zeroFile) ContentBaseName() (o0 string) {
+ z.log.Println(".File.ContentBaseName on zero object. Wrap it in if or with: {{ with .File }}{{ .ContentBaseName }}{{ end }}")
+ return
+}
+func (z zeroFile) UniqueID() (o0 string) {
+ z.log.Println(".File.UniqueID on zero object. Wrap it in if or with: {{ with .File }}{{ .UniqueID }}{{ end }}")
+ return
+}
+func (z zeroFile) FileInfo() (o0 os.FileInfo) {
+ z.log.Println(".File.FileInfo on zero object. Wrap it in if or with: {{ with .File }}{{ .FileInfo }}{{ end }}")
+ return
+}
diff --git a/resources/resource.go b/resources/resource.go
new file mode 100644
index 000000000..abd251548
--- /dev/null
+++ b/resources/resource.go
@@ -0,0 +1,750 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resources
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "mime"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "github.com/gohugoio/hugo/media"
+
+ "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/tpl"
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/cache/filecache"
+ "github.com/gohugoio/hugo/common/collections"
+ "github.com/gohugoio/hugo/common/hugio"
+ "github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/resources/resource"
+
+ "github.com/spf13/afero"
+
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/source"
+)
+
+var (
+ _ resource.ContentResource = (*genericResource)(nil)
+ _ resource.ReadSeekCloserResource = (*genericResource)(nil)
+ _ resource.Resource = (*genericResource)(nil)
+ _ resource.Source = (*genericResource)(nil)
+ _ resource.Cloner = (*genericResource)(nil)
+ _ resource.ResourcesLanguageMerger = (*resource.Resources)(nil)
+ _ permalinker = (*genericResource)(nil)
+ _ collections.Slicer = (*genericResource)(nil)
+ _ resource.Identifier = (*genericResource)(nil)
+)
+
+var noData = make(map[string]interface{})
+
+type permalinker interface {
+ relPermalinkFor(target string) string
+ permalinkFor(target string) string
+ relTargetPathsFor(target string) []string
+ relTargetPaths() []string
+ TargetPath() string
+}
+
+type Spec struct {
+ *helpers.PathSpec
+
+ MediaTypes media.Types
+ OutputFormats output.Formats
+
+ Logger *loggers.Logger
+
+ TextTemplates tpl.TemplateParseFinder
+
+ Permalinks page.PermalinkExpander
+
+ // Holds default filter settings etc.
+ imaging *Imaging
+
+ imageCache *imageCache
+ ResourceCache *ResourceCache
+ FileCaches filecache.Caches
+}
+
+func NewSpec(
+ s *helpers.PathSpec,
+ fileCaches filecache.Caches,
+ logger *loggers.Logger,
+ outputFormats output.Formats,
+ mimeTypes media.Types) (*Spec, error) {
+
+ imaging, err := decodeImaging(s.Cfg.GetStringMap("imaging"))
+ if err != nil {
+ return nil, err
+ }
+
+ if logger == nil {
+ logger = loggers.NewErrorLogger()
+ }
+
+ permalinks, err := page.NewPermalinkExpander(s)
+ if err != nil {
+ return nil, err
+ }
+
+ rs := &Spec{PathSpec: s,
+ Logger: logger,
+ imaging: &imaging,
+ MediaTypes: mimeTypes,
+ OutputFormats: outputFormats,
+ Permalinks: permalinks,
+ FileCaches: fileCaches,
+ imageCache: newImageCache(
+ fileCaches.ImageCache(),
+
+ s,
+ )}
+
+ rs.ResourceCache = newResourceCache(rs)
+
+ return rs, nil
+
+}
+
+type ResourceSourceDescriptor struct {
+ // TargetPaths is a callback to fetch paths's relative to its owner.
+ TargetPaths func() page.TargetPaths
+
+ // Need one of these to load the resource content.
+ SourceFile source.File
+ OpenReadSeekCloser resource.OpenReadSeekCloser
+
+ // If OpenReadSeekerCloser is not set, we use this to open the file.
+ SourceFilename string
+
+ // The relative target filename without any language code.
+ RelTargetFilename string
+
+ // Any base paths prepended to the target path. This will also typically be the
+ // language code, but setting it here means that it should not have any effect on
+ // the permalink.
+ // This may be several values. In multihost mode we may publish the same resources to
+ // multiple targets.
+ TargetBasePaths []string
+
+ // Delay publishing until either Permalink or RelPermalink is called. Maybe never.
+ LazyPublish bool
+}
+
+func (r ResourceSourceDescriptor) Filename() string {
+ if r.SourceFile != nil {
+ return r.SourceFile.Filename()
+ }
+ return r.SourceFilename
+}
+
+func (r *Spec) sourceFs() afero.Fs {
+ return r.PathSpec.BaseFs.Content.Fs
+}
+
+func (r *Spec) New(fd ResourceSourceDescriptor) (resource.Resource, error) {
+ return r.newResourceForFs(r.sourceFs(), fd)
+}
+
+func (r *Spec) NewForFs(sourceFs afero.Fs, fd ResourceSourceDescriptor) (resource.Resource, error) {
+ return r.newResourceForFs(sourceFs, fd)
+}
+
+func (r *Spec) newResourceForFs(sourceFs afero.Fs, fd ResourceSourceDescriptor) (resource.Resource, error) {
+ if fd.OpenReadSeekCloser == nil {
+ if fd.SourceFile != nil && fd.SourceFilename != "" {
+ return nil, errors.New("both SourceFile and AbsSourceFilename provided")
+ } else if fd.SourceFile == nil && fd.SourceFilename == "" {
+ return nil, errors.New("either SourceFile or AbsSourceFilename must be provided")
+ }
+ }
+
+ if fd.RelTargetFilename == "" {
+ fd.RelTargetFilename = fd.Filename()
+ }
+
+ if len(fd.TargetBasePaths) == 0 {
+ // If not set, we publish the same resource to all hosts.
+ fd.TargetBasePaths = r.MultihostTargetBasePaths
+ }
+
+ return r.newResource(sourceFs, fd)
+}
+
+func (r *Spec) newResource(sourceFs afero.Fs, fd ResourceSourceDescriptor) (resource.Resource, error) {
+ var fi os.FileInfo
+ var sourceFilename string
+
+ if fd.OpenReadSeekCloser != nil {
+
+ } else if fd.SourceFilename != "" {
+ var err error
+ fi, err = sourceFs.Stat(fd.SourceFilename)
+ if err != nil {
+ return nil, err
+ }
+ sourceFilename = fd.SourceFilename
+ } else {
+ fi = fd.SourceFile.FileInfo()
+ sourceFilename = fd.SourceFile.Filename()
+ }
+
+ if fd.RelTargetFilename == "" {
+ fd.RelTargetFilename = sourceFilename
+ }
+
+ ext := filepath.Ext(fd.RelTargetFilename)
+ mimeType, found := r.MediaTypes.GetFirstBySuffix(strings.TrimPrefix(ext, "."))
+ // TODO(bep) we need to handle these ambigous types better, but in this context
+ // we most likely want the application/xml type.
+ if mimeType.Suffix() == "xml" && mimeType.SubType == "rss" {
+ mimeType, found = r.MediaTypes.GetByType("application/xml")
+ }
+
+ if !found {
+ // A fallback. Note that mime.TypeByExtension is slow by Hugo standards,
+ // so we should configure media types to avoid this lookup for most
+ // situations.
+ mimeStr := mime.TypeByExtension(ext)
+ if mimeStr != "" {
+ mimeType, _ = media.FromStringAndExt(mimeStr, ext)
+ }
+ }
+
+ gr := r.newGenericResourceWithBase(
+ sourceFs,
+ fd.LazyPublish,
+ fd.OpenReadSeekCloser,
+ fd.TargetBasePaths,
+ fd.TargetPaths,
+ fi,
+ sourceFilename,
+ fd.RelTargetFilename,
+ mimeType)
+
+ if mimeType.MainType == "image" {
+ ext := strings.ToLower(helpers.Ext(sourceFilename))
+
+ imgFormat, ok := imageFormats[ext]
+ if !ok {
+ // This allows SVG etc. to be used as resources. They will not have the methods of the Image, but
+ // that would not (currently) have worked.
+ return gr, nil
+ }
+
+ if err := gr.initHash(); err != nil {
+ return nil, err
+ }
+
+ return &Image{
+ format: imgFormat,
+ imaging: r.imaging,
+ genericResource: gr}, nil
+ }
+ return gr, nil
+
+}
+
+// TODO(bep) unify
+func (r *Spec) IsInImageCache(key string) bool {
+ // This is used for cache pruning. We currently only have images, but we could
+ // imagine expanding on this.
+ return r.imageCache.isInCache(key)
+}
+
+func (r *Spec) DeleteCacheByPrefix(prefix string) {
+ r.imageCache.deleteByPrefix(prefix)
+}
+
+func (r *Spec) ClearCaches() {
+ r.imageCache.clear()
+ r.ResourceCache.clear()
+}
+
+func (r *Spec) CacheStats() string {
+ r.imageCache.mu.RLock()
+ defer r.imageCache.mu.RUnlock()
+
+ s := fmt.Sprintf("Cache entries: %d", len(r.imageCache.store))
+
+ count := 0
+ for k := range r.imageCache.store {
+ if count > 5 {
+ break
+ }
+ s += "\n" + k
+ count++
+ }
+
+ return s
+}
+
+type dirFile struct {
+ // This is the directory component with Unix-style slashes.
+ dir string
+ // This is the file component.
+ file string
+}
+
+func (d dirFile) path() string {
+ return path.Join(d.dir, d.file)
+}
+
+type resourcePathDescriptor struct {
+ // The relative target directory and filename.
+ relTargetDirFile dirFile
+
+ // Callback used to construct a target path relative to its owner.
+ targetPathBuilder func() page.TargetPaths
+
+ // This will normally be the same as above, but this will only apply to publishing
+ // of resources. It may be mulltiple values when in multihost mode.
+ baseTargetPathDirs []string
+
+ // baseOffset is set when the output format's path has a offset, e.g. for AMP.
+ baseOffset string
+}
+
+type resourceContent struct {
+ content string
+ contentInit sync.Once
+}
+
+type resourceHash struct {
+ hash string
+ hashInit sync.Once
+}
+
+type publishOnce struct {
+ publisherInit sync.Once
+ publisherErr error
+ logger *loggers.Logger
+}
+
+func (l *publishOnce) publish(s resource.Source) error {
+ l.publisherInit.Do(func() {
+ l.publisherErr = s.Publish()
+ if l.publisherErr != nil {
+ l.logger.ERROR.Printf("failed to publish Resource: %s", l.publisherErr)
+ }
+ })
+ return l.publisherErr
+}
+
+// genericResource represents a generic linkable resource.
+type genericResource struct {
+ commonResource
+ resourcePathDescriptor
+
+ title string
+ name string
+ params map[string]interface{}
+
+ // Absolute filename to the source, including any content folder path.
+ // Note that this is absolute in relation to the filesystem it is stored in.
+ // It can be a base path filesystem, and then this filename will not match
+ // the path to the file on the real filesystem.
+ sourceFilename string
+
+ // Will be set if this resource is backed by something other than a file.
+ openReadSeekerCloser resource.OpenReadSeekCloser
+
+ // A hash of the source content. Is only calculated in caching situations.
+ *resourceHash
+
+ // This may be set to tell us to look in another filesystem for this resource.
+ // We, by default, use the sourceFs filesystem in the spec below.
+ overriddenSourceFs afero.Fs
+
+ spec *Spec
+
+ resourceType string
+ mediaType media.Type
+
+ osFileInfo os.FileInfo
+
+ // We create copies of this struct, so this needs to be a pointer.
+ *resourceContent
+
+ // May be set to signal lazy/delayed publishing.
+ *publishOnce
+}
+
+type commonResource struct {
+}
+
+func (l *genericResource) Data() interface{} {
+ return noData
+}
+
+func (l *genericResource) Content() (interface{}, error) {
+ if err := l.initContent(); err != nil {
+ return nil, err
+ }
+
+ return l.content, nil
+}
+
+func (l *genericResource) ReadSeekCloser() (hugio.ReadSeekCloser, error) {
+ if l.openReadSeekerCloser != nil {
+ return l.openReadSeekerCloser()
+ }
+ f, err := l.sourceFs().Open(l.sourceFilename)
+ if err != nil {
+ return nil, err
+ }
+ return f, nil
+
+}
+
+func (l *genericResource) MediaType() media.Type {
+ return l.mediaType
+}
+
+// Implement the Cloner interface.
+func (l genericResource) WithNewBase(base string) resource.Resource {
+ l.baseOffset = base
+ l.resourceContent = &resourceContent{}
+ return &l
+}
+
+// Slice is not meant to be used externally. It's a bridge function
+// for the template functions. See collections.Slice.
+func (commonResource) Slice(in interface{}) (interface{}, error) {
+ switch items := in.(type) {
+ case resource.Resources:
+ return items, nil
+ case []interface{}:
+ groups := make(resource.Resources, len(items))
+ for i, v := range items {
+ g, ok := v.(resource.Resource)
+ if !ok {
+ return nil, fmt.Errorf("type %T is not a Resource", v)
+ }
+ groups[i] = g
+ }
+ return groups, nil
+ default:
+ return nil, fmt.Errorf("invalid slice type %T", items)
+ }
+}
+
+func (l *genericResource) initHash() error {
+ var err error
+ l.hashInit.Do(func() {
+ var hash string
+ var f hugio.ReadSeekCloser
+ f, err = l.ReadSeekCloser()
+ if err != nil {
+ err = errors.Wrap(err, "failed to open source file")
+ return
+ }
+ defer f.Close()
+
+ hash, err = helpers.MD5FromFileFast(f)
+ if err != nil {
+ return
+ }
+ l.hash = hash
+
+ })
+
+ return err
+}
+
+func (l *genericResource) initContent() error {
+ var err error
+ l.contentInit.Do(func() {
+ var r hugio.ReadSeekCloser
+ r, err = l.ReadSeekCloser()
+ if err != nil {
+ return
+ }
+ defer r.Close()
+
+ var b []byte
+ b, err = ioutil.ReadAll(r)
+ if err != nil {
+ return
+ }
+
+ l.content = string(b)
+
+ })
+
+ return err
+}
+
+func (l *genericResource) sourceFs() afero.Fs {
+ if l.overriddenSourceFs != nil {
+ return l.overriddenSourceFs
+ }
+ return l.spec.sourceFs()
+}
+
+func (l *genericResource) publishIfNeeded() {
+ if l.publishOnce != nil {
+ l.publishOnce.publish(l)
+ }
+}
+
+func (l *genericResource) Permalink() string {
+ l.publishIfNeeded()
+ return l.spec.PermalinkForBaseURL(l.relPermalinkForRel(l.relTargetDirFile.path(), true), l.spec.BaseURL.HostURL())
+}
+
+func (l *genericResource) RelPermalink() string {
+ l.publishIfNeeded()
+ return l.relPermalinkFor(l.relTargetDirFile.path())
+}
+
+func (l *genericResource) Key() string {
+ return l.relTargetDirFile.path()
+}
+
+func (l *genericResource) relPermalinkFor(target string) string {
+ return l.relPermalinkForRel(target, false)
+
+}
+func (l *genericResource) permalinkFor(target string) string {
+ return l.spec.PermalinkForBaseURL(l.relPermalinkForRel(target, true), l.spec.BaseURL.HostURL())
+
+}
+func (l *genericResource) relTargetPathsFor(target string) []string {
+ return l.relTargetPathsForRel(target)
+}
+
+func (l *genericResource) relTargetPaths() []string {
+ return l.relTargetPathsForRel(l.TargetPath())
+}
+
+func (l *genericResource) Name() string {
+ return l.name
+}
+
+func (l *genericResource) Title() string {
+ return l.title
+}
+
+func (l *genericResource) Params() map[string]interface{} {
+ return l.params
+}
+
+func (l *genericResource) setTitle(title string) {
+ l.title = title
+}
+
+func (l *genericResource) setName(name string) {
+ l.name = name
+}
+
+func (l *genericResource) updateParams(params map[string]interface{}) {
+ if l.params == nil {
+ l.params = params
+ return
+ }
+
+ // Sets the params not already set
+ for k, v := range params {
+ if _, found := l.params[k]; !found {
+ l.params[k] = v
+ }
+ }
+}
+
+func (l *genericResource) relPermalinkForRel(rel string, isAbs bool) string {
+ return l.spec.PathSpec.URLizeFilename(l.relTargetPathForRel(rel, false, isAbs, true))
+}
+
+func (l *genericResource) relTargetPathsForRel(rel string) []string {
+ if len(l.baseTargetPathDirs) == 0 {
+ return []string{l.relTargetPathForRelAndBasePath(rel, "", false, false)}
+ }
+
+ var targetPaths = make([]string, len(l.baseTargetPathDirs))
+ for i, dir := range l.baseTargetPathDirs {
+ targetPaths[i] = l.relTargetPathForRelAndBasePath(rel, dir, false, false)
+ }
+ return targetPaths
+}
+
+func (l *genericResource) relTargetPathForRel(rel string, addBaseTargetPath, isAbs, isURL bool) string {
+ if addBaseTargetPath && len(l.baseTargetPathDirs) > 1 {
+ panic("multiple baseTargetPathDirs")
+ }
+ var basePath string
+ if addBaseTargetPath && len(l.baseTargetPathDirs) > 0 {
+ basePath = l.baseTargetPathDirs[0]
+ }
+
+ return l.relTargetPathForRelAndBasePath(rel, basePath, isAbs, isURL)
+}
+
+func (l *genericResource) createBasePath(rel string, isURL bool) string {
+ if l.targetPathBuilder == nil {
+ return rel
+ }
+ tp := l.targetPathBuilder()
+
+ if isURL {
+ return path.Join(tp.SubResourceBaseLink, rel)
+ }
+
+ // TODO(bep) path
+ return path.Join(filepath.ToSlash(tp.SubResourceBaseTarget), rel)
+}
+
+func (l *genericResource) relTargetPathForRelAndBasePath(rel, basePath string, isAbs, isURL bool) string {
+ rel = l.createBasePath(rel, isURL)
+
+ if basePath != "" {
+ rel = path.Join(basePath, rel)
+ }
+
+ if l.baseOffset != "" {
+ rel = path.Join(l.baseOffset, rel)
+ }
+
+ if isURL {
+ bp := l.spec.PathSpec.GetBasePath(!isAbs)
+ if bp != "" {
+ rel = path.Join(bp, rel)
+ }
+ }
+
+ if len(rel) == 0 || rel[0] != '/' {
+ rel = "/" + rel
+ }
+
+ return rel
+}
+
+func (l *genericResource) ResourceType() string {
+ return l.resourceType
+}
+
+func (l *genericResource) String() string {
+ return fmt.Sprintf("Resource(%s: %s)", l.resourceType, l.name)
+}
+
+func (l *genericResource) Publish() error {
+ fr, err := l.ReadSeekCloser()
+ if err != nil {
+ return err
+ }
+ defer fr.Close()
+
+ fw, err := helpers.OpenFilesForWriting(l.spec.BaseFs.PublishFs, l.targetFilenames()...)
+ if err != nil {
+ return err
+ }
+ defer fw.Close()
+
+ _, err = io.Copy(fw, fr)
+ return err
+}
+
+// Path is stored with Unix style slashes.
+func (l *genericResource) TargetPath() string {
+ return l.relTargetDirFile.path()
+}
+
+func (l *genericResource) targetFilenames() []string {
+ paths := l.relTargetPaths()
+ for i, p := range paths {
+ paths[i] = filepath.Clean(p)
+ }
+ return paths
+}
+
+// TODO(bep) clean up below
+func (r *Spec) newGenericResource(sourceFs afero.Fs,
+ targetPathBuilder func() page.TargetPaths,
+ osFileInfo os.FileInfo,
+ sourceFilename,
+ baseFilename string,
+ mediaType media.Type) *genericResource {
+ return r.newGenericResourceWithBase(
+ sourceFs,
+ false,
+ nil,
+ nil,
+ targetPathBuilder,
+ osFileInfo,
+ sourceFilename,
+ baseFilename,
+ mediaType,
+ )
+
+}
+
+func (r *Spec) newGenericResourceWithBase(
+ sourceFs afero.Fs,
+ lazyPublish bool,
+ openReadSeekerCloser resource.OpenReadSeekCloser,
+ targetPathBaseDirs []string,
+ targetPathBuilder func() page.TargetPaths,
+ osFileInfo os.FileInfo,
+ sourceFilename,
+ baseFilename string,
+ mediaType media.Type) *genericResource {
+
+ // This value is used both to construct URLs and file paths, but start
+ // with a Unix-styled path.
+ baseFilename = helpers.ToSlashTrimLeading(baseFilename)
+ fpath, fname := path.Split(baseFilename)
+
+ var resourceType string
+ if mediaType.MainType == "image" {
+ resourceType = mediaType.MainType
+ } else {
+ resourceType = mediaType.SubType
+ }
+
+ pathDescriptor := resourcePathDescriptor{
+ baseTargetPathDirs: helpers.UniqueStrings(targetPathBaseDirs),
+ targetPathBuilder: targetPathBuilder,
+ relTargetDirFile: dirFile{dir: fpath, file: fname},
+ }
+
+ var po *publishOnce
+ if lazyPublish {
+ po = &publishOnce{logger: r.Logger}
+ }
+
+ return &genericResource{
+ openReadSeekerCloser: openReadSeekerCloser,
+ publishOnce: po,
+ resourcePathDescriptor: pathDescriptor,
+ overriddenSourceFs: sourceFs,
+ osFileInfo: osFileInfo,
+ sourceFilename: sourceFilename,
+ mediaType: mediaType,
+ resourceType: resourceType,
+ spec: r,
+ params: make(map[string]interface{}),
+ name: baseFilename,
+ title: baseFilename,
+ resourceContent: &resourceContent{},
+ resourceHash: &resourceHash{},
+ }
+}
diff --git a/resources/resource/dates.go b/resources/resource/dates.go
new file mode 100644
index 000000000..f26c44787
--- /dev/null
+++ b/resources/resource/dates.go
@@ -0,0 +1,81 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resource
+
+import "time"
+
+var _ Dated = Dates{}
+
+// Dated wraps a "dated resource". These are the 4 dates that makes
+// the date logic in Hugo.
+type Dated interface {
+ Date() time.Time
+ Lastmod() time.Time
+ PublishDate() time.Time
+ ExpiryDate() time.Time
+}
+
+// Dates holds the 4 Hugo dates.
+type Dates struct {
+ FDate time.Time
+ FLastmod time.Time
+ FPublishDate time.Time
+ FExpiryDate time.Time
+}
+
+func (d *Dates) UpdateDateAndLastmodIfAfter(in Dated) {
+ if in.Date().After(d.Date()) {
+ d.FDate = in.Date()
+ }
+ if in.Lastmod().After(d.Lastmod()) {
+ d.FLastmod = in.Lastmod()
+ }
+}
+
+// IsFuture returns whether the argument represents the future.
+func IsFuture(d Dated) bool {
+ if d.PublishDate().IsZero() {
+ return false
+ }
+ return d.PublishDate().After(time.Now())
+}
+
+// IsExpired returns whether the argument is expired.
+func IsExpired(d Dated) bool {
+ if d.ExpiryDate().IsZero() {
+ return false
+ }
+ return d.ExpiryDate().Before(time.Now())
+}
+
+// IsZeroDates returns true if all of the dates are zero.
+func IsZeroDates(d Dated) bool {
+ return d.Date().IsZero() && d.Lastmod().IsZero() && d.ExpiryDate().IsZero() && d.PublishDate().IsZero()
+}
+
+func (p Dates) Date() time.Time {
+ return p.FDate
+}
+
+func (p Dates) Lastmod() time.Time {
+ return p.FLastmod
+}
+
+func (p Dates) PublishDate() time.Time {
+ return p.FPublishDate
+}
+
+func (p Dates) ExpiryDate() time.Time {
+ return p.FExpiryDate
+}
diff --git a/resources/resource/params.go b/resources/resource/params.go
new file mode 100644
index 000000000..f6ecea35a
--- /dev/null
+++ b/resources/resource/params.go
@@ -0,0 +1,89 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resource
+
+import (
+ "strings"
+
+ "github.com/spf13/cast"
+)
+
+func Param(r ResourceParamsProvider, fallback map[string]interface{}, key interface{}) (interface{}, error) {
+ keyStr, err := cast.ToStringE(key)
+ if err != nil {
+ return nil, err
+ }
+
+ keyStr = strings.ToLower(keyStr)
+ result, _ := traverseDirectParams(r, fallback, keyStr)
+ if result != nil {
+ return result, nil
+ }
+
+ keySegments := strings.Split(keyStr, ".")
+ if len(keySegments) == 1 {
+ return nil, nil
+ }
+
+ return traverseNestedParams(r, fallback, keySegments)
+}
+
+func traverseDirectParams(r ResourceParamsProvider, fallback map[string]interface{}, key string) (interface{}, error) {
+ keyStr := strings.ToLower(key)
+ if val, ok := r.Params()[keyStr]; ok {
+ return val, nil
+ }
+
+ if fallback == nil {
+ return nil, nil
+ }
+
+ return fallback[keyStr], nil
+}
+
+func traverseNestedParams(r ResourceParamsProvider, fallback map[string]interface{}, keySegments []string) (interface{}, error) {
+ result := traverseParams(keySegments, r.Params())
+ if result != nil {
+ return result, nil
+ }
+
+ if fallback != nil {
+ result = traverseParams(keySegments, fallback)
+ if result != nil {
+ return result, nil
+ }
+ }
+
+ // Didn't find anything, but also no problems.
+ return nil, nil
+}
+
+func traverseParams(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
+ }
+
+ // That was not the last key.
+ return traverseParams(rest, cast.ToStringMap(result))
+}
diff --git a/resources/resource/resource_helpers.go b/resources/resource/resource_helpers.go
new file mode 100644
index 000000000..b0830a83c
--- /dev/null
+++ b/resources/resource/resource_helpers.go
@@ -0,0 +1,70 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resource
+
+import (
+ "strings"
+ "time"
+
+ "github.com/gohugoio/hugo/helpers"
+
+ "github.com/spf13/cast"
+)
+
+// GetParam will return the param with the given key from the Resource,
+// nil if not found.
+func GetParam(r Resource, key string) interface{} {
+ return getParam(r, key, false)
+}
+
+// GetParamToLower is the same as GetParam but it will lower case any string
+// result, including string slices.
+func GetParamToLower(r Resource, key string) interface{} {
+ return getParam(r, key, true)
+}
+
+func getParam(r Resource, key string, stringToLower bool) interface{} {
+ v := r.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
+ }
+
+ return nil
+}
diff --git a/resources/resource/resources.go b/resources/resource/resources.go
new file mode 100644
index 000000000..5c661c24e
--- /dev/null
+++ b/resources/resource/resources.go
@@ -0,0 +1,123 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resource
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/gohugoio/hugo/resources/internal"
+)
+
+// Resources represents a slice of resources, which can be a mix of different types.
+// I.e. both pages and images etc.
+type Resources []Resource
+
+// ResourcesConverter converts a given slice of Resource objects to Resources.
+type ResourcesConverter interface {
+ ToResources() Resources
+}
+
+// ByType returns resources of a given resource type (ie. "image").
+func (r Resources) ByType(tp string) Resources {
+ var filtered Resources
+
+ for _, resource := range r {
+ if resource.ResourceType() == tp {
+ filtered = append(filtered, resource)
+ }
+ }
+ return filtered
+}
+
+// GetMatch finds the first Resource matching the given pattern, or nil if none found.
+// See Match for a more complete explanation about the rules used.
+func (r Resources) GetMatch(pattern string) Resource {
+ g, err := internal.GetGlob(pattern)
+ if err != nil {
+ return nil
+ }
+
+ for _, resource := range r {
+ if g.Match(strings.ToLower(resource.Name())) {
+ return resource
+ }
+ }
+
+ return nil
+}
+
+// Match gets all resources matching the given base filename prefix, e.g
+// "*.png" will match all png files. The "*" does not match path delimiters (/),
+// so if you organize your resources in sub-folders, you need to be explicit about it, e.g.:
+// "images/*.png". To match any PNG image anywhere in the bundle you can do "**.png", and
+// to match all PNG images below the images folder, use "images/**.jpg".
+// The matching is case insensitive.
+// Match matches by using the value of Resource.Name, which, by default, is a filename with
+// path relative to the bundle root with Unix style slashes (/) and no leading slash, e.g. "images/logo.png".
+// See https://github.com/gobwas/glob for the full rules set.
+func (r Resources) Match(pattern string) Resources {
+ g, err := internal.GetGlob(pattern)
+ if err != nil {
+ return nil
+ }
+
+ var matches Resources
+ for _, resource := range r {
+ if g.Match(strings.ToLower(resource.Name())) {
+ matches = append(matches, resource)
+ }
+ }
+ return matches
+}
+
+type translatedResource interface {
+ TranslationKey() string
+}
+
+// MergeByLanguage adds missing translations in r1 from r2.
+func (r Resources) MergeByLanguage(r2 Resources) Resources {
+ result := append(Resources(nil), r...)
+ m := make(map[string]bool)
+ for _, rr := range r {
+ if translated, ok := rr.(translatedResource); ok {
+ m[translated.TranslationKey()] = true
+ }
+ }
+
+ for _, rr := range r2 {
+ if translated, ok := rr.(translatedResource); ok {
+ if _, found := m[translated.TranslationKey()]; !found {
+ result = append(result, rr)
+ }
+ }
+ }
+ return result
+}
+
+// MergeByLanguageInterface is the generic version of MergeByLanguage. It
+// is here just so it can be called from the tpl package.
+func (r Resources) MergeByLanguageInterface(in interface{}) (interface{}, error) {
+ r2, ok := in.(Resources)
+ if !ok {
+ return nil, fmt.Errorf("%T cannot be merged by language", in)
+ }
+ return r.MergeByLanguage(r2), nil
+}
+
+// Source is an internal template and not meant for use in the templates. It
+// may change without notice.
+type Source interface {
+ Publish() error
+}
diff --git a/resources/resource/resourcetypes.go b/resources/resource/resourcetypes.go
new file mode 100644
index 000000000..5a5839735
--- /dev/null
+++ b/resources/resource/resourcetypes.go
@@ -0,0 +1,166 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resource
+
+import (
+ "github.com/gohugoio/hugo/langs"
+ "github.com/gohugoio/hugo/media"
+
+ "github.com/gohugoio/hugo/common/hugio"
+)
+
+// Cloner is an internal template and not meant for use in the templates. It
+// may change without notice.
+type Cloner interface {
+ WithNewBase(base string) Resource
+}
+
+// Resource represents a linkable resource, i.e. a content page, image etc.
+type Resource interface {
+ ResourceTypesProvider
+ ResourceLinksProvider
+ ResourceMetaProvider
+ ResourceParamsProvider
+ ResourceDataProvider
+}
+
+type ResourceTypesProvider interface {
+ // MediaType is this resource's MIME type.
+ MediaType() media.Type
+
+ // ResourceType is the resource type. For most file types, this is the main
+ // part of the MIME type, e.g. "image", "application", "text" etc.
+ // For content pages, this value is "page".
+ ResourceType() string
+}
+
+type ResourceLinksProvider interface {
+ // Permalink represents the absolute link to this resource.
+ Permalink() string
+
+ // RelPermalink represents the host relative link to this resource.
+ RelPermalink() string
+}
+
+type ResourceMetaProvider interface {
+ // Name is the logical name of this resource. This can be set in the front matter
+ // metadata for this resource. If not set, Hugo will assign a value.
+ // This will in most cases be the base filename.
+ // So, for the image "/some/path/sunset.jpg" this will be "sunset.jpg".
+ // The value returned by this method will be used in the GetByPrefix and ByPrefix methods
+ // on Resources.
+ Name() string
+
+ // Title returns the title if set in front matter. For content pages, this will be the expected value.
+ Title() string
+}
+
+type ResourceParamsProvider interface {
+ // Params set in front matter for this resource.
+ Params() map[string]interface{}
+}
+
+type ResourceDataProvider interface {
+ // Resource specific data set by Hugo.
+ // One example would be.Data.Digest for fingerprinted resources.
+ Data() interface{}
+}
+
+// ResourcesLanguageMerger describes an interface for merging resources from a
+// different language.
+type ResourcesLanguageMerger interface {
+ MergeByLanguage(other Resources) Resources
+ // Needed for integration with the tpl package.
+ MergeByLanguageInterface(other interface{}) (interface{}, error)
+}
+
+// Identifier identifies a resource.
+type Identifier interface {
+ Key() string
+}
+
+// ContentResource represents a Resource that provides a way to get to its content.
+// Most Resource types in Hugo implements this interface, including Page.
+type ContentResource interface {
+ MediaType() media.Type
+ ContentProvider
+}
+
+// ContentProvider provides Content.
+// This should be used with care, as it will read the file content into memory, but it
+// should be cached as effectively as possible by the implementation.
+type ContentProvider interface {
+ // Content returns this resource's content. It will be equivalent to reading the content
+ // that RelPermalink points to in the published folder.
+ // The return type will be contextual, and should be what you would expect:
+ // * Page: template.HTML
+ // * JSON: String
+ // * Etc.
+ Content() (interface{}, error)
+}
+
+// OpenReadSeekCloser allows setting some other way (than reading from a filesystem)
+// to open or create a ReadSeekCloser.
+type OpenReadSeekCloser func() (hugio.ReadSeekCloser, error)
+
+// ReadSeekCloserResource is a Resource that supports loading its content.
+type ReadSeekCloserResource interface {
+ MediaType() media.Type
+ ReadSeekCloser() (hugio.ReadSeekCloser, error)
+}
+
+// LengthProvider is a Resource that provides a length
+// (typically the length of the content).
+type LengthProvider interface {
+ Len() int
+}
+
+// LanguageProvider is a Resource in a language.
+type LanguageProvider interface {
+ Language() *langs.Language
+}
+
+// TranslationKeyProvider connects translations of the same Resource.
+type TranslationKeyProvider interface {
+ TranslationKey() string
+}
+
+type resourceTypesHolder struct {
+ mediaType media.Type
+ resourceType string
+}
+
+func (r resourceTypesHolder) MediaType() media.Type {
+ return r.mediaType
+}
+
+func (r resourceTypesHolder) ResourceType() string {
+ return r.resourceType
+}
+
+func NewResourceTypesProvider(mediaType media.Type, resourceType string) ResourceTypesProvider {
+ return resourceTypesHolder{mediaType: mediaType, resourceType: resourceType}
+}
+
+type languageHolder struct {
+ lang *langs.Language
+}
+
+func (l languageHolder) Language() *langs.Language {
+ return l.lang
+}
+
+func NewLanguageProvider(lang *langs.Language) LanguageProvider {
+ return languageHolder{lang: lang}
+}
diff --git a/resources/resource_cache.go b/resources/resource_cache.go
new file mode 100644
index 000000000..8ff63beb0
--- /dev/null
+++ b/resources/resource_cache.go
@@ -0,0 +1,217 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resources
+
+import (
+ "encoding/json"
+ "io"
+ "path"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "github.com/gohugoio/hugo/resources/resource"
+
+ "github.com/gohugoio/hugo/cache/filecache"
+
+ "github.com/BurntSushi/locker"
+)
+
+const (
+ CACHE_CLEAR_ALL = "clear_all"
+ CACHE_OTHER = "other"
+)
+
+type ResourceCache struct {
+ rs *Spec
+
+ sync.RWMutex
+ cache map[string]resource.Resource
+
+ fileCache *filecache.Cache
+
+ // Provides named resource locks.
+ nlocker *locker.Locker
+}
+
+// ResourceKeyPartition returns a partition name
+// to allow for more fine grained cache flushes.
+// It will return the file extension without the leading ".". If no
+// extension, it will return "other".
+func ResourceKeyPartition(filename string) string {
+ ext := strings.TrimPrefix(path.Ext(filepath.ToSlash(filename)), ".")
+ if ext == "" {
+ ext = CACHE_OTHER
+ }
+ return ext
+}
+
+func newResourceCache(rs *Spec) *ResourceCache {
+ return &ResourceCache{
+ rs: rs,
+ fileCache: rs.FileCaches.AssetsCache(),
+ cache: make(map[string]resource.Resource),
+ nlocker: locker.NewLocker(),
+ }
+}
+
+func (c *ResourceCache) clear() {
+ c.Lock()
+ defer c.Unlock()
+
+ c.cache = make(map[string]resource.Resource)
+ c.nlocker = locker.NewLocker()
+}
+
+func (c *ResourceCache) Contains(key string) bool {
+ key = c.cleanKey(filepath.ToSlash(key))
+ _, found := c.get(key)
+ return found
+}
+
+func (c *ResourceCache) cleanKey(key string) string {
+ return strings.TrimPrefix(path.Clean(key), "/")
+}
+
+func (c *ResourceCache) get(key string) (resource.Resource, bool) {
+ c.RLock()
+ defer c.RUnlock()
+ r, found := c.cache[key]
+ return r, found
+}
+
+func (c *ResourceCache) GetOrCreate(partition, key string, f func() (resource.Resource, error)) (resource.Resource, error) {
+ key = c.cleanKey(path.Join(partition, key))
+ // First check in-memory cache.
+ r, found := c.get(key)
+ if found {
+ return r, nil
+ }
+ // This is a potentially long running operation, so get a named lock.
+ c.nlocker.Lock(key)
+
+ // Double check in-memory cache.
+ r, found = c.get(key)
+ if found {
+ c.nlocker.Unlock(key)
+ return r, nil
+ }
+
+ defer c.nlocker.Unlock(key)
+
+ r, err := f()
+ if err != nil {
+ return nil, err
+ }
+
+ c.set(key, r)
+
+ return r, nil
+
+}
+
+func (c *ResourceCache) getFilenames(key string) (string, string) {
+ filenameMeta := key + ".json"
+ filenameContent := key + ".content"
+
+ return filenameMeta, filenameContent
+}
+
+func (c *ResourceCache) getFromFile(key string) (filecache.ItemInfo, io.ReadCloser, transformedResourceMetadata, bool) {
+ c.RLock()
+ defer c.RUnlock()
+
+ var meta transformedResourceMetadata
+ filenameMeta, filenameContent := c.getFilenames(key)
+
+ _, jsonContent, _ := c.fileCache.GetBytes(filenameMeta)
+ if jsonContent == nil {
+ return filecache.ItemInfo{}, nil, meta, false
+ }
+
+ if err := json.Unmarshal(jsonContent, &meta); err != nil {
+ return filecache.ItemInfo{}, nil, meta, false
+ }
+
+ fi, rc, _ := c.fileCache.Get(filenameContent)
+
+ return fi, rc, meta, rc != nil
+
+}
+
+// writeMeta writes the metadata to file and returns a writer for the content part.
+func (c *ResourceCache) writeMeta(key string, meta transformedResourceMetadata) (filecache.ItemInfo, io.WriteCloser, error) {
+ filenameMeta, filenameContent := c.getFilenames(key)
+ raw, err := json.Marshal(meta)
+ if err != nil {
+ return filecache.ItemInfo{}, nil, err
+ }
+
+ _, fm, err := c.fileCache.WriteCloser(filenameMeta)
+ if err != nil {
+ return filecache.ItemInfo{}, nil, err
+ }
+ defer fm.Close()
+
+ if _, err := fm.Write(raw); err != nil {
+ return filecache.ItemInfo{}, nil, err
+ }
+
+ fi, fc, err := c.fileCache.WriteCloser(filenameContent)
+
+ return fi, fc, err
+
+}
+
+func (c *ResourceCache) set(key string, r resource.Resource) {
+ c.Lock()
+ defer c.Unlock()
+ c.cache[key] = r
+}
+
+func (c *ResourceCache) DeletePartitions(partitions ...string) {
+ partitionsSet := map[string]bool{
+ // Always clear out the resources not matching the partition.
+ "other": true,
+ }
+ for _, p := range partitions {
+ partitionsSet[p] = true
+ }
+
+ if partitionsSet[CACHE_CLEAR_ALL] {
+ c.clear()
+ return
+ }
+
+ c.Lock()
+ defer c.Unlock()
+
+ for k := range c.cache {
+ clear := false
+ partIdx := strings.Index(k, "/")
+ if partIdx == -1 {
+ clear = true
+ } else {
+ partition := k[:partIdx]
+ if partitionsSet[partition] {
+ clear = true
+ }
+ }
+
+ if clear {
+ delete(c.cache, k)
+ }
+ }
+
+}
diff --git a/resources/resource_factories/bundler/bundler.go b/resources/resource_factories/bundler/bundler.go
new file mode 100644
index 000000000..ca0ccf86e
--- /dev/null
+++ b/resources/resource_factories/bundler/bundler.go
@@ -0,0 +1,122 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package bundler contains functions for concatenation etc. of Resource objects.
+package bundler
+
+import (
+ "fmt"
+ "io"
+ "path/filepath"
+
+ "github.com/gohugoio/hugo/common/hugio"
+ "github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/resources"
+ "github.com/gohugoio/hugo/resources/resource"
+)
+
+// Client contains methods perform concatenation and other bundling related
+// tasks to Resource objects.
+type Client struct {
+ rs *resources.Spec
+}
+
+// New creates a new Client with the given specification.
+func New(rs *resources.Spec) *Client {
+ return &Client{rs: rs}
+}
+
+type multiReadSeekCloser struct {
+ mr io.Reader
+ sources []hugio.ReadSeekCloser
+}
+
+func (r *multiReadSeekCloser) Read(p []byte) (n int, err error) {
+ return r.mr.Read(p)
+}
+
+func (r *multiReadSeekCloser) Seek(offset int64, whence int) (newOffset int64, err error) {
+ for _, s := range r.sources {
+ newOffset, err = s.Seek(offset, whence)
+ if err != nil {
+ return
+ }
+ }
+ return
+}
+
+func (r *multiReadSeekCloser) Close() error {
+ for _, s := range r.sources {
+ s.Close()
+ }
+ return nil
+}
+
+// Concat concatenates the list of Resource objects.
+func (c *Client) Concat(targetPath string, r resource.Resources) (resource.Resource, error) {
+ // The CACHE_OTHER will make sure this will be re-created and published on rebuilds.
+ return c.rs.ResourceCache.GetOrCreate(resources.CACHE_OTHER, targetPath, func() (resource.Resource, error) {
+ var resolvedm media.Type
+
+ // The given set of resources must be of the same Media Type.
+ // We may improve on that in the future, but then we need to know more.
+ for i, r := range r {
+ if i > 0 && r.MediaType().Type() != resolvedm.Type() {
+ return nil, fmt.Errorf("resources in Concat must be of the same Media Type, got %q and %q", r.MediaType().Type(), resolvedm.Type())
+ }
+ resolvedm = r.MediaType()
+ }
+
+ concatr := func() (hugio.ReadSeekCloser, error) {
+ var rcsources []hugio.ReadSeekCloser
+ for _, s := range r {
+ rcr, ok := s.(resource.ReadSeekCloserResource)
+ if !ok {
+ return nil, fmt.Errorf("resource %T does not implement resource.ReadSeekerCloserResource", s)
+ }
+ rc, err := rcr.ReadSeekCloser()
+ if err != nil {
+ // Close the already opened.
+ for _, rcs := range rcsources {
+ rcs.Close()
+ }
+ return nil, err
+ }
+ rcsources = append(rcsources, rc)
+ }
+
+ readers := make([]io.Reader, len(rcsources))
+ for i := 0; i < len(rcsources); i++ {
+ readers[i] = rcsources[i]
+ }
+
+ mr := io.MultiReader(readers...)
+
+ return &multiReadSeekCloser{mr: mr, sources: rcsources}, nil
+ }
+
+ composite, err := c.rs.NewForFs(
+ c.rs.FileCaches.AssetsCache().Fs,
+ resources.ResourceSourceDescriptor{
+ LazyPublish: true,
+ OpenReadSeekCloser: concatr,
+ RelTargetFilename: filepath.Clean(targetPath)})
+
+ if err != nil {
+ return nil, err
+ }
+
+ return composite, nil
+ })
+
+}
diff --git a/resources/resource_factories/create/create.go b/resources/resource_factories/create/create.go
new file mode 100644
index 000000000..dc565056d
--- /dev/null
+++ b/resources/resource_factories/create/create.go
@@ -0,0 +1,65 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 contains functions for to create Resource objects. This will
+// typically non-files.
+package create
+
+import (
+ "path/filepath"
+
+ "github.com/spf13/afero"
+
+ "github.com/gohugoio/hugo/common/hugio"
+ "github.com/gohugoio/hugo/resources"
+ "github.com/gohugoio/hugo/resources/resource"
+)
+
+// Client contains methods to create Resource objects.
+// tasks to Resource objects.
+type Client struct {
+ rs *resources.Spec
+}
+
+// New creates a new Client with the given specification.
+func New(rs *resources.Spec) *Client {
+ return &Client{rs: rs}
+}
+
+// Get creates a new Resource by opening the given filename in the given filesystem.
+func (c *Client) Get(fs afero.Fs, filename string) (resource.Resource, error) {
+ filename = filepath.Clean(filename)
+ return c.rs.ResourceCache.GetOrCreate(resources.ResourceKeyPartition(filename), filename, func() (resource.Resource, error) {
+ return c.rs.NewForFs(fs,
+ resources.ResourceSourceDescriptor{
+ LazyPublish: true,
+ SourceFilename: filename})
+ })
+
+}
+
+// FromString creates a new Resource from a string with the given relative target path.
+func (c *Client) FromString(targetPath, content string) (resource.Resource, error) {
+ return c.rs.ResourceCache.GetOrCreate(resources.CACHE_OTHER, targetPath, func() (resource.Resource, error) {
+ return c.rs.NewForFs(
+ c.rs.FileCaches.AssetsCache().Fs,
+ resources.ResourceSourceDescriptor{
+ LazyPublish: true,
+ OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) {
+ return hugio.NewReadSeekerNoOpCloserFromString(content), nil
+ },
+ RelTargetFilename: filepath.Clean(targetPath)})
+
+ })
+
+}
diff --git a/resources/resource_metadata.go b/resources/resource_metadata.go
new file mode 100644
index 000000000..e019133d7
--- /dev/null
+++ b/resources/resource_metadata.go
@@ -0,0 +1,132 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resources
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/gohugoio/hugo/resources/internal"
+ "github.com/gohugoio/hugo/resources/resource"
+
+ "github.com/pkg/errors"
+ "github.com/spf13/cast"
+
+ "strings"
+
+ "github.com/gohugoio/hugo/common/maps"
+)
+
+var (
+ _ metaAssigner = (*genericResource)(nil)
+)
+
+// metaAssigner allows updating metadata in resources that supports it.
+type metaAssigner interface {
+ setTitle(title string)
+ setName(name string)
+ updateParams(params map[string]interface{})
+}
+
+const counterPlaceHolder = ":counter"
+
+// AssignMetadata assigns the given metadata to those resources that supports updates
+// and matching by wildcard given in `src` using `filepath.Match` with lower cased values.
+// This assignment is additive, but the most specific match needs to be first.
+// The `name` and `title` metadata field support shell-matched collection it got a match in.
+// See https://golang.org/pkg/path/#Match
+func AssignMetadata(metadata []map[string]interface{}, resources ...resource.Resource) error {
+ counters := make(map[string]int)
+
+ for _, r := range resources {
+ if _, ok := r.(metaAssigner); !ok {
+ continue
+ }
+
+ var (
+ nameSet, titleSet bool
+ nameCounter, titleCounter = 0, 0
+ nameCounterFound, titleCounterFound bool
+ resourceSrcKey = strings.ToLower(r.Name())
+ )
+
+ ma := r.(metaAssigner)
+ for _, meta := range metadata {
+ src, found := meta["src"]
+ if !found {
+ return fmt.Errorf("missing 'src' in metadata for resource")
+ }
+
+ srcKey := strings.ToLower(cast.ToString(src))
+
+ glob, err := internal.GetGlob(srcKey)
+ if err != nil {
+ return errors.Wrap(err, "failed to match resource with metadata")
+ }
+
+ match := glob.Match(resourceSrcKey)
+
+ if match {
+ if !nameSet {
+ name, found := meta["name"]
+ if found {
+ name := cast.ToString(name)
+ if !nameCounterFound {
+ nameCounterFound = strings.Contains(name, counterPlaceHolder)
+ }
+ if nameCounterFound && nameCounter == 0 {
+ counterKey := "name_" + srcKey
+ nameCounter = counters[counterKey] + 1
+ counters[counterKey] = nameCounter
+ }
+
+ ma.setName(replaceResourcePlaceholders(name, nameCounter))
+ nameSet = true
+ }
+ }
+
+ if !titleSet {
+ title, found := meta["title"]
+ if found {
+ title := cast.ToString(title)
+ if !titleCounterFound {
+ titleCounterFound = strings.Contains(title, counterPlaceHolder)
+ }
+ if titleCounterFound && titleCounter == 0 {
+ counterKey := "title_" + srcKey
+ titleCounter = counters[counterKey] + 1
+ counters[counterKey] = titleCounter
+ }
+ ma.setTitle((replaceResourcePlaceholders(title, titleCounter)))
+ titleSet = true
+ }
+ }
+
+ params, found := meta["params"]
+ if found {
+ m := cast.ToStringMap(params)
+ // Needed for case insensitive fetching of params values
+ maps.ToLower(m)
+ ma.updateParams(m)
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+func replaceResourcePlaceholders(in string, counter int) string {
+ return strings.Replace(in, counterPlaceHolder, strconv.Itoa(counter), -1)
+}
diff --git a/resources/resource_metadata_test.go b/resources/resource_metadata_test.go
new file mode 100644
index 000000000..1dd452ebf
--- /dev/null
+++ b/resources/resource_metadata_test.go
@@ -0,0 +1,231 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resources
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/resources/resource"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestAssignMetadata(t *testing.T) {
+ assert := require.New(t)
+ spec := newTestResourceSpec(assert)
+
+ var foo1, foo2, foo3, logo1, logo2, logo3 resource.Resource
+ var resources resource.Resources
+
+ for _, this := range []struct {
+ metaData []map[string]interface{}
+ assertFunc func(err error)
+ }{
+ {[]map[string]interface{}{
+ {
+ "title": "My Resource",
+ "name": "My Name",
+ "src": "*",
+ },
+ }, func(err error) {
+ assert.Equal("My Resource", logo1.Title())
+ assert.Equal("My Name", logo1.Name())
+ assert.Equal("My Name", foo2.Name())
+
+ }},
+ {[]map[string]interface{}{
+ {
+ "title": "My Logo",
+ "src": "*loGo*",
+ },
+ {
+ "title": "My Resource",
+ "name": "My Name",
+ "src": "*",
+ },
+ }, func(err error) {
+ assert.Equal("My Logo", logo1.Title())
+ assert.Equal("My Logo", logo2.Title())
+ assert.Equal("My Name", logo1.Name())
+ assert.Equal("My Name", foo2.Name())
+ assert.Equal("My Name", foo3.Name())
+ assert.Equal("My Resource", foo3.Title())
+
+ }},
+ {[]map[string]interface{}{
+ {
+ "title": "My Logo",
+ "src": "*loGo*",
+ "params": map[string]interface{}{
+ "Param1": true,
+ "icon": "logo",
+ },
+ },
+ {
+ "title": "My Resource",
+ "src": "*",
+ "params": map[string]interface{}{
+ "Param2": true,
+ "icon": "resource",
+ },
+ },
+ }, func(err error) {
+ assert.NoError(err)
+ assert.Equal("My Logo", logo1.Title())
+ assert.Equal("My Resource", foo3.Title())
+ _, p1 := logo2.Params()["param1"]
+ _, p2 := foo2.Params()["param2"]
+ _, p1_2 := foo2.Params()["param1"]
+ _, p2_2 := logo2.Params()["param2"]
+
+ icon1 := logo2.Params()["icon"]
+ icon2 := foo2.Params()["icon"]
+
+ assert.True(p1)
+ assert.True(p2)
+
+ // Check merge
+ assert.True(p2_2)
+ assert.False(p1_2)
+
+ assert.Equal("logo", icon1)
+ assert.Equal("resource", icon2)
+
+ }},
+ {[]map[string]interface{}{
+ {
+ "name": "Logo Name #:counter",
+ "src": "*logo*",
+ },
+ {
+ "title": "Resource #:counter",
+ "name": "Name #:counter",
+ "src": "*",
+ },
+ }, func(err error) {
+ assert.NoError(err)
+ assert.Equal("Resource #2", logo2.Title())
+ assert.Equal("Logo Name #1", logo2.Name())
+ assert.Equal("Resource #4", logo1.Title())
+ assert.Equal("Logo Name #2", logo1.Name())
+ assert.Equal("Resource #1", foo2.Title())
+ assert.Equal("Resource #3", foo1.Title())
+ assert.Equal("Name #2", foo1.Name())
+ assert.Equal("Resource #5", foo3.Title())
+
+ assert.Equal(logo2, resources.GetMatch("logo name #1*"))
+
+ }},
+ {[]map[string]interface{}{
+ {
+ "title": "Third Logo #:counter",
+ "src": "logo3.png",
+ },
+ {
+ "title": "Other Logo #:counter",
+ "name": "Name #:counter",
+ "src": "logo*",
+ },
+ }, func(err error) {
+ assert.NoError(err)
+ assert.Equal("Third Logo #1", logo3.Title())
+ assert.Equal("Name #3", logo3.Name())
+ assert.Equal("Other Logo #1", logo2.Title())
+ assert.Equal("Name #1", logo2.Name())
+ assert.Equal("Other Logo #2", logo1.Title())
+ assert.Equal("Name #2", logo1.Name())
+
+ }},
+ {[]map[string]interface{}{
+ {
+ "title": "Third Logo",
+ "src": "logo3.png",
+ },
+ {
+ "title": "Other Logo #:counter",
+ "name": "Name #:counter",
+ "src": "logo*",
+ },
+ }, func(err error) {
+ assert.NoError(err)
+ assert.Equal("Third Logo", logo3.Title())
+ assert.Equal("Name #3", logo3.Name())
+ assert.Equal("Other Logo #1", logo2.Title())
+ assert.Equal("Name #1", logo2.Name())
+ assert.Equal("Other Logo #2", logo1.Title())
+ assert.Equal("Name #2", logo1.Name())
+
+ }},
+ {[]map[string]interface{}{
+ {
+ "name": "third-logo",
+ "src": "logo3.png",
+ },
+ {
+ "title": "Logo #:counter",
+ "name": "Name #:counter",
+ "src": "logo*",
+ },
+ }, func(err error) {
+ assert.NoError(err)
+ assert.Equal("Logo #3", logo3.Title())
+ assert.Equal("third-logo", logo3.Name())
+ assert.Equal("Logo #1", logo2.Title())
+ assert.Equal("Name #1", logo2.Name())
+ assert.Equal("Logo #2", logo1.Title())
+ assert.Equal("Name #2", logo1.Name())
+
+ }},
+ {[]map[string]interface{}{
+ {
+ "title": "Third Logo #:counter",
+ },
+ }, func(err error) {
+ // Missing src
+ assert.Error(err)
+
+ }},
+ {[]map[string]interface{}{
+ {
+ "title": "Title",
+ "src": "[]",
+ },
+ }, func(err error) {
+ // Invalid pattern
+ assert.Error(err)
+
+ }},
+ } {
+
+ foo2 = spec.newGenericResource(nil, nil, nil, "/b/foo2.css", "foo2.css", media.CSSType)
+ logo2 = spec.newGenericResource(nil, nil, nil, "/b/Logo2.png", "Logo2.png", pngType)
+ foo1 = spec.newGenericResource(nil, nil, nil, "/a/foo1.css", "foo1.css", media.CSSType)
+ logo1 = spec.newGenericResource(nil, nil, nil, "/a/logo1.png", "logo1.png", pngType)
+ foo3 = spec.newGenericResource(nil, nil, nil, "/b/foo3.css", "foo3.css", media.CSSType)
+ logo3 = spec.newGenericResource(nil, nil, nil, "/b/logo3.png", "logo3.png", pngType)
+
+ resources = resource.Resources{
+ foo2,
+ logo2,
+ foo1,
+ logo1,
+ foo3,
+ logo3,
+ }
+
+ this.assertFunc(AssignMetadata(this.metaData, resources...))
+ }
+
+}
diff --git a/resources/resource_test.go b/resources/resource_test.go
new file mode 100644
index 000000000..af7867eb1
--- /dev/null
+++ b/resources/resource_test.go
@@ -0,0 +1,277 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resources
+
+import (
+ "fmt"
+ "math/rand"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/gohugoio/hugo/resources/resource"
+
+ "github.com/gohugoio/hugo/media"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestGenericResource(t *testing.T) {
+ assert := require.New(t)
+ spec := newTestResourceSpec(assert)
+
+ r := spec.newGenericResource(nil, nil, nil, "/a/foo.css", "foo.css", media.CSSType)
+
+ assert.Equal("https://example.com/foo.css", r.Permalink())
+ assert.Equal("/foo.css", r.RelPermalink())
+ assert.Equal("css", r.ResourceType())
+
+}
+
+func TestGenericResourceWithLinkFacory(t *testing.T) {
+ assert := require.New(t)
+ spec := newTestResourceSpec(assert)
+
+ factory := newTargetPaths("/foo")
+
+ r := spec.newGenericResource(nil, factory, nil, "/a/foo.css", "foo.css", media.CSSType)
+
+ assert.Equal("https://example.com/foo/foo.css", r.Permalink())
+ assert.Equal("/foo/foo.css", r.RelPermalink())
+ assert.Equal("foo.css", r.Key())
+ assert.Equal("css", r.ResourceType())
+}
+
+func TestNewResourceFromFilename(t *testing.T) {
+ assert := require.New(t)
+ spec := newTestResourceSpec(assert)
+
+ writeSource(t, spec.Fs, "content/a/b/logo.png", "image")
+ writeSource(t, spec.Fs, "content/a/b/data.json", "json")
+
+ r, err := spec.New(ResourceSourceDescriptor{SourceFilename: "a/b/logo.png"})
+
+ assert.NoError(err)
+ assert.NotNil(r)
+ assert.Equal("image", r.ResourceType())
+ assert.Equal("/a/b/logo.png", r.RelPermalink())
+ assert.Equal("https://example.com/a/b/logo.png", r.Permalink())
+
+ r, err = spec.New(ResourceSourceDescriptor{SourceFilename: "a/b/data.json"})
+
+ assert.NoError(err)
+ assert.NotNil(r)
+ assert.Equal("json", r.ResourceType())
+
+ cloned := r.(resource.Cloner).WithNewBase("aceof")
+ assert.Equal(r.ResourceType(), cloned.ResourceType())
+ assert.Equal("/aceof/a/b/data.json", cloned.RelPermalink())
+}
+
+func TestNewResourceFromFilenameSubPathInBaseURL(t *testing.T) {
+ assert := require.New(t)
+ spec := newTestResourceSpecForBaseURL(assert, "https://example.com/docs")
+
+ writeSource(t, spec.Fs, "content/a/b/logo.png", "image")
+
+ r, err := spec.New(ResourceSourceDescriptor{SourceFilename: filepath.FromSlash("a/b/logo.png")})
+
+ assert.NoError(err)
+ assert.NotNil(r)
+ assert.Equal("image", r.ResourceType())
+ assert.Equal("/docs/a/b/logo.png", r.RelPermalink())
+ assert.Equal("https://example.com/docs/a/b/logo.png", r.Permalink())
+ img := r.(*Image)
+ assert.Equal(filepath.FromSlash("/a/b/logo.png"), img.targetFilenames()[0])
+
+}
+
+var pngType, _ = media.FromStringAndExt("image/png", "png")
+
+func TestResourcesByType(t *testing.T) {
+ assert := require.New(t)
+ spec := newTestResourceSpec(assert)
+ resources := resource.Resources{
+ spec.newGenericResource(nil, nil, nil, "/a/foo1.css", "foo1.css", media.CSSType),
+ spec.newGenericResource(nil, nil, nil, "/a/logo.png", "logo.css", pngType),
+ spec.newGenericResource(nil, nil, nil, "/a/foo2.css", "foo2.css", media.CSSType),
+ spec.newGenericResource(nil, nil, nil, "/a/foo3.css", "foo3.css", media.CSSType)}
+
+ assert.Len(resources.ByType("css"), 3)
+ assert.Len(resources.ByType("image"), 1)
+
+}
+
+func TestResourcesGetByPrefix(t *testing.T) {
+ assert := require.New(t)
+ spec := newTestResourceSpec(assert)
+ resources := resource.Resources{
+ spec.newGenericResource(nil, nil, nil, "/a/foo1.css", "foo1.css", media.CSSType),
+ spec.newGenericResource(nil, nil, nil, "/a/logo1.png", "logo1.png", pngType),
+ spec.newGenericResource(nil, nil, nil, "/b/Logo2.png", "Logo2.png", pngType),
+ spec.newGenericResource(nil, nil, nil, "/b/foo2.css", "foo2.css", media.CSSType),
+ spec.newGenericResource(nil, nil, nil, "/b/foo3.css", "foo3.css", media.CSSType)}
+
+ assert.Nil(resources.GetMatch("asdf*"))
+ assert.Equal("/logo1.png", resources.GetMatch("logo*").RelPermalink())
+ assert.Equal("/logo1.png", resources.GetMatch("loGo*").RelPermalink())
+ assert.Equal("/Logo2.png", resources.GetMatch("logo2*").RelPermalink())
+ assert.Equal("/foo2.css", resources.GetMatch("foo2*").RelPermalink())
+ assert.Equal("/foo1.css", resources.GetMatch("foo1*").RelPermalink())
+ assert.Equal("/foo1.css", resources.GetMatch("foo1*").RelPermalink())
+ assert.Nil(resources.GetMatch("asdfasdf*"))
+
+ assert.Equal(2, len(resources.Match("logo*")))
+ assert.Equal(1, len(resources.Match("logo2*")))
+
+ logo := resources.GetMatch("logo*")
+ assert.NotNil(logo.Params())
+ assert.Equal("logo1.png", logo.Name())
+ assert.Equal("logo1.png", logo.Title())
+
+}
+
+func TestResourcesGetMatch(t *testing.T) {
+ assert := require.New(t)
+ spec := newTestResourceSpec(assert)
+ resources := resource.Resources{
+ spec.newGenericResource(nil, nil, nil, "/a/foo1.css", "foo1.css", media.CSSType),
+ spec.newGenericResource(nil, nil, nil, "/a/logo1.png", "logo1.png", pngType),
+ spec.newGenericResource(nil, nil, nil, "/b/Logo2.png", "Logo2.png", pngType),
+ spec.newGenericResource(nil, nil, nil, "/b/foo2.css", "foo2.css", media.CSSType),
+ spec.newGenericResource(nil, nil, nil, "/b/foo3.css", "foo3.css", media.CSSType),
+ spec.newGenericResource(nil, nil, nil, "/b/c/foo4.css", "c/foo4.css", media.CSSType),
+ spec.newGenericResource(nil, nil, nil, "/b/c/foo5.css", "c/foo5.css", media.CSSType),
+ spec.newGenericResource(nil, nil, nil, "/b/c/d/foo6.css", "c/d/foo6.css", media.CSSType),
+ }
+
+ assert.Equal("/logo1.png", resources.GetMatch("logo*").RelPermalink())
+ assert.Equal("/logo1.png", resources.GetMatch("loGo*").RelPermalink())
+ assert.Equal("/Logo2.png", resources.GetMatch("logo2*").RelPermalink())
+ assert.Equal("/foo2.css", resources.GetMatch("foo2*").RelPermalink())
+ assert.Equal("/foo1.css", resources.GetMatch("foo1*").RelPermalink())
+ assert.Equal("/foo1.css", resources.GetMatch("foo1*").RelPermalink())
+ assert.Equal("/c/foo4.css", resources.GetMatch("*/foo*").RelPermalink())
+
+ assert.Nil(resources.GetMatch("asdfasdf"))
+
+ assert.Equal(2, len(resources.Match("Logo*")))
+ assert.Equal(1, len(resources.Match("logo2*")))
+ assert.Equal(2, len(resources.Match("c/*")))
+
+ assert.Equal(6, len(resources.Match("**.css")))
+ assert.Equal(3, len(resources.Match("**/*.css")))
+ assert.Equal(1, len(resources.Match("c/**/*.css")))
+
+ // Matches only CSS files in c/
+ assert.Equal(3, len(resources.Match("c/**.css")))
+
+ // Matches all CSS files below c/ (including in c/d/)
+ assert.Equal(3, len(resources.Match("c/**.css")))
+
+ // Patterns beginning with a slash will not match anything.
+ // We could maybe consider trimming that slash, but let's be explicit about this.
+ // (it is possible for users to do a rename)
+ // This is analogous to standing in a directory and doing "ls *.*".
+ assert.Equal(0, len(resources.Match("/c/**.css")))
+
+}
+
+func BenchmarkResourcesMatch(b *testing.B) {
+ resources := benchResources(b)
+ prefixes := []string{"abc*", "jkl*", "nomatch*", "sub/*"}
+ rnd := rand.New(rand.NewSource(time.Now().Unix()))
+
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ resources.Match(prefixes[rnd.Intn(len(prefixes))])
+ }
+ })
+}
+
+// This adds a benchmark for the a100 test case as described by Russ Cox here:
+// https://research.swtch.com/glob (really interesting article)
+// I don't expect Hugo users to "stumble upon" this problem, so this is more to satisfy
+// my own curiosity.
+func BenchmarkResourcesMatchA100(b *testing.B) {
+ assert := require.New(b)
+ spec := newTestResourceSpec(assert)
+ a100 := strings.Repeat("a", 100)
+ pattern := "a*a*a*a*a*a*a*a*b"
+
+ resources := resource.Resources{spec.newGenericResource(nil, nil, nil, "/a/"+a100, a100, media.CSSType)}
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ resources.Match(pattern)
+ }
+
+}
+
+func benchResources(b *testing.B) resource.Resources {
+ assert := require.New(b)
+ spec := newTestResourceSpec(assert)
+ var resources resource.Resources
+
+ for i := 0; i < 30; i++ {
+ name := fmt.Sprintf("abcde%d_%d.css", i%5, i)
+ resources = append(resources, spec.newGenericResource(nil, nil, nil, "/a/"+name, name, media.CSSType))
+ }
+
+ for i := 0; i < 30; i++ {
+ name := fmt.Sprintf("efghi%d_%d.css", i%5, i)
+ resources = append(resources, spec.newGenericResource(nil, nil, nil, "/a/"+name, name, media.CSSType))
+ }
+
+ for i := 0; i < 30; i++ {
+ name := fmt.Sprintf("jklmn%d_%d.css", i%5, i)
+ resources = append(resources, spec.newGenericResource(nil, nil, nil, "/b/sub/"+name, "sub/"+name, media.CSSType))
+ }
+
+ return resources
+
+}
+
+func BenchmarkAssignMetadata(b *testing.B) {
+ assert := require.New(b)
+ spec := newTestResourceSpec(assert)
+
+ for i := 0; i < b.N; i++ {
+ b.StopTimer()
+ var resources resource.Resources
+ var meta = []map[string]interface{}{
+ {
+ "title": "Foo #:counter",
+ "name": "Foo Name #:counter",
+ "src": "foo1*",
+ },
+ {
+ "title": "Rest #:counter",
+ "name": "Rest Name #:counter",
+ "src": "*",
+ },
+ }
+ for i := 0; i < 20; i++ {
+ name := fmt.Sprintf("foo%d_%d.css", i%5, i)
+ resources = append(resources, spec.newGenericResource(nil, nil, nil, "/a/"+name, name, media.CSSType))
+ }
+ b.StartTimer()
+
+ if err := AssignMetadata(meta, resources...); err != nil {
+ b.Fatal(err)
+ }
+
+ }
+}
diff --git a/resources/resource_transformers/integrity/integrity.go b/resources/resource_transformers/integrity/integrity.go
new file mode 100644
index 000000000..95065603d
--- /dev/null
+++ b/resources/resource_transformers/integrity/integrity.go
@@ -0,0 +1,113 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package integrity
+
+import (
+ "crypto/md5"
+ "crypto/sha256"
+ "crypto/sha512"
+ "encoding/base64"
+ "encoding/hex"
+ "hash"
+ "html/template"
+ "io"
+
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/resources"
+ "github.com/gohugoio/hugo/resources/resource"
+)
+
+const defaultHashAlgo = "sha256"
+
+// Client contains methods to fingerprint (cachebusting) and other integrity-related
+// methods.
+type Client struct {
+ rs *resources.Spec
+}
+
+// New creates a new Client with the given specification.
+func New(rs *resources.Spec) *Client {
+ return &Client{rs: rs}
+}
+
+type fingerprintTransformation struct {
+ algo string
+}
+
+func (t *fingerprintTransformation) Key() resources.ResourceTransformationKey {
+ return resources.NewResourceTransformationKey("fingerprint", t.algo)
+}
+
+// Transform creates a MD5 hash of the Resource content and inserts that hash before
+// the extension in the filename.
+func (t *fingerprintTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
+
+ h, err := newHash(t.algo)
+ if err != nil {
+ return err
+ }
+
+ io.Copy(io.MultiWriter(h, ctx.To), ctx.From)
+ d, err := digest(h)
+ if err != nil {
+ return err
+ }
+
+ ctx.Data["Integrity"] = integrity(t.algo, d)
+ ctx.AddOutPathIdentifier("." + hex.EncodeToString(d[:]))
+ return nil
+}
+
+func newHash(algo string) (hash.Hash, error) {
+ switch algo {
+ case "md5":
+ return md5.New(), nil
+ case "sha256":
+ return sha256.New(), nil
+ case "sha384":
+ return sha512.New384(), nil
+ case "sha512":
+ return sha512.New(), nil
+ default:
+ return nil, errors.Errorf("unsupported crypto algo: %q, use either md5, sha256, sha384 or sha512", algo)
+ }
+}
+
+// Fingerprint applies fingerprinting of the given resource and hash algorithm.
+// It defaults to sha256 if none given, and the options are md5, sha256 or sha512.
+// The same algo is used for both the fingerprinting part (aka cache busting) and
+// the base64-encoded Subresource Integrity hash, so you will have to stay away from
+// md5 if you plan to use both.
+// See https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
+func (c *Client) Fingerprint(res resource.Resource, algo string) (resource.Resource, error) {
+ if algo == "" {
+ algo = defaultHashAlgo
+ }
+
+ return c.rs.Transform(
+ res,
+ &fingerprintTransformation{algo: algo},
+ )
+}
+
+func integrity(algo string, sum []byte) template.HTMLAttr {
+ encoded := base64.StdEncoding.EncodeToString(sum)
+ return template.HTMLAttr(algo + "-" + encoded)
+}
+
+func digest(h hash.Hash) ([]byte, error) {
+ sum := h.Sum(nil)
+ return sum, nil
+}
diff --git a/resources/resource_transformers/integrity/integrity_test.go b/resources/resource_transformers/integrity/integrity_test.go
new file mode 100644
index 000000000..7e32e3275
--- /dev/null
+++ b/resources/resource_transformers/integrity/integrity_test.go
@@ -0,0 +1,48 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package integrity
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestHashFromAlgo(t *testing.T) {
+
+ for _, algo := range []struct {
+ name string
+ bits int
+ }{
+ {"md5", 128},
+ {"sha256", 256},
+ {"sha384", 384},
+ {"sha512", 512},
+ {"shaman", -1},
+ } {
+
+ t.Run(algo.name, func(t *testing.T) {
+ assert := require.New(t)
+ h, err := newHash(algo.name)
+ if algo.bits > 0 {
+ assert.NoError(err)
+ assert.Equal(algo.bits/8, h.Size())
+ } else {
+ assert.Error(err)
+ assert.Contains(err.Error(), "use either md5, sha256, sha384 or sha512")
+ }
+
+ })
+ }
+}
diff --git a/resources/resource_transformers/minifier/minify.go b/resources/resource_transformers/minifier/minify.go
new file mode 100644
index 000000000..952c6a99c
--- /dev/null
+++ b/resources/resource_transformers/minifier/minify.go
@@ -0,0 +1,59 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package minifier
+
+import (
+ "github.com/gohugoio/hugo/minifiers"
+ "github.com/gohugoio/hugo/resources"
+ "github.com/gohugoio/hugo/resources/resource"
+)
+
+// Client for minification of Resource objects. Supported minfiers are:
+// css, html, js, json, svg and xml.
+type Client struct {
+ rs *resources.Spec
+ m minifiers.Client
+}
+
+// New creates a new Client given a specification. Note that it is the media types
+// configured for the site that is used to match files to the correct minifier.
+func New(rs *resources.Spec) *Client {
+ return &Client{rs: rs, m: minifiers.New(rs.MediaTypes, rs.OutputFormats)}
+}
+
+type minifyTransformation struct {
+ rs *resources.Spec
+ m minifiers.Client
+}
+
+func (t *minifyTransformation) Key() resources.ResourceTransformationKey {
+ return resources.NewResourceTransformationKey("minify")
+}
+
+func (t *minifyTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
+ if err := t.m.Minify(ctx.InMediaType, ctx.To, ctx.From); err != nil {
+ return err
+ }
+ ctx.AddOutPathIdentifier(".min")
+ return nil
+}
+
+func (c *Client) Minify(res resource.Resource) (resource.Resource, error) {
+ return c.rs.Transform(
+ res,
+ &minifyTransformation{
+ rs: c.rs,
+ m: c.m},
+ )
+}
diff --git a/resources/resource_transformers/postcss/postcss.go b/resources/resource_transformers/postcss/postcss.go
new file mode 100644
index 000000000..5350eebc5
--- /dev/null
+++ b/resources/resource_transformers/postcss/postcss.go
@@ -0,0 +1,185 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package postcss
+
+import (
+ "io"
+ "path/filepath"
+
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/pkg/errors"
+
+ "os"
+ "os/exec"
+
+ "github.com/mitchellh/mapstructure"
+
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/resources"
+ "github.com/gohugoio/hugo/resources/resource"
+)
+
+// Some of the options from https://github.com/postcss/postcss-cli
+type Options struct {
+
+ // Set a custom path to look for a config file.
+ Config string
+
+ NoMap bool `mapstructure:"no-map"` // Disable the default inline sourcemaps
+
+ // Options for when not using a config file
+ Use string // List of postcss plugins to use
+ Parser string // Custom postcss parser
+ Stringifier string // Custom postcss stringifier
+ Syntax string // Custom postcss syntax
+}
+
+func DecodeOptions(m map[string]interface{}) (opts Options, err error) {
+ if m == nil {
+ return
+ }
+ err = mapstructure.WeakDecode(m, &opts)
+ return
+}
+
+func (opts Options) toArgs() []string {
+ var args []string
+ if opts.NoMap {
+ args = append(args, "--no-map")
+ }
+ if opts.Use != "" {
+ args = append(args, "--use", opts.Use)
+ }
+ if opts.Parser != "" {
+ args = append(args, "--parser", opts.Parser)
+ }
+ if opts.Stringifier != "" {
+ args = append(args, "--stringifier", opts.Stringifier)
+ }
+ if opts.Syntax != "" {
+ args = append(args, "--syntax", opts.Syntax)
+ }
+ return args
+}
+
+// Client is the client used to do PostCSS transformations.
+type Client struct {
+ rs *resources.Spec
+}
+
+// New creates a new Client with the given specification.
+func New(rs *resources.Spec) *Client {
+ return &Client{rs: rs}
+}
+
+type postcssTransformation struct {
+ options Options
+ rs *resources.Spec
+}
+
+func (t *postcssTransformation) Key() resources.ResourceTransformationKey {
+ return resources.NewResourceTransformationKey("postcss", t.options)
+}
+
+// Transform shells out to postcss-cli to do the heavy lifting.
+// For this to work, you need some additional tools. To install them globally:
+// npm install -g postcss-cli
+// npm install -g autoprefixer
+func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
+
+ const localPostCSSPath = "node_modules/postcss-cli/bin/"
+ const binaryName = "postcss"
+
+ // Try first in the project's node_modules.
+ csiBinPath := filepath.Join(t.rs.WorkingDir, localPostCSSPath, binaryName)
+
+ binary := csiBinPath
+
+ if _, err := exec.LookPath(binary); err != nil {
+ // Try PATH
+ binary = binaryName
+ if _, err := exec.LookPath(binary); err != nil {
+ // This may be on a CI server etc. Will fall back to pre-built assets.
+ return herrors.ErrFeatureNotAvailable
+ }
+ }
+
+ var configFile string
+ logger := t.rs.Logger
+
+ if t.options.Config != "" {
+ configFile = t.options.Config
+ } else {
+ configFile = "postcss.config.js"
+ }
+
+ configFile = filepath.Clean(configFile)
+
+ // We need an abolute filename to the config file.
+ if !filepath.IsAbs(configFile) {
+ // We resolve this against the virtual Work filesystem, to allow
+ // this config file to live in one of the themes if needed.
+ fi, err := t.rs.BaseFs.Work.Fs.Stat(configFile)
+ if err != nil {
+ if t.options.Config != "" {
+ // Only fail if the user specificed config file is not found.
+ return errors.Wrapf(err, "postcss config %q not found:", configFile)
+ }
+ configFile = ""
+ } else {
+ configFile = fi.(hugofs.RealFilenameInfo).RealFilename()
+ }
+ }
+
+ var cmdArgs []string
+
+ if configFile != "" {
+ logger.INFO.Println("postcss: use config file", configFile)
+ cmdArgs = []string{"--config", configFile}
+ }
+
+ if optArgs := t.options.toArgs(); len(optArgs) > 0 {
+ cmdArgs = append(cmdArgs, optArgs...)
+ }
+
+ cmd := exec.Command(binary, cmdArgs...)
+
+ cmd.Stdout = ctx.To
+ cmd.Stderr = os.Stderr
+
+ stdin, err := cmd.StdinPipe()
+ if err != nil {
+ return err
+ }
+
+ go func() {
+ defer stdin.Close()
+ io.Copy(stdin, ctx.From)
+ }()
+
+ err = cmd.Run()
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// Process transforms the given Resource with the PostCSS processor.
+func (c *Client) Process(res resource.Resource, options Options) (resource.Resource, error) {
+ return c.rs.Transform(
+ res,
+ &postcssTransformation{rs: c.rs, options: options},
+ )
+}
diff --git a/resources/resource_transformers/templates/execute_as_template.go b/resources/resource_transformers/templates/execute_as_template.go
new file mode 100644
index 000000000..b3ec3cf43
--- /dev/null
+++ b/resources/resource_transformers/templates/execute_as_template.go
@@ -0,0 +1,76 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package templates contains functions for template processing of Resource objects.
+package templates
+
+import (
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/resources"
+ "github.com/gohugoio/hugo/resources/resource"
+ "github.com/gohugoio/hugo/tpl"
+ "github.com/pkg/errors"
+)
+
+// Client contains methods to perform template processing of Resource objects.
+type Client struct {
+ rs *resources.Spec
+
+ textTemplate tpl.TemplateParseFinder
+}
+
+// New creates a new Client with the given specification.
+func New(rs *resources.Spec, textTemplate tpl.TemplateParseFinder) *Client {
+ if rs == nil {
+ panic("must provice a resource Spec")
+ }
+ if textTemplate == nil {
+ panic("must provide a textTemplate")
+ }
+ return &Client{rs: rs, textTemplate: textTemplate}
+}
+
+type executeAsTemplateTransform struct {
+ rs *resources.Spec
+ textTemplate tpl.TemplateParseFinder
+ targetPath string
+ data interface{}
+}
+
+func (t *executeAsTemplateTransform) Key() resources.ResourceTransformationKey {
+ return resources.NewResourceTransformationKey("execute-as-template", t.targetPath)
+}
+
+func (t *executeAsTemplateTransform) Transform(ctx *resources.ResourceTransformationCtx) error {
+ tplStr := helpers.ReaderToString(ctx.From)
+ templ, err := t.textTemplate.Parse(ctx.InPath, tplStr)
+ if err != nil {
+ return errors.Wrapf(err, "failed to parse Resource %q as Template:", ctx.InPath)
+ }
+
+ ctx.OutPath = t.targetPath
+
+ return templ.Execute(ctx.To, t.data)
+}
+
+func (c *Client) ExecuteAsTemplate(res resource.Resource, targetPath string, data interface{}) (resource.Resource, error) {
+ return c.rs.Transform(
+ res,
+ &executeAsTemplateTransform{
+ rs: c.rs,
+ targetPath: helpers.ToSlashTrimLeading(targetPath),
+ textTemplate: c.textTemplate,
+ data: data,
+ },
+ )
+}
diff --git a/resources/resource_transformers/tocss/scss/client.go b/resources/resource_transformers/tocss/scss/client.go
new file mode 100644
index 000000000..41ff67433
--- /dev/null
+++ b/resources/resource_transformers/tocss/scss/client.go
@@ -0,0 +1,111 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package scss
+
+import (
+ "github.com/bep/go-tocss/scss"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugolib/filesystems"
+ "github.com/gohugoio/hugo/resources"
+ "github.com/gohugoio/hugo/resources/resource"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+type Client struct {
+ rs *resources.Spec
+ sfs *filesystems.SourceFilesystem
+ workFs *filesystems.SourceFilesystem
+}
+
+func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) (*Client, error) {
+ return &Client{sfs: fs, workFs: rs.BaseFs.Work, rs: rs}, nil
+}
+
+type Options struct {
+
+ // Hugo, will by default, just replace the extension of the source
+ // to .css, e.g. "scss/main.scss" becomes "scss/main.css". You can
+ // control this by setting this, e.g. "styles/main.css" will create
+ // a Resource with that as a base for RelPermalink etc.
+ TargetPath string
+
+ // Hugo automatically adds the entry directories (where the main.scss lives)
+ // for project and themes to the list of include paths sent to LibSASS.
+ // Any paths set in this setting will be appended. Note that these will be
+ // treated as relative to the working dir, i.e. no include paths outside the
+ // project/themes.
+ IncludePaths []string
+
+ // Default is nested.
+ // One of nested, expanded, compact, compressed.
+ OutputStyle string
+
+ // Precision of floating point math.
+ Precision int
+
+ // When enabled, Hugo will generate a source map.
+ EnableSourceMap bool
+}
+
+type options struct {
+ // The options we receive from the end user.
+ from Options
+
+ // The options we send to the SCSS library.
+ to scss.Options
+}
+
+func (c *Client) ToCSS(res resource.Resource, opts Options) (resource.Resource, error) {
+ internalOptions := options{
+ from: opts,
+ }
+
+ // Transfer values from client.
+ internalOptions.to.Precision = opts.Precision
+ internalOptions.to.OutputStyle = scss.OutputStyleFromString(opts.OutputStyle)
+
+ if internalOptions.to.Precision == 0 {
+ // bootstrap-sass requires 8 digits precision. The libsass default is 5.
+ // https://github.com/twbs/bootstrap-sass/blob/master/README.md#sass-number-precision
+ internalOptions.to.Precision = 8
+ }
+
+ return c.rs.Transform(
+ res,
+ &toCSSTransformation{c: c, options: internalOptions},
+ )
+}
+
+type toCSSTransformation struct {
+ c *Client
+ options options
+}
+
+func (t *toCSSTransformation) Key() resources.ResourceTransformationKey {
+ return resources.NewResourceTransformationKey("tocss", t.options.from)
+}
+
+func DecodeOptions(m map[string]interface{}) (opts Options, err error) {
+ if m == nil {
+ return
+ }
+ err = mapstructure.WeakDecode(m, &opts)
+
+ if opts.TargetPath != "" {
+ opts.TargetPath = helpers.ToSlashTrimLeading(opts.TargetPath)
+ }
+
+ return
+}
diff --git a/resources/resource_transformers/tocss/scss/tocss.go b/resources/resource_transformers/tocss/scss/tocss.go
new file mode 100644
index 000000000..17c32ea8e
--- /dev/null
+++ b/resources/resource_transformers/tocss/scss/tocss.go
@@ -0,0 +1,173 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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 extended
+
+package scss
+
+import (
+ "fmt"
+ "io"
+ "path"
+ "path/filepath"
+ "strings"
+
+ "github.com/bep/go-tocss/scss"
+ "github.com/bep/go-tocss/scss/libsass"
+ "github.com/bep/go-tocss/tocss"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/resources"
+ "github.com/pkg/errors"
+)
+
+// Used in tests. This feature requires Hugo to be built with the extended tag.
+func Supports() bool {
+ return true
+}
+
+func (t *toCSSTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
+ ctx.OutMediaType = media.CSSType
+
+ var outName string
+ if t.options.from.TargetPath != "" {
+ ctx.OutPath = t.options.from.TargetPath
+ } else {
+ ctx.ReplaceOutPathExtension(".css")
+ }
+
+ outName = path.Base(ctx.OutPath)
+
+ options := t.options
+ baseDir := path.Dir(ctx.SourcePath)
+ options.to.IncludePaths = t.c.sfs.RealDirs(baseDir)
+
+ // Append any workDir relative include paths
+ for _, ip := range options.from.IncludePaths {
+ options.to.IncludePaths = append(options.to.IncludePaths, t.c.workFs.RealDirs(filepath.Clean(ip))...)
+ }
+
+ // To allow for overrides of SCSS files anywhere in the project/theme hierarchy, we need
+ // to help libsass revolve the filename by looking in the composite filesystem first.
+ // We add the entry directories for both project and themes to the include paths list, but
+ // that only work for overrides on the top level.
+ options.to.ImportResolver = func(url string, prev string) (newUrl string, body string, resolved bool) {
+ // We get URL paths from LibSASS, but we need file paths.
+ url = filepath.FromSlash(url)
+ prev = filepath.FromSlash(prev)
+
+ var basePath string
+ urlDir := filepath.Dir(url)
+ var prevDir string
+ if prev == "stdin" {
+ prevDir = baseDir
+ } else {
+ prevDir = t.c.sfs.MakePathRelative(filepath.Dir(prev))
+ if prevDir == "" {
+ // Not a member of this filesystem. Let LibSASS handle it.
+ return "", "", false
+ }
+ }
+
+ basePath = filepath.Join(prevDir, urlDir)
+ name := filepath.Base(url)
+
+ // Libsass throws an error in cases where you have several possible candidates.
+ // We make this simpler and pick the first match.
+ var namePatterns []string
+ if strings.Contains(name, ".") {
+ namePatterns = []string{"_%s", "%s"}
+ } else if strings.HasPrefix(name, "_") {
+ namePatterns = []string{"_%s.scss", "_%s.sass"}
+ } else {
+ namePatterns = []string{"_%s.scss", "%s.scss", "_%s.sass", "%s.sass"}
+ }
+
+ name = strings.TrimPrefix(name, "_")
+
+ for _, namePattern := range namePatterns {
+ filenameToCheck := filepath.Join(basePath, fmt.Sprintf(namePattern, name))
+ fi, err := t.c.sfs.Fs.Stat(filenameToCheck)
+ if err == nil {
+ if fir, ok := fi.(hugofs.RealFilenameInfo); ok {
+ return fir.RealFilename(), "", true
+ }
+ }
+ }
+
+ // Not found, let LibSASS handle it
+ return "", "", false
+ }
+
+ if ctx.InMediaType.SubType == media.SASSType.SubType {
+ options.to.SassSyntax = true
+ }
+
+ if options.from.EnableSourceMap {
+
+ options.to.SourceMapFilename = outName + ".map"
+ options.to.SourceMapRoot = t.c.rs.WorkingDir
+
+ // Setting this to the relative input filename will get the source map
+ // more correct for the main entry path (main.scss typically), but
+ // it will mess up the import mappings. As a workaround, we do a replacement
+ // in the source map itself (see below).
+ //options.InputPath = inputPath
+ options.to.OutputPath = outName
+ options.to.SourceMapContents = true
+ options.to.OmitSourceMapURL = false
+ options.to.EnableEmbeddedSourceMap = false
+ }
+
+ res, err := t.c.toCSS(options.to, ctx.To, ctx.From)
+ if err != nil {
+ return err
+ }
+
+ if options.from.EnableSourceMap && res.SourceMapContent != "" {
+ sourcePath := t.c.sfs.RealFilename(ctx.SourcePath)
+
+ if strings.HasPrefix(sourcePath, t.c.rs.WorkingDir) {
+ sourcePath = strings.TrimPrefix(sourcePath, t.c.rs.WorkingDir+helpers.FilePathSeparator)
+ }
+
+ // This needs to be Unix-style slashes, even on Windows.
+ // See https://github.com/gohugoio/hugo/issues/4968
+ sourcePath = filepath.ToSlash(sourcePath)
+
+ // This is a workaround for what looks like a bug in Libsass. But
+ // getting this resolution correct in tools like Chrome Workspaces
+ // is important enough to go this extra mile.
+ mapContent := strings.Replace(res.SourceMapContent, `stdin",`, fmt.Sprintf("%s\",", sourcePath), 1)
+
+ return ctx.PublishSourceMap(mapContent)
+ }
+ return nil
+}
+
+func (c *Client) toCSS(options scss.Options, dst io.Writer, src io.Reader) (tocss.Result, error) {
+ var res tocss.Result
+
+ transpiler, err := libsass.New(options)
+ if err != nil {
+ return res, err
+ }
+
+ res, err = transpiler.Execute(dst, src)
+ if err != nil {
+ return res, errors.Wrap(err, "SCSS processing failed")
+ }
+
+ return res, nil
+}
diff --git a/resources/resource_transformers/tocss/scss/tocss_notavailable.go b/resources/resource_transformers/tocss/scss/tocss_notavailable.go
new file mode 100644
index 000000000..ad6b42b98
--- /dev/null
+++ b/resources/resource_transformers/tocss/scss/tocss_notavailable.go
@@ -0,0 +1,30 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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 !extended
+
+package scss
+
+import (
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/resources"
+)
+
+// Used in tests.
+func Supports() bool {
+ return false
+}
+
+func (t *toCSSTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
+ return herrors.ErrFeatureNotAvailable
+}
diff --git a/resources/smartcrop.go b/resources/smartcrop.go
new file mode 100644
index 000000000..05bc55cd7
--- /dev/null
+++ b/resources/smartcrop.go
@@ -0,0 +1,80 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resources
+
+import (
+ "image"
+
+ "github.com/disintegration/imaging"
+ "github.com/muesli/smartcrop"
+)
+
+const (
+ // Do not change.
+ smartCropIdentifier = "smart"
+
+ // This is just a increment, starting on 1. If Smart Crop improves its cropping, we
+ // need a way to trigger a re-generation of the crops in the wild, so increment this.
+ smartCropVersionNumber = 1
+)
+
+// Needed by smartcrop
+type imagingResizer struct {
+ filter imaging.ResampleFilter
+}
+
+func (r imagingResizer) Resize(img image.Image, width, height uint) image.Image {
+ return imaging.Resize(img, int(width), int(height), r.filter)
+}
+
+func newSmartCropAnalyzer(filter imaging.ResampleFilter) smartcrop.Analyzer {
+ return smartcrop.NewAnalyzer(imagingResizer{filter: filter})
+}
+
+func smartCrop(img image.Image, width, height int, anchor imaging.Anchor, filter imaging.ResampleFilter) (*image.NRGBA, error) {
+
+ if width <= 0 || height <= 0 {
+ return &image.NRGBA{}, nil
+ }
+
+ srcBounds := img.Bounds()
+ srcW := srcBounds.Dx()
+ srcH := srcBounds.Dy()
+
+ if srcW <= 0 || srcH <= 0 {
+ return &image.NRGBA{}, nil
+ }
+
+ if srcW == width && srcH == height {
+ return imaging.Clone(img), nil
+ }
+
+ smart := newSmartCropAnalyzer(filter)
+
+ rect, err := smart.FindBestCrop(img, width, height)
+
+ if err != nil {
+ return nil, err
+ }
+
+ b := img.Bounds().Intersect(rect)
+
+ cropped, err := imaging.Crop(img, b), nil
+ if err != nil {
+ return nil, err
+ }
+
+ return imaging.Resize(cropped, width, height, filter), nil
+
+}
diff --git a/resources/testdata/1234567890qwertyuiopasdfghjklzxcvbnm5to6eeeeee7via8eleph.jpg b/resources/testdata/1234567890qwertyuiopasdfghjklzxcvbnm5to6eeeeee7via8eleph.jpg
new file mode 100644
index 000000000..7d7307bed
--- /dev/null
+++ b/resources/testdata/1234567890qwertyuiopasdfghjklzxcvbnm5to6eeeeee7via8eleph.jpg
Binary files differ
diff --git a/resources/testdata/circle.svg b/resources/testdata/circle.svg
new file mode 100644
index 000000000..2759ae703
--- /dev/null
+++ b/resources/testdata/circle.svg
@@ -0,0 +1,5 @@
+<svg height="100" width="100">
+ <circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
+ Sorry, your browser does not support inline SVG.
+</svg>
+ \ No newline at end of file
diff --git a/resources/testdata/gohugoio.png b/resources/testdata/gohugoio.png
new file mode 100644
index 000000000..0591db959
--- /dev/null
+++ b/resources/testdata/gohugoio.png
Binary files differ
diff --git a/resources/testdata/sub/gohugoio2.png b/resources/testdata/sub/gohugoio2.png
new file mode 100644
index 000000000..0591db959
--- /dev/null
+++ b/resources/testdata/sub/gohugoio2.png
Binary files differ
diff --git a/resources/testdata/sunset.jpg b/resources/testdata/sunset.jpg
new file mode 100644
index 000000000..7d7307bed
--- /dev/null
+++ b/resources/testdata/sunset.jpg
Binary files differ
diff --git a/resources/testhelpers_test.go b/resources/testhelpers_test.go
new file mode 100644
index 000000000..d064fa570
--- /dev/null
+++ b/resources/testhelpers_test.go
@@ -0,0 +1,191 @@
+package resources
+
+import (
+ "path/filepath"
+ "testing"
+
+ "fmt"
+ "image"
+ "io"
+ "io/ioutil"
+ "os"
+ "runtime"
+ "strings"
+
+ "github.com/gohugoio/hugo/cache/filecache"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/resources/resource"
+ "github.com/spf13/afero"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/require"
+)
+
+func newTestResourceSpec(assert *require.Assertions) *Spec {
+ return newTestResourceSpecForBaseURL(assert, "https://example.com/")
+}
+
+func newTestResourceSpecForBaseURL(assert *require.Assertions, baseURL string) *Spec {
+ cfg := viper.New()
+ cfg.Set("baseURL", baseURL)
+ cfg.Set("resourceDir", "resources")
+ cfg.Set("contentDir", "content")
+ cfg.Set("dataDir", "data")
+ cfg.Set("i18nDir", "i18n")
+ cfg.Set("layoutDir", "layouts")
+ cfg.Set("assetDir", "assets")
+ cfg.Set("archetypeDir", "archetypes")
+ cfg.Set("publishDir", "public")
+
+ imagingCfg := map[string]interface{}{
+ "resampleFilter": "linear",
+ "quality": 68,
+ "anchor": "left",
+ }
+
+ cfg.Set("imaging", imagingCfg)
+
+ fs := hugofs.NewMem(cfg)
+
+ s, err := helpers.NewPathSpec(fs, cfg)
+ assert.NoError(err)
+
+ filecaches, err := filecache.NewCaches(s)
+ assert.NoError(err)
+
+ spec, err := NewSpec(s, filecaches, nil, output.DefaultFormats, media.DefaultTypes)
+ assert.NoError(err)
+ return spec
+}
+
+func newTargetPaths(link string) func() page.TargetPaths {
+ return func() page.TargetPaths {
+ return page.TargetPaths{
+ SubResourceBaseTarget: filepath.FromSlash(link),
+ SubResourceBaseLink: link,
+ }
+ }
+}
+
+func newTestResourceOsFs(assert *require.Assertions) *Spec {
+ cfg := viper.New()
+ cfg.Set("baseURL", "https://example.com")
+
+ workDir, _ := ioutil.TempDir("", "hugores")
+
+ if runtime.GOOS == "darwin" && !strings.HasPrefix(workDir, "/private") {
+ // To get the entry folder in line with the rest. This its a little bit
+ // mysterious, but so be it.
+ workDir = "/private" + workDir
+ }
+
+ cfg.Set("workingDir", workDir)
+ cfg.Set("resourceDir", "resources")
+ cfg.Set("contentDir", "content")
+ cfg.Set("dataDir", "data")
+ cfg.Set("i18nDir", "i18n")
+ cfg.Set("layoutDir", "layouts")
+ cfg.Set("assetDir", "assets")
+ cfg.Set("archetypeDir", "archetypes")
+ cfg.Set("publishDir", "public")
+
+ fs := hugofs.NewFrom(hugofs.Os, cfg)
+ fs.Destination = &afero.MemMapFs{}
+
+ s, err := helpers.NewPathSpec(fs, cfg)
+ assert.NoError(err)
+
+ filecaches, err := filecache.NewCaches(s)
+ assert.NoError(err)
+
+ spec, err := NewSpec(s, filecaches, nil, output.DefaultFormats, media.DefaultTypes)
+ assert.NoError(err)
+ return spec
+
+}
+
+func fetchSunset(assert *require.Assertions) *Image {
+ return fetchImage(assert, "sunset.jpg")
+}
+
+func fetchImage(assert *require.Assertions, name string) *Image {
+ spec := newTestResourceSpec(assert)
+ return fetchImageForSpec(spec, assert, name)
+}
+
+func fetchImageForSpec(spec *Spec, assert *require.Assertions, name string) *Image {
+ r := fetchResourceForSpec(spec, assert, name)
+ assert.IsType(&Image{}, r)
+ return r.(*Image)
+}
+
+func fetchResourceForSpec(spec *Spec, assert *require.Assertions, name string) resource.ContentResource {
+ src, err := os.Open(filepath.FromSlash("testdata/" + name))
+ assert.NoError(err)
+
+ out, err := helpers.OpenFileForWriting(spec.BaseFs.Content.Fs, name)
+ assert.NoError(err)
+ _, err = io.Copy(out, src)
+ out.Close()
+ src.Close()
+ assert.NoError(err)
+
+ factory := newTargetPaths("/a")
+
+ r, err := spec.New(ResourceSourceDescriptor{TargetPaths: factory, LazyPublish: true, SourceFilename: name})
+ assert.NoError(err)
+
+ return r.(resource.ContentResource)
+}
+
+func assertImageFile(assert *require.Assertions, fs afero.Fs, filename string, width, height int) {
+ filename = filepath.Clean(filename)
+ f, err := fs.Open(filename)
+ if err != nil {
+ printFs(fs, "", os.Stdout)
+ }
+ assert.NoError(err)
+ defer f.Close()
+
+ config, _, err := image.DecodeConfig(f)
+ assert.NoError(err)
+
+ assert.Equal(width, config.Width)
+ assert.Equal(height, config.Height)
+}
+
+func assertFileCache(assert *require.Assertions, fs afero.Fs, filename string, width, height int) {
+ assertImageFile(assert, fs, filepath.Clean(filename), width, height)
+}
+
+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 printFs(fs afero.Fs, path string, w io.Writer) {
+ if fs == nil {
+ return
+ }
+ afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error {
+ if info != nil && !info.IsDir() {
+ s := path
+ if lang, ok := info.(hugofs.LanguageAnnouncer); ok {
+ s = s + "\t" + lang.Lang()
+ }
+ if fp, ok := info.(hugofs.FilePather); ok {
+ s += "\tFilename: " + fp.Filename() + "\tBase: " + fp.BaseDir()
+ }
+ fmt.Fprintln(w, " ", s)
+ }
+ return nil
+ })
+}
diff --git a/resources/transform.go b/resources/transform.go
new file mode 100644
index 000000000..934c71327
--- /dev/null
+++ b/resources/transform.go
@@ -0,0 +1,554 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resources
+
+import (
+ "bytes"
+ "path"
+ "strconv"
+ "strings"
+
+ "github.com/gohugoio/hugo/common/collections"
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/common/hugio"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/resources/resource"
+ "github.com/mitchellh/hashstructure"
+
+ "fmt"
+ "io"
+ "sync"
+
+ "github.com/gohugoio/hugo/media"
+
+ bp "github.com/gohugoio/hugo/bufferpool"
+)
+
+var (
+ _ resource.ContentResource = (*transformedResource)(nil)
+ _ resource.ReadSeekCloserResource = (*transformedResource)(nil)
+ _ collections.Slicer = (*transformedResource)(nil)
+ _ resource.Identifier = (*transformedResource)(nil)
+)
+
+func (s *Spec) Transform(r resource.Resource, t ResourceTransformation) (resource.Resource, error) {
+ return &transformedResource{
+ Resource: r,
+ transformation: t,
+ transformedResourceMetadata: transformedResourceMetadata{MetaData: make(map[string]interface{})},
+ cache: s.ResourceCache}, nil
+}
+
+type ResourceTransformationCtx struct {
+ // The content to transform.
+ From io.Reader
+
+ // The target of content transformation.
+ // The current implementation requires that r is written to w
+ // even if no transformation is performed.
+ To io.Writer
+
+ // This is the relative path to the original source. Unix styled slashes.
+ SourcePath string
+
+ // This is the relative target path to the resource. Unix styled slashes.
+ InPath string
+
+ // The relative target path to the transformed resource. Unix styled slashes.
+ OutPath string
+
+ // The input media type
+ InMediaType media.Type
+
+ // The media type of the transformed resource.
+ OutMediaType media.Type
+
+ // Data data can be set on the transformed Resource. Not that this need
+ // to be simple types, as it needs to be serialized to JSON and back.
+ Data map[string]interface{}
+
+ // This is used to publis additional artifacts, e.g. source maps.
+ // We may improve this.
+ OpenResourcePublisher func(relTargetPath string) (io.WriteCloser, error)
+}
+
+// AddOutPathIdentifier transforming InPath to OutPath adding an identifier,
+// eg '.min' before any extension.
+func (ctx *ResourceTransformationCtx) AddOutPathIdentifier(identifier string) {
+ ctx.OutPath = ctx.addPathIdentifier(ctx.InPath, identifier)
+}
+
+func (ctx *ResourceTransformationCtx) addPathIdentifier(inPath, identifier string) string {
+ dir, file := path.Split(inPath)
+ base, ext := helpers.PathAndExt(file)
+ return path.Join(dir, (base + identifier + ext))
+}
+
+// ReplaceOutPathExtension transforming InPath to OutPath replacing the file
+// extension, e.g. ".scss"
+func (ctx *ResourceTransformationCtx) ReplaceOutPathExtension(newExt string) {
+ dir, file := path.Split(ctx.InPath)
+ base, _ := helpers.PathAndExt(file)
+ ctx.OutPath = path.Join(dir, (base + newExt))
+}
+
+// PublishSourceMap writes the content to the target folder of the main resource
+// with the ".map" extension added.
+func (ctx *ResourceTransformationCtx) PublishSourceMap(content string) error {
+ target := ctx.OutPath + ".map"
+ f, err := ctx.OpenResourcePublisher(target)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ _, err = f.Write([]byte(content))
+ return err
+}
+
+// ResourceTransformationKey are provided by the different transformation implementations.
+// It identifies the transformation (name) and its configuration (elements).
+// We combine this in a chain with the rest of the transformations
+// with the target filename and a content hash of the origin to use as cache key.
+type ResourceTransformationKey struct {
+ name string
+ elements []interface{}
+}
+
+// NewResourceTransformationKey creates a new ResourceTransformationKey from the transformation
+// name and elements. We will create a 64 bit FNV hash from the elements, which when combined
+// with the other key elements should be unique for all practical applications.
+func NewResourceTransformationKey(name string, elements ...interface{}) ResourceTransformationKey {
+ return ResourceTransformationKey{name: name, elements: elements}
+}
+
+// Do not change this without good reasons.
+func (k ResourceTransformationKey) key() string {
+ if len(k.elements) == 0 {
+ return k.name
+ }
+
+ sb := bp.GetBuffer()
+ defer bp.PutBuffer(sb)
+
+ sb.WriteString(k.name)
+ for _, element := range k.elements {
+ hash, err := hashstructure.Hash(element, nil)
+ if err != nil {
+ panic(err)
+ }
+ sb.WriteString("_")
+ sb.WriteString(strconv.FormatUint(hash, 10))
+ }
+
+ return sb.String()
+}
+
+// ResourceTransformation is the interface that a resource transformation step
+// needs to implement.
+type ResourceTransformation interface {
+ Key() ResourceTransformationKey
+ Transform(ctx *ResourceTransformationCtx) error
+}
+
+// We will persist this information to disk.
+type transformedResourceMetadata struct {
+ Target string `json:"Target"`
+ MediaTypeV string `json:"MediaType"`
+ MetaData map[string]interface{} `json:"Data"`
+}
+
+type transformedResource struct {
+ commonResource
+
+ cache *ResourceCache
+
+ // This is the filename inside resources/_gen/assets
+ sourceFilename string
+
+ linker permalinker
+
+ // The transformation to apply.
+ transformation ResourceTransformation
+
+ // We apply the tranformations lazily.
+ transformInit sync.Once
+ transformErr error
+
+ // We delay publishing until either .RelPermalink or .Permalink
+ // is invoked.
+ publishInit sync.Once
+ published bool
+
+ // The transformed values
+ content string
+ contentInit sync.Once
+ transformedResourceMetadata
+
+ // The source
+ resource.Resource
+}
+
+func (r *transformedResource) ReadSeekCloser() (hugio.ReadSeekCloser, error) {
+ if err := r.initContent(); err != nil {
+ return nil, err
+ }
+ return hugio.NewReadSeekerNoOpCloserFromString(r.content), nil
+}
+
+func (r *transformedResource) transferTransformedValues(another *transformedResource) {
+ if another.content != "" {
+ r.contentInit.Do(func() {
+ r.content = another.content
+ })
+ }
+ r.transformedResourceMetadata = another.transformedResourceMetadata
+}
+
+func (r *transformedResource) tryTransformedFileCache(key string) io.ReadCloser {
+ fi, f, meta, found := r.cache.getFromFile(key)
+ if !found {
+ return nil
+ }
+ r.transformedResourceMetadata = meta
+ r.sourceFilename = fi.Name
+
+ return f
+}
+
+func (r *transformedResource) Content() (interface{}, error) {
+ if err := r.initTransform(true, false); err != nil {
+ return nil, err
+ }
+ if err := r.initContent(); err != nil {
+ return "", err
+ }
+ return r.content, nil
+}
+
+func (r *transformedResource) Data() interface{} {
+ if err := r.initTransform(false, false); err != nil {
+ return noData
+ }
+ return r.MetaData
+}
+
+func (r *transformedResource) MediaType() media.Type {
+ if err := r.initTransform(false, false); err != nil {
+ return media.Type{}
+ }
+ m, _ := r.cache.rs.MediaTypes.GetByType(r.MediaTypeV)
+ return m
+}
+
+func (r *transformedResource) Key() string {
+ if err := r.initTransform(false, false); err != nil {
+ return ""
+ }
+ return r.linker.relPermalinkFor(r.Target)
+}
+
+func (r *transformedResource) Permalink() string {
+ if err := r.initTransform(false, true); err != nil {
+ return ""
+ }
+ return r.linker.permalinkFor(r.Target)
+}
+
+func (r *transformedResource) RelPermalink() string {
+ if err := r.initTransform(false, true); err != nil {
+ return ""
+ }
+ return r.linker.relPermalinkFor(r.Target)
+}
+
+func (r *transformedResource) initContent() error {
+ var err error
+ r.contentInit.Do(func() {
+ var b []byte
+ _, b, err = r.cache.fileCache.GetBytes(r.sourceFilename)
+ if err != nil {
+ return
+ }
+ r.content = string(b)
+ })
+ return err
+}
+
+func (r *transformedResource) openPublishFileForWriting(relTargetPath string) (io.WriteCloser, error) {
+ return helpers.OpenFilesForWriting(r.cache.rs.PublishFs, r.linker.relTargetPathsFor(relTargetPath)...)
+}
+
+func (r *transformedResource) transform(setContent, publish bool) (err error) {
+
+ // This can be the last resource in a chain.
+ // Rewind and create a processing chain.
+ var chain []resource.Resource
+ current := r
+ for {
+ rr := current.Resource
+ chain = append(chain[:0], append([]resource.Resource{rr}, chain[0:]...)...)
+ if tr, ok := rr.(*transformedResource); ok {
+ current = tr
+ } else {
+ break
+ }
+ }
+
+ // Append the current transformer at the end
+ chain = append(chain, r)
+
+ first := chain[0]
+
+ // Files with a suffix will be stored in cache (both on disk and in memory)
+ // partitioned by their suffix. There will be other files below /other.
+ // This partition is also how we determine what to delete on server reloads.
+ var key, base string
+ for _, element := range chain {
+ switch v := element.(type) {
+ case *transformedResource:
+ key = key + "_" + v.transformation.Key().key()
+ case permalinker:
+ r.linker = v
+ p := v.TargetPath()
+ if p == "" {
+ panic("target path needed for key creation")
+ }
+ partition := ResourceKeyPartition(p)
+ base = partition + "/" + p
+ default:
+ return fmt.Errorf("transformation not supported for type %T", element)
+ }
+ }
+
+ key = r.cache.cleanKey(base + "_" + helpers.MD5String(key))
+
+ cached, found := r.cache.get(key)
+ if found {
+ r.transferTransformedValues(cached.(*transformedResource))
+ return
+ }
+
+ // Acquire a write lock for the named transformation.
+ r.cache.nlocker.Lock(key)
+ // Check the cache again.
+ cached, found = r.cache.get(key)
+ if found {
+ r.transferTransformedValues(cached.(*transformedResource))
+ r.cache.nlocker.Unlock(key)
+ return
+ }
+
+ defer r.cache.nlocker.Unlock(key)
+ defer r.cache.set(key, r)
+
+ b1 := bp.GetBuffer()
+ b2 := bp.GetBuffer()
+ defer bp.PutBuffer(b1)
+ defer bp.PutBuffer(b2)
+
+ tctx := &ResourceTransformationCtx{
+ Data: r.transformedResourceMetadata.MetaData,
+ OpenResourcePublisher: r.openPublishFileForWriting,
+ }
+
+ tctx.InMediaType = first.MediaType()
+ tctx.OutMediaType = first.MediaType()
+
+ contentrc, err := contentReadSeekerCloser(first)
+ if err != nil {
+ return err
+ }
+ defer contentrc.Close()
+
+ tctx.From = contentrc
+ tctx.To = b1
+
+ if r.linker != nil {
+ tctx.InPath = r.linker.TargetPath()
+ tctx.SourcePath = tctx.InPath
+ }
+
+ counter := 0
+
+ var transformedContentr io.Reader
+
+ for _, element := range chain {
+ tr, ok := element.(*transformedResource)
+ if !ok {
+ continue
+ }
+ counter++
+ if counter != 1 {
+ tctx.InMediaType = tctx.OutMediaType
+ }
+ if counter%2 == 0 {
+ tctx.From = b1
+ b2.Reset()
+ tctx.To = b2
+ } else {
+ if counter != 1 {
+ // The first reader is the file.
+ tctx.From = b2
+ }
+ b1.Reset()
+ tctx.To = b1
+ }
+
+ if err := tr.transformation.Transform(tctx); err != nil {
+ if err == herrors.ErrFeatureNotAvailable {
+ // This transformation is not available in this
+ // Hugo installation (scss not compiled in, PostCSS not available etc.)
+ // If a prepared bundle for this transformation chain is available, use that.
+ f := r.tryTransformedFileCache(key)
+ if f == nil {
+ errMsg := err.Error()
+ if tr.transformation.Key().name == "postcss" {
+ errMsg = "PostCSS not found; install with \"npm install postcss-cli\". See https://gohugo.io/hugo-pipes/postcss/"
+ }
+ return fmt.Errorf("%s: failed to transform %q (%s): %s", strings.ToUpper(tr.transformation.Key().name), tctx.InPath, tctx.InMediaType.Type(), errMsg)
+ }
+ transformedContentr = f
+ defer f.Close()
+
+ // The reader above is all we need.
+ break
+ }
+
+ // Abort.
+ return err
+ }
+
+ if tctx.OutPath != "" {
+ tctx.InPath = tctx.OutPath
+ tctx.OutPath = ""
+ }
+ }
+
+ if transformedContentr == nil {
+ r.Target = tctx.InPath
+ r.MediaTypeV = tctx.OutMediaType.Type()
+ }
+
+ var publishwriters []io.WriteCloser
+
+ if publish {
+ publicw, err := r.openPublishFileForWriting(r.Target)
+ if err != nil {
+ r.transformErr = err
+ return err
+ }
+ defer publicw.Close()
+
+ publishwriters = append(publishwriters, publicw)
+ }
+
+ if transformedContentr == nil {
+ // Also write it to the cache
+ fi, metaw, err := r.cache.writeMeta(key, r.transformedResourceMetadata)
+ if err != nil {
+ return err
+ }
+ r.sourceFilename = fi.Name
+
+ publishwriters = append(publishwriters, metaw)
+
+ if counter > 0 {
+ transformedContentr = tctx.To.(*bytes.Buffer)
+ } else {
+ transformedContentr = contentrc
+ }
+ }
+
+ // Also write it to memory
+ var contentmemw *bytes.Buffer
+
+ if setContent {
+ contentmemw = bp.GetBuffer()
+ defer bp.PutBuffer(contentmemw)
+ publishwriters = append(publishwriters, hugio.ToWriteCloser(contentmemw))
+ }
+
+ publishw := hugio.NewMultiWriteCloser(publishwriters...)
+ _, r.transformErr = io.Copy(publishw, transformedContentr)
+ publishw.Close()
+
+ if setContent {
+ r.contentInit.Do(func() {
+ r.content = contentmemw.String()
+ })
+ }
+
+ return nil
+}
+
+func (r *transformedResource) initTransform(setContent, publish bool) error {
+ r.transformInit.Do(func() {
+ r.published = publish
+ if err := r.transform(setContent, publish); err != nil {
+ r.transformErr = err
+ r.cache.rs.Logger.ERROR.Println("error: failed to transform resource:", err)
+ }
+
+ })
+
+ if !publish {
+ return r.transformErr
+ }
+
+ r.publishInit.Do(func() {
+ if r.published {
+ return
+ }
+
+ r.published = true
+
+ // Copy the file from cache to /public
+ _, src, err := r.cache.fileCache.Get(r.sourceFilename)
+
+ if err == nil {
+ defer src.Close()
+
+ var dst io.WriteCloser
+ dst, err = r.openPublishFileForWriting(r.Target)
+ if err == nil {
+ defer dst.Close()
+ io.Copy(dst, src)
+ }
+ }
+
+ if err != nil {
+ r.transformErr = err
+ r.cache.rs.Logger.ERROR.Println("error: failed to publish resource:", err)
+ return
+ }
+
+ })
+
+ return r.transformErr
+}
+
+// contentReadSeekerCloser returns a ReadSeekerCloser if possible for a given Resource.
+func contentReadSeekerCloser(r resource.Resource) (hugio.ReadSeekCloser, error) {
+ switch rr := r.(type) {
+ case resource.ReadSeekCloserResource:
+ rc, err := rr.ReadSeekCloser()
+ if err != nil {
+ return nil, err
+ }
+ return rc, nil
+ default:
+ return nil, fmt.Errorf("cannot transform content of Resource of type %T", r)
+
+ }
+}
diff --git a/resources/transform_test.go b/resources/transform_test.go
new file mode 100644
index 000000000..ed462cd2a
--- /dev/null
+++ b/resources/transform_test.go
@@ -0,0 +1,36 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resources
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+type testStruct struct {
+ Name string
+ V1 int64
+ V2 int32
+ V3 int
+ V4 uint64
+}
+
+func TestResourceTransformationKey(t *testing.T) {
+ // We really need this key to be portable across OSes.
+ key := NewResourceTransformationKey("testing",
+ testStruct{Name: "test", V1: int64(10), V2: int32(20), V3: 30, V4: uint64(40)})
+ assert := require.New(t)
+ assert.Equal(key.key(), "testing_518996646957295636")
+}
diff --git a/snap/plugins/x-nodejs.yaml b/snap/plugins/x-nodejs.yaml
new file mode 100644
index 000000000..60b465459
--- /dev/null
+++ b/snap/plugins/x-nodejs.yaml
@@ -0,0 +1,8 @@
+options:
+ source:
+ required: true
+ source-type:
+ source-tag:
+ source-branch:
+ nodejs-target:
+ required: true
diff --git a/snap/plugins/x_nodejs.py b/snap/plugins/x_nodejs.py
new file mode 100644
index 000000000..848cac596
--- /dev/null
+++ b/snap/plugins/x_nodejs.py
@@ -0,0 +1,332 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Modified by Anthony Fok on 2018-10-01 to add support for ppc64el and s390x
+#
+# Copyright (C) 2015-2017 Canonical Ltd
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""The nodejs plugin is useful for node/npm based parts.
+
+The plugin uses node to install dependencies from `package.json`. It
+also sets up binaries defined in `package.json` into the `PATH`.
+
+This plugin uses the common plugin keywords as well as those for "sources".
+For more information check the 'plugins' topic for the former and the
+'sources' topic for the latter.
+
+Additionally, this plugin uses the following plugin-specific keywords:
+
+ - node-packages:
+ (list)
+ A list of dependencies to fetch using npm.
+ - node-engine:
+ (string)
+ The version of nodejs you want the snap to run on.
+ - npm-run:
+ (list)
+ A list of targets to `npm run`.
+ These targets will be run in order, after `npm install`
+ - npm-flags:
+ (list)
+ A list of flags for npm.
+ - node-package-manager
+ (string; default: npm)
+ The language package manager to use to drive installation
+ of node packages. Can be either `npm` (default) or `yarn`.
+"""
+
+import collections
+import contextlib
+import json
+import logging
+import os
+import shutil
+import subprocess
+import sys
+
+import snapcraft
+from snapcraft import sources
+from snapcraft.file_utils import link_or_copy_tree
+from snapcraft.internal import errors
+
+logger = logging.getLogger(__name__)
+
+_NODEJS_BASE = "node-v{version}-linux-{arch}"
+_NODEJS_VERSION = "8.12.0"
+_NODEJS_TMPL = "https://nodejs.org/dist/v{version}/{base}.tar.gz"
+_NODEJS_ARCHES = {"i386": "x86", "amd64": "x64", "armhf": "armv7l", "arm64": "arm64", "ppc64el": "ppc64le", "s390x": "s390x"}
+_YARN_URL = "https://yarnpkg.com/latest.tar.gz"
+
+
+class NodePlugin(snapcraft.BasePlugin):
+ @classmethod
+ def schema(cls):
+ schema = super().schema()
+
+ schema["properties"]["node-packages"] = {
+ "type": "array",
+ "minitems": 1,
+ "uniqueItems": True,
+ "items": {"type": "string"},
+ "default": [],
+ }
+ schema["properties"]["node-engine"] = {
+ "type": "string",
+ "default": _NODEJS_VERSION,
+ }
+ schema["properties"]["node-package-manager"] = {
+ "type": "string",
+ "default": "npm",
+ "enum": ["npm", "yarn"],
+ }
+ schema["properties"]["npm-run"] = {
+ "type": "array",
+ "minitems": 1,
+ "uniqueItems": False,
+ "items": {"type": "string"},
+ "default": [],
+ }
+ schema["properties"]["npm-flags"] = {
+ "type": "array",
+ "minitems": 1,
+ "uniqueItems": False,
+ "items": {"type": "string"},
+ "default": [],
+ }
+
+ if "required" in schema:
+ del schema["required"]
+
+ return schema
+
+ @classmethod
+ def get_build_properties(cls):
+ # Inform Snapcraft of the properties associated with building. If these
+ # change in the YAML Snapcraft will consider the build step dirty.
+ return ["node-packages", "npm-run", "npm-flags"]
+
+ @classmethod
+ def get_pull_properties(cls):
+ # Inform Snapcraft of the properties associated with pulling. If these
+ # change in the YAML Snapcraft will consider the build step dirty.
+ return ["node-engine", "node-package-manager"]
+
+ @property
+ def _nodejs_tar(self):
+ if self._nodejs_tar_handle is None:
+ self._nodejs_tar_handle = sources.Tar(
+ self._nodejs_release_uri, self._npm_dir
+ )
+ return self._nodejs_tar_handle
+
+ @property
+ def _yarn_tar(self):
+ if self._yarn_tar_handle is None:
+ self._yarn_tar_handle = sources.Tar(_YARN_URL, self._npm_dir)
+ return self._yarn_tar_handle
+
+ def __init__(self, name, options, project):
+ super().__init__(name, options, project)
+ self._source_package_json = os.path.join(
+ os.path.abspath(self.options.source), "package.json"
+ )
+ self._npm_dir = os.path.join(self.partdir, "npm")
+ self._manifest = collections.OrderedDict()
+ self._nodejs_release_uri = get_nodejs_release(
+ self.options.node_engine, self.project.deb_arch
+ )
+ self._nodejs_tar_handle = None
+ self._yarn_tar_handle = None
+
+ def pull(self):
+ super().pull()
+ os.makedirs(self._npm_dir, exist_ok=True)
+ self._nodejs_tar.download()
+ if self.options.node_package_manager == "yarn":
+ self._yarn_tar.download()
+ # do the install in the pull phase to download all dependencies.
+ if self.options.node_package_manager == "npm":
+ self._npm_install(rootdir=self.sourcedir)
+ else:
+ self._yarn_install(rootdir=self.sourcedir)
+
+ def clean_pull(self):
+ super().clean_pull()
+
+ # Remove the npm directory (if any)
+ if os.path.exists(self._npm_dir):
+ shutil.rmtree(self._npm_dir)
+
+ def build(self):
+ super().build()
+ if self.options.node_package_manager == "npm":
+ installed_node_packages = self._npm_install(rootdir=self.builddir)
+ # Copy the content of the symlink to the build directory
+ # LP: #1702661
+ modules_dir = os.path.join(self.installdir, "lib", "node_modules")
+ _copy_symlinked_content(modules_dir)
+ else:
+ installed_node_packages = self._yarn_install(rootdir=self.builddir)
+ lock_file_path = os.path.join(self.sourcedir, "yarn.lock")
+ if os.path.isfile(lock_file_path):
+ with open(lock_file_path) as lock_file:
+ self._manifest["yarn-lock-contents"] = lock_file.read()
+
+ self._manifest["node-packages"] = [
+ "{}={}".format(name, installed_node_packages[name])
+ for name in installed_node_packages
+ ]
+
+ def _npm_install(self, rootdir):
+ self._nodejs_tar.provision(
+ self.installdir, clean_target=False, keep_tarball=True
+ )
+ npm_cmd = ["npm"] + self.options.npm_flags
+ npm_install = npm_cmd + ["--cache-min=Infinity", "install"]
+ for pkg in self.options.node_packages:
+ self.run(npm_install + ["--global"] + [pkg], cwd=rootdir)
+ if os.path.exists(os.path.join(rootdir, "package.json")):
+ self.run(npm_install, cwd=rootdir)
+ self.run(npm_install + ["--global"], cwd=rootdir)
+ for target in self.options.npm_run:
+ self.run(npm_cmd + ["run", target], cwd=rootdir)
+ return self._get_installed_node_packages("npm", self.installdir)
+
+ def _yarn_install(self, rootdir):
+ self._nodejs_tar.provision(
+ self.installdir, clean_target=False, keep_tarball=True
+ )
+ self._yarn_tar.provision(self._npm_dir, clean_target=False, keep_tarball=True)
+ yarn_cmd = [os.path.join(self._npm_dir, "bin", "yarn")]
+ yarn_cmd.extend(self.options.npm_flags)
+ if "http_proxy" in os.environ:
+ yarn_cmd.extend(["--proxy", os.environ["http_proxy"]])
+ if "https_proxy" in os.environ:
+ yarn_cmd.extend(["--https-proxy", os.environ["https_proxy"]])
+ flags = []
+ if rootdir == self.builddir:
+ yarn_add = yarn_cmd + ["global", "add"]
+ flags.extend(
+ [
+ "--offline",
+ "--prod",
+ "--global-folder",
+ self.installdir,
+ "--prefix",
+ self.installdir,
+ ]
+ )
+ else:
+ yarn_add = yarn_cmd + ["add"]
+ for pkg in self.options.node_packages:
+ self.run(yarn_add + [pkg] + flags, cwd=rootdir)
+
+ # local packages need to be added as if they were remote, we
+ # remove the local package.json so `yarn add` doesn't pollute it.
+ if os.path.exists(self._source_package_json):
+ with contextlib.suppress(FileNotFoundError):
+ os.unlink(os.path.join(rootdir, "package.json"))
+ shutil.copy(
+ self._source_package_json, os.path.join(rootdir, "package.json")
+ )
+ self.run(yarn_add + ["file:{}".format(rootdir)] + flags, cwd=rootdir)
+
+ # npm run would require to bring back package.json
+ if self.options.npm_run and os.path.exists(self._source_package_json):
+ # The current package.json is the yarn prefilled one.
+ with contextlib.suppress(FileNotFoundError):
+ os.unlink(os.path.join(rootdir, "package.json"))
+ os.link(self._source_package_json, os.path.join(rootdir, "package.json"))
+ for target in self.options.npm_run:
+ self.run(
+ yarn_cmd + ["run", target],
+ cwd=rootdir,
+ env=self._build_environment(rootdir),
+ )
+ return self._get_installed_node_packages("npm", self.installdir)
+
+ def _get_installed_node_packages(self, package_manager, cwd):
+ try:
+ output = self.run_output(
+ [package_manager, "ls", "--global", "--json"], cwd=cwd
+ )
+ except subprocess.CalledProcessError as error:
+ # XXX When dependencies have missing dependencies, an error like
+ # this is printed to stderr:
+ # npm ERR! peer dep missing: glob@*, required by glob-promise@3.1.0
+ # retcode is not 0, which raises an exception.
+ output = error.output.decode(sys.getfilesystemencoding()).strip()
+ packages = collections.OrderedDict()
+ dependencies = json.loads(output, object_pairs_hook=collections.OrderedDict)[
+ "dependencies"
+ ]
+ while dependencies:
+ key, value = dependencies.popitem(last=False)
+ # XXX Just as above, dependencies without version are the ones
+ # missing.
+ if "version" in value:
+ packages[key] = value["version"]
+ if "dependencies" in value:
+ dependencies.update(value["dependencies"])
+ return packages
+
+ def get_manifest(self):
+ return self._manifest
+
+ def _build_environment(self, rootdir):
+ env = os.environ.copy()
+ if rootdir.endswith("src"):
+ hidden_path = os.path.join(rootdir, "node_modules", ".bin")
+ if env.get("PATH"):
+ new_path = "{}:{}".format(hidden_path, env.get("PATH"))
+ else:
+ new_path = hidden_path
+ env["PATH"] = new_path
+ return env
+
+
+def _get_nodejs_base(node_engine, machine):
+ if machine not in _NODEJS_ARCHES:
+ raise errors.SnapcraftEnvironmentError(
+ "architecture not supported ({})".format(machine)
+ )
+ return _NODEJS_BASE.format(version=node_engine, arch=_NODEJS_ARCHES[machine])
+
+
+def get_nodejs_release(node_engine, arch):
+ return _NODEJS_TMPL.format(
+ version=node_engine, base=_get_nodejs_base(node_engine, arch)
+ )
+
+
+def _copy_symlinked_content(modules_dir):
+ """Copy symlinked content.
+
+ When running newer versions of npm, symlinks to the local tree are
+ created from the part's installdir to the root of the builddir of the
+ part (this only affects some build configurations in some projects)
+ which is valid when running from the context of the part but invalid
+ as soon as the artifacts migrate across the steps,
+ i.e.; stage and prime.
+
+ If modules_dir does not exist we simply return.
+ """
+ if not os.path.exists(modules_dir):
+ return
+ modules = [os.path.join(modules_dir, d) for d in os.listdir(modules_dir)]
+ symlinks = [l for l in modules if os.path.islink(l)]
+ for link_path in symlinks:
+ link_target = os.path.realpath(link_path)
+ os.unlink(link_path)
+ link_or_copy_tree(link_target, link_path)
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
new file mode 100644
index 000000000..237d61e68
--- /dev/null
+++ b/snap/snapcraft.yaml
@@ -0,0 +1,95 @@
+name: hugo
+version: "0.56.0-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
+ completer: hugo-completion
+ plugs: [home, network-bind, removable-media]
+
+parts:
+ git:
+ plugin: nil
+ stage-packages:
+ - git
+ organize:
+ usr/bin/: bin/
+ prime:
+ - bin/git
+
+ hugo:
+ plugin: nil
+ build-snaps: [go/1.12/stable]
+ source: .
+ override-build: |
+ set -ex
+
+ echo "\nStarting override-build:"
+ export GOPATH=$(realpath ../go)
+ export PATH=$GOPATH/bin:$PATH
+
+ echo ' * Running "go get -v github.com/magefile/mage"...'
+ GO111MODULE=off go get -v github.com/magefile/mage
+
+ echo ' * Running "mage -v test"...'
+ export GO111MODULE=on
+ mage -v test
+
+ echo " * SNAPCRAFT_IMAGE_INFO=$SNAPCRAFT_IMAGE_INFO"
+ # Example: SNAPCRAFT_IMAGE_INFO='{"build_url": "https://launchpad.net/~gohugoio/+snap/hugo-extended-dev/+build/344022"}'
+ if echo $SNAPCRAFT_IMAGE_INFO | grep -q '/+snap/hugo-extended'; then
+ export HUGO_BUILD_TAGS="extended"
+ fi
+ echo " * Building hugo (HUGO_BUILD_TAGS=\"$HUGO_BUILD_TAGS\")..."
+ [ "$SNAPCRAFT_PROJECT_GRADE" = "stable" ] && mage -v hugoNoGitInfo || mage -v hugo
+ ./hugo version
+ ldd hugo || :
+
+ echo " * Building shell completion..."
+ ./hugo gen autocomplete --completionfile=hugo-completion
+
+ echo " * Installing to ${SNAPCRAFT_PART_INSTALL}..."
+ install -d $SNAPCRAFT_PART_INSTALL/bin
+ cp -av hugo $SNAPCRAFT_PART_INSTALL/bin/
+ mv -v hugo-completion $SNAPCRAFT_PART_INSTALL/
+
+ echo " * Stripping binary..."
+ ls -l $SNAPCRAFT_PART_INSTALL/bin/hugo
+ strip --remove-section=.comment --remove-section=.note $SNAPCRAFT_PART_INSTALL/bin/hugo
+ ls -l $SNAPCRAFT_PART_INSTALL/bin/hugo
+
+ node:
+ plugin: x-nodejs
+ node-packages: [postcss-cli]
+ filesets:
+ node:
+ - bin/node
+ postcss:
+ - bin/postcss
+ - lib/node_modules/postcss-cli/*
+ prime:
+ - $node
+ - $postcss
+
+ pygments:
+ plugin: python
+ python-packages: [Pygments]
+ prime:
+ - bin/pygmentize
+ - lib/python*/site-packages/Pygments-*.dist-info/*
+ - lib/python*/site-packages/pygments/*
+ - usr/bin/python*
+ - -usr/bin/python*m
+ - usr/lib/python*/*
+ - -usr/lib/python*/distutils/*
+ - -usr/lib/python*/email/*
+ - -usr/lib/python*/lib2to3/*
+ - -usr/lib/python*/tkinter/*
+ - -usr/lib/python*/unittest/*
diff --git a/source/content_directory_test.go b/source/content_directory_test.go
new file mode 100644
index 000000000..7f050e0da
--- /dev/null
+++ b/source/content_directory_test.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 source
+
+import (
+ "path/filepath"
+ "testing"
+
+ "github.com/gohugoio/hugo/helpers"
+
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/stretchr/testify/require"
+)
+
+func TestIgnoreDotFilesAndDirectories(t *testing.T) {
+ assert := require.New(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},
+ {"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.md$"}},
+ {"foobar/foo.md", true, []string{"*", "\\.md$", "\\.boo$"}},
+ {"foobar/.#content.md", true, []string{"/\\.#"}},
+ {".#foobar.md", true, []string{"^\\.#"}},
+ }
+
+ for i, test := range tests {
+ v := newTestConfig()
+ v.Set("ignoreFiles", test.ignoreFilesRegexpes)
+ fs := hugofs.NewMem(v)
+ ps, err := helpers.NewPathSpec(fs, v)
+ assert.NoError(err)
+
+ s := NewSourceSpec(ps, fs.Source)
+
+ if ignored := s.IgnoreFile(filepath.FromSlash(test.path)); test.ignore != ignored {
+ t.Errorf("[%d] File not ignored", i)
+ }
+ }
+}
diff --git a/source/fileInfo.go b/source/fileInfo.go
new file mode 100644
index 000000000..072b55b6c
--- /dev/null
+++ b/source/fileInfo.go
@@ -0,0 +1,290 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "github.com/gohugoio/hugo/common/hugio"
+
+ "github.com/gohugoio/hugo/hugofs"
+
+ "github.com/gohugoio/hugo/helpers"
+)
+
+// fileInfo implements the File interface.
+var (
+ _ File = (*FileInfo)(nil)
+ _ ReadableFile = (*FileInfo)(nil)
+)
+
+// File represents a source file.
+// This is a temporary construct until we resolve page.Page conflicts.
+// TODO(bep) remove this construct once we have resolved page deprecations
+type File interface {
+ fileOverlap
+ FileWithoutOverlap
+}
+
+// Temporary to solve duplicate/deprecated names in page.Page
+type fileOverlap interface {
+ // Path gets the relative path including file name and extension.
+ // The directory is relative to the content root.
+ Path() string
+
+ // Section is first directory below the content root.
+ // For page bundles in root, the Section will be empty.
+ Section() string
+
+ // Lang is the language code for this page. It will be the
+ // same as the site's language code.
+ Lang() string
+
+ IsZero() bool
+}
+
+type FileWithoutOverlap interface {
+
+ // Filename gets the full path and filename to the file.
+ Filename() string
+
+ // Dir gets the name of the directory that contains this file.
+ // The directory is relative to the content root.
+ Dir() string
+
+ // Extension gets the file extension, i.e "myblogpost.md" will return "md".
+ Extension() string
+
+ // Ext is an alias for Extension.
+ Ext() string // Hmm... Deprecate Extension
+
+ // LogicalName is filename and extension of the file.
+ LogicalName() string
+
+ // BaseFileName is a filename without extension.
+ BaseFileName() string
+
+ // TranslationBaseName is a filename with no extension,
+ // not even the optional language extension part.
+ TranslationBaseName() string
+
+ // ContentBaseName is a either TranslationBaseName or name of containing folder
+ // if file is a leaf bundle.
+ ContentBaseName() string
+
+ // 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.
+ UniqueID() string
+
+ FileInfo() os.FileInfo
+}
+
+// A ReadableFile is a File that is readable.
+type ReadableFile interface {
+ File
+ Open() (hugio.ReadSeekCloser, error)
+}
+
+// FileInfo describes a source file.
+type FileInfo struct {
+
+ // Absolute filename to the file on disk.
+ filename string
+
+ sp *SourceSpec
+
+ fi os.FileInfo
+
+ // Derived from filename
+ ext string // Extension without any "."
+ lang string
+
+ name string
+
+ dir string
+ relDir string
+ relPath string
+ baseName string
+ translationBaseName string
+ contentBaseName string
+ section string
+ isLeafBundle bool
+
+ uniqueID string
+
+ lazyInit sync.Once
+}
+
+// Filename returns a file's absolute path and filename on disk.
+func (fi *FileInfo) Filename() string { return fi.filename }
+
+// Path gets the relative path including file name and extension. The directory
+// is relative to the content root.
+func (fi *FileInfo) Path() string { return fi.relPath }
+
+// Dir gets the name of the directory that contains this file. The directory is
+// relative to the content root.
+func (fi *FileInfo) Dir() string { return fi.relDir }
+
+// Extension is an alias to Ext().
+func (fi *FileInfo) Extension() string { return fi.Ext() }
+
+// Ext returns a file's extension without the leading period (ie. "md").
+func (fi *FileInfo) Ext() string { return fi.ext }
+
+// Lang returns a file's language (ie. "sv").
+func (fi *FileInfo) Lang() string { return fi.lang }
+
+// LogicalName returns a file's name and extension (ie. "page.sv.md").
+func (fi *FileInfo) LogicalName() string { return fi.name }
+
+// BaseFileName returns a file's name without extension (ie. "page.sv").
+func (fi *FileInfo) BaseFileName() string { return fi.baseName }
+
+// TranslationBaseName returns a file's translation base name without the
+// language segement (ie. "page").
+func (fi *FileInfo) TranslationBaseName() string { return fi.translationBaseName }
+
+// ContentBaseName is a either TranslationBaseName or name of containing folder
+// if file is a leaf bundle.
+func (fi *FileInfo) ContentBaseName() string {
+ fi.init()
+ return fi.contentBaseName
+}
+
+// Section returns a file's section.
+func (fi *FileInfo) Section() string {
+ fi.init()
+ return fi.section
+}
+
+// UniqueID returns a file's unique, MD5 hash identifier.
+func (fi *FileInfo) UniqueID() string {
+ fi.init()
+ return fi.uniqueID
+}
+
+// FileInfo returns a file's underlying os.FileInfo.
+func (fi *FileInfo) FileInfo() os.FileInfo { return fi.fi }
+
+func (fi *FileInfo) String() string { return fi.BaseFileName() }
+
+// Open implements ReadableFile.
+func (fi *FileInfo) Open() (hugio.ReadSeekCloser, error) {
+ f, err := fi.sp.SourceFs.Open(fi.Filename())
+ return f, err
+}
+
+func (fi *FileInfo) IsZero() bool {
+ return fi == nil
+}
+
+// We create a lot of these FileInfo objects, but there are parts of it used only
+// in some cases that is slightly expensive to construct.
+func (fi *FileInfo) init() {
+ fi.lazyInit.Do(func() {
+ relDir := strings.Trim(fi.relDir, helpers.FilePathSeparator)
+ parts := strings.Split(relDir, helpers.FilePathSeparator)
+ var section string
+ if (!fi.isLeafBundle && len(parts) == 1) || len(parts) > 1 {
+ section = parts[0]
+ }
+ fi.section = section
+
+ if fi.isLeafBundle && len(parts) > 0 {
+ fi.contentBaseName = parts[len(parts)-1]
+ } else {
+ fi.contentBaseName = fi.translationBaseName
+ }
+
+ fi.uniqueID = helpers.MD5String(filepath.ToSlash(fi.relPath))
+ })
+}
+
+// NewTestFile creates a partially filled File used in unit tests.
+// TODO(bep) improve this package
+func NewTestFile(filename string) *FileInfo {
+ base := filepath.Base(filepath.Dir(filename))
+ return &FileInfo{
+ filename: filename,
+ translationBaseName: base,
+ }
+}
+
+// NewFileInfo returns a new FileInfo structure.
+func (sp *SourceSpec) NewFileInfo(baseDir, filename string, isLeafBundle bool, fi os.FileInfo) *FileInfo {
+
+ var lang, translationBaseName, relPath string
+
+ if fp, ok := fi.(hugofs.FilePather); ok {
+ filename = fp.Filename()
+ baseDir = fp.BaseDir()
+ relPath = fp.Path()
+ }
+
+ if fl, ok := fi.(hugofs.LanguageAnnouncer); ok {
+ lang = fl.Lang()
+ translationBaseName = fl.TranslationBaseName()
+ }
+
+ dir, name := filepath.Split(filename)
+ if !strings.HasSuffix(dir, helpers.FilePathSeparator) {
+ dir = dir + helpers.FilePathSeparator
+ }
+
+ baseDir = strings.TrimSuffix(baseDir, helpers.FilePathSeparator)
+
+ relDir := ""
+ if dir != baseDir {
+ relDir = strings.TrimPrefix(dir, baseDir)
+ }
+
+ relDir = strings.TrimPrefix(relDir, helpers.FilePathSeparator)
+
+ if relPath == "" {
+ relPath = filepath.Join(relDir, name)
+ }
+
+ ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(name), "."))
+ baseName := helpers.Filename(name)
+
+ if translationBaseName == "" {
+ // This is usyally provided by the filesystem. But this FileInfo is also
+ // created in a standalone context when doing "hugo new". This is
+ // an approximate implementation, which is "good enough" in that case.
+ fileLangExt := filepath.Ext(baseName)
+ translationBaseName = strings.TrimSuffix(baseName, fileLangExt)
+ }
+
+ f := &FileInfo{
+ sp: sp,
+ filename: filename,
+ fi: fi,
+ lang: lang,
+ ext: ext,
+ dir: dir,
+ relDir: relDir,
+ relPath: relPath,
+ name: name,
+ baseName: baseName,
+ translationBaseName: translationBaseName,
+ isLeafBundle: isLeafBundle,
+ }
+
+ return f
+
+}
diff --git a/source/fileInfo_test.go b/source/fileInfo_test.go
new file mode 100644
index 000000000..9390c6247
--- /dev/null
+++ b/source/fileInfo_test.go
@@ -0,0 +1,110 @@
+// 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 source
+
+import (
+ "path/filepath"
+ "testing"
+
+ "github.com/gohugoio/hugo/helpers"
+
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/spf13/afero"
+ "github.com/stretchr/testify/require"
+)
+
+func TestFileInfo(t *testing.T) {
+ assert := require.New(t)
+
+ s := newTestSourceSpec()
+
+ for _, this := range []struct {
+ base string
+ filename string
+ assert func(f *FileInfo)
+ }{
+ {filepath.FromSlash("/a/"), filepath.FromSlash("/a/b/page.md"), func(f *FileInfo) {
+ assert.Equal(filepath.FromSlash("/a/b/page.md"), f.Filename())
+ assert.Equal(filepath.FromSlash("b/"), f.Dir())
+ assert.Equal(filepath.FromSlash("b/page.md"), f.Path())
+ assert.Equal("b", f.Section())
+ assert.Equal(filepath.FromSlash("page"), f.TranslationBaseName())
+ assert.Equal(filepath.FromSlash("page"), f.BaseFileName())
+
+ }},
+ {filepath.FromSlash("/a/"), filepath.FromSlash("/a/b/c/d/page.md"), func(f *FileInfo) {
+ assert.Equal("b", f.Section())
+
+ }},
+ {filepath.FromSlash("/a/"), filepath.FromSlash("/a/b/page.en.MD"), func(f *FileInfo) {
+ assert.Equal("b", f.Section())
+ assert.Equal(filepath.FromSlash("b/page.en.MD"), f.Path())
+ assert.Equal(filepath.FromSlash("page"), f.TranslationBaseName())
+ assert.Equal(filepath.FromSlash("page.en"), f.BaseFileName())
+
+ }},
+ } {
+ f := s.NewFileInfo(this.base, this.filename, false, nil)
+ this.assert(f)
+ }
+
+}
+
+func TestFileInfoLanguage(t *testing.T) {
+ assert := require.New(t)
+ langs := map[string]bool{
+ "sv": true,
+ "en": true,
+ }
+
+ m := afero.NewMemMapFs()
+ lfs := hugofs.NewLanguageFs("sv", langs, m)
+ v := newTestConfig()
+
+ fs := hugofs.NewFrom(m, v)
+
+ ps, err := helpers.NewPathSpec(fs, v)
+ assert.NoError(err)
+ s := SourceSpec{SourceFs: lfs, PathSpec: ps}
+ s.Languages = map[string]interface{}{
+ "en": true,
+ }
+
+ err = afero.WriteFile(lfs, "page.md", []byte("abc"), 0777)
+ assert.NoError(err)
+ err = afero.WriteFile(lfs, "page.en.md", []byte("abc"), 0777)
+ assert.NoError(err)
+
+ sv, _ := lfs.Stat("page.md")
+ en, _ := lfs.Stat("page.en.md")
+
+ fiSv := s.NewFileInfo("", "page.md", false, sv)
+ fiEn := s.NewFileInfo("", "page.en.md", false, en)
+
+ assert.Equal("sv", fiSv.Lang())
+ assert.Equal("en", fiEn.Lang())
+
+ // test contentBaseName implementation
+ fi := s.NewFileInfo("", "2018-10-01-contentbasename.md", false, nil)
+ assert.Equal("2018-10-01-contentbasename", fi.ContentBaseName())
+
+ fi = s.NewFileInfo("", "2018-10-01-contentbasename.en.md", false, nil)
+ assert.Equal("2018-10-01-contentbasename", fi.ContentBaseName())
+
+ fi = s.NewFileInfo("", filepath.Join("2018-10-01-contentbasename", "index.en.md"), true, nil)
+ assert.Equal("2018-10-01-contentbasename", fi.ContentBaseName())
+
+ fi = s.NewFileInfo("", filepath.Join("2018-10-01-contentbasename", "_index.en.md"), false, nil)
+ assert.Equal("_index", fi.ContentBaseName())
+}
diff --git a/source/filesystem.go b/source/filesystem.go
new file mode 100644
index 000000000..0c1a6ac7b
--- /dev/null
+++ b/source/filesystem.go
@@ -0,0 +1,130 @@
+// 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 (
+ "os"
+ "path/filepath"
+ "runtime"
+ "sync"
+
+ "github.com/gohugoio/hugo/helpers"
+ jww "github.com/spf13/jwalterweatherman"
+ "golang.org/x/text/unicode/norm"
+)
+
+// Filesystem represents a source filesystem.
+type Filesystem struct {
+ files []ReadableFile
+ filesInit sync.Once
+
+ Base string
+
+ SourceSpec
+}
+
+// Input describes a source input.
+type Input interface {
+ Files() []ReadableFile
+}
+
+// NewFilesystem returns a new filesytem for a given source spec.
+func (sp SourceSpec) NewFilesystem(base string) *Filesystem {
+ return &Filesystem{SourceSpec: sp, Base: base}
+}
+
+// Files returns a slice of readable files.
+func (f *Filesystem) Files() []ReadableFile {
+ f.filesInit.Do(func() {
+ f.captureFiles()
+ })
+ return f.files
+}
+
+// add populates a file in the Filesystem.files
+func (f *Filesystem) add(name string, fi os.FileInfo) (err error) {
+ var file ReadableFile
+
+ if runtime.GOOS == "darwin" {
+ // When a file system is HFS+, its filepath is in NFD form.
+ name = norm.NFC.String(name)
+ }
+
+ file = f.SourceSpec.NewFileInfo(f.Base, name, false, fi)
+ 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 {
+ f.add(filePath, fi)
+ }
+ return err
+ }
+
+ if f.SourceFs == nil {
+ panic("Must have a fs")
+ }
+ err := helpers.SymbolicWalk(f.SourceFs, f.Base, walker)
+
+ if err != nil {
+ jww.ERROR.Println(err)
+ }
+
+}
+
+func (f *Filesystem) shouldRead(filename string, fi os.FileInfo) (bool, error) {
+ if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
+ link, err := filepath.EvalSymlinks(filename)
+ if err != nil {
+ jww.ERROR.Printf("Cannot read symbolic link '%s', error was: %s", filename, err)
+ return false, nil
+ }
+ linkfi, err := f.SourceFs.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'", filename)
+ }
+ return false, nil
+ }
+
+ ignore := f.SourceSpec.IgnoreFile(filename)
+
+ if fi.IsDir() {
+ if ignore {
+ return false, filepath.SkipDir
+ }
+ return false, nil
+ }
+
+ if ignore {
+ return false, nil
+ }
+
+ return true, nil
+}
diff --git a/source/filesystem_test.go b/source/filesystem_test.go
new file mode 100644
index 000000000..8c8e30413
--- /dev/null
+++ b/source/filesystem_test.go
@@ -0,0 +1,84 @@
+// 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 (
+ "os"
+ "runtime"
+ "testing"
+
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugofs"
+
+ "github.com/spf13/viper"
+)
+
+func TestEmptySourceFilesystem(t *testing.T) {
+ ss := newTestSourceSpec()
+ src := ss.NewFilesystem("Empty")
+ if len(src.Files()) != 0 {
+ t.Errorf("new filesystem should contain 0 files.")
+ }
+}
+
+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()
+ var fi os.FileInfo
+
+ for _, path := range paths {
+ src := ss.NewFilesystem("base")
+ _ = src.add(path.NFD, fi)
+ f := src.Files()[0]
+ if f.BaseFileName() != path.NFC {
+ t.Fatalf("file name in NFD form should be normalized (%s)", path.NFC)
+ }
+ }
+
+}
+
+func newTestConfig() *viper.Viper {
+ v := viper.New()
+ v.Set("contentDir", "content")
+ v.Set("dataDir", "data")
+ v.Set("i18nDir", "i18n")
+ v.Set("layoutDir", "layouts")
+ v.Set("archetypeDir", "archetypes")
+ v.Set("resourceDir", "resources")
+ v.Set("publishDir", "public")
+ v.Set("assetDir", "assets")
+ return v
+}
+
+func newTestSourceSpec() *SourceSpec {
+ v := newTestConfig()
+ fs := hugofs.NewMem(v)
+ ps, err := helpers.NewPathSpec(fs, v)
+ if err != nil {
+ panic(err)
+ }
+ return NewSourceSpec(ps, fs.Source)
+}
diff --git a/source/sourceSpec.go b/source/sourceSpec.go
new file mode 100644
index 000000000..9731a8d8d
--- /dev/null
+++ b/source/sourceSpec.go
@@ -0,0 +1,142 @@
+// 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 source
+
+import (
+ "os"
+ "path/filepath"
+ "regexp"
+
+ "github.com/gohugoio/hugo/langs"
+ "github.com/spf13/afero"
+
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/spf13/cast"
+)
+
+// SourceSpec abstracts language-specific file creation.
+// TODO(bep) rename to Spec
+type SourceSpec struct {
+ *helpers.PathSpec
+
+ SourceFs afero.Fs
+
+ // This is set if the ignoreFiles config is set.
+ ignoreFilesRe []*regexp.Regexp
+
+ Languages map[string]interface{}
+ DefaultContentLanguage string
+ DisabledLanguages map[string]bool
+}
+
+// NewSourceSpec initializes SourceSpec using languages the given filesystem and PathSpec.
+func NewSourceSpec(ps *helpers.PathSpec, fs afero.Fs) *SourceSpec {
+ cfg := ps.Cfg
+ defaultLang := cfg.GetString("defaultContentLanguage")
+ languages := cfg.GetStringMap("languages")
+
+ disabledLangsSet := make(map[string]bool)
+
+ for _, disabledLang := range cfg.GetStringSlice("disableLanguages") {
+ disabledLangsSet[disabledLang] = true
+ }
+
+ if len(languages) == 0 {
+ l := langs.NewDefaultLanguage(cfg)
+ languages[l.Lang] = l
+ defaultLang = l.Lang
+ }
+
+ ignoreFiles := cast.ToStringSlice(cfg.Get("ignoreFiles"))
+ var regexps []*regexp.Regexp
+ if len(ignoreFiles) > 0 {
+ for _, ignorePattern := range ignoreFiles {
+ re, err := regexp.Compile(ignorePattern)
+ if err != nil {
+ helpers.DistinctErrorLog.Printf("Invalid regexp %q in ignoreFiles: %s", ignorePattern, err)
+ } else {
+ regexps = append(regexps, re)
+ }
+
+ }
+ }
+
+ return &SourceSpec{ignoreFilesRe: regexps, PathSpec: ps, SourceFs: fs, Languages: languages, DefaultContentLanguage: defaultLang, DisabledLanguages: disabledLangsSet}
+
+}
+
+// IgnoreFile returns whether a given file should be ignored.
+func (s *SourceSpec) IgnoreFile(filename string) bool {
+ if filename == "" {
+ if _, ok := s.SourceFs.(*afero.OsFs); ok {
+ return true
+ }
+ return false
+ }
+
+ base := filepath.Base(filename)
+
+ if len(base) > 0 {
+ first := base[0]
+ last := base[len(base)-1]
+ if first == '.' ||
+ first == '#' ||
+ last == '~' {
+ return true
+ }
+ }
+
+ if len(s.ignoreFilesRe) == 0 {
+ return false
+ }
+
+ for _, re := range s.ignoreFilesRe {
+ if re.MatchString(filename) {
+ return true
+ }
+ }
+
+ return false
+}
+
+// IsRegularSourceFile returns whether filename represents a regular file in the
+// source filesystem.
+func (s *SourceSpec) IsRegularSourceFile(filename string) (bool, error) {
+ fi, err := helpers.LstatIfPossible(s.SourceFs, filename)
+ if err != nil {
+ return false, err
+ }
+
+ if fi.IsDir() {
+ return false, nil
+ }
+
+ if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
+ link, err := filepath.EvalSymlinks(filename)
+ if err != nil {
+ return false, err
+ }
+
+ fi, err = helpers.LstatIfPossible(s.SourceFs, link)
+ if err != nil {
+ return false, err
+ }
+
+ if fi.IsDir() {
+ return false, nil
+ }
+ }
+
+ return true, nil
+}
diff --git a/tpl/cast/cast.go b/tpl/cast/cast.go
new file mode 100644
index 000000000..c864b5e32
--- /dev/null
+++ b/tpl/cast/cast.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 cast provides template functions for data type conversions.
+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) {
+ v = convertTemplateToString(v)
+ return _cast.ToIntE(v)
+}
+
+// ToString converts the given value to a string.
+func (ns *Namespace) ToString(v interface{}) (string, error) {
+ return _cast.ToStringE(v)
+}
+
+// ToFloat converts the given value to a float.
+func (ns *Namespace) ToFloat(v interface{}) (float64, error) {
+ v = convertTemplateToString(v)
+ return _cast.ToFloat64E(v)
+}
+
+func convertTemplateToString(v interface{}) interface{} {
+ 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 v
+}
diff --git a/tpl/cast/cast_test.go b/tpl/cast/cast_test.go
new file mode 100644
index 000000000..fc20934f8
--- /dev/null
+++ b/tpl/cast/cast_test.go
@@ -0,0 +1,120 @@
+// 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)
+ }
+}
+
+func TestToFloat(t *testing.T) {
+ t.Parallel()
+
+ ns := New()
+
+ for i, test := range []struct {
+ v interface{}
+ expect interface{}
+ }{
+ {"1", 1.0},
+ {template.HTML("2"), 2.0},
+ {template.CSS("3"), 3.0},
+ {template.HTMLAttr("4"), 4.0},
+ {template.JS("-5.67"), -5.67},
+ {template.JSStr("6"), 6.0},
+ {"1.23", 1.23},
+ {"-1.23", -1.23},
+ {"0", 0.0},
+ {float64(2.12), 2.12},
+ {int64(123), 123.0},
+ {2, 2.0},
+ {t, false},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test.v)
+
+ result, err := ns.ToFloat(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..6fc35f3c7
--- /dev/null
+++ b/tpl/cast/docshelper.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 cast
+
+import (
+ "github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/docshelper"
+ "github.com/gohugoio/hugo/htesting"
+ "github.com/gohugoio/hugo/tpl/internal"
+ "github.com/spf13/viper"
+)
+
+// 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{
+ Cfg: viper.New(),
+ Log: loggers.NewErrorLogger(),
+ BuildStartListeners: &deps.Listeners{},
+ Site: htesting.NewTestHugoSite(),
+ }
+
+ 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..3aee6f036
--- /dev/null
+++ b/tpl/cast/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 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`},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.ToFloat,
+ []string{"float"},
+ [][2]string{
+ {`{{ "1234" | float | printf "%T" }}`, `float64`},
+ },
+ )
+
+ 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/append.go b/tpl/collections/append.go
new file mode 100644
index 000000000..297328dc8
--- /dev/null
+++ b/tpl/collections/append.go
@@ -0,0 +1,38 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+
+ "github.com/gohugoio/hugo/common/collections"
+)
+
+// Append appends the arguments up to the last one to the slice in the last argument.
+// This construct allows template constructs like this:
+// {{ $pages = $pages | append $p2 $p1 }}
+// Note that with 2 arguments where both are slices of the same type,
+// the first slice will be appended to the second:
+// {{ $pages = $pages | append .Site.RegularPages }}
+func (ns *Namespace) Append(args ...interface{}) (interface{}, error) {
+ if len(args) < 2 {
+ return nil, errors.New("need at least 2 arguments to append")
+ }
+
+ to := args[len(args)-1]
+ from := args[:len(args)-1]
+
+ return collections.Append(to, from...)
+
+}
diff --git a/tpl/collections/append_test.go b/tpl/collections/append_test.go
new file mode 100644
index 000000000..f886aca22
--- /dev/null
+++ b/tpl/collections/append_test.go
@@ -0,0 +1,66 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+
+ "github.com/gohugoio/hugo/deps"
+ "github.com/stretchr/testify/require"
+)
+
+// Also see tests in common/collection.
+func TestAppend(t *testing.T) {
+ t.Parallel()
+
+ ns := New(&deps.Deps{})
+
+ for i, test := range []struct {
+ start interface{}
+ addend []interface{}
+ expected interface{}
+ }{
+ {[]string{"a", "b"}, []interface{}{"c"}, []string{"a", "b", "c"}},
+ {[]string{"a", "b"}, []interface{}{"c", "d", "e"}, []string{"a", "b", "c", "d", "e"}},
+ {[]string{"a", "b"}, []interface{}{[]string{"c", "d", "e"}}, []string{"a", "b", "c", "d", "e"}},
+ // Errors
+ {"", []interface{}{[]string{"a", "b"}}, false},
+ {[]string{"a", "b"}, []interface{}{}, false},
+ // No string concatenation.
+ {"ab",
+ []interface{}{"c"},
+ false},
+ } {
+
+ errMsg := fmt.Sprintf("[%d]", i)
+
+ args := append(test.addend, test.start)
+
+ result, err := ns.Append(args...)
+
+ if b, ok := test.expected.(bool); ok && !b {
+ require.Error(t, err, errMsg)
+ continue
+ }
+
+ require.NoError(t, err, errMsg)
+
+ if !reflect.DeepEqual(test.expected, result) {
+ t.Fatalf("%s got\n%T: %v\nexpected\n%T: %v", errMsg, result, result, test.expected, test.expected)
+ }
+ }
+
+}
diff --git a/tpl/collections/apply.go b/tpl/collections/apply.go
new file mode 100644
index 000000000..d715aeb00
--- /dev/null
+++ b/tpl/collections/apply.go
@@ -0,0 +1,162 @@
+// 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 borrowed from the Go stdlib: '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
+}
+
+func indirectInterface(v reflect.Value) (rv reflect.Value, isNil bool) {
+ for ; 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..edec3da18
--- /dev/null
+++ b/tpl/collections/apply_test.go
@@ -0,0 +1,68 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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.Template, bool) {
+ return nil, false
+}
+
+func (templateFinder) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
+ return nil, false, false
+}
+
+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..3839ad472
--- /dev/null
+++ b/tpl/collections/collections.go
@@ -0,0 +1,699 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 provides template functions for manipulating collections
+// such as arrays, maps, and slices.
+package collections
+
+import (
+ "fmt"
+ "html/template"
+ "math/rand"
+ "net/url"
+ "reflect"
+ "strings"
+ "time"
+
+ "github.com/gohugoio/hugo/common/collections"
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/common/types"
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/pkg/errors"
+ "github.com/spf13/cast"
+)
+
+func init() {
+ rand.Seed(time.Now().UTC().UnixNano())
+}
+
+// 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 seqv.Slice(0, 0).Interface(), nil
+ }
+
+ 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 < 0 {
+ return nil, errors.New("can't return negative 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, error) {
+ if l == nil || v == nil {
+ return false, nil
+ }
+
+ lv := reflect.ValueOf(l)
+ vv := reflect.ValueOf(v)
+
+ if !vv.Type().Comparable() {
+ return false, errors.Errorf("value to check must be comparable: %T", v)
+ }
+
+ // Normalize numeric types to float64 etc.
+ vvk := normalize(vv)
+
+ switch lv.Kind() {
+ case reflect.Array, reflect.Slice:
+ for i := 0; i < lv.Len(); i++ {
+ lvv, isNil := indirectInterface(lv.Index(i))
+ if isNil || !lvv.Type().Comparable() {
+ continue
+ }
+
+ lvvk := normalize(lvv)
+
+ if lvvk == vvk {
+ return true, nil
+ }
+ }
+ case reflect.String:
+ if vv.Type() == lv.Type() && strings.Contains(lv.String(), vv.String()) {
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+// 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
+ }
+
+ var ins *intersector
+
+ l1v := reflect.ValueOf(l1)
+ l2v := reflect.ValueOf(l2)
+
+ switch l1v.Kind() {
+ case reflect.Array, reflect.Slice:
+ ins = &intersector{r: reflect.MakeSlice(l1v.Type(), 0, 0), seen: make(map[interface{}]bool)}
+ switch l2v.Kind() {
+ case reflect.Array, reflect.Slice:
+ for i := 0; i < l1v.Len(); i++ {
+ l1vv := l1v.Index(i)
+ if !l1vv.Type().Comparable() {
+ return make([]interface{}, 0), errors.New("intersect does not support slices or arrays of uncomparable types")
+ }
+
+ for j := 0; j < l2v.Len(); j++ {
+ l2vv := l2v.Index(j)
+ if !l2vv.Type().Comparable() {
+ return make([]interface{}, 0), errors.New("intersect does not support slices or arrays of uncomparable types")
+ }
+
+ ins.handleValuePair(l1vv, l2vv)
+ }
+ }
+ return ins.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())
+ }
+}
+
+// Group groups a set of elements by the given key.
+// This is currently only supported for Pages.
+func (ns *Namespace) Group(key interface{}, items interface{}) (interface{}, error) {
+ if key == nil {
+ return nil, errors.New("nil is not a valid key to group by")
+ }
+
+ if g, ok := items.(collections.Grouper); ok {
+ return g.Group(key, items)
+ }
+
+ in := newSliceElement(items)
+
+ if g, ok := in.(collections.Grouper); ok {
+ return g.Group(key, items)
+ }
+
+ return nil, fmt.Errorf("grouping not supported for type %T %T", items, in)
+}
+
+// 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:
+ k, err := cast.ToIntE(key)
+ if err != nil {
+ return false, fmt.Errorf("isset unable to use key of type %T as index", key)
+ }
+ if av.Len() > k {
+ 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())
+
+ 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{} {
+ if len(args) == 0 {
+ return args
+ }
+
+ return collections.Slice(args...)
+}
+
+type intersector struct {
+ r reflect.Value
+ seen map[interface{}]bool
+}
+
+func (i *intersector) appendIfNotSeen(v reflect.Value) {
+
+ vi := v.Interface()
+ if !i.seen[vi] {
+ i.r = reflect.Append(i.r, v)
+ i.seen[vi] = true
+ }
+}
+
+func (i *intersector) handleValuePair(l1vv, l2vv reflect.Value) {
+ switch kind := l1vv.Kind(); {
+ case kind == reflect.String:
+ l2t, err := toString(l2vv)
+ if err == nil && l1vv.String() == l2t {
+ i.appendIfNotSeen(l1vv)
+ }
+ case isNumber(kind):
+ f1, err1 := numberToFloat(l1vv)
+ f2, err2 := numberToFloat(l2vv)
+ if err1 == nil && err2 == nil && f1 == f2 {
+ i.appendIfNotSeen(l1vv)
+ }
+ case kind == reflect.Ptr, kind == reflect.Struct:
+ if l1vv.Interface() == l2vv.Interface() {
+ i.appendIfNotSeen(l1vv)
+ }
+ case kind == reflect.Interface:
+ i.handleValuePair(reflect.ValueOf(l1vv.Interface()), l2vv)
+ }
+}
+
+// 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)
+
+ var ins *intersector
+
+ switch l1v.Kind() {
+ case reflect.Array, reflect.Slice:
+ switch l2v.Kind() {
+ case reflect.Array, reflect.Slice:
+ ins = &intersector{r: reflect.MakeSlice(l1v.Type(), 0, 0), seen: make(map[interface{}]bool)}
+
+ if l1v.Type() != l2v.Type() &&
+ l1v.Type().Elem().Kind() != reflect.Interface &&
+ l2v.Type().Elem().Kind() != reflect.Interface {
+ return ins.r.Interface(), nil
+ }
+
+ var (
+ l1vv reflect.Value
+ isNil bool
+ )
+
+ for i := 0; i < l1v.Len(); i++ {
+ l1vv, isNil = indirectInterface(l1v.Index(i))
+
+ if !l1vv.Type().Comparable() {
+ return []interface{}{}, errors.New("union does not support slices or arrays of uncomparable types")
+ }
+
+ if !isNil {
+ ins.appendIfNotSeen(l1vv)
+ }
+ }
+
+ if !l1vv.IsValid() {
+ // The first slice may be empty. Pick the first value of the second
+ // to use as a prototype.
+ if l2v.Len() > 0 {
+ l1vv = l2v.Index(0)
+ }
+ }
+
+ for j := 0; j < l2v.Len(); j++ {
+ l2vv := l2v.Index(j)
+
+ switch kind := l1vv.Kind(); {
+ case kind == reflect.String:
+ l2t, err := toString(l2vv)
+ if err == nil {
+ ins.appendIfNotSeen(reflect.ValueOf(l2t))
+ }
+ case isNumber(kind):
+ var err error
+ l2vv, err = convertNumber(l2vv, kind)
+ if err == nil {
+ ins.appendIfNotSeen(l2vv)
+ }
+ case kind == reflect.Interface, kind == reflect.Struct, kind == reflect.Ptr:
+ ins.appendIfNotSeen(l2vv)
+
+ }
+ }
+
+ return ins.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(seq interface{}) (interface{}, error) {
+ if seq == nil {
+ return make([]interface{}, 0), nil
+ }
+
+ v := reflect.ValueOf(seq)
+ var slice reflect.Value
+
+ switch v.Kind() {
+ case reflect.Slice:
+ slice = reflect.MakeSlice(v.Type(), 0, 0)
+ case reflect.Array:
+ slice = reflect.MakeSlice(reflect.SliceOf(v.Type().Elem()), 0, 0)
+ default:
+ return nil, errors.Errorf("type %T not supported", seq)
+ }
+
+ seen := make(map[interface{}]bool)
+ for i := 0; i < v.Len(); i++ {
+ ev, _ := indirectInterface(v.Index(i))
+ if !ev.Type().Comparable() {
+ return nil, errors.New("elements must be comparable")
+ }
+ key := normalize(ev)
+ if _, found := seen[key]; !found {
+ slice = reflect.Append(slice, ev)
+ seen[key] = true
+ }
+ }
+
+ return slice.Interface(), nil
+
+}
+
+// KeyVals creates a key and values wrapper.
+func (ns *Namespace) KeyVals(key interface{}, vals ...interface{}) (types.KeyValues, error) {
+ return types.KeyValues{Key: key, Values: vals}, nil
+}
+
+// NewScratch creates a new Scratch which can be used to store values in a
+// thread safe way.
+func (ns *Namespace) NewScratch() *maps.Scratch {
+ return maps.NewScratch()
+}
diff --git a/tpl/collections/collections_test.go b/tpl/collections/collections_test.go
new file mode 100644
index 000000000..137c6fa3a
--- /dev/null
+++ b/tpl/collections/collections_test.go
@@ -0,0 +1,872 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/langs"
+ "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"}, []string{}},
+ {int64(2), []int{100, 200, 300}, []int{300}},
+ {100, []int{100, 200}, []int{}},
+ {"1", []int{100, 200, 300}, []int{200, 300}},
+ {int64(-1), []int{100, 200, 300}, false},
+ {"noint", []int{100, 200, 300}, false},
+ {2, []string{}, []string{}},
+ {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)
+ require.Equal(t, test.expect, result, errMsg)
+ }
+}
+
+type tstGrouper struct {
+}
+
+type tstGroupers []*tstGrouper
+
+func (g tstGrouper) Group(key interface{}, items interface{}) (interface{}, error) {
+ ilen := reflect.ValueOf(items).Len()
+ return fmt.Sprintf("%v(%d)", key, ilen), nil
+}
+
+type tstGrouper2 struct {
+}
+
+func (g *tstGrouper2) Group(key interface{}, items interface{}) (interface{}, error) {
+ ilen := reflect.ValueOf(items).Len()
+ return fmt.Sprintf("%v(%d)", key, ilen), nil
+}
+
+func TestGroup(t *testing.T) {
+ t.Parallel()
+
+ ns := New(&deps.Deps{})
+
+ for i, test := range []struct {
+ key interface{}
+ items interface{}
+ expect interface{}
+ }{
+ {"a", []*tstGrouper{{}, {}}, "a(2)"},
+ {"b", tstGroupers{&tstGrouper{}, &tstGrouper{}}, "b(2)"},
+ {"a", []tstGrouper{{}, {}}, "a(2)"},
+ {"a", []*tstGrouper2{{}, {}}, "a(2)"},
+ {"b", []tstGrouper2{{}, {}}, "b(2)"},
+ {"a", []*tstGrouper{}, "a(0)"},
+ {"a", []string{"a", "b"}, false},
+ {"a", "asdf", false},
+ {"a", nil, false},
+ {nil, []*tstGrouper{{}, {}}, false},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test)
+
+ result, err := ns.Group(test.key, test.items)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ require.Error(t, err, errMsg)
+ continue
+ }
+
+ require.NoError(t, err, errMsg)
+ require.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}},
+ {0, []string{"h", "u", "g", "o"}, []string{}},
+ {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()
+ assert := require.New(t)
+
+ 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},
+ {[]float64{1, 2, 3}, 1, true},
+ {[]float32{1, 2, 3}, 1, true},
+ {"this substring should be found", "substring", true},
+ {"this substring should not be found", "subseastring", false},
+ {nil, "foo", false},
+ // Pointers
+ {pagesPtr{p1, p2, p3, p2}, p2, true},
+ {pagesPtr{p1, p2, p3, p2}, p4, false},
+ // Structs
+ {pagesVals{p3v, p2v, p3v, p2v}, p2v, true},
+ {pagesVals{p3v, p2v, p3v, p2v}, p4v, false},
+ } {
+
+ errMsg := fmt.Sprintf("[%d] %v", i, test)
+
+ result, err := ns.In(test.l1, test.l2)
+ assert.NoError(err)
+ assert.Equal(test.expect, result, errMsg)
+ }
+
+ // Slices are not comparable
+ _, err := ns.In([]string{"a", "b"}, []string{"a", "b"})
+ assert.Error(err)
+}
+
+type testPage struct {
+ Title string
+}
+
+func (p testPage) String() string {
+ return "p-" + p.Title
+}
+
+type pagesPtr []*testPage
+type pagesVals []testPage
+
+var (
+ p1 = &testPage{"A"}
+ p2 = &testPage{"B"}
+ p3 = &testPage{"C"}
+ p4 = &testPage{"D"}
+
+ p1v = testPage{"A"}
+ p2v = testPage{"B"}
+ p3v = testPage{"C"}
+ p4v = testPage{"D"}
+)
+
+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}},
+
+ // []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}},
+
+ // Structs
+ {pagesPtr{p1, p4, p2, p3}, pagesPtr{p4, p2, p2}, pagesPtr{p4, p2}},
+ {pagesVals{p1v, p4v, p2v, p3v}, pagesVals{p1v, p3v, p3v}, pagesVals{p1v, p3v}},
+ {[]interface{}{p1, p4, p2, p3}, []interface{}{p4, p2, p2}, []interface{}{p4, p2}},
+ {[]interface{}{p1v, p4v, p2v, p3v}, []interface{}{p1v, p3v, p3v}, []interface{}{p1v, p3v}},
+ {pagesPtr{p1, p4, p2, p3}, pagesPtr{}, pagesPtr{}},
+ {pagesVals{}, pagesVals{p1v, p3v, p3v}, pagesVals{}},
+ {[]interface{}{p1, p4, p2, p3}, []interface{}{}, []interface{}{}},
+ {[]interface{}{}, []interface{}{p1v, p3v, p3v}, []interface{}{}},
+
+ // errors
+ {"not array or slice", []string{"a"}, false},
+ {[]string{"a"}, "not array or slice", false},
+
+ // uncomparable types - #3820
+ {[]map[int]int{{1: 1}, {2: 2}}, []map[int]int{{2: 2}, {3: 3}}, false},
+ {[][]int{{1, 1}, {1, 2}}, [][]int{{1, 2}, {1, 2}, {1, 3}}, false},
+ {[]int{1, 1}, [][]int{{1, 2}, {1, 2}, {1, 3}}, false},
+ } {
+
+ 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)
+ if !reflect.DeepEqual(result, test.expect) {
+ t.Fatalf("[%d] Got\n%v expected\n%v", i, result, test.expect)
+ }
+ }
+}
+
+func TestIsSet(t *testing.T) {
+ t.Parallel()
+
+ ns := newTestNs()
+
+ for i, test := range []struct {
+ a interface{}
+ key interface{}
+ expect bool
+ isErr bool
+ }{
+ {[]interface{}{1, 2, 3, 5}, 2, true, false},
+ {[]interface{}{1, 2, 3, 5}, "2", true, false},
+ {[]interface{}{1, 2, 3, 5}, 2.0, 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},
+ {[]interface{}{1, 2, 3, 5}, TstX{}, false, true},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test)
+
+ result, err := ns.IsSet(test.a, test.key)
+ if test.isErr {
+ 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")
+ }
+}
+
+// Also see tests in commons/collection.
+func TestSlice(t *testing.T) {
+ t.Parallel()
+
+ ns := New(&deps.Deps{})
+
+ for i, test := range []struct {
+ args []interface{}
+ expected interface{}
+ }{
+ {[]interface{}{"a", "b"}, []string{"a", "b"}},
+ {[]interface{}{}, []interface{}{}},
+ {[]interface{}{nil}, []interface{}{nil}},
+ {[]interface{}{5, "b"}, []interface{}{5, "b"}},
+ {[]interface{}{tstNoStringer{}}, []tstNoStringer{{}}},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test.args)
+
+ result := ns.Slice(test.args...)
+
+ assert.Equal(t, test.expected, result, errMsg)
+ }
+
+ assert.Len(t, ns.Slice(), 0)
+}
+
+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, 3}, false}, // 28
+ {[]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},
+
+ // Structs
+ {pagesPtr{p1, p4}, pagesPtr{p4, p2, p2}, pagesPtr{p1, p4, p2}, false},
+ {pagesVals{p1v}, pagesVals{p3v, p3v}, pagesVals{p1v, p3v}, false},
+ {[]interface{}{p1, p4}, []interface{}{p4, p2, p2}, []interface{}{p1, p4, p2}, false},
+ {[]interface{}{p1v}, []interface{}{p3v, p3v}, []interface{}{p1v, p3v}, false},
+ // #3686
+ {[]interface{}{p1v}, []interface{}{}, []interface{}{p1v}, false},
+ {[]interface{}{}, []interface{}{p1v}, []interface{}{p1v}, false},
+ {pagesPtr{p1}, pagesPtr{}, pagesPtr{p1}, false},
+ {pagesVals{p1v}, pagesVals{}, pagesVals{p1v}, false},
+ {pagesPtr{}, pagesPtr{p1}, pagesPtr{p1}, false},
+ {pagesVals{}, pagesVals{p1v}, pagesVals{p1v}, false},
+
+ // errors
+ {"not array or slice", []string{"a"}, false, true},
+ {[]string{"a"}, "not array or slice", false, true},
+
+ // uncomparable types - #3820
+ {[]map[string]int{{"K1": 1}}, []map[string]int{{"K2": 2}, {"K2": 2}}, false, true},
+ {[][]int{{1, 1}, {1, 2}}, [][]int{{2, 1}, {2, 2}}, 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)
+ if !reflect.DeepEqual(result, test.expect) {
+ t.Fatalf("[%d] Got\n%v expected\n%v", i, result, test.expect)
+ }
+ }
+}
+
+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},
+ // Pointers
+ {pagesPtr{p1, p2, p3, p2}, pagesPtr{p1, p2, p3}, false},
+ {pagesPtr{}, pagesPtr{}, false},
+ // Structs
+ {pagesVals{p3v, p2v, p3v, p2v}, pagesVals{p3v, p2v}, false},
+
+ // should fail
+ // uncomparable types
+ {[]map[string]int{{"K1": 1}}, []map[string]int{{"K2": 2}, {"K2": 2}}, true},
+ {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 := langs.NewLanguage("en", cfg)
+ l.Set("i18nDir", "i18n")
+ cs, err := helpers.NewContentSpec(l)
+ if err != nil {
+ panic(err)
+ }
+ return &deps.Deps{
+ Cfg: cfg,
+ Fs: hugofs.NewMem(l),
+ ContentSpec: cs,
+ Log: loggers.NewErrorLogger(),
+ }
+}
+
+func newTestNs() *Namespace {
+ v := viper.New()
+ v.Set("contentDir", "content")
+ return New(newDeps(v))
+}
diff --git a/tpl/collections/complement.go b/tpl/collections/complement.go
new file mode 100644
index 000000000..a5633f8b4
--- /dev/null
+++ b/tpl/collections/complement.go
@@ -0,0 +1,58 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+)
+
+// Complement gives the elements in the last element of seqs that are not in
+// any of the others.
+// All elements of seqs must be slices or arrays of comparable types.
+//
+// The reasoning behind this rather clumsy API is so we can do this in the templates:
+// {{ $c := .Pages | complement $last4 }}
+func (ns *Namespace) Complement(seqs ...interface{}) (interface{}, error) {
+ if len(seqs) < 2 {
+ return nil, errors.New("complement needs at least two arguments")
+ }
+
+ universe := seqs[len(seqs)-1]
+ as := seqs[:len(seqs)-1]
+
+ aset, err := collectIdentities(as...)
+ if err != nil {
+ return nil, err
+ }
+
+ v := reflect.ValueOf(universe)
+ switch v.Kind() {
+ case reflect.Array, reflect.Slice:
+ sl := reflect.MakeSlice(v.Type(), 0, 0)
+ for i := 0; i < v.Len(); i++ {
+ ev, _ := indirectInterface(v.Index(i))
+ if !ev.Type().Comparable() {
+ return nil, errors.New("elements in complement must be comparable")
+ }
+ if _, found := aset[normalize(ev)]; !found {
+ sl = reflect.Append(sl, ev)
+ }
+ }
+ return sl.Interface(), nil
+ default:
+ return nil, fmt.Errorf("arguments to complement must be slices or arrays")
+ }
+}
diff --git a/tpl/collections/complement_test.go b/tpl/collections/complement_test.go
new file mode 100644
index 000000000..07611bd5b
--- /dev/null
+++ b/tpl/collections/complement_test.go
@@ -0,0 +1,95 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+
+ "github.com/gohugoio/hugo/deps"
+
+ "github.com/stretchr/testify/require"
+)
+
+type StructWithSlice struct {
+ A string
+ B []string
+}
+
+type StructWithSlicePointers []*StructWithSlice
+
+func TestComplement(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ ns := New(&deps.Deps{})
+
+ s1 := []TstX{{A: "a"}, {A: "b"}, {A: "d"}, {A: "e"}}
+ s2 := []TstX{{A: "b"}, {A: "e"}}
+
+ xa, xb, xd, xe := &StructWithSlice{A: "a"}, &StructWithSlice{A: "b"}, &StructWithSlice{A: "d"}, &StructWithSlice{A: "e"}
+
+ sp1 := []*StructWithSlice{xa, xb, xd, xe}
+ sp2 := []*StructWithSlice{xb, xe}
+
+ sp1_2 := StructWithSlicePointers{xa, xb, xd, xe}
+ sp2_2 := StructWithSlicePointers{xb, xe}
+
+ for i, test := range []struct {
+ s interface{}
+ t []interface{}
+ expected interface{}
+ }{
+ {[]string{"a", "b", "c"}, []interface{}{[]string{"c", "d"}}, []string{"a", "b"}},
+ {[]string{"a", "b", "c"}, []interface{}{[]string{"c", "d"}, []string{"a", "b"}}, []string{}},
+ {[]interface{}{"a", "b", nil}, []interface{}{[]string{"a", "d"}}, []interface{}{"b", nil}},
+ {[]int{1, 2, 3, 4, 5}, []interface{}{[]int{1, 3}, []string{"a", "b"}, []int{1, 2}}, []int{4, 5}},
+ {[]int{1, 2, 3, 4, 5}, []interface{}{[]int64{1, 3}}, []int{2, 4, 5}},
+ {s1, []interface{}{s2}, []TstX{{A: "a"}, {A: "d"}}},
+ {sp1, []interface{}{sp2}, []*StructWithSlice{xa, xd}},
+ {sp1_2, []interface{}{sp2_2}, StructWithSlicePointers{xa, xd}},
+
+ // Errors
+ {[]string{"a", "b", "c"}, []interface{}{"error"}, false},
+ {"error", []interface{}{[]string{"c", "d"}, []string{"a", "b"}}, false},
+ {[]string{"a", "b", "c"}, []interface{}{[][]string{{"c", "d"}}}, false},
+ {[]interface{}{[][]string{{"c", "d"}}}, []interface{}{[]string{"c", "d"}, []string{"a", "b"}}, false},
+ } {
+
+ errMsg := fmt.Sprintf("[%d]", i)
+
+ args := append(test.t, test.s)
+
+ result, err := ns.Complement(args...)
+
+ if b, ok := test.expected.(bool); ok && !b {
+ require.Error(t, err, errMsg)
+ continue
+ }
+
+ require.NoError(t, err, errMsg)
+
+ if !reflect.DeepEqual(test.expected, result) {
+ t.Fatalf("%s got\n%T: %v\nexpected\n%T: %v", errMsg, result, result, test.expected, test.expected)
+ }
+ }
+
+ _, err := ns.Complement()
+ assert.Error(err)
+ _, err = ns.Complement([]string{"a", "b"})
+ assert.Error(err)
+
+}
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..bd752d666
--- /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{{1, 2}, {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..8dbef75c9
--- /dev/null
+++ b/tpl/collections/init.go
@@ -0,0 +1,191 @@
+// 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.Complement,
+ []string{"complement"},
+ [][2]string{
+ {`{{ slice "a" "b" "c" "d" "e" "f" | complement (slice "b" "c") (slice "d" "e") }}`, `[a f]`},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.SymDiff,
+ []string{"symdiff"},
+ [][2]string{
+ {`{{ slice 1 2 3 | symdiff (slice 3 4) }}`, `[1 2 4]`},
+ },
+ )
+
+ 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.KeyVals,
+ []string{"keyVals"},
+ [][2]string{
+ {`{{ keyVals "key" "a" "b" }}`, `key: [a b]`},
+ },
+ )
+
+ 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&amp;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.Append,
+ []string{"append"},
+ [][2]string{},
+ )
+
+ ns.AddMethodMapping(ctx.Group,
+ []string{"group"},
+ [][2]string{},
+ )
+
+ ns.AddMethodMapping(ctx.Seq,
+ []string{"seq"},
+ [][2]string{
+ {`{{ seq 3 }}`, `[1 2 3]`},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.NewScratch,
+ []string{"newScratch"},
+ [][2]string{
+ {`{{ $scratch := newScratch }}{{ $scratch.Add "b" 2 }}{{ $scratch.Add "b" 2 }}{{ $scratch.Get "b" }}`, `4`},
+ },
+ )
+
+ 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/reflect_helpers.go b/tpl/collections/reflect_helpers.go
new file mode 100644
index 000000000..69425fcb0
--- /dev/null
+++ b/tpl/collections/reflect_helpers.go
@@ -0,0 +1,209 @@
+// 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"
+ "time"
+
+ "github.com/pkg/errors"
+)
+
+var (
+ zero reflect.Value
+ errorType = reflect.TypeOf((*error)(nil)).Elem()
+ timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
+)
+
+func numberToFloat(v reflect.Value) (float64, error) {
+ switch kind := v.Kind(); {
+ case isFloat(kind):
+ return v.Float(), nil
+ case isInt(kind):
+ return float64(v.Int()), nil
+ case isUint(kind):
+ return float64(v.Uint()), nil
+ case kind == reflect.Interface:
+ return numberToFloat(v.Elem())
+ default:
+ return 0, fmt.Errorf("invalid kind %s in numberToFloat", kind)
+ }
+}
+
+// normalizes different numeric types to make them comparable.
+func normalize(v reflect.Value) interface{} {
+ k := v.Kind()
+
+ switch {
+ case isNumber(k):
+ f, err := numberToFloat(v)
+ if err == nil {
+ return f
+ }
+ }
+
+ return v.Interface()
+}
+
+// collects identities from the slices in seqs into a set. Numeric values are normalized,
+// pointers unwrapped.
+func collectIdentities(seqs ...interface{}) (map[interface{}]bool, error) {
+ seen := make(map[interface{}]bool)
+ for _, seq := range seqs {
+ v := reflect.ValueOf(seq)
+ switch v.Kind() {
+ case reflect.Array, reflect.Slice:
+ for i := 0; i < v.Len(); i++ {
+ ev, _ := indirectInterface(v.Index(i))
+
+ if !ev.Type().Comparable() {
+ return nil, errors.New("elements must be comparable")
+ }
+
+ seen[normalize(ev)] = true
+ }
+ default:
+ return nil, fmt.Errorf("arguments must be slices or arrays")
+ }
+ }
+
+ return seen, nil
+}
+
+// We have some different numeric and string types that we try to behave like
+// they were the same.
+func convertValue(v reflect.Value, to reflect.Type) (reflect.Value, error) {
+ if v.Type().AssignableTo(to) {
+ return v, nil
+ }
+ switch kind := to.Kind(); {
+ case kind == reflect.String:
+ s, err := toString(v)
+ return reflect.ValueOf(s), err
+ case isNumber(kind):
+ return convertNumber(v, kind)
+ default:
+ return reflect.Value{}, errors.Errorf("%s is not assignable to %s", v.Type(), to)
+ }
+}
+
+// There are potential overflows in this function, but the downconversion of
+// int64 etc. into int8 etc. is coming from the synthetic unit tests for Union etc.
+// TODO(bep) We should consider normalizing the slices to int64 etc.
+func convertNumber(v reflect.Value, to reflect.Kind) (reflect.Value, error) {
+ var n reflect.Value
+ if isFloat(to) {
+ f, err := toFloat(v)
+ if err != nil {
+ return n, err
+ }
+ switch to {
+ case reflect.Float32:
+ n = reflect.ValueOf(float32(f))
+ default:
+ n = reflect.ValueOf(float64(f))
+ }
+ } else if isInt(to) {
+ i, err := toInt(v)
+ if err != nil {
+ return n, err
+ }
+ switch to {
+ case reflect.Int:
+ n = reflect.ValueOf(int(i))
+ case reflect.Int8:
+ n = reflect.ValueOf(int8(i))
+ case reflect.Int16:
+ n = reflect.ValueOf(int16(i))
+ case reflect.Int32:
+ n = reflect.ValueOf(int32(i))
+ case reflect.Int64:
+ n = reflect.ValueOf(int64(i))
+ }
+ } else if isUint(to) {
+ i, err := toUint(v)
+ if err != nil {
+ return n, err
+ }
+ switch to {
+ case reflect.Uint:
+ n = reflect.ValueOf(uint(i))
+ case reflect.Uint8:
+ n = reflect.ValueOf(uint8(i))
+ case reflect.Uint16:
+ n = reflect.ValueOf(uint16(i))
+ case reflect.Uint32:
+ n = reflect.ValueOf(uint32(i))
+ case reflect.Uint64:
+ n = reflect.ValueOf(uint64(i))
+ }
+
+ }
+
+ if !n.IsValid() {
+ return n, errors.New("invalid values")
+ }
+
+ return n, nil
+
+}
+
+func newSliceElement(items interface{}) interface{} {
+ tp := reflect.TypeOf(items)
+ if tp == nil {
+ return nil
+ }
+ switch tp.Kind() {
+ case reflect.Array, reflect.Slice:
+ tp = tp.Elem()
+ if tp.Kind() == reflect.Ptr {
+ tp = tp.Elem()
+ }
+
+ return reflect.New(tp).Interface()
+ }
+ return nil
+}
+
+func isNumber(kind reflect.Kind) bool {
+ return isInt(kind) || isUint(kind) || isFloat(kind)
+}
+
+func isInt(kind reflect.Kind) bool {
+ switch kind {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ return true
+ default:
+ return false
+ }
+}
+
+func isUint(kind reflect.Kind) bool {
+ switch kind {
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ return true
+ default:
+ return false
+ }
+}
+
+func isFloat(kind reflect.Kind) bool {
+ switch kind {
+ case reflect.Float32, reflect.Float64:
+ return true
+ default:
+ return false
+ }
+}
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/symdiff.go b/tpl/collections/symdiff.go
new file mode 100644
index 000000000..1c58257e4
--- /dev/null
+++ b/tpl/collections/symdiff.go
@@ -0,0 +1,71 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+
+ "github.com/pkg/errors"
+)
+
+// SymDiff returns the symmetric difference of s1 and s2.
+// Arguments must be either a slice or an array of comparable types.
+func (ns *Namespace) SymDiff(s2, s1 interface{}) (interface{}, error) {
+ ids1, err := collectIdentities(s1)
+ if err != nil {
+ return nil, err
+ }
+ ids2, err := collectIdentities(s2)
+ if err != nil {
+ return nil, err
+ }
+
+ var slice reflect.Value
+ var sliceElemType reflect.Type
+
+ for i, s := range []interface{}{s1, s2} {
+ v := reflect.ValueOf(s)
+
+ switch v.Kind() {
+ case reflect.Array, reflect.Slice:
+ if i == 0 {
+ sliceType := v.Type()
+ sliceElemType = sliceType.Elem()
+ slice = reflect.MakeSlice(sliceType, 0, 0)
+ }
+
+ for i := 0; i < v.Len(); i++ {
+ ev, _ := indirectInterface(v.Index(i))
+ if !ev.Type().Comparable() {
+ return nil, errors.New("symdiff: elements must be comparable")
+ }
+ key := normalize(ev)
+ // Append if the key is not in their intersection.
+ if ids1[key] != ids2[key] {
+ v, err := convertValue(ev, sliceElemType)
+ if err != nil {
+ return nil, errors.WithMessage(err, "symdiff: failed to convert value")
+ }
+ slice = reflect.Append(slice, v)
+ }
+ }
+ default:
+ return nil, fmt.Errorf("arguments to symdiff must be slices or arrays")
+ }
+ }
+
+ return slice.Interface(), nil
+
+}
diff --git a/tpl/collections/symdiff_test.go b/tpl/collections/symdiff_test.go
new file mode 100644
index 000000000..b62fdb73b
--- /dev/null
+++ b/tpl/collections/symdiff_test.go
@@ -0,0 +1,80 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+
+ "github.com/gohugoio/hugo/deps"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestSymDiff(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ ns := New(&deps.Deps{})
+
+ s1 := []TstX{{A: "a"}, {A: "b"}}
+ s2 := []TstX{{A: "a"}, {A: "e"}}
+
+ xa, xb, xd, xe := &StructWithSlice{A: "a"}, &StructWithSlice{A: "b"}, &StructWithSlice{A: "d"}, &StructWithSlice{A: "e"}
+
+ sp1 := []*StructWithSlice{xa, xb, xd, xe}
+ sp2 := []*StructWithSlice{xb, xe}
+
+ for i, test := range []struct {
+ s1 interface{}
+ s2 interface{}
+ expected interface{}
+ }{
+ {[]string{"a", "x", "b", "c"}, []string{"a", "b", "y", "c"}, []string{"x", "y"}},
+ {[]string{"a", "b", "c"}, []string{"a", "b", "c"}, []string{}},
+ {[]interface{}{"a", "b", nil}, []interface{}{"a"}, []interface{}{"b", nil}},
+ {[]int{1, 2, 3}, []int{3, 4}, []int{1, 2, 4}},
+ {[]int{1, 2, 3}, []int64{3, 4}, []int{1, 2, 4}},
+ {s1, s2, []TstX{{A: "b"}, {A: "e"}}},
+ {sp1, sp2, []*StructWithSlice{xa, xd}},
+
+ // Errors
+ {"error", "error", false},
+ {[]int{1, 2, 3}, []string{"3", "4"}, false},
+ } {
+
+ errMsg := fmt.Sprintf("[%d]", i)
+
+ result, err := ns.SymDiff(test.s2, test.s1)
+
+ if b, ok := test.expected.(bool); ok && !b {
+ require.Error(t, err, errMsg)
+ continue
+ }
+
+ require.NoError(t, err, errMsg)
+
+ if !reflect.DeepEqual(test.expected, result) {
+ t.Fatalf("%s got\n%T: %v\nexpected\n%T: %v", errMsg, result, result, test.expected, test.expected)
+ }
+ }
+
+ _, err := ns.Complement()
+ assert.Error(err)
+ _, err = ns.Complement([]string{"a", "b"})
+ assert.Error(err)
+
+}
diff --git a/tpl/collections/where.go b/tpl/collections/where.go
new file mode 100644
index 000000000..c96c38910
--- /dev/null
+++ b/tpl/collections/where.go
@@ -0,0 +1,476 @@
+// 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"
+)
+
+// 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 fvp, fmvp *float64
+ var svp, smvp *string
+ var slv, slmv interface{}
+ var ima []int64
+ var fma []float64
+ 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.Float64:
+ fv := v.Float()
+ fvp = &fv
+ fmv := mv.Float()
+ fmvp = &fmv
+ 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() && v.Kind() != reflect.Array && v.Kind() != reflect.Slice {
+ 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.Float64:
+ fv := v.Float()
+ fvp = &fv
+ for i := 0; i < mv.Len(); i++ {
+ if aFloat, err := toFloat(mv.Index(i)); err == nil {
+ fma = append(fma, aFloat)
+ }
+ }
+ 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)))
+ }
+ }
+ case reflect.Array, reflect.Slice:
+ slv = v.Interface()
+ slmv = mv.Interface()
+ }
+ }
+
+ switch op {
+ case "", "=", "==", "eq":
+ switch {
+ case ivp != nil && imvp != nil:
+ return *ivp == *imvp, nil
+ case svp != nil && smvp != nil:
+ return *svp == *smvp, nil
+ case fvp != nil && fmvp != nil:
+ return *fvp == *fmvp, nil
+ }
+ case "!=", "<>", "ne":
+ switch {
+ case ivp != nil && imvp != nil:
+ return *ivp != *imvp, nil
+ case svp != nil && smvp != nil:
+ return *svp != *smvp, nil
+ case fvp != nil && fmvp != nil:
+ return *fvp != *fmvp, nil
+ }
+ case ">=", "ge":
+ switch {
+ case ivp != nil && imvp != nil:
+ return *ivp >= *imvp, nil
+ case svp != nil && smvp != nil:
+ return *svp >= *smvp, nil
+ case fvp != nil && fmvp != nil:
+ return *fvp >= *fmvp, nil
+ }
+ case ">", "gt":
+ switch {
+ case ivp != nil && imvp != nil:
+ return *ivp > *imvp, nil
+ case svp != nil && smvp != nil:
+ return *svp > *smvp, nil
+ case fvp != nil && fmvp != nil:
+ return *fvp > *fmvp, nil
+ }
+ case "<=", "le":
+ switch {
+ case ivp != nil && imvp != nil:
+ return *ivp <= *imvp, nil
+ case svp != nil && smvp != nil:
+ return *svp <= *smvp, nil
+ case fvp != nil && fmvp != nil:
+ return *fvp <= *fmvp, nil
+ }
+ case "<", "lt":
+ switch {
+ case ivp != nil && imvp != nil:
+ return *ivp < *imvp, nil
+ case svp != nil && smvp != nil:
+ return *svp < *smvp, nil
+ case fvp != nil && fmvp != nil:
+ return *fvp < *fmvp, nil
+ }
+ case "in", "not in":
+ var r bool
+ switch {
+ case ivp != nil && len(ima) > 0:
+ r, _ = ns.In(ima, *ivp)
+ case fvp != nil && len(fma) > 0:
+ r, _ = ns.In(fma, *fvp)
+ case svp != nil:
+ if len(sma) > 0 {
+ r, _ = ns.In(sma, *svp)
+ } else if smvp != nil {
+ r, _ = ns.In(*smvp, *svp)
+ }
+ default:
+ 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 {
+ switch {
+ case mt.PkgPath != "":
+ return zero, fmt.Errorf("%s is an unexported method of type %s", elemName, typ)
+ case mt.Type.NumIn() > 1:
+ return zero, fmt.Errorf("%s is a method of type %s but requires more than 1 parameter", elemName, typ)
+ case mt.Type.NumOut() == 0:
+ return zero, fmt.Errorf("%s is a method of type %s but returns no output", elemName, typ)
+ case mt.Type.NumOut() > 2:
+ return zero, fmt.Errorf("%s is a method of type %s but returns more than 2 outputs", elemName, typ)
+ case mt.Type.NumOut() == 1 && mt.Type.Out(0).Implements(errorType):
+ return zero, fmt.Errorf("%s is a method of type %s but only returns an error type", elemName, typ)
+ case mt.Type.NumOut() == 2 && !mt.Type.Out(1).Implements(errorType):
+ return zero, fmt.Errorf("%s is a method of type %s returning two values but the second value is not an error type", 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 {
+ continue
+ }
+ }
+ } 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 float 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.
+// TODO(bep) consolidate all these reflect funcs.
+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")
+}
+
+func toUint(v reflect.Value) (uint64, error) {
+ switch v.Kind() {
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ return v.Uint(), nil
+ case reflect.Interface:
+ return toUint(v.Elem())
+ }
+ return 0, errors.New("unable to convert value to uint")
+}
+
+// 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")
+}
+
+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..fb768cfde
--- /dev/null
+++ b/tpl/collections/where_test.go
@@ -0,0 +1,724 @@
+// 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: []map[string]float64{
+ {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "x": 4},
+ },
+ key: "b", match: 4.0,
+ expect: []map[string]float64{{"a": 3, "b": 4}},
+ },
+ {
+ seq: []map[string]float64{
+ {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "x": 4},
+ },
+ key: "b", match: 4.0, op: "!=",
+ expect: []map[string]float64{{"a": 1, "b": 2}, {"a": 5, "x": 4}},
+ },
+ {
+ seq: []map[string]float64{
+ {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "x": 4},
+ },
+ key: "b", match: 4.0, op: "<",
+ expect: []map[string]float64{{"a": 1, "b": 2}},
+ },
+ {
+ seq: []map[string]float64{
+ {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "x": 4},
+ },
+ key: "b", match: 4.0, op: "<=",
+ expect: []map[string]float64{{"a": 1, "b": 2}, {"a": 3, "b": 4}},
+ },
+ {
+ seq: []map[string]float64{
+ {"a": 1, "b": 2}, {"a": 3, "b": 3}, {"a": 5, "x": 4},
+ },
+ key: "b", match: 2.0, op: ">",
+ expect: []map[string]float64{{"a": 3, "b": 3}},
+ },
+ {
+ seq: []map[string]float64{
+ {"a": 1, "b": 2}, {"a": 3, "b": 3}, {"a": 5, "x": 4},
+ },
+ key: "b", match: 2.0, op: ">=",
+ expect: []map[string]float64{{"a": 1, "b": 2}, {"a": 3, "b": 3}},
+ },
+ {
+ 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{
+ {"baz": TstX{A: "a", B: "b"}}, {"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: []map[string]float64{
+ {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6},
+ },
+ key: "b", op: ">", match: 3.0,
+ expect: []map[string]float64{
+ {"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]float64{
+ {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6},
+ },
+ key: "b", op: "in", match: []float64{3, 4, 5},
+ expect: []map[string]float64{
+ {"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]float64{
+ {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6},
+ },
+ key: "b", op: "in", match: ns.Slice(3.0, 4.0, 5.0),
+ expect: []map[string]float64{
+ {"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]float64{
+ {"a": 1, "b": 2}, {"a": 3}, {"a": 5, "b": 6},
+ },
+ key: "b", op: "", match: nil,
+ expect: []map[string]float64{
+ {"a": 3},
+ },
+ },
+ {
+ seq: []map[string]float64{
+ {"a": 1, "b": 2}, {"a": 3}, {"a": 5, "b": 6},
+ },
+ key: "b", op: "!=", match: nil,
+ expect: []map[string]float64{
+ {"a": 1, "b": 2}, {"a": 5, "b": 6},
+ },
+ },
+ {
+ seq: []map[string]float64{
+ {"a": 1, "b": 2}, {"a": 3}, {"a": 5, "b": 6},
+ },
+ key: "b", op: ">", match: nil,
+ expect: []map[string]float64{},
+ },
+ {
+ 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: []map[string]bool{
+ {"a": true, "b": false}, {"c": true, "b": true}, {"d": true, "b": false},
+ },
+ key: "b.z", 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: []map[string]*TstX{}},
+ {seq: []map[string]*TstX{{"foo": nil}}, key: "foo.B.Z", match: "d", expect: []map[string]*TstX{}},
+ {
+ 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}},
+ },
+ },
+ } {
+ t.Run(fmt.Sprintf("test case %d for key %s", i, test.key), func(t *testing.T) {
+ 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)
+ }
+ 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}},
+
+ // Issue #3718
+ {reflect.ValueOf([]interface{}{"a"}), reflect.ValueOf([]string{"a", "b"}), "intersect", expect{true, false}},
+ {reflect.ValueOf([]string{"a"}), reflect.ValueOf([]interface{}{"a", "b"}), "intersect", expect{true, false}},
+ {reflect.ValueOf([]interface{}{1, 2}), reflect.ValueOf([]int{1}), "intersect", expect{true, false}},
+ {reflect.ValueOf([]int{1}), reflect.ValueOf([]interface{}{1, 2}), "intersect", expect{true, false}},
+ } {
+ 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..251b3d13b
--- /dev/null
+++ b/tpl/compare/compare.go
@@ -0,0 +1,254 @@
+// 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 provides template functions for comparing values.
+package compare
+
+import (
+ "fmt"
+ "reflect"
+ "strconv"
+ "time"
+
+ "github.com/gohugoio/hugo/common/types"
+
+ "github.com/gohugoio/hugo/compare"
+)
+
+// 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 {
+ if e, ok := x.(compare.Eqer); ok {
+ return e.Eq(y)
+ }
+
+ if e, ok := y.(compare.Eqer); ok {
+ return e.Eq(x)
+ }
+
+ normalize := func(v interface{}) interface{} {
+ if types.IsNil(v) {
+ return nil
+ }
+ 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.compareGet(a, b)
+ return left >= right
+}
+
+// Gt returns the boolean truth of arg1 > arg2.
+func (n *Namespace) Gt(a, b interface{}) bool {
+ left, right := n.compareGet(a, b)
+ return left > right
+}
+
+// Le returns the boolean truth of arg1 <= arg2.
+func (n *Namespace) Le(a, b interface{}) bool {
+ left, right := n.compareGet(a, b)
+ return left <= right
+}
+
+// Lt returns the boolean truth of arg1 < arg2.
+func (n *Namespace) Lt(a, b interface{}) bool {
+ left, right := n.compareGet(a, b)
+ return left < right
+}
+
+// Conditional can be used as a ternary operator.
+// It returns a if condition, else b.
+func (n *Namespace) Conditional(condition bool, a, b interface{}) interface{} {
+ if condition {
+ return a
+ }
+ return b
+}
+
+func (*Namespace) compareGet(a interface{}, b interface{}) (float64, float64) {
+ if ac, ok := a.(compare.Comparer); ok {
+ c := ac.Compare(b)
+ if c < 0 {
+ return 1, 0
+ } else if c == 0 {
+ return 0, 0
+ } else {
+ return 0, 1
+ }
+ }
+
+ if bc, ok := b.(compare.Comparer); ok {
+ c := bc.Compare(a)
+ if c < 0 {
+ return 0, 1
+ } else if c == 0 {
+ return 0, 0
+ } else {
+ return 1, 0
+ }
+ }
+
+ 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..6c4be7e50
--- /dev/null
+++ b/tpl/compare/compare_test.go
@@ -0,0 +1,266 @@
+// 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/gohugoio/hugo/common/hugo"
+ "github.com/spf13/cast"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type T struct {
+ NonEmptyInterfaceNil I
+ NonEmptyInterfaceTypedNil I
+}
+
+type I interface {
+ Foo() string
+}
+
+func (t *T) Foo() string {
+ return "foo"
+}
+
+var testT = &T{
+ NonEmptyInterfaceTypedNil: (*T)(nil),
+}
+
+type tstEqerType1 string
+type tstEqerType2 string
+
+func (t tstEqerType2) Eq(other interface{}) bool {
+ return cast.ToString(t) == cast.ToString(other)
+}
+
+func (t tstEqerType2) String() string {
+ return string(t)
+}
+
+func (t tstEqerType1) Eq(other interface{}) bool {
+ return cast.ToString(t) == cast.ToString(other)
+}
+
+func (t tstEqerType1) String() string {
+ return string(t)
+}
+
+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},
+ {tstEqerType1("a"), tstEqerType1("a"), 0},
+ {tstEqerType1("a"), tstEqerType2("a"), 0},
+ {tstEqerType2("a"), tstEqerType1("a"), 0},
+ {tstEqerType2("a"), tstEqerType1("b"), -1},
+ {hugo.MustParseVersion("0.32.1").Version(), hugo.MustParseVersion("0.32").Version(), 1},
+ {hugo.MustParseVersion("0.35").Version(), hugo.MustParseVersion("0.32").Version(), 1},
+ {hugo.MustParseVersion("0.36").Version(), hugo.MustParseVersion("0.36").Version(), 0},
+ {hugo.MustParseVersion("0.32").Version(), hugo.MustParseVersion("0.36").Version(), -1},
+ {hugo.MustParseVersion("0.32").Version(), "0.36", -1},
+ {"0.36", hugo.MustParseVersion("0.32").Version(), 1},
+ {"0.36", hugo.MustParseVersion("0.36").Version(), 0},
+ {"0.37", hugo.MustParseVersion("0.37-DEV").Version(), 1},
+ {"0.37-DEV", hugo.MustParseVersion("0.37").Version(), -1},
+ {"0.36", hugo.MustParseVersion("0.37-DEV").Version(), -1},
+ {"0.37-DEV", hugo.MustParseVersion("0.37-DEV").Version(), 0},
+ // https://github.com/gohugoio/hugo/issues/5905
+ {nil, nil, 0},
+ {testT.NonEmptyInterfaceNil, nil, 0},
+ {testT.NonEmptyInterfaceTypedNil, nil, 0},
+ } {
+
+ 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.Fatalf("[%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)
+}
+
+func TestConditional(t *testing.T) {
+ assert := require.New(t)
+ n := New()
+ a, b := "a", "b"
+
+ assert.Equal(a, n.Conditional(true, a, b))
+ assert.Equal(b, n.Conditional(false, a, b))
+}
diff --git a/tpl/compare/init.go b/tpl/compare/init.go
new file mode 100644
index 000000000..619293203
--- /dev/null
+++ b/tpl/compare/init.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 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{
+ {`{{ if ge .Hugo.Version "0.36" }}Reasonable new Hugo version!{{ end }}`, `Reasonable new Hugo version!`},
+ },
+ )
+
+ 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{},
+ )
+
+ ns.AddMethodMapping(ctx.And,
+ []string{"and"},
+ [][2]string{},
+ )
+
+ ns.AddMethodMapping(ctx.Or,
+ []string{"or"},
+ [][2]string{},
+ )
+
+ // getif is used internally by Hugo. Do not document.
+ ns.AddMethodMapping(ctx.getIf,
+ []string{"getif"},
+ [][2]string{},
+ )
+
+ ns.AddMethodMapping(ctx.Not,
+ []string{"not"},
+ [][2]string{},
+ )
+
+ ns.AddMethodMapping(ctx.Conditional,
+ []string{"cond"},
+ [][2]string{
+ {`{{ cond (eq (add 2 2) 4) "2+2 is 4" "what?" | safeHTML }}`, `2+2 is 4`},
+ },
+ )
+
+ 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/compare/truth.go b/tpl/compare/truth.go
new file mode 100644
index 000000000..85ee22121
--- /dev/null
+++ b/tpl/compare/truth.go
@@ -0,0 +1,73 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+// The functions in this file is based on the Go source code, copyright
+// The Go Authors and governed by a BSD-style license.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 provides template functions for comparing values.
+package compare
+
+import (
+ "reflect"
+
+ "github.com/gohugoio/hugo/common/hreflect"
+)
+
+// Boolean logic, based on:
+// https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/funcs.go#L302
+
+func truth(arg reflect.Value) bool {
+ return hreflect.IsTruthfulValue(arg)
+}
+
+// getIf will return the given arg if it is considered truthful, else an empty string.
+func (*Namespace) getIf(arg reflect.Value) reflect.Value {
+ if truth(arg) {
+ return arg
+ }
+ return reflect.ValueOf("")
+}
+
+// And computes the Boolean AND of its arguments, returning
+// the first false argument it encounters, or the last argument.
+func (*Namespace) And(arg0 reflect.Value, args ...reflect.Value) reflect.Value {
+ if !truth(arg0) {
+ return arg0
+ }
+ for i := range args {
+ arg0 = args[i]
+ if !truth(arg0) {
+ break
+ }
+ }
+ return arg0
+}
+
+// Or computes the Boolean OR of its arguments, returning
+// the first true argument it encounters, or the last argument.
+func (*Namespace) Or(arg0 reflect.Value, args ...reflect.Value) reflect.Value {
+ if truth(arg0) {
+ return arg0
+ }
+ for i := range args {
+ arg0 = args[i]
+ if truth(arg0) {
+ break
+ }
+ }
+ return arg0
+}
+
+// Not returns the Boolean negation of its argument.
+func (*Namespace) Not(arg reflect.Value) bool {
+ return !truth(arg)
+}
diff --git a/tpl/compare/truth_test.go b/tpl/compare/truth_test.go
new file mode 100644
index 000000000..04d897212
--- /dev/null
+++ b/tpl/compare/truth_test.go
@@ -0,0 +1,60 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/gohugoio/hugo/common/hreflect"
+ "github.com/stretchr/testify/require"
+)
+
+func TestTruth(t *testing.T) {
+ n := New()
+
+ truthv, falsev := reflect.ValueOf(time.Now()), reflect.ValueOf(false)
+
+ assertTruth := func(t *testing.T, v reflect.Value, expected bool) {
+ if hreflect.IsTruthfulValue(v) != expected {
+ t.Fatal("truth mismatch")
+ }
+ }
+
+ t.Run("And", func(t *testing.T) {
+ assertTruth(t, n.And(truthv, truthv), true)
+ assertTruth(t, n.And(truthv, falsev), false)
+
+ })
+
+ t.Run("Or", func(t *testing.T) {
+ assertTruth(t, n.Or(truthv, truthv), true)
+ assertTruth(t, n.Or(falsev, truthv, falsev), true)
+ assertTruth(t, n.Or(falsev, falsev), false)
+ })
+
+ t.Run("Not", func(t *testing.T) {
+ assert := require.New(t)
+ assert.True(n.Not(falsev))
+ assert.False(n.Not(truthv))
+ })
+
+ t.Run("getIf", func(t *testing.T) {
+ assert := require.New(t)
+ assertTruth(t, n.getIf(reflect.ValueOf(nil)), false)
+ s := reflect.ValueOf("Hugo")
+ assert.Equal(s, n.getIf(s))
+ })
+}
diff --git a/tpl/crypto/crypto.go b/tpl/crypto/crypto.go
new file mode 100644
index 000000000..5771c98b5
--- /dev/null
+++ b/tpl/crypto/crypto.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 crypto provides template functions for cryptographic operations.
+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/data.go b/tpl/data/data.go
new file mode 100644
index 000000000..15f039294
--- /dev/null
+++ b/tpl/data/data.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 data provides template functions for working with external data
+// sources.
+package data
+
+import (
+ "bytes"
+ "encoding/csv"
+ "encoding/json"
+ "errors"
+ "net/http"
+ "strings"
+
+ "github.com/gohugoio/hugo/cache/filecache"
+ "github.com/gohugoio/hugo/deps"
+ _errors "github.com/pkg/errors"
+)
+
+// New returns a new instance of the data-namespaced template functions.
+func New(deps *deps.Deps) *Namespace {
+
+ return &Namespace{
+ deps: deps,
+ cacheGetCSV: deps.FileCaches.GetCSVCache(),
+ cacheGetJSON: deps.FileCaches.GetJSONCache(),
+ client: http.DefaultClient,
+ }
+}
+
+// Namespace provides template functions for the "data" namespace.
+type Namespace struct {
+ deps *deps.Deps
+
+ cacheGetJSON *filecache.Cache
+ cacheGetCSV *filecache.Cache
+
+ 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, "")
+ cache := ns.cacheGetCSV
+
+ unmarshal := func(b []byte) (bool, error) {
+ if !bytes.Contains(b, []byte(sep)) {
+ return false, _errors.Errorf("cannot find separator %s in CSV for %s", sep, url)
+ }
+
+ if d, err = parseCSV(b, sep); err != nil {
+ err = _errors.Wrapf(err, "failed to parse CSV file %s", url)
+
+ return true, err
+ }
+
+ return false, nil
+ }
+
+ var req *http.Request
+ req, err = http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, _errors.Wrapf(err, "failed to create request for getCSV for resource %s", url)
+ }
+
+ req.Header.Add("Accept", "text/csv")
+ req.Header.Add("Accept", "text/plain")
+
+ err = ns.getResource(cache, unmarshal, req)
+ if err != nil {
+ ns.deps.Log.ERROR.Printf("Failed to get CSV resource %q: %s", url, err)
+ return nil, nil
+ }
+
+ 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) (interface{}, error) {
+ var v interface{}
+ url := strings.Join(urlParts, "")
+ cache := ns.cacheGetJSON
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, _errors.Wrapf(err, "Failed to create request for getJSON resource %s", url)
+ }
+
+ unmarshal := func(b []byte) (bool, error) {
+ err := json.Unmarshal(b, &v)
+ if err != nil {
+ return true, err
+ }
+ return false, nil
+ }
+
+ req.Header.Add("Accept", "application/json")
+
+ err = ns.getResource(cache, unmarshal, req)
+ if err != nil {
+ ns.deps.Log.ERROR.Printf("Failed to get JSON resource %q: %s", url, err)
+ return nil, nil
+ }
+
+ return v, nil
+}
+
+// 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..9e7c0d0a6
--- /dev/null
+++ b/tpl/data/data_test.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 data
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestGetCSV(t *testing.T) {
+ t.Parallel()
+
+ 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)
+
+ ns := newTestNs()
+
+ // 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 {
+ require.Equal(t, 1, int(ns.deps.Log.ErrorCounter.Count()))
+ //require.Error(t, err, msg)
+ require.Nil(t, got)
+ continue
+ }
+
+ require.NoError(t, err, msg)
+ require.Equal(t, 0, int(ns.deps.Log.ErrorCounter.Count()))
+ require.NotNil(t, got, msg)
+
+ assert.EqualValues(t, test.expect, got, msg)
+ }
+}
+
+func TestGetJSON(t *testing.T) {
+ t.Parallel()
+
+ 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)
+ ns := newTestNs()
+
+ // 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, _ := ns.GetJSON(test.url)
+
+ if _, ok := test.expect.(bool); ok {
+ require.Equal(t, 1, int(ns.deps.Log.ErrorCounter.Count()))
+ //require.Error(t, err, msg)
+ continue
+ }
+
+ require.Equal(t, 0, int(ns.deps.Log.ErrorCounter.Count()), 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..c4751e892
--- /dev/null
+++ b/tpl/data/init_test.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 data
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/tpl/internal"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/require"
+)
+
+func TestInit(t *testing.T) {
+ var found bool
+ var ns *internal.TemplateFuncsNamespace
+
+ v := viper.New()
+ v.Set("contentDir", "content")
+
+ for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
+ ns = nsf(newDeps(v))
+ 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..7de440ca6
--- /dev/null
+++ b/tpl/data/resources.go
@@ -0,0 +1,123 @@
+// 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 (
+ "io/ioutil"
+ "net/http"
+ "path/filepath"
+ "time"
+
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/cache/filecache"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/spf13/afero"
+)
+
+var (
+ 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
+)
+
+// getRemote loads the content of a remote file. This method is thread safe.
+func (ns *Namespace) getRemote(cache *filecache.Cache, unmarshal func([]byte) (bool, error), req *http.Request) error {
+ url := req.URL.String()
+ id := helpers.MD5String(url)
+ var handled bool
+ var retry bool
+
+ _, b, err := cache.GetOrCreateBytes(id, func() ([]byte, error) {
+ var err error
+ handled = true
+ for i := 0; i <= resRetries; i++ {
+ ns.deps.Log.INFO.Printf("Downloading: %s ...", url)
+ var res *http.Response
+ res, err = ns.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ if isHTTPError(res) {
+ return nil, errors.Errorf("Failed to retrieve remote file: %s", http.StatusText(res.StatusCode))
+ }
+
+ var b []byte
+ b, err = ioutil.ReadAll(res.Body)
+
+ if err != nil {
+ return nil, err
+ }
+ res.Body.Close()
+
+ retry, err = unmarshal(b)
+
+ if err == nil {
+ // Return it so it can be cached.
+ return b, nil
+ }
+
+ if !retry {
+ return nil, err
+ }
+
+ ns.deps.Log.INFO.Printf("Cannot read remote resource %s: %s", url, err)
+ ns.deps.Log.INFO.Printf("Retry #%d for %s and sleeping for %s", i+1, url, resSleep)
+ time.Sleep(resSleep)
+ }
+
+ return nil, err
+
+ })
+
+ if !handled {
+ // This is cached content and should be correct.
+ _, err = unmarshal(b)
+ }
+
+ return err
+}
+
+// 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 and returns its content and the
+// cache ID used, if relevant.
+func (ns *Namespace) getResource(cache *filecache.Cache, unmarshal func(b []byte) (bool, error), req *http.Request) error {
+ switch req.URL.Scheme {
+ case "":
+ b, err := getLocal(req.URL.String(), ns.deps.Fs.Source, ns.deps.Cfg)
+ if err != nil {
+ return err
+ }
+ _, err = unmarshal(b)
+ return err
+ default:
+ return ns.getRemote(cache, unmarshal, req)
+ }
+}
+
+func isHTTPError(res *http.Response) bool {
+ return res.StatusCode < 200 || res.StatusCode > 299
+}
diff --git a/tpl/data/resources_test.go b/tpl/data/resources_test.go
new file mode 100644
index 000000000..a42232f94
--- /dev/null
+++ b/tpl/data/resources_test.go
@@ -0,0 +1,223 @@
+// 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/helpers"
+
+ "github.com/gohugoio/hugo/cache/filecache"
+ "github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/langs"
+ "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)
+ cache := filecache.NewCache(fs, 100)
+
+ tests := []struct {
+ path string
+ content []byte
+ }{
+ {"http://Foo.Bar/foo_Bar-Foo", []byte(`T€st Content 123`)},
+ {"http://Doppel.Gänger/foo_Bar-Foo", []byte(`T€st Cont€nt 123`)},
+ {"http://Doppel.Gänger/Fizz_Bazz-Foo", []byte(`T€st Банковский кассир Cont€nt 123`)},
+ {"http://Doppel.Gänger/Fizz_Bazz-Bar", []byte(`T€st Банковский кассир Cont€nt 456`)},
+ }
+
+ 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() }()
+
+ ns := newTestNs()
+ ns.client = cl
+
+ var c []byte
+ f := func(b []byte) (bool, error) {
+ c = b
+ return false, nil
+ }
+
+ err = ns.getRemote(cache, f, req)
+ require.NoError(t, err, msg)
+ assert.Equal(t, string(test.content), string(c))
+
+ assert.Equal(t, string(test.content), string(c))
+
+ }
+}
+
+func TestScpGetRemoteParallel(t *testing.T) {
+ t.Parallel()
+
+ 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} {
+ cfg := viper.New()
+ cfg.Set("ignoreCache", ignoreCache)
+ cfg.Set("contentDir", "content")
+
+ ns := New(newDeps(cfg))
+ ns.client = cl
+
+ var wg sync.WaitGroup
+
+ for i := 0; i < 1; i++ {
+ wg.Add(1)
+ go func(gor int) {
+ defer wg.Done()
+ for j := 0; j < 10; j++ {
+ var c []byte
+ f := func(b []byte) (bool, error) {
+ c = b
+ return false, nil
+ }
+ err := ns.getRemote(ns.cacheGetJSON, f, req)
+
+ assert.NoError(t, err)
+ if string(content) != string(c) {
+ t.Errorf("expected\n%q\ngot\n%q", content, c)
+ }
+
+ time.Sleep(23 * time.Millisecond)
+ }
+ }(i)
+ }
+
+ wg.Wait()
+ }
+}
+
+func newDeps(cfg config.Provider) *deps.Deps {
+ cfg.Set("resourceDir", "resources")
+ cfg.Set("dataDir", "resources")
+ cfg.Set("i18nDir", "i18n")
+ cfg.Set("assetDir", "assets")
+ cfg.Set("layoutDir", "layouts")
+ cfg.Set("archetypeDir", "archetypes")
+
+ l := langs.NewLanguage("en", cfg)
+ l.Set("i18nDir", "i18n")
+ cs, err := helpers.NewContentSpec(l)
+ if err != nil {
+ panic(err)
+ }
+
+ fs := hugofs.NewMem(l)
+ logger := loggers.NewErrorLogger()
+
+ p, err := helpers.NewPathSpec(fs, cfg)
+ if err != nil {
+ panic(err)
+ }
+
+ fileCaches, err := filecache.NewCaches(p)
+ if err != nil {
+ panic(err)
+ }
+
+ return &deps.Deps{
+ Cfg: cfg,
+ Fs: fs,
+ FileCaches: fileCaches,
+ ContentSpec: cs,
+ Log: logger,
+ DistinctErrorLog: helpers.NewDistinctLogger(logger.ERROR),
+ }
+}
+
+func newTestNs() *Namespace {
+ v := viper.New()
+ v.Set("contentDir", "content")
+ return New(newDeps(v))
+}
diff --git a/tpl/encoding/encoding.go b/tpl/encoding/encoding.go
new file mode 100644
index 000000000..9045acd1c
--- /dev/null
+++ b/tpl/encoding/encoding.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 encoding provides template functions for encoding content.
+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..8b24648cb
--- /dev/null
+++ b/tpl/fmt/fmt.go
@@ -0,0 +1,55 @@
+// 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 provides template functions for formatting strings.
+package fmt
+
+import (
+ _fmt "fmt"
+
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/helpers"
+)
+
+// New returns a new instance of the fmt-namespaced template functions.
+func New(d *deps.Deps) *Namespace {
+ return &Namespace{helpers.NewDistinctLogger(d.Log.ERROR)}
+}
+
+// Namespace provides template functions for the "fmt" namespace.
+type Namespace struct {
+ errorLogger *helpers.DistinctLogger
+}
+
+// Print returns string representation of the passed arguments.
+func (ns *Namespace) Print(a ...interface{}) string {
+ return _fmt.Sprint(a...)
+}
+
+// Printf returns a formatted string representation of the passed arguments.
+func (ns *Namespace) Printf(format string, a ...interface{}) string {
+ return _fmt.Sprintf(format, a...)
+
+}
+
+// Println returns string representation of the passed arguments ending with a newline.
+func (ns *Namespace) Println(a ...interface{}) string {
+ return _fmt.Sprintln(a...)
+}
+
+// Errorf formats according to a format specifier and returns the string as a
+// value that satisfies error.
+func (ns *Namespace) Errorf(format string, a ...interface{}) string {
+ ns.errorLogger.Printf(format, a...)
+ return _fmt.Sprintf(format, a...)
+}
diff --git a/tpl/fmt/init.go b/tpl/fmt/init.go
new file mode 100644
index 000000000..117055801
--- /dev/null
+++ b/tpl/fmt/init.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 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(d)
+
+ 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!`},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.Errorf,
+ []string{"errorf"},
+ [][2]string{
+ {`{{ errorf "%s." "failed" }}`, `failed.`},
+ },
+ )
+
+ 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..b693ffa2b
--- /dev/null
+++ b/tpl/fmt/init_test.go
@@ -0,0 +1,39 @@
+// 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/common/loggers"
+ "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{Log: loggers.NewErrorLogger()})
+ if ns.Name == name {
+ found = true
+ break
+ }
+ }
+
+ require.True(t, found)
+ require.IsType(t, &Namespace{}, ns.Context())
+}
diff --git a/tpl/hugo/init.go b/tpl/hugo/init.go
new file mode 100644
index 000000000..1556b759c
--- /dev/null
+++ b/tpl/hugo/init.go
@@ -0,0 +1,41 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package hugo provides template functions for accessing the Site Hugo object.
+package hugo
+
+import (
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/tpl/internal"
+)
+
+const name = "hugo"
+
+func init() {
+ f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
+
+ h := d.Site.Hugo()
+
+ ns := &internal.TemplateFuncsNamespace{
+ Name: name,
+ Context: func(args ...interface{}) interface{} { return h },
+ }
+
+ // We just add the Hugo struct as the namespace here. No method mappings.
+
+ return ns
+
+ }
+
+ internal.AddTemplateFuncsNamespace(f)
+}
diff --git a/tpl/hugo/init_test.go b/tpl/hugo/init_test.go
new file mode 100644
index 000000000..128f6fc19
--- /dev/null
+++ b/tpl/hugo/init_test.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 hugo
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/htesting"
+
+ "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
+ s := htesting.NewTestHugoSite()
+
+ for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
+ ns = nsf(&deps.Deps{Site: s})
+ if ns.Name == name {
+ found = true
+ break
+ }
+ }
+
+ require.True(t, found)
+ require.IsType(t, s.Hugo(), ns.Context())
+}
diff --git a/tpl/images/images.go b/tpl/images/images.go
new file mode 100644
index 000000000..4cb809df7
--- /dev/null
+++ b/tpl/images/images.go
@@ -0,0 +1,87 @@
+// 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 provides template functions for manipulating images.
+package images
+
+import (
+ "errors"
+ "image"
+ "sync"
+
+ // Importing image codecs for image.DecodeConfig
+ _ "image/gif"
+ _ "image/jpeg"
+ _ "image/png"
+
+ // Import webp codec
+ _ "golang.org/x/image/webp"
+
+ "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
+ }
+ defer f.Close()
+
+ 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..187f360d6
--- /dev/null
+++ b/tpl/inflect/inflect.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 inflect provides template functions for the inflection of words.
+package inflect
+
+import (
+ "strconv"
+
+ _inflect "github.com/markbates/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..a94a20218
--- /dev/null
+++ b/tpl/inflect/inflect_test.go
@@ -0,0 +1,49 @@
+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, "óbito", "Óbito"},
+ {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..ec7fbeb1a
--- /dev/null
+++ b/tpl/internal/templatefuncRegistry_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 internal
+
+import (
+ "runtime"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+type Test struct {
+}
+
+func (t *Test) MyTestMethod() string {
+ return "abcde"
+}
+
+func TestMethodToName(t *testing.T) {
+ test := &Test{}
+
+ if runtime.Compiler == "gccgo" {
+ require.Contains(t, methodToName(test.MyTestMethod), "thunk")
+ } else {
+ 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..99877dcca
--- /dev/null
+++ b/tpl/internal/templatefuncsRegistry.go
@@ -0,0 +1,285 @@
+// 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"
+)
+
+// TemplateFuncsNamespaceRegistry describes a registry of functions that provide
+// namespaces.
+var TemplateFuncsNamespaceRegistry []func(d *deps.Deps) *TemplateFuncsNamespace
+
+// AddTemplateFuncsNamespace adds a given function to a registry.
+func AddTemplateFuncsNamespace(ns func(d *deps.Deps) *TemplateFuncsNamespace) {
+ TemplateFuncsNamespaceRegistry = append(TemplateFuncsNamespaceRegistry, ns)
+}
+
+// TemplateFuncsNamespace represents a template function namespace.
+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
+}
+
+// TemplateFuncsNamespaces is a slice of TemplateFuncsNamespace.
+type TemplateFuncsNamespaces []*TemplateFuncsNamespace
+
+// AddMethodMapping adds a method to a template function namespace.
+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,
+ }
+
+}
+
+// TemplateFuncMethodMapping represents a mapping of functions to methods for a
+// given namespace.
+type TemplateFuncMethodMapping struct {
+ Method interface{}
+
+ // Any template funcs aliases. This is mainly motivated by keeping
+ // backwards compatibility, 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
+}
+
+// MarshalJSON returns the JSON encoding of namespaces.
+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..9a9f467bb
--- /dev/null
+++ b/tpl/lang/lang.go
@@ -0,0 +1,163 @@
+// 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 provides template functions for content internationalization.
+package lang
+
+import (
+ "errors"
+ "fmt"
+ "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 returns a translated string for id.
+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 {
+ // defaults
+ neg, dec, grp = "-", ".", ","
+ } else {
+ delim := " "
+
+ if len(options) == 2 {
+ // custom delimiter
+ s, err := cast.ToStringE(options[1])
+ if err != nil {
+ return "", nil
+ }
+
+ delim = s
+ }
+
+ s, err := cast.ToStringE(options[0])
+ if err != nil {
+ return "", nil
+ }
+
+ rs := strings.Split(s, delim)
+ 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
+}
+
+type pagesLanguageMerger interface {
+ MergeByLanguageInterface(other interface{}) (interface{}, error)
+}
+
+// Merge creates a union of pages from two languages.
+func (ns *Namespace) Merge(p2, p1 interface{}) (interface{}, error) {
+ merger, ok := p1.(pagesLanguageMerger)
+ if !ok {
+ return nil, fmt.Errorf("language merge not supported for %T", p1)
+ }
+ return merger.MergeByLanguageInterface(p2)
+}
diff --git a/tpl/lang/lang_test.go b/tpl/lang/lang_test.go
new file mode 100644
index 000000000..aee567502
--- /dev/null
+++ b/tpl/lang/lang_test.go
@@ -0,0 +1,63 @@
+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
+ delim 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"},
+
+ {3, -12345.6789, "-|,| ", "|", "-12 345,679"},
+ {6, -12345.6789, "-|,| ", "|", "-12 345,678900"},
+
+ // Arabic, ar_AE
+ {6, -12345.6789, "‏- ٫ ٬", "", "‏-12٬345٫678900"},
+ {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 {
+ if c.delim == "" {
+ s, err = ns.NumFmt(c.prec, c.n, c.runes)
+ } else {
+ s, err = ns.NumFmt(c.prec, c.n, c.runes, c.delim)
+ }
+ }
+
+ 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..bbffb23aa
--- /dev/null
+++ b/tpl/math/init.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 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.Ceil,
+ nil,
+ [][2]string{
+ {"{{math.Ceil 2.1}}", "3"},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.Div,
+ []string{"div"},
+ [][2]string{
+ {"{{div 6 3}}", "2"},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.Floor,
+ nil,
+ [][2]string{
+ {"{{math.Floor 1.9}}", "1"},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.Log,
+ nil,
+ [][2]string{
+ {"{{math.Log 1}}", "0"},
+ },
+ )
+
+ 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.Round,
+ nil,
+ [][2]string{
+ {"{{math.Round 1.5}}", "2"},
+ },
+ )
+
+ 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..08be42b47
--- /dev/null
+++ b/tpl/math/math.go
@@ -0,0 +1,119 @@
+// 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 provides template functions for mathmatical operations.
+package math
+
+import (
+ "errors"
+ "math"
+
+ _math "github.com/gohugoio/hugo/common/math"
+
+ "github.com/spf13/cast"
+)
+
+// 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{}
+
+// Add adds two numbers.
+func (ns *Namespace) Add(a, b interface{}) (interface{}, error) {
+ return _math.DoArithmetic(a, b, '+')
+}
+
+// Ceil returns the least integer value greater than or equal to x.
+func (ns *Namespace) Ceil(x interface{}) (float64, error) {
+ xf, err := cast.ToFloat64E(x)
+ if err != nil {
+ return 0, errors.New("Ceil operator can't be used with non-float value")
+ }
+
+ return math.Ceil(xf), nil
+}
+
+// Div divides two numbers.
+func (ns *Namespace) Div(a, b interface{}) (interface{}, error) {
+ return _math.DoArithmetic(a, b, '/')
+}
+
+// Floor returns the greatest integer value less than or equal to x.
+func (ns *Namespace) Floor(x interface{}) (float64, error) {
+ xf, err := cast.ToFloat64E(x)
+ if err != nil {
+ return 0, errors.New("Floor operator can't be used with non-float value")
+ }
+
+ return math.Floor(xf), nil
+}
+
+// Log returns the natural logarithm of a number.
+func (ns *Namespace) Log(a interface{}) (float64, error) {
+ af, err := cast.ToFloat64E(a)
+
+ if err != nil {
+ return 0, errors.New("Log operator can't be used with non integer or float value")
+ }
+
+ return math.Log(af), nil
+}
+
+// Mod returns a % b.
+func (ns *Namespace) Mod(a, b interface{}) (int64, error) {
+ ai, erra := cast.ToInt64E(a)
+ bi, errb := cast.ToInt64E(b)
+
+ if erra != nil || errb != nil {
+ 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
+}
+
+// Mul multiplies two numbers.
+func (ns *Namespace) Mul(a, b interface{}) (interface{}, error) {
+ return _math.DoArithmetic(a, b, '*')
+}
+
+// Round returns the nearest integer, rounding half away from zero.
+func (ns *Namespace) Round(x interface{}) (float64, error) {
+ xf, err := cast.ToFloat64E(x)
+ if err != nil {
+ return 0, errors.New("Round operator can't be used with non-float value")
+ }
+
+ return _round(xf), nil
+}
+
+// Sub subtracts two numbers.
+func (ns *Namespace) Sub(a, b interface{}) (interface{}, error) {
+ return _math.DoArithmetic(a, b, '-')
+}
diff --git a/tpl/math/math_test.go b/tpl/math/math_test.go
new file mode 100644
index 000000000..f2e6236af
--- /dev/null
+++ b/tpl/math/math_test.go
@@ -0,0 +1,278 @@
+// 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"
+ "math"
+ "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 TestCeil(t *testing.T) {
+ t.Parallel()
+
+ ns := New()
+
+ for i, test := range []struct {
+ x interface{}
+ expect interface{}
+ }{
+ {0.1, 1.0},
+ {0.5, 1.0},
+ {1.1, 2.0},
+ {1.5, 2.0},
+ {-0.1, 0.0},
+ {-0.5, 0.0},
+ {-1.1, -1.0},
+ {-1.5, -1.0},
+ {"abc", false},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test)
+
+ result, err := ns.Ceil(test.x)
+
+ 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 TestFloor(t *testing.T) {
+ t.Parallel()
+
+ ns := New()
+
+ for i, test := range []struct {
+ x interface{}
+ expect interface{}
+ }{
+ {0.1, 0.0},
+ {0.5, 0.0},
+ {1.1, 1.0},
+ {1.5, 1.0},
+ {-0.1, -1.0},
+ {-0.5, -1.0},
+ {-1.1, -2.0},
+ {-1.5, -2.0},
+ {"abc", false},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test)
+
+ result, err := ns.Floor(test.x)
+
+ 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 TestLog(t *testing.T) {
+ t.Parallel()
+
+ ns := New()
+
+ for i, test := range []struct {
+ a interface{}
+ expect interface{}
+ }{
+ {1, float64(0)},
+ {3, float64(1.0986)},
+ {0, float64(math.Inf(-1))},
+ {1.0, float64(0)},
+ {3.1, float64(1.1314)},
+ {"abc", false},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test)
+
+ result, err := ns.Log(test.a)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ require.Error(t, err, errMsg)
+ continue
+ }
+
+ // we compare only 4 digits behind point if its a real float
+ // otherwise we usually get different float values on the last positions
+ if result != math.Inf(-1) {
+ result = float64(int(result*10000)) / 10000
+ }
+
+ 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, int64(1)},
+ {3, 2.1, int64(1)},
+ {3.1, 2.1, int64(1)},
+ {int8(3), int8(2), int64(1)},
+ {int16(3), int16(2), int64(1)},
+ {int32(3), int32(2), int64(1)},
+ {int64(3), int64(2), int64(1)},
+ {"3", "2", int64(1)},
+ {"3.1", "2", false},
+ {"aaa", "0", false},
+ {"3", "aaa", false},
+ } {
+ 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, false},
+ {3, 2.1, false},
+ {3.1, 2.1, false},
+ {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},
+ {"3", "3", true},
+ {"3", "2", false},
+ {"3.1", "2", nil},
+ {"aaa", "0", nil},
+ {"3", "aaa", nil},
+ } {
+ 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)
+ }
+}
+
+func TestRound(t *testing.T) {
+ t.Parallel()
+
+ ns := New()
+
+ for i, test := range []struct {
+ x interface{}
+ expect interface{}
+ }{
+ {0.1, 0.0},
+ {0.5, 1.0},
+ {1.1, 1.0},
+ {1.5, 2.0},
+ {-0.1, -0.0},
+ {-0.5, -1.0},
+ {-1.1, -1.0},
+ {-1.5, -2.0},
+ {"abc", false},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test)
+
+ result, err := ns.Round(test.x)
+
+ 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/math/round.go b/tpl/math/round.go
new file mode 100644
index 000000000..9b33120af
--- /dev/null
+++ b/tpl/math/round.go
@@ -0,0 +1,61 @@
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// According to https://github.com/golang/go/issues/20100, the Go stdlib will
+// include math.Round beginning with Go 1.10.
+//
+// The following implementation was taken from https://golang.org/cl/43652.
+
+package math
+
+import "math"
+
+const (
+ mask = 0x7FF
+ shift = 64 - 11 - 1
+ bias = 1023
+)
+
+// Round returns the nearest integer, rounding half away from zero.
+//
+// Special cases are:
+// Round(±0) = ±0
+// Round(±Inf) = ±Inf
+// Round(NaN) = NaN
+func _round(x float64) float64 {
+ // Round is a faster implementation of:
+ //
+ // func Round(x float64) float64 {
+ // t := Trunc(x)
+ // if Abs(x-t) >= 0.5 {
+ // return t + Copysign(1, x)
+ // }
+ // return t
+ // }
+ const (
+ signMask = 1 << 63
+ fracMask = 1<<shift - 1
+ half = 1 << (shift - 1)
+ one = bias << shift
+ )
+
+ bits := math.Float64bits(x)
+ e := uint(bits>>shift) & mask
+ if e < bias {
+ // Round abs(x) < 1 including denormals.
+ bits &= signMask // +-0
+ if e == bias-1 {
+ bits |= one // +-1
+ }
+ } else if e < bias+shift {
+ // Round any abs(x) >= 1 containing a fractional component [0,1).
+ //
+ // Numbers with larger exponents are returned unchanged since they
+ // must be either an integer, infinity, or NaN.
+ e -= bias
+ bits += half >> e
+ bits &^= fracMask >> e
+ }
+ return math.Float64frombits(bits)
+}
diff --git a/tpl/os/init.go b/tpl/os/init.go
new file mode 100644
index 000000000..3ef8702d6
--- /dev/null
+++ b/tpl/os/init.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 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 "files") }}{{ .Name }}{{ end }}`, "README.txt"},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.ReadFile,
+ []string{"readFile"},
+ [][2]string{
+ {`{{ readFile "files/README.txt" }}`, `Hugo Rocks!`},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.FileExists,
+ []string{"fileExists"},
+ [][2]string{
+ {`{{ fileExists "foo.txt" }}`, `false`},
+ },
+ )
+
+ 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..2dab5c490
--- /dev/null
+++ b/tpl/os/os.go
@@ -0,0 +1,153 @@
+// 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 provides template functions for interacting with the operating
+// system.
+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 {
+
+ // Since Hugo 0.38 we can have multiple content dirs. This can make it hard to
+ // reason about where the file is placed relative to the project root.
+ // To make the {{ readFile .Filename }} variant just work, we create a composite
+ // filesystem that first checks the work dir fs and then the content fs.
+ var rfs afero.Fs
+ if deps.Fs != nil {
+ rfs = deps.Fs.WorkingDir
+ if deps.PathSpec != nil && deps.PathSpec.BaseFs != nil {
+ rfs = afero.NewReadOnlyFs(afero.NewCopyOnWriteFs(deps.PathSpec.BaseFs.Content.Fs, deps.Fs.WorkingDir))
+ }
+ }
+
+ return &Namespace{
+ readFileFs: rfs,
+ deps: deps,
+ }
+}
+
+// Namespace provides template functions for the "os" namespace.
+type Namespace struct {
+ readFileFs afero.Fs
+ 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 in the given filesystem
+// and returns the contents as a string.
+// There is a upper size limit set at 1 megabytes.
+func readFile(fs afero.Fs, 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
+}
+
+// ReadFile reads the file named by filename relative to the configured WorkingDir.
+// It returns the contents as a string.
+// There is an 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.readFileFs, 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 %q: %s", path, err)
+ }
+
+ return list, nil
+}
+
+// FileExists checks whether a file exists under the given path.
+func (ns *Namespace) FileExists(i interface{}) (bool, error) {
+ path, err := cast.ToStringE(i)
+ if err != nil {
+ return false, err
+ }
+
+ if path == "" {
+ return false, errors.New("fileExists needs a path to a file")
+ }
+
+ status, err := afero.Exists(ns.readFileFs, path)
+ if err != nil {
+ return false, err
+ }
+
+ return status, nil
+}
+
+// Stat returns the os.FileInfo structure describing file.
+func (ns *Namespace) Stat(i interface{}) (_os.FileInfo, error) {
+ path, err := cast.ToStringE(i)
+ if err != nil {
+ return nil, err
+ }
+
+ if path == "" {
+ return nil, errors.New("fileStat needs a path to a file")
+ }
+
+ r, err := ns.readFileFs.Stat(path)
+ if err != nil {
+ return nil, err
+ }
+
+ return r, nil
+}
diff --git a/tpl/os/os_test.go b/tpl/os/os_test.go
new file mode 100644
index 000000000..46dafd842
--- /dev/null
+++ b/tpl/os/os_test.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 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)
+ }
+}
+
+func TestFileExists(t *testing.T) {
+ t.Parallel()
+
+ workingDir := "/home/hugo"
+
+ v := viper.New()
+ v.Set("workingDir", workingDir)
+
+ 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"), true},
+ {filepath.FromSlash("f/f1.txt"), true},
+ {filepath.FromSlash("../f2.txt"), false},
+ {"b", false},
+ {"", nil},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test)
+ result, err := ns.FileExists(test.filename)
+
+ if test.expect == nil {
+ require.Error(t, err, errMsg)
+ continue
+ }
+
+ require.NoError(t, err, errMsg)
+ assert.Equal(t, test.expect, result, errMsg)
+ }
+}
+
+func TestStat(t *testing.T) {
+ t.Parallel()
+
+ workingDir := "/home/hugo"
+
+ v := viper.New()
+ v.Set("workingDir", workingDir)
+
+ ns := New(&deps.Deps{Fs: hugofs.NewMem(v)})
+
+ afero.WriteFile(ns.deps.Fs.Source, filepath.Join(workingDir, "/f/f1.txt"), []byte("f1-content"), 0755)
+
+ for i, test := range []struct {
+ filename string
+ expect interface{}
+ }{
+ {filepath.FromSlash("/f/f1.txt"), int64(10)},
+ {filepath.FromSlash("f/f1.txt"), int64(10)},
+ {"b", nil},
+ {"", nil},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test)
+ result, err := ns.Stat(test.filename)
+
+ if test.expect == nil {
+ require.Error(t, err, errMsg)
+ continue
+ }
+
+ require.NoError(t, err, errMsg)
+ assert.Equal(t, test.expect, result.Size(), errMsg)
+ }
+}
diff --git a/tpl/partials/init.go b/tpl/partials/init.go
new file mode 100644
index 000000000..c2135bca5
--- /dev/null
+++ b/tpl/partials/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 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>`},
+ },
+ )
+
+ // TODO(bep) we need the return to be a valid identifier, but
+ // should consider another way of adding it.
+ ns.AddMethodMapping(func() string { return "" },
+ []string{"return"},
+ [][2]string{},
+ )
+
+ ns.AddMethodMapping(ctx.IncludeCached,
+ []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..0513f1572
--- /dev/null
+++ b/tpl/partials/init_test.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 partials
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/common/loggers"
+ "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{
+ BuildStartListeners: &deps.Listeners{},
+ Log: loggers.NewErrorLogger(),
+ })
+ 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..2599a5d01
--- /dev/null
+++ b/tpl/partials/partials.go
@@ -0,0 +1,192 @@
+// 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 provides template functions for working with reusable
+// templates.
+package partials
+
+import (
+ "fmt"
+ "html/template"
+ "io"
+ "io/ioutil"
+ "strings"
+ "sync"
+ texttemplate "text/template"
+
+ "github.com/gohugoio/hugo/tpl"
+
+ bp "github.com/gohugoio/hugo/bufferpool"
+ "github.com/gohugoio/hugo/deps"
+)
+
+// TestTemplateProvider is global deps.ResourceProvider.
+// NOTE: It's currently unused.
+var TestTemplateProvider deps.ResourceProvider
+
+// partialCache represents a cache of partials protected by a mutex.
+type partialCache struct {
+ sync.RWMutex
+ p map[string]interface{}
+}
+
+func (p *partialCache) clear() {
+ p.Lock()
+ defer p.Unlock()
+ p.p = make(map[string]interface{})
+}
+
+// New returns a new instance of the templates-namespaced template functions.
+func New(deps *deps.Deps) *Namespace {
+ cache := &partialCache{p: make(map[string]interface{})}
+ deps.BuildStartListeners.Add(
+ func() {
+ cache.clear()
+ })
+
+ return &Namespace{
+ deps: deps,
+ cachedPartials: cache,
+ }
+}
+
+// Namespace provides template functions for the "templates" namespace.
+type Namespace struct {
+ deps *deps.Deps
+ cachedPartials *partialCache
+}
+
+// contextWrapper makes room for a return value in a partial invocation.
+type contextWrapper struct {
+ Arg interface{}
+ Result interface{}
+}
+
+// Set sets the return value and returns an empty string.
+func (c *contextWrapper) Set(in interface{}) string {
+ c.Result = in
+ return ""
+}
+
+// Include executes the named partial.
+// If the partial contains a return statement, that value will be returned.
+// Else, the rendered output will be returned:
+// A string if 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(name, "partials/") {
+ name = name[8:]
+ }
+ var context interface{}
+
+ if len(contextList) == 0 {
+ context = nil
+ } else {
+ context = contextList[0]
+ }
+
+ n := "partials/" + name
+ templ, found := ns.deps.Tmpl.Lookup(n)
+
+ if !found {
+ // For legacy reasons.
+ templ, found = ns.deps.Tmpl.Lookup(n + ".html")
+ }
+
+ if !found {
+ return "", fmt.Errorf("partial %q not found", name)
+ }
+
+ var info tpl.Info
+ if ip, ok := templ.(tpl.TemplateInfoProvider); ok {
+ info = ip.TemplateInfo()
+ }
+
+ var w io.Writer
+
+ if info.HasReturn {
+ // Wrap the context sent to the template to capture the return value.
+ // Note that the template is rewritten to make sure that the dot (".")
+ // and the $ variable points to Arg.
+ context = &contextWrapper{
+ Arg: context,
+ }
+
+ // We don't care about any template output.
+ w = ioutil.Discard
+ } else {
+ b := bp.GetBuffer()
+ defer bp.PutBuffer(b)
+ w = b
+ }
+
+ if err := templ.Execute(w, context); err != nil {
+ return "", err
+ }
+
+ var result interface{}
+
+ if ctx, ok := context.(*contextWrapper); ok {
+ result = ctx.Result
+ } else if _, ok := templ.(*texttemplate.Template); ok {
+ result = w.(fmt.Stringer).String()
+ } else {
+ result = template.HTML(w.(fmt.Stringer).String())
+ }
+
+ if ns.deps.Metrics != nil {
+ ns.deps.Metrics.TrackValue(n, result)
+ }
+
+ return result, nil
+
+}
+
+// IncludeCached 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) IncludeCached(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{}) (interface{}, error) {
+
+ ns.cachedPartials.RLock()
+ p, ok := ns.cachedPartials.p[key]
+ ns.cachedPartials.RUnlock()
+
+ if ok {
+ return p, nil
+ }
+
+ p, err := ns.Include(name, context)
+ if err != nil {
+ return nil, err
+ }
+
+ ns.cachedPartials.Lock()
+ defer ns.cachedPartials.Unlock()
+ // Double-check.
+ if p2, ok := ns.cachedPartials.p[key]; ok {
+ return p2, nil
+ }
+ ns.cachedPartials.p[key] = p
+
+ return p, nil
+}
diff --git a/tpl/path/init.go b/tpl/path/init.go
new file mode 100644
index 000000000..518dcad22
--- /dev/null
+++ b/tpl/path/init.go
@@ -0,0 +1,61 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package path
+
+import (
+ "fmt"
+ "path/filepath"
+
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/tpl/internal"
+)
+
+const name = "path"
+
+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.Split,
+ nil,
+ [][2]string{
+ {`{{ "/my/path/filename.txt" | path.Split }}`, `/my/path/|filename.txt`},
+ {fmt.Sprintf(`{{ %q | path.Split }}`, filepath.FromSlash("/my/path/filename.txt")), `/my/path/|filename.txt`},
+ },
+ )
+
+ testDir := filepath.Join("my", "path")
+ testFile := filepath.Join(testDir, "filename.txt")
+
+ ns.AddMethodMapping(ctx.Join,
+ nil,
+ [][2]string{
+ {fmt.Sprintf(`{{ slice %q "filename.txt" | path.Join }}`, testDir), `my/path/filename.txt`},
+ {`{{ path.Join "my" "path" "filename.txt" }}`, `my/path/filename.txt`},
+ {fmt.Sprintf(`{{ %q | path.Ext }}`, testFile), `.txt`},
+ {fmt.Sprintf(`{{ %q | path.Base }}`, testFile), `filename.txt`},
+ {fmt.Sprintf(`{{ %q | path.Dir }}`, testFile), `my/path`},
+ },
+ )
+
+ return ns
+
+ }
+ internal.AddTemplateFuncsNamespace(f)
+}
diff --git a/tpl/path/init_test.go b/tpl/path/init_test.go
new file mode 100644
index 000000000..b0aeab358
--- /dev/null
+++ b/tpl/path/init_test.go
@@ -0,0 +1,38 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package path
+
+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/path/path.go b/tpl/path/path.go
new file mode 100644
index 000000000..641055224
--- /dev/null
+++ b/tpl/path/path.go
@@ -0,0 +1,146 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package path provides template functions for manipulating paths.
+package path
+
+import (
+ "fmt"
+ _path "path"
+ "path/filepath"
+
+ "github.com/gohugoio/hugo/deps"
+ "github.com/spf13/cast"
+)
+
+// New returns a new instance of the path-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
+}
+
+// DirFile holds the result from path.Split.
+type DirFile struct {
+ Dir string
+ File string
+}
+
+// Used in test.
+func (df DirFile) String() string {
+ return fmt.Sprintf("%s|%s", df.Dir, df.File)
+}
+
+// Ext returns the file name extension used by path.
+// The extension is the suffix beginning at the final dot
+// in the final slash-separated element of path;
+// it is empty if there is no dot.
+// The input path is passed into filepath.ToSlash converting any Windows slashes
+// to forward slashes.
+func (ns *Namespace) Ext(path interface{}) (string, error) {
+ spath, err := cast.ToStringE(path)
+ if err != nil {
+ return "", err
+ }
+ spath = filepath.ToSlash(spath)
+ return _path.Ext(spath), nil
+}
+
+// Dir returns all but the last element of path, typically the path's directory.
+// After dropping the final element using Split, the path is Cleaned and trailing
+// slashes are removed.
+// If the path is empty, Dir returns ".".
+// If the path consists entirely of slashes followed by non-slash bytes, Dir
+// returns a single slash. In any other case, the returned path does not end in a
+// slash.
+// The input path is passed into filepath.ToSlash converting any Windows slashes
+// to forward slashes.
+func (ns *Namespace) Dir(path interface{}) (string, error) {
+ spath, err := cast.ToStringE(path)
+ if err != nil {
+ return "", err
+ }
+ spath = filepath.ToSlash(spath)
+ return _path.Dir(spath), nil
+}
+
+// Base returns the last element of path.
+// Trailing slashes are removed before extracting the last element.
+// If the path is empty, Base returns ".".
+// If the path consists entirely of slashes, Base returns "/".
+// The input path is passed into filepath.ToSlash converting any Windows slashes
+// to forward slashes.
+func (ns *Namespace) Base(path interface{}) (string, error) {
+ spath, err := cast.ToStringE(path)
+ if err != nil {
+ return "", err
+ }
+ spath = filepath.ToSlash(spath)
+ return _path.Base(spath), nil
+}
+
+// Split splits path immediately following the final slash,
+// separating it into a directory and file name component.
+// If there is no slash in path, Split returns an empty dir and
+// file set to path.
+// The input path is passed into filepath.ToSlash converting any Windows slashes
+// to forward slashes.
+// The returned values have the property that path = dir+file.
+func (ns *Namespace) Split(path interface{}) (DirFile, error) {
+ spath, err := cast.ToStringE(path)
+ if err != nil {
+ return DirFile{}, err
+ }
+ spath = filepath.ToSlash(spath)
+ dir, file := _path.Split(spath)
+
+ return DirFile{Dir: dir, File: file}, nil
+}
+
+// Join joins any number of path elements into a single path, adding a
+// separating slash if necessary. All the input
+// path elements are passed into filepath.ToSlash converting any Windows slashes
+// to forward slashes.
+// The result is Cleaned; in particular,
+// all empty strings are ignored.
+func (ns *Namespace) Join(elements ...interface{}) (string, error) {
+ var pathElements []string
+ for _, elem := range elements {
+ switch v := elem.(type) {
+ case []string:
+ for _, e := range v {
+ pathElements = append(pathElements, filepath.ToSlash(e))
+ }
+ case []interface{}:
+ for _, e := range v {
+ elemStr, err := cast.ToStringE(e)
+ if err != nil {
+ return "", err
+ }
+ pathElements = append(pathElements, filepath.ToSlash(elemStr))
+ }
+ default:
+ elemStr, err := cast.ToStringE(elem)
+ if err != nil {
+ return "", err
+ }
+ pathElements = append(pathElements, filepath.ToSlash(elemStr))
+ }
+ }
+ return _path.Join(pathElements...), nil
+}
diff --git a/tpl/path/path_test.go b/tpl/path/path_test.go
new file mode 100644
index 000000000..b9a29a285
--- /dev/null
+++ b/tpl/path/path_test.go
@@ -0,0 +1,179 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package path
+
+import (
+ "fmt"
+ "path/filepath"
+ "testing"
+
+ "github.com/gohugoio/hugo/deps"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+var ns = New(&deps.Deps{Cfg: viper.New()})
+
+type tstNoStringer struct{}
+
+func TestBase(t *testing.T) {
+ t.Parallel()
+
+ for i, test := range []struct {
+ path interface{}
+ expect interface{}
+ }{
+ {filepath.FromSlash(`foo/bar.txt`), `bar.txt`},
+ {filepath.FromSlash(`foo/bar/txt `), `txt `},
+ {filepath.FromSlash(`foo/bar.t`), `bar.t`},
+ {`foo.bar.txt`, `foo.bar.txt`},
+ {`.x`, `.x`},
+ {``, `.`},
+ // errors
+ {tstNoStringer{}, false},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test)
+
+ result, err := ns.Base(test.path)
+
+ 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 TestDir(t *testing.T) {
+ t.Parallel()
+
+ for i, test := range []struct {
+ path interface{}
+ expect interface{}
+ }{
+ {filepath.FromSlash(`foo/bar.txt`), `foo`},
+ {filepath.FromSlash(`foo/bar/txt `), `foo/bar`},
+ {filepath.FromSlash(`foo/bar.t`), `foo`},
+ {`foo.bar.txt`, `.`},
+ {`.x`, `.`},
+ {``, `.`},
+ // errors
+ {tstNoStringer{}, false},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test)
+
+ result, err := ns.Dir(test.path)
+
+ 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 TestExt(t *testing.T) {
+ t.Parallel()
+
+ for i, test := range []struct {
+ path interface{}
+ expect interface{}
+ }{
+ {filepath.FromSlash(`foo/bar.json`), `.json`},
+ {`foo.bar.txt `, `.txt `},
+ {``, ``},
+ {`.x`, `.x`},
+ // errors
+ {tstNoStringer{}, false},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test)
+
+ result, err := ns.Ext(test.path)
+
+ 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 TestJoin(t *testing.T) {
+ t.Parallel()
+
+ for i, test := range []struct {
+ elements interface{}
+ expect interface{}
+ }{
+ {
+ []string{"", "baz", filepath.FromSlash(`foo/bar.txt`)},
+ `baz/foo/bar.txt`,
+ },
+ {
+ []interface{}{"", "baz", DirFile{"big", "john"}, filepath.FromSlash(`foo/bar.txt`)},
+ `baz/big|john/foo/bar.txt`,
+ },
+ {nil, ""},
+ // errors
+ {tstNoStringer{}, false},
+ {[]interface{}{"", tstNoStringer{}}, false},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test)
+
+ result, err := ns.Join(test.elements)
+
+ 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 TestSplit(t *testing.T) {
+ t.Parallel()
+
+ for i, test := range []struct {
+ path interface{}
+ expect interface{}
+ }{
+ {filepath.FromSlash(`foo/bar.txt`), DirFile{`foo/`, `bar.txt`}},
+ {filepath.FromSlash(`foo/bar/txt `), DirFile{`foo/bar/`, `txt `}},
+ {`foo.bar.txt`, DirFile{``, `foo.bar.txt`}},
+ {``, DirFile{``, ``}},
+ // errors
+ {tstNoStringer{}, false},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test)
+
+ result, err := ns.Split(test.path)
+
+ 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/reflect/init.go b/tpl/reflect/init.go
new file mode 100644
index 000000000..6ff3f8093
--- /dev/null
+++ b/tpl/reflect/init.go
@@ -0,0 +1,51 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package reflect provides template functions for run-time object reflection.
+package reflect
+
+import (
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/tpl/internal"
+)
+
+const name = "reflect"
+
+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.IsMap,
+ nil,
+ [][2]string{
+ {`{{ if reflect.IsMap (dict "a" 1) }}Map{{ end }}`, `Map`},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.IsSlice,
+ nil,
+ [][2]string{
+ {`{{ if reflect.IsSlice (slice 1 2 3) }}Slice{{ end }}`, `Slice`},
+ },
+ )
+
+ return ns
+ }
+
+ internal.AddTemplateFuncsNamespace(f)
+}
diff --git a/tpl/reflect/init_test.go b/tpl/reflect/init_test.go
new file mode 100644
index 000000000..4357200ab
--- /dev/null
+++ b/tpl/reflect/init_test.go
@@ -0,0 +1,39 @@
+// 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 reflect
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/common/loggers"
+ "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{Log: loggers.NewErrorLogger()})
+ if ns.Name == name {
+ found = true
+ break
+ }
+ }
+
+ require.True(t, found)
+ require.IsType(t, &Namespace{}, ns.Context())
+}
diff --git a/tpl/reflect/reflect.go b/tpl/reflect/reflect.go
new file mode 100644
index 000000000..17646e9a0
--- /dev/null
+++ b/tpl/reflect/reflect.go
@@ -0,0 +1,36 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package reflect
+
+import (
+ "reflect"
+)
+
+// New returns a new instance of the reflect-namespaced template functions.
+func New() *Namespace {
+ return &Namespace{}
+}
+
+// Namespace provides template functions for the "reflect" namespace.
+type Namespace struct{}
+
+// IsMap reports whether v is a map.
+func (ns *Namespace) IsMap(v interface{}) bool {
+ return reflect.ValueOf(v).Kind() == reflect.Map
+}
+
+// IsSlice reports whether v is a slice.
+func (ns *Namespace) IsSlice(v interface{}) bool {
+ return reflect.ValueOf(v).Kind() == reflect.Slice
+}
diff --git a/tpl/reflect/reflect_test.go b/tpl/reflect/reflect_test.go
new file mode 100644
index 000000000..9b2ad97a6
--- /dev/null
+++ b/tpl/reflect/reflect_test.go
@@ -0,0 +1,55 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package reflect
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var ns = New()
+
+type tstNoStringer struct{}
+
+func TestIsMap(t *testing.T) {
+ for i, test := range []struct {
+ v interface{}
+ expect interface{}
+ }{
+ {map[int]int{1: 1}, true},
+ {"foo", false},
+ {nil, false},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test)
+ result := ns.IsMap(test.v)
+ assert.Equal(t, test.expect, result, errMsg)
+ }
+}
+
+func TestIsSlice(t *testing.T) {
+ for i, test := range []struct {
+ v interface{}
+ expect interface{}
+ }{
+ {[]int{1, 2}, true},
+ {"foo", false},
+ {nil, false},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test)
+ result := ns.IsSlice(test.v)
+ assert.Equal(t, test.expect, result, errMsg)
+ }
+}
diff --git a/tpl/resources/init.go b/tpl/resources/init.go
new file mode 100644
index 000000000..3e750f325
--- /dev/null
+++ b/tpl/resources/init.go
@@ -0,0 +1,68 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resources
+
+import (
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/tpl/internal"
+)
+
+const name = "resources"
+
+func init() {
+ f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
+ ctx, err := New(d)
+ if err != nil {
+ // TODO(bep) no panic.
+ panic(err)
+ }
+
+ ns := &internal.TemplateFuncsNamespace{
+ Name: name,
+ Context: func(args ...interface{}) interface{} { return ctx },
+ }
+
+ ns.AddMethodMapping(ctx.Get,
+ nil,
+ [][2]string{},
+ )
+
+ // Add aliases for the most common transformations.
+
+ ns.AddMethodMapping(ctx.Fingerprint,
+ []string{"fingerprint"},
+ [][2]string{},
+ )
+
+ ns.AddMethodMapping(ctx.Minify,
+ []string{"minify"},
+ [][2]string{},
+ )
+
+ ns.AddMethodMapping(ctx.ToCSS,
+ []string{"toCSS"},
+ [][2]string{},
+ )
+
+ ns.AddMethodMapping(ctx.PostCSS,
+ []string{"postCSS"},
+ [][2]string{},
+ )
+
+ return ns
+
+ }
+
+ internal.AddTemplateFuncsNamespace(f)
+}
diff --git a/tpl/resources/resources.go b/tpl/resources/resources.go
new file mode 100644
index 000000000..d32e12a05
--- /dev/null
+++ b/tpl/resources/resources.go
@@ -0,0 +1,270 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package resources provides template functions for working with resources.
+package resources
+
+import (
+ "errors"
+ "fmt"
+ "path/filepath"
+
+ _errors "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/resources/resource"
+ "github.com/gohugoio/hugo/resources/resource_factories/bundler"
+ "github.com/gohugoio/hugo/resources/resource_factories/create"
+ "github.com/gohugoio/hugo/resources/resource_transformers/integrity"
+ "github.com/gohugoio/hugo/resources/resource_transformers/minifier"
+ "github.com/gohugoio/hugo/resources/resource_transformers/postcss"
+ "github.com/gohugoio/hugo/resources/resource_transformers/templates"
+ "github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss"
+ "github.com/spf13/cast"
+)
+
+// New returns a new instance of the resources-namespaced template functions.
+func New(deps *deps.Deps) (*Namespace, error) {
+ if deps.ResourceSpec == nil {
+ return &Namespace{}, nil
+ }
+
+ scssClient, err := scss.New(deps.BaseFs.Assets, deps.ResourceSpec)
+ if err != nil {
+ return nil, err
+ }
+ return &Namespace{
+ deps: deps,
+ scssClient: scssClient,
+ createClient: create.New(deps.ResourceSpec),
+ bundlerClient: bundler.New(deps.ResourceSpec),
+ integrityClient: integrity.New(deps.ResourceSpec),
+ minifyClient: minifier.New(deps.ResourceSpec),
+ postcssClient: postcss.New(deps.ResourceSpec),
+ templatesClient: templates.New(deps.ResourceSpec, deps.TextTmpl),
+ }, nil
+}
+
+// Namespace provides template functions for the "resources" namespace.
+type Namespace struct {
+ deps *deps.Deps
+
+ createClient *create.Client
+ bundlerClient *bundler.Client
+ scssClient *scss.Client
+ integrityClient *integrity.Client
+ minifyClient *minifier.Client
+ postcssClient *postcss.Client
+ templatesClient *templates.Client
+}
+
+// Get locates the filename given in Hugo's filesystems: static, assets and content (in that order)
+// and creates a Resource object that can be used for further transformations.
+func (ns *Namespace) Get(filename interface{}) (resource.Resource, error) {
+ filenamestr, err := cast.ToStringE(filename)
+ if err != nil {
+ return nil, err
+ }
+
+ filenamestr = filepath.Clean(filenamestr)
+
+ // Resource Get'ing is currently limited to /assets to make it simpler
+ // to control the behaviour of publishing and partial rebuilding.
+ return ns.createClient.Get(ns.deps.BaseFs.Assets.Fs, filenamestr)
+
+}
+
+// Concat concatenates a slice of Resource objects. These resources must
+// (currently) be of the same Media Type.
+func (ns *Namespace) Concat(targetPathIn interface{}, r interface{}) (resource.Resource, error) {
+ targetPath, err := cast.ToStringE(targetPathIn)
+ if err != nil {
+ return nil, err
+ }
+
+ var rr resource.Resources
+
+ switch v := r.(type) {
+ case resource.Resources:
+ rr = v
+ case resource.ResourcesConverter:
+ rr = v.ToResources()
+ default:
+ return nil, fmt.Errorf("slice %T not supported in concat", r)
+ }
+
+ if len(rr) == 0 {
+ return nil, errors.New("must provide one or more Resource objects to concat")
+ }
+
+ return ns.bundlerClient.Concat(targetPath, rr)
+}
+
+// FromString creates a Resource from a string published to the relative target path.
+func (ns *Namespace) FromString(targetPathIn, contentIn interface{}) (resource.Resource, error) {
+ targetPath, err := cast.ToStringE(targetPathIn)
+ if err != nil {
+ return nil, err
+ }
+ content, err := cast.ToStringE(contentIn)
+ if err != nil {
+ return nil, err
+ }
+
+ return ns.createClient.FromString(targetPath, content)
+}
+
+// ExecuteAsTemplate creates a Resource from a Go template, parsed and executed with
+// the given data, and published to the relative target path.
+func (ns *Namespace) ExecuteAsTemplate(args ...interface{}) (resource.Resource, error) {
+ if len(args) != 3 {
+ return nil, fmt.Errorf("must provide targetPath, the template data context and a Resource object")
+ }
+ targetPath, err := cast.ToStringE(args[0])
+ if err != nil {
+ return nil, err
+ }
+ data := args[1]
+
+ r, ok := args[2].(resource.Resource)
+ if !ok {
+ return nil, fmt.Errorf("type %T not supported in Resource transformations", args[2])
+ }
+
+ return ns.templatesClient.ExecuteAsTemplate(r, targetPath, data)
+}
+
+// Fingerprint transforms the given Resource with a MD5 hash of the content in
+// the RelPermalink and Permalink.
+func (ns *Namespace) Fingerprint(args ...interface{}) (resource.Resource, error) {
+ if len(args) < 1 || len(args) > 2 {
+ return nil, errors.New("must provide a Resource and (optional) crypto algo")
+ }
+
+ var algo string
+ resIdx := 0
+
+ if len(args) == 2 {
+ resIdx = 1
+ var err error
+ algo, err = cast.ToStringE(args[0])
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ r, ok := args[resIdx].(resource.Resource)
+ if !ok {
+ return nil, fmt.Errorf("%T is not a Resource", args[resIdx])
+ }
+
+ return ns.integrityClient.Fingerprint(r, algo)
+}
+
+// Minify minifies the given Resource using the MediaType to pick the correct
+// minifier.
+func (ns *Namespace) Minify(r resource.Resource) (resource.Resource, error) {
+ return ns.minifyClient.Minify(r)
+}
+
+// ToCSS converts the given Resource to CSS. You can optional provide an Options
+// object or a target path (string) as first argument.
+func (ns *Namespace) ToCSS(args ...interface{}) (resource.Resource, error) {
+ var (
+ r resource.Resource
+ m map[string]interface{}
+ targetPath string
+ err error
+ ok bool
+ )
+
+ r, targetPath, ok = ns.resolveIfFirstArgIsString(args)
+
+ if !ok {
+ r, m, err = ns.resolveArgs(args)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ var options scss.Options
+ if targetPath != "" {
+ options.TargetPath = targetPath
+ } else if m != nil {
+ options, err = scss.DecodeOptions(m)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return ns.scssClient.ToCSS(r, options)
+}
+
+// PostCSS processes the given Resource with PostCSS
+func (ns *Namespace) PostCSS(args ...interface{}) (resource.Resource, error) {
+ r, m, err := ns.resolveArgs(args)
+ if err != nil {
+ return nil, err
+ }
+ var options postcss.Options
+ if m != nil {
+ options, err = postcss.DecodeOptions(m)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return ns.postcssClient.Process(r, options)
+}
+
+// We allow string or a map as the first argument in some cases.
+func (ns *Namespace) resolveIfFirstArgIsString(args []interface{}) (resource.Resource, string, bool) {
+ if len(args) != 2 {
+ return nil, "", false
+ }
+
+ v1, ok1 := args[0].(string)
+ if !ok1 {
+ return nil, "", false
+ }
+ v2, ok2 := args[1].(resource.Resource)
+
+ return v2, v1, ok2
+}
+
+// This roundabout way of doing it is needed to get both pipeline behaviour and options as arguments.
+func (ns *Namespace) resolveArgs(args []interface{}) (resource.Resource, map[string]interface{}, error) {
+ if len(args) == 0 {
+ return nil, nil, errors.New("no Resource provided in transformation")
+ }
+
+ if len(args) == 1 {
+ r, ok := args[0].(resource.Resource)
+ if !ok {
+ return nil, nil, fmt.Errorf("type %T not supported in Resource transformations", args[0])
+ }
+ return r, nil, nil
+ }
+
+ r, ok := args[1].(resource.Resource)
+ if !ok {
+ return nil, nil, fmt.Errorf("type %T not supported in Resource transformations", args[0])
+ }
+
+ m, err := cast.ToStringMapE(args[0])
+ if err != nil {
+ return nil, nil, _errors.Wrap(err, "invalid options type")
+ }
+
+ return r, m, nil
+}
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&amp;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..4abd34e7f
--- /dev/null
+++ b/tpl/safe/safe.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 safe provides template functions for escaping untrusted content or
+// encapsulating trusted content.
+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> &amp;tc!`, template.HTML(`Hello, <b>World</b> &amp;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/site/init.go b/tpl/site/init.go
new file mode 100644
index 000000000..48713bb3b
--- /dev/null
+++ b/tpl/site/init.go
@@ -0,0 +1,45 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package site provides template functions for accessing the Site object.
+package site
+
+import (
+ "github.com/gohugoio/hugo/deps"
+
+ "github.com/gohugoio/hugo/tpl/internal"
+)
+
+const name = "site"
+
+func init() {
+ f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
+
+ s := d.Site
+ ns := &internal.TemplateFuncsNamespace{
+ Name: name,
+ Context: func(args ...interface{}) interface{} { return s },
+ }
+
+ if s == nil {
+ panic("no Site")
+ }
+
+ // We just add the Site as the namespace here. No method mappings.
+
+ return ns
+
+ }
+
+ internal.AddTemplateFuncsNamespace(f)
+}
diff --git a/tpl/site/init_test.go b/tpl/site/init_test.go
new file mode 100644
index 000000000..00704d943
--- /dev/null
+++ b/tpl/site/init_test.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 site
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/htesting"
+ "github.com/gohugoio/hugo/tpl/internal"
+ "github.com/stretchr/testify/require"
+)
+
+func TestInit(t *testing.T) {
+ var found bool
+ var ns *internal.TemplateFuncsNamespace
+ s := htesting.NewTestHugoSite()
+
+ for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
+ ns = nsf(&deps.Deps{Site: s})
+ if ns.Name == name {
+ found = true
+ break
+ }
+ }
+
+ require.True(t, found)
+ require.IsType(t, s, ns.Context())
+}
diff --git a/tpl/strings/init.go b/tpl/strings/init.go
new file mode 100644
index 000000000..7e638e6fc
--- /dev/null
+++ b/tpl/strings/init.go
@@ -0,0 +1,192 @@
+// 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" | safeHTML }}`, `<p>Blockhead</p>`},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.CountRunes,
+ []string{"countrunes"},
+ [][2]string{},
+ )
+
+ ns.AddMethodMapping(ctx.RuneCount,
+ nil,
+ [][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.TrimLeft,
+ nil,
+ [][2]string{
+ {`{{ "aabbaa" | strings.TrimLeft "a" }}`, `bbaa`},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.TrimPrefix,
+ nil,
+ [][2]string{
+ {`{{ "aabbaa" | strings.TrimPrefix "a" }}`, `abbaa`},
+ {`{{ "aabbaa" | strings.TrimPrefix "aa" }}`, `bbaa`},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.TrimRight,
+ nil,
+ [][2]string{
+ {`{{ "aabbaa" | strings.TrimRight "a" }}`, `aabb`},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.TrimSuffix,
+ nil,
+ [][2]string{
+ {`{{ "aabbaa" | strings.TrimSuffix "a" }}`, `aabba`},
+ {`{{ "aabbaa" | strings.TrimSuffix "aa" }}`, `aabb`},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.Title,
+ []string{"title"},
+ [][2]string{
+ {`{{title "Bat man"}}`, `Bat Man`},
+ {`{{title "somewhere over the rainbow"}}`, `Somewhere Over the Rainbow`},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.FirstUpper,
+ nil,
+ [][2]string{
+ {`{{ "hugo rocks!" | strings.FirstUpper }}`, `Hugo rocks!`},
+ },
+ )
+
+ 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.Repeat,
+ nil,
+ [][2]string{
+ {`{{ "yo" | strings.Repeat 4 }}`, `yoyoyoyo`},
+ },
+ )
+
+ 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..904e486f7
--- /dev/null
+++ b/tpl/strings/init_test.go
@@ -0,0 +1,39 @@
+// 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/spf13/viper"
+ "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{Cfg: viper.New()})
+ 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..91d533af9
--- /dev/null
+++ b/tpl/strings/strings.go
@@ -0,0 +1,460 @@
+// 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 provides template functions for manipulating strings.
+package strings
+
+import (
+ "errors"
+ "fmt"
+ "html/template"
+ _strings "strings"
+ "unicode/utf8"
+
+ _errors "github.com/pkg/errors"
+
+ "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 {
+ titleCaseStyle := d.Cfg.GetString("titleCaseStyle")
+ titleFunc := helpers.GetTitleFunc(titleCaseStyle)
+ return &Namespace{deps: d, titleFunc: titleFunc}
+}
+
+// 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 {
+ titleFunc func(s string) string
+ 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, _errors.Wrap(err, "Failed to convert content to string")
+ }
+
+ counter := 0
+ for _, r := range helpers.StripHTML(ss) {
+ if !helpers.IsWhitespace(r) {
+ counter++
+ }
+ }
+
+ return counter, nil
+}
+
+// RuneCount returns the number of runes in s.
+func (ns *Namespace) RuneCount(s interface{}) (int, error) {
+ ss, err := cast.ToStringE(s)
+ if err != nil {
+ return 0, _errors.Wrap(err, "Failed to convert content to string")
+ }
+ return utf8.RuneCountInString(ss), 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, _errors.Wrap(err, "Failed to convert content to string")
+ }
+
+ 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{}) (interface{}, error) {
+ ss, err := cast.ToStringE(s)
+ if err != nil {
+ return "", err
+ }
+
+ res := _strings.TrimRight(ss, "\r\n")
+ switch s.(type) {
+ case template.HTML:
+ return template.HTML(res), nil
+ default:
+ return res, 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 ns.titleFunc(ss), nil
+}
+
+// FirstUpper returns a string with the first character as upper case.
+func (ns *Namespace) FirstUpper(s interface{}) (string, error) {
+ ss, err := cast.ToStringE(s)
+ if err != nil {
+ return "", err
+ }
+
+ return helpers.FirstUpper(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
+}
+
+// TrimLeft returns a slice of the string s with all leading characters
+// contained in cutset removed.
+func (ns *Namespace) TrimLeft(cutset, s 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.TrimLeft(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(prefix, s 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
+}
+
+// TrimRight returns a slice of the string s with all trailing characters
+// contained in cutset removed.
+func (ns *Namespace) TrimRight(cutset, s 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.TrimRight(ss, sc), 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(suffix, s 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
+}
+
+// Repeat returns a new string consisting of count copies of the string s.
+func (ns *Namespace) Repeat(n, s interface{}) (string, error) {
+ ss, err := cast.ToStringE(s)
+ if err != nil {
+ return "", err
+ }
+
+ sn, err := cast.ToIntE(n)
+ if err != nil {
+ return "", err
+ }
+
+ if sn < 0 {
+ return "", errors.New("strings: negative Repeat count")
+ }
+
+ return _strings.Repeat(ss, sn), nil
+}
diff --git a/tpl/strings/strings_test.go b/tpl/strings/strings_test.go
new file mode 100644
index 000000000..22695ba08
--- /dev/null
+++ b/tpl/strings/strings_test.go
@@ -0,0 +1,773 @@
+// 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/spf13/cast"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+var ns = New(&deps.Deps{Cfg: viper.New()})
+
+type tstNoStringer struct{}
+
+func TestChomp(t *testing.T) {
+ t.Parallel()
+
+ for i, test := range []struct {
+ s interface{}
+ expect interface{}
+ }{
+ {"\n a\n", "\n a"},
+ {"\n a\n\n", "\n a"},
+ {"\n a\r\n", "\n a"},
+ {"\n a\n\r\n", "\n a"},
+ {"\n a\r\r", "\n a"},
+ {"\n a\r", "\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)
+
+ // repeat the check with template.HTML input
+ result, err = ns.Chomp(template.HTML(cast.ToString(test.s)))
+ require.NoError(t, err, errMsg)
+ assert.Equal(t, template.HTML(cast.ToString(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 TestRuneCount(t *testing.T) {
+ t.Parallel()
+
+ for i, test := range []struct {
+ s interface{}
+ expect interface{}
+ }{
+ {"foo bar", 7},
+ {"旁边", 2},
+ {`<div class="test">旁边</div>`, 26},
+ // errors
+ {tstNoStringer{}, false},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test.s)
+
+ result, err := ns.RuneCount(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 TestTrimLeft(t *testing.T) {
+ t.Parallel()
+
+ for i, test := range []struct {
+ s interface{}
+ cutset interface{}
+ expect interface{}
+ }{
+ {"abba", "a", "bba"},
+ {"abba", "ab", ""},
+ {"<tag>", "<>", "tag>"},
+ {`"quote"`, `"`, `quote"`},
+ {1221, "1", "221"},
+ {1221, "12", ""},
+ {"007", "0", "7"},
+ {template.HTML("<tag>"), "<>", "tag>"},
+ {[]byte("<tag>"), "<>", "tag>"},
+ // errors
+ {"", tstNoStringer{}, false},
+ {tstNoStringer{}, "", false},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test)
+
+ result, err := ns.TrimLeft(test.cutset, 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 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.prefix, 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 TestTrimRight(t *testing.T) {
+ t.Parallel()
+
+ for i, test := range []struct {
+ s interface{}
+ cutset interface{}
+ expect interface{}
+ }{
+ {"abba", "a", "abb"},
+ {"abba", "ab", ""},
+ {"<tag>", "<>", "<tag"},
+ {`"quote"`, `"`, `"quote`},
+ {1221, "1", "122"},
+ {1221, "12", ""},
+ {"007", "0", "007"},
+ {template.HTML("<tag>"), "<>", "<tag"},
+ {[]byte("<tag>"), "<>", "<tag"},
+ // errors
+ {"", tstNoStringer{}, false},
+ {tstNoStringer{}, "", false},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test)
+
+ result, err := ns.TrimRight(test.cutset, 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 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.suffix, 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 TestRepeat(t *testing.T) {
+ t.Parallel()
+
+ for i, test := range []struct {
+ s interface{}
+ n interface{}
+ expect interface{}
+ }{
+ {"yo", "2", "yoyo"},
+ {"~", "16", "~~~~~~~~~~~~~~~~"},
+ {"<tag>", "0", ""},
+ {"yay", "1", "yay"},
+ {1221, "1", "1221"},
+ {1221, 2, "12211221"},
+ {template.HTML("<tag>"), "2", "<tag><tag>"},
+ {[]byte("<tag>"), 2, "<tag><tag>"},
+ // errors
+ {"", tstNoStringer{}, false},
+ {tstNoStringer{}, "", false},
+ {"ab", -1, false},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test)
+
+ result, err := ns.Repeat(test.n, 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/truncate.go b/tpl/strings/truncate.go
new file mode 100644
index 000000000..6e3a50ed2
--- /dev/null
+++ b/tpl/strings/truncate.go
@@ -0,0 +1,157 @@
+// 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
+}
+
+// Truncate truncates a given string to the specified length.
+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("&lt;b&gt;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..935771364
--- /dev/null
+++ b/tpl/template.go
@@ -0,0 +1,305 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "fmt"
+ "io"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "time"
+
+ "github.com/gohugoio/hugo/output"
+
+ "github.com/gohugoio/hugo/common/herrors"
+
+ "github.com/gohugoio/hugo/hugofs"
+
+ "github.com/spf13/afero"
+
+ "html/template"
+ texttemplate "text/template"
+ "text/template/parse"
+
+ bp "github.com/gohugoio/hugo/bufferpool"
+ "github.com/gohugoio/hugo/metrics"
+ "github.com/pkg/errors"
+)
+
+var (
+ _ TemplateExecutor = (*TemplateAdapter)(nil)
+ _ TemplateInfoProvider = (*TemplateAdapter)(nil)
+)
+
+// TemplateHandler manages the collection of templates.
+type TemplateHandler interface {
+ TemplateFinder
+ AddTemplate(name, tpl string) error
+ AddLateTemplate(name, tpl string) error
+ LoadTemplates(prefix string) error
+
+ NewTextTemplate() TemplateParseFinder
+
+ MarkReady() error
+ RebuildClone()
+}
+
+// TemplateVariants describes the possible variants of a template.
+// All of these may be empty.
+type TemplateVariants struct {
+ Language string
+ OutputFormat output.Format
+}
+
+// TemplateFinder finds templates.
+type TemplateFinder interface {
+ TemplateLookup
+ TemplateLookupVariant
+}
+
+type TemplateLookup interface {
+ Lookup(name string) (Template, bool)
+}
+
+type TemplateLookupVariant interface {
+ // TODO(bep) this currently only works for shortcodes.
+ // We may unify and expand this variant pattern to the
+ // other templates, but we need this now for the shortcodes to
+ // quickly determine if a shortcode has a template for a given
+ // output format.
+ // It returns the template, if it was found or not and if there are
+ // alternative representations (output format, language).
+ // We are currently only interested in output formats, so we should improve
+ // this for speed.
+ LookupVariant(name string, variants TemplateVariants) (Template, bool, bool)
+}
+
+// Template is the common interface between text/template and html/template.
+type Template interface {
+ Execute(wr io.Writer, data interface{}) error
+ Name() string
+}
+
+// TemplateInfoProvider provides some contextual information about a template.
+type TemplateInfoProvider interface {
+ TemplateInfo() Info
+}
+
+// TemplateParser is used to parse ad-hoc templates, e.g. in the Resource chain.
+type TemplateParser interface {
+ Parse(name, tpl string) (Template, error)
+}
+
+// TemplateParseFinder provides both parsing and finding.
+type TemplateParseFinder interface {
+ TemplateParser
+ TemplateFinder
+}
+
+// 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
+ Metrics metrics.Provider
+
+ Info Info
+
+ // The filesystem where the templates are stored.
+ Fs afero.Fs
+
+ // Maps to base template if relevant.
+ NameBaseTemplateName map[string]string
+}
+
+var baseOfRe = regexp.MustCompile("template: (.*?):")
+
+func extractBaseOf(err string) string {
+ m := baseOfRe.FindStringSubmatch(err)
+ if len(m) == 2 {
+ return m[1]
+ }
+ return ""
+}
+
+// Execute executes the current template. The actual execution is performed
+// by the embedded text or html template, but we add an implementation here so
+// we can add a timer for some metrics.
+func (t *TemplateAdapter) Execute(w io.Writer, data interface{}) (execErr error) {
+ defer func() {
+ // Panics in templates are a little bit too common (nil pointers etc.)
+ // See https://github.com/gohugoio/hugo/issues/5327
+ if r := recover(); r != nil {
+ execErr = t.addFileContext(t.Name(), fmt.Errorf(`panic in Execute: %s. See "https://github.com/gohugoio/hugo/issues/5327" for the reason why we cannot provide a better error message for this`, r))
+ }
+ }()
+
+ if t.Metrics != nil {
+ defer t.Metrics.MeasureSince(t.Name(), time.Now())
+ }
+
+ execErr = t.Template.Execute(w, data)
+ if execErr != nil {
+ execErr = t.addFileContext(t.Name(), execErr)
+ }
+
+ return
+}
+
+func (t *TemplateAdapter) TemplateInfo() Info {
+ return t.Info
+}
+
+// The identifiers may be truncated in the log, e.g.
+// "executing "main" at <$scaled.SRelPermalin...>: can't evaluate field SRelPermalink in type *resource.Image"
+var identifiersRe = regexp.MustCompile(`at \<(.*?)(\.{3})?\>:`)
+
+func (t *TemplateAdapter) extractIdentifiers(line string) []string {
+ m := identifiersRe.FindAllStringSubmatch(line, -1)
+ identifiers := make([]string, len(m))
+ for i := 0; i < len(m); i++ {
+ identifiers[i] = m[i][1]
+ }
+ return identifiers
+}
+
+func (t *TemplateAdapter) addFileContext(name string, inerr error) error {
+ if strings.HasPrefix(t.Name(), "_internal") {
+ return inerr
+ }
+
+ f, realFilename, err := t.fileAndFilename(t.Name())
+ if err != nil {
+ return inerr
+
+ }
+ defer f.Close()
+
+ master, hasMaster := t.NameBaseTemplateName[name]
+
+ ferr := errors.Wrap(inerr, "execute of template failed")
+
+ // Since this can be a composite of multiple template files (single.html + baseof.html etc.)
+ // we potentially need to look in both -- and cannot rely on line number alone.
+ lineMatcher := func(m herrors.LineMatcher) bool {
+ if m.Position.LineNumber != m.LineNumber {
+ return false
+ }
+ if !hasMaster {
+ return true
+ }
+
+ identifiers := t.extractIdentifiers(m.Error.Error())
+
+ for _, id := range identifiers {
+ if strings.Contains(m.Line, id) {
+ return true
+ }
+ }
+ return false
+ }
+
+ fe, ok := herrors.WithFileContext(ferr, realFilename, f, lineMatcher)
+ if ok || !hasMaster {
+ return fe
+ }
+
+ // Try the base template if relevant
+ f, realFilename, err = t.fileAndFilename(master)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ fe, ok = herrors.WithFileContext(ferr, realFilename, f, lineMatcher)
+
+ if !ok {
+ // Return the most specific.
+ return ferr
+
+ }
+ return fe
+
+}
+
+func (t *TemplateAdapter) fileAndFilename(name string) (afero.File, string, error) {
+ fs := t.Fs
+ filename := filepath.FromSlash(name)
+
+ fi, err := fs.Stat(filename)
+ if err != nil {
+ return nil, "", err
+ }
+ f, err := fs.Open(filename)
+ if err != nil {
+ return nil, "", errors.Wrapf(err, "failed to open template file %q:", filename)
+ }
+
+ return f, fi.(hugofs.RealFilenameInfo).RealFilename(), nil
+}
+
+// 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
+}
+
+// TemplateFuncsGetter allows to get a map of functions.
+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/template_info.go b/tpl/template_info.go
new file mode 100644
index 000000000..be0566958
--- /dev/null
+++ b/tpl/template_info.go
@@ -0,0 +1,42 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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
+
+// Increments on breaking changes.
+const TemplateVersion = 2
+
+// Info holds some info extracted from a parsed template.
+type Info struct {
+
+ // Set for shortcode templates with any {{ .Inner }}
+ IsInner bool
+
+ // Set for partials with a return statement.
+ HasReturn bool
+
+ // Config extracted from template.
+ Config Config
+}
+
+func (info Info) IsZero() bool {
+ return info.Config.Version == 0
+}
+
+type Config struct {
+ Version int
+}
+
+var DefaultConfig = Config{
+ Version: TemplateVersion,
+}
diff --git a/tpl/template_test.go b/tpl/template_test.go
new file mode 100644
index 000000000..73e9640be
--- /dev/null
+++ b/tpl/template_test.go
@@ -0,0 +1,31 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestExtractBaseof(t *testing.T) {
+ assert := require.New(t)
+
+ replaced := extractBaseOf(`failed: template: _default/baseof.html:37:11: executing "_default/baseof.html" at <.Parents>: can't evaluate field Parents in type *hugolib.PageOutput`)
+
+ assert.Equal("_default/baseof.html", replaced)
+ assert.Equal("", extractBaseOf("not baseof for you"))
+ assert.Equal("blog/baseof.html", extractBaseOf("template: blog/baseof.html:23:11:"))
+ assert.Equal("blog/baseof.ace", extractBaseOf("template: blog/baseof.ace:23:11:"))
+}
diff --git a/tpl/templates/init.go b/tpl/templates/init.go
new file mode 100644
index 000000000..8bc53ef49
--- /dev/null
+++ b/tpl/templates/init.go
@@ -0,0 +1,44 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package templates
+
+import (
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/tpl/internal"
+)
+
+const name = "templates"
+
+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.Exists,
+ nil,
+ [][2]string{{`{{ if (templates.Exists "partials/header.html") }}Yes!{{ end }}`, `Yes!`},
+ {`{{ if not (templates.Exists "partials/doesnotexist.html") }}No!{{ end }}`, `No!`},
+ },
+ )
+
+ return ns
+
+ }
+
+ internal.AddTemplateFuncsNamespace(f)
+}
diff --git a/tpl/templates/init_test.go b/tpl/templates/init_test.go
new file mode 100644
index 000000000..9a0533fe8
--- /dev/null
+++ b/tpl/templates/init_test.go
@@ -0,0 +1,38 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package templates
+
+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/templates/templates.go b/tpl/templates/templates.go
new file mode 100644
index 000000000..44d397e68
--- /dev/null
+++ b/tpl/templates/templates.go
@@ -0,0 +1,40 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package templates provides template functions for working with templates.
+package templates
+
+import (
+ "github.com/gohugoio/hugo/deps"
+)
+
+// New returns a new instance of the templates-namespaced template functions.
+func New(deps *deps.Deps) *Namespace {
+ return &Namespace{
+ deps: deps,
+ }
+}
+
+// Namespace provides template functions for the "templates" namespace.
+type Namespace struct {
+ deps *deps.Deps
+}
+
+// Exists returns whether the template with the given name exists.
+// Note that this is the Unix-styled relative path including filename suffix,
+// e.g. partials/header.html
+func (ns *Namespace) Exists(name string) bool {
+ _, found := ns.deps.Tmpl.Lookup(name)
+ return found
+
+}
diff --git a/tpl/time/init.go b/tpl/time/init.go
new file mode 100644
index 000000000..3112999e4
--- /dev/null
+++ b/tpl/time/init.go
@@ -0,0 +1,87 @@
+// 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`},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.Duration,
+ []string{"duration"},
+ [][2]string{
+ {`{{ mul 60 60 | duration "second" }}`, `1h0m0s`},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.ParseDuration,
+ nil,
+ [][2]string{
+ {`{{ "1h12m10s" | time.ParseDuration }}`, `1h12m10s`},
+ },
+ )
+
+ 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..598124648
--- /dev/null
+++ b/tpl/time/time.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 time provides template functions for measuring and displaying time.
+package time
+
+import (
+ "fmt"
+ _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()
+}
+
+// ParseDuration parses a duration string.
+// A duration string is a possibly signed sequence of
+// decimal numbers, each with optional fraction and a unit suffix,
+// such as "300ms", "-1.5h" or "2h45m".
+// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
+// See https://golang.org/pkg/time/#ParseDuration
+func (ns *Namespace) ParseDuration(in interface{}) (_time.Duration, error) {
+ s, err := cast.ToStringE(in)
+ if err != nil {
+ return 0, err
+ }
+
+ return _time.ParseDuration(s)
+}
+
+var durationUnits = map[string]_time.Duration{
+ "nanosecond": _time.Nanosecond,
+ "ns": _time.Nanosecond,
+ "microsecond": _time.Microsecond,
+ "us": _time.Microsecond,
+ "µs": _time.Microsecond,
+ "millisecond": _time.Millisecond,
+ "ms": _time.Millisecond,
+ "second": _time.Second,
+ "s": _time.Second,
+ "minute": _time.Minute,
+ "m": _time.Minute,
+ "hour": _time.Hour,
+ "h": _time.Hour,
+}
+
+// Duration converts the given number to a time.Duration.
+// Unit is one of nanosecond/ns, microsecond/us/µs, millisecond/ms, second/s, minute/m or hour/h.
+func (ns *Namespace) Duration(unit interface{}, number interface{}) (_time.Duration, error) {
+ unitStr, err := cast.ToStringE(unit)
+ if err != nil {
+ return 0, err
+ }
+ unitDuration, found := durationUnits[unitStr]
+ if !found {
+ return 0, fmt.Errorf("%q is not a valid duration unit", unit)
+ }
+ n, err := cast.ToInt64E(number)
+ if err != nil {
+ return 0, err
+ }
+ return _time.Duration(n) * unitDuration, nil
+}
diff --git a/tpl/time/time_test.go b/tpl/time/time_test.go
new file mode 100644
index 000000000..01cf4e03b
--- /dev/null
+++ b/tpl/time/time_test.go
@@ -0,0 +1,100 @@
+// 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)
+ }
+ }
+ }
+}
+
+func TestDuration(t *testing.T) {
+ t.Parallel()
+
+ ns := New()
+
+ for i, test := range []struct {
+ unit interface{}
+ num interface{}
+ expect interface{}
+ }{
+ {"nanosecond", 10, 10 * time.Nanosecond},
+ {"ns", 10, 10 * time.Nanosecond},
+ {"microsecond", 20, 20 * time.Microsecond},
+ {"us", 20, 20 * time.Microsecond},
+ {"µs", 20, 20 * time.Microsecond},
+ {"millisecond", 20, 20 * time.Millisecond},
+ {"ms", 20, 20 * time.Millisecond},
+ {"second", 30, 30 * time.Second},
+ {"s", 30, 30 * time.Second},
+ {"minute", 20, 20 * time.Minute},
+ {"m", 20, 20 * time.Minute},
+ {"hour", 20, 20 * time.Hour},
+ {"h", 20, 20 * time.Hour},
+ {"hours", 20, false},
+ {"hour", "30", 30 * time.Hour},
+ } {
+ result, err := ns.Duration(test.unit, test.num)
+ if b, ok := test.expect.(bool); ok && !b {
+ if err == nil {
+ t.Errorf("[%d] Duration didn't return an expected error, got %v", i, result)
+ }
+ } else {
+ if err != nil {
+ t.Errorf("[%d] Duration failed: %s", i, err)
+ continue
+ }
+ if result != test.expect {
+ t.Errorf("[%d] Duration 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..bdbc71059
--- /dev/null
+++ b/tpl/tplimpl/ace.go
@@ -0,0 +1,68 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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
+ withoutExt := name[:len(name)-len(filepath.Ext(innerPath))]
+ name = withoutExt + ".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
+ }
+
+ typ := resolveTemplateType(name)
+
+ c, err := applyTemplateTransformersToHMLTTemplate(typ, templ)
+ if err != nil {
+ return err
+ }
+
+ if typ == templateShortcode {
+ t.addShortcodeVariant(name, c.Info, templ)
+ } else {
+ t.templateInfo[name] = c.Info
+ }
+
+ return nil
+}
diff --git a/tpl/tplimpl/amber_compiler.go b/tpl/tplimpl/amber_compiler.go
new file mode 100644
index 000000000..b37becb3d
--- /dev/null
+++ b/tpl/tplimpl/amber_compiler.go
@@ -0,0 +1,44 @@
+// 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"
+ "github.com/spf13/afero"
+)
+
+func (t *templateHandler) compileAmberWithTemplate(b []byte, path string, templ *template.Template) (*template.Template, error) {
+ c := amber.New()
+ c.Options.VirtualFilesystem = afero.NewHttpFs(t.layoutsFs)
+
+ 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/embedded/.gitattributes b/tpl/tplimpl/embedded/.gitattributes
new file mode 100644
index 000000000..721b3af6b
--- /dev/null
+++ b/tpl/tplimpl/embedded/.gitattributes
@@ -0,0 +1 @@
+*autogen.go linguist-generated=true
diff --git a/tpl/tplimpl/embedded/README.md b/tpl/tplimpl/embedded/README.md
new file mode 100644
index 000000000..1c01961e1
--- /dev/null
+++ b/tpl/tplimpl/embedded/README.md
@@ -0,0 +1,5 @@
+
+
+## Build Templates
+
+If you add or modify any template in the templates folder, you also need to run `mage generate` to get the Go code in synch.
diff --git a/tpl/tplimpl/embedded/generate/generate.go b/tpl/tplimpl/embedded/generate/generate.go
new file mode 100644
index 000000000..df4de4799
--- /dev/null
+++ b/tpl/tplimpl/embedded/generate/generate.go
@@ -0,0 +1,100 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:generate go run generate.go
+
+package main
+
+import (
+ "fmt"
+ "io/ioutil"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+func main() {
+
+ templateFolder := filepath.Join("..", "templates")
+
+ temlatePath := filepath.Join(".", templateFolder)
+
+ file, err := os.Create("../templates.autogen.go")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer file.Close()
+
+ var nameValues []string
+
+ err = filepath.Walk(temlatePath, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ if info.IsDir() {
+ return nil
+ }
+ if strings.HasPrefix(info.Name(), ".") {
+ return nil
+ }
+
+ templateName := filepath.ToSlash(strings.TrimPrefix(path, templateFolder+string(os.PathSeparator)))
+
+ templateContent, err := ioutil.ReadFile(path)
+ if err != nil {
+ return err
+ }
+
+ nameValues = append(nameValues, nameValue(templateName, string(templateContent)))
+
+ return nil
+ })
+
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fmt.Fprint(file, `// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This file is autogenerated.
+
+// Package embedded defines the internal templates that Hugo provides.
+package embedded
+
+// EmbeddedTemplates represents all embedded templates.
+var EmbeddedTemplates = [][2]string{
+`)
+
+ for _, v := range nameValues {
+ fmt.Fprint(file, " ", v, ",\n")
+ }
+ fmt.Fprint(file, "}\n")
+
+}
+
+func nameValue(name, value string) string {
+ return fmt.Sprintf("{`%s`, `%s`}", name, value)
+}
diff --git a/tpl/tplimpl/embedded/templates.autogen.go b/tpl/tplimpl/embedded/templates.autogen.go
new file mode 100644
index 000000000..58d0b2799
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates.autogen.go
@@ -0,0 +1,532 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This file is autogenerated.
+
+// Package embedded defines the internal templates that Hugo provides.
+package embedded
+
+// EmbeddedTemplates represents all embedded templates.
+var EmbeddedTemplates = [][2]string{
+ {`_default/robots.txt`, `User-agent: *`},
+ {`_default/rss.xml`, `{{- $pages := .Data.Pages -}}
+{{- $limit := .Site.Config.Services.RSS.Limit -}}
+{{- if ge $limit 1 -}}
+{{- $pages = $pages | first $limit -}}
+{{- end -}}
+{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>" | safeHTML }}
+<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 $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>`},
+ {`_default/sitemap.xml`, `{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>" | safeHTML }}
+<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="{{ .Language.Lang }}"
+ href="{{ .Permalink }}"
+ />{{ end }}
+ <xhtml:link
+ rel="alternate"
+ hreflang="{{ .Language.Lang }}"
+ href="{{ .Permalink }}"
+ />{{ end }}
+ </url>
+ {{ end }}
+</urlset>`},
+ {`_default/sitemapindex.xml`, `{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>" | safeHTML }}
+<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>
+`},
+ {`disqus.html`, `{{- $pc := .Site.Config.Privacy.Disqus -}}
+{{- if not $pc.Disable -}}
+{{ if .Site.DisqusShortname }}<div id="disqus_thread"></div>
+<script type="application/javascript">
+ var disqus_config = function () {
+ {{with .Params.disqus_identifier }}this.page.identifier = '{{ . }}';{{end}}
+ {{with .Params.disqus_title }}this.page.title = '{{ . }}';{{end}}
+ {{with .Params.disqus_url }}this.page.url = '{{ . | html }}';{{end}}
+ };
+ (function() {
+ if (["localhost", "127.0.0.1"].indexOf(window.location.hostname) != -1) {
+ document.getElementById('disqus_thread').innerHTML = 'Disqus comments not available by default when the website is previewed locally.';
+ return;
+ }
+ var d = document, s = d.createElement('script'); s.async = true;
+ s.src = '//' + {{ .Site.DisqusShortname }} + '.disqus.com/embed.js';
+ s.setAttribute('data-timestamp', +new Date());
+ (d.head || d.body).appendChild(s);
+ })();
+</script>
+<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
+<a href="https://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>{{end}}
+{{- end -}}`},
+ {`google_analytics.html`, `{{- $pc := .Site.Config.Privacy.GoogleAnalytics -}}
+{{- if not $pc.Disable -}}
+{{ with .Site.GoogleAnalytics }}
+<script type="application/javascript">
+{{ template "__ga_js_set_doNotTrack" $ }}
+if (!doNotTrack) {
+ (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');
+ {{- if $pc.UseSessionStorage }}
+ if (window.sessionStorage) {
+ var GA_SESSION_STORAGE_KEY = 'ga:clientId';
+ ga('create', '{{ . }}', {
+ 'storage': 'none',
+ 'clientId': sessionStorage.getItem(GA_SESSION_STORAGE_KEY)
+ });
+ ga(function(tracker) {
+ sessionStorage.setItem(GA_SESSION_STORAGE_KEY, tracker.get('clientId'));
+ });
+ }
+ {{ else }}
+ ga('create', '{{ . }}', 'auto');
+ {{ end -}}
+ {{ if $pc.AnonymizeIP }}ga('set', 'anonymizeIp', true);{{ end }}
+ ga('send', 'pageview');
+}
+</script>
+{{ end }}
+{{- end -}}
+{{- define "__ga_js_set_doNotTrack" -}}{{/* This is also used in the async version. */}}
+{{- $pc := .Site.Config.Privacy.GoogleAnalytics -}}
+{{- if not $pc.RespectDoNotTrack -}}
+var doNotTrack = false;
+{{- else -}}
+var dnt = (navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack);
+var doNotTrack = (dnt == "1" || dnt == "yes");
+{{- end -}}
+{{- end -}}`},
+ {`google_analytics_async.html`, `{{- $pc := .Site.Config.Privacy.GoogleAnalytics -}}
+{{- if not $pc.Disable -}}
+{{ with .Site.GoogleAnalytics }}
+<script type="application/javascript">
+{{ template "__ga_js_set_doNotTrack" $ }}
+if (!doNotTrack) {
+ window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
+ {{- if $pc.UseSessionStorage }}
+ if (window.sessionStorage) {
+ var GA_SESSION_STORAGE_KEY = 'ga:clientId';
+ ga('create', '{{ . }}', {
+ 'storage': 'none',
+ 'clientId': sessionStorage.getItem(GA_SESSION_STORAGE_KEY)
+ });
+ ga(function(tracker) {
+ sessionStorage.setItem(GA_SESSION_STORAGE_KEY, tracker.get('clientId'));
+ });
+ }
+ {{ else }}
+ ga('create', '{{ . }}', 'auto');
+ {{ end -}}
+ {{ if $pc.AnonymizeIP }}ga('set', 'anonymizeIp', true);{{ end }}
+ ga('send', 'pageview');
+}
+</script>
+<script async src='https://www.google-analytics.com/analytics.js'></script>
+{{ end }}
+{{- end -}}
+`},
+ {`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 }}`},
+ {`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 $.Param "images" }}{{ range first 6 . }}
+<meta property="og:image" content="{{ . | absURL }}" />
+{{ end }}{{ end }}
+
+{{- $iso8601 := "2006-01-02T15:04:05-07:00" -}}
+{{- if .IsPage }}
+{{- if not .PublishDate.IsZero }}<meta property="article:published_time" {{ .PublishDate.Format $iso8601 | printf "content=%q" | safeHTMLAttr }} />
+{{ else if not .Date.IsZero }}<meta property="article:published_time" {{ .Date.Format $iso8601 | printf "content=%q" | safeHTMLAttr }} />
+{{ end }}
+{{- if not .Lastmod.IsZero }}<meta property="article:modified_time" {{ .Lastmod.Format $iso8601 | printf "content=%q" | safeHTMLAttr }} />{{ end }}
+{{- else }}
+{{- if not .Date.IsZero }}
+<meta property="og:updated_time" {{ .Date.Format $iso8601 | printf "content=%q" | safeHTMLAttr }} />
+{{- end }}
+{{- end }}{{/* .IsPage */}}
+
+{{- 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 . }}
+<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 }}
+`},
+ {`pagination.html`, `{{ $pag := $.Paginator }}
+{{ if gt $pag.TotalPages 1 }}
+<ul class="pagination">
+ {{ with $pag.First }}
+ <li class="page-item">
+ <a href="{{ .URL }}" class="page-link" aria-label="First"><span aria-hidden="true">&laquo;&laquo;</span></a>
+ </li>
+ {{ end }}
+ <li class="page-item{{ if not $pag.HasPrev }} disabled{{ end }}">
+ <a href="{{ if $pag.HasPrev }}{{ $pag.Prev.URL }}{{ end }}" class="page-link" aria-label="Previous"><span aria-hidden="true">&laquo;</span></a>
+ </li>
+ {{ $ellipsed := false }}
+ {{ $shouldEllipse := 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 }}
+ {{ $ellipsed = false }}
+ {{ $shouldEllipse = false }}
+ {{ else }}
+ {{ $shouldEllipse = not $ellipsed }}
+ {{ $ellipsed = true }}
+ {{ end }}
+ {{ if $showNumber }}
+ <li class="page-item{{ if eq . $pag }} active{{ end }}"><a class="page-link" href="{{ .URL }}">{{ .PageNumber }}</a></li>
+ {{ else if $shouldEllipse }}
+ <li class="page-item disabled"><span aria-hidden="true">&nbsp;&hellip;&nbsp;</span></li>
+ {{ end }}
+ {{ end }}
+ <li class="page-item{{ if not $pag.HasNext }} disabled{{ end }}">
+ <a href="{{ if $pag.HasNext }}{{ $pag.Next.URL }}{{ end }}" class="page-link" aria-label="Next"><span aria-hidden="true">&raquo;</span></a>
+ </li>
+ {{ with $pag.Last }}
+ <li class="page-item">
+ <a href="{{ .URL }}" class="page-link" aria-label="Last"><span aria-hidden="true">&raquo;&raquo;</span></a>
+ </li>
+ {{ end }}
+</ul>
+{{ end }}`},
+ {`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 .Lastmod.IsZero }}<meta itemprop="dateModified" content="{{ .Lastmod.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="{{ if .IsPage}}{{ range $index, $tag := .Params.tags }}{{ $tag }},{{ end }}{{ else }}{{ range $plural, $terms := .Site.Taxonomies }}{{ range $term, $val := $terms }}{{ printf "%s," $term }}{{ end }}{{ end }}{{ end }}" />
+{{ end }}`},
+ {`shortcodes/__h_simple_assets.html`, `{{ define "__h_simple_css" }}{{/* These template definitions are global. */}}
+{{- if not (.Page.Scratch.Get "__h_simple_css") -}}
+{{/* Only include once */}}
+{{- .Page.Scratch.Set "__h_simple_css" true -}}
+<style>
+.__h_video {
+ position: relative;
+ padding-bottom: 56.23%;
+ height: 0;
+ overflow: hidden;
+ width: 100%;
+ background: #000;
+}
+.__h_video img {
+ width: 100%;
+ height: auto;
+ color: #000;
+}
+.__h_video .play {
+ height: 72px;
+ width: 72px;
+ left: 50%;
+ top: 50%;
+ margin-left: -36px;
+ margin-top: -36px;
+ position: absolute;
+ cursor: pointer;
+}
+</style>
+{{- end -}}
+{{- end -}}
+{{- define "__h_simple_icon_play" -}}
+<svg version="1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 61 61"><circle cx="30.5" cy="30.5" r="30.5" opacity=".8" fill="#000"></circle><path d="M25.3 19.2c-2.1-1.2-3.8-.2-3.8 2.2v18.1c0 2.4 1.7 3.4 3.8 2.2l16.6-9.1c2.1-1.2 2.1-3.2 0-4.4l-16.6-9z" fill="#fff"></path></svg>
+{{- end -}}
+`},
+ {`shortcodes/figure.html`, `<figure{{ with .Get "class" }} class="{{ . }}"{{ end }}>
+ {{- if .Get "link" -}}
+ <a href="{{ .Get "link" }}"{{ with .Get "target" }} target="{{ . }}"{{ end }}{{ with .Get "rel" }} rel="{{ . }}"{{ end }}>
+ {{- end }}
+ <img src="{{ .Get "src" }}"
+ {{- if or (.Get "alt") (.Get "caption") }}
+ alt="{{ with .Get "alt" }}{{ . }}{{ else }}{{ .Get "caption" | markdownify| plainify }}{{ end }}"
+ {{- end -}}
+ {{- with .Get "width" }} width="{{ . }}"{{ end -}}
+ {{- with .Get "height" }} height="{{ . }}"{{ end -}}
+ /> <!-- Closing img tag -->
+ {{- if .Get "link" }}</a>{{ end -}}
+ {{- if or (or (.Get "title") (.Get "caption")) (.Get "attr") -}}
+ <figcaption>
+ {{ with (.Get "title") -}}
+ <h4>{{ . }}</h4>
+ {{- end -}}
+ {{- if or (.Get "caption") (.Get "attr") -}}<p>
+ {{- .Get "caption" | markdownify -}}
+ {{- with .Get "attrlink" }}
+ <a href="{{ . }}">
+ {{- end -}}
+ {{- .Get "attr" | markdownify -}}
+ {{- if .Get "attrlink" }}</a>{{ end }}</p>
+ {{- end }}
+ </figcaption>
+ {{- end }}
+</figure>
+`},
+ {`shortcodes/gist.html`, `<script type="application/javascript" src="//gist.github.com/{{ index .Params 0 }}/{{ index .Params 1 }}.js{{if len .Params | eq 3 }}?file={{ index .Params 2 }}{{end}}"></script>`},
+ {`shortcodes/highlight.html`, `{{ if len .Params | eq 2 }}{{ highlight (trim .Inner "\n\r") (.Get 0) (.Get 1) }}{{ else }}{{ highlight (trim .Inner "\n\r") (.Get 0) "" }}{{ end }}`},
+ {`shortcodes/instagram.html`, `{{- $pc := .Page.Site.Config.Privacy.Instagram -}}
+{{- if not $pc.Disable -}}
+{{- if $pc.Simple -}}
+{{ template "_internal/shortcodes/instagram_simple.html" . }}
+{{- else -}}
+{{ $id := .Get 0 }}
+{{ $hideCaption := cond (eq (.Get 1) "hidecaption") "1" "0" }}
+{{ with getJSON "https://api.instagram.com/oembed/?url=https://instagram.com/p/" $id "/&hidecaption=" $hideCaption }}{{ .html | safeHTML }}{{ end }}
+{{- end -}}
+{{- end -}}`},
+ {`shortcodes/instagram_simple.html`, `{{- $pc := .Page.Site.Config.Privacy.Instagram -}}
+{{- $sc := .Page.Site.Config.Services.Instagram -}}
+{{- if not $pc.Disable -}}
+{{- $id := .Get 0 -}}
+{{- $item := getJSON "https://api.instagram.com/oembed/?url=https://www.instagram.com/p/" $id "/&amp;maxwidth=640&amp;omitscript=true" -}}
+{{- $class1 := "__h_instagram" -}}
+{{- $class2 := "s_instagram_simple" -}}
+{{- $hideCaption := (eq (.Get 1) "hidecaption") -}}
+{{ with $item }}
+{{- $mediaURL := printf "https://instagram.com/p/%s/" $id | safeURL -}}
+{{- if not $sc.DisableInlineCSS -}}
+{{ template "__h_simple_instagram_css" $ }}
+{{- end -}}
+<div class="{{ $class1 }} {{ $class2 }} card" style="max-width: {{ $item.thumbnail_width }}px">
+ <div class="card-header">
+ <a href="{{ $item.author_url | safeURL }}" class="card-link">{{ $item.author_name }}</a>
+ </div>
+ <a href="{{ $mediaURL }}" target="_blank"><img class="card-img-top img-fluid" src="{{ $item.thumbnail_url }}" width="{{ $item.thumbnail_width }}" height="{{ $item.thumbnail_height }}" alt="Instagram Image"></a>
+ <div class="card-body">
+ {{ if not $hideCaption }}<p class="card-text"><a href="{{ $item.author_url | safeURL }}" class="card-link">{{ $item.author_name }}</a> {{ $item.title}}</p>{{ end }}
+ <a href="{{ $item.author_url | safeURL }}" class="card-link">View More on Instagram</a>
+ </div>
+</div>
+{{ end }}
+{{- end -}}
+
+{{ define "__h_simple_instagram_css" }}
+{{ if not (.Page.Scratch.Get "__h_simple_instagram_css") }}
+{{/* Only include once */}}
+{{ .Page.Scratch.Set "__h_simple_instagram_css" true }}
+<style type="text/css">
+ .__h_instagram.card {
+ font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
+ font-size: 14px;
+ border: 1px solid rgb(219, 219, 219);
+ padding: 0;
+ margin-top: 30px;
+ }
+ .__h_instagram.card .card-header, .__h_instagram.card .card-body {
+ padding: 10px 10px 10px;
+ }
+ .__h_instagram.card img {
+ width: 100%;
+ height: auto;
+ }
+</style>
+{{ end }}
+{{ end }}`},
+ {`shortcodes/param.html`, `{{- $name := (.Get 0) -}}
+{{- with $name -}}
+{{- with ($.Page.Param .) }}{{ . }}{{ else }}{{ errorf "Param %q not found: %s" $name $.Position }}{{ end -}}
+{{- else }}{{ errorf "Missing param key: %s" $.Position }}{{ end -}}`},
+ {`shortcodes/ref.html`, `{{ ref . .Params }}`},
+ {`shortcodes/relref.html`, `{{ relref . .Params }}`},
+ {`shortcodes/twitter.html`, `{{- $pc := .Page.Site.Config.Privacy.Twitter -}}
+{{- if not $pc.Disable -}}
+{{- if $pc.Simple -}}
+{{ template "_internal/shortcodes/twitter_simple.html" . }}
+{{- else -}}
+{{- $url := printf "https://api.twitter.com/1/statuses/oembed.json?id=%s&dnt=%t" (index .Params 0) $pc.EnableDNT -}}
+{{- $json := getJSON $url -}}
+{{ $json.html | safeHTML }}
+{{- end -}}
+{{- end -}}`},
+ {`shortcodes/twitter_simple.html`, `{{- $pc := .Page.Site.Config.Privacy.Twitter -}}
+{{- $sc := .Page.Site.Config.Services.Twitter -}}
+{{- if not $pc.Disable -}}
+{{- $id := .Get 0 -}}
+{{- $json := getJSON "https://api.twitter.com/1/statuses/oembed.json?id=" $id "&omit_script=true" -}}
+{{- if not $sc.DisableInlineCSS -}}
+{{ template "__h_simple_twitter_css" $ }}
+{{- end -}}
+{{ $json.html | safeHTML }}
+{{- end -}}
+
+{{ define "__h_simple_twitter_css" }}
+{{ if not (.Page.Scratch.Get "__h_simple_twitter_css") }}
+{{/* Only include once */}}
+{{ .Page.Scratch.Set "__h_simple_twitter_css" true }}
+<style type="text/css">
+ .twitter-tweet {
+ font: 14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
+ border-left: 4px solid #2b7bb9;
+ padding-left: 1.5em;
+ color: #555;
+}
+ .twitter-tweet a {
+ color: #2b7bb9;
+ text-decoration: none;
+}
+ blockquote.twitter-tweet a:hover,
+ blockquote.twitter-tweet a:focus {
+ text-decoration: underline;
+}
+</style>
+{{ end }}
+{{ end }}`},
+ {`shortcodes/vimeo.html`, `{{- $pc := .Page.Site.Config.Privacy.Vimeo -}}
+{{- if not $pc.Disable -}}
+{{- if $pc.Simple -}}
+{{ template "_internal/shortcodes/vimeo_simple.html" . }}
+{{- else -}}
+{{ if .IsNamedParams }}<div {{ if .Get "class" }}class="{{ .Get "class" }}"{{ else }}style="position: relative; padding-bottom: 56.25%; 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%; border:0;" {{ end }}webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
+ </div>{{ else }}
+<div {{ if len .Params | eq 2 }}class="{{ .Get 1 }}"{{ else }}style="position: relative; padding-bottom: 56.25%; 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%; border:0;" {{ end }}webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
+ </div>
+{{ end }}
+{{- end -}}
+{{- end -}}`},
+ {`shortcodes/vimeo_simple.html`, `{{ $id := .Get "id" | default (.Get 0) }}
+{{- $item := getJSON "https://vimeo.com/api/oembed.json?url=https://vimeo.com/" $id -}}
+{{ $class := .Get "class" | default (.Get 1) }}
+{{ $hasClass := $class }}
+{{ $class := $class | default "__h_video" }}
+{{ if not $hasClass }}
+{{/* If class is set, assume the user wants to provide his own styles. */}}
+{{ template "__h_simple_css" $ }}
+{{ end }}
+{{ $secondClass := "s_video_simple" }}
+<div class="{{ $secondClass }} {{ $class }}">
+{{- with $item }}
+<a href="{{ .provider_url }}{{ .video_id }}" target="_blank">
+{{ $thumb := .thumbnail_url }}
+{{ $original := $thumb | replaceRE "(_.*\\.)" "." }}
+<img src="{{ $thumb }}" srcset="{{ $thumb }} 1x, {{ $original }} 2x" alt="{{ .title }}">
+<div class="play">{{ template "__h_simple_icon_play" $ }}</div></a></div>
+{{- end -}}
+`},
+ {`shortcodes/youtube.html`, `{{- $pc := .Page.Site.Config.Privacy.YouTube -}}
+{{- if not $pc.Disable -}}
+{{- $ytHost := cond $pc.PrivacyEnhanced "www.youtube-nocookie.com" "www.youtube.com" -}}
+{{- $id := .Get "id" | default (.Get 0) -}}
+{{- $class := .Get "class" | default (.Get 1) }}
+<div {{ with $class }}class="{{ . }}"{{ else }}style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"{{ end }}>
+ <iframe src="//{{ $ytHost }}/embed/{{ $id }}{{ with .Get "autoplay" }}{{ if eq . "true" }}?autoplay=1{{ end }}{{ end }}" {{ if not $class }}style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" {{ end }}allowfullscreen title="YouTube Video"></iframe>
+</div>
+{{ end -}}
+`},
+ {`twitter_cards.html`, `{{- with $.Params.images -}}
+<meta name="twitter:card" content="summary_large_image"/>
+<meta name="twitter:image" content="{{ index . 0 | absURL }}"/>
+{{ else -}}
+{{- $images := $.Resources.ByType "image" -}}
+{{- $featured := $images.GetMatch "*feature*" -}}
+{{- $featured := cond (ne $featured nil) $featured ($images.GetMatch "{*cover*,*thumbnail*}") -}}
+{{- with $featured -}}
+<meta name="twitter:card" content="summary_large_image"/>
+<meta name="twitter:image" content="{{ $featured.Permalink }}"/>
+{{- else -}}
+{{- with $.Site.Params.images -}}
+<meta name="twitter:card" content="summary_large_image"/>
+<meta name="twitter:image" content="{{ index . 0 | absURL }}"/>
+{{ else -}}
+<meta name="twitter:card" content="summary"/>
+{{- end -}}
+{{- end -}}
+{{- end }}
+<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 -}}`},
+}
diff --git a/tpl/tplimpl/embedded/templates/_default/robots.txt b/tpl/tplimpl/embedded/templates/_default/robots.txt
new file mode 100644
index 000000000..4f9540ba3
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/_default/robots.txt
@@ -0,0 +1 @@
+User-agent: * \ No newline at end of file
diff --git a/tpl/tplimpl/embedded/templates/_default/rss.xml b/tpl/tplimpl/embedded/templates/_default/rss.xml
new file mode 100644
index 000000000..675ecd43c
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/_default/rss.xml
@@ -0,0 +1,32 @@
+{{- $pages := .Data.Pages -}}
+{{- $limit := .Site.Config.Services.RSS.Limit -}}
+{{- if ge $limit 1 -}}
+{{- $pages = $pages | first $limit -}}
+{{- end -}}
+{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>" | safeHTML }}
+<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 $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> \ No newline at end of file
diff --git a/tpl/tplimpl/embedded/templates/_default/sitemap.xml b/tpl/tplimpl/embedded/templates/_default/sitemap.xml
new file mode 100644
index 000000000..f5b44c410
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/_default/sitemap.xml
@@ -0,0 +1,22 @@
+{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>" | safeHTML }}
+<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="{{ .Language.Lang }}"
+ href="{{ .Permalink }}"
+ />{{ end }}
+ <xhtml:link
+ rel="alternate"
+ hreflang="{{ .Language.Lang }}"
+ href="{{ .Permalink }}"
+ />{{ end }}
+ </url>
+ {{ end }}
+</urlset> \ No newline at end of file
diff --git a/tpl/tplimpl/embedded/templates/_default/sitemapindex.xml b/tpl/tplimpl/embedded/templates/_default/sitemapindex.xml
new file mode 100644
index 000000000..60724c7b8
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/_default/sitemapindex.xml
@@ -0,0 +1,11 @@
+{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>" | safeHTML }}
+<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>
diff --git a/tpl/tplimpl/embedded/templates/disqus.html b/tpl/tplimpl/embedded/templates/disqus.html
new file mode 100644
index 000000000..fed512ff0
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/disqus.html
@@ -0,0 +1,23 @@
+{{- $pc := .Site.Config.Privacy.Disqus -}}
+{{- if not $pc.Disable -}}
+{{ if .Site.DisqusShortname }}<div id="disqus_thread"></div>
+<script type="application/javascript">
+ var disqus_config = function () {
+ {{with .Params.disqus_identifier }}this.page.identifier = '{{ . }}';{{end}}
+ {{with .Params.disqus_title }}this.page.title = '{{ . }}';{{end}}
+ {{with .Params.disqus_url }}this.page.url = '{{ . | html }}';{{end}}
+ };
+ (function() {
+ if (["localhost", "127.0.0.1"].indexOf(window.location.hostname) != -1) {
+ document.getElementById('disqus_thread').innerHTML = 'Disqus comments not available by default when the website is previewed locally.';
+ return;
+ }
+ var d = document, s = d.createElement('script'); s.async = true;
+ s.src = '//' + {{ .Site.DisqusShortname }} + '.disqus.com/embed.js';
+ s.setAttribute('data-timestamp', +new Date());
+ (d.head || d.body).appendChild(s);
+ })();
+</script>
+<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
+<a href="https://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>{{end}}
+{{- end -}} \ No newline at end of file
diff --git a/tpl/tplimpl/embedded/templates/google_analytics.html b/tpl/tplimpl/embedded/templates/google_analytics.html
new file mode 100644
index 000000000..97588113e
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/google_analytics.html
@@ -0,0 +1,39 @@
+{{- $pc := .Site.Config.Privacy.GoogleAnalytics -}}
+{{- if not $pc.Disable -}}
+{{ with .Site.GoogleAnalytics }}
+<script type="application/javascript">
+{{ template "__ga_js_set_doNotTrack" $ }}
+if (!doNotTrack) {
+ (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');
+ {{- if $pc.UseSessionStorage }}
+ if (window.sessionStorage) {
+ var GA_SESSION_STORAGE_KEY = 'ga:clientId';
+ ga('create', '{{ . }}', {
+ 'storage': 'none',
+ 'clientId': sessionStorage.getItem(GA_SESSION_STORAGE_KEY)
+ });
+ ga(function(tracker) {
+ sessionStorage.setItem(GA_SESSION_STORAGE_KEY, tracker.get('clientId'));
+ });
+ }
+ {{ else }}
+ ga('create', '{{ . }}', 'auto');
+ {{ end -}}
+ {{ if $pc.AnonymizeIP }}ga('set', 'anonymizeIp', true);{{ end }}
+ ga('send', 'pageview');
+}
+</script>
+{{ end }}
+{{- end -}}
+{{- define "__ga_js_set_doNotTrack" -}}{{/* This is also used in the async version. */}}
+{{- $pc := .Site.Config.Privacy.GoogleAnalytics -}}
+{{- if not $pc.RespectDoNotTrack -}}
+var doNotTrack = false;
+{{- else -}}
+var dnt = (navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack);
+var doNotTrack = (dnt == "1" || dnt == "yes");
+{{- end -}}
+{{- end -}} \ No newline at end of file
diff --git a/tpl/tplimpl/embedded/templates/google_analytics_async.html b/tpl/tplimpl/embedded/templates/google_analytics_async.html
new file mode 100644
index 000000000..499cb6fe3
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/google_analytics_async.html
@@ -0,0 +1,28 @@
+{{- $pc := .Site.Config.Privacy.GoogleAnalytics -}}
+{{- if not $pc.Disable -}}
+{{ with .Site.GoogleAnalytics }}
+<script type="application/javascript">
+{{ template "__ga_js_set_doNotTrack" $ }}
+if (!doNotTrack) {
+ window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
+ {{- if $pc.UseSessionStorage }}
+ if (window.sessionStorage) {
+ var GA_SESSION_STORAGE_KEY = 'ga:clientId';
+ ga('create', '{{ . }}', {
+ 'storage': 'none',
+ 'clientId': sessionStorage.getItem(GA_SESSION_STORAGE_KEY)
+ });
+ ga(function(tracker) {
+ sessionStorage.setItem(GA_SESSION_STORAGE_KEY, tracker.get('clientId'));
+ });
+ }
+ {{ else }}
+ ga('create', '{{ . }}', 'auto');
+ {{ end -}}
+ {{ if $pc.AnonymizeIP }}ga('set', 'anonymizeIp', true);{{ end }}
+ ga('send', 'pageview');
+}
+</script>
+<script async src='https://www.google-analytics.com/analytics.js'></script>
+{{ end }}
+{{- end -}}
diff --git a/tpl/tplimpl/embedded/templates/google_news.html b/tpl/tplimpl/embedded/templates/google_news.html
new file mode 100644
index 000000000..9361de16a
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/google_news.html
@@ -0,0 +1,3 @@
+{{ if .IsPage }}{{ with .Params.news_keywords }}
+ <meta name="news_keywords" content="{{ range $i, $kw := first 10 . }}{{ if $i }},{{ end }}{{ $kw }}{{ end }}" />
+{{ end }}{{ end }} \ No newline at end of file
diff --git a/tpl/tplimpl/embedded/templates/opengraph.html b/tpl/tplimpl/embedded/templates/opengraph.html
new file mode 100644
index 000000000..de2d2fddf
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/opengraph.html
@@ -0,0 +1,49 @@
+<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 $.Param "images" }}{{ range first 6 . }}
+<meta property="og:image" content="{{ . | absURL }}" />
+{{ end }}{{ end }}
+
+{{- $iso8601 := "2006-01-02T15:04:05-07:00" -}}
+{{- if .IsPage }}
+{{- if not .PublishDate.IsZero }}<meta property="article:published_time" {{ .PublishDate.Format $iso8601 | printf "content=%q" | safeHTMLAttr }} />
+{{ else if not .Date.IsZero }}<meta property="article:published_time" {{ .Date.Format $iso8601 | printf "content=%q" | safeHTMLAttr }} />
+{{ end }}
+{{- if not .Lastmod.IsZero }}<meta property="article:modified_time" {{ .Lastmod.Format $iso8601 | printf "content=%q" | safeHTMLAttr }} />{{ end }}
+{{- else }}
+{{- if not .Date.IsZero }}
+<meta property="og:updated_time" {{ .Date.Format $iso8601 | printf "content=%q" | safeHTMLAttr }} />
+{{- end }}
+{{- end }}{{/* .IsPage */}}
+
+{{- 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 . }}
+<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 }}
diff --git a/tpl/tplimpl/embedded/templates/pagination.html b/tpl/tplimpl/embedded/templates/pagination.html
new file mode 100644
index 000000000..1c2d2d82f
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/pagination.html
@@ -0,0 +1,40 @@
+{{ $pag := $.Paginator }}
+{{ if gt $pag.TotalPages 1 }}
+<ul class="pagination">
+ {{ with $pag.First }}
+ <li class="page-item">
+ <a href="{{ .URL }}" class="page-link" aria-label="First"><span aria-hidden="true">&laquo;&laquo;</span></a>
+ </li>
+ {{ end }}
+ <li class="page-item{{ if not $pag.HasPrev }} disabled{{ end }}">
+ <a href="{{ if $pag.HasPrev }}{{ $pag.Prev.URL }}{{ end }}" class="page-link" aria-label="Previous"><span aria-hidden="true">&laquo;</span></a>
+ </li>
+ {{ $ellipsed := false }}
+ {{ $shouldEllipse := 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 }}
+ {{ $ellipsed = false }}
+ {{ $shouldEllipse = false }}
+ {{ else }}
+ {{ $shouldEllipse = not $ellipsed }}
+ {{ $ellipsed = true }}
+ {{ end }}
+ {{ if $showNumber }}
+ <li class="page-item{{ if eq . $pag }} active{{ end }}"><a class="page-link" href="{{ .URL }}">{{ .PageNumber }}</a></li>
+ {{ else if $shouldEllipse }}
+ <li class="page-item disabled"><span aria-hidden="true">&nbsp;&hellip;&nbsp;</span></li>
+ {{ end }}
+ {{ end }}
+ <li class="page-item{{ if not $pag.HasNext }} disabled{{ end }}">
+ <a href="{{ if $pag.HasNext }}{{ $pag.Next.URL }}{{ end }}" class="page-link" aria-label="Next"><span aria-hidden="true">&raquo;</span></a>
+ </li>
+ {{ with $pag.Last }}
+ <li class="page-item">
+ <a href="{{ .URL }}" class="page-link" aria-label="Last"><span aria-hidden="true">&raquo;&raquo;</span></a>
+ </li>
+ {{ end }}
+</ul>
+{{ end }} \ No newline at end of file
diff --git a/tpl/tplimpl/embedded/templates/schema.html b/tpl/tplimpl/embedded/templates/schema.html
new file mode 100644
index 000000000..16c97be60
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/schema.html
@@ -0,0 +1,15 @@
+{{ 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 .Lastmod.IsZero }}<meta itemprop="dateModified" content="{{ .Lastmod.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="{{ if .IsPage}}{{ range $index, $tag := .Params.tags }}{{ $tag }},{{ end }}{{ else }}{{ range $plural, $terms := .Site.Taxonomies }}{{ range $term, $val := $terms }}{{ printf "%s," $term }}{{ end }}{{ end }}{{ end }}" />
+{{ end }} \ No newline at end of file
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/__h_simple_assets.html b/tpl/tplimpl/embedded/templates/shortcodes/__h_simple_assets.html
new file mode 100644
index 000000000..da1bb82eb
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/shortcodes/__h_simple_assets.html
@@ -0,0 +1,34 @@
+{{ define "__h_simple_css" }}{{/* These template definitions are global. */}}
+{{- if not (.Page.Scratch.Get "__h_simple_css") -}}
+{{/* Only include once */}}
+{{- .Page.Scratch.Set "__h_simple_css" true -}}
+<style>
+.__h_video {
+ position: relative;
+ padding-bottom: 56.23%;
+ height: 0;
+ overflow: hidden;
+ width: 100%;
+ background: #000;
+}
+.__h_video img {
+ width: 100%;
+ height: auto;
+ color: #000;
+}
+.__h_video .play {
+ height: 72px;
+ width: 72px;
+ left: 50%;
+ top: 50%;
+ margin-left: -36px;
+ margin-top: -36px;
+ position: absolute;
+ cursor: pointer;
+}
+</style>
+{{- end -}}
+{{- end -}}
+{{- define "__h_simple_icon_play" -}}
+<svg version="1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 61 61"><circle cx="30.5" cy="30.5" r="30.5" opacity=".8" fill="#000"></circle><path d="M25.3 19.2c-2.1-1.2-3.8-.2-3.8 2.2v18.1c0 2.4 1.7 3.4 3.8 2.2l16.6-9.1c2.1-1.2 2.1-3.2 0-4.4l-16.6-9z" fill="#fff"></path></svg>
+{{- end -}}
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/figure.html b/tpl/tplimpl/embedded/templates/shortcodes/figure.html
new file mode 100644
index 000000000..f81bdadfc
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/shortcodes/figure.html
@@ -0,0 +1,28 @@
+<figure{{ with .Get "class" }} class="{{ . }}"{{ end }}>
+ {{- if .Get "link" -}}
+ <a href="{{ .Get "link" }}"{{ with .Get "target" }} target="{{ . }}"{{ end }}{{ with .Get "rel" }} rel="{{ . }}"{{ end }}>
+ {{- end }}
+ <img src="{{ .Get "src" }}"
+ {{- if or (.Get "alt") (.Get "caption") }}
+ alt="{{ with .Get "alt" }}{{ . }}{{ else }}{{ .Get "caption" | markdownify| plainify }}{{ end }}"
+ {{- end -}}
+ {{- with .Get "width" }} width="{{ . }}"{{ end -}}
+ {{- with .Get "height" }} height="{{ . }}"{{ end -}}
+ /> <!-- Closing img tag -->
+ {{- if .Get "link" }}</a>{{ end -}}
+ {{- if or (or (.Get "title") (.Get "caption")) (.Get "attr") -}}
+ <figcaption>
+ {{ with (.Get "title") -}}
+ <h4>{{ . }}</h4>
+ {{- end -}}
+ {{- if or (.Get "caption") (.Get "attr") -}}<p>
+ {{- .Get "caption" | markdownify -}}
+ {{- with .Get "attrlink" }}
+ <a href="{{ . }}">
+ {{- end -}}
+ {{- .Get "attr" | markdownify -}}
+ {{- if .Get "attrlink" }}</a>{{ end }}</p>
+ {{- end }}
+ </figcaption>
+ {{- end }}
+</figure>
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/gist.html b/tpl/tplimpl/embedded/templates/shortcodes/gist.html
new file mode 100644
index 000000000..97ef54838
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/shortcodes/gist.html
@@ -0,0 +1 @@
+<script type="application/javascript" src="//gist.github.com/{{ index .Params 0 }}/{{ index .Params 1 }}.js{{if len .Params | eq 3 }}?file={{ index .Params 2 }}{{end}}"></script> \ No newline at end of file
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/highlight.html b/tpl/tplimpl/embedded/templates/shortcodes/highlight.html
new file mode 100644
index 000000000..b063f92ad
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/shortcodes/highlight.html
@@ -0,0 +1 @@
+{{ if len .Params | eq 2 }}{{ highlight (trim .Inner "\n\r") (.Get 0) (.Get 1) }}{{ else }}{{ highlight (trim .Inner "\n\r") (.Get 0) "" }}{{ end }} \ No newline at end of file
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/instagram.html b/tpl/tplimpl/embedded/templates/shortcodes/instagram.html
new file mode 100644
index 000000000..67ff2e72c
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/shortcodes/instagram.html
@@ -0,0 +1,10 @@
+{{- $pc := .Page.Site.Config.Privacy.Instagram -}}
+{{- if not $pc.Disable -}}
+{{- if $pc.Simple -}}
+{{ template "_internal/shortcodes/instagram_simple.html" . }}
+{{- else -}}
+{{ $id := .Get 0 }}
+{{ $hideCaption := cond (eq (.Get 1) "hidecaption") "1" "0" }}
+{{ with getJSON "https://api.instagram.com/oembed/?url=https://instagram.com/p/" $id "/&hidecaption=" $hideCaption }}{{ .html | safeHTML }}{{ end }}
+{{- end -}}
+{{- end -}} \ No newline at end of file
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/instagram_simple.html b/tpl/tplimpl/embedded/templates/shortcodes/instagram_simple.html
new file mode 100644
index 000000000..075fe980f
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/shortcodes/instagram_simple.html
@@ -0,0 +1,48 @@
+{{- $pc := .Page.Site.Config.Privacy.Instagram -}}
+{{- $sc := .Page.Site.Config.Services.Instagram -}}
+{{- if not $pc.Disable -}}
+{{- $id := .Get 0 -}}
+{{- $item := getJSON "https://api.instagram.com/oembed/?url=https://www.instagram.com/p/" $id "/&amp;maxwidth=640&amp;omitscript=true" -}}
+{{- $class1 := "__h_instagram" -}}
+{{- $class2 := "s_instagram_simple" -}}
+{{- $hideCaption := (eq (.Get 1) "hidecaption") -}}
+{{ with $item }}
+{{- $mediaURL := printf "https://instagram.com/p/%s/" $id | safeURL -}}
+{{- if not $sc.DisableInlineCSS -}}
+{{ template "__h_simple_instagram_css" $ }}
+{{- end -}}
+<div class="{{ $class1 }} {{ $class2 }} card" style="max-width: {{ $item.thumbnail_width }}px">
+ <div class="card-header">
+ <a href="{{ $item.author_url | safeURL }}" class="card-link">{{ $item.author_name }}</a>
+ </div>
+ <a href="{{ $mediaURL }}" target="_blank"><img class="card-img-top img-fluid" src="{{ $item.thumbnail_url }}" width="{{ $item.thumbnail_width }}" height="{{ $item.thumbnail_height }}" alt="Instagram Image"></a>
+ <div class="card-body">
+ {{ if not $hideCaption }}<p class="card-text"><a href="{{ $item.author_url | safeURL }}" class="card-link">{{ $item.author_name }}</a> {{ $item.title}}</p>{{ end }}
+ <a href="{{ $item.author_url | safeURL }}" class="card-link">View More on Instagram</a>
+ </div>
+</div>
+{{ end }}
+{{- end -}}
+
+{{ define "__h_simple_instagram_css" }}
+{{ if not (.Page.Scratch.Get "__h_simple_instagram_css") }}
+{{/* Only include once */}}
+{{ .Page.Scratch.Set "__h_simple_instagram_css" true }}
+<style type="text/css">
+ .__h_instagram.card {
+ font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
+ font-size: 14px;
+ border: 1px solid rgb(219, 219, 219);
+ padding: 0;
+ margin-top: 30px;
+ }
+ .__h_instagram.card .card-header, .__h_instagram.card .card-body {
+ padding: 10px 10px 10px;
+ }
+ .__h_instagram.card img {
+ width: 100%;
+ height: auto;
+ }
+</style>
+{{ end }}
+{{ end }} \ No newline at end of file
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/param.html b/tpl/tplimpl/embedded/templates/shortcodes/param.html
new file mode 100644
index 000000000..74aa3ee7b
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/shortcodes/param.html
@@ -0,0 +1,4 @@
+{{- $name := (.Get 0) -}}
+{{- with $name -}}
+{{- with ($.Page.Param .) }}{{ . }}{{ else }}{{ errorf "Param %q not found: %s" $name $.Position }}{{ end -}}
+{{- else }}{{ errorf "Missing param key: %s" $.Position }}{{ end -}} \ No newline at end of file
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/ref.html b/tpl/tplimpl/embedded/templates/shortcodes/ref.html
new file mode 100644
index 000000000..cd9c3defc
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/shortcodes/ref.html
@@ -0,0 +1 @@
+{{ ref . .Params }} \ No newline at end of file
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/relref.html b/tpl/tplimpl/embedded/templates/shortcodes/relref.html
new file mode 100644
index 000000000..82005bd82
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/shortcodes/relref.html
@@ -0,0 +1 @@
+{{ relref . .Params }} \ No newline at end of file
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/twitter.html b/tpl/tplimpl/embedded/templates/shortcodes/twitter.html
new file mode 100644
index 000000000..ea7f10c38
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/shortcodes/twitter.html
@@ -0,0 +1,10 @@
+{{- $pc := .Page.Site.Config.Privacy.Twitter -}}
+{{- if not $pc.Disable -}}
+{{- if $pc.Simple -}}
+{{ template "_internal/shortcodes/twitter_simple.html" . }}
+{{- else -}}
+{{- $url := printf "https://api.twitter.com/1/statuses/oembed.json?id=%s&dnt=%t" (index .Params 0) $pc.EnableDNT -}}
+{{- $json := getJSON $url -}}
+{{ $json.html | safeHTML }}
+{{- end -}}
+{{- end -}} \ No newline at end of file
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/twitter_simple.html b/tpl/tplimpl/embedded/templates/shortcodes/twitter_simple.html
new file mode 100644
index 000000000..45d594fd9
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/shortcodes/twitter_simple.html
@@ -0,0 +1,33 @@
+{{- $pc := .Page.Site.Config.Privacy.Twitter -}}
+{{- $sc := .Page.Site.Config.Services.Twitter -}}
+{{- if not $pc.Disable -}}
+{{- $id := .Get 0 -}}
+{{- $json := getJSON "https://api.twitter.com/1/statuses/oembed.json?id=" $id "&omit_script=true" -}}
+{{- if not $sc.DisableInlineCSS -}}
+{{ template "__h_simple_twitter_css" $ }}
+{{- end -}}
+{{ $json.html | safeHTML }}
+{{- end -}}
+
+{{ define "__h_simple_twitter_css" }}
+{{ if not (.Page.Scratch.Get "__h_simple_twitter_css") }}
+{{/* Only include once */}}
+{{ .Page.Scratch.Set "__h_simple_twitter_css" true }}
+<style type="text/css">
+ .twitter-tweet {
+ font: 14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
+ border-left: 4px solid #2b7bb9;
+ padding-left: 1.5em;
+ color: #555;
+}
+ .twitter-tweet a {
+ color: #2b7bb9;
+ text-decoration: none;
+}
+ blockquote.twitter-tweet a:hover,
+ blockquote.twitter-tweet a:focus {
+ text-decoration: underline;
+}
+</style>
+{{ end }}
+{{ end }} \ No newline at end of file
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/vimeo.html b/tpl/tplimpl/embedded/templates/shortcodes/vimeo.html
new file mode 100644
index 000000000..2929aa4ab
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/shortcodes/vimeo.html
@@ -0,0 +1,14 @@
+{{- $pc := .Page.Site.Config.Privacy.Vimeo -}}
+{{- if not $pc.Disable -}}
+{{- if $pc.Simple -}}
+{{ template "_internal/shortcodes/vimeo_simple.html" . }}
+{{- else -}}
+{{ if .IsNamedParams }}<div {{ if .Get "class" }}class="{{ .Get "class" }}"{{ else }}style="position: relative; padding-bottom: 56.25%; 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%; border:0;" {{ end }}webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
+ </div>{{ else }}
+<div {{ if len .Params | eq 2 }}class="{{ .Get 1 }}"{{ else }}style="position: relative; padding-bottom: 56.25%; 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%; border:0;" {{ end }}webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
+ </div>
+{{ end }}
+{{- end -}}
+{{- end -}} \ No newline at end of file
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/vimeo_simple.html b/tpl/tplimpl/embedded/templates/shortcodes/vimeo_simple.html
new file mode 100644
index 000000000..50699ecd4
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/shortcodes/vimeo_simple.html
@@ -0,0 +1,18 @@
+{{ $id := .Get "id" | default (.Get 0) }}
+{{- $item := getJSON "https://vimeo.com/api/oembed.json?url=https://vimeo.com/" $id -}}
+{{ $class := .Get "class" | default (.Get 1) }}
+{{ $hasClass := $class }}
+{{ $class := $class | default "__h_video" }}
+{{ if not $hasClass }}
+{{/* If class is set, assume the user wants to provide his own styles. */}}
+{{ template "__h_simple_css" $ }}
+{{ end }}
+{{ $secondClass := "s_video_simple" }}
+<div class="{{ $secondClass }} {{ $class }}">
+{{- with $item }}
+<a href="{{ .provider_url }}{{ .video_id }}" target="_blank">
+{{ $thumb := .thumbnail_url }}
+{{ $original := $thumb | replaceRE "(_.*\\.)" "." }}
+<img src="{{ $thumb }}" srcset="{{ $thumb }} 1x, {{ $original }} 2x" alt="{{ .title }}">
+<div class="play">{{ template "__h_simple_icon_play" $ }}</div></a></div>
+{{- end -}}
diff --git a/tpl/tplimpl/embedded/templates/shortcodes/youtube.html b/tpl/tplimpl/embedded/templates/shortcodes/youtube.html
new file mode 100644
index 000000000..ff268c511
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/shortcodes/youtube.html
@@ -0,0 +1,9 @@
+{{- $pc := .Page.Site.Config.Privacy.YouTube -}}
+{{- if not $pc.Disable -}}
+{{- $ytHost := cond $pc.PrivacyEnhanced "www.youtube-nocookie.com" "www.youtube.com" -}}
+{{- $id := .Get "id" | default (.Get 0) -}}
+{{- $class := .Get "class" | default (.Get 1) }}
+<div {{ with $class }}class="{{ . }}"{{ else }}style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"{{ end }}>
+ <iframe src="//{{ $ytHost }}/embed/{{ $id }}{{ with .Get "autoplay" }}{{ if eq . "true" }}?autoplay=1{{ end }}{{ end }}" {{ if not $class }}style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" {{ end }}allowfullscreen title="YouTube Video"></iframe>
+</div>
+{{ end -}}
diff --git a/tpl/tplimpl/embedded/templates/twitter_cards.html b/tpl/tplimpl/embedded/templates/twitter_cards.html
new file mode 100644
index 000000000..fc4895b56
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/twitter_cards.html
@@ -0,0 +1,29 @@
+{{- with $.Params.images -}}
+<meta name="twitter:card" content="summary_large_image"/>
+<meta name="twitter:image" content="{{ index . 0 | absURL }}"/>
+{{ else -}}
+{{- $images := $.Resources.ByType "image" -}}
+{{- $featured := $images.GetMatch "*feature*" -}}
+{{- $featured := cond (ne $featured nil) $featured ($images.GetMatch "{*cover*,*thumbnail*}") -}}
+{{- with $featured -}}
+<meta name="twitter:card" content="summary_large_image"/>
+<meta name="twitter:image" content="{{ $featured.Permalink }}"/>
+{{- else -}}
+{{- with $.Site.Params.images -}}
+<meta name="twitter:card" content="summary_large_image"/>
+<meta name="twitter:image" content="{{ index . 0 | absURL }}"/>
+{{ else -}}
+<meta name="twitter:card" content="summary"/>
+{{- end -}}
+{{- end -}}
+{{- end }}
+<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 -}} \ No newline at end of file
diff --git a/tpl/tplimpl/shortcodes.go b/tpl/tplimpl/shortcodes.go
new file mode 100644
index 000000000..92c25f108
--- /dev/null
+++ b/tpl/tplimpl/shortcodes.go
@@ -0,0 +1,160 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "strings"
+
+ "github.com/gohugoio/hugo/tpl"
+)
+
+// Currently lang, outFormat, suffix
+const numTemplateVariants = 3
+
+type shortcodeVariant struct {
+
+ // The possible variants: lang, outFormat, suffix
+ // gtag
+ // gtag.html
+ // gtag.no.html
+ // gtag.no.amp.html
+ // A slice of length numTemplateVariants.
+ variants []string
+
+ info tpl.Info
+ templ tpl.Template
+}
+
+type shortcodeTemplates struct {
+ variants []shortcodeVariant
+}
+
+func (s *shortcodeTemplates) indexOf(variants []string) int {
+L:
+ for i, v1 := range s.variants {
+ for i, v2 := range v1.variants {
+ if v2 != variants[i] {
+ continue L
+ }
+ }
+ return i
+ }
+ return -1
+}
+
+func (s *shortcodeTemplates) fromVariants(variants tpl.TemplateVariants) (shortcodeVariant, bool) {
+ return s.fromVariantsSlice([]string{
+ variants.Language,
+ strings.ToLower(variants.OutputFormat.Name),
+ variants.OutputFormat.MediaType.Suffix(),
+ })
+}
+
+// Get the most specific template given a full name, e.g gtag.no.amp.html.
+func (s *shortcodeTemplates) fromName(name string) (shortcodeVariant, bool) {
+ return s.fromVariantsSlice(templateVariants(name))
+}
+
+func (s *shortcodeTemplates) fromVariantsSlice(variants []string) (shortcodeVariant, bool) {
+ var (
+ bestMatch shortcodeVariant
+ bestMatchWeight int
+ )
+
+ for _, variant := range s.variants {
+ w := s.compareVariants(variants, variant.variants)
+ if bestMatchWeight == 0 || w > bestMatchWeight {
+ bestMatch = variant
+ bestMatchWeight = w
+ }
+ }
+
+ return bestMatch, true
+}
+
+// calculate a weight for two string slices of same lenght.
+// higher value means "better match".
+func (s *shortcodeTemplates) compareVariants(a, b []string) int {
+
+ weight := 0
+ for i, av := range a {
+ bv := b[i]
+ if av == bv {
+ weight++
+ } else {
+ weight--
+ }
+ }
+ return weight
+}
+
+func templateVariants(name string) []string {
+ _, variants := templateNameAndVariants(name)
+ return variants
+}
+
+func templateNameAndVariants(name string) (string, []string) {
+
+ variants := make([]string, numTemplateVariants)
+
+ parts := strings.Split(name, ".")
+
+ if len(parts) <= 1 {
+ // No variants.
+ return name, variants
+ }
+
+ name = parts[0]
+ parts = parts[1:]
+ lp := len(parts)
+ start := len(variants) - lp
+
+ for i, j := start, 0; i < len(variants); i, j = i+1, j+1 {
+ variants[i] = parts[j]
+ }
+
+ if lp > 1 && lp < len(variants) {
+ for i := lp - 1; i > 0; i-- {
+ variants[i-1] = variants[i]
+ }
+ }
+
+ if lp == 1 {
+ // Suffix only. Duplicate it into the output format field to
+ // make HTML win over AMP.
+ variants[len(variants)-2] = variants[len(variants)-1]
+ }
+
+ return name, variants
+}
+
+func resolveTemplateType(name string) templateType {
+ if isShortcode(name) {
+ return templateShortcode
+ }
+
+ if strings.Contains(name, "partials/") {
+ return templatePartial
+ }
+
+ return templateUndefined
+}
+
+func isShortcode(name string) bool {
+ return strings.Contains(name, shortcodesPathPrefix)
+}
+
+func isInternal(name string) bool {
+ return strings.HasPrefix(name, internalPathPrefix)
+}
diff --git a/tpl/tplimpl/shortcodes_test.go b/tpl/tplimpl/shortcodes_test.go
new file mode 100644
index 000000000..da30d4149
--- /dev/null
+++ b/tpl/tplimpl/shortcodes_test.go
@@ -0,0 +1,98 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestShortcodesTemplate(t *testing.T) {
+
+ t.Run("isShortcode", func(t *testing.T) {
+ assert := require.New(t)
+ assert.True(isShortcode("shortcodes/figures.html"))
+ assert.True(isShortcode("_internal/shortcodes/figures.html"))
+ assert.False(isShortcode("shortcodes\\figures.html"))
+ assert.False(isShortcode("myshortcodes"))
+
+ })
+
+ t.Run("variantsFromName", func(t *testing.T) {
+ assert := require.New(t)
+ assert.Equal([]string{"", "html", "html"}, templateVariants("figure.html"))
+ assert.Equal([]string{"no", "no", "html"}, templateVariants("figure.no.html"))
+ assert.Equal([]string{"no", "amp", "html"}, templateVariants("figure.no.amp.html"))
+ assert.Equal([]string{"amp", "amp", "html"}, templateVariants("figure.amp.html"))
+
+ name, variants := templateNameAndVariants("figure.html")
+ assert.Equal("figure", name)
+ assert.Equal([]string{"", "html", "html"}, variants)
+
+ })
+
+ t.Run("compareVariants", func(t *testing.T) {
+ assert := require.New(t)
+ var s *shortcodeTemplates
+
+ tests := []struct {
+ name string
+ name1 string
+ name2 string
+ expected int
+ }{
+ {"Same suffix", "figure.html", "figure.html", 3},
+ {"Same suffix and output format", "figure.html.html", "figure.html.html", 3},
+ {"Same suffix, output format and language", "figure.no.html.html", "figure.no.html.html", 3},
+ {"No suffix", "figure", "figure", 3},
+ {"Different output format", "figure.amp.html", "figure.html.html", -1},
+ {"One with output format, one without", "figure.amp.html", "figure.html", -1},
+ }
+
+ for i, test := range tests {
+ w := s.compareVariants(templateVariants(test.name1), templateVariants(test.name2))
+ assert.Equal(test.expected, w, fmt.Sprintf("[%d] %s", i, test.name))
+ }
+
+ })
+
+ t.Run("indexOf", func(t *testing.T) {
+ assert := require.New(t)
+
+ s := &shortcodeTemplates{
+ variants: []shortcodeVariant{
+ {variants: []string{"a", "b", "c"}},
+ {variants: []string{"a", "b", "d"}},
+ },
+ }
+
+ assert.Equal(0, s.indexOf([]string{"a", "b", "c"}))
+ assert.Equal(1, s.indexOf([]string{"a", "b", "d"}))
+ assert.Equal(-1, s.indexOf([]string{"a", "b", "x"}))
+
+ })
+
+ t.Run("Name", func(t *testing.T) {
+ assert := require.New(t)
+
+ assert.Equal("foo.html", templateBaseName(templateShortcode, "shortcodes/foo.html"))
+ assert.Equal("foo.html", templateBaseName(templateShortcode, "_internal/shortcodes/foo.html"))
+ assert.Equal("test/foo.html", templateBaseName(templateShortcode, "shortcodes/test/foo.html"))
+
+ assert.True(true)
+
+ })
+}
diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go
new file mode 100644
index 000000000..f0d3066e2
--- /dev/null
+++ b/tpl/tplimpl/template.go
@@ -0,0 +1,1057 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "text/template/parse"
+
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/tpl/tplimpl/embedded"
+ "github.com/pkg/errors"
+
+ "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)
+ _ 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) (templateInfo, error)) error
+ addTemplate(name, tpl string) (*templateContext, error)
+ addLateTemplate(name, tpl string) error
+}
+
+type templateFuncsterTemplater interface {
+ templateFuncsterSetter
+ tpl.TemplateFinder
+ setFuncs(funcMap map[string]interface{})
+}
+
+type templateFuncsterSetter interface {
+ setTemplateFuncster(f *templateFuncster)
+}
+
+// templateHandler holds the templates in play.
+// It implements the templateLoader and tpl.TemplateHandler interfaces.
+type templateHandler struct {
+ mu sync.Mutex
+
+ // shortcodes maps shortcode name to template variants
+ // (language, output format etc.) of that shortcode.
+ shortcodes map[string]*shortcodeTemplates
+
+ // templateInfo maps template name to some additional information about that template.
+ // Note that for shortcodes that same information is embedded in the
+ // shortcodeTemplates type.
+ templateInfo map[string]tpl.Info
+
+ // text holds all the pure text templates.
+ text *textTemplates
+ html *htmlTemplates
+
+ extTextTemplates []*textTemplate
+
+ amberFuncMap template.FuncMap
+
+ errors []*templateErr
+
+ // This is the filesystem to load the templates from. All the templates are
+ // stored in the root of this filesystem.
+ layoutsFs afero.Fs
+
+ *deps.Deps
+}
+
+const (
+ shortcodesPathPrefix = "shortcodes/"
+ internalPathPrefix = "_internal/"
+)
+
+// resolves _internal/shortcodes/param.html => param.html etc.
+func templateBaseName(typ templateType, name string) string {
+ name = strings.TrimPrefix(name, internalPathPrefix)
+ switch typ {
+ case templateShortcode:
+ return strings.TrimPrefix(name, shortcodesPathPrefix)
+ default:
+ panic("not implemented")
+ }
+
+}
+
+func (t *templateHandler) addShortcodeVariant(name string, info tpl.Info, templ tpl.Template) {
+ base := templateBaseName(templateShortcode, name)
+
+ shortcodename, variants := templateNameAndVariants(base)
+
+ templs, found := t.shortcodes[shortcodename]
+ if !found {
+ templs = &shortcodeTemplates{}
+ t.shortcodes[shortcodename] = templs
+ }
+
+ sv := shortcodeVariant{variants: variants, info: info, templ: templ}
+
+ i := templs.indexOf(variants)
+
+ if i != -1 {
+ // Only replace if it's an override of an internal template.
+ if !isInternal(name) {
+ templs.variants[i] = sv
+ }
+ } else {
+ templs.variants = append(templs.variants, sv)
+ }
+}
+
+// NewTextTemplate provides a text template parser that has all the Hugo
+// template funcs etc. built-in.
+func (t *templateHandler) NewTextTemplate() tpl.TemplateParseFinder {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ tt := &textTemplate{t: texttemplate.New("")}
+ t.extTextTemplates = append(t.extTextTemplates, tt)
+
+ return struct {
+ tpl.TemplateParser
+ tpl.TemplateLookup
+ tpl.TemplateLookupVariant
+ }{
+ tt,
+ tt,
+ new(nopLookupVariant),
+ }
+
+}
+
+type nopLookupVariant int
+
+func (l nopLookupVariant) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
+ return nil, false, false
+}
+
+func (t *templateHandler) Debug() {
+ fmt.Println("HTML templates:\n", t.html.t.DefinedTemplates())
+ fmt.Println("\n\nText templates:\n", t.text.t.DefinedTemplates())
+}
+
+// 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.Template, bool) {
+
+ 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.applyTemplateInfo(t.text.Lookup(name))
+ }
+
+ // Look in both
+ if te, found := t.html.Lookup(name); found {
+ return t.applyTemplateInfo(te, true)
+ }
+
+ return t.applyTemplateInfo(t.text.Lookup(name))
+
+}
+
+func (t *templateHandler) applyTemplateInfo(templ tpl.Template, found bool) (tpl.Template, bool) {
+ if adapter, ok := templ.(*tpl.TemplateAdapter); ok {
+ if adapter.Info.IsZero() {
+ if info, found := t.templateInfo[templ.Name()]; found {
+ adapter.Info = info
+ }
+ }
+ }
+
+ return templ, found
+}
+
+// This currently only applies to shortcodes and what we get here is the
+// shortcode name.
+func (t *templateHandler) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
+ name = templateBaseName(templateShortcode, name)
+ s, found := t.shortcodes[name]
+ if !found {
+ return nil, false, false
+ }
+
+ sv, found := s.fromVariants(variants)
+ if !found {
+ return nil, false, false
+ }
+
+ more := len(s.variants) > 1
+
+ return &tpl.TemplateAdapter{
+ Template: sv.templ,
+ Info: sv.info,
+ Metrics: t.Deps.Metrics,
+ Fs: t.layoutsFs,
+ NameBaseTemplateName: t.html.nameBaseTemplateName}, true, more
+
+}
+
+func (t *textTemplates) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
+ return t.handler.LookupVariant(name, variants)
+}
+
+func (t *htmlTemplates) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
+ return t.handler.LookupVariant(name, variants)
+}
+
+func (t *templateHandler) lookupTemplate(in interface{}) tpl.Template {
+ switch templ := in.(type) {
+ case *texttemplate.Template:
+ return t.text.lookup(templ.Name())
+ case *template.Template:
+ return t.html.lookup(templ.Name())
+ }
+
+ panic(fmt.Sprintf("%T is not a template", in))
+}
+
+func (t *templateHandler) setFuncMapInTemplate(in interface{}, funcs map[string]interface{}) {
+ switch templ := in.(type) {
+ case *texttemplate.Template:
+ templ.Funcs(funcs)
+ return
+ case *template.Template:
+ templ.Funcs(funcs)
+ return
+ }
+
+ panic(fmt.Sprintf("%T is not a template", in))
+}
+
+func (t *templateHandler) clone(d *deps.Deps) *templateHandler {
+ c := &templateHandler{
+ Deps: d,
+ layoutsFs: d.BaseFs.Layouts.Fs,
+ shortcodes: make(map[string]*shortcodeTemplates),
+ templateInfo: t.templateInfo,
+ html: &htmlTemplates{t: template.Must(t.html.t.Clone()), overlays: make(map[string]*template.Template), templatesCommon: t.html.templatesCommon},
+ text: &textTemplates{textTemplate: &textTemplate{t: texttemplate.Must(t.text.t.Clone())}, overlays: make(map[string]*texttemplate.Template), templatesCommon: t.text.templatesCommon},
+ errors: make([]*templateErr, 0),
+ }
+
+ for k, v := range t.shortcodes {
+ other := *v
+ variantsc := make([]shortcodeVariant, len(v.variants))
+ for i, variant := range v.variants {
+ variantsc[i] = shortcodeVariant{
+ info: variant.info,
+ variants: variant.variants,
+ templ: c.lookupTemplate(variant.templ),
+ }
+ }
+ other.variants = variantsc
+ c.shortcodes[k] = &other
+ }
+
+ 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 {
+ common := &templatesCommon{
+ nameBaseTemplateName: make(map[string]string),
+ transformNotFound: make(map[string]bool),
+ }
+
+ htmlT := &htmlTemplates{
+ t: template.New(""),
+ overlays: make(map[string]*template.Template),
+ templatesCommon: common,
+ }
+ textT := &textTemplates{
+ textTemplate: &textTemplate{t: texttemplate.New("")},
+ overlays: make(map[string]*texttemplate.Template),
+ templatesCommon: common,
+ }
+ h := &templateHandler{
+ Deps: deps,
+ layoutsFs: deps.BaseFs.Layouts.Fs,
+ shortcodes: make(map[string]*shortcodeTemplates),
+ templateInfo: make(map[string]tpl.Info),
+ html: htmlT,
+ text: textT,
+ errors: make([]*templateErr, 0),
+ }
+
+ common.handler = h
+
+ return h
+
+}
+
+// Shared by both HTML and text templates.
+type templatesCommon struct {
+ handler *templateHandler
+ funcster *templateFuncster
+
+ // Used to get proper filenames in errors
+ nameBaseTemplateName map[string]string
+
+ // Holds names of the templates not found during the first AST transformation
+ // pass.
+ transformNotFound map[string]bool
+}
+type htmlTemplates struct {
+ mu sync.RWMutex
+
+ *templatesCommon
+
+ 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.Template, bool) {
+ templ := t.lookup(name)
+ if templ == nil {
+ return nil, false
+ }
+
+ return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics, Fs: t.handler.layoutsFs, NameBaseTemplateName: t.nameBaseTemplateName}, true
+}
+
+func (t *htmlTemplates) lookup(name string) *template.Template {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ // Need to check in the overlay registry first as it will also be found below.
+ if t.overlays != nil {
+ if templ, ok := t.overlays[name]; ok {
+ return templ
+ }
+ }
+
+ if templ := t.t.Lookup(name); templ != nil {
+ return templ
+ }
+
+ if t.clone != nil {
+ return t.clone.Lookup(name)
+ }
+
+ return nil
+}
+
+func (t *textTemplates) setTemplateFuncster(f *templateFuncster) {
+ t.funcster = f
+}
+
+type textTemplates struct {
+ *templatesCommon
+ *textTemplate
+ clone *texttemplate.Template
+ cloneClone *texttemplate.Template
+
+ overlays map[string]*texttemplate.Template
+}
+
+func (t *textTemplates) Lookup(name string) (tpl.Template, bool) {
+ templ := t.lookup(name)
+ if templ == nil {
+ return nil, false
+ }
+ return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics, Fs: t.handler.layoutsFs, NameBaseTemplateName: t.nameBaseTemplateName}, true
+}
+
+func (t *textTemplates) lookup(name string) *texttemplate.Template {
+
+ // Need to check in the overlay registry first as it will also be found below.
+ if t.overlays != nil {
+ if templ, ok := t.overlays[name]; ok {
+ return templ
+ }
+ }
+
+ if templ := t.t.Lookup(name); templ != nil {
+ 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 from the layouts filesystem.
+// A prefix can be given to indicate a template namespace to load the templates
+// into, i.e. "_internal" etc.
+func (t *templateHandler) LoadTemplates(prefix string) error {
+ return t.loadTemplates(prefix)
+
+}
+
+func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, tpl string) (*templateContext, error) {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ templ, err := tt.New(name).Parse(tpl)
+ if err != nil {
+ return nil, err
+ }
+
+ typ := resolveTemplateType(name)
+
+ c, err := applyTemplateTransformersToHMLTTemplate(typ, templ)
+ if err != nil {
+ return nil, err
+ }
+
+ for k, _ := range c.notFound {
+ t.transformNotFound[k] = true
+ }
+
+ if typ == templateShortcode {
+ t.handler.addShortcodeVariant(name, c.Info, templ)
+ } else {
+ t.handler.templateInfo[name] = c.Info
+ }
+
+ return c, nil
+}
+
+func (t *htmlTemplates) addTemplate(name, tpl string) (*templateContext, error) {
+ return t.addTemplateIn(t.t, name, tpl)
+}
+
+func (t *htmlTemplates) addLateTemplate(name, tpl string) error {
+ _, err := t.addTemplateIn(t.clone, name, tpl)
+ return err
+}
+
+type textTemplate struct {
+ mu sync.RWMutex
+ t *texttemplate.Template
+}
+
+func (t *textTemplate) Parse(name, tpl string) (tpl.Template, error) {
+ return t.parseIn(t.t, name, tpl)
+}
+
+func (t *textTemplate) Lookup(name string) (tpl.Template, bool) {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ tpl := t.t.Lookup(name)
+ return tpl, tpl != nil
+}
+
+func (t *textTemplate) parseIn(tt *texttemplate.Template, name, tpl string) (*texttemplate.Template, error) {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ templ, err := tt.New(name).Parse(tpl)
+ if err != nil {
+ return nil, err
+ }
+
+ if _, err := applyTemplateTransformersToTextTemplate(templateUndefined, templ); err != nil {
+ return nil, err
+ }
+ return templ, nil
+}
+
+func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tpl string) (*templateContext, error) {
+ name = strings.TrimPrefix(name, textTmplNamePrefix)
+ templ, err := t.parseIn(tt, name, tpl)
+ if err != nil {
+ return nil, err
+ }
+
+ typ := resolveTemplateType(name)
+
+ c, err := applyTemplateTransformersToTextTemplate(typ, templ)
+ if err != nil {
+ return nil, err
+ }
+
+ for k, _ := range c.notFound {
+ t.transformNotFound[k] = true
+ }
+
+ if typ == templateShortcode {
+ t.handler.addShortcodeVariant(name, c.Info, templ)
+ } else {
+ t.handler.templateInfo[name] = c.Info
+ }
+
+ return c, nil
+}
+
+func (t *textTemplates) addTemplate(name, tpl string) (*templateContext, error) {
+ return t.addTemplateIn(t.t, name, tpl)
+}
+
+func (t *textTemplates) addLateTemplate(name, tpl string) error {
+ _, err := t.addTemplateIn(t.clone, name, tpl)
+ return err
+}
+
+func (t *templateHandler) addTemplate(name, tpl string) error {
+ return t.AddTemplate(name, tpl)
+}
+
+func (t *templateHandler) postTransform() error {
+ if len(t.html.transformNotFound) == 0 && len(t.text.transformNotFound) == 0 {
+ return nil
+ }
+
+ defer func() {
+ t.text.transformNotFound = make(map[string]bool)
+ t.html.transformNotFound = make(map[string]bool)
+ }()
+
+ for _, s := range []struct {
+ lookup func(name string) *parse.Tree
+ transformNotFound map[string]bool
+ }{
+ // html templates
+ {func(name string) *parse.Tree {
+ templ := t.html.lookup(name)
+ if templ == nil {
+ return nil
+ }
+ return templ.Tree
+ }, t.html.transformNotFound},
+ // text templates
+ {func(name string) *parse.Tree {
+ templT := t.text.lookup(name)
+ if templT == nil {
+ return nil
+ }
+ return templT.Tree
+ }, t.text.transformNotFound},
+ } {
+ for name, _ := range s.transformNotFound {
+ templ := s.lookup(name)
+ if templ != nil {
+ _, err := applyTemplateTransformers(templateUndefined, templ, s.lookup)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+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 {
+ 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.
+// TODO(bep) clean up these addTemplate variants
+func (t *templateHandler) AddTemplate(name, tpl string) error {
+ h := t.getTemplateHandler(name)
+ _, err := h.addTemplate(name, tpl)
+ if err != nil {
+ 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() error {
+ if err := t.postTransform(); err != nil {
+ return err
+ }
+
+ 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())
+ }
+
+ return nil
+}
+
+// RebuildClone rebuilds the cloned templates. Used for live-reloads.
+func (t *templateHandler) RebuildClone() {
+ if t.html != nil && t.html.cloneClone != nil {
+ t.html.clone = template.Must(t.html.cloneClone.Clone())
+ }
+ if t.text != nil && t.text.cloneClone != nil {
+ t.text.clone = texttemplate.Must(t.text.cloneClone.Clone())
+ }
+}
+
+func (t *templateHandler) loadTemplates(prefix string) error {
+
+ walker := func(path string, fi os.FileInfo, err error) error {
+ if err != nil || fi.IsDir() {
+ return err
+ }
+
+ if isDotFile(path) || isBackupFile(path) || isBaseTemplate(path) {
+ return nil
+ }
+
+ workingDir := t.PathSpec.WorkingDir
+
+ descriptor := output.TemplateLookupDescriptor{
+ WorkingDir: workingDir,
+ RelPath: path,
+ Prefix: prefix,
+ OutputFormats: t.OutputFormatsConfig,
+ FileExists: func(filename string) (bool, error) {
+ return helpers.Exists(filename, t.Layouts.Fs)
+ },
+ ContainsAny: func(filename string, subslices [][]byte) (bool, error) {
+ return helpers.FileContainsAny(filename, subslices, t.Layouts.Fs)
+ },
+ }
+
+ 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 {
+ return err
+ }
+
+ return nil
+ }
+
+ if err := helpers.SymbolicWalk(t.Layouts.Fs, "", walker); err != nil {
+ if !os.IsNotExist(err) {
+ return err
+ }
+ return nil
+ }
+
+ return nil
+
+}
+
+func (t *templateHandler) initFuncs() {
+
+ // Both template types will get their own funcster instance, which
+ // in the current case contains the same set of funcs.
+ funcMap := createFuncMap(t.Deps)
+ for _, funcsterHolder := range []templateFuncsterSetter{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(funcMap)
+
+ funcsterHolder.setTemplateFuncster(funcster)
+
+ }
+
+ for _, v := range t.shortcodes {
+ for _, variant := range v.variants {
+ t.setFuncMapInTemplate(variant.templ, funcMap)
+ }
+ }
+
+ for _, extText := range t.extTextTemplates {
+ extText.t.Funcs(funcMap)
+ }
+
+ // 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) (templateInfo, 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) (templateInfo, 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.template)
+ if err != nil {
+ return templ.errWithFileContext("parse master failed", err)
+ }
+ }
+
+ templ, err := onMissing(overlayFilename)
+ if err != nil {
+ return err
+ }
+
+ overlayTpl, err := template.Must(masterTpl.Clone()).Parse(templ.template)
+ if err != nil {
+ return templ.errWithFileContext("parse failed", 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(templateUndefined, overlayTpl); err != nil {
+ return err
+ }
+
+ t.overlays[name] = overlayTpl
+ t.nameBaseTemplateName[name] = masterFilename
+
+ return err
+
+}
+
+func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, 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(masterFilename).Parse(templ.template)
+ if err != nil {
+ return errors.Wrapf(err, "failed to parse %q:", templ.filename)
+ }
+ t.nameBaseTemplateName[masterFilename] = templ.filename
+ }
+
+ templ, err := onMissing(overlayFilename)
+ if err != nil {
+ return err
+ }
+
+ overlayTpl, err := texttemplate.Must(masterTpl.Clone()).Parse(templ.template)
+ if err != nil {
+ return errors.Wrapf(err, "failed to parse %q:", templ.filename)
+ }
+
+ overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
+ if _, err := applyTemplateTransformersToTextTemplate(templateUndefined, overlayTpl); err != nil {
+ return err
+ }
+ t.overlays[name] = overlayTpl
+ t.nameBaseTemplateName[name] = templ.filename
+
+ return err
+
+}
+
+func removeLeadingBOM(s string) string {
+ const bom = '\ufeff'
+
+ for i, r := range s {
+ if i == 0 && r != bom {
+ return s
+ }
+ if i > 0 {
+ return s[i:]
+ }
+ }
+
+ return s
+
+}
+
+func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) error {
+ t.checkState()
+
+ t.Log.DEBUG.Printf("Add template file: name %q, baseTemplatePath %q, path %q", name, baseTemplatePath, path)
+
+ getTemplate := func(filename string) (templateInfo, error) {
+ fs := t.Layouts.Fs
+ b, err := afero.ReadFile(fs, filename)
+ if err != nil {
+ return templateInfo{filename: filename, fs: fs}, err
+ }
+
+ s := removeLeadingBOM(string(b))
+
+ realFilename := filename
+ if fi, err := fs.Stat(filename); err == nil {
+ if fir, ok := fi.(hugofs.RealFilenameInfo); ok {
+ realFilename = fir.RealFilename()
+ }
+ }
+
+ return templateInfo{template: s, filename: filename, realFilename: realFilename, fs: fs}, nil
+ }
+
+ // get the suffix and switch on that
+ ext := filepath.Ext(path)
+ switch ext {
+ case ".amber":
+ // Only HTML support for Amber
+ withoutExt := strings.TrimSuffix(name, filepath.Ext(name))
+ templateName := withoutExt + ".html"
+ b, err := afero.ReadFile(t.Layouts.Fs, 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
+ }
+
+ typ := resolveTemplateType(name)
+
+ c, err := applyTemplateTransformersToHMLTTemplate(typ, templ)
+ if err != nil {
+ return err
+ }
+
+ if typ == templateShortcode {
+ t.addShortcodeVariant(templateName, c.Info, templ)
+ } else {
+ t.templateInfo[name] = c.Info
+ }
+
+ return nil
+
+ case ".ace":
+ // Only HTML support for Ace
+ var innerContent, baseContent []byte
+ innerContent, err := afero.ReadFile(t.Layouts.Fs, path)
+
+ if err != nil {
+ return err
+ }
+
+ if baseTemplatePath != "" {
+ baseContent, err = afero.ReadFile(t.Layouts.Fs, 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
+ }
+
+ err = t.AddTemplate(name, templ.template)
+ if err != nil {
+ return templ.errWithFileContext("parse failed", err)
+ }
+ return nil
+ }
+}
+
+var embeddedTemplatesAliases = map[string][]string{
+ "shortcodes/twitter.html": {"shortcodes/tweet.html"},
+}
+
+func (t *templateHandler) loadEmbedded() error {
+ for _, kv := range embedded.EmbeddedTemplates {
+ name, templ := kv[0], kv[1]
+ if err := t.addInternalTemplate(name, templ); err != nil {
+ return err
+ }
+ if aliases, found := embeddedTemplatesAliases[name]; found {
+ for _, alias := range aliases {
+ if err := t.addInternalTemplate(alias, templ); err != nil {
+ return err
+ }
+ }
+
+ }
+ }
+
+ return nil
+
+}
+
+func (t *templateHandler) addInternalTemplate(name, tpl string) error {
+ return t.AddTemplate("_internal/"+name, tpl)
+}
+
+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(filepath.Base(path), baseFileBase)
+}
diff --git a/tpl/tplimpl/templateFuncster.go b/tpl/tplimpl/templateFuncster.go
new file mode 100644
index 000000000..ad51fbad7
--- /dev/null
+++ b/tpl/tplimpl/templateFuncster.go
@@ -0,0 +1,33 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/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,
+ }
+}
diff --git a/tpl/tplimpl/templateProvider.go b/tpl/tplimpl/templateProvider.go
new file mode 100644
index 000000000..932222344
--- /dev/null
+++ b/tpl/tplimpl/templateProvider.go
@@ -0,0 +1,63 @@
+// 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"
+)
+
+// TemplateProvider manages templates.
+type TemplateProvider struct{}
+
+// DefaultTemplateProvider is a globally available TemplateProvider.
+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
+
+ deps.TextTmpl = newTmpl.NewTextTemplate()
+
+ newTmpl.initFuncs()
+
+ if err := newTmpl.loadEmbedded(); err != nil {
+ return err
+ }
+
+ if deps.WithTemplate != nil {
+ err := deps.WithTemplate(newTmpl)
+ if err != nil {
+ return err
+ }
+
+ }
+
+ return newTmpl.MarkReady()
+
+}
+
+// Clone clones.
+func (*TemplateProvider) Clone(d *deps.Deps) error {
+
+ t := d.Tmpl.(*templateHandler)
+ clone := t.clone(d)
+
+ d.Tmpl = clone
+
+ return clone.MarkReady()
+
+}
diff --git a/tpl/tplimpl/template_ast_transformers.go b/tpl/tplimpl/template_ast_transformers.go
new file mode 100644
index 000000000..fb0728a63
--- /dev/null
+++ b/tpl/tplimpl/template_ast_transformers.go
@@ -0,0 +1,535 @@
+// 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 (
+ "html/template"
+ "strings"
+ texttemplate "text/template"
+ "text/template/parse"
+
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/tpl"
+ "github.com/mitchellh/mapstructure"
+ "github.com/spf13/cast"
+)
+
+// decl keeps track of the variable mappings, i.e. $mysite => .Site etc.
+type decl map[string]string
+
+const (
+ paramsIdentifier = "Params"
+)
+
+// Containers that may contain Params that we will not touch.
+var reservedContainers = map[string]bool{
+ // Aka .Site.Data.Params which must stay case sensitive.
+ "Data": true,
+}
+
+type templateType int
+
+const (
+ templateUndefined templateType = iota
+ templateShortcode
+ templatePartial
+)
+
+type templateContext struct {
+ decl decl
+ visited map[string]bool
+ notFound map[string]bool
+ lookupFn func(name string) *parse.Tree
+
+ // The last error encountered.
+ err error
+
+ typ templateType
+
+ // Set when we're done checking for config header.
+ configChecked bool
+
+ // Contains some info about the template
+ tpl.Info
+
+ // Store away the return node in partials.
+ returnNode *parse.CommandNode
+}
+
+func (c templateContext) getIfNotVisited(name string) *parse.Tree {
+ if c.visited[name] {
+ return nil
+ }
+ c.visited[name] = true
+ templ := c.lookupFn(name)
+ if templ == nil {
+ // This may be a inline template defined outside of this file
+ // and not yet parsed. Unusual, but it happens.
+ // Store the name to try again later.
+ c.notFound[name] = true
+ }
+
+ return templ
+}
+
+func newTemplateContext(lookupFn func(name string) *parse.Tree) *templateContext {
+ return &templateContext{
+ Info: tpl.Info{Config: tpl.DefaultConfig},
+ lookupFn: lookupFn,
+ decl: make(map[string]string),
+ visited: make(map[string]bool),
+ notFound: 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(typ templateType, templ *template.Template) (*templateContext, error) {
+ return applyTemplateTransformers(typ, templ.Tree, createParseTreeLookup(templ))
+}
+
+func applyTemplateTransformersToTextTemplate(typ templateType, templ *texttemplate.Template) (*templateContext, error) {
+ return applyTemplateTransformers(typ, templ.Tree,
+ func(nn string) *parse.Tree {
+ tt := templ.Lookup(nn)
+ if tt != nil {
+ return tt.Tree
+ }
+ return nil
+ })
+}
+
+func applyTemplateTransformers(typ templateType, templ *parse.Tree, lookupFn func(name string) *parse.Tree) (*templateContext, error) {
+ if templ == nil {
+ return nil, errors.New("expected template, but none provided")
+ }
+
+ c := newTemplateContext(lookupFn)
+ c.typ = typ
+
+ _, err := c.applyTransformations(templ.Root)
+
+ if err == nil && c.returnNode != nil {
+ // This is a partial with a return statement.
+ c.Info.HasReturn = true
+ templ.Root = c.wrapInPartialReturnWrapper(templ.Root)
+ }
+
+ return c, err
+}
+
+const (
+ partialReturnWrapperTempl = `{{ $_hugo_dot := $ }}{{ $ := .Arg }}{{ with .Arg }}{{ $_hugo_dot.Set ("PLACEHOLDER") }}{{ end }}`
+)
+
+var partialReturnWrapper *parse.ListNode
+
+func init() {
+ templ, err := texttemplate.New("").Parse(partialReturnWrapperTempl)
+ if err != nil {
+ panic(err)
+ }
+ partialReturnWrapper = templ.Tree.Root
+}
+
+func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
+ wrapper := partialReturnWrapper.CopyList()
+ withNode := wrapper.Nodes[2].(*parse.WithNode)
+ retn := withNode.List.Nodes[0]
+ setCmd := retn.(*parse.ActionNode).Pipe.Cmds[0]
+ setPipe := setCmd.Args[1].(*parse.PipeNode)
+ // Replace PLACEHOLDER with the real return value.
+ // Note that this is a PipeNode, so it will be wrapped in parens.
+ setPipe.Cmds = []*parse.CommandNode{c.returnNode}
+ withNode.List.Nodes = append(n.Nodes, retn)
+
+ return wrapper
+
+}
+
+// The truth logic in Go's template package is broken for certain values
+// for the if and with keywords. This works around that problem by wrapping
+// the node passed to if/with in a getif conditional.
+// getif works slightly different than the Go built-in in that it also
+// considers any IsZero methods on the values (as in time.Time).
+// See https://github.com/gohugoio/hugo/issues/5738
+func (c *templateContext) wrapWithGetIf(p *parse.PipeNode) {
+ if len(p.Cmds) == 0 {
+ return
+ }
+
+ // getif will return an empty string if not evaluated as truthful,
+ // which is when we need the value in the with clause.
+ firstArg := parse.NewIdentifier("getif")
+ secondArg := p.CopyPipe()
+ newCmd := p.Cmds[0].Copy().(*parse.CommandNode)
+
+ // secondArg is a PipeNode and will behave as it was wrapped in parens, e.g:
+ // {{ getif (len .Params | eq 2) }}
+ newCmd.Args = []parse.Node{firstArg, secondArg}
+
+ p.Cmds = []*parse.CommandNode{newCmd}
+
+}
+
+// applyTransformations do 3 things:
+// 1) Make all .Params.CamelCase and similar into lowercase.
+// 2) Wraps every with and if pipe in getif
+// 3) Collects some information about the template content.
+func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
+ switch x := n.(type) {
+ case *parse.ListNode:
+ if x != nil {
+ c.applyTransformationsToNodes(x.Nodes...)
+ }
+ case *parse.ActionNode:
+ c.applyTransformationsToNodes(x.Pipe)
+ case *parse.IfNode:
+ c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
+ c.wrapWithGetIf(x.Pipe)
+ case *parse.WithNode:
+ c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
+ c.wrapWithGetIf(x.Pipe)
+ case *parse.RangeNode:
+ c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
+ case *parse.TemplateNode:
+ subTempl := c.getIfNotVisited(x.Name)
+ if subTempl != nil {
+ c.applyTransformationsToNodes(subTempl.Root)
+ }
+ case *parse.PipeNode:
+ c.collectConfig(x)
+ if len(x.Decl) == 1 && len(x.Cmds) == 1 {
+ // maps $site => .Site etc.
+ c.decl[x.Decl[0].Ident[0]] = x.Cmds[0].String()
+ }
+
+ for i, cmd := range x.Cmds {
+ keep, _ := c.applyTransformations(cmd)
+ if !keep {
+ x.Cmds = append(x.Cmds[:i], x.Cmds[i+1:]...)
+ }
+ }
+
+ case *parse.CommandNode:
+ c.collectInner(x)
+ keep := c.collectReturnNode(x)
+
+ 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.applyTransformations(an)
+ case *parse.ChainNode:
+ // site.Params...
+ if len(an.Field) > 1 && an.Field[0] == paramsIdentifier {
+ c.updateIdentsIfNeeded(an.Field)
+ }
+ }
+ }
+ return keep, c.err
+ }
+
+ return true, c.err
+}
+
+func (c *templateContext) applyTransformationsToNodes(nodes ...parse.Node) {
+ for _, node := range nodes {
+ c.applyTransformations(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])
+ }
+
+}
+
+func (c *templateContext) hasIdent(idents []string, ident string) bool {
+ for _, id := range idents {
+ if id == ident {
+ return true
+ }
+ }
+ return false
+}
+
+// collectConfig collects and parses any leading template config variable declaration.
+// This will be the first PipeNode in the template, and will be a variable declaration
+// on the form:
+// {{ $_hugo_config:= `{ "version": 1 }` }}
+func (c *templateContext) collectConfig(n *parse.PipeNode) {
+ if c.typ != templateShortcode {
+ return
+ }
+ if c.configChecked {
+ return
+ }
+ c.configChecked = true
+
+ if len(n.Decl) != 1 || len(n.Cmds) != 1 {
+ // This cannot be a config declaration
+ return
+ }
+
+ v := n.Decl[0]
+
+ if len(v.Ident) == 0 || v.Ident[0] != "$_hugo_config" {
+ return
+ }
+
+ cmd := n.Cmds[0]
+
+ if len(cmd.Args) == 0 {
+ return
+ }
+
+ if s, ok := cmd.Args[0].(*parse.StringNode); ok {
+ errMsg := "failed to decode $_hugo_config in template"
+ m, err := cast.ToStringMapE(s.Text)
+ if err != nil {
+ c.err = errors.Wrap(err, errMsg)
+ return
+ }
+ if err := mapstructure.WeakDecode(m, &c.Info.Config); err != nil {
+ c.err = errors.Wrap(err, errMsg)
+ }
+ }
+
+}
+
+// collectInner determines if the given CommandNode represents a
+// shortcode call to its .Inner.
+func (c *templateContext) collectInner(n *parse.CommandNode) {
+ if c.typ != templateShortcode {
+ return
+ }
+ if c.Info.IsInner || len(n.Args) == 0 {
+ return
+ }
+
+ for _, arg := range n.Args {
+ var idents []string
+ switch nt := arg.(type) {
+ case *parse.FieldNode:
+ idents = nt.Ident
+ case *parse.VariableNode:
+ idents = nt.Ident
+ }
+
+ if c.hasIdent(idents, "Inner") {
+ c.Info.IsInner = true
+ break
+ }
+ }
+
+}
+
+func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool {
+ if c.typ != templatePartial || c.returnNode != nil {
+ return true
+ }
+
+ if len(n.Args) < 2 {
+ return true
+ }
+
+ ident, ok := n.Args[0].(*parse.IdentifierNode)
+ if !ok || ident.Ident != "return" {
+ return true
+ }
+
+ c.returnNode = n
+ // Remove the "return" identifiers
+ c.returnNode.Args = c.returnNode.Args[1:]
+
+ return false
+
+}
+
+// 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
+ }
+
+ if l == 1 {
+ first := idents[0]
+ if first == "" || first == paramsIdentifier || first[0] == '$' {
+ // This can not be a Params.x
+ return -1
+ }
+ }
+
+ var lookFurther bool
+ var needsVarExpansion bool
+ for _, ident := range idents {
+ if ident[0] == '$' {
+ lookFurther = true
+ needsVarExpansion = true
+ break
+ } else if ident == paramsIdentifier {
+ lookFurther = true
+ break
+ }
+ }
+
+ if !lookFurther {
+ return -1
+ }
+
+ var resolvedIdents []string
+
+ if !needsVarExpansion {
+ resolvedIdents = idents
+ } else {
+ var ok bool
+ resolvedIdents, ok = d.resolveVariables(idents)
+ if !ok {
+ return -1
+ }
+ }
+
+ var paramFound bool
+ for i, ident := range resolvedIdents {
+ if ident == paramsIdentifier {
+ if i > 0 {
+ container := resolvedIdents[i-1]
+ if reservedContainers[container] {
+ // .Data.Params.someKey
+ return -1
+ }
+ }
+
+ paramFound = true
+ break
+ }
+ }
+
+ if !paramFound {
+ return -1
+ }
+
+ var paramSeen bool
+ idx := -1
+ for i, ident := range idents {
+ if ident == "" || ident[0] == '$' {
+ continue
+ }
+
+ if ident == paramsIdentifier {
+ paramSeen = true
+ idx = -1
+
+ } else {
+ if paramSeen {
+ return i
+ }
+ if idx == -1 {
+ idx = i
+ }
+ }
+ }
+ return idx
+
+}
+
+func (d decl) resolveVariables(idents []string) ([]string, bool) {
+ var (
+ 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 nil, false
+ }
+
+ 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 nil, false
+ }
+
+ if !d.isKeyword(replacement) {
+ // This can not be .Site.Params etc.
+ return nil, false
+ }
+
+ 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, ".")...)
+ }
+ }
+
+ return append(replaced, idents[1:]...), true
+
+}
+
+func (d decl) isKeyword(s string) bool {
+ return !strings.ContainsAny(s, " -\"")
+}
diff --git a/tpl/tplimpl/template_ast_transformers_test.go b/tpl/tplimpl/template_ast_transformers_test.go
new file mode 100644
index 000000000..be1efc522
--- /dev/null
+++ b/tpl/tplimpl/template_ast_transformers_test.go
@@ -0,0 +1,551 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "html/template"
+ "testing"
+ "time"
+
+ "github.com/gohugoio/hugo/tpl"
+
+ "github.com/spf13/cast"
+
+ "github.com/stretchr/testify/require"
+)
+
+var (
+ testFuncs = map[string]interface{}{
+ "getif": func(v interface{}) interface{} { return v },
+ "ToTime": func(v interface{}) interface{} { return cast.ToTime(v) },
+ "First": func(v ...interface{}) interface{} { return v[0] },
+ "Echo": func(v interface{}) interface{} { return v },
+ "where": func(seq, key interface{}, args ...interface{}) (interface{}, error) {
+ return map[string]interface{}{
+ "ByWeight": fmt.Sprintf("%v:%v:%v", seq, key, args),
+ }, nil
+ },
+ "site": func() interface{} {
+ return map[string]interface{}{
+ "Params": map[string]interface{}{
+ "lower": "global-site",
+ },
+ }
+ },
+ }
+
+ paramsData = map[string]interface{}{
+ "NotParam": "Hi There",
+ "Slice": []int{1, 3},
+ "Params": map[string]interface{}{
+ "lower": "P1L",
+ "slice": []int{1, 3},
+ "mydate": "1972-01-28",
+ },
+ "Pages": map[string]interface{}{
+ "ByWeight": []int{1, 3},
+ },
+ "CurrentSection": map[string]interface{}{
+ "Params": map[string]interface{}{
+ "lower": "pcurrentsection",
+ },
+ },
+ "Site": map[string]interface{}{
+ "Params": map[string]interface{}{
+ "lower": "P2L",
+ "slice": []int{1, 3},
+ },
+ "Language": map[string]interface{}{
+ "Params": map[string]interface{}{
+ "lower": "P22L",
+ "nested": map[string]interface{}{
+ "lower": "P22L_nested",
+ },
+ },
+ },
+ "Data": map[string]interface{}{
+ "Params": map[string]interface{}{
+ "NOLOW": "P3H",
+ },
+ },
+ },
+ }
+
+ paramsTempl = `
+{{ $page := . }}
+{{ $pages := .Pages }}
+{{ $pageParams := .Params }}
+{{ $site := .Site }}
+{{ $siteParams := .Site.Params }}
+{{ $data := .Site.Data }}
+{{ $notparam := .NotParam }}
+
+PCurrentSection: {{ .CurrentSection.Params.LOWER }}
+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 }}
+P22_nested: {{ .Site.Language.Params.NESTED.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) }}
+
+PSLICE: {{ range .Params.SLICE }}PSLICE{{.}}|{{ end }}
+
+{{ $pages := "foo" }}
+{{ $pages := where $pages ".Params.toc_hide" "!=" true }}
+PARAMS STRING: {{ $pages.ByWeight }}
+PARAMS STRING2: {{ with $pages }}{{ .ByWeight }}{{ end }}
+{{ $pages3 := where ".Params.TOC_HIDE" "!=" .Params.LOWER }}
+PARAMS STRING3: {{ $pages3.ByWeight }}
+{{ $first := First .Pages .Site.Params.LOWER }}
+PARAMS COMPOSITE: {{ $first.ByWeight }}
+
+
+{{ $time := $.Params.MyDate | ToTime }}
+{{ $time = $time.AddDate 0 1 0 }}
+PARAMS TIME: {{ $time.Format "2006-01-02" }}
+
+{{ $_x := $.Params.MyDate | ToTime }}
+PARAMS TIME2: {{ $_x.AddDate 0 1 0 }}
+
+PARAMS SITE GLOBAL1: {{ site.Params.LOwER }}
+{{ $lower := site.Params.LOwER }}
+{{ $site := site }}
+PARAMS SITE GLOBAL2: {{ $lower }}
+PARAMS SITE GLOBAL3: {{ $site.Params.LOWER }}
+`
+)
+
+func TestParamsKeysToLower(t *testing.T) {
+ t.Parallel()
+
+ _, err := applyTemplateTransformers(templateUndefined, nil, nil)
+ require.Error(t, err)
+
+ 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.applyTransformations(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, "P22_nested: P22L_nested")
+ 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")
+
+ require.Contains(t, result, "PSLICE: PSLICE1|PSLICE3|")
+ require.Contains(t, result, "PARAMS STRING: foo:.Params.toc_hide:[!= true]")
+ require.Contains(t, result, "PARAMS STRING2: foo:.Params.toc_hide:[!= true]")
+ require.Contains(t, result, "PARAMS STRING3: .Params.TOC_HIDE:!=:[P1L]")
+
+ // Issue #5094
+ require.Contains(t, result, "PARAMS COMPOSITE: [1 3]")
+
+ // Issue #5068
+ require.Contains(t, result, "PCurrentSection: pcurrentsection")
+
+ // Issue #5541
+ require.Contains(t, result, "PARAMS TIME: 1972-02-28")
+ require.Contains(t, result, "PARAMS TIME2: 1972-02-28")
+
+ // Issue ##5615
+ require.Contains(t, result, "PARAMS SITE GLOBAL1: global-site")
+ require.Contains(t, result, "PARAMS SITE GLOBAL2: global-site")
+ require.Contains(t, result, "PARAMS SITE GLOBAL3: global-site")
+
+}
+
+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.applyTransformations(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",
+ "pretty": map[string]interface{}{
+ "first": "Indigo",
+ },
+ },
+ },
+ }
+
+ // This is how Amber behaves:
+ paramsTempl = `
+{{$__amber_1 := .Params.Colors}}
+{{$__amber_2 := $__amber_1.Blue}}
+{{$__amber_3 := $__amber_1.Pretty}}
+{{$__amber_4 := .Params}}
+
+Color: {{$__amber_2}}
+Blue: {{ $__amber_1.Blue}}
+Pretty First1: {{ $__amber_3.First}}
+Pretty First2: {{ $__amber_1.Pretty.First}}
+Pretty First3: {{ $__amber_4.COLORS.PRETTY.FIRST}}
+`
+ )
+
+ templ, err := template.New("foo").Parse(paramsTempl)
+
+ require.NoError(t, err)
+
+ c := newTemplateContext(createParseTreeLookup(templ))
+
+ c.applyTransformations(templ.Tree.Root)
+
+ var b bytes.Buffer
+
+ require.NoError(t, templ.Execute(&b, ctx))
+
+ result := b.String()
+
+ require.Contains(t, result, "Color: Amber")
+ require.Contains(t, result, "Blue: Amber")
+ require.Contains(t, result, "Pretty First1: Indigo")
+ require.Contains(t, result, "Pretty First2: Indigo")
+ require.Contains(t, result, "Pretty First3: Indigo")
+
+}
+
+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.applyTransformations(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.applyTransformations(templ.Tree.Root)
+
+}
+
+type I interface {
+ Method0()
+}
+
+type T struct {
+ NonEmptyInterfaceTypedNil I
+}
+
+func (T) Method0() {
+}
+
+func TestInsertIsZeroFunc(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ var (
+ ctx = map[string]interface{}{
+ "True": true,
+ "Now": time.Now(),
+ "TimeZero": time.Time{},
+ "T": &T{NonEmptyInterfaceTypedNil: (*T)(nil)},
+ }
+
+ templ1 = `
+{{ if .True }}.True: TRUE{{ else }}.True: FALSE{{ end }}
+{{ if .TimeZero }}.TimeZero1: TRUE{{ else }}.TimeZero1: FALSE{{ end }}
+{{ if (.TimeZero) }}.TimeZero2: TRUE{{ else }}.TimeZero2: FALSE{{ end }}
+{{ if not .TimeZero }}.TimeZero3: TRUE{{ else }}.TimeZero3: FALSE{{ end }}
+{{ if .Now }}.Now: TRUE{{ else }}.Now: FALSE{{ end }}
+{{ with .TimeZero }}.TimeZero1 with: {{ . }}{{ else }}.TimeZero1 with: FALSE{{ end }}
+{{ template "mytemplate" . }}
+{{ if .T.NonEmptyInterfaceTypedNil }}.NonEmptyInterfaceTypedNil: TRUE{{ else }}.NonEmptyInterfaceTypedNil: FALSE{{ end }}
+
+{{ template "other-file-template" . }}
+
+{{ define "mytemplate" }}
+{{ if .TimeZero }}.TimeZero1: mytemplate: TRUE{{ else }}.TimeZero1: mytemplate: FALSE{{ end }}
+{{ end }}
+
+`
+
+ // https://github.com/gohugoio/hugo/issues/5865
+ templ2 = `{{ define "other-file-template" }}
+{{ if .TimeZero }}.TimeZero1: other-file-template: TRUE{{ else }}.TimeZero1: other-file-template: FALSE{{ end }}
+{{ end }}
+`
+ )
+
+ d := newD(assert)
+ h := d.Tmpl.(tpl.TemplateHandler)
+
+ // HTML templates
+ assert.NoError(h.AddTemplate("mytemplate.html", templ1))
+ assert.NoError(h.AddTemplate("othertemplate.html", templ2))
+
+ // Text templates
+ assert.NoError(h.AddTemplate("_text/mytexttemplate.txt", templ1))
+ assert.NoError(h.AddTemplate("_text/myothertexttemplate.txt", templ2))
+
+ assert.NoError(h.MarkReady())
+
+ for _, name := range []string{"mytemplate.html", "mytexttemplate.txt"} {
+ tt, _ := d.Tmpl.Lookup(name)
+ result, err := tt.(tpl.TemplateExecutor).ExecuteToString(ctx)
+ assert.NoError(err)
+
+ assert.Contains(result, ".True: TRUE")
+ assert.Contains(result, ".TimeZero1: FALSE")
+ assert.Contains(result, ".TimeZero2: FALSE")
+ assert.Contains(result, ".TimeZero3: TRUE")
+ assert.Contains(result, ".Now: TRUE")
+ assert.Contains(result, "TimeZero1 with: FALSE")
+ assert.Contains(result, ".TimeZero1: mytemplate: FALSE")
+ assert.Contains(result, ".TimeZero1: other-file-template: FALSE")
+ assert.Contains(result, ".NonEmptyInterfaceTypedNil: FALSE")
+ }
+
+}
+
+func TestCollectInfo(t *testing.T) {
+
+ configStr := `{ "version": 42 }`
+
+ tests := []struct {
+ name string
+ tplString string
+ expected tpl.Info
+ }{
+ {"Basic Inner", `{{ .Inner }}`, tpl.Info{IsInner: true, Config: tpl.DefaultConfig}},
+ {"Basic config map", "{{ $_hugo_config := `" + configStr + "` }}", tpl.Info{
+ Config: tpl.Config{
+ Version: 42,
+ },
+ }},
+ }
+
+ echo := func(in interface{}) interface{} {
+ return in
+ }
+
+ funcs := template.FuncMap{
+ "highlight": echo,
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ assert := require.New(t)
+
+ templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
+ require.NoError(t, err)
+
+ c := newTemplateContext(createParseTreeLookup(templ))
+ c.typ = templateShortcode
+ c.applyTransformations(templ.Tree.Root)
+
+ assert.Equal(test.expected, c.Info)
+ })
+ }
+
+}
+
+func TestPartialReturn(t *testing.T) {
+
+ tests := []struct {
+ name string
+ tplString string
+ expected bool
+ }{
+ {"Basic", `
+{{ $a := "Hugo Rocks!" }}
+{{ return $a }}
+`, true},
+ {"Expression", `
+{{ return add 32 }}
+`, true},
+ }
+
+ echo := func(in interface{}) interface{} {
+ return in
+ }
+
+ funcs := template.FuncMap{
+ "return": echo,
+ "add": echo,
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ assert := require.New(t)
+
+ templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
+ require.NoError(t, err)
+
+ _, err = applyTemplateTransformers(templatePartial, templ.Tree, createParseTreeLookup(templ))
+
+ // Just check that it doesn't fail in this test. We have functional tests
+ // in hugoblib.
+ assert.NoError(err)
+
+ })
+ }
+
+}
diff --git a/tpl/tplimpl/template_errors.go b/tpl/tplimpl/template_errors.go
new file mode 100644
index 000000000..63695c5f6
--- /dev/null
+++ b/tpl/tplimpl/template_errors.go
@@ -0,0 +1,46 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/common/herrors"
+ "github.com/pkg/errors"
+ "github.com/spf13/afero"
+)
+
+type templateInfo struct {
+ template string
+
+ // Used to create some error context in error situations
+ fs afero.Fs
+
+ // The filename relative to the fs above.
+ filename string
+
+ // The real filename (if possible). Used for logging.
+ realFilename string
+}
+
+func (info templateInfo) errWithFileContext(what string, err error) error {
+ err = errors.Wrapf(err, what)
+
+ err, _ = herrors.WithFileContextForFile(
+ err,
+ info.realFilename,
+ info.filename,
+ info.fs,
+ herrors.SimpleLineMatcher)
+
+ return err
+}
diff --git a/tpl/tplimpl/template_funcs.go b/tpl/tplimpl/template_funcs.go
new file mode 100644
index 000000000..bbaf44ae2
--- /dev/null
+++ b/tpl/tplimpl/template_funcs.go
@@ -0,0 +1,80 @@
+// 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/deps"
+
+ "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/hugo"
+ _ "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/path"
+ _ "github.com/gohugoio/hugo/tpl/reflect"
+ _ "github.com/gohugoio/hugo/tpl/resources"
+ _ "github.com/gohugoio/hugo/tpl/safe"
+ _ "github.com/gohugoio/hugo/tpl/site"
+ _ "github.com/gohugoio/hugo/tpl/strings"
+ _ "github.com/gohugoio/hugo/tpl/templates"
+ _ "github.com/gohugoio/hugo/tpl/time"
+ _ "github.com/gohugoio/hugo/tpl/transform"
+ _ "github.com/gohugoio/hugo/tpl/urls"
+)
+
+func createFuncMap(d *deps.Deps) map[string]interface{} {
+ funcMap := template.FuncMap{}
+
+ // Merge the namespace funcs
+ for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
+ ns := nsf(d)
+ 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
+ }
+
+ }
+
+ }
+
+ return funcMap
+
+}
+func (t *templateFuncster) initFuncMap(funcMap template.FuncMap) {
+ 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..449d20fd4
--- /dev/null
+++ b/tpl/tplimpl/template_funcs_test.go
@@ -0,0 +1,222 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "time"
+
+ "github.com/gohugoio/hugo/htesting"
+
+ "github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/langs"
+ "github.com/gohugoio/hugo/langs/i18n"
+ "github.com/gohugoio/hugo/tpl"
+ "github.com/gohugoio/hugo/tpl/internal"
+ "github.com/gohugoio/hugo/tpl/partials"
+ "github.com/spf13/afero"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/require"
+)
+
+var (
+ logger = loggers.NewErrorLogger()
+)
+
+func newTestConfig() config.Provider {
+ v := viper.New()
+ v.Set("contentDir", "content")
+ v.Set("dataDir", "data")
+ v.Set("i18nDir", "i18n")
+ v.Set("layoutDir", "layouts")
+ v.Set("archetypeDir", "archetypes")
+ v.Set("assetDir", "assets")
+ v.Set("resourceDir", "resources")
+ v.Set("publishDir", "public")
+ return v
+}
+
+func newDepsConfig(cfg config.Provider) deps.DepsCfg {
+ l := langs.NewLanguage("en", cfg)
+ return deps.DepsCfg{
+ Language: l,
+ Site: htesting.NewTestHugoSite(),
+ Cfg: cfg,
+ Fs: hugofs.NewMem(l),
+ Logger: logger,
+ TemplateProvider: DefaultTemplateProvider,
+ TranslationProvider: i18n.NewTranslationProvider(),
+ }
+}
+
+func TestTemplateFuncsExamples(t *testing.T) {
+ t.Parallel()
+
+ workingDir := "/home/hugo"
+
+ v := newTestConfig()
+
+ v.Set("workingDir", workingDir)
+ v.Set("multilingual", true)
+ v.Set("contentDir", "content")
+ v.Set("assetDir", "assets")
+ v.Set("baseURL", "http://mysite.com/hugo/")
+ v.Set("CurrentContentLanguage", langs.NewLanguage("en", v))
+
+ fs := hugofs.NewMem(v)
+
+ afero.WriteFile(fs.Source, filepath.Join(workingDir, "files", "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
+ Hugo map[string]interface{}
+ Params map[string]interface{}
+ }
+
+ data.Title = "**BatMan**"
+ data.Section = "blog"
+ data.Params = map[string]interface{}{"langCode": "en"}
+ data.Hugo = map[string]interface{}{"Version": hugo.MustParseVersion("0.36.1").Version()}
+
+ 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
+ templ, _ := d.Tmpl.Lookup("test")
+ require.NoError(t, templ.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()
+
+ assert := require.New(t)
+
+ partial := `Now: {{ now.UnixNano }}`
+ name := "testing"
+
+ var data struct {
+ }
+
+ v := newTestConfig()
+
+ config := newDepsConfig(v)
+
+ config.WithTemplate = func(templ tpl.TemplateHandler) error {
+ err := templ.AddTemplate("partials/"+name, partial)
+ if err != nil {
+ return err
+ }
+
+ return nil
+ }
+
+ de, err := deps.New(config)
+ assert.NoError(err)
+ assert.NoError(de.LoadResources())
+
+ ns := partials.New(de)
+
+ res1, err := ns.IncludeCached(name, &data)
+ assert.NoError(err)
+
+ for j := 0; j < 10; j++ {
+ time.Sleep(2 * time.Nanosecond)
+ res2, err := ns.IncludeCached(name, &data)
+ assert.NoError(err)
+
+ if !reflect.DeepEqual(res1, res2) {
+ t.Fatalf("cache mismatch")
+ }
+
+ res3, err := ns.IncludeCached(name, &data, fmt.Sprintf("variant%d", j))
+ assert.NoError(err)
+
+ if reflect.DeepEqual(res1, res3) {
+ t.Fatalf("cache mismatch")
+ }
+ }
+
+}
+
+func BenchmarkPartial(b *testing.B) {
+ doBenchmarkPartial(b, func(ns *partials.Namespace) error {
+ _, err := ns.Include("bench1")
+ return err
+ })
+}
+
+func BenchmarkPartialCached(b *testing.B) {
+ doBenchmarkPartial(b, func(ns *partials.Namespace) error {
+ _, err := ns.IncludeCached("bench1", nil)
+ return err
+ })
+}
+
+func doBenchmarkPartial(b *testing.B, f func(ns *partials.Namespace) error) {
+ config := newDepsConfig(viper.New())
+ config.WithTemplate = func(templ tpl.TemplateHandler) error {
+ 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())
+
+ ns := partials.New(de)
+
+ b.ResetTimer()
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ if err := f(ns); err != nil {
+ b.Fatalf("error executing template: %s", err)
+ }
+ }
+ })
+}
diff --git a/tpl/tplimpl/template_info_test.go b/tpl/tplimpl/template_info_test.go
new file mode 100644
index 000000000..be9d7e2f1
--- /dev/null
+++ b/tpl/tplimpl/template_info_test.go
@@ -0,0 +1,56 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "testing"
+
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/tpl"
+ "github.com/stretchr/testify/require"
+)
+
+func TestTemplateInfoShortcode(t *testing.T) {
+ assert := require.New(t)
+ d := newD(assert)
+ h := d.Tmpl.(tpl.TemplateHandler)
+
+ assert.NoError(h.AddTemplate("shortcodes/mytemplate.html", `
+{{ .Inner }}
+`))
+ tt, found, _ := d.Tmpl.LookupVariant("mytemplate", tpl.TemplateVariants{})
+
+ assert.True(found)
+ tti, ok := tt.(tpl.TemplateInfoProvider)
+ assert.True(ok)
+ assert.True(tti.TemplateInfo().IsInner)
+
+}
+
+// TODO(bep) move and use in other places
+func newD(assert *require.Assertions) *deps.Deps {
+ v := newTestConfig()
+ fs := hugofs.NewMem(v)
+
+ depsCfg := newDepsConfig(v)
+ depsCfg.Fs = fs
+ d, err := deps.New(depsCfg)
+ assert.NoError(err)
+
+ provider := DefaultTemplateProvider
+ provider.Update(d)
+
+ return d
+
+}
diff --git a/tpl/transform/init.go b/tpl/transform/init.go
new file mode 100644
index 000000000..62cb0a9c3
--- /dev/null
+++ b/tpl/transform/init.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 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 <cathal@foo.bar>" | safeHTML}}`,
+ `Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;`},
+ {
+ `{{ htmlEscape "Cathal Garvey & The Sunshine Band <cathal@foo.bar>"}}`,
+ `Cathal Garvey &amp;amp; The Sunshine Band &amp;lt;cathal@foo.bar&amp;gt;`},
+ {
+ `{{ htmlEscape "Cathal Garvey & The Sunshine Band <cathal@foo.bar>" | htmlUnescape | safeHTML }}`,
+ `Cathal Garvey & The Sunshine Band <cathal@foo.bar>`},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.HTMLUnescape,
+ []string{"htmlUnescape"},
+ [][2]string{
+ {
+ `{{ htmlUnescape "Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;" | safeHTML}}`,
+ `Cathal Garvey & The Sunshine Band <cathal@foo.bar>`},
+ {
+ `{{"Cathal Garvey &amp;amp; The Sunshine Band &amp;lt;cathal@foo.bar&amp;gt;" | htmlUnescape | htmlUnescape | safeHTML}}`,
+ `Cathal Garvey & The Sunshine Band <cathal@foo.bar>`},
+ {
+ `{{"Cathal Garvey &amp;amp; The Sunshine Band &amp;lt;cathal@foo.bar&amp;gt;" | htmlUnescape | htmlUnescape }}`,
+ `Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;`},
+ {
+ `{{ htmlUnescape "Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;" | htmlEscape | safeHTML }}`,
+ `Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;`},
+ },
+ )
+
+ 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!`},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.Remarshal,
+ nil,
+ [][2]string{
+ {`{{ "title = \"Hello World\"" | transform.Remarshal "json" | safeHTML }}`, "{\n \"title\": \"Hello World\"\n}\n"},
+ },
+ )
+
+ ns.AddMethodMapping(ctx.Unmarshal,
+ []string{"unmarshal"},
+ [][2]string{
+ {`{{ "hello = \"Hello World\"" | transform.Unmarshal }}`, "map[hello:Hello World]"},
+ {`{{ "hello = \"Hello World\"" | resources.FromString "data/greetings.toml" | transform.Unmarshal }}`, "map[hello:Hello World]"},
+ },
+ )
+
+ 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/remarshal.go b/tpl/transform/remarshal.go
new file mode 100644
index 000000000..182bd21d6
--- /dev/null
+++ b/tpl/transform/remarshal.go
@@ -0,0 +1,62 @@
+package transform
+
+import (
+ "bytes"
+ "strings"
+
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/parser"
+ "github.com/gohugoio/hugo/parser/metadecoders"
+ "github.com/spf13/cast"
+)
+
+// Remarshal is used in the Hugo documentation to convert configuration
+// examples from YAML to JSON, TOML (and possibly the other way around).
+// The is primarily a helper for the Hugo docs site.
+// It is not a general purpose YAML to TOML converter etc., and may
+// change without notice if it serves a purpose in the docs.
+// Format is one of json, yaml or toml.
+func (ns *Namespace) Remarshal(format string, data interface{}) (string, error) {
+ from, err := cast.ToStringE(data)
+ if err != nil {
+ return "", err
+ }
+
+ from = strings.TrimSpace(from)
+ format = strings.TrimSpace(strings.ToLower(format))
+
+ if from == "" {
+ return "", nil
+ }
+
+ mark, err := toFormatMark(format)
+ if err != nil {
+ return "", err
+ }
+
+ fromFormat := metadecoders.Default.FormatFromContentString(from)
+ if fromFormat == "" {
+ return "", errors.New("failed to detect format from content")
+ }
+
+ meta, err := metadecoders.Default.UnmarshalToMap([]byte(from), fromFormat)
+ if err != nil {
+ return "", err
+ }
+
+ var result bytes.Buffer
+ if err := parser.InterfaceToConfig(meta, mark, &result); err != nil {
+ return "", err
+ }
+
+ return result.String(), nil
+}
+
+func toFormatMark(format string) (metadecoders.Format, error) {
+ if f := metadecoders.FormatFromString(format); f != "" {
+ return f, nil
+ }
+
+ return "", errors.New("failed to detect target data serialization format")
+}
diff --git a/tpl/transform/remarshal_test.go b/tpl/transform/remarshal_test.go
new file mode 100644
index 000000000..07414ccb4
--- /dev/null
+++ b/tpl/transform/remarshal_test.go
@@ -0,0 +1,172 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "testing"
+
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/require"
+)
+
+func TestRemarshal(t *testing.T) {
+ t.Parallel()
+
+ v := viper.New()
+ v.Set("contentDir", "content")
+ ns := New(newDeps(v))
+ assert := require.New(t)
+
+ tomlExample := `title = "Test Metadata"
+
+[[resources]]
+ src = "**image-4.png"
+ title = "The Fourth Image!"
+ [resources.params]
+ byline = "picasso"
+
+[[resources]]
+ name = "my-cool-image-:counter"
+ src = "**.png"
+ title = "TOML: The Image #:counter"
+ [resources.params]
+ byline = "bep"
+`
+
+ yamlExample := `resources:
+- params:
+ byline: picasso
+ src: '**image-4.png'
+ title: The Fourth Image!
+- name: my-cool-image-:counter
+ params:
+ byline: bep
+ src: '**.png'
+ title: 'TOML: The Image #:counter'
+title: Test Metadata
+`
+
+ jsonExample := `{
+ "resources": [
+ {
+ "params": {
+ "byline": "picasso"
+ },
+ "src": "**image-4.png",
+ "title": "The Fourth Image!"
+ },
+ {
+ "name": "my-cool-image-:counter",
+ "params": {
+ "byline": "bep"
+ },
+ "src": "**.png",
+ "title": "TOML: The Image #:counter"
+ }
+ ],
+ "title": "Test Metadata"
+}
+`
+
+ variants := []struct {
+ format string
+ data string
+ }{
+ {"yaml", yamlExample},
+ {"json", jsonExample},
+ {"toml", tomlExample},
+ {"TOML", tomlExample},
+ {"Toml", tomlExample},
+ {" TOML ", tomlExample},
+ }
+
+ for _, v1 := range variants {
+ for _, v2 := range variants {
+ // Both from and to may be the same here, but that is fine.
+ fromTo := fmt.Sprintf("%s => %s", v2.format, v1.format)
+
+ converted, err := ns.Remarshal(v1.format, v2.data)
+ assert.NoError(err, fromTo)
+ diff := helpers.DiffStrings(v1.data, converted)
+ if len(diff) > 0 {
+ t.Errorf("[%s] Expected \n%v\ngot\n%v\ndiff:\n%v", fromTo, v1.data, converted, diff)
+ }
+
+ }
+ }
+
+}
+
+func TestRemarshalComments(t *testing.T) {
+ t.Parallel()
+
+ v := viper.New()
+ v.Set("contentDir", "content")
+ ns := New(newDeps(v))
+
+ assert := require.New(t)
+
+ input := `
+Hugo = "Rules"
+
+# It really does!
+
+[m]
+# A comment
+a = "b"
+
+`
+
+ expected := `
+Hugo = "Rules"
+
+[m]
+ a = "b"
+`
+
+ for _, format := range []string{"json", "yaml", "toml"} {
+ fromTo := fmt.Sprintf("%s => %s", "toml", format)
+
+ converted := input
+ var err error
+ // Do a round-trip conversion
+ for _, toFormat := range []string{format, "toml"} {
+ converted, err = ns.Remarshal(toFormat, converted)
+ assert.NoError(err, fromTo)
+ }
+
+ diff := helpers.DiffStrings(expected, converted)
+ if len(diff) > 0 {
+ t.Fatalf("[%s] Expected \n%v\ngot\n%v\ndiff:\n%v\n", fromTo, expected, converted, diff)
+ }
+ }
+}
+
+func TestTestRemarshalError(t *testing.T) {
+ t.Parallel()
+
+ v := viper.New()
+ v.Set("contentDir", "content")
+ ns := New(newDeps(v))
+ assert := require.New(t)
+
+ _, err := ns.Remarshal("asdf", "asdf")
+ assert.Error(err)
+
+ _, err = ns.Remarshal("json", "asdf")
+ assert.Error(err)
+
+}
diff --git a/tpl/transform/transform.go b/tpl/transform/transform.go
new file mode 100644
index 000000000..2aa0c1959
--- /dev/null
+++ b/tpl/transform/transform.go
@@ -0,0 +1,123 @@
+// 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 provides template functions for transforming content.
+package transform
+
+import (
+ "html"
+ "html/template"
+
+ "github.com/gohugoio/hugo/cache/namedmemcache"
+
+ "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 {
+ cache := namedmemcache.New()
+ deps.BuildStartListeners.Add(
+ func() {
+ cache.Clear()
+ })
+
+ return &Namespace{
+ cache: cache,
+ deps: deps,
+ }
+}
+
+// Namespace provides template functions for the "transform" namespace.
+type Namespace struct {
+ cache *namedmemcache.Cache
+ 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
+ }
+
+ highlighted, _ := ns.deps.ContentSpec.Highlight(ss, lang, opts)
+ return template.HTML(highlighted), 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
+}
+
+// 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.BlackFriday,
+ },
+ )
+
+ // Strip if this is a short inline type of text.
+ m = ns.deps.ContentSpec.TrimShortHTML(m)
+
+ return helpers.BytesToHTML(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..a09ec6fbd
--- /dev/null
+++ b/tpl/transform/transform_test.go
@@ -0,0 +1,257 @@
+// 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/gohugoio/hugo/langs"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type tstNoStringer struct{}
+
+func TestEmojify(t *testing.T) {
+ t.Parallel()
+
+ v := viper.New()
+ ns := New(newDeps(v))
+
+ 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()
+
+ v := viper.New()
+ v.Set("contentDir", "content")
+ ns := New(newDeps(v))
+
+ for i, test := range []struct {
+ s interface{}
+ lang string
+ opts string
+ expect interface{}
+ }{
+ {"func boo() {}", "go", "", "boo"},
+ // Issue #4179
+ {`<Foo attr=" &lt; "></Foo>`, "xml", "", `&amp;lt;`},
+ {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, test.expect.(string), errMsg)
+ }
+}
+
+func TestHTMLEscape(t *testing.T) {
+ t.Parallel()
+
+ v := viper.New()
+ v.Set("contentDir", "content")
+ ns := New(newDeps(v))
+
+ for i, test := range []struct {
+ s interface{}
+ expect interface{}
+ }{
+ {`"Foo & Bar's Diner" <y@z>`, `&#34;Foo &amp; Bar&#39;s Diner&#34; &lt;y@z&gt;`},
+ {"Hugo & Caddy > Wordpress & Apache", "Hugo &amp; Caddy &gt; Wordpress &amp; 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()
+
+ v := viper.New()
+ v.Set("contentDir", "content")
+ ns := New(newDeps(v))
+
+ for i, test := range []struct {
+ s interface{}
+ expect interface{}
+ }{
+ {`&quot;Foo &amp; Bar&#39;s Diner&quot; &lt;y@z&gt;`, `"Foo & Bar's Diner" <y@z>`},
+ {"Hugo &amp; Caddy &gt; Wordpress &amp; 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()
+
+ v := viper.New()
+ v.Set("contentDir", "content")
+ ns := New(newDeps(v))
+
+ 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)
+ }
+}
+
+// Issue #3040
+func TestMarkdownifyBlocksOfText(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ v := viper.New()
+ v.Set("contentDir", "content")
+ ns := New(newDeps(v))
+
+ text := `
+#First
+
+This is some *bold* text.
+
+## Second
+
+This is some more text.
+
+And then some.
+`
+
+ result, err := ns.Markdownify(text)
+ assert.NoError(err)
+ assert.Equal(template.HTML(
+ "<p>#First</p>\n\n<p>This is some <em>bold</em> text.</p>\n\n<h2 id=\"second\">Second</h2>\n\n<p>This is some more text.</p>\n\n<p>And then some.</p>\n"),
+ result)
+
+}
+
+func TestPlainify(t *testing.T) {
+ t.Parallel()
+
+ v := viper.New()
+ ns := New(newDeps(v))
+
+ 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 {
+ cfg.Set("contentDir", "content")
+ cfg.Set("i18nDir", "i18n")
+
+ l := langs.NewLanguage("en", cfg)
+
+ cs, err := helpers.NewContentSpec(l)
+ if err != nil {
+ panic(err)
+ }
+
+ return &deps.Deps{
+ Cfg: cfg,
+ Fs: hugofs.NewMem(l),
+ ContentSpec: cs,
+ }
+}
diff --git a/tpl/transform/unmarshal.go b/tpl/transform/unmarshal.go
new file mode 100644
index 000000000..da06b6aa1
--- /dev/null
+++ b/tpl/transform/unmarshal.go
@@ -0,0 +1,167 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 (
+ "io/ioutil"
+ "strings"
+
+ "github.com/mitchellh/mapstructure"
+
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/parser/metadecoders"
+ "github.com/gohugoio/hugo/resources/resource"
+ "github.com/pkg/errors"
+
+ "github.com/spf13/cast"
+)
+
+// Unmarshal unmarshals the data given, which can be either a string
+// or a Resource. Supported formats are JSON, TOML, YAML, and CSV.
+// You can optionally provide an options map as the first argument.
+func (ns *Namespace) Unmarshal(args ...interface{}) (interface{}, error) {
+ if len(args) < 1 || len(args) > 2 {
+ return nil, errors.New("unmarshal takes 1 or 2 arguments")
+ }
+
+ var data interface{}
+ var decoder = metadecoders.Default
+
+ if len(args) == 1 {
+ data = args[0]
+ } else {
+ m, ok := args[0].(map[string]interface{})
+ if !ok {
+ return nil, errors.New("first argument must be a map")
+ }
+
+ var err error
+
+ data = args[1]
+ decoder, err = decodeDecoder(m)
+ if err != nil {
+ return nil, errors.WithMessage(err, "failed to decode options")
+ }
+ }
+
+ if r, ok := data.(unmarshableResource); ok {
+ key := r.Key()
+
+ if key == "" {
+ return nil, errors.New("no Key set in Resource")
+ }
+
+ if decoder != metadecoders.Default {
+ key += decoder.OptionsKey()
+ }
+
+ return ns.cache.GetOrCreate(key, func() (interface{}, error) {
+ f := metadecoders.FormatFromMediaType(r.MediaType())
+ if f == "" {
+ return nil, errors.Errorf("MIME %q not supported", r.MediaType())
+ }
+
+ reader, err := r.ReadSeekCloser()
+ if err != nil {
+ return nil, err
+ }
+ defer reader.Close()
+
+ b, err := ioutil.ReadAll(reader)
+ if err != nil {
+ return nil, err
+ }
+
+ return decoder.Unmarshal(b, f)
+ })
+ }
+
+ dataStr, err := cast.ToStringE(data)
+ if err != nil {
+ return nil, errors.Errorf("type %T not supported", data)
+ }
+
+ key := helpers.MD5String(dataStr)
+
+ return ns.cache.GetOrCreate(key, func() (interface{}, error) {
+ f := decoder.FormatFromContentString(dataStr)
+ if f == "" {
+ return nil, errors.New("unknown format")
+ }
+
+ return decoder.Unmarshal([]byte(dataStr), f)
+ })
+}
+
+// All the relevant resources implements this interface.
+type unmarshableResource interface {
+ resource.ReadSeekCloserResource
+ resource.Identifier
+}
+
+func decodeDecoder(m map[string]interface{}) (metadecoders.Decoder, error) {
+ opts := metadecoders.Default
+
+ if m == nil {
+ return opts, nil
+ }
+
+ // mapstructure does not support string to rune conversion, so do that manually.
+ // See https://github.com/mitchellh/mapstructure/issues/151
+ for k, v := range m {
+ if strings.EqualFold(k, "Delimiter") {
+ r, err := stringToRune(v)
+ if err != nil {
+ return opts, err
+ }
+ opts.Delimiter = r
+ delete(m, k)
+
+ } else if strings.EqualFold(k, "Comment") {
+ r, err := stringToRune(v)
+ if err != nil {
+ return opts, err
+ }
+ opts.Comment = r
+ delete(m, k)
+ }
+ }
+
+ err := mapstructure.WeakDecode(m, &opts)
+
+ return opts, err
+}
+
+func stringToRune(v interface{}) (rune, error) {
+ s, err := cast.ToStringE(v)
+ if err != nil {
+ return 0, err
+ }
+
+ if len(s) == 0 {
+ return 0, nil
+ }
+
+ var r rune
+
+ for i, rr := range s {
+ if i == 0 {
+ r = rr
+ } else {
+ return 0, errors.Errorf("invalid character: %q", v)
+ }
+ }
+
+ return r, nil
+}
diff --git a/tpl/transform/unmarshal_test.go b/tpl/transform/unmarshal_test.go
new file mode 100644
index 000000000..e91f680c2
--- /dev/null
+++ b/tpl/transform/unmarshal_test.go
@@ -0,0 +1,226 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+ "math/rand"
+ "strings"
+ "testing"
+
+ "github.com/gohugoio/hugo/common/hugio"
+
+ "github.com/gohugoio/hugo/media"
+
+ "github.com/gohugoio/hugo/resources/resource"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/require"
+)
+
+const (
+ testJSON = `
+
+{
+ "ROOT_KEY": {
+ "title": "example glossary",
+ "GlossDiv": {
+ "title": "S",
+ "GlossList": {
+ "GlossEntry": {
+ "ID": "SGML",
+ "SortAs": "SGML",
+ "GlossTerm": "Standard Generalized Markup Language",
+ "Acronym": "SGML",
+ "Abbrev": "ISO 8879:1986",
+ "GlossDef": {
+ "para": "A meta-markup language, used to create markup languages such as DocBook.",
+ "GlossSeeAlso": ["GML", "XML"]
+ },
+ "GlossSee": "markup"
+ }
+ }
+ }
+ }
+}
+
+ `
+)
+
+var _ resource.ReadSeekCloserResource = (*testContentResource)(nil)
+
+type testContentResource struct {
+ content string
+ mime media.Type
+
+ key string
+}
+
+func (t testContentResource) ReadSeekCloser() (hugio.ReadSeekCloser, error) {
+ return hugio.NewReadSeekerNoOpCloserFromString(t.content), nil
+}
+
+func (t testContentResource) MediaType() media.Type {
+ return t.mime
+}
+
+func (t testContentResource) Key() string {
+ return t.key
+}
+
+func TestUnmarshal(t *testing.T) {
+
+ v := viper.New()
+ ns := New(newDeps(v))
+ assert := require.New(t)
+
+ assertSlogan := func(m map[string]interface{}) {
+ assert.Equal("Hugo Rocks!", m["slogan"])
+ }
+
+ for i, test := range []struct {
+ data interface{}
+ options interface{}
+ expect interface{}
+ }{
+ {`{ "slogan": "Hugo Rocks!" }`, nil, func(m map[string]interface{}) {
+ assertSlogan(m)
+ }},
+ {`slogan: "Hugo Rocks!"`, nil, func(m map[string]interface{}) {
+ assertSlogan(m)
+ }},
+ {`slogan = "Hugo Rocks!"`, nil, func(m map[string]interface{}) {
+ assertSlogan(m)
+ }},
+ {testContentResource{key: "r1", content: `slogan: "Hugo Rocks!"`, mime: media.YAMLType}, nil, func(m map[string]interface{}) {
+ assertSlogan(m)
+ }},
+ {testContentResource{key: "r1", content: `{ "slogan": "Hugo Rocks!" }`, mime: media.JSONType}, nil, func(m map[string]interface{}) {
+ assertSlogan(m)
+ }},
+ {testContentResource{key: "r1", content: `slogan = "Hugo Rocks!"`, mime: media.TOMLType}, nil, func(m map[string]interface{}) {
+ assertSlogan(m)
+ }},
+ {testContentResource{key: "r1", content: `1997,Ford,E350,"ac, abs, moon",3000.00
+1999,Chevy,"Venture ""Extended Edition""","",4900.00`, mime: media.CSVType}, nil, func(r [][]string) {
+ assert.Equal(2, len(r))
+ first := r[0]
+ assert.Equal(5, len(first))
+ assert.Equal("Ford", first[1])
+ }},
+ {testContentResource{key: "r1", content: `a;b;c`, mime: media.CSVType}, map[string]interface{}{"delimiter": ";"}, func(r [][]string) {
+ assert.Equal(r, [][]string{{"a", "b", "c"}})
+
+ }},
+ {"a,b,c", nil, func(r [][]string) {
+ assert.Equal(r, [][]string{{"a", "b", "c"}})
+
+ }},
+ {"a;b;c", map[string]interface{}{"delimiter": ";"}, func(r [][]string) {
+ assert.Equal(r, [][]string{{"a", "b", "c"}})
+
+ }},
+ {testContentResource{key: "r1", content: `
+% This is a comment
+a;b;c`, mime: media.CSVType}, map[string]interface{}{"DElimiter": ";", "Comment": "%"}, func(r [][]string) {
+ assert.Equal(r, [][]string{{"a", "b", "c"}})
+
+ }},
+ // errors
+ {"thisisnotavaliddataformat", nil, false},
+ {testContentResource{key: "r1", content: `invalid&toml"`, mime: media.TOMLType}, nil, false},
+ {testContentResource{key: "r1", content: `unsupported: MIME"`, mime: media.CalendarType}, nil, false},
+ {"thisisnotavaliddataformat", nil, false},
+ {`{ notjson }`, nil, false},
+ {tstNoStringer{}, nil, false},
+ } {
+ errMsg := fmt.Sprintf("[%d]", i)
+
+ ns.cache.Clear()
+
+ var args []interface{}
+
+ if test.options != nil {
+ args = []interface{}{test.options, test.data}
+ } else {
+ args = []interface{}{test.data}
+ }
+
+ result, err := ns.Unmarshal(args...)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ assert.Error(err, errMsg)
+ } else if fn, ok := test.expect.(func(m map[string]interface{})); ok {
+ assert.NoError(err, errMsg)
+ m, ok := result.(map[string]interface{})
+ assert.True(ok, errMsg)
+ fn(m)
+ } else if fn, ok := test.expect.(func(r [][]string)); ok {
+ assert.NoError(err, errMsg)
+ r, ok := result.([][]string)
+ assert.True(ok, errMsg)
+ fn(r)
+ } else {
+ assert.NoError(err, errMsg)
+ assert.Equal(test.expect, result, errMsg)
+ }
+
+ }
+}
+
+func BenchmarkUnmarshalString(b *testing.B) {
+ v := viper.New()
+ ns := New(newDeps(v))
+
+ const numJsons = 100
+
+ var jsons [numJsons]string
+ for i := 0; i < numJsons; i++ {
+ jsons[i] = strings.Replace(testJSON, "ROOT_KEY", fmt.Sprintf("root%d", i), 1)
+ }
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ result, err := ns.Unmarshal(jsons[rand.Intn(numJsons)])
+ if err != nil {
+ b.Fatal(err)
+ }
+ if result == nil {
+ b.Fatal("no result")
+ }
+ }
+}
+
+func BenchmarkUnmarshalResource(b *testing.B) {
+ v := viper.New()
+ ns := New(newDeps(v))
+
+ const numJsons = 100
+
+ var jsons [numJsons]testContentResource
+ for i := 0; i < numJsons; i++ {
+ key := fmt.Sprintf("root%d", i)
+ jsons[i] = testContentResource{key: key, content: strings.Replace(testJSON, "ROOT_KEY", key, 1), mime: media.JSONType}
+ }
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ result, err := ns.Unmarshal(jsons[rand.Intn(numJsons)])
+ if err != nil {
+ b.Fatal(err)
+ }
+ if result == nil {
+ b.Fatal("no result")
+ }
+ }
+}
diff --git a/tpl/urls/init.go b/tpl/urls/init.go
new file mode 100644
index 000000000..debaaabf9
--- /dev/null
+++ b/tpl/urls/init.go
@@ -0,0 +1,74 @@
+// 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{},
+ )
+
+ ns.AddMethodMapping(ctx.Anchorize,
+ []string{"anchorize"},
+ [][2]string{
+ {`{{ "This is a title" | anchorize }}`, `this-is-a-title`},
+ },
+ )
+
+ 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..a678ee6b1
--- /dev/null
+++ b/tpl/urls/init_test.go
@@ -0,0 +1,39 @@
+// 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/spf13/viper"
+ "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{Cfg: viper.New()})
+ 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..754114b2b
--- /dev/null
+++ b/tpl/urls/urls.go
@@ -0,0 +1,182 @@
+// 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 provides template functions to deal with URLs.
+package urls
+
+import (
+ "errors"
+ "fmt"
+
+ "html/template"
+ "net/url"
+
+ "github.com/gohugoio/hugo/common/urls"
+ "github.com/gohugoio/hugo/deps"
+ _errors "github.com/pkg/errors"
+ "github.com/russross/blackfriday"
+ "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,
+ multihost: deps.Cfg.GetBool("multihost"),
+ }
+}
+
+// Namespace provides template functions for the "urls" namespace.
+type Namespace struct {
+ deps *deps.Deps
+ multihost bool
+}
+
+// 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
+}
+
+// Parse parses rawurl into a URL structure. The rawurl may be relative or
+// absolute.
+func (ns *Namespace) Parse(rawurl interface{}) (*url.URL, error) {
+ s, err := cast.ToStringE(rawurl)
+ if err != nil {
+ return nil, _errors.Wrap(err, "Error in Parse")
+ }
+
+ return url.Parse(s)
+}
+
+// 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
+}
+
+// URLize returns the given argument formatted as URL.
+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
+}
+
+// Anchorize creates sanitized anchor names that are compatible with Blackfriday.
+func (ns *Namespace) Anchorize(a interface{}) (string, error) {
+ s, err := cast.ToStringE(a)
+ if err != nil {
+ return "", nil
+ }
+ return blackfriday.SanitizedAnchorName(s), nil
+}
+
+// Ref returns the absolute URL path to a given content item.
+func (ns *Namespace) Ref(in interface{}, args interface{}) (template.HTML, error) {
+ p, ok := in.(urls.RefLinker)
+ if !ok {
+ return "", errors.New("invalid Page received in Ref")
+ }
+ argsm, err := ns.refArgsToMap(args)
+ if err != nil {
+ return "", err
+ }
+ s, err := p.Ref(argsm)
+ return template.HTML(s), err
+}
+
+// RelRef returns the relative URL path to a given content item.
+func (ns *Namespace) RelRef(in interface{}, args interface{}) (template.HTML, error) {
+ p, ok := in.(urls.RefLinker)
+ if !ok {
+ return "", errors.New("invalid Page received in RelRef")
+ }
+ argsm, err := ns.refArgsToMap(args)
+ if err != nil {
+ return "", err
+ }
+
+ s, err := p.RelRef(argsm)
+ return template.HTML(s), err
+}
+
+func (ns *Namespace) refArgsToMap(args interface{}) (map[string]interface{}, error) {
+ var (
+ s string
+ of string
+ )
+ switch v := args.(type) {
+ case map[string]interface{}:
+ return v, nil
+ case map[string]string:
+ m := make(map[string]interface{})
+ for k, v := range v {
+ m[k] = v
+ }
+ return m, nil
+ case []string:
+ if len(v) == 0 || len(v) > 2 {
+ return nil, fmt.Errorf("invalid numer of arguments to ref")
+ }
+ // These where the options before we introduced the map type:
+ s = v[0]
+ if len(v) == 2 {
+ of = v[1]
+ }
+ default:
+ var err error
+ s, err = cast.ToStringE(args)
+ if err != nil {
+ return nil, err
+ }
+
+ }
+ return map[string]interface{}{
+ "path": s,
+ "outputFormat": of,
+ }, nil
+}
+
+// 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, !ns.multihost)), 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, !ns.multihost)), nil
+}
diff --git a/tpl/urls/urls_test.go b/tpl/urls/urls_test.go
new file mode 100644
index 000000000..7bcef9cd5
--- /dev/null
+++ b/tpl/urls/urls_test.go
@@ -0,0 +1,68 @@
+// 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 (
+ "fmt"
+ "net/url"
+ "testing"
+
+ "github.com/gohugoio/hugo/deps"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+var ns = New(&deps.Deps{Cfg: viper.New()})
+
+type tstNoStringer struct{}
+
+func TestParse(t *testing.T) {
+ t.Parallel()
+
+ for i, test := range []struct {
+ rawurl interface{}
+ expect interface{}
+ }{
+ {
+ "http://www.google.com",
+ &url.URL{
+ Scheme: "http",
+ Host: "www.google.com",
+ },
+ },
+ {
+ "http://j@ne:password@google.com",
+ &url.URL{
+ Scheme: "http",
+ User: url.UserPassword("j@ne", "password"),
+ Host: "google.com",
+ },
+ },
+ // errors
+ {tstNoStringer{}, false},
+ } {
+ errMsg := fmt.Sprintf("[%d] %v", i, test)
+
+ result, err := ns.Parse(test.rawurl)
+
+ 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/transform/chain.go b/transform/chain.go
new file mode 100644
index 000000000..74217dc72
--- /dev/null
+++ b/transform/chain.go
@@ -0,0 +1,112 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+)
+
+// Transformer is the func that needs to be implemented by a transformation step.
+type Transformer func(ft FromTo) error
+
+// BytesReader wraps the Bytes method, usually implemented by bytes.Buffer, and an
+// io.Reader.
+type BytesReader interface {
+ // The slice given by Bytes is valid for use only until the next buffer modification.
+ // That is, if you want to use this value outside of the current transformer step,
+ // you need to take a copy.
+ Bytes() []byte
+
+ io.Reader
+}
+
+// FromTo is sent to each transformation step in the chain.
+type FromTo interface {
+ From() BytesReader
+ To() io.Writer
+}
+
+// Chain is an ordered processing chain. The next transform operation will
+// receive the output from the previous.
+type Chain []Transformer
+
+// New creates a content transformer chain given the provided transform funcs.
+func New(trs ...Transformer) Chain {
+ return trs
+}
+
+// NewEmpty creates a new slice of transformers with a capacity of 20.
+func NewEmpty() Chain {
+ return make(Chain, 0, 20)
+}
+
+// Implements contentTransformer
+// Content is read from the from-buffer and rewritten to to the to-buffer.
+type fromToBuffer struct {
+ from *bytes.Buffer
+ to *bytes.Buffer
+}
+
+func (ft fromToBuffer) From() BytesReader {
+ return ft.from
+}
+
+func (ft fromToBuffer) To() io.Writer {
+ return ft.to
+}
+
+// Apply passes the given from io.Reader through the transformation chain.
+// The result is written to to.
+func (c *Chain) Apply(to io.Writer, from io.Reader) error {
+ if len(*c) == 0 {
+ _, err := io.Copy(to, from)
+ return err
+ }
+
+ b1 := bp.GetBuffer()
+ defer bp.PutBuffer(b1)
+
+ if _, err := b1.ReadFrom(from); err != nil {
+ return err
+ }
+
+ b2 := bp.GetBuffer()
+ defer bp.PutBuffer(b2)
+
+ fb := &fromToBuffer{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()
+ }
+ }
+
+ if err := tr(fb); err != nil {
+ return err
+ }
+ }
+
+ _, err := fb.to.WriteTo(to)
+ return err
+}
diff --git a/transform/chain_test.go b/transform/chain_test.go
new file mode 100644
index 000000000..a8d59f902
--- /dev/null
+++ b/transform/chain_test.go
@@ -0,0 +1,69 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestChainZeroTransformers(t *testing.T) {
+ tr := New()
+ in := new(bytes.Buffer)
+ out := new(bytes.Buffer)
+ if err := tr.Apply(in, out); err != nil {
+ t.Errorf("A zero transformer chain returned an error.")
+ }
+}
+
+func TestChaingMultipleTransformers(t *testing.T) {
+ f1 := func(ct FromTo) error {
+ _, err := ct.To().Write(bytes.Replace(ct.From().Bytes(), []byte("f1"), []byte("f1r"), -1))
+ return err
+ }
+ f2 := func(ct FromTo) error {
+ _, err := ct.To().Write(bytes.Replace(ct.From().Bytes(), []byte("f2"), []byte("f2r"), -1))
+ return err
+ }
+ f3 := func(ct FromTo) error {
+ _, err := ct.To().Write(bytes.Replace(ct.From().Bytes(), []byte("f3"), []byte("f3r"), -1))
+ return err
+ }
+
+ f4 := func(ct FromTo) error {
+ _, err := ct.To().Write(bytes.Replace(ct.From().Bytes(), []byte("f4"), []byte("f4r"), -1))
+ return err
+ }
+
+ tr := New(f1, f2, f3, f4)
+
+ out := new(bytes.Buffer)
+ if err := tr.Apply(out, strings.NewReader("Test: f4 f3 f1 f2 f1 The End.")); err != nil {
+ t.Errorf("Multi transformer chain returned an error: %s", err)
+ }
+
+ expected := "Test: f4r f3r f1r f2r f1r The End."
+
+ if out.String() != expected {
+ t.Errorf("Expected %s got %s", expected, out.String())
+ }
+}
+
+func TestNewEmptyTransforms(t *testing.T) {
+ transforms := NewEmpty()
+ assert.Equal(t, 20, cap(transforms))
+}
diff --git a/transform/livereloadinject/livereloadinject.go b/transform/livereloadinject/livereloadinject.go
new file mode 100644
index 000000000..e04b977f7
--- /dev/null
+++ b/transform/livereloadinject/livereloadinject.go
@@ -0,0 +1,47 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package livereloadinject
+
+import (
+ "bytes"
+ "fmt"
+
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/transform"
+)
+
+// New creates a function that can be used
+// to inject a script tag for the livereload JavaScript in a HTML document.
+func New(port int) transform.Transformer {
+ return func(ft transform.FromTo) error {
+ b := ft.From().Bytes()
+ 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(b, match, replace, 1)
+ if len(newcontent) == len(b) {
+ endBodyTag = "</BODY>"
+ replace := []byte(fmt.Sprintf(replaceTemplate, port, endBodyTag))
+ match := []byte(endBodyTag)
+ newcontent = bytes.Replace(b, match, replace, 1)
+ }
+
+ if _, err := ft.To().Write(newcontent); err != nil {
+ helpers.DistinctWarnLog.Println("Failed to inject LiveReload script:", err)
+ }
+ return nil
+ }
+}
diff --git a/transform/livereloadinject/livereloadinject_test.go b/transform/livereloadinject/livereloadinject_test.go
new file mode 100644
index 000000000..1058244b4
--- /dev/null
+++ b/transform/livereloadinject/livereloadinject_test.go
@@ -0,0 +1,41 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package livereloadinject
+
+import (
+ "bytes"
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/gohugoio/hugo/transform"
+)
+
+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 := transform.New(New(1313))
+ tr.Apply(out, in)
+
+ expected := fmt.Sprintf(`<script data-no-instant>document.write('<script src="/livereload.js?port=1313&mindelay=10"></' + 'script>')</script>%s`, bodyEndTag)
+ if out.String() != expected {
+ t.Errorf("Expected %s got %s", expected, out.String())
+ }
+}
diff --git a/transform/metainject/hugogenerator.go b/transform/metainject/hugogenerator.go
new file mode 100644
index 000000000..5f3a8f63b
--- /dev/null
+++ b/transform/metainject/hugogenerator.go
@@ -0,0 +1,55 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metainject
+
+import (
+ "bytes"
+ "fmt"
+ "regexp"
+
+ "github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/transform"
+)
+
+var metaTagsCheck = regexp.MustCompile(`(?i)<meta\s+name=['|"]?generator['|"]?`)
+var hugoGeneratorTag = fmt.Sprintf(`<meta name="generator" content="Hugo %s" />`, hugo.CurrentVersion)
+
+// HugoGenerator injects a meta generator tag for Hugo if none present.
+func HugoGenerator(ft transform.FromTo) error {
+ b := ft.From().Bytes()
+ if metaTagsCheck.Match(b) {
+ if _, err := ft.To().Write(b); err != nil {
+ helpers.DistinctWarnLog.Println("Failed to inject Hugo generator tag:", err)
+ }
+ return nil
+ }
+
+ head := "<head>"
+ replace := []byte(fmt.Sprintf("%s\n\t%s", head, hugoGeneratorTag))
+ newcontent := bytes.Replace(b, []byte(head), replace, 1)
+
+ if len(newcontent) == len(b) {
+ head := "<HEAD>"
+ replace := []byte(fmt.Sprintf("%s\n\t%s", head, hugoGeneratorTag))
+ newcontent = bytes.Replace(b, []byte(head), replace, 1)
+ }
+
+ if _, err := ft.To().Write(newcontent); err != nil {
+ helpers.DistinctWarnLog.Println("Failed to inject Hugo generator tag:", err)
+ }
+
+ return nil
+
+}
diff --git a/transform/metainject/hugogenerator_test.go b/transform/metainject/hugogenerator_test.go
new file mode 100644
index 000000000..ffb4c1425
--- /dev/null
+++ b/transform/metainject/hugogenerator_test.go
@@ -0,0 +1,61 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metainject
+
+import (
+ "bytes"
+ "strings"
+ "testing"
+
+ "github.com/gohugoio/hugo/transform"
+)
+
+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 := transform.New(HugoGenerator)
+ tr.Apply(out, in)
+
+ if out.String() != this.expect {
+ t.Errorf("[%d] Expected \n%q got \n%q", i, this.expect, out.String())
+ }
+ }
+
+}
diff --git a/transform/urlreplacers/absurl.go b/transform/urlreplacers/absurl.go
new file mode 100644
index 000000000..029d94da2
--- /dev/null
+++ b/transform/urlreplacers/absurl.go
@@ -0,0 +1,36 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package urlreplacers
+
+import "github.com/gohugoio/hugo/transform"
+
+var ar = newAbsURLReplacer()
+
+// NewAbsURLTransformer replaces relative URLs with absolute ones
+// in HTML files, using the baseURL setting.
+func NewAbsURLTransformer(path string) transform.Transformer {
+ return func(ft transform.FromTo) error {
+ ar.replaceInHTML(path, ft)
+ return nil
+ }
+}
+
+// NewAbsURLInXMLTransformer replaces relative URLs with absolute ones
+// in XML files, using the baseURL setting.
+func NewAbsURLInXMLTransformer(path string) transform.Transformer {
+ return func(ft transform.FromTo) error {
+ ar.replaceInXML(path, ft)
+ return nil
+ }
+}
diff --git a/transform/urlreplacers/absurlreplacer.go b/transform/urlreplacers/absurlreplacer.go
new file mode 100644
index 000000000..545df914a
--- /dev/null
+++ b/transform/urlreplacers/absurlreplacer.go
@@ -0,0 +1,245 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package urlreplacers
+
+import (
+ "bytes"
+ "io"
+ "unicode"
+ "unicode/utf8"
+
+ "github.com/gohugoio/hugo/transform"
+)
+
+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
+
+ quotes [][]byte
+}
+
+type prefix struct {
+ disabled bool
+ b []byte
+ f func(l *absurllexer)
+}
+
+func newPrefixState() []*prefix {
+ return []*prefix{
+ {b: []byte("src="), f: checkCandidateBase},
+ {b: []byte("href="), f: checkCandidateBase},
+ {b: []byte("action="), f: checkCandidateBase},
+ {b: []byte("srcset="), f: checkCandidateSrcset},
+ }
+}
+
+func (l *absurllexer) emit() {
+ l.w.Write(l.content[l.start:l.pos])
+ l.start = l.pos
+}
+
+var (
+ relURLPrefix = []byte("/")
+ relURLPrefixLen = len(relURLPrefix)
+)
+
+func (l *absurllexer) consumeQuote() []byte {
+ for _, q := range l.quotes {
+ if bytes.HasPrefix(l.content[l.pos:], q) {
+ l.pos += len(q)
+ l.emit()
+ return q
+ }
+ }
+ return nil
+}
+
+// handle URLs in src and href.
+func checkCandidateBase(l *absurllexer) {
+ l.consumeQuote()
+
+ if !bytes.HasPrefix(l.content[l.pos:], relURLPrefix) {
+ return
+ }
+
+ // check for schemaless URLs
+ posAfter := l.pos + relURLPrefixLen
+ 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 += relURLPrefixLen
+ l.w.Write(l.path)
+ l.start = l.pos
+}
+
+func (l *absurllexer) posAfterURL(q []byte) int {
+ if len(q) > 0 {
+ // look for end quote
+ return bytes.Index(l.content[l.pos:], q)
+ }
+
+ return bytes.IndexFunc(l.content[l.pos:], func(r rune) bool {
+ return r == '>' || unicode.IsSpace(r)
+ })
+
+}
+
+// handle URLs in srcset.
+func checkCandidateSrcset(l *absurllexer) {
+ q := l.consumeQuote()
+ if q == nil {
+ // srcset needs to be quoted.
+ return
+ }
+
+ // special case, not frequent (me think)
+ if !bytes.HasPrefix(l.content[l.pos:], relURLPrefix) {
+ return
+ }
+
+ // check for schemaless URLs
+ posAfter := l.pos + relURLPrefixLen
+ if posAfter >= len(l.content) {
+ return
+ }
+ r, _ := utf8.DecodeRune(l.content[posAfter:])
+ if r == '/' {
+ // schemaless: skip
+ return
+ }
+
+ posEnd := l.posAfterURL(q)
+
+ // safe guard
+ if posEnd < 0 || posEnd > 2000 {
+ return
+ }
+
+ if l.pos > l.start {
+ l.emit()
+ }
+
+ section := l.content[l.pos : l.pos+posEnd+1]
+
+ fields := bytes.Fields(section)
+ 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.pos += len(section)
+ l.start = l.pos
+
+}
+
+// main loop
+func (l *absurllexer) replace() {
+ contentLength := len(l.content)
+
+ prefixes := newPrefixState()
+
+ for {
+ if l.pos >= contentLength {
+ break
+ }
+
+ nextPos := -1
+
+ var match *prefix
+
+ for _, p := range prefixes {
+ if p.disabled {
+ continue
+ }
+ idx := bytes.Index(l.content[l.pos:], p.b)
+
+ if idx == -1 {
+ p.disabled = true
+ // Find the closest match
+ } else if nextPos == -1 || idx < nextPos {
+ nextPos = idx
+ match = p
+ }
+ }
+
+ if nextPos == -1 {
+ // Done!
+ l.pos = contentLength
+ break
+ } else {
+ l.pos += nextPos + len(match.b)
+ match.f(l)
+ }
+ }
+
+ // Done!
+ if l.pos > l.start {
+ l.emit()
+ }
+}
+
+func doReplace(path string, ct transform.FromTo, quotes [][]byte) {
+
+ lexer := &absurllexer{
+ content: ct.From().Bytes(),
+ w: ct.To(),
+ path: []byte(path),
+ quotes: quotes}
+
+ lexer.replace()
+}
+
+type absURLReplacer struct {
+ htmlQuotes [][]byte
+ xmlQuotes [][]byte
+}
+
+func newAbsURLReplacer() *absURLReplacer {
+ return &absURLReplacer{
+ htmlQuotes: [][]byte{[]byte("\""), []byte("'")},
+ xmlQuotes: [][]byte{[]byte("&#34;"), []byte("&#39;")}}
+}
+
+func (au *absURLReplacer) replaceInHTML(path string, ct transform.FromTo) {
+ doReplace(path, ct, au.htmlQuotes)
+}
+
+func (au *absURLReplacer) replaceInXML(path string, ct transform.FromTo) {
+ doReplace(path, ct, au.xmlQuotes)
+}
diff --git a/transform/urlreplacers/absurlreplacer_test.go b/transform/urlreplacers/absurlreplacer_test.go
new file mode 100644
index 000000000..3c2dbf777
--- /dev/null
+++ b/transform/urlreplacers/absurlreplacer_test.go
@@ -0,0 +1,237 @@
+// Copyright 2018 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package urlreplacers
+
+import (
+ "path/filepath"
+ "testing"
+
+ bp "github.com/gohugoio/hugo/bufferpool"
+
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/transform"
+)
+
+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\">&lt;p&gt;&lt;a href=&#34;/foobar&#34;&gt;foobar&lt;/a&gt;&lt;/p&gt; &lt;p&gt;A video: &lt;iframe src=&#39;/foo&#39;&gt;&lt;/iframe&gt;&lt;/p&gt;</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\">&lt;p&gt;&lt;a href=&#34;http://base/foobar&#34;&gt;foobar&lt;/a&gt;&lt;/p&gt; &lt;p&gt;A video: &lt;iframe src=&#39;http://base/foo&#39;&gt;&lt;/iframe&gt;&lt;/p&gt;</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\">&lt;p&gt;&lt;a href=&#34;//foobar&#34;&gt;foobar&lt;/a&gt;&lt;/p&gt; &lt;p&gt;A video: &lt;iframe src=&#39;//foo&#39;&gt;&lt;/iframe&gt;&lt;/p&gt;</content></entry></feed>"
+)
+
+const (
+ // additional sanity tests for replacements testing
+ replace1 = "No replacements."
+ replace2 = "ᚠᛇᚻ ᛒᛦᚦ ᚠᚱᚩᚠᚢᚱ\nᚠᛁᚱᚪ ᚷᛖᚻᚹᛦᛚᚳᚢᛗ"
+ replace3 = `End of file: src="/`
+ 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=&#39;//schemaless&#39; src=&#39;/normal&#39; <a href=&#39;//schemaless&#39;>Schemaless</a>. <a href=&#39;/normal&#39;>normal</a>. Post.`
+ replaceSchemalessXMLCorrect = `Pre. src=&#39;//schemaless&#39; src=&#39;http://base/normal&#39; <a href=&#39;//schemaless&#39;>Schemaless</a>. <a href=&#39;http://base/normal&#39;>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=&#34;/img/small.jpg 200w, /img/big.jpg 700w&#34; alt=&#34;text&#34; src=&#34;/img/foo.jpg&#34;>`
+ srcsetXMLBasicCorrect = `Pre. <img srcset=&#34;http://base/img/small.jpg 200w, http://base/img/big.jpg 700w&#34; alt=&#34;text&#34; src=&#34;http://base/img/foo.jpg&#34;>`
+ srcsetXMLSingleQuote = `Pre. <img srcset=&#34;/img/small.jpg 200w, /img/big.jpg 700w&#34; alt=&#34;text&#34; src=&#34;/img/foo.jpg&#34;>`
+ srcsetXMLSingleQuoteCorrect = `Pre. <img srcset=&#34;http://base/img/small.jpg 200w, http://base/img/big.jpg 700w&#34; alt=&#34;text&#34; src=&#34;http://base/img/foo.jpg&#34;>`
+ 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: &lt;img srcset=/img/small.jpg 200w /img/big.jpg 700w&quot; alt=&quot;text&quot;&gt; src=&#39;/img/foo.jpg&#39;&gt; FOO.
+&lt;img srcset=&#39;/img.jpg&#39;&gt;
+schemaless: &lt;img srcset=&#39;//img.jpg&#39; src=&#39;//basic.jpg&#39;&gt;
+schemaless2: &lt;img srcset=&quot;//img.jpg&quot; src=&quot;//basic.jpg2&gt; POST
+`
+ srcsetXMLVariationsCorrect = `Pre.
+Missing start quote: &lt;img srcset=/img/small.jpg 200w /img/big.jpg 700w&quot; alt=&quot;text&quot;&gt; src=&#39;http://base/img/foo.jpg&#39;&gt; FOO.
+&lt;img srcset=&#39;http://base/img.jpg&#39;&gt;
+schemaless: &lt;img srcset=&#39;//img.jpg&#39; src=&#39;//basic.jpg&#39;&gt;
+schemaless2: &lt;img srcset=&quot;//img.jpg&quot; src=&quot;//basic.jpg2&gt; POST
+`
+
+ relPathVariations = `PRE. a href="/img/small.jpg" input action="/foo.html" POST.`
+ relPathVariationsCorrect = `PRE. a href="../../img/small.jpg" input action="../../foo.html" 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 BenchmarkAbsURL(b *testing.B) {
+ tr := transform.New(NewAbsURLTransformer(testBaseURL))
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ apply(b.Errorf, tr, absURLlBenchTests)
+ }
+}
+
+func BenchmarkAbsURLSrcset(b *testing.B) {
+ tr := transform.New(NewAbsURLTransformer(testBaseURL))
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ apply(b.Errorf, tr, srcsetTests)
+ }
+}
+
+func BenchmarkXMLAbsURLSrcset(b *testing.B) {
+ tr := transform.New(NewAbsURLInXMLTransformer(testBaseURL))
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ apply(b.Errorf, tr, srcsetXMLTests)
+ }
+}
+
+func TestAbsURL(t *testing.T) {
+ tr := transform.New(NewAbsURLTransformer(testBaseURL))
+
+ apply(t.Errorf, tr, absURLTests)
+
+}
+
+func TestAbsURLUnqoted(t *testing.T) {
+ tr := transform.New(NewAbsURLTransformer(testBaseURL))
+
+ apply(t.Errorf, tr, []test{
+ {
+ content: `Link: <a href=/asdf>ASDF</a>`,
+ expected: `Link: <a href=http://base/asdf>ASDF</a>`,
+ },
+ {
+ content: `Link: <a href=/asdf >ASDF</a>`,
+ expected: `Link: <a href=http://base/asdf >ASDF</a>`,
+ },
+ })
+}
+
+func TestRelativeURL(t *testing.T) {
+ tr := transform.New(NewAbsURLTransformer(helpers.GetDottedRelativePath(filepath.FromSlash("/post/sub/"))))
+
+ applyWithPath(t.Errorf, tr, relurlTests)
+
+}
+
+func TestAbsURLSrcSet(t *testing.T) {
+ tr := transform.New(NewAbsURLTransformer(testBaseURL))
+
+ apply(t.Errorf, tr, srcsetTests)
+}
+
+func TestAbsXMLURLSrcSet(t *testing.T) {
+ tr := transform.New(NewAbsURLInXMLTransformer(testBaseURL))
+
+ apply(t.Errorf, tr, srcsetXMLTests)
+}
+
+func BenchmarkXMLAbsURL(b *testing.B) {
+ tr := transform.New(NewAbsURLInXMLTransformer(testBaseURL))
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ apply(b.Errorf, tr, xmlAbsURLBenchTests)
+ }
+}
+
+func TestXMLAbsURL(t *testing.T) {
+ tr := transform.New(NewAbsURLInXMLTransformer(testBaseURL))
+ apply(t.Errorf, tr, xmlAbsURLTests)
+}
+
+func apply(ef errorf, tr transform.Chain, tests []test) {
+ applyWithPath(ef, tr, tests)
+}
+
+func applyWithPath(ef errorf, tr transform.Chain, tests []test) {
+ out := bp.GetBuffer()
+ defer bp.PutBuffer(out)
+
+ in := bp.GetBuffer()
+ defer bp.PutBuffer(in)
+
+ for _, test := range tests {
+ var err error
+ in.WriteString(test.content)
+ err = tr.Apply(out, in)
+ if err != nil {
+ ef("Unexpected error: %s", err)
+ }
+ if test.expected != out.String() {
+ ef("Expected:\n%s\nGot:\n%s", test.expected, out.String())
+ }
+ out.Reset()
+ in.Reset()
+ }
+}
+
+type test struct {
+ content string
+ expected string
+}
+
+type errorf func(string, ...interface{})
diff --git a/watcher/batcher.go b/watcher/batcher.go
new file mode 100644
index 000000000..6f4b276cf
--- /dev/null
+++ b/watcher/batcher.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 watcher
+
+import (
+ "time"
+
+ "github.com/fsnotify/fsnotify"
+)
+
+// Batcher batches file watch events in a given interval.
+type Batcher struct {
+ *fsnotify.Watcher
+ interval time.Duration
+ done chan struct{}
+
+ Events chan []fsnotify.Event // Events are returned on this channel
+}
+
+// New creates and starts a Batcher with the given time interval.
+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)
+}
+
+// Close stops the watching of the files.
+func (b *Batcher) Close() {
+ b.done <- struct{}{}
+ b.Watcher.Close()
+}