Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# Overview

Este proyecto es una aplicación **Angular 20 standalone** orientada a la edición de Markdown con vista previa en vivo. La app se inicializa con `bootstrapApplication` y tiene a `AppComponent` como punto central de composición. El estado local se gestiona principalmente con **signals** y **computed**, evitando complejidad innecesaria cuando no hace falta un store global.

La aplicación ofrece un flujo de edición centrado en productividad: renderizado Markdown en tiempo real, sanitización del HTML generado, resaltado de bloques de código, extensiones personalizadas para alertas, modales de inserción de contenido, búsqueda dentro del editor, atajos de teclado globales y soporte de importación/exportación de archivos `.md`.

---

# Estructura del proyecto

Estructura relevante del código:

- `src/app/components`
Componentes reutilizables y piezas principales de UI, incluyendo editor, preview, modales y controles asociados.

- `src/app/layouts`
Layouts y composición estructural de pantallas o contenedores visuales.

- `src/app/services`
Servicios compartidos, por ejemplo para atajos de teclado u otra lógica transversal que no debe quedar dispersa en componentes.

- `src/app/config`
Configuración reutilizable de la app, integraciones y constantes de comportamiento.

- `src/app/utils/data`
Datos utilitarios y estructuras estáticas compartidas.

- `src/app/utils/types`
Tipos de TypeScript reutilizables para mantener contratos claros y evitar tipado implícito o duplicado.

- `public/`
Assets públicos estáticos.

- `tests/`
Pruebas unitarias/integración con **Jest**.

- `e2e/`
Pruebas end-to-end con **Playwright**.

---

# Dependencias clave

## Runtime y UI

- **Angular 20**: base del proyecto, arquitectura standalone y composición principal.
- **marked**: parser/renderizador de Markdown.
- **DOMPurify**: sanitización del HTML generado antes de mostrarlo en preview.
- **highlight.js**: resaltado sintáctico para bloques de código.
- **Tailwind CSS v4**: estilos utilitarios y soporte del tema visual.

## Calidad y testing

- **Jest**: pruebas unitarias.
- **Playwright**: pruebas end-to-end.
- **ESLint**: reglas de calidad y consistencia.
- **Prettier**: formato de código.
- **Husky**: automatización de hooks de git.
- **commitlint**: validación del formato de commits.

---

# Principales funcionalidades

- **Editor Markdown con preview en vivo**
La edición actualiza la representación renderizada de forma inmediata.

- **Render de Markdown con `marked`**
El contenido se transforma a HTML usando `marked`.

- **Sanitización con `DOMPurify`**
Todo HTML destinado al preview debe pasar por sanitización antes de insertarse en la UI.

- **Resaltado de código con `highlight.js`**
Los bloques de código renderizados se mejoran visualmente con highlight sintáctico.

- **Extensiones custom para alerts**
El parser/render del Markdown incorpora extensiones específicas para bloques tipo alerta.

- **Modales de inserción**
Existen modales dedicados para insertar o configurar:
- Link
- Image
- Block Code
- Table

- **Servicio global de atajos de teclado**
Los atajos no deben implementarse de forma aislada en cada componente si su comportamiento es transversal.

- **Búsqueda dentro del editor**
El editor incorpora capacidades de búsqueda sobre el contenido.

- **Importación y exportación de `.md`**
El usuario puede cargar y descargar contenido Markdown.

- **Tema oscuro en editor y preview**
La experiencia visual contempla modo oscuro como comportamiento relevante del producto.

---

# Ejecución

## Scripts principales

- `npm run start`
Levanta la aplicación en desarrollo.

- `npm run build`
Genera la build de producción.

- `npm run build:gh`
Genera la build adaptada al flujo de despliegue para GitHub Pages.

- `npm run watch`
Ejecuta compilación en modo observación.

## Calidad

- `npm run lint`
Ejecuta ESLint.
- `npm run lint:fix`
Intenta corregir automáticamente problemas de lint/formato.

## Testing unitario

- `npm run test`
Ejecuta la suite con Jest.
- `npm run test:watch`
Ejecuta Jest en modo watch.
- `npm run test:coverage`
Genera cobertura de pruebas.

## Testing end-to-end

- `npm run e2e`
Ejecuta Playwright.
- `npm run e2e:ui`
Abre Playwright en modo UI.
- `npm run e2e:debug`
Ejecuta pruebas e2e con depuración.
- `npm run e2e:report`
Muestra el reporte de Playwright.

---

# Reglas

## Arquitectura y organización

- Usar **standalone components** como enfoque por defecto.
- Mantener `AppComponent` como punto central de composición de la app.
- Evitar dispersar lógica reusable en componentes; mover comportamiento compartido a:
- `services`
- `config`
- `utils/data`
- `utils/types`

## Estado y reactividad

- **Preferir `signals` y `computed` para estado local**.
- Evitar introducir soluciones de estado más pesadas si el caso puede resolverse localmente y de forma clara.

## Estilo Angular/TypeScript

- Usar `inject()` de forma consistente en lugar de patrones más antiguos cuando aplique.
- Mantener **type imports** consistentes.
- **No usar `any`** salvo justificación excepcional y explícita.
- Priorizar tipado claro, pequeño y reutilizable.

## Seguridad y renderizado

- Todo HTML generado desde Markdown debe considerarse no confiable hasta pasar por **DOMPurify**.
- No saltarse la sanitización en flujos de preview o render intermedio.

## Calidad

- **ESLint y Prettier son obligatorios** antes de integrar cambios.
- Mantener el código alineado con las reglas ya establecidas por el repositorio.
- No introducir patrones incompatibles con la arquitectura standalone actual.

## Testing

