Files
lescarnetsdakkadien.netig.net/scripts/check-content.rb
T
2026-09-30 14:27:05 +02:00

145 lines
4.0 KiB
Ruby
Executable File

#!/usr/bin/env ruby
# frozen_string_literal: true
require "json"
require_relative "schema"
ROOT = File.expand_path("..", __dir__)
CONTENT = File.join(ROOT, "src", "content")
def error(path, message)
label = path ? "#{path.delete_prefix(ROOT + "/")} : " : ""
warn "ERREUR : #{label}#{message}"
$errors += 1
end
$errors = 0
if ARGV.any?
warn "Usage : #{File.basename($PROGRAM_NAME)}"
exit 1
end
def front_matter(path)
text = File.read(path, encoding: "UTF-8")
match = text.match(/\A\+\+\+\r?\n(.*?)\r?\n\+\+\+(?:\r?\n|\z)/m)
unless match
error(path, "front matter délimité par +++ absent")
return nil
end
match[1]
rescue StandardError => e
error(path, "lecture impossible (#{e.message})")
nil
end
def fields_by_table(source)
tables = Hash.new { |hash, key| hash[key] = {} }
table = "root"
source.each_line do |line|
line = line.strip
next if line.empty? || line.start_with?("#")
if (match = line.match(/\A\[([^\]]+)\]\z/))
table = match[1]
elsif (match = line.match(/\A([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*(.*)\z/))
tables[table][match[1]] = match[2]
end
end
tables
end
def check_filename(path, section)
stem = File.basename(path, ".md")
error(path, "nom de fichier non normalisé en Unicode NFC") if stem != stem.unicode_normalize(:nfc)
if section == "signes"
match = stem.match(/\Amzl-([0-9]{3})\z/)
error(path, "un signe doit être nommé mzl-NNN.md (001 à 999)") unless match && match[1].to_i.between?(1, 999)
return
end
leading_hyphen = stem.start_with?("-")
body = leading_hyphen ? stem[1..] : stem
if leading_hyphen && section != "vocabulaire"
error(path, "seul le vocabulaire accepte un nom commençant par un tiret")
return
end
valid = body && !body.empty? &&
stem != "_index" &&
stem == stem.downcase &&
!stem.end_with?("-") &&
!stem.include?("--") &&
body.match?(/\A[\p{Alnum}-]+\z/u)
error(path, "nom de fichier invalide : utiliser des lettres Unicode minuscules, des chiffres et des tirets") unless valid
end
def require_fields(path, tables, table, fields)
missing = fields.reject { |field| tables.fetch(table, {}).key?(field) }
error(path, "champs #{table} manquants : #{missing.join(', ')}") unless missing.empty?
end
unless Dir.exist?(CONTENT)
error(nil, "répertoire introuvable : #{CONTENT}")
else
puts "Vérification des fiches…"
Dir.glob(File.join(CONTENT, "**", "*.md")).sort.each do |path|
relative = path.delete_prefix(CONTENT + "/")
parts = relative.split("/")
next if File.basename(path) == "_index.md" || parts.length == 1
section = parts.first
unless CONTENT_SECTIONS.include?(section)
error(path, "section inconnue : #{section}")
next
end
check_filename(path, section)
source = front_matter(path)
next unless source
tables = fields_by_table(source)
require_fields(path, tables, "root", %w[title date description draft])
error(path, "la fiche doit contenir exactement une ligne updated") unless source.scan(UPDATED_RE).length == 1
if section == "vocabulaire"
value = tables.dig("taxonomies", "natures")
unless value
error(path, "champ taxonomies manquant : natures")
next
end
begin
natures = JSON.parse(value)
rescue JSON::ParserError
natures = nil
end
unless natures.is_a?(Array) && natures.length == 1 && natures.first.is_a?(String)
error(path, "taxonomies.natures doit contenir exactement une nature")
next
end
nature = natures.first
fields = VOCABULARY_FIELDS[nature]
if fields
require_fields(path, tables, "extra", fields)
else
error(path, "nature grammaticale inconnue : #{nature.inspect}")
end
else
SECTION_FIELDS.fetch(section).each do |table, fields|
require_fields(path, tables, table, fields)
end
end
end
end
puts "\nRésultat : #{$errors} erreur(s)."
exit($errors.zero? ? 0 : 1)