diff --git a/.github/probots.yml b/.github/probots.yml
new file mode 100644
index 0000000..1491d27
--- /dev/null
+++ b/.github/probots.yml
@@ -0,0 +1,2 @@
+enabled:
+ - cla
diff --git a/.gitignore b/.gitignore
index 7ac01c1..90bf6dc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ pkg
.ruby-version
Gemfile.lock
.bundle
+.byebug_history
diff --git a/.rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml b/.rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml
new file mode 100644
index 0000000..5f5212c
--- /dev/null
+++ b/.rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml
@@ -0,0 +1,1027 @@
+AllCops:
+ Exclude:
+ - 'db/schema.rb'
+ DisabledByDefault: true
+ StyleGuideBaseURL: https://shopify.github.io/ruby-style-guide/
+
+Lint/AssignmentInCondition:
+ Enabled: true
+
+Layout/AccessModifierIndentation:
+ EnforcedStyle: indent
+ SupportedStyles:
+ - outdent
+ - indent
+ IndentationWidth:
+
+Style/Alias:
+ EnforcedStyle: prefer_alias_method
+ SupportedStyles:
+ - prefer_alias
+ - prefer_alias_method
+
+Layout/AlignHash:
+ EnforcedHashRocketStyle: key
+ EnforcedColonStyle: key
+ EnforcedLastArgumentHashStyle: ignore_implicit
+ SupportedLastArgumentHashStyles:
+ - always_inspect
+ - always_ignore
+ - ignore_implicit
+ - ignore_explicit
+
+Layout/AlignParameters:
+ EnforcedStyle: with_fixed_indentation
+ SupportedStyles:
+ - with_first_parameter
+ - with_fixed_indentation
+ IndentationWidth:
+
+Style/AndOr:
+ EnforcedStyle: always
+ SupportedStyles:
+ - always
+ - conditionals
+
+Style/BarePercentLiterals:
+ EnforcedStyle: bare_percent
+ SupportedStyles:
+ - percent_q
+ - bare_percent
+
+Style/BlockDelimiters:
+ EnforcedStyle: line_count_based
+ SupportedStyles:
+ - line_count_based
+ - semantic
+ - braces_for_chaining
+ ProceduralMethods:
+ - benchmark
+ - bm
+ - bmbm
+ - create
+ - each_with_object
+ - measure
+ - new
+ - realtime
+ - tap
+ - with_object
+ FunctionalMethods:
+ - let
+ - let!
+ - subject
+ - watch
+ IgnoredMethods:
+ - lambda
+ - proc
+ - it
+
+Style/BracesAroundHashParameters:
+ EnforcedStyle: no_braces
+ SupportedStyles:
+ - braces
+ - no_braces
+ - context_dependent
+
+Layout/CaseIndentation:
+ EnforcedStyle: end
+ SupportedStyles:
+ - case
+ - end
+ IndentOneStep: false
+ IndentationWidth:
+
+Style/ClassAndModuleChildren:
+ EnforcedStyle: nested
+ SupportedStyles:
+ - nested
+ - compact
+
+Style/ClassCheck:
+ EnforcedStyle: is_a?
+ SupportedStyles:
+ - is_a?
+ - kind_of?
+
+Style/CommandLiteral:
+ EnforcedStyle: percent_x
+ SupportedStyles:
+ - backticks
+ - percent_x
+ - mixed
+ AllowInnerBackticks: false
+
+Style/CommentAnnotation:
+ Keywords:
+ - TODO
+ - FIXME
+ - OPTIMIZE
+ - HACK
+ - REVIEW
+
+Style/ConditionalAssignment:
+ EnforcedStyle: assign_to_condition
+ SupportedStyles:
+ - assign_to_condition
+ - assign_inside_condition
+ SingleLineConditionsOnly: true
+
+Layout/DotPosition:
+ EnforcedStyle: leading
+ SupportedStyles:
+ - leading
+ - trailing
+
+Style/EmptyElse:
+ EnforcedStyle: both
+ SupportedStyles:
+ - empty
+ - nil
+ - both
+
+Layout/EmptyLineBetweenDefs:
+ AllowAdjacentOneLineDefs: false
+
+Layout/EmptyLinesAroundBlockBody:
+ EnforcedStyle: no_empty_lines
+ SupportedStyles:
+ - empty_lines
+ - no_empty_lines
+
+Layout/EmptyLinesAroundClassBody:
+ EnforcedStyle: no_empty_lines
+ SupportedStyles:
+ - empty_lines
+ - empty_lines_except_namespace
+ - no_empty_lines
+
+Layout/EmptyLinesAroundModuleBody:
+ EnforcedStyle: no_empty_lines
+ SupportedStyles:
+ - empty_lines
+ - empty_lines_except_namespace
+ - no_empty_lines
+
+Layout/ExtraSpacing:
+ AllowForAlignment: true
+ ForceEqualSignAlignment: false
+
+Naming/FileName:
+ Exclude: []
+ ExpectMatchingDefinition: false
+ Regex:
+ IgnoreExecutableScripts: true
+
+Layout/IndentFirstArgument:
+ EnforcedStyle: consistent
+ SupportedStyles:
+ - consistent
+ - special_for_inner_method_call
+ - special_for_inner_method_call_in_parentheses
+ IndentationWidth:
+
+Style/For:
+ EnforcedStyle: each
+ SupportedStyles:
+ - for
+ - each
+
+Style/FormatString:
+ EnforcedStyle: format
+ SupportedStyles:
+ - format
+ - sprintf
+ - percent
+
+Style/FrozenStringLiteralComment:
+ Details: >-
+ Add `# frozen_string_literal: true` to the top of the file. Frozen string
+ literals will become the default in a future Ruby version, and we want to
+ make sure we're ready.
+ EnforcedStyle: always
+ SupportedStyles:
+ - always
+ - never
+
+Style/GlobalVars:
+ AllowedVariables: []
+
+Style/HashSyntax:
+ EnforcedStyle: ruby19
+ SupportedStyles:
+ - ruby19
+ - hash_rockets
+ - no_mixed_keys
+ - ruby19_no_mixed_keys
+ UseHashRocketsWithSymbolValues: false
+ PreferHashRocketsForNonAlnumEndingSymbols: false
+
+Layout/IndentationConsistency:
+ EnforcedStyle: normal
+ SupportedStyles:
+ - normal
+ - rails
+
+Layout/IndentationWidth:
+ Width: 2
+
+Layout/IndentFirstArrayElement:
+ EnforcedStyle: consistent
+ SupportedStyles:
+ - special_inside_parentheses
+ - consistent
+ - align_brackets
+ IndentationWidth:
+
+Layout/IndentAssignment:
+ IndentationWidth:
+
+Layout/IndentFirstHashElement:
+ EnforcedStyle: consistent
+ SupportedStyles:
+ - special_inside_parentheses
+ - consistent
+ - align_braces
+ IndentationWidth:
+
+Style/LambdaCall:
+ EnforcedStyle: call
+ SupportedStyles:
+ - call
+ - braces
+
+Style/Next:
+ EnforcedStyle: skip_modifier_ifs
+ MinBodyLength: 3
+ SupportedStyles:
+ - skip_modifier_ifs
+ - always
+
+Style/NonNilCheck:
+ IncludeSemanticChanges: false
+
+Style/MethodCallWithArgsParentheses:
+ Enabled: true
+ IgnoreMacros: true
+ IgnoredMethods:
+ - require
+ - require_relative
+ - require_dependency
+ - yield
+ - raise
+ - puts
+ Exclude:
+ - Gemfile
+
+Style/MethodDefParentheses:
+ EnforcedStyle: require_parentheses
+ SupportedStyles:
+ - require_parentheses
+ - require_no_parentheses
+ - require_no_parentheses_except_multiline
+
+Naming/MethodName:
+ EnforcedStyle: snake_case
+ SupportedStyles:
+ - snake_case
+ - camelCase
+
+Layout/MultilineArrayBraceLayout:
+ EnforcedStyle: symmetrical
+ SupportedStyles:
+ - symmetrical
+ - new_line
+ - same_line
+
+Layout/MultilineHashBraceLayout:
+ EnforcedStyle: symmetrical
+ SupportedStyles:
+ - symmetrical
+ - new_line
+ - same_line
+
+Layout/MultilineMethodCallBraceLayout:
+ EnforcedStyle: symmetrical
+ SupportedStyles:
+ - symmetrical
+ - new_line
+ - same_line
+
+Layout/MultilineMethodCallIndentation:
+ EnforcedStyle: indented
+ SupportedStyles:
+ - aligned
+ - indented
+ - indented_relative_to_receiver
+ IndentationWidth: 2
+
+Layout/MultilineMethodDefinitionBraceLayout:
+ EnforcedStyle: symmetrical
+ SupportedStyles:
+ - symmetrical
+ - new_line
+ - same_line
+
+Style/NumericLiteralPrefix:
+ EnforcedOctalStyle: zero_only
+ SupportedOctalStyles:
+ - zero_with_o
+ - zero_only
+
+Style/ParenthesesAroundCondition:
+ AllowSafeAssignment: true
+
+Style/PercentQLiterals:
+ EnforcedStyle: lower_case_q
+ SupportedStyles:
+ - lower_case_q
+ - upper_case_q
+
+Naming/PredicateName:
+ NamePrefix:
+ - is_
+ NamePrefixBlacklist:
+ - is_
+ NameWhitelist:
+ - is_a?
+ Exclude:
+ - 'spec/**/*'
+
+Style/PreferredHashMethods:
+ EnforcedStyle: short
+ SupportedStyles:
+ - short
+ - verbose
+
+Style/RaiseArgs:
+ EnforcedStyle: exploded
+ SupportedStyles:
+ - compact
+ - exploded
+
+Style/RedundantReturn:
+ AllowMultipleReturnValues: false
+
+Style/RegexpLiteral:
+ EnforcedStyle: mixed
+ SupportedStyles:
+ - slashes
+ - percent_r
+ - mixed
+ AllowInnerSlashes: false
+
+Style/SafeNavigation:
+ ConvertCodeThatCanStartToReturnNil: false
+ Enabled: true
+
+Lint/SafeNavigationChain:
+ Enabled: true
+
+Style/Semicolon:
+ AllowAsExpressionSeparator: false
+
+Style/SignalException:
+ EnforcedStyle: only_raise
+ SupportedStyles:
+ - only_raise
+ - only_fail
+ - semantic
+
+Style/SingleLineMethods:
+ AllowIfMethodIsEmpty: true
+
+Layout/SpaceBeforeFirstArg:
+ AllowForAlignment: true
+
+Style/SpecialGlobalVars:
+ EnforcedStyle: use_english_names
+ SupportedStyles:
+ - use_perl_names
+ - use_english_names
+
+Style/StabbyLambdaParentheses:
+ EnforcedStyle: require_parentheses
+ SupportedStyles:
+ - require_parentheses
+ - require_no_parentheses
+
+Style/StringLiteralsInInterpolation:
+ EnforcedStyle: single_quotes
+ SupportedStyles:
+ - single_quotes
+ - double_quotes
+
+Layout/SpaceAroundBlockParameters:
+ EnforcedStyleInsidePipes: no_space
+ SupportedStylesInsidePipes:
+ - space
+ - no_space
+
+Layout/SpaceAroundEqualsInParameterDefault:
+ EnforcedStyle: space
+ SupportedStyles:
+ - space
+ - no_space
+
+Layout/SpaceAroundOperators:
+ AllowForAlignment: true
+
+Layout/SpaceBeforeBlockBraces:
+ EnforcedStyle: space
+ EnforcedStyleForEmptyBraces: space
+ SupportedStyles:
+ - space
+ - no_space
+
+Layout/SpaceInsideBlockBraces:
+ EnforcedStyle: space
+ SupportedStyles:
+ - space
+ - no_space
+ EnforcedStyleForEmptyBraces: no_space
+ SpaceBeforeBlockParameters: true
+
+Layout/SpaceInsideHashLiteralBraces:
+ EnforcedStyle: space
+ EnforcedStyleForEmptyBraces: no_space
+ SupportedStyles:
+ - space
+ - no_space
+ - compact
+
+Layout/SpaceInsideStringInterpolation:
+ EnforcedStyle: no_space
+ SupportedStyles:
+ - space
+ - no_space
+
+Style/SymbolProc:
+ IgnoredMethods:
+ - respond_to
+ - define_method
+
+Style/TernaryParentheses:
+ EnforcedStyle: require_no_parentheses
+ SupportedStyles:
+ - require_parentheses
+ - require_no_parentheses
+ AllowSafeAssignment: true
+
+Layout/TrailingBlankLines:
+ EnforcedStyle: final_newline
+ SupportedStyles:
+ - final_newline
+ - final_blank_line
+
+Style/TrivialAccessors:
+ ExactNameMatch: true
+ AllowPredicates: true
+ AllowDSLWriters: false
+ IgnoreClassMethods: false
+ Whitelist:
+ - to_ary
+ - to_a
+ - to_c
+ - to_enum
+ - to_h
+ - to_hash
+ - to_i
+ - to_int
+ - to_io
+ - to_open
+ - to_path
+ - to_proc
+ - to_r
+ - to_regexp
+ - to_str
+ - to_s
+ - to_sym
+
+Naming/VariableName:
+ EnforcedStyle: snake_case
+ SupportedStyles:
+ - snake_case
+ - camelCase
+
+Style/WhileUntilModifier:
+ Enabled: true
+
+Metrics/BlockNesting:
+ Max: 3
+
+Metrics/LineLength:
+ Max: 120
+ AllowHeredoc: true
+ AllowURI: true
+ URISchemes:
+ - http
+ - https
+ IgnoreCopDirectives: false
+ IgnoredPatterns:
+ - '\A\s*(remote_)?test(_\w+)?\s.*(do|->)(\s|\Z)'
+
+Metrics/ParameterLists:
+ Max: 5
+ CountKeywordArgs: false
+
+Layout/BlockAlignment:
+ EnforcedStyleAlignWith: either
+ SupportedStylesAlignWith:
+ - either
+ - start_of_block
+ - start_of_line
+
+Layout/EndAlignment:
+ EnforcedStyleAlignWith: variable
+ SupportedStylesAlignWith:
+ - keyword
+ - variable
+ - start_of_line
+
+Layout/DefEndAlignment:
+ EnforcedStyleAlignWith: start_of_line
+ SupportedStylesAlignWith:
+ - start_of_line
+ - def
+
+Lint/InheritException:
+ EnforcedStyle: runtime_error
+ SupportedStyles:
+ - runtime_error
+ - standard_error
+
+Lint/UnusedBlockArgument:
+ IgnoreEmptyBlocks: true
+ AllowUnusedKeywordArguments: false
+
+Lint/UnusedMethodArgument:
+ AllowUnusedKeywordArguments: false
+ IgnoreEmptyMethods: true
+
+Naming/AccessorMethodName:
+ Enabled: true
+
+Layout/AlignArray:
+ Enabled: true
+
+Style/ArrayJoin:
+ Enabled: true
+
+Naming/AsciiIdentifiers:
+ Enabled: true
+
+Style/Attr:
+ Enabled: true
+
+Style/BeginBlock:
+ Enabled: true
+
+Style/BlockComments:
+ Enabled: true
+
+Layout/BlockEndNewline:
+ Enabled: true
+
+Style/CaseEquality:
+ Enabled: true
+
+Style/CharacterLiteral:
+ Enabled: true
+
+Naming/ClassAndModuleCamelCase:
+ Enabled: true
+
+Style/ClassMethods:
+ Enabled: true
+
+Style/ClassVars:
+ Enabled: true
+
+Layout/ClosingParenthesisIndentation:
+ Enabled: true
+
+Style/ColonMethodCall:
+ Enabled: true
+
+Layout/CommentIndentation:
+ Enabled: true
+
+Naming/ConstantName:
+ Enabled: true
+
+Style/DateTime:
+ Enabled: true
+
+Style/DefWithParentheses:
+ Enabled: true
+
+Style/EachForSimpleLoop:
+ Enabled: true
+
+Style/EachWithObject:
+ Enabled: true
+
+Layout/ElseAlignment:
+ Enabled: true
+
+Style/EmptyCaseCondition:
+ Enabled: true
+
+Layout/EmptyLines:
+ Enabled: true
+
+Layout/EmptyLinesAroundAccessModifier:
+ Enabled: true
+
+Layout/EmptyLinesAroundMethodBody:
+ Enabled: true
+
+Style/EmptyLiteral:
+ Enabled: true
+
+Style/EndBlock:
+ Enabled: true
+
+Layout/EndOfLine:
+ Enabled: true
+
+Style/EvenOdd:
+ Enabled: true
+
+Layout/InitialIndentation:
+ Enabled: true
+
+Lint/FlipFlop:
+ Enabled: true
+
+Style/IfInsideElse:
+ Enabled: true
+
+Style/IfUnlessModifierOfIfUnless:
+ Enabled: true
+
+Style/IfWithSemicolon:
+ Enabled: true
+
+Style/IdenticalConditionalBranches:
+ Enabled: true
+
+Style/InfiniteLoop:
+ Enabled: true
+
+Layout/LeadingCommentSpace:
+ Enabled: true
+
+Style/LineEndConcatenation:
+ Enabled: true
+
+Style/MethodCallWithoutArgsParentheses:
+ Enabled: true
+
+Style/MethodMissingSuper:
+ Enabled: true
+
+Style/MissingRespondToMissing:
+ Enabled: true
+
+Layout/MultilineBlockLayout:
+ Enabled: true
+
+Style/MultilineIfThen:
+ Enabled: true
+
+Style/MultilineMemoization:
+ Enabled: true
+
+Style/MultilineTernaryOperator:
+ Enabled: true
+
+Style/NegatedIf:
+ Enabled: true
+
+Style/NegatedWhile:
+ Enabled: true
+
+Style/NestedModifier:
+ Enabled: true
+
+Style/NestedParenthesizedCalls:
+ Enabled: true
+
+Style/NestedTernaryOperator:
+ Enabled: true
+
+Style/NilComparison:
+ Enabled: true
+
+Style/Not:
+ Enabled: true
+
+Style/OneLineConditional:
+ Enabled: true
+
+Naming/BinaryOperatorParameterName:
+ Enabled: true
+
+Style/OptionalArguments:
+ Enabled: true
+
+Style/ParallelAssignment:
+ Enabled: true
+
+Style/PerlBackrefs:
+ Enabled: true
+
+Style/Proc:
+ Enabled: true
+
+Style/RedundantBegin:
+ Enabled: true
+
+Style/RedundantException:
+ Enabled: true
+
+Style/RedundantFreeze:
+ Enabled: true
+
+Style/RedundantParentheses:
+ Enabled: true
+
+Style/RedundantSelf:
+ Enabled: true
+
+Style/RedundantSortBy:
+ Enabled: true
+
+Layout/RescueEnsureAlignment:
+ Enabled: true
+
+Style/RescueModifier:
+ Enabled: true
+
+Style/Sample:
+ Enabled: true
+
+Style/SelfAssignment:
+ Enabled: true
+
+Layout/SpaceAfterColon:
+ Enabled: true
+
+Layout/SpaceAfterComma:
+ Enabled: true
+
+Layout/SpaceAfterMethodName:
+ Enabled: true
+
+Layout/SpaceAfterNot:
+ Enabled: true
+
+Layout/SpaceAfterSemicolon:
+ Enabled: true
+
+Layout/SpaceBeforeComma:
+ Enabled: true
+
+Layout/SpaceBeforeComment:
+ Enabled: true
+
+Layout/SpaceBeforeSemicolon:
+ Enabled: true
+
+Layout/SpaceAroundKeyword:
+ Enabled: true
+
+Layout/SpaceInsideArrayPercentLiteral:
+ Enabled: true
+
+Layout/SpaceInsidePercentLiteralDelimiters:
+ Enabled: true
+
+Layout/SpaceInsideArrayLiteralBrackets:
+ Enabled: true
+
+Layout/SpaceInsideParens:
+ Enabled: true
+
+Layout/SpaceInsideRangeLiteral:
+ Enabled: true
+
+Style/SymbolLiteral:
+ Enabled: true
+
+Layout/Tab:
+ Enabled: true
+
+Layout/TrailingWhitespace:
+ Enabled: true
+
+Style/UnlessElse:
+ Enabled: true
+
+Style/UnneededCapitalW:
+ Enabled: true
+
+Style/UnneededInterpolation:
+ Enabled: true
+
+Style/UnneededPercentQ:
+ Enabled: true
+
+Style/VariableInterpolation:
+ Enabled: true
+
+Style/WhenThen:
+ Enabled: true
+
+Style/WhileUntilDo:
+ Enabled: true
+
+Style/ZeroLengthPredicate:
+ Enabled: true
+
+Layout/IndentHeredoc:
+ EnforcedStyle: squiggly
+
+Lint/AmbiguousOperator:
+ Enabled: true
+
+Lint/AmbiguousRegexpLiteral:
+ Enabled: true
+
+Lint/CircularArgumentReference:
+ Enabled: true
+
+Layout/ConditionPosition:
+ Enabled: true
+
+Lint/Debugger:
+ Enabled: true
+
+Lint/DeprecatedClassMethods:
+ Enabled: true
+
+Lint/DuplicateMethods:
+ Enabled: true
+
+Lint/DuplicatedKey:
+ Enabled: true
+
+Lint/EachWithObjectArgument:
+ Enabled: true
+
+Lint/ElseLayout:
+ Enabled: true
+
+Lint/EmptyEnsure:
+ Enabled: true
+
+Lint/EmptyInterpolation:
+ Enabled: true
+
+Lint/EndInMethod:
+ Enabled: true
+
+Lint/EnsureReturn:
+ Enabled: true
+
+Lint/FloatOutOfRange:
+ Enabled: true
+
+Lint/FormatParameterMismatch:
+ Enabled: true
+
+Lint/HandleExceptions:
+ Enabled: true
+
+Lint/ImplicitStringConcatenation:
+ Description: Checks for adjacent string literals on the same line, which could
+ better be represented as a single string literal.
+
+Lint/IneffectiveAccessModifier:
+ Description: Checks for attempts to use `private` or `protected` to set the visibility
+ of a class method, which does not work.
+
+Lint/LiteralAsCondition:
+ Enabled: true
+
+Lint/LiteralInInterpolation:
+ Enabled: true
+
+Lint/Loop:
+ Description: Use Kernel#loop with break rather than begin/end/until or begin/end/while
+ for post-loop tests.
+
+Lint/NestedMethodDefinition:
+ Enabled: true
+
+Lint/NextWithoutAccumulator:
+ Description: Do not omit the accumulator when calling `next` in a `reduce`/`inject`
+ block.
+
+Lint/NonLocalExitFromIterator:
+ Enabled: true
+
+Lint/ParenthesesAsGroupedExpression:
+ Enabled: true
+
+Lint/PercentStringArray:
+ Enabled: true
+
+Lint/PercentSymbolArray:
+ Enabled: true
+
+Lint/RandOne:
+ Description: Checks for `rand(1)` calls. Such calls always return `0` and most
+ likely a mistake.
+
+Lint/RequireParentheses:
+ Enabled: true
+
+Lint/RescueException:
+ Enabled: true
+
+Lint/ShadowedException:
+ Enabled: true
+
+Lint/ShadowingOuterLocalVariable:
+ Enabled: true
+
+Lint/StringConversionInInterpolation:
+ Enabled: true
+
+Lint/UnderscorePrefixedVariableName:
+ Enabled: true
+
+Lint/UnifiedInteger:
+ Enabled: true
+
+Lint/UnneededCopDisableDirective:
+ Enabled: true
+
+Lint/UnneededCopEnableDirective:
+ Enabled: true
+
+Lint/UnneededSplatExpansion:
+ Enabled: true
+
+Lint/UnreachableCode:
+ Enabled: true
+
+Lint/UselessAccessModifier:
+ ContextCreatingMethods: []
+
+Lint/UselessAssignment:
+ Enabled: true
+
+Lint/UselessComparison:
+ Enabled: true
+
+Lint/UselessElseWithoutRescue:
+ Enabled: true
+
+Lint/UselessSetterCall:
+ Enabled: true
+
+Lint/Void:
+ Enabled: true
+
+Security/Eval:
+ Enabled: true
+
+Security/JSONLoad:
+ Enabled: true
+
+Security/Open:
+ Enabled: true
+
+Lint/BigDecimalNew:
+ Enabled: true
+
+Style/Strip:
+ Enabled: true
+
+Style/TrailingBodyOnClass:
+ Enabled: true
+
+Style/TrailingBodyOnModule:
+ Enabled: true
+
+Style/TrailingCommaInArrayLiteral:
+ EnforcedStyleForMultiline: comma
+ Enabled: true
+
+Style/TrailingCommaInHashLiteral:
+ EnforcedStyleForMultiline: comma
+ Enabled: true
+
+Layout/SpaceInsideReferenceBrackets:
+ EnforcedStyle: no_space
+ EnforcedStyleForEmptyBrackets: no_space
+ Enabled: true
+
+Style/ModuleFunction:
+ EnforcedStyle: extend_self
+
+Lint/OrderedMagicComments:
+ Enabled: true
diff --git a/.rubocop.yml b/.rubocop.yml
index 4e18078..1c0f832 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,129 +1,16 @@
-inherit_from: ./.rubocop_todo.yml
+inherit_from:
+ - 'https://shopify.github.io/ruby-style-guide/rubocop.yml'
+ - .rubocop_todo.yml
+
+require: rubocop-performance
+
+Performance:
+ Enabled: true
AllCops:
+ TargetRubyVersion: 2.4
Exclude:
- - 'performance/shopify/*'
- - 'pkg/**'
-
-Metrics/BlockNesting:
- Max: 3
-
-Metrics/ModuleLength:
- Enabled: false
-
-Metrics/ClassLength:
- Enabled: false
-
-Lint/AssignmentInCondition:
- Enabled: false
-
-Lint/AmbiguousOperator:
- Enabled: false
-
-Lint/AmbiguousRegexpLiteral:
- Enabled: false
-
-Lint/ParenthesesAsGroupedExpression:
- Enabled: false
-
-Lint/UnusedBlockArgument:
- Enabled: false
-
-Lint/EndAlignment:
- EnforcedStyleAlignWith: variable
-
-Lint/UnusedMethodArgument:
- Enabled: false
-
-Style/SingleLineBlockParams:
- Enabled: false
-
-Style/DoubleNegation:
- Enabled: false
-
-Style/StringLiteralsInInterpolation:
- Enabled: false
-
-Style/AndOr:
- Enabled: false
-
-Style/SignalException:
- Enabled: false
-
-Style/StringLiterals:
- Enabled: false
-
-Style/BracesAroundHashParameters:
- Enabled: false
-
-Style/NumericLiterals:
- Enabled: false
-
-Layout/SpaceInsideBrackets:
- Enabled: false
-
-Layout/SpaceBeforeBlockBraces:
- Enabled: false
-
-Style/Documentation:
- Enabled: false
-
-Style/ClassAndModuleChildren:
- Enabled: false
-
-Style/TrailingCommaInArrayLiteral:
- Enabled: false
-
-Style/TrailingCommaInHashLiteral:
- Enabled: false
-
-Layout/IndentHash:
- EnforcedStyle: consistent
-
-Style/FormatString:
- Enabled: false
-
-Layout/AlignParameters:
- EnforcedStyle: with_fixed_indentation
-
-Layout/MultilineOperationIndentation:
- EnforcedStyle: indented
-
-Style/IfUnlessModifier:
- Enabled: false
-
-Style/RaiseArgs:
- Enabled: false
-
-Style/PreferredHashMethods:
- Enabled: false
-
-Style/RegexpLiteral:
- Enabled: false
-
-Style/SymbolLiteral:
- Enabled: false
-
-Performance/Count:
- Enabled: false
-
-Style/ConstantName:
- Enabled: false
-
-Layout/CaseIndentation:
- Enabled: false
-
-Style/ClassVars:
- Enabled: false
-
-Style/PerlBackrefs:
- Enabled: false
-
-Style/TrivialAccessors:
- AllowPredicates: true
-
-Style/WordArray:
- Enabled: false
+ - 'vendor/bundle/**/*'
Naming/MethodName:
Exclude:
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 8860c19..34a2e24 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,248 +1,48 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
-# on 2017-11-22 11:35:55 -0500 using RuboCop version 0.49.1.
+# on 2019-09-11 06:34:25 +1000 using RuboCop version 0.74.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
-# Offense count: 3
-# Cop supports --auto-correct.
-Layout/ClosingParenthesisIndentation:
- Exclude:
- - 'test/integration/error_handling_test.rb'
-
-# Offense count: 1
-# Cop supports --auto-correct.
-Layout/EmptyLineAfterMagicComment:
- Exclude:
- - 'lib/liquid/version.rb'
-
-# Offense count: 1
-# Cop supports --auto-correct.
-# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
-Layout/ExtraSpacing:
- Exclude:
- - 'test/integration/parsing_quirks_test.rb'
-
-# Offense count: 5
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent
-Layout/IndentHeredoc:
- Exclude:
- - 'test/integration/tags/for_tag_test.rb'
- - 'test/integration/trim_mode_test.rb'
-
-# Offense count: 6
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: symmetrical, new_line, same_line
-Layout/MultilineMethodCallBraceLayout:
- Exclude:
- - 'test/integration/error_handling_test.rb'
- - 'test/unit/strainer_unit_test.rb'
-
# Offense count: 2
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
+# Configuration parameters: EnforcedStyle.
# SupportedStyles: runtime_error, standard_error
Lint/InheritException:
Exclude:
- 'lib/liquid/interrupts.rb'
-# Offense count: 1
-Lint/ScriptPermission:
- Exclude:
- - 'test/test_helper.rb'
-
-# Offense count: 52
-Metrics/AbcSize:
- Max: 56
-
-# Offense count: 13
-Metrics/CyclomaticComplexity:
- Max: 12
-
-# Offense count: 620
-# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
+# Offense count: 98
+# Cop supports --auto-correct.
+# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Metrics/LineLength:
Max: 294
-# Offense count: 102
-# Configuration parameters: CountComments.
-Metrics/MethodLength:
- Max: 37
-
-# Offense count: 9
-Metrics/PerceivedComplexity:
- Max: 11
-
-# Offense count: 10
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: prefer_alias, prefer_alias_method
-Style/Alias:
- Exclude:
- - 'lib/liquid/drop.rb'
- - 'lib/liquid/i18n.rb'
- - 'lib/liquid/profiler/hooks.rb'
- - 'lib/liquid/standardfilters.rb'
- - 'lib/liquid/tag.rb'
- - 'lib/liquid/tags/include.rb'
- - 'lib/liquid/variable.rb'
-
-# Offense count: 1
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly, IncludeTernaryExpressions.
-# SupportedStyles: assign_to_condition, assign_inside_condition
-Style/ConditionalAssignment:
- Exclude:
- - 'lib/liquid/errors.rb'
-
-# Offense count: 2
-# Cop supports --auto-correct.
-Style/EmptyCaseCondition:
+# Offense count: 44
+Naming/ConstantName:
Exclude:
+ - 'lib/liquid.rb'
- 'lib/liquid/block_body.rb'
- - 'lib/liquid/lexer.rb'
-
-# Offense count: 5
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: compact, expanded
-Style/EmptyMethod:
- Exclude:
- - 'lib/liquid/tag.rb'
- - 'lib/liquid/tags/comment.rb'
- - 'lib/liquid/tags/include.rb'
- - 'test/integration/tags/include_tag_test.rb'
- - 'test/unit/context_unit_test.rb'
-
-# Offense count: 2
-# Configuration parameters: SupportedStyles.
-# SupportedStyles: annotated, template
-Style/FormatStringToken:
- EnforcedStyle: template
-
-# Offense count: 14
-# Configuration parameters: MinBodyLength.
-Style/GuardClause:
- Exclude:
- - 'lib/liquid/condition.rb'
- - 'lib/liquid/lexer.rb'
- - 'lib/liquid/strainer.rb'
- 'lib/liquid/tags/assign.rb'
- 'lib/liquid/tags/capture.rb'
- 'lib/liquid/tags/case.rb'
+ - 'lib/liquid/tags/cycle.rb'
- 'lib/liquid/tags/for.rb'
+ - 'lib/liquid/tags/if.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/tags/raw.rb'
- 'lib/liquid/tags/table_row.rb'
- 'lib/liquid/variable.rb'
- - 'test/unit/tokenizer_unit_test.rb'
+ - 'performance/shopify/comment_form.rb'
+ - 'performance/shopify/paginate.rb'
+ - 'test/integration/tags/include_tag_test.rb'
-# Offense count: 4
-# Configuration parameters: SupportedStyles.
-# SupportedStyles: snake_case, camelCase
-Style/MethodName:
- EnforcedStyle: snake_case
-
-# Offense count: 6
-# Cop supports --auto-correct.
-Style/MutableConstant:
- Exclude:
- - 'lib/liquid/expression.rb'
- - 'lib/liquid/lexer.rb'
- - 'lib/liquid/standardfilters.rb'
- - 'lib/liquid/tags/if.rb'
- - 'lib/liquid/variable_lookup.rb'
- - 'lib/liquid/version.rb'
-
-# Offense count: 1
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
-# SupportedStyles: skip_modifier_ifs, always
-Style/Next:
- Exclude:
- - 'lib/liquid/tags/for.rb'
-
-# Offense count: 4
-# Cop supports --auto-correct.
-# Configuration parameters: AutoCorrect, EnforcedStyle, SupportedStyles.
-# SupportedStyles: predicate, comparison
-Style/NumericPredicate:
- Exclude:
- - 'spec/**/*'
- - 'lib/liquid/context.rb'
- - 'lib/liquid/forloop_drop.rb'
- - 'lib/liquid/standardfilters.rb'
- - 'lib/liquid/tablerowloop_drop.rb'
-
-# Offense count: 14
-# Cop supports --auto-correct.
-# Configuration parameters: PreferredDelimiters.
-Style/PercentLiteralDelimiters:
- Exclude:
- - 'lib/liquid/tags/if.rb'
- - 'liquid.gemspec'
- - 'test/integration/assign_test.rb'
- - 'test/integration/standard_filter_test.rb'
-
-# Offense count: 2
-# Cop supports --auto-correct.
-Style/RedundantParentheses:
- Exclude:
- - 'test/unit/condition_unit_test.rb'
-
-# Offense count: 1
-# Cop supports --auto-correct.
-Style/RedundantSelf:
+# Offense count: 5
+Style/ClassVars:
Exclude:
+ - 'lib/liquid/condition.rb'
- 'lib/liquid/strainer.rb'
-
-# Offense count: 9
-# Cop supports --auto-correct.
-# Configuration parameters: AllowAsExpressionSeparator.
-Style/Semicolon:
- Exclude:
- - 'test/integration/error_handling_test.rb'
- - 'test/integration/template_test.rb'
- - 'test/unit/context_unit_test.rb'
-
-# Offense count: 7
-# Cop supports --auto-correct.
-# Configuration parameters: MinSize, SupportedStyles.
-# SupportedStyles: percent, brackets
-Style/SymbolArray:
- EnforcedStyle: brackets
-
-# Offense count: 2
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles, AllowSafeAssignment.
-# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex
-Style/TernaryParentheses:
- Exclude:
- - 'lib/liquid/context.rb'
- - 'lib/liquid/utils.rb'
-
-# Offense count: 4
-# Cop supports --auto-correct.
-Style/UnneededInterpolation:
- Exclude:
- - 'lib/liquid/i18n.rb'
- - 'test/integration/standard_filter_test.rb'
-
-# Offense count: 2
-# Cop supports --auto-correct.
-Style/UnneededPercentQ:
- Exclude:
- - 'test/integration/error_handling_test.rb'
-
-# Offense count: 1
-# Cop supports --auto-correct.
-# Configuration parameters: MaxLineLength.
-Style/WhileUntilModifier:
- Exclude:
- - 'lib/liquid/tags/case.rb'
+ - 'lib/liquid/template.rb'
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index 0e3e476..f9c8c7f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,32 +1,26 @@
language: ruby
+cache: bundler
rvm:
- - 2.1
- - 2.2
- - 2.3
- 2.4
- 2.5
+ - &latest_ruby 2.6
+ - 2.7
- ruby-head
- - jruby-head
-# - rbx-2
-
-sudo: false
-
-addons:
- apt:
- packages:
- - libgmp3-dev
matrix:
+ include:
+ - rvm: *latest_ruby
+ script: bundle exec rake memory_profile:run
+ name: Profiling Memory Usage
allow_failures:
- rvm: ruby-head
- - rvm: jruby-head
-install:
- - gem install rainbow -v 2.2.1
- - bundle install
-
-script: bundle exec rake
+branches:
+ only:
+ - master
+ - gh-pages
+ - /.*-stable/
notifications:
disable: true
diff --git a/Gemfile b/Gemfile
index bdeefac..f520934 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
source 'https://rubygems.org'
git_source(:github) do |repo_name|
"https://github.com/#{repo_name}.git"
@@ -5,16 +7,21 @@ end
gemspec
-gem 'stackprof', platforms: :mri
-
group :benchmark, :test do
gem 'benchmark-ips'
+ gem 'memory_profiler'
+ gem 'terminal-table'
+
+ install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do
+ gem 'stackprof'
+ end
end
group :test do
- gem 'rubocop', '~> 0.49.0'
+ gem 'rubocop', '~> 0.74.0', require: false
+ gem 'rubocop-performance', require: false
- platform :mri do
- gem 'liquid-c', github: 'Shopify/liquid-c', ref: '9168659de45d6d576fce30c735f857e597fa26f6'
+ platform :mri, :truffleruby do
+ gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'master'
end
end
diff --git a/History.md b/History.md
index 2dc8f3d..9a82faa 100644
--- a/History.md
+++ b/History.md
@@ -1,5 +1,58 @@
# Liquid Change Log
+## 4.0.3 / 2019-03-12
+
+### Fixed
+* Fix break and continue tags inside included templates in loops (#1072) [Justin Li]
+
+## 4.0.2 / 2019-03-08
+
+### Changed
+* Add `where` filter (#1026) [Samuel Doiron]
+* Add `ParseTreeVisitor` to iterate the Liquid AST (#1025) [Stephen Paul Weber]
+* Improve `strip_html` performance (#1032) [printercu]
+
+### Fixed
+* Add error checking for invalid combinations of inputs to sort, sort_natural, where, uniq, map, compact filters (#1059) [Garland Zhang]
+* Validate the character encoding in url_decode (#1070) [Clayton Smith]
+
+## 4.0.1 / 2018-10-09
+
+### Changed
+* Add benchmark group in Gemfile (#855) [Jerry Liu]
+* Allow benchmarks to benchmark render by itself (#851) [Jerry Liu]
+* Avoid calling `line_number` on String node when rescuing a render error. (#860) [Dylan Thacker-Smith]
+* Avoid duck typing to detect whether to call render on a node. [Dylan Thacker-Smith]
+* Clarify spelling of `reversed` on `for` block tag (#843) [Mark Crossfield]
+* Replace recursion with loop to avoid potential stack overflow from malicious input (#891, #892) [Dylan Thacker-Smith]
+* Limit block tag nesting to 100 (#894) [Dylan Thacker-Smith]
+* Replace `assert_equal nil` with `assert_nil` (#895) [Dylan Thacker-Smith]
+* Remove Spy Gem (#896) [Dylan Thacker-Smith]
+* Add `collection_name` and `variable_name` reader to `For` block (#909)
+* Symbols render as strings (#920) [Justin Li]
+* Remove default value from Hash objects (#932) [Maxime Bedard]
+* Remove one level of nesting (#944) [Dylan Thacker-Smith]
+* Update Rubocop version (#952) [Justin Li]
+* Add `at_least` and `at_most` filters (#954, #958) [Nithin Bekal]
+* Add a regression test for a liquid-c trim mode bug (#972) [Dylan Thacker-Smith]
+* Use https rather than git protocol to fetch liquid-c [Dylan Thacker-Smith]
+* Add tests against Ruby 2.4 (#963) and 2.5 (#981)
+* Replace RegExp literals with constants (#988) [Ashwin Maroli]
+* Replace unnecessary `#each_with_index` with `#each` (#992) [Ashwin Maroli]
+* Improve the unexpected end delimiter message for block tags. (#1003) [Dylan Thacker-Smith]
+* Refactor and optimize rendering (#1005) [Christopher Aue]
+* Add installation instruction (#1006) [Ben Gift]
+* Remove Circle CI (#1010)
+* Rename deprecated `BigDecimal.new` to `BigDecimal` (#1024) [Koichi ITO]
+* Rename deprecated Rubocop name (#1027) [Justin Li]
+
+### Fixed
+* Handle `join` filter on non String joiners (#857) [Richard Monette]
+* Fix duplicate inclusion condition logic error of `Liquid::Strainer.add_filter` method (#861)
+* Fix `escape`, `url_encode`, `url_decode` not handling non-string values (#898) [Thierry Joyal]
+* Fix raise when variable is defined but nil when using `strict_variables` [Pascal Betz]
+* Fix `sort` and `sort_natural` to handle arrays with nils (#930) [Eric Chan]
+
## 4.0.0 / 2016-12-14 / branch "4-0-stable"
### Changed
diff --git a/README.md b/README.md
index 77e9ff4..6802a71 100644
--- a/README.md
+++ b/README.md
@@ -106,3 +106,9 @@ template = Liquid::Template.parse("{{x}} {{y}}")
template.render!({ 'x' => 1}, { strict_variables: true })
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y
```
+
+### Usage tracking
+
+To help track usages of a feature or code path in production, we have released opt-in usage tracking. To enable this, we provide an empty `Liquid:: Usage.increment` method which you can customize to your needs. The feature is well suited to https://github.com/Shopify/statsd-instrument. However, the choice of implementation is up to you.
+
+Once you have enabled usage tracking, we recommend reporting any events through Github Issues that your system may be logging. It is highly likely this event has been added to consider deprecating or improving code specific to this event, so please raise any concerns.
\ No newline at end of file
diff --git a/Rakefile b/Rakefile
index e3633f5..66ae0fa 100755
--- a/Rakefile
+++ b/Rakefile
@@ -1,29 +1,33 @@
+# frozen_string_literal: true
+
require 'rake'
require 'rake/testtask'
-$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
+$LOAD_PATH.unshift(File.expand_path("../lib", __FILE__))
require "liquid/version"
-task default: [:rubocop, :test]
+task(default: [:test, :rubocop])
-desc 'run test suite with default parser'
+desc('run test suite with default parser')
Rake::TestTask.new(:base_test) do |t|
t.libs << '.' << 'lib' << 'test'
t.test_files = FileList['test/{integration,unit}/**/*_test.rb']
t.verbose = false
end
-desc 'run test suite with warn error mode'
+desc('run test suite with warn error mode')
task :warn_test do
ENV['LIQUID_PARSER_MODE'] = 'warn'
Rake::Task['base_test'].invoke
end
task :rubocop do
- require 'rubocop/rake_task'
- RuboCop::RakeTask.new
+ if RUBY_ENGINE == 'ruby'
+ require 'rubocop/rake_task'
+ RuboCop::RakeTask.new
+ end
end
-desc 'runs test suite with both strict and lax parsers'
+desc('runs test suite with both strict and lax parsers')
task :test do
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].invoke
@@ -32,8 +36,8 @@ task :test do
Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke
- if RUBY_ENGINE == 'ruby'
- ENV['LIQUID-C'] = '1'
+ if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
+ ENV['LIQUID_C'] = '1'
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].reenable
@@ -45,7 +49,7 @@ task :test do
end
end
-task gem: :build
+task(gem: :build)
task :build do
system "gem build liquid.gemspec"
end
@@ -85,7 +89,14 @@ namespace :profile do
end
end
-desc "Run example"
+namespace :memory_profile do
+ desc "Run memory profiler"
+ task :run do
+ ruby "./performance/memory_profile.rb"
+ end
+end
+
+desc("Run example")
task :example do
ruby "-w -d -Ilib example/server/server.rb"
end
diff --git a/example/server/example_servlet.rb b/example/server/example_servlet.rb
index dbc7a4b..b09e3bb 100644
--- a/example/server/example_servlet.rb
+++ b/example/server/example_servlet.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
module ProductsFilter
def price(integer)
- sprintf("$%.2d USD", integer / 100.0)
+ format("$%.2d USD", integer / 100.0)
end
def prettyprint(text)
diff --git a/example/server/liquid_servlet.rb b/example/server/liquid_servlet.rb
index b2bf515..55e21d2 100644
--- a/example/server/liquid_servlet.rb
+++ b/example/server/liquid_servlet.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
def do_GET(req, res)
handle(:get, req, res)
@@ -9,12 +11,12 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
private
- def handle(type, req, res)
+ def handle(_type, req, res)
@request = req
@response = res
@request.path_info =~ /(\w+)\z/
- @action = $1 || 'index'
+ @action = Regexp.last_match(1) || 'index'
@assigns = send(@action) if respond_to?(@action)
@response['Content-Type'] = "text/html"
diff --git a/example/server/server.rb b/example/server/server.rb
index 703b361..bb7a4bc 100644
--- a/example/server/server.rb
+++ b/example/server/server.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'webrick'
require 'rexml/document'
@@ -8,5 +10,5 @@ require_relative 'example_servlet'
# Setup webrick
server = WEBrick::HTTPServer.new(Port: ARGV[1] || 3000)
server.mount('/', Servlet)
-trap("INT"){ server.shutdown }
+trap("INT") { server.shutdown }
server.start
diff --git a/lib/liquid.rb b/lib/liquid.rb
index 7d9da26..cfaccee 100644
--- a/lib/liquid.rb
+++ b/lib/liquid.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Copyright (c) 2005 Tobias Luetke
#
# Permission is hereby granted, free of charge, to any person obtaining
@@ -21,10 +23,10 @@
module Liquid
FilterSeparator = /\|/
- ArgumentSeparator = ','.freeze
- FilterArgumentSeparator = ':'.freeze
- VariableAttributeSeparator = '.'.freeze
- WhitespaceControl = '-'.freeze
+ ArgumentSeparator = ','
+ FilterArgumentSeparator = ':'
+ VariableAttributeSeparator = '.'
+ WhitespaceControl = '-'
TagStart = /\{\%/
TagEnd = /\%\}/
VariableSignature = /\(?[\w\-\.\[\]]\)?/
@@ -45,6 +47,7 @@ module Liquid
end
require "liquid/version"
+require 'liquid/parse_tree_visitor'
require 'liquid/lexer'
require 'liquid/parser'
require 'liquid/i18n'
@@ -73,7 +76,12 @@ require 'liquid/condition'
require 'liquid/utils'
require 'liquid/tokenizer'
require 'liquid/parse_context'
+require 'liquid/partial_cache'
+require 'liquid/usage'
+require 'liquid/register'
+require 'liquid/static_registers'
# Load all the tags of the standard library
#
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
+Dir["#{__dir__}/liquid/registers/*.rb"].each { |f| require f }
diff --git a/lib/liquid/block.rb b/lib/liquid/block.rb
index 00c59b2..f669844 100644
--- a/lib/liquid/block.rb
+++ b/lib/liquid/block.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Liquid
class Block < Tag
MAX_DEPTH = 100
@@ -13,6 +15,7 @@ module Liquid
end
end
+ # For backwards compatibility
def render(context)
@body.render(context)
end
@@ -26,16 +29,16 @@ module Liquid
end
def unknown_tag(tag, _params, _tokens)
- if tag == 'else'.freeze
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
- block_name: block_name))
- elsif tag.start_with?('end'.freeze)
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze,
+ if tag == 'else'
+ raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else",
+ block_name: block_name)
+ elsif tag.start_with?('end')
+ raise SyntaxError, parse_context.locale.t("errors.syntax.invalid_delimiter",
tag: tag,
block_name: block_name,
- block_delimiter: block_delimiter))
+ block_delimiter: block_delimiter)
else
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
+ raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
end
end
@@ -51,7 +54,7 @@ module Liquid
def parse_body(body, tokens)
if parse_context.depth >= MAX_DEPTH
- raise StackLevelError, "Nesting too deep".freeze
+ raise StackLevelError, "Nesting too deep"
end
parse_context.depth += 1
begin
@@ -60,7 +63,7 @@ module Liquid
return false if end_tag_name == block_delimiter
unless end_tag_name
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
+ raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
end
# this tag is not registered with the system
diff --git a/lib/liquid/block_body.rb b/lib/liquid/block_body.rb
index 266d8ed..45be8b8 100644
--- a/lib/liquid/block_body.rb
+++ b/lib/liquid/block_body.rb
@@ -1,10 +1,13 @@
+# frozen_string_literal: true
+
module Liquid
class BlockBody
- FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
+ LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
+ FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
WhitespaceOrNothing = /\A\s*\z/
- TAGSTART = "{%".freeze
- VARSTART = "{{".freeze
+ TAGSTART = "{%"
+ VARSTART = "{{"
attr_reader :nodelist
@@ -13,9 +16,43 @@ module Liquid
@blank = true
end
- def parse(tokenizer, parse_context)
+ def parse(tokenizer, parse_context, &block)
parse_context.line_number = tokenizer.line_number
- while token = tokenizer.shift
+
+ if tokenizer.for_liquid_tag
+ parse_for_liquid_tag(tokenizer, parse_context, &block)
+ else
+ parse_for_document(tokenizer, parse_context, &block)
+ end
+ end
+
+ private def parse_for_liquid_tag(tokenizer, parse_context)
+ while (token = tokenizer.shift)
+ unless token.empty? || token =~ WhitespaceOrNothing
+ unless token =~ LiquidTagToken
+ # line isn't empty but didn't match tag syntax, yield and let the
+ # caller raise a syntax error
+ return yield token, token
+ end
+ tag_name = Regexp.last_match(1)
+ markup = Regexp.last_match(2)
+ unless (tag = registered_tags[tag_name])
+ # end parsing if we reach an unknown tag and let the caller decide
+ # determine how to proceed
+ return yield tag_name, markup
+ end
+ new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
+ @blank &&= new_tag.blank?
+ @nodelist << new_tag
+ end
+ parse_context.line_number = tokenizer.line_number
+ end
+
+ yield nil, nil
+ end
+
+ private def parse_for_document(tokenizer, parse_context, &block)
+ while (token = tokenizer.shift)
next if token.empty?
case
when token.start_with?(TAGSTART)
@@ -23,10 +60,21 @@ module Liquid
unless token =~ FullToken
raise_missing_tag_terminator(token, parse_context)
end
- tag_name = $1
- markup = $2
- # fetch the tag from registered blocks
- unless tag = registered_tags[tag_name]
+ tag_name = Regexp.last_match(2)
+ markup = Regexp.last_match(4)
+
+ if parse_context.line_number
+ # newlines inside the tag should increase the line number,
+ # particularly important for multiline {% liquid %} tags
+ parse_context.line_number += Regexp.last_match(1).count("\n") + Regexp.last_match(3).count("\n")
+ end
+
+ if tag_name == 'liquid'
+ liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
+ next parse_for_liquid_tag(liquid_tag_tokenizer, parse_context, &block)
+ end
+
+ unless (tag = registered_tags[tag_name])
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
return yield tag_name, markup
@@ -55,7 +103,7 @@ module Liquid
def whitespace_handler(token, parse_context)
if token[2] == WhitespaceControl
previous_token = @nodelist.last
- if previous_token.is_a? String
+ if previous_token.is_a?(String)
previous_token.rstrip!
end
end
@@ -67,19 +115,23 @@ module Liquid
end
def render(context)
- output = []
+ render_to_output_buffer(context, +'')
+ end
+
+ def render_to_output_buffer(context, output)
context.resource_limits.render_score += @nodelist.length
idx = 0
- while node = @nodelist[idx]
+ while (node = @nodelist[idx])
+ previous_output_size = output.bytesize
+
case node
when String
- check_resources(context, node)
output << node
when Variable
- render_node_to_output(node, output, context)
+ render_node(context, output, node)
when Block
- render_node_to_output(node, output, context, node.blank?)
+ render_node(context, node.blank? ? +'' : output, node)
break if context.interrupt? # might have happened in a for-block
when Continue, Break
# If we get an Interrupt that means the block must stop processing. An
@@ -88,35 +140,43 @@ module Liquid
context.push_interrupt(node.interrupt)
break
else # Other non-Block tags
- render_node_to_output(node, output, context)
+ render_node(context, output, node)
+ break if context.interrupt? # might have happened through an include
end
idx += 1
+
+ raise_if_resource_limits_reached(context, output.bytesize - previous_output_size)
end
- output.join
+ output
end
private
- def render_node_to_output(node, output, context, skip_output = false)
- node_output = node.render(context)
- node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
- check_resources(context, node_output)
- output << node_output unless skip_output
- rescue MemoryError => e
- raise e
+ def render_node(context, output, node)
+ if node.disabled?(context)
+ output << node.disabled_error_message
+ return
+ end
+ disable_tags(context, node.disabled_tags) do
+ node.render_to_output_buffer(context, output)
+ end
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, node.line_number)
- output << nil
rescue ::StandardError => e
line_number = node.is_a?(String) ? nil : node.line_number
output << context.handle_error(e, line_number)
end
- def check_resources(context, node_output)
- context.resource_limits.render_length += node_output.length
+ def disable_tags(context, tags, &block)
+ return yield if tags.empty?
+ context.registers[:disabled_tags].disable(tags, &block)
+ end
+
+ def raise_if_resource_limits_reached(context, length)
+ context.resource_limits.render_length += length
return unless context.resource_limits.reached?
- raise MemoryError.new("Memory limits exceeded".freeze)
+ raise MemoryError, "Memory limits exceeded"
end
def create_variable(token, parse_context)
@@ -128,11 +188,11 @@ module Liquid
end
def raise_missing_tag_terminator(token, parse_context)
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect))
+ raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
end
def raise_missing_variable_terminator(token, parse_context)
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect))
+ raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
end
def registered_tags
diff --git a/lib/liquid/condition.rb b/lib/liquid/condition.rb
index 3e79849..93ec68b 100644
--- a/lib/liquid/condition.rb
+++ b/lib/liquid/condition.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Liquid
# Container for liquid nodes which conveniently wraps decision making logic
#
@@ -8,35 +10,35 @@ module Liquid
#
class Condition #:nodoc:
@@operators = {
- '=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
- '!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
- '<>'.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
- '<'.freeze => :<,
- '>'.freeze => :>,
- '>='.freeze => :>=,
- '<='.freeze => :<=,
- 'contains'.freeze => lambda do |cond, left, right|
+ '==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
+ '!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
+ '<>' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
+ '<' => :<,
+ '>' => :>,
+ '>=' => :>=,
+ '<=' => :<=,
+ 'contains' => lambda do |_cond, left, right|
if left && right && left.respond_to?(:include?)
right = right.to_s if left.is_a?(String)
left.include?(right)
else
false
end
- end
+ end,
}
def self.operators
@@operators
end
- attr_reader :attachment
+ attr_reader :attachment, :child_condition
attr_accessor :left, :operator, :right
def initialize(left = nil, operator = nil, right = nil)
@left = left
@operator = operator
@right = right
- @child_relation = nil
+ @child_relation = nil
@child_condition = nil
end
@@ -78,12 +80,12 @@ module Liquid
end
def inspect
- "#
tags in front of all newlines in input string
def newline_to_br(input)
- input.to_s.gsub(/\n/, "
\n".freeze)
+ input.to_s.gsub(/\n/, "
\n")
end
# Reformat a date using Ruby's core Time#strftime( string ) -> string
@@ -292,7 +344,7 @@ module Liquid
def date(input, format)
return input if format.to_s.empty?
- return input unless date = Utils.to_date(input)
+ return input unless (date = Utils.to_date(input))
date.strftime(format.to_s)
end
@@ -386,8 +438,9 @@ module Liquid
result.is_a?(BigDecimal) ? result.to_f : result
end
- def default(input, default_value = ''.freeze)
+ def default(input, default_value = '')
if !input || input.respond_to?(:empty?) && input.empty?
+ Usage.increment("default_filter_received_false_value") if input == false # See https://github.com/Shopify/liquid/issues/1127
default_value
else
input
@@ -396,11 +449,31 @@ module Liquid
private
+ def raise_property_error(property)
+ raise Liquid::ArgumentError, "cannot select the property '#{property}'"
+ end
+
def apply_operation(input, operand, operation)
result = Utils.to_number(input).send(operation, Utils.to_number(operand))
result.is_a?(BigDecimal) ? result.to_f : result
end
+ def nil_safe_compare(a, b)
+ if !a.nil? && !b.nil?
+ a <=> b
+ else
+ a.nil? ? 1 : -1
+ end
+ end
+
+ def nil_safe_casecmp(a, b)
+ if !a.nil? && !b.nil?
+ a.to_s.casecmp(b.to_s)
+ else
+ a.nil? ? 1 : -1
+ end
+ end
+
class InputIterator
include Enumerable
diff --git a/lib/liquid/static_registers.rb b/lib/liquid/static_registers.rb
new file mode 100644
index 0000000..06c52ab
--- /dev/null
+++ b/lib/liquid/static_registers.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Liquid
+ class StaticRegisters
+ attr_reader :static, :registers
+
+ def initialize(registers = {})
+ @static = registers.is_a?(StaticRegisters) ? registers.static : registers
+ @registers = {}
+ end
+
+ def []=(key, value)
+ @registers[key] = value
+ end
+
+ def [](key)
+ if @registers.key?(key)
+ @registers[key]
+ else
+ @static[key]
+ end
+ end
+
+ def delete(key)
+ @registers.delete(key)
+ end
+
+ def fetch(key, default = nil)
+ key?(key) ? self[key] : default
+ end
+
+ def key?(key)
+ @registers.key?(key) || @static.key?(key)
+ end
+ end
+end
diff --git a/lib/liquid/strainer.rb b/lib/liquid/strainer.rb
index 76d56d2..3f3417e 100644
--- a/lib/liquid/strainer.rb
+++ b/lib/liquid/strainer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'set'
module Liquid
@@ -27,7 +29,7 @@ module Liquid
def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
- unless self.include?(filter)
+ unless include?(filter)
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
if invokable_non_public_methods.any?
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
@@ -54,7 +56,7 @@ module Liquid
def invoke(method, *args)
if self.class.invokable?(method)
send(method, *args)
- elsif @context && @context.strict_filters
+ elsif @context&.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}"
else
args.first
diff --git a/lib/liquid/tablerowloop_drop.rb b/lib/liquid/tablerowloop_drop.rb
index cda4a1e..0d00b6f 100644
--- a/lib/liquid/tablerowloop_drop.rb
+++ b/lib/liquid/tablerowloop_drop.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Liquid
class TablerowloopDrop < Drop
def initialize(length, cols)
diff --git a/lib/liquid/tag.rb b/lib/liquid/tag.rb
index 06970c1..ffd2286 100644
--- a/lib/liquid/tag.rb
+++ b/lib/liquid/tag.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Liquid
class Tag
attr_reader :nodelist, :tag_name, :line_number, :parse_context
@@ -5,13 +7,21 @@ module Liquid
include ParserSwitching
class << self
- def parse(tag_name, markup, tokenizer, options)
- tag = new(tag_name, markup, options)
+ def parse(tag_name, markup, tokenizer, parse_context)
+ tag = new(tag_name, markup, parse_context)
tag.parse(tokenizer)
tag
end
+ def disable_tags(*tags)
+ disabled_tags.push(*tags)
+ end
+
private :new
+
+ def disabled_tags
+ @disabled_tags ||= []
+ end
end
def initialize(tag_name, markup, parse_context)
@@ -33,11 +43,31 @@ module Liquid
end
def render(_context)
- ''.freeze
+ ''
+ end
+
+ def disabled?(context)
+ context.registers[:disabled_tags].disabled?(tag_name)
+ end
+
+ def disabled_error_message
+ "#{tag_name} #{options[:locale].t('errors.disabled.tag')}"
+ end
+
+ # For backwards compatibility with custom tags. In a future release, the semantics
+ # of the `render_to_output_buffer` method will become the default and the `render`
+ # method will be removed.
+ def render_to_output_buffer(context, output)
+ output << render(context)
+ output
end
def blank?
false
end
+
+ def disabled_tags
+ self.class.disabled_tags
+ end
end
end
diff --git a/lib/liquid/tags/assign.rb b/lib/liquid/tags/assign.rb
index f6cd5fa..aaad14c 100644
--- a/lib/liquid/tags/assign.rb
+++ b/lib/liquid/tags/assign.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Liquid
# Assign sets a variable in your template.
#
@@ -10,21 +12,27 @@ module Liquid
class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
+ def self.syntax_error_translation_key
+ "errors.syntax.assign"
+ end
+
+ attr_reader :to, :from
+
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
- @to = $1
- @from = Variable.new($2, options)
+ @to = Regexp.last_match(1)
+ @from = Variable.new(Regexp.last_match(2), options)
else
- raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
+ raise SyntaxError, options[:locale].t(self.class.syntax_error_translation_key)
end
end
- def render(context)
+ def render_to_output_buffer(context, output)
val = @from.render(context)
context.scopes.last[@to] = val
context.resource_limits.assign_score += assign_score_of(val)
- ''.freeze
+ output
end
def blank?
@@ -35,7 +43,7 @@ module Liquid
def assign_score_of(val)
if val.instance_of?(String)
- val.length
+ val.bytesize
elsif val.instance_of?(Array) || val.instance_of?(Hash)
sum = 1
# Uses #each to avoid extra allocations.
@@ -45,7 +53,13 @@ module Liquid
1
end
end
+
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
+ def children
+ [@node.from]
+ end
+ end
end
- Template.register_tag('assign'.freeze, Assign)
+ Template.register_tag('assign', Assign)
end
diff --git a/lib/liquid/tags/break.rb b/lib/liquid/tags/break.rb
index 6fe0969..80f4627 100644
--- a/lib/liquid/tags/break.rb
+++ b/lib/liquid/tags/break.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Liquid
# Break tag to be used to break out of a for loop.
#
@@ -14,5 +16,5 @@ module Liquid
end
end
- Template.register_tag('break'.freeze, Break)
+ Template.register_tag('break', Break)
end
diff --git a/lib/liquid/tags/capture.rb b/lib/liquid/tags/capture.rb
index 8674356..1cace9c 100644
--- a/lib/liquid/tags/capture.rb
+++ b/lib/liquid/tags/capture.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Liquid
# Capture stores the result of a block into a variable without rendering it inplace.
#
@@ -16,17 +18,18 @@ module Liquid
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
- @to = $1
+ @to = Regexp.last_match(1)
else
- raise SyntaxError.new(options[:locale].t("errors.syntax.capture"))
+ raise SyntaxError, options[:locale].t("errors.syntax.capture")
end
end
- def render(context)
- output = super
+ def render_to_output_buffer(context, output)
+ previous_output_size = output.bytesize
+ super
context.scopes.last[@to] = output
- context.resource_limits.assign_score += output.length
- ''.freeze
+ context.resource_limits.assign_score += (output.bytesize - previous_output_size)
+ output
end
def blank?
@@ -34,5 +37,5 @@ module Liquid
end
end
- Template.register_tag('capture'.freeze, Capture)
+ Template.register_tag('capture', Capture)
end
diff --git a/lib/liquid/tags/case.rb b/lib/liquid/tags/case.rb
index 453b4d6..30484c6 100644
--- a/lib/liquid/tags/case.rb
+++ b/lib/liquid/tags/case.rb
@@ -1,24 +1,26 @@
+# frozen_string_literal: true
+
module Liquid
class Case < Block
Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
+ attr_reader :blocks, :left
+
def initialize(tag_name, markup, options)
super
@blocks = []
if markup =~ Syntax
- @left = Expression.parse($1)
+ @left = Expression.parse(Regexp.last_match(1))
else
- raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze))
+ raise SyntaxError, options[:locale].t("errors.syntax.case")
end
end
def parse(tokens)
body = BlockBody.new
- while parse_body(body, tokens)
- body = @blocks.last.attachment
- end
+ body = @blocks.last.attachment while parse_body(body, tokens)
end
def nodelist
@@ -27,30 +29,28 @@ module Liquid
def unknown_tag(tag, markup, tokens)
case tag
- when 'when'.freeze
+ when 'when'
record_when_condition(markup)
- when 'else'.freeze
+ when 'else'
record_else_condition(markup)
else
super
end
end
- def render(context)
- context.stack do
- execute_else_block = true
+ def render_to_output_buffer(context, output)
+ execute_else_block = true
- output = ''
- @blocks.each do |block|
- if block.else?
- return block.attachment.render(context) if execute_else_block
- elsif block.evaluate(context)
- execute_else_block = false
- output << block.attachment.render(context)
- end
+ @blocks.each do |block|
+ if block.else?
+ block.attachment.render_to_output_buffer(context, output) if execute_else_block
+ elsif block.evaluate(context)
+ execute_else_block = false
+ block.attachment.render_to_output_buffer(context, output)
end
- output
end
+
+ output
end
private
@@ -60,12 +60,12 @@ module Liquid
while markup
unless markup =~ WhenSyntax
- raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
+ raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when")
end
- markup = $2
+ markup = Regexp.last_match(2)
- block = Condition.new(@left, '=='.freeze, Expression.parse($1))
+ block = Condition.new(@left, '==', Expression.parse(Regexp.last_match(1)))
block.attach(body)
@blocks << block
end
@@ -73,14 +73,20 @@ module Liquid
def record_else_condition(markup)
unless markup.strip.empty?
- raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
+ raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else")
end
block = ElseCondition.new
block.attach(BlockBody.new)
@blocks << block
end
+
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
+ def children
+ [@node.left] + @node.blocks
+ end
+ end
end
- Template.register_tag('case'.freeze, Case)
+ Template.register_tag('case', Case)
end
diff --git a/lib/liquid/tags/comment.rb b/lib/liquid/tags/comment.rb
index c57c9cd..a5460f9 100644
--- a/lib/liquid/tags/comment.rb
+++ b/lib/liquid/tags/comment.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
module Liquid
class Comment < Block
- def render(_context)
- ''.freeze
+ def render_to_output_buffer(_context, output)
+ output
end
def unknown_tag(_tag, _markup, _tokens)
@@ -12,5 +14,5 @@ module Liquid
end
end
- Template.register_tag('comment'.freeze, Comment)
+ Template.register_tag('comment', Comment)
end
diff --git a/lib/liquid/tags/continue.rb b/lib/liquid/tags/continue.rb
index 9c81ec2..fb1f371 100644
--- a/lib/liquid/tags/continue.rb
+++ b/lib/liquid/tags/continue.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Liquid
# Continue tag to be used to break out of a for loop.
#
@@ -14,5 +16,5 @@ module Liquid
end
end
- Template.register_tag('continue'.freeze, Continue)
+ Template.register_tag('continue', Continue)
end
diff --git a/lib/liquid/tags/cycle.rb b/lib/liquid/tags/cycle.rb
index ad116a6..b203c78 100644
--- a/lib/liquid/tags/cycle.rb
+++ b/lib/liquid/tags/cycle.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Liquid
# Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
#
@@ -15,32 +17,43 @@ module Liquid
SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
+ attr_reader :variables
+
def initialize(tag_name, markup, options)
super
case markup
when NamedSyntax
- @variables = variables_from_string($2)
- @name = Expression.parse($1)
+ @variables = variables_from_string(Regexp.last_match(2))
+ @name = Expression.parse(Regexp.last_match(1))
when SimpleSyntax
@variables = variables_from_string(markup)
@name = @variables.to_s
else
- raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze))
+ raise SyntaxError, options[:locale].t("errors.syntax.cycle")
end
end
- def render(context)
+ def render_to_output_buffer(context, output)
context.registers[:cycle] ||= {}
- context.stack do
- key = context.evaluate(@name)
- iteration = context.registers[:cycle][key].to_i
- result = context.evaluate(@variables[iteration])
- iteration += 1
- iteration = 0 if iteration >= @variables.size
- context.registers[:cycle][key] = iteration
- result
+ key = context.evaluate(@name)
+ iteration = context.registers[:cycle][key].to_i
+
+ val = context.evaluate(@variables[iteration])
+
+ if val.is_a?(Array)
+ val = val.join
+ elsif !val.is_a?(String)
+ val = val.to_s
end
+
+ output << val
+
+ iteration += 1
+ iteration = 0 if iteration >= @variables.size
+ context.registers[:cycle][key] = iteration
+
+ output
end
private
@@ -48,9 +61,15 @@ module Liquid
def variables_from_string(markup)
markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/o
- $1 ? Expression.parse($1) : nil
+ Regexp.last_match(1) ? Expression.parse(Regexp.last_match(1)) : nil
end.compact
end
+
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
+ def children
+ Array(@node.variables)
+ end
+ end
end
Template.register_tag('cycle', Cycle)
diff --git a/lib/liquid/tags/decrement.rb b/lib/liquid/tags/decrement.rb
index b5cdaaa..d761a0c 100644
--- a/lib/liquid/tags/decrement.rb
+++ b/lib/liquid/tags/decrement.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Liquid
# decrement is used in a place where one needs to insert a counter
# into a template, and needs the counter to survive across
@@ -23,13 +25,14 @@ module Liquid
@variable = markup.strip
end
- def render(context)
+ def render_to_output_buffer(context, output)
value = context.environments.first[@variable] ||= 0
value -= 1
context.environments.first[@variable] = value
- value.to_s
+ output << value.to_s
+ output
end
end
- Template.register_tag('decrement'.freeze, Decrement)
+ Template.register_tag('decrement', Decrement)
end
diff --git a/lib/liquid/tags/echo.rb b/lib/liquid/tags/echo.rb
new file mode 100644
index 0000000..1f78937
--- /dev/null
+++ b/lib/liquid/tags/echo.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Liquid
+ # Echo outputs an expression
+ #
+ # {% echo monkey %}
+ # {% echo user.name %}
+ #
+ # This is identical to variable output syntax, like {{ foo }}, but works
+ # inside {% liquid %} tags. The full syntax is supported, including filters:
+ #
+ # {% echo user | link %}
+ #
+ class Echo < Tag
+ def initialize(tag_name, markup, parse_context)
+ super
+ @variable = Variable.new(markup, parse_context)
+ end
+
+ def render(context)
+ @variable.render_to_output_buffer(context, +'')
+ end
+ end
+
+ Template.register_tag('echo', Echo)
+end
diff --git a/lib/liquid/tags/for.rb b/lib/liquid/tags/for.rb
index 6c95624..5c7b560 100644
--- a/lib/liquid/tags/for.rb
+++ b/lib/liquid/tags/for.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Liquid
# "For" iterates over an array or collection.
# Several useful variables are available to you within the loop.
@@ -46,8 +48,7 @@ module Liquid
class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
- attr_reader :collection_name
- attr_reader :variable_name
+ attr_reader :collection_name, :variable_name, :limit, :from
def initialize(tag_name, markup, options)
super
@@ -67,49 +68,51 @@ module Liquid
end
def unknown_tag(tag, markup, tokens)
- return super unless tag == 'else'.freeze
+ return super unless tag == 'else'
@else_block = BlockBody.new
end
- def render(context)
+ def render_to_output_buffer(context, output)
segment = collection_segment(context)
if segment.empty?
- render_else(context)
+ render_else(context, output)
else
- render_segment(context, segment)
+ render_segment(context, output, segment)
end
+
+ output
end
protected
def lax_parse(markup)
if markup =~ Syntax
- @variable_name = $1
- collection_name = $2
- @reversed = !!$3
+ @variable_name = Regexp.last_match(1)
+ collection_name = Regexp.last_match(2)
+ @reversed = !!Regexp.last_match(3)
@name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name)
markup.scan(TagAttributes) do |key, value|
set_attribute(key, value)
end
else
- raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze))
+ raise SyntaxError, options[:locale].t("errors.syntax.for")
end
end
def strict_parse(markup)
p = Parser.new(markup)
@variable_name = p.consume(:id)
- raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
+ raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
collection_name = p.expression
@name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name)
- @reversed = p.id?('reversed'.freeze)
+ @reversed = p.id?('reversed')
while p.look(:id) && p.look(:colon, 1)
- unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze)
- raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze))
+ unless (attribute = p.id?('limit') || p.id?('offset'))
+ raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
end
p.consume
set_attribute(attribute, p.expression)
@@ -125,14 +128,23 @@ module Liquid
from = if @from == :continue
offsets[@name].to_i
else
- context.evaluate(@from).to_i
+ from_value = context.evaluate(@from)
+ if from_value.nil?
+ 0
+ else
+ Utils.to_integer(from_value)
+ end
end
collection = context.evaluate(@collection_name)
collection = collection.to_a if collection.is_a?(Range)
- limit = context.evaluate(@limit)
- to = limit ? limit.to_i + from : nil
+ limit_value = context.evaluate(@limit)
+ to = if limit_value.nil?
+ nil
+ else
+ Utils.to_integer(limit_value) + from
+ end
segment = Utils.slice_collection(collection, from, to)
segment.reverse! if @reversed
@@ -142,57 +154,64 @@ module Liquid
segment
end
- def render_segment(context, segment)
+ def render_segment(context, output, segment)
for_stack = context.registers[:for_stack] ||= []
length = segment.length
- result = ''
-
context.stack do
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
for_stack.push(loop_vars)
begin
- context['forloop'.freeze] = loop_vars
+ context['forloop'] = loop_vars
segment.each do |item|
context[@variable_name] = item
- result << @for_block.render(context)
+ @for_block.render_to_output_buffer(context, output)
loop_vars.send(:increment!)
# Handle any interrupts if they exist.
- if context.interrupt?
- interrupt = context.pop_interrupt
- break if interrupt.is_a? BreakInterrupt
- next if interrupt.is_a? ContinueInterrupt
- end
+ next unless context.interrupt?
+ interrupt = context.pop_interrupt
+ break if interrupt.is_a?(BreakInterrupt)
+ next if interrupt.is_a?(ContinueInterrupt)
end
ensure
for_stack.pop
end
end
- result
+ output
end
def set_attribute(key, expr)
case key
- when 'offset'.freeze
- @from = if expr == 'continue'.freeze
+ when 'offset'
+ @from = if expr == 'continue'
:continue
else
Expression.parse(expr)
end
- when 'limit'.freeze
+ when 'limit'
@limit = Expression.parse(expr)
end
end
- def render_else(context)
- @else_block ? @else_block.render(context) : ''.freeze
+ def render_else(context, output)
+ if @else_block
+ @else_block.render_to_output_buffer(context, output)
+ else
+ output
+ end
+ end
+
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
+ def children
+ (super + [@node.limit, @node.from, @node.collection_name]).compact
+ end
end
end
- Template.register_tag('for'.freeze, For)
+ Template.register_tag('for', For)
end
diff --git a/lib/liquid/tags/if.rb b/lib/liquid/tags/if.rb
index 904369d..b68e309 100644
--- a/lib/liquid/tags/if.rb
+++ b/lib/liquid/tags/if.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Liquid
# If is the conditional block
#
@@ -12,12 +14,18 @@ module Liquid
class If < Block
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
- BOOLEAN_OPERATORS = %w(and or)
+ BOOLEAN_OPERATORS = %w(and or).freeze
+
+ attr_reader :blocks
def initialize(tag_name, markup, options)
super
@blocks = []
- push_block('if'.freeze, markup)
+ push_block('if', markup)
+ end
+
+ def nodelist
+ @blocks.map(&:attachment)
end
def parse(tokens)
@@ -25,33 +33,28 @@ module Liquid
end
end
- def nodelist
- @blocks.map(&:attachment)
- end
-
def unknown_tag(tag, markup, tokens)
- if ['elsif'.freeze, 'else'.freeze].include?(tag)
+ if ['elsif', 'else'].include?(tag)
push_block(tag, markup)
else
super
end
end
- def render(context)
- context.stack do
- @blocks.each do |block|
- if block.evaluate(context)
- return block.attachment.render(context)
- end
+ def render_to_output_buffer(context, output)
+ @blocks.each do |block|
+ if block.evaluate(context)
+ return block.attachment.render_to_output_buffer(context, output)
end
- ''.freeze
end
+
+ output
end
private
def push_block(tag, markup)
- block = if tag == 'else'.freeze
+ block = if tag == 'else'
ElseCondition.new
else
parse_with_selected_parser(markup)
@@ -63,17 +66,17 @@ module Liquid
def lax_parse(markup)
expressions = markup.scan(ExpressionsAndOperators)
- raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
+ raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax
- condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
+ condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3)))
until expressions.empty?
operator = expressions.pop.to_s.strip
- raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax
+ raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax
- new_condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
- raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
+ new_condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3)))
+ raise SyntaxError, options[:locale].t("errors.syntax.if") unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition)
condition = new_condition
end
@@ -91,7 +94,7 @@ module Liquid
def parse_binary_comparisons(p)
condition = parse_comparison(p)
first_condition = condition
- while op = (p.id?('and'.freeze) || p.id?('or'.freeze))
+ while (op = (p.id?('and') || p.id?('or')))
child_condition = parse_comparison(p)
condition.send(op, child_condition)
condition = child_condition
@@ -101,14 +104,20 @@ module Liquid
def parse_comparison(p)
a = Expression.parse(p.expression)
- if op = p.consume?(:comparison)
+ if (op = p.consume?(:comparison))
b = Expression.parse(p.expression)
Condition.new(a, op, b)
else
Condition.new(a)
end
end
+
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
+ def children
+ @node.blocks
+ end
+ end
end
- Template.register_tag('if'.freeze, If)
+ Template.register_tag('if', If)
end
diff --git a/lib/liquid/tags/ifchanged.rb b/lib/liquid/tags/ifchanged.rb
index d70cbe1..dd3be53 100644
--- a/lib/liquid/tags/ifchanged.rb
+++ b/lib/liquid/tags/ifchanged.rb
@@ -1,18 +1,19 @@
+# frozen_string_literal: true
+
module Liquid
class Ifchanged < Block
- def render(context)
- context.stack do
- output = super
+ def render_to_output_buffer(context, output)
+ block_output = +''
+ super(context, block_output)
- if output != context.registers[:ifchanged]
- context.registers[:ifchanged] = output
- output
- else
- ''.freeze
- end
+ if block_output != context.registers[:ifchanged]
+ context.registers[:ifchanged] = block_output
+ output << block_output
end
+
+ output
end
end
- Template.register_tag('ifchanged'.freeze, Ifchanged)
+ Template.register_tag('ifchanged', Ifchanged)
end
diff --git a/lib/liquid/tags/include.rb b/lib/liquid/tags/include.rb
index a800703..bbcfb1c 100644
--- a/lib/liquid/tags/include.rb
+++ b/lib/liquid/tags/include.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Liquid
# Include allows templates to relate with other templates
#
@@ -16,13 +18,15 @@ module Liquid
class Include < Tag
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
+ attr_reader :template_name_expr, :variable_name_expr, :attributes
+
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
- template_name = $1
- variable_name = $3
+ template_name = Regexp.last_match(1)
+ variable_name = Regexp.last_match(3)
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
@template_name_expr = Expression.parse(template_name)
@@ -33,19 +37,24 @@ module Liquid
end
else
- raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze))
+ raise SyntaxError, options[:locale].t("errors.syntax.include")
end
end
def parse(_tokens)
end
- def render(context)
+ def render_to_output_buffer(context, output)
template_name = context.evaluate(@template_name_expr)
- raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
+ raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
- partial = load_cached_partial(template_name, context)
- context_variable_name = template_name.split('/'.freeze).last
+ partial = PartialCache.load(
+ template_name,
+ context: context,
+ parse_context: parse_context
+ )
+
+ context_variable_name = template_name.split('/').last
variable = if @variable_name_expr
context.evaluate(@variable_name_expr)
@@ -64,50 +73,35 @@ module Liquid
end
if variable.is_a?(Array)
- variable.collect do |var|
+ variable.each do |var|
context[context_variable_name] = var
- partial.render(context)
+ partial.render_to_output_buffer(context, output)
end
else
context[context_variable_name] = variable
- partial.render(context)
+ partial.render_to_output_buffer(context, output)
end
end
ensure
context.template_name = old_template_name
context.partial = old_partial
end
- end
- private
+ output
+ end
alias_method :parse_context, :options
private :parse_context
- def load_cached_partial(template_name, context)
- cached_partials = context.registers[:cached_partials] || {}
-
- if cached = cached_partials[template_name]
- return cached
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
+ def children
+ [
+ @node.template_name_expr,
+ @node.variable_name_expr,
+ ] + @node.attributes.values
end
- source = read_template_from_file_system(context)
- begin
- parse_context.partial = true
- partial = Liquid::Template.parse(source, parse_context)
- ensure
- parse_context.partial = false
- end
- cached_partials[template_name] = partial
- context.registers[:cached_partials] = cached_partials
- partial
- end
-
- def read_template_from_file_system(context)
- file_system = context.registers[:file_system] || Liquid::Template.file_system
-
- file_system.read_template_file(context.evaluate(@template_name_expr))
end
end
- Template.register_tag('include'.freeze, Include)
+ Template.register_tag('include', Include)
end
diff --git a/lib/liquid/tags/increment.rb b/lib/liquid/tags/increment.rb
index baa0cbb..241b316 100644
--- a/lib/liquid/tags/increment.rb
+++ b/lib/liquid/tags/increment.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Liquid
# increment is used in a place where one needs to insert a counter
# into a template, and needs the counter to survive across
@@ -20,12 +22,14 @@ module Liquid
@variable = markup.strip
end
- def render(context)
+ def render_to_output_buffer(context, output)
value = context.environments.first[@variable] ||= 0
context.environments.first[@variable] = value + 1
- value.to_s
+
+ output << value.to_s
+ output
end
end
- Template.register_tag('increment'.freeze, Increment)
+ Template.register_tag('increment', Increment)
end
diff --git a/lib/liquid/tags/raw.rb b/lib/liquid/tags/raw.rb
index 6b461bd..e4a78a8 100644
--- a/lib/liquid/tags/raw.rb
+++ b/lib/liquid/tags/raw.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Liquid
class Raw < Block
Syntax = /\A\s*\z/
@@ -10,20 +12,21 @@ module Liquid
end
def parse(tokens)
- @body = ''
- while token = tokens.shift
+ @body = +''
+ while (token = tokens.shift)
if token =~ FullTokenPossiblyInvalid
- @body << $1 if $1 != "".freeze
- return if block_delimiter == $2
+ @body << Regexp.last_match(1) if Regexp.last_match(1) != ""
+ return if block_delimiter == Regexp.last_match(2)
end
@body << token unless token.empty?
end
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
+ raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
end
- def render(_context)
- @body
+ def render_to_output_buffer(_context, output)
+ output << @body
+ output
end
def nodelist
@@ -37,11 +40,11 @@ module Liquid
protected
def ensure_valid_markup(tag_name, markup, parse_context)
- unless markup =~ Syntax
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
+ unless Syntax.match?(markup)
+ raise SyntaxError, parse_context.locale.t("errors.syntax.tag_unexpected_args", tag: tag_name)
end
end
end
- Template.register_tag('raw'.freeze, Raw)
+ Template.register_tag('raw', Raw)
end
diff --git a/lib/liquid/tags/render.rb b/lib/liquid/tags/render.rb
new file mode 100644
index 0000000..1403b58
--- /dev/null
+++ b/lib/liquid/tags/render.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module Liquid
+ class Render < Tag
+ SYNTAX = /(#{QuotedString})#{QuotedFragment}*/o
+
+ disable_tags "include"
+
+ attr_reader :template_name_expr, :attributes
+
+ def initialize(tag_name, markup, options)
+ super
+
+ raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
+
+ template_name = Regexp.last_match(1)
+
+ @template_name_expr = Expression.parse(template_name)
+
+ @attributes = {}
+ markup.scan(TagAttributes) do |key, value|
+ @attributes[key] = Expression.parse(value)
+ end
+ end
+
+ def render_to_output_buffer(context, output)
+ render_tag(context, output)
+ end
+
+ def render_tag(context, output)
+ # Though we evaluate this here we will only ever parse it as a string literal.
+ template_name = context.evaluate(@template_name_expr)
+ raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
+
+ partial = PartialCache.load(
+ template_name,
+ context: context,
+ parse_context: parse_context
+ )
+
+ inner_context = context.new_isolated_subcontext
+ inner_context.template_name = template_name
+ inner_context.partial = true
+ @attributes.each do |key, value|
+ inner_context[key] = context.evaluate(value)
+ end
+ partial.render_to_output_buffer(inner_context, output)
+
+ output
+ end
+
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
+ def children
+ [
+ @node.template_name_expr,
+ ] + @node.attributes.values
+ end
+ end
+ end
+
+ Template.register_tag('render', Render)
+end
diff --git a/lib/liquid/tags/table_row.rb b/lib/liquid/tags/table_row.rb
index cfdef33..7c59bd3 100644
--- a/lib/liquid/tags/table_row.rb
+++ b/lib/liquid/tags/table_row.rb
@@ -1,54 +1,67 @@
+# frozen_string_literal: true
+
module Liquid
class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
+ attr_reader :variable_name, :collection_name, :attributes
+
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
- @variable_name = $1
- @collection_name = Expression.parse($2)
+ @variable_name = Regexp.last_match(1)
+ @collection_name = Expression.parse(Regexp.last_match(2))
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value)
end
else
- raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze))
+ raise SyntaxError, options[:locale].t("errors.syntax.table_row")
end
end
- def render(context)
- collection = context.evaluate(@collection_name) or return ''.freeze
+ def render_to_output_buffer(context, output)
+ (collection = context.evaluate(@collection_name)) || (return '')
- from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0
- to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil
+ from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0
+ to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
collection = Utils.slice_collection(collection, from, to)
length = collection.length
- cols = context.evaluate(@attributes['cols'.freeze]).to_i
+ cols = context.evaluate(@attributes['cols']).to_i
- result = "\n"
+ output << " \n"
context.stack do
tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
- context['tablerowloop'.freeze] = tablerowloop
+ context['tablerowloop'] = tablerowloop
collection.each do |item|
context[@variable_name] = item
- result << " \n" << super << ' '
+ output << ""
+ super
+ output << ' '
if tablerowloop.col_last && !tablerowloop.last
- result << ""
+ output << " \n"
end
tablerowloop.send(:increment!)
end
end
- result << " \n"
- result
+
+ output << "\n"
+ output
+ end
+
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
+ def children
+ super + @node.attributes.values + [@node.collection_name]
+ end
end
end
- Template.register_tag('tablerow'.freeze, TableRow)
+ Template.register_tag('tablerow', TableRow)
end
diff --git a/lib/liquid/tags/unless.rb b/lib/liquid/tags/unless.rb
index 1d4280d..f67f57a 100644
--- a/lib/liquid/tags/unless.rb
+++ b/lib/liquid/tags/unless.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require_relative 'if'
module Liquid
@@ -6,25 +8,23 @@ module Liquid
# {% unless x < 0 %} x is greater than zero {% endunless %}
#
class Unless < If
- def render(context)
- context.stack do
- # First condition is interpreted backwards ( if not )
- first_block = @blocks.first
- unless first_block.evaluate(context)
- return first_block.attachment.render(context)
- end
-
- # After the first condition unless works just like if
- @blocks[1..-1].each do |block|
- if block.evaluate(context)
- return block.attachment.render(context)
- end
- end
-
- ''.freeze
+ def render_to_output_buffer(context, output)
+ # First condition is interpreted backwards ( if not )
+ first_block = @blocks.first
+ unless first_block.evaluate(context)
+ return first_block.attachment.render_to_output_buffer(context, output)
end
+
+ # After the first condition unless works just like if
+ @blocks[1..-1].each do |block|
+ if block.evaluate(context)
+ return block.attachment.render_to_output_buffer(context, output)
+ end
+ end
+
+ output
end
end
- Template.register_tag('unless'.freeze, Unless)
+ Template.register_tag('unless', Unless)
end
diff --git a/lib/liquid/template.rb b/lib/liquid/template.rb
index 31a67e4..e23df24 100644
--- a/lib/liquid/template.rb
+++ b/lib/liquid/template.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Liquid
# Templates are central to liquid.
# Interpretating templates is a two step process. First you compile the
@@ -50,7 +52,7 @@ module Liquid
private
def lookup_class(name)
- name.split("::").reject(&:empty?).reduce(Object) { |scope, const| scope.const_get(const) }
+ Object.const_get(name)
end
end
@@ -90,6 +92,14 @@ module Liquid
@tags ||= TagRegistry.new
end
+ def add_register(name, klass)
+ registers[name.to_sym] = klass
+ end
+
+ def registers
+ @registers ||= {}
+ end
+
def error_mode
@error_mode ||= :lax
end
@@ -165,14 +175,14 @@ module Liquid
# filters and tags and might be useful to integrate liquid more with its host application
#
def render(*args)
- return ''.freeze if @root.nil?
+ return '' if @root.nil?
context = case args.first
when Liquid::Context
c = args.shift
if @rethrow_errors
- c.exception_renderer = ->(e) { raise }
+ c.exception_renderer = ->(_e) { raise }
end
c
@@ -187,27 +197,37 @@ module Liquid
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
end
+ output = nil
+
+ context_register = context.registers.is_a?(StaticRegisters) ? context.registers.static : context.registers
+
case args.last
when Hash
options = args.pop
+ output = options[:output] if options[:output]
- registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
+ options[:registers]&.each do |key, register|
+ context_register[key] = register
+ end
apply_options_to_context(context, options)
when Module, Array
context.add_filters(args.pop)
end
+ Template.registers.each do |key, register|
+ context_register[key] = register
+ end
+
# Retrying a render resets resource usage
context.resource_limits.reset
begin
# render the nodelist.
# for performance reasons we get an array back here. join will make a string out of it.
- result = with_profiling(context) do
- @root.render(context)
+ with_profiling(context) do
+ @root.render_to_output_buffer(context, output || +'')
end
- result.respond_to?(:join) ? result.join : result
rescue Liquid::MemoryError => e
context.handle_error(e)
ensure
@@ -220,6 +240,10 @@ module Liquid
render(*args)
end
+ def render_to_output_buffer(context, output)
+ render(context, output: output)
+ end
+
private
def tokenize(source)
diff --git a/lib/liquid/tokenizer.rb b/lib/liquid/tokenizer.rb
index d03657e..a89c789 100644
--- a/lib/liquid/tokenizer.rb
+++ b/lib/liquid/tokenizer.rb
@@ -1,29 +1,37 @@
+# frozen_string_literal: true
+
module Liquid
class Tokenizer
- attr_reader :line_number
+ attr_reader :line_number, :for_liquid_tag
- def initialize(source, line_numbers = false)
+ def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
@source = source
- @line_number = line_numbers ? 1 : nil
+ @line_number = line_number || (line_numbers ? 1 : nil)
+ @for_liquid_tag = for_liquid_tag
@tokens = tokenize
end
def shift
- token = @tokens.shift
- @line_number += token.count("\n") if @line_number && token
+ (token = @tokens.shift) || return
+
+ if @line_number
+ @line_number += @for_liquid_tag ? 1 : token.count("\n")
+ end
+
token
end
private
def tokenize
- @source = @source.source if @source.respond_to?(:source)
return [] if @source.to_s.empty?
+ return @source.split("\n") if @for_liquid_tag
+
tokens = @source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array
- tokens.shift if tokens[0] && tokens[0].empty?
+ tokens.shift if tokens[0]&.empty?
tokens
end
diff --git a/lib/liquid/usage.rb b/lib/liquid/usage.rb
new file mode 100644
index 0000000..141eccb
--- /dev/null
+++ b/lib/liquid/usage.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Liquid
+ module Usage
+ def self.increment(name)
+ end
+ end
+end
diff --git a/lib/liquid/utils.rb b/lib/liquid/utils.rb
index 516ac0c..709fb00 100644
--- a/lib/liquid/utils.rb
+++ b/lib/liquid/utils.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Liquid
module Utils
def self.slice_collection(collection, from, to)
@@ -50,7 +52,7 @@ module Liquid
when Numeric
obj
when String
- (obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal(obj) : obj.to_i
+ /\A-?\d+\.\d+\z/.match?(obj.strip) ? BigDecimal(obj) : obj.to_i
else
if obj.respond_to?(:to_number)
obj.to_number
@@ -69,7 +71,7 @@ module Liquid
end
case obj
- when 'now'.freeze, 'today'.freeze
+ when 'now', 'today'
Time.now
when /\A\d+\z/, Integer
Time.at(obj.to_i)
diff --git a/lib/liquid/variable.rb b/lib/liquid/variable.rb
index 5f88eb3..5b686e2 100644
--- a/lib/liquid/variable.rb
+++ b/lib/liquid/variable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Liquid
# Holds variables. Variables are only loaded "just in time"
# and are not evaluated as part of the render stage
@@ -43,11 +45,11 @@ module Liquid
@filters = []
return unless markup =~ MarkupWithQuotedFragment
- name_markup = $1
- filter_markup = $2
+ name_markup = Regexp.last_match(1)
+ filter_markup = Regexp.last_match(2)
@name = Expression.parse(name_markup)
if filter_markup =~ FilterMarkupRegex
- filters = $1.scan(FilterParser)
+ filters = Regexp.last_match(1).scan(FilterParser)
filters.each do |f|
next unless f =~ /\w+/
filtername = Regexp.last_match(0)
@@ -85,31 +87,51 @@ module Liquid
end
obj = context.apply_global_filter(obj)
-
taint_check(context, obj)
-
obj
end
+ def render_to_output_buffer(context, output)
+ obj = render(context)
+
+ if obj.is_a?(Array)
+ output << obj.join
+ elsif obj.nil?
+ else
+ output << obj.to_s
+ end
+
+ output
+ end
+
+ def disabled?(_context)
+ false
+ end
+
+ def disabled_tags
+ []
+ end
+
private
def parse_filter_expressions(filter_name, unparsed_args)
filter_args = []
- keyword_args = {}
+ keyword_args = nil
unparsed_args.each do |a|
- if matches = a.match(JustTagAttributes)
+ if (matches = a.match(JustTagAttributes))
+ keyword_args ||= {}
keyword_args[matches[1]] = Expression.parse(matches[2])
else
filter_args << Expression.parse(a)
end
end
result = [filter_name, filter_args]
- result << keyword_args unless keyword_args.empty?
+ result << keyword_args if keyword_args
result
end
def evaluate_filter_expressions(context, filter_args, filter_kwargs)
- parsed_args = filter_args.map{ |expr| context.evaluate(expr) }
+ parsed_args = filter_args.map { |expr| context.evaluate(expr) }
if filter_kwargs
parsed_kwargs = {}
filter_kwargs.each do |key, expr|
@@ -138,5 +160,11 @@ module Liquid
raise error
end
end
+
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
+ def children
+ [@node.name] + @node.filters.flatten
+ end
+ end
end
end
diff --git a/lib/liquid/variable_lookup.rb b/lib/liquid/variable_lookup.rb
index 3ed4e4a..112373d 100644
--- a/lib/liquid/variable_lookup.rb
+++ b/lib/liquid/variable_lookup.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
module Liquid
class VariableLookup
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
- COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze]
+ COMMAND_METHODS = ['size', 'first', 'last'].freeze
attr_reader :name, :lookups
@@ -14,7 +16,7 @@ module Liquid
name = lookups.shift
if name =~ SQUARE_BRACKETED
- name = Expression.parse($1)
+ name = Expression.parse(Regexp.last_match(1))
end
@name = name
@@ -24,7 +26,7 @@ module Liquid
@lookups.each_index do |i|
lookup = lookups[i]
if lookup =~ SQUARE_BRACKETED
- lookups[i] = Expression.parse($1)
+ lookups[i] = Expression.parse(Regexp.last_match(1))
elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i
end
@@ -78,5 +80,11 @@ module Liquid
def state
[@name, @lookups, @command_flags]
end
+
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
+ def children
+ @node.lookups
+ end
+ end
end
end
diff --git a/lib/liquid/version.rb b/lib/liquid/version.rb
index af15e07..9af2973 100644
--- a/lib/liquid/version.rb
+++ b/lib/liquid/version.rb
@@ -1,4 +1,6 @@
# encoding: utf-8
+# frozen_string_literal: true
+
module Liquid
- VERSION = "4.0.0"
+ VERSION = "4.0.3"
end
diff --git a/liquid.gemspec b/liquid.gemspec
index e0e4ddb..54a11fb 100644
--- a/liquid.gemspec
+++ b/liquid.gemspec
@@ -1,7 +1,8 @@
# encoding: utf-8
+# frozen_string_literal: true
lib = File.expand_path('../lib/', __FILE__)
-$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
+$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "liquid/version"
@@ -16,7 +17,7 @@ Gem::Specification.new do |s|
s.license = "MIT"
# s.description = "A secure, non-evaling end user template engine with aesthetic markup."
- s.required_ruby_version = ">= 2.1.0"
+ s.required_ruby_version = ">= 2.4.0"
s.required_rubygems_version = ">= 1.3.7"
s.test_files = Dir.glob("{test}/**/*")
@@ -26,6 +27,6 @@ Gem::Specification.new do |s|
s.require_path = "lib"
- s.add_development_dependency 'rake', '~> 11.3'
- s.add_development_dependency 'minitest'
+ s.add_development_dependency('rake', '~> 11.3')
+ s.add_development_dependency('minitest')
end
diff --git a/performance/benchmark.rb b/performance/benchmark.rb
index 68c568c..4d28b9a 100644
--- a/performance/benchmark.rb
+++ b/performance/benchmark.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'benchmark/ips'
require_relative 'theme_runner'
diff --git a/performance/memory_profile.rb b/performance/memory_profile.rb
new file mode 100644
index 0000000..14b3770
--- /dev/null
+++ b/performance/memory_profile.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'benchmark/ips'
+require 'memory_profiler'
+require 'terminal-table'
+require_relative 'theme_runner'
+
+class Profiler
+ LOG_LABEL = "Profiling: ".rjust(14).freeze
+ REPORTS_DIR = File.expand_path('.memprof', __dir__).freeze
+
+ def self.run
+ puts
+ yield new
+ end
+
+ def initialize
+ @allocated = []
+ @retained = []
+ @headings = []
+ end
+
+ def profile(phase, &block)
+ print LOG_LABEL
+ print "#{phase}.. ".ljust(10)
+ report = MemoryProfiler.report(&block)
+ puts 'Done.'
+ @headings << phase.capitalize
+ @allocated << "#{report.scale_bytes(report.total_allocated_memsize)} (#{report.total_allocated} objects)"
+ @retained << "#{report.scale_bytes(report.total_retained_memsize)} (#{report.total_retained} objects)"
+
+ return if ENV['CI']
+
+ require 'fileutils'
+ report_file = File.join(REPORTS_DIR, "#{sanitize(phase)}.txt")
+ FileUtils.mkdir_p(REPORTS_DIR)
+ report.pretty_print(to_file: report_file, scale_bytes: true)
+ end
+
+ def tabulate
+ table = Terminal::Table.new(headings: @headings.unshift('Phase')) do |t|
+ t << @allocated.unshift('Total allocated')
+ t << @retained.unshift('Total retained')
+ end
+
+ puts
+ puts table
+ puts "\nDetailed report(s) saved to #{REPORTS_DIR}/" unless ENV['CI']
+ end
+
+ def sanitize(string)
+ string.downcase.gsub(/[\W]/, '-').squeeze('-')
+ end
+end
+
+Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
+
+runner = ThemeRunner.new
+Profiler.run do |x|
+ x.profile('parse') { runner.compile }
+ x.profile('render') { runner.render }
+ x.tabulate
+end
diff --git a/performance/profile.rb b/performance/profile.rb
index c6fb193..7074077 100644
--- a/performance/profile.rb
+++ b/performance/profile.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'stackprof'
require_relative 'theme_runner'
@@ -13,7 +15,7 @@ profiler.run
end
end
- if profile_type == :cpu && graph_filename = ENV['GRAPH_FILENAME']
+ if profile_type == :cpu && (graph_filename = ENV['GRAPH_FILENAME'])
File.open(graph_filename, 'w') do |f|
StackProf::Report.new(results).print_graphviz(nil, f)
end
diff --git a/performance/shopify/comment_form.rb b/performance/shopify/comment_form.rb
index d661c31..7648e1a 100644
--- a/performance/shopify/comment_form.rb
+++ b/performance/shopify/comment_form.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CommentForm < Liquid::Block
Syntax = /(#{Liquid::VariableSignature}+)/
@@ -5,14 +7,14 @@ class CommentForm < Liquid::Block
super
if markup =~ Syntax
- @variable_name = $1
+ @variable_name = Regexp.last_match(1)
@attributes = {}
else
- raise SyntaxError.new("Syntax Error in 'comment_form' - Valid syntax: comment_form [article]")
+ raise SyntaxError, "Syntax Error in 'comment_form' - Valid syntax: comment_form [article]"
end
end
- def render(context)
+ def render_to_output_buffer(context, output)
article = context[@variable_name]
context.stack do
@@ -20,10 +22,12 @@ class CommentForm < Liquid::Block
'posted_successfully?' => context.registers[:posted_successfully],
'errors' => context['comment.errors'],
'author' => context['comment.author'],
- 'email' => context['comment.email'],
- 'body' => context['comment.body']
+ 'email' => context['comment.email'],
+ 'body' => context['comment.body'],
}
- wrap_in_form(article, render_all(@nodelist, context))
+
+ output << wrap_in_form(article, render_all(@nodelist, context, output))
+ output
end
end
diff --git a/performance/shopify/database.rb b/performance/shopify/database.rb
index 2b5bca4..2db6d30 100644
--- a/performance/shopify/database.rb
+++ b/performance/shopify/database.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'yaml'
module Database
@@ -16,9 +18,10 @@ module Database
end
# key the tables by handles, as this is how liquid expects it.
- db = db.inject({}) do |assigns, (key, values)|
- assigns[key] = values.inject({}) { |h, v| h[v['handle']] = v; h; }
- assigns
+ db = db.each_with_object({}) do |(key, values), assigns|
+ assigns[key] = values.each_with_object({}) do |v, h|
+ h[v['handle']] = v
+ end
end
# Some standard direct accessors so that the specialized templates
@@ -29,9 +32,9 @@ module Database
db['article'] = db['blog']['articles'].first
db['cart'] = {
- 'total_price' => db['line_items'].values.inject(0) { |sum, item| sum += item['line_price'] * item['quantity'] },
- 'item_count' => db['line_items'].values.inject(0) { |sum, item| sum += item['quantity'] },
- 'items' => db['line_items'].values
+ 'total_price' => db['line_items'].values.inject(0) { |sum, item| sum + item['line_price'] * item['quantity'] },
+ 'item_count' => db['line_items'].values.inject(0) { |sum, item| sum + item['quantity'] },
+ 'items' => db['line_items'].values,
}
db
@@ -40,6 +43,6 @@ module Database
end
if __FILE__ == $PROGRAM_NAME
- p Database.tables['collections']['frontpage'].keys
+ p(Database.tables['collections']['frontpage'].keys)
# p Database.tables['blog']['articles']
end
diff --git a/performance/shopify/json_filter.rb b/performance/shopify/json_filter.rb
index 8fbb5b6..c7c25d8 100644
--- a/performance/shopify/json_filter.rb
+++ b/performance/shopify/json_filter.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
require 'json'
module JsonFilter
def json(object)
- JSON.dump(object.reject { |k, v| k == "collections" })
+ JSON.dump(object.reject { |k, _v| k == "collections" })
end
end
diff --git a/performance/shopify/liquid.rb b/performance/shopify/liquid.rb
index 7716deb..40444c3 100644
--- a/performance/shopify/liquid.rb
+++ b/performance/shopify/liquid.rb
@@ -1,4 +1,6 @@
-$:.unshift __dir__ + '/../../lib'
+# frozen_string_literal: true
+
+$LOAD_PATH.unshift(__dir__ + '/../../lib')
require_relative '../../lib/liquid'
require_relative 'comment_form'
@@ -9,11 +11,11 @@ require_relative 'shop_filter'
require_relative 'tag_filter'
require_relative 'weight_filter'
-Liquid::Template.register_tag 'paginate', Paginate
-Liquid::Template.register_tag 'form', CommentForm
+Liquid::Template.register_tag('paginate', Paginate)
+Liquid::Template.register_tag('form', CommentForm)
-Liquid::Template.register_filter JsonFilter
-Liquid::Template.register_filter MoneyFilter
-Liquid::Template.register_filter WeightFilter
-Liquid::Template.register_filter ShopFilter
-Liquid::Template.register_filter TagFilter
+Liquid::Template.register_filter(JsonFilter)
+Liquid::Template.register_filter(MoneyFilter)
+Liquid::Template.register_filter(WeightFilter)
+Liquid::Template.register_filter(ShopFilter)
+Liquid::Template.register_filter(TagFilter)
diff --git a/performance/shopify/money_filter.rb b/performance/shopify/money_filter.rb
index 8dad789..b0135e3 100644
--- a/performance/shopify/money_filter.rb
+++ b/performance/shopify/money_filter.rb
@@ -1,12 +1,14 @@
+# frozen_string_literal: true
+
module MoneyFilter
def money_with_currency(money)
return '' if money.nil?
- sprintf("$ %.2f USD", money / 100.0)
+ format("$ %.2f USD", money / 100.0)
end
def money(money)
return '' if money.nil?
- sprintf("$ %.2f", money / 100.0)
+ format("$ %.2f", money / 100.0)
end
private
diff --git a/performance/shopify/paginate.rb b/performance/shopify/paginate.rb
index 38a9a1a..f723823 100644
--- a/performance/shopify/paginate.rb
+++ b/performance/shopify/paginate.rb
@@ -1,13 +1,15 @@
+# frozen_string_literal: true
+
class Paginate < Liquid::Block
- Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/
+ Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
- @collection_name = $1
- @page_size = if $2
- $3.to_i
+ @collection_name = Regexp.last_match(1)
+ @page_size = if Regexp.last_match(2)
+ Regexp.last_match(3).to_i
else
20
end
@@ -17,27 +19,27 @@ class Paginate < Liquid::Block
@attributes[key] = value
end
else
- raise SyntaxError.new("Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number")
+ raise SyntaxError, "Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number"
end
end
- def render(context)
+ def render_to_output_buffer(context, output)
@context = context
context.stack do
- current_page = context['current_page'].to_i
+ current_page = context['current_page'].to_i
pagination = {
- 'page_size' => @page_size,
- 'current_page' => 5,
- 'current_offset' => @page_size * 5
+ 'page_size' => @page_size,
+ 'current_page' => 5,
+ 'current_offset' => @page_size * 5,
}
context['paginate'] = pagination
- collection_size = context[@collection_name].size
+ collection_size = context[@collection_name].size
- raise ArgumentError.new("Cannot paginate array '#{@collection_name}'. Not found.") if collection_size.nil?
+ raise ArgumentError, "Cannot paginate array '#{@collection_name}'. Not found." if collection_size.nil?
page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1
diff --git a/performance/shopify/shop_filter.rb b/performance/shopify/shop_filter.rb
index 89c9083..9f0cdc2 100644
--- a/performance/shopify/shop_filter.rb
+++ b/performance/shopify/shop_filter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ShopFilter
def asset_url(input)
"/files/1/[shop_id]/[shop_id]/assets/#{input}"
@@ -52,7 +54,7 @@ module ShopFilter
end
def product_img_url(url, style = 'small')
- unless url =~ /\Aproducts\/([\w\-\_]+)\.(\w{2,4})/
+ unless url =~ %r{\Aproducts/([\w\-\_]+)\.(\w{2,4})}
raise ArgumentError, 'filter "size" can only be called on product images'
end
@@ -60,7 +62,7 @@ module ShopFilter
when 'original'
return '/files/shops/random_number/' + url
when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon'
- "/files/shops/random_number/products/#{$1}_#{style}.#{$2}"
+ "/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}"
else
raise ArgumentError, 'valid parameters for filter "size" are: original, grande, large, medium, compact, small, thumb and icon '
end
@@ -70,16 +72,14 @@ module ShopFilter
html = []
html << %(#{link_to(paginate['previous']['title'], paginate['previous']['url'])}) if paginate['previous']
- for part in paginate['parts']
-
- if part['is_link']
- html << %(#{link_to(part['title'], part['url'])})
+ paginate['parts'].each do |part|
+ html << if part['is_link']
+ %(#{link_to(part['title'], part['url'])})
elsif part['title'].to_i == paginate['current_page'].to_i
- html << %(#{part['title']})
+ %(#{part['title']})
else
- html << %(#{part['title']})
+ %(#{part['title']})
end
-
end
html << %(#{link_to(paginate['next']['title'], paginate['next']['url'])}) if paginate['next']
diff --git a/performance/shopify/tag_filter.rb b/performance/shopify/tag_filter.rb
index ab5aef6..58f066b 100644
--- a/performance/shopify/tag_filter.rb
+++ b/performance/shopify/tag_filter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module TagFilter
def link_to_tag(label, tag)
"#{label}"
@@ -13,11 +15,11 @@ module TagFilter
def link_to_add_tag(label, tag)
tags = (@context['current_tags'] + [tag]).uniq
- "#{label}"
+ "#{label}"
end
def link_to_remove_tag(label, tag)
tags = (@context['current_tags'] - [tag]).uniq
- "#{label}"
+ "#{label}"
end
end
diff --git a/performance/shopify/weight_filter.rb b/performance/shopify/weight_filter.rb
index a0a15fc..6ba95f3 100644
--- a/performance/shopify/weight_filter.rb
+++ b/performance/shopify/weight_filter.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
module WeightFilter
def weight(grams)
- sprintf("%.2f", grams / 1000)
+ format("%.2f", grams / 1000)
end
def weight_with_unit(grams)
diff --git a/performance/theme_runner.rb b/performance/theme_runner.rb
index 9f6a1fc..5ad01c5 100644
--- a/performance/theme_runner.rb
+++ b/performance/theme_runner.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This profiler run simulates Shopify.
# We are looking in the tests directory for liquid files and render them within the designated layout file.
# We will also export a substantial database to liquid which the templates can render values of.
@@ -31,7 +33,7 @@ class ThemeRunner
{
liquid: File.read(test),
layout: (File.file?(theme_path) ? File.read(theme_path) : nil),
- template_name: test
+ template_name: test,
}
end.compact
diff --git a/test/integration/assign_test.rb b/test/integration/assign_test.rb
index 5502289..ffcb8a3 100644
--- a/test/integration/assign_test.rb
+++ b/test/integration/assign_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'test_helper'
class AssignTest < Minitest::Test
diff --git a/test/integration/blank_test.rb b/test/integration/blank_test.rb
index e9b56df..f92490b 100644
--- a/test/integration/blank_test.rb
+++ b/test/integration/blank_test.rb
@@ -1,11 +1,12 @@
+# frozen_string_literal: true
+
require 'test_helper'
class FoobarTag < Liquid::Tag
- def render(*args)
- " "
+ def render_to_output_buffer(_context, output)
+ output << ' '
+ output
end
-
- Liquid::Template.register_tag('foobar', FoobarTag)
end
class BlankTestFileSystem
@@ -31,7 +32,9 @@ class BlankTest < Minitest::Test
end
def test_new_tags_are_not_blank_by_default
- assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
+ with_custom_tag('foobar', FoobarTag) do
+ assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
+ end
end
def test_loops_are_blank
diff --git a/test/integration/block_test.rb b/test/integration/block_test.rb
index 0824530..5603b53 100644
--- a/test/integration/block_test.rb
+++ b/test/integration/block_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'test_helper'
class BlockTest < Minitest::Test
diff --git a/test/integration/capture_test.rb b/test/integration/capture_test.rb
index 8d965b3..f28e1b1 100644
--- a/test/integration/capture_test.rb
+++ b/test/integration/capture_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'test_helper'
class CaptureTest < Minitest::Test
diff --git a/test/integration/context_test.rb b/test/integration/context_test.rb
index 2d109bb..cd6d7a8 100644
--- a/test/integration/context_test.rb
+++ b/test/integration/context_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'test_helper'
class ContextTest < Minitest::Test
diff --git a/test/integration/document_test.rb b/test/integration/document_test.rb
index bcc4a21..375ccfa 100644
--- a/test/integration/document_test.rb
+++ b/test/integration/document_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'test_helper'
class DocumentTest < Minitest::Test
diff --git a/test/integration/drop_test.rb b/test/integration/drop_test.rb
index 2de4a5a..3fe6175 100644
--- a/test/integration/drop_test.rb
+++ b/test/integration/drop_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'test_helper'
class ContextDrop < Liquid::Drop
@@ -31,7 +33,7 @@ class ProductDrop < Liquid::Drop
class CatchallDrop < Liquid::Drop
def liquid_method_missing(method)
- 'catchall_method: ' << method.to_s
+ "catchall_method: #{method}"
end
end
@@ -48,7 +50,7 @@ class ProductDrop < Liquid::Drop
end
def user_input
- "foo".taint
+ (+"foo").taint
end
protected
@@ -201,9 +203,9 @@ class DropsTest < Minitest::Test
end
def test_scope_though_proc
- assert_equal '1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] })
- assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] }, 'dummy' => [1])
- assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] }, 'dummy' => [1])
+ assert_equal '1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] })
+ assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1])
+ assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1])
end
def test_scope_with_assigns
@@ -241,7 +243,7 @@ class DropsTest < Minitest::Test
end
def test_some_enumerable_methods_still_get_invoked
- [ :count, :max ].each do |method|
+ [:count, :max].each do |method|
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
@@ -250,7 +252,7 @@ class DropsTest < Minitest::Test
assert_equal "yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new)
- [ :min, :first ].each do |method|
+ [:min, :first].each do |method|
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
@@ -270,4 +272,11 @@ class DropsTest < Minitest::Test
assert_equal 'ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new)
assert_equal 'EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new)
end
+
+ def test_invokable_methods
+ assert_equal %w(to_liquid catchall user_input context texts).to_set, ProductDrop.invokable_methods
+ assert_equal %w(to_liquid scopes_as_array loop_pos scopes).to_set, ContextDrop.invokable_methods
+ assert_equal %w(to_liquid size max min first count).to_set, EnumerableDrop.invokable_methods
+ assert_equal %w(to_liquid max min sort count first).to_set, RealEnumerableDrop.invokable_methods
+ end
end # DropsTest
diff --git a/test/integration/error_handling_test.rb b/test/integration/error_handling_test.rb
index ba81861..7abaec0 100644
--- a/test/integration/error_handling_test.rb
+++ b/test/integration/error_handling_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'test_helper'
class ErrorHandlingTest < Minitest::Test
@@ -83,15 +85,14 @@ class ErrorHandlingTest < Minitest::Test
def test_with_line_numbers_adds_numbers_to_parser_errors
err = assert_raises(SyntaxError) do
- Liquid::Template.parse(%q(
+ Liquid::Template.parse('
foobar
{% "cat" | foobar %}
bla
- ),
- line_numbers: true
- )
+ ',
+ line_numbers: true)
end
assert_match(/Liquid syntax error \(line 4\)/, err.message)
@@ -99,15 +100,14 @@ class ErrorHandlingTest < Minitest::Test
def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim
err = assert_raises(SyntaxError) do
- Liquid::Template.parse(%q(
+ Liquid::Template.parse('
foobar
{%- "cat" | foobar -%}
bla
- ),
- line_numbers: true
- )
+ ',
+ line_numbers: true)
end
assert_match(/Liquid syntax error \(line 4\)/, err.message)
@@ -122,8 +122,7 @@ class ErrorHandlingTest < Minitest::Test
bla
',
error_mode: :warn,
- line_numbers: true
- )
+ line_numbers: true)
assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
template.warnings.map(&:message)
@@ -139,8 +138,7 @@ class ErrorHandlingTest < Minitest::Test
bla
',
error_mode: :strict,
- line_numbers: true
- )
+ line_numbers: true)
end
assert_equal 'Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message
@@ -157,8 +155,7 @@ class ErrorHandlingTest < Minitest::Test
bla
',
- line_numbers: true
- )
+ line_numbers: true)
end
assert_equal "Liquid syntax error (line 5): Unknown tag 'foo'", err.message
@@ -205,7 +202,7 @@ class ErrorHandlingTest < Minitest::Test
def test_default_exception_renderer_with_internal_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
- output = template.render({ 'errors' => ErrorDrop.new })
+ output = template.render('errors' => ErrorDrop.new)
assert_equal 'This is a runtime error: Liquid error (line 1): internal', output
assert_equal [Liquid::InternalError], template.errors.map(&:class)
@@ -214,10 +211,13 @@ class ErrorHandlingTest < Minitest::Test
def test_setting_default_exception_renderer
old_exception_renderer = Liquid::Template.default_exception_renderer
exceptions = []
- Liquid::Template.default_exception_renderer = ->(e) { exceptions << e; '' }
+ Liquid::Template.default_exception_renderer = ->(e) {
+ exceptions << e
+ ''
+ }
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
- output = template.render({ 'errors' => ErrorDrop.new })
+ output = template.render('errors' => ErrorDrop.new)
assert_equal 'This is a runtime error: ', output
assert_equal [Liquid::ArgumentError], template.errors.map(&:class)
@@ -228,7 +228,10 @@ class ErrorHandlingTest < Minitest::Test
def test_exception_renderer_exposing_non_liquid_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
exceptions = []
- handler = ->(e) { exceptions << e; e.cause }
+ handler = ->(e) {
+ exceptions << e
+ e.cause
+ }
output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler)
@@ -239,7 +242,7 @@ class ErrorHandlingTest < Minitest::Test
end
class TestFileSystem
- def read_template_file(template_path)
+ def read_template_file(_template_path)
"{{ errors.argument_error }}"
end
end
diff --git a/test/integration/filter_test.rb b/test/integration/filter_test.rb
index d3c880e..270477e 100644
--- a/test/integration/filter_test.rb
+++ b/test/integration/filter_test.rb
@@ -1,24 +1,26 @@
+# frozen_string_literal: true
+
require 'test_helper'
module MoneyFilter
def money(input)
- sprintf(' %d$ ', input)
+ format(' %d$ ', input)
end
def money_with_underscore(input)
- sprintf(' %d$ ', input)
+ format(' %d$ ', input)
end
end
module CanadianMoneyFilter
def money(input)
- sprintf(' %d$ CAD ', input)
+ format(' %d$ CAD ', input)
end
end
module SubstituteFilter
def substitute(input, params = {})
- input.gsub(/%\{(\w+)\}/) { |match| params[$1] }
+ input.gsub(/%\{(\w+)\}/) { |_match| params[Regexp.last_match(1)] }
end
end
@@ -26,7 +28,7 @@ class FiltersTest < Minitest::Test
include Liquid
module OverrideObjectMethodFilter
- def tap(input)
+ def tap(_input)
"tap overridden"
end
end
@@ -149,7 +151,7 @@ class FiltersTest < Minitest::Test
assert_equal "tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter])
# tap still treated as a non-existent filter
- assert_equal "1000", Template.parse("{{var | tap}}").render!({ 'var' => 1000 })
+ assert_equal "1000", Template.parse("{{var | tap}}").render!('var' => 1000)
end
end
diff --git a/test/integration/hash_ordering_test.rb b/test/integration/hash_ordering_test.rb
index dfc1c29..27d0b9b 100644
--- a/test/integration/hash_ordering_test.rb
+++ b/test/integration/hash_ordering_test.rb
@@ -1,15 +1,17 @@
+# frozen_string_literal: true
+
require 'test_helper'
class HashOrderingTest < Minitest::Test
module MoneyFilter
def money(input)
- sprintf(' %d$ ', input)
+ format(' %d$ ', input)
end
end
module CanadianMoneyFilter
def money(input)
- sprintf(' %d$ CAD ', input)
+ format(' %d$ CAD ', input)
end
end
diff --git a/test/integration/output_test.rb b/test/integration/output_test.rb
index b4cf9d7..687cad8 100644
--- a/test/integration/output_test.rb
+++ b/test/integration/output_test.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
require 'test_helper'
module FunnyFilter
- def make_funny(input)
+ def make_funny(_input)
'LOL'
end
@@ -32,7 +34,7 @@ class OutputTest < Minitest::Test
def setup
@assigns = {
'best_cars' => 'bmw',
- 'car' => { 'bmw' => 'good', 'gm' => 'bad' }
+ 'car' => { 'bmw' => 'good', 'gm' => 'bad' },
}
end
diff --git a/test/integration/parse_tree_visitor_test.rb b/test/integration/parse_tree_visitor_test.rb
new file mode 100644
index 0000000..d1af123
--- /dev/null
+++ b/test/integration/parse_tree_visitor_test.rb
@@ -0,0 +1,247 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class ParseTreeVisitorTest < Minitest::Test
+ include Liquid
+
+ def test_variable
+ assert_equal(
+ ["test"],
+ visit(%({{ test }}))
+ )
+ end
+
+ def test_varible_with_filter
+ assert_equal(
+ ["test", "infilter"],
+ visit(%({{ test | split: infilter }}))
+ )
+ end
+
+ def test_dynamic_variable
+ assert_equal(
+ ["test", "inlookup"],
+ visit(%({{ test[inlookup] }}))
+ )
+ end
+
+ def test_if_condition
+ assert_equal(
+ ["test"],
+ visit(%({% if test %}{% endif %}))
+ )
+ end
+
+ def test_complex_if_condition
+ assert_equal(
+ ["test"],
+ visit(%({% if 1 == 1 and 2 == test %}{% endif %}))
+ )
+ end
+
+ def test_if_body
+ assert_equal(
+ ["test"],
+ visit(%({% if 1 == 1 %}{{ test }}{% endif %}))
+ )
+ end
+
+ def test_unless_condition
+ assert_equal(
+ ["test"],
+ visit(%({% unless test %}{% endunless %}))
+ )
+ end
+
+ def test_complex_unless_condition
+ assert_equal(
+ ["test"],
+ visit(%({% unless 1 == 1 and 2 == test %}{% endunless %}))
+ )
+ end
+
+ def test_unless_body
+ assert_equal(
+ ["test"],
+ visit(%({% unless 1 == 1 %}{{ test }}{% endunless %}))
+ )
+ end
+
+ def test_elsif_condition
+ assert_equal(
+ ["test"],
+ visit(%({% if 1 == 1 %}{% elsif test %}{% endif %}))
+ )
+ end
+
+ def test_complex_elsif_condition
+ assert_equal(
+ ["test"],
+ visit(%({% if 1 == 1 %}{% elsif 1 == 1 and 2 == test %}{% endif %}))
+ )
+ end
+
+ def test_elsif_body
+ assert_equal(
+ ["test"],
+ visit(%({% if 1 == 1 %}{% elsif 2 == 2 %}{{ test }}{% endif %}))
+ )
+ end
+
+ def test_else_body
+ assert_equal(
+ ["test"],
+ visit(%({% if 1 == 1 %}{% else %}{{ test }}{% endif %}))
+ )
+ end
+
+ def test_case_left
+ assert_equal(
+ ["test"],
+ visit(%({% case test %}{% endcase %}))
+ )
+ end
+
+ def test_case_condition
+ assert_equal(
+ ["test"],
+ visit(%({% case 1 %}{% when test %}{% endcase %}))
+ )
+ end
+
+ def test_case_when_body
+ assert_equal(
+ ["test"],
+ visit(%({% case 1 %}{% when 2 %}{{ test }}{% endcase %}))
+ )
+ end
+
+ def test_case_else_body
+ assert_equal(
+ ["test"],
+ visit(%({% case 1 %}{% else %}{{ test }}{% endcase %}))
+ )
+ end
+
+ def test_for_in
+ assert_equal(
+ ["test"],
+ visit(%({% for x in test %}{% endfor %}))
+ )
+ end
+
+ def test_for_limit
+ assert_equal(
+ ["test"],
+ visit(%({% for x in (1..5) limit: test %}{% endfor %}))
+ )
+ end
+
+ def test_for_offset
+ assert_equal(
+ ["test"],
+ visit(%({% for x in (1..5) offset: test %}{% endfor %}))
+ )
+ end
+
+ def test_for_body
+ assert_equal(
+ ["test"],
+ visit(%({% for x in (1..5) %}{{ test }}{% endfor %}))
+ )
+ end
+
+ def test_tablerow_in
+ assert_equal(
+ ["test"],
+ visit(%({% tablerow x in test %}{% endtablerow %}))
+ )
+ end
+
+ def test_tablerow_limit
+ assert_equal(
+ ["test"],
+ visit(%({% tablerow x in (1..5) limit: test %}{% endtablerow %}))
+ )
+ end
+
+ def test_tablerow_offset
+ assert_equal(
+ ["test"],
+ visit(%({% tablerow x in (1..5) offset: test %}{% endtablerow %}))
+ )
+ end
+
+ def test_tablerow_body
+ assert_equal(
+ ["test"],
+ visit(%({% tablerow x in (1..5) %}{{ test }}{% endtablerow %}))
+ )
+ end
+
+ def test_cycle
+ assert_equal(
+ ["test"],
+ visit(%({% cycle test %}))
+ )
+ end
+
+ def test_assign
+ assert_equal(
+ ["test"],
+ visit(%({% assign x = test %}))
+ )
+ end
+
+ def test_capture
+ assert_equal(
+ ["test"],
+ visit(%({% capture x %}{{ test }}{% endcapture %}))
+ )
+ end
+
+ def test_include
+ assert_equal(
+ ["test"],
+ visit(%({% include test %}))
+ )
+ end
+
+ def test_include_with
+ assert_equal(
+ ["test"],
+ visit(%({% include "hai" with test %}))
+ )
+ end
+
+ def test_include_for
+ assert_equal(
+ ["test"],
+ visit(%({% include "hai" for test %}))
+ )
+ end
+
+ def test_preserve_tree_structure
+ assert_equal(
+ [[nil, [
+ [nil, [[nil, [["other", []]]]]],
+ ["test", []],
+ ["xs", []],
+ ]]],
+ traversal(%({% for x in xs offset: test %}{{ other }}{% endfor %})).visit
+ )
+ end
+
+ private
+
+ def traversal(template)
+ ParseTreeVisitor
+ .for(Template.parse(template).root)
+ .add_callback_for(VariableLookup, &:name)
+ end
+
+ def visit(template)
+ traversal(template).visit.flatten.compact
+ end
+end
diff --git a/test/integration/parsing_quirks_test.rb b/test/integration/parsing_quirks_test.rb
index 23742dc..c210b48 100644
--- a/test/integration/parsing_quirks_test.rb
+++ b/test/integration/parsing_quirks_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'test_helper'
class ParsingQuirksTest < Minitest::Test
@@ -99,7 +101,7 @@ class ParsingQuirksTest < Minitest::Test
# After the messed up quotes a filter without parameters (reverse) should work
# but one with parameters (remove) shouldn't be detected.
assert_template_result('here', "{{ 'hi there' | split:\"t\"\" | reverse | first}}")
- assert_template_result('hi ', "{{ 'hi there' | split:\"t\"\" | remove:\"i\" | first}}")
+ assert_template_result('hi ', "{{ 'hi there' | split:\"t\"\" | remove:\"i\" | first}}")
end
end
diff --git a/test/integration/registers/disabled_tags_test.rb b/test/integration/registers/disabled_tags_test.rb
new file mode 100644
index 0000000..1fb2458
--- /dev/null
+++ b/test/integration/registers/disabled_tags_test.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class DisabledTagsTest < Minitest::Test
+ include Liquid
+
+ class DisableRaw < Block
+ disable_tags "raw"
+ end
+
+ class DisableRawEcho < Block
+ disable_tags "raw", "echo"
+ end
+
+ def test_disables_raw
+ with_custom_tag('disable', DisableRaw) do
+ assert_template_result 'raw usage is not allowed in this contextfoo', '{% disable %}{% raw %}Foobar{% endraw %}{% echo "foo" %}{% enddisable %}'
+ end
+ end
+
+ def test_disables_echo_and_raw
+ with_custom_tag('disable', DisableRawEcho) do
+ assert_template_result 'raw usage is not allowed in this contextecho usage is not allowed in this context', '{% disable %}{% raw %}Foobar{% endraw %}{% echo "foo" %}{% enddisable %}'
+ end
+ end
+end
diff --git a/test/integration/render_profiling_test.rb b/test/integration/render_profiling_test.rb
index d0111e7..753b2be 100644
--- a/test/integration/render_profiling_test.rb
+++ b/test/integration/render_profiling_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'test_helper'
class RenderProfilingTest < Minitest::Test
@@ -128,7 +130,7 @@ class RenderProfilingTest < Minitest::Test
t.render!
timing_count = 0
- t.profiler.each do |timing|
+ t.profiler.each do |_timing|
timing_count += 1
end
@@ -145,7 +147,7 @@ class RenderProfilingTest < Minitest::Test
def test_profiling_marks_children_of_for_blocks
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
- t.render!({ "collection" => ["one", "two"] })
+ t.render!("collection" => ["one", "two"])
assert_equal 1, t.profiler.length
# Will profile each invocation of the for block
diff --git a/test/integration/security_test.rb b/test/integration/security_test.rb
index f603ff0..28e9d39 100644
--- a/test/integration/security_test.rb
+++ b/test/integration/security_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'test_helper'
module SecurityFilter
diff --git a/test/integration/standard_filter_test.rb b/test/integration/standard_filter_test.rb
index 025908d..8cc7dc3 100644
--- a/test/integration/standard_filter_test.rb
+++ b/test/integration/standard_filter_test.rb
@@ -1,4 +1,5 @@
# encoding: utf-8
+# frozen_string_literal: true
require 'test_helper'
@@ -17,7 +18,7 @@ class TestThing
"woot: #{@foo}"
end
- def [](whatever)
+ def [](_whatever)
to_s
end
@@ -37,7 +38,7 @@ class TestEnumerable < Liquid::Drop
include Enumerable
def each(&block)
- [ { "foo" => 1, "bar" => 2 }, { "foo" => 2, "bar" => 1 }, { "foo" => 3, "bar" => 3 } ].each(&block)
+ [{ "foo" => 1, "bar" => 2 }, { "foo" => 2, "bar" => 1 }, { "foo" => 3, "bar" => 3 }].each(&block)
end
end
@@ -158,6 +159,10 @@ class StandardFiltersTest < Minitest::Test
assert_equal '1', @filters.url_decode(1)
assert_equal '2001-02-03', @filters.url_decode(Date.new(2001, 2, 3))
assert_nil @filters.url_decode(nil)
+ exception = assert_raises Liquid::ArgumentError do
+ @filters.url_decode('%ff')
+ end
+ assert_equal 'Liquid error: invalid byte sequence in UTF-8', exception.message
end
def test_truncatewords
@@ -177,6 +182,9 @@ class StandardFiltersTest < Minitest::Test
assert_equal 'test', @filters.strip_html("
-#{whitespace} - yes -#{whitespace} -
-+ #{whitespace} + yes + #{whitespace} +
+-#{whitespace} -
-+ #{whitespace} +
+-#{whitespace} -
-+ #{whitespace} +
+- {{- 'John' -}} -
- {%- endif -%} -#{whitespace} -+ {{- 'John' -}} +
+ {%- endif -%} + #{whitespace} +