diff --git a/.djlintrc b/.djlintrc new file mode 100644 index 000000000..6e3911be5 --- /dev/null +++ b/.djlintrc @@ -0,0 +1,14 @@ +{ + "profile": "golang", + "indent": 2, + "max_line_length": 120, + "format_css": true, + "format_js": true, + "exclude": "node_modules,public,resources,static,.git", + "ignore": "H006,H013,H016,H021,H030,H031,T001,T002", + "custom_blocks": "define,block,range,with,if,else,end", + "linter_output_format": "### {message}\n{filename}, line {line}, code {code} \n```html\n{match}\n```", + "require_pragma": false, + "preserve_blank_lines": true, + "max_blank_lines": 2 +} diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml new file mode 100644 index 000000000..9ee4dae40 --- /dev/null +++ b/.github/workflows/analysis.yml @@ -0,0 +1,44 @@ +name: Hugo Static Analysis + +on: push + +permissions: + contents: read + pull-requests: write + checks: write + +jobs: + template-lint: + name: Lint Hugo Templates + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.13' + + - name: Install djLint + run: pip install djlint + + - name: Lint layouts + run: | + echo "## djLint" >> report.txt + djlint layouts/ >> report.txt || true + + - name: Install Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4.1' + + - name: Complexity + run: | + ruby bin/hugolint.rb >> report.txt || true + + - name: Comment pull request + uses: mshick/add-pr-comment@v2 + with: + message-path: | + report.txt \ No newline at end of file diff --git a/.hugolint b/.hugolint new file mode 100644 index 000000000..72aefe962 --- /dev/null +++ b/.hugolint @@ -0,0 +1,12 @@ +engines: + directories: + exclude: + - partials/blocks/templates + complexity: + exclude: + lines: + exclude: + calls: + exclude: + - partials/projects/single/sidebar.html + - partials/blocks/templates/categories.html diff --git a/bin/hugolint.rb b/bin/hugolint.rb new file mode 100644 index 000000000..b17729295 --- /dev/null +++ b/bin/hugolint.rb @@ -0,0 +1,2 @@ +require_relative 'hugolint/analyzer' +puts Hugolint::Analyzer.run diff --git a/bin/hugolint/analyzer.rb b/bin/hugolint/analyzer.rb new file mode 100644 index 000000000..8b586ea09 --- /dev/null +++ b/bin/hugolint/analyzer.rb @@ -0,0 +1,41 @@ +require_relative 'engines/base' +require_relative 'engines/complexity' +require_relative 'engines/directories' +require_relative 'engines/lines' +require_relative 'engines/calls' +require_relative 'file' +require_relative 'utils' +require 'yaml' + +module Hugolint + class Analyzer + + LAYOUTS = './layouts/**/*' + CONFIG = '.hugolint' + + def self.run + new.to_s + end + + def to_s + message = "## Hugo analyzer\n" + message += Engines::Directories.new(self).to_s + message += Engines::Calls.new(self).to_s + message += Engines::Lines.new(self).to_s + message += Engines::Complexity.new(self).to_s + message + end + + def paths + @paths ||= Dir.glob(LAYOUTS) + end + + def files + @files ||= paths.map { |path| Hugolint::File.new(path) } + end + + def config + @config ||= YAML.load_file(CONFIG) + end + end +end diff --git a/bin/hugolint/engines/base.rb b/bin/hugolint/engines/base.rb new file mode 100644 index 000000000..1d22a2af8 --- /dev/null +++ b/bin/hugolint/engines/base.rb @@ -0,0 +1,54 @@ +module Hugolint + module Engines + class Base + attr_reader :analyzer + + ICON_DANGER = '❌' + ICON_WARNING = '⚠️' + ICON_OK = '✅' + + def initialize(analyzer) + @analyzer = analyzer + end + + def analyzed_files + unless @analyzed_files + @analyzed_files = [] + analyzer.files.each do |file| + next unless should_analyze?(file) + @analyzed_files << analyze(file) + end + sort! + end + @analyzed_files + end + + def to_s + end + + protected + + def should_analyze?(file) + !files_excluded.include?(file.short_path) + end + + def analyze(file) + end + + def sort! + end + + def engine_identifier + self.class.name.split('::').last.downcase + end + + def engine_config + @engine_config ||= analyzer.config.dig('engines', engine_identifier) + end + + def files_excluded + @files_excluded ||= engine_config&.dig('exclude') || [] + end + end + end +end diff --git a/bin/hugolint/engines/calls.rb b/bin/hugolint/engines/calls.rb new file mode 100644 index 000000000..1cade8d6f --- /dev/null +++ b/bin/hugolint/engines/calls.rb @@ -0,0 +1,62 @@ +module Hugolint + module Engines + class Calls < Base + + ROOT = './layouts/partials/' + + def to_s + message = "### Partials calls\n" + message += "Partials called once might be in the wrong place. Partials never called might be metaprogrammed, or obsolete.\n" + message += "| Id | State | Calls | Fragment | Partial |\n" + message += "|---|---|---|---|---|\n" + index = 1 + analyzed_files.each do |file| + calls = file.json[:calls] + next unless calls[:problem] + message += "| cal-#{index} | #{calls[:icon]} | #{calls[:count]} | #{calls[:fragment]} | #{file.short_path} |\n" + index += 1 + end + message + end + + protected + + def should_analyze?(file) + super && + !file.directory? && + file.path.include?(ROOT) + end + + def analyze(file) + fragment = file.path.gsub(ROOT, '').gsub('.html', '') + call = "partial \"#{fragment}" + count = Hugolint::Utils.occurrences_in_files(call, analyzer.files) + # Les fichiers ont le droit d'être utilisés 1 seule fois, + # si et seulement si ce ne sont pas des helpers à la racine + is_helper = !fragment.include?('/') + if count == 0 + problem = true + icon = ICON_DANGER + elsif count == 1 && is_helper + problem = true + icon = ICON_WARNING + else + problem = false + icon = ICON_OK + end + file.json[:calls] = { + fragment: fragment, + count: count, + problem: problem, + icon: icon + } + file + end + + def sort! + @analyzed_files.sort_by! { |file| file.json[:calls][:count] } + .reverse! + end + end + end +end diff --git a/bin/hugolint/engines/complexity.rb b/bin/hugolint/engines/complexity.rb new file mode 100644 index 000000000..2204bca2c --- /dev/null +++ b/bin/hugolint/engines/complexity.rb @@ -0,0 +1,63 @@ +module Hugolint + module Engines + class Complexity < Base + + KEYWORDS = [ + 'if', + 'else', + 'for', + 'range', + 'with', + ].freeze + + DANGER = 10 + WARNING = 5 + + def to_s + message = "### Complexity\n" + message += "Cyclomatic complexity should not be too high.\n" + message += "| Id | State | Complexity | File |\n" + message += "|---|---|---|---|\n" + index = 1 + analyzed_files.each do |file| + complexity = file.json[:complexity] + next unless complexity[:problem] + message += "| cpx-#{index} | #{complexity[:icon]} | #{complexity[:score]} | #{file.short_path} |\n" + index += 1 + end + message + end + + protected + + def should_analyze?(file) + super && + !file.directory? + end + + def sort! + @analyzed_files.sort_by!{ |file| file.json[:complexity][:score] }.reverse! + end + + def analyze(file) + score = 1 + KEYWORDS.each do |keyword| + score += Hugolint::Utils.occurences("#{keyword} ", file.data) + end + problem = score > WARNING + icon = ICON_OK + if score > DANGER + icon = ICON_DANGER + elsif score > WARNING + icon = ICON_WARNING + end + file.json[:complexity] = { + score: score, + icon: icon, + problem: problem + } + file + end + end + end +end diff --git a/bin/hugolint/engines/directories.rb b/bin/hugolint/engines/directories.rb new file mode 100644 index 000000000..7efc95f80 --- /dev/null +++ b/bin/hugolint/engines/directories.rb @@ -0,0 +1,53 @@ +module Hugolint + module Engines + class Directories < Base + + DANGER = 20 + WARNING = 10 + + def to_s + message = "### Directories \n" + message += "Directories should not contain too many files, it's probably a sign of mess.\n" + message += "| Id | State | Files | Directory |\n" + message += "|---|---|---|---|\n" + index = 1 + analyzed_files.each do |file| + directory = file.json[:directory] + next unless directory[:problem] + message += "| dir-#{index} | #{directory[:icon]} | #{directory[:count]} | #{file.short_path} |\n" + index += 1 + end + message + end + + protected + + def should_analyze?(file) + super && + file.directory? + end + + def analyze(file) + count = Dir["#{file.path}/*.*"].length + problem = count > WARNING + icon = ICON_OK + if count > DANGER + icon = ICON_DANGER + elsif count > WARNING + icon = ICON_WARNING + end + file.json[:directory] = { + count: count, + icon: icon, + problem: problem + } + file + end + + def sort! + @analyzed_files.sort_by! { |file| file.json[:directory][:count] } + .reverse! + end + end + end +end diff --git a/bin/hugolint/engines/lines.rb b/bin/hugolint/engines/lines.rb new file mode 100644 index 000000000..3b9b046e8 --- /dev/null +++ b/bin/hugolint/engines/lines.rb @@ -0,0 +1,53 @@ +module Hugolint + module Engines + class Lines < Base + + DANGER = 70 + WARNING = 35 + + def to_s + message = "### Too many lines \n" + message += "Files should not be too long, it's a sign of mess and a difficulty for overrides.\n" + message += "| Id | State | Lines | Path |\n" + message += "|---|---|---|---|\n" + index = 1 + analyzed_files.each do |file| + lines = file.json[:lines] + next unless lines[:problem] + message += "| lin-#{index} | #{lines[:icon]} | #{lines[:count]} | #{file.short_path} |\n" + index += 1 + end + message + end + + protected + + def should_analyze?(file) + super && + !file.directory? + end + + def analyze(file) + count = file.data.lines.count + problem = count > WARNING + icon = ICON_OK + if count > DANGER + icon = ICON_DANGER + elsif count > WARNING + icon = ICON_WARNING + end + file.json[:lines] = { + count: count, + icon: icon, + problem: problem + } + file + end + + def sort! + @analyzed_files.sort_by! { |file| file.json[:lines][:count] } + .reverse! + end + end + end +end diff --git a/bin/hugolint/file.rb b/bin/hugolint/file.rb new file mode 100644 index 000000000..e852c5ca8 --- /dev/null +++ b/bin/hugolint/file.rb @@ -0,0 +1,22 @@ +module Hugolint + class File + attr_reader :path, :json + + def initialize(path) + @path = path + @json = {} + end + + def data + @data ||= ::File.read(path) + end + + def directory? + ::File.directory?(path) + end + + def short_path + @short_path ||= path.gsub('./layouts/', '') + end + end +end diff --git a/bin/hugolint/utils.rb b/bin/hugolint/utils.rb new file mode 100644 index 000000000..ba1579086 --- /dev/null +++ b/bin/hugolint/utils.rb @@ -0,0 +1,17 @@ +module Hugolint + class Utils + def self.occurences(needle, haystack) + return 0 if haystack.empty? + haystack.split(needle).count - 1 + end + + def self.occurrences_in_files(needle, files) + occurences = 0 + files.each do |file| + next if file.directory? + occurences += occurences(needle, file.data) + end + occurences + end + end +end \ No newline at end of file diff --git a/layouts/partials/AddCreditMention b/layouts/partials/AddCreditMention deleted file mode 100644 index 58804e863..000000000 --- a/layouts/partials/AddCreditMention +++ /dev/null @@ -1,13 +0,0 @@ -{{ $credit := .credit }} -{{ $first_paragraph := "" }} - -{{ $sr_only_text := printf "%s" (i18n "commons.accessibility.credits") }} -{{ $credit = replace $credit "

