# core/factories/document.output.factory.ts

> 116 lines of code and 24 definitions.

Tree: GovLab Docs
Language: typescript
Layer: infrastructure
Canonical: https://banes-lab.com/anatomy/docs#file-docs-core-factories-document-output-factory-ts
Source text: https://banes-lab.com/source/docs/core/factories/document.output.factory.ts.txt

Listed in [core/factories](https://banes-lab.com/api/source/docs/core/factories.md), after [core/factories/document.factory.ts](https://banes-lab.com/source/docs/core/factories/document.factory.ts.md) and before [core/factories/environment.factory.ts](https://banes-lab.com/source/docs/core/factories/environment.factory.ts.md).

## Definitions

- `withoutRepoMetrics` (lexical_declaration, line 16)
- `LABEL_SEPARATOR` (lexical_declaration, line 14)
- `unchanged` (lexical_declaration, line 20)
- `membersBelow` (lexical_declaration, line 24)
- `prefix` (lexical_declaration, line 25)
- `SpecOutputs` (class_declaration, line 29, exported)
- `discoverAll` (public_field_definition, line 30, exported)
- `engine` (public_field_definition, line 31, exported)
- `formatMarkdown` (public_field_definition, line 32, exported)
- `root` (public_field_definition, line 33, exported)
- `constructor` (method_definition, line 35, exported)
- `moduleOutputs` (method_definition, line 42, exported)
- `members` (lexical_declaration, line 43, exported)
- `charts` (lexical_declaration, line 44, exported)
- `hasCharts` (lexical_declaration, line 45, exported)
- `composeReadme` (method_definition, line 56, exported)
- `body` (lexical_declaration, line 57, exported)
- `readmeSpecs` (method_definition, line 61, exported)
- `docLocation` (method_definition, line 76, exported)
- `documentSpec` (method_definition, line 87, exported)
- `rel` (lexical_declaration, line 88, exported)
- `documentSpecs` (method_definition, line 105, exported)
- `{ documents }` (lexical_declaration, line 106, exported)
- `chartSpecs` (method_definition, line 110, exported)

## Contained in

- [core/factories](https://banes-lab.com/anatomy/docs/folder-docs-core-factories.md)

## Uses

- [core/converters/layer.converter.ts](https://banes-lab.com/source/docs/core/converters/layer.converter.ts.md)

## Linked from

- [core/converters](https://banes-lab.com/anatomy/docs/folder-docs-core-converters.md)

## Source

```typescript
import { DRIFT_CODES, README_FILE } from "#configuration/constants/document.constants";
import type { DocModule, ModuleOutput, Spec, SpecEngine, SpecOutputsDeps } from "#types/document.output.types";
import { resolve, sep } from "node:path";
import type { Charts } from "#types/figure.types";
import type { DocumentDecl } from "#types/document.types";
import type { ManifestModule } from "#types/manifest.types";
import { REPO_METRICS_LAYER } from "#configuration/constants/layer.constants";
import { relativePath } from "@ssot/paths";
import { renderChartsFor } from "#core/coordinators/figure.coordinator";
import { stampGenerated } from "@govlab/canonical-write";
import { stripConcern } from "#core/converters/layer.converter";
import { validateCharts } from "#core/validators/diagram.validator";

const LABEL_SEPARATOR = "/";

const withoutRepoMetrics = function withoutRepoMetrics(text: string): string {
    return stripConcern(text, REPO_METRICS_LAYER);
};

const unchanged = function unchanged(text: string): string {
    return text;
};

const membersBelow = function membersBelow(all: readonly ManifestModule[], moduleDir: string): ManifestModule[] {
    const prefix = resolve(moduleDir) + sep;
    return all.filter((member) => resolve(member.dir).startsWith(prefix));
};

export class SpecOutputs {
    private readonly discoverAll: () => ManifestModule[];
    private readonly engine: SpecEngine;
    private readonly formatMarkdown: (markdown: string) => Promise<string>;
    private readonly root: string;

    public constructor(deps: SpecOutputsDeps) {
        this.discoverAll = deps.discoverAll;
        this.engine = deps.engine;
        this.formatMarkdown = deps.formatMarkdown;
        this.root = deps.root;
    }

    public async moduleOutputs(module: DocModule): Promise<ModuleOutput> {
        const members = membersBelow(this.discoverAll(), module.dir);
        const charts = await renderChartsFor(module.dir, module.label, members);
        const hasCharts = charts !== null;
        return {
            shared: { charts, hasCharts, isAggregate: members.length > 0 },
            specs: [
                ...this.readmeSpecs(module, hasCharts),
                ...this.documentSpecs(module),
                ...this.chartSpecs(module, charts),
            ],
        };
    }

    private async composeReadme(moduleDir: string, onDisk: string, hasCharts: boolean): Promise<string> {
        const body = await this.formatMarkdown(this.engine.generateReadme(moduleDir, hasCharts));
        return stampGenerated(body, onDisk, new Date(), withoutRepoMetrics);
    }

    private readmeSpecs(module: DocModule, hasCharts: boolean): Spec[] {
        if (module.manifest.docs === undefined) {
            return [];
        }
        return [
            {
                driftCode: DRIFT_CODES.readme,
                label: [module.label, README_FILE].join(LABEL_SEPARATOR),
                normalize: withoutRepoMetrics,
                path: resolve(module.dir, README_FILE),
                produce: async (onDisk) => this.composeReadme(module.dir, onDisk, hasCharts),
            },
        ];
    }

    private docLocation(doc: DocumentDecl): string | null {
        try {
            return this.engine.declaredDocLocation(doc);
        } catch (error) {
            if (!(error instanceof Error)) {
                throw error;
            }
            return null;
        }
    }

    private documentSpec(doc: DocumentDecl): Spec[] {
        const rel = this.docLocation(doc);
        if (rel === null) {
            return [];
        }
        return [
            {
                driftCode: DRIFT_CODES.doc,
                label: rel,
                mkdir: true,
                normalize: unchanged,
                path: resolve(this.root, rel),
                produce: async (onDisk) =>
                    stampGenerated(await this.formatMarkdown(this.engine.renderDeclaredDoc(doc)), onDisk, new Date()),
            },
        ];
    }

    private documentSpecs(module: DocModule): Spec[] {
        const { documents } = module.manifest;
        return Array.isArray(documents) ? documents.flatMap((doc) => this.documentSpec(doc)) : [];
    }

    private chartSpecs(module: DocModule, charts: Charts | null): Spec[] {
        if (charts === null) {
            return [];
        }
        return [
            {
                driftCode: DRIFT_CODES.charts,
                gate: validateCharts,
                gateCode: DRIFT_CODES.chartSyntax,
                harden: true,
                label: [module.label, relativePath("moduleInfo.charts")].join(LABEL_SEPARATOR),
                mkdir: true,
                normalize: unchanged,
                path: charts.path,
                produce: async (onDisk) =>
                    stampGenerated(await this.formatMarkdown(charts.content), onDisk, new Date()),
            },
        ];
    }
}
```
