#!/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)