Add remaining samples, tooling, and local project assets
This commit is contained in:
175
samples/src/lib/aptabase.debug.js
Normal file
175
samples/src/lib/aptabase.debug.js
Normal file
@@ -0,0 +1,175 @@
|
||||
"use strict";
|
||||
var aptabase = (() => {
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/index.ts
|
||||
var src_exports = {};
|
||||
__export(src_exports, {
|
||||
init: () => init,
|
||||
trackEvent: () => trackEvent
|
||||
});
|
||||
|
||||
// ../shared.ts
|
||||
var defaultLocale;
|
||||
var defaultIsDebug;
|
||||
var isInBrowser = typeof window !== "undefined" && typeof window.fetch !== "undefined";
|
||||
var isInBrowserExtension = typeof chrome !== "undefined" && !!chrome.runtime?.id;
|
||||
var _sessionId = newSessionId();
|
||||
var _lastTouched = /* @__PURE__ */ new Date();
|
||||
var _hosts = {
|
||||
US: "https://us.aptabase.com",
|
||||
EU: "https://eu.aptabase.com",
|
||||
DEV: "https://localhost:3000",
|
||||
SH: ""
|
||||
};
|
||||
function inMemorySessionId(timeout) {
|
||||
let now = /* @__PURE__ */ new Date();
|
||||
const diffInMs = now.getTime() - _lastTouched.getTime();
|
||||
const diffInSec = Math.floor(diffInMs / 1e3);
|
||||
if (diffInSec > timeout) {
|
||||
_sessionId = newSessionId();
|
||||
}
|
||||
_lastTouched = now;
|
||||
return _sessionId;
|
||||
}
|
||||
function newSessionId() {
|
||||
const epochInSeconds = Math.floor(Date.now() / 1e3).toString();
|
||||
const random = Math.floor(Math.random() * 1e8).toString().padStart(8, "0");
|
||||
return epochInSeconds + random;
|
||||
}
|
||||
function validateAppKey(appKey) {
|
||||
const parts = appKey.split("-");
|
||||
if (parts.length !== 3 || _hosts[parts[1]] === void 0) {
|
||||
console.warn(`The Aptabase App Key "${appKey}" is invalid. Tracking will be disabled.`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function getApiUrl(appKey, options) {
|
||||
const region = appKey.split("-")[1];
|
||||
if (region === "SH") {
|
||||
if (!options?.host) {
|
||||
console.warn(`Host parameter must be defined when using Self-Hosted App Key. Tracking will be disabled.`);
|
||||
return;
|
||||
}
|
||||
return `${options.host}/api/v0/event`;
|
||||
}
|
||||
const host = options?.host ?? _hosts[region];
|
||||
return `${host}/api/v0/event`;
|
||||
}
|
||||
async function sendEvent(opts) {
|
||||
if (!isInBrowser && !isInBrowserExtension) {
|
||||
console.warn(`Aptabase: trackEvent requires a browser environment. Event "${opts.eventName}" will be discarded.`);
|
||||
return;
|
||||
}
|
||||
if (!opts.appKey) {
|
||||
console.warn(`Aptabase: init must be called before trackEvent. Event "${opts.eventName}" will be discarded.`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(opts.apiUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"App-Key": opts.appKey
|
||||
},
|
||||
credentials: "omit",
|
||||
body: JSON.stringify({
|
||||
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
||||
sessionId: opts.sessionId,
|
||||
eventName: opts.eventName,
|
||||
systemProps: {
|
||||
locale: opts.locale ?? getBrowserLocale(),
|
||||
isDebug: opts.isDebug ?? getIsDebug(),
|
||||
appVersion: opts.appVersion ?? "",
|
||||
sdkVersion: opts.sdkVersion
|
||||
},
|
||||
props: opts.props
|
||||
})
|
||||
});
|
||||
if (response.status >= 300) {
|
||||
const responseBody = await response.text();
|
||||
console.warn(`Failed to send event "${opts.eventName}": ${response.status} ${responseBody}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Failed to send event "${opts.eventName}"`);
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
function getBrowserLocale() {
|
||||
if (defaultLocale) {
|
||||
return defaultLocale;
|
||||
}
|
||||
if (typeof navigator === "undefined") {
|
||||
return void 0;
|
||||
}
|
||||
if (navigator.languages.length > 0) {
|
||||
defaultLocale = navigator.languages[0];
|
||||
} else {
|
||||
defaultLocale = navigator.language;
|
||||
}
|
||||
return defaultLocale;
|
||||
}
|
||||
function getIsDebug() {
|
||||
if (defaultIsDebug !== void 0) {
|
||||
return defaultIsDebug;
|
||||
}
|
||||
if (true) {
|
||||
defaultIsDebug = true;
|
||||
return defaultIsDebug;
|
||||
}
|
||||
if (typeof location === "undefined") {
|
||||
defaultIsDebug = false;
|
||||
return defaultIsDebug;
|
||||
}
|
||||
defaultIsDebug = location.hostname === "localhost";
|
||||
return defaultIsDebug;
|
||||
}
|
||||
|
||||
// src/index.ts
|
||||
var SESSION_TIMEOUT = 1 * 60 * 60;
|
||||
var sdkVersion = `aptabase-web@${"0.4.3"}`;
|
||||
var _appKey = "";
|
||||
var _apiUrl;
|
||||
var _options;
|
||||
function init(appKey, options) {
|
||||
if (!validateAppKey(appKey))
|
||||
return;
|
||||
_apiUrl = options?.apiUrl ?? getApiUrl(appKey, options);
|
||||
_appKey = appKey;
|
||||
_options = options;
|
||||
}
|
||||
async function trackEvent(eventName, props) {
|
||||
if (!_apiUrl)
|
||||
return;
|
||||
const sessionId = inMemorySessionId(SESSION_TIMEOUT);
|
||||
await sendEvent({
|
||||
apiUrl: _apiUrl,
|
||||
sessionId,
|
||||
appKey: _appKey,
|
||||
isDebug: _options?.isDebug,
|
||||
appVersion: _options?.appVersion,
|
||||
sdkVersion,
|
||||
eventName,
|
||||
props
|
||||
});
|
||||
}
|
||||
return __toCommonJS(src_exports);
|
||||
})();
|
||||
//# sourceMappingURL=aptabase.debug.js.map
|
||||
2
samples/src/lib/aptabase.min.js
vendored
Normal file
2
samples/src/lib/aptabase.min.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
"use strict";var aptabase=(()=>{var a=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var y=Object.getOwnPropertyNames;var S=Object.prototype.hasOwnProperty;var I=(e,n)=>{for(var t in n)a(e,t,{get:n[t],enumerable:!0})},A=(e,n,t,o)=>{if(n&&typeof n=="object"||typeof n=="function")for(let s of y(n))!S.call(e,s)&&s!==t&&a(e,s,{get:()=>n[s],enumerable:!(o=w(n,s))||o.enumerable});return e};var E=e=>A(a({},"__esModule",{value:!0}),e);var k={};I(k,{init:()=>T,trackEvent:()=>$});var r,i,D=typeof window<"u"&&typeof window.fetch<"u",O=typeof chrome<"u"&&!!chrome.runtime?.id,p=g(),c=new Date,u={US:"https://us.aptabase.com",EU:"https://eu.aptabase.com",DEV:"https://localhost:3000",SH:""};function f(e){let n=new Date,t=n.getTime()-c.getTime();return Math.floor(t/1e3)>e&&(p=g()),c=n,p}function g(){let e=Math.floor(Date.now()/1e3).toString(),n=Math.floor(Math.random()*1e8).toString().padStart(8,"0");return e+n}function b(e){let n=e.split("-");return n.length!==3||u[n[1]]===void 0?(console.warn(`The Aptabase App Key "${e}" is invalid. Tracking will be disabled.`),!1):!0}function m(e,n){let t=e.split("-")[1];if(t==="SH"){if(!n?.host){console.warn("Host parameter must be defined when using Self-Hosted App Key. Tracking will be disabled.");return}return`${n.host}/api/v0/event`}return`${n?.host??u[t]}/api/v0/event`}async function v(e){if(!D&&!O){console.warn(`Aptabase: trackEvent requires a browser environment. Event "${e.eventName}" will be discarded.`);return}if(!e.appKey){console.warn(`Aptabase: init must be called before trackEvent. Event "${e.eventName}" will be discarded.`);return}try{let n=await fetch(e.apiUrl,{method:"POST",headers:{"Content-Type":"application/json","App-Key":e.appKey},credentials:"omit",body:JSON.stringify({timestamp:new Date().toISOString(),sessionId:e.sessionId,eventName:e.eventName,systemProps:{locale:e.locale??V(),isDebug:e.isDebug??N(),appVersion:e.appVersion??"",sdkVersion:e.sdkVersion},props:e.props})});if(n.status>=300){let t=await n.text();console.warn(`Failed to send event "${e.eventName}": ${n.status} ${t}`)}}catch(n){console.warn(`Failed to send event "${e.eventName}"`),console.warn(n)}}function V(){if(r)return r;if(!(typeof navigator>"u"))return navigator.languages.length>0?r=navigator.languages[0]:r=navigator.language,r}function N(){return i!==void 0||(i=!0),i}var x=1*60*60,U="aptabase-web@0.4.3",h="",d,l;function T(e,n){b(e)&&(d=n?.apiUrl??m(e,n),h=e,l=n)}async function $(e,n){if(!d)return;let t=f(x);await v({apiUrl:d,sessionId:t,appKey:h,isDebug:l?.isDebug,appVersion:l?.appVersion,sdkVersion:U,eventName:e,props:n})}return E(k);})();
|
||||
//# sourceMappingURL=aptabase.min.js.map
|
||||
17
samples/src/lib/componentLoader.ts
Normal file
17
samples/src/lib/componentLoader.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// 상대 경로를 정확하게 맞춰야 합니다
|
||||
// utils 폴더에서 content/sub로 가는 경로
|
||||
const allComponents = import.meta.glob("../content/sub/**/*.astro");
|
||||
|
||||
export async function loadComponent(path: string) {
|
||||
// path 앞에 ../ 추가
|
||||
const componentPath = `../content/sub/${path}.astro`;
|
||||
const loader = allComponents[componentPath];
|
||||
|
||||
if (!loader) {
|
||||
console.error("Available paths:", Object.keys(allComponents));
|
||||
console.error("Requested path:", componentPath);
|
||||
throw new Error(`Component not found: ${componentPath}`);
|
||||
}
|
||||
|
||||
return (await loader()).default;
|
||||
}
|
||||
39
samples/src/lib/markdownToHtml.js
Normal file
39
samples/src/lib/markdownToHtml.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { unified } from "unified";
|
||||
import remarkParse from "remark-parse";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkRehype from "remark-rehype";
|
||||
import rehypeStringify from "rehype-stringify";
|
||||
import rehypeEnhancer from "./rehypeEnhancer.js";
|
||||
import { resolveImagePath } from "./markdownUtils.js";
|
||||
|
||||
export async function markdownToHtml(raw) {
|
||||
const cleaned = raw
|
||||
.replace(/^import\s+.*from\s+['"].*['"];?$/gm, "")
|
||||
// 🔹 참조 스타일 이미지 정의를 저장
|
||||
.replace(
|
||||
/\[([^\]]+)\]:\s*\.\.\/.*assets\/images\/(.+)/g,
|
||||
(_, ref, path) => `[${ref}]: /civil-engineering-lab/images/${path}`
|
||||
)
|
||||
// 🔹 참조 스타일 이미지 사용 시 클래스 추가
|
||||
.replace(
|
||||
/!\[\]\[([^\]]+)\]\{\.([a-zA-Z0-9-_]+)\}/g,
|
||||
(match, ref, className) => {
|
||||
// ![][image1]{.my-class} → HTML로 변환
|
||||
return `<img data-ref="${ref}" class="${className}" />`;
|
||||
}
|
||||
)
|
||||
.replace(
|
||||
/!\[([^\]]*)\]\((@img\/[^)]+)\)/g,
|
||||
(_, alt, path) => `})`
|
||||
);
|
||||
|
||||
return String(
|
||||
await unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkRehype)
|
||||
.use(rehypeEnhancer)
|
||||
.use(rehypeStringify)
|
||||
.process(cleaned)
|
||||
);
|
||||
}
|
||||
22
samples/src/lib/markdownUtils.js
Normal file
22
samples/src/lib/markdownUtils.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// /src/utils/markdownUtils.js
|
||||
const imageCache = new Map();
|
||||
|
||||
export function resolveImagePath(src) {
|
||||
if (!src) return src;
|
||||
if (src.startsWith("@img/")) {
|
||||
return src.replace("@img/", "/civil-engineering-lab/images/");
|
||||
}
|
||||
if (src.startsWith("../assets/images/")) {
|
||||
return src.replace("../assets/images/", "/civil-engineering-lab/images/");
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
/**
|
||||
* href가 외부 링크인지 확인
|
||||
* - http://, https://, // 로 시작하면 외부 링크로 판단
|
||||
*/
|
||||
export function isExternalLink(href) {
|
||||
if (!href) return false;
|
||||
return /^https?:\/\//.test(href) || href.startsWith("//");
|
||||
}
|
||||
72
samples/src/lib/modalController.ts
Normal file
72
samples/src/lib/modalController.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
// modalController.ts
|
||||
|
||||
let isInitialized = false;
|
||||
let escapeHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||
|
||||
export function initModalController() {
|
||||
if (isInitialized) {
|
||||
return; // 이미 초기화되었으면 재초기화하지 않음
|
||||
}
|
||||
|
||||
isInitialized = true;
|
||||
|
||||
// 버튼 클릭 이벤트 위임
|
||||
document.addEventListener("click", (e) => {
|
||||
const target = (e.target as HTMLElement).closest("[data-modal]");
|
||||
if (target) {
|
||||
const modalId = target.getAttribute("data-modal");
|
||||
if (modalId) openModal(modalId);
|
||||
document.querySelector(".nav-wrap")?.classList.add("open");
|
||||
}
|
||||
|
||||
// 모달 닫기 버튼 클릭
|
||||
const closeBtn = (e.target as HTMLElement).closest(".modal .close");
|
||||
if (closeBtn) {
|
||||
const modal = closeBtn.closest(".modal") as HTMLElement;
|
||||
if (modal) closeModal(modal);
|
||||
document.querySelector(".nav-wrap")?.classList.remove("open");
|
||||
}
|
||||
|
||||
// 모달 외부 클릭
|
||||
const modalEl = (e.target as HTMLElement).closest(".modal");
|
||||
if (modalEl && e.target === modalEl) {
|
||||
closeModal(modalEl);
|
||||
document.querySelector(".nav-wrap")?.classList.remove("open");
|
||||
}
|
||||
});
|
||||
|
||||
// ESC 키 이벤트
|
||||
escapeHandler = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
const activeModals = document.querySelectorAll<HTMLElement>(".modal");
|
||||
activeModals.forEach((modal) => {
|
||||
if (modal.style.display === "block") closeModal(modal);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", escapeHandler);
|
||||
}
|
||||
|
||||
function openModal(modalId: string) {
|
||||
const modal = document.getElementById(modalId) as HTMLElement;
|
||||
if (!modal) return;
|
||||
|
||||
const video = modal.querySelector("video") as HTMLVideoElement;
|
||||
modal.style.display = "block";
|
||||
|
||||
if (video) {
|
||||
video.currentTime = 0;
|
||||
video.play();
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal(modal: HTMLElement) {
|
||||
const video = modal.querySelector("video") as HTMLVideoElement;
|
||||
modal.style.display = "none";
|
||||
|
||||
if (video) {
|
||||
video.pause();
|
||||
video.currentTime = 0;
|
||||
}
|
||||
}
|
||||
118
samples/src/lib/navigation.ts
Normal file
118
samples/src/lib/navigation.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
// ✅ /help 경로용 컴포넌트 네비게이션만 정의
|
||||
export const navigation = [
|
||||
{
|
||||
label: "인터페이스",
|
||||
directory: "interface",
|
||||
type: "component" as const,
|
||||
items: [
|
||||
{ slug: "information", title: "로그인/로그아웃" },
|
||||
{ slug: "interface01", title: "메인화면구성" },
|
||||
{ slug: "interface02", title: "기본기능" },
|
||||
{ slug: "interface03", title: "사용자 설정 백업 & 복원" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "사용자화",
|
||||
directory: "customize",
|
||||
type: "component" as const,
|
||||
items: [
|
||||
{ slug: "customize01", title: "명령어아이콘바" },
|
||||
{ slug: "customize02", title: "시스템설정" },
|
||||
{ slug: "customize03", title: "작업환경설정" },
|
||||
{ slug: "customize04", title: "단축키설정" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "멀티작업공간",
|
||||
directory: "multi",
|
||||
type: "component" as const,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ slug: "multi01", title: "파일탭 분리" },
|
||||
{ slug: "multi02", title: "Layout 탭 분리" },
|
||||
{ slug: "multi03", title: "3D 작업 전용" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "명령어 전체보기",
|
||||
directory: "command",
|
||||
type: "component" as const,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ slug: "command01", title: "명령어 전체보기 구성" },
|
||||
{ slug: "command02", title: "토목/도로 특화명령어" },
|
||||
{ slug: "command03", title: "구조/배근 특화명령어" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "스타일 관리",
|
||||
directory: "style",
|
||||
type: "component" as const,
|
||||
items: [
|
||||
{ slug: "style01", title: "선스타일 상세보기" },
|
||||
{ slug: "style02", title: "면스타일 상세보기" },
|
||||
{ slug: "style03", title: "문자스타일 상세보기" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "객체특성관리",
|
||||
directory: "feature",
|
||||
type: "component" as const,
|
||||
items: [
|
||||
{ slug: "feature01", title: "속성바,속성창" },
|
||||
{ slug: "feature02", title: "색상상세보기" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "레이어관리",
|
||||
directory: "layer",
|
||||
type: "component" as const,
|
||||
items: [{ slug: "layer01", title: "레이어 상세보기" }],
|
||||
},
|
||||
{
|
||||
label: "통합블록관리",
|
||||
directory: "block",
|
||||
type: "component" as const,
|
||||
items: [
|
||||
{ slug: "block01", title: "블록의 종류" },
|
||||
{ slug: "block02", title: "블록" },
|
||||
{ slug: "block03", title: "속성블록" },
|
||||
{ slug: "block04", title: "외부참조" },
|
||||
{ slug: "block05", title: "블록 라이브러리" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "인쇄",
|
||||
directory: "print",
|
||||
type: "component" as const,
|
||||
items: [
|
||||
{ slug: "print01", title: "인쇄창 화면구성" },
|
||||
{ slug: "multiprint01", title: "도면 출력 옵션 설정" },
|
||||
{ slug: "multiprint02", title: "다중 인쇄창 화면 구성" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "도면관리",
|
||||
directory: "floorplan",
|
||||
type: "component" as const,
|
||||
items: [
|
||||
{ slug: "floorplan01", title: "도면탐색 및 정보열람" },
|
||||
{ slug: "floorplan02", title: "도면정보 수정" },
|
||||
{ slug: "floorplan03", title: "도면정보항목선택" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ✅ component 라우트만 생성
|
||||
export const pageRoutes = navigation.flatMap((section) =>
|
||||
section.items.map((item) => ({
|
||||
slug: item.slug,
|
||||
title: `가이드-${item.title}`,
|
||||
type: section.type,
|
||||
path: `${section.directory}/${item.slug}`,
|
||||
group: section.directory,
|
||||
sectionLabel: section.label,
|
||||
}))
|
||||
);
|
||||
|
||||
export const defaultPage = pageRoutes[0];
|
||||
21
samples/src/lib/rehypeEnhancer.js
Normal file
21
samples/src/lib/rehypeEnhancer.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// src/utils/rehypeEnhancer.js
|
||||
import { visit } from "unist-util-visit";
|
||||
import { resolveImagePath, isExternalLink } from "./markdownUtils.js";
|
||||
|
||||
export default function rehypeEnhancer() {
|
||||
return (tree) => {
|
||||
visit(tree, "element", (node) => {
|
||||
// 이미지 경로 처리
|
||||
if (node.tagName === "img" && node.properties?.src) {
|
||||
node.properties.src = resolveImagePath(node.properties.src);
|
||||
node.properties.loading = "lazy";
|
||||
}
|
||||
|
||||
// 외부 링크 처리
|
||||
if (node.tagName === "a" && isExternalLink(node.properties?.href)) {
|
||||
node.properties.target = "_blank";
|
||||
node.properties.rel = "noopener noreferrer";
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
57
samples/src/lib/remarkNumberedHeadings.js
Normal file
57
samples/src/lib/remarkNumberedHeadings.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { visit } from "unist-util-visit";
|
||||
|
||||
const parseFlag = (value, fallback = false) => {
|
||||
if (typeof value === "boolean") return value;
|
||||
if (typeof value === "string") {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (["true", "1", "yes", "y", "on"].includes(normalized)) return true;
|
||||
if (["false", "0", "no", "n", "off"].includes(normalized)) return false;
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attach hierarchical numbers (e.g., 1, 1.1, 1.1.1) to heading nodes.
|
||||
* Numbers are stored in data-heading-number and can be rendered via CSS/TOC.
|
||||
*/
|
||||
export default function remarkNumberedHeadings(options = {}) {
|
||||
const {
|
||||
minLevel = 2,
|
||||
maxLevel = 4,
|
||||
frontmatterField = "numberedHeadings",
|
||||
defaultEnabled = false,
|
||||
} = options;
|
||||
|
||||
return (tree, file) => {
|
||||
const frontmatter =
|
||||
file?.data?.astro?.frontmatter ??
|
||||
file?.data?.frontmatter ??
|
||||
file?.data?.matter ??
|
||||
{};
|
||||
const enabled = parseFlag(frontmatter?.[frontmatterField], defaultEnabled);
|
||||
if (!enabled) return;
|
||||
|
||||
const counters = Array.from({ length: maxLevel + 2 }, () => 0);
|
||||
|
||||
visit(tree, "heading", (node) => {
|
||||
const level = node.depth ?? 0;
|
||||
if (level < minLevel || level > maxLevel) return;
|
||||
|
||||
counters[level] += 1;
|
||||
for (let i = level + 1; i < counters.length; i += 1) {
|
||||
counters[i] = 0;
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
for (let i = minLevel; i <= level; i += 1) {
|
||||
if (counters[i] === 0) break;
|
||||
parts.push(String(counters[i]));
|
||||
}
|
||||
const number = `${parts.join(".")}.`;
|
||||
|
||||
node.data = node.data ?? {};
|
||||
node.data.hProperties = node.data.hProperties ?? {};
|
||||
node.data.hProperties["data-heading-number"] = number;
|
||||
});
|
||||
};
|
||||
}
|
||||
143
samples/src/lib/searchIndex.js
Normal file
143
samples/src/lib/searchIndex.js
Normal file
@@ -0,0 +1,143 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { unified } from 'unified';
|
||||
import remarkParse from 'remark-parse';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkRehype from 'remark-rehype';
|
||||
import rehypeStringify from 'rehype-stringify';
|
||||
import { pageRoutes } from './navigation.js';
|
||||
|
||||
/* -------------------------------
|
||||
* Markdown → 순수 텍스트
|
||||
* ------------------------------- */
|
||||
export async function extractPlainText(markdown) {
|
||||
if (!markdown) return '';
|
||||
|
||||
const cleaned = markdown
|
||||
.replace(/^import\s+.*from\s+['"].*['"];?$/gm, '')
|
||||
.replace(/<!--[\s\S]*?-->/g, '')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.trim();
|
||||
|
||||
const result = await unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkRehype)
|
||||
.use(rehypeStringify)
|
||||
.process(cleaned);
|
||||
|
||||
return String(result)
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/&[a-zA-Z#0-9]+;/g, ' ')
|
||||
.replace(/\s{2,}/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/* -------------------------------
|
||||
* Astro 파일 → 순수 텍스트
|
||||
* ------------------------------- */
|
||||
export function extractAstroText(fileContent) {
|
||||
return fileContent
|
||||
.replace(/---[\s\S]*?---/g, '')
|
||||
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
||||
.replace(/<!--[\s\S]*?-->/g, '')
|
||||
.replace(/\{[\s\S]*?\}/g, '')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ /gi, ' ')
|
||||
.replace(/\s{2,}/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/* -------------------------------
|
||||
* 폴더 재귀 탐색
|
||||
* ------------------------------- */
|
||||
function getAstroFiles(dir) {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
let results = [];
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) results = results.concat(getAstroFiles(fullPath));
|
||||
else if (entry.isFile() && entry.name.endsWith('.astro')) results.push(fullPath);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/* -------------------------------
|
||||
* 전체 검색 데이터 생성
|
||||
* ------------------------------- */
|
||||
export async function generateSearchData() {
|
||||
// ① Docs 전부
|
||||
const allDocs = await getCollection('docs');
|
||||
const mdxSearchData = await Promise.all(
|
||||
allDocs.map(async (doc) => {
|
||||
const plain = await extractPlainText(doc.body);
|
||||
const text = plain.toLowerCase();
|
||||
const slugPath = (doc.slug || doc.id).replace(/^ko\//, '');
|
||||
const url = `/${slugPath}${slugPath.endsWith('/') ? '' : '/'}`;
|
||||
return {
|
||||
id: slugPath,
|
||||
title: doc.data.title || slugPath,
|
||||
titleLower: (doc.data.title || slugPath).toLowerCase(),
|
||||
content: plain.slice(0, 500),
|
||||
fullContent: plain,
|
||||
fullLower: text,
|
||||
url,
|
||||
category: 'mdx',
|
||||
type: 'mdx'
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// ② 메뉴
|
||||
const menuSearchData = pageRoutes
|
||||
.filter(r => r.path?.startsWith('/'))
|
||||
.map(route => {
|
||||
const rawTitle = route.title || path.basename(route.path);
|
||||
const titleText = rawTitle.startsWith('가이드-') ? rawTitle.substring(4) : rawTitle;
|
||||
return {
|
||||
id: route.path,
|
||||
title: titleText,
|
||||
titleLower: titleText.toLowerCase(),
|
||||
content: '',
|
||||
fullContent: '',
|
||||
fullLower: '',
|
||||
url: `/help${route.path}`,
|
||||
category: 'menu',
|
||||
type: 'menu'
|
||||
};
|
||||
});
|
||||
|
||||
// ③ 실제 Astro 파일
|
||||
const subRoot = path.resolve('./src/content/sub');
|
||||
const subFiles = getAstroFiles(subRoot);
|
||||
const subSearchData = subFiles.map(filePath => {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const text = extractAstroText(content);
|
||||
const fileName = path.basename(filePath, '.astro');
|
||||
|
||||
// ✅ navigation/pageRoutes에서 title 가져오기
|
||||
const route = pageRoutes.find(r => r.path.endsWith(fileName));
|
||||
let title = route
|
||||
? route.title.startsWith('가이드-') ? route.title.substring(4) : route.title
|
||||
: content.match(/<title>(.*?)<\/title>/i)?.[1] ||
|
||||
content.match(/<h1[^>]*>(.*?)<\/h1>/i)?.[1] ||
|
||||
fileName;
|
||||
|
||||
return {
|
||||
id: fileName,
|
||||
title,
|
||||
titleLower: title.toLowerCase(),
|
||||
content: text.slice(0, 500),
|
||||
fullContent: text,
|
||||
fullLower: text.toLowerCase(),
|
||||
url: `/help/${fileName}`,
|
||||
category: 'astro-file',
|
||||
type: 'astro-file'
|
||||
};
|
||||
});
|
||||
|
||||
return [...mdxSearchData, ...menuSearchData, ...subSearchData];
|
||||
}
|
||||
Reference in New Issue
Block a user