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 - "#" + "#" end protected - attr_reader :child_relation, :child_condition + attr_reader :child_relation private @@ -116,7 +118,7 @@ module Liquid left = context.evaluate(left) right = context.evaluate(right) - operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}")) + operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}") if operation.respond_to?(:call) operation.call(self, left, right) @@ -124,10 +126,19 @@ module Liquid begin left.send(operation, right) rescue ::ArgumentError => e - raise Liquid::ArgumentError.new(e.message) + raise Liquid::ArgumentError, e.message end end end + + class ParseTreeVisitor < Liquid::ParseTreeVisitor + def children + [ + @node.left, @node.right, + @node.child_condition, @node.attachment + ].compact + end + end end class ElseCondition < Condition diff --git a/lib/liquid/context.rb b/lib/liquid/context.rb index 2dcc6af..88e4671 100644 --- a/lib/liquid/context.rb +++ b/lib/liquid/context.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Liquid # Context keeps the variable stack and resolves variables, as well as keywords # @@ -12,30 +14,38 @@ module Liquid # # context['bob'] #=> nil class Context class Context - attr_reader :scopes, :errors, :registers, :environments, :resource_limits + attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters - def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil) - @environments = [environments].flatten - @scopes = [(outer_scope || {})] - @registers = registers - @errors = [] - @partial = false - @strict_variables = false - @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits) - squash_instance_assigns_with_environments + # rubocop:disable Metrics/ParameterLists + def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}) + new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments) + end - @this_stack_used = false + def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {}) + @environments = [environments] + @environments.flatten! + + @static_environments = [static_environments].flat_map(&:freeze).freeze + @scopes = [(outer_scope || {})] + @registers = registers + @errors = [] + @partial = false + @strict_variables = false + @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits) + @base_scope_depth = 0 + squash_instance_assigns_with_environments self.exception_renderer = Template.default_exception_renderer if rethrow_errors - self.exception_renderer = ->(e) { raise } + self.exception_renderer = ->(_e) { raise } end @interrupts = [] @filters = [] @global_filter = nil end + # rubocop:enable Metrics/ParameterLists def warnings @warnings ||= [] @@ -89,7 +99,7 @@ module Liquid # Push new local scope on the stack. use Context#stack instead def push(new_scope = {}) @scopes.unshift(new_scope) - raise StackLevelError, "Nesting too deep".freeze if @scopes.length > Block::MAX_DEPTH + check_overflow end # Merge a hash of variables in the current local scope @@ -111,19 +121,30 @@ module Liquid # end # # context['var] #=> nil - def stack(new_scope = nil) - old_stack_used = @this_stack_used - if new_scope - push(new_scope) - @this_stack_used = true - else - @this_stack_used = false - end - + def stack(new_scope = {}) + push(new_scope) yield ensure - pop if @this_stack_used - @this_stack_used = old_stack_used + pop + end + + # Creates a new context inheriting resource limits, filters, environment etc., + # but with an isolated scope. + def new_isolated_subcontext + check_overflow + + Context.build( + resource_limits: resource_limits, + static_environments: static_environments, + registers: StaticRegisters.new(registers) + ).tap do |subcontext| + subcontext.base_scope_depth = base_scope_depth + 1 + subcontext.exception_renderer = exception_renderer + subcontext.filters = @filters + subcontext.strainer = nil + subcontext.errors = errors + subcontext.warnings = warnings + end end def clear_instance_assigns @@ -132,10 +153,6 @@ module Liquid # Only allow String, Numeric, Hash, Array, Proc, Boolean or Liquid::Drop def []=(key, value) - unless @this_stack_used - @this_stack_used = true - push({}) - end @scopes[0][key] = value end @@ -164,25 +181,13 @@ module Liquid # This was changed from find() to find_index() because this is a very hot # path and find_index() is optimized in MRI to reduce object allocation index = @scopes.find_index { |s| s.key?(key) } - scope = @scopes[index] if index - variable = nil - - if scope.nil? - @environments.each do |e| - variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found) - # When lookup returned a value OR there is no value but the lookup also did not raise - # then it is the value we are looking for. - if !variable.nil? || @strict_variables && raise_on_not_found - scope = e - break - end - end + variable = if index + lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found) + else + try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found) end - scope ||= @environments.last || @scopes.last - variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found) - variable = variable.to_liquid variable.context = self if variable.respond_to?(:context=) @@ -197,14 +202,44 @@ module Liquid value = obj[key] if value.is_a?(Proc) && obj.respond_to?(:[]=) - obj[key] = (value.arity == 0) ? value.call : value.call(self) + obj[key] = value.arity == 0 ? value.call : value.call(self) else value end end + protected + + attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters + private + attr_reader :base_scope_depth + + def try_variable_find_in_environments(key, raise_on_not_found:) + @environments.each do |environment| + found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found) + if !found_variable.nil? || @strict_variables && raise_on_not_found + return found_variable + end + end + @static_environments.each do |environment| + found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found) + if !found_variable.nil? || @strict_variables && raise_on_not_found + return found_variable + end + end + nil + end + + def check_overflow + raise StackLevelError, "Nesting too deep" if overflow? + end + + def overflow? + base_scope_depth + @scopes.length > Block::MAX_DEPTH + end + def internal_error # raise and catch to set backtrace and cause on exception raise Liquid::InternalError, 'internal' diff --git a/lib/liquid/document.rb b/lib/liquid/document.rb index d035dd4..e160886 100644 --- a/lib/liquid/document.rb +++ b/lib/liquid/document.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Liquid class Document < BlockBody def self.parse(tokens, parse_context) @@ -7,7 +9,7 @@ module Liquid end def parse(tokens, parse_context) - super do |end_tag_name, end_tag_params| + super do |end_tag_name, _end_tag_params| unknown_tag(end_tag_name, parse_context) if end_tag_name end rescue SyntaxError => e @@ -17,10 +19,10 @@ module Liquid def unknown_tag(tag, parse_context) case tag - when 'else'.freeze, 'end'.freeze - raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag)) + when 'else', 'end' + raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag) 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 end diff --git a/lib/liquid/drop.rb b/lib/liquid/drop.rb index 6b5aa99..d4d8950 100644 --- a/lib/liquid/drop.rb +++ b/lib/liquid/drop.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'set' module Liquid @@ -25,7 +27,7 @@ module Liquid # Catch all for the method def liquid_method_missing(method) - return nil unless @context && @context.strict_variables + return nil unless @context&.strict_variables raise Liquid::UndefinedDropMethod, "undefined method #{method}" end @@ -67,7 +69,7 @@ module Liquid if include?(Enumerable) blacklist += Enumerable.public_instance_methods - blacklist -= [:sort, :count, :first, :min, :max, :include?] + blacklist -= [:sort, :count, :first, :min, :max] end whitelist = [:to_liquid] + (public_instance_methods - blacklist) diff --git a/lib/liquid/errors.rb b/lib/liquid/errors.rb index defa5ea..eda0bd2 100644 --- a/lib/liquid/errors.rb +++ b/lib/liquid/errors.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Liquid class Error < ::StandardError attr_accessor :line_number @@ -5,7 +7,7 @@ module Liquid attr_accessor :markup_context def to_s(with_prefix = true) - str = "" + str = +"" str << message_prefix if with_prefix str << super() @@ -20,11 +22,11 @@ module Liquid private def message_prefix - str = "" - if is_a?(SyntaxError) - str << "Liquid syntax error" + str = +"" + str << if is_a?(SyntaxError) + "Liquid syntax error" else - str << "Liquid error" + "Liquid error" end if line_number diff --git a/lib/liquid/expression.rb b/lib/liquid/expression.rb index 1d01cdd..9670906 100644 --- a/lib/liquid/expression.rb +++ b/lib/liquid/expression.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Liquid class Expression class MethodLiteral @@ -14,12 +16,12 @@ module Liquid end LITERALS = { - nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil, - 'true'.freeze => true, - 'false'.freeze => false, - 'blank'.freeze => MethodLiteral.new(:blank?, '').freeze, - 'empty'.freeze => MethodLiteral.new(:empty?, '').freeze - } + nil => nil, 'nil' => nil, 'null' => nil, '' => nil, + 'true' => true, + 'false' => false, + 'blank' => MethodLiteral.new(:blank?, '').freeze, + 'empty' => MethodLiteral.new(:empty?, '').freeze + }.freeze SINGLE_QUOTED_STRING = /\A'(.*)'\z/m DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m @@ -33,13 +35,13 @@ module Liquid else case markup when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING - $1 + Regexp.last_match(1) when INTEGERS_REGEX - $1.to_i + Regexp.last_match(1).to_i when RANGES_REGEX - RangeLookup.parse($1, $2) + RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2)) when FLOATS_REGEX - $1.to_f + Regexp.last_match(1).to_f else VariableLookup.parse(markup) end diff --git a/lib/liquid/extensions.rb b/lib/liquid/extensions.rb index 0907819..d185498 100644 --- a/lib/liquid/extensions.rb +++ b/lib/liquid/extensions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'time' require 'date' diff --git a/lib/liquid/file_system.rb b/lib/liquid/file_system.rb index 13f1f46..27ab632 100644 --- a/lib/liquid/file_system.rb +++ b/lib/liquid/file_system.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Liquid # A Liquid file system is a way to let your templates retrieve other templates for use with the include tag. # @@ -44,7 +46,7 @@ module Liquid class LocalFileSystem attr_accessor :root - def initialize(root, pattern = "_%s.liquid".freeze) + def initialize(root, pattern = "_%s.liquid") @root = root @pattern = pattern end @@ -57,9 +59,9 @@ module Liquid end def full_path(template_path) - raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/ + raise FileSystemError, "Illegal template name '#{template_path}'" unless %r{\A[^./][a-zA-Z0-9_/]+\z}.match?(template_path) - full_path = if template_path.include?('/'.freeze) + full_path = if template_path.include?('/') File.join(root, File.dirname(template_path), @pattern % File.basename(template_path)) else File.join(root, @pattern % template_path) diff --git a/lib/liquid/forloop_drop.rb b/lib/liquid/forloop_drop.rb index 81b2d1a..0ffa255 100644 --- a/lib/liquid/forloop_drop.rb +++ b/lib/liquid/forloop_drop.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Liquid class ForloopDrop < Drop def initialize(name, length, parentloop) diff --git a/lib/liquid/i18n.rb b/lib/liquid/i18n.rb index e998d58..4a2885e 100644 --- a/lib/liquid/i18n.rb +++ b/lib/liquid/i18n.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'yaml' module Liquid @@ -26,13 +28,13 @@ module Liquid def interpolate(name, vars) name.gsub(/%\{(\w+)\}/) do # raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym] - "#{vars[$1.to_sym]}" + (vars[Regexp.last_match(1).to_sym]).to_s end end def deep_fetch_translation(name) - name.split('.'.freeze).reduce(locale) do |level, cur| - level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}" + name.split('.').reduce(locale) do |level, cur| + level[cur] || raise(TranslationError, "Translation for #{name} does not exist in locale #{path}") end end end diff --git a/lib/liquid/interrupts.rb b/lib/liquid/interrupts.rb index 41359d7..28355b8 100644 --- a/lib/liquid/interrupts.rb +++ b/lib/liquid/interrupts.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + module Liquid # An interrupt is any command that breaks processing of a block (ex: a for loop). class Interrupt attr_reader :message def initialize(message = nil) - @message = message || "interrupt".freeze + @message = message || "interrupt" end end diff --git a/lib/liquid/lexer.rb b/lib/liquid/lexer.rb index e9114df..a251c3e 100644 --- a/lib/liquid/lexer.rb +++ b/lib/liquid/lexer.rb @@ -1,18 +1,20 @@ +# frozen_string_literal: true + require "strscan" module Liquid class Lexer SPECIALS = { - '|'.freeze => :pipe, - '.'.freeze => :dot, - ':'.freeze => :colon, - ','.freeze => :comma, - '['.freeze => :open_square, - ']'.freeze => :close_square, - '('.freeze => :open_round, - ')'.freeze => :close_round, - '?'.freeze => :question, - '-'.freeze => :dash - } + '|' => :pipe, + '.' => :dot, + ':' => :colon, + ',' => :comma, + '[' => :open_square, + ']' => :close_square, + '(' => :open_round, + ')' => :close_round, + '?' => :question, + '-' => :dash, + }.freeze IDENTIFIER = /[a-zA-Z_][\w-]*\??/ SINGLE_STRING_LITERAL = /'[^\']*'/ DOUBLE_STRING_LITERAL = /"[^\"]*"/ @@ -31,16 +33,21 @@ module Liquid until @ss.eos? @ss.skip(WHITESPACE_OR_NOTHING) break if @ss.eos? - tok = case - when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t] - when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t] - when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t] - when t = @ss.scan(NUMBER_LITERAL) then [:number, t] - when t = @ss.scan(IDENTIFIER) then [:id, t] - when t = @ss.scan(DOTDOT) then [:dotdot, t] + tok = if (t = @ss.scan(COMPARISON_OPERATOR)) + [:comparison, t] + elsif (t = @ss.scan(SINGLE_STRING_LITERAL)) + [:string, t] + elsif (t = @ss.scan(DOUBLE_STRING_LITERAL)) + [:string, t] + elsif (t = @ss.scan(NUMBER_LITERAL)) + [:number, t] + elsif (t = @ss.scan(IDENTIFIER)) + [:id, t] + elsif (t = @ss.scan(DOTDOT)) + [:dotdot, t] else c = @ss.getch - if s = SPECIALS[c] + if (s = SPECIALS[c]) [s, c] else raise SyntaxError, "Unexpected character #{c}" diff --git a/lib/liquid/locales/en.yml b/lib/liquid/locales/en.yml index 48b3b1d..a26320b 100644 --- a/lib/liquid/locales/en.yml +++ b/lib/liquid/locales/en.yml @@ -22,5 +22,8 @@ tag_never_closed: "'%{block_name}' tag was never closed" meta_syntax_error: "Liquid syntax error: #{e.message}" table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3" + render: "Syntax error in tag 'render' - Template name must be a quoted string" argument: include: "Argument error in tag 'include' - Illegal template name" + disabled: + tag: "usage is not allowed in this context" diff --git a/lib/liquid/parse_context.rb b/lib/liquid/parse_context.rb index abcdaeb..4afdbe5 100644 --- a/lib/liquid/parse_context.rb +++ b/lib/liquid/parse_context.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Liquid class ParseContext attr_accessor :locale, :line_number, :trim_whitespace, :depth @@ -19,7 +21,6 @@ module Liquid @partial = value @options = value ? partial_options : @template_options @error_mode = @options[:error_mode] || Template.error_mode - value end def partial_options @@ -28,7 +29,7 @@ module Liquid if dont_pass == true { locale: locale } elsif dont_pass.is_a?(Array) - @template_options.reject { |k, v| dont_pass.include?(k) } + @template_options.reject { |k, _v| dont_pass.include?(k) } else @template_options end diff --git a/lib/liquid/parse_tree_visitor.rb b/lib/liquid/parse_tree_visitor.rb new file mode 100644 index 0000000..d50943f --- /dev/null +++ b/lib/liquid/parse_tree_visitor.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Liquid + class ParseTreeVisitor + def self.for(node, callbacks = Hash.new(proc {})) + if defined?(node.class::ParseTreeVisitor) + node.class::ParseTreeVisitor + else + self + end.new(node, callbacks) + end + + def initialize(node, callbacks) + @node = node + @callbacks = callbacks + end + + def add_callback_for(*classes, &block) + callback = block + callback = ->(node, _) { yield node } if block.arity.abs == 1 + callback = ->(_, _) { yield } if block.arity.zero? + classes.each { |klass| @callbacks[klass] = callback } + self + end + + def visit(context = nil) + children.map do |node| + item, new_context = @callbacks[node.class].call(node, context) + [ + item, + ParseTreeVisitor.for(node, @callbacks).visit(new_context || context), + ] + end + end + + protected + + def children + @node.respond_to?(:nodelist) ? Array(@node.nodelist) : [] + end + end +end diff --git a/lib/liquid/parser.rb b/lib/liquid/parser.rb index 6954343..6b9e837 100644 --- a/lib/liquid/parser.rb +++ b/lib/liquid/parser.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Liquid class Parser def initialize(input) @@ -44,11 +46,14 @@ module Liquid tok[0] == type end + SINGLE_TOKEN_EXPRESSION_TYPES = [:string, :number].freeze + private_constant :SINGLE_TOKEN_EXPRESSION_TYPES + def expression token = @tokens[@p] if token[0] == :id variable_signature - elsif [:string, :number].include? token[0] + elsif SINGLE_TOKEN_EXPRESSION_TYPES.include?(token[0]) consume elsif token.first == :open_round consume @@ -63,10 +68,10 @@ module Liquid end def argument - str = "" + str = +"" # might be a keyword argument (identifier: expression) if look(:id) && look(:colon, 1) - str << consume << consume << ' '.freeze + str << consume << consume << ' ' end str << expression diff --git a/lib/liquid/parser_switching.rb b/lib/liquid/parser_switching.rb index 3aa664a..402b056 100644 --- a/lib/liquid/parser_switching.rb +++ b/lib/liquid/parser_switching.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Liquid module ParserSwitching def parse_with_selected_parser(markup) diff --git a/lib/liquid/partial_cache.rb b/lib/liquid/partial_cache.rb new file mode 100644 index 0000000..43c2e39 --- /dev/null +++ b/lib/liquid/partial_cache.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Liquid + class PartialCache + def self.load(template_name, context:, parse_context:) + cached_partials = (context.registers[:cached_partials] ||= {}) + cached = cached_partials[template_name] + return cached if cached + + file_system = (context.registers[:file_system] ||= Liquid::Template.file_system) + source = file_system.read_template_file(template_name) + parse_context.partial = true + + partial = Liquid::Template.parse(source, parse_context) + cached_partials[template_name] = partial + ensure + parse_context.partial = false + end + end +end diff --git a/lib/liquid/profiler.rb b/lib/liquid/profiler.rb index dc9db60..dc3f1db 100644 --- a/lib/liquid/profiler.rb +++ b/lib/liquid/profiler.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'liquid/profiler/hooks' module Liquid diff --git a/lib/liquid/profiler/hooks.rb b/lib/liquid/profiler/hooks.rb index cb11cd7..e708653 100644 --- a/lib/liquid/profiler/hooks.rb +++ b/lib/liquid/profiler/hooks.rb @@ -1,23 +1,25 @@ +# frozen_string_literal: true + module Liquid class BlockBody - def render_node_with_profiling(node, output, context, skip_output = false) + def render_node_with_profiling(context, output, node) Profiler.profile_node_render(node) do - render_node_without_profiling(node, output, context, skip_output) + render_node_without_profiling(context, output, node) end end - alias_method :render_node_without_profiling, :render_node_to_output - alias_method :render_node_to_output, :render_node_with_profiling + alias_method :render_node_without_profiling, :render_node + alias_method :render_node, :render_node_with_profiling end class Include < Tag - def render_with_profiling(context) + def render_to_output_buffer_with_profiling(context, output) Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do - render_without_profiling(context) + render_to_output_buffer_without_profiling(context, output) end end - alias_method :render_without_profiling, :render - alias_method :render, :render_with_profiling + alias_method :render_to_output_buffer_without_profiling, :render_to_output_buffer + alias_method :render_to_output_buffer, :render_to_output_buffer_with_profiling end end diff --git a/lib/liquid/range_lookup.rb b/lib/liquid/range_lookup.rb index 93bb420..8e4d765 100644 --- a/lib/liquid/range_lookup.rb +++ b/lib/liquid/range_lookup.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Liquid class RangeLookup def self.parse(start_markup, end_markup) diff --git a/lib/liquid/register.rb b/lib/liquid/register.rb new file mode 100644 index 0000000..92d0226 --- /dev/null +++ b/lib/liquid/register.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Liquid + class Register + end +end diff --git a/lib/liquid/registers/disabled_tags.rb b/lib/liquid/registers/disabled_tags.rb new file mode 100644 index 0000000..b1cd3bd --- /dev/null +++ b/lib/liquid/registers/disabled_tags.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true +module Liquid + class DisabledTags < Register + def initialize + @disabled_tags = {} + end + + def disabled?(tag) + @disabled_tags.key?(tag) && @disabled_tags[tag] > 0 + end + + def disable(tags) + tags.each(&method(:increment)) + yield + ensure + tags.each(&method(:decrement)) + end + + private + + def increment(tag) + @disabled_tags[tag] ||= 0 + @disabled_tags[tag] += 1 + end + + def decrement(tag) + @disabled_tags[tag] -= 1 + end + end + + Template.add_register(:disabled_tags, DisabledTags.new) +end diff --git a/lib/liquid/resource_limits.rb b/lib/liquid/resource_limits.rb index 08b359b..5b7e8e4 100644 --- a/lib/liquid/resource_limits.rb +++ b/lib/liquid/resource_limits.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Liquid class ResourceLimits attr_accessor :render_length, :render_score, :assign_score, diff --git a/lib/liquid/standardfilters.rb b/lib/liquid/standardfilters.rb index 51ecd70..644967f 100644 --- a/lib/liquid/standardfilters.rb +++ b/lib/liquid/standardfilters.rb @@ -1,16 +1,24 @@ +# frozen_string_literal: true + require 'cgi' require 'bigdecimal' module Liquid module StandardFilters HTML_ESCAPE = { - '&'.freeze => '&'.freeze, - '>'.freeze => '>'.freeze, - '<'.freeze => '<'.freeze, - '"'.freeze => '"'.freeze, - "'".freeze => '''.freeze - } + '&' => '&', + '>' => '>', + '<' => '<', + '"' => '"', + "'" => ''', + }.freeze HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/ + STRIP_HTML_BLOCKS = Regexp.union( + %r{}m, + //m, + %r{}m + ) + STRIP_HTML_TAGS = /<.*?>/m # Return the size of an array or of an string def size(input) @@ -46,7 +54,12 @@ module Liquid end def url_decode(input) - CGI.unescape(input.to_s) unless input.nil? + return if input.nil? + + result = CGI.unescape(input.to_s) + raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding? + + result end def slice(input, offset, length = nil) @@ -61,23 +74,23 @@ module Liquid end # Truncate a string down to x characters - def truncate(input, length = 50, truncate_string = "...".freeze) + def truncate(input, length = 50, truncate_string = "...") return if input.nil? input_str = input.to_s length = Utils.to_integer(length) truncate_string_str = truncate_string.to_s l = length - truncate_string_str.length l = 0 if l < 0 - input_str.length > length ? input_str[0...l] + truncate_string_str : input_str + input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str end - def truncatewords(input, words = 15, truncate_string = "...".freeze) + def truncatewords(input, words = 15, truncate_string = "...") return if input.nil? wordlist = input.to_s.split words = Utils.to_integer(words) l = words - 1 l = 0 if l < 0 - wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string.to_s : input + wordlist.length > l ? wordlist[0..l].join(" ").concat(truncate_string.to_s) : input end # Split input string into an array of substrings separated by given pattern. @@ -102,17 +115,19 @@ module Liquid end def strip_html(input) - empty = ''.freeze - input.to_s.gsub(//m, empty).gsub(//m, empty).gsub(//m, empty).gsub(/<.*?>/m, empty) + empty = '' + result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty) + result.gsub!(STRIP_HTML_TAGS, empty) + result end # Remove all newlines from the string def strip_newlines(input) - input.to_s.gsub(/\r?\n/, ''.freeze) + input.to_s.gsub(/\r?\n/, '') end # Join elements of the array with certain character between them - def join(input, glue = ' '.freeze) + def join(input, glue = ' ') InputIterator.new(input).join(glue) end @@ -120,19 +135,18 @@ module Liquid # provide optional property with which to sort an array of hashes or drops def sort(input, property = nil) ary = InputIterator.new(input) + + return [] if ary.empty? + if property.nil? - ary.sort - elsif ary.empty? # The next two cases assume a non-empty array. - [] - elsif ary.first.respond_to?(:[]) && !ary.first[property].nil? ary.sort do |a, b| - a = a[property] - b = b[property] - if a && b - a <=> b - else - a ? -1 : 1 - end + nil_safe_compare(a, b) + end + elsif ary.all? { |el| el.respond_to?(:[]) } + begin + ary.sort { |a, b| nil_safe_compare(a[property], b[property]) } + rescue TypeError + raise_property_error(property) end end end @@ -142,12 +156,40 @@ module Liquid def sort_natural(input, property = nil) ary = InputIterator.new(input) + return [] if ary.empty? + if property.nil? - ary.sort { |a, b| a.casecmp(b) } - elsif ary.empty? # The next two cases assume a non-empty array. + ary.sort do |a, b| + nil_safe_casecmp(a, b) + end + elsif ary.all? { |el| el.respond_to?(:[]) } + begin + ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) } + rescue TypeError + raise_property_error(property) + end + end + end + + # Filter the elements of an array to those with a certain property value. + # By default the target is any truthy value. + def where(input, property, target_value = nil) + ary = InputIterator.new(input) + + if ary.empty? [] - elsif ary.first.respond_to?(:[]) && !ary.first[property].nil? - ary.sort { |a, b| a[property].casecmp(b[property]) } + elsif ary.first.respond_to?(:[]) && target_value.nil? + begin + ary.select { |item| item[property] } + rescue TypeError + raise_property_error(property) + end + elsif ary.first.respond_to?(:[]) + begin + ary.select { |item| item[property] == target_value } + rescue TypeError + raise_property_error(property) + end end end @@ -178,7 +220,11 @@ module Liquid elsif ary.empty? # The next two cases assume a non-empty array. [] elsif ary.first.respond_to?(:[]) - ary.uniq{ |a| a[property] } + begin + ary.uniq { |a| a[property] } + rescue TypeError + raise_property_error(property) + end end end @@ -193,13 +239,15 @@ module Liquid InputIterator.new(input).map do |e| e = e.call if e.is_a?(Proc) - if property == "to_liquid".freeze + if property == "to_liquid" e elsif e.respond_to?(:[]) r = e[property] r.is_a?(Proc) ? r.call : r end end + rescue TypeError + raise_property_error(property) end # Remove nils within an array @@ -212,28 +260,32 @@ module Liquid elsif ary.empty? # The next two cases assume a non-empty array. [] elsif ary.first.respond_to?(:[]) - ary.reject{ |a| a[property].nil? } + begin + ary.reject { |a| a[property].nil? } + rescue TypeError + raise_property_error(property) + end end end # Replace occurrences of a string with another - def replace(input, string, replacement = ''.freeze) + def replace(input, string, replacement = '') input.to_s.gsub(string.to_s, replacement.to_s) end # Replace the first occurrences of a string with another - def replace_first(input, string, replacement = ''.freeze) + def replace_first(input, string, replacement = '') input.to_s.sub(string.to_s, replacement.to_s) end # remove a substring def remove(input, string) - input.to_s.gsub(string.to_s, ''.freeze) + input.to_s.gsub(string.to_s, '') end # remove the first occurrences of a substring def remove_first(input, string) - input.to_s.sub(string.to_s, ''.freeze) + input.to_s.sub(string.to_s, '') end # add one string to another @@ -243,7 +295,7 @@ module Liquid def concat(input, array) unless array.respond_to?(:to_ary) - raise ArgumentError.new("concat filter requires an array argument") + raise ArgumentError, "concat filter requires an array argument" end InputIterator.new(input).concat(array) end @@ -255,7 +307,7 @@ module Liquid # Add
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 << "" << super << '' + output << "" + super + output << '' if tablerowloop.col_last && !tablerowloop.last - result << "\n" + 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("test") assert_equal 'test', @filters.strip_html("test") assert_equal '', @filters.strip_html(nil) + + # Quirk of the existing implementation + assert_equal 'foo;', @filters.strip_html("<<") end def test_join @@ -192,9 +200,13 @@ class StandardFiltersTest < Minitest::Test def test_sort_numeric assert_equal ['1', '2', '3', '10'], @filters.sort_numeric(['10', '3', '2', '1']) - assert_equal [{ "a" => '1' }, { "a" => '2' }, { "a" => '3' }, { "a" => '10' }], @filters.sort_numeric([{ "a" => '10' }, { "a" => '3' }, { "a" => '1' }, { "a" => '2' }], "a") + end + + def test_sort_with_nils + assert_equal [1, 2, 3, 4, nil], @filters.sort([nil, 4, 3, 2, 1]) + assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }, {}], @filters.sort([{ "a" => 4 }, { "a" => 3 }, {}, { "a" => 1 }, { "a" => 2 }], "a") end def test_sort_when_property_is_sometimes_missing_puts_nils_last @@ -203,22 +215,85 @@ class StandardFiltersTest < Minitest::Test { "handle" => "beta" }, { "price" => 1, "handle" => "gamma" }, { "handle" => "delta" }, - { "price" => 2, "handle" => "epsilon" } + { "price" => 2, "handle" => "epsilon" }, ] expectation = [ { "price" => 1, "handle" => "gamma" }, { "price" => 2, "handle" => "epsilon" }, { "price" => 4, "handle" => "alpha" }, { "handle" => "delta" }, - { "handle" => "beta" } + { "handle" => "beta" }, ] assert_equal expectation, @filters.sort(input, "price") end + def test_sort_natural + assert_equal ["a", "B", "c", "D"], @filters.sort_natural(["c", "D", "a", "B"]) + assert_equal [{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, { "a" => "a" }, { "a" => "B" }], "a") + end + + def test_sort_natural_with_nils + assert_equal ["a", "B", "c", "D", nil], @filters.sort_natural([nil, "c", "D", "a", "B"]) + assert_equal [{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }, {}], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, {}, { "a" => "a" }, { "a" => "B" }], "a") + end + + def test_sort_natural_when_property_is_sometimes_missing_puts_nils_last + input = [ + { "price" => "4", "handle" => "alpha" }, + { "handle" => "beta" }, + { "price" => "1", "handle" => "gamma" }, + { "handle" => "delta" }, + { "price" => 2, "handle" => "epsilon" }, + ] + expectation = [ + { "price" => "1", "handle" => "gamma" }, + { "price" => 2, "handle" => "epsilon" }, + { "price" => "4", "handle" => "alpha" }, + { "handle" => "delta" }, + { "handle" => "beta" }, + ] + assert_equal expectation, @filters.sort_natural(input, "price") + end + + def test_sort_natural_case_check + input = [ + { "key" => "X" }, + { "key" => "Y" }, + { "key" => "Z" }, + { "fake" => "t" }, + { "key" => "a" }, + { "key" => "b" }, + { "key" => "c" }, + ] + expectation = [ + { "key" => "a" }, + { "key" => "b" }, + { "key" => "c" }, + { "key" => "X" }, + { "key" => "Y" }, + { "key" => "Z" }, + { "fake" => "t" }, + ] + assert_equal expectation, @filters.sort_natural(input, "key") + assert_equal ["a", "b", "c", "X", "Y", "Z"], @filters.sort_natural(["X", "Y", "Z", "a", "b", "c"]) + end + def test_sort_empty_array assert_equal [], @filters.sort([], "a") end + def test_sort_invalid_property + foo = [ + [1], + [2], + [3], + ] + + assert_raises Liquid::ArgumentError do + @filters.sort(foo, "bar") + end + end + def test_sort_natural_empty_array assert_equal [], @filters.sort_natural([], "a") end @@ -226,9 +301,21 @@ class StandardFiltersTest < Minitest::Test def test_sort_numeric_empty_array assert_equal [], @filters.sort_numeric([], "a") end + + def test_sort_natural_invalid_property + foo = [ + [1], + [2], + [3], + ] + + assert_raises Liquid::ArgumentError do + @filters.sort_natural(foo, "bar") + end + end def test_legacy_sort_hash - assert_equal [{ a: 1, b: 2 }], @filters.sort({ a: 1, b: 2 }) + assert_equal [{ a: 1, b: 2 }], @filters.sort(a: 1, b: 2) end def test_numerical_vs_lexicographical_sort @@ -250,10 +337,34 @@ class StandardFiltersTest < Minitest::Test assert_equal [], @filters.uniq([], "a") end + def test_uniq_invalid_property + foo = [ + [1], + [2], + [3], + ] + + assert_raises Liquid::ArgumentError do + @filters.uniq(foo, "bar") + end + end + def test_compact_empty_array assert_equal [], @filters.compact([], "a") end + def test_compact_invalid_property + foo = [ + [1], + [2], + [3], + ] + + assert_raises Liquid::ArgumentError do + @filters.compact(foo, "bar") + end + end + def test_reverse assert_equal [4, 3, 2, 1], @filters.reverse([1, 2, 3, 4]) end @@ -280,7 +391,7 @@ class StandardFiltersTest < Minitest::Test def test_map_on_hashes assert_template_result "4217", '{{ thing | map: "foo" | map: "bar" }}', - "thing" => { "foo" => [ { "bar" => 42 }, { "bar" => 17 } ] } + "thing" => { "foo" => [{ "bar" => 42 }, { "bar" => 17 }] } end def test_legacy_map_on_hashes_with_dynamic_key @@ -297,7 +408,7 @@ class StandardFiltersTest < Minitest::Test def test_map_over_proc drop = TestDrop.new - p = proc{ drop } + p = proc { drop } templ = '{{ procs | map: "test" }}' assert_template_result "testfoo", templ, "procs" => [p] end @@ -305,10 +416,10 @@ class StandardFiltersTest < Minitest::Test def test_map_over_drops_returning_procs drops = [ { - "proc" => ->{ "foo" }, + "proc" => -> { "foo" }, }, { - "proc" => ->{ "bar" }, + "proc" => -> { "bar" }, }, ] templ = '{{ drops | map: "proc" }}' @@ -319,6 +430,29 @@ class StandardFiltersTest < Minitest::Test assert_template_result "123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new end + def test_map_returns_empty_on_2d_input_array + foo = [ + [1], + [2], + [3], + ] + + assert_raises Liquid::ArgumentError do + @filters.map(foo, "bar") + end + end + + def test_map_returns_empty_with_no_property + foo = [ + [1], + [2], + [3], + ] + assert_raises Liquid::ArgumentError do + @filters.map(foo, nil) + end + end + def test_sort_works_on_enumerables assert_template_result "213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new end @@ -349,9 +483,9 @@ class StandardFiltersTest < Minitest::Test assert_equal '07/05/2006', @filters.date("2006-07-05 10:00:00", "%m/%d/%Y") assert_equal "07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y") - assert_equal "#{Date.today.year}", @filters.date('now', '%Y') - assert_equal "#{Date.today.year}", @filters.date('today', '%Y') - assert_equal "#{Date.today.year}", @filters.date('Today', '%Y') + assert_equal Date.today.year.to_s, @filters.date('now', '%Y') + assert_equal Date.today.year.to_s, @filters.date('today', '%Y') + assert_equal Date.today.year.to_s, @filters.date('Today', '%Y') assert_nil @filters.date(nil, "%B") @@ -569,6 +703,78 @@ class StandardFiltersTest < Minitest::Test assert_template_result('abc', "{{ 'abc' | date: '%D' }}") end + def test_where + input = [ + { "handle" => "alpha", "ok" => true }, + { "handle" => "beta", "ok" => false }, + { "handle" => "gamma", "ok" => false }, + { "handle" => "delta", "ok" => true }, + ] + + expectation = [ + { "handle" => "alpha", "ok" => true }, + { "handle" => "delta", "ok" => true }, + ] + + assert_equal expectation, @filters.where(input, "ok", true) + assert_equal expectation, @filters.where(input, "ok") + end + + def test_where_no_key_set + input = [ + { "handle" => "alpha", "ok" => true }, + { "handle" => "beta" }, + { "handle" => "gamma" }, + { "handle" => "delta", "ok" => true }, + ] + + expectation = [ + { "handle" => "alpha", "ok" => true }, + { "handle" => "delta", "ok" => true }, + ] + + assert_equal expectation, @filters.where(input, "ok", true) + assert_equal expectation, @filters.where(input, "ok") + end + + def test_where_non_array_map_input + assert_equal [{ "a" => "ok" }], @filters.where({ "a" => "ok" }, "a", "ok") + assert_equal [], @filters.where({ "a" => "not ok" }, "a", "ok") + end + + def test_where_indexable_but_non_map_value + assert_raises(Liquid::ArgumentError) { @filters.where(1, "ok", true) } + assert_raises(Liquid::ArgumentError) { @filters.where(1, "ok") } + end + + def test_where_non_boolean_value + input = [ + { "message" => "Bonjour!", "language" => "French" }, + { "message" => "Hello!", "language" => "English" }, + { "message" => "Hallo!", "language" => "German" }, + ] + + assert_equal [{ "message" => "Bonjour!", "language" => "French" }], @filters.where(input, "language", "French") + assert_equal [{ "message" => "Hallo!", "language" => "German" }], @filters.where(input, "language", "German") + assert_equal [{ "message" => "Hello!", "language" => "English" }], @filters.where(input, "language", "English") + end + + def test_where_array_of_only_unindexable_values + assert_nil @filters.where([nil], "ok", true) + assert_nil @filters.where([nil], "ok") + end + + def test_where_no_target_value + input = [ + { "foo" => false }, + { "foo" => true }, + { "foo" => "for sure" }, + { "bar" => true }, + ] + + assert_equal [{ "foo" => true }, { "foo" => "for sure" }], @filters.where(input, "foo") + end + private def with_timezone(tz) diff --git a/test/integration/tags/break_tag_test.rb b/test/integration/tags/break_tag_test.rb index 0fbde83..c3a4679 100644 --- a/test/integration/tags/break_tag_test.rb +++ b/test/integration/tags/break_tag_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class BreakTagTest < Minitest::Test diff --git a/test/integration/tags/continue_tag_test.rb b/test/integration/tags/continue_tag_test.rb index ce4c158..00cca17 100644 --- a/test/integration/tags/continue_tag_test.rb +++ b/test/integration/tags/continue_tag_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class ContinueTagTest < Minitest::Test diff --git a/test/integration/tags/echo_test.rb b/test/integration/tags/echo_test.rb new file mode 100644 index 0000000..c64932e --- /dev/null +++ b/test/integration/tags/echo_test.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'test_helper' + +class EchoTest < Minitest::Test + include Liquid + + def test_echo_outputs_its_input + assert_template_result('BAR', <<~LIQUID, 'variable-name' => 'bar') + {%- echo variable-name | upcase -%} + LIQUID + end +end diff --git a/test/integration/tags/for_tag_test.rb b/test/integration/tags/for_tag_test.rb index cb7a822..667efac 100644 --- a/test/integration/tags/for_tag_test.rb +++ b/test/integration/tags/for_tag_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class ThingWithValue < Liquid::Drop @@ -23,16 +25,16 @@ class ForTagTest < Minitest::Test yo HERE - template = < [1, 2, 3]) end def test_for_reversed - assigns = { 'array' => [ 1, 2, 3] } + assigns = { 'array' => [1, 2, 3] } assert_template_result('321', '{%for item in array reversed %}{{item}}{%endfor%}', assigns) end @@ -103,6 +105,34 @@ HERE assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns) end + def test_limiting_with_invalid_limit + assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } + template = <<-MKUP + {% for i in array limit: true offset: 1 %} + {{ i }} + {% endfor %} + MKUP + + exception = assert_raises(Liquid::ArgumentError) do + Template.parse(template).render!(assigns) + end + assert_equal("Liquid error: invalid integer", exception.message) + end + + def test_limiting_with_invalid_offset + assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } + template = <<-MKUP + {% for i in array limit: 1 offset: true %} + {{ i }} + {% endfor %} + MKUP + + exception = assert_raises(Liquid::ArgumentError) do + Template.parse(template).render!(assigns) + end + assert_equal("Liquid error: invalid integer", exception.message) + end + def test_dynamic_variable_limiting assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } assigns['limit'] = 2 diff --git a/test/integration/tags/if_else_tag_test.rb b/test/integration/tags/if_else_tag_test.rb index 45a5d3a..d54b2fb 100644 --- a/test/integration/tags/if_else_tag_test.rb +++ b/test/integration/tags/if_else_tag_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class IfElseTagTest < Minitest::Test @@ -132,7 +134,7 @@ class IfElseTagTest < Minitest::Test end def test_syntax_error_no_variable - assert_raises(SyntaxError){ assert_template_result('', '{% if jerry == 1 %}') } + assert_raises(SyntaxError) { assert_template_result('', '{% if jerry == 1 %}') } end def test_syntax_error_no_expression diff --git a/test/integration/tags/include_tag_test.rb b/test/integration/tags/include_tag_test.rb index 1d854b0..45410a7 100644 --- a/test/integration/tags/include_tag_test.rb +++ b/test/integration/tags/include_tag_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class TestFileSystem @@ -30,6 +32,9 @@ class TestFileSystem when 'assignments' "{% assign foo = 'bar' %}" + when 'break' + "{% break %}" + else template_path end @@ -37,14 +42,14 @@ class TestFileSystem end class OtherFileSystem - def read_template_file(template_path) + def read_template_file(_template_path) 'from OtherFileSystem' end end class CountingFileSystem attr_reader :count - def read_template_file(template_path) + def read_template_file(_template_path) @count ||= 0 @count += 1 'from CountingFileSystem' @@ -56,15 +61,16 @@ class CustomInclude < Liquid::Tag def initialize(tag_name, markup, tokens) markup =~ Syntax - @template_name = $1 + @template_name = Regexp.last_match(1) super end def parse(tokens) end - def render(context) - @template_name[1..-2] + def render_to_output_buffer(_context, output) + output << @template_name[1..-2] + output end end @@ -82,7 +88,7 @@ class IncludeTagTest < Minitest::Test def test_include_tag_with assert_template_result "Product: Draft 151cm ", - "{% include 'product' with products[0] %}", "products" => [ { 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' } ] + "{% include 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] end def test_include_tag_with_default_name @@ -92,7 +98,7 @@ class IncludeTagTest < Minitest::Test def test_include_tag_for assert_template_result "Product: Draft 151cm Product: Element 155cm ", - "{% include 'product' for products %}", "products" => [ { 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' } ] + "{% include 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] end def test_include_tag_with_local_variables @@ -130,7 +136,7 @@ class IncludeTagTest < Minitest::Test def test_recursively_included_template_does_not_produce_endless_loop infinite_file_system = Class.new do - def read_template_file(template_path) + def read_template_file(_template_path) "-{% include 'loop' %}" end end @@ -242,4 +248,9 @@ class IncludeTagTest < Minitest::Test assert_equal [], template.errors end + + def test_break_through_include + assert_template_result "1", "{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}" + assert_template_result "1", "{% for i in (1..3) %}{{ i }}{% include 'break' %}{{ i }}{% endfor %}" + end end # IncludeTagTest diff --git a/test/integration/tags/increment_tag_test.rb b/test/integration/tags/increment_tag_test.rb index 97c51ac..d561a1b 100644 --- a/test/integration/tags/increment_tag_test.rb +++ b/test/integration/tags/increment_tag_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class IncrementTagTest < Minitest::Test @@ -13,11 +15,11 @@ class IncrementTagTest < Minitest::Test end def test_dec - assert_template_result('9', '{%decrement port %}', { 'port' => 10 }) + assert_template_result('9', '{%decrement port %}', 'port' => 10) assert_template_result('-1 -2', '{%decrement port %} {%decrement port%}', {}) assert_template_result('1 5 2 2 5', '{%increment port %} {%increment starboard%} ' \ '{%increment port %} {%decrement port%} ' \ - '{%decrement starboard %}', { 'port' => 1, 'starboard' => 5 }) + '{%decrement starboard %}', 'port' => 1, 'starboard' => 5) end end diff --git a/test/integration/tags/liquid_tag_test.rb b/test/integration/tags/liquid_tag_test.rb new file mode 100644 index 0000000..b5f6b49 --- /dev/null +++ b/test/integration/tags/liquid_tag_test.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'test_helper' + +class LiquidTagTest < Minitest::Test + include Liquid + + def test_liquid_tag + assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3]) + {%- liquid + echo array | join: " " + -%} + LIQUID + + assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3]) + {%- liquid + for value in array + echo value + unless forloop.last + echo " " + endunless + endfor + -%} + LIQUID + + assert_template_result('4 8 12 6', <<~LIQUID, 'array' => [1, 2, 3]) + {%- liquid + for value in array + assign double_value = value | times: 2 + echo double_value | times: 2 + unless forloop.last + echo " " + endunless + endfor + + echo " " + echo double_value + -%} + LIQUID + + assert_template_result('abc', <<~LIQUID) + {%- liquid echo "a" -%} + b + {%- liquid echo "c" -%} + LIQUID + end + + def test_liquid_tag_errors + assert_match_syntax_error("syntax error (line 1): Unknown tag 'error'", <<~LIQUID) + {%- liquid error no such tag -%} + LIQUID + + assert_match_syntax_error("syntax error (line 7): Unknown tag 'error'", <<~LIQUID) + {{ test }} + + {%- + liquid + for value in array + + error no such tag + endfor + -%} + LIQUID + + assert_match_syntax_error("syntax error (line 2): Unknown tag '!!! the guards are vigilant'", <<~LIQUID) + {%- liquid + !!! the guards are vigilant + -%} + LIQUID + + assert_match_syntax_error("syntax error (line 4): 'for' tag was never closed", <<~LIQUID) + {%- liquid + for value in array + echo 'forgot to close the for tag' + -%} + LIQUID + end + + def test_line_number_is_correct_after_a_blank_token + assert_match_syntax_error("syntax error (line 3): Unknown tag 'error'", "{% liquid echo ''\n\n error %}") + assert_match_syntax_error("syntax error (line 3): Unknown tag 'error'", "{% liquid echo ''\n \n error %}") + end + + def test_cannot_open_blocks_living_past_a_liquid_tag + assert_match_syntax_error("syntax error (line 3): 'if' tag was never closed", <<~LIQUID) + {%- liquid + if true + -%} + {%- endif -%} + LIQUID + end + + def test_quirk_can_close_blocks_created_before_a_liquid_tag + assert_template_result("42", <<~LIQUID) + {%- if true -%} + 42 + {%- liquid endif -%} + LIQUID + end + + def test_liquid_tag_in_raw + assert_template_result("{% liquid echo 'test' %}\n", <<~LIQUID) + {% raw %}{% liquid echo 'test' %}{% endraw %} + LIQUID + end +end diff --git a/test/integration/tags/raw_tag_test.rb b/test/integration/tags/raw_tag_test.rb index 634d052..461e5bf 100644 --- a/test/integration/tags/raw_tag_test.rb +++ b/test/integration/tags/raw_tag_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class RawTagTest < Minitest::Test diff --git a/test/integration/tags/render_tag_test.rb b/test/integration/tags/render_tag_test.rb new file mode 100644 index 0000000..87373a2 --- /dev/null +++ b/test/integration/tags/render_tag_test.rb @@ -0,0 +1,168 @@ +# frozen_string_literal: true + +require 'test_helper' + +class RenderTagTest < Minitest::Test + include Liquid + + def test_render_with_no_arguments + Liquid::Template.file_system = StubFileSystem.new('source' => 'rendered content') + assert_template_result 'rendered content', '{% render "source" %}' + end + + def test_render_tag_looks_for_file_system_in_registers_first + file_system = StubFileSystem.new('pick_a_source' => 'from register file system') + assert_equal 'from register file system', + Template.parse('{% render "pick_a_source" %}').render!({}, registers: { file_system: file_system }) + end + + def test_render_passes_named_arguments_into_inner_scope + Liquid::Template.file_system = StubFileSystem.new('product' => '{{ inner_product.title }}') + assert_template_result 'My Product', '{% render "product", inner_product: outer_product %}', + 'outer_product' => { 'title' => 'My Product' } + end + + def test_render_accepts_literals_as_arguments + Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ price }}') + assert_template_result '123', '{% render "snippet", price: 123 %}' + end + + def test_render_accepts_multiple_named_arguments + Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ one }} {{ two }}') + assert_template_result '1 2', '{% render "snippet", one: 1, two: 2 %}' + end + + def test_render_does_not_inherit_parent_scope_variables + Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ outer_variable }}') + assert_template_result '', '{% assign outer_variable = "should not be visible" %}{% render "snippet" %}' + end + + def test_render_does_not_inherit_variable_with_same_name_as_snippet + Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ snippet }}') + assert_template_result '', "{% assign snippet = 'should not be visible' %}{% render 'snippet' %}" + end + + def test_render_sets_the_correct_template_name_for_errors + Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ unsafe }}') + + with_taint_mode :error do + template = Liquid::Template.parse('{% render "snippet", unsafe: unsafe %}') + context = Context.new('unsafe' => (+'unsafe').tap(&:taint)) + template.render(context) + + assert_equal [Liquid::TaintedError], template.errors.map(&:class) + assert_equal 'snippet', template.errors.first.template_name + end + end + + def test_render_sets_the_correct_template_name_for_warnings + Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ unsafe }}') + + with_taint_mode :warn do + template = Liquid::Template.parse('{% render "snippet", unsafe: unsafe %}') + context = Context.new('unsafe' => (+'unsafe').tap(&:taint)) + template.render(context) + + assert_equal [Liquid::TaintedError], context.warnings.map(&:class) + assert_equal 'snippet', context.warnings.first.template_name + end + end + + def test_render_does_not_mutate_parent_scope + Liquid::Template.file_system = StubFileSystem.new('snippet' => '{% assign inner = 1 %}') + assert_template_result '', "{% render 'snippet' %}{{ inner }}" + end + + def test_nested_render_tag + Liquid::Template.file_system = StubFileSystem.new( + 'one' => "one {% render 'two' %}", + 'two' => 'two' + ) + assert_template_result 'one two', "{% render 'one' %}" + end + + def test_recursively_rendered_template_does_not_produce_endless_loop + Liquid::Template.file_system = StubFileSystem.new('loop' => '{% render "loop" %}') + + assert_raises Liquid::StackLevelError do + Template.parse('{% render "loop" %}').render! + end + end + + def test_sub_contexts_count_towards_the_same_recursion_limit + Liquid::Template.file_system = StubFileSystem.new( + 'loop_render' => '{% render "loop_render" %}', + ) + assert_raises Liquid::StackLevelError do + Template.parse('{% render "loop_render" %}').render! + end + end + + def test_dynamically_choosen_templates_are_not_allowed + Liquid::Template.file_system = StubFileSystem.new('snippet' => 'should not be rendered') + + assert_raises Liquid::SyntaxError do + Liquid::Template.parse("{% assign name = 'snippet' %}{% render name %}") + end + end + + def test_include_tag_caches_second_read_of_same_partial + file_system = StubFileSystem.new('snippet' => 'echo') + assert_equal 'echoecho', + Template.parse('{% render "snippet" %}{% render "snippet" %}') + .render!({}, registers: { file_system: file_system }) + assert_equal 1, file_system.file_read_count + end + + def test_render_tag_doesnt_cache_partials_across_renders + file_system = StubFileSystem.new('snippet' => 'my message') + + assert_equal 'my message', + Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system }) + assert_equal 1, file_system.file_read_count + + assert_equal 'my message', + Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system }) + assert_equal 2, file_system.file_read_count + end + + def test_render_tag_within_if_statement + Liquid::Template.file_system = StubFileSystem.new('snippet' => 'my message') + assert_template_result 'my message', '{% if true %}{% render "snippet" %}{% endif %}' + end + + def test_break_through_render + Liquid::Template.file_system = StubFileSystem.new('break' => '{% break %}') + assert_template_result '1', '{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}' + assert_template_result '112233', '{% for i in (1..3) %}{{ i }}{% render "break" %}{{ i }}{% endfor %}' + end + + def test_increment_is_isolated_between_renders + Liquid::Template.file_system = StubFileSystem.new('incr' => '{% increment %}') + assert_template_result '010', '{% increment %}{% increment %}{% render "incr" %}' + end + + def test_decrement_is_isolated_between_renders + Liquid::Template.file_system = StubFileSystem.new('decr' => '{% decrement %}') + assert_template_result '-1-2-1', '{% decrement %}{% decrement %}{% render "decr" %}' + end + + def test_includes_will_not_render_inside_render_tag + Liquid::Template.file_system = StubFileSystem.new( + 'foo' => 'bar', + 'test_include' => '{% include "foo" %}' + ) + + assert_template_result 'include usage is not allowed in this context', '{% render "test_include" %}' + end + + def test_includes_will_not_render_inside_nested_sibling_tags + Liquid::Template.file_system = StubFileSystem.new( + 'foo' => 'bar', + 'nested_render_with_sibling_include' => '{% render "test_include" %}{% include "foo" %}', + 'test_include' => '{% include "foo" %}' + ) + + assert_template_result 'include usage is not allowed in this contextinclude usage is not allowed in this context', '{% render "nested_render_with_sibling_include" %}' + end +end diff --git a/test/integration/tags/standard_tag_test.rb b/test/integration/tags/standard_tag_test.rb index 4b4703a..7939cd3 100644 --- a/test/integration/tags/standard_tag_test.rb +++ b/test/integration/tags/standard_tag_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class StandardTagTest < Minitest::Test @@ -69,7 +71,7 @@ class StandardTagTest < Minitest::Test assert_raises(SyntaxError) do assert_template_result('content foo content foo ', '{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', - { 'var' => 'content' }) + 'var' => 'content') end end @@ -183,32 +185,32 @@ class StandardTagTest < Minitest::Test def test_case_when_or code = '{% case condition %}{% when 1 or 2 or 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' - assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 }) - assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 2 }) - assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 3 }) - assert_template_result(' its 4 ', code, { 'condition' => 4 }) - assert_template_result('', code, { 'condition' => 5 }) + assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 1) + assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 2) + assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 3) + assert_template_result(' its 4 ', code, 'condition' => 4) + assert_template_result('', code, 'condition' => 5) code = '{% case condition %}{% when 1 or "string" or null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' - assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 }) - assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 'string' }) - assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => nil }) - assert_template_result('', code, { 'condition' => 'something else' }) + assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 1) + assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 'string') + assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => nil) + assert_template_result('', code, 'condition' => 'something else') end def test_case_when_comma code = '{% case condition %}{% when 1, 2, 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' - assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 }) - assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 2 }) - assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 3 }) - assert_template_result(' its 4 ', code, { 'condition' => 4 }) - assert_template_result('', code, { 'condition' => 5 }) + assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 1) + assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 2) + assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 3) + assert_template_result(' its 4 ', code, 'condition' => 4) + assert_template_result('', code, 'condition' => 5) code = '{% case condition %}{% when 1, "string", null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' - assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 }) - assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 'string' }) - assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => nil }) - assert_template_result('', code, { 'condition' => 'something else' }) + assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 1) + assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 'string') + assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => nil) + assert_template_result('', code, 'condition' => 'something else') end def test_assign @@ -283,10 +285,10 @@ class StandardTagTest < Minitest::Test end def test_ifchanged - assigns = { 'array' => [ 1, 1, 2, 2, 3, 3] } + assigns = { 'array' => [1, 1, 2, 2, 3, 3] } assert_template_result('123', '{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}', assigns) - assigns = { 'array' => [ 1, 1, 1, 1] } + assigns = { 'array' => [1, 1, 1, 1] } assert_template_result('1', '{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}', assigns) end diff --git a/test/integration/tags/statements_test.rb b/test/integration/tags/statements_test.rb index eeff166..0d024d0 100644 --- a/test/integration/tags/statements_test.rb +++ b/test/integration/tags/statements_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class StatementsTest < Minitest::Test diff --git a/test/integration/tags/table_row_test.rb b/test/integration/tags/table_row_test.rb index d7bc14c..71df4f3 100644 --- a/test/integration/tags/table_row_test.rb +++ b/test/integration/tags/table_row_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class TableRowTest < Minitest::Test diff --git a/test/integration/tags/unless_else_tag_test.rb b/test/integration/tags/unless_else_tag_test.rb index c414a71..469d1c0 100644 --- a/test/integration/tags/unless_else_tag_test.rb +++ b/test/integration/tags/unless_else_tag_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class UnlessElseTagTest < Minitest::Test diff --git a/test/integration/template_test.rb b/test/integration/template_test.rb index d10e1c5..48549f5 100644 --- a/test/integration/template_test.rb +++ b/test/integration/template_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' require 'timeout' @@ -79,7 +81,10 @@ class TemplateTest < Minitest::Test def test_lambda_is_called_once_from_persistent_assigns_over_multiple_parses_and_renders t = Template.new - t.assigns['number'] = -> { @global ||= 0; @global += 1 } + t.assigns['number'] = -> { + @global ||= 0 + @global += 1 + } assert_equal '1', t.parse("{{number}}").render! assert_equal '1', t.parse("{{number}}").render! assert_equal '1', t.render! @@ -88,7 +93,10 @@ class TemplateTest < Minitest::Test def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders t = Template.new - assigns = { 'number' => -> { @global ||= 0; @global += 1 } } + assigns = { 'number' => -> { + @global ||= 0 + @global += 1 + } } assert_equal '1', t.parse("{{number}}").render!(assigns) assert_equal '1', t.parse("{{number}}").render!(assigns) assert_equal '1', t.render!(assigns) @@ -139,6 +147,16 @@ class TemplateTest < Minitest::Test refute_nil t.resource_limits.assign_score end + def test_resource_limits_assign_score_counts_bytes_not_characters + t = Template.parse("{% assign foo = 'すごい' %}") + t.render + assert_equal 9, t.resource_limits.assign_score + + t = Template.parse("{% capture foo %}すごい{% endcapture %}") + t.render + assert_equal 9, t.resource_limits.assign_score + end + def test_resource_limits_assign_score_nested t = Template.parse("{% assign foo = 'aaaa' | reverse %}") @@ -187,6 +205,14 @@ class TemplateTest < Minitest::Test assert_equal "ababab", t.render end + def test_render_length_uses_number_of_bytes_not_characters + t = Template.parse("{% if true %}すごい{% endif %}") + t.resource_limits.render_length_limit = 10 + assert_equal "Liquid error: Memory limits exceeded", t.render + t.resource_limits.render_length_limit = 18 + assert_equal "すごい", t.render + end + def test_default_resource_limits_unaffected_by_render_with_context context = Context.new t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}") @@ -206,7 +232,7 @@ class TemplateTest < Minitest::Test end def test_render_bang_force_rethrow_errors_on_passed_context - context = Context.new({ 'drop' => ErroneousDrop.new }) + context = Context.new('drop' => ErroneousDrop.new) t = Template.new.parse('{{ drop.bad_method }}') e = assert_raises RuntimeError do @@ -217,7 +243,10 @@ class TemplateTest < Minitest::Test def test_exception_renderer_that_returns_string exception = nil - handler = ->(e) { exception = e; '' } + handler = ->(e) { + exception = e + '' + } output = Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: handler) @@ -228,7 +257,10 @@ class TemplateTest < Minitest::Test def test_exception_renderer_that_raises exception = nil assert_raises(Liquid::ZeroDivisionError) do - Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: ->(e) { exception = e; raise }) + Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: ->(e) { + exception = e + raise + }) end assert exception.is_a?(Liquid::ZeroDivisionError) end @@ -249,7 +281,7 @@ class TemplateTest < Minitest::Test def test_undefined_variables t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}") - result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true }) + result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true) assert_equal '33 32 ', result assert_equal 3, t.errors.count @@ -274,14 +306,14 @@ class TemplateTest < Minitest::Test t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}") assert_raises UndefinedVariable do - t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true }) + t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true) end end def test_undefined_drop_methods d = DropWithUndefinedMethod.new t = Template.new.parse('{{ foo }} {{ woot }}') - result = t.render(d, { strict_variables: true }) + result = t.render(d, strict_variables: true) assert_equal 'foo ', result assert_equal 1, t.errors.count @@ -293,7 +325,7 @@ class TemplateTest < Minitest::Test t = Template.new.parse('{{ foo }} {{ woot }}') assert_raises UndefinedDropMethod do - t.render!(d, { strict_variables: true }) + t.render!(d, strict_variables: true) end end @@ -304,7 +336,7 @@ class TemplateTest < Minitest::Test "-#{v}-" end end - result = t.render({ 'a' => 123, 'x' => 'foo' }, { filters: [filters], strict_filters: true }) + result = t.render({ 'a' => 123, 'x' => 'foo' }, filters: [filters], strict_filters: true) assert_equal '123 ', result assert_equal 1, t.errors.count @@ -316,17 +348,17 @@ class TemplateTest < Minitest::Test t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}") assert_raises UndefinedFilter do - t.render!({ 'x' => 'foo' }, { strict_filters: true }) + t.render!({ 'x' => 'foo' }, strict_filters: true) end end def test_using_range_literal_works_as_expected t = Template.parse("{% assign foo = (x..y) %}{{ foo }}") - result = t.render({ 'x' => 1, 'y' => 5 }) + result = t.render('x' => 1, 'y' => 5) assert_equal '1..5', result t = Template.parse("{% assign nums = (x..y) %}{% for num in nums %}{{ num }}{% endfor %}") - result = t.render({ 'x' => 1, 'y' => 5 }) + result = t.render('x' => 1, 'y' => 5) assert_equal '12345', result end end diff --git a/test/integration/trim_mode_test.rb b/test/integration/trim_mode_test.rb index 52248cf..438f86b 100644 --- a/test/integration/trim_mode_test.rb +++ b/test/integration/trim_mode_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class TrimModeTest < Minitest::Test @@ -76,14 +78,14 @@ class TrimModeTest < Minitest::Test

