852 lines
20 KiB
JavaScript
852 lines
20 KiB
JavaScript
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
const docusDir = path.join(repoRoot, 'docus');
|
|
const contentDir = path.join(docusDir, 'content');
|
|
|
|
const generatedPages = [
|
|
{
|
|
source: 'docs/FRAMEWORK_ARCHITECTURE.md',
|
|
target: '1.getting-started/1.architecture.md'
|
|
},
|
|
{
|
|
source: 'docs/MODULE_REFERENCE.md',
|
|
target: '1.getting-started/2.module-reference.md'
|
|
},
|
|
{
|
|
source: 'docs/DEVELOPMENT_GUIDE.md',
|
|
target: '1.getting-started/3.development.md'
|
|
},
|
|
{
|
|
source: 'docs/MISSION_DESIGNER_GUIDE.md',
|
|
target: '1.getting-started/4.mission-designer.md'
|
|
},
|
|
{
|
|
source: 'docs/PLAYER_GUIDE.md',
|
|
target: '1.getting-started/5.player-guide.md'
|
|
},
|
|
{
|
|
source: 'docs/surrealdb-setup.md',
|
|
target: '1.getting-started/6.surrealdb-setup.md'
|
|
},
|
|
{
|
|
source: 'arma/server/docs/README.md',
|
|
target: '2.server-extension/0.index.md'
|
|
},
|
|
{
|
|
source: 'arma/server/docs/api-reference.md',
|
|
target: '2.server-extension/1.api-reference.md'
|
|
},
|
|
{
|
|
source: 'arma/server/docs/usage-examples.md',
|
|
target: '2.server-extension/2.usage-examples.md'
|
|
},
|
|
{
|
|
source: 'arma/server/addons/common/README.md',
|
|
target: '2.server-extension/3.common.md'
|
|
},
|
|
{
|
|
source: 'docs/ICOM_USAGE_GUIDE.md',
|
|
target: '2.server-extension/4.icom.md'
|
|
},
|
|
{
|
|
source: 'docs/ACTOR_USAGE_GUIDE.md',
|
|
target: '3.server-modules/1.actor.md'
|
|
},
|
|
{
|
|
source: 'docs/BANK_USAGE_GUIDE.md',
|
|
target: '3.server-modules/2.bank.md'
|
|
},
|
|
{
|
|
source: 'docs/CAD_USAGE_GUIDE.md',
|
|
target: '3.server-modules/3.cad.md'
|
|
},
|
|
{
|
|
source: 'docs/ECONOMY_USAGE_GUIDE.md',
|
|
target: '3.server-modules/4.economy.md'
|
|
},
|
|
{
|
|
source: 'docs/GARAGE_USAGE_GUIDE.md',
|
|
target: '3.server-modules/5.garage.md'
|
|
},
|
|
{
|
|
source: 'docs/LOCKER_USAGE_GUIDE.md',
|
|
target: '3.server-modules/6.locker.md'
|
|
},
|
|
{
|
|
source: 'docs/ORG_USAGE_GUIDE.md',
|
|
target: '3.server-modules/7.organization.md'
|
|
},
|
|
{
|
|
source: 'docs/OWNED_STORAGE_USAGE_GUIDE.md',
|
|
target: '3.server-modules/8.owned-storage.md'
|
|
},
|
|
{
|
|
source: 'docs/PHONE_USAGE_GUIDE.md',
|
|
target: '3.server-modules/9.phone.md'
|
|
},
|
|
{
|
|
source: 'docs/STORE_USAGE_GUIDE.md',
|
|
target: '3.server-modules/10.store.md'
|
|
},
|
|
{
|
|
source: 'docs/TASK_USAGE_GUIDE.md',
|
|
target: '3.server-modules/11.task.md'
|
|
},
|
|
{
|
|
source: 'docs/CLIENT_USAGE_GUIDE.md',
|
|
target: '4.client-addons/0.index.md'
|
|
},
|
|
{
|
|
source: 'docs/CLIENT_MAIN_USAGE_GUIDE.md',
|
|
target: '4.client-addons/1.main.md'
|
|
},
|
|
{
|
|
source: 'docs/CLIENT_COMMON_USAGE_GUIDE.md',
|
|
target: '4.client-addons/2.common.md'
|
|
},
|
|
{
|
|
source: 'docs/CLIENT_ACTOR_USAGE_GUIDE.md',
|
|
target: '4.client-addons/3.actor.md'
|
|
},
|
|
{
|
|
source: 'docs/CLIENT_BANK_USAGE_GUIDE.md',
|
|
target: '4.client-addons/4.bank.md'
|
|
},
|
|
{
|
|
source: 'docs/CLIENT_CAD_USAGE_GUIDE.md',
|
|
target: '4.client-addons/5.cad.md'
|
|
},
|
|
{
|
|
source: 'docs/CLIENT_GARAGE_USAGE_GUIDE.md',
|
|
target: '4.client-addons/6.garage.md'
|
|
},
|
|
{
|
|
source: 'docs/CLIENT_LOCKER_USAGE_GUIDE.md',
|
|
target: '4.client-addons/7.locker.md'
|
|
},
|
|
{
|
|
source: 'docs/CLIENT_NOTIFICATIONS_USAGE_GUIDE.md',
|
|
target: '4.client-addons/8.notifications.md'
|
|
},
|
|
{
|
|
source: 'docs/CLIENT_ORG_USAGE_GUIDE.md',
|
|
target: '4.client-addons/9.organization.md'
|
|
},
|
|
{
|
|
source: 'docs/CLIENT_PHONE_USAGE_GUIDE.md',
|
|
target: '4.client-addons/10.phone.md'
|
|
},
|
|
{
|
|
source: 'docs/CLIENT_STORE_USAGE_GUIDE.md',
|
|
target: '4.client-addons/11.store.md'
|
|
}
|
|
];
|
|
|
|
const virtualRoutes = new Map([
|
|
['README.md', '/getting-started'],
|
|
['docs/README.md', '/getting-started']
|
|
]);
|
|
|
|
for (const page of generatedPages) {
|
|
virtualRoutes.set(toPosix(page.source), toRoute(page.target));
|
|
}
|
|
|
|
const staticFiles = [
|
|
{
|
|
target: 'index.md',
|
|
content: `---
|
|
seo:
|
|
title: Forge Framework Documentation
|
|
description: Documentation for the Forge Arma 3 framework, covering architecture, persistence, extension APIs, gameplay modules, and client UIs.
|
|
---
|
|
|
|
::u-page-hero
|
|
#title
|
|
Forge Framework Documentation
|
|
|
|
#description
|
|
Forge is a persistent Arma 3 framework that combines SQF addons, a Rust
|
|
\`arma-rs\` extension, SurrealDB persistence, shared domain crates, and
|
|
browser-backed player interfaces.
|
|
|
|
Use these docs to understand the runtime architecture, extension API surface,
|
|
server gameplay modules, and client addon integration patterns.
|
|
|
|
Server owners and developers must start SurrealDB and place a matching
|
|
\`config.toml\` beside \`forge_server_x64.dll\` before launching a
|
|
Forge-enabled server or local multiplayer test.
|
|
|
|
#links
|
|
:::u-button
|
|
---
|
|
color: primary
|
|
size: xl
|
|
to: /getting-started
|
|
trailing-icon: i-lucide-arrow-right
|
|
---
|
|
Start here
|
|
:::
|
|
|
|
:::u-button
|
|
---
|
|
color: neutral
|
|
icon: simple-icons-github
|
|
size: xl
|
|
to: https://github.com/InnovativeDevSolutions/forge
|
|
variant: outline
|
|
---
|
|
View source
|
|
:::
|
|
::
|
|
|
|
::u-page-section
|
|
#title
|
|
What Forge Covers
|
|
|
|
#features
|
|
:::u-page-feature
|
|
---
|
|
icon: i-lucide-boxes
|
|
---
|
|
#title
|
|
Domain [Modules]{.text-primary}
|
|
|
|
#description
|
|
Actor, bank, CAD, garage, locker, organization, phone, store, task, and
|
|
owned-storage workflows share a consistent service and extension model.
|
|
:::
|
|
|
|
:::u-page-feature
|
|
---
|
|
icon: i-lucide-server
|
|
---
|
|
#title
|
|
Rust [Extension]{.text-primary}
|
|
|
|
#description
|
|
The server extension keeps command parsing thin, routes domain requests into
|
|
services, and persists durable state through SurrealDB.
|
|
:::
|
|
|
|
:::u-page-feature
|
|
---
|
|
icon: i-lucide-database-zap
|
|
---
|
|
#title
|
|
Durable [Persistence]{.text-primary}
|
|
|
|
#description
|
|
Repository traits stay storage-agnostic while concrete adapters in the
|
|
extension handle schema and database mapping.
|
|
:::
|
|
|
|
:::u-page-feature
|
|
---
|
|
icon: i-lucide-monitor-smartphone
|
|
---
|
|
#title
|
|
Browser [UIs]{.text-primary}
|
|
|
|
#description
|
|
Client addons host web-based interfaces inside Arma displays and synchronize
|
|
state through namespaced browser bridge events.
|
|
:::
|
|
|
|
:::u-page-feature
|
|
---
|
|
icon: i-lucide-arrow-left-right
|
|
---
|
|
#title
|
|
Transport [Layer]{.text-primary}
|
|
|
|
#description
|
|
Large payloads move through chunked request and response transport while
|
|
smaller commands still use direct \`callExtension\` paths.
|
|
:::
|
|
|
|
:::u-page-feature
|
|
---
|
|
icon: i-lucide-wrench
|
|
---
|
|
#title
|
|
Development [Workflow]{.text-primary}
|
|
|
|
#description
|
|
The docs cover module boundaries, local validation checks, and where new
|
|
domain logic belongs across Rust, SQF, and web UI layers.
|
|
:::
|
|
::
|
|
|
|
::u-page-section
|
|
#title
|
|
Documentation Areas
|
|
|
|
#features
|
|
:::u-page-feature
|
|
---
|
|
icon: i-lucide-rocket
|
|
to: /getting-started
|
|
---
|
|
#title
|
|
[Getting Started]{.text-primary}
|
|
|
|
#description
|
|
Framework overview, architecture, module reference, and development rules.
|
|
:::
|
|
|
|
:::u-page-feature
|
|
---
|
|
icon: i-lucide-map
|
|
to: /getting-started/mission-designer
|
|
---
|
|
#title
|
|
Mission [Designers]{.text-primary}
|
|
|
|
#description
|
|
Eden object placement, garage markers, and CAD-compatible task setup.
|
|
:::
|
|
|
|
:::u-page-feature
|
|
---
|
|
icon: i-lucide-server-cog
|
|
to: /server-extension
|
|
---
|
|
#title
|
|
Server [Extension]{.text-primary}
|
|
|
|
#description
|
|
Extension architecture, command surface, and SQF usage examples.
|
|
:::
|
|
|
|
:::u-page-feature
|
|
---
|
|
icon: i-lucide-network
|
|
to: /server-extension/icom
|
|
---
|
|
#title
|
|
ICOM [Events]{.text-primary}
|
|
|
|
#description
|
|
Inter-server event routing through the Forge ICOM hub and extension commands.
|
|
:::
|
|
|
|
:::u-page-feature
|
|
---
|
|
icon: i-lucide-layers-3
|
|
to: /server-modules
|
|
---
|
|
#title
|
|
Server [Modules]{.text-primary}
|
|
|
|
#description
|
|
Gameplay-domain usage guides for persistence, hot state, and command flows.
|
|
:::
|
|
|
|
:::u-page-feature
|
|
---
|
|
icon: i-lucide-monitor-smartphone
|
|
to: /client-addons
|
|
---
|
|
#title
|
|
Client [Addons]{.text-primary}
|
|
|
|
#description
|
|
Browser bridge, client UX entry points, and addon-specific event contracts.
|
|
:::
|
|
::
|
|
`
|
|
},
|
|
{
|
|
target: '1.getting-started/.navigation.yml',
|
|
content: `title: Getting Started
|
|
icon: i-lucide-rocket
|
|
`
|
|
},
|
|
{
|
|
target: '1.getting-started/0.index.md',
|
|
content: `---
|
|
title: Getting Started
|
|
description: Use this section as the main entry point for the Forge framework.
|
|
---
|
|
|
|
Forge combines:
|
|
|
|
- Arma 3 client addons for UX and browser-hosted interfaces
|
|
- Arma 3 server addons for mission integration and authoritative flow control
|
|
- a Rust server extension for command routing and persistence
|
|
- shared Rust crates for models, repositories, and services
|
|
- SurrealDB for durable storage
|
|
|
|
## Launch Prerequisites
|
|
|
|
Before starting a Forge-enabled dedicated server or local multiplayer test,
|
|
server owners and developers must start SurrealDB and make sure
|
|
\`config.toml\` is beside \`forge_server_x64.dll\`. The config values must match
|
|
the running SurrealDB endpoint, namespace, database, username, and password.
|
|
|
|
Mission designers and players do not need their own SurrealDB instance unless
|
|
they are hosting locally, but the server they join must have these prerequisites
|
|
ready.
|
|
|
|
## Common Commands
|
|
|
|
\`\`\`powershell
|
|
cargo test
|
|
npm run build:webui
|
|
.\\build-arma.ps1
|
|
\`\`\`
|
|
|
|
## Start Here
|
|
|
|
::u-page-grid
|
|
:::u-page-card
|
|
---
|
|
icon: i-lucide-network
|
|
title: Architecture
|
|
to: /getting-started/architecture
|
|
---
|
|
Understand how SQF, Rust services, SurrealDB, and browser UIs fit together.
|
|
:::
|
|
|
|
:::u-page-card
|
|
---
|
|
icon: i-lucide-boxes
|
|
title: Module Reference
|
|
to: /getting-started/module-reference
|
|
---
|
|
Review gameplay domains, infrastructure modules, and extension command groups.
|
|
:::
|
|
|
|
:::u-page-card
|
|
---
|
|
icon: i-lucide-wrench
|
|
title: Development Guide
|
|
to: /getting-started/development
|
|
---
|
|
See the rules for adding modules and changing boundaries without regressions.
|
|
:::
|
|
|
|
:::u-page-card
|
|
---
|
|
icon: i-lucide-map
|
|
title: Mission Designer Guide
|
|
to: /getting-started/mission-designer
|
|
---
|
|
Place Eden interaction objects, garage markers, and Forge task modules for
|
|
playable missions.
|
|
:::
|
|
|
|
:::u-page-card
|
|
---
|
|
icon: i-lucide-user-round-check
|
|
title: Player Guide
|
|
to: /getting-started/player-guide
|
|
---
|
|
Learn the player-facing CAD, phone, bank, store, locker, garage, and economy
|
|
workflows.
|
|
:::
|
|
|
|
:::u-page-card
|
|
---
|
|
icon: i-lucide-database
|
|
title: SurrealDB Setup
|
|
to: /getting-started/surrealdb-setup
|
|
---
|
|
Install SurrealDB, match Forge config values, and choose the right setup path
|
|
for developers or admin-facing roles.
|
|
:::
|
|
|
|
:::u-page-card
|
|
---
|
|
icon: i-lucide-server-cog
|
|
title: Server Extension
|
|
to: /server-extension
|
|
---
|
|
Follow the extension architecture, API surface, and SQF usage examples.
|
|
:::
|
|
|
|
:::u-page-card
|
|
---
|
|
icon: i-lucide-layers-3
|
|
title: Server Modules
|
|
to: /server-modules
|
|
---
|
|
Dive into the actor, bank, CAD, garage, locker, organization, phone, store,
|
|
task, and owned-storage guides.
|
|
:::
|
|
|
|
:::u-page-card
|
|
---
|
|
icon: i-lucide-monitor-smartphone
|
|
title: Client Addons
|
|
to: /client-addons
|
|
---
|
|
Explore the client bridge model and addon-specific browser integration rules.
|
|
:::
|
|
::
|
|
`
|
|
},
|
|
{
|
|
target: '2.server-extension/.navigation.yml',
|
|
content: `title: Forge Server Extension
|
|
icon: i-lucide-server-cog
|
|
`
|
|
},
|
|
{
|
|
target: '3.server-modules/.navigation.yml',
|
|
content: `title: Server Modules
|
|
icon: i-lucide-layers-3
|
|
`
|
|
},
|
|
{
|
|
target: '3.server-modules/0.index.md',
|
|
content: `---
|
|
title: Server Module Guides
|
|
description: These pages document the authoritative server-side workflows in Forge.
|
|
---
|
|
|
|
Most modules follow the same shape:
|
|
|
|
1. Server SQF gathers game context and validates mission/runtime assumptions.
|
|
2. The \`forge_server\` extension routes the request into the matching command group.
|
|
3. Services apply business rules through storage-agnostic repository traits.
|
|
4. The extension persists durable state through SurrealDB adapters when needed.
|
|
|
|
## Gameplay Domains
|
|
|
|
::u-page-grid
|
|
:::u-page-card
|
|
---
|
|
icon: i-lucide-user-round
|
|
title: Actor
|
|
to: /server-modules/actor
|
|
---
|
|
Persistent player identity, position, loadout, contact fields, and hot state.
|
|
:::
|
|
|
|
:::u-page-card
|
|
---
|
|
icon: i-lucide-wallet
|
|
title: Bank
|
|
to: /server-modules/bank
|
|
---
|
|
Player funds, transfers, PIN validation, checkout charging, and bank hot state.
|
|
:::
|
|
|
|
:::u-page-card
|
|
---
|
|
icon: i-lucide-map
|
|
title: CAD
|
|
to: /server-modules/cad
|
|
---
|
|
Dispatch requests, assignments, profiles, grouped state, and hydrated views.
|
|
:::
|
|
|
|
:::u-page-card
|
|
---
|
|
icon: i-lucide-ambulance
|
|
title: Economy
|
|
to: /server-modules/economy
|
|
---
|
|
Fuel, service, and medical charging rules across player and organization funds.
|
|
:::
|
|
|
|
:::u-page-card
|
|
---
|
|
icon: i-lucide-car-front
|
|
title: Garage
|
|
to: /server-modules/garage
|
|
---
|
|
Vehicle storage, hot-state updates, and persistence of vehicle condition.
|
|
:::
|
|
|
|
:::u-page-card
|
|
---
|
|
icon: i-lucide-package
|
|
title: Locker
|
|
to: /server-modules/locker
|
|
---
|
|
Player inventory storage, unique item limits, and locker hot-state behavior.
|
|
:::
|
|
|
|
:::u-page-card
|
|
---
|
|
icon: i-lucide-building-2
|
|
title: Organization
|
|
to: /server-modules/organization
|
|
---
|
|
Membership, treasury, shared assets, fleet, and organization hot workflows.
|
|
:::
|
|
|
|
:::u-page-card
|
|
---
|
|
icon: i-lucide-key-round
|
|
title: Owned Storage
|
|
to: /server-modules/owned-storage
|
|
---
|
|
Owner-scoped locker and vehicle unlock storage used by org-linked features.
|
|
:::
|
|
|
|
:::u-page-card
|
|
---
|
|
icon: i-lucide-smartphone
|
|
title: Phone
|
|
to: /server-modules/phone
|
|
---
|
|
Contacts, message threads, and email state for in-game phone workflows.
|
|
:::
|
|
|
|
:::u-page-card
|
|
---
|
|
icon: i-lucide-shopping-cart
|
|
title: Store
|
|
to: /server-modules/store
|
|
---
|
|
Checkout orchestration across pricing, grants, payment sources, and rollback.
|
|
:::
|
|
|
|
:::u-page-card
|
|
---
|
|
icon: i-lucide-flag
|
|
title: Task
|
|
to: /server-modules/task
|
|
---
|
|
Task catalog, ownership, status transitions, defuse counters, and rewards.
|
|
:::
|
|
::
|
|
`
|
|
},
|
|
{
|
|
target: '4.client-addons/.navigation.yml',
|
|
content: `title: Client Addons
|
|
icon: i-lucide-monitor-smartphone
|
|
`
|
|
}
|
|
];
|
|
|
|
await fs.rm(contentDir, { recursive: true, force: true });
|
|
await fs.mkdir(contentDir, { recursive: true });
|
|
|
|
for (const file of staticFiles) {
|
|
await writeContentFile(file.target, file.content);
|
|
}
|
|
|
|
for (const page of generatedPages) {
|
|
const sourceRel = toPosix(page.source);
|
|
const sourcePath = path.join(repoRoot, page.source);
|
|
const rawContent = await fs.readFile(sourcePath, 'utf8');
|
|
const content = prepareGeneratedPageContent(rewriteMarkdownLinks(rawContent, sourceRel));
|
|
await writeContentFile(page.target, content);
|
|
}
|
|
|
|
console.log(`Generated ${staticFiles.length + generatedPages.length} Docus content files.`);
|
|
|
|
function rewriteMarkdownLinks(content, sourceRel) {
|
|
const sourceDir = path.posix.dirname(sourceRel);
|
|
|
|
return content.replace(/\]\(([^)]+)\)/g, (match, rawTarget) => {
|
|
if (
|
|
rawTarget.startsWith('http://') ||
|
|
rawTarget.startsWith('https://') ||
|
|
rawTarget.startsWith('#') ||
|
|
rawTarget.startsWith('mailto:')
|
|
) {
|
|
return match;
|
|
}
|
|
|
|
const [targetPath, targetHash] = rawTarget.split('#');
|
|
if (!targetPath || !targetPath.toLowerCase().endsWith('.md')) {
|
|
return match;
|
|
}
|
|
|
|
const normalizedTarget = toPosix(
|
|
path.posix.normalize(path.posix.join(sourceDir, targetPath.replace(/\\/g, '/')))
|
|
);
|
|
const route = virtualRoutes.get(normalizedTarget);
|
|
if (!route) {
|
|
return match;
|
|
}
|
|
|
|
return `](${route}${targetHash ? `#${targetHash}` : ''})`;
|
|
});
|
|
}
|
|
|
|
function prepareGeneratedPageContent(content) {
|
|
const title = extractFirstH1(content);
|
|
const description = extractLeadParagraph(content);
|
|
const body = stripMatchingLeadParagraph(stripFirstH1(content), description).trimStart();
|
|
const frontmatter = [
|
|
'---',
|
|
title ? `title: ${yamlString(title)}` : undefined,
|
|
description ? `description: ${yamlString(description)}` : undefined,
|
|
'---'
|
|
].filter(Boolean).join('\n');
|
|
|
|
return `${frontmatter}\n\n${body}`;
|
|
}
|
|
|
|
function extractFirstH1(content) {
|
|
const match = content.match(/^#\s+(.+?)\s*#*\s*$/m);
|
|
return match ? match[1].trim() : '';
|
|
}
|
|
|
|
function extractLeadParagraph(content) {
|
|
const lines = stripFirstH1(content).split(/\r?\n/);
|
|
|
|
for (let index = 0; index < lines.length; index += 1) {
|
|
const line = lines[index].trim();
|
|
if (!line) {
|
|
continue;
|
|
}
|
|
|
|
if (!isParagraphStart(line)) {
|
|
continue;
|
|
}
|
|
|
|
const paragraph = [];
|
|
for (let paragraphIndex = index; paragraphIndex < lines.length; paragraphIndex += 1) {
|
|
const paragraphLine = lines[paragraphIndex].trim();
|
|
if (!paragraphLine) {
|
|
break;
|
|
}
|
|
|
|
paragraph.push(paragraphLine);
|
|
}
|
|
|
|
return normalizeParagraph(paragraph.join(' '));
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
function stripFirstH1(content) {
|
|
const lines = content.split(/\r?\n/);
|
|
const headingIndex = lines.findIndex((line) => /^#\s+.+/.test(line.trim()));
|
|
if (headingIndex === -1) {
|
|
return content;
|
|
}
|
|
|
|
lines.splice(headingIndex, 1);
|
|
while (headingIndex < lines.length && !lines[headingIndex].trim()) {
|
|
lines.splice(headingIndex, 1);
|
|
}
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
function stripMatchingLeadParagraph(content, description) {
|
|
if (!description) {
|
|
return content;
|
|
}
|
|
|
|
const lines = content.split(/\r?\n/);
|
|
let startIndex = 0;
|
|
while (startIndex < lines.length && !lines[startIndex].trim()) {
|
|
startIndex += 1;
|
|
}
|
|
|
|
if (startIndex < lines.length && /^##\s+overview\s*$/i.test(lines[startIndex].trim())) {
|
|
const sectionStart = startIndex;
|
|
startIndex += 1;
|
|
while (startIndex < lines.length && !lines[startIndex].trim()) {
|
|
startIndex += 1;
|
|
}
|
|
|
|
let sectionEnd = startIndex;
|
|
const sectionLines = [];
|
|
while (sectionEnd < lines.length && !/^#{2,}\s+/.test(lines[sectionEnd].trim())) {
|
|
if (lines[sectionEnd].trim()) {
|
|
sectionLines.push(lines[sectionEnd].trim());
|
|
}
|
|
|
|
sectionEnd += 1;
|
|
}
|
|
|
|
if (normalizeParagraph(sectionLines.join(' ')) === description) {
|
|
while (sectionEnd < lines.length && !lines[sectionEnd].trim()) {
|
|
sectionEnd += 1;
|
|
}
|
|
|
|
return [...lines.slice(0, sectionStart), ...lines.slice(sectionEnd)].join('\n');
|
|
}
|
|
|
|
startIndex = sectionStart;
|
|
}
|
|
|
|
if (startIndex >= lines.length || !isParagraphStart(lines[startIndex].trim())) {
|
|
return content;
|
|
}
|
|
|
|
let endIndex = startIndex;
|
|
const paragraph = [];
|
|
while (endIndex < lines.length && lines[endIndex].trim()) {
|
|
paragraph.push(lines[endIndex].trim());
|
|
endIndex += 1;
|
|
}
|
|
|
|
if (normalizeParagraph(paragraph.join(' ')) !== description) {
|
|
return content;
|
|
}
|
|
|
|
while (endIndex < lines.length && !lines[endIndex].trim()) {
|
|
endIndex += 1;
|
|
}
|
|
|
|
return [...lines.slice(0, startIndex), ...lines.slice(endIndex)].join('\n');
|
|
}
|
|
|
|
function isParagraphStart(line) {
|
|
return !(
|
|
line.startsWith('#') ||
|
|
line.startsWith('![') ||
|
|
line.startsWith('```') ||
|
|
line.startsWith(':::') ||
|
|
line.startsWith('::') ||
|
|
line.startsWith('|') ||
|
|
/^[-*+]\s+/.test(line) ||
|
|
/^\d+\.\s+/.test(line)
|
|
);
|
|
}
|
|
|
|
function normalizeParagraph(value) {
|
|
return value.replace(/\s+/g, ' ').trim();
|
|
}
|
|
|
|
function yamlString(value) {
|
|
return JSON.stringify(value);
|
|
}
|
|
|
|
function toRoute(target) {
|
|
const withoutExt = toPosix(target).replace(/\.md$/i, '');
|
|
const parts = withoutExt.split('/');
|
|
|
|
if (parts.length === 1 && parts[0] === 'index') {
|
|
return '/';
|
|
}
|
|
|
|
const mapped = parts
|
|
.map((part, index) => {
|
|
if (index === parts.length - 1 && (part === '0.index' || part === 'index')) {
|
|
return '';
|
|
}
|
|
|
|
return part.replace(/^\d+\./, '');
|
|
})
|
|
.filter(Boolean);
|
|
|
|
return `/${mapped.join('/')}`;
|
|
}
|
|
|
|
function toPosix(value) {
|
|
return value.replace(/\\/g, '/');
|
|
}
|
|
|
|
async function writeContentFile(target, content) {
|
|
const targetPath = path.join(contentDir, target);
|
|
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
await fs.writeFile(targetPath, content.endsWith('\n') ? content : `${content}\n`, 'utf8');
|
|
}
|