@thecodecodex/tools 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +44 -2
- package/package.json +46 -4
- package/src/base64.ts +36 -0
- package/src/br/cnpj.ts +55 -0
- package/src/br/cpf.ts +61 -0
- package/src/cli/main.ts +93 -0
- package/src/cli/tools.ts +643 -0
- package/src/compensation-comparator.ts +124 -0
- package/src/css-playground.ts +699 -0
- package/src/dev-ref.ts +295 -0
- package/src/diff.ts +301 -0
- package/src/finance/clt.ts +189 -0
- package/src/finance/compound.ts +78 -0
- package/src/finance/pj.ts +146 -0
- package/src/fov.ts +145 -0
- package/src/hash.ts +13 -0
- package/src/image-to-pdf.ts +299 -0
- package/src/invoice-pdf.ts +134 -0
- package/src/invoice.ts +94 -0
- package/src/jwt.ts +44 -0
- package/src/market.ts +49 -0
- package/src/password.ts +51 -0
- package/src/pdf-studio.ts +453 -0
- package/src/random/index.ts +19 -0
- package/src/regex.ts +426 -0
- package/src/skills.json +60 -0
- package/src/skills.ts +52 -0
- package/src/state-holidays.ts +94 -0
- package/src/table-convert/code.ts +99 -0
- package/src/table-convert/index.ts +42 -0
- package/src/table-convert/model.ts +72 -0
- package/src/table-convert/parse.ts +242 -0
- package/src/table-convert/serialize.ts +435 -0
- package/src/table-convert/transform.ts +133 -0
- package/src/table-convert/xlsx.ts +262 -0
- package/src/table-convert/zip.ts +127 -0
- package/src/timestamp.ts +50 -0
- package/src/url-codec.ts +25 -0
- package/src/uuid.ts +53 -0
- package/src/vacation.ts +171 -0
package/src/dev-ref.ts
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
export const REF_SECTIONS = ["all", "http", "ports", "mime", "ascii"] as const
|
|
2
|
+
export type RefSection = (typeof REF_SECTIONS)[number]
|
|
3
|
+
|
|
4
|
+
export const HTTP_CLASSES = ["all", "1xx", "2xx", "3xx", "4xx", "5xx"] as const
|
|
5
|
+
export type HttpClass = (typeof HTTP_CLASSES)[number]
|
|
6
|
+
|
|
7
|
+
export interface HttpStatusEntry {
|
|
8
|
+
code: number
|
|
9
|
+
phrase: string
|
|
10
|
+
category: Exclude<HttpClass, "all">
|
|
11
|
+
rfc: string
|
|
12
|
+
cacheable: boolean
|
|
13
|
+
pt: string
|
|
14
|
+
en: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface NetworkPortEntry {
|
|
18
|
+
port: number
|
|
19
|
+
protocol: "TCP" | "UDP" | "TCP/UDP"
|
|
20
|
+
service: string
|
|
21
|
+
category: "web" | "database" | "messaging" | "infra" | "mail"
|
|
22
|
+
pt: string
|
|
23
|
+
en: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MimeTypeEntry {
|
|
27
|
+
essence: string
|
|
28
|
+
extensions: string[]
|
|
29
|
+
category: "api" | "web" | "image" | "media" | "document"
|
|
30
|
+
pt: string
|
|
31
|
+
en: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AsciiEntry {
|
|
35
|
+
dec: number
|
|
36
|
+
hex: string
|
|
37
|
+
oct: string
|
|
38
|
+
bin: string
|
|
39
|
+
symbol: string
|
|
40
|
+
escape?: string
|
|
41
|
+
category: "control" | "whitespace" | "digit" | "upper" | "lower" | "punctuation"
|
|
42
|
+
pt: string
|
|
43
|
+
en: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const HTTP_CODES: HttpStatusEntry[] = [
|
|
47
|
+
{ code: 100, phrase: "Continue", category: "1xx", rfc: "RFC 9110", cacheable: false, pt: "Cabeçalhos recebidos; o cliente pode enviar o corpo da requisição.", en: "Request headers received; client should proceed to send the body." },
|
|
48
|
+
{ code: 101, phrase: "Switching Protocols", category: "1xx", rfc: "RFC 9110", cacheable: false, pt: "Servidor aceitou trocar o protocolo (ex.: upgrade para WebSocket).", en: "Server is switching protocols as requested (e.g., WebSocket upgrade)." },
|
|
49
|
+
{ code: 103, phrase: "Early Hints", category: "1xx", rfc: "RFC 8297", cacheable: false, pt: "Antecipa cabeçalhos Link para pré-carregar recursos antes da resposta final.", en: "Sends preliminary Link headers so the browser can preload assets early." },
|
|
50
|
+
{ code: 200, phrase: "OK", category: "2xx", rfc: "RFC 9110", cacheable: true, pt: "Requisição bem-sucedida com conteúdo no corpo da resposta.", en: "Standard response for successful HTTP requests." },
|
|
51
|
+
{ code: 201, phrase: "Created", category: "2xx", rfc: "RFC 9110", cacheable: false, pt: "Novo recurso criado com sucesso (geralmente inclui cabeçalho Location).", en: "Request succeeded and a new resource was created (often with Location header)." },
|
|
52
|
+
{ code: 202, phrase: "Accepted", category: "2xx", rfc: "RFC 9110", cacheable: false, pt: "Recebido para processamento assíncrono em fila/background.", en: "Accepted for asynchronous processing, but not yet completed." },
|
|
53
|
+
{ code: 204, phrase: "No Content", category: "2xx", rfc: "RFC 9110", cacheable: true, pt: "Sucesso sem corpo de resposta (comum em DELETE ou PUT/PATCH).", en: "Succeeded with no response payload body (common for DELETE or PUT)." },
|
|
54
|
+
{ code: 206, phrase: "Partial Content", category: "2xx", rfc: "RFC 9110", cacheable: true, pt: "Entrega parcial de bytes solicitada via cabeçalho Range (streaming/download).", en: "Partial byte range delivered in response to a Range header." },
|
|
55
|
+
{ code: 207, phrase: "Multi-Status", category: "2xx", rfc: "RFC 4918", cacheable: false, pt: "Resposta lote/batch contendo múltiplos códigos de status independentes.", en: "Batch response conveying status for multiple independent operations." },
|
|
56
|
+
{ code: 301, phrase: "Moved Permanently", category: "3xx", rfc: "RFC 9110", cacheable: true, pt: "Redirecionamento definitivo; clientes podem converter POST em GET.", en: "Permanent redirect; user agents may change POST to GET." },
|
|
57
|
+
{ code: 302, phrase: "Found", category: "3xx", rfc: "RFC 9110", cacheable: false, pt: "Redirecionamento temporário para outra URI (Location).", en: "Temporary redirect to another URI specified in Location." },
|
|
58
|
+
{ code: 303, phrase: "See Other", category: "3xx", rfc: "RFC 9110", cacheable: false, pt: "Redireciona para um GET em outra URI após processar um POST (padrão PRG).", en: "Redirects to another URI using GET after a POST (Post/Redirect/Get)." },
|
|
59
|
+
{ code: 304, phrase: "Not Modified", category: "3xx", rfc: "RFC 9110", cacheable: true, pt: "Recurso não mudou desde ETag / If-Modified-Since; use o cache local.", en: "Resource has not changed since ETag / If-Modified-Since; use cached copy." },
|
|
60
|
+
{ code: 307, phrase: "Temporary Redirect", category: "3xx", rfc: "RFC 9110", cacheable: false, pt: "Redirecionamento temporário preservando o método HTTP (POST continua POST).", en: "Temporary redirect that strictly preserves the HTTP method and body." },
|
|
61
|
+
{ code: 308, phrase: "Permanent Redirect", category: "3xx", rfc: "RFC 9110", cacheable: true, pt: "Redirecionamento permanente preservando o método HTTP e o corpo.", en: "Permanent redirect that strictly preserves the HTTP method and body." },
|
|
62
|
+
{ code: 400, phrase: "Bad Request", category: "4xx", rfc: "RFC 9110", cacheable: false, pt: "Sintaxe inválida, JSON malformado ou parâmetros obrigatórios ausentes.", en: "Malformed syntax, invalid JSON, or missing required parameters." },
|
|
63
|
+
{ code: 401, phrase: "Unauthorized", category: "4xx", rfc: "RFC 9110", cacheable: false, pt: "Não autenticado: falta credencial/token válido (WWW-Authenticate).", en: "Unauthenticated: valid credentials or token required." },
|
|
64
|
+
{ code: 403, phrase: "Forbidden", category: "4xx", rfc: "RFC 9110", cacheable: false, pt: "Autenticado, porém sem permissão de acesso ao recurso.", en: "Authenticated, but lacks permission to access the target resource." },
|
|
65
|
+
{ code: 404, phrase: "Not Found", category: "4xx", rfc: "RFC 9110", cacheable: true, pt: "Recurso não encontrado na URI solicitada (ou ocultado por segurança).", en: "Resource not found at the requested URI." },
|
|
66
|
+
{ code: 405, phrase: "Method Not Allowed", category: "4xx", rfc: "RFC 9110", cacheable: true, pt: "A rota existe, mas não aceita este verbo HTTP (ex.: POST em rota somente GET).", en: "Target resource does not support this HTTP method (see Allow header)." },
|
|
67
|
+
{ code: 408, phrase: "Request Timeout", category: "4xx", rfc: "RFC 9110", cacheable: false, pt: "Cliente demorou demais para enviar a requisição completa.", en: "Server timed out waiting for the client to finish sending the request." },
|
|
68
|
+
{ code: 409, phrase: "Conflict", category: "4xx", rfc: "RFC 9110", cacheable: false, pt: "Conflito com o estado atual (ex.: chave única duplicada ou versão otimista defasada).", en: "Conflict with current resource state (e.g., duplicate key or version mismatch)." },
|
|
69
|
+
{ code: 410, phrase: "Gone", category: "4xx", rfc: "RFC 9110", cacheable: true, pt: "Recurso foi removido permanentemente e não voltará a existir.", en: "Resource was permanently removed and is no longer available." },
|
|
70
|
+
{ code: 412, phrase: "Precondition Failed", category: "4xx", rfc: "RFC 9110", cacheable: false, pt: "Condição If-Match / If-Unmodified-Since falhou (controle de concorrência).", en: "Precondition in If-Match / If-Unmodified-Since evaluated to false." },
|
|
71
|
+
{ code: 413, phrase: "Content Too Large", category: "4xx", rfc: "RFC 9110", cacheable: false, pt: "Payload da requisição excede o limite aceito pelo servidor.", en: "Request payload is larger than the server is willing or able to process." },
|
|
72
|
+
{ code: 415, phrase: "Unsupported Media Type", category: "4xx", rfc: "RFC 9110", cacheable: false, pt: "Content-Type enviado não é suportado pelo endpoint.", en: "Payload format (Content-Type) is not supported by the target resource." },
|
|
73
|
+
{ code: 418, phrase: "I'm a teapot", category: "4xx", rfc: "RFC 2324", cacheable: false, pt: "Easter egg HTCPCP: o bule de chá recusa-se a passar café.", en: "HTCPCP easter egg: the server refuses to brew coffee because it is a teapot." },
|
|
74
|
+
{ code: 422, phrase: "Unprocessable Content", category: "4xx", rfc: "RFC 9110", cacheable: false, pt: "Sintaxe JSON válida, mas falhou nas regras de validação semântica/negócio.", en: "Well-formed syntax, but failed semantic or domain validation rules." },
|
|
75
|
+
{ code: 429, phrase: "Too Many Requests", category: "4xx", rfc: "RFC 6585", cacheable: false, pt: "Rate limit atingido; consulte o cabeçalho Retry-After.", en: "Rate limit exceeded; check the Retry-After header before retrying." },
|
|
76
|
+
{ code: 451, phrase: "Unavailable For Legal Reasons", category: "4xx", rfc: "RFC 7725", cacheable: true, pt: "Acesso bloqueado por exigência legal, ordem judicial ou restrição geográfica.", en: "Access denied due to legal demand, censorship, or regional compliance." },
|
|
77
|
+
{ code: 500, phrase: "Internal Server Error", category: "5xx", rfc: "RFC 9110", cacheable: false, pt: "Erro inesperado não tratado na aplicação servidora.", en: "Unhandled exception or unexpected condition on the server." },
|
|
78
|
+
{ code: 501, phrase: "Not Implemented", category: "5xx", rfc: "RFC 9110", cacheable: true, pt: "Funcionalidade ou método não implementado pelo servidor.", en: "Server does not support the functionality required to fulfill the request." },
|
|
79
|
+
{ code: 502, phrase: "Bad Gateway", category: "5xx", rfc: "RFC 9110", cacheable: false, pt: "Proxy/gateway recebeu resposta inválida do servidor upstream.", en: "Gateway or reverse proxy received an invalid response from upstream." },
|
|
80
|
+
{ code: 503, phrase: "Service Unavailable", category: "5xx", rfc: "RFC 9110", cacheable: false, pt: "Servidor temporariamente sobrecarregado ou em manutenção (veja Retry-After).", en: "Server is temporarily overloaded or down for maintenance." },
|
|
81
|
+
{ code: 504, phrase: "Gateway Timeout", category: "5xx", rfc: "RFC 9110", cacheable: false, pt: "Proxy/gateway não recebeu resposta do servidor upstream dentro do prazo.", en: "Gateway or reverse proxy timed out waiting for the upstream server." },
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
export const NETWORK_PORTS: NetworkPortEntry[] = [
|
|
85
|
+
{ port: 20, protocol: "TCP", service: "FTP Data", category: "infra", pt: "Transferência ativa de dados FTP.", en: "FTP active data transfer channel." },
|
|
86
|
+
{ port: 21, protocol: "TCP", service: "FTP Control", category: "infra", pt: "Controle de comandos FTP.", en: "FTP command and control channel." },
|
|
87
|
+
{ port: 22, protocol: "TCP", service: "SSH / SFTP / SCP", category: "infra", pt: "Acesso remoto seguro, túneis e transferência de arquivos via SSH.", en: "Secure Shell remote login, port forwarding, and SFTP/SCP." },
|
|
88
|
+
{ port: 25, protocol: "TCP", service: "SMTP", category: "mail", pt: "Roteamento de e-mail entre servidores (MTA).", en: "Simple Mail Transfer Protocol for server-to-server relay." },
|
|
89
|
+
{ port: 53, protocol: "TCP/UDP", service: "DNS", category: "infra", pt: "Resolução de nomes de domínio (UDP para consultas, TCP para zonas/respostas grandes).", en: "Domain Name System resolver and zone transfers." },
|
|
90
|
+
{ port: 67, protocol: "UDP", service: "DHCP Server", category: "infra", pt: "Servidor de configuração dinâmica de IP na rede local.", en: "Dynamic Host Configuration Protocol server." },
|
|
91
|
+
{ port: 80, protocol: "TCP", service: "HTTP", category: "web", pt: "Tráfego web HTTP em texto claro (sem TLS).", en: "Unencrypted Hypertext Transfer Protocol." },
|
|
92
|
+
{ port: 123, protocol: "UDP", service: "NTP", category: "infra", pt: "Sincronização de relógio de rede (Network Time Protocol).", en: "Network Time Protocol clock synchronization." },
|
|
93
|
+
{ port: 143, protocol: "TCP", service: "IMAP", category: "mail", pt: "Leitura de caixas de e-mail sem TLS.", en: "Internet Message Access Protocol (unencrypted)." },
|
|
94
|
+
{ port: 389, protocol: "TCP/UDP", service: "LDAP", category: "infra", pt: "Diretório de usuários e autenticação (Active Directory / OpenLDAP).", en: "Lightweight Directory Access Protocol." },
|
|
95
|
+
{ port: 443, protocol: "TCP/UDP", service: "HTTPS / HTTP/3 (QUIC)", category: "web", pt: "Tráfego web criptografado com TLS (TCP) ou QUIC (UDP).", en: "Encrypted HTTP over TLS (TCP) or QUIC/HTTP/3 (UDP)." },
|
|
96
|
+
{ port: 445, protocol: "TCP", service: "SMB", category: "infra", pt: "Compartilhamento de arquivos e impressoras em rede Windows/Samba.", en: "Server Message Block file sharing." },
|
|
97
|
+
{ port: 465, protocol: "TCP", service: "SMTPS", category: "mail", pt: "Submissão de e-mail SMTP com TLS implícito.", en: "SMTP over implicit TLS for email submission." },
|
|
98
|
+
{ port: 587, protocol: "TCP", service: "SMTP Submission", category: "mail", pt: "Submissão de e-mail por clientes autenticados (STARTTLS).", en: "Authenticated email message submission (STARTTLS)." },
|
|
99
|
+
{ port: 636, protocol: "TCP", service: "LDAPS", category: "infra", pt: "Diretório LDAP criptografado sobre TLS.", en: "LDAP over TLS." },
|
|
100
|
+
{ port: 993, protocol: "TCP", service: "IMAPS", category: "mail", pt: "Acesso IMAP criptografado sobre TLS.", en: "IMAP over TLS." },
|
|
101
|
+
{ port: 1433, protocol: "TCP", service: "SQL Server", category: "database", pt: "Porta padrão do Microsoft SQL Server.", en: "Microsoft SQL Server default listener." },
|
|
102
|
+
{ port: 1883, protocol: "TCP", service: "MQTT", category: "messaging", pt: "Mensageria leve para IoT e telemetria.", en: "Message Queuing Telemetry Transport (IoT)." },
|
|
103
|
+
{ port: 2049, protocol: "TCP/UDP", service: "NFS", category: "infra", pt: "Sistema de arquivos em rede (Network File System).", en: "Network File System shared mounts." },
|
|
104
|
+
{ port: 2181, protocol: "TCP", service: "ZooKeeper", category: "messaging", pt: "Coordenação distribuída para clientes Apache ZooKeeper.", en: "Apache ZooKeeper client coordination port." },
|
|
105
|
+
{ port: 2379, protocol: "TCP", service: "etcd", category: "database", pt: "API de clientes do banco chave-valor distribuído etcd (Kubernetes).", en: "etcd key-value store client API." },
|
|
106
|
+
{ port: 3000, protocol: "TCP", service: "Dev Server / Grafana", category: "web", pt: "Porta comum para servidores Node/React/Astro de desenvolvimento e Grafana.", en: "Common local web dev server and Grafana dashboard port." },
|
|
107
|
+
{ port: 3306, protocol: "TCP", service: "MySQL / MariaDB", category: "database", pt: "Porta padrão dos bancos relacionais MySQL e MariaDB.", en: "MySQL and MariaDB relational database default port." },
|
|
108
|
+
{ port: 3389, protocol: "TCP/UDP", service: "RDP", category: "infra", pt: "Área de trabalho remota do Windows (Remote Desktop Protocol).", en: "Windows Remote Desktop Protocol." },
|
|
109
|
+
{ port: 4222, protocol: "TCP", service: "NATS", category: "messaging", pt: "Conexão de clientes ao broker de mensageria NATS.", en: "NATS messaging system client port." },
|
|
110
|
+
{ port: 4317, protocol: "TCP", service: "OTLP / gRPC", category: "infra", pt: "Coletor OpenTelemetry via protocolo gRPC (traces, métricas e logs).", en: "OpenTelemetry Collector receiver over gRPC." },
|
|
111
|
+
{ port: 4318, protocol: "TCP", service: "OTLP / HTTP", category: "infra", pt: "Coletor OpenTelemetry via HTTP/protobuf ou JSON.", en: "OpenTelemetry Collector receiver over HTTP." },
|
|
112
|
+
{ port: 5432, protocol: "TCP", service: "PostgreSQL", category: "database", pt: "Porta padrão do banco de dados PostgreSQL.", en: "PostgreSQL database default port." },
|
|
113
|
+
{ port: 5672, protocol: "TCP", service: "AMQP / RabbitMQ", category: "messaging", pt: "Conexão de produtores e consumidores AMQP 0-9-1 (RabbitMQ).", en: "AMQP 0-9-1 messaging broker (RabbitMQ)." },
|
|
114
|
+
{ port: 6379, protocol: "TCP", service: "Redis / Valkey", category: "database", pt: "Banco em memória, cache e pub/sub Redis/Valkey.", en: "Redis and Valkey in-memory data store." },
|
|
115
|
+
{ port: 6443, protocol: "TCP", service: "Kubernetes API", category: "infra", pt: "Control plane HTTPS API Server do Kubernetes.", en: "Kubernetes API server HTTPS endpoint." },
|
|
116
|
+
{ port: 8080, protocol: "TCP", service: "HTTP Alternate", category: "web", pt: "Porta HTTP alternativa comum para APIs backend, proxies e containers.", en: "Standard alternative HTTP port for backends, proxies, and containers." },
|
|
117
|
+
{ port: 9090, protocol: "TCP", service: "Prometheus", category: "infra", pt: "Interface web e API HTTP de métricas do Prometheus.", en: "Prometheus monitoring web UI and HTTP API." },
|
|
118
|
+
{ port: 9092, protocol: "TCP", service: "Apache Kafka", category: "messaging", pt: "Broker de eventos e streaming Apache Kafka (plaintext/SASL).", en: "Apache Kafka event streaming broker." },
|
|
119
|
+
{ port: 9200, protocol: "TCP", service: "Elasticsearch / OpenSearch", category: "database", pt: "API REST HTTP de busca e indexação do Elasticsearch/OpenSearch.", en: "Elasticsearch and OpenSearch REST HTTP API." },
|
|
120
|
+
{ port: 11211, protocol: "TCP/UDP", service: "Memcached", category: "database", pt: "Sistema distribuído de cache em memória Memcached.", en: "Memcached in-memory caching system." },
|
|
121
|
+
{ port: 15672, protocol: "TCP", service: "RabbitMQ UI", category: "messaging", pt: "Painel web de administração e API HTTP do RabbitMQ.", en: "RabbitMQ management web UI and HTTP API." },
|
|
122
|
+
{ port: 27017, protocol: "TCP", service: "MongoDB", category: "database", pt: "Porta padrão do banco de documentos MongoDB (mongod/mongos).", en: "MongoDB document database default port." },
|
|
123
|
+
{ port: 51820, protocol: "UDP", service: "WireGuard", category: "infra", pt: "Túnel VPN criptografado de alta performance WireGuard.", en: "WireGuard encrypted VPN tunnel." },
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
export const MIME_TYPES: MimeTypeEntry[] = [
|
|
127
|
+
{ essence: "application/json", extensions: [".json", ".map"], category: "api", pt: "Formato padrão de objetos e arrays JSON em APIs REST (UTF-8).", en: "Standard JSON payload format for REST APIs (UTF-8)." },
|
|
128
|
+
{ essence: "application/problem+json", extensions: [".json"], category: "api", pt: "Formato padronizado de detalhes de erro em APIs HTTP (RFC 9457 / RFC 7807).", en: "Standard Problem Details error payload for HTTP APIs (RFC 9457)." },
|
|
129
|
+
{ essence: "application/x-ndjson", extensions: [".ndjson", ".jsonl"], category: "api", pt: "JSON delimitado por quebra de linha (streaming e logs estruturados).", en: "Newline-delimited JSON for streaming and structured logs." },
|
|
130
|
+
{ essence: "text/event-stream", extensions: [".sse"], category: "api", pt: "Server-Sent Events (SSE) para streaming unidirecional em tempo real.", en: "Server-Sent Events (SSE) real-time push stream." },
|
|
131
|
+
{ essence: "application/x-www-form-urlencoded", extensions: [], category: "api", pt: "Parâmetros chave=valor codificados como query string no corpo do POST.", en: "Key-value pairs encoded like a URL query string in the request body." },
|
|
132
|
+
{ essence: "multipart/form-data", extensions: [], category: "api", pt: "Envio de formulários com upload de arquivos binários separados por boundary.", en: "Form submission containing binary file uploads separated by a boundary." },
|
|
133
|
+
{ essence: "application/graphql", extensions: [".graphql", ".gql"], category: "api", pt: "Consulta GraphQL enviada diretamente no corpo da requisição.", en: "GraphQL query string sent directly in the request body." },
|
|
134
|
+
{ essence: "application/wasm", extensions: [".wasm"], category: "web", pt: "Módulo binário WebAssembly (necessário para WebAssembly.instantiateStreaming).", en: "WebAssembly binary module (required for instantiateStreaming)." },
|
|
135
|
+
{ essence: "text/html", extensions: [".html", ".htm"], category: "web", pt: "Documento HTML renderizado pelo navegador.", en: "HTML document rendered by the browser." },
|
|
136
|
+
{ essence: "text/css", extensions: [".css"], category: "web", pt: "Folha de estilos CSS (navegadores rejeitam CSS com MIME incorreto em modo estrito).", en: "Cascading Style Sheets (strict mode requires this exact MIME type)." },
|
|
137
|
+
{ essence: "text/javascript", extensions: [".js", ".mjs"], category: "web", pt: "Código-fonte JavaScript / ES Modules (padrão oficial RFC 9239).", en: "JavaScript / ES Module source code (official standard per RFC 9239)." },
|
|
138
|
+
{ essence: "text/plain", extensions: [".txt", ".log", ".conf"], category: "web", pt: "Texto puro sem marcação.", en: "Unformatted plain text." },
|
|
139
|
+
{ essence: "text/markdown", extensions: [".md", ".markdown"], category: "document", pt: "Documento em sintaxe Markdown (RFC 7763).", en: "Markdown text document (RFC 7763)." },
|
|
140
|
+
{ essence: "text/csv", extensions: [".csv"], category: "document", pt: "Valores separados por vírgula para planilhas e importação de dados.", en: "Comma-separated values for spreadsheets and data exchange." },
|
|
141
|
+
{ essence: "application/pdf", extensions: [".pdf"], category: "document", pt: "Documento portátil Adobe/ISO PDF.", en: "Portable Document Format (PDF)." },
|
|
142
|
+
{ essence: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", extensions: [".xlsx"], category: "document", pt: "Planilha Microsoft Excel OpenXML (.xlsx).", en: "Microsoft Excel OpenXML workbook (.xlsx)." },
|
|
143
|
+
{ essence: "image/png", extensions: [".png"], category: "image", pt: "Imagem raster sem perdas com suporte a transparência alfa.", en: "Lossless raster image supporting alpha transparency." },
|
|
144
|
+
{ essence: "image/jpeg", extensions: [".jpg", ".jpeg"], category: "image", pt: "Imagem fotográfica com compressão com perdas JPEG.", en: "Photographic image with lossy JPEG compression." },
|
|
145
|
+
{ essence: "image/webp", extensions: [".webp"], category: "image", pt: "Formato moderno de imagem para web (com ou sem perdas, alfa e animação).", en: "Modern web image format supporting lossy/lossless, alpha, and animation." },
|
|
146
|
+
{ essence: "image/avif", extensions: [".avif"], category: "image", pt: "Imagem de alta eficiência baseada no codec AV1 (suporte a HDR).", en: "High-efficiency image format based on AV1 video codec." },
|
|
147
|
+
{ essence: "image/svg+xml", extensions: [".svg"], category: "image", pt: "Gráfico vetorial escalável baseado em XML.", en: "XML-based Scalable Vector Graphics." },
|
|
148
|
+
{ essence: "font/woff2", extensions: [".woff2"], category: "web", pt: "Fonte web comprimida com Brotli (Web Open Font Format 2).", en: "Brotli-compressed Web Open Font Format 2." },
|
|
149
|
+
{ essence: "audio/mpeg", extensions: [".mp3"], category: "media", pt: "Áudio comprimido MP3 (MPEG-1/2 Audio Layer III).", en: "MP3 audio stream (MPEG-1/2 Audio Layer III)." },
|
|
150
|
+
{ essence: "video/mp4", extensions: [".mp4", ".m4v"], category: "media", pt: "Contêiner de vídeo e áudio MP4 (H.264, HEVC, AV1).", en: "MP4 video and audio container." },
|
|
151
|
+
{ essence: "application/zip", extensions: [".zip"], category: "document", pt: "Arquivo compactado ZIP.", en: "ZIP compressed archive." },
|
|
152
|
+
{ essence: "application/gzip", extensions: [".gz", ".tgz"], category: "document", pt: "Arquivo ou stream compactado com gzip (RFC 1952).", en: "Gzip compressed archive or stream." },
|
|
153
|
+
{ essence: "application/octet-stream", extensions: [".bin", ".exe", ".iso"], category: "api", pt: "Fluxo binário genérico (força download quando associado a Content-Disposition).", en: "Arbitrary binary data stream." },
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
const CONTROL_CHARS: Record<number, { symbol: string, escape?: string, pt: string, en: string }> = {
|
|
157
|
+
0: { symbol: "NUL", escape: "\\0", pt: "Caractere nulo (terminador de string em C)", en: "Null character (C string terminator)" },
|
|
158
|
+
1: { symbol: "SOH", pt: "Início de cabeçalho (Start of Heading)", en: "Start of Heading" },
|
|
159
|
+
2: { symbol: "STX", pt: "Início de texto (Start of Text)", en: "Start of Text" },
|
|
160
|
+
3: { symbol: "ETX", pt: "Fim de texto (Ctrl+C / SIGINT no terminal)", en: "End of Text (Ctrl+C interrupt)" },
|
|
161
|
+
4: { symbol: "EOT", pt: "Fim de transmissão (Ctrl+D / EOF no terminal)", en: "End of Transmission (Ctrl+D / EOF)" },
|
|
162
|
+
5: { symbol: "ENQ", pt: "Consulta (Enquiry)", en: "Enquiry" },
|
|
163
|
+
6: { symbol: "ACK", pt: "Confirmação (Acknowledge)", en: "Acknowledge" },
|
|
164
|
+
7: { symbol: "BEL", escape: "\\a", pt: "Alerta sonoro do terminal (Bell)", en: "Terminal Bell alert" },
|
|
165
|
+
8: { symbol: "BS", escape: "\\b", pt: "Retrocesso (Backspace)", en: "Backspace" },
|
|
166
|
+
9: { symbol: "TAB", escape: "\\t", pt: "Tabulação horizontal (Horizontal Tab)", en: "Horizontal Tab" },
|
|
167
|
+
10: { symbol: "LF", escape: "\\n", pt: "Quebra de linha Unix/Linux/macOS (Line Feed)", en: "Line Feed (Unix/Linux/macOS newline)" },
|
|
168
|
+
11: { symbol: "VT", escape: "\\v", pt: "Tabulação vertical", en: "Vertical Tab" },
|
|
169
|
+
12: { symbol: "FF", escape: "\\f", pt: "Avanço de página (Form Feed / Ctrl+L limpa terminal)", en: "Form Feed (Ctrl+L clears terminal)" },
|
|
170
|
+
13: { symbol: "CR", escape: "\\r", pt: "Retorno de carro (usado em CRLF \\r\\n no Windows e HTTP)", en: "Carriage Return (used in Windows/HTTP CRLF \\r\\n)" },
|
|
171
|
+
14: { symbol: "SO", pt: "Shift Out", en: "Shift Out" },
|
|
172
|
+
15: { symbol: "SI", pt: "Shift In", en: "Shift In" },
|
|
173
|
+
16: { symbol: "DLE", pt: "Data Link Escape", en: "Data Link Escape" },
|
|
174
|
+
17: { symbol: "DC1", pt: "Device Control 1 (XON)", en: "Device Control 1 (XON)" },
|
|
175
|
+
18: { symbol: "DC2", pt: "Device Control 2", en: "Device Control 2" },
|
|
176
|
+
19: { symbol: "DC3", pt: "Device Control 3 (XOFF)", en: "Device Control 3 (XOFF)" },
|
|
177
|
+
20: { symbol: "DC4", pt: "Device Control 4", en: "Device Control 4" },
|
|
178
|
+
21: { symbol: "NAK", pt: "Confirmação negativa (Negative Acknowledge)", en: "Negative Acknowledge" },
|
|
179
|
+
22: { symbol: "SYN", pt: "Synchronous Idle", en: "Synchronous Idle" },
|
|
180
|
+
23: { symbol: "ETB", pt: "Fim do bloco de transmissão", en: "End of Transmission Block" },
|
|
181
|
+
24: { symbol: "CAN", pt: "Cancelar (Cancel)", en: "Cancel" },
|
|
182
|
+
25: { symbol: "EM", pt: "Fim da mídia (End of Medium)", en: "End of Medium" },
|
|
183
|
+
26: { symbol: "SUB", pt: "Substituto (Ctrl+Z / SIGTSTP no Unix)", en: "Substitute (Ctrl+Z suspend in Unix)" },
|
|
184
|
+
27: { symbol: "ESC", escape: "\\x1b", pt: "Escape (inicia sequências de cores ANSI no terminal, ex.: \\x1b[32m)", en: "Escape (starts ANSI terminal color/cursor sequences, e.g. \\x1b[32m)" },
|
|
185
|
+
28: { symbol: "FS", pt: "Separador de arquivo (File Separator)", en: "File Separator" },
|
|
186
|
+
29: { symbol: "GS", pt: "Separador de grupo (Group Separator)", en: "Group Separator" },
|
|
187
|
+
30: { symbol: "RS", pt: "Separador de registro (Record Separator)", en: "Record Separator" },
|
|
188
|
+
31: { symbol: "US", pt: "Separador de unidade (Unit Separator)", en: "Unit Separator" },
|
|
189
|
+
127: { symbol: "DEL", pt: "Delete", en: "Delete character" },
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export const ASCII_TABLE: AsciiEntry[] = Array.from({ length: 128 }, (_, dec) => {
|
|
193
|
+
const hex = `0x${dec.toString(16).toUpperCase().padStart(2, "0")}`
|
|
194
|
+
const oct = dec.toString(8).padStart(3, "0")
|
|
195
|
+
const bin = dec.toString(2).padStart(8, "0")
|
|
196
|
+
const ctrl = CONTROL_CHARS[dec]
|
|
197
|
+
if (ctrl) {
|
|
198
|
+
const isWs = dec === 9 || dec === 10 || dec === 11 || dec === 12 || dec === 13
|
|
199
|
+
return {
|
|
200
|
+
dec,
|
|
201
|
+
hex,
|
|
202
|
+
oct,
|
|
203
|
+
bin,
|
|
204
|
+
symbol: ctrl.symbol,
|
|
205
|
+
escape: ctrl.escape,
|
|
206
|
+
category: isWs ? "whitespace" : "control",
|
|
207
|
+
pt: ctrl.pt,
|
|
208
|
+
en: ctrl.en,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (dec === 32) {
|
|
212
|
+
return { dec, hex, oct, bin, symbol: "SPACE", escape: "' '", category: "whitespace", pt: "Espaço em branco padrão", en: "Standard space character" }
|
|
213
|
+
}
|
|
214
|
+
const ch = String.fromCharCode(dec)
|
|
215
|
+
if (dec >= 48 && dec <= 57) {
|
|
216
|
+
return { dec, hex, oct, bin, symbol: ch, category: "digit", pt: `Dígito numérico '${ch}'`, en: `Numeric digit '${ch}'` }
|
|
217
|
+
}
|
|
218
|
+
if (dec >= 65 && dec <= 90) {
|
|
219
|
+
return { dec, hex, oct, bin, symbol: ch, category: "upper", pt: `Letra maiúscula '${ch}' (minúscula = +32 / bit 0x20)`, en: `Uppercase letter '${ch}' (lowercase = +32 / bit 0x20)` }
|
|
220
|
+
}
|
|
221
|
+
if (dec >= 97 && dec <= 122) {
|
|
222
|
+
return { dec, hex, oct, bin, symbol: ch, category: "lower", pt: `Letra minúscula '${ch}' (maiúscula = −32)`, en: `Lowercase letter '${ch}' (uppercase = −32)` }
|
|
223
|
+
}
|
|
224
|
+
return { dec, hex, oct, bin, symbol: ch, category: "punctuation", pt: `Símbolo / pontuação '${ch}'`, en: `Symbol / punctuation '${ch}'` }
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
function normalizeSearch(s: string): string {
|
|
228
|
+
return s
|
|
229
|
+
.normalize("NFD")
|
|
230
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
231
|
+
.toLowerCase()
|
|
232
|
+
.trim()
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export interface DevRefQuery {
|
|
236
|
+
section?: RefSection
|
|
237
|
+
query?: string
|
|
238
|
+
httpClass?: HttpClass
|
|
239
|
+
onlyCommonAscii?: boolean
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface DevRefResult {
|
|
243
|
+
http: HttpStatusEntry[]
|
|
244
|
+
ports: NetworkPortEntry[]
|
|
245
|
+
mime: MimeTypeEntry[]
|
|
246
|
+
ascii: AsciiEntry[]
|
|
247
|
+
totalMatches: number
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Códigos ASCII mais consultados quando não há filtro de busca específico
|
|
251
|
+
const HIGHLIGHT_ASCII = new Set([0, 3, 4, 8, 9, 10, 13, 27, 32, 48, 57, 65, 90, 97, 122, 127])
|
|
252
|
+
|
|
253
|
+
export function queryDevRef(opts: DevRefQuery = {}): DevRefResult {
|
|
254
|
+
const section = opts.section ?? "all"
|
|
255
|
+
const httpClass = opts.httpClass ?? "all"
|
|
256
|
+
const rawQuery = opts.query ?? ""
|
|
257
|
+
const terms = normalizeSearch(rawQuery).split(/\s+/).filter(Boolean)
|
|
258
|
+
|
|
259
|
+
const matchAll = (haystack: string) => {
|
|
260
|
+
if (terms.length === 0) return true
|
|
261
|
+
const norm = normalizeSearch(haystack)
|
|
262
|
+
return terms.every((t) => norm.includes(t))
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const http =
|
|
266
|
+
section === "all" || section === "http"
|
|
267
|
+
? HTTP_CODES.filter((item) => (httpClass === "all" || item.category === httpClass) && matchAll(`${item.code} ${item.phrase} ${item.category} ${item.rfc} ${item.pt} ${item.en}`))
|
|
268
|
+
: []
|
|
269
|
+
|
|
270
|
+
const ports =
|
|
271
|
+
section === "all" || section === "ports"
|
|
272
|
+
? NETWORK_PORTS.filter((item) => matchAll(`${item.port} ${item.protocol} ${item.service} ${item.category} ${item.pt} ${item.en}`))
|
|
273
|
+
: []
|
|
274
|
+
|
|
275
|
+
const mime =
|
|
276
|
+
section === "all" || section === "mime"
|
|
277
|
+
? MIME_TYPES.filter((item) => matchAll(`${item.essence} ${item.extensions.join(" ")} ${item.category} ${item.pt} ${item.en}`))
|
|
278
|
+
: []
|
|
279
|
+
|
|
280
|
+
const ascii =
|
|
281
|
+
section === "all" || section === "ascii"
|
|
282
|
+
? ASCII_TABLE.filter((item) => {
|
|
283
|
+
if (terms.length === 0 && opts.onlyCommonAscii && section === "all") return HIGHLIGHT_ASCII.has(item.dec)
|
|
284
|
+
return matchAll(`${item.dec} ${item.hex} ${item.oct} ${item.bin} ${item.symbol} ${item.escape ?? ""} ${item.category} ${item.pt} ${item.en}`)
|
|
285
|
+
})
|
|
286
|
+
: []
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
http,
|
|
290
|
+
ports,
|
|
291
|
+
mime,
|
|
292
|
+
ascii,
|
|
293
|
+
totalMatches: http.length + ports.length + mime.length + ascii.length,
|
|
294
|
+
}
|
|
295
|
+
}
|
package/src/diff.ts
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// Diff de textos sem dependências: algoritmo de Myers (O(ND)) sobre linhas e, dentro das linhas alteradas,
|
|
2
|
+
// sobre caracteres ou palavras. Usado pela ferramenta Diff no site e pelo comando `diff` do cli.js.
|
|
3
|
+
|
|
4
|
+
export type Op = { type: "equal", a: number, b: number } | { type: "delete", a: number } | { type: "insert", b: number }
|
|
5
|
+
|
|
6
|
+
// Myers: menor sequência de inserções/remoções que transforma `a` em `b` (comparando por igualdade de string)
|
|
7
|
+
export function myers(a: string[], b: string[]): Op[] {
|
|
8
|
+
const n = a.length
|
|
9
|
+
const m = b.length
|
|
10
|
+
const max = n + m
|
|
11
|
+
const offset = max + 1
|
|
12
|
+
const v = new Int32Array(2 * max + 3)
|
|
13
|
+
const trace: Int32Array[] = []
|
|
14
|
+
outer: for (let d = 0; d <= max; d++) {
|
|
15
|
+
trace.push(v.slice())
|
|
16
|
+
for (let k = -d; k <= d; k += 2) {
|
|
17
|
+
let x = k === -d || (k !== d && (v[offset + k - 1] ?? 0) < (v[offset + k + 1] ?? 0)) ? (v[offset + k + 1] ?? 0) : (v[offset + k - 1] ?? 0) + 1
|
|
18
|
+
let y = x - k
|
|
19
|
+
while (x < n && y < m && a[x] === b[y]) {
|
|
20
|
+
x++
|
|
21
|
+
y++
|
|
22
|
+
}
|
|
23
|
+
v[offset + k] = x
|
|
24
|
+
if (x >= n && y >= m) break outer
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// reconstrói o caminho de trás para frente
|
|
29
|
+
const ops: Op[] = []
|
|
30
|
+
let x = n
|
|
31
|
+
let y = m
|
|
32
|
+
for (let d = trace.length - 1; d >= 0; d--) {
|
|
33
|
+
const vd = trace[d] as Int32Array
|
|
34
|
+
const k = x - y
|
|
35
|
+
const prevK = k === -d || (k !== d && (vd[offset + k - 1] ?? 0) < (vd[offset + k + 1] ?? 0)) ? k + 1 : k - 1
|
|
36
|
+
const prevX = d === 0 ? 0 : (vd[offset + prevK] ?? 0)
|
|
37
|
+
const prevY = prevX - prevK
|
|
38
|
+
while (x > prevX && y > prevY) {
|
|
39
|
+
x--
|
|
40
|
+
y--
|
|
41
|
+
ops.push({ type: "equal", a: x, b: y })
|
|
42
|
+
}
|
|
43
|
+
if (d > 0) {
|
|
44
|
+
if (x === prevX) ops.push({ type: "insert", b: y - 1 })
|
|
45
|
+
else ops.push({ type: "delete", a: x - 1 })
|
|
46
|
+
}
|
|
47
|
+
x = prevX
|
|
48
|
+
y = prevY
|
|
49
|
+
}
|
|
50
|
+
return ops.reverse()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type Granularity = "char" | "word"
|
|
54
|
+
export type JsonMode = "off" | "format" | "sortKeys"
|
|
55
|
+
|
|
56
|
+
export interface DiffOptions {
|
|
57
|
+
granularity?: Granularity
|
|
58
|
+
ignoreWhitespace?: boolean
|
|
59
|
+
ignoreCase?: boolean
|
|
60
|
+
ignoreLineEndings?: boolean
|
|
61
|
+
sortLines?: boolean
|
|
62
|
+
json?: JsonMode
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface Segment {
|
|
66
|
+
text: string
|
|
67
|
+
changed: boolean
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface Row {
|
|
71
|
+
type: "equal" | "insert" | "delete" | "modified"
|
|
72
|
+
aLine?: number
|
|
73
|
+
bLine?: number
|
|
74
|
+
aText?: string
|
|
75
|
+
bText?: string
|
|
76
|
+
// só em linhas modificadas: trechos de cada lado com o que mudou marcado
|
|
77
|
+
inline?: { a: Segment[], b: Segment[] }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface DiffStats {
|
|
81
|
+
linesEqual: number
|
|
82
|
+
linesAdded: number
|
|
83
|
+
linesRemoved: number
|
|
84
|
+
linesModified: number
|
|
85
|
+
// linhas diferentes = adicionadas + removidas + modificadas
|
|
86
|
+
linesDifferent: number
|
|
87
|
+
charsAdded: number
|
|
88
|
+
charsRemoved: number
|
|
89
|
+
// 0 a 1: caracteres iguais × 2 ÷ (tamanho de A + tamanho de B)
|
|
90
|
+
similarity: number
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface DiffResult {
|
|
94
|
+
rows: Row[]
|
|
95
|
+
stats: DiffStats
|
|
96
|
+
identical: boolean
|
|
97
|
+
// o conteúdo é o mesmo e só a ordem (de chaves JSON ou de linhas) muda
|
|
98
|
+
onlyOrderDiffers: boolean
|
|
99
|
+
// textos efetivamente comparados (após formatar/ordenar)
|
|
100
|
+
a: string
|
|
101
|
+
b: string
|
|
102
|
+
// acima do limite, não calcula diferenças dentro das linhas
|
|
103
|
+
inlineSkipped: boolean
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class JsonInputError extends Error {
|
|
107
|
+
constructor(
|
|
108
|
+
readonly side: "a" | "b",
|
|
109
|
+
message: string,
|
|
110
|
+
) {
|
|
111
|
+
super(message)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const INLINE_LINE_LIMIT = 5000
|
|
116
|
+
|
|
117
|
+
function sortKeysDeep(value: unknown): unknown {
|
|
118
|
+
if (Array.isArray(value)) return value.map(sortKeysDeep)
|
|
119
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortKeysDeep((value as Record<string, unknown>)[key])]))
|
|
120
|
+
return value
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function transform(text: string, side: "a" | "b", options: DiffOptions, applySorting = true): string {
|
|
124
|
+
let result = options.ignoreLineEndings ? text.replace(/\r\n?/g, "\n") : text
|
|
125
|
+
const json = options.json ?? "off"
|
|
126
|
+
if (json !== "off" && result.trim()) {
|
|
127
|
+
try {
|
|
128
|
+
const parsed: unknown = JSON.parse(result)
|
|
129
|
+
result = JSON.stringify(json === "sortKeys" && applySorting ? sortKeysDeep(parsed) : parsed, null, 2)
|
|
130
|
+
} catch (error) {
|
|
131
|
+
throw new JsonInputError(side, error instanceof Error ? error.message : String(error))
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (options.sortLines && applySorting) result = result.split("\n").sort((x, y) => x.localeCompare(y)).join("\n")
|
|
135
|
+
return result
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// chave usada para comparar linhas (o texto exibido continua o original)
|
|
139
|
+
function lineKey(line: string, options: DiffOptions): string {
|
|
140
|
+
let key = options.ignoreLineEndings ? line.replace(/\r$/, "") : line
|
|
141
|
+
if (options.ignoreWhitespace) key = key.trim().replace(/\s+/g, " ")
|
|
142
|
+
if (options.ignoreCase) key = key.toLowerCase()
|
|
143
|
+
return key
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const WORD = /[\p{L}\p{N}_]+|\s+|[^\p{L}\p{N}_\s]/gu
|
|
147
|
+
|
|
148
|
+
export function tokenize(text: string, granularity: Granularity): string[] {
|
|
149
|
+
return granularity === "word" ? (text.match(WORD) ?? []) : [...text]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Diferença dentro de uma linha: trechos de cada lado, com os alterados marcados (adjacentes são fundidos)
|
|
153
|
+
export function diffInline(a: string, b: string, granularity: Granularity = "char", options: DiffOptions = {}): { a: Segment[], b: Segment[] } {
|
|
154
|
+
const ta = tokenize(a, granularity)
|
|
155
|
+
const tb = tokenize(b, granularity)
|
|
156
|
+
const key = (token: string) => (options.ignoreCase ? token.toLowerCase() : token)
|
|
157
|
+
const ops = myers(ta.map(key), tb.map(key))
|
|
158
|
+
const left: Segment[] = []
|
|
159
|
+
const right: Segment[] = []
|
|
160
|
+
const push = (list: Segment[], text: string, changed: boolean) => {
|
|
161
|
+
const last = list[list.length - 1]
|
|
162
|
+
if (last && last.changed === changed) last.text += text
|
|
163
|
+
else list.push({ text, changed })
|
|
164
|
+
}
|
|
165
|
+
for (const op of ops) {
|
|
166
|
+
if (op.type === "equal") {
|
|
167
|
+
push(left, ta[op.a] ?? "", false)
|
|
168
|
+
push(right, tb[op.b] ?? "", false)
|
|
169
|
+
} else if (op.type === "delete") push(left, ta[op.a] ?? "", true)
|
|
170
|
+
else push(right, tb[op.b] ?? "", true)
|
|
171
|
+
}
|
|
172
|
+
return { a: left, b: right }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const splitLines = (text: string) => (text === "" ? [] : text.split("\n"))
|
|
176
|
+
const changedLength = (segments: Segment[]) => segments.reduce((sum, s) => sum + (s.changed ? s.text.length : 0), 0)
|
|
177
|
+
|
|
178
|
+
export function diffTexts(rawA: string, rawB: string, options: DiffOptions = {}): DiffResult {
|
|
179
|
+
const a = transform(rawA, "a", options)
|
|
180
|
+
const b = transform(rawB, "b", options)
|
|
181
|
+
const linesA = splitLines(a)
|
|
182
|
+
const linesB = splitLines(b)
|
|
183
|
+
const ops = myers(
|
|
184
|
+
linesA.map((l) => lineKey(l, options)),
|
|
185
|
+
linesB.map((l) => lineKey(l, options)),
|
|
186
|
+
)
|
|
187
|
+
const inlineSkipped = Math.max(linesA.length, linesB.length) > INLINE_LINE_LIMIT
|
|
188
|
+
const granularity = options.granularity ?? "char"
|
|
189
|
+
|
|
190
|
+
const rows: Row[] = []
|
|
191
|
+
let deletes: number[] = []
|
|
192
|
+
let inserts: number[] = []
|
|
193
|
+
// remoções seguidas de inserções adjacentes viram pares "modified" (linha alterada)
|
|
194
|
+
const flush = () => {
|
|
195
|
+
const pairs = Math.min(deletes.length, inserts.length)
|
|
196
|
+
for (let i = 0; i < pairs; i++) {
|
|
197
|
+
const ai = deletes[i] as number
|
|
198
|
+
const bi = inserts[i] as number
|
|
199
|
+
const aText = linesA[ai] ?? ""
|
|
200
|
+
const bText = linesB[bi] ?? ""
|
|
201
|
+
const row: Row = { type: "modified", aLine: ai + 1, bLine: bi + 1, aText, bText }
|
|
202
|
+
if (!inlineSkipped) row.inline = diffInline(aText, bText, granularity, options)
|
|
203
|
+
rows.push(row)
|
|
204
|
+
}
|
|
205
|
+
for (const ai of deletes.slice(pairs)) rows.push({ type: "delete", aLine: ai + 1, aText: linesA[ai] ?? "" })
|
|
206
|
+
for (const bi of inserts.slice(pairs)) rows.push({ type: "insert", bLine: bi + 1, bText: linesB[bi] ?? "" })
|
|
207
|
+
deletes = []
|
|
208
|
+
inserts = []
|
|
209
|
+
}
|
|
210
|
+
for (const op of ops) {
|
|
211
|
+
if (op.type === "equal") {
|
|
212
|
+
flush()
|
|
213
|
+
rows.push({ type: "equal", aLine: op.a + 1, bLine: op.b + 1, aText: linesA[op.a] ?? "", bText: linesB[op.b] ?? "" })
|
|
214
|
+
} else if (op.type === "delete") deletes.push(op.a)
|
|
215
|
+
else inserts.push(op.b)
|
|
216
|
+
}
|
|
217
|
+
flush()
|
|
218
|
+
|
|
219
|
+
const stats: DiffStats = { linesEqual: 0, linesAdded: 0, linesRemoved: 0, linesModified: 0, linesDifferent: 0, charsAdded: 0, charsRemoved: 0, similarity: 1 }
|
|
220
|
+
let equalChars = 0
|
|
221
|
+
for (const row of rows) {
|
|
222
|
+
if (row.type === "equal") {
|
|
223
|
+
stats.linesEqual++
|
|
224
|
+
equalChars += (row.aText ?? "").length
|
|
225
|
+
} else if (row.type === "insert") {
|
|
226
|
+
stats.linesAdded++
|
|
227
|
+
stats.charsAdded += (row.bText ?? "").length
|
|
228
|
+
} else if (row.type === "delete") {
|
|
229
|
+
stats.linesRemoved++
|
|
230
|
+
stats.charsRemoved += (row.aText ?? "").length
|
|
231
|
+
} else {
|
|
232
|
+
stats.linesModified++
|
|
233
|
+
const added = row.inline ? changedLength(row.inline.b) : (row.bText ?? "").length
|
|
234
|
+
const removed = row.inline ? changedLength(row.inline.a) : (row.aText ?? "").length
|
|
235
|
+
stats.charsAdded += added
|
|
236
|
+
stats.charsRemoved += removed
|
|
237
|
+
equalChars += (row.aText ?? "").length - removed
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
stats.linesDifferent = stats.linesAdded + stats.linesRemoved + stats.linesModified
|
|
241
|
+
const total = linesA.join("").length + linesB.join("").length
|
|
242
|
+
stats.similarity = total === 0 ? 1 : (2 * equalChars) / total
|
|
243
|
+
const identical = stats.linesDifferent === 0
|
|
244
|
+
|
|
245
|
+
// "só a ordem mudou": diferente sem ordenar, igual depois de ordenar (chaves JSON e/ou linhas)
|
|
246
|
+
let onlyOrderDiffers = false
|
|
247
|
+
const canSort = (options.json ?? "off") !== "off" || options.sortLines
|
|
248
|
+
if (canSort) {
|
|
249
|
+
const unsortedA = transform(rawA, "a", options, false)
|
|
250
|
+
const unsortedB = transform(rawB, "b", options, false)
|
|
251
|
+
const sortedOptions: DiffOptions = { ...options, json: (options.json ?? "off") === "off" ? "off" : "sortKeys" }
|
|
252
|
+
const sortedEqual = transform(rawA, "a", sortedOptions) === transform(rawB, "b", sortedOptions) || identical
|
|
253
|
+
onlyOrderDiffers = unsortedA !== unsortedB && sortedEqual
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { rows, stats, identical, onlyOrderDiffers, a, b, inlineSkipped }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Patch unificado (formato do `diff -u`/git) a partir das linhas comparadas
|
|
260
|
+
export function unifiedPatch(result: Pick<DiffResult, "rows">, context = 3, names: [string, string] = ["a", "b"]): string {
|
|
261
|
+
// expande "modified" em remoção + inserção, como no formato unificado
|
|
262
|
+
type Line = { kind: " " | "-" | "+", text: string, aLine?: number, bLine?: number }
|
|
263
|
+
const lines: Line[] = []
|
|
264
|
+
for (const row of result.rows) {
|
|
265
|
+
if (row.type === "equal") lines.push({ kind: " ", text: row.aText ?? "", aLine: row.aLine, bLine: row.bLine })
|
|
266
|
+
else if (row.type === "delete") lines.push({ kind: "-", text: row.aText ?? "", aLine: row.aLine })
|
|
267
|
+
else if (row.type === "insert") lines.push({ kind: "+", text: row.bText ?? "", bLine: row.bLine })
|
|
268
|
+
else {
|
|
269
|
+
lines.push({ kind: "-", text: row.aText ?? "", aLine: row.aLine })
|
|
270
|
+
lines.push({ kind: "+", text: row.bText ?? "", bLine: row.bLine })
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const changed = lines.map((l, i) => (l.kind !== " " ? i : -1)).filter((i) => i >= 0)
|
|
274
|
+
if (changed.length === 0) return ""
|
|
275
|
+
|
|
276
|
+
// agrupa mudanças próximas em hunks com `context` linhas iguais em volta
|
|
277
|
+
const hunks: [number, number][] = []
|
|
278
|
+
for (const index of changed) {
|
|
279
|
+
const start = Math.max(0, index - context)
|
|
280
|
+
const end = Math.min(lines.length - 1, index + context)
|
|
281
|
+
const last = hunks[hunks.length - 1]
|
|
282
|
+
if (last && start <= last[1] + 1) last[1] = Math.max(last[1], end)
|
|
283
|
+
else hunks.push([start, end])
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const out = [`--- ${names[0]}`, `+++ ${names[1]}`]
|
|
287
|
+
for (const [start, end] of hunks) {
|
|
288
|
+
const slice = lines.slice(start, end + 1)
|
|
289
|
+
// número da linha anterior ao hunk em cada lado (para hunks que começam com inserção/remoção)
|
|
290
|
+
const before = lines.slice(0, start)
|
|
291
|
+
const aBefore = before.filter((l) => l.kind !== "+").length
|
|
292
|
+
const bBefore = before.filter((l) => l.kind !== "-").length
|
|
293
|
+
const aCount = slice.filter((l) => l.kind !== "+").length
|
|
294
|
+
const bCount = slice.filter((l) => l.kind !== "-").length
|
|
295
|
+
const aStart = aCount === 0 ? aBefore : aBefore + 1
|
|
296
|
+
const bStart = bCount === 0 ? bBefore : bBefore + 1
|
|
297
|
+
out.push(`@@ -${aStart},${aCount} +${bStart},${bCount} @@`)
|
|
298
|
+
for (const line of slice) out.push(`${line.kind}${line.text}`)
|
|
299
|
+
}
|
|
300
|
+
return `${out.join("\n")}\n`
|
|
301
|
+
}
|