END_TEMPLATE - expected = <<-END_EXPECTED -
-

-#{whitespace} - yes -#{whitespace} -

-
+ expected = <<~END_EXPECTED +
+

+ #{whitespace} + yes + #{whitespace} +

+
END_EXPECTED assert_template_result(expected, text) @@ -96,12 +98,12 @@ class TrimModeTest < Minitest::Test

END_TEMPLATE - expected = <<-END_EXPECTED -
-

-#{whitespace} -

-
+ expected = <<~END_EXPECTED +
+

+ #{whitespace} +

+
END_EXPECTED assert_template_result(expected, text) end @@ -337,12 +339,12 @@ class TrimModeTest < Minitest::Test

END_TEMPLATE - expected = <<-END_EXPECTED -
-

-#{whitespace} -

-
+ expected = <<~END_EXPECTED +
+

+ #{whitespace} +

+
END_EXPECTED assert_template_result(expected, text) end @@ -513,16 +515,16 @@ class TrimModeTest < Minitest::Test {% endraw %} END_TEMPLATE - expected = <<-END_EXPECTED -
-#{whitespace} - {%- if true -%} -

- {{- 'John' -}} -

- {%- endif -%} -#{whitespace} -
+ expected = <<~END_EXPECTED +
+ #{whitespace} + {%- if true -%} +