" (printf "

%s" $sr_only_text) 1 }} - -{{ if .before }} - {{ $credit = replace $credit "

" "

" 1 }} -{{ else if .after }} - {{ $credit = replace $credit "

" "

" 1 }} -{{ end }} - -{{ return ($credit | safeHTML) }} \ No newline at end of file diff --git a/layouts/partials/AddCreditMention.html b/layouts/partials/AddCreditMention.html new file mode 100644 index 000000000..150036d45 --- /dev/null +++ b/layouts/partials/AddCreditMention.html @@ -0,0 +1,7 @@ +{{ $credit := . }} + +{{ $sr_only_text := printf "%s" (i18n "commons.accessibility.credits") }} +{{ $icon := "©" }} +{{ $credit = replace $credit "

" (printf "

%s%s" $sr_only_text $icon) 1 }} + +{{ return ($credit | safeHTML) }} \ No newline at end of file diff --git a/layouts/partials/AnchorAttribute b/layouts/partials/AnchorAttribute.html similarity index 100% rename from layouts/partials/AnchorAttribute rename to layouts/partials/AnchorAttribute.html diff --git a/layouts/partials/FilterIframeLazy b/layouts/partials/FilterIframeLazy.html similarity index 100% rename from layouts/partials/FilterIframeLazy rename to layouts/partials/FilterIframeLazy.html diff --git a/layouts/partials/FilterPublications b/layouts/partials/FilterPublications.html similarity index 100% rename from layouts/partials/FilterPublications rename to layouts/partials/FilterPublications.html diff --git a/layouts/partials/GetBlockClass b/layouts/partials/GetBlockClass.html similarity index 100% rename from layouts/partials/GetBlockClass rename to layouts/partials/GetBlockClass.html diff --git a/layouts/partials/GetBodyclass b/layouts/partials/GetBodyclass.html similarity index 100% rename from layouts/partials/GetBodyclass rename to layouts/partials/GetBodyclass.html diff --git a/layouts/partials/GetCategoriesSlug b/layouts/partials/GetCategoriesSlug.html similarity index 100% rename from layouts/partials/GetCategoriesSlug rename to layouts/partials/GetCategoriesSlug.html diff --git a/layouts/partials/GetDayAnchor b/layouts/partials/GetDayAnchor.html similarity index 100% rename from layouts/partials/GetDayAnchor rename to layouts/partials/GetDayAnchor.html diff --git a/layouts/partials/GetExtensionFile b/layouts/partials/GetExtensionFile.html similarity index 100% rename from layouts/partials/GetExtensionFile rename to layouts/partials/GetExtensionFile.html diff --git a/layouts/partials/GetFigureAriaLabel b/layouts/partials/GetFigureAriaLabel.html similarity index 100% rename from layouts/partials/GetFigureAriaLabel rename to layouts/partials/GetFigureAriaLabel.html diff --git a/layouts/partials/GetFileExtensionWithSize b/layouts/partials/GetFileExtensionWithSize.html similarity index 100% rename from layouts/partials/GetFileExtensionWithSize rename to layouts/partials/GetFileExtensionWithSize.html diff --git a/layouts/partials/GetHeadTitle b/layouts/partials/GetHeadTitle.html similarity index 100% rename from layouts/partials/GetHeadTitle rename to layouts/partials/GetHeadTitle.html diff --git a/layouts/partials/GetHeadingTag b/layouts/partials/GetHeadingTag.html similarity index 100% rename from layouts/partials/GetHeadingTag rename to layouts/partials/GetHeadingTag.html diff --git a/layouts/partials/GetHumanSize b/layouts/partials/GetHumanSize.html similarity index 100% rename from layouts/partials/GetHumanSize rename to layouts/partials/GetHumanSize.html diff --git a/layouts/partials/GetImageDimensions b/layouts/partials/GetImageDimensions.html similarity index 100% rename from layouts/partials/GetImageDimensions rename to layouts/partials/GetImageDimensions.html diff --git a/layouts/partials/GetImageDirection b/layouts/partials/GetImageDirection.html similarity index 100% rename from layouts/partials/GetImageDirection rename to layouts/partials/GetImageDirection.html diff --git a/layouts/partials/GetImageUrl b/layouts/partials/GetImageUrl.html similarity index 100% rename from layouts/partials/GetImageUrl rename to layouts/partials/GetImageUrl.html diff --git a/layouts/partials/GetImageUrlKeycdn b/layouts/partials/GetImageUrlKeycdn.html similarity index 100% rename from layouts/partials/GetImageUrlKeycdn rename to layouts/partials/GetImageUrlKeycdn.html diff --git a/layouts/partials/GetImageUrlOsuny b/layouts/partials/GetImageUrlOsuny.html similarity index 100% rename from layouts/partials/GetImageUrlOsuny rename to layouts/partials/GetImageUrlOsuny.html diff --git a/layouts/partials/GetLayoutAndOptions b/layouts/partials/GetLayoutAndOptions.html similarity index 100% rename from layouts/partials/GetLayoutAndOptions rename to layouts/partials/GetLayoutAndOptions.html diff --git a/layouts/partials/GetLightboxUrl b/layouts/partials/GetLightboxUrl.html similarity index 100% rename from layouts/partials/GetLightboxUrl rename to layouts/partials/GetLightboxUrl.html diff --git a/layouts/partials/GetLogoUrl b/layouts/partials/GetLogoUrl.html similarity index 100% rename from layouts/partials/GetLogoUrl rename to layouts/partials/GetLogoUrl.html diff --git a/layouts/partials/GetMainClass b/layouts/partials/GetMainClass.html similarity index 100% rename from layouts/partials/GetMainClass rename to layouts/partials/GetMainClass.html diff --git a/layouts/partials/GetMainSearchAttributes b/layouts/partials/GetMainSearchAttributes.html similarity index 100% rename from layouts/partials/GetMainSearchAttributes rename to layouts/partials/GetMainSearchAttributes.html diff --git a/layouts/partials/GetMedia b/layouts/partials/GetMedia.html similarity index 100% rename from layouts/partials/GetMedia rename to layouts/partials/GetMedia.html diff --git a/layouts/partials/GetMenu b/layouts/partials/GetMenu.html similarity index 100% rename from layouts/partials/GetMenu rename to layouts/partials/GetMenu.html diff --git a/layouts/partials/GetMenuSummary b/layouts/partials/GetMenuSummary.html similarity index 100% rename from layouts/partials/GetMenuSummary rename to layouts/partials/GetMenuSummary.html diff --git a/layouts/partials/GetMonthFromNow b/layouts/partials/GetMonthFromNow.html similarity index 100% rename from layouts/partials/GetMonthFromNow rename to layouts/partials/GetMonthFromNow.html diff --git a/layouts/partials/GetObjectsFromPathSlice b/layouts/partials/GetObjectsFromPathSlice.html similarity index 100% rename from layouts/partials/GetObjectsFromPathSlice rename to layouts/partials/GetObjectsFromPathSlice.html diff --git a/layouts/partials/GetPathSliceFromObjects b/layouts/partials/GetPathSliceFromObjects.html similarity index 100% rename from layouts/partials/GetPathSliceFromObjects rename to layouts/partials/GetPathSliceFromObjects.html diff --git a/layouts/partials/GetPermalink b/layouts/partials/GetPermalink.html similarity index 100% rename from layouts/partials/GetPermalink rename to layouts/partials/GetPermalink.html diff --git a/layouts/partials/GetRichSummary b/layouts/partials/GetRichSummary.html similarity index 100% rename from layouts/partials/GetRichSummary rename to layouts/partials/GetRichSummary.html diff --git a/layouts/partials/GetSiteParamWithDefault b/layouts/partials/GetSiteParamWithDefault.html similarity index 100% rename from layouts/partials/GetSiteParamWithDefault rename to layouts/partials/GetSiteParamWithDefault.html diff --git a/layouts/partials/GetSummaryPosition b/layouts/partials/GetSummaryPosition.html similarity index 100% rename from layouts/partials/GetSummaryPosition rename to layouts/partials/GetSummaryPosition.html diff --git a/layouts/partials/GetTaxonomiesPosition b/layouts/partials/GetTaxonomiesPosition.html similarity index 100% rename from layouts/partials/GetTaxonomiesPosition rename to layouts/partials/GetTaxonomiesPosition.html diff --git a/layouts/partials/GetTermsFromTaxonomies b/layouts/partials/GetTermsFromTaxonomies.html similarity index 100% rename from layouts/partials/GetTermsFromTaxonomies rename to layouts/partials/GetTermsFromTaxonomies.html diff --git a/layouts/partials/GetTextFromHTML b/layouts/partials/GetTextFromHTML.html similarity index 100% rename from layouts/partials/GetTextFromHTML rename to layouts/partials/GetTextFromHTML.html diff --git a/layouts/partials/GetTruncatedText b/layouts/partials/GetTruncatedText.html similarity index 100% rename from layouts/partials/GetTruncatedText rename to layouts/partials/GetTruncatedText.html diff --git a/layouts/partials/HasAdministrativeInformation b/layouts/partials/HasAdministrativeInformation.html similarity index 100% rename from layouts/partials/HasAdministrativeInformation rename to layouts/partials/HasAdministrativeInformation.html diff --git a/layouts/partials/IsFirstPage b/layouts/partials/IsFirstPage.html similarity index 100% rename from layouts/partials/IsFirstPage rename to layouts/partials/IsFirstPage.html diff --git a/layouts/partials/PrepareHTML b/layouts/partials/PrepareHTML.html similarity index 100% rename from layouts/partials/PrepareHTML rename to layouts/partials/PrepareHTML.html diff --git a/layouts/partials/PrepareText b/layouts/partials/PrepareText.html similarity index 100% rename from layouts/partials/PrepareText rename to layouts/partials/PrepareText.html diff --git a/layouts/partials/RemoveSrOnlyTag b/layouts/partials/RemoveSrOnlyTag.html similarity index 100% rename from layouts/partials/RemoveSrOnlyTag rename to layouts/partials/RemoveSrOnlyTag.html diff --git a/layouts/partials/categories/section/hero.html b/layouts/partials/categories/section/hero.html index e884f83d8..25ca2a7a2 100644 --- a/layouts/partials/categories/section/hero.html +++ b/layouts/partials/categories/section/hero.html @@ -2,7 +2,7 @@ {{/* Check if page is osuny taxonomy */}} {{ if .Params.is_taxonomy }} - {{ with or .Params.header_text .Title }} + {{ with or .Params.header_text .Title }} {{- $title = . -}} {{ end }} {{ end }} diff --git a/layouts/partials/events/partials/agenda.html b/layouts/partials/events/partials/agenda.html index 74158403b..831a48cec 100644 --- a/layouts/partials/events/partials/agenda.html +++ b/layouts/partials/events/partials/agenda.html @@ -4,7 +4,7 @@ {{ $is_sub_event := .is_sub_event }} {{ $event_attributes := "itemscope itemtype='https://schema.org/Event'" }} -