์ด ๋ฌธ์๋ scripts/docc_to_md.js ์คํฌ๋ฆฝํธ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ๋์ํ๊ธฐ ์ํด Montage ์ฝ๋ ์์ฑ ์ ์ค์ํด์ผ ํ ์ฌํญ๋ค์ ์ ๋ฆฌํฉ๋๋ค.
- ์คํฌ๋ฆฝํธ๋ ์ ๊ท์์ผ๋ก
publicํค์๋๋ฅผ ์ฐพ์ ํ์ ๊ณผ ๋ฉค๋ฒ๋ฅผ ์ธ์ํฉ๋๋ค. public์ด ์์ผ๋ฉด ๋ฌธ์ํ๋์ง ์์ต๋๋ค.
// โ Not documented
struct MyComponent {
func myMethod() { }
}
// โ
Documented
public struct MyComponent {
public func myMethod() { }
} const typeRegex = /public\s+(enum|struct|class|protocol|actor)\s+(\w+)/g;
let match;
while ((match = typeRegex.exec(content)) !== null) {
const typeName = match[2];
swiftFileMap[typeName] = relBase;
} const funcRegex = /public\s+(?:(static|class|mutating|nonmutating)\s+)?func\s+([^\{]+)/g;
let result;
while ((result = funcRegex.exec(body)) !== null) {
const modifier = (result[1] || '').trim();
const signatureBody = (result[2] || '').trim().replace(/\s+$/g, '');
const kind = modifier ? `${modifier} func` : 'func';
recordComponentExtensionSignature(componentTitle, typeName, kind, signatureBody);
}-
ํ์ผ๋ช (
.swift์ ๊ฑฐ)์ด ์ปดํฌ๋ํธ ์ ๋ชฉ์ผ๋ก ์ฌ์ฉ๋ฉ๋๋ค. -
ํ์ผ ๋ด๋ถ์ ํ์ ๋ช ๊ณผ ํ์ผ๋ช ์ด ๋ค๋ฅด๋ฉด ๋งคํ์ด ๋ณต์กํด์ง ์ ์์ต๋๋ค.
-
ํ์ผ๋ช ๊ณผ ์ฃผ์ ํ์ ๋ช ์ ์ผ์น์ํค์ธ์.
-
์:
Button.swiftโpublic struct Button
// Filename-based mapping (keep existing logic)
const componentTitle = file.replace(/\.swift$/, '');
swiftFileMap[componentTitle] = relBase;
if (relPath.includes('1 Components')) {
convertedSwiftFileMap[componentTitle.toLowerCase()] = { componentTitle, isConverted: false };
}Popover.swift์ฒ๋ผ ํ์ผ ๋ด์ public enum Popover ๊ฐ์ ํ์
์ด ์๊ณ , View extension ํจ์๋ง ์๋ ๊ฒฝ์ฐ:
// Popover.swift
import SwiftUI
extension View {
public func popover(...) -> some View {
// Implementation
}
}์ด ๊ฒฝ์ฐ ๋ฌธ์ ํ์ด์ง ์์ฒด๊ฐ ์์ฑ๋์ง ์์ต๋๋ค.
- DocC๋ ํ์ผ ๋ด์
publicํ์ ์ ๊ธฐ๋ฐ์ผ๋ก JSON ๋ฌธ์๋ฅผ ์์ฑํฉ๋๋ค. - View extension๋ง ์๋ ๊ฒฝ์ฐ, DocC๊ฐ ํด๋น ํ์ผ์ ๋ํ ๋ ๋ฆฝ์ ์ธ JSON ํ์ผ์ ์์ฑํ์ง ์์ต๋๋ค. (SwiftUICore ํ์ด์ง๊ฐ ์์ฑ๋๊ธฐ๋ ํ์ง๋ง ๋ค๋ฅธ extension ํจ์๊ฐ ๋ง์์ ์ฌ์ฉ์๊ฐ ์ฐพ๊ธฐ์ ๋ถํธํฉ๋๋ค.)
- ์คํฌ๋ฆฝํธ๋ DocC๊ฐ ์์ฑํ JSON ํ์ผ์ ๊ธฐ๋ฐ์ผ๋ก ๋งํฌ๋ค์ด์ ์์ฑํ๋ฏ๋ก, JSON์ด ์์ผ๋ฉด ๋ฌธ์ ํ์ด์ง๋ ์์ฑ๋์ง ์์ต๋๋ค.
ํ์ผ๋ช ๊ณผ ๋์ผํ ์ด๋ฆ์ View struct๊ฐ ์ ์๋์ง ์์ ๊ฒฝ์ฐ ๋น enum ํน์ struct ํ์ ์ ์ถ๊ฐํ์ธ์:
// Popover.swift
import SwiftUI
// โ
Add type for documentation page generation
public enum Popover {
// Empty enum is sufficient (namespace role)
}
extension View {
public func popover(...) -> some View {
// Implementation
}
}๋๋:
// Popover.swift
import SwiftUI
// โ
Or use struct
public struct Popover {
// Empty struct is sufficient
}
extension View {
public func popover(...) -> some View {
// Implementation
}
}- ์ฐ๊ด๋ ๋ค๋ฅธ ํ์ ์ Extension ํจ์๋ค์ ํด๋น ํ์ ์ "Associated Extensions" ์น์ ์ ์๋์ผ๋ก ํฌํจ๋ฉ๋๋ค.
- ํ์ ์์ฒด๋ ๋น์ด์์ด๋ ์๊ด์์ต๋๋ค. ๋จ์ง ๋ฌธ์ ํ์ด์ง๋ฅผ ์์ฑํ๊ธฐ ์ํ "์ต์ปค" ์ญํ ๋ง ํฉ๋๋ค.
์ปดํฌ๋ํธ์ ๊ด๋ จ๋ ํ์ ํ์
(์: Style, Size, Configuration ๋ฑ)์ ๋ณ๋์ ์ต์์ ํ์
์ผ๋ก ์ ์ํ๋ฉด, ๊ฐ๊ฐ์ด ๋
๋ฆฝ์ ์ธ ๋ฌธ์ ํ์ด์ง๋ก ์์ฑ๋ฉ๋๋ค.
// Button.swift
public struct Button { }
// โ Defined as separate top-level type in separate file or same file
public enum ButtonStyle { }
public enum ButtonSize { }์ด ๊ฒฝ์ฐ ButtonStyle๊ณผ ButtonSize๊ฐ ๊ฐ๊ฐ ๋ณ๋์ ๋ฌธ์ ํ์ด์ง๋ก ์์ฑ๋์ด, ํ๋์ ์ปดํฌ๋ํธ ๋ฌธ์๋ก ํตํฉ๋์ง ์์ต๋๋ค.
์ปดํฌ๋ํธ ์ด๋ฆ์ enum ๋ค์์คํ์ด์ค ์์ ํ์ ํ์ ์ ์ ์ํ์ธ์:
// Button.swift
public struct Button { }
// โ
Defined within component namespace
public enum Button {
public enum Style {
case primary
case secondary
}
public enum Size {
case small
case medium
case large
}
}- ์คํฌ๋ฆฝํธ๋ ํ์
๋ช
์ ์ (
.)์ด ์๋ ์ค์ฒฉ ํ์ (Button.Style,Button.Size)์ ๋ณ๊ฐ์ ๋ฌธ์๋ก ์์ฑํ์ง ์์ต๋๋ค. - ๋์ ๋ถ๋ชจ ํ์ ์ Topics ์น์ ์ ํฌํจ์์ผ ํ๋์ ์ปดํฌ๋ํธ ๋ฌธ์๋ก ํตํฉํฉ๋๋ค.
- ์ด๋ ๊ฒ ํ๋ฉด ๊ด๋ จ ํ์ ๋ค์ด ๋ ผ๋ฆฌ์ ์ผ๋ก ๊ทธ๋ฃนํ๋์ด ์ฌ์ฉ์๊ฐ ์ฐพ๊ธฐ ์ฝ์ต๋๋ค.
json.metadata.roleHeading === 'Enumeration' && json.metadata.title.split('.').length > 1 ||
json.metadata.roleHeading === 'Case' ||
json.metadata.roleHeading === 'Extended Class' ||
json.metadata.roleHeading === 'Extended Structure' ||
json.metadata.roleHeading === 'Extended Enumeration' ||
json.metadata.roleHeading === 'Extended Protocol' ||
json.metadata.roleHeading === 'API Collection' ||
json.metadata.roleHeading === 'Structure' && json.metadata.title.split('.').length > 1) {// โ
Correct example: Integrated into a single component document
public struct Button {
// Button implementation
}
public enum Button {
public enum Style { }
public enum Size { }
public struct Configuration { }
}
// โ Incorrect example: Each generated as separate document
public struct Button { }
public enum ButtonStyle { } // Separate document
public enum ButtonSize { } // Separate document- Extension ๋ด๋ถ์ ํจ์๋ ํ๋กํผํฐ์
publicํค์๋๊ฐ ์์ผ๋ฉด ๋ฌธ์ํ๋์ง ์์ต๋๋ค. - ์คํฌ๋ฆฝํธ๋ ์ ๊ท์์ผ๋ก ๊ฐ ๋ฉค๋ฒ ์ ์ธ ์์
publicํค์๋๋ฅผ ์ฐพ์ extension ๋ฉค๋ฒ๋ฅผ ์ธ์ํฉ๋๋ค.
extension View {
// โ Not documented
func button() -> some View { }
// โ
Documented
public func button() -> some View { }
}
// โ Even if extension is public, members without public are not documented
public extension View {
func button() -> some View { } // No public โ Not documented
}
// โ
Both extension and members must be public
public extension View {
public func button() -> some View { } // Has public โ Documented
}- Extension ๋ด๋ถ์
public func์public var๋ง ์ธ์๋ฉ๋๋ค. public subscript,public init๋ฑ์ ์ ๊ท์์ ํฌํจ๋์ง ์์ ์ธ์๋์ง ์์ ์ ์์ต๋๋ค.- ๋ณต์กํ ์๊ทธ๋์ฒ๋ ํน์ํ ํจํด์ ํ์ฑ์ ์คํจํ ์ ์์ต๋๋ค.
// โ
Recognized
extension SomeType {
public func myMethod() { }
public static func myStaticMethod() { }
public class func myClassMethod() { }
public mutating func myMutatingMethod() { }
public var myProperty: String { }
public static var myStaticProperty: String { }
}
// โ May not be recognized
extension SomeType {
func privateMethod() { } // No public
public subscript(index: Int) -> Element { } // subscript not in regex
public init() { } // init not in regex
} const funcRegex = /public\s+(?:(static|class|mutating|nonmutating)\s+)?func\s+([^\{]+)/g;
let result;
while ((result = funcRegex.exec(body)) !== null) {
const modifier = (result[1] || '').trim();
const signatureBody = (result[2] || '').trim().replace(/\s+$/g, '');
const kind = modifier ? `${modifier} func` : 'func';
recordComponentExtensionSignature(componentTitle, typeName, kind, signatureBody);
}
const varRegex = /public\s+(?:(static|class)\s+)?var\s+([^=\{]+)/g;
while ((result = varRegex.exec(body)) !== null) {
const modifier = (result[1] || '').trim();
const signatureBody = (result[2] || '').trim().replace(/\s+$/g, '');
const kind = modifier ? `${modifier} var` : 'var';
recordComponentExtensionSignature(componentTitle, typeName, kind, signatureBody);
}- ์ค๊ดํธ ๋งค์นญ์ผ๋ก extension ๋ณธ๋ฌธ์ ์ถ์ถํฉ๋๋ค.
- ์ค์ฒฉ๋ ์ค๊ดํธ๊ฐ ๋ง๊ฑฐ๋ ๋ณต์กํ ๊ตฌ์กฐ์์๋ ํ์ฑ์ด ์คํจํ ์ ์์ต๋๋ค.
function extractExtensionBodies(content) {
const bodies = [];
const extRegex = /extension\s+(?:[A-Za-z0-9_]+\.)?([A-Za-z0-9_]+)\s*\{/g;
let match;
while ((match = extRegex.exec(content)) !== null) {
const typeName = match[1];
let braceDepth = 1;
let idx = extRegex.lastIndex;
while (idx < content.length && braceDepth > 0) {
const char = content[idx];
if (char === '{') braceDepth++;
else if (char === '}') braceDepth--;
idx++;
}
const body = content.slice(extRegex.lastIndex, idx - 1);
bodies.push({ typeName, body });
extRegex.lastIndex = idx;
}
return bodies;
}Button.swift ํ์ผ์ extension View { func button(...) }๋ฅผ ์์ฑํ๋๋ฐ, ์์ฑ๋ Button ์ปดํฌ๋ํธ ๋ฌธ์์ "Associated Extensions" ์น์
์ ์ด ํจ์๊ฐ ๋ํ๋์ง ์์ต๋๋ค.
์คํฌ๋ฆฝํธ๊ฐ Swift ํ์ผ์ extension ์๊ทธ๋์ฒ์ DocC๊ฐ ์์ฑํ ์๊ทธ๋์ฒ๋ฅผ ๋น๊ตํ ๋, ์ ๊ทํ ๊ณผ์ ์์ ์ผ๋ถ ์ ๋ณด๊ฐ ์ ๊ฑฐ๋์ด ๋งค์นญ์ด ์คํจํ ์ ์์ต๋๋ค.
// โ May cause issues: Matching fails if type names differ
extension View {
public func button(title: String) -> some View { }
}
// Matching fails if DocC recognizes different type names-
์ ๋ค๋ฆญ ์ ์ฝ์ด ์๋ ๊ฒฝ์ฐ
-
ํ๋ผ๋ฏธํฐ ์์ฑ(
@escaping,@ViewBuilder๋ฑ)์ด ๋ง์ ๊ฒฝ์ฐ -
๊ธฐ๋ณธ๊ฐ์ด ์๋ ํ๋ผ๋ฏธํฐ๊ฐ ๋ง์ ๊ฒฝ์ฐ
-
๊ณต๋ฐฑ ์ฐจ์ด๋ ์๋์ผ๋ก ์ ๋ฆฌ๋๋ฏ๋ก ๋ฌธ์ ๊ฐ ๋์ง ์์ต๋๋ค.
-
@escaping,@ViewBuilder๊ฐ์ ์์ฑ์ ์ ๊ทํ ๊ณผ์ ์์ ์ ๊ฑฐ๋๋ฏ๋ก, ์ด๊ฒ๋ง์ผ๋ก๋ ๋งค์นญ์ด ์คํจํ์ง ์์ต๋๋ค. -
๋ฌธ์ ๋ ์ฃผ๋ก ํ์ ๋ช , ํ๋ผ๋ฏธํฐ๋ช , ํจ์๋ช ์ ์ฐจ์ด์์ ๋ฐ์ํฉ๋๋ค.
์๊ทธ๋์ฒ ์ ๊ทํ ํจ์ (Signature Normalization Function):
function canonicalizeSignature(signature) {
let normalized = signature
.replace(/\s+/g, ' ')
.replace(/\(\s+/g, '(')
.replace(/\s+\)/g, ')')
.replace(/\s*,\s*/g, ', ')
.replace(/\s*:\s*/g, ': ')
.replace(/\s*->\s*/g, ' -> ')
.replace(/\s+where\s+/g, ' where ');
// Remove parameter attributes (e.g., @escaping, @ViewBuilder, etc.)
normalized = normalized.replace(/@\w+\s+/g, '');
if (signature.includes('func')) {
// Remove generic constraints (<T: Foo, U: Bar> -> <T, U>)
normalized = normalized.replace(/<([^>]+)>/g, (_, contents) => {
const cleaned = contents
.split(',')
.map((part) => part.trim().replace(/([A-Za-z0-9_]+)\s*:\s*[^,]+/, '$1'))
.join(', ');
return `<${cleaned}>`;
});
// Normalize external/internal parameter names
normalized = normalized
// Remove internal name when external label + internal name exist
.replace(/([A-Za-z0-9_]+)\s+[A-Za-z0-9_]+\s*:/g, '$1:')
// Remove label when external label is _
.replace(/_\s*:\s*/g, '');
// Remove parameter default values
normalized = stripParameterDefaults(normalized);
}
// Remove unnecessary whitespace
normalized = normalized
.replace(/,\s+\)/g, ')')
.replace(/\s*,\s*/g, ', ')
.replace(/\(\s+/g, '(')
.replace(/\s+\)/g, ')')
.replace(/\s{2,}/g, ' ')
.replace(/\s+,/g, ',')
.replace(/:\s+/g, ': ')
.replace(/\(\s+/g, '(')
.replace(/\s+\)/g, ')');
return normalized.trim();
}DocC JSON์์ extension ๋ฉค๋ฒ ๋ ๋๋ง ์ ์๊ทธ๋์ฒ ๋น๊ต (Signature Comparison When Rendering Extension Members from DocC JSON):
function renderExtensionMemberMarkdown(ref, dataRoot, mdPath = 'documentation/utilities/ios-extensions') {
if (!ref) return null;
const signatureRaw = (ref.fragments || []).map((f) => f.text).join('') || ref.title || '';
const canonicalSignature = canonicalizeSignature(signatureRaw);
const hash = generateHash(canonicalSignature);- ์ฐ๊ด Extension ์์ง์
1 Components๋๋ ํ ๋ฆฌ ๋ด์ ํ์ผ์๋ง ์ ์ฉ๋ฉ๋๋ค. - ๋ค๋ฅธ ๋๋ ํ ๋ฆฌ์ ํ์ผ์ extension์ด ๋ฌธ์ํ๋์ง ์์ ์ ์์ต๋๋ค.
if (relPath.includes('1 Components')) {
convertedSwiftFileMap[componentTitle.toLowerCase()] = { componentTitle, isConverted: false };
}
// Extract public type names from Swift files
try {
const content = fs.readFileSync(fullPath, 'utf-8');
// Find public enum, struct, class, protocol, actor, etc.
const typeRegex = /public\s+(enum|struct|class|protocol|actor)\s+(\w+)/g;
let match;
while ((match = typeRegex.exec(content)) !== null) {
const typeName = match[2];
swiftFileMap[typeName] = relBase;
}
if (/\b1 Components\b/.test(relBase)) {
collectComponentExtensionHashes(componentTitle, content);
}์ธ์๋๋ ํ์ :
public enumpublic structpublic classpublic protocolpublic actor
์ธ์๋์ง ์๋ ํ์ :
typealiasassociatedtype(ํ๋กํ ์ฝ ๋ด๋ถ)- ์ค์ฒฉ ํ์ (๋ณ๋ ์ฒ๋ฆฌ ์์)
const typeRegex = /public\s+(enum|struct|class|protocol|actor)\s+(\w+)/g;- ํ์ผ๋ช
์ด
ui๋ก ์์ํ๊ฑฐ๋montage.json์ผ๋ก ๋๋๋ ํ์ผ์ ์ ์ธ๋ฉ๋๋ค. - UIKit ๊ด๋ จ ํ์ ์ ๋ฌธ์ํ๋์ง ์์ต๋๋ค.
if (ref.title === 'UIKit') continue; const urlPathLastComponent = url.split('/').at(-1);
if (urlPathLastComponent.startsWith('UI')) {
// Exclude UIKit-related documentation
return;
} const jsonFileName = jsonPath.split('/').at(-1);
if (jsonFileName.startsWith('ui') || jsonFileName.endsWith('montage.json')) {
// Exclude UIKit-related documentation
return;๋จ, ์ต์์ ๋ ๋ฒจ์ ์ ์๋ enum๊ณผ struct๋ ๋จ๋ ํ์ด์ง๋ก ์์ฑํฉ๋๋ค.
InitializerInstance MethodInstance PropertyType MethodType PropertyOperatorClassEnumeration(์ต์์ ๋์ค๋ ์ ์ธ)CaseExtended ClassExtended StructureExtended EnumerationExtended ProtocolAPI CollectionStructure(์ต์์ ๋์ค๋ ์ ์ธ)
if (json.metadata.roleHeading === 'Initializer' ||
json.metadata.roleHeading === 'Instance Method' ||
json.metadata.roleHeading === 'Instance Property' ||
json.metadata.roleHeading === 'Type Method' ||
json.metadata.roleHeading === 'Type Property' ||
json.metadata.roleHeading === 'Operator' ||
json.metadata.roleHeading === 'Class' ||
json.metadata.roleHeading === 'Enumeration' && json.metadata.title.split('.').length > 1 ||
json.metadata.roleHeading === 'Case' ||
json.metadata.roleHeading === 'Extended Class' ||
json.metadata.roleHeading === 'Extended Structure' ||
json.metadata.roleHeading === 'Extended Enumeration' ||
json.metadata.roleHeading === 'Extended Protocol' ||
json.metadata.roleHeading === 'API Collection' ||
json.metadata.roleHeading === 'Structure' && json.metadata.title.split('.').length > 1) {
// Exclude Topic section items from separate document generation
return;
}๋ฌธ์ํ๊ฐ ์ ๋๋ก ๋์ง ์๋๋ค๋ฉด:
publicํค์๋ ํ์ธ- ํ์ผ๋ช ๊ณผ ํ์ ๋ช ์ผ์น ํ์ธ
- ์ฐ๊ด Extension์ด
1 Components๋๋ ํ ๋ฆฌ์ ์๋์ง ํ์ธ - ์ฐ๊ด Extension ๋ด๋ถ์ ํจ์/ํ๋กํผํฐ๋
public์ผ๋ก ์ ์ธ๋์ด ์๋์ง ํ์ธ - UIKit ๊ด๋ จ ํ์ ์ด ์๋์ง ํ์ธ
- ์คํฌ๋ฆฝํธ ์คํ ๋ก๊ทธ์์ ์๋ฌ ๋ฉ์์ง ํ์ธ