+ {{- 'John' -}} +

+ {%- endif -%} + #{whitespace} +
END_EXPECTED assert_template_result(expected, text) end diff --git a/test/integration/variable_test.rb b/test/integration/variable_test.rb index abd6e70..94ed1ec 100644 --- a/test/integration/variable_test.rb +++ b/test/integration/variable_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class VariableTest < Minitest::Test @@ -76,7 +78,7 @@ class VariableTest < Minitest::Test def test_hash_with_default_proc template = Template.parse(%(Hello {{ test }})) - assigns = Hash.new { |h, k| raise "Unknown variable '#{k}'" } + assigns = Hash.new { |_h, k| raise "Unknown variable '#{k}'" } assigns['test'] = 'Tobi' assert_equal 'Hello Tobi', template.render!(assigns) assigns.delete('test') diff --git a/test/test_helper.rb b/test/test_helper.rb old mode 100644 new mode 100755 index ac5ab53..9606ef8 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true ENV["MT_NO_EXPECTATIONS"] = "1" require 'minitest/autorun' @@ -8,13 +9,13 @@ require 'liquid.rb' require 'liquid/profiler' mode = :strict -if env_mode = ENV['LIQUID_PARSER_MODE'] +if (env_mode = ENV['LIQUID_PARSER_MODE']) puts "-- #{env_mode.upcase} ERROR MODE" mode = env_mode.to_sym end Liquid::Template.error_mode = mode -if ENV['LIQUID-C'] == '1' +if ENV['LIQUID_C'] == '1' puts "-- LIQUID C" require 'liquid/c' end @@ -37,18 +38,18 @@ module Minitest include Liquid def assert_template_result(expected, template, assigns = {}, message = nil) - assert_equal expected, Template.parse(template).render!(assigns), message + assert_equal expected, Template.parse(template, line_numbers: true).render!(assigns), message end def assert_template_result_matches(expected, template, assigns = {}, message = nil) - return assert_template_result(expected, template, assigns, message) unless expected.is_a? Regexp + return assert_template_result(expected, template, assigns, message) unless expected.is_a?(Regexp) - assert_match expected, Template.parse(template).render!(assigns), message + assert_match expected, Template.parse(template, line_numbers: true).render!(assigns), message end def assert_match_syntax_error(match, template, assigns = {}) exception = assert_raises(Liquid::SyntaxError) do - Template.parse(template).render(assigns) + Template.parse(template, line_numbers: true).render(assigns) end assert_match match, exception.message end @@ -84,6 +85,13 @@ module Minitest ensure Liquid::Template.error_mode = old_mode end + + def with_custom_tag(tag_name, tag_class) + Liquid::Template.register_tag(tag_name, tag_class) + yield + ensure + Liquid::Template.tags.delete(tag_name) + end end end @@ -114,3 +122,17 @@ class ErrorDrop < Liquid::Drop raise Exception, 'exception' end end + +class StubFileSystem + attr_reader :file_read_count + + def initialize(values) + @file_read_count = 0 + @values = values + end + + def read_template_file(template_path) + @file_read_count += 1 + @values.fetch(template_path) + end +end diff --git a/test/unit/block_unit_test.rb b/test/unit/block_unit_test.rb index 6a27a7d..fa06a87 100644 --- a/test/unit/block_unit_test.rb +++ b/test/unit/block_unit_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class BlockUnitTest < Minitest::Test @@ -44,10 +46,47 @@ class BlockUnitTest < Minitest::Test end def test_with_custom_tag - Liquid::Template.register_tag("testtag", Block) - assert Liquid::Template.parse("{% testtag %} {% endtesttag %}") - ensure - Liquid::Template.tags.delete('testtag') + with_custom_tag('testtag', Block) do + assert Liquid::Template.parse("{% testtag %} {% endtesttag %}") + end + end + + def test_custom_block_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility + klass1 = Class.new(Block) do + def render(*) + 'hello' + end + end + + with_custom_tag('blabla', klass1) do + template = Liquid::Template.parse("{% blabla %} bla {% endblabla %}") + + assert_equal 'hello', template.render + + buf = +'' + output = template.render({}, output: buf) + assert_equal 'hello', output + assert_equal 'hello', buf + assert_equal buf.object_id, output.object_id + end + + klass2 = Class.new(klass1) do + def render(*) + 'foo' + super + 'bar' + end + end + + with_custom_tag('blabla', klass2) do + template = Liquid::Template.parse("{% blabla %} foo {% endblabla %}") + + assert_equal 'foohellobar', template.render + + buf = +'' + output = template.render({}, output: buf) + assert_equal 'foohellobar', output + assert_equal 'foohellobar', buf + assert_equal buf.object_id, output.object_id + end end private diff --git a/test/unit/condition_unit_test.rb b/test/unit/condition_unit_test.rb index 5afa2b7..8d4e02f 100644 --- a/test/unit/condition_unit_test.rb +++ b/test/unit/condition_unit_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class ConditionUnitTest < Minitest::Test @@ -24,9 +26,9 @@ class ConditionUnitTest < Minitest::Test assert_evaluates_true 1, '<=', 1 # negative numbers assert_evaluates_true 1, '>', -1 - assert_evaluates_true (-1), '<', 1 + assert_evaluates_true(-1, '<', 1) assert_evaluates_true 1.0, '>', -1.0 - assert_evaluates_true (-1.0), '<', 1.0 + assert_evaluates_true(-1.0, '<', 1.0) end def test_default_operators_evalute_false @@ -68,7 +70,7 @@ class ConditionUnitTest < Minitest::Test assert_nil Condition.new({}, '>', 2).evaluate assert_nil Condition.new(2, '>', {}).evaluate assert_equal false, Condition.new({}, '==', 2).evaluate - assert_equal true, Condition.new({ 'a' => 1 }, '==', { 'a' => 1 }).evaluate + assert_equal true, Condition.new({ 'a' => 1 }, '==', 'a' => 1).evaluate assert_equal true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate end @@ -107,11 +109,11 @@ class ConditionUnitTest < Minitest::Test assert_equal false, condition.evaluate - condition.or Condition.new(2, '==', 1) + condition.or(Condition.new(2, '==', 1)) assert_equal false, condition.evaluate - condition.or Condition.new(1, '==', 1) + condition.or(Condition.new(1, '==', 1)) assert_equal true, condition.evaluate end @@ -121,22 +123,22 @@ class ConditionUnitTest < Minitest::Test assert_equal true, condition.evaluate - condition.and Condition.new(2, '==', 2) + condition.and(Condition.new(2, '==', 2)) assert_equal true, condition.evaluate - condition.and Condition.new(2, '==', 1) + condition.and(Condition.new(2, '==', 1)) assert_equal false, condition.evaluate end def test_should_allow_custom_proc_operator - Condition.operators['starts_with'] = proc { |cond, left, right| left =~ %r{^#{right}} } + Condition.operators['starts_with'] = proc { |_cond, left, right| left =~ /^#{right}/ } assert_evaluates_true 'bob', 'starts_with', 'b' assert_evaluates_false 'bob', 'starts_with', 'o' ensure - Condition.operators.delete 'starts_with' + Condition.operators.delete('starts_with') end def test_left_or_right_may_contain_operators diff --git a/test/unit/context_unit_test.rb b/test/unit/context_unit_test.rb index fab19b8..3b460d7 100644 --- a/test/unit/context_unit_test.rb +++ b/test/unit/context_unit_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class HundredCentes @@ -83,7 +85,7 @@ class ContextUnitTest < Minitest::Test @context['date'] = Date.today assert_equal Date.today, @context['date'] - now = DateTime.now + now = Time.now @context['datetime'] = now assert_equal now, @context['datetime'] @@ -206,9 +208,9 @@ class ContextUnitTest < Minitest::Test end def test_merge - @context.merge({ "test" => "test" }) + @context.merge("test" => "test") assert_equal 'test', @context['test'] - @context.merge({ "test" => "newvalue", "foo" => "bar" }) + @context.merge("test" => "newvalue", "foo" => "bar") assert_equal 'newvalue', @context['test'] assert_equal 'bar', @context['foo'] end @@ -235,10 +237,10 @@ class ContextUnitTest < Minitest::Test def test_hash_to_array_transition @context['colors'] = { - 'Blue' => ['003366', '336699', '6699CC', '99CCFF'], - 'Green' => ['003300', '336633', '669966', '99CC99'], - 'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'], - 'Red' => ['660000', '993333', 'CC6666', 'FF9999'] + 'Blue' => ['003366', '336699', '6699CC', '99CCFF'], + 'Green' => ['003300', '336633', '669966', '99CC99'], + 'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'], + 'Red' => ['660000', '993333', 'CC6666', 'FF9999'], } assert_equal '003366', @context['colors.Blue[0]'] @@ -263,7 +265,7 @@ class ContextUnitTest < Minitest::Test def test_access_hashes_with_hash_notation @context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } - @context['product'] = { 'variants' => [ { 'title' => 'draft151cm' }, { 'title' => 'element151cm' } ] } + @context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } assert_equal 5, @context['products["count"]'] assert_equal 'deepsnow', @context['products["tags"][0]'] @@ -301,7 +303,7 @@ class ContextUnitTest < Minitest::Test end def test_first_can_appear_in_middle_of_callchain - @context['product'] = { 'variants' => [ { 'title' => 'draft151cm' }, { 'title' => 'element151cm' } ] } + @context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } assert_equal 'draft151cm', @context['product.variants[0].title'] assert_equal 'element151cm', @context['product.variants[1].title'] @@ -403,7 +405,11 @@ class ContextUnitTest < Minitest::Test end def test_lambda_is_called_once - @context['callcount'] = proc { @global ||= 0; @global += 1; @global.to_s } + @context['callcount'] = proc { + @global ||= 0 + @global += 1 + @global.to_s + } assert_equal '1', @context['callcount'] assert_equal '1', @context['callcount'] @@ -413,7 +419,11 @@ class ContextUnitTest < Minitest::Test end def test_nested_lambda_is_called_once - @context['callcount'] = { "lambda" => proc { @global ||= 0; @global += 1; @global.to_s } } + @context['callcount'] = { "lambda" => proc { + @global ||= 0 + @global += 1 + @global.to_s + } } assert_equal '1', @context['callcount.lambda'] assert_equal '1', @context['callcount.lambda'] @@ -423,7 +433,11 @@ class ContextUnitTest < Minitest::Test end def test_lambda_in_array_is_called_once - @context['callcount'] = [1, 2, proc { @global ||= 0; @global += 1; @global.to_s }, 4, 5] + @context['callcount'] = [1, 2, proc { + @global ||= 0 + @global += 1 + @global.to_s + }, 4, 5] assert_equal '1', @context['callcount[2]'] assert_equal '1', @context['callcount[2]'] @@ -453,7 +467,7 @@ class ContextUnitTest < Minitest::Test end def test_context_initialization_with_a_proc_in_environment - contx = Context.new([test: ->(c) { c['poutine'] }], { test: :foo }) + contx = Context.new([test: ->(c) { c['poutine'] }], test: :foo) assert contx assert_nil contx['poutine'] @@ -468,11 +482,87 @@ class ContextUnitTest < Minitest::Test assert_equal 'hi filtered', context.apply_global_filter('hi') end + def test_static_environments_are_read_with_lower_priority_than_environments + context = Context.build( + static_environments: { 'shadowed' => 'static', 'unshadowed' => 'static' }, + environments: { 'shadowed' => 'dynamic' } + ) + + assert_equal 'dynamic', context['shadowed'] + assert_equal 'static', context['unshadowed'] + end + def test_apply_global_filter_when_no_global_filter_exist context = Context.new assert_equal 'hi', context.apply_global_filter('hi') end + def test_new_isolated_subcontext_does_not_inherit_variables + super_context = Context.new + super_context['my_variable'] = 'some value' + subcontext = super_context.new_isolated_subcontext + + assert_nil subcontext['my_variable'] + end + + def test_new_isolated_subcontext_inherits_static_environment + super_context = Context.build(static_environments: { 'my_environment_value' => 'my value' }) + subcontext = super_context.new_isolated_subcontext + + assert_equal 'my value', subcontext['my_environment_value'] + end + + def test_new_isolated_subcontext_inherits_resource_limits + resource_limits = ResourceLimits.new({}) + super_context = Context.new({}, {}, {}, false, resource_limits) + subcontext = super_context.new_isolated_subcontext + assert_equal resource_limits, subcontext.resource_limits + end + + def test_new_isolated_subcontext_inherits_exception_renderer + super_context = Context.new + super_context.exception_renderer = ->(_e) { 'my exception message' } + subcontext = super_context.new_isolated_subcontext + assert_equal 'my exception message', subcontext.handle_error(Liquid::Error.new) + end + + def test_new_isolated_subcontext_does_not_inherit_non_static_registers + registers = { + my_register: :my_value, + } + super_context = Context.new({}, {}, StaticRegisters.new(registers)) + super_context.registers[:my_register] = :my_alt_value + subcontext = super_context.new_isolated_subcontext + assert_equal :my_value, subcontext.registers[:my_register] + end + + def test_new_isolated_subcontext_inherits_static_registers + super_context = Context.build(registers: { my_register: :my_value }) + subcontext = super_context.new_isolated_subcontext + assert_equal :my_value, subcontext.registers[:my_register] + end + + def test_new_isolated_subcontext_registers_do_not_pollute_context + super_context = Context.build(registers: { my_register: :my_value }) + subcontext = super_context.new_isolated_subcontext + subcontext.registers[:my_register] = :my_alt_value + assert_equal :my_value, super_context.registers[:my_register] + end + + def test_new_isolated_subcontext_inherits_filters + my_filter = Module.new do + def my_filter(*) + 'my filter result' + end + end + + super_context = Context.new + super_context.add_filters([my_filter]) + subcontext = super_context.new_isolated_subcontext + template = Template.parse('{{ 123 | my_filter }}') + assert_equal 'my filter result', template.render(subcontext) + end + private def assert_no_object_allocations diff --git a/test/unit/file_system_unit_test.rb b/test/unit/file_system_unit_test.rb index 2c7250b..c76a7ed 100644 --- a/test/unit/file_system_unit_test.rb +++ b/test/unit/file_system_unit_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class FileSystemUnitTest < Minitest::Test diff --git a/test/unit/i18n_unit_test.rb b/test/unit/i18n_unit_test.rb index b57500e..338787e 100644 --- a/test/unit/i18n_unit_test.rb +++ b/test/unit/i18n_unit_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class I18nUnitTest < Minitest::Test diff --git a/test/unit/lexer_unit_test.rb b/test/unit/lexer_unit_test.rb index 5adcf2b..7a2a4a5 100644 --- a/test/unit/lexer_unit_test.rb +++ b/test/unit/lexer_unit_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class LexerUnitTest < Minitest::Test diff --git a/test/unit/parser_unit_test.rb b/test/unit/parser_unit_test.rb index 9f23337..7456bf3 100644 --- a/test/unit/parser_unit_test.rb +++ b/test/unit/parser_unit_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class ParserUnitTest < Minitest::Test diff --git a/test/unit/partial_cache_unit_test.rb b/test/unit/partial_cache_unit_test.rb new file mode 100644 index 0000000..dd43185 --- /dev/null +++ b/test/unit/partial_cache_unit_test.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'test_helper' + +class PartialCacheUnitTest < Minitest::Test + def test_uses_the_file_system_register_if_present + context = Liquid::Context.build( + registers: { + file_system: StubFileSystem.new('my_partial' => 'my partial body'), + } + ) + + partial = Liquid::PartialCache.load( + 'my_partial', + context: context, + parse_context: Liquid::ParseContext.new + ) + + assert_equal 'my partial body', partial.render + end + + def test_reads_from_the_file_system_only_once_per_file + file_system = StubFileSystem.new('my_partial' => 'some partial body') + context = Liquid::Context.build( + registers: { file_system: file_system } + ) + + 2.times do + Liquid::PartialCache.load( + 'my_partial', + context: context, + parse_context: Liquid::ParseContext.new + ) + end + + assert_equal 1, file_system.file_read_count + end + + def test_cache_state_is_stored_per_context + parse_context = Liquid::ParseContext.new + shared_file_system = StubFileSystem.new( + 'my_partial' => 'my shared value' + ) + context_one = Liquid::Context.build( + registers: { + file_system: shared_file_system, + } + ) + context_two = Liquid::Context.build( + registers: { + file_system: shared_file_system, + } + ) + + 2.times do + Liquid::PartialCache.load( + 'my_partial', + context: context_one, + parse_context: parse_context + ) + end + + Liquid::PartialCache.load( + 'my_partial', + context: context_two, + parse_context: parse_context + ) + + assert_equal 2, shared_file_system.file_read_count + end + + def test_cache_is_not_broken_when_a_different_parse_context_is_used + file_system = StubFileSystem.new('my_partial' => 'some partial body') + context = Liquid::Context.build( + registers: { file_system: file_system } + ) + + Liquid::PartialCache.load( + 'my_partial', + context: context, + parse_context: Liquid::ParseContext.new(my_key: 'value one') + ) + Liquid::PartialCache.load( + 'my_partial', + context: context, + parse_context: Liquid::ParseContext.new(my_key: 'value two') + ) + + # Technically what we care about is that the file was parsed twice, + # but measuring file reads is an OK proxy for this. + assert_equal 1, file_system.file_read_count + end +end diff --git a/test/unit/regexp_unit_test.rb b/test/unit/regexp_unit_test.rb index 0821229..666bc66 100644 --- a/test/unit/regexp_unit_test.rb +++ b/test/unit/regexp_unit_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class RegexpUnitTest < Minitest::Test diff --git a/test/unit/registers/disabled_tags_unit_test.rb b/test/unit/registers/disabled_tags_unit_test.rb new file mode 100644 index 0000000..90ac016 --- /dev/null +++ b/test/unit/registers/disabled_tags_unit_test.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'test_helper' + +class DisabledTagsUnitTest < Minitest::Test + include Liquid + + def test_disables_tag_specified + register = DisabledTags.new + register.disable(%w(foo bar)) do + assert_equal true, register.disabled?("foo") + assert_equal true, register.disabled?("bar") + assert_equal false, register.disabled?("unknown") + end + end + + def test_disables_nested_tags + register = DisabledTags.new + register.disable(["foo"]) do + register.disable(["foo"]) do + assert_equal true, register.disabled?("foo") + assert_equal false, register.disabled?("bar") + end + register.disable(["bar"]) do + assert_equal true, register.disabled?("foo") + assert_equal true, register.disabled?("bar") + register.disable(["foo"]) do + assert_equal true, register.disabled?("foo") + assert_equal true, register.disabled?("bar") + end + end + assert_equal true, register.disabled?("foo") + assert_equal false, register.disabled?("bar") + end + end +end diff --git a/test/unit/static_registers_unit_test.rb b/test/unit/static_registers_unit_test.rb new file mode 100644 index 0000000..125440f --- /dev/null +++ b/test/unit/static_registers_unit_test.rb @@ -0,0 +1,248 @@ +# frozen_string_literal: true + +require 'test_helper' + +class StaticRegistersUnitTest < Minitest::Test + include Liquid + + def set + static_register = StaticRegisters.new + static_register[nil] = true + static_register[1] = :one + static_register[:one] = "one" + static_register["two"] = "three" + static_register["two"] = 3 + static_register[false] = nil + + assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil }, static_register.registers) + + static_register + end + + def test_get + static_register = set + + assert_equal true, static_register[nil] + assert_equal :one, static_register[1] + assert_equal "one", static_register[:one] + assert_equal 3, static_register["two"] + assert_nil static_register[false] + assert_nil static_register["unknown"] + end + + def test_delete + static_register = set + + assert_equal true, static_register.delete(nil) + assert_equal :one, static_register.delete(1) + assert_equal "one", static_register.delete(:one) + assert_equal 3, static_register.delete("two") + assert_nil static_register.delete(false) + assert_nil static_register.delete("unknown") + + assert_equal({}, static_register.registers) + end + + def test_fetch + static_register = set + + assert_equal true, static_register.fetch(nil) + assert_equal :one, static_register.fetch(1) + assert_equal "one", static_register.fetch(:one) + assert_equal 3, static_register.fetch("two") + assert_nil static_register.fetch(false) + assert_nil static_register.fetch("unknown") + end + + def test_fetch_default + static_register = StaticRegisters.new + + assert_equal true, static_register.fetch(nil, true) + assert_equal :one, static_register.fetch(1, :one) + assert_equal "one", static_register.fetch(:one, "one") + assert_equal 3, static_register.fetch("two", 3) + assert_nil static_register.fetch(false, nil) + end + + def test_key + static_register = set + + assert_equal true, static_register.key?(nil) + assert_equal true, static_register.key?(1) + assert_equal true, static_register.key?(:one) + assert_equal true, static_register.key?("two") + assert_equal true, static_register.key?(false) + assert_equal false, static_register.key?("unknown") + assert_equal false, static_register.key?(true) + end + + def set_with_static + static_register = StaticRegisters.new(nil => true, 1 => :one, :one => "one", "two" => 3, false => nil) + static_register[nil] = false + static_register["two"] = 4 + static_register[true] = "foo" + + assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil }, static_register.static) + assert_equal({ nil => false, "two" => 4, true => "foo" }, static_register.registers) + + static_register + end + + def test_get_with_static + static_register = set_with_static + + assert_equal false, static_register[nil] + assert_equal :one, static_register[1] + assert_equal "one", static_register[:one] + assert_equal 4, static_register["two"] + assert_equal "foo", static_register[true] + assert_nil static_register[false] + end + + def test_delete_with_static + static_register = set_with_static + + assert_equal false, static_register.delete(nil) + assert_equal 4, static_register.delete("two") + assert_equal "foo", static_register.delete(true) + assert_nil static_register.delete("unknown") + assert_nil static_register.delete(:one) + + assert_equal({}, static_register.registers) + assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil }, static_register.static) + end + + def test_fetch_with_static + static_register = set_with_static + + assert_equal false, static_register.fetch(nil) + assert_equal :one, static_register.fetch(1) + assert_equal "one", static_register.fetch(:one) + assert_equal 4, static_register.fetch("two") + assert_equal "foo", static_register.fetch(true) + assert_nil static_register.fetch(false) + end + + def test_key_with_static + static_register = set_with_static + + assert_equal true, static_register.key?(nil) + assert_equal true, static_register.key?(1) + assert_equal true, static_register.key?(:one) + assert_equal true, static_register.key?("two") + assert_equal true, static_register.key?(false) + assert_equal false, static_register.key?("unknown") + assert_equal true, static_register.key?(true) + end + + def test_static_register_can_be_frozen + static_register = set_with_static + + static = static_register.static.freeze + + assert_raises(RuntimeError) do + static["two"] = "foo" + end + + assert_raises(RuntimeError) do + static["unknown"] = "foo" + end + + assert_raises(RuntimeError) do + static.delete("two") + end + end + + def test_new_static_retains_static + static_register = StaticRegisters.new(nil => true, 1 => :one, :one => "one", "two" => 3, false => nil) + static_register["one"] = 1 + static_register["two"] = 2 + static_register["three"] = 3 + + new_register = StaticRegisters.new(static_register) + assert_equal({}, new_register.registers) + + new_register["one"] = 4 + new_register["two"] = 5 + new_register["three"] = 6 + + newest_register = StaticRegisters.new(new_register) + assert_equal({}, newest_register.registers) + + newest_register["one"] = 7 + newest_register["two"] = 8 + newest_register["three"] = 9 + + assert_equal({ "one" => 1, "two" => 2, "three" => 3 }, static_register.registers) + assert_equal({ "one" => 4, "two" => 5, "three" => 6 }, new_register.registers) + assert_equal({ "one" => 7, "two" => 8, "three" => 9 }, newest_register.registers) + assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil }, static_register.static) + assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil }, new_register.static) + assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil }, newest_register.static) + end + + def test_multiple_instances_are_unique + static_register = StaticRegisters.new(nil => true, 1 => :one, :one => "one", "two" => 3, false => nil) + static_register["one"] = 1 + static_register["two"] = 2 + static_register["three"] = 3 + + new_register = StaticRegisters.new(foo: :bar) + assert_equal({}, new_register.registers) + + new_register["one"] = 4 + new_register["two"] = 5 + new_register["three"] = 6 + + newest_register = StaticRegisters.new(bar: :foo) + assert_equal({}, newest_register.registers) + + newest_register["one"] = 7 + newest_register["two"] = 8 + newest_register["three"] = 9 + + assert_equal({ "one" => 1, "two" => 2, "three" => 3 }, static_register.registers) + assert_equal({ "one" => 4, "two" => 5, "three" => 6 }, new_register.registers) + assert_equal({ "one" => 7, "two" => 8, "three" => 9 }, newest_register.registers) + assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil }, static_register.static) + assert_equal({ foo: :bar }, new_register.static) + assert_equal({ bar: :foo }, newest_register.static) + end + + def test_can_update_static_directly_and_updates_all_instances + static_register = StaticRegisters.new(nil => true, 1 => :one, :one => "one", "two" => 3, false => nil) + static_register["one"] = 1 + static_register["two"] = 2 + static_register["three"] = 3 + + new_register = StaticRegisters.new(static_register) + assert_equal({}, new_register.registers) + + assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil }, static_register.static) + + new_register["one"] = 4 + new_register["two"] = 5 + new_register["three"] = 6 + new_register.static["four"] = 10 + + newest_register = StaticRegisters.new(new_register) + assert_equal({}, newest_register.registers) + + assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil, "four" => 10 }, new_register.static) + + newest_register["one"] = 7 + newest_register["two"] = 8 + newest_register["three"] = 9 + new_register.static["four"] = 5 + new_register.static["five"] = 15 + + assert_equal({ "one" => 1, "two" => 2, "three" => 3 }, static_register.registers) + assert_equal({ "one" => 4, "two" => 5, "three" => 6 }, new_register.registers) + assert_equal({ "one" => 7, "two" => 8, "three" => 9 }, newest_register.registers) + + assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil, "four" => 5, "five" => 15 }, newest_register.static) + + assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil, "four" => 5, "five" => 15 }, static_register.static) + assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil, "four" => 5, "five" => 15 }, new_register.static) + end +end diff --git a/test/unit/strainer_unit_test.rb b/test/unit/strainer_unit_test.rb index 5ce2100..2fb9ad4 100644 --- a/test/unit/strainer_unit_test.rb +++ b/test/unit/strainer_unit_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class StrainerUnitTest < Minitest::Test @@ -36,7 +38,8 @@ class StrainerUnitTest < Minitest::Test rescue Liquid::ArgumentError => e assert_match( /\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/, - e.message) + e.message + ) assert_equal e.backtrace[0].split(':')[0], __FILE__ end end @@ -135,7 +138,7 @@ class StrainerUnitTest < Minitest::Test end module LateAddedFilter - def late_added_filter(input) + def late_added_filter(_input) "filtered" end end @@ -150,7 +153,7 @@ class StrainerUnitTest < Minitest::Test mod = Module.new do class << self attr_accessor :include_count - def included(mod) + def included(_mod) self.include_count += 1 end end diff --git a/test/unit/tag_unit_test.rb b/test/unit/tag_unit_test.rb index c4b901b..c9543e9 100644 --- a/test/unit/tag_unit_test.rb +++ b/test/unit/tag_unit_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class TagUnitTest < Minitest::Test @@ -18,4 +20,42 @@ class TagUnitTest < Minitest::Test tag = Tag.parse("some_tag", "", Tokenizer.new(""), ParseContext.new) assert_equal 'some_tag', tag.tag_name end + + def test_custom_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility + klass1 = Class.new(Tag) do + def render(*) + 'hello' + end + end + + with_custom_tag('blabla', klass1) do + template = Liquid::Template.parse("{% blabla %}") + + assert_equal 'hello', template.render + + buf = +'' + output = template.render({}, output: buf) + assert_equal 'hello', output + assert_equal 'hello', buf + assert_equal buf.object_id, output.object_id + end + + klass2 = Class.new(klass1) do + def render(*) + 'foo' + super + 'bar' + end + end + + with_custom_tag('blabla', klass2) do + template = Liquid::Template.parse("{% blabla %}") + + assert_equal 'foohellobar', template.render + + buf = +'' + output = template.render({}, output: buf) + assert_equal 'foohellobar', output + assert_equal 'foohellobar', buf + assert_equal buf.object_id, output.object_id + end + end end diff --git a/test/unit/tags/case_tag_unit_test.rb b/test/unit/tags/case_tag_unit_test.rb index 7110308..0f3a61f 100644 --- a/test/unit/tags/case_tag_unit_test.rb +++ b/test/unit/tags/case_tag_unit_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class CaseTagUnitTest < Minitest::Test diff --git a/test/unit/tags/for_tag_unit_test.rb b/test/unit/tags/for_tag_unit_test.rb index b8fc520..e6306c3 100644 --- a/test/unit/tags/for_tag_unit_test.rb +++ b/test/unit/tags/for_tag_unit_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class ForTagUnitTest < Minitest::Test diff --git a/test/unit/tags/if_tag_unit_test.rb b/test/unit/tags/if_tag_unit_test.rb index 7ecfc40..32243b7 100644 --- a/test/unit/tags/if_tag_unit_test.rb +++ b/test/unit/tags/if_tag_unit_test.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + require 'test_helper' class IfTagUnitTest < Minitest::Test def test_if_nodelist template = Liquid::Template.parse('{% if true %}IF{% else %}ELSE{% endif %}') - assert_equal ['IF', 'ELSE'], template.root.nodelist[0].nodelist.map(&:nodelist).flatten + assert_equal(['IF', 'ELSE'], template.root.nodelist[0].nodelist.map(&:nodelist).flatten) end end diff --git a/test/unit/template_unit_test.rb b/test/unit/template_unit_test.rb index 6328be5..bc02896 100644 --- a/test/unit/template_unit_test.rb +++ b/test/unit/template_unit_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class TemplateUnitTest < Minitest::Test diff --git a/test/unit/tokenizer_unit_test.rb b/test/unit/tokenizer_unit_test.rb index de84c1f..44342d6 100644 --- a/test/unit/tokenizer_unit_test.rb +++ b/test/unit/tokenizer_unit_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class TokenizerTest < Minitest::Test @@ -33,7 +35,7 @@ class TokenizerTest < Minitest::Test def tokenize(source) tokenizer = Liquid::Tokenizer.new(source) tokens = [] - while t = tokenizer.shift + while (t = tokenizer.shift) tokens << t end tokens diff --git a/test/unit/variable_unit_test.rb b/test/unit/variable_unit_test.rb index 5a21ace..da1d4ea 100644 --- a/test/unit/variable_unit_test.rb +++ b/test/unit/variable_unit_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class VariableUnitTest < Minitest::Test