- Las **pruebas unitarias** viven en `tests/`.
- Las **pruebas e2e** viven en `e2e/`.
- Los cambios relevantes en lógica, UI crítica o flujos principales deben venir acompañados por pruebas adecuadas.

## Alcance de cambios

- Favorecer cambios pequeños, coherentes y localizados.
- Si una lógica empieza a reutilizarse o crecer, extraerla tempranamente a una abstracción compartida en lugar de duplicarla.
7 changes: 7 additions & 0 deletions src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,13 @@
(hideOrShowModal)="openModal($event, '')"
/>
}
@case ('Save File') {
<app-modal-save-file
[initialFileName]="suggestedMarkdownFileName()"
(hideOrShowModal)="openModal($event, '')"
(saveFileName)="saveMarkdownWithCustomName($event)"
/>
}
}
</app-modal>
</div>
Expand Down
65 changes: 60 additions & 5 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { listIconsOthers } from './utils/data/list-other-options';
import { ModalUrlComponent } from './components/modal-url/modal-url.component';
import { ModalCodeComponent } from './components/modal-code/modal-code.component';
import { ModalImageComponent } from './components/modal-image/modal-image.component';
import { ModalSaveFileComponent } from './components/modal-save-file/modal-save-file.component';
import { ModalTableComponent } from './components/modal-table/modal-table.component';
import { ShortcutsService } from './services/shortcuts.service';
import { ModalComponent } from './layouts/modal/modal.component';
Expand All @@ -33,6 +34,8 @@ import { codeExtension } from './config/marked-code';
import { SelectComponent } from './components/select/select.component';
import type { Options } from './utils/types/option';
import { listAlerts } from './utils/data/list-alerts';
import { MARKDOWN_FILE } from './config/markdown-file';
import type { SaveFilePickerWindow } from './utils/types/save-file';

marked.setOptions({
gfm: true,
Expand All @@ -51,6 +54,7 @@ marked.use({ extensions: [alertExtension] });
ModalUrlComponent,
ModalImageComponent,
ModalCodeComponent,
ModalSaveFileComponent,
ModalTableComponent,
ModalComponent,
SelectComponent,
Expand All @@ -68,6 +72,7 @@ export class AppComponent {
typeOfModal = signal<string>('');
searchQuery = signal<string>('');
lastMatchIndex = signal<number>(0);
suggestedMarkdownFileName = signal<string>(MARKDOWN_FILE.defaultName);

listIconsText: AsideElement[] = listIconsText;
listIconsOthers: AsideElement[] = listIconsOthers;
Expand Down Expand Up @@ -337,16 +342,66 @@ export class AppComponent {
}
}

downloadMarkdown() {
const content = this.inputValue();
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
private createMarkdownBlob(): Blob {
return new Blob([this.inputValue()], { type: `${MARKDOWN_FILE.mimeType};charset=utf-8` });
}

private triggerBrowserDownload(blob: Blob, fileName: string) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'document.md';
a.download = fileName;
a.click();

URL.revokeObjectURL(url);
}

private isAbortError(error: unknown): boolean {
return error instanceof DOMException && error.name === 'AbortError';
}

private openSaveFileModal(defaultFileName: string = MARKDOWN_FILE.defaultName) {
this.suggestedMarkdownFileName.set(defaultFileName);
this.openModal(true, 'Save File');
}

saveMarkdownWithCustomName(fileName: string) {
const blob = this.createMarkdownBlob();
this.triggerBrowserDownload(blob, fileName);
this.openModal(false, '');
}

async downloadMarkdown() {
const blob = this.createMarkdownBlob();
const pickerWindow = window as SaveFilePickerWindow;

if (!pickerWindow.showSaveFilePicker) {
this.openSaveFileModal();
return;
}

try {
const handle = await pickerWindow.showSaveFilePicker({
suggestedName: MARKDOWN_FILE.defaultName,
excludeAcceptAllOption: true,
types: [
{
description: MARKDOWN_FILE.description,
accept: {
[MARKDOWN_FILE.mimeType]: [MARKDOWN_FILE.extension],
},
},
],
});

const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
} catch (error: unknown) {
if (this.isAbortError(error)) {
return;
}

this.openSaveFileModal();
}
}
}
41 changes: 41 additions & 0 deletions src/app/components/modal-save-file/modal-save-file.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<div id="head-modal" data-testid="modal-save-file-head" class="w-full border-b border-[#cccc]">
<p class="text-2xl text-gray-700 font-medium">Guardar archivo</p>
</div>

<div id="body-modal" data-testid="modal-save-file-body" class="flex flex-col py-4 gap-y-3">
<div class="flex flex-col justify-center">
<label for="save-file-name" class="mb-2">Nombre del archivo:</label>
<input
id="save-file-name"
data-testid="save-file-name"
type="text"
[value]="fileName()"
(input)="onInput($event)"
(keydown.enter)="saveModal()"
class="w-full h-10 rounded-md border border-[#cccc] focus:outline-none focus:ring-0 focus:border-gray-400 focus:border-2 px-3"
/>
</div>
</div>

<div
id="footer-modal"
data-testid="modal-save-file-footer"
class="mx-auto flex justify-between items-center w-1/2 h-fit"
>
<button
data-testid="save-file-confirm"
type="button"
(click)="saveModal()"
class="rounded-md border-none w-24 h-fit p-2 bg-gray-800 text-white text-base cursor-pointer hover:opacity-80 transition-opacity"
>
Guardar
</button>
<button
data-testid="save-file-cancel"
type="button"
(click)="closeModal()"
class="rounded-md border-none w-24 h-fit p-2 bg-gray-500 text-white text-base cursor-pointer hover:opacity-80 transition-opacity"
>
Cancelar
</button>
</div>
Loading
Loading