Adicionado documentação dos componentes de Vitruvio.

This commit is contained in:
victor
2026-05-14 11:32:24 -03:00
parent dbe669d03e
commit 7410422af7
79 changed files with 8180 additions and 0 deletions
@@ -0,0 +1,332 @@
#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use File::Find;
use File::Basename;
use Encode qw(decode_utf8 is_utf8);
binmode(STDOUT, ':encoding(UTF-8)');
my $root = '/home/victor/davinti';
my $components_dir = "$root/Vitruvio/Documentação/Componentes";
my @sources = ("$root/Vitruvio/Paineis", "$root/Vitruvio/Processos");
my @component_files;
find(
sub {
return unless -f $_;
return unless $_ =~ /\.ts$/i;
return if $_ eq 'index.ts';
push @component_files, $File::Find::name;
},
$components_dir
);
my @xml_files;
for my $src (@sources) {
next unless -d $src;
find(
sub {
return unless -f $_;
return unless $_ =~ /\.xml$/i;
push @xml_files, $File::Find::name;
},
$src
);
}
for my $doc_file (@component_files) {
my $component = basename($doc_file, '.ts');
my %attrs;
my %attr_values;
my %events;
my @examples;
my $occ_total = 0;
my $occ_paineis = 0;
my $occ_processos = 0;
my $tag_regex = qr{<\s*\Q$component\E\b([^<>]*?)(?:/\s*>|>(?:.*?)</\s*\Q$component\E\s*>)}si;
for my $xml (@xml_files) {
my $content = read_text($xml);
next if $content eq '';
while ($content =~ /$tag_regex/g) {
my $inside = defined $1 ? $1 : '';
$occ_total++;
if ($xml =~ m{/Vitruvio/Paineis/}) {
$occ_paineis++;
} elsif ($xml =~ m{/Vitruvio/Processos/}) {
$occ_processos++;
}
if (scalar(@examples) < 12) {
my $line = line_from_offset($content, pos($content));
push @examples, rel_path($xml, $root) . ':' . $line;
}
while ($inside =~ /([A-Za-z_][\w:.-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g) {
my $attr = $1;
my $value = defined $2 ? $2 : $3;
$attrs{$attr} = 1;
$attr_values{$attr}{$value} = 1;
}
}
while ($content =~ /<\s*\Q$component\E\b[\s\S]*?<event\b[^>]*\bname\s*=\s*(?:"([^"]+)"|'([^']+)')[^>]*>/gsi) {
my $event = defined $1 ? $1 : $2;
next unless defined $event;
$events{$event} = 1;
}
}
my @props_sorted = sort { lc($a) cmp lc($b) } keys %attrs;
my @events_sorted = sort { lc($a) cmp lc($b) } keys %events;
my $family = infer_family($component);
my $purpose = infer_purpose($component, $family);
my $ts = build_ts_doc(
component => $component,
family => $family,
purpose => $purpose,
total => $occ_total,
paineis => $occ_paineis,
processos => $occ_processos,
attrs => \@props_sorted,
attr_values => \%attr_values,
events => \@events_sorted,
examples => \@examples,
);
write_text($doc_file, $ts);
}
print "OK: " . scalar(@component_files) . " componentes documentados\n";
sub read_text {
my ($file) = @_;
open my $fh, '<:encoding(UTF-8)', $file or return '';
local $/ = undef;
my $txt = <$fh>;
close $fh;
return defined $txt ? $txt : '';
}
sub write_text {
my ($file, $content) = @_;
open my $fh, '>:encoding(UTF-8)', $file or die "Erro ao escrever $file: $!";
print {$fh} $content;
close $fh;
}
sub line_from_offset {
my ($txt, $offset) = @_;
$offset = 0 unless defined $offset;
my $prefix = substr($txt, 0, $offset);
my $count = ($prefix =~ tr/\n//);
return $count + 1;
}
sub rel_path {
my ($path, $base) = @_;
$path = decode_utf8($path) unless is_utf8($path);
$base = decode_utf8($base) unless is_utf8($base);
$path =~ s/^\Q$base\E\/?//;
return $path;
}
sub quote_str {
my ($s) = @_;
$s = '' unless defined $s;
$s =~ s/\\/\\\\/g;
$s =~ s/"/\\"/g;
$s =~ s/\r/\\r/g;
$s =~ s/\n/\\n/g;
$s =~ s/\t/\\t/g;
return '"' . $s . '"';
}
sub infer_family {
my ($component) = @_;
return 'BancoDeDados' if $component =~ /^(DB|Data)/;
return 'Layout' if $component =~ /(Layout|Panel|Section|Window|Tab|Accordion|Grid|Cell|Container|Split|Card)$/;
return 'EntradaDeDados' if $component =~ /(Field|Input|Combo|Select|Picker|Check|Radio|Upload|Editor|TextArea|Date|Time|Number)/;
return 'Visualizacao' if $component =~ /(Label|Image|Chart|Table|List|Tree|Map|HTML|Widget|View|Viewer|Progress|Badge|Icon|Tooltip)/;
return 'Acao' if $component =~ /(Button|Action|Menu|Dialog|Modal|Popup|Confirm|Toolbar)/;
return 'Infraestrutura';
}
sub infer_purpose {
my ($component, $family) = @_;
return 'Componente orientado a dados com integracao de datasource SQL e bind de parametros.' if $family eq 'BancoDeDados';
return 'Componente de organizacao visual da tela para estruturar conteudo e navegacao.' if $family eq 'Layout';
return 'Componente de captura e edicao de dados em formularios e filtros.' if $family eq 'EntradaDeDados';
return 'Componente para exibicao de informacoes, resultados e feedback visual.' if $family eq 'Visualizacao';
return 'Componente de interacao para disparar acoes e fluxos de processo.' if $family eq 'Acao';
return 'Componente de suporte para configuracao, integracao e comportamento da tela.';
}
sub build_ts_doc {
my (%p) = @_;
my @props = @{ $p{attrs} || [] };
my %prop_values = %{ $p{attr_values} || {} };
my @events = @{ $p{events} || [] };
my @examples = @{ $p{examples} || [] };
my $props_block = join(",\n", map { ' ' . quote_str($_) } @props);
my $events_block = join(",\n", map { ' ' . quote_str($_) } @events);
my $examples_block = join(",\n", map { ' ' . quote_str($_) } @examples);
my @pv_lines;
my @pv_type_lines;
for my $prop (@props) {
my @vals_raw = sort { lc($a) cmp lc($b) } keys %{ $prop_values{$prop} || {} };
my @vals = grep { is_value_relevant($prop, $_) } @vals_raw;
@vals = @vals_raw if !@vals && @vals_raw;
my $value_type = infer_value_type($prop, \@vals);
my $truncated = 0;
if (scalar(@vals) > 15) {
@vals = @vals[0..14];
$truncated = 1;
}
push @vals, '__TRUNCADO__' if $truncated;
my $vals_txt = '[' . join(', ', map { quote_str($_) } @vals) . ']';
push @pv_lines, ' ' . quote_str($prop) . ': ' . $vals_txt;
push @pv_type_lines, ' ' . quote_str($prop) . ': ' . quote_str($value_type);
}
my $property_values_block = join(",\n", @pv_lines);
my $property_value_types_block = join(",\n", @pv_type_lines);
my $txt = '';
$txt .= "export type VitruvioComponentDoc = {\n";
$txt .= " component: string;\n";
$txt .= " summary: string;\n";
$txt .= " status: \"draft\" | \"review\" | \"ready\";\n";
$txt .= " context: {\n";
$txt .= " module: string;\n";
$txt .= " family: string;\n";
$txt .= " mappedFrom: string[];\n";
$txt .= " occurrences: {\n";
$txt .= " total: number;\n";
$txt .= " paineis: number;\n";
$txt .= " processos: number;\n";
$txt .= " };\n";
$txt .= " };\n";
$txt .= " sections: {\n";
$txt .= " purpose: string;\n";
$txt .= " basicUsage: string;\n";
$txt .= " properties: string[];\n";
$txt .= " propertyValues: { [property: string]: string[] };\n";
$txt .= " propertyValueTypes: { [property: string]: \"boolean\" | \"enum\" | \"numeric\" | \"text\" | \"mixed\" };\n";
$txt .= " events: string[];\n";
$txt .= " examples: string[];\n";
$txt .= " notes: string[];\n";
$txt .= " };\n";
$txt .= "};\n\n";
$txt .= "const " . $p{component} . "Doc: VitruvioComponentDoc = {\n";
$txt .= " component: " . quote_str($p{component}) . ",\n";
$txt .= " summary: " . quote_str("Documentacao do componente " . $p{component} . " mapeada a partir de exemplos reais de XML.") . ",\n";
$txt .= " status: \"draft\",\n";
$txt .= " context: {\n";
$txt .= " module: \"Vitruvio\",\n";
$txt .= " family: " . quote_str($p{family}) . ",\n";
$txt .= " mappedFrom: [\"Vitruvio/Paineis\", \"Vitruvio/Processos\"],\n";
$txt .= " occurrences: { total: " . ($p{total} || 0) . ", paineis: " . ($p{paineis} || 0) . ", processos: " . ($p{processos} || 0) . " }\n";
$txt .= " },\n";
$txt .= " sections: {\n";
$txt .= " purpose: " . quote_str($p{purpose}) . ",\n";
$txt .= " basicUsage: " . quote_str("Usar " . $p{component} . " no contexto adequado da tela; as propriedades abaixo foram observadas em exemplos reais do projeto.") . ",\n";
$txt .= " properties: [\n$props_block\n ],\n";
$txt .= " propertyValues: {\n$property_values_block\n },\n";
$txt .= " propertyValueTypes: {\n$property_value_types_block\n },\n";
$txt .= " events: [\n$events_block\n ],\n";
$txt .= " examples: [\n$examples_block\n ],\n";
$txt .= " notes: [\n";
$txt .= " \"Mapeamento automatico baseado em uso observado nos XMLs de paineis e processos.\",\n";
$txt .= " \"Valores ruidosos/textos muito longos sao filtrados para facilitar consulta.\",\n";
$txt .= " \"propertyValueTypes classifica o perfil observado da propriedade (boolean, enum, numeric, text ou mixed).\",\n";
$txt .= " \"Validar com documentacao oficial do Vitruvio quando houver divergencia funcional.\"\n";
$txt .= " ]\n";
$txt .= " }\n";
$txt .= "};\n\n";
$txt .= "export default " . $p{component} . "Doc;\n";
return $txt;
}
sub is_value_relevant {
my ($prop, $value) = @_;
return 0 unless defined $value;
my $raw = $value;
my $trim = $value;
$trim =~ s/^\s+//;
$trim =~ s/\s+$//;
return 1 if $trim eq '';
# Remove trechos tecnicos comuns de ruido em mapeamento automatico.
return 0 if $raw =~ /<[^>]+>/;
return 0 if $raw =~ /\{\{|\}\}|\$\{|#\{/;
return 0 if $raw =~ /\bSELECT\b|\bUPDATE\b|\bINSERT\b|\bDELETE\b/i;
my $is_text_prop = $prop =~ /caption|description|label|title|subCaption|placeholder|message|tooltip|hint/i;
my $len = length($trim);
my $word_count = scalar(grep { $_ ne '' } split(/\s+/, $trim));
# Para campos textuais, ignora frases grandes que viram ruido na documentação.
if ($is_text_prop) {
return 0 if $len > 70;
return 0 if $word_count > 10;
}
# Filtro leve global para blocos muito longos.
return 0 if $len > 120;
return 1;
}
sub infer_value_type {
my ($prop, $vals_ref) = @_;
my @vals = @{ $vals_ref || [] };
return 'mixed' if !@vals;
my @clean = map {
my $x = $_;
$x =~ s/^\s+//;
$x =~ s/\s+$//;
$x;
} @vals;
my @non_empty = grep { $_ ne '' } @clean;
return 'text' if !@non_empty;
my $all_boolean = 1;
my $all_numeric = 1;
my $has_space = 0;
for my $v (@non_empty) {
my $lc = lc($v);
$all_boolean = 0 unless $lc eq 'true' || $lc eq 'false';
$all_numeric = 0 unless $v =~ /^-?\d+(?:\.\d+)?(?:%|px)?$/;
$has_space = 1 if $v =~ /\s/;
}
return 'boolean' if $all_boolean;
return 'numeric' if $all_numeric;
my $is_text_prop = $prop =~ /caption|description|label|title|subCaption|placeholder|message|tooltip|hint|text/i;
return 'text' if $is_text_prop;
my $unique = scalar(@non_empty);
return 'enum' if $unique <= 20 && !$has_space;
return 'mixed' if $unique <= 30;
return 'text';
}