@softize/opus 25.5.0 → 25.7.0
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/CHANGELOG.md +50 -1
- package/docs/adr/0017-ui-assumes-a-fixed-desktop-layout.md +19 -0
- package/docs/adr/0056-doc-browser-separa-runtime-leve-do-catalogo.md +8 -0
- package/docs/adr/0057-doc-browser-permite-busca-no-chrome-do-consumidor.md +152 -0
- package/docs/adr/README.md +12 -0
- package/package.json +5 -1
- package/src/ui/components/patterns/sidebar.tsx +32 -11
- package/src/ui/components/primitives/markdown.tsx +2 -11
- package/src/ui/docs/DocBrowser.tsx +404 -69
- package/src/ui/docs/DocCatalog.tsx +255 -0
- package/src/ui/docs/DocHeader.tsx +74 -0
- package/src/ui/docs/browser.tsx +17 -1
- package/src/ui/docs/changelog.tsx +16 -41
- package/src/ui/docs/content/actions.md +3 -3
- package/src/ui/docs/content/customization.md +5 -1
- package/src/ui/docs/content/documentation-portal.md +190 -0
- package/src/ui/docs/content/getting-started.md +129 -87
- package/src/ui/docs/content/installation.md +94 -0
- package/src/ui/docs/content/markdown.md +41 -0
- package/src/ui/docs/content/mcp.md +2 -2
- package/src/ui/docs/content/overview.md +58 -0
- package/src/ui/docs/content/sidebar.md +4 -0
- package/src/ui/docs/folder.tsx +2 -1
- package/src/ui/docs/index.ts +8 -1
- package/src/ui/docs/links.ts +102 -0
- package/src/ui/docs/markdown.tsx +5 -3
- package/src/ui/docs/records.ts +144 -0
- package/src/ui/docs/registry.tsx +149 -59
- package/src/ui/docs/types.ts +2 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Visão geral
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Opus
|
|
6
|
+
|
|
7
|
+
Opus é um SDK para aplicações TypeScript. Você declara as operações do produto em contratos
|
|
8
|
+
compartilhados, liga a implementação no servidor e usa essas mesmas definições no cliente,
|
|
9
|
+
nos testes e nas ferramentas do projeto.
|
|
10
|
+
|
|
11
|
+
Uma consulta de mensalidade, por exemplo, recebe o tamanho da equipe e devolve o preço. O
|
|
12
|
+
contrato define o formato dessas duas informações e permite o acesso sem login. O handler
|
|
13
|
+
calcula o valor. A interface importa o contrato para chamar a operação com os mesmos tipos
|
|
14
|
+
e validações usados no servidor.
|
|
15
|
+
|
|
16
|
+
## O que você precisa entender primeiro
|
|
17
|
+
|
|
18
|
+
Uma **action** é uma operação do sistema: consultar um preço, listar tarefas, salvar um cadastro
|
|
19
|
+
ou ler um registro. Cada action tem um nome, uma entrada e uma saída.
|
|
20
|
+
|
|
21
|
+
O **contrato** é a parte compartilhada da action. Ele declara o formato dos dados e as informações
|
|
22
|
+
que seus consumidores precisam conhecer. O **binding** acrescenta o handler que executa a regra
|
|
23
|
+
no servidor. Essa divisão permite importar o contrato na interface sem levar banco ou segredo
|
|
24
|
+
para o navegador.
|
|
25
|
+
|
|
26
|
+
O **domínio** agrupa actions e dados de um assunto do produto. O **runtime** executa o que foi
|
|
27
|
+
registrado e oferece as dependências configuradas pelo aplicativo, como autenticação, banco ou
|
|
28
|
+
auditoria. Essas dependências entram por adapters; você escolhe as peças necessárias ao projeto.
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
Contrato compartilhado ──→ Cliente e interface
|
|
32
|
+
│
|
|
33
|
+
└── + handler ──→ Action registrada no runtime
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Você escreve a regra de negócio e monta o aplicativo. O Opus fornece as declarações, a execução,
|
|
37
|
+
os componentes de interface e as verificações que trabalham sobre essas declarações.
|
|
38
|
+
|
|
39
|
+
## Faça uma primeira operação
|
|
40
|
+
|
|
41
|
+
Em [Primeiros passos](/docs/getting-started), você calcula uma mensalidade, valida a entrada e
|
|
42
|
+
testa o resultado. O exemplo usa uma regra pequena e dispensa banco, servidor HTTP e login.
|
|
43
|
+
|
|
44
|
+
Depois desse percurso, [Actions e contratos](/docs/actions) explica como a mesma estrutura atende
|
|
45
|
+
a formulários, listas, detalhes e operações com autorização.
|
|
46
|
+
|
|
47
|
+
## Continue o trabalho que trouxe você até aqui
|
|
48
|
+
|
|
49
|
+
| Você precisa… | Comece por… |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| Criar uma operação ou mudar uma regra | [Actions e contratos](/docs/actions) |
|
|
52
|
+
| Publicar uma API e conectar dependências | [Runtime e adapters](/docs/runtime) |
|
|
53
|
+
| Modelar dados persistidos | [Camada de dados](/docs/data) |
|
|
54
|
+
| Construir uma tela | [Como consumir](/ui/como-consumir) |
|
|
55
|
+
| Testar uma operação e suas permissões | [Testes de actions](/docs/testing) |
|
|
56
|
+
| Atualizar a versão usada no projeto | [Atualização do Opus](/docs/upgrading) |
|
|
57
|
+
|
|
58
|
+
O catálogo de [UI](/ui) tem navegação própria pelo volume de componentes e exemplos visuais.
|
|
@@ -69,6 +69,10 @@ render(
|
|
|
69
69
|
Use `SidebarNav` quando grupos e itens já puderem ser descritos como dados. Use `SidebarItem`
|
|
70
70
|
diretamente quando a composição precisar de controles, badges ou hierarquia particular.
|
|
71
71
|
|
|
72
|
+
`SidebarItem` aceita `href` para destinos nativos, mantendo foco e abertura em outra aba.
|
|
73
|
+
Quando o host usa um roteador, forneça também `onClick`: somente o clique simples é interceptado;
|
|
74
|
+
cliques com modificadores preservam o comportamento do link. Sem `href`, o item continua sendo um botão.
|
|
75
|
+
|
|
72
76
|
## Colapso
|
|
73
77
|
|
|
74
78
|
`collapsed` informa o estado compacto aos itens. Forneça labels acessíveis mesmo quando somente os
|
package/src/ui/docs/folder.tsx
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* históricas injetam o renderizador de exemplos vivos em `catalog-folder.tsx`.
|
|
21
21
|
*/
|
|
22
22
|
import { Markdown } from '../components/primitives/markdown.tsx'
|
|
23
|
+
import { documentationSlugFromSourcePath } from './links.ts'
|
|
23
24
|
import type { DocEntry, DocGroup, DocSection } from './types.ts'
|
|
24
25
|
|
|
25
26
|
interface FrontMatter {
|
|
@@ -123,7 +124,7 @@ export function docSectionsFromFolder(files: Record<string, string>, options: Fo
|
|
|
123
124
|
const file = splitOrder(segs[segs.length - 1]!)
|
|
124
125
|
const dirs = segs.slice(0, -1).map(splitOrder)
|
|
125
126
|
rows.push({
|
|
126
|
-
slug:
|
|
127
|
+
slug: documentationSlugFromSourcePath(sourcePath),
|
|
127
128
|
title: data.title ?? firstH1(body) ?? humanize(file.name),
|
|
128
129
|
body,
|
|
129
130
|
sourcePath,
|
package/src/ui/docs/index.ts
CHANGED
|
@@ -7,7 +7,14 @@
|
|
|
7
7
|
* `@softize/opus/ui/docs/standalone` — não passa por aqui.
|
|
8
8
|
*/
|
|
9
9
|
export { DocBrowser } from './CatalogDocBrowser.tsx'
|
|
10
|
+
export { DocHeader } from './DocHeader.tsx'
|
|
11
|
+
export { DocCatalog, DocRecordDetail } from './DocCatalog.tsx'
|
|
12
|
+
export type { DocCatalogProps, DocRecordDetailProps } from './DocCatalog.tsx'
|
|
13
|
+
export { decisionRecord, releaseRecords, parseChangelog, filterDocumentationRecords } from './records.ts'
|
|
14
|
+
export type { DocumentationRecord } from './records.ts'
|
|
15
|
+
export type { DocHeaderProps } from './DocHeader.tsx'
|
|
10
16
|
export type { DocBrowserProps } from './CatalogDocBrowser.tsx'
|
|
17
|
+
export type { DocBrowserSearchOptions } from './DocBrowser.tsx'
|
|
11
18
|
export { DOC_SECTIONS, PROTOCOL_SECTIONS, UI_SECTIONS } from './registry.tsx'
|
|
12
19
|
export type { DocEntry, DocGroup, DocSection } from './types.ts'
|
|
13
20
|
export { docSectionsFromFolder } from './catalog-folder.tsx'
|
|
@@ -17,5 +24,5 @@ export { Example, CodeBlock, DocPage } from './doc.tsx'
|
|
|
17
24
|
// Renderer de markdown do DocBrowser: prosa + fence ```tsx preview vira componente vivo.
|
|
18
25
|
export { DocMarkdown } from './markdown.tsx'
|
|
19
26
|
export type { DocMarkdownProps } from './markdown.tsx'
|
|
20
|
-
// O changelog do pacote, pronto
|
|
27
|
+
// O changelog do pacote, pronto para montar em um destino global ou no catálogo embutido.
|
|
21
28
|
export { OpusChangelog, ChangelogView } from './changelog.tsx'
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export interface DocumentationLinkTarget {
|
|
2
|
+
sourcePath: string
|
|
3
|
+
anchor?: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function decodePathSegment(segment: string): string {
|
|
7
|
+
try {
|
|
8
|
+
return decodeURIComponent(segment)
|
|
9
|
+
} catch {
|
|
10
|
+
return segment
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeDocumentationPath(path: string): string | null {
|
|
15
|
+
const segments: string[] = []
|
|
16
|
+
for (const encodedSegment of path.split('/')) {
|
|
17
|
+
const segment = decodePathSegment(encodedSegment)
|
|
18
|
+
if (segment === '' || segment === '.') continue
|
|
19
|
+
if (segment === '..') {
|
|
20
|
+
if (segments.length === 0) return null
|
|
21
|
+
segments.pop()
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
if (segment.includes('/') || segment.includes('\\')) return null
|
|
25
|
+
segments.push(segment)
|
|
26
|
+
}
|
|
27
|
+
return segments.join('/')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeDocumentationAnchor(value: string, decodeUri: boolean): string {
|
|
31
|
+
let decoded = value
|
|
32
|
+
if (decodeUri) {
|
|
33
|
+
try {
|
|
34
|
+
decoded = decodeURIComponent(value)
|
|
35
|
+
} catch {
|
|
36
|
+
// Fragmentos malformados continuam navegáveis pela normalização conservadora.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return decoded
|
|
40
|
+
.normalize('NFD')
|
|
41
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
42
|
+
.toLocaleLowerCase('pt-BR')
|
|
43
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
44
|
+
.trim()
|
|
45
|
+
.replace(/\s+/g, '-')
|
|
46
|
+
.replace(/-+/g, '-')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Normaliza um fragmento URI para a âncora usada pela documentação. */
|
|
50
|
+
export function documentationAnchor(value: string): string {
|
|
51
|
+
return normalizeDocumentationAnchor(value, true)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Normaliza o texto visível de um título sem interpretar sequências percentuais como URI. */
|
|
55
|
+
export function documentationHeadingAnchor(value: string): string {
|
|
56
|
+
return normalizeDocumentationAnchor(value, false)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Deriva o slug de um caminho já relativo à raiz documental do consumidor. */
|
|
60
|
+
export function documentationSlugFromSourcePath(sourcePath: string): string {
|
|
61
|
+
return sourcePath
|
|
62
|
+
.replace(/^\/+/, '')
|
|
63
|
+
.replace(/\.mdx?$/i, '')
|
|
64
|
+
.split('/')
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.map((segment) => segment.replace(/^\d+[-_](.+)$/, '$1'))
|
|
67
|
+
.join('/')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve links Markdown internos. `currentSourcePath` deve ser relativo à raiz documental.
|
|
72
|
+
* `/docs/` indica essa raiz em um link absoluto; `docs/` sem a barra inicial é uma pasta normal.
|
|
73
|
+
* Retorna `null` para protocolos, arquivos que não são Markdown e caminhos fora da raiz.
|
|
74
|
+
*/
|
|
75
|
+
export function resolveDocumentationLinkTarget(
|
|
76
|
+
currentSourcePath: string,
|
|
77
|
+
href: string,
|
|
78
|
+
): DocumentationLinkTarget | null {
|
|
79
|
+
if (/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(href)) return null
|
|
80
|
+
|
|
81
|
+
const current = normalizeDocumentationPath(currentSourcePath.replace(/^\/+/, ''))
|
|
82
|
+
if (current === null) return null
|
|
83
|
+
if (href.startsWith('#')) {
|
|
84
|
+
return { sourcePath: current, anchor: documentationAnchor(href.slice(1)) }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const fragmentIndex = href.indexOf('#')
|
|
88
|
+
const target = fragmentIndex < 0 ? href : href.slice(0, fragmentIndex)
|
|
89
|
+
const fragment = fragmentIndex < 0 ? undefined : href.slice(fragmentIndex + 1)
|
|
90
|
+
if (target.startsWith('/') && !target.startsWith('/docs/')) return null
|
|
91
|
+
const currentDirectory = current.slice(0, current.lastIndexOf('/') + 1)
|
|
92
|
+
const candidate = normalizeDocumentationPath(
|
|
93
|
+
target.startsWith('/docs/')
|
|
94
|
+
? target.slice('/docs/'.length)
|
|
95
|
+
: `${currentDirectory}${target}`,
|
|
96
|
+
)
|
|
97
|
+
if (candidate === null || !/\.mdx?$/i.test(candidate)) return null
|
|
98
|
+
return {
|
|
99
|
+
sourcePath: candidate,
|
|
100
|
+
...(fragment === undefined ? {} : { anchor: documentationAnchor(fragment) }),
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/ui/docs/markdown.tsx
CHANGED
|
@@ -88,9 +88,11 @@ export interface DocMarkdownProps {
|
|
|
88
88
|
/** Reescreve links do Markdown antes da renderização. Útil para resolver caminhos relativos
|
|
89
89
|
* quando o documento é publicado sob uma rota do aplicativo. */
|
|
90
90
|
resolveHref?: (href: string) => string
|
|
91
|
+
/** Mantém código didático aberto quando false; exemplos vivos preservam sua composição. */
|
|
92
|
+
codeCollapsible?: boolean
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
export function DocMarkdown({ content, scope, resolveHref }: DocMarkdownProps): React.ReactElement {
|
|
95
|
+
export function DocMarkdown({ content, scope, resolveHref, codeCollapsible = true }: DocMarkdownProps): React.ReactElement {
|
|
94
96
|
// O `scope` ESTENDE o default (não substitui): o pattern injeta o palco doc-client
|
|
95
97
|
// (DocBrowserActionProvider + contratos) sem perder o opus/ui + lucide + hooks. Memoiza pra
|
|
96
98
|
// não re-avaliar os LiveBlocks a cada render (o scope injetado é estável no chamador).
|
|
@@ -135,7 +137,7 @@ export function DocMarkdown({ content, scope, resolveHref }: DocMarkdownProps):
|
|
|
135
137
|
{/\b(preview|live)\b/.test(info) ? (
|
|
136
138
|
<LiveBlock code={t.content} scope={liveScope} previewClassName={previewClassName} />
|
|
137
139
|
) : (
|
|
138
|
-
<CodeBlock code={t.content.replace(/\n$/, '')} lang={lang} />
|
|
140
|
+
<CodeBlock code={t.content.replace(/\n$/, '')} lang={lang} collapsible={codeCollapsible} />
|
|
139
141
|
)}
|
|
140
142
|
</div>,
|
|
141
143
|
)
|
|
@@ -144,5 +146,5 @@ export function DocMarkdown({ content, scope, resolveHref }: DocMarkdownProps):
|
|
|
144
146
|
}
|
|
145
147
|
}
|
|
146
148
|
flush()
|
|
147
|
-
return <div className="prose prose-sm max-w-none space-y-4">{parts}</div>
|
|
149
|
+
return <div data-slot="markdown" className="prose prose-sm max-w-none space-y-4">{parts}</div>
|
|
148
150
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
export interface DocumentationRecord {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
href: string;
|
|
5
|
+
date?: string;
|
|
6
|
+
status?: string;
|
|
7
|
+
searchText: string;
|
|
8
|
+
body: string;
|
|
9
|
+
summary?: string;
|
|
10
|
+
content?: string;
|
|
11
|
+
breaking?: string;
|
|
12
|
+
afterBreaking?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ChangelogVersion {
|
|
16
|
+
version: string;
|
|
17
|
+
date?: string;
|
|
18
|
+
body: string;
|
|
19
|
+
breaking?: string;
|
|
20
|
+
beforeBreaking?: string;
|
|
21
|
+
afterBreaking?: string;
|
|
22
|
+
/** Corpo original, com a ordem das seções preservada para a leitura integral. */
|
|
23
|
+
sourceBody?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ParsedChangelog {
|
|
27
|
+
intro: string;
|
|
28
|
+
versions: ChangelogVersion[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Preserva a convenção publicada: `## versão — data` e seção `### Breaking`. */
|
|
32
|
+
export function parseChangelog(source: string): ParsedChangelog {
|
|
33
|
+
const chunks = `\n${source.trim()}`.split(/\n## /);
|
|
34
|
+
const versions = chunks.slice(1).map((chunk) => {
|
|
35
|
+
const nl = chunk.indexOf("\n");
|
|
36
|
+
const header = (nl === -1 ? chunk : chunk.slice(0, nl)).trim();
|
|
37
|
+
const rest = nl === -1 ? "" : chunk.slice(nl + 1);
|
|
38
|
+
const [version, date] = header.split(/\s+—\s+/);
|
|
39
|
+
const sourceBody = rest.trim();
|
|
40
|
+
const breakingSection =
|
|
41
|
+
/^### Breaking[\t ]*\r?\n([\s\S]*?)(?=^###[\t ]|(?![\s\S]))/m.exec(sourceBody);
|
|
42
|
+
const body = breakingSection
|
|
43
|
+
? sourceBody.slice(0, breakingSection.index) +
|
|
44
|
+
sourceBody.slice(breakingSection.index + breakingSection[0].length)
|
|
45
|
+
: sourceBody;
|
|
46
|
+
return {
|
|
47
|
+
version: (version ?? header).trim(),
|
|
48
|
+
...(date !== undefined ? { date: date.trim() } : {}),
|
|
49
|
+
body: (body ?? "").trim(),
|
|
50
|
+
sourceBody,
|
|
51
|
+
...(breakingSection
|
|
52
|
+
? {
|
|
53
|
+
breaking: breakingSection[1]!.trim(),
|
|
54
|
+
beforeBreaking: sourceBody.slice(0, breakingSection.index).trim(),
|
|
55
|
+
afterBreaking: sourceBody
|
|
56
|
+
.slice(breakingSection.index + breakingSection[0].length)
|
|
57
|
+
.trim(),
|
|
58
|
+
}
|
|
59
|
+
: {}),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
return { intro: chunks[0]?.trim() ?? "", versions };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Extrai só os campos declarados; a ausência de metadata não vira uma decisão presumida. */
|
|
66
|
+
export function decisionRecord(
|
|
67
|
+
source: string,
|
|
68
|
+
href: string,
|
|
69
|
+
): DocumentationRecord | undefined {
|
|
70
|
+
const heading = source.match(/^#\s+ADR\s+(\d+)\s*[—–:-]\s*(.+)$/m);
|
|
71
|
+
if (!heading) return undefined;
|
|
72
|
+
const header = source.split(/^##\s/m)[0] ?? "";
|
|
73
|
+
const metadata = (name: string): string | undefined => {
|
|
74
|
+
const value = header.match(
|
|
75
|
+
new RegExp(`^[\\t ]*-?[\\t ]*\\*\\*${name}:?\\*\\*:?[\\t ]*(.+)$`, "mi"),
|
|
76
|
+
)?.[1];
|
|
77
|
+
return value?.trim().replace(/\.$/, "");
|
|
78
|
+
};
|
|
79
|
+
const declaredStatus = metadata("Status");
|
|
80
|
+
// O estado resume a metadata; ressalvas e andamento permanecem no texto integral da decisão.
|
|
81
|
+
const status =
|
|
82
|
+
declaredStatus?.match(
|
|
83
|
+
/^(parcialmente (?:substituída|supersedida)|substituída|supersedida|aceita(?: para (?:protótipo|implementação(?: local)?|candidato local)| como primeiro corte)?|proposta|implementada|rejeitada)\b/i,
|
|
84
|
+
)?.[1] ?? declaredStatus;
|
|
85
|
+
const context = source
|
|
86
|
+
.match(/^##[\t ]+Contexto\b[^\n]*\n([\s\S]*)/im)?.[1]
|
|
87
|
+
?.split(/^##[\t ]/m)[0];
|
|
88
|
+
return {
|
|
89
|
+
id: heading[1]!,
|
|
90
|
+
title: heading[2]!.trim(),
|
|
91
|
+
href,
|
|
92
|
+
date: metadata("Data"),
|
|
93
|
+
status,
|
|
94
|
+
summary:
|
|
95
|
+
context
|
|
96
|
+
?.replace(/^#{1,6}[\t ].*(?:\r?\n|$)/gm, "")
|
|
97
|
+
.trim()
|
|
98
|
+
.split(/\n\s*\n/)[0]
|
|
99
|
+
?.trim() || undefined,
|
|
100
|
+
searchText: source,
|
|
101
|
+
body: source,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function releaseRecords(
|
|
106
|
+
source: string,
|
|
107
|
+
basePath: string,
|
|
108
|
+
): DocumentationRecord[] {
|
|
109
|
+
return parseChangelog(source).versions.map((release) => {
|
|
110
|
+
return {
|
|
111
|
+
id: release.version,
|
|
112
|
+
title: release.version,
|
|
113
|
+
href: `${basePath}/${encodeURIComponent(release.version)}`,
|
|
114
|
+
date: release.date,
|
|
115
|
+
content: release.beforeBreaking ?? release.body,
|
|
116
|
+
breaking: release.breaking,
|
|
117
|
+
afterBreaking: release.afterBreaking,
|
|
118
|
+
body: `# ${release.version}\n\n${release.date ?? ""}\n\n${release.sourceBody}`,
|
|
119
|
+
searchText: `${release.version} ${release.date ?? ""} ${release.sourceBody}`,
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function searchable(value: string): string {
|
|
125
|
+
return value
|
|
126
|
+
.normalize("NFD")
|
|
127
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
128
|
+
.toLocaleLowerCase("pt-BR");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function filterDocumentationRecords(
|
|
132
|
+
records: readonly DocumentationRecord[],
|
|
133
|
+
query: string,
|
|
134
|
+
): DocumentationRecord[] {
|
|
135
|
+
const terms = searchable(query).trim().split(/\s+/).filter(Boolean);
|
|
136
|
+
return records.filter(
|
|
137
|
+
(record) =>
|
|
138
|
+
terms.every((term) =>
|
|
139
|
+
searchable(
|
|
140
|
+
`${record.id} ${record.title} ${record.status ?? ""} ${record.searchText}`,
|
|
141
|
+
).includes(term),
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
}
|