Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
+605
View File
@@ -0,0 +1,605 @@
# -----------------------------------------------------------------------------
# This file has been formatted via a Rake Task configuring cops from
# RuboCop v1.57.2.
#
# To add more cops, paste configurations at the end of the file and run
# the rake task via `bundle exec rake rubocop:format_config`.
# -----------------------------------------------------------------------------
inherit_from: ".rubocop_todo.yml"
require:
- "./rubocop/jekyll"
- rubocop-minitest
- rubocop-performance
- rubocop-rake
- rubocop-rspec
# Directive for all cops
AllCops:
TargetRubyVersion: 2.7
Include:
- lib/**/*.rb
- test/**/*.rb
Exclude:
- benchmark/**/*
- bin/**/*
- exe/**/*
- script/**/*
- tmp/**/*
- vendor/**/*
# Configure custom cop
Jekyll/NoPutsAllowed:
Exclude:
- rake/*.rake
# Configure built-in cops
# =======================
# Gemspec cops
# ----------------------------------------
Gemspec/DeprecatedAttributeAssignment:
Enabled: true
Gemspec/DevelopmentDependencies:
Enabled: false
Gemspec/RequireMFA:
Enabled: false
# Layout cops
# ----------------------------------------
Layout/BeginEndAlignment:
Enabled: true
Layout/EmptyComment:
Enabled: false
Layout/EmptyLinesAroundAttributeAccessor:
Enabled: true
Layout/EndAlignment:
Severity: error
Layout/EndOfLine:
Enabled: false
Layout/FirstArrayElementIndentation:
EnforcedStyle: consistent
Layout/FirstHashElementIndentation:
EnforcedStyle: consistent
Layout/HashAlignment:
EnforcedHashRocketStyle: table
Layout/IndentationWidth:
Severity: error
Layout/LineContinuationLeadingSpace:
Enabled: true
Layout/LineContinuationSpacing:
Enabled: true
Layout/LineEndStringConcatenationIndentation:
Enabled: true
Layout/LineLength:
Exclude:
- Gemfile
- Rakefile
- features/**/*.rb
- rake/**/*.rake
Max: 100
Severity: warning
Layout/MultilineMethodCallIndentation:
EnforcedStyle: indented
Layout/MultilineOperationIndentation:
EnforcedStyle: indented
Layout/SpaceAroundMethodCallOperator:
Enabled: true
Layout/SpaceBeforeBrackets:
Enabled: true
Layout/SpaceInsideHashLiteralBraces:
Enabled: true
Exclude:
- test/**/*.rb
# Lint cops
# ----------------------------------------
Lint/AmbiguousAssignment:
Enabled: true
Lint/AmbiguousOperatorPrecedence:
Enabled: true
Lint/AmbiguousRange:
Enabled: true
Lint/BinaryOperatorWithIdenticalOperands:
Enabled: true
Lint/ConstantDefinitionInBlock:
Enabled: true
Exclude:
- test/**/*.rb
Lint/ConstantOverwrittenInRescue:
Enabled: true
Lint/DeprecatedConstants:
Enabled: true
Lint/DeprecatedOpenSSLConstant:
Enabled: true
Lint/DuplicateBranch:
Enabled: true
Lint/DuplicateElsifCondition:
Enabled: true
Lint/DuplicateMagicComment:
Enabled: true
Lint/DuplicateMatchPattern:
Enabled: true
Lint/DuplicateRegexpCharacterClassElement:
Enabled: true
Lint/DuplicateRequire:
Enabled: true
Lint/DuplicateRescueException:
Enabled: true
Lint/EmptyBlock:
Enabled: true
Lint/EmptyClass:
Enabled: true
Lint/EmptyConditionalBody:
Enabled: true
Lint/EmptyFile:
Enabled: true
Lint/FloatComparison:
Enabled: true
Lint/HashCompareByIdentity:
Enabled: true
Lint/IdentityComparison:
Enabled: true
Lint/LambdaWithoutLiteralBlock:
Enabled: true
Lint/MissingSuper:
Enabled: false
Lint/MixedCaseRange:
Enabled: true
Lint/MixedRegexpCaptureTypes:
Enabled: false
Lint/NestedPercentLiteral:
Exclude:
- test/test_site.rb
Lint/NoReturnInBeginEndBlocks:
Enabled: true
Lint/NumberedParameterAssignment:
Enabled: true
Lint/OrAssignmentToConstant:
Enabled: true
Lint/OutOfRangeRegexpRef:
Enabled: true
Lint/RaiseException:
Enabled: true
Lint/RedundantDirGlobSort:
Enabled: true
Lint/RedundantRegexpQuantifiers:
Enabled: true
Lint/RedundantSafeNavigation:
Enabled: true
Lint/RefinementImportMethods:
Enabled: true
Lint/RequireRangeParentheses:
Enabled: true
Lint/RequireRelativeSelfPath:
Enabled: true
Lint/SelfAssignment:
Enabled: true
Lint/StructNewOverride:
Enabled: true
Lint/SymbolConversion:
Enabled: true
Lint/ToEnumArguments:
Enabled: false
Lint/TopLevelReturnWithArgument:
Enabled: true
Lint/TrailingCommaInAttributeDeclaration:
Enabled: true
Lint/TripleQuotes:
Enabled: true
Lint/UnexpectedBlockArity:
Enabled: true
Lint/UnmodifiedReduceAccumulator:
Enabled: true
Lint/UnreachableCode:
Severity: error
Lint/UnreachableLoop:
Enabled: true
Lint/UselessMethodDefinition:
Enabled: true
Lint/UselessRuby2Keywords:
Enabled: true
Lint/UselessTimes:
Enabled: true
Lint/Void:
Exclude:
- lib/jekyll/site.rb
# Metrics cops
# ----------------------------------------
Metrics/AbcSize:
Max: 23
Metrics/BlockLength:
Exclude:
- lib/jekyll/configuration.rb
- rake/**/*.rake
- test/**/*.rb
Metrics/ClassLength:
Exclude:
- features/**/*.rb
- lib/jekyll/commands/serve.rb
- lib/jekyll/configuration.rb
- lib/jekyll/document.rb
- lib/jekyll/site.rb
- test/**/*.rb
Max: 240
Metrics/CollectionLiteralLength:
Enabled: true
Metrics/CyclomaticComplexity:
Exclude:
- lib/jekyll/commands/serve.rb
- lib/jekyll/utils.rb
Max: 11
Metrics/MethodLength:
CountComments: false
Max: 20
Severity: error
Metrics/ModuleLength:
Exclude:
- lib/jekyll/filters.rb
Max: 240
Metrics/ParameterLists:
Max: 4
Metrics/PerceivedComplexity:
Max: 13
# Minitest cops
# ----------------------------------------
Minitest/AssertEmptyLiteral:
Enabled: false
Minitest/AssertInDelta:
Enabled: true
Minitest/AssertKindOf:
Enabled: true
Minitest/AssertOperator:
Enabled: false
Minitest/AssertOutput:
Enabled: true
Minitest/AssertPathExists:
Enabled: true
Minitest/AssertPredicate:
Enabled: false
Minitest/AssertRaisesWithRegexpArgument:
Enabled: true
Minitest/AssertSame:
Enabled: true
Minitest/AssertSilent:
Enabled: true
Minitest/AssertWithExpectedArgument:
Enabled: true
Minitest/AssertionInLifecycleHook:
Enabled: true
Minitest/DuplicateTestRun:
Enabled: true
Minitest/EmptyLineBeforeAssertionMethods:
Enabled: false
Minitest/LifecycleHooksOrder:
Enabled: true
Minitest/LiteralAsActualArgument:
Enabled: true
Minitest/MultipleAssertions:
Enabled: true
Minitest/NonExecutableTestMethod:
Enabled: true
Minitest/NonPublicTestMethod:
Enabled: true
Minitest/RedundantMessageArgument:
Enabled: true
Minitest/RefuteInDelta:
Enabled: true
Minitest/RefuteKindOf:
Enabled: true
Minitest/RefuteOperator:
Enabled: false
Minitest/RefutePathExists:
Enabled: true
Minitest/RefutePredicate:
Enabled: false
Minitest/RefuteSame:
Enabled: true
Minitest/ReturnInTestMethod:
Enabled: true
Minitest/SkipEnsure:
Enabled: true
Minitest/SkipWithoutReason:
Enabled: true
Minitest/TestFileName:
Enabled: true
Exclude:
- test/helper.rb
Minitest/TestMethodName:
Enabled: false
Minitest/UnreachableAssertion:
Enabled: true
Minitest/UnspecifiedException:
Enabled: true
Minitest/UselessAssertion:
Enabled: true
# Naming cops
# ----------------------------------------
Naming/FileName:
Enabled: false
Naming/HeredocDelimiterNaming:
Exclude:
- test/**/*.rb
Naming/MemoizedInstanceVariableName:
Exclude:
- lib/jekyll/convertible.rb
- lib/jekyll/drops/site_drop.rb
- lib/jekyll/drops/unified_payload_drop.rb
- lib/jekyll/page_without_a_file.rb
# Performance cops
# ----------------------------------------
Performance/AncestorsInclude:
Enabled: false
Performance/ArraySemiInfiniteRangeSlice:
Enabled: true
Performance/BigDecimalWithNumericArgument:
Enabled: true
Performance/BlockGivenWithExplicitBlock:
Enabled: true
Performance/ChainArrayAllocation:
Enabled: true
Performance/CollectionLiteralInLoop:
Enabled: true
Performance/ConstantRegexp:
Enabled: true
Performance/MapCompact:
Enabled: true
Performance/MapMethodChain:
Enabled: true
Performance/MethodObjectAsBlock:
Enabled: true
Performance/RedundantEqualityComparisonBlock:
Enabled: false
Performance/RedundantSortBlock:
Enabled: true
Performance/RedundantSplitRegexpArgument:
Enabled: true
Performance/RedundantStringChars:
Enabled: true
Performance/ReverseFirst:
Enabled: true
Performance/SortReverse:
Enabled: false
Performance/Squeeze:
Enabled: true
Performance/StringIdentifierArgument:
Enabled: true
Performance/StringInclude:
Enabled: true
Exclude:
- lib/jekyll/utils/platforms.rb
Performance/Sum:
Enabled: true
# Security cops
# ----------------------------------------
Security/CompoundHash:
Enabled: true
Security/IoMethods:
Enabled: true
Security/MarshalLoad:
Exclude:
- lib/jekyll/regenerator.rb
- test/**/*.rb
Security/YAMLLoad:
Exclude:
- features/**/*.rb
- test/**/*.rb
# Style cops
# ----------------------------------------
Style/AccessModifierDeclarations:
Enabled: false
Style/AccessorGrouping:
Enabled: true
Style/Alias:
EnforcedStyle: prefer_alias_method
Style/AndOr:
Severity: error
Style/ArgumentsForwarding:
Enabled: false
Style/ArrayCoercion:
Enabled: true
Style/ArrayIntersect:
Enabled: true
Style/BisectedAttrAccessor:
Enabled: true
Style/CaseLikeIf:
Enabled: true
Style/ClassAndModuleChildren:
Exclude:
- test/**/*.rb
Style/ClassEqualityComparison:
Enabled: true
Style/CollectionCompact:
Enabled: true
Style/CombinableLoops:
Enabled: true
Style/ConcatArrayLiterals:
Enabled: true
Style/DirEmpty:
Enabled: true
Style/DocumentDynamicEvalDefinition:
Enabled: true
Style/Documentation:
Enabled: false
Style/DoubleNegation:
Enabled: false
Style/EmptyHeredoc:
Enabled: true
Style/EndlessMethod:
Enabled: true
Style/EnvHome:
Enabled: true
Style/ExactRegexpMatch:
Enabled: true
Style/ExplicitBlockArgument:
Enabled: false
Style/ExponentialNotation:
Enabled: true
Style/FetchEnvVar:
Enabled: false
Style/FileEmpty:
Enabled: true
Style/FileRead:
Enabled: false
Style/FileWrite:
Enabled: true
Style/FormatStringToken:
Exclude:
- lib/jekyll/liquid_renderer/table.rb
- lib/jekyll/profiler.rb
- lib/jekyll/utils/ansi.rb
Style/FrozenStringLiteralComment:
EnforcedStyle: always
Style/GlobalStdStream:
Enabled: true
Style/GuardClause:
Enabled: false
Style/HashAsLastArrayItem:
Enabled: true
Style/HashConversion:
Enabled: true
Style/HashEachMethods:
Enabled: true
Style/HashExcept:
Enabled: true
Style/HashLikeCase:
Enabled: true
Style/HashSyntax:
EnforcedStyle: hash_rockets
Severity: error
Style/HashTransformKeys:
Enabled: false
Style/HashTransformValues:
Enabled: true
Style/IfWithBooleanLiteralBranches:
Enabled: true
Style/KeywordParametersOrder:
Enabled: true
Style/MagicCommentFormat:
Enabled: true
Style/MapCompactWithConditionalBlock:
Enabled: true
Style/MapToHash:
Enabled: true
Style/MinMaxComparison:
Enabled: true
Style/MixinUsage:
Exclude:
- test/helper.rb
Style/ModuleFunction:
Enabled: false
Style/MultilineTernaryOperator:
Severity: error
Style/NegatedIfElseCondition:
Enabled: true
Style/NestedFileDirname:
Enabled: true
Style/NilLambda:
Enabled: true
Style/ObjectThen:
Enabled: true
Style/OperatorMethodCall:
Enabled: true
Style/OptionalBooleanParameter:
Enabled: true
Exclude:
- lib/jekyll/log_adapter.rb
Style/PercentLiteralDelimiters:
PreferredDelimiters:
"%Q": "{}"
"%W": "()"
"%q": "{}"
"%r": "!!"
"%s": "()"
"%w": "()"
"%x": "()"
Style/QuotedSymbols:
Enabled: true
Style/RedundantArgument:
Enabled: true
Style/RedundantArrayConstructor:
Enabled: true
Style/RedundantAssignment:
Enabled: true
Style/RedundantConstantBase:
Enabled: true
Style/RedundantCurrentDirectoryInPath:
Enabled: true
Style/RedundantDoubleSplatHashBraces:
Enabled: true
Style/RedundantEach:
Enabled: true
Style/RedundantFetchBlock:
Enabled: false
Style/RedundantFileExtensionInRequire:
Enabled: true
Style/RedundantFilterChain:
Enabled: true
Style/RedundantHeredocDelimiterQuotes:
Enabled: true
Style/RedundantInitialize:
Enabled: true
Exclude:
- lib/jekyll/plugin.rb
Style/RedundantLineContinuation:
Enabled: true
Style/RedundantRegexpArgument:
Enabled: true
Style/RedundantRegexpCharacterClass:
Enabled: true
Style/RedundantRegexpConstructor:
Enabled: true
Style/RedundantRegexpEscape:
Enabled: true
Style/RedundantSelfAssignment:
Enabled: true
Style/RedundantSelfAssignmentBranch:
Enabled: true
Style/RedundantStringEscape:
Enabled: true
Style/RegexpLiteral:
EnforcedStyle: percent_r
Style/RescueModifier:
Enabled: false
Style/ReturnNilInPredicateMethodDefinition:
Enabled: true
Style/SafeNavigation:
Exclude:
- lib/jekyll/document.rb
- lib/jekyll/page.rb
Style/SignalException:
EnforcedStyle: only_raise
Style/SingleArgumentDig:
Enabled: true
Style/SingleLineDoEndBlock:
Enabled: true
Style/SlicingWithRange:
Enabled: false
Style/SoleNestedConditional:
Enabled: true
Style/StringChars:
Enabled: true
Style/StringConcatenation:
Enabled: true
Exclude:
- lib/jekyll/commands/*.rb
- test/**/*.rb
Style/StringLiterals:
EnforcedStyle: double_quotes
Style/StringLiteralsInInterpolation:
EnforcedStyle: double_quotes
Style/SwapValues:
Enabled: true
Style/SymbolArray:
EnforcedStyle: brackets
Style/TrailingCommaInArrayLiteral:
EnforcedStyleForMultiline: consistent_comma
Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: consistent_comma
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2008-present Tom Preston-Werner and Jekyll contributors
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.
+82
View File
@@ -0,0 +1,82 @@
# [Jekyll](https://jekyllrb.com/)
[![Gem Version](https://img.shields.io/gem/v/jekyll.svg)][ruby-gems]
[![Build Status](https://github.com/jekyll/jekyll/workflows/Continuous%20Integration/badge.svg)][ci-workflow]
[![Backers on Open Collective](https://opencollective.com/jekyll/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/jekyll/sponsors/badge.svg)](#sponsors)
[ruby-gems]: https://rubygems.org/gems/jekyll
[ci-workflow]: https://github.com/jekyll/jekyll/actions?query=workflow%3A%22Continuous+Integration%22+branch%3Amaster
[appveyor]: https://ci.appveyor.com/project/jekyll/jekyll/branch/master
Jekyll is a simple, blog-aware, static site generator perfect for personal, project, or organization sites. Think of it like a file-based CMS, without all the complexity. Jekyll takes your content, renders Markdown and Liquid templates, and spits out a complete, static website ready to be served by Apache, Nginx or another web server. Jekyll is the engine behind [GitHub Pages](https://pages.github.com), which you can use to host sites right from your GitHub repositories.
## Philosophy
Jekyll does what you tell it to do — no more, no less. It doesn't try to outsmart users by making bold assumptions, nor does it burden them with needless complexity and configuration. Put simply, Jekyll gets out of your way and allows you to concentrate on what truly matters: your content.
See: [https://jekyllrb.com/philosophy](https://jekyllrb.com/philosophy)
## Getting Started
* [Install](https://jekyllrb.com/docs/installation/) the gem
* Read up about its [Usage](https://jekyllrb.com/docs/usage/) and [Configuration](https://jekyllrb.com/docs/configuration/)
* Take a gander at some existing [Sites](https://github.com/jekyll/jekyll/wiki/sites)
* [Fork](https://github.com/jekyll/jekyll/fork) and [Contribute](https://jekyllrb.com/docs/contributing/) your own modifications
* Have questions? Check out our official forum community [Jekyll Talk](https://talk.jekyllrb.com/) and [`#jekyll` Channel on Libera IRC](https://libera.chat)
## Diving In
* [Migrate](https://import.jekyllrb.com/docs/home/) from your previous system
* Learn how [Front Matter](https://jekyllrb.com/docs/front-matter/) works
* Put information on your site with [Variables](https://jekyllrb.com/docs/variables/)
* Customize the [Permalinks](https://jekyllrb.com/docs/permalinks/) your posts are generated with
* Use the built-in [Liquid Extensions](https://jekyllrb.com/docs/templates/) to make your life easier
* Use custom [Plugins](https://jekyllrb.com/docs/plugins/) to generate content specific to your site
* Watch [video tutorials from Giraffe Academy](https://jekyllrb.com/tutorials/video-walkthroughs/)
## Need help?
If you don't find the answer to your problem in our [docs](https://jekyllrb.com/docs/), or in the [troubleshooting section](https://jekyllrb.com/docs/troubleshooting/), ask the [community](https://jekyllrb.com/docs/community/) for help.
## Code of Conduct
In order to have a more open and welcoming community, Jekyll adheres to a
[code of conduct](https://jekyllrb.com/docs/conduct/) adapted from the Ruby on Rails code of
conduct.
Please adhere to this code of conduct in any interactions you have in the
Jekyll community. It is strictly enforced on all official Jekyll
repositories, websites, and resources. If you encounter someone violating
these terms, please let one of our [core team members](https://jekyllrb.com/team/#core-team) know and we will address it as soon as possible.
## Credits
### Sponsors
Support this project by becoming a sponsor. Your logo will show up in this README with a link to your website. [Become a sponsor!](https://opencollective.com/jekyll#sponsor)
[![Jekyll Sponsor 0](https://opencollective.com/jekyll/sponsor/0/avatar.svg)](https://opencollective.com/jekyll/sponsor/0/website)
[![Jekyll Sponsor 1](https://opencollective.com/jekyll/sponsor/1/avatar.svg)](https://opencollective.com/jekyll/sponsor/1/website)
[![Jekyll Sponsor 2](https://opencollective.com/jekyll/sponsor/2/avatar.svg)](https://opencollective.com/jekyll/sponsor/2/website)
[![Jekyll Sponsor 3](https://opencollective.com/jekyll/sponsor/3/avatar.svg)](https://opencollective.com/jekyll/sponsor/3/website)
[![Jekyll Sponsor 4](https://opencollective.com/jekyll/sponsor/4/avatar.svg)](https://opencollective.com/jekyll/sponsor/4/website)
[![Jekyll Sponsor 5](https://opencollective.com/jekyll/sponsor/5/avatar.svg)](https://opencollective.com/jekyll/sponsor/5/website)
[![Jekyll Sponsor 6](https://opencollective.com/jekyll/sponsor/6/avatar.svg)](https://opencollective.com/jekyll/sponsor/6/website)
[![Jekyll Sponsor 7](https://opencollective.com/jekyll/sponsor/7/avatar.svg)](https://opencollective.com/jekyll/sponsor/7/website)
[![Jekyll Sponsor 8](https://opencollective.com/jekyll/sponsor/8/avatar.svg)](https://opencollective.com/jekyll/sponsor/8/website)
[![Jekyll Sponsor 9](https://opencollective.com/jekyll/sponsor/9/avatar.svg)](https://opencollective.com/jekyll/sponsor/9/website)
### Contributors
This project exists thanks to all the people who contribute.
[![Jekyll Contributors](https://opencollective.com/jekyll/contributors.svg?width=890&button=false)](../../graphs/contributors)
### Backers
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/jekyll#backer)
[![Jekyll Backers](https://opencollective.com/jekyll/backers.svg?width=890)](https://opencollective.com/jekyll#backers)
## License
See the [LICENSE](https://github.com/jekyll/jekyll/blob/master/LICENSE) file.
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
STDOUT.sync = true
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
require "jekyll"
require "mercenary"
Jekyll::PluginManager.require_from_bundler
Jekyll::Deprecator.process(ARGV)
Mercenary.program(:jekyll) do |p|
p.version Jekyll::VERSION
p.description "Jekyll is a blog-aware, static site generator in Ruby"
p.syntax "jekyll <subcommand> [options]"
p.option "source", "-s", "--source [DIR]", "Source directory (defaults to ./)"
p.option "destination", "-d", "--destination [DIR]",
"Destination directory (defaults to ./_site)"
p.option "safe", "--safe", "Safe mode (defaults to false)"
p.option "plugins_dir", "-p", "--plugins PLUGINS_DIR1[,PLUGINS_DIR2[,...]]", Array,
"Plugins directory (defaults to ./_plugins)"
p.option "layouts_dir", "--layouts DIR", String,
"Layouts directory (defaults to ./_layouts)"
p.option "profile", "--profile", "Generate a Liquid rendering profile"
Jekyll::External.require_if_present(Jekyll::External.blessed_gems) do |g, ver_constraint|
cmd = g.split("-").last
p.command(cmd.to_sym) do |c|
c.syntax cmd
c.action do
Jekyll.logger.abort_with "You must install the '#{g}' gem" \
" version #{ver_constraint} to use the 'jekyll #{cmd}' command."
end
end
end
Jekyll::Command.subclasses.each { |c| c.init_with_program(p) }
p.action do |args, _|
if args.empty?
Jekyll.logger.error "A subcommand is required."
puts p
abort
else
subcommand = args.first
unless p.has_command? subcommand
Jekyll.logger.abort_with "fatal: 'jekyll #{args.first}' could not" \
" be found. You may need to install the jekyll-#{args.first} gem" \
" or a related gem to be able to use this subcommand."
end
end
end
end
@@ -0,0 +1,3 @@
url: "" # the base hostname & protocol for your site, e.g. http://example.com
baseurl: "" # the subpath of your site, e.g. /blog
title: "" # the name of your site, e.g. ACME Corp.
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="{{ site.lang | default: "en-US" }}">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8">
<title>{{ page.title }} - {{ site.title }}</title>
<link rel="stylesheet" href="{{ "/assets/css/main.css" | relative_url }}">
</head>
<body>
{{ content }}
</body>
</html>
@@ -0,0 +1,9 @@
$backgroundColor: #ffffff;
$bodyColor: #000000;
$bodyFont: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
body {
background: $backgroundColor;
color: $bodyColor;
font-family: $bodyFont;
}
@@ -0,0 +1,4 @@
---
---
@import "base";
@@ -0,0 +1,8 @@
---
layout: default
title: "Happy Jekylling!"
---
## You're ready to go!
Start developing your Jekyll website.
+195
View File
@@ -0,0 +1,195 @@
# frozen_string_literal: true
$LOAD_PATH.unshift __dir__ # For use/testing when no gem is installed
# Require all of the Ruby files in the given directory.
#
# path - The String relative path from here to the directory.
#
# Returns nothing.
def require_all(path)
glob = File.join(__dir__, path, "*.rb")
Dir[glob].sort.each do |f|
require f
end
end
# rubygems
require "rubygems"
# stdlib
require "forwardable"
require "fileutils"
require "time"
require "English"
require "pathname"
require "logger"
require "set"
require "csv"
require "json"
# 3rd party
require "pathutil"
require "addressable/uri"
require "safe_yaml/load"
require "liquid"
require "kramdown"
require "colorator"
require "i18n"
SafeYAML::OPTIONS[:suppress_warnings] = true
module Jekyll
# internal requires
autoload :Cleaner, "jekyll/cleaner"
autoload :Collection, "jekyll/collection"
autoload :Configuration, "jekyll/configuration"
autoload :Convertible, "jekyll/convertible"
autoload :Deprecator, "jekyll/deprecator"
autoload :Document, "jekyll/document"
autoload :EntryFilter, "jekyll/entry_filter"
autoload :Errors, "jekyll/errors"
autoload :Excerpt, "jekyll/excerpt"
autoload :PageExcerpt, "jekyll/page_excerpt"
autoload :External, "jekyll/external"
autoload :FrontmatterDefaults, "jekyll/frontmatter_defaults"
autoload :Hooks, "jekyll/hooks"
autoload :Layout, "jekyll/layout"
autoload :Inclusion, "jekyll/inclusion"
autoload :Cache, "jekyll/cache"
autoload :CollectionReader, "jekyll/readers/collection_reader"
autoload :DataReader, "jekyll/readers/data_reader"
autoload :LayoutReader, "jekyll/readers/layout_reader"
autoload :PostReader, "jekyll/readers/post_reader"
autoload :PageReader, "jekyll/readers/page_reader"
autoload :StaticFileReader, "jekyll/readers/static_file_reader"
autoload :ThemeAssetsReader, "jekyll/readers/theme_assets_reader"
autoload :LogAdapter, "jekyll/log_adapter"
autoload :Page, "jekyll/page"
autoload :PageWithoutAFile, "jekyll/page_without_a_file"
autoload :PathManager, "jekyll/path_manager"
autoload :PluginManager, "jekyll/plugin_manager"
autoload :Publisher, "jekyll/publisher"
autoload :Profiler, "jekyll/profiler"
autoload :Reader, "jekyll/reader"
autoload :Regenerator, "jekyll/regenerator"
autoload :RelatedPosts, "jekyll/related_posts"
autoload :Renderer, "jekyll/renderer"
autoload :LiquidRenderer, "jekyll/liquid_renderer"
autoload :Site, "jekyll/site"
autoload :StaticFile, "jekyll/static_file"
autoload :Stevenson, "jekyll/stevenson"
autoload :Theme, "jekyll/theme"
autoload :ThemeBuilder, "jekyll/theme_builder"
autoload :URL, "jekyll/url"
autoload :Utils, "jekyll/utils"
autoload :VERSION, "jekyll/version"
# extensions
require "jekyll/plugin"
require "jekyll/converter"
require "jekyll/generator"
require "jekyll/command"
require "jekyll/liquid_extensions"
require "jekyll/filters"
class << self
# Public: Tells you which Jekyll environment you are building in so you can skip tasks
# if you need to. This is useful when doing expensive compression tasks on css and
# images and allows you to skip that when working in development.
def env
ENV["JEKYLL_ENV"] || "development"
end
# Public: Generate a Jekyll configuration Hash by merging the default
# options with anything in _config.yml, and adding the given options on top.
#
# override - A Hash of config directives that override any options in both
# the defaults and the config file.
# See Jekyll::Configuration::DEFAULTS for a
# list of option names and their defaults.
#
# Returns the final configuration Hash.
def configuration(override = {})
config = Configuration.new
override = Configuration[override].stringify_keys
unless override.delete("skip_config_files")
config = config.read_config_files(config.config_files(override))
end
# Merge DEFAULTS < _config.yml < override
Configuration.from(Utils.deep_merge_hashes(config, override)).tap do |obj|
set_timezone(obj["timezone"]) if obj["timezone"]
end
end
# Public: Set the TZ environment variable to use the timezone specified
#
# timezone - the IANA Time Zone
#
# Returns nothing
# rubocop:disable Naming/AccessorMethodName
def set_timezone(timezone)
ENV["TZ"] = if Utils::Platforms.really_windows?
Utils::WinTZ.calculate(timezone)
else
timezone
end
end
# rubocop:enable Naming/AccessorMethodName
# Public: Fetch the logger instance for this Jekyll process.
#
# Returns the LogAdapter instance.
def logger
@logger ||= LogAdapter.new(Stevenson.new, (ENV["JEKYLL_LOG_LEVEL"] || :info).to_sym)
end
# Public: Set the log writer.
# New log writer must respond to the same methods
# as Ruby's internal Logger.
#
# writer - the new Logger-compatible log transport
#
# Returns the new logger.
def logger=(writer)
@logger = LogAdapter.new(writer, (ENV["JEKYLL_LOG_LEVEL"] || :info).to_sym)
end
# Public: An array of sites
#
# Returns the Jekyll sites created.
def sites
@sites ||= []
end
# Public: Ensures the questionable path is prefixed with the base directory
# and prepends the questionable path with the base directory if false.
#
# base_directory - the directory with which to prefix the questionable path
# questionable_path - the path we're unsure about, and want prefixed
#
# Returns the sanitized path.
def sanitized_path(base_directory, questionable_path)
return base_directory if base_directory.eql?(questionable_path)
return base_directory if questionable_path.nil?
+Jekyll::PathManager.sanitized_path(base_directory, questionable_path)
end
# Conditional optimizations
Jekyll::External.require_if_present("liquid/c")
end
end
require "jekyll/drops/drop"
require "jekyll/drops/document_drop"
require_all "jekyll/commands"
require_all "jekyll/converters"
require_all "jekyll/converters/markdown"
require_all "jekyll/drops"
require_all "jekyll/generators"
require_all "jekyll/tags"
require "jekyll-sass-converter"
+186
View File
@@ -0,0 +1,186 @@
# frozen_string_literal: true
require "digest"
module Jekyll
class Cache
# class-wide base cache
@base_cache = {}
# class-wide directive to write cache to disk is enabled by default
@disk_cache_enabled = true
class << self
attr_accessor :cache_dir # class-wide cache location
attr_reader :base_cache, # class-wide base cache reader
:disk_cache_enabled # class-wide directive to write cache to disk
# Disable Marshaling cached items to disk
def disable_disk_cache!
@disk_cache_enabled = false
end
# Clear all caches
def clear
delete_cache_files
base_cache.each_value(&:clear)
end
# Compare the current config to the cached config
# If they are different, clear all caches
#
# Returns nothing.
def clear_if_config_changed(config)
config = config.inspect
cache = Jekyll::Cache.new "Jekyll::Cache"
return if cache.key?("config") && cache["config"] == config
clear
cache = Jekyll::Cache.new "Jekyll::Cache"
cache["config"] = config
nil
end
private
# Delete all cached items from all caches
#
# Returns nothing.
def delete_cache_files
FileUtils.rm_rf(@cache_dir) if disk_cache_enabled
end
end
#
# Get an existing named cache, or create a new one if none exists
#
# name - name of the cache
#
# Returns nothing.
def initialize(name)
@cache = Jekyll::Cache.base_cache[name] ||= {}
@name = name.gsub(%r![^\w\s-]!, "-")
end
# Clear this particular cache
def clear
delete_cache_files
@cache.clear
end
# Retrieve a cached item
# Raises if key does not exist in cache
#
# Returns cached value
def [](key)
return @cache[key] if @cache.key?(key)
path = path_to(hash(key))
if disk_cache_enabled? && File.file?(path) && File.readable?(path)
@cache[key] = load(path)
else
raise
end
end
# Add an item to cache
#
# Returns nothing.
def []=(key, value)
@cache[key] = value
return unless disk_cache_enabled?
path = path_to(hash(key))
value = new Hash(value) if value.is_a?(Hash) && !value.default.nil?
dump(path, value)
rescue TypeError
Jekyll.logger.debug "Cache:", "Cannot dump object #{key}"
end
# If an item already exists in the cache, retrieve it.
# Else execute code block, and add the result to the cache, and return that result.
def getset(key)
self[key]
rescue StandardError
value = yield
self[key] = value
value
end
# Remove one particular item from the cache
#
# Returns nothing.
def delete(key)
@cache.delete(key)
File.delete(path_to(hash(key))) if disk_cache_enabled?
end
# Check if `key` already exists in this cache
#
# Returns true if key exists in the cache, false otherwise
def key?(key)
# First, check if item is already cached in memory
return true if @cache.key?(key)
# Otherwise, it might be cached on disk
# but we should not consider the disk cache if it is disabled
return false unless disk_cache_enabled?
path = path_to(hash(key))
File.file?(path) && File.readable?(path)
end
def disk_cache_enabled?
!!Jekyll::Cache.disk_cache_enabled
end
private
# Given a hashed key, return the path to where this item would be saved on disk.
def path_to(hash = nil)
@base_dir ||= File.join(Jekyll::Cache.cache_dir, @name)
return @base_dir if hash.nil?
File.join(@base_dir, hash[0..1], hash[2..-1]).freeze
end
# Given a key, return a SHA2 hash that can be used for caching this item to disk.
def hash(key)
Digest::SHA2.hexdigest(key).freeze
end
# Remove all this caches items from disk
#
# Returns nothing.
def delete_cache_files
FileUtils.rm_rf(path_to) if disk_cache_enabled?
end
# Load `path` from disk and return the result.
# This MUST NEVER be called in Safe Mode
# rubocop:disable Security/MarshalLoad
def load(path)
raise unless disk_cache_enabled?
cached_file = File.open(path, "rb")
value = Marshal.load(cached_file)
cached_file.close
value
end
# rubocop:enable Security/MarshalLoad
# Given a path and a value, save value to disk at path.
# This should NEVER be called in Safe Mode
#
# Returns nothing.
def dump(path, value)
return unless disk_cache_enabled?
FileUtils.mkdir_p(File.dirname(path))
File.open(path, "wb") do |cached_file|
Marshal.dump(value, cached_file)
end
end
end
end
+111
View File
@@ -0,0 +1,111 @@
# frozen_string_literal: true
module Jekyll
# Handles the cleanup of a site's destination before it is built.
class Cleaner
HIDDEN_FILE_REGEX = %r!/\.{1,2}$!.freeze
attr_reader :site
def initialize(site)
@site = site
end
# Cleans up the site's destination directory
def cleanup!
FileUtils.rm_rf(obsolete_files)
FileUtils.rm_rf(metadata_file) unless @site.incremental?
end
private
# Private: The list of files and directories to be deleted during cleanup process
#
# Returns an Array of the file and directory paths
def obsolete_files
out = (existing_files - new_files - new_dirs + replaced_files).to_a
Jekyll::Hooks.trigger :clean, :on_obsolete, out
out
end
# Private: The metadata file storing dependency tree and build history
#
# Returns an Array with the metadata file as the only item
def metadata_file
[site.regenerator.metadata_file]
end
# Private: The list of existing files, apart from those included in
# keep_files and hidden files.
#
# Returns a Set with the file paths
def existing_files
files = Set.new
regex = keep_file_regex
dirs = keep_dirs
Utils.safe_glob(site.in_dest_dir, ["**", "*"], File::FNM_DOTMATCH).each do |file|
next if HIDDEN_FILE_REGEX.match?(file) || regex.match?(file) || dirs.include?(file)
files << file
end
files
end
# Private: The list of files to be created when site is built.
#
# Returns a Set with the file paths
def new_files
@new_files ||= Set.new.tap do |files|
site.each_site_file { |item| files << item.destination(site.dest) }
end
end
# Private: The list of directories to be created when site is built.
# These are the parent directories of the files in #new_files.
#
# Returns a Set with the directory paths
def new_dirs
@new_dirs ||= new_files.flat_map { |file| parent_dirs(file) }.to_set
end
# Private: The list of parent directories of a given file
#
# Returns an Array with the directory paths
def parent_dirs(file)
parent_dir = File.dirname(file)
if parent_dir == site.dest
[]
else
parent_dirs(parent_dir).unshift(parent_dir)
end
end
# Private: The list of existing files that will be replaced by a directory
# during build
#
# Returns a Set with the file paths
def replaced_files
new_dirs.select { |dir| File.file?(dir) }.to_set
end
# Private: The list of directories that need to be kept because they are
# parent directories of files specified in keep_files
#
# Returns a Set with the directory paths
def keep_dirs
site.keep_files.flat_map { |file| parent_dirs(site.in_dest_dir(file)) }.to_set
end
# Private: Creates a regular expression from the config's keep_files array
#
# Examples
# ['.git','.svn'] with site.dest "/myblog/_site" creates
# the following regex: /\A\/myblog\/_site\/(\.git|\/.svn)/
#
# Returns the regular expression
def keep_file_regex
%r!\A#{Regexp.quote(site.dest)}/(#{Regexp.union(site.keep_files).source})!
end
end
end
@@ -0,0 +1,310 @@
# frozen_string_literal: true
module Jekyll
class Collection
attr_reader :site, :label, :metadata
attr_writer :docs
# Create a new Collection.
#
# site - the site to which this collection belongs.
# label - the name of the collection
#
# Returns nothing.
def initialize(site, label)
@site = site
@label = sanitize_label(label)
@metadata = extract_metadata
end
# Fetch the Documents in this collection.
# Defaults to an empty array if no documents have been read in.
#
# Returns an array of Jekyll::Document objects.
def docs
@docs ||= []
end
# Override of normal respond_to? to match method_missing's logic for
# looking in @data.
def respond_to_missing?(method, include_private = false)
docs.respond_to?(method.to_sym, include_private) || super
end
# Override of method_missing to check in @data for the key.
def method_missing(method, *args, &blck)
if docs.respond_to?(method.to_sym)
Jekyll.logger.warn "Deprecation:",
"#{label}.#{method} should be changed to #{label}.docs.#{method}."
Jekyll.logger.warn "", "Called by #{caller(0..0)}."
docs.public_send(method.to_sym, *args, &blck)
else
super
end
end
# Fetch the static files in this collection.
# Defaults to an empty array if no static files have been read in.
#
# Returns an array of Jekyll::StaticFile objects.
def files
@files ||= []
end
# Read the allowed documents into the collection's array of docs.
#
# Returns the sorted array of docs.
def read
filtered_entries.each do |file_path|
full_path = collection_dir(file_path)
next if File.directory?(full_path)
if Utils.has_yaml_header? full_path
read_document(full_path)
else
read_static_file(file_path, full_path)
end
end
site.static_files.concat(files) unless files.empty?
sort_docs!
end
# All the entries in this collection.
#
# Returns an Array of file paths to the documents in this collection
# relative to the collection's directory
def entries
return [] unless exists?
@entries ||= begin
collection_dir_slash = "#{collection_dir}/"
Utils.safe_glob(collection_dir, ["**", "*"], File::FNM_DOTMATCH).map do |entry|
entry[collection_dir_slash] = ""
entry
end
end
end
# Filtered version of the entries in this collection.
# See `Jekyll::EntryFilter#filter` for more information.
#
# Returns a list of filtered entry paths.
def filtered_entries
return [] unless exists?
@filtered_entries ||=
Dir.chdir(directory) do
entry_filter.filter(entries).reject do |f|
path = collection_dir(f)
File.directory?(path) || entry_filter.symlink?(f)
end
end
end
# The directory for this Collection, relative to the site source or the directory
# containing the collection.
#
# Returns a String containing the directory name where the collection
# is stored on the filesystem.
def relative_directory
@relative_directory ||= "_#{label}"
end
# The full path to the directory containing the collection.
#
# Returns a String containing th directory name where the collection
# is stored on the filesystem.
def directory
@directory ||= site.in_source_dir(
File.join(container, relative_directory)
)
end
# The full path to the directory containing the collection, with
# optional subpaths.
#
# *files - (optional) any other path pieces relative to the
# directory to append to the path
#
# Returns a String containing th directory name where the collection
# is stored on the filesystem.
def collection_dir(*files)
return directory if files.empty?
site.in_source_dir(container, relative_directory, *files)
end
# Checks whether the directory "exists" for this collection.
# The directory must exist on the filesystem and must not be a symlink
# if in safe mode.
#
# Returns false if the directory doesn't exist or if it's a symlink
# and we're in safe mode.
def exists?
File.directory?(directory) && !entry_filter.symlink?(directory)
end
# The entry filter for this collection.
# Creates an instance of Jekyll::EntryFilter.
#
# Returns the instance of Jekyll::EntryFilter for this collection.
def entry_filter
@entry_filter ||= Jekyll::EntryFilter.new(site, relative_directory)
end
# An inspect string.
#
# Returns the inspect string
def inspect
"#<#{self.class} @label=#{label} docs=#{docs}>"
end
# Produce a sanitized label name
# Label names may not contain anything but alphanumeric characters,
# underscores, and hyphens.
#
# label - the possibly-unsafe label
#
# Returns a sanitized version of the label.
def sanitize_label(label)
label.gsub(%r![^a-z0-9_\-.]!i, "")
end
# Produce a representation of this Collection for use in Liquid.
# Exposes two attributes:
# - label
# - docs
#
# Returns a representation of this collection for use in Liquid.
def to_liquid
Drops::CollectionDrop.new self
end
# Whether the collection's documents ought to be written as individual
# files in the output.
#
# Returns true if the 'write' metadata is true, false otherwise.
def write?
!!metadata.fetch("output", false)
end
# The URL template to render collection's documents at.
#
# Returns the URL template to render collection's documents at.
def url_template
@url_template ||= metadata.fetch("permalink") do
Utils.add_permalink_suffix("/:collection/:path", site.permalink_style)
end
end
# Extract options for this collection from the site configuration.
#
# Returns the metadata for this collection
def extract_metadata
if site.config["collections"].is_a?(Hash)
site.config["collections"][label] || {}
else
{}
end
end
private
def container
@container ||= site.config["collections_dir"]
end
def read_document(full_path)
doc = Document.new(full_path, :site => site, :collection => self)
doc.read
docs << doc if site.unpublished || doc.published?
end
def sort_docs!
if metadata["order"].is_a?(Array)
rearrange_docs!
elsif metadata["sort_by"].is_a?(String)
sort_docs_by_key!
else
docs.sort!
end
end
# A custom sort function based on Schwartzian transform
# Refer https://byparker.com/blog/2017/schwartzian-transform-faster-sorting/ for details
def sort_docs_by_key!
meta_key = metadata["sort_by"]
# Modify `docs` array to cache document's property along with the Document instance
docs.map! { |doc| [doc.data[meta_key], doc] }.sort! do |apples, olives|
order = determine_sort_order(meta_key, apples, olives)
# Fall back to `Document#<=>` if the properties were equal or were non-sortable
# Otherwise continue with current sort-order
if order.nil? || order.zero?
apples[-1] <=> olives[-1]
else
order
end
# Finally restore the `docs` array with just the Document objects themselves
end.map!(&:last)
end
def determine_sort_order(sort_key, apples, olives)
apple_property, apple_document = apples
olive_property, olive_document = olives
if apple_property.nil? && !olive_property.nil?
order_with_warning(sort_key, apple_document, 1)
elsif !apple_property.nil? && olive_property.nil?
order_with_warning(sort_key, olive_document, -1)
else
apple_property <=> olive_property
end
end
def order_with_warning(sort_key, document, order)
Jekyll.logger.warn "Sort warning:", "'#{sort_key}' not defined in #{document.relative_path}"
order
end
# Rearrange documents within the `docs` array as listed in the `metadata["order"]` array.
#
# Involves converting the two arrays into hashes based on relative_paths as keys first, then
# merging them to remove duplicates and finally retrieving the Document instances from the
# merged array.
def rearrange_docs!
docs_table = {}
custom_order = {}
# pre-sort to normalize default array across platforms and then proceed to create a Hash
# from that sorted array.
docs.sort.each do |doc|
docs_table[doc.relative_path] = doc
end
metadata["order"].each do |entry|
custom_order[File.join(relative_directory, entry)] = nil
end
result = Jekyll::Utils.deep_merge_hashes(custom_order, docs_table).values
result.compact!
self.docs = result
end
def read_static_file(file_path, full_path)
relative_dir = Jekyll.sanitized_path(
relative_directory,
File.dirname(file_path)
).chomp("/.")
files << StaticFile.new(
site,
site.source,
relative_dir,
File.basename(full_path),
self
)
end
end
end
+105
View File
@@ -0,0 +1,105 @@
# frozen_string_literal: true
module Jekyll
class Command
class << self
# A list of subclasses of Jekyll::Command
def subclasses
@subclasses ||= []
end
# Keep a list of subclasses of Jekyll::Command every time it's inherited
# Called automatically.
#
# base - the subclass
#
# Returns nothing
def inherited(base)
subclasses << base
super(base)
end
# Run Site#process and catch errors
#
# site - the Jekyll::Site object
#
# Returns nothing
def process_site(site)
site.process
rescue Jekyll::Errors::FatalException => e
Jekyll.logger.error "ERROR:", "YOUR SITE COULD NOT BE BUILT:"
Jekyll.logger.error "", "------------------------------------"
Jekyll.logger.error "", e.message
exit(1)
end
# Create a full Jekyll configuration with the options passed in as overrides
#
# options - the configuration overrides
#
# Returns a full Jekyll configuration
def configuration_from_options(options)
return options if options.is_a?(Jekyll::Configuration)
Jekyll.configuration(options)
end
# Add common options to a command for building configuration
#
# cmd - the Jekyll::Command to add these options to
#
# Returns nothing
# rubocop:disable Metrics/MethodLength
def add_build_options(cmd)
cmd.option "config", "--config CONFIG_FILE[,CONFIG_FILE2,...]",
Array, "Custom configuration file"
cmd.option "destination", "-d", "--destination DESTINATION",
"The current folder will be generated into DESTINATION"
cmd.option "source", "-s", "--source SOURCE", "Custom source directory"
cmd.option "future", "--future", "Publishes posts with a future date"
cmd.option "limit_posts", "--limit_posts MAX_POSTS", Integer,
"Limits the number of posts to parse and publish"
cmd.option "watch", "-w", "--[no-]watch", "Watch for changes and rebuild"
cmd.option "baseurl", "-b", "--baseurl URL",
"Serve the website from the given base URL"
cmd.option "force_polling", "--force_polling", "Force watch to use polling"
cmd.option "lsi", "--lsi", "Use LSI for improved related posts"
cmd.option "show_drafts", "-D", "--drafts", "Render posts in the _drafts folder"
cmd.option "unpublished", "--unpublished",
"Render posts that were marked as unpublished"
cmd.option "disable_disk_cache", "--disable-disk-cache",
"Disable caching to disk in non-safe mode"
cmd.option "quiet", "-q", "--quiet", "Silence output."
cmd.option "verbose", "-V", "--verbose", "Print verbose output."
cmd.option "incremental", "-I", "--incremental", "Enable incremental rebuild."
cmd.option "strict_front_matter", "--strict_front_matter",
"Fail if errors are present in front matter"
end
# rubocop:enable Metrics/MethodLength
# Run ::process method in a given set of Jekyll::Command subclasses and suggest
# re-running the associated command with --trace switch to obtain any additional
# information or backtrace regarding the encountered Exception.
#
# cmd - the Jekyll::Command to be handled
# options - configuration overrides
# klass - an array of Jekyll::Command subclasses associated with the command
#
# Note that all exceptions are rescued..
# rubocop: disable Lint/RescueException
def process_with_graceful_fail(cmd, options, *klass)
klass.each { |k| k.process(options) if k.respond_to?(:process) }
rescue Exception => e
raise e if cmd.trace
msg = " Please append `--trace` to the `#{cmd.name}` command "
dashes = "-" * msg.length
Jekyll.logger.error "", dashes
Jekyll.logger.error "Jekyll #{Jekyll::VERSION} ", msg
Jekyll.logger.error "", " for any additional information or backtrace. "
Jekyll.logger.abort_with "", dashes
end
# rubocop: enable Lint/RescueException
end
end
end
@@ -0,0 +1,82 @@
# frozen_string_literal: true
module Jekyll
module Commands
class Build < Command
class << self
# Create the Mercenary command for the Jekyll CLI for this Command
def init_with_program(prog)
prog.command(:build) do |c|
c.syntax "build [options]"
c.description "Build your site"
c.alias :b
add_build_options(c)
c.action do |_, options|
options["serving"] = false
process_with_graceful_fail(c, options, self)
end
end
end
# Build your jekyll site
# Continuously watch if `watch` is set to true in the config.
def process(options)
# Adjust verbosity quickly
Jekyll.logger.adjust_verbosity(options)
options = configuration_from_options(options)
site = Jekyll::Site.new(options)
if options.fetch("skip_initial_build", false)
Jekyll.logger.warn "Build Warning:",
"Skipping the initial build. This may result in an out-of-date site."
else
build(site, options)
end
if options.fetch("detach", false)
Jekyll.logger.info "Auto-regeneration:",
"disabled when running server detached."
elsif options.fetch("watch", false)
watch(site, options)
else
Jekyll.logger.info "Auto-regeneration:", "disabled. Use --watch to enable."
end
end
# Build your Jekyll site.
#
# site - the Jekyll::Site instance to build
# options - A Hash of options passed to the command
#
# Returns nothing.
def build(site, options)
t = Time.now
source = File.expand_path(options["source"])
destination = File.expand_path(options["destination"])
incremental = options["incremental"]
Jekyll.logger.info "Source:", source
Jekyll.logger.info "Destination:", destination
Jekyll.logger.info "Incremental build:",
(incremental ? "enabled" : "disabled. Enable with --incremental")
Jekyll.logger.info "Generating..."
process_site(site)
Jekyll.logger.info "", "done in #{(Time.now - t).round(3)} seconds."
end
# Private: Watch for file changes and rebuild the site.
#
# site - A Jekyll::Site instance
# options - A Hash of options passed to the command
#
# Returns nothing.
def watch(site, options)
External.require_with_graceful_fail "jekyll-watch"
Jekyll::Watcher.watch(options, site)
end
end
end
end
end
@@ -0,0 +1,44 @@
# frozen_string_literal: true
module Jekyll
module Commands
class Clean < Command
class << self
def init_with_program(prog)
prog.command(:clean) do |c|
c.syntax "clean [subcommand]"
c.description "Clean the site (removes site output and metadata file) without building."
add_build_options(c)
c.action do |_, options|
Jekyll::Commands::Clean.process(options)
end
end
end
def process(options)
options = configuration_from_options(options)
destination = options["destination"]
metadata_file = File.join(options["source"], ".jekyll-metadata")
cache_dir = File.join(options["source"], options["cache_dir"])
sass_cache = ".sass-cache"
remove(destination, :checker_func => :directory?)
remove(metadata_file, :checker_func => :file?)
remove(cache_dir, :checker_func => :directory?)
remove(sass_cache, :checker_func => :directory?)
end
def remove(filename, checker_func: :file?)
if File.public_send(checker_func, filename)
Jekyll.logger.info "Cleaner:", "Removing #{filename}..."
FileUtils.rm_rf(filename)
else
Jekyll.logger.info "Cleaner:", "Nothing to do for #{filename}."
end
end
end
end
end
end
@@ -0,0 +1,177 @@
# frozen_string_literal: true
module Jekyll
module Commands
class Doctor < Command
class << self
def init_with_program(prog)
prog.command(:doctor) do |c|
c.syntax "doctor"
c.description "Search site and print specific deprecation warnings"
c.alias(:hyde)
c.option "config", "--config CONFIG_FILE[,CONFIG_FILE2,...]", Array,
"Custom configuration file"
c.action do |_, options|
Jekyll::Commands::Doctor.process(options)
end
end
end
def process(options)
site = Jekyll::Site.new(configuration_from_options(options))
site.reset
site.read
site.generate
if healthy?(site)
Jekyll.logger.info "Your test results", "are in. Everything looks fine."
else
abort
end
end
def healthy?(site)
[
fsnotify_buggy?(site),
!deprecated_relative_permalinks(site),
!conflicting_urls(site),
!urls_only_differ_by_case(site),
proper_site_url?(site),
properly_gathered_posts?(site),
].all?
end
def properly_gathered_posts?(site)
return true if site.config["collections_dir"].empty?
posts_at_root = site.in_source_dir("_posts")
return true unless File.directory?(posts_at_root)
Jekyll.logger.warn "Warning:",
"Detected '_posts' directory outside custom `collections_dir`!"
Jekyll.logger.warn "",
"Please move '#{posts_at_root}' into the custom directory at " \
"'#{site.in_source_dir(site.config["collections_dir"])}'"
false
end
def deprecated_relative_permalinks(site)
if site.config["relative_permalinks"]
Jekyll::Deprecator.deprecation_message "Your site still uses relative permalinks, " \
"which was removed in Jekyll v3.0.0."
true
end
end
def conflicting_urls(site)
conflicting_urls = false
destination_map(site).each do |dest, paths|
next unless paths.size > 1
conflicting_urls = true
Jekyll.logger.warn "Conflict:",
"The following destination is shared by multiple files."
Jekyll.logger.warn "", "The written file may end up with unexpected contents."
Jekyll.logger.warn "", dest.to_s.cyan
paths.each { |path| Jekyll.logger.warn "", " - #{path}" }
Jekyll.logger.warn ""
end
conflicting_urls
end
def fsnotify_buggy?(_site)
return true unless Utils::Platforms.osx?
if Dir.pwd != `pwd`.strip
Jekyll.logger.error <<~STR
We have detected that there might be trouble using fsevent on your
operating system, you can read https://github.com/thibaudgg/rb-fsevent/wiki/no-fsevents-fired-(OSX-bug)
for possible workarounds or you can work around it immediately
with `--force-polling`.
STR
false
end
true
end
def urls_only_differ_by_case(site)
urls_only_differ_by_case = false
urls = case_insensitive_urls(site.pages + site.docs_to_write, site.dest)
urls.each_value do |real_urls|
next unless real_urls.uniq.size > 1
urls_only_differ_by_case = true
Jekyll.logger.warn "Warning:", "The following URLs only differ by case. On a " \
"case-insensitive file system one of the URLs will be " \
"overwritten by the other: #{real_urls.join(", ")}"
end
urls_only_differ_by_case
end
def proper_site_url?(site)
url = site.config["url"]
[
url_exists?(url),
url_valid?(url),
url_absolute(url),
].all?
end
private
def destination_map(site)
{}.tap do |result|
site.each_site_file do |thing|
next if allow_used_permalink?(thing)
dest_path = thing.destination(site.dest)
(result[dest_path] ||= []) << thing.path
end
end
end
def allow_used_permalink?(item)
defined?(JekyllRedirectFrom) && item.is_a?(JekyllRedirectFrom::RedirectPage)
end
def case_insensitive_urls(things, destination)
things.each_with_object({}) do |thing, memo|
dest = thing.destination(destination)
(memo[dest.downcase] ||= []) << dest
end
end
def url_exists?(url)
return true unless url.nil? || url.empty?
Jekyll.logger.warn "Warning:", "You didn't set an URL in the config file, you may " \
"encounter problems with some plugins."
false
end
def url_valid?(url)
Addressable::URI.parse(url)
true
# Addressable::URI#parse only raises a TypeError
# https://github.com/sporkmonger/addressable/blob/0a0e96acb17225f9b1c9cab0bad332b448934c9a/lib/addressable/uri.rb#L103
rescue TypeError
Jekyll.logger.warn "Warning:", "The site URL does not seem to be valid, " \
"check the value of `url` in your config file."
false
end
def url_absolute(url)
return true if url.is_a?(String) && Addressable::URI.parse(url).absolute?
Jekyll.logger.warn "Warning:", "Your site URL does not seem to be absolute, " \
"check the value of `url` in your config file."
false
end
end
end
end
end
@@ -0,0 +1,34 @@
# frozen_string_literal: true
module Jekyll
module Commands
class Help < Command
class << self
def init_with_program(prog)
prog.command(:help) do |c|
c.syntax "help [subcommand]"
c.description "Show the help message, optionally for a given subcommand."
c.action do |args, _|
cmd = (args.first || "").to_sym
if args.empty?
Jekyll.logger.info prog.to_s
elsif prog.has_command? cmd
Jekyll.logger.info prog.commands[cmd].to_s
else
invalid_command(prog, cmd)
abort
end
end
end
end
def invalid_command(prog, cmd)
Jekyll.logger.error "Error:",
"Hmm... we don't know what the '#{cmd}' command is."
Jekyll.logger.info "Valid commands:", prog.commands.keys.join(", ")
end
end
end
end
end
@@ -0,0 +1,168 @@
# frozen_string_literal: true
require "erb"
module Jekyll
module Commands
class New < Command
class << self
def init_with_program(prog)
prog.command(:new) do |c|
c.syntax "new PATH"
c.description "Creates a new Jekyll site scaffold in PATH"
c.option "force", "--force", "Force creation even if PATH already exists"
c.option "blank", "--blank", "Creates scaffolding but with empty files"
c.option "skip-bundle", "--skip-bundle", "Skip 'bundle install'"
c.action do |args, options|
Jekyll::Commands::New.process(args, options)
end
end
end
def process(args, options = {})
raise ArgumentError, "You must specify a path." if args.empty?
new_blog_path = File.expand_path(args.join(" "), Dir.pwd)
FileUtils.mkdir_p new_blog_path
if preserve_source_location?(new_blog_path, options)
Jekyll.logger.error "Conflict:", "#{new_blog_path} exists and is not empty."
Jekyll.logger.abort_with "", "Ensure #{new_blog_path} is empty or else try again " \
"with `--force` to proceed and overwrite any files."
end
if options["blank"]
create_blank_site new_blog_path
else
create_site new_blog_path
end
after_install(new_blog_path, options)
end
def blank_template
File.expand_path("../../blank_template", __dir__)
end
def create_blank_site(path)
FileUtils.cp_r blank_template + "/.", path
FileUtils.chmod_R "u+w", path
Dir.chdir(path) do
FileUtils.mkdir(%w(_data _drafts _includes _posts))
end
end
def scaffold_post_content
ERB.new(File.read(File.expand_path(scaffold_path, site_template))).result
end
# Internal: Gets the filename of the sample post to be created
#
# Returns the filename of the sample post, as a String
def initialized_post_name
"_posts/#{Time.now.strftime("%Y-%m-%d")}-welcome-to-jekyll.markdown"
end
private
def gemfile_contents
<<~RUBY
source "https://rubygems.org"
# Hello! This is where you manage which Jekyll version is used to run.
# When you want to use a different version, change it below, save the
# file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
#
# bundle exec jekyll serve
#
# This will help ensure the proper Jekyll version is running.
# Happy Jekylling!
gem "jekyll", "~> #{Jekyll::VERSION}"
# This is the default theme for new Jekyll sites. You may change this to anything you like.
gem "minima", "~> 2.5"
# If you want to use GitHub Pages, remove the "gem "jekyll"" above and
# uncomment the line below. To upgrade, run `bundle update github-pages`.
# gem "github-pages", group: :jekyll_plugins
# If you have any plugins, put them here!
group :jekyll_plugins do
gem "jekyll-feed", "~> 0.12"
end
# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
# and associated library.
platforms :mingw, :x64_mingw, :mswin, :jruby do
gem "tzinfo", ">= 1", "< 3"
gem "tzinfo-data"
end
# Performance-booster for watching directories on Windows
gem "wdm", "~> 0.1", :platforms => [:mingw, :x64_mingw, :mswin]
# Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem
# do not have a Java counterpart.
gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby]
RUBY
end
def create_site(new_blog_path)
create_sample_files new_blog_path
File.write(File.expand_path(initialized_post_name, new_blog_path), scaffold_post_content)
File.write(File.expand_path("Gemfile", new_blog_path), gemfile_contents)
end
def preserve_source_location?(path, options)
!options["force"] && !Dir["#{path}/**/*"].empty?
end
def create_sample_files(path)
FileUtils.cp_r site_template + "/.", path
FileUtils.chmod_R "u+w", path
FileUtils.rm File.expand_path(scaffold_path, path)
end
def site_template
File.expand_path("../../site_template", __dir__)
end
def scaffold_path
"_posts/0000-00-00-welcome-to-jekyll.markdown.erb"
end
# After a new blog has been created, print a success notification and
# then automatically execute bundle install from within the new blog dir
# unless the user opts to generate a blank blog or skip 'bundle install'.
def after_install(path, options = {})
unless options["blank"] || options["skip-bundle"]
begin
require "bundler"
bundle_install path
rescue LoadError
Jekyll.logger.info "Could not load Bundler. Bundle install skipped."
end
end
Jekyll.logger.info "New jekyll site installed in #{path.cyan}."
Jekyll.logger.info "Bundle install skipped." if options["skip-bundle"]
end
def bundle_install(path)
Jekyll.logger.info "Running bundle install in #{path.cyan}..."
Dir.chdir(path) do
exe = Gem.bin_path("bundler", "bundle")
process, output = Jekyll::Utils::Exec.run("ruby", exe, "install")
output.to_s.each_line do |line|
Jekyll.logger.info("Bundler:".green, line.strip) unless line.to_s.empty?
end
raise SystemExit unless process.success?
end
end
end
end
end
end
@@ -0,0 +1,39 @@
# frozen_string_literal: true
require "erb"
module Jekyll
module Commands
class NewTheme < Jekyll::Command
class << self
def init_with_program(prog)
prog.command(:"new-theme") do |c|
c.syntax "new-theme NAME"
c.description "Creates a new Jekyll theme scaffold"
c.option "code_of_conduct", "-c", "--code-of-conduct",
"Include a Code of Conduct. (defaults to false)"
c.action do |args, opts|
Jekyll::Commands::NewTheme.process(args, opts)
end
end
end
def process(args, opts)
if !args || args.empty?
raise Jekyll::Errors::InvalidThemeName, "You must specify a theme name."
end
new_theme_name = args.join("_")
theme = Jekyll::ThemeBuilder.new(new_theme_name, opts)
Jekyll.logger.abort_with "Conflict:", "#{theme.path} already exists." if theme.path.exist?
theme.create!
Jekyll.logger.info "Your new Jekyll theme, #{theme.name.cyan}, " \
"is ready for you in #{theme.path.to_s.cyan}!"
Jekyll.logger.info "For help getting started, read #{theme.path}/README.md."
end
end
end
end
end
@@ -0,0 +1,372 @@
# frozen_string_literal: true
module Jekyll
module Commands
class Serve < Command
# Similar to the pattern in Utils::ThreadEvent except we are maintaining the
# state of @running instead of just signaling an event. We have to maintain this
# state since Serve is just called via class methods instead of an instance
# being created each time.
@mutex = Mutex.new
@run_cond = ConditionVariable.new
@running = false
class << self
COMMAND_OPTIONS = {
"ssl_cert" => ["--ssl-cert [CERT]", "X.509 (SSL) certificate."],
"host" => ["host", "-H", "--host [HOST]", "Host to bind to"],
"open_url" => ["-o", "--open-url", "Launch your site in a browser"],
"detach" => ["-B", "--detach",
"Run the server in the background",],
"ssl_key" => ["--ssl-key [KEY]", "X.509 (SSL) Private Key."],
"port" => ["-P", "--port [PORT]", "Port to listen on"],
"show_dir_listing" => ["--show-dir-listing",
"Show a directory listing instead of loading " \
"your index file.",],
"skip_initial_build" => ["skip_initial_build", "--skip-initial-build",
"Skips the initial site build which occurs before " \
"the server is started.",],
"livereload" => ["-l", "--livereload",
"Use LiveReload to automatically refresh browsers",],
"livereload_ignore" => ["--livereload-ignore GLOB1[,GLOB2[,...]]",
Array,
"Files for LiveReload to ignore. " \
"Remember to quote the values so your shell " \
"won't expand them",],
"livereload_min_delay" => ["--livereload-min-delay [SECONDS]",
"Minimum reload delay",],
"livereload_max_delay" => ["--livereload-max-delay [SECONDS]",
"Maximum reload delay",],
"livereload_port" => ["--livereload-port [PORT]", Integer,
"Port for LiveReload to listen on",],
}.freeze
DIRECTORY_INDEX = %w(
index.htm
index.html
index.rhtml
index.xht
index.xhtml
index.cgi
index.xml
index.json
).freeze
LIVERELOAD_PORT = 35_729
LIVERELOAD_DIR = File.join(__dir__, "serve", "livereload_assets")
attr_reader :mutex, :run_cond, :running
alias_method :running?, :running
def init_with_program(prog)
prog.command(:serve) do |cmd|
cmd.description "Serve your site locally"
cmd.syntax "serve [options]"
cmd.alias :server
cmd.alias :s
add_build_options(cmd)
COMMAND_OPTIONS.each do |key, val|
cmd.option key, *val
end
cmd.action do |_, opts|
opts["serving"] = true
opts["watch"] = true unless opts.key?("watch")
# Set the reactor to nil so any old reactor will be GCed.
# We can't unregister a hook so while running tests we don't want to
# inadvertently keep using a reactor created by a previous test.
@reload_reactor = nil
config = configuration_from_options(opts)
config["url"] = default_url(config) if Jekyll.env == "development"
process_with_graceful_fail(cmd, config, Build, Serve)
end
end
end
#
def process(opts)
opts = configuration_from_options(opts)
destination = opts["destination"]
if opts["livereload"]
opts["livereload_port"] ||= LIVERELOAD_PORT
validate_options(opts)
register_reload_hooks(opts)
end
setup(destination)
start_up_webrick(opts, destination)
end
def shutdown
@server.shutdown if running?
end
# Perform logical validation of CLI options
private
def validate_options(opts)
if opts["livereload"]
if opts["detach"]
Jekyll.logger.warn "Warning:", "--detach and --livereload are mutually exclusive. " \
"Choosing --livereload"
opts["detach"] = false
end
if opts["ssl_cert"] || opts["ssl_key"]
# This is not technically true. LiveReload works fine over SSL, but
# EventMachine's SSL support in Windows requires building the gem's
# native extensions against OpenSSL and that proved to be a process
# so tedious that expecting users to do it is a non-starter.
Jekyll.logger.abort_with "Error:", "LiveReload does not support SSL"
end
unless opts["watch"]
# Using livereload logically implies you want to watch the files
opts["watch"] = true
end
elsif %w(livereload_min_delay
livereload_max_delay
livereload_ignore
livereload_port).any? { |o| opts[o] }
Jekyll.logger.abort_with "--livereload-min-delay, --livereload-max-delay, " \
"--livereload-ignore, and --livereload-port require " \
"the --livereload option."
end
end
def register_reload_hooks(opts)
require_relative "serve/live_reload_reactor"
@reload_reactor = LiveReloadReactor.new
Jekyll::Hooks.register(:site, :post_render) do |site|
@changed_pages = []
site.each_site_file do |item|
@changed_pages << item if site.regenerator.regenerate?(item)
end
end
# A note on ignoring files: LiveReload errs on the side of reloading when it
# comes to the message it gets. If, for example, a page is ignored but a CSS
# file linked in the page isn't, the page will still be reloaded if the CSS
# file is contained in the message sent to LiveReload. Additionally, the
# path matching is very loose so that a message to reload "/" will always
# lead the page to reload since every page starts with "/".
Jekyll::Hooks.register(:site, :post_write) do
if @changed_pages && @reload_reactor && @reload_reactor.running?
ignore, @changed_pages = @changed_pages.partition do |p|
Array(opts["livereload_ignore"]).any? do |filter|
File.fnmatch(filter, p.relative_path)
end
end
Jekyll.logger.debug "LiveReload:", "Ignoring #{ignore.map(&:relative_path)}"
@reload_reactor.reload(@changed_pages)
end
@changed_pages = nil
end
end
# Do a base pre-setup of WEBRick so that everything is in place
# when we get ready to party, checking for an setting up an error page
# and making sure our destination exists.
#
# rubocop:disable Security/IoMethods
def setup(destination)
require_relative "serve/servlet"
FileUtils.mkdir_p(destination)
if File.exist?(File.join(destination, "404.html"))
WEBrick::HTTPResponse.class_eval do
def create_error_page
@header["Content-Type"] = "text/html; charset=UTF-8"
@body = IO.read(File.join(@config[:DocumentRoot], "404.html"))
end
end
end
end
# rubocop:enable Security/IoMethods
def webrick_opts(opts)
opts = {
:JekyllOptions => opts,
:DoNotReverseLookup => true,
:MimeTypes => mime_types,
:MimeTypesCharset => mime_types_charset,
:DocumentRoot => opts["destination"],
:StartCallback => start_callback(opts["detach"]),
:StopCallback => stop_callback(opts["detach"]),
:BindAddress => opts["host"],
:Port => opts["port"],
:DirectoryIndex => DIRECTORY_INDEX,
}
opts[:DirectoryIndex] = [] if opts[:JekyllOptions]["show_dir_listing"]
enable_ssl(opts)
enable_logging(opts)
opts
end
def start_up_webrick(opts, destination)
@reload_reactor.start(opts) if opts["livereload"]
@server = WEBrick::HTTPServer.new(webrick_opts(opts)).tap { |o| o.unmount("") }
@server.mount(opts["baseurl"].to_s, Servlet, destination, file_handler_opts)
Jekyll.logger.info "Server address:", server_address(@server, opts)
launch_browser @server, opts if opts["open_url"]
boot_or_detach @server, opts
end
# Recreate NondisclosureName under utf-8 circumstance
def file_handler_opts
WEBrick::Config::FileHandler.merge(
:FancyIndexing => true,
:NondisclosureName => [
".ht*", "~*",
]
)
end
def server_address(server, options = {})
format_url(
server.config[:SSLEnable],
server.config[:BindAddress],
server.config[:Port],
options["baseurl"]
)
end
def format_url(ssl_enabled, address, port, baseurl = nil)
format("%<prefix>s://%<address>s:%<port>i%<baseurl>s",
:prefix => ssl_enabled ? "https" : "http",
:address => address,
:port => port,
:baseurl => baseurl ? "#{baseurl}/" : "")
end
def default_url(opts)
config = configuration_from_options(opts)
format_url(
config["ssl_cert"] && config["ssl_key"],
config["host"] == "127.0.0.1" ? "localhost" : config["host"],
config["port"]
)
end
def launch_browser(server, opts)
address = server_address(server, opts)
return system "start", address if Utils::Platforms.windows?
return system "xdg-open", address if Utils::Platforms.linux?
return system "open", address if Utils::Platforms.osx?
Jekyll.logger.error "Refusing to launch browser. Platform launcher unknown."
end
# Keep in our area with a thread or detach the server as requested
# by the user. This method determines what we do based on what you
# ask us to do.
def boot_or_detach(server, opts)
if opts["detach"]
pid = Process.fork do
# Detach the process from controlling terminal
$stdin.reopen("/dev/null", "r")
$stdout.reopen("/dev/null", "w")
$stderr.reopen("/dev/null", "w")
server.start
end
Process.detach(pid)
Jekyll.logger.info "Server detached with pid '#{pid}'.",
"Run `pkill -f jekyll' or `kill -9 #{pid}' to stop the server."
# Exit without running `at_exit` inherited by the forked process.
Process.exit! 0
else
t = Thread.new { server.start }
trap("INT") { server.shutdown }
t.join
end
end
# Make the stack verbose if the user requests it.
def enable_logging(opts)
opts[:AccessLog] = []
level = WEBrick::Log.const_get(opts[:JekyllOptions]["verbose"] ? :DEBUG : :WARN)
opts[:Logger] = WEBrick::Log.new($stdout, level)
end
# Add SSL to the stack if the user triggers --enable-ssl and they
# provide both types of certificates commonly needed. Raise if they
# forget to add one of the certificates.
def enable_ssl(opts)
cert, key, src =
opts[:JekyllOptions].values_at("ssl_cert", "ssl_key", "source")
return if cert.nil? && key.nil?
raise "Missing --ssl_cert or --ssl_key. Both are required." unless cert && key
require "openssl"
require "webrick/https"
opts[:SSLCertificate] = OpenSSL::X509::Certificate.new(read_file(src, cert))
begin
opts[:SSLPrivateKey] = OpenSSL::PKey::RSA.new(read_file(src, key))
rescue StandardError
if defined?(OpenSSL::PKey::EC)
opts[:SSLPrivateKey] = OpenSSL::PKey::EC.new(read_file(src, key))
else
raise
end
end
opts[:SSLEnable] = true
end
def start_callback(detached)
unless detached
proc do
mutex.synchronize do
# Block until EventMachine reactor starts
@reload_reactor&.started_event&.wait
@running = true
Jekyll.logger.info("Server running...", "press ctrl-c to stop.")
@run_cond.broadcast
end
end
end
end
def stop_callback(detached)
unless detached
proc do
mutex.synchronize do
unless @reload_reactor.nil?
@reload_reactor.stop
@reload_reactor.stopped_event.wait
end
@running = false
@run_cond.broadcast
end
end
end
end
def mime_types
file = File.expand_path("../mime.types", __dir__)
WEBrick::HTTPUtils.load_mime_types(file)
end
def mime_types_charset
SafeYAML.load_file(File.expand_path("serve/mime_types_charset.json", __dir__))
end
def read_file(source_dir, file_path)
File.read(Jekyll.sanitized_path(source_dir, file_path))
end
end
end
end
end
@@ -0,0 +1,119 @@
# frozen_string_literal: true
require "em-websocket"
require_relative "websockets"
module Jekyll
module Commands
class Serve
class LiveReloadReactor
attr_reader :started_event, :stopped_event, :thread
def initialize
@websockets = []
@connections_count = 0
@started_event = Utils::ThreadEvent.new
@stopped_event = Utils::ThreadEvent.new
end
def stop
# There is only one EventMachine instance per Ruby process so stopping
# it here will stop the reactor thread we have running.
EM.stop if EM.reactor_running?
Jekyll.logger.debug "LiveReload Server:", "halted"
end
def running?
EM.reactor_running?
end
def handle_websockets_event(websocket)
websocket.onopen { |handshake| connect(websocket, handshake) }
websocket.onclose { disconnect(websocket) }
websocket.onmessage { |msg| print_message(msg) }
websocket.onerror { |error| log_error(error) }
end
def start(opts)
@thread = Thread.new do
# Use epoll if the kernel supports it
EM.epoll
EM.run do
EM.error_handler { |e| log_error(e) }
EM.start_server(
opts["host"],
opts["livereload_port"],
HttpAwareConnection,
opts
) do |ws|
handle_websockets_event(ws)
end
# Notify blocked threads that EventMachine has started or shutdown
EM.schedule { @started_event.set }
EM.add_shutdown_hook { @stopped_event.set }
Jekyll.logger.info "LiveReload address:",
"http://#{opts["host"]}:#{opts["livereload_port"]}"
end
end
@thread.abort_on_exception = true
end
# For a description of the protocol see
# http://feedback.livereload.com/knowledgebase/articles/86174-livereload-protocol
def reload(pages)
pages.each do |p|
json_message = JSON.dump(
:command => "reload",
:path => p.url,
:liveCSS => true
)
Jekyll.logger.debug "LiveReload:", "Reloading URL #{p.url.inspect}"
@websockets.each { |ws| ws.send(json_message) }
end
end
private
def connect(websocket, handshake)
@connections_count += 1
if @connections_count == 1
message = "Browser connected"
message += " over SSL/TLS" if handshake.secure?
Jekyll.logger.info "LiveReload:", message
end
websocket.send(
JSON.dump(
:command => "hello",
:protocols => ["http://livereload.com/protocols/official-7"],
:serverName => "jekyll"
)
)
@websockets << websocket
end
def disconnect(websocket)
@websockets.delete(websocket)
end
def print_message(json_message)
msg = JSON.parse(json_message)
# Not sure what the 'url' command even does in LiveReload. The spec is silent
# on its purpose.
Jekyll.logger.info "LiveReload:", "Browser URL: #{msg["url"]}" if msg["command"] == "url"
end
def log_error(error)
Jekyll.logger.error "LiveReload experienced an error. " \
"Run with --trace for more information."
raise error
end
end
end
end
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,71 @@
{
"application/javascript": "UTF-8",
"application/json": "UTF-8",
"application/manifest+json": "UTF-8",
"application/vnd.syncml+xml": "UTF-8",
"application/vnd.syncml.dm+wbxml": "UTF-8",
"application/vnd.syncml.dm+xml": "UTF-8",
"application/vnd.syncml.dmddf+xml": "UTF-8",
"application/vnd.wap.wbxml": "UTF-8",
"text/cache-manifest": "UTF-8",
"text/calendar": "UTF-8",
"text/coffeescript": "UTF-8",
"text/css": "UTF-8",
"text/csv": "UTF-8",
"text/html": "UTF-8",
"text/jade": "UTF-8",
"text/jsx": "UTF-8",
"text/less": "UTF-8",
"text/markdown": "UTF-8",
"text/mathml": "UTF-8",
"text/mdx": "UTF-8",
"text/n3": "UTF-8",
"text/plain": "UTF-8",
"text/prs.lines.tag": "UTF-8",
"text/richtext": "UTF-8",
"text/sgml": "UTF-8",
"text/shex": "UTF-8",
"text/slim": "UTF-8",
"text/spdx": "UTF-8",
"text/stylus": "UTF-8",
"text/tab-separated-values": "UTF-8",
"text/troff": "UTF-8",
"text/turtle": "UTF-8",
"text/uri-list": "UTF-8",
"text/vcard": "UTF-8",
"text/vnd.curl": "UTF-8",
"text/vnd.curl.dcurl": "UTF-8",
"text/vnd.curl.mcurl": "UTF-8",
"text/vnd.curl.scurl": "UTF-8",
"text/vnd.familysearch.gedcom": "UTF-8",
"text/vnd.fly": "UTF-8",
"text/vnd.fmi.flexstor": "UTF-8",
"text/vnd.graphviz": "UTF-8",
"text/vnd.in3d.3dml": "UTF-8",
"text/vnd.in3d.spot": "UTF-8",
"text/vnd.sun.j2me.app-descriptor": "UTF-8",
"text/vnd.wap.wml": "UTF-8",
"text/vnd.wap.wmlscript": "UTF-8",
"text/vtt": "UTF-8",
"text/x-asm": "UTF-8",
"text/x-c": "UTF-8",
"text/x-component": "UTF-8",
"text/x-fortran": "UTF-8",
"text/x-handlebars-template": "UTF-8",
"text/x-java-source": "UTF-8",
"text/x-lua": "UTF-8",
"text/x-markdown": "UTF-8",
"text/x-nfo": "UTF-8",
"text/x-opml": "UTF-8",
"text/x-pascal": "UTF-8",
"text/x-processing": "UTF-8",
"text/x-sass": "UTF-8",
"text/x-scss": "UTF-8",
"text/x-setext": "UTF-8",
"text/x-sfv": "UTF-8",
"text/x-suse-ymp": "UTF-8",
"text/x-uuencode": "UTF-8",
"text/x-vcalendar": "UTF-8",
"text/x-vcard": "UTF-8",
"text/yaml": "UTF-8"
}
@@ -0,0 +1,206 @@
# frozen_string_literal: true
require "webrick"
module Jekyll
module Commands
class Serve
# This class is used to determine if the Servlet should modify a served file
# to insert the LiveReload script tags
class SkipAnalyzer
BAD_USER_AGENTS = [%r!MSIE!].freeze
def self.skip_processing?(request, response, options)
new(request, response, options).skip_processing?
end
def initialize(request, response, options)
@options = options
@request = request
@response = response
end
def skip_processing?
!html? || chunked? || inline? || bad_browser?
end
def chunked?
@response["Transfer-Encoding"] == "chunked"
end
def inline?
@response["Content-Disposition"].to_s.start_with?("inline")
end
def bad_browser?
BAD_USER_AGENTS.any? { |pattern| pattern.match?(@request["User-Agent"]) }
end
def html?
@response["Content-Type"].to_s.include?("text/html")
end
end
# This class inserts the LiveReload script tags into HTML as it is served
class BodyProcessor
HEAD_TAG_REGEX = %r!<head>|<head[^(er)][^<]*>!.freeze
attr_reader :content_length, :new_body, :livereload_added
def initialize(body, options)
@body = body
@options = options
@processed = false
end
def processed?
@processed
end
# rubocop:disable Metrics/MethodLength
def process!
@new_body = []
# @body will usually be a File object but Strings occur in rare cases
if @body.respond_to?(:each)
begin
@body.each { |line| @new_body << line.to_s }
ensure
@body.close
end
else
@new_body = @body.lines
end
@content_length = 0
@livereload_added = false
@new_body.each do |line|
if !@livereload_added && line["<head"]
line.gsub!(HEAD_TAG_REGEX) do |match|
%(#{match}#{template.result(binding)})
end
@livereload_added = true
end
@content_length += line.bytesize
@processed = true
end
@new_body = @new_body.join
end
# rubocop:enable Metrics/MethodLength
def template
# Unclear what "snipver" does. Doc at
# https://github.com/livereload/livereload-js states that the recommended
# setting is 1.
# Complicated JavaScript to ensure that livereload.js is loaded from the
# same origin as the page. Mostly useful for dealing with the browser's
# distinction between 'localhost' and 127.0.0.1
@template ||= ERB.new(<<~TEMPLATE)
<script>
document.write(
'<script src="' + location.protocol + '//' +
(location.host || 'localhost').split(':')[0] +
':<%=@options["livereload_port"] %>/livereload.js?snipver=1<%= livereload_args %>"' +
'></' +
'script>');
</script>
TEMPLATE
end
def livereload_args
# XHTML standard requires ampersands to be encoded as entities when in
# attributes. See http://stackoverflow.com/a/2190292
src = ""
if @options["livereload_min_delay"]
src += "&amp;mindelay=#{@options["livereload_min_delay"]}"
end
if @options["livereload_max_delay"]
src += "&amp;maxdelay=#{@options["livereload_max_delay"]}"
end
src += "&amp;port=#{@options["livereload_port"]}" if @options["livereload_port"]
src
end
end
class Servlet < WEBrick::HTTPServlet::FileHandler
DEFAULTS = {
"Cache-Control" => "private, max-age=0, proxy-revalidate, " \
"no-store, no-cache, must-revalidate",
}.freeze
def initialize(server, root, callbacks)
# So we can access them easily.
@jekyll_opts = server.config[:JekyllOptions]
@mime_types_charset = server.config[:MimeTypesCharset]
set_defaults
super
end
def search_index_file(req, res)
super ||
search_file(req, res, ".html") ||
search_file(req, res, ".xhtml")
end
# Add the ability to tap file.html the same way that Nginx does on our
# Docker images (or on GitHub Pages.) The difference is that we might end
# up with a different preference on which comes first.
def search_file(req, res, basename)
# /file.* > /file/index.html > /file.html
super ||
super(req, res, "#{basename}.html") ||
super(req, res, "#{basename}.xhtml")
end
# rubocop:disable Naming/MethodName
def do_GET(req, res)
rtn = super
if @jekyll_opts["livereload"]
return rtn if SkipAnalyzer.skip_processing?(req, res, @jekyll_opts)
processor = BodyProcessor.new(res.body, @jekyll_opts)
processor.process!
res.body = processor.new_body
res.content_length = processor.content_length.to_s
if processor.livereload_added
# Add a header to indicate that the page content has been modified
res["X-Rack-LiveReload"] = "1"
end
end
conditionally_inject_charset(res)
res.header.merge!(@headers)
rtn
end
# rubocop:enable Naming/MethodName
private
# Inject charset based on Jekyll config only if our mime-types database contains
# the charset metadata.
#
# Refer `script/vendor-mimes` in the repository for further details.
def conditionally_inject_charset(res)
typ = res.header["content-type"]
return unless @mime_types_charset.key?(typ)
return if %r!;\s*charset=!.match?(typ)
res.header["content-type"] = "#{typ}; charset=#{@jekyll_opts["encoding"]}"
end
def set_defaults
hash_ = @jekyll_opts.fetch("webrick", {}).fetch("headers", {})
DEFAULTS.each_with_object(@headers = hash_) do |(key, val), hash|
hash[key] = val unless hash.key?(key)
end
end
end
end
end
end
@@ -0,0 +1,81 @@
# frozen_string_literal: true
require "http/parser"
module Jekyll
module Commands
class Serve
# The LiveReload protocol requires the server to serve livereload.js over HTTP
# despite the fact that the protocol itself uses WebSockets. This custom connection
# class addresses the dual protocols that the server needs to understand.
class HttpAwareConnection < EventMachine::WebSocket::Connection
attr_reader :reload_body, :reload_size
def initialize(_opts)
# If EventMachine SSL support on Windows ever gets better, the code below will
# set up the reactor to handle SSL
#
# @ssl_enabled = opts["ssl_cert"] && opts["ssl_key"]
# if @ssl_enabled
# em_opts[:tls_options] = {
# :private_key_file => Jekyll.sanitized_path(opts["source"], opts["ssl_key"]),
# :cert_chain_file => Jekyll.sanitized_path(opts["source"], opts["ssl_cert"])
# }
# em_opts[:secure] = true
# end
# This is too noisy even for --verbose, but uncomment if you need it for
# a specific WebSockets issue. Adding ?LR-verbose=true onto the URL will
# enable logging on the client side.
# em_opts[:debug] = true
em_opts = {}
super(em_opts)
reload_file = File.join(Serve.singleton_class::LIVERELOAD_DIR, "livereload.js")
@reload_body = File.read(reload_file)
@reload_size = @reload_body.bytesize
end
# rubocop:disable Metrics/MethodLength
def dispatch(data)
parser = Http::Parser.new
parser << data
# WebSockets requests will have a Connection: Upgrade header
if parser.http_method != "GET" || parser.upgrade?
super
elsif parser.request_url.start_with?("/livereload.js")
headers = [
"HTTP/1.1 200 OK",
"Content-Type: application/javascript",
"Content-Length: #{reload_size}",
"",
"",
].join("\r\n")
send_data(headers)
# stream_file_data would free us from keeping livereload.js in memory
# but JRuby blocks on that call and never returns
send_data(reload_body)
close_connection_after_writing
else
body = "This port only serves livereload.js over HTTP.\n"
headers = [
"HTTP/1.1 400 Bad Request",
"Content-Type: text/plain",
"Content-Length: #{body.bytesize}",
"",
"",
].join("\r\n")
send_data(headers)
send_data(body)
close_connection_after_writing
end
end
# rubocop:enable Metrics/MethodLength
end
end
end
end
@@ -0,0 +1,313 @@
# frozen_string_literal: true
module Jekyll
class Configuration < Hash
# Default options. Overridden by values in _config.yml.
# Strings rather than symbols are used for compatibility with YAML.
DEFAULTS = {
# Where things are
"source" => Dir.pwd,
"destination" => File.join(Dir.pwd, "_site"),
"collections_dir" => "",
"cache_dir" => ".jekyll-cache",
"plugins_dir" => "_plugins",
"layouts_dir" => "_layouts",
"data_dir" => "_data",
"includes_dir" => "_includes",
"collections" => {},
# Handling Reading
"safe" => false,
"include" => [".htaccess"],
"exclude" => [],
"keep_files" => [".git", ".svn"],
"encoding" => "utf-8",
"markdown_ext" => "markdown,mkdown,mkdn,mkd,md",
"strict_front_matter" => false,
# Filtering Content
"show_drafts" => nil,
"limit_posts" => 0,
"future" => false,
"unpublished" => false,
# Plugins
"whitelist" => [],
"plugins" => [],
# Conversion
"markdown" => "kramdown",
"highlighter" => "rouge",
"lsi" => false,
"excerpt_separator" => "\n\n",
"incremental" => false,
# Serving
"detach" => false, # default to not detaching the server
"port" => "4000",
"host" => "127.0.0.1",
"baseurl" => nil, # this mounts at /, i.e. no subdirectory
"show_dir_listing" => false,
# Output Configuration
"permalink" => "date",
"paginate_path" => "/page:num",
"timezone" => nil, # use the local timezone
"quiet" => false,
"verbose" => false,
"defaults" => [],
"liquid" => {
"error_mode" => "warn",
"strict_filters" => false,
"strict_variables" => false,
},
"kramdown" => {
"auto_ids" => true,
"toc_levels" => (1..6).to_a,
"entity_output" => "as_char",
"smart_quotes" => "lsquo,rsquo,ldquo,rdquo",
"input" => "GFM",
"hard_wrap" => false,
"guess_lang" => true,
"footnote_nr" => 1,
"show_warnings" => false,
},
}.each_with_object(Configuration.new) { |(k, v), hsh| hsh[k] = v.freeze }.freeze
class << self
# Static: Produce a Configuration ready for use in a Site.
# It takes the input, fills in the defaults where values do not exist.
#
# user_config - a Hash or Configuration of overrides.
#
# Returns a Configuration filled with defaults.
def from(user_config)
Utils.deep_merge_hashes(DEFAULTS, Configuration[user_config].stringify_keys)
.add_default_collections.add_default_excludes
end
end
# Public: Turn all keys into string
#
# Return a copy of the hash where all its keys are strings
def stringify_keys
each_with_object({}) { |(k, v), hsh| hsh[k.to_s] = v }
end
def get_config_value_with_override(config_key, override)
override[config_key] || self[config_key] || DEFAULTS[config_key]
end
# Public: Directory of the Jekyll source folder
#
# override - the command-line options hash
#
# Returns the path to the Jekyll source directory
def source(override)
get_config_value_with_override("source", override)
end
def quiet(override = {})
get_config_value_with_override("quiet", override)
end
alias_method :quiet?, :quiet
def verbose(override = {})
get_config_value_with_override("verbose", override)
end
alias_method :verbose?, :verbose
def safe_load_file(filename)
case File.extname(filename)
when %r!\.toml!i
Jekyll::External.require_with_graceful_fail("tomlrb") unless defined?(Tomlrb)
Tomlrb.load_file(filename)
when %r!\.ya?ml!i
SafeYAML.load_file(filename) || {}
else
raise ArgumentError,
"No parser for '#{filename}' is available. Use a .y(a)ml or .toml file instead."
end
end
# Public: Generate list of configuration files from the override
#
# override - the command-line options hash
#
# Returns an Array of config files
def config_files(override)
# Adjust verbosity quickly
Jekyll.logger.adjust_verbosity(
:quiet => quiet?(override),
:verbose => verbose?(override)
)
# Get configuration from <source>/_config.yml or <source>/<config_file>
config_files = override["config"]
if config_files.to_s.empty?
default = %w(yml yaml toml).find(-> { "yml" }) do |ext|
File.exist?(Jekyll.sanitized_path(source(override), "_config.#{ext}"))
end
config_files = Jekyll.sanitized_path(source(override), "_config.#{default}")
@default_config_file = true
end
Array(config_files)
end
# Public: Read configuration and return merged Hash
#
# file - the path to the YAML file to be read in
#
# Returns this configuration, overridden by the values in the file
def read_config_file(file)
file = File.expand_path(file)
next_config = safe_load_file(file)
unless next_config.is_a?(Hash)
raise ArgumentError, "Configuration file: (INVALID) #{file}".yellow
end
Jekyll.logger.info "Configuration file:", file
next_config
rescue SystemCallError
if @default_config_file ||= nil
Jekyll.logger.warn "Configuration file:", "none"
{}
else
Jekyll.logger.error "Fatal:", "The configuration file '#{file}' could not be found."
raise LoadError, "The Configuration file '#{file}' could not be found."
end
end
# Public: Read in a list of configuration files and merge with this hash
#
# files - the list of configuration file paths
#
# Returns the full configuration, with the defaults overridden by the values in the
# configuration files
def read_config_files(files)
configuration = clone
begin
files.each do |config_file|
next if config_file.nil? || config_file.empty?
new_config = read_config_file(config_file)
configuration = Utils.deep_merge_hashes(configuration, new_config)
end
rescue ArgumentError => e
Jekyll.logger.warn "WARNING:", "Error reading configuration. Using defaults (and options)."
warn e
end
configuration.validate.add_default_collections
end
# Public: Split a CSV string into an array containing its values
#
# csv - the string of comma-separated values
#
# Returns an array of the values contained in the CSV
def csv_to_array(csv)
csv.split(",").map(&:strip)
end
# Public: Ensure the proper options are set in the configuration
#
# Returns the configuration Hash
def validate
config = clone
check_plugins(config)
check_include_exclude(config)
config
end
def add_default_collections
config = clone
# It defaults to `{}`, so this is only if someone sets it to null manually.
return config if config["collections"].nil?
# Ensure we have a hash.
if config["collections"].is_a?(Array)
config["collections"] = config["collections"].each_with_object({}) do |collection, hash|
hash[collection] = {}
end
end
config["collections"] = Utils.deep_merge_hashes(
{ "posts" => {} }, config["collections"]
).tap do |collections|
collections["posts"]["output"] = true
if config["permalink"]
collections["posts"]["permalink"] ||= style_to_permalink(config["permalink"])
end
end
config
end
DEFAULT_EXCLUDES = %w(
.sass-cache .jekyll-cache
gemfiles Gemfile Gemfile.lock
node_modules
vendor/bundle/ vendor/cache/ vendor/gems/ vendor/ruby/
).freeze
def add_default_excludes
config = clone
return config if config["exclude"].nil?
config["exclude"].concat(DEFAULT_EXCLUDES).uniq!
config
end
private
STYLE_TO_PERMALINK = {
:none => "/:categories/:title:output_ext",
:date => "/:categories/:year/:month/:day/:title:output_ext",
:ordinal => "/:categories/:year/:y_day/:title:output_ext",
:pretty => "/:categories/:year/:month/:day/:title/",
:weekdate => "/:categories/:year/W:week/:short_day/:title:output_ext",
}.freeze
private_constant :STYLE_TO_PERMALINK
def style_to_permalink(permalink_style)
STYLE_TO_PERMALINK[permalink_style.to_sym] || permalink_style.to_s
end
def check_include_exclude(config)
%w(include exclude).each do |option|
next unless config.key?(option)
next if config[option].is_a?(Array)
raise Jekyll::Errors::InvalidConfigurationError,
"'#{option}' should be set as an array, but was: #{config[option].inspect}."
end
end
# Private: Checks if the `plugins` config is a String
#
# config - the config hash
#
# Raises a Jekyll::Errors::InvalidConfigurationError if the config `plugins`
# is not an Array.
def check_plugins(config)
return unless config.key?("plugins")
return if config["plugins"].is_a?(Array)
Jekyll.logger.error "'plugins' should be set as an array of gem-names, but was: " \
"#{config["plugins"].inspect}. Use 'plugins_dir' instead to set " \
"the directory for your non-gemified Ruby plugins."
raise Jekyll::Errors::InvalidConfigurationError,
"'plugins' should be set as an array, but was: #{config["plugins"].inspect}."
end
end
end
@@ -0,0 +1,54 @@
# frozen_string_literal: true
module Jekyll
class Converter < Plugin
# Public: Get or set the highlighter prefix. When an argument is specified,
# the prefix will be set. If no argument is specified, the current prefix
# will be returned.
#
# highlighter_prefix - The String prefix (default: nil).
#
# Returns the String prefix.
def self.highlighter_prefix(highlighter_prefix = nil)
unless defined?(@highlighter_prefix) && highlighter_prefix.nil?
@highlighter_prefix = highlighter_prefix
end
@highlighter_prefix
end
# Public: Get or set the highlighter suffix. When an argument is specified,
# the suffix will be set. If no argument is specified, the current suffix
# will be returned.
#
# highlighter_suffix - The String suffix (default: nil).
#
# Returns the String suffix.
def self.highlighter_suffix(highlighter_suffix = nil)
unless defined?(@highlighter_suffix) && highlighter_suffix.nil?
@highlighter_suffix = highlighter_suffix
end
@highlighter_suffix
end
# Initialize the converter.
#
# Returns an initialized Converter.
def initialize(config = {})
@config = config
end
# Get the highlighter prefix.
#
# Returns the String prefix.
def highlighter_prefix
self.class.highlighter_prefix
end
# Get the highlighter suffix.
#
# Returns the String suffix.
def highlighter_suffix
self.class.highlighter_suffix
end
end
end
@@ -0,0 +1,41 @@
# frozen_string_literal: true
module Jekyll
module Converters
# Identity converter. Returns same content as given.
# For more info on converters see https://jekyllrb.com/docs/plugins/converters/
class Identity < Converter
safe true
priority :lowest
# Public: Does the given extension match this converter's list of acceptable extensions?
# Takes one argument: the file's extension (including the dot).
#
# _ext - The String extension to check (not relevant here)
#
# Returns true since it always matches.
def matches(_ext)
true
end
# Public: The extension to be given to the output file (including the dot).
#
# ext - The String extension or original file.
#
# Returns The String output file extension.
def output_ext(ext)
ext
end
# Logic to do the content conversion.
#
# content - String content of file (without front matter).
#
# Returns a String of the converted content.
def convert(content)
content
end
end
end
end
@@ -0,0 +1,113 @@
# frozen_string_literal: true
module Jekyll
module Converters
# Markdown converter.
# For more info on converters see https://jekyllrb.com/docs/plugins/converters/
class Markdown < Converter
highlighter_prefix "\n"
highlighter_suffix "\n"
safe true
def setup
return if @setup ||= false
unless (@parser = get_processor)
if @config["safe"]
Jekyll.logger.warn "Build Warning:", "Custom processors are not loaded in safe mode"
end
Jekyll.logger.error "Markdown processor:",
"#{@config["markdown"].inspect} is not a valid Markdown processor."
Jekyll.logger.error "", "Available processors are: #{valid_processors.join(", ")}"
Jekyll.logger.error ""
raise Errors::FatalException, "Invalid Markdown processor given: #{@config["markdown"]}"
end
@cache = Jekyll::Cache.new("Jekyll::Converters::Markdown")
@setup = true
end
# RuboCop does not allow reader methods to have names starting with `get_`
# To ensure compatibility, this check has been disabled on this method
#
# rubocop:disable Naming/AccessorMethodName
def get_processor
case @config["markdown"].downcase
when "kramdown" then KramdownParser.new(@config)
else
custom_processor
end
end
# rubocop:enable Naming/AccessorMethodName
# Public: Provides you with a list of processors comprised of the ones we support internally
# and the ones that you have provided to us (if they're whitelisted for use in safe mode).
#
# Returns an array of symbols.
def valid_processors
[:kramdown] + third_party_processors
end
# Public: A list of processors that you provide via plugins.
#
# Returns an array of symbols
def third_party_processors
self.class.constants - [:KramdownParser, :PRIORITIES]
end
# Does the given extension match this converter's list of acceptable extensions?
# Takes one argument: the file's extension (including the dot).
#
# ext - The String extension to check.
#
# Returns true if it matches, false otherwise.
def matches(ext)
extname_list.include?(ext.downcase)
end
# Public: The extension to be given to the output file (including the dot).
#
# ext - The String extension or original file.
#
# Returns The String output file extension.
def output_ext(_ext)
".html"
end
# Logic to do the content conversion.
#
# content - String content of file (without front matter).
#
# Returns a String of the converted content.
def convert(content)
setup
@cache.getset(content) do
@parser.convert(content)
end
end
def extname_list
@extname_list ||= @config["markdown_ext"].split(",").map! { |e| ".#{e.downcase}" }
end
private
def custom_processor
converter_name = @config["markdown"]
self.class.const_get(converter_name).new(@config) if custom_class_allowed?(converter_name)
end
# Private: Determine whether a class name is an allowed custom
# markdown class name.
#
# parser_name - the name of the parser class
#
# Returns true if the parser name contains only alphanumeric characters and is defined
# within Jekyll::Converters::Markdown
def custom_class_allowed?(parser_name)
parser_name !~ %r![^A-Za-z0-9_]! && self.class.constants.include?(parser_name.to_sym)
end
end
end
end
@@ -0,0 +1,197 @@
# frozen_string_literal: true
module Kramdown
# A Kramdown::Document subclass meant to optimize memory usage from initializing
# a kramdown document for parsing.
#
# The optimization is by using the same options Hash (and its derivatives) for
# converting all Markdown documents in a Jekyll site.
class JekyllDocument < Document
class << self
attr_reader :options, :parser
# The implementation is basically the core logic in +Kramdown::Document#initialize+
#
# rubocop:disable Naming/MemoizedInstanceVariableName
def setup(options)
@cache ||= {}
# reset variables on a subsequent set up with a different options Hash
unless @cache[:id] == options.hash
@options = @parser = nil
@cache[:id] = options.hash
end
@options ||= Options.merge(options).freeze
@parser ||= begin
parser_name = (@options[:input] || "kramdown").to_s
parser_name = parser_name[0..0].upcase + parser_name[1..-1]
try_require("parser", parser_name)
if Parser.const_defined?(parser_name)
Parser.const_get(parser_name)
else
raise Kramdown::Error, "kramdown has no parser to handle the specified " \
"input format: #{@options[:input]}"
end
end
end
# rubocop:enable Naming/MemoizedInstanceVariableName
private
def try_require(type, name)
require "kramdown/#{type}/#{Utils.snake_case(name)}"
rescue LoadError
false
end
end
def initialize(source, options = {})
JekyllDocument.setup(options)
@options = JekyllDocument.options
@root, @warnings = JekyllDocument.parser.parse(source, @options)
end
# Use Kramdown::Converter::Html class to convert this document into HTML.
#
# The implementation is basically an optimized version of core logic in
# +Kramdown::Document#method_missing+ from kramdown-2.1.0.
def to_html
output, warnings = Kramdown::Converter::Html.convert(@root, @options)
@warnings.concat(warnings)
output
end
end
end
#
module Jekyll
module Converters
class Markdown
class KramdownParser
CODERAY_DEFAULTS = {
"css" => "style",
"bold_every" => 10,
"line_numbers" => "inline",
"line_number_start" => 1,
"tab_width" => 4,
"wrap" => "div",
}.freeze
def initialize(config)
@main_fallback_highlighter = config["highlighter"] || "rouge"
@config = config["kramdown"] || {}
@highlighter = nil
setup
load_dependencies
end
# Setup and normalize the configuration:
# * Create Kramdown if it doesn't exist.
# * Set syntax_highlighter, detecting enable_coderay and merging
# highlighter if none.
# * Merge kramdown[coderay] into syntax_highlighter_opts stripping coderay_.
# * Make sure `syntax_highlighter_opts` exists.
def setup
@config["syntax_highlighter"] ||= highlighter
@config["syntax_highlighter_opts"] ||= {}
@config["syntax_highlighter_opts"]["default_lang"] ||= "plaintext"
@config["syntax_highlighter_opts"]["guess_lang"] = @config["guess_lang"]
@config["coderay"] ||= {} # XXX: Legacy.
modernize_coderay_config
end
def convert(content)
document = Kramdown::JekyllDocument.new(content, @config)
html_output = document.to_html
if @config["show_warnings"]
document.warnings.each do |warning|
Jekyll.logger.warn "Kramdown warning:", warning
end
end
html_output
end
private
def load_dependencies
require "kramdown-parser-gfm" if @config["input"] == "GFM"
if highlighter == "coderay"
Jekyll::External.require_with_graceful_fail("kramdown-syntax-coderay")
end
# `mathjax` engine is bundled within kramdown-2.x and will be handled by
# kramdown itself.
if (math_engine = @config["math_engine"]) && math_engine != "mathjax"
Jekyll::External.require_with_graceful_fail("kramdown-math-#{math_engine}")
end
end
# config[kramdown][syntax_highlighter] >
# config[kramdown][enable_coderay] >
# config[highlighter]
# Where `enable_coderay` is now deprecated because Kramdown
# supports Rouge now too.
def highlighter
return @highlighter if @highlighter
if @config["syntax_highlighter"]
return @highlighter = @config[
"syntax_highlighter"
]
end
@highlighter = if @config.key?("enable_coderay") && @config["enable_coderay"]
Jekyll::Deprecator.deprecation_message(
"You are using 'enable_coderay', " \
"use syntax_highlighter: coderay in your configuration file."
)
"coderay"
else
@main_fallback_highlighter
end
end
def strip_coderay_prefix(hash)
hash.each_with_object({}) do |(key, val), hsh|
cleaned_key = key.to_s.delete_prefix("coderay_")
if key != cleaned_key
Jekyll::Deprecator.deprecation_message(
"You are using '#{key}'. Normalizing to #{cleaned_key}."
)
end
hsh[cleaned_key] = val
end
end
# If our highlighter is CodeRay we go in to merge the CodeRay defaults
# with your "coderay" key if it's there, deprecating it in the
# process of you using it.
def modernize_coderay_config
unless @config["coderay"].empty?
Jekyll::Deprecator.deprecation_message(
"You are using 'kramdown.coderay' in your configuration, " \
"please use 'syntax_highlighter_opts' instead."
)
@config["syntax_highlighter_opts"] = begin
strip_coderay_prefix(
@config["syntax_highlighter_opts"] \
.merge(CODERAY_DEFAULTS) \
.merge(@config["coderay"])
)
end
end
end
end
end
end
end
@@ -0,0 +1,70 @@
# frozen_string_literal: true
module Kramdown
module Parser
class SmartyPants < Kramdown::Parser::Kramdown
def initialize(source, options)
super
@block_parsers = [:block_html, :content]
@span_parsers = [:smart_quotes, :html_entity, :typographic_syms, :span_html]
end
def parse_content
add_text @src.scan(%r!\A.*\n!)
end
define_parser(:content, %r!\A!)
end
end
end
module Jekyll
module Converters
# SmartyPants converter.
# For more info on converters see https://jekyllrb.com/docs/plugins/converters/
class SmartyPants < Converter
safe true
priority :low
def initialize(config)
Jekyll::External.require_with_graceful_fail "kramdown" unless defined?(Kramdown)
@config = config["kramdown"].dup || {}
@config[:input] = :SmartyPants
end
# Does the given extension match this converter's list of acceptable extensions?
# Takes one argument: the file's extension (including the dot).
#
# ext - The String extension to check.
#
# Returns true if it matches, false otherwise.
def matches(_ext)
false
end
# Public: The extension to be given to the output file (including the dot).
#
# ext - The String extension or original file.
#
# Returns The String output file extension.
def output_ext(_ext)
nil
end
# Logic to do the content conversion.
#
# content - String content of file (without front matter).
#
# Returns a String of the converted content.
def convert(content)
document = Kramdown::Document.new(content, @config)
html_output = document.to_html.chomp
if @config["show_warnings"]
document.warnings.each do |warning|
Jekyll.logger.warn "Kramdown warning:", warning.sub(%r!^Warning:\s+!, "")
end
end
html_output
end
end
end
end
@@ -0,0 +1,257 @@
# frozen_string_literal: true
# Convertible provides methods for converting a pagelike item
# from a certain type of markup into actual content
#
# Requires
# self.site -> Jekyll::Site
# self.content
# self.content=
# self.data=
# self.ext=
# self.output=
# self.name
# self.path
# self.type -> :page, :post or :draft
module Jekyll
module Convertible
# Returns the contents as a String.
def to_s
content || ""
end
# Whether the file is published or not, as indicated in YAML front-matter
def published?
!(data.key?("published") && data["published"] == false)
end
# Read the YAML frontmatter.
#
# base - The String path to the dir containing the file.
# name - The String filename of the file.
# opts - optional parameter to File.read, default at site configs
#
# Returns nothing.
# rubocop:disable Metrics/AbcSize
def read_yaml(base, name, opts = {})
filename = @path || site.in_source_dir(base, name)
Jekyll.logger.debug "Reading:", relative_path
begin
self.content = File.read(filename, **Utils.merged_file_read_opts(site, opts))
if content =~ Document::YAML_FRONT_MATTER_REGEXP
self.content = Regexp.last_match.post_match
self.data = SafeYAML.load(Regexp.last_match(1))
end
rescue Psych::SyntaxError => e
Jekyll.logger.warn "YAML Exception reading #{filename}: #{e.message}"
raise e if site.config["strict_front_matter"]
rescue StandardError => e
Jekyll.logger.warn "Error reading file #{filename}: #{e.message}"
raise e if site.config["strict_front_matter"]
end
self.data ||= {}
validate_data! filename
validate_permalink! filename
self.data
end
# rubocop:enable Metrics/AbcSize
def validate_data!(filename)
unless self.data.is_a?(Hash)
raise Errors::InvalidYAMLFrontMatterError,
"Invalid YAML front matter in #{filename}"
end
end
def validate_permalink!(filename)
if self.data["permalink"] == ""
raise Errors::InvalidPermalinkError, "Invalid permalink in #{filename}"
end
end
# Transform the contents based on the content type.
#
# Returns the transformed contents.
def transform
renderer.convert(content)
end
# Determine the extension depending on content_type.
#
# Returns the String extension for the output file.
# e.g. ".html" for an HTML output file.
def output_ext
renderer.output_ext
end
# Determine which converter to use based on this convertible's
# extension.
#
# Returns the Converter instance.
def converters
renderer.converters
end
# Render Liquid in the content
#
# content - the raw Liquid content to render
# payload - the payload for Liquid
# info - the info for Liquid
#
# Returns the converted content
def render_liquid(content, payload, info, path)
renderer.render_liquid(content, payload, info, path)
end
# Convert this Convertible's data to a Hash suitable for use by Liquid.
#
# Returns the Hash representation of this Convertible.
def to_liquid(attrs = nil)
further_data = \
(attrs || self.class::ATTRIBUTES_FOR_LIQUID).each_with_object({}) do |attribute, hsh|
hsh[attribute] = send(attribute)
end
Utils.deep_merge_hashes defaults, Utils.deep_merge_hashes(data, further_data)
end
# The type of a document,
# i.e., its classname downcase'd and to_sym'd.
#
# Returns the type of self.
def type
:pages if is_a?(Page)
end
# returns the owner symbol for hook triggering
def hook_owner
:pages if is_a?(Page)
end
# Determine whether the document is an asset file.
# Asset files include CoffeeScript files and Sass/SCSS files.
#
# Returns true if the extname belongs to the set of extensions
# that asset files use.
def asset_file?
sass_file? || coffeescript_file?
end
# Determine whether the document is a Sass file.
#
# Returns true if extname == .sass or .scss, false otherwise.
def sass_file?
Jekyll::Document::SASS_FILE_EXTS.include?(ext)
end
# Determine whether the document is a CoffeeScript file.
#
# Returns true if extname == .coffee, false otherwise.
def coffeescript_file?
ext == ".coffee"
end
# Determine whether the file should be rendered with Liquid.
#
# Returns true if the file has Liquid Tags or Variables, false otherwise.
def render_with_liquid?
return false if data["render_with_liquid"] == false
Jekyll::Utils.has_liquid_construct?(content)
end
# Determine whether the file should be placed into layouts.
#
# Returns false if the document is an asset file or if the front matter
# specifies `layout: none`
def place_in_layout?
!(asset_file? || no_layout?)
end
# Checks if the layout specified in the document actually exists
#
# layout - the layout to check
#
# Returns true if the layout is invalid, false if otherwise
def invalid_layout?(layout)
!data["layout"].nil? && layout.nil? && !(is_a? Jekyll::Excerpt)
end
# Recursively render layouts
#
# layouts - a list of the layouts
# payload - the payload for Liquid
# info - the info for Liquid
#
# Returns nothing
def render_all_layouts(layouts, payload, info)
renderer.layouts = layouts
self.output = renderer.place_in_layouts(output, payload, info)
ensure
@renderer = nil # this will allow the modifications above to disappear
end
# Add any necessary layouts to this convertible document.
#
# payload - The site payload Drop or Hash.
# layouts - A Hash of {"name" => "layout"}.
#
# Returns nothing.
def do_layout(payload, layouts)
self.output = renderer.tap do |doc_renderer|
doc_renderer.layouts = layouts
doc_renderer.payload = payload
end.run
Jekyll.logger.debug "Post-Render Hooks:", relative_path
Jekyll::Hooks.trigger hook_owner, :post_render, self
ensure
@renderer = nil # this will allow the modifications above to disappear
end
# Write the generated page file to the destination directory.
#
# dest - The String path to the destination dir.
#
# Returns nothing.
def write(dest)
path = destination(dest)
FileUtils.mkdir_p(File.dirname(path))
Jekyll.logger.debug "Writing:", path
File.write(path, output, :mode => "wb")
Jekyll::Hooks.trigger hook_owner, :post_write, self
end
# Accessor for data properties by Liquid.
#
# property - The String name of the property to retrieve.
#
# Returns the String value or nil if the property isn't included.
def [](property)
if self.class::ATTRIBUTES_FOR_LIQUID.include?(property)
send(property)
else
data[property]
end
end
def renderer
@renderer ||= Jekyll::Renderer.new(site, self)
end
private
def defaults
@defaults ||= site.frontmatter_defaults.all(relative_path, type)
end
def no_layout?
data["layout"] == "none"
end
end
end
@@ -0,0 +1,50 @@
# frozen_string_literal: true
module Jekyll
module Deprecator
extend self
def process(args)
arg_is_present? args, "--server", "The --server command has been replaced by the \
'serve' subcommand."
arg_is_present? args, "--serve", "The --serve command has been replaced by the \
'serve' subcommand."
arg_is_present? args, "--no-server", "To build Jekyll without launching a server, \
use the 'build' subcommand."
arg_is_present? args, "--auto", "The switch '--auto' has been replaced with \
'--watch'."
arg_is_present? args, "--no-auto", "To disable auto-replication, simply leave off \
the '--watch' switch."
arg_is_present? args, "--pygments", "The 'pygments' settings has been removed in \
favour of 'highlighter'."
arg_is_present? args, "--paginate", "The 'paginate' setting can only be set in \
your config files."
arg_is_present? args, "--url", "The 'url' setting can only be set in your \
config files."
no_subcommand(args)
end
def no_subcommand(args)
unless args.empty? ||
args.first !~ %r(!/^--/!) || %w(--help --version).include?(args.first)
deprecation_message "Jekyll now uses subcommands instead of just switches. \
Run `jekyll help` to find out more."
abort
end
end
def arg_is_present?(args, deprecated_argument, message)
deprecation_message(message) if args.include?(deprecated_argument)
end
def deprecation_message(message)
Jekyll.logger.warn "Deprecation:", message
end
def defaults_deprecate_type(old, current)
Jekyll.logger.warn "Defaults:", "The '#{old}' type has become '#{current}'."
Jekyll.logger.warn "Defaults:", "Please update your front-matter defaults to use \
'type: #{current}'."
end
end
end
@@ -0,0 +1,543 @@
# frozen_string_literal: true
module Jekyll
class Document
include Comparable
extend Forwardable
attr_reader :path, :site, :extname, :collection, :type
attr_accessor :content, :output
def_delegator :self, :read_post_data, :post_read
YAML_FRONT_MATTER_REGEXP = %r!\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)!m.freeze
DATELESS_FILENAME_MATCHER = %r!^(?:.+/)*(.*)(\.[^.]+)$!.freeze
DATE_FILENAME_MATCHER = %r!^(?>.+/)*?(\d{2,4}-\d{1,2}-\d{1,2})-([^/]*)(\.[^.]+)$!.freeze
SASS_FILE_EXTS = %w(.sass .scss).freeze
YAML_FILE_EXTS = %w(.yaml .yml).freeze
#
# Class-wide cache to stash and retrieve regexp to detect "super-directories"
# of a particular Jekyll::Document object.
#
# dirname - The *special directory* for the Document.
# e.g. "_posts" or "_drafts" for Documents from the `site.posts` collection.
def self.superdirs_regex(dirname)
@superdirs_regex ||= {}
@superdirs_regex[dirname] ||= %r!#{dirname}.*!
end
#
# Create a new Document.
#
# path - the path to the file
# relations - a hash with keys :site and :collection, the values of which
# are the Jekyll::Site and Jekyll::Collection to which this
# Document belong.
#
# Returns nothing.
def initialize(path, relations = {})
@site = relations[:site]
@path = path
@extname = File.extname(path)
@collection = relations[:collection]
@type = @collection.label.to_sym
@has_yaml_header = nil
if draft?
categories_from_path("_drafts")
else
categories_from_path(collection.relative_directory)
end
data.default_proc = proc do |_, key|
site.frontmatter_defaults.find(relative_path, type, key)
end
trigger_hooks(:post_init)
end
# Fetch the Document's data.
#
# Returns a Hash containing the data. An empty hash is returned if
# no data was read.
def data
@data ||= {}
end
# Merge some data in with this document's data.
#
# Returns the merged data.
def merge_data!(other, source: "YAML front matter")
merge_categories!(other)
Utils.deep_merge_hashes!(data, other)
merge_date!(source)
data
end
# Returns the document date. If metadata is not present then calculates it
# based on Jekyll::Site#time or the document file modification time.
#
# Return document date string.
def date
data["date"] ||= (draft? ? source_file_mtime : site.time)
end
# Return document file modification time in the form of a Time object.
#
# Return document file modification Time object.
def source_file_mtime
File.mtime(path)
end
# Returns whether the document is a draft. This is only the case if
# the document is in the 'posts' collection but in a different
# directory than '_posts'.
#
# Returns whether the document is a draft.
def draft?
data["draft"] ||= relative_path.index(collection.relative_directory).nil? &&
collection.label == "posts"
end
# The path to the document, relative to the collections_dir.
#
# Returns a String path which represents the relative path from the collections_dir
# to this document.
def relative_path
@relative_path ||= path.sub("#{site.collections_path}/", "")
end
# The output extension of the document.
#
# Returns the output extension
def output_ext
renderer.output_ext
end
# The base filename of the document, without the file extname.
#
# Returns the basename without the file extname.
def basename_without_ext
@basename_without_ext ||= File.basename(path, ".*")
end
# The base filename of the document.
#
# Returns the base filename of the document.
def basename
@basename ||= File.basename(path)
end
def renderer
@renderer ||= Jekyll::Renderer.new(site, self)
end
# Produces a "cleaned" relative path.
# The "cleaned" relative path is the relative path without the extname
# and with the collection's directory removed as well.
# This method is useful when building the URL of the document.
#
# NOTE: `String#gsub` removes all trailing periods (in comparison to `String#chomp`)
#
# Examples:
# When relative_path is "_methods/site/generate...md":
# cleaned_relative_path
# # => "/site/generate"
#
# Returns the cleaned relative path of the document.
def cleaned_relative_path
@cleaned_relative_path ||=
relative_path[0..-extname.length - 1]
.sub(collection.relative_directory, "")
.gsub(%r!\.*\z!, "")
end
# Determine whether the document is a YAML file.
#
# Returns true if the extname is either .yml or .yaml, false otherwise.
def yaml_file?
YAML_FILE_EXTS.include?(extname)
end
# Determine whether the document is an asset file.
# Asset files include CoffeeScript files and Sass/SCSS files.
#
# Returns true if the extname belongs to the set of extensions
# that asset files use.
def asset_file?
sass_file? || coffeescript_file?
end
# Determine whether the document is a Sass file.
#
# Returns true if extname == .sass or .scss, false otherwise.
def sass_file?
SASS_FILE_EXTS.include?(extname)
end
# Determine whether the document is a CoffeeScript file.
#
# Returns true if extname == .coffee, false otherwise.
def coffeescript_file?
extname == ".coffee"
end
# Determine whether the file should be rendered with Liquid.
#
# Returns false if the document is either an asset file or a yaml file,
# or if the document doesn't contain any Liquid Tags or Variables,
# true otherwise.
def render_with_liquid?
return false if data["render_with_liquid"] == false
!(coffeescript_file? || yaml_file? || !Utils.has_liquid_construct?(content))
end
# Determine whether the file should be rendered with a layout.
#
# Returns true if the Front Matter specifies that `layout` is set to `none`.
def no_layout?
data["layout"] == "none"
end
# Determine whether the file should be placed into layouts.
#
# Returns false if the document is set to `layouts: none`, or is either an
# asset file or a yaml file. Returns true otherwise.
def place_in_layout?
!(asset_file? || yaml_file? || no_layout?)
end
# The URL template where the document would be accessible.
#
# Returns the URL template for the document.
def url_template
collection.url_template
end
# Construct a Hash of key-value pairs which contain a mapping between
# a key in the URL template and the corresponding value for this document.
#
# Returns the Hash of key-value pairs for replacement in the URL.
def url_placeholders
@url_placeholders ||= Drops::UrlDrop.new(self)
end
# The permalink for this Document.
# Permalink is set via the data Hash.
#
# Returns the permalink or nil if no permalink was set in the data.
def permalink
data && data.is_a?(Hash) && data["permalink"]
end
# The computed URL for the document. See `Jekyll::URL#to_s` for more details.
#
# Returns the computed URL for the document.
def url
@url ||= URL.new(
:template => url_template,
:placeholders => url_placeholders,
:permalink => permalink
).to_s
end
def [](key)
data[key]
end
# The full path to the output file.
#
# base_directory - the base path of the output directory
#
# Returns the full path to the output file of this document.
def destination(base_directory)
@destination ||= {}
@destination[base_directory] ||= begin
path = site.in_dest_dir(base_directory, URL.unescape_path(url))
if url.end_with? "/"
path = File.join(path, "index.html")
else
path << output_ext unless path.end_with? output_ext
end
path
end
end
# Write the generated Document file to the destination directory.
#
# dest - The String path to the destination dir.
#
# Returns nothing.
def write(dest)
path = destination(dest)
FileUtils.mkdir_p(File.dirname(path))
Jekyll.logger.debug "Writing:", path
File.write(path, output, :mode => "wb")
trigger_hooks(:post_write)
end
# Whether the file is published or not, as indicated in YAML front-matter
#
# Returns 'false' if the 'published' key is specified in the
# YAML front-matter and is 'false'. Otherwise returns 'true'.
def published?
!(data.key?("published") && data["published"] == false)
end
# Read in the file and assign the content and data based on the file contents.
# Merge the frontmatter of the file with the frontmatter default
# values
#
# Returns nothing.
def read(opts = {})
Jekyll.logger.debug "Reading:", relative_path
if yaml_file?
@data = SafeYAML.load_file(path)
else
begin
merge_defaults
read_content(**opts)
read_post_data
rescue StandardError => e
handle_read_error(e)
end
end
end
# Create a Liquid-understandable version of this Document.
#
# Returns a Hash representing this Document's data.
def to_liquid
@to_liquid ||= Drops::DocumentDrop.new(self)
end
# The inspect string for this document.
# Includes the relative path and the collection label.
#
# Returns the inspect string for this document.
def inspect
"#<#{self.class} #{relative_path} collection=#{collection.label}>"
end
# The string representation for this document.
#
# Returns the content of the document
def to_s
output || content || "NO CONTENT"
end
# Compare this document against another document.
# Comparison is a comparison between the 2 paths of the documents.
#
# Returns -1, 0, +1 or nil depending on whether this doc's path is less than,
# equal or greater than the other doc's path. See String#<=> for more details.
def <=>(other)
return nil unless other.respond_to?(:data)
cmp = data["date"] <=> other.data["date"]
cmp = path <=> other.path if cmp.nil? || cmp.zero?
cmp
end
# Determine whether this document should be written.
# Based on the Collection to which it belongs.
#
# True if the document has a collection and if that collection's #write?
# method returns true, and if the site's Publisher will publish the document.
# False otherwise.
#
# rubocop:disable Naming/MemoizedInstanceVariableName
def write?
return @write_p if defined?(@write_p)
@write_p = collection&.write? && site.publisher.publish?(self)
end
# rubocop:enable Naming/MemoizedInstanceVariableName
# The Document excerpt_separator, from the YAML Front-Matter or site
# default excerpt_separator value
#
# Returns the document excerpt_separator
def excerpt_separator
@excerpt_separator ||= (data["excerpt_separator"] || site.config["excerpt_separator"]).to_s
end
# Whether to generate an excerpt
#
# Returns true if the excerpt separator is configured.
def generate_excerpt?
!excerpt_separator.empty?
end
def next_doc
pos = collection.docs.index { |post| post.equal?(self) }
collection.docs[pos + 1] if pos && pos < collection.docs.length - 1
end
def previous_doc
pos = collection.docs.index { |post| post.equal?(self) }
collection.docs[pos - 1] if pos && pos.positive?
end
def trigger_hooks(hook_name, *args)
Jekyll::Hooks.trigger collection.label.to_sym, hook_name, self, *args if collection
Jekyll::Hooks.trigger :documents, hook_name, self, *args
end
def id
@id ||= File.join(File.dirname(url), (data["slug"] || basename_without_ext).to_s)
end
# Calculate related posts.
#
# Returns an Array of related Posts.
def related_posts
@related_posts ||= Jekyll::RelatedPosts.new(self).build
end
# Override of method_missing to check in @data for the key.
def method_missing(method, *args, &blck)
if data.key?(method.to_s)
Jekyll::Deprecator.deprecation_message "Document##{method} is now a key in the #data hash."
Jekyll.logger.warn "", "Called by #{caller(1..1)[0]}."
data[method.to_s]
else
super
end
end
def respond_to_missing?(method, *)
data.key?(method.to_s) || super
end
# Add superdirectories of the special_dir to categories.
# In the case of es/_posts, 'es' is added as a category.
# In the case of _posts/es, 'es' is NOT added as a category.
#
# Returns nothing.
def categories_from_path(special_dir)
if relative_path.start_with?(special_dir)
superdirs = []
else
superdirs = relative_path.sub(Document.superdirs_regex(special_dir), "")
superdirs = superdirs.split(File::SEPARATOR)
superdirs.reject! { |c| c.empty? || c == special_dir || c == basename }
end
merge_data!({ "categories" => superdirs }, :source => "file path")
end
def populate_categories
categories = Array(data["categories"]) + Utils.pluralized_array_from_hash(
data, "category", "categories"
)
categories.map!(&:to_s)
categories.flatten!
categories.uniq!
merge_data!({ "categories" => categories })
end
def populate_tags
tags = Utils.pluralized_array_from_hash(data, "tag", "tags")
tags.flatten!
merge_data!({ "tags" => tags })
end
private
def merge_categories!(other)
if other.key?("categories") && !other["categories"].nil?
other["categories"] = other["categories"].split if other["categories"].is_a?(String)
if data["categories"].is_a?(Array)
other["categories"] = data["categories"] | other["categories"]
end
end
end
def merge_date!(source)
if data.key?("date")
data["date"] = Utils.parse_date(
data["date"].to_s,
"Document '#{relative_path}' does not have a valid date in the #{source}."
)
end
end
def merge_defaults
defaults = @site.frontmatter_defaults.all(relative_path, type)
merge_data!(defaults, :source => "front matter defaults") unless defaults.empty?
end
def read_content(**opts)
self.content = File.read(path, **Utils.merged_file_read_opts(site, opts))
if content =~ YAML_FRONT_MATTER_REGEXP
self.content = Regexp.last_match.post_match
data_file = SafeYAML.load(Regexp.last_match(1))
merge_data!(data_file, :source => "YAML front matter") if data_file
end
end
def read_post_data
populate_title
populate_categories
populate_tags
generate_excerpt
end
def handle_read_error(error)
if error.is_a? Psych::SyntaxError
Jekyll.logger.error "Error:", "YAML Exception reading #{path}: #{error.message}"
else
Jekyll.logger.error "Error:", "could not read file #{path}: #{error.message}"
end
if site.config["strict_front_matter"] || error.is_a?(Jekyll::Errors::FatalException)
raise error
end
end
def populate_title
if relative_path =~ DATE_FILENAME_MATCHER
date, slug, ext = Regexp.last_match.captures
modify_date(date)
elsif relative_path =~ DATELESS_FILENAME_MATCHER
slug, ext = Regexp.last_match.captures
end
# `slug` will be nil for documents without an extension since the regex patterns
# above tests for an extension as well.
# In such cases, assign `basename_without_ext` as the slug.
slug ||= basename_without_ext
# slugs shouldn't end with a period
# `String#gsub!` removes all trailing periods (in comparison to `String#chomp!`)
slug.gsub!(%r!\.*\z!, "")
# Try to ensure the user gets a title.
data["title"] ||= Utils.titleize_slug(slug)
# Only overwrite slug & ext if they aren't specified.
data["slug"] ||= slug
data["ext"] ||= ext
end
def modify_date(date)
if !data["date"] || data["date"].to_i == site.time.to_i
merge_data!({ "date" => date }, :source => "filename")
end
end
def generate_excerpt
data["excerpt"] ||= Jekyll::Excerpt.new(self) if generate_excerpt?
end
end
end
@@ -0,0 +1,20 @@
# frozen_string_literal: true
module Jekyll
module Drops
class CollectionDrop < Drop
extend Forwardable
mutable false
delegate_method_as :write?, :output
delegate_methods :label, :docs, :files, :directory, :relative_directory
private delegate_method_as :metadata, :fallback_data
def to_s
docs.to_s
end
end
end
end
@@ -0,0 +1,74 @@
# frozen_string_literal: true
module Jekyll
module Drops
class DocumentDrop < Drop
extend Forwardable
NESTED_OBJECT_FIELD_BLACKLIST = %w(
content output excerpt next previous
).freeze
mutable false
delegate_method_as :relative_path, :path
private delegate_method_as :data, :fallback_data
delegate_methods :id, :output, :content, :to_s, :relative_path, :url, :date
data_delegators "title", "categories", "tags"
def collection
@obj.collection.label
end
def excerpt
fallback_data["excerpt"].to_s
end
def name
fallback_data["name"] || @obj.basename
end
def <=>(other)
return nil unless other.is_a? DocumentDrop
cmp = self["date"] <=> other["date"]
cmp = self["path"] <=> other["path"] if cmp.nil? || cmp.zero?
cmp
end
def previous
@obj.previous_doc.to_liquid
end
def next
@obj.next_doc.to_liquid
end
# Generate a Hash for use in generating JSON.
# This is useful if fields need to be cleared before the JSON can generate.
#
# state - the JSON::State object which determines the state of current processing.
#
# Returns a Hash ready for JSON generation.
def hash_for_json(state = nil)
to_h.tap do |hash|
if state && state.depth >= 2
hash["previous"] = collapse_document(hash["previous"]) if hash["previous"]
hash["next"] = collapse_document(hash["next"]) if hash["next"]
end
end
end
# Generate a Hash which breaks the recursive chain.
# Certain fields which are normally available are omitted.
#
# Returns a Hash with only non-recursive fields present.
def collapse_document(doc)
doc.keys.each_with_object({}) do |(key, _), result|
result[key] = doc[key] unless NESTED_OBJECT_FIELD_BLACKLIST.include?(key)
end
end
end
end
end
@@ -0,0 +1,294 @@
# frozen_string_literal: true
module Jekyll
module Drops
class Drop < Liquid::Drop
include Enumerable
NON_CONTENT_METHODS = [:fallback_data, :collapse_document].freeze
NON_CONTENT_METHOD_NAMES = NON_CONTENT_METHODS.map(&:to_s).freeze
private_constant :NON_CONTENT_METHOD_NAMES
# A private stash to avoid repeatedly generating the setter method name string for
# a call to `Drops::Drop#[]=`.
# The keys of the stash below have a very high probability of being called upon during
# the course of various `Jekyll::Renderer#run` calls.
SETTER_KEYS_STASH = {
"content" => "content=",
"layout" => "layout=",
"page" => "page=",
"paginator" => "paginator=",
"highlighter_prefix" => "highlighter_prefix=",
"highlighter_suffix" => "highlighter_suffix=",
}.freeze
private_constant :SETTER_KEYS_STASH
class << self
# Get or set whether the drop class is mutable.
# Mutability determines whether or not pre-defined fields may be
# overwritten.
#
# is_mutable - Boolean set mutability of the class (default: nil)
#
# Returns the mutability of the class
def mutable(is_mutable = nil)
@is_mutable = is_mutable || false
end
def mutable?
@is_mutable
end
# public delegation helper methods that calls onto Drop's instance
# variable `@obj`.
# Generate private Drop instance_methods for each symbol in the given list.
#
# Returns nothing.
def private_delegate_methods(*symbols)
symbols.each { |symbol| private delegate_method(symbol) }
nil
end
# Generate public Drop instance_methods for each symbol in the given list.
#
# Returns nothing.
def delegate_methods(*symbols)
symbols.each { |symbol| delegate_method(symbol) }
nil
end
# Generate public Drop instance_method for given symbol that calls `@obj.<sym>`.
#
# Returns delegated method symbol.
def delegate_method(symbol)
define_method(symbol) { @obj.send(symbol) }
end
# Generate public Drop instance_method named `delegate` that calls `@obj.<original>`.
#
# Returns delegated method symbol.
def delegate_method_as(original, delegate)
define_method(delegate) { @obj.send(original) }
end
# Generate public Drop instance_methods for each string entry in the given list.
# The generated method(s) access(es) `@obj`'s data hash.
#
# Returns nothing.
def data_delegators(*strings)
strings.each do |key|
data_delegator(key) if key.is_a?(String)
end
nil
end
# Generate public Drop instance_methods for given string `key`.
# The generated method access(es) `@obj`'s data hash.
#
# Returns method symbol.
def data_delegator(key)
define_method(key.to_sym) { @obj.data[key] }
end
# Array of stringified instance methods that do not end with the assignment operator.
#
# (<klass>.instance_methods always generates a new Array object so it can be mutated)
#
# Returns array of strings.
def getter_method_names
@getter_method_names ||= instance_methods.map!(&:to_s).tap do |list|
list.reject! { |item| item.end_with?("=") }
end
end
end
# Create a new Drop
#
# obj - the Jekyll Site, Collection, or Document required by the
# drop.
#
# Returns nothing
def initialize(obj)
@obj = obj
end
# Access a method in the Drop or a field in the underlying hash data.
# If mutable, checks the mutations first. Then checks the methods,
# and finally check the underlying hash (e.g. document front matter)
# if all the previous places didn't match.
#
# key - the string key whose value to fetch
#
# Returns the value for the given key, or nil if none exists
def [](key)
if self.class.mutable? && mutations.key?(key)
mutations[key]
elsif self.class.invokable? key
public_send key
else
fallback_data[key]
end
end
alias_method :invoke_drop, :[]
# Set a field in the Drop. If mutable, sets in the mutations and
# returns. If not mutable, checks first if it's trying to override a
# Drop method and raises a DropMutationException if so. If not
# mutable and the key is not a method on the Drop, then it sets the
# key to the value in the underlying hash (e.g. document front
# matter)
#
# key - the String key whose value to set
# val - the Object to set the key's value to
#
# Returns the value the key was set to unless the Drop is not mutable
# and the key matches a method in which case it raises a
# DropMutationException.
def []=(key, val)
setter = SETTER_KEYS_STASH[key] || "#{key}="
if respond_to?(setter)
public_send(setter, val)
elsif respond_to?(key.to_s)
if self.class.mutable?
mutations[key] = val
else
raise Errors::DropMutationException, "Key #{key} cannot be set in the drop."
end
else
fallback_data[key] = val
end
end
# Generates a list of strings which correspond to content getter
# methods.
#
# Returns an Array of strings which represent method-specific keys.
def content_methods
@content_methods ||= \
self.class.getter_method_names \
- Jekyll::Drops::Drop.getter_method_names \
- NON_CONTENT_METHOD_NAMES
end
# Check if key exists in Drop
#
# key - the string key whose value to fetch
#
# Returns true if the given key is present
def key?(key)
return false if key.nil?
return true if self.class.mutable? && mutations.key?(key)
respond_to?(key) || fallback_data.key?(key)
end
# Generates a list of keys with user content as their values.
# This gathers up the Drop methods and keys of the mutations and
# underlying data hashes and performs a set union to ensure a list
# of unique keys for the Drop.
#
# Returns an Array of unique keys for content for the Drop.
def keys
(content_methods |
mutations.keys |
fallback_data.keys).flatten
end
# Generate a Hash representation of the Drop by resolving each key's
# value. It includes Drop methods, mutations, and the underlying object's
# data. See the documentation for Drop#keys for more.
#
# Returns a Hash with all the keys and values resolved.
def to_h
keys.each_with_object({}) do |(key, _), result|
result[key] = self[key]
end
end
alias_method :to_hash, :to_h
# Inspect the drop's keys and values through a JSON representation
# of its keys and values.
#
# Returns a pretty generation of the hash representation of the Drop.
def inspect
JSON.pretty_generate to_h
end
# Generate a Hash for use in generating JSON.
# This is useful if fields need to be cleared before the JSON can generate.
#
# Returns a Hash ready for JSON generation.
def hash_for_json(*)
to_h
end
# Generate a JSON representation of the Drop.
#
# state - the JSON::State object which determines the state of current processing.
#
# Returns a JSON representation of the Drop in a String.
def to_json(state = nil)
JSON.generate(hash_for_json(state), state)
end
# Collects all the keys and passes each to the block in turn.
#
# block - a block which accepts one argument, the key
#
# Returns nothing.
def each_key(&block)
keys.each(&block)
end
def each
each_key.each do |key|
yield key, self[key]
end
end
def merge(other, &block)
dup.tap do |me|
if block.nil?
me.merge!(other)
else
me.merge!(other, block)
end
end
end
def merge!(other)
other.each_key do |key|
if block_given?
self[key] = yield key, self[key], other[key]
else
if Utils.mergable?(self[key]) && Utils.mergable?(other[key])
self[key] = Utils.deep_merge_hashes(self[key], other[key])
next
end
self[key] = other[key] unless other[key].nil?
end
end
end
# Imitate Hash.fetch method in Drop
#
# Returns value if key is present in Drop, otherwise returns default value
# KeyError is raised if key is not present and no default value given
def fetch(key, default = nil, &block)
return self[key] if key?(key)
raise KeyError, %(key not found: "#{key}") if default.nil? && block.nil?
return yield(key) unless block.nil?
default unless default.nil?
end
private
def mutations
@mutations ||= {}
end
end
end
end
@@ -0,0 +1,23 @@
# frozen_string_literal: true
module Jekyll
module Drops
class ExcerptDrop < DocumentDrop
def layout
@obj.doc.data["layout"]
end
def date
@obj.doc.date
end
def excerpt
nil
end
def name
@obj.doc.data["name"] || @obj.doc.basename
end
end
end
end
@@ -0,0 +1,32 @@
# frozen_string_literal: true
module Jekyll
module Drops
class JekyllDrop < Liquid::Drop
class << self
def global
@global ||= JekyllDrop.new
end
end
def version
Jekyll::VERSION
end
def environment
Jekyll.env
end
def to_h
@to_h ||= {
"version" => version,
"environment" => environment,
}
end
def to_json(state = nil)
JSON.generate(to_h, state)
end
end
end
end
@@ -0,0 +1,66 @@
# frozen_string_literal: true
module Jekyll
module Drops
class SiteDrop < Drop
extend Forwardable
mutable false
delegate_method_as :site_data, :data
delegate_methods :time, :pages, :static_files, :tags, :categories
private delegate_method_as :config, :fallback_data
def [](key)
if key != "posts" && @obj.collections.key?(key)
@obj.collections[key].docs
else
super(key)
end
end
def key?(key)
(key != "posts" && @obj.collections.key?(key)) || super
end
def posts
@site_posts ||= @obj.posts.docs.sort { |a, b| b <=> a }
end
def html_pages
@site_html_pages ||= @obj.pages.select do |page|
page.html? || page.url.end_with?("/")
end
end
def collections
@site_collections ||= @obj.collections.values.sort_by(&:label).map(&:to_liquid)
end
# `Site#documents` cannot be memoized so that `Site#docs_to_write` can access the
# latest state of the attribute.
#
# Since this method will be called after `Site#pre_render` hook, the `Site#documents`
# array shouldn't thereafter change and can therefore be safely memoized to prevent
# additional computation of `Site#documents`.
def documents
@documents ||= @obj.documents
end
# `{{ site.related_posts }}` is how posts can get posts related to
# them, either through LSI if it's enabled, or through the most
# recent posts.
# We should remove this in 4.0 and switch to `{{ post.related_posts }}`.
def related_posts
return nil unless @current_document.is_a?(Jekyll::Document)
@current_document.related_posts
end
attr_writer :current_document
# return nil for `{{ site.config }}` even if --config was passed via CLI
def config; end
end
end
end
@@ -0,0 +1,14 @@
# frozen_string_literal: true
module Jekyll
module Drops
class StaticFileDrop < Drop
extend Forwardable
delegate_methods :name, :extname, :modified_time, :basename
delegate_method_as :relative_path, :path
delegate_method_as :type, :collection
private delegate_method_as :data, :fallback_data
end
end
end
@@ -0,0 +1,39 @@
# frozen_string_literal: true
module Jekyll
module Drops
class ThemeDrop < Drop
delegate_method_as :runtime_dependencies, :dependencies
def root
@root ||= ENV["JEKYLL_ENV"] == "development" ? @obj.root : ""
end
def authors
@authors ||= gemspec.authors.join(", ")
end
def version
@version ||= gemspec.version.to_s
end
def description
@description ||= gemspec.description || gemspec.summary
end
def metadata
@metadata ||= gemspec.metadata
end
private
def gemspec
@gemspec ||= @obj.send(:gemspec)
end
def fallback_data
@fallback_data ||= {}
end
end
end
end
@@ -0,0 +1,30 @@
# frozen_string_literal: true
module Jekyll
module Drops
class UnifiedPayloadDrop < Drop
mutable true
attr_accessor :content, :page, :layout, :paginator,
:highlighter_prefix, :highlighter_suffix
def jekyll
JekyllDrop.global
end
def site
@site_drop ||= SiteDrop.new(@obj)
end
def theme
@theme_drop ||= ThemeDrop.new(@obj.theme) if @obj.theme
end
private
def fallback_data
@fallback_data ||= {}
end
end
end
end
@@ -0,0 +1,140 @@
# frozen_string_literal: true
module Jekyll
module Drops
class UrlDrop < Drop
extend Forwardable
mutable false
delegate_method :output_ext
delegate_method_as :cleaned_relative_path, :path
def collection
@obj.collection.label
end
def name
Utils.slugify(@obj.basename_without_ext)
end
def title
Utils.slugify(@obj.data["slug"], :mode => "pretty", :cased => true) ||
Utils.slugify(@obj.basename_without_ext, :mode => "pretty", :cased => true)
end
def slug
Utils.slugify(@obj.data["slug"]) || Utils.slugify(@obj.basename_without_ext)
end
def categories
category_set = Set.new
Array(@obj.data["categories"]).each do |category|
category_set << category.to_s.downcase
end
category_set.to_a.join("/")
end
# Similar to output from #categories, but each category will be downcased and
# all non-alphanumeric characters of the category replaced with a hyphen.
def slugified_categories
Array(@obj.data["categories"]).each_with_object(Set.new) do |category, set|
set << Utils.slugify(category.to_s)
end.to_a.join("/")
end
# CCYY
def year
@obj.date.strftime("%Y")
end
# MM: 01..12
def month
@obj.date.strftime("%m")
end
# DD: 01..31
def day
@obj.date.strftime("%d")
end
# hh: 00..23
def hour
@obj.date.strftime("%H")
end
# mm: 00..59
def minute
@obj.date.strftime("%M")
end
# ss: 00..59
def second
@obj.date.strftime("%S")
end
# D: 1..31
def i_day
@obj.date.strftime("%-d")
end
# M: 1..12
def i_month
@obj.date.strftime("%-m")
end
# MMM: Jan..Dec
def short_month
@obj.date.strftime("%b")
end
# MMMM: January..December
def long_month
@obj.date.strftime("%B")
end
# YY: 00..99
def short_year
@obj.date.strftime("%y")
end
# CCYYw, ISO week year
# may differ from CCYY for the first days of January and last days of December
def w_year
@obj.date.strftime("%G")
end
# WW: 01..53
# %W and %U do not comply with ISO 8601-1
def week
@obj.date.strftime("%V")
end
# d: 1..7 (Monday..Sunday)
def w_day
@obj.date.strftime("%u")
end
# dd: Mon..Sun
def short_day
@obj.date.strftime("%a")
end
# ddd: Monday..Sunday
def long_day
@obj.date.strftime("%A")
end
# DDD: 001..366
def y_day
@obj.date.strftime("%j")
end
private
def fallback_data
@fallback_data ||= {}
end
end
end
end
@@ -0,0 +1,117 @@
# frozen_string_literal: true
module Jekyll
class EntryFilter
attr_reader :site
SPECIAL_LEADING_CHAR_REGEX = %r!\A#{Regexp.union([".", "_", "#", "~"])}!o.freeze
def initialize(site, base_directory = nil)
@site = site
@base_directory = derive_base_directory(
@site, base_directory.to_s.dup
)
end
def base_directory
@base_directory.to_s
end
def derive_base_directory(site, base_dir)
base_dir[site.source] = "" if base_dir.start_with?(site.source)
base_dir
end
def relative_to_source(entry)
File.join(
base_directory, entry
)
end
def filter(entries)
entries.reject do |e|
# Reject this entry if it is just a "dot" representation.
# e.g.: '.', '..', '_movies/.', 'music/..', etc
next true if e.end_with?(".")
# Check if the current entry is explicitly included and cache the result
included = included?(e)
# Reject current entry if it is excluded but not explicitly included as well.
next true if excluded?(e) && !included
# Reject current entry if it is a symlink.
next true if symlink?(e)
# Do not reject current entry if it is explicitly included.
next false if included
# Reject current entry if it is special or a backup file.
special?(e) || backup?(e)
end
end
def included?(entry)
glob_include?(site.include, entry) ||
glob_include?(site.include, File.basename(entry))
end
def special?(entry)
SPECIAL_LEADING_CHAR_REGEX.match?(entry) ||
SPECIAL_LEADING_CHAR_REGEX.match?(File.basename(entry))
end
def backup?(entry)
entry.end_with?("~")
end
def excluded?(entry)
glob_include?(site.exclude - site.include, relative_to_source(entry)).tap do |excluded|
if excluded
Jekyll.logger.debug(
"EntryFilter:",
"excluded #{relative_to_source(entry)}"
)
end
end
end
# --
# Check if a file is a symlink.
# NOTE: This can be converted to allowing even in safe,
# since we use Pathutil#in_path? now.
# --
def symlink?(entry)
site.safe && File.symlink?(entry) && symlink_outside_site_source?(entry)
end
# --
# Check if given path is outside of current site's configured source directory.
# --
def symlink_outside_site_source?(entry)
!File.realpath(entry).start_with?(site.in_source_dir)
end
# Check if an entry matches a specific pattern.
# Returns true if path matches against any glob pattern, else false.
def glob_include?(enumerator, entry)
entry_with_source = PathManager.join(site.source, entry)
entry_is_directory = File.directory?(entry_with_source)
enumerator.any? do |pattern|
case pattern
when String
pattern_with_source = PathManager.join(site.source, pattern)
File.fnmatch?(pattern_with_source, entry_with_source) ||
entry_with_source.start_with?(pattern_with_source) ||
(pattern_with_source == "#{entry_with_source}/" if entry_is_directory)
when Regexp
pattern.match?(entry_with_source)
else
false
end
end
end
end
end
@@ -0,0 +1,20 @@
# frozen_string_literal: true
module Jekyll
module Errors
FatalException = Class.new(::RuntimeError)
InvalidThemeName = Class.new(FatalException)
DropMutationException = Class.new(FatalException)
InvalidPermalinkError = Class.new(FatalException)
InvalidYAMLFrontMatterError = Class.new(FatalException)
MissingDependencyException = Class.new(FatalException)
InvalidDateError = Class.new(FatalException)
InvalidPostNameError = Class.new(FatalException)
PostURLError = Class.new(FatalException)
InvalidURLError = Class.new(FatalException)
InvalidConfigurationError = Class.new(FatalException)
end
end
+200
View File
@@ -0,0 +1,200 @@
# frozen_string_literal: true
module Jekyll
class Excerpt
extend Forwardable
attr_accessor :content, :doc, :ext
attr_writer :output
def_delegators :@doc,
:site, :name, :ext, :extname,
:collection, :related_posts, :type,
:coffeescript_file?, :yaml_file?,
:url, :next_doc, :previous_doc
private :coffeescript_file?, :yaml_file?
# Initialize this Excerpt instance.
#
# doc - The Document.
#
# Returns the new Excerpt.
def initialize(doc)
self.doc = doc
self.content = extract_excerpt(doc.content)
end
# Fetch YAML front-matter data from related doc, without layout key
#
# Returns Hash of doc data
def data
@data ||= doc.data.dup
@data.delete("layout")
@data.delete("excerpt")
@data
end
def trigger_hooks(*); end
# 'Path' of the excerpt.
#
# Returns the path for the doc this excerpt belongs to with #excerpt appended
def path
File.join(doc.path, "#excerpt")
end
# 'Relative Path' of the excerpt.
#
# Returns the relative_path for the doc this excerpt belongs to with #excerpt appended
def relative_path
@relative_path ||= File.join(doc.relative_path, "#excerpt")
end
# Check if excerpt includes a string
#
# Returns true if the string passed in
def include?(something)
output&.include?(something) || content.include?(something)
end
# The UID for this doc (useful in feeds).
# e.g. /2008/11/05/my-awesome-doc
#
# Returns the String UID.
def id
"#{doc.id}#excerpt"
end
def to_s
output || content
end
def to_liquid
Jekyll::Drops::ExcerptDrop.new(self)
end
# Returns the shorthand String identifier of this doc.
def inspect
"<#{self.class} id=#{id}>"
end
def output
@output ||= Renderer.new(doc.site, self, site.site_payload).run
end
def place_in_layout?
false
end
def render_with_liquid?
return false if data["render_with_liquid"] == false
!(coffeescript_file? || yaml_file? || !Utils.has_liquid_construct?(content))
end
protected
# Internal: Extract excerpt from the content
#
# By default excerpt is your first paragraph of a doc: everything before
# the first two new lines:
#
# ---
# title: Example
# ---
#
# First paragraph with [link][1].
#
# Second paragraph.
#
# [1]: http://example.com/
#
# This is fairly good option for Markdown and Textile files. But might cause
# problems for HTML docs (which is quite unusual for Jekyll). If default
# excerpt delimiter is not good for you, you might want to set your own via
# configuration option `excerpt_separator`. For example, following is a good
# alternative for HTML docs:
#
# # file: _config.yml
# excerpt_separator: "<!-- more -->"
#
# Notice that all markdown-style link references will be appended to the
# excerpt. So the example doc above will have this excerpt source:
#
# First paragraph with [link][1].
#
# [1]: http://example.com/
#
# Excerpts are rendered same time as content is rendered.
#
# Returns excerpt String
LIQUID_TAG_REGEX = %r!{%-?\s*(\w+)\s*.*?-?%}!m.freeze
MKDWN_LINK_REF_REGEX = %r!^ {0,3}(?:(\[[^\]]+\])(:.+))$!.freeze
def extract_excerpt(doc_content)
head, _, tail = doc_content.to_s.partition(doc.excerpt_separator)
return head if tail.empty?
head = sanctify_liquid_tags(head) if head.include?("{%")
definitions = extract_markdown_link_reference_definitions(head, tail)
return head if definitions.empty?
head << "\n\n" << definitions.join("\n")
end
private
# append appropriate closing tag(s) (for each Liquid block), to the `head` if the
# partitioning resulted in leaving the closing tag somewhere in the `tail` partition.
def sanctify_liquid_tags(head)
modified = false
tag_names = head.scan(LIQUID_TAG_REGEX)
tag_names.flatten!
tag_names.reverse_each do |tag_name|
next unless liquid_block?(tag_name)
next if endtag_regex_stash(tag_name).match?(head)
modified = true
head << "\n{% end#{tag_name} %}"
end
print_build_warning if modified
head
end
def extract_markdown_link_reference_definitions(head, tail)
[].tap do |definitions|
tail.scan(MKDWN_LINK_REF_REGEX).each do |segments|
definitions << segments.join if head.include?(segments[0])
end
end
end
def endtag_regex_stash(tag_name)
@endtag_regex_stash ||= {}
@endtag_regex_stash[tag_name] ||= %r!{%-?\s*end#{tag_name}.*?\s*-?%}!m
end
def liquid_block?(tag_name)
return false unless tag_name.is_a?(String)
return false unless Liquid::Template.tags[tag_name]
Liquid::Template.tags[tag_name].ancestors.include?(Liquid::Block)
rescue NoMethodError
Jekyll.logger.error "Error:",
"A Liquid tag in the excerpt of #{doc.relative_path} couldn't be parsed."
raise
end
def print_build_warning
Jekyll.logger.warn "Warning:", "Excerpt modified in #{doc.relative_path}!"
Jekyll.logger.warn "", "Found a Liquid block containing the excerpt separator " \
"#{doc.excerpt_separator.inspect}."
Jekyll.logger.warn "", "The block has been modified with the appropriate closing tag."
Jekyll.logger.warn "", "Feel free to define a custom excerpt or excerpt_separator in the"
Jekyll.logger.warn "", "document's Front Matter if the generated excerpt is unsatisfactory."
end
end
end
@@ -0,0 +1,75 @@
# frozen_string_literal: true
module Jekyll
module External
class << self
#
# Gems that, if installed, should be loaded.
# Usually contain subcommands.
#
def blessed_gems
%w(
jekyll-compose
jekyll-docs
jekyll-import
)
end
#
# Require a gem or file if it's present, otherwise silently fail.
#
# names - a string gem name or array of gem names
#
def require_if_present(names)
Array(names).each do |name|
require name
rescue LoadError
Jekyll.logger.debug "Couldn't load #{name}. Skipping."
yield(name, version_constraint(name)) if block_given?
false
end
end
#
# The version constraint required to activate a given gem.
# Usually the gem version requirement is "> 0," because any version
# will do. In the case of jekyll-docs, however, we require the exact
# same version as Jekyll.
#
# Returns a String version constraint in a parseable form for
# RubyGems.
def version_constraint(gem_name)
return "= #{Jekyll::VERSION}" if gem_name.to_s.eql?("jekyll-docs")
"> 0"
end
#
# Require a gem or gems. If it's not present, show a very nice error
# message that explains everything and is much more helpful than the
# normal LoadError.
#
# names - a string gem name or array of gem names
#
def require_with_graceful_fail(names)
Array(names).each do |name|
Jekyll.logger.debug "Requiring:", name.to_s
require name
rescue LoadError => e
Jekyll.logger.error "Dependency Error:", <<~MSG
Yikes! It looks like you don't have #{name} or one of its dependencies installed.
In order to use Jekyll as currently configured, you'll need to install this gem.
If you've run Jekyll with `bundle exec`, ensure that you have included the #{name}
gem in your Gemfile as well.
The full error message from Ruby is: '#{e.message}'
If you run into trouble, you can find helpful resources at https://jekyllrb.com/help/!
MSG
raise Jekyll::Errors::MissingDependencyException, name
end
end
end
end
end
+540
View File
@@ -0,0 +1,540 @@
# frozen_string_literal: true
require_all "jekyll/filters"
module Jekyll
module Filters
include URLFilters
include GroupingFilters
include DateFilters
# Convert a Markdown string into HTML output.
#
# input - The Markdown String to convert.
#
# Returns the HTML formatted String.
def markdownify(input)
@context.registers[:site].find_converter_instance(
Jekyll::Converters::Markdown
).convert(input.to_s)
end
# Convert quotes into smart quotes.
#
# input - The String to convert.
#
# Returns the smart-quotified String.
def smartify(input)
@context.registers[:site].find_converter_instance(
Jekyll::Converters::SmartyPants
).convert(input.to_s)
end
# Convert a Sass string into CSS output.
#
# input - The Sass String to convert.
#
# Returns the CSS formatted String.
def sassify(input)
@context.registers[:site].find_converter_instance(
Jekyll::Converters::Sass
).convert(input)
end
# Convert a Scss string into CSS output.
#
# input - The Scss String to convert.
#
# Returns the CSS formatted String.
def scssify(input)
@context.registers[:site].find_converter_instance(
Jekyll::Converters::Scss
).convert(input)
end
# Slugify a filename or title.
#
# input - The filename or title to slugify.
# mode - how string is slugified
#
# Returns the given filename or title as a lowercase URL String.
# See Utils.slugify for more detail.
def slugify(input, mode = nil)
Utils.slugify(input, :mode => mode)
end
# XML escape a string for use. Replaces any special characters with
# appropriate HTML entity replacements.
#
# input - The String to escape.
#
# Examples
#
# xml_escape('foo "bar" <baz>')
# # => "foo &quot;bar&quot; &lt;baz&gt;"
#
# Returns the escaped String.
def xml_escape(input)
input.to_s.encode(:xml => :attr).gsub(%r!\A"|"\Z!, "")
end
# CGI escape a string for use in a URL. Replaces any special characters
# with appropriate %XX replacements.
#
# input - The String to escape.
#
# Examples
#
# cgi_escape('foo,bar;baz?')
# # => "foo%2Cbar%3Bbaz%3F"
#
# Returns the escaped String.
def cgi_escape(input)
CGI.escape(input)
end
# URI escape a string.
#
# input - The String to escape.
#
# Examples
#
# uri_escape('foo, bar \\baz?')
# # => "foo,%20bar%20%5Cbaz?"
#
# Returns the escaped String.
def uri_escape(input)
Addressable::URI.normalize_component(input)
end
# Replace any whitespace in the input string with a single space
#
# input - The String on which to operate.
#
# Returns the formatted String
def normalize_whitespace(input)
input.to_s.gsub(%r!\s+!, " ").tap(&:strip!)
end
# Count the number of words in the input string.
#
# input - The String on which to operate.
#
# Returns the Integer word count.
def number_of_words(input, mode = nil)
cjk_charset = '\p{Han}\p{Katakana}\p{Hiragana}\p{Hangul}'
cjk_regex = %r![#{cjk_charset}]!o
word_regex = %r![^#{cjk_charset}\s]+!o
case mode
when "cjk"
input.scan(cjk_regex).length + input.scan(word_regex).length
when "auto"
cjk_count = input.scan(cjk_regex).length
cjk_count.zero? ? input.split.length : cjk_count + input.scan(word_regex).length
else
input.split.length
end
end
# Join an array of things into a string by separating with commas and the
# word "and" for the last one.
#
# array - The Array of Strings to join.
# connector - Word used to connect the last 2 items in the array
#
# Examples
#
# array_to_sentence_string(["apples", "oranges", "grapes"])
# # => "apples, oranges, and grapes"
#
# Returns the formatted String.
def array_to_sentence_string(array, connector = "and")
case array.length
when 0
""
when 1
array[0].to_s
when 2
"#{array[0]} #{connector} #{array[1]}"
else
"#{array[0...-1].join(", ")}, #{connector} #{array[-1]}"
end
end
# Convert the input into json string
#
# input - The Array or Hash to be converted
#
# Returns the converted json string
def jsonify(input)
as_liquid(input).to_json
end
# Filter an array of objects
#
# input - the object array.
# property - the property within each object to filter by.
# value - the desired value.
# Cannot be an instance of Array nor Hash since calling #to_s on them returns
# their `#inspect` string object.
#
# Returns the filtered array of objects
def where(input, property, value)
return input if !property || value.is_a?(Array) || value.is_a?(Hash)
return input unless input.respond_to?(:select)
input = input.values if input.is_a?(Hash)
input_id = input.hash
# implement a hash based on method parameters to cache the end-result
# for given parameters.
@where_filter_cache ||= {}
@where_filter_cache[input_id] ||= {}
@where_filter_cache[input_id][property] ||= {}
# stash or retrieve results to return
@where_filter_cache[input_id][property][value] ||= input.select do |object|
compare_property_vs_target(item_property(object, property), value)
end.to_a
end
# Filters an array of objects against an expression
#
# input - the object array
# variable - the variable to assign each item to in the expression
# expression - a Liquid comparison expression passed in as a string
#
# Returns the filtered array of objects
def where_exp(input, variable, expression)
return input unless input.respond_to?(:select)
input = input.values if input.is_a?(Hash) # FIXME
condition = parse_condition(expression)
@context.stack do
input.select do |object|
@context[variable] = object
condition.evaluate(@context)
end
end || []
end
# Search an array of objects and returns the first object that has the queried attribute
# with the given value or returns nil otherwise.
#
# input - the object array.
# property - the property within each object to search by.
# value - the desired value.
# Cannot be an instance of Array nor Hash since calling #to_s on them returns
# their `#inspect` string object.
#
# Returns the found object or nil
#
# rubocop:disable Metrics/CyclomaticComplexity
def find(input, property, value)
return input if !property || value.is_a?(Array) || value.is_a?(Hash)
return input unless input.respond_to?(:find)
input = input.values if input.is_a?(Hash)
input_id = input.hash
# implement a hash based on method parameters to cache the end-result for given parameters.
@find_filter_cache ||= {}
@find_filter_cache[input_id] ||= {}
@find_filter_cache[input_id][property] ||= {}
# stash or retrieve results to return
# Since `enum.find` can return nil or false, we use a placeholder string "<__NO MATCH__>"
# to validate caching.
result = @find_filter_cache[input_id][property][value] ||= input.find do |object|
compare_property_vs_target(item_property(object, property), value)
end || "<__NO MATCH__>"
return nil if result == "<__NO MATCH__>"
result
end
# rubocop:enable Metrics/CyclomaticComplexity
# Searches an array of objects against an expression and returns the first object for which
# the expression evaluates to true, or returns nil otherwise.
#
# input - the object array
# variable - the variable to assign each item to in the expression
# expression - a Liquid comparison expression passed in as a string
#
# Returns the found object or nil
def find_exp(input, variable, expression)
return input unless input.respond_to?(:find)
input = input.values if input.is_a?(Hash)
condition = parse_condition(expression)
@context.stack do
input.find do |object|
@context[variable] = object
condition.evaluate(@context)
end
end
end
# Convert the input into integer
#
# input - the object string
#
# Returns the integer value
def to_integer(input)
return 1 if input == true
return 0 if input == false
input.to_i
end
# Sort an array of objects
#
# input - the object array
# property - property within each object to filter by
# nils ('first' | 'last') - nils appear before or after non-nil values
#
# Returns the filtered array of objects
def sort(input, property = nil, nils = "first")
raise ArgumentError, "Cannot sort a null object." if input.nil?
if property.nil?
input.sort
else
case nils
when "first"
order = - 1
when "last"
order = + 1
else
raise ArgumentError, "Invalid nils order: " \
"'#{nils}' is not a valid nils order. It must be 'first' or 'last'."
end
sort_input(input, property, order)
end
end
def pop(array, num = 1)
return array unless array.is_a?(Array)
num = Liquid::Utils.to_integer(num)
new_ary = array.dup
new_ary.pop(num)
new_ary
end
def push(array, input)
return array unless array.is_a?(Array)
new_ary = array.dup
new_ary.push(input)
new_ary
end
def shift(array, num = 1)
return array unless array.is_a?(Array)
num = Liquid::Utils.to_integer(num)
new_ary = array.dup
new_ary.shift(num)
new_ary
end
def unshift(array, input)
return array unless array.is_a?(Array)
new_ary = array.dup
new_ary.unshift(input)
new_ary
end
def sample(input, num = 1)
return input unless input.respond_to?(:sample)
num = Liquid::Utils.to_integer(num) rescue 1
if num == 1
input.sample
else
input.sample(num)
end
end
# Convert an object into its String representation for debugging
#
# input - The Object to be converted
#
# Returns a String representation of the object.
def inspect(input)
xml_escape(input.inspect)
end
private
# Sort the input Enumerable by the given property.
# If the property doesn't exist, return the sort order respective of
# which item doesn't have the property.
# We also utilize the Schwartzian transform to make this more efficient.
def sort_input(input, property, order)
input.map { |item| [item_property(item, property), item] }
.sort! do |a_info, b_info|
a_property = a_info.first
b_property = b_info.first
if !a_property.nil? && b_property.nil?
- order
elsif a_property.nil? && !b_property.nil?
+ order
else
a_property <=> b_property || a_property.to_s <=> b_property.to_s
end
end
.map!(&:last)
end
# `where` filter helper
#
def compare_property_vs_target(property, target)
case target
when NilClass
return true if property.nil?
when Liquid::Expression::MethodLiteral # `empty` or `blank`
target = target.to_s
return true if property == target || Array(property).join == target
else
target = target.to_s
if property.is_a? String
return true if property == target
else
Array(property).each do |prop|
return true if prop.to_s == target
end
end
end
false
end
def item_property(item, property)
@item_property_cache ||= @context.registers[:site].filter_cache[:item_property] ||= {}
@item_property_cache[property] ||= {}
@item_property_cache[property][item] ||= begin
property = property.to_s
property = if item.respond_to?(:to_liquid)
read_liquid_attribute(item.to_liquid, property)
elsif item.respond_to?(:data)
item.data[property]
else
item[property]
end
parse_sort_input(property)
end
end
def read_liquid_attribute(liquid_data, property)
return liquid_data[property] unless property.include?(".")
property.split(".").reduce(liquid_data) do |data, key|
data.respond_to?(:[]) && data[key]
end
rescue TypeError => e
msg = if liquid_data.is_a?(Array)
"Error accessing object (#{liquid_data.to_s[0...20]}) with given key. Expected an " \
"integer but got #{property.inspect} instead."
else
e.message
end
raise e, msg
end
FLOAT_LIKE = %r!\A\s*-?(?:\d+\.?\d*|\.\d+)\s*\Z!.freeze
INTEGER_LIKE = %r!\A\s*-?\d+\s*\Z!.freeze
private_constant :FLOAT_LIKE, :INTEGER_LIKE
# return numeric values as numbers for proper sorting
def parse_sort_input(property)
stringified = property.to_s
return property.to_i if INTEGER_LIKE.match?(stringified)
return property.to_f if FLOAT_LIKE.match?(stringified)
property
end
def as_liquid(item)
case item
when Hash
item.each_with_object({}) { |(k, v), result| result[as_liquid(k)] = as_liquid(v) }
when Array
item.map { |i| as_liquid(i) }
else
if item.respond_to?(:to_liquid)
liquidated = item.to_liquid
# prevent infinite recursion for simple types (which return `self`)
if liquidated == item
item
else
as_liquid(liquidated)
end
else
item
end
end
end
# ----------- The following set of code was *adapted* from Liquid::If
# ----------- ref: https://github.com/Shopify/liquid/blob/ffb0ace30315bbcf3548a0383fab531452060ae8/lib/liquid/tags/if.rb#L84-L107
# Parse a string to a Liquid Condition
def parse_condition(exp)
parser = Liquid::Parser.new(exp)
condition = parse_binary_comparison(parser)
parser.consume(:end_of_string)
condition
end
# Generate a Liquid::Condition object from a Liquid::Parser object additionally processing
# the parsed expression based on whether the expression consists of binary operations with
# Liquid operators `and` or `or`
#
# - parser: an instance of Liquid::Parser
#
# Returns an instance of Liquid::Condition
def parse_binary_comparison(parser)
condition = parse_comparison(parser)
first_condition = condition
while (binary_operator = parser.id?("and") || parser.id?("or"))
child_condition = parse_comparison(parser)
condition.send(binary_operator, child_condition)
condition = child_condition
end
first_condition
end
# Generates a Liquid::Condition object from a Liquid::Parser object based on whether the parsed
# expression involves a "comparison" operator (e.g. <, ==, >, !=, etc)
#
# - parser: an instance of Liquid::Parser
#
# Returns an instance of Liquid::Condition
def parse_comparison(parser)
left_operand = Liquid::Expression.parse(parser.expression)
operator = parser.consume?(:comparison)
# No comparison-operator detected. Initialize a Liquid::Condition using only left operand
return Liquid::Condition.new(left_operand) unless operator
# Parse what remained after extracting the left operand and the `:comparison` operator
# and initialize a Liquid::Condition object using the operands and the comparison-operator
Liquid::Condition.new(left_operand, operator, Liquid::Expression.parse(parser.expression))
end
end
end
Liquid::Template.register_filter(
Jekyll::Filters
)
@@ -0,0 +1,110 @@
# frozen_string_literal: true
module Jekyll
module Filters
module DateFilters
# Format a date in short format e.g. "27 Jan 2011".
# Ordinal format is also supported, in both the UK
# (e.g. "27th Jan 2011") and US ("e.g. Jan 27th, 2011") formats.
# UK format is the default.
#
# date - the Time to format.
# type - if "ordinal" the returned String will be in ordinal format
# style - if "US" the returned String will be in US format.
# Otherwise it will be in UK format.
#
# Returns the formatting String.
def date_to_string(date, type = nil, style = nil)
stringify_date(date, "%b", type, style)
end
# Format a date in long format e.g. "27 January 2011".
# Ordinal format is also supported, in both the UK
# (e.g. "27th January 2011") and US ("e.g. January 27th, 2011") formats.
# UK format is the default.
#
# date - the Time to format.
# type - if "ordinal" the returned String will be in ordinal format
# style - if "US" the returned String will be in US format.
# Otherwise it will be in UK format.
#
# Returns the formatted String.
def date_to_long_string(date, type = nil, style = nil)
stringify_date(date, "%B", type, style)
end
# Format a date for use in XML.
#
# date - The Time to format.
#
# Examples
#
# date_to_xmlschema(Time.now)
# # => "2011-04-24T20:34:46+08:00"
#
# Returns the formatted String.
def date_to_xmlschema(date)
return date if date.to_s.empty?
time(date).xmlschema
end
# Format a date according to RFC-822
#
# date - The Time to format.
#
# Examples
#
# date_to_rfc822(Time.now)
# # => "Sun, 24 Apr 2011 12:34:46 +0000"
#
# Returns the formatted String.
def date_to_rfc822(date)
return date if date.to_s.empty?
time(date).rfc822
end
private
# month_type: Notations that evaluate to 'Month' via `Time#strftime` ("%b", "%B")
# type: nil (default) or "ordinal"
# style: nil (default) or "US"
#
# Returns a stringified date or the empty input.
def stringify_date(date, month_type, type = nil, style = nil)
return date if date.to_s.empty?
time = time(date)
if type == "ordinal"
day = time.day
ordinal_day = "#{day}#{ordinal(day)}"
return time.strftime("#{month_type} #{ordinal_day}, %Y") if style == "US"
return time.strftime("#{ordinal_day} #{month_type} %Y")
end
time.strftime("%d #{month_type} %Y")
end
def ordinal(number)
return "th" if (11..13).cover?(number)
case number % 10
when 1 then "st"
when 2 then "nd"
when 3 then "rd"
else "th"
end
end
def time(input)
date = Liquid::Utils.to_date(input)
unless date.respond_to?(:to_time)
raise Errors::InvalidDateError,
"Invalid Date: '#{input.inspect}' is not a valid datetime."
end
date.to_time.dup.localtime
end
end
end
end
@@ -0,0 +1,64 @@
# frozen_string_literal: true
module Jekyll
module Filters
module GroupingFilters
# Group an array of items by a property
#
# input - the inputted Enumerable
# property - the property
#
# Returns an array of Hashes, each looking something like this:
# {"name" => "larry"
# "items" => [...] } # all the items where `property` == "larry"
def group_by(input, property)
if groupable?(input)
groups = input.group_by { |item| item_property(item, property).to_s }
grouped_array(groups)
else
input
end
end
# Group an array of items by an expression
#
# input - the object array
# variable - the variable to assign each item to in the expression
# expression -a Liquid comparison expression passed in as a string
#
# Returns the filtered array of objects
def group_by_exp(input, variable, expression)
return input unless groupable?(input)
parsed_expr = parse_expression(expression)
@context.stack do
groups = input.group_by do |item|
@context[variable] = item
parsed_expr.render(@context)
end
grouped_array(groups)
end
end
private
def parse_expression(str)
Liquid::Variable.new(str, Liquid::ParseContext.new)
end
def groupable?(element)
element.respond_to?(:group_by)
end
def grouped_array(groups)
groups.each_with_object([]) do |item, array|
array << {
"name" => item.first,
"items" => item.last,
"size" => item.last.size,
}
end
end
end
end
end
@@ -0,0 +1,98 @@
# frozen_string_literal: true
module Jekyll
module Filters
module URLFilters
# Produces an absolute URL based on site.url and site.baseurl.
#
# input - the URL to make absolute.
#
# Returns the absolute URL as a String.
def absolute_url(input)
return if input.nil?
cache = if input.is_a?(String)
(@context.registers[:site].filter_cache[:absolute_url] ||= {})
else
(@context.registers[:cached_absolute_url] ||= {})
end
cache[input] ||= compute_absolute_url(input)
# Duplicate cached string so that the cached value is never mutated by
# a subsequent filter.
cache[input].dup
end
# Produces a URL relative to the domain root based on site.baseurl
# unless it is already an absolute url with an authority (host).
#
# input - the URL to make relative to the domain root
#
# Returns a URL relative to the domain root as a String.
def relative_url(input)
return if input.nil?
cache = if input.is_a?(String)
(@context.registers[:site].filter_cache[:relative_url] ||= {})
else
(@context.registers[:cached_relative_url] ||= {})
end
cache[input] ||= compute_relative_url(input)
# Duplicate cached string so that the cached value is never mutated by
# a subsequent filter.
cache[input].dup
end
# Strips trailing `/index.html` from URLs to create pretty permalinks
#
# input - the URL with a possible `/index.html`
#
# Returns a URL with the trailing `/index.html` removed
def strip_index(input)
return if input.nil? || input.to_s.empty?
input.sub(%r!/index\.html?$!, "/")
end
private
def compute_absolute_url(input)
input = input.url if input.respond_to?(:url)
return input if Addressable::URI.parse(input.to_s).absolute?
site = @context.registers[:site]
site_url = site.config["url"]
return relative_url(input) if site_url.nil? || site_url == ""
Addressable::URI.parse(
site_url.to_s + relative_url(input)
).normalize.to_s
end
def compute_relative_url(input)
input = input.url if input.respond_to?(:url)
return input if Addressable::URI.parse(input.to_s).absolute?
parts = [sanitized_baseurl, input]
Addressable::URI.parse(
parts.map! { |part| ensure_leading_slash(part.to_s) }.join
).normalize.to_s
end
def sanitized_baseurl
site = @context.registers[:site]
baseurl = site.config["baseurl"]
return "" if baseurl.nil?
baseurl.to_s.chomp("/")
end
def ensure_leading_slash(input)
return input if input.nil? || input.empty? || input.start_with?("/")
"/#{input}"
end
end
end
end
@@ -0,0 +1,238 @@
# frozen_string_literal: true
module Jekyll
# This class handles custom defaults for YAML frontmatter settings.
# These are set in _config.yml and apply both to internal use (e.g. layout)
# and the data available to liquid.
#
# It is exposed via the frontmatter_defaults method on the site class.
class FrontmatterDefaults
# Initializes a new instance.
def initialize(site)
@site = site
end
def reset
@glob_cache = {} if @glob_cache
end
def update_deprecated_types(set)
return set unless set.key?("scope") && set["scope"].key?("type")
set["scope"]["type"] =
case set["scope"]["type"]
when "page"
Deprecator.defaults_deprecate_type("page", "pages")
"pages"
when "post"
Deprecator.defaults_deprecate_type("post", "posts")
"posts"
when "draft"
Deprecator.defaults_deprecate_type("draft", "drafts")
"drafts"
else
set["scope"]["type"]
end
set
end
def ensure_time!(set)
return set unless set.key?("values") && set["values"].key?("date")
return set if set["values"]["date"].is_a?(Time)
set["values"]["date"] = Utils.parse_date(
set["values"]["date"],
"An invalid date format was found in a front-matter default set: #{set}"
)
set
end
# Finds a default value for a given setting, filtered by path and type
#
# path - the path (relative to the source) of the page,
# post or :draft the default is used in
# type - a symbol indicating whether a :page,
# a :post or a :draft calls this method
#
# Returns the default value or nil if none was found
def find(path, type, setting)
value = nil
old_scope = nil
matching_sets(path, type).each do |set|
if set["values"].key?(setting) && has_precedence?(old_scope, set["scope"])
value = set["values"][setting]
old_scope = set["scope"]
end
end
value
end
# Collects a hash with all default values for a page or post
#
# path - the relative path of the page or post
# type - a symbol indicating the type (:post, :page or :draft)
#
# Returns a hash with all default values (an empty hash if there are none)
def all(path, type)
defaults = {}
old_scope = nil
matching_sets(path, type).each do |set|
if has_precedence?(old_scope, set["scope"])
defaults = Utils.deep_merge_hashes(defaults, set["values"])
old_scope = set["scope"]
else
defaults = Utils.deep_merge_hashes(set["values"], defaults)
end
end
defaults
end
private
# Checks if a given default setting scope matches the given path and type
#
# scope - the hash indicating the scope, as defined in _config.yml
# path - the path to check for
# type - the type (:post, :page or :draft) to check for
#
# Returns true if the scope applies to the given type and path
def applies?(scope, path, type)
applies_type?(scope, type) && applies_path?(scope, path)
end
def applies_path?(scope, path)
rel_scope_path = scope["path"]
return true if !rel_scope_path.is_a?(String) || rel_scope_path.empty?
sanitized_path = sanitize_path(path)
if rel_scope_path.include?("*")
glob_scope(sanitized_path, rel_scope_path)
else
path_is_subpath?(sanitized_path, strip_collections_dir(rel_scope_path))
end
end
def glob_scope(sanitized_path, rel_scope_path)
site_source = Pathname.new(@site.source)
abs_scope_path = site_source.join(rel_scope_path).to_s
glob_cache(abs_scope_path).each do |scope_path|
scope_path = Pathname.new(scope_path).relative_path_from(site_source).to_s
scope_path = strip_collections_dir(scope_path)
Jekyll.logger.debug "Globbed Scope Path:", scope_path
return true if path_is_subpath?(sanitized_path, scope_path)
end
false
end
def glob_cache(path)
@glob_cache ||= {}
@glob_cache[path] ||= Dir.glob(path)
end
def path_is_subpath?(path, parent_path)
path.start_with?(parent_path)
end
def strip_collections_dir(path)
collections_dir = @site.config["collections_dir"]
slashed_coll_dir = collections_dir.empty? ? "/" : "#{collections_dir}/"
return path if collections_dir.empty? || !path.to_s.start_with?(slashed_coll_dir)
path.sub(slashed_coll_dir, "")
end
# Determines whether the scope applies to type.
# The scope applies to the type if:
# 1. no 'type' is specified
# 2. the 'type' in the scope is the same as the type asked about
#
# scope - the Hash defaults set being asked about application
# type - the type of the document being processed / asked about
# its defaults.
#
# Returns true if either of the above conditions are satisfied,
# otherwise returns false
def applies_type?(scope, type)
!scope.key?("type") || type&.to_sym.eql?(scope["type"].to_sym)
end
# Checks if a given set of default values is valid
#
# set - the default value hash, as defined in _config.yml
#
# Returns true if the set is valid and can be used in this class
def valid?(set)
set.is_a?(Hash) && set["values"].is_a?(Hash)
end
# Determines if a new scope has precedence over an old one
#
# old_scope - the old scope hash, or nil if there's none
# new_scope - the new scope hash
#
# Returns true if the new scope has precedence over the older
# rubocop: disable Naming/PredicateName
def has_precedence?(old_scope, new_scope)
return true if old_scope.nil?
new_path = sanitize_path(new_scope["path"])
old_path = sanitize_path(old_scope["path"])
if new_path.length != old_path.length
new_path.length >= old_path.length
elsif new_scope.key?("type")
true
else
!old_scope.key? "type"
end
end
# rubocop: enable Naming/PredicateName
# Collects a list of sets that match the given path and type
#
# Returns an array of hashes
def matching_sets(path, type)
@matched_set_cache ||= {}
@matched_set_cache[path] ||= {}
@matched_set_cache[path][type] ||= valid_sets.select do |set|
!set.key?("scope") || applies?(set["scope"], path, type)
end
end
# Returns a list of valid sets
#
# This is not cached to allow plugins to modify the configuration
# and have their changes take effect
#
# Returns an array of hashes
def valid_sets
sets = @site.config["defaults"]
return [] unless sets.is_a?(Array)
sets.map do |set|
if valid?(set)
ensure_time!(update_deprecated_types(set))
else
Jekyll.logger.warn "Defaults:", "An invalid front-matter default set was found:"
Jekyll.logger.warn set.to_s
nil
end
end.tap(&:compact!)
end
# Sanitizes the given path by removing a leading slash
def sanitize_path(path)
if path.nil? || path.empty?
""
elsif path.start_with?("/")
path.gsub(%r!\A/|(?<=[^/])\z!, "")
else
path
end
end
end
end
@@ -0,0 +1,5 @@
# frozen_string_literal: true
module Jekyll
Generator = Class.new(Plugin)
end
+107
View File
@@ -0,0 +1,107 @@
# frozen_string_literal: true
module Jekyll
module Hooks
DEFAULT_PRIORITY = 20
# compatibility layer for octopress-hooks users
PRIORITY_MAP = {
:low => 10,
:normal => 20,
:high => 30,
}.freeze
# initial empty hooks
@registry = {
:site => {
:after_init => [],
:after_reset => [],
:post_read => [],
:pre_render => [],
:post_render => [],
:post_write => [],
},
:pages => {
:post_init => [],
:pre_render => [],
:post_convert => [],
:post_render => [],
:post_write => [],
},
:posts => {
:post_init => [],
:pre_render => [],
:post_convert => [],
:post_render => [],
:post_write => [],
},
:documents => {
:post_init => [],
:pre_render => [],
:post_convert => [],
:post_render => [],
:post_write => [],
},
:clean => {
:on_obsolete => [],
},
}
# map of all hooks and their priorities
@hook_priority = {}
NotAvailable = Class.new(RuntimeError)
Uncallable = Class.new(RuntimeError)
# register hook(s) to be called later, public API
def self.register(owners, event, priority: DEFAULT_PRIORITY, &block)
Array(owners).each do |owner|
register_one(owner, event, priority_value(priority), &block)
end
end
# Ensure the priority is a Fixnum
def self.priority_value(priority)
return priority if priority.is_a?(Integer)
PRIORITY_MAP[priority] || DEFAULT_PRIORITY
end
# register a single hook to be called later, internal API
def self.register_one(owner, event, priority, &block)
@registry[owner] ||= {
:post_init => [],
:pre_render => [],
:post_convert => [],
:post_render => [],
:post_write => [],
}
unless @registry[owner][event]
raise NotAvailable, "Invalid hook. #{owner} supports only the following hooks " \
"#{@registry[owner].keys.inspect}"
end
raise Uncallable, "Hooks must respond to :call" unless block.respond_to? :call
insert_hook owner, event, priority, &block
end
def self.insert_hook(owner, event, priority, &block)
@hook_priority[block] = [-priority, @hook_priority.size]
@registry[owner][event] << block
end
# interface for Jekyll core components to trigger hooks
def self.trigger(owner, event, *args)
# proceed only if there are hooks to call
hooks = @registry.dig(owner, event)
return if hooks.nil? || hooks.empty?
# sort and call hooks according to priority and load order
hooks.sort_by { |h| @hook_priority[h] }.each do |hook|
hook.call(*args)
end
end
end
end
@@ -0,0 +1,32 @@
# frozen_string_literal: true
module Jekyll
class Inclusion
attr_reader :site, :name, :path
private :site
def initialize(site, base, name)
@site = site
@name = name
@path = PathManager.join(base, name)
end
def render(context)
@template ||= site.liquid_renderer.file(path).parse(content)
@template.render!(context)
rescue Liquid::Error => e
e.template_name = path
e.markup_context = "included " if e.markup_context.nil?
raise e
end
def content
@content ||= File.read(path, **site.file_read_opts)
end
def inspect
"#{self.class} #{path.inspect}"
end
alias_method :to_s, :inspect
end
end
@@ -0,0 +1,55 @@
# frozen_string_literal: true
module Jekyll
class Layout
include Convertible
attr_accessor :content, # content of layout
:data, # the Hash that holds the metadata for this layout
:ext # extension of layout
attr_reader :name, # name of layout
:path, # path to layout
:site, # the Site object
:relative_path # path to layout relative to its base
# Initialize a new Layout.
#
# site - The Site.
# base - The String path to the source.
# name - The String filename of the post file.
def initialize(site, base, name)
@site = site
@base = base
@name = name
if site.theme && site.theme.layouts_path.eql?(base)
@base_dir = site.theme.root
@path = site.in_theme_dir(base, name)
else
@base_dir = site.source
@path = site.in_source_dir(base, name)
end
@relative_path = @path.sub(@base_dir, "")
self.data = {}
process(name)
read_yaml(base, name)
end
# Extract information from the layout filename.
#
# name - The String filename of the layout file.
#
# Returns nothing.
def process(name)
self.ext = File.extname(name)
end
# Returns the object as a debug String.
def inspect
"#<#{self.class} @path=#{@path.inspect}>"
end
end
end
@@ -0,0 +1,22 @@
# frozen_string_literal: true
module Jekyll
module LiquidExtensions
# Lookup a Liquid variable in the given context.
#
# context - the Liquid context in question.
# variable - the variable name, as a string.
#
# Returns the value of the variable in the context
# or the variable name if not found.
def lookup_variable(context, variable)
lookup = context
variable.split(".").each do |value|
lookup = lookup[value]
end
lookup || variable
end
end
end
@@ -0,0 +1,80 @@
# frozen_string_literal: true
require_relative "liquid_renderer/file"
require_relative "liquid_renderer/table"
module Jekyll
class LiquidRenderer
def initialize(site)
@site = site
Liquid::Template.error_mode = @site.config["liquid"]["error_mode"].to_sym
reset
end
def reset
@stats = {}
@cache = {}
end
def file(filename)
filename = normalize_path(filename)
LiquidRenderer::File.new(self, filename).tap do
@stats[filename] ||= new_profile_hash
end
end
def increment_bytes(filename, bytes)
@stats[filename][:bytes] += bytes
end
def increment_time(filename, time)
@stats[filename][:time] += time
end
def increment_count(filename)
@stats[filename][:count] += 1
end
def stats_table(num_of_rows = 50)
LiquidRenderer::Table.new(@stats).to_s(num_of_rows)
end
def self.format_error(error, path)
"#{error.message} in #{path}"
end
# A persistent cache to store and retrieve parsed templates based on the filename
# via `LiquidRenderer::File#parse`
#
# It is emptied when `self.reset` is called.
def cache
@cache ||= {}
end
private
def normalize_path(filename)
@normalize_path ||= {}
@normalize_path[filename] ||= begin
theme_dir = @site.theme&.root
case filename
when %r!\A(#{Regexp.escape(@site.source)}/)(?<rest>.*)!io
Regexp.last_match(:rest)
when %r!(/gems/.*)*/gems/(?<dirname>[^/]+)(?<rest>.*)!,
%r!(?<dirname>[^/]+/lib)(?<rest>.*)!
"#{Regexp.last_match(:dirname)}#{Regexp.last_match(:rest)}"
when theme_dir && %r!\A#{Regexp.escape(theme_dir)}/(?<rest>.*)!io
PathManager.join(@site.theme.basename, Regexp.last_match(:rest))
when %r!\A/(.*)!
Regexp.last_match(1)
else
filename
end
end
end
def new_profile_hash
Hash.new { |hash, key| hash[key] = 0 }
end
end
end
@@ -0,0 +1,77 @@
# frozen_string_literal: true
module Jekyll
class LiquidRenderer
class File
def initialize(renderer, filename)
@renderer = renderer
@filename = filename
end
def parse(content)
measure_time do
@renderer.cache[@filename] ||= Liquid::Template.parse(content, :line_numbers => true)
end
@template = @renderer.cache[@filename]
self
end
def render(*args)
reset_template_assigns
measure_time do
measure_bytes do
measure_counts do
@template.render(*args)
end
end
end
end
# This method simply 'rethrows any error' before attempting to render the template.
def render!(*args)
reset_template_assigns
measure_time do
measure_bytes do
measure_counts do
@template.render!(*args)
end
end
end
end
def warnings
@template.warnings
end
private
# clear assigns to `Liquid::Template` instance prior to rendering since
# `Liquid::Template` instances are cached in Jekyll 4.
def reset_template_assigns
@template.instance_assigns.clear
end
def measure_counts
@renderer.increment_count(@filename)
yield
end
def measure_bytes
yield.tap do |str|
@renderer.increment_bytes(@filename, str.bytesize)
end
end
def measure_time
before = Time.now
yield
ensure
after = Time.now
@renderer.increment_time(@filename, after - before)
end
end
end
end
@@ -0,0 +1,45 @@
# frozen_string_literal: true
module Jekyll
class LiquidRenderer
class Table
GAUGES = [:count, :bytes, :time].freeze
def initialize(stats)
@stats = stats
end
def to_s(num_of_rows = 50)
Jekyll::Profiler.tabulate(data_for_table(num_of_rows))
end
private
def data_for_table(num_of_rows)
sorted = @stats.sort_by { |_, file_stats| -file_stats[:time] }
sorted = sorted.slice(0, num_of_rows)
table = [header_labels]
sorted.each do |filename, file_stats|
row = []
row << filename
row << file_stats[:count].to_s
row << format_bytes(file_stats[:bytes])
row << format("%.3f", file_stats[:time])
table << row
end
table
end
def header_labels
GAUGES.map { |gauge| gauge.to_s.capitalize }.unshift("Filename")
end
def format_bytes(bytes)
bytes /= 1024.0
format("%.2fK", bytes)
end
end
end
end
@@ -0,0 +1,151 @@
# frozen_string_literal: true
module Jekyll
class LogAdapter
attr_reader :writer, :messages, :level
LOG_LEVELS = {
:debug => ::Logger::DEBUG,
:info => ::Logger::INFO,
:warn => ::Logger::WARN,
:error => ::Logger::ERROR,
}.freeze
# Public: Create a new instance of a log writer
#
# writer - Logger compatible instance
# log_level - (optional, symbol) the log level
#
# Returns nothing
def initialize(writer, level = :info)
@messages = []
@writer = writer
self.log_level = level
end
# Public: Set the log level on the writer
#
# level - (symbol) the log level
#
# Returns nothing
def log_level=(level)
writer.level = level if level.is_a?(Integer) && level.between?(0, 3)
writer.level = LOG_LEVELS[level] ||
raise(ArgumentError, "unknown log level")
@level = level
end
def adjust_verbosity(options = {})
# Quiet always wins.
if options[:quiet]
self.log_level = :error
elsif options[:verbose]
self.log_level = :debug
end
debug "Logging at level:", LOG_LEVELS.key(writer.level).to_s
debug "Jekyll Version:", Jekyll::VERSION
end
# Public: Print a debug message
#
# topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
# message - the message detail
#
# Returns nothing
def debug(topic, message = nil, &block)
write(:debug, topic, message, &block)
end
# Public: Print a message
#
# topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
# message - the message detail
#
# Returns nothing
def info(topic, message = nil, &block)
write(:info, topic, message, &block)
end
# Public: Print a message
#
# topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
# message - the message detail
#
# Returns nothing
def warn(topic, message = nil, &block)
write(:warn, topic, message, &block)
end
# Public: Print an error message
#
# topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
# message - the message detail
#
# Returns nothing
def error(topic, message = nil, &block)
write(:error, topic, message, &block)
end
# Public: Print an error message and immediately abort the process
#
# topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
# message - the message detail (can be omitted)
#
# Returns nothing
def abort_with(topic, message = nil, &block)
error(topic, message, &block)
abort
end
# Internal: Build a topic method
#
# topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
# message - the message detail
#
# Returns the formatted message
def message(topic, message = nil)
raise ArgumentError, "block or message, not both" if block_given? && message
message = yield if block_given?
message = message.to_s.gsub(%r!\s+!, " ")
topic = formatted_topic(topic, block_given?)
out = topic + message
messages << out
out
end
# Internal: Format the topic
#
# topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
# colon -
#
# Returns the formatted topic statement
def formatted_topic(topic, colon = false)
"#{topic}#{colon ? ": " : " "}".rjust(20)
end
# Internal: Check if the message should be written given the log level.
#
# level_of_message - the Symbol level of message, one of :debug, :info, :warn, :error
#
# Returns whether the message should be written.
def write_message?(level_of_message)
LOG_LEVELS.fetch(level) <= LOG_LEVELS.fetch(level_of_message)
end
# Internal: Log a message.
#
# level_of_message - the Symbol level of message, one of :debug, :info, :warn, :error
# topic - the String topic or full message
# message - the String message (optional)
# block - a block containing the message (optional)
#
# Returns false if the message was not written, otherwise returns the value of calling
# the appropriate writer method, e.g. writer.info.
def write(level_of_message, topic, message = nil, &block)
return false unless write_message?(level_of_message)
writer.public_send(level_of_message, message(topic, message, &block))
end
end
end
+940
View File
@@ -0,0 +1,940 @@
# Woah there. Do not edit this file directly.
# This file is generated automatically by script/vendor-mimes.
application/andrew-inset ez
application/applixware aw
application/atom+xml atom
application/atomcat+xml atomcat
application/atomdeleted+xml atomdeleted
application/atomsvc+xml atomsvc
application/atsc-dwd+xml dwd
application/atsc-held+xml held
application/atsc-rsat+xml rsat
application/bdoc bdoc
application/calendar+xml xcs
application/ccxml+xml ccxml
application/cdfx+xml cdfx
application/cdmi-capability cdmia
application/cdmi-container cdmic
application/cdmi-domain cdmid
application/cdmi-object cdmio
application/cdmi-queue cdmiq
application/cpl+xml cpl
application/cu-seeme cu
application/dash+xml mpd
application/dash-patch+xml mpp
application/davmount+xml davmount
application/docbook+xml dbk
application/dssc+der dssc
application/dssc+xml xdssc
application/ecmascript ecma es
application/emma+xml emma
application/emotionml+xml emotionml
application/epub+zip epub
application/exi exi
application/express exp
application/fdt+xml fdt
application/font-tdpfr pfr
application/geo+json geojson
application/gml+xml gml
application/gpx+xml gpx
application/gxf gxf
application/gzip gz
application/hjson hjson
application/hyperstudio stk
application/inkml+xml ink inkml
application/ipfix ipfix
application/its+xml its
application/java-archive ear jar war
application/java-serialized-object ser
application/java-vm class
application/javascript js mjs
application/json json map
application/json5 json5
application/jsonml+json jsonml
application/ld+json jsonld
application/lgr+xml lgr
application/lost+xml lostxml
application/mac-binhex40 hqx
application/mac-compactpro cpt
application/mads+xml mads
application/manifest+json webmanifest
application/marc mrc
application/marcxml+xml mrcx
application/mathematica ma mb nb
application/mathml+xml mathml
application/mbox mbox
application/media-policy-dataset+xml mpf
application/mediaservercontrol+xml mscml
application/metalink+xml metalink
application/metalink4+xml meta4
application/mets+xml mets
application/mmt-aei+xml maei
application/mmt-usd+xml musd
application/mods+xml mods
application/mp21 m21 mp21
application/mp4 m4p mp4s
application/msword doc dot
application/mxf mxf
application/n-quads nq
application/n-triples nt
application/node cjs
application/octet-stream bin bpk buffer deb deploy dist distz dll dmg dms dump elc exe img iso lrf mar msi msm msp pkg so
application/oda oda
application/oebps-package+xml opf
application/ogg ogx
application/omdoc+xml omdoc
application/onenote onepkg onetmp onetoc onetoc2
application/oxps oxps
application/p2p-overlay+xml relo
application/patch-ops-error+xml xer
application/pdf pdf
application/pgp-encrypted pgp
application/pgp-keys asc
application/pgp-signature sig
application/pics-rules prf
application/pkcs10 p10
application/pkcs7-mime p7c p7m
application/pkcs7-signature p7s
application/pkcs8 p8
application/pkix-attr-cert ac
application/pkix-cert cer
application/pkix-crl crl
application/pkix-pkipath pkipath
application/pkixcmp pki
application/pls+xml pls
application/postscript ai eps ps
application/provenance+xml provx
application/prs.cww cww
application/pskc+xml pskcxml
application/raml+yaml raml
application/rdf+xml owl rdf
application/reginfo+xml rif
application/relax-ng-compact-syntax rnc
application/resource-lists+xml rl
application/resource-lists-diff+xml rld
application/rls-services+xml rs
application/route-apd+xml rapd
application/route-s-tsid+xml sls
application/route-usd+xml rusd
application/rpki-ghostbusters gbr
application/rpki-manifest mft
application/rpki-roa roa
application/rsd+xml rsd
application/rss+xml rss
application/rtf rtf
application/sbml+xml sbml
application/scvp-cv-request scq
application/scvp-cv-response scs
application/scvp-vp-request spq
application/scvp-vp-response spp
application/sdp sdp
application/senml+xml senmlx
application/sensml+xml sensmlx
application/set-payment-initiation setpay
application/set-registration-initiation setreg
application/shf+xml shf
application/sieve sieve siv
application/smil+xml smi smil
application/sparql-query rq
application/sparql-results+xml srx
application/srgs gram
application/srgs+xml grxml
application/sru+xml sru
application/ssdl+xml ssdl
application/ssml+xml ssml
application/swid+xml swidtag
application/tei+xml tei teicorpus
application/thraud+xml tfi
application/timestamped-data tsd
application/toml toml
application/trig trig
application/ttml+xml ttml
application/ubjson ubj
application/urc-ressheet+xml rsheet
application/urc-targetdesc+xml td
application/vnd.1000minds.decision-model+xml 1km
application/vnd.3gpp.pic-bw-large plb
application/vnd.3gpp.pic-bw-small psb
application/vnd.3gpp.pic-bw-var pvb
application/vnd.3gpp2.tcap tcap
application/vnd.3m.post-it-notes pwn
application/vnd.accpac.simply.aso aso
application/vnd.accpac.simply.imp imp
application/vnd.acucobol acu
application/vnd.acucorp acutc atc
application/vnd.adobe.air-application-installer-package+zip air
application/vnd.adobe.formscentral.fcdt fcdt
application/vnd.adobe.fxp fxp fxpl
application/vnd.adobe.xdp+xml xdp
application/vnd.adobe.xfdf xfdf
application/vnd.age age
application/vnd.ahead.space ahead
application/vnd.airzip.filesecure.azf azf
application/vnd.airzip.filesecure.azs azs
application/vnd.amazon.ebook azw
application/vnd.americandynamics.acc acc
application/vnd.amiga.ami ami
application/vnd.android.package-archive apk
application/vnd.anser-web-certificate-issue-initiation cii
application/vnd.anser-web-funds-transfer-initiation fti
application/vnd.antix.game-component atx
application/vnd.apple.installer+xml mpkg
application/vnd.apple.keynote key
application/vnd.apple.mpegurl m3u8
application/vnd.apple.numbers numbers
application/vnd.apple.pages pages
application/vnd.apple.pkpass pkpass
application/vnd.aristanetworks.swi swi
application/vnd.astraea-software.iota iota
application/vnd.audiograph aep
application/vnd.balsamiq.bmml+xml bmml
application/vnd.blueice.multipass mpm
application/vnd.bmi bmi
application/vnd.businessobjects rep
application/vnd.chemdraw+xml cdxml
application/vnd.chipnuts.karaoke-mmd mmd
application/vnd.cinderella cdy
application/vnd.citationstyles.style+xml csl
application/vnd.claymore cla
application/vnd.cloanto.rp9 rp9
application/vnd.clonk.c4group c4d c4f c4g c4p c4u
application/vnd.cluetrust.cartomobile-config c11amc
application/vnd.cluetrust.cartomobile-config-pkg c11amz
application/vnd.commonspace csp
application/vnd.contact.cmsg cdbcmsg
application/vnd.cosmocaller cmc
application/vnd.crick.clicker clkx
application/vnd.crick.clicker.keyboard clkk
application/vnd.crick.clicker.palette clkp
application/vnd.crick.clicker.template clkt
application/vnd.crick.clicker.wordbank clkw
application/vnd.criticaltools.wbs+xml wbs
application/vnd.ctc-posml pml
application/vnd.cups-ppd ppd
application/vnd.curl.car car
application/vnd.curl.pcurl pcurl
application/vnd.dart dart
application/vnd.data-vision.rdz rdz
application/vnd.dbf dbf
application/vnd.dece.data uvd uvf uvvd uvvf
application/vnd.dece.ttml+xml uvt uvvt
application/vnd.dece.unspecified uvvx uvx
application/vnd.dece.zip uvvz uvz
application/vnd.denovo.fcselayout-link fe_launch
application/vnd.dna dna
application/vnd.dolby.mlp mlp
application/vnd.dpgraph dpg
application/vnd.dreamfactory dfac
application/vnd.ds-keypoint kpxx
application/vnd.dvb.ait ait
application/vnd.dvb.service svc
application/vnd.dynageo geo
application/vnd.ecowin.chart mag
application/vnd.enliven nml
application/vnd.epson.esf esf
application/vnd.epson.msf msf
application/vnd.epson.quickanime qam
application/vnd.epson.salt slt
application/vnd.epson.ssf ssf
application/vnd.eszigno3+xml es3 et3
application/vnd.ezpix-album ez2
application/vnd.ezpix-package ez3
application/vnd.fdf fdf
application/vnd.fdsn.mseed mseed
application/vnd.fdsn.seed dataless seed
application/vnd.flographit gph
application/vnd.fluxtime.clip ftc
application/vnd.framemaker book fm frame maker
application/vnd.frogans.fnc fnc
application/vnd.frogans.ltf ltf
application/vnd.fsc.weblaunch fsc
application/vnd.fujitsu.oasys oas
application/vnd.fujitsu.oasys2 oa2
application/vnd.fujitsu.oasys3 oa3
application/vnd.fujitsu.oasysgp fg5
application/vnd.fujitsu.oasysprs bh2
application/vnd.fujixerox.ddd ddd
application/vnd.fujixerox.docuworks xdw
application/vnd.fujixerox.docuworks.binder xbd
application/vnd.fuzzysheet fzs
application/vnd.genomatix.tuxedo txd
application/vnd.geogebra.file ggb
application/vnd.geogebra.tool ggt
application/vnd.geometry-explorer gex gre
application/vnd.geonext gxt
application/vnd.geoplan g2w
application/vnd.geospace g3w
application/vnd.gmx gmx
application/vnd.google-apps.document gdoc
application/vnd.google-apps.presentation gslides
application/vnd.google-apps.spreadsheet gsheet
application/vnd.google-earth.kml+xml kml
application/vnd.google-earth.kmz kmz
application/vnd.grafeq gqf gqs
application/vnd.groove-account gac
application/vnd.groove-help ghf
application/vnd.groove-identity-message gim
application/vnd.groove-injector grv
application/vnd.groove-tool-message gtm
application/vnd.groove-tool-template tpl
application/vnd.groove-vcard vcg
application/vnd.hal+xml hal
application/vnd.handheld-entertainment+xml zmm
application/vnd.hbci hbci
application/vnd.hhe.lesson-player les
application/vnd.hp-hpgl hpgl
application/vnd.hp-hpid hpid
application/vnd.hp-hps hps
application/vnd.hp-jlyt jlt
application/vnd.hp-pcl pcl
application/vnd.hp-pclxl pclxl
application/vnd.hydrostatix.sof-data sfd-hdstx
application/vnd.ibm.minipay mpy
application/vnd.ibm.modcap afp list3820 listafp
application/vnd.ibm.rights-management irm
application/vnd.ibm.secure-container sc
application/vnd.iccprofile icc icm
application/vnd.igloader igl
application/vnd.immervision-ivp ivp
application/vnd.immervision-ivu ivu
application/vnd.insors.igm igm
application/vnd.intercon.formnet xpw xpx
application/vnd.intergeo i2g
application/vnd.intu.qbo qbo
application/vnd.intu.qfx qfx
application/vnd.ipunplugged.rcprofile rcprofile
application/vnd.irepository.package+xml irp
application/vnd.is-xpr xpr
application/vnd.isac.fcs fcs
application/vnd.jam jam
application/vnd.jcp.javame.midlet-rms rms
application/vnd.jisp jisp
application/vnd.joost.joda-archive joda
application/vnd.kahootz ktr ktz
application/vnd.kde.karbon karbon
application/vnd.kde.kchart chrt
application/vnd.kde.kformula kfo
application/vnd.kde.kivio flw
application/vnd.kde.kontour kon
application/vnd.kde.kpresenter kpr kpt
application/vnd.kde.kspread ksp
application/vnd.kde.kword kwd kwt
application/vnd.kenameaapp htke
application/vnd.kidspiration kia
application/vnd.kinar kne knp
application/vnd.koan skd skm skp skt
application/vnd.kodak-descriptor sse
application/vnd.las.las+xml lasxml
application/vnd.llamagraphics.life-balance.desktop lbd
application/vnd.llamagraphics.life-balance.exchange+xml lbe
application/vnd.lotus-1-2-3 123
application/vnd.lotus-approach apr
application/vnd.lotus-freelance pre
application/vnd.lotus-notes nsf
application/vnd.lotus-organizer org
application/vnd.lotus-screencam scm
application/vnd.lotus-wordpro lwp
application/vnd.macports.portpkg portpkg
application/vnd.mapbox-vector-tile mvt
application/vnd.mcd mcd
application/vnd.medcalcdata mc1
application/vnd.mediastation.cdkey cdkey
application/vnd.mfer mwf
application/vnd.mfmp mfm
application/vnd.micrografx.flo flo
application/vnd.micrografx.igx igx
application/vnd.mif mif
application/vnd.mobius.daf daf
application/vnd.mobius.dis dis
application/vnd.mobius.mbk mbk
application/vnd.mobius.mqy mqy
application/vnd.mobius.msl msl
application/vnd.mobius.plc plc
application/vnd.mobius.txf txf
application/vnd.mophun.application mpn
application/vnd.mophun.certificate mpc
application/vnd.mozilla.xul+xml xul
application/vnd.ms-artgalry cil
application/vnd.ms-cab-compressed cab
application/vnd.ms-excel xla xlc xlm xls xlt xlw
application/vnd.ms-excel.addin.macroenabled.12 xlam
application/vnd.ms-excel.sheet.binary.macroenabled.12 xlsb
application/vnd.ms-excel.sheet.macroenabled.12 xlsm
application/vnd.ms-excel.template.macroenabled.12 xltm
application/vnd.ms-fontobject eot
application/vnd.ms-htmlhelp chm
application/vnd.ms-ims ims
application/vnd.ms-lrm lrm
application/vnd.ms-officetheme thmx
application/vnd.ms-outlook msg
application/vnd.ms-pki.seccat cat
application/vnd.ms-pki.stl stl
application/vnd.ms-powerpoint pot pps ppt
application/vnd.ms-powerpoint.addin.macroenabled.12 ppam
application/vnd.ms-powerpoint.presentation.macroenabled.12 pptm
application/vnd.ms-powerpoint.slide.macroenabled.12 sldm
application/vnd.ms-powerpoint.slideshow.macroenabled.12 ppsm
application/vnd.ms-powerpoint.template.macroenabled.12 potm
application/vnd.ms-project mpt
application/vnd.ms-word.document.macroenabled.12 docm
application/vnd.ms-word.template.macroenabled.12 dotm
application/vnd.ms-works wcm wdb wks wps
application/vnd.ms-wpl wpl
application/vnd.ms-xpsdocument xps
application/vnd.mseq mseq
application/vnd.musician mus
application/vnd.muvee.style msty
application/vnd.mynfc taglet
application/vnd.neurolanguage.nlu nlu
application/vnd.nitf nitf ntf
application/vnd.noblenet-directory nnd
application/vnd.noblenet-sealer nns
application/vnd.noblenet-web nnw
application/vnd.nokia.n-gage.data ngdat
application/vnd.nokia.n-gage.symbian.install n-gage
application/vnd.nokia.radio-preset rpst
application/vnd.nokia.radio-presets rpss
application/vnd.novadigm.edm edm
application/vnd.novadigm.edx edx
application/vnd.novadigm.ext ext
application/vnd.oasis.opendocument.chart odc
application/vnd.oasis.opendocument.chart-template otc
application/vnd.oasis.opendocument.database odb
application/vnd.oasis.opendocument.formula odf
application/vnd.oasis.opendocument.formula-template odft
application/vnd.oasis.opendocument.graphics odg
application/vnd.oasis.opendocument.graphics-template otg
application/vnd.oasis.opendocument.image odi
application/vnd.oasis.opendocument.image-template oti
application/vnd.oasis.opendocument.presentation odp
application/vnd.oasis.opendocument.presentation-template otp
application/vnd.oasis.opendocument.spreadsheet ods
application/vnd.oasis.opendocument.spreadsheet-template ots
application/vnd.oasis.opendocument.text odt
application/vnd.oasis.opendocument.text-master odm
application/vnd.oasis.opendocument.text-template ott
application/vnd.oasis.opendocument.text-web oth
application/vnd.olpc-sugar xo
application/vnd.oma.dd2+xml dd2
application/vnd.openblox.game+xml obgx
application/vnd.openofficeorg.extension oxt
application/vnd.openstreetmap.data+xml osm
application/vnd.openxmlformats-officedocument.presentationml.presentation pptx
application/vnd.openxmlformats-officedocument.presentationml.slide sldx
application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx
application/vnd.openxmlformats-officedocument.presentationml.template potx
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx
application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx
application/vnd.openxmlformats-officedocument.wordprocessingml.document docx
application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx
application/vnd.osgeo.mapguide.package mgp
application/vnd.osgi.dp dp
application/vnd.osgi.subsystem esa
application/vnd.palm oprc pdb pqa
application/vnd.pawaafile paw
application/vnd.pg.format str
application/vnd.pg.osasli ei6
application/vnd.picsel efif
application/vnd.pmi.widget wg
application/vnd.pocketlearn plf
application/vnd.powerbuilder6 pbd
application/vnd.previewsystems.box box
application/vnd.proteus.magazine mgz
application/vnd.publishare-delta-tree qps
application/vnd.pvi.ptid1 ptid
application/vnd.quark.quarkxpress qwd qwt qxb qxd qxl qxt
application/vnd.rar rar
application/vnd.realvnc.bed bed
application/vnd.recordare.musicxml mxl
application/vnd.recordare.musicxml+xml musicxml
application/vnd.rig.cryptonote cryptonote
application/vnd.rim.cod cod
application/vnd.rn-realmedia rm
application/vnd.rn-realmedia-vbr rmvb
application/vnd.route66.link66+xml link66
application/vnd.sailingtracker.track st
application/vnd.seemail see
application/vnd.sema sema
application/vnd.semd semd
application/vnd.semf semf
application/vnd.shana.informed.formdata ifm
application/vnd.shana.informed.formtemplate itp
application/vnd.shana.informed.interchange iif
application/vnd.shana.informed.package ipk
application/vnd.simtech-mindmapper twd twds
application/vnd.smaf mmf
application/vnd.smart.teacher teacher
application/vnd.software602.filler.form+xml fo
application/vnd.solent.sdkm+xml sdkd sdkm
application/vnd.spotfire.dxp dxp
application/vnd.spotfire.sfs sfs
application/vnd.stardivision.calc sdc
application/vnd.stardivision.draw sda
application/vnd.stardivision.impress sdd
application/vnd.stardivision.math smf
application/vnd.stardivision.writer sdw vor
application/vnd.stardivision.writer-global sgl
application/vnd.stepmania.package smzip
application/vnd.stepmania.stepchart sm
application/vnd.sun.wadl+xml wadl
application/vnd.sun.xml.calc sxc
application/vnd.sun.xml.calc.template stc
application/vnd.sun.xml.draw sxd
application/vnd.sun.xml.draw.template std
application/vnd.sun.xml.impress sxi
application/vnd.sun.xml.impress.template sti
application/vnd.sun.xml.math sxm
application/vnd.sun.xml.writer sxw
application/vnd.sun.xml.writer.global sxg
application/vnd.sun.xml.writer.template stw
application/vnd.sus-calendar sus susp
application/vnd.svd svd
application/vnd.symbian.install sis sisx
application/vnd.syncml+xml xsm
application/vnd.syncml.dm+wbxml bdm
application/vnd.syncml.dm+xml xdm
application/vnd.syncml.dmddf+xml ddf
application/vnd.tao.intent-module-archive tao
application/vnd.tcpdump.pcap cap dmp pcap
application/vnd.tmobile-livetv tmo
application/vnd.trid.tpt tpt
application/vnd.triscape.mxs mxs
application/vnd.trueapp tra
application/vnd.ufdl ufd ufdl
application/vnd.uiq.theme utz
application/vnd.umajin umj
application/vnd.unity unityweb
application/vnd.uoml+xml uoml
application/vnd.vcx vcx
application/vnd.visio vsd vss vst vsw
application/vnd.visionary vis
application/vnd.vsf vsf
application/vnd.wap.wbxml wbxml
application/vnd.wap.wmlc wmlc
application/vnd.wap.wmlscriptc wmlsc
application/vnd.webturbo wtb
application/vnd.wolfram.player nbp
application/vnd.wordperfect wpd
application/vnd.wqd wqd
application/vnd.wt.stf stf
application/vnd.xara xar
application/vnd.xfdl xfdl
application/vnd.yamaha.hv-dic hvd
application/vnd.yamaha.hv-script hvs
application/vnd.yamaha.hv-voice hvp
application/vnd.yamaha.openscoreformat osf
application/vnd.yamaha.openscoreformat.osfpvg+xml osfpvg
application/vnd.yamaha.smaf-audio saf
application/vnd.yamaha.smaf-phrase spf
application/vnd.yellowriver-custom-menu cmp
application/vnd.zul zir zirz
application/vnd.zzazz.deck+xml zaz
application/voicexml+xml vxml
application/wasm wasm
application/watcherinfo+xml wif
application/widget wgt
application/winhlp hlp
application/wsdl+xml wsdl
application/wspolicy+xml wspolicy
application/x-7z-compressed 7z
application/x-abiword abw
application/x-ace-compressed ace
application/x-arj arj
application/x-authorware-bin aab u32 vox x32
application/x-authorware-map aam
application/x-authorware-seg aas
application/x-bcpio bcpio
application/x-bittorrent torrent
application/x-blorb blb blorb
application/x-bzip bz
application/x-bzip2 boz bz2
application/x-cbr cb7 cba cbr cbt cbz
application/x-cdlink vcd
application/x-cfs-compressed cfs
application/x-chat chat
application/x-chess-pgn pgn
application/x-chrome-extension crx
application/x-cocoa cco
application/x-conference nsc
application/x-cpio cpio
application/x-csh csh
application/x-debian-package udeb
application/x-dgc-compressed dgc
application/x-director cct cst cxt dcr dir dxr fgd swa w3d
application/x-doom wad
application/x-dtbncx+xml ncx
application/x-dtbook+xml dtb
application/x-dtbresource+xml res
application/x-dvi dvi
application/x-envoy evy
application/x-eva eva
application/x-font-bdf bdf
application/x-font-ghostscript gsf
application/x-font-linux-psf psf
application/x-font-pcf pcf
application/x-font-snf snf
application/x-font-type1 afm pfa pfb pfm
application/x-freearc arc
application/x-futuresplash spl
application/x-gca-compressed gca
application/x-glulx ulx
application/x-gnumeric gnumeric
application/x-gramps-xml gramps
application/x-gtar gtar
application/x-hdf hdf
application/x-httpd-php php
application/x-install-instructions install
application/x-java-archive-diff jardiff
application/x-java-jnlp-file jnlp
application/x-keepass2 kdbx
application/x-latex latex
application/x-lua-bytecode luac
application/x-lzh-compressed lha lzh
application/x-makeself run
application/x-mie mie
application/x-mobipocket-ebook mobi prc
application/x-ms-application application
application/x-ms-shortcut lnk
application/x-ms-wmd wmd
application/x-ms-wmz wmz
application/x-ms-xbap xbap
application/x-msaccess mdb
application/x-msbinder obd
application/x-mscardfile crd
application/x-msclip clp
application/x-msdownload bat com
application/x-msmediaview m13 m14 mvb
application/x-msmetafile emf emz wmf
application/x-msmoney mny
application/x-mspublisher pub
application/x-msschedule scd
application/x-msterminal trm
application/x-mswrite wri
application/x-netcdf cdf nc
application/x-ns-proxy-autoconfig pac
application/x-nzb nzb
application/x-perl pl pm
application/x-pkcs12 p12 pfx
application/x-pkcs7-certificates p7b spc
application/x-pkcs7-certreqresp p7r
application/x-redhat-package-manager rpm
application/x-research-info-systems ris
application/x-sea sea
application/x-sh sh
application/x-shar shar
application/x-shockwave-flash swf
application/x-silverlight-app xap
application/x-sql sql
application/x-stuffit sit
application/x-stuffitx sitx
application/x-subrip srt
application/x-sv4cpio sv4cpio
application/x-sv4crc sv4crc
application/x-t3vm-image t3
application/x-tads gam
application/x-tar tar
application/x-tcl tcl tk
application/x-tex tex
application/x-tex-tfm tfm
application/x-texinfo texi texinfo
application/x-tgif obj
application/x-ustar ustar
application/x-virtualbox-hdd hdd
application/x-virtualbox-ova ova
application/x-virtualbox-ovf ovf
application/x-virtualbox-vbox vbox
application/x-virtualbox-vbox-extpack vbox-extpack
application/x-virtualbox-vdi vdi
application/x-virtualbox-vhd vhd
application/x-virtualbox-vmdk vmdk
application/x-wais-source src
application/x-web-app-manifest+json webapp
application/x-x509-ca-cert crt der pem
application/x-xfig fig
application/x-xliff+xml xlf
application/x-xpinstall xpi
application/x-xz xz
application/x-zmachine z1 z2 z3 z4 z5 z6 z7 z8
application/xaml+xml xaml
application/xcap-att+xml xav
application/xcap-caps+xml xca
application/xcap-diff+xml xdf
application/xcap-el+xml xel
application/xcap-ns+xml xns
application/xenc+xml xenc
application/xhtml+xml xht xhtml
application/xml rng xml xsd xsl
application/xml-dtd dtd
application/xop+xml xop
application/xproc+xml xpl
application/xslt+xml xslt
application/xspf+xml xspf
application/xv+xml mxml xhvml xvm xvml
application/yang yang
application/yin+xml yin
application/zip zip
audio/3gpp 3gpp
audio/adpcm adp
audio/amr amr
audio/basic au snd
audio/midi kar mid midi rmi
audio/mobile-xmf mxmf
audio/mp3 mp3
audio/mp4 m4a mp4a
audio/mpeg m2a m3a mp2 mp2a mpga
audio/ogg oga ogg opus spx
audio/s3m s3m
audio/silk sil
audio/vnd.dece.audio uva uvva
audio/vnd.digital-winds eol
audio/vnd.dra dra
audio/vnd.dts dts
audio/vnd.dts.hd dtshd
audio/vnd.lucent.voice lvp
audio/vnd.ms-playready.media.pya pya
audio/vnd.nuera.ecelp4800 ecelp4800
audio/vnd.nuera.ecelp7470 ecelp7470
audio/vnd.nuera.ecelp9600 ecelp9600
audio/vnd.rip rip
audio/wav wav
audio/webm weba
audio/x-aac aac
audio/x-aiff aif aifc aiff
audio/x-caf caf
audio/x-flac flac
audio/x-matroska mka
audio/x-mpegurl m3u
audio/x-ms-wax wax
audio/x-ms-wma wma
audio/x-pn-realaudio ra ram
audio/x-pn-realaudio-plugin rmp
audio/xm xm
chemical/x-cdx cdx
chemical/x-cif cif
chemical/x-cmdf cmdf
chemical/x-cml cml
chemical/x-csml csml
chemical/x-xyz xyz
font/collection ttc
font/otf otf
font/ttf ttf
font/woff woff
font/woff2 woff2
image/aces exr
image/apng apng
image/avci avci
image/avcs avcs
image/avif avif
image/bmp bmp
image/cgm cgm
image/dicom-rle drle
image/fits fits
image/g3fax g3
image/gif gif
image/heic heic
image/heic-sequence heics
image/heif heif
image/heif-sequence heifs
image/hej2k hej2
image/hsj2 hsj2
image/ief ief
image/jls jls
image/jp2 jp2 jpg2
image/jpeg jpe jpeg jpg
image/jph jph
image/jphc jhc
image/jpm jpm
image/jpx jpf jpx
image/jxr jxr
image/jxra jxra
image/jxrs jxrs
image/jxs jxs
image/jxsc jxsc
image/jxsi jxsi
image/jxss jxss
image/ktx ktx
image/ktx2 ktx2
image/png png
image/prs.btif btif
image/prs.pti pti
image/sgi sgi
image/svg+xml svg svgz
image/t38 t38
image/tiff tif tiff
image/tiff-fx tfx
image/vnd.adobe.photoshop psd
image/vnd.airzip.accelerator.azv azv
image/vnd.dece.graphic uvg uvi uvvg uvvi
image/vnd.djvu djv djvu
image/vnd.dvb.subtitle sub
image/vnd.dwg dwg
image/vnd.dxf dxf
image/vnd.fastbidsheet fbs
image/vnd.fpx fpx
image/vnd.fst fst
image/vnd.fujixerox.edmics-mmr mmr
image/vnd.fujixerox.edmics-rlc rlc
image/vnd.microsoft.icon ico
image/vnd.ms-dds dds
image/vnd.ms-modi mdi
image/vnd.ms-photo wdp
image/vnd.net-fpx npx
image/vnd.pco.b16 b16
image/vnd.tencent.tap tap
image/vnd.valve.source.texture vtf
image/vnd.wap.wbmp wbmp
image/vnd.xiff xif
image/vnd.zbrush.pcx pcx
image/webp webp
image/x-3ds 3ds
image/x-cmu-raster ras
image/x-cmx cmx
image/x-freehand fh fh4 fh5 fh7 fhc
image/x-jng jng
image/x-mrsid-image sid
image/x-pict pct pic
image/x-portable-anymap pnm
image/x-portable-bitmap pbm
image/x-portable-graymap pgm
image/x-portable-pixmap ppm
image/x-rgb rgb
image/x-tga tga
image/x-xbitmap xbm
image/x-xpixmap xpm
image/x-xwindowdump xwd
message/disposition-notification disposition-notification
message/global u8msg
message/global-delivery-status u8dsn
message/global-disposition-notification u8mdn
message/global-headers u8hdr
message/rfc822 eml mime
message/vnd.wfa.wsc wsc
model/3mf 3mf
model/gltf+json gltf
model/gltf-binary glb
model/iges iges igs
model/mesh mesh msh silo
model/mtl mtl
model/step+xml stpx
model/step+zip stpz
model/step-xml+zip stpxz
model/vnd.collada+xml dae
model/vnd.dwf dwf
model/vnd.gdl gdl
model/vnd.gtw gtw
model/vnd.mts mts
model/vnd.opengex ogex
model/vnd.parasolid.transmit.binary x_b
model/vnd.parasolid.transmit.text x_t
model/vnd.sap.vds vds
model/vnd.usdz+zip usdz
model/vnd.valve.source.compiled-map bsp
model/vnd.vtu vtu
model/vrml vrml wrl
model/x3d+binary x3db x3dbz
model/x3d+vrml x3dv x3dvz
model/x3d+xml x3d x3dz
text/cache-manifest appcache manifest
text/calendar ics ifb
text/coffeescript coffee litcoffee
text/css css
text/csv csv
text/html htm html shtml
text/jade jade
text/jsx jsx
text/less less
text/markdown markdown md
text/mathml mml
text/mdx mdx
text/n3 n3
text/plain conf def in ini list log text txt
text/prs.lines.tag dsc
text/richtext rtx
text/sgml sgm sgml
text/shex shex
text/slim slim slm
text/spdx spdx
text/stylus styl stylus
text/tab-separated-values tsv
text/troff man me ms roff t tr
text/turtle ttl
text/uri-list uri uris urls
text/vcard vcard
text/vnd.curl curl
text/vnd.curl.dcurl dcurl
text/vnd.curl.mcurl mcurl
text/vnd.curl.scurl scurl
text/vnd.familysearch.gedcom ged
text/vnd.fly fly
text/vnd.fmi.flexstor flx
text/vnd.graphviz gv
text/vnd.in3d.3dml 3dml
text/vnd.in3d.spot spot
text/vnd.sun.j2me.app-descriptor jad
text/vnd.wap.wml wml
text/vnd.wap.wmlscript wmls
text/vtt vtt
text/x-asm asm s
text/x-c c cc cpp cxx dic h hh
text/x-component htc
text/x-fortran f f77 f90 for
text/x-handlebars-template hbs
text/x-java-source java
text/x-lua lua
text/x-markdown mkd
text/x-nfo nfo
text/x-opml opml
text/x-pascal p pas
text/x-processing pde
text/x-sass sass
text/x-scss scss
text/x-setext etx
text/x-sfv sfv
text/x-suse-ymp ymp
text/x-uuencode uu
text/x-vcalendar vcs
text/x-vcard vcf
text/yaml yaml yml
video/3gpp 3gp
video/3gpp2 3g2
video/h261 h261
video/h263 h263
video/h264 h264
video/iso.segment m4s
video/jpeg jpgv
video/jpm jpgm
video/mj2 mj2 mjp2
video/mp2t ts
video/mp4 mp4 mp4v mpg4
video/mpeg m1v m2v mpe mpeg mpg
video/ogg ogv
video/quicktime mov qt
video/vnd.dece.hd uvh uvvh
video/vnd.dece.mobile uvm uvvm
video/vnd.dece.pd uvp uvvp
video/vnd.dece.sd uvs uvvs
video/vnd.dece.video uvv uvvv
video/vnd.dvb.file dvb
video/vnd.fvt fvt
video/vnd.mpegurl m4u mxu
video/vnd.ms-playready.media.pyv pyv
video/vnd.uvvu.mp4 uvu uvvu
video/vnd.vivo viv
video/webm webm
video/x-f4v f4v
video/x-fli fli
video/x-flv flv
video/x-m4v m4v
video/x-matroska mk3d mks mkv
video/x-mng mng
video/x-ms-asf asf asx
video/x-ms-vob vob
video/x-ms-wm wm
video/x-ms-wmv wmv
video/x-ms-wmx wmx
video/x-ms-wvx wvx
video/x-msvideo avi
video/x-sgi-movie movie
video/x-smv smv
x-conference/x-cooltalk ice
+215
View File
@@ -0,0 +1,215 @@
# frozen_string_literal: true
module Jekyll
class Page
include Convertible
attr_writer :dir
attr_accessor :basename, :content, :data, :ext, :name, :output, :pager, :site
alias_method :extname, :ext
# Attributes for Liquid templates
ATTRIBUTES_FOR_LIQUID = %w(
content
dir
excerpt
name
path
url
).freeze
# A set of extensions that are considered HTML or HTML-like so we
# should not alter them, this includes .xhtml through XHTM5.
HTML_EXTENSIONS = %w(
.html
.xhtml
.htm
).freeze
# Initialize a new Page.
#
# site - The Site object.
# base - The String path to the source.
# dir - The String path between the source and the file.
# name - The String filename of the file.
def initialize(site, base, dir, name)
@site = site
@base = base
@dir = dir
@name = name
@path = if site.in_theme_dir(base) == base # we're in a theme
site.in_theme_dir(base, dir, name)
else
site.in_source_dir(base, dir, name)
end
process(name)
read_yaml(PathManager.join(base, dir), name)
generate_excerpt if site.config["page_excerpts"]
data.default_proc = proc do |_, key|
site.frontmatter_defaults.find(relative_path, type, key)
end
Jekyll::Hooks.trigger :pages, :post_init, self
end
# The generated directory into which the page will be placed
# upon generation. This is derived from the permalink or, if
# permalink is absent, will be '/'
#
# Returns the String destination directory.
def dir
url.end_with?("/") ? url : url_dir
end
# The full path and filename of the post. Defined in the YAML of the post
# body.
#
# Returns the String permalink or nil if none has been set.
def permalink
data.nil? ? nil : data["permalink"]
end
# The template of the permalink.
#
# Returns the template String.
def template
if !html?
"/:path/:basename:output_ext"
elsif index?
"/:path/"
else
Utils.add_permalink_suffix("/:path/:basename", site.permalink_style)
end
end
# The generated relative url of this page. e.g. /about.html.
#
# Returns the String url.
def url
@url ||= URL.new(
:template => template,
:placeholders => url_placeholders,
:permalink => permalink
).to_s
end
# Returns a hash of URL placeholder names (as symbols) mapping to the
# desired placeholder replacements. For details see "url.rb"
def url_placeholders
{
:path => @dir,
:basename => basename,
:output_ext => output_ext,
}
end
# Extract information from the page filename.
#
# name - The String filename of the page file.
#
# NOTE: `String#gsub` removes all trailing periods (in comparison to `String#chomp`)
# Returns nothing.
def process(name)
return unless name
self.ext = File.extname(name)
self.basename = name[0..-ext.length - 1].gsub(%r!\.*\z!, "")
end
# Add any necessary layouts to this post
#
# layouts - The Hash of {"name" => "layout"}.
# site_payload - The site payload Hash.
#
# Returns String rendered page.
def render(layouts, site_payload)
site_payload["page"] = to_liquid
site_payload["paginator"] = pager.to_liquid
do_layout(site_payload, layouts)
end
# The path to the source file
#
# Returns the path to the source file
def path
data.fetch("path") { relative_path }
end
# The path to the page source file, relative to the site source
def relative_path
@relative_path ||= PathManager.join(@dir, @name).delete_prefix("/")
end
# Obtain destination path.
#
# dest - The String path to the destination dir.
#
# Returns the destination file path String.
def destination(dest)
@destination ||= {}
@destination[dest] ||= begin
path = site.in_dest_dir(dest, URL.unescape_path(url))
path = File.join(path, "index") if url.end_with?("/")
path << output_ext unless path.end_with? output_ext
path
end
end
# Returns the object as a debug String.
def inspect
"#<#{self.class} @relative_path=#{relative_path.inspect}>"
end
# Returns the Boolean of whether this Page is HTML or not.
def html?
HTML_EXTENSIONS.include?(output_ext)
end
# Returns the Boolean of whether this Page is an index file or not.
def index?
basename == "index"
end
def trigger_hooks(hook_name, *args)
Jekyll::Hooks.trigger :pages, hook_name, self, *args
end
def write?
true
end
def excerpt_separator
@excerpt_separator ||= (data["excerpt_separator"] || site.config["excerpt_separator"]).to_s
end
def excerpt
return @excerpt if defined?(@excerpt)
@excerpt = data["excerpt"] ? data["excerpt"].to_s : nil
end
def generate_excerpt?
!excerpt_separator.empty? && instance_of?(Jekyll::Page) && html?
end
private
def generate_excerpt
return unless generate_excerpt?
data["excerpt"] ||= Jekyll::PageExcerpt.new(self)
end
def url_dir
@url_dir ||= begin
value = File.dirname(url)
value.end_with?("/") ? value : "#{value}/"
end
end
end
end
@@ -0,0 +1,25 @@
# frozen_string_literal: true
module Jekyll
class PageExcerpt < Excerpt
attr_reader :doc
alias_method :id, :relative_path
EXCERPT_ATTRIBUTES = (Page::ATTRIBUTES_FOR_LIQUID - %w(excerpt)).freeze
private_constant :EXCERPT_ATTRIBUTES
def to_liquid
@to_liquid ||= doc.to_liquid(EXCERPT_ATTRIBUTES)
end
def render_with_liquid?
return false if data["render_with_liquid"] == false
Jekyll::Utils.has_liquid_construct?(content)
end
def inspect
"#<#{self.class} id=#{id.inspect}>"
end
end
end
@@ -0,0 +1,14 @@
# frozen_string_literal: true
module Jekyll
# A Jekyll::Page subclass to handle processing files without reading it to
# determine the page-data and page-content based on Front Matter delimiters.
#
# The class instance is basically just a bare-bones entity with just
# attributes "dir", "name", "path", "url" defined on it.
class PageWithoutAFile < Page
def read_yaml(*)
@data ||= {}
end
end
end
@@ -0,0 +1,74 @@
# frozen_string_literal: true
module Jekyll
# A singleton class that caches frozen instances of path strings returned from its methods.
#
# NOTE:
# This class exists because `File.join` allocates an Array and returns a new String on every
# call using **the same arguments**. Caching the result means reduced memory usage.
# However, the caches are never flushed so that they can be used even when a site is
# regenerating. The results are frozen to deter mutation of the cached string.
#
# Therefore, employ this class only for situations where caching the result is necessary
# for performance reasons.
#
class PathManager
# This class cannot be initialized from outside
private_class_method :new
class << self
# Wraps `File.join` to cache the frozen result.
# Reassigns `nil`, empty strings and empty arrays to a frozen empty string beforehand.
#
# Returns a frozen string.
def join(base, item)
base = "" if base.nil? || base.empty?
item = "" if item.nil? || item.empty?
@join ||= {}
@join[base] ||= {}
@join[base][item] ||= File.join(base, item).freeze
end
# Ensures the questionable path is prefixed with the base directory
# and prepends the questionable path with the base directory if false.
#
# Returns a frozen string.
def sanitized_path(base_directory, questionable_path)
@sanitized_path ||= {}
@sanitized_path[base_directory] ||= {}
@sanitized_path[base_directory][questionable_path] ||= if questionable_path.nil?
base_directory.freeze
else
sanitize_and_join(
base_directory, questionable_path
).freeze
end
end
private
def sanitize_and_join(base_directory, questionable_path)
clean_path = if questionable_path.start_with?("~")
questionable_path.dup.insert(0, "/")
else
questionable_path
end
clean_path = File.expand_path(clean_path, "/")
return clean_path if clean_path.eql?(base_directory)
# remove any remaining extra leading slashes not stripped away by calling
# `File.expand_path` above.
clean_path.squeeze!("/")
return clean_path if clean_path.start_with?(slashed_dir_cache(base_directory))
clean_path.sub!(%r!\A\w:/!, "/")
join(base_directory, clean_path)
end
def slashed_dir_cache(base_directory)
@slashed_dir_cache ||= {}
@slashed_dir_cache[base_directory] ||= base_directory.sub(%r!\z!, "/")
end
end
end
end
@@ -0,0 +1,92 @@
# frozen_string_literal: true
module Jekyll
class Plugin
PRIORITIES = {
:low => -10,
:highest => 100,
:lowest => -100,
:normal => 0,
:high => 10,
}.freeze
#
def self.inherited(const)
catch_inheritance(const) do |const_|
catch_inheritance(const_)
end
end
#
def self.catch_inheritance(const)
const.define_singleton_method :inherited do |const_|
(@children ||= Set.new).add const_
yield const_ if block_given?
end
end
#
def self.descendants
@children ||= Set.new
out = @children.map(&:descendants)
out << self unless superclass == Plugin
Set.new(out).flatten
end
# Get or set the priority of this plugin. When called without an
# argument it returns the priority. When an argument is given, it will
# set the priority.
#
# priority - The Symbol priority (default: nil). Valid options are:
# :lowest, :low, :normal, :high, :highest
#
# Returns the Symbol priority.
def self.priority(priority = nil)
@priority ||= nil
@priority = priority if priority && PRIORITIES.key?(priority)
@priority || :normal
end
# Get or set the safety of this plugin. When called without an argument
# it returns the safety. When an argument is given, it will set the
# safety.
#
# safe - The Boolean safety (default: nil).
#
# Returns the safety Boolean.
def self.safe(safe = nil)
@safe = safe unless defined?(@safe) && safe.nil?
@safe || false
end
# Spaceship is priority [higher -> lower]
#
# other - The class to be compared.
#
# Returns -1, 0, 1.
def self.<=>(other)
PRIORITIES[other.priority] <=> PRIORITIES[priority]
end
# Spaceship is priority [higher -> lower]
#
# other - The class to be compared.
#
# Returns -1, 0, 1.
def <=>(other)
self.class <=> other.class
end
# Initialize a new plugin. This should be overridden by the subclass.
#
# config - The Hash of configuration options.
#
# Returns a new instance.
def initialize(config = {})
# no-op for default
end
end
end
@@ -0,0 +1,123 @@
# frozen_string_literal: true
module Jekyll
class PluginManager
attr_reader :site
# Create an instance of this class.
#
# site - the instance of Jekyll::Site we're concerned with
#
# Returns nothing
def initialize(site)
@site = site
end
# Require all the plugins which are allowed.
#
# Returns nothing
def conscientious_require
require_theme_deps if site.theme
require_plugin_files
require_gems
deprecation_checks
end
# Require each of the gem plugins specified.
#
# Returns nothing.
def require_gems
Jekyll::External.require_with_graceful_fail(
site.gems.select { |plugin| plugin_allowed?(plugin) }
)
end
# Require each of the runtime_dependencies specified by the theme's gemspec.
#
# Returns false only if no dependencies have been specified, otherwise nothing.
def require_theme_deps
return false unless site.theme.runtime_dependencies
site.theme.runtime_dependencies.each do |dep|
next if dep.name == "jekyll"
External.require_with_graceful_fail(dep.name) if plugin_allowed?(dep.name)
end
end
def self.require_from_bundler
if !ENV["JEKYLL_NO_BUNDLER_REQUIRE"] && gemfile_exists?
require "bundler"
Bundler.setup
required_gems = Bundler.require(:jekyll_plugins)
message = "Required #{required_gems.map(&:name).join(", ")}"
Jekyll.logger.debug("PluginManager:", message)
ENV["JEKYLL_NO_BUNDLER_REQUIRE"] = "true"
true
else
false
end
end
# Check for the existence of a Gemfile.
#
# Returns true if a Gemfile exists in the places bundler will look
def self.gemfile_exists?
File.file?("Gemfile") || (ENV["BUNDLE_GEMFILE"] && File.file?(ENV["BUNDLE_GEMFILE"]))
end
# Check whether a gem plugin is allowed to be used during this build.
#
# plugin_name - the name of the plugin
#
# Returns true if the plugin name is in the whitelist or if the site is not
# in safe mode.
def plugin_allowed?(plugin_name)
!site.safe || whitelist.include?(plugin_name)
end
# Build an array of allowed plugin gem names.
#
# Returns an array of strings, each string being the name of a gem name
# that is allowed to be used.
def whitelist
@whitelist ||= [site.config["whitelist"]].flatten
end
# Require all .rb files if safe mode is off
#
# Returns nothing.
def require_plugin_files
unless site.safe
plugins_path.each do |plugin_search_path|
plugin_files = Utils.safe_glob(plugin_search_path, File.join("**", "*.rb"))
Jekyll::External.require_with_graceful_fail(plugin_files)
end
end
end
# Public: Setup the plugin search path
#
# Returns an Array of plugin search paths
def plugins_path
if site.config["plugins_dir"].eql? Jekyll::Configuration::DEFAULTS["plugins_dir"]
[site.in_source_dir(site.config["plugins_dir"])]
else
Array(site.config["plugins_dir"]).map { |d| File.expand_path(d) }
end
end
def deprecation_checks
pagination_included = (site.config["plugins"] || []).include?("jekyll-paginate") ||
defined?(Jekyll::Paginate)
if site.config["paginate"] && !pagination_included
Jekyll::Deprecator.deprecation_message <<~MSG
You appear to have pagination turned on, but you haven't included the `jekyll-paginate`
gem. Ensure you have `plugins: [jekyll-paginate]` in your configuration file.
MSG
end
end
end
end
@@ -0,0 +1,51 @@
# frozen_string_literal: true
module Jekyll
class Profiler
TERMINAL_TABLE_STYLES = {
:alignment => :right,
:border_top => false,
:border_bottom => false,
}.freeze
private_constant :TERMINAL_TABLE_STYLES
def self.tabulate(table_rows)
require "terminal-table"
rows = table_rows.dup
header = rows.shift
output = +"\n"
table = Terminal::Table.new do |t|
t << header
t << :separator
rows.each { |row| t << row }
t.style = TERMINAL_TABLE_STYLES
t.align_column(0, :left)
end
output << table.to_s << "\n"
end
def initialize(site)
@site = site
end
def profile_process
profile_data = { "PHASE" => "TIME" }
[:reset, :read, :generate, :render, :cleanup, :write].each do |method|
start_time = Time.now
@site.send(method)
end_time = (Time.now - start_time).round(4)
profile_data[method.to_s.upcase] = format("%.4f", end_time)
end
Jekyll.logger.info "\nBuild Process Summary:"
Jekyll.logger.info Profiler.tabulate(Array(profile_data))
Jekyll.logger.info "\nSite Render Stats:"
@site.print_stats
end
end
end
@@ -0,0 +1,23 @@
# frozen_string_literal: true
module Jekyll
class Publisher
def initialize(site)
@site = site
end
def publish?(thing)
can_be_published?(thing) && !hidden_in_the_future?(thing)
end
def hidden_in_the_future?(thing)
thing.respond_to?(:date) && !@site.future && thing.date.to_i > @site.time.to_i
end
private
def can_be_published?(thing)
thing.data.fetch("published", true) || @site.unpublished
end
end
end
+209
View File
@@ -0,0 +1,209 @@
# frozen_string_literal: true
module Jekyll
class Reader
attr_reader :site
def initialize(site)
@site = site
end
# Read Site data from disk and load it into internal data structures.
#
# Returns nothing.
def read
@site.layouts = LayoutReader.new(site).read
read_directories
read_included_excludes
sort_files!
CollectionReader.new(site).read
ThemeAssetsReader.new(site).read
read_data
end
# Read and merge the data files.
# If a theme is specified and it contains data, it will be read.
# Site data will overwrite theme data with the same key using the
# semantics of Utils.deep_merge_hashes.
#
# Returns nothing.
def read_data
@site.data = DataReader.new(site).read(site.config["data_dir"])
return unless site.theme&.data_path
theme_data = DataReader.new(
site,
:in_source_dir => site.method(:in_theme_dir)
).read(site.theme.data_path)
@site.data = Jekyll::Utils.deep_merge_hashes(theme_data, @site.data)
end
# Sorts posts, pages, and static files.
def sort_files!
site.collections.each_value { |c| c.docs.sort! }
site.pages.sort_by!(&:name)
site.static_files.sort_by!(&:relative_path)
end
# Recursively traverse directories to find pages and static files
# that will become part of the site according to the rules in
# filter_entries.
#
# dir - The String relative path of the directory to read. Default: ''.
#
# Returns nothing.
def read_directories(dir = "")
base = site.in_source_dir(dir)
return unless File.directory?(base)
dot_dirs = []
dot_pages = []
dot_static_files = []
dot = Dir.chdir(base) { filter_entries(Dir.entries("."), base) }
dot.each do |entry|
file_path = @site.in_source_dir(base, entry)
if File.directory?(file_path)
dot_dirs << entry
elsif Utils.has_yaml_header?(file_path)
dot_pages << entry
else
dot_static_files << entry
end
end
retrieve_posts(dir)
retrieve_dirs(base, dir, dot_dirs)
retrieve_pages(dir, dot_pages)
retrieve_static_files(dir, dot_static_files)
end
# Retrieves all the posts(posts/drafts) from the given directory
# and add them to the site and sort them.
#
# dir - The String representing the directory to retrieve the posts from.
#
# Returns nothing.
def retrieve_posts(dir)
return if outside_configured_directory?(dir)
site.posts.docs.concat(post_reader.read_posts(dir))
site.posts.docs.concat(post_reader.read_drafts(dir)) if site.show_drafts
end
# Recursively traverse directories with the read_directories function.
#
# base - The String representing the site's base directory.
# dir - The String representing the directory to traverse down.
# dot_dirs - The Array of subdirectories in the dir.
#
# Returns nothing.
def retrieve_dirs(_base, dir, dot_dirs)
dot_dirs.each do |file|
dir_path = site.in_source_dir(dir, file)
rel_path = PathManager.join(dir, file)
@site.reader.read_directories(rel_path) unless @site.dest.chomp("/") == dir_path
end
end
# Retrieve all the pages from the current directory,
# add them to the site and sort them.
#
# dir - The String representing the directory retrieve the pages from.
# dot_pages - The Array of pages in the dir.
#
# Returns nothing.
def retrieve_pages(dir, dot_pages)
site.pages.concat(PageReader.new(site, dir).read(dot_pages))
end
# Retrieve all the static files from the current directory,
# add them to the site and sort them.
#
# dir - The directory retrieve the static files from.
# dot_static_files - The static files in the dir.
#
# Returns nothing.
def retrieve_static_files(dir, dot_static_files)
site.static_files.concat(StaticFileReader.new(site, dir).read(dot_static_files))
end
# Filter out any files/directories that are hidden or backup files (start
# with "." or "#" or end with "~"), or contain site content (start with "_"),
# or are excluded in the site configuration, unless they are web server
# files such as '.htaccess'.
#
# entries - The Array of String file/directory entries to filter.
# base_directory - The string representing the optional base directory.
#
# Returns the Array of filtered entries.
def filter_entries(entries, base_directory = nil)
EntryFilter.new(site, base_directory).filter(entries)
end
# Read the entries from a particular directory for processing
#
# dir - The String representing the relative path of the directory to read.
# subfolder - The String representing the directory to read.
#
# Returns the list of entries to process
def get_entries(dir, subfolder)
base = site.in_source_dir(dir, subfolder)
return [] unless File.exist?(base)
entries = Dir.chdir(base) { filter_entries(Dir["**/*"], base) }
entries.delete_if { |e| File.directory?(site.in_source_dir(base, e)) }
end
private
# Internal
#
# Determine if the directory is supposed to contain posts and drafts.
# If the user has defined a custom collections_dir, then attempt to read
# posts and drafts only from within that directory.
#
# Returns true if a custom collections_dir has been set but current directory lies
# outside that directory.
def outside_configured_directory?(dir)
collections_dir = site.config["collections_dir"]
!collections_dir.empty? && !dir.start_with?("/#{collections_dir}")
end
# Create a single PostReader instance to retrieve drafts and posts from all valid
# directories in current site.
def post_reader
@post_reader ||= PostReader.new(site)
end
def read_included_excludes
entry_filter = EntryFilter.new(site)
site.include.each do |entry|
entry_path = site.in_source_dir(entry)
next if File.directory?(entry_path)
next if entry_filter.symlink?(entry_path)
read_included_file(entry_path) if File.file?(entry_path)
end
end
def read_included_file(entry_path)
if Utils.has_yaml_header?(entry_path)
conditionally_generate_entry(entry_path, site.pages, PageReader)
else
conditionally_generate_entry(entry_path, site.static_files, StaticFileReader)
end
end
def conditionally_generate_entry(entry_path, container, reader)
return if container.find { |item| site.in_source_dir(item.relative_path) == entry_path }
dir, files = File.split(entry_path)
dir.sub!(site.source, "")
files = Array(files)
container.concat(reader.new(site, dir).read(files))
end
end
end
@@ -0,0 +1,23 @@
# frozen_string_literal: true
module Jekyll
class CollectionReader
SPECIAL_COLLECTIONS = %w(posts data).freeze
attr_reader :site, :content
def initialize(site)
@site = site
@content = {}
end
# Read in all collections specified in the configuration
#
# Returns nothing.
def read
site.collections.each_value do |collection|
collection.read unless SPECIAL_COLLECTIONS.include?(collection.label)
end
end
end
end
@@ -0,0 +1,113 @@
# frozen_string_literal: true
module Jekyll
class DataReader
attr_reader :site, :content
def initialize(site, in_source_dir: nil)
@site = site
@content = {}
@entry_filter = EntryFilter.new(site)
@in_source_dir = in_source_dir || @site.method(:in_source_dir)
@source_dir = @in_source_dir.call("/")
end
# Read all the files in <dir> and adds them to @content
#
# dir - The String relative path of the directory to read.
#
# Returns @content, a Hash of the .yaml, .yml,
# .json, and .csv files in the base directory
def read(dir)
base = @in_source_dir.call(dir)
read_data_to(base, @content)
@content
end
# Read and parse all .yaml, .yml, .json, .csv and .tsv
# files under <dir> and add them to the <data> variable.
#
# dir - The string absolute path of the directory to read.
# data - The variable to which data will be added.
#
# Returns nothing
def read_data_to(dir, data)
return unless File.directory?(dir) && !@entry_filter.symlink?(dir)
entries = Dir.chdir(dir) do
Dir["*.{yaml,yml,json,csv,tsv}"] + Dir["*"].select { |fn| File.directory?(fn) }
end
entries.each do |entry|
path = @in_source_dir.call(dir, entry)
next if @entry_filter.symlink?(path)
if File.directory?(path)
read_data_to(path, data[sanitize_filename(entry)] = {})
else
key = sanitize_filename(File.basename(entry, ".*"))
data[key] = read_data_file(path)
end
end
end
# Determines how to read a data file.
#
# Returns the contents of the data file.
def read_data_file(path)
Jekyll.logger.debug "Reading:", path.sub(@source_dir, "")
case File.extname(path).downcase
when ".csv"
CSV.read(path, **csv_config).map { |row| convert_row(row) }
when ".tsv"
CSV.read(path, **tsv_config).map { |row| convert_row(row) }
else
SafeYAML.load_file(path)
end
end
def sanitize_filename(name)
name.gsub(%r![^\w\s-]+|(?<=^|\b\s)\s+(?=$|\s?\b)!, "")
.gsub(%r!\s+!, "_")
end
private
# @return [Hash]
def csv_config
@csv_config ||= read_config("csv_reader")
end
# @return [Hash]
def tsv_config
@tsv_config ||= read_config("tsv_reader", { :col_sep => "\t" })
end
# @param config_key [String]
# @param overrides [Hash]
# @return [Hash]
# @see https://ruby-doc.org/stdlib-2.5.0/libdoc/csv/rdoc/CSV.html#Converters
def read_config(config_key, overrides = {})
reader_config = config[config_key] || {}
defaults = {
:converters => reader_config.fetch("csv_converters", []).map(&:to_sym),
:headers => reader_config.fetch("headers", true),
:encoding => reader_config.fetch("encoding", config["encoding"]),
}
defaults.merge(overrides)
end
def config
@config ||= site.config
end
# @param row [Array, CSV::Row]
# @return [Array, Hash]
def convert_row(row)
row.instance_of?(CSV::Row) ? row.to_hash : row
end
end
end
@@ -0,0 +1,62 @@
# frozen_string_literal: true
module Jekyll
class LayoutReader
attr_reader :site
def initialize(site)
@site = site
@layouts = {}
end
def read
layout_entries.each do |layout_file|
@layouts[layout_name(layout_file)] = \
Layout.new(site, layout_directory, layout_file)
end
theme_layout_entries.each do |layout_file|
@layouts[layout_name(layout_file)] ||= \
Layout.new(site, theme_layout_directory, layout_file)
end
@layouts
end
def layout_directory
@layout_directory ||= site.in_source_dir(site.config["layouts_dir"])
end
def theme_layout_directory
@theme_layout_directory ||= site.theme.layouts_path if site.theme
end
private
def layout_entries
entries_in layout_directory
end
def theme_layout_entries
theme_layout_directory ? entries_in(theme_layout_directory) : []
end
def entries_in(dir)
entries = []
within(dir) do
entries = EntryFilter.new(site).filter(Dir["**/*.*"])
end
entries
end
def layout_name(file)
file.split(".")[0..-2].join(".")
end
def within(directory)
return unless File.exist?(directory)
Dir.chdir(directory) { yield }
end
end
end
@@ -0,0 +1,25 @@
# frozen_string_literal: true
module Jekyll
class PageReader
attr_reader :site, :dir, :unfiltered_content
def initialize(site, dir)
@site = site
@dir = dir
@unfiltered_content = []
end
# Create a new `Jekyll::Page` object for each entry in a given array.
#
# files - An array of file names inside `@dir`
#
# Returns an array of publishable `Jekyll::Page` objects.
def read(files)
files.each do |page|
@unfiltered_content << Page.new(@site, @site.source, @dir, page)
end
@unfiltered_content.select { |page| site.publisher.publish?(page) }
end
end
end
@@ -0,0 +1,85 @@
# frozen_string_literal: true
module Jekyll
class PostReader
attr_reader :site, :unfiltered_content
def initialize(site)
@site = site
end
# Read all the files in <source>/<dir>/_drafts and create a new
# Document object with each one.
#
# dir - The String relative path of the directory to read.
#
# Returns nothing.
def read_drafts(dir)
read_publishable(dir, "_drafts", Document::DATELESS_FILENAME_MATCHER)
end
# Read all the files in <source>/<dir>/_posts and create a new Document
# object with each one.
#
# dir - The String relative path of the directory to read.
#
# Returns nothing.
def read_posts(dir)
read_publishable(dir, "_posts", Document::DATE_FILENAME_MATCHER)
end
# Read all the files in <source>/<dir>/<magic_dir> and create a new
# Document object with each one insofar as it matches the regexp matcher.
#
# dir - The String relative path of the directory to read.
#
# Returns nothing.
def read_publishable(dir, magic_dir, matcher)
read_content(dir, magic_dir, matcher)
.tap { |docs| docs.each(&:read) }
.select { |doc| processable?(doc) }
end
# Read all the content files from <source>/<dir>/magic_dir
# and return them with the type klass.
#
# dir - The String relative path of the directory to read.
# magic_dir - The String relative directory to <dir>,
# looks for content here.
# klass - The return type of the content.
#
# Returns klass type of content files
def read_content(dir, magic_dir, matcher)
@site.reader.get_entries(dir, magic_dir).map do |entry|
next unless matcher.match?(entry)
path = @site.in_source_dir(File.join(dir, magic_dir, entry))
Document.new(path,
:site => @site,
:collection => @site.posts)
end.tap(&:compact!)
end
private
def processable?(doc)
if doc.content.nil?
Jekyll.logger.debug "Skipping:", "Content in #{doc.relative_path} is nil"
false
elsif !doc.content.valid_encoding?
Jekyll.logger.debug "Skipping:", "#{doc.relative_path} is not valid UTF-8"
false
else
publishable?(doc)
end
end
def publishable?(doc)
site.publisher.publish?(doc).tap do |will_publish|
if !will_publish && site.publisher.hidden_in_the_future?(doc)
Jekyll.logger.warn "Skipping:", "#{doc.relative_path} has a future date"
end
end
end
end
end
@@ -0,0 +1,25 @@
# frozen_string_literal: true
module Jekyll
class StaticFileReader
attr_reader :site, :dir, :unfiltered_content
def initialize(site, dir)
@site = site
@dir = dir
@unfiltered_content = []
end
# Create a new StaticFile object for every entry in a given list of basenames.
#
# files - an array of file basenames.
#
# Returns an array of static files.
def read(files)
files.each do |file|
@unfiltered_content << StaticFile.new(@site, @site.source, @dir, file)
end
@unfiltered_content
end
end
end
@@ -0,0 +1,52 @@
# frozen_string_literal: true
module Jekyll
class ThemeAssetsReader
attr_reader :site
def initialize(site)
@site = site
end
def read
return unless site.theme&.assets_path
Find.find(site.theme.assets_path) do |path|
next if File.directory?(path)
if File.symlink?(path)
Jekyll.logger.warn "Theme reader:", "Ignored symlinked asset: #{path}"
else
read_theme_asset(path)
end
end
end
private
def read_theme_asset(path)
base = site.theme.root
dir = File.dirname(path.sub("#{site.theme.root}/", ""))
name = File.basename(path)
if Utils.has_yaml_header?(path)
append_unless_exists site.pages,
Jekyll::Page.new(site, base, dir, name)
else
append_unless_exists site.static_files,
Jekyll::StaticFile.new(site, base, "/#{dir}", name)
end
end
def append_unless_exists(haystack, new_item)
if haystack.any? { |file| file.relative_path == new_item.relative_path }
Jekyll.logger.debug "Theme:",
"Ignoring #{new_item.relative_path} in theme due to existing file " \
"with that path in site."
return
end
haystack << new_item
end
end
end
@@ -0,0 +1,195 @@
# frozen_string_literal: true
module Jekyll
class Regenerator
attr_reader :site, :metadata, :cache
attr_accessor :disabled
private :disabled, :disabled=
def initialize(site)
@site = site
# Read metadata from file
read_metadata
# Initialize cache to an empty hash
clear_cache
end
# Checks if a renderable object needs to be regenerated
#
# Returns a boolean.
def regenerate?(document)
return true if disabled
case document
when Page
regenerate_page?(document)
when Document
regenerate_document?(document)
else
source_path = document.respond_to?(:path) ? document.path : nil
dest_path = document.destination(@site.dest) if document.respond_to?(:destination)
source_modified_or_dest_missing?(source_path, dest_path)
end
end
# Add a path to the metadata
#
# Returns true, also on failure.
def add(path)
return true unless File.exist?(path)
metadata[path] = {
"mtime" => File.mtime(path),
"deps" => [],
}
cache[path] = true
end
# Force a path to regenerate
#
# Returns true.
def force(path)
cache[path] = true
end
# Clear the metadata and cache
#
# Returns nothing
def clear
@metadata = {}
clear_cache
end
# Clear just the cache
#
# Returns nothing
def clear_cache
@cache = {}
end
# Checks if the source has been modified or the
# destination is missing
#
# returns a boolean
def source_modified_or_dest_missing?(source_path, dest_path)
modified?(source_path) || (dest_path && !File.exist?(dest_path))
end
# Checks if a path's (or one of its dependencies)
# mtime has changed
#
# Returns a boolean.
def modified?(path)
return true if disabled?
# objects that don't have a path are always regenerated
return true if path.nil?
# Check for path in cache
return cache[path] if cache.key? path
if metadata[path]
# If we have seen this file before,
# check if it or one of its dependencies has been modified
existing_file_modified?(path)
else
# If we have not seen this file before, add it to the metadata and regenerate it
add(path)
end
end
# Add a dependency of a path
#
# Returns nothing.
def add_dependency(path, dependency)
return if metadata[path].nil? || disabled
unless metadata[path]["deps"].include? dependency
metadata[path]["deps"] << dependency
add(dependency) unless metadata.include?(dependency)
end
regenerate? dependency
end
# Write the metadata to disk
#
# Returns nothing.
def write_metadata
unless disabled?
Jekyll.logger.debug "Writing Metadata:", ".jekyll-metadata"
File.binwrite(metadata_file, Marshal.dump(metadata))
end
end
# Produce the absolute path of the metadata file
#
# Returns the String path of the file.
def metadata_file
@metadata_file ||= site.in_source_dir(".jekyll-metadata")
end
# Check if metadata has been disabled
#
# Returns a Boolean (true for disabled, false for enabled).
def disabled?
self.disabled = !site.incremental? if disabled.nil?
disabled
end
private
# Read metadata from the metadata file, if no file is found,
# initialize with an empty hash
#
# Returns the read metadata.
def read_metadata
@metadata =
if !disabled? && File.file?(metadata_file)
content = File.binread(metadata_file)
begin
Marshal.load(content)
rescue TypeError
SafeYAML.load(content)
rescue ArgumentError => e
Jekyll.logger.warn("Failed to load #{metadata_file}: #{e}")
{}
end
else
{}
end
end
def regenerate_page?(document)
document.asset_file? || document.data["regenerate"] ||
source_modified_or_dest_missing?(
site.in_source_dir(document.relative_path), document.destination(@site.dest)
)
end
def regenerate_document?(document)
!document.write? || document.data["regenerate"] ||
source_modified_or_dest_missing?(
document.path, document.destination(@site.dest)
)
end
def existing_file_modified?(path)
# If one of this file dependencies have been modified,
# set the regeneration bit for both the dependency and the file to true
metadata[path]["deps"].each do |dependency|
return cache[dependency] = cache[path] = true if modified?(dependency)
end
if File.exist?(path) && metadata[path]["mtime"].eql?(File.mtime(path))
# If this file has not been modified, set the regeneration bit to false
cache[path] = false
else
# If it has been modified, set it to true
add(path)
end
end
end
end
@@ -0,0 +1,52 @@
# frozen_string_literal: true
module Jekyll
class RelatedPosts
class << self
attr_accessor :lsi
end
attr_reader :post, :site
def initialize(post)
@post = post
@site = post.site
Jekyll::External.require_with_graceful_fail("classifier-reborn") if site.lsi
end
def build
return [] unless site.posts.docs.size > 1
if site.lsi
build_index
lsi_related_posts
else
most_recent_posts
end
end
def build_index
self.class.lsi ||= begin
lsi = ClassifierReborn::LSI.new(:auto_rebuild => false)
Jekyll.logger.info("Populating LSI...")
site.posts.docs.each do |x|
lsi.add_item(x)
end
Jekyll.logger.info("Rebuilding index...")
lsi.build_index
Jekyll.logger.info("")
lsi
end
end
def lsi_related_posts
self.class.lsi.find_related(post, 11)
end
def most_recent_posts
@most_recent_posts ||= (site.posts.docs.last(11).reverse! - [post]).first(10)
end
end
end
@@ -0,0 +1,263 @@
# frozen_string_literal: true
module Jekyll
class Renderer
attr_reader :document, :site
attr_writer :layouts, :payload
def initialize(site, document, site_payload = nil)
@site = site
@document = document
@payload = site_payload
@layouts = nil
end
# Fetches the payload used in Liquid rendering.
# It can be written with #payload=(new_payload)
# Falls back to site.site_payload if no payload is set.
#
# Returns a Jekyll::Drops::UnifiedPayloadDrop
def payload
@payload ||= site.site_payload
end
# The list of layouts registered for this Renderer.
# It can be written with #layouts=(new_layouts)
# Falls back to site.layouts if no layouts are registered.
#
# Returns a Hash of String => Jekyll::Layout identified
# as basename without the extension name.
def layouts
@layouts || site.layouts
end
# Determine which converters to use based on this document's
# extension.
#
# Returns Array of Converter instances.
def converters
@converters ||= site.converters.select { |c| c.matches(document.extname) }.tap(&:sort!)
end
# Determine the extname the outputted file should have
#
# Returns String the output extname including the leading period.
def output_ext
@output_ext ||= (permalink_ext || converter_output_ext)
end
# Prepare payload and render the document
#
# Returns String rendered document output
def run
Jekyll.logger.debug "Rendering:", document.relative_path
assign_pages!
assign_current_document!
assign_highlighter_options!
assign_layout_data!
Jekyll.logger.debug "Pre-Render Hooks:", document.relative_path
document.trigger_hooks(:pre_render, payload)
render_document
end
# Render the document.
#
# Returns String rendered document output
# rubocop: disable Metrics/AbcSize, Metrics/MethodLength
def render_document
info = {
:registers => { :site => site, :page => payload["page"] },
:strict_filters => liquid_options["strict_filters"],
:strict_variables => liquid_options["strict_variables"],
}
output = document.content
if document.render_with_liquid?
Jekyll.logger.debug "Rendering Liquid:", document.relative_path
output = render_liquid(output, payload, info, document.path)
end
Jekyll.logger.debug "Rendering Markup:", document.relative_path
output = convert(output.to_s)
document.content = output
Jekyll.logger.debug "Post-Convert Hooks:", document.relative_path
document.trigger_hooks(:post_convert)
output = document.content
if document.place_in_layout?
Jekyll.logger.debug "Rendering Layout:", document.relative_path
output = place_in_layouts(output, payload, info)
end
output
end
# rubocop: enable Metrics/AbcSize, Metrics/MethodLength
# Convert the document using the converters which match this renderer's document.
#
# Returns String the converted content.
def convert(content)
converters.reduce(content) do |output, converter|
converter.convert output
rescue StandardError => e
Jekyll.logger.error "Conversion error:",
"#{converter.class} encountered an error while " \
"converting '#{document.relative_path}':"
Jekyll.logger.error("", e.to_s)
raise e
end
end
# Render the given content with the payload and info
#
# content -
# payload -
# info -
# path - (optional) the path to the file, for use in ex
#
# Returns String the content, rendered by Liquid.
def render_liquid(content, payload, info, path = nil)
template = site.liquid_renderer.file(path).parse(content)
template.warnings.each do |e|
Jekyll.logger.warn "Liquid Warning:",
LiquidRenderer.format_error(e, path || document.relative_path)
end
template.render!(payload, info)
# rubocop: disable Lint/RescueException
rescue Exception => e
Jekyll.logger.error "Liquid Exception:",
LiquidRenderer.format_error(e, path || document.relative_path)
raise e
end
# rubocop: enable Lint/RescueException
# Checks if the layout specified in the document actually exists
#
# layout - the layout to check
#
# Returns Boolean true if the layout is invalid, false if otherwise
def invalid_layout?(layout)
!document.data["layout"].nil? && layout.nil? && !(document.is_a? Jekyll::Excerpt)
end
# Render layouts and place document content inside.
#
# Returns String rendered content
def place_in_layouts(content, payload, info)
output = content.dup
layout = layouts[document.data["layout"].to_s]
validate_layout(layout)
used = Set.new([layout])
# Reset the payload layout data to ensure it starts fresh for each page.
payload["layout"] = nil
while layout
output = render_layout(output, layout, info)
add_regenerator_dependencies(layout)
next unless (layout = site.layouts[layout.data["layout"]])
break if used.include?(layout)
used << layout
end
output
end
private
# Checks if the layout specified in the document actually exists
#
# layout - the layout to check
# Returns nothing
def validate_layout(layout)
return unless invalid_layout?(layout)
Jekyll.logger.warn "Build Warning:", "Layout '#{document.data["layout"]}' requested " \
"in #{document.relative_path} does not exist."
end
# Render layout content into document.output
#
# Returns String rendered content
def render_layout(output, layout, info)
payload["content"] = output
payload["layout"] = Utils.deep_merge_hashes(layout.data, payload["layout"] || {})
render_liquid(
layout.content,
payload,
info,
layout.path
)
end
def add_regenerator_dependencies(layout)
return unless document.write?
site.regenerator.add_dependency(
site.in_source_dir(document.path),
layout.path
)
end
# Set page content to payload and assign pager if document has one.
#
# Returns nothing
def assign_pages!
payload["page"] = document.to_liquid
payload["paginator"] = (document.pager.to_liquid if document.respond_to?(:pager))
end
# Set related posts to payload if document is a post.
#
# Returns nothing
def assign_current_document!
payload["site"].current_document = document
end
# Set highlighter prefix and suffix
#
# Returns nothing
def assign_highlighter_options!
payload["highlighter_prefix"] = converters.first.highlighter_prefix
payload["highlighter_suffix"] = converters.first.highlighter_suffix
end
def assign_layout_data!
layout = layouts[document.data["layout"]]
payload["layout"] = Utils.deep_merge_hashes(layout.data, payload["layout"] || {}) if layout
end
def permalink_ext
document_permalink = document.permalink
if document_permalink && !document_permalink.end_with?("/")
permalink_ext = File.extname(document_permalink)
permalink_ext unless permalink_ext.empty?
end
end
def converter_output_ext
if output_exts.size == 1
output_exts.last
else
output_exts[-2]
end
end
def output_exts
@output_exts ||= converters.map do |c|
c.output_ext(document.extname)
end.tap(&:compact!)
end
def liquid_options
@liquid_options ||= site.config["liquid"]
end
end
end
+576
View File
@@ -0,0 +1,576 @@
# frozen_string_literal: true
module Jekyll
class Site
attr_accessor :baseurl, :converters, :data, :drafts, :exclude,
:file_read_opts, :future, :gems, :generators, :highlighter,
:include, :inclusions, :keep_files, :layouts, :limit_posts,
:lsi, :pages, :permalink_style, :plugin_manager, :plugins,
:reader, :safe, :show_drafts, :static_files, :theme, :time,
:unpublished
attr_reader :cache_dir, :config, :dest, :filter_cache, :includes_load_paths,
:liquid_renderer, :profiler, :regenerator, :source
# Public: Initialize a new Site.
#
# config - A Hash containing site configuration details.
def initialize(config)
# Source and destination may not be changed after the site has been created.
@source = File.expand_path(config["source"]).freeze
@dest = File.expand_path(config["destination"]).freeze
self.config = config
@cache_dir = in_source_dir(config["cache_dir"])
@filter_cache = {}
@reader = Reader.new(self)
@profiler = Profiler.new(self)
@regenerator = Regenerator.new(self)
@liquid_renderer = LiquidRenderer.new(self)
Jekyll.sites << self
reset
setup
Jekyll::Hooks.trigger :site, :after_init, self
end
# Public: Set the site's configuration. This handles side-effects caused by
# changing values in the configuration.
#
# config - a Jekyll::Configuration, containing the new configuration.
#
# Returns the new configuration.
def config=(config)
@config = config.clone
%w(safe lsi highlighter baseurl exclude include future unpublished
show_drafts limit_posts keep_files).each do |opt|
send(:"#{opt}=", config[opt])
end
# keep using `gems` to avoid breaking change
self.gems = config["plugins"]
configure_cache
configure_plugins
configure_theme
configure_include_paths
configure_file_read_opts
self.permalink_style = config["permalink"].to_sym
# Read in a _config.yml from the current theme-gem at the very end.
@config = load_theme_configuration(config) if theme
@config
end
# Public: Read, process, and write this Site to output.
#
# Returns nothing.
def process
return profiler.profile_process if config["profile"]
reset
read
generate
render
cleanup
write
end
def print_stats
Jekyll.logger.info @liquid_renderer.stats_table
end
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
#
# Reset Site details.
#
# Returns nothing
def reset
self.time = if config["time"]
Utils.parse_date(config["time"].to_s, "Invalid time in _config.yml.")
else
Time.now
end
self.layouts = {}
self.inclusions = {}
self.pages = []
self.static_files = []
self.data = {}
@post_attr_hash = {}
@site_data = nil
@collections = nil
@documents = nil
@docs_to_write = nil
@regenerator.clear_cache
@liquid_renderer.reset
@site_cleaner = nil
frontmatter_defaults.reset
raise ArgumentError, "limit_posts must be a non-negative number" if limit_posts.negative?
Jekyll::Cache.clear_if_config_changed config
Jekyll::Hooks.trigger :site, :after_reset, self
nil
end
# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/AbcSize
# Load necessary libraries, plugins, converters, and generators.
#
# Returns nothing.
def setup
ensure_not_in_dest
plugin_manager.conscientious_require
self.converters = instantiate_subclasses(Jekyll::Converter)
self.generators = instantiate_subclasses(Jekyll::Generator)
end
# Check that the destination dir isn't the source dir or a directory
# parent to the source dir.
def ensure_not_in_dest
dest_pathname = Pathname.new(dest)
Pathname.new(source).ascend do |path|
if path == dest_pathname
raise Errors::FatalException,
"Destination directory cannot be or contain the Source directory."
end
end
end
# The list of collections and their corresponding Jekyll::Collection instances.
# If config['collections'] is set, a new instance is created
# for each item in the collection, a new hash is returned otherwise.
#
# Returns a Hash containing collection name-to-instance pairs.
def collections
@collections ||= collection_names.each_with_object({}) do |name, hsh|
hsh[name] = Jekyll::Collection.new(self, name)
end
end
# The list of collection names.
#
# Returns an array of collection names from the configuration,
# or an empty array if the `collections` key is not set.
def collection_names
case config["collections"]
when Hash
config["collections"].keys
when Array
config["collections"]
when nil
[]
else
raise ArgumentError, "Your `collections` key must be a hash or an array."
end
end
# Read Site data from disk and load it into internal data structures.
#
# Returns nothing.
def read
reader.read
limit_posts!
Jekyll::Hooks.trigger :site, :post_read, self
nil
end
# Run each of the Generators.
#
# Returns nothing.
def generate
generators.each do |generator|
start = Time.now
generator.generate(self)
Jekyll.logger.debug "Generating:",
"#{generator.class} finished in #{Time.now - start} seconds."
end
nil
end
# Render the site to the destination.
#
# Returns nothing.
def render
relative_permalinks_are_deprecated
payload = site_payload
Jekyll::Hooks.trigger :site, :pre_render, self, payload
render_docs(payload)
render_pages(payload)
Jekyll::Hooks.trigger :site, :post_render, self, payload
nil
end
# Remove orphaned files and empty directories in destination.
#
# Returns nothing.
def cleanup
site_cleaner.cleanup!
nil
end
# Write static files, pages, and posts.
#
# Returns nothing.
def write
Jekyll::Commands::Doctor.conflicting_urls(self)
each_site_file do |item|
item.write(dest) if regenerator.regenerate?(item)
end
regenerator.write_metadata
Jekyll::Hooks.trigger :site, :post_write, self
nil
end
def posts
collections["posts"] ||= Collection.new(self, "posts")
end
# Construct a Hash of Posts indexed by the specified Post attribute.
#
# post_attr - The String name of the Post attribute.
#
# Examples
#
# post_attr_hash('categories')
# # => { 'tech' => [<Post A>, <Post B>],
# # 'ruby' => [<Post B>] }
#
# Returns the Hash: { attr => posts } where
# attr - One of the values for the requested attribute.
# posts - The Array of Posts with the given attr value.
def post_attr_hash(post_attr)
# Build a hash map based on the specified post attribute ( post attr =>
# array of posts ) then sort each array in reverse order.
@post_attr_hash[post_attr] ||= begin
hash = Hash.new { |h, key| h[key] = [] }
posts.docs.each do |p|
p.data[post_attr]&.each { |t| hash[t] << p }
end
hash.each_value { |posts| posts.sort!.reverse! }
hash
end
end
def tags
post_attr_hash("tags")
end
def categories
post_attr_hash("categories")
end
# Prepare site data for site payload. The method maintains backward compatibility
# if the key 'data' is already used in _config.yml.
#
# Returns the Hash to be hooked to site.data.
def site_data
@site_data ||= (config["data"] || data)
end
# The Hash payload containing site-wide data.
#
# Returns the Hash: { "site" => data } where data is a Hash with keys:
# "time" - The Time as specified in the configuration or the
# current time if none was specified.
# "posts" - The Array of Posts, sorted chronologically by post date
# and then title.
# "pages" - The Array of all Pages.
# "html_pages" - The Array of HTML Pages.
# "categories" - The Hash of category values and Posts.
# See Site#post_attr_hash for type info.
# "tags" - The Hash of tag values and Posts.
# See Site#post_attr_hash for type info.
def site_payload
Drops::UnifiedPayloadDrop.new self
end
alias_method :to_liquid, :site_payload
# Get the implementation class for the given Converter.
# Returns the Converter instance implementing the given Converter.
# klass - The Class of the Converter to fetch.
def find_converter_instance(klass)
@find_converter_instance ||= {}
@find_converter_instance[klass] ||= converters.find do |converter|
converter.instance_of?(klass)
end || \
raise("No Converters found for #{klass}")
end
# klass - class or module containing the subclasses.
# Returns array of instances of subclasses of parameter.
# Create array of instances of the subclasses of the class or module
# passed in as argument.
def instantiate_subclasses(klass)
klass.descendants.select { |c| !safe || c.safe }.tap do |result|
result.sort!
result.map! { |c| c.new(config) }
end
end
# Warns the user if permanent links are relative to the parent
# directory. As this is a deprecated function of Jekyll.
#
# Returns
def relative_permalinks_are_deprecated
if config["relative_permalinks"]
Jekyll.logger.abort_with "Since v3.0, permalinks for pages " \
"in subfolders must be relative to the " \
"site source directory, not the parent " \
"directory. Check https://jekyllrb.com/docs/upgrading/ " \
"for more info."
end
end
# Get the to be written documents
#
# Returns an Array of Documents which should be written
def docs_to_write
documents.select(&:write?)
end
# Get the to be written static files
#
# Returns an Array of StaticFiles which should be written
def static_files_to_write
static_files.select(&:write?)
end
# Get all the documents
#
# Returns an Array of all Documents
def documents
collections.each_with_object(Set.new) do |(_, collection), set|
set.merge(collection.docs).merge(collection.files)
end.to_a
end
def each_site_file
pages.each { |page| yield page }
static_files.each { |file| yield(file) if file.write? }
collections.each_value { |coll| coll.docs.each { |doc| yield(doc) if doc.write? } }
end
# Returns the FrontmatterDefaults or creates a new FrontmatterDefaults
# if it doesn't already exist.
#
# Returns The FrontmatterDefaults
def frontmatter_defaults
@frontmatter_defaults ||= FrontmatterDefaults.new(self)
end
# Whether to perform a full rebuild without incremental regeneration
#
# Returns a Boolean: true for a full rebuild, false for normal build
def incremental?(override = {})
override["incremental"] || config["incremental"]
end
# Returns the publisher or creates a new publisher if it doesn't
# already exist.
#
# Returns The Publisher
def publisher
@publisher ||= Publisher.new(self)
end
# Public: Prefix a given path with the source directory.
#
# paths - (optional) path elements to a file or directory within the
# source directory
#
# Returns a path which is prefixed with the source directory.
def in_source_dir(*paths)
paths.reduce(source) do |base, path|
Jekyll.sanitized_path(base, path)
end
end
# Public: Prefix a given path with the theme directory.
#
# paths - (optional) path elements to a file or directory within the
# theme directory
#
# Returns a path which is prefixed with the theme root directory.
def in_theme_dir(*paths)
return nil unless theme
paths.reduce(theme.root) do |base, path|
Jekyll.sanitized_path(base, path)
end
end
# Public: Prefix a given path with the destination directory.
#
# paths - (optional) path elements to a file or directory within the
# destination directory
#
# Returns a path which is prefixed with the destination directory.
def in_dest_dir(*paths)
paths.reduce(dest) do |base, path|
Jekyll.sanitized_path(base, path)
end
end
# Public: Prefix a given path with the cache directory.
#
# paths - (optional) path elements to a file or directory within the
# cache directory
#
# Returns a path which is prefixed with the cache directory.
def in_cache_dir(*paths)
paths.reduce(cache_dir) do |base, path|
Jekyll.sanitized_path(base, path)
end
end
# Public: The full path to the directory that houses all the collections registered
# with the current site.
#
# Returns the source directory or the absolute path to the custom collections_dir
def collections_path
dir_str = config["collections_dir"]
@collections_path ||= dir_str.empty? ? source : in_source_dir(dir_str)
end
# Public
#
# Returns the object as a debug String.
def inspect
"#<#{self.class} @source=#{@source}>"
end
private
def load_theme_configuration(config)
return config if config["ignore_theme_config"] == true
theme_config_file = in_theme_dir("_config.yml")
return config unless File.exist?(theme_config_file)
# Bail out if the theme_config_file is a symlink file irrespective of safe mode
return config if File.symlink?(theme_config_file)
theme_config = SafeYAML.load_file(theme_config_file)
return config unless theme_config.is_a?(Hash)
Jekyll.logger.info "Theme Config file:", theme_config_file
# theme_config should not be overriding Jekyll's defaults
theme_config.delete_if { |key, _| Configuration::DEFAULTS.key?(key) }
# Override theme_config with existing config and return the result.
# Additionally ensure we return a `Jekyll::Configuration` instance instead of a Hash.
Utils.deep_merge_hashes(theme_config, config)
.each_with_object(Jekyll::Configuration.new) do |(key, value), conf|
conf[key] = value
end
end
# Limits the current posts; removes the posts which exceed the limit_posts
#
# Returns nothing
def limit_posts!
if limit_posts.positive?
limit = [posts.docs.length, limit_posts].min
posts.docs = posts.docs[-limit, limit]
end
end
# Returns the Cleaner or creates a new Cleaner if it doesn't
# already exist.
#
# Returns The Cleaner
def site_cleaner
@site_cleaner ||= Cleaner.new(self)
end
def hide_cache_dir_from_git
@cache_gitignore_path ||= in_source_dir(config["cache_dir"], ".gitignore")
return if File.exist?(@cache_gitignore_path)
cache_dir_path = in_source_dir(config["cache_dir"])
FileUtils.mkdir_p(cache_dir_path) unless File.directory?(cache_dir_path)
File.open(@cache_gitignore_path, "wb") do |file|
file.puts("# ignore everything in this directory\n*")
end
end
# Disable Marshaling cache to disk in Safe Mode
def configure_cache
Jekyll::Cache.cache_dir = in_source_dir(config["cache_dir"], "Jekyll/Cache")
if safe || config["disable_disk_cache"]
Jekyll::Cache.disable_disk_cache!
else
hide_cache_dir_from_git
end
end
def configure_plugins
self.plugin_manager = Jekyll::PluginManager.new(self)
self.plugins = plugin_manager.plugins_path
end
def configure_theme
self.theme = nil
return if config["theme"].nil?
self.theme =
if config["theme"].is_a?(String)
Jekyll::Theme.new(config["theme"])
else
Jekyll.logger.warn "Theme:", "value of 'theme' in config should be String to use " \
"gem-based themes, but got #{config["theme"].class}"
nil
end
end
def configure_include_paths
@includes_load_paths = Array(in_source_dir(config["includes_dir"].to_s))
@includes_load_paths << theme.includes_path if theme&.includes_path
end
def configure_file_read_opts
self.file_read_opts = {}
file_read_opts[:encoding] = config["encoding"] if config["encoding"]
self.file_read_opts = Jekyll::Utils.merged_file_read_opts(self, {})
end
def render_docs(payload)
collections.each_value do |collection|
collection.docs.each do |document|
render_regenerated(document, payload)
end
end
end
def render_pages(payload)
pages.each do |page|
render_regenerated(page, payload)
end
end
def render_regenerated(document, payload)
return unless regenerator.regenerate?(document)
document.renderer.payload = payload
document.output = document.renderer.run
document.trigger_hooks(:post_render)
end
end
end
@@ -0,0 +1,203 @@
# frozen_string_literal: true
module Jekyll
class StaticFile
extend Forwardable
attr_reader :relative_path, :extname,
:type, # Returns the type of the collection if present, nil otherwise.
:name
def_delegator :to_liquid, :to_json, :to_json
class << self
# The cache of last modification times [path] -> mtime.
def mtimes
@mtimes ||= {}
end
def reset_cache
@mtimes = nil
end
end
# Initialize a new StaticFile.
#
# site - The Site.
# base - The String path to the <source>.
# dir - The String path between <source> and the file.
# name - The String filename of the file.
# rubocop: disable Metrics/ParameterLists
def initialize(site, base, dir, name, collection = nil)
@site = site
@base = base
@dir = dir
@name = name
@collection = collection
@relative_path = File.join(*[@dir, @name].compact)
@extname = File.extname(@name)
@type = @collection&.label&.to_sym
end
# rubocop: enable Metrics/ParameterLists
# Returns source file path.
def path
@path ||= if !@collection.nil? && !@site.config["collections_dir"].empty?
File.join(*[@base, @site.config["collections_dir"], @dir, @name].compact)
else
File.join(*[@base, @dir, @name].compact)
end
end
# Obtain destination path.
#
# dest - The String path to the destination dir.
#
# Returns destination file path.
def destination(dest)
@destination ||= {}
@destination[dest] ||= @site.in_dest_dir(dest, Jekyll::URL.unescape_path(url))
end
def destination_rel_dir
if @collection
File.dirname(url)
else
@dir
end
end
def modified_time
@modified_time ||= File.stat(path).mtime
end
# Returns last modification time for this file.
def mtime
modified_time.to_i
end
# Is source path modified?
#
# Returns true if modified since last write.
def modified?
self.class.mtimes[path] != mtime
end
# Whether to write the file to the filesystem
#
# Returns true unless the defaults for the destination path from
# _config.yml contain `published: false`.
def write?
publishable = defaults.fetch("published", true)
return publishable unless @collection
publishable && @collection.write?
end
# Write the static file to the destination directory (if modified).
#
# dest - The String path to the destination dir.
#
# Returns false if the file was not modified since last time (no-op).
def write(dest)
dest_path = destination(dest)
return false if File.exist?(dest_path) && !modified?
self.class.mtimes[path] = mtime
FileUtils.mkdir_p(File.dirname(dest_path))
FileUtils.rm(dest_path) if File.exist?(dest_path)
copy_file(dest_path)
true
end
def data
@data ||= @site.frontmatter_defaults.all(relative_path, type)
end
def to_liquid
@to_liquid ||= Drops::StaticFileDrop.new(self)
end
# Generate "basename without extension" and strip away any trailing periods.
# NOTE: `String#gsub` removes all trailing periods (in comparison to `String#chomp`)
def basename
@basename ||= File.basename(name, extname).gsub(%r!\.*\z!, "")
end
def placeholders
{
:collection => @collection.label,
:path => cleaned_relative_path,
:output_ext => "",
:name => basename,
:title => "",
}
end
# Similar to Jekyll::Document#cleaned_relative_path.
# Generates a relative path with the collection's directory removed when applicable
# and additionally removes any multiple periods in the string.
#
# NOTE: `String#gsub!` removes all trailing periods (in comparison to `String#chomp!`)
#
# Examples:
# When `relative_path` is "_methods/site/my-cool-avatar...png":
# cleaned_relative_path
# # => "/site/my-cool-avatar"
#
# Returns the cleaned relative path of the static file.
def cleaned_relative_path
@cleaned_relative_path ||= begin
cleaned = relative_path[0..-extname.length - 1]
cleaned.gsub!(%r!\.*\z!, "")
cleaned.sub!(@collection.relative_directory, "") if @collection
cleaned
end
end
# Applies a similar URL-building technique as Jekyll::Document that takes
# the collection's URL template into account. The default URL template can
# be overridden in the collection's configuration in _config.yml.
def url
@url ||= begin
base = if @collection.nil?
cleaned_relative_path
else
Jekyll::URL.new(
:template => @collection.url_template,
:placeholders => placeholders
)
end.to_s.chomp("/")
base << extname
end
end
# Returns the front matter defaults defined for the file's URL and/or type
# as defined in _config.yml.
def defaults
@defaults ||= @site.frontmatter_defaults.all url, type
end
# Returns a debug string on inspecting the static file.
# Includes only the relative path of the object.
def inspect
"#<#{self.class} @relative_path=#{relative_path.inspect}>"
end
private
def copy_file(dest_path)
if @site.safe || Jekyll.env == "production"
FileUtils.cp(path, dest_path)
else
FileUtils.copy_entry(path, dest_path)
end
unless File.symlink?(dest_path)
File.utime(self.class.mtimes[path], self.class.mtimes[path], dest_path)
end
end
end
end
@@ -0,0 +1,57 @@
# frozen_string_literal: true
module Jekyll
class Stevenson < ::Logger
def initialize
formatter = proc do |_, _, _, msg|
msg.to_s
end
super($stdout, :formatter => formatter)
end
def add(severity, message = nil, progname = nil)
severity ||= UNKNOWN
@logdev = logdevice(severity)
return true if @logdev.nil? || severity < @level
progname ||= @progname
if message.nil?
if block_given?
message = yield
else
message = progname
progname = @progname
end
end
@logdev.puts(
format_message(format_severity(severity), Time.now, progname, message)
)
true
end
# Log a +WARN+ message
def warn(progname = nil, &block)
add(WARN, nil, progname.yellow, &block)
end
# Log an +ERROR+ message
def error(progname = nil, &block)
add(ERROR, nil, progname.red, &block)
end
def close
# No LogDevice in use
end
private
def logdevice(severity)
if severity > INFO
$stderr
else
$stdout
end
end
end
end
@@ -0,0 +1,129 @@
# frozen_string_literal: true
module Jekyll
module Tags
class HighlightBlock < Liquid::Block
include Liquid::StandardFilters
# The regular expression syntax checker. Start with the language specifier.
# Follow that by zero or more space separated options that take one of three
# forms: name, name=value, or name="<quoted list>"
#
# <quoted list> is a space-separated list of numbers
SYNTAX = %r!^([a-zA-Z0-9.+#_-]+)((\s+\w+(=(\w+|"([0-9]+\s)*[0-9]+"))?)*)$!.freeze
def initialize(tag_name, markup, tokens)
super
if markup.strip =~ SYNTAX
@lang = Regexp.last_match(1).downcase
@highlight_options = parse_options(Regexp.last_match(2))
else
raise SyntaxError, <<~MSG
Syntax Error in tag 'highlight' while parsing the following markup:
#{markup}
Valid syntax: highlight <lang> [linenos] [mark_lines="3 4 5"]
See https://jekyllrb.com/docs/liquid/tags/#code-snippet-highlighting for more details.
MSG
end
end
LEADING_OR_TRAILING_LINE_TERMINATORS = %r!\A(\n|\r)+|(\n|\r)+\z!.freeze
def render(context)
prefix = context["highlighter_prefix"] || ""
suffix = context["highlighter_suffix"] || ""
code = super.to_s.gsub(LEADING_OR_TRAILING_LINE_TERMINATORS, "")
output =
case context.registers[:site].highlighter
when "rouge"
render_rouge(code)
when "pygments"
render_pygments(code, context)
else
render_codehighlighter(code)
end
rendered_output = add_code_tag(output)
prefix + rendered_output + suffix
end
private
OPTIONS_REGEX = %r!(?:\w="[^"]*"|\w=\w|\w)+!.freeze
def parse_options(input)
options = {}
return options if input.empty?
# Split along 3 possible forms -- key="<quoted list>", key=value, or key
input.scan(OPTIONS_REGEX) do |opt|
key, value = opt.split("=")
# If a quoted list, convert to array
if value&.include?('"')
value.delete!('"')
value = value.split
end
options[key.to_sym] = value || true
end
options[:linenos] = "inline" if options[:linenos] == true
options
end
def render_pygments(code, _context)
Jekyll.logger.warn "Warning:", "Highlight Tag no longer supports rendering with Pygments."
Jekyll.logger.warn "", "Using the default highlighter, Rouge, instead."
render_rouge(code)
end
def render_rouge(code)
require "rouge"
formatter = Rouge::Formatters::HTML.new
formatter = line_highlighter_formatter(formatter) if @highlight_options[:mark_lines]
formatter = table_formatter(formatter) if @highlight_options[:linenos]
lexer = Rouge::Lexer.find_fancy(@lang, code) || Rouge::Lexers::PlainText
formatter.format(lexer.lex(code))
end
def line_highlighter_formatter(formatter)
Rouge::Formatters::HTMLLineHighlighter.new(
formatter,
:highlight_lines => mark_lines
)
end
def mark_lines
value = @highlight_options[:mark_lines]
return value.map(&:to_i) if value.is_a?(Array)
raise SyntaxError, "Syntax Error for mark_lines declaration. Expected a " \
"double-quoted list of integers."
end
def table_formatter(formatter)
Rouge::Formatters::HTMLTable.new(
formatter,
:css_class => "highlight",
:gutter_class => "gutter",
:code_class => "code"
)
end
def render_codehighlighter(code)
h(code).strip
end
def add_code_tag(code)
code_attrs = %(class="language-#{@lang.tr("+", "-")}" data-lang="#{@lang}")
%(<figure class="highlight"><pre><code #{code_attrs}>#{code.chomp}</code></pre></figure>)
end
end
end
end
Liquid::Template.register_tag("highlight", Jekyll::Tags::HighlightBlock)
@@ -0,0 +1,275 @@
# frozen_string_literal: true
module Jekyll
module Tags
class IncludeTag < Liquid::Tag
VALID_SYNTAX = %r!
([\w-]+)\s*=\s*
(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|([\w.-]+))
!x.freeze
VARIABLE_SYNTAX = %r!
(?<variable>[^{]*(\{\{\s*[\w\-.]+\s*(\|.*)?\}\}[^\s{}]*)+)
(?<params>.*)
!mx.freeze
FULL_VALID_SYNTAX = %r!\A\s*(?:#{VALID_SYNTAX}(?=\s|\z)\s*)*\z!.freeze
VALID_FILENAME_CHARS = %r!^[\w/.\-()+~\#@]+$!.freeze
INVALID_SEQUENCES = %r![./]{2,}!.freeze
def initialize(tag_name, markup, tokens)
super
markup = markup.strip
matched = markup.match(VARIABLE_SYNTAX)
if matched
@file = matched["variable"].strip
@params = matched["params"].strip
else
@file, @params = markup.split(%r!\s+!, 2)
end
validate_params if @params
@tag_name = tag_name
end
def syntax_example
"{% #{@tag_name} file.ext param='value' param2='value' %}"
end
def parse_params(context)
params = {}
@params.scan(VALID_SYNTAX) do |key, d_quoted, s_quoted, variable|
value = if d_quoted
d_quoted.include?('\\"') ? d_quoted.gsub('\\"', '"') : d_quoted
elsif s_quoted
s_quoted.include?("\\'") ? s_quoted.gsub("\\'", "'") : s_quoted
elsif variable
context[variable]
end
params[key] = value
end
params
end
def validate_file_name(file)
if INVALID_SEQUENCES.match?(file) || !VALID_FILENAME_CHARS.match?(file)
raise ArgumentError, <<~MSG
Invalid syntax for include tag. File contains invalid characters or sequences:
#{file}
Valid syntax:
#{syntax_example}
MSG
end
end
def validate_params
unless FULL_VALID_SYNTAX.match?(@params)
raise ArgumentError, <<~MSG
Invalid syntax for include tag:
#{@params}
Valid syntax:
#{syntax_example}
MSG
end
end
# Grab file read opts in the context
def file_read_opts(context)
context.registers[:site].file_read_opts
end
# Render the variable if required
def render_variable(context)
Liquid::Template.parse(@file).render(context) if VARIABLE_SYNTAX.match?(@file)
end
def tag_includes_dirs(context)
context.registers[:site].includes_load_paths.freeze
end
def locate_include_file(context, file, safe)
includes_dirs = tag_includes_dirs(context)
includes_dirs.each do |dir|
path = PathManager.join(dir, file)
return path if valid_include_file?(path, dir.to_s, safe)
end
raise IOError, could_not_locate_message(file, includes_dirs, safe)
end
def render(context)
site = context.registers[:site]
file = render_variable(context) || @file
validate_file_name(file)
path = locate_include_file(context, file, site.safe)
return unless path
add_include_to_dependency(site, path, context)
partial = load_cached_partial(path, context)
context.stack do
context["include"] = parse_params(context) if @params
begin
partial.render!(context)
rescue Liquid::Error => e
e.template_name = path
e.markup_context = "included " if e.markup_context.nil?
raise e
end
end
end
def add_include_to_dependency(site, path, context)
if context.registers[:page]&.key?("path")
site.regenerator.add_dependency(
site.in_source_dir(context.registers[:page]["path"]),
path
)
end
end
def load_cached_partial(path, context)
context.registers[:cached_partials] ||= {}
cached_partial = context.registers[:cached_partials]
if cached_partial.key?(path)
cached_partial[path]
else
unparsed_file = context.registers[:site]
.liquid_renderer
.file(path)
begin
cached_partial[path] = unparsed_file.parse(read_file(path, context))
rescue Liquid::Error => e
e.template_name = path
e.markup_context = "included " if e.markup_context.nil?
raise e
end
end
end
def valid_include_file?(path, dir, safe)
!outside_site_source?(path, dir, safe) && File.file?(path)
end
def outside_site_source?(path, dir, safe)
safe && !realpath_prefixed_with?(path, dir)
end
def realpath_prefixed_with?(path, dir)
File.exist?(path) && File.realpath(path).start_with?(dir)
rescue StandardError
false
end
# This method allows to modify the file content by inheriting from the class.
def read_file(file, context)
File.read(file, **file_read_opts(context))
end
private
def could_not_locate_message(file, includes_dirs, safe)
message = "Could not locate the included file '#{file}' in any of #{includes_dirs}. " \
"Ensure it exists in one of those directories and"
message + if safe
" is not a symlink as those are not allowed in safe mode."
else
", if it is a symlink, does not point outside your site source."
end
end
end
# Do not inherit from this class.
# TODO: Merge into the `Jekyll::Tags::IncludeTag` in v5.0
class OptimizedIncludeTag < IncludeTag
def render(context)
@site ||= context.registers[:site]
file = render_variable(context) || @file
validate_file_name(file)
@site.inclusions[file] ||= locate_include_file(file)
inclusion = @site.inclusions[file]
add_include_to_dependency(inclusion, context) if @site.config["incremental"]
context.stack do
context["include"] = parse_params(context) if @params
inclusion.render(context)
end
end
private
def locate_include_file(file)
@site.includes_load_paths.each do |dir|
path = PathManager.join(dir, file)
return Inclusion.new(@site, dir, file) if valid_include_file?(path, dir)
end
raise IOError, could_not_locate_message(file, @site.includes_load_paths, @site.safe)
end
def valid_include_file?(path, dir)
File.file?(path) && !outside_scope?(path, dir)
end
def outside_scope?(path, dir)
@site.safe && !realpath_prefixed_with?(path, dir)
end
def realpath_prefixed_with?(path, dir)
File.realpath(path).start_with?(dir)
rescue StandardError
false
end
def add_include_to_dependency(inclusion, context)
page = context.registers[:page]
return unless page&.key?("path")
absolute_path = \
if page["collection"]
@site.in_source_dir(@site.config["collections_dir"], page["path"])
else
@site.in_source_dir(page["path"])
end
@site.regenerator.add_dependency(absolute_path, inclusion.path)
end
end
class IncludeRelativeTag < IncludeTag
def tag_includes_dirs(context)
Array(page_path(context)).freeze
end
def page_path(context)
page, site = context.registers.values_at(:page, :site)
return site.source unless page
site.in_source_dir File.dirname(resource_path(page, site))
end
private
def resource_path(page, site)
path = page["path"]
path = File.join(site.config["collections_dir"], path) if page["collection"]
path.delete_suffix("/#excerpt")
end
end
end
end
Liquid::Template.register_tag("include", Jekyll::Tags::OptimizedIncludeTag)
Liquid::Template.register_tag("include_relative", Jekyll::Tags::IncludeRelativeTag)
@@ -0,0 +1,42 @@
# frozen_string_literal: true
module Jekyll
module Tags
class Link < Liquid::Tag
include Jekyll::Filters::URLFilters
class << self
def tag_name
name.split("::").last.downcase
end
end
def initialize(tag_name, relative_path, tokens)
super
@relative_path = relative_path.strip
end
def render(context)
@context = context
site = context.registers[:site]
relative_path = Liquid::Template.parse(@relative_path).render(context)
relative_path_with_leading_slash = PathManager.join("", relative_path)
site.each_site_file do |item|
return relative_url(item) if item.relative_path == relative_path
# This takes care of the case for static files that have a leading /
return relative_url(item) if item.relative_path == relative_path_with_leading_slash
end
raise ArgumentError, <<~MSG
Could not find document '#{relative_path}' in tag '#{self.class.tag_name}'.
Make sure the document exists and the path is correct.
MSG
end
end
end
end
Liquid::Template.register_tag(Jekyll::Tags::Link.tag_name, Jekyll::Tags::Link)
@@ -0,0 +1,106 @@
# frozen_string_literal: true
module Jekyll
module Tags
class PostComparer
MATCHER = %r!^(.+/)*(\d+-\d+-\d+)-(.*)$!.freeze
attr_reader :path, :date, :slug, :name
def initialize(name)
@name = name
all, @path, @date, @slug = *name.sub(%r!^/!, "").match(MATCHER)
unless all
raise Jekyll::Errors::InvalidPostNameError,
"'#{name}' does not contain valid date and/or title."
end
basename_pattern = "#{date}-#{Regexp.escape(slug)}\\.[^.]+"
@name_regex = %r!^_posts/#{path}#{basename_pattern}|^#{path}_posts/?#{basename_pattern}!
end
def post_date
@post_date ||= Utils.parse_date(
date,
"'#{date}' does not contain valid date and/or title."
)
end
def ==(other)
other.relative_path.match(@name_regex)
end
def deprecated_equality(other)
slug == post_slug(other) &&
post_date.year == other.date.year &&
post_date.month == other.date.month &&
post_date.day == other.date.day
end
private
# Construct the directory-aware post slug for a Jekyll::Post
#
# other - the Jekyll::Post
#
# Returns the post slug with the subdirectory (relative to _posts)
def post_slug(other)
path = other.basename.split("/")[0...-1].join("/")
if path.nil? || path == ""
other.data["slug"]
else
"#{path}/#{other.data["slug"]}"
end
end
end
class PostUrl < Liquid::Tag
include Jekyll::Filters::URLFilters
def initialize(tag_name, post, tokens)
super
@orig_post = post.strip
begin
@post = PostComparer.new(@orig_post)
rescue StandardError => e
raise Jekyll::Errors::PostURLError, <<~MSG
Could not parse name of post "#{@orig_post}" in tag 'post_url'.
Make sure the post exists and the name is correct.
#{e.class}: #{e.message}
MSG
end
end
def render(context)
@context = context
site = context.registers[:site]
site.posts.docs.each do |document|
return relative_url(document) if @post == document
end
# New matching method did not match, fall back to old method
# with deprecation warning if this matches
site.posts.docs.each do |document|
next unless @post.deprecated_equality document
Jekyll::Deprecator.deprecation_message(
"A call to '{% post_url #{@post.name} %}' did not match a post using the new " \
"matching method of checking name (path-date-slug) equality. Please make sure " \
"that you change this tag to match the post's name exactly."
)
return relative_url(document)
end
raise Jekyll::Errors::PostURLError, <<~MSG
Could not find post "#{@orig_post}" in tag 'post_url'.
Make sure the post exists and the name is correct.
MSG
end
end
end
end
Liquid::Template.register_tag("post_url", Jekyll::Tags::PostUrl)
+90
View File
@@ -0,0 +1,90 @@
# frozen_string_literal: true
module Jekyll
class Theme
extend Forwardable
attr_reader :name
def_delegator :gemspec, :version, :version
def initialize(name)
@name = name.downcase.strip
Jekyll.logger.debug "Theme:", name
Jekyll.logger.debug "Theme source:", root
end
def root
# Must use File.realpath to resolve symlinks created by rbenv
# Otherwise, Jekyll.sanitized path with prepend the unresolved root
@root ||= File.realpath(gemspec.full_gem_path)
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
raise "Path #{gemspec.full_gem_path} does not exist, is not accessible or includes " \
"a symbolic link loop"
end
# The name of theme directory
def basename
@basename ||= File.basename(root)
end
def includes_path
@includes_path ||= path_for "_includes"
end
def layouts_path
@layouts_path ||= path_for "_layouts"
end
def sass_path
@sass_path ||= path_for "_sass"
end
def assets_path
@assets_path ||= path_for "assets"
end
def data_path
@data_path ||= path_for "_data"
end
def runtime_dependencies
gemspec.runtime_dependencies
end
private
def path_for(folder)
path = realpath_for(folder)
path if path && File.directory?(path)
end
def realpath_for(folder)
# This resolves all symlinks for the theme subfolder and then ensures that the directory
# remains inside the theme root. This prevents the use of symlinks for theme subfolders to
# escape the theme root.
# However, symlinks are allowed to point to other directories within the theme.
Jekyll.sanitized_path(root, File.realpath(Jekyll.sanitized_path(root, folder.to_s)))
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP => e
log_realpath_exception(e, folder)
nil
end
def log_realpath_exception(err, folder)
return if err.is_a?(Errno::ENOENT)
case err
when Errno::EACCES
Jekyll.logger.error "Theme error:", "Directory '#{folder}' is not accessible."
when Errno::ELOOP
Jekyll.logger.error "Theme error:", "Directory '#{folder}' includes a symbolic link loop."
end
end
def gemspec
@gemspec ||= Gem::Specification.find_by_name(name)
rescue Gem::LoadError
raise Jekyll::Errors::MissingDependencyException,
"The #{name} theme could not be found."
end
end
end
@@ -0,0 +1,121 @@
# frozen_string_literal: true
module Jekyll
class ThemeBuilder
SCAFFOLD_DIRECTORIES = %w(
assets _data _layouts _includes _sass
).freeze
attr_reader :name, :path, :code_of_conduct
def initialize(theme_name, opts)
@name = theme_name.to_s.tr(" ", "_").squeeze("_")
@path = Pathname.new(File.expand_path(name, Dir.pwd))
@code_of_conduct = !!opts["code_of_conduct"]
end
def create!
create_directories
create_starter_files
create_gemspec
create_accessories
initialize_git_repo
end
def user_name
@user_name ||= `git config user.name`.chomp
end
def user_email
@user_email ||= `git config user.email`.chomp
end
private
def root
@root ||= Pathname.new(File.expand_path("../", __dir__))
end
def template_file(filename)
[
root.join("theme_template", "#{filename}.erb"),
root.join("theme_template", filename.to_s),
].find(&:exist?)
end
def template(filename)
erb.render(template_file(filename).read)
end
def erb
@erb ||= ERBRenderer.new(self)
end
def mkdir_p(directories)
Array(directories).each do |directory|
full_path = path.join(directory)
Jekyll.logger.info "create", full_path.to_s
FileUtils.mkdir_p(full_path)
end
end
def write_file(filename, contents)
full_path = path.join(filename)
Jekyll.logger.info "create", full_path.to_s
File.write(full_path, contents)
end
def create_directories
mkdir_p(SCAFFOLD_DIRECTORIES)
end
def create_starter_files
%w(page post default).each do |layout|
write_file("_layouts/#{layout}.html", template("_layouts/#{layout}.html"))
end
end
def create_gemspec
write_file("Gemfile", template("Gemfile"))
write_file("#{name}.gemspec", template("theme.gemspec"))
end
def create_accessories
accessories = %w(README.md LICENSE.txt)
accessories << "CODE_OF_CONDUCT.md" if code_of_conduct
accessories.each do |filename|
write_file(filename, template(filename))
end
end
def initialize_git_repo
Jekyll.logger.info "initialize", path.join(".git").to_s
Dir.chdir(path.to_s) { `git init` }
write_file(".gitignore", template("gitignore"))
end
class ERBRenderer
extend Forwardable
def_delegator :@theme_builder, :name, :theme_name
def_delegator :@theme_builder, :user_name, :user_name
def_delegator :@theme_builder, :user_email, :user_email
def initialize(theme_builder)
@theme_builder = theme_builder
end
def jekyll_version_with_minor
Jekyll::VERSION.split(".").take(2).join(".")
end
def theme_directories
SCAFFOLD_DIRECTORIES
end
def render(contents)
ERB.new(contents).result binding
end
end
end
end
+167
View File
@@ -0,0 +1,167 @@
# frozen_string_literal: true
# Public: Methods that generate a URL for a resource such as a Post or a Page.
#
# Examples
#
# URL.new({
# :template => /:categories/:title.html",
# :placeholders => {:categories => "ruby", :title => "something"}
# }).to_s
#
module Jekyll
class URL
# options - One of :permalink or :template must be supplied.
# :template - The String used as template for URL generation,
# for example "/:path/:basename:output_ext", where
# a placeholder is prefixed with a colon.
# :placeholders - A hash containing the placeholders which will be
# replaced when used inside the template. E.g.
# { "year" => Time.now.strftime("%Y") } would replace
# the placeholder ":year" with the current year.
# :permalink - If supplied, no URL will be generated from the
# template. Instead, the given permalink will be
# used as URL.
def initialize(options)
@template = options[:template]
@placeholders = options[:placeholders] || {}
@permalink = options[:permalink]
if (@template || @permalink).nil?
raise ArgumentError, "One of :template or :permalink must be supplied."
end
end
# The generated relative URL of the resource
#
# Returns the String URL
def to_s
sanitize_url(generated_permalink || generated_url)
end
# Generates a URL from the permalink
#
# Returns the _unsanitized String URL
def generated_permalink
(@generated_permalink ||= generate_url(@permalink)) if @permalink
end
# Generates a URL from the template
#
# Returns the unsanitized String URL
def generated_url
@generated_url ||= generate_url(@template)
end
# Internal: Generate the URL by replacing all placeholders with their
# respective values in the given template
#
# Returns the unsanitized String URL
def generate_url(template)
if @placeholders.is_a? Drops::UrlDrop
generate_url_from_drop(template)
else
generate_url_from_hash(template)
end
end
def generate_url_from_hash(template)
@placeholders.inject(template) do |result, token|
break result if result.index(":").nil?
if token.last.nil?
# Remove leading "/" to avoid generating urls with `//`
result.gsub("/:#{token.first}", "")
else
result.gsub(":#{token.first}", self.class.escape_path(token.last))
end
end
end
# We include underscores in keys to allow for 'i_month' and so forth.
# This poses a problem for keys which are followed by an underscore
# but the underscore is not part of the key, e.g. '/:month_:day'.
# That should be :month and :day, but our key extraction regexp isn't
# smart enough to know that so we have to make it an explicit
# possibility.
def possible_keys(key)
if key.end_with?("_")
[key, key.chomp("_")]
else
[key]
end
end
def generate_url_from_drop(template)
template.gsub(%r!:([a-z_]+)!) do |match|
name = Regexp.last_match(1)
pool = name.end_with?("_") ? [name, name.chomp!("_")] : [name]
winner = pool.find { |key| @placeholders.key?(key) }
if winner.nil?
raise NoMethodError,
"The URL template doesn't have #{pool.join(" or ")} keys. " \
"Check your permalink template!"
end
value = @placeholders[winner]
value = "" if value.nil?
replacement = self.class.escape_path(value)
match.sub!(":#{winner}", replacement)
end
end
# Returns a sanitized String URL, stripping "../../" and multiples of "/",
# as well as the beginning "/" so we can enforce and ensure it.
def sanitize_url(str)
"/#{str}".gsub("..", "/").tap do |result|
result.gsub!("./", "")
result.squeeze!("/")
end
end
# Escapes a path to be a valid URL path segment
#
# path - The path to be escaped.
#
# Examples:
#
# URL.escape_path("/a b")
# # => "/a%20b"
#
# Returns the escaped path.
def self.escape_path(path)
return path if path.empty? || %r!^[a-zA-Z0-9./-]+$!.match?(path)
# Because URI.escape doesn't escape "?", "[" and "]" by default,
# specify unsafe string (except unreserved, sub-delims, ":", "@" and "/").
#
# URI path segment is defined in RFC 3986 as follows:
# segment = *pchar
# pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
# pct-encoded = "%" HEXDIG HEXDIG
# sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
# / "*" / "+" / "," / ";" / "="
Addressable::URI.encode(path).encode("utf-8").sub("#", "%23")
end
# Unescapes a URL path segment
#
# path - The path to be unescaped.
#
# Examples:
#
# URL.unescape_path("/a%20b")
# # => "/a b"
#
# Returns the unescaped path.
def self.unescape_path(path)
path = path.encode("utf-8")
return path unless path.include?("%")
Addressable::URI.unencode(path)
end
end
end
+371
View File
@@ -0,0 +1,371 @@
# frozen_string_literal: true
module Jekyll
module Utils
extend self
autoload :Ansi, "jekyll/utils/ansi"
autoload :Exec, "jekyll/utils/exec"
autoload :Internet, "jekyll/utils/internet"
autoload :Platforms, "jekyll/utils/platforms"
autoload :ThreadEvent, "jekyll/utils/thread_event"
autoload :WinTZ, "jekyll/utils/win_tz"
# Constants for use in #slugify
SLUGIFY_MODES = %w(raw default pretty ascii latin).freeze
SLUGIFY_RAW_REGEXP = Regexp.new('\\s+').freeze
SLUGIFY_DEFAULT_REGEXP = Regexp.new("[^\\p{M}\\p{L}\\p{Nd}]+").freeze
SLUGIFY_PRETTY_REGEXP = Regexp.new("[^\\p{M}\\p{L}\\p{Nd}._~!$&'()+,;=@]+").freeze
SLUGIFY_ASCII_REGEXP = Regexp.new("[^[A-Za-z0-9]]+").freeze
# Takes a slug and turns it into a simple title.
def titleize_slug(slug)
slug.split("-").map!(&:capitalize).join(" ")
end
# Non-destructive version of deep_merge_hashes! See that method.
#
# Returns the merged hashes.
def deep_merge_hashes(master_hash, other_hash)
deep_merge_hashes!(master_hash.dup, other_hash)
end
# Merges a master hash with another hash, recursively.
#
# master_hash - the "parent" hash whose values will be overridden
# other_hash - the other hash whose values will be persisted after the merge
#
# This code was lovingly stolen from some random gem:
# http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html
#
# Thanks to whoever made it.
def deep_merge_hashes!(target, overwrite)
merge_values(target, overwrite)
merge_default_proc(target, overwrite)
duplicate_frozen_values(target)
target
end
def mergable?(value)
value.is_a?(Hash) || value.is_a?(Drops::Drop)
end
def duplicable?(obj)
case obj
when nil, false, true, Symbol, Numeric
false
else
true
end
end
# Read array from the supplied hash favouring the singular key
# and then the plural key, and handling any nil entries.
#
# hash - the hash to read from
# singular_key - the singular key
# plural_key - the plural key
#
# Returns an array
def pluralized_array_from_hash(hash, singular_key, plural_key)
array = []
value = value_from_singular_key(hash, singular_key)
value ||= value_from_plural_key(hash, plural_key)
array << value
array.flatten!
array.compact!
array
end
def value_from_singular_key(hash, key)
hash[key] if hash.key?(key) || (hash.default_proc && hash[key])
end
def value_from_plural_key(hash, key)
if hash.key?(key) || (hash.default_proc && hash[key])
val = hash[key]
case val
when String
val.split
when Array
val.compact
end
end
end
def transform_keys(hash)
result = {}
hash.each_key do |key|
result[yield(key)] = hash[key]
end
result
end
# Apply #to_sym to all keys in the hash
#
# hash - the hash to which to apply this transformation
#
# Returns a new hash with symbolized keys
def symbolize_hash_keys(hash)
transform_keys(hash) { |key| key.to_sym rescue key }
end
# Apply #to_s to all keys in the Hash
#
# hash - the hash to which to apply this transformation
#
# Returns a new hash with stringified keys
def stringify_hash_keys(hash)
transform_keys(hash) { |key| key.to_s rescue key }
end
# Parse a date/time and throw an error if invalid
#
# input - the date/time to parse
# msg - (optional) the error message to show the user
#
# Returns the parsed date if successful, throws a FatalException
# if not
def parse_date(input, msg = "Input could not be parsed.")
@parse_date_cache ||= {}
@parse_date_cache[input] ||= Time.parse(input).localtime
rescue ArgumentError
raise Errors::InvalidDateError, "Invalid date '#{input}': #{msg}"
end
# Determines whether a given file has
#
# Returns true if the YAML front matter is present.
# rubocop: disable Naming/PredicateName
def has_yaml_header?(file)
File.open(file, "rb", &:readline).match? %r!\A---\s*\r?\n!
rescue EOFError
false
end
# Determine whether the given content string contains Liquid Tags or Variables
#
# Returns true is the string contains sequences of `{%` or `{{`
def has_liquid_construct?(content)
return false if content.nil? || content.empty?
content.include?("{%") || content.include?("{{")
end
# rubocop: enable Naming/PredicateName
# Slugify a filename or title.
#
# string - the filename or title to slugify
# mode - how string is slugified
# cased - whether to replace all uppercase letters with their
# lowercase counterparts
#
# When mode is "none", return the given string.
#
# When mode is "raw", return the given string,
# with every sequence of spaces characters replaced with a hyphen.
#
# When mode is "default" or nil, non-alphabetic characters are
# replaced with a hyphen too.
#
# When mode is "pretty", some non-alphabetic characters (._~!$&'()+,;=@)
# are not replaced with hyphen.
#
# When mode is "ascii", some everything else except ASCII characters
# a-z (lowercase), A-Z (uppercase) and 0-9 (numbers) are not replaced with hyphen.
#
# When mode is "latin", the input string is first preprocessed so that
# any letters with accents are replaced with the plain letter. Afterwards,
# it follows the "default" mode of operation.
#
# If cased is true, all uppercase letters in the result string are
# replaced with their lowercase counterparts.
#
# Examples:
# slugify("The _config.yml file")
# # => "the-config-yml-file"
#
# slugify("The _config.yml file", "pretty")
# # => "the-_config.yml-file"
#
# slugify("The _config.yml file", "pretty", true)
# # => "The-_config.yml file"
#
# slugify("The _config.yml file", "ascii")
# # => "the-config-yml-file"
#
# slugify("The _config.yml file", "latin")
# # => "the-config-yml-file"
#
# Returns the slugified string.
def slugify(string, mode: nil, cased: false)
mode ||= "default"
return nil if string.nil?
unless SLUGIFY_MODES.include?(mode)
return cased ? string : string.downcase
end
# Drop accent marks from latin characters. Everything else turns to ?
if mode == "latin"
I18n.config.available_locales = :en if I18n.config.available_locales.empty?
string = I18n.transliterate(string)
end
slug = replace_character_sequence_with_hyphen(string, :mode => mode)
# Remove leading/trailing hyphen
slug.gsub!(%r!^-|-$!i, "")
slug.downcase! unless cased
Jekyll.logger.warn("Warning:", "Empty `slug` generated for '#{string}'.") if slug.empty?
slug
end
# Add an appropriate suffix to template so that it matches the specified
# permalink style.
#
# template - permalink template without trailing slash or file extension
# permalink_style - permalink style, either built-in or custom
#
# The returned permalink template will use the same ending style as
# specified in permalink_style. For example, if permalink_style contains a
# trailing slash (or is :pretty, which indirectly has a trailing slash),
# then so will the returned template. If permalink_style has a trailing
# ":output_ext" (or is :none, :date, or :ordinal) then so will the returned
# template. Otherwise, template will be returned without modification.
#
# Examples:
# add_permalink_suffix("/:basename", :pretty)
# # => "/:basename/"
#
# add_permalink_suffix("/:basename", :date)
# # => "/:basename:output_ext"
#
# add_permalink_suffix("/:basename", "/:year/:month/:title/")
# # => "/:basename/"
#
# add_permalink_suffix("/:basename", "/:year/:month/:title")
# # => "/:basename"
#
# Returns the updated permalink template
def add_permalink_suffix(template, permalink_style)
template = template.dup
case permalink_style
when :pretty
template << "/"
when :date, :ordinal, :none
template << ":output_ext"
else
template << "/" if permalink_style.to_s.end_with?("/")
template << ":output_ext" if permalink_style.to_s.end_with?(":output_ext")
end
template
end
# Work the same way as Dir.glob but separating the input into two parts
# ('dir' + '/' + 'pattern') to make sure the first part('dir') does not act
# as a pattern.
#
# For example, Dir.glob("path[/*") always returns an empty array,
# because the method fails to find the closing pattern to '[' which is ']'
#
# Examples:
# safe_glob("path[", "*")
# # => ["path[/file1", "path[/file2"]
#
# safe_glob("path", "*", File::FNM_DOTMATCH)
# # => ["path/.", "path/..", "path/file1"]
#
# safe_glob("path", ["**", "*"])
# # => ["path[/file1", "path[/folder/file2"]
#
# dir - the dir where glob will be executed under
# (the dir will be included to each result)
# patterns - the patterns (or the pattern) which will be applied under the dir
# flags - the flags which will be applied to the pattern
#
# Returns matched paths
def safe_glob(dir, patterns, flags = 0)
return [] unless Dir.exist?(dir)
pattern = File.join(Array(patterns))
return [dir] if pattern.empty?
Dir.chdir(dir) do
Dir.glob(pattern, flags).map { |f| File.join(dir, f) }
end
end
# Returns merged option hash for File.read of self.site (if exists)
# and a given param
def merged_file_read_opts(site, opts)
merged = (site ? site.file_read_opts : {}).merge(opts)
# always use BOM when reading UTF-encoded files
if merged[:encoding]&.downcase&.start_with?("utf-")
merged[:encoding] = "bom|#{merged[:encoding]}"
end
if merged["encoding"]&.downcase&.start_with?("utf-")
merged["encoding"] = "bom|#{merged["encoding"]}"
end
merged
end
private
def merge_values(target, overwrite)
target.merge!(overwrite) do |_key, old_val, new_val|
if new_val.nil?
old_val
elsif mergable?(old_val) && mergable?(new_val)
deep_merge_hashes(old_val, new_val)
else
new_val
end
end
end
def merge_default_proc(target, overwrite)
if target.is_a?(Hash) && overwrite.is_a?(Hash) && target.default_proc.nil?
target.default_proc = overwrite.default_proc
end
end
def duplicate_frozen_values(target)
target.each do |key, val|
target[key] = val.dup if val.frozen? && duplicable?(val)
end
end
# Replace each character sequence with a hyphen.
#
# See Utils#slugify for a description of the character sequence specified
# by each mode.
def replace_character_sequence_with_hyphen(string, mode: "default")
replaceable_char =
case mode
when "raw"
SLUGIFY_RAW_REGEXP
when "pretty"
# "._~!$&'()+,;=@" is human readable (not URI-escaped) in URL
# and is allowed in both extN and NTFS.
SLUGIFY_PRETTY_REGEXP
when "ascii"
# For web servers not being able to handle Unicode, the safe
# method is to ditch anything else but latin letters and numeric
# digits.
SLUGIFY_ASCII_REGEXP
else
SLUGIFY_DEFAULT_REGEXP
end
# Strip according to the mode
string.gsub(replaceable_char, "-")
end
end
end
@@ -0,0 +1,57 @@
# frozen_string_literal: true
module Jekyll
module Utils
module Ansi
extend self
ESCAPE = format("%c", 27)
MATCH = %r!#{ESCAPE}\[(?:\d+)(?:;\d+)*(j|k|m|s|u|A|B|G)|\e\(B\e\[m!ix.freeze
COLORS = {
:red => 31,
:green => 32,
:black => 30,
:magenta => 35,
:yellow => 33,
:white => 37,
:blue => 34,
:cyan => 36,
}.freeze
# Strip ANSI from the current string. It also strips cursor stuff,
# well some of it, and it also strips some other stuff that a lot of
# the other ANSI strippers don't.
def strip(str)
str.gsub MATCH, ""
end
#
def has?(str)
!!(str =~ MATCH)
end
# Reset the color back to the default color so that you do not leak any
# colors when you move onto the next line. This is probably normally
# used as part of a wrapper so that we don't leak colors.
def reset(str = "")
@ansi_reset ||= format("%c[0m", 27)
"#{@ansi_reset}#{str}"
end
# SEE: `self::COLORS` for a list of methods. They are mostly
# standard base colors supported by pretty much any xterm-color, we do
# not need more than the base colors so we do not include them.
# Actually... if I'm honest we don't even need most of the
# base colors.
COLORS.each do |color, num|
define_method color do |str|
"#{format("%c", 27)}[#{num}m#{str}#{reset}"
end
end
end
end
end
@@ -0,0 +1,26 @@
# frozen_string_literal: true
require "open3"
module Jekyll
module Utils
module Exec
extend self
# Runs a program in a sub-shell.
#
# *args - a list of strings containing the program name and arguments
#
# Returns a Process::Status and a String of output in an array in
# that order.
def run(*args)
stdin, stdout, stderr, process = Open3.popen3(*args)
out = stdout.read.strip
err = stderr.read.strip
[stdin, stdout, stderr].each(&:close)
[process.value, out + err]
end
end
end
end
@@ -0,0 +1,37 @@
# frozen_string_literal: true
module Jekyll
module Utils
module Internet
# Public: Determine whether the present device has a connection to
# the Internet. This allows plugin writers which require the outside
# world to have a neat fallback mechanism for offline building.
#
# Example:
# if Internet.connected?
# Typhoeus.get("https://pages.github.com/versions.json")
# else
# Jekyll.logger.warn "Warning:", "Version check has been disabled."
# Jekyll.logger.warn "", "Connect to the Internet to enable it."
# nil
# end
#
# Returns true if a DNS call can successfully be made, or false if not.
module_function
def connected?
!dns("example.com").nil?
end
def dns(domain)
require "resolv"
Resolv::DNS.open do |resolver|
resolver.getaddress(domain)
end
rescue Resolv::ResolvError, Resolv::ResolvTimeout
nil
end
end
end
end
@@ -0,0 +1,67 @@
# frozen_string_literal: true
module Jekyll
module Utils
module Platforms
extend self
def jruby?
RUBY_ENGINE == "jruby"
end
def mri?
RUBY_ENGINE == "ruby"
end
def windows?
vanilla_windows? || bash_on_windows?
end
# Not a Windows Subsystem for Linux (WSL)
def vanilla_windows?
rbconfig_host.match?(%r!mswin|mingw|cygwin!) && proc_version.empty?
end
alias_method :really_windows?, :vanilla_windows?
# Determine if Windows Subsystem for Linux (WSL)
def bash_on_windows?
linux_os? && microsoft_proc_version?
end
def linux?
linux_os? && !microsoft_proc_version?
end
def osx?
rbconfig_host.match?(%r!darwin|mac os!)
end
def unix?
rbconfig_host.match?(%r!solaris|bsd!)
end
private
def proc_version
@proc_version ||= \
begin
File.read("/proc/version").downcase
rescue Errno::ENOENT, Errno::EACCES
""
end
end
def rbconfig_host
@rbconfig_host ||= RbConfig::CONFIG["host_os"].downcase
end
def linux_os?
rbconfig_host.include?("linux")
end
def microsoft_proc_version?
proc_version.include?("microsoft")
end
end
end
end
@@ -0,0 +1,31 @@
# frozen_string_literal: true
module Jekyll
module Utils
# Based on the pattern and code from
# https://emptysqua.re/blog/an-event-synchronization-primitive-for-ruby/
class ThreadEvent
attr_reader :flag
def initialize
@lock = Mutex.new
@cond = ConditionVariable.new
@flag = false
end
def set
@lock.synchronize do
yield if block_given?
@flag = true
@cond.broadcast
end
end
def wait
@lock.synchronize do
@cond.wait(@lock) unless @flag
end
end
end
end
end
@@ -0,0 +1,46 @@
# frozen_string_literal: true
module Jekyll
module Utils
module WinTZ
extend self
# Public: Calculate the Timezone for Windows when the config file has a defined
# 'timezone' key.
#
# timezone - the IANA Time Zone specified in "_config.yml"
#
# Returns a string that ultimately re-defines ENV["TZ"] in Windows
def calculate(timezone, now = Time.now)
External.require_with_graceful_fail("tzinfo") unless defined?(TZInfo)
tz = TZInfo::Timezone.get(timezone)
#
# Use period_for_utc and utc_total_offset instead of
# period_for and observed_utc_offset for compatibility with tzinfo v1.
offset = tz.period_for_utc(now.getutc).utc_total_offset
#
# POSIX style definition reverses the offset sign.
# e.g. Eastern Standard Time (EST) that is 5Hrs. to the 'west' of Prime Meridian
# is denoted as:
# EST+5 (or) EST+05:00
# Reference: https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
sign = offset.positive? ? "-" : "+"
rational_hours = offset.abs.to_r / 3600
hours = rational_hours.to_i
minutes = ((rational_hours - hours) * 60).to_i
#
# Format the hours and minutes as two-digit numbers.
time = format("%<hours>02d:%<minutes>02d", :hours => hours, :minutes => minutes)
Jekyll.logger.debug "Timezone:", "#{timezone} #{sign}#{time}"
#
# Note: The 3-letter-word below doesn't have a particular significance.
"WTZ#{sign}#{time}"
end
end
end
end

Some files were not shown because too many files have changed in this diff Show More