Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
8 changes: 8 additions & 0 deletions docs/app/_llms/__fixtures__/component-grid/basic.output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## Buttons

- [Action Button](https://example.com/llms/docs/components/action-button.txt) — 기본 인터랙션 컴포넌트입니다.
- [Floating Action Button](https://example.com/llms/docs/components/floating-action-button.txt)

## Controls

- [Checkbox](https://example.com/llms/docs/components/checkbox.txt) — 옵션 선택 컴포넌트입니다.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<SomethingElse />
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<SomethingElse />
66 changes: 66 additions & 0 deletions docs/app/_llms/rules/component-grid-rule.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, expect, it } from "bun:test";
import { normalizeLLMBodyWithRules } from "../normalize-llm-body";
import { normalizeForAssert, readFixture } from "../test-utils";
import {
buildMarkdown,
componentGridRule,
isDeprecatedValue,
type ComponentEntry,
} from "./component-grid-rule";

const sampleEntries: ComponentEntry[] = [
{
category: "Controls",
title: "Checkbox",
description: "옵션 선택 컴포넌트입니다.",
url: "https://example.com/llms/docs/components/checkbox.txt",
},
{
category: "Buttons",
title: "Floating Action Button",
description: "",
url: "https://example.com/llms/docs/components/floating-action-button.txt",
},
{
category: "Buttons",
title: "Action Button",
description: "기본 인터랙션 컴포넌트입니다.",
url: "https://example.com/llms/docs/components/action-button.txt",
},
];

describe("componentGridRule", () => {
it("renders categorized markdown from entries", () => {
const expected = readFixture("component-grid", "basic.output.md");

const actual = buildMarkdown(sampleEntries);

expect(normalizeForAssert(actual)).toBe(normalizeForAssert(expected));
});

it("non-target node passthrough", () => {
const input = readFixture("component-grid", "passthrough.input.mdx");
const expected = readFixture("component-grid", "passthrough.output.mdx");

const actual = normalizeLLMBodyWithRules(input, [componentGridRule]);

expect(normalizeForAssert(actual)).toBe(normalizeForAssert(expected));
});

describe("isDeprecatedValue", () => {
it("treats message strings as deprecated", () => {
expect(isDeprecatedValue("더 이상 사용되지 않습니다.")).toBe(true);
expect(isDeprecatedValue("true")).toBe(true);
expect(isDeprecatedValue("yes")).toBe(true);
});

it("treats explicit false/empty as not deprecated", () => {
expect(isDeprecatedValue(undefined)).toBe(false);
expect(isDeprecatedValue("")).toBe(false);
expect(isDeprecatedValue(" ")).toBe(false);
expect(isDeprecatedValue("false")).toBe(false);
expect(isDeprecatedValue("False")).toBe(false);
expect(isDeprecatedValue("no")).toBe(false);
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
131 changes: 131 additions & 0 deletions docs/app/_llms/rules/component-grid-rule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { MdxJsxFlowElement } from "mdast-util-mdx-jsx";
import { baseUrl } from "@/app/metadata";
import { getLLMMarkdownUrl } from "../config";
import type { Rule } from "./types";

export interface ComponentEntry {
category: string;
title: string;
description: string;
url: string;
}

export function isDeprecatedValue(value: string | undefined): boolean {
if (value === undefined) return false;
const normalized = value.trim().toLowerCase();
if (normalized === "") return false;
return normalized !== "false" && normalized !== "no";
}

function resolveComponentsDir(): string | null {
const candidates = [
path.resolve(process.cwd(), "content/docs/components"),
path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../content/docs/components"),
];
for (const candidate of candidates) {
if (fs.existsSync(candidate)) return candidate;
}
return null;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function parseFrontmatter(source: string): Record<string, string> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

언젠가는 gray-matter로 다 갈아 버리시죠..!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0cb61b8 이전 PR 에서 gray-matter 도입해서 바로 수정해뒀어요.
gray-matter 스크립트 쪽만 적용해뒀는데 더 rule 파싱 로직에도 적용해볼 수 있겠네요

const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!match) return {};
const result: Record<string, string> = {};
for (const line of match[1].split(/\r?\n/)) {
const colon = line.indexOf(":");
if (colon === -1) continue;
const key = line.slice(0, colon).trim();
const value = line
.slice(colon + 1)
.trim()
.replace(/^["']|["']$/g, "");
if (key) result[key] = value;
}
return result;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

function titleCase(value: string): string {
return value.charAt(0).toUpperCase() + value.slice(1);
}

function loadEntries(): ComponentEntry[] {
const componentsDir = resolveComponentsDir();
if (!componentsDir) return [];

const entries: ComponentEntry[] = [];
for (const dirent of fs.readdirSync(componentsDir, { withFileTypes: true })) {
if (!dirent.isDirectory()) continue;
const match = dirent.name.match(/^\(([^)]+)\)$/);
if (!match) continue;

const category = titleCase(match[1]);
const categoryDir = path.join(componentsDir, dirent.name);

for (const file of fs.readdirSync(categoryDir)) {
if (!file.endsWith(".mdx")) continue;

const source = fs.readFileSync(path.join(categoryDir, file), "utf8");
const fm = parseFrontmatter(source);
if (isDeprecatedValue(fm.deprecated)) continue;

const slug = file.slice(0, -".mdx".length);
entries.push({
category,
title: fm.title ?? slug,
description: fm.description ?? "",
url: new URL(getLLMMarkdownUrl("docs", ["components", slug]), baseUrl).toString(),
});
}
}
return entries;
}

let cachedEntries: ComponentEntry[] | null = null;

function getEntries(): ComponentEntry[] {
if (cachedEntries === null) cachedEntries = loadEntries();
return cachedEntries;
}

export function buildMarkdown(entries: ComponentEntry[]): string {
const grouped = new Map<string, ComponentEntry[]>();
for (const entry of entries) {
if (!grouped.has(entry.category)) grouped.set(entry.category, []);
grouped.get(entry.category)!.push(entry);
}
for (const list of grouped.values()) {
list.sort((a, b) => a.title.localeCompare(b.title));
}

const sections: string[] = [];
for (const [category, list] of Array.from(grouped.entries()).sort(([a], [b]) =>
a.localeCompare(b),
)) {
const lines = [`## ${category}`, ""];
for (const entry of list) {
const suffix = entry.description ? ` — ${entry.description}` : "";
lines.push(`- [${entry.title}](${entry.url})${suffix}`);
}
sections.push(lines.join("\n"));
}
return sections.join("\n\n");
}

export const componentGridRule: Rule = {
name: "ComponentGrid",
match: (node): node is MdxJsxFlowElement =>
node.type === "mdxJsxFlowElement" && node.name === "ComponentGrid",
transform: (node) => {
try {
const entries = getEntries();
if (entries.length === 0) return [node];
return [{ type: "html", value: buildMarkdown(entries) }];
} catch {
return [node];
}
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};
3 changes: 3 additions & 0 deletions docs/app/_llms/rules/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { changelogPageRule } from "./changelog-page-rule";
import { codeBlockTabsRule } from "./codeblock-tabs-rule";
import { componentExampleRule } from "./component-example-rule";
import { componentGridRule } from "./component-grid-rule";
import { platformStatusRule } from "./platform-status-rule";
import { typeTableRule } from "./type-table-rule";
import { tokenReferenceRule } from "./token-reference-rule";
Expand All @@ -15,6 +16,7 @@ export const activeRules: Rule[] = [
tokenReferenceRule,
platformStatusRule,
iconLibraryRule,
componentGridRule,
componentSpecBlockRule,
changelogPageRule,
];
Expand All @@ -23,6 +25,7 @@ export {
changelogPageRule,
codeBlockTabsRule,
componentExampleRule,
componentGridRule,
typeTableRule,
tokenReferenceRule,
componentSpecBlockRule,
Expand Down
Loading