diff options
| author | Yong He <yonghe@outlook.com> | 2024-10-08 13:29:57 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-10-08 13:29:57 -0700 |
| commit | c42a9faad8d84f7bd05457d5f8e1fe45d6eecfa2 (patch) | |
| tree | f6b5a249074882755e0232b1c9560118b7ccd6b2 /source/slang/slang-doc-markdown-writer.cpp | |
| parent | 50f44c178de4c614dc45fc48938e6881c0373f6a (diff) | |
Overhaul docgen tool and setup CI to generate stdlib reference. (#5232)
* Overhaul docgen tool and setup CI to generate stdlib reference.
* Fix build error.
* Write parsed doc for all decls.
* fix.
* fix callout.
* Fix.
* Fix comment.
* Fix.
* Delete obsolete doc tests.
* Fix.
* Categorize functions and types.
* Fix CI.
* Update comments.
Diffstat (limited to 'source/slang/slang-doc-markdown-writer.cpp')
| -rw-r--r-- | source/slang/slang-doc-markdown-writer.cpp | 2437 |
1 files changed, 1983 insertions, 454 deletions
diff --git a/source/slang/slang-doc-markdown-writer.cpp b/source/slang/slang-doc-markdown-writer.cpp index e32a738c7..86cdb6d29 100644 --- a/source/slang/slang-doc-markdown-writer.cpp +++ b/source/slang/slang-doc-markdown-writer.cpp @@ -3,6 +3,8 @@ #include "../core/slang-string-util.h" #include "../core/slang-type-text-util.h" +#include "../core/slang-char-util.h" +#include "../core/slang-token-reader.h" #include "slang-ast-builder.h" #include "slang-lookup.h" @@ -25,15 +27,50 @@ static void _getDecls(ContainerDecl* containerDecl, List<T*>& out) } template <typename T> -static void _getDeclsOfType(ContainerDecl* containerDecl, List<Decl*>& out) +static void _getDeclsOfType(DocMarkdownWriter* writer, ContainerDecl* containerDecl, List<Decl*>& out) { for (Decl* decl : containerDecl->members) { if (as<T>(decl)) { + if (!writer->isVisible(decl)) + continue; out.add(decl); } + else if (auto genericDecl = as<GenericDecl>(decl)) + { + if (as<T>(genericDecl->inner)) + { + if (!writer->isVisible(decl)) + continue; + out.add(genericDecl); + } + } + } +} + +template <typename T> +static void _getDeclsOfType(DocMarkdownWriter* writer, DocumentPage* page, List<Decl*>& out) +{ + // Collect all decls of type T from all entries for the page. + List<Decl*> declList; + for (auto entry : page->entries) + { + _getDeclsOfType<T>(writer, as<ContainerDecl>(entry->m_node), declList); + } + // Deduplicate based on name. + Dictionary<Name*, Decl*> nameDict; + for (auto decl : declList) + { + nameDict[decl->getName()] = decl; + } + // Sort by name. + for (auto pair : nameDict) + { + if (pair.first) + out.add(pair.second); } + out.sort([](Decl* a, Decl* b) -> bool { return getText(a->getName()) < getText(b->getName()); }); } template <typename T> @@ -54,61 +91,118 @@ static void _appendAsSingleLine(const UnownedStringSlice& in, StringBuilder& out StringUtil::join(lines.getBuffer(), lines.getCount(), ' ', out); } -void DocMarkdownWriter::_appendAsBullets(const List<NameAndText>& values, char wrapChar) +String getDocPath(const DocumentationConfig& config, String path) +{ + return config.rootDir + Path::getPathWithoutExt(path); +} + +void DocMarkdownWriter::_appendAsBullets(const List<NameAndText>& values, bool insertLinkForName, char wrapChar) { - auto& out = m_builder; + auto& out = *m_builder; for (const auto& value : values) { - out << "* "; - + out << "#### "; const String& name = value.name; + auto path = findLinkForToken(m_currentPage, name); if (name.getLength()) { if (wrapChar) { + if (path.getLength()) + { + out << "["; + } out.appendChar(wrapChar); out << name; out.appendChar(wrapChar); + if (path.getLength()) + { + out << "](" << getDocPath(m_config, path) << ")"; + } } else { - out << name; + if (insertLinkForName) + out << translateToMarkdownWithLinks(name); + else + { + auto spaceLoc = name.indexOf(' '); + if (spaceLoc == -1) + out << escapeMarkdownText(name); + else + { + auto first = name.getUnownedSlice().head(spaceLoc); + auto rest = name.getUnownedSlice().tail(spaceLoc + 1); + out << escapeMarkdownText(first) << " "; + out << translateToMarkdownWithLinks(rest); + } + } + } + } + if (value.decl) + { + // Add anchor ID for the decl. + if (as<GenericTypeParamDeclBase>(value.decl)) + { + out << " {#typeparam-" << getText(value.decl->getName()) << "}"; + } + else + { + out << " {#decl-" << getText(value.decl->getName()) << "}"; } } - if (value.text.getLength()) { - out.appendChar(' '); + out.appendChar('\n'); - // Hmm, we'll want to make something multiline into a single line - _appendAsSingleLine(value.text.getUnownedSlice(), out); + ParsedDescription desc; + desc.parse(value.text.getUnownedSlice()); + desc.write(this, value.decl, out); + } + else + { + out << "\n"; } - - out << "\n"; } } void DocMarkdownWriter::_appendAsBullets(const List<String>& values, char wrapChar) { - auto& out = m_builder; + auto& out = *m_builder; for (const auto& value : values) { out << "* "; + const String& name = value; - if (value.getLength()) + String path; + path = findLinkForToken(m_currentPage, name); + if (path.getLength() == 0) + { + Slang::Misc::TokenReader tokenReader(name); + if (!tokenReader.IsEnd()) + path = findLinkForToken(m_currentPage, tokenReader.ReadToken().Content); + } + if (path.getLength()) + { + out << "["; + } + if (name.getLength()) { if (wrapChar) { out.appendChar(wrapChar); - out << value; + out << escapeMarkdownText(name); out.appendChar(wrapChar); - } else { - out << value; + out << escapeMarkdownText(name); } } + if (path.getLength()) + { + out << "](" << getDocPath(m_config, path) << ")"; + } out << "\n"; } } @@ -120,6 +214,93 @@ String DocMarkdownWriter::_getName(Decl* decl) return buf.produceString(); } +String DocMarkdownWriter::_getFullName(Decl* decl) +{ + ASTPrinter printer(m_astBuilder); + printer.addDeclPath(decl); + return printer.getStringBuilder().produceString(); +} + +String _translateNameToPath(const UnownedStringSlice& name) +{ + StringBuilder buf; + for (const char c : name) + { + // Removing leading underscores to prevent url issues. + if (c == '_' && buf.getLength() == 0) + continue; + + if (c == ' ') + { + buf.appendChar('-'); + } + else if (CharUtil::isAlphaOrDigit(c) || c == '_') + { + buf.appendChar(c); + } + else + { + buf.appendChar('x'); + buf << String((int)c, 16); + } + } + return buf; +} + +String DocMarkdownWriter::_getDocFilePath(Decl* decl) +{ + if (!decl) + return ""; + + StringBuilder sb; + if (as<NamespaceDeclBase>(getParentDecl(decl))) + { + if (as<InterfaceDecl>(decl)) + { + sb << "interfaces/"; + } + else if (as<AggTypeDeclBase>(decl) || as<TypeDefDecl>(decl)) + { + sb << "types/"; + } + else + { + sb << "global-decls/"; + } + } + + if (auto extDecl = as<ExtensionDecl>(decl)) + { + if (auto declRef = isDeclRefTypeOf<Decl>(extDecl->targetType)) + { + auto name = _translateNameToPath(_getName(declRef.getDecl()).getUnownedSlice()); + sb << name; + sb << "/index.md"; + return sb.produceString(); + } + } + if (as<AggTypeDeclBase>(decl)) + { + auto name = _translateNameToPath(_getName(decl).getUnownedSlice()); + sb << name; + sb << "/index.md"; + return sb.produceString(); + } + auto parentPath = _getDocFilePath(getParentDecl(decl)); + if (parentPath.endsWith(".md")) + { + parentPath = Path::getParentDirectory(parentPath); + } + if (parentPath.getLength() > 0) + { + sb << parentPath; + sb << "/"; + } + sb << _translateNameToPath(_getName(decl).getUnownedSlice()); + sb << ".md"; + return sb.produceString(); +} + String DocMarkdownWriter::_getName(InheritanceDecl* decl) { StringBuilder buf; @@ -132,12 +313,64 @@ DocMarkdownWriter::NameAndText DocMarkdownWriter::_getNameAndText(ASTMarkup::Ent { NameAndText nameAndText; + nameAndText.decl = decl; nameAndText.name = _getName(decl); - - // We could extract different text here, but for now just do all markup - if (entry) + + StringBuilder sb; + if (auto varDeclBase = as<VarDeclBase>(decl)) + { + sb << " : " << varDeclBase->type->toString(); + + if (varDeclBase->initExpr) + { + sb << " = "; + _appendExpr(sb, varDeclBase->initExpr); + } + } + else if (auto typeParam = as<GenericTypeParamDeclBase>(decl)) + { + bool isFirst = true; + for (auto member : decl->parentDecl->members) + { + if (auto constraint = as<TypeConstraintDecl>(member)) + { + if (isDeclRefTypeOf<Decl>(getSub(m_astBuilder, constraint)).getDecl() == typeParam) + { + if (isFirst) + { + sb << ": "; + isFirst = false; + } + else + { + sb << ", "; + } + sb << constraint->getSup().type->toString(); + break; + } + } + } + if (auto genericTypeParam = as<GenericTypeParamDecl>(decl)) + { + if (genericTypeParam->initType.type) + { + sb << " = "; + sb << genericTypeParam->initType.type->toString(); + } + } + } + else if (auto enumCase = as<EnumCaseDecl>(decl)) + { + if (enumCase->tagExpr) + { + sb << " = "; + _appendExpr(sb, enumCase->tagExpr); + } + } + nameAndText.name.append(sb.produceString()); + + if (entry && entry->m_markup.getLength()) { - // For now we'll just use all markup, but really we need something more sophisticated here nameAndText.text = entry->m_markup; } @@ -172,21 +405,22 @@ List<String> DocMarkdownWriter::_getAsStringList(const List<Decl*>& in) void DocMarkdownWriter::_appendCommaList(const List<String>& strings, char wrapChar) { + auto& out = *m_builder; for (Index i = 0; i < strings.getCount(); ++i) { if (i > 0) { - m_builder << toSlice(", "); + out << toSlice(", "); } if (wrapChar) { - m_builder.appendChar(wrapChar); - m_builder << strings[i]; - m_builder.appendChar(wrapChar); + out.appendChar(wrapChar); + out << strings[i]; + out.appendChar(wrapChar); } else { - m_builder << strings[i]; + out << translateToMarkdownWithLinks(strings[i]); } } } @@ -242,35 +476,276 @@ void DocMarkdownWriter::_appendCommaList(const List<String>& strings, char wrapC } } +void escapeHTMLContent(StringBuilder& sb, UnownedStringSlice str) +{ + for (auto ch : str) + { + switch (ch) + { + case '<': sb << "<"; break; + case '>': sb << ">"; break; + case '&': sb << "&"; break; + case '"': sb << """; break; + default: sb.appendChar(ch); break; + } + } +} + void DocMarkdownWriter::writeVar(const ASTMarkup::Entry& entry, VarDecl* varDecl) { - writePreamble(entry); - auto& out = m_builder; + auto& out = *m_builder; + + ASTPrinter printer(m_astBuilder); + printer.addDeclPath(DeclRef<Decl>(varDecl)); + + out << toSlice("# ") << printer.getSlice() << toSlice("\n\n"); + + DeclDocumentation declDoc; + declDoc.parse(entry.m_markup.getUnownedSlice()); + declDoc.writeDescription(out, this, varDecl); + registerCategory(m_currentPage, declDoc); + + out << toSlice("## Signature\n"); + out << toSlice("<pre>\n"); + if (varDecl->hasModifier<HLSLStaticModifier>()) + { + out << toSlice("<span class='code_keyword'>static</span> "); + } + if (varDecl->hasModifier<ConstModifier>()) + { + out << toSlice("<span class='code_keyword'>const</span> "); + } + if (varDecl->hasModifier<ConstExprModifier>()) + { + out << toSlice("<span class='code_keyword'>constexpr</span> "); + } + if (varDecl->hasModifier<InModifier>()) + { + out << toSlice("<span class='code_keyword'>in</span> "); + } + if (varDecl->hasModifier<OutModifier>()) + { + out << toSlice("<span class='code_keyword'>out</span> "); + } + StringBuilder typeSB; + varDecl->type->toText(typeSB); + out << translateToHTMLWithLinks(varDecl, typeSB.produceString()) << toSlice(" "); + out << translateToHTMLWithLinks(varDecl, printer.getSlice()); + + if (varDecl->initExpr) + { + out << toSlice(" = "); + _appendExpr(out, varDecl->initExpr); + } + + out << toSlice(";\n</pre>\n\n"); + + declDoc.writeSection(out, this, varDecl, DocPageSection::Remarks); + declDoc.writeSection(out, this, varDecl, DocPageSection::Example); + declDoc.writeSection(out, this, varDecl, DocPageSection::SeeAlso); +} + +void DocMarkdownWriter::writeProperty(const ASTMarkup::Entry& entry, PropertyDecl* propertyDecl) +{ + auto& out = *m_builder; + out << toSlice("# property "); + + ASTPrinter printer(m_astBuilder); + printer.addDeclPath(DeclRef<Decl>(propertyDecl)); + out << escapeMarkdownText(printer.getSlice()) << toSlice("\n\n"); + + DeclDocumentation declDoc; + declDoc.parse(entry.m_markup.getUnownedSlice()); + declDoc.writeDescription(out, this, propertyDecl); + registerCategory(m_currentPage, declDoc); + + out << toSlice("## Signature\n\n"); + + out << toSlice("<pre>\n<span class='code_keyword'>property</span> "); + out << translateToHTMLWithLinks(propertyDecl, printer.getSlice()); + out << toSlice(" : "); + StringBuilder typeSB; + propertyDecl->type->toText(typeSB); + out << translateToHTMLWithLinks(propertyDecl, typeSB.produceString()); + out << "\n{\n"; + for (auto member : propertyDecl->members) + { + if (as<GetterDecl>(member)) + { + out << " get;\n"; + } + else if (as<SetterDecl>(member)) + { + out << " set;\n"; + } + else if (as<RefAccessorDecl>(member)) + { + out << " ref;\n"; + } + } + out << "}\n</pre>\n\n"; + + declDoc.writeSection(out, this, propertyDecl, DocPageSection::ReturnInfo); + declDoc.writeSection(out, this, propertyDecl, DocPageSection::Remarks); + declDoc.writeSection(out, this, propertyDecl, DocPageSection::Example); + declDoc.writeSection(out, this, propertyDecl, DocPageSection::SeeAlso); +} + +void DocMarkdownWriter::writeTypeDef(const ASTMarkup::Entry& entry, TypeDefDecl* typeDefDecl) +{ + auto& out = *m_builder; + + out << toSlice("# "); + ASTMarkup::Entry newEntry = entry; + _appendAggTypeName(newEntry, typeDefDecl); + out << toSlice("\n\n"); + + DeclDocumentation declDoc; + declDoc.parse(entry.m_markup.getUnownedSlice()); + registerCategory(m_currentPage, declDoc); + + declDoc.writeDescription(out, this, typeDefDecl); + + out << toSlice("## Signature\n\n"); + + out << toSlice("<pre>\n<span class='code_keyword'>typealias</span> "); + ASTPrinter printer(m_astBuilder); + printer.addDeclPath(typeDefDecl); + out << translateToHTMLWithLinks(typeDefDecl, printer.getSlice()); + out << toSlice(" = "); + + // Insert a line break if the type name is already long. + if (printer.getSlice().getLength() > 25) + { + out << "\n "; + } + out << translateToHTMLWithLinks(typeDefDecl, typeDefDecl->type->toString()); + out << ";\n</pre>\n\n"; + + declDoc.writeGenericParameters(out, this, typeDefDecl); + + declDoc.writeSection(out, this, typeDefDecl, DocPageSection::Remarks); + declDoc.writeSection(out, this, typeDefDecl, DocPageSection::Example); + declDoc.writeSection(out, this, typeDefDecl, DocPageSection::SeeAlso); +} + +void DocMarkdownWriter::writeExtensionConditions(StringBuilder& out, ExtensionDecl* extensionDecl, const char* prefix, bool isHtml) +{ + // Synthesize `where` clause for things defined in an extension. + auto targetTypeDeclRef = isDeclRefTypeOf<ContainerDecl>(extensionDecl->targetType); + if (!targetTypeDeclRef) + return; - out << toSlice("# ") << varDecl->getName()->text << toSlice("\n\n"); + if (auto genAppDeclRef = as<GenericAppDeclRef>(targetTypeDeclRef.declRefBase)) + { + for (Index i = 0; i < genAppDeclRef->getArgCount(); i++) + { + auto arg = genAppDeclRef->getArg(i); + Decl* genericParamDecl = nullptr; + Index parameterIndex = i; + if (auto extTypeParamDecl = isDeclRefTypeOf<GenericTypeParamDeclBase>(arg)) + { + genericParamDecl = extTypeParamDecl.getDecl(); + } + else if (auto extValueParamVal = as<GenericParamIntVal>(arg)) + { + genericParamDecl = extValueParamVal->getDeclRef().getDecl(); + } - // TODO(JS): The outputting of types this way isn't right - it doesn't handle int a[10] for example. - //ASTPrinter printer(m_astBuilder, ASTPrinter::OptionFlag::ParamNames); + // Locate the original generic parameter defined on the type being extended. + Decl* originalParamDecl = nullptr; + if (auto targetTypeParentGenericDecl = as<GenericDecl>(targetTypeDeclRef.getDecl()->parentDecl)) + { + for (auto member : targetTypeParentGenericDecl->members) + { + if (auto typeParamDecl = as<GenericTypeParamDeclBase>(member)) + { + if (typeParamDecl->parameterIndex == parameterIndex) + { + originalParamDecl = typeParamDecl; + break; + } + } + else if (auto valParamDecl = as<GenericValueParamDecl>(member)) + { + if (valParamDecl->parameterIndex == parameterIndex) + { + originalParamDecl = valParamDecl; + break; + } + } + } + } - out << toSlice("```\n"); - out << varDecl->type << toSlice(" ") << varDecl << toSlice("\n"); - out << toSlice("```\n\n"); + // If we can't find such parameter, bail. + if (!originalParamDecl) + continue; + + bool isEqualityConstraint = false; + Val* constraintVal = nullptr; + if (genericParamDecl) + { + // If we have `TargetType<T>` the member belongs to `extension<X : C> TargetType<X>`, + // We want to print a synthesized `where T : C` clause. + // Here `extTypeParamDecl` is a reference to `X`, so we need to find the corresponding `T`. - writeDescription(entry); + // Find constraints on the originalParamDecl. + for (auto member : genericParamDecl->parentDecl->members) + { + if (auto typeConstraint = as<GenericTypeConstraintDecl>(member)) + { + if (isDeclRefTypeOf<Decl>(typeConstraint->sub.type).getDecl() == genericParamDecl) + { + if (typeConstraint->isEqualityConstraint) + { + isEqualityConstraint = true; + } + constraintVal = typeConstraint->getSup().type; + break; + } + } + } + } + else + { + // If we have `extension TargetType<Y>` where `Y` does not name a generic parameter defined + // on the extension itself, we want to print a synthesized `where T == Y` clause, where + // `T` is the original generic parameter on the target type. + isEqualityConstraint = true; + constraintVal = arg; + } + if (constraintVal) + { + out << prefix; + if (isHtml) + out << translateToHTMLWithLinks(originalParamDecl, originalParamDecl->getName()->text); + else + out << translateToMarkdownWithLinks(originalParamDecl->getName()->text); + if (isEqualityConstraint) + out << " == "; + else + out << " : "; + if (isHtml) + out << translateToHTMLWithLinks(originalParamDecl, constraintVal->toString()); + else + out << translateToMarkdownWithLinks(constraintVal->toString()); + } + } + } } void DocMarkdownWriter::writeSignature(CallableDecl* callableDecl) { - auto& out = m_builder; + StringBuilder& out = *getBuilder(callableDecl); if (callableDecl->hasModifier<HLSLStaticModifier>()) { - out << "static "; + out << "<span class='code_keyword'>static</span> "; } List<ASTPrinter::Part> parts; - ASTPrinter printer(m_astBuilder, ASTPrinter::OptionFlag::ParamNames, &parts); + ASTPrinter printer(m_astBuilder, ASTPrinter::OptionFlag::ParamNames | ASTPrinter::OptionFlag::NoSpecializedExtensionTypeName, &parts); printer.addDeclSignature(makeDeclRef(callableDecl)); Signature signature; @@ -283,52 +758,34 @@ void DocMarkdownWriter::writeSignature(CallableDecl* callableDecl) const UnownedStringSlice returnType = printer.getPartSlice(signature.returnType); if (returnType.getLength() > 0) { - out << returnType << toSlice(" "); + out << translateToHTMLWithLinks(callableDecl, returnType) << toSlice(" "); } } - out << printer.getPartSlice(signature.name); - -#if 0 - if (signature.genericParams.getCount()) - { - out << toSlice("<"); - const Index count = signature.genericParams.getCount(); - for (Index i = 0; i < count; ++i) - { - const auto& genericParam = signature.genericParams[i]; - if (i > 0) - { - out << toSlice(", "); - } - out << printer.getPartSlice(genericParam.name); - - if (genericParam.type.type != Part::Type::None) - { - out << toSlice(" : "); - out << printer.getPartSlice(genericParam.type); - } - } - out << toSlice(">"); - } -#endif + out << translateToHTMLWithLinks(callableDecl, printer.getPartSlice(signature.name)); switch (paramCount) { case 0: { // Has no parameters - out << toSlice("();\n"); + out << toSlice("()"); break; } case 1: { - // Place all on single line - out.appendChar('('); - const auto& param = signature.params[0]; - out << printer.getPartSlice(param.first) << toSlice(" ") << printer.getPartSlice(param.second); - out << ");\n"; - break; + if (signature.name.end - signature.name.start < 40) + { + // Place all on single line + out.appendChar('('); + const auto& param = signature.params[0]; + out << translateToHTMLWithLinks(callableDecl, printer.getPartSlice(param.first)) << toSlice(" "); + out << translateToHTMLWithLinks(callableDecl, printer.getPartSlice(param.second)); + out << ")"; + break; + } + // If the name is already long, fall through to default. + [[fallthrough]]; } default: { @@ -339,20 +796,11 @@ void DocMarkdownWriter::writeSignature(CallableDecl* callableDecl) { const auto& param = signature.params[i]; line.clear(); - // If we want to tab these over... we'll need to know how must space I have - line << " " << printer.getPartSlice(param.first); - Index indent = 25; - if (line.getLength() < indent) - { - line.appendRepeatedChar(' ', indent - line.getLength()); - } - else - { - line.appendChar(' '); - } + line << " " << translateToHTMLWithLinks(callableDecl, printer.getPartSlice(param.first)); - line << printer.getPartSlice(param.second); + line.appendChar(' '); + line << translateToHTMLWithLinks(callableDecl, printer.getPartSlice(param.second)); if (i < paramCount - 1) { line << ",\n"; @@ -361,17 +809,53 @@ void DocMarkdownWriter::writeSignature(CallableDecl* callableDecl) out << line; } - out << ");\n"; + out << ")"; break; } } + + // Print `where` clause. + Decl* parentDecl = callableDecl; + while (parentDecl) + { + if (auto extensionDecl = as<ExtensionDecl>(parentDecl)) + { + // Synthesize `where` clause for things defined in an extension. + if (auto targetTypeDeclRef = isDeclRefTypeOf<ContainerDecl>(extensionDecl->targetType)) + { + writeExtensionConditions(out, extensionDecl, "\n <span class='code_keyword'>where</span> ", true); + // We need to follow the parent of the target type instead of the parent of the extension decl. + parentDecl = getParentDecl(targetTypeDeclRef.getDecl()); + continue; + } + } + + if (auto genericParent = as<GenericDecl>(parentDecl->parentDecl)) + { + for (auto member : genericParent->members) + { + if (auto typeConstraint = as<GenericTypeConstraintDecl>(member)) + { + out << toSlice("\n <span class='code_keyword'>where</span> "); + out << translateToHTMLWithLinks(parentDecl, getSub(m_astBuilder, typeConstraint)->toString()); + if (typeConstraint->isEqualityConstraint) + out << " == "; + else + out << toSlice(" : "); + out << translateToHTMLWithLinks(parentDecl, getSup(m_astBuilder, typeConstraint)->toString()); + } + } + } + parentDecl = getParentDecl(parentDecl); + } + out << ";\n"; } -List<DocMarkdownWriter::NameAndText> DocMarkdownWriter::_getUniqueParams(const List<Decl*>& decls) +List<DocMarkdownWriter::NameAndText> DocMarkdownWriter::_getUniqueParams(const List<Decl*>& decls, DeclDocumentation* funcDoc) { List<NameAndText> out; - Dictionary<Name*, Index> nameDict; + Dictionary<String, Index> nameDict; for (auto decl : decls) { @@ -380,231 +864,187 @@ List<DocMarkdownWriter::NameAndText> DocMarkdownWriter::_getUniqueParams(const L { continue; } - - Index index = nameDict.getOrAddValue(name, out.getCount()); + auto nameText = _getNameAndText(decl); + Index index = nameDict.getOrAddValue(nameText.name, out.getCount()); if (index >= out.getCount()) { - out.add(NameAndText{ getText(name), String() }); + out.add(nameText); } + // Extract text. NameAndText& nameAndMarkup = out[index]; if (nameAndMarkup.text.getLength() > 0) { continue; } - auto entry = m_markup->getEntry(decl); - if (entry && entry->m_markup.getLength()) + ParamDocumentation paramDoc; + if (funcDoc->parameters.tryGetValue(getText(name), paramDoc)) { - nameAndMarkup.text = entry->m_markup; + StringBuilder sb; + if (paramDoc.direction.getLength()) + sb << "\\[" << paramDoc.direction << "\\] "; + sb << paramDoc.description.ownedText; + nameAndMarkup.text = sb.produceString(); + } + else + { + auto entry = m_markup->getEntry(decl); + if (entry && entry->m_markup.getLength()) + { + nameAndMarkup.text = entry->m_markup; + } } } return out; } -static void _addRequirement(const DocMarkdownWriter::Requirement& req, List<DocMarkdownWriter::Requirement>& ioReqs) +static Index _addRequirement(const DocMarkdownWriter::Requirement& req, List<DocMarkdownWriter::Requirement>& ioReqs) { - if (ioReqs.indexOf(req) < 0) + auto index = ioReqs.indexOf(req); + if (index < 0) { ioReqs.add(req); + return ioReqs.getCount() - 1; } + return index; } -static void _addRequirement(CodeGenTarget target, const String& value, List<DocMarkdownWriter::Requirement>& ioReqs) +static Index _addRequirement(CapabilitySet set, List<DocMarkdownWriter::Requirement>& ioReqs) { - _addRequirement(DocMarkdownWriter::Requirement{ target, value }, ioReqs); + return _addRequirement(DocMarkdownWriter::Requirement{ set }, ioReqs); } -static DocMarkdownWriter::Requirement _getRequirementFromTargetToken(const Token& tok) +static Index _addRequirements(Decl* decl, List<DocMarkdownWriter::Requirement>& ioReqs) { - typedef DocMarkdownWriter::Requirement Requirement; - - if (tok.type == TokenType::Unknown) - { - return Requirement{ CodeGenTarget::None, String() }; - } - - auto targetName = tok.getContent(); - if (targetName == "spirv") - { - return Requirement{CodeGenTarget::SPIRV, UnownedStringSlice("")}; - } - - const CapabilityAtom targetCap = asAtom(findCapabilityName(targetName)); - - if (targetCap == CapabilityAtom::Invalid) - { - return Requirement{ CodeGenTarget::None, String() }; - } - SLANG_ASSERT(targetCap < CapabilityAtom::Count); - - static const CapabilityAtom rootAtoms[] = - { - CapabilityAtom::glsl, - CapabilityAtom::hlsl, - CapabilityAtom::cuda, - CapabilityAtom::cpp, - CapabilityAtom::c, - }; - - for (auto rootAtom : rootAtoms) - { - if (rootAtom == targetCap) - { - // If its one of the roots we don't need to store the name - targetName = UnownedStringSlice(); - break; - } - } + StringBuilder buf; - if (isCapabilityDerivedFrom(targetCap, CapabilityAtom::glsl)) - { - return Requirement{CodeGenTarget::GLSL, targetName}; - } - else if (isCapabilityDerivedFrom(targetCap, CapabilityAtom::hlsl)) - { - return Requirement{ CodeGenTarget::HLSL, targetName }; - } - else if (isCapabilityDerivedFrom(targetCap, CapabilityAtom::cuda)) - { - return Requirement{ CodeGenTarget::CUDASource, targetName }; - } - else if (isCapabilityDerivedFrom(targetCap, CapabilityAtom::cpp)) - { - return Requirement{ CodeGenTarget::CPPSource, targetName }; - } - else if (isCapabilityDerivedFrom(targetCap, CapabilityAtom::c)) - { - return Requirement{ CodeGenTarget::CSource, targetName }; - } - else if (isCapabilityDerivedFrom(targetCap, CapabilityAtom::metal)) - { - return Requirement{ CodeGenTarget::Metal, targetName }; - } - else if (isCapabilityDerivedFrom(targetCap, CapabilityAtom::wgsl)) + if (auto capAttr = decl->findModifier<RequireCapabilityAttribute>()) { - return Requirement{ CodeGenTarget::WGSL, targetName }; + return _addRequirement(capAttr->capabilitySet, ioReqs); } - return Requirement{ CodeGenTarget::Unknown, String() }; + return -1; } -static void _addRequirementFromTargetToken(const Token& tok, List<DocMarkdownWriter::Requirement>& ioReqs) +static String getCapabilityName(CapabilityName name) { - auto req = _getRequirementFromTargetToken(tok); - if (req.target != CodeGenTarget::None) + auto text = capabilityNameToString(name); + if (text.startsWith("_")) { - _addRequirement(req, ioReqs); + return text.tail(1); } + return text; } -static void _addRequirements(Decl* decl, List<DocMarkdownWriter::Requirement>& ioReqs) +static String getCapabilityName(CapabilityAtom atom) { - StringBuilder buf; - - if (auto spirvRequiredModifier = decl->findModifier<RequiredSPIRVVersionModifier>()) - { - buf.clear(); - buf << "SPIR-V "; - spirvRequiredModifier->version.append(buf); - _addRequirement(CodeGenTarget::GLSL, buf, ioReqs); - } - - if (auto glslRequiredModifier = decl->findModifier<RequiredGLSLVersionModifier>()) - { - buf.clear(); - buf << "GLSL" << glslRequiredModifier->versionNumberToken.getContent(); - _addRequirement(CodeGenTarget::GLSL, buf, ioReqs); - } - - if (auto cudaSMVersionModifier = decl->findModifier<RequiredCUDASMVersionModifier>()) - { - buf.clear(); - buf << "SM "; - cudaSMVersionModifier->version.append(buf); - _addRequirement(CodeGenTarget::CUDASource, buf, ioReqs); - } - - if (auto extensionModifier = decl->findModifier<RequiredGLSLExtensionModifier>()) - { - buf.clear(); - buf << extensionModifier->extensionNameToken.getContent(); - _addRequirement(CodeGenTarget::GLSL, buf, ioReqs); - } + return getCapabilityName((CapabilityName)atom); +} - if (const auto requiresNVAPIAttribute = decl->findModifier<RequiresNVAPIAttribute>()) +void DocMarkdownWriter::_appendExpr(StringBuilder& sb, Expr* expr) +{ + if (auto typeCast = as<TypeCastExpr>(expr)) + _appendExpr(sb, typeCast->arguments[0]); + else if (auto declRefExpr = as<DeclRefExpr>(expr)) { - _addRequirement(CodeGenTarget::HLSL, "NVAPI", ioReqs); + ASTPrinter printer(m_astBuilder); + printer.addDeclPath(declRefExpr->declRef); + sb << escapeMarkdownText(printer.getSlice()); } - - for (auto targetIntrinsic : decl->getModifiersOfType<TargetIntrinsicModifier>()) + else if (auto litExpr = as<LiteralExpr>(expr)) { - _addRequirementFromTargetToken(targetIntrinsic->targetToken, ioReqs); + sb << litExpr->token.getContent(); } - for (auto specializedForTarget : decl->getModifiersOfType<SpecializedForTargetModifier>()) + else { - _addRequirementFromTargetToken(specializedForTarget->targetToken, ioReqs); + sb << "..."; } } -void DocMarkdownWriter::_writeTargetRequirements(const Requirement* reqs, Index reqsCount) +void DocMarkdownWriter::_appendRequirements(const Requirement& requirement) { - if (reqsCount == 0) + auto capabilitySet = requirement.capabilitySet; + m_builder->append("Defined for the following targets:\n\n"); + for (auto targetSet : capabilitySet.getCapabilityTargetSets()) { - return; - } - - auto& out = m_builder; - - // Okay we need the name of the CodeGen target - UnownedStringSlice name = TypeTextUtil::getCompileTargetName(SlangCompileTarget(reqs->target)); + m_builder->append("#### "); + m_builder->append(getCapabilityName(targetSet.first)); + m_builder->append("\n"); - out << toSlice("**") << String(name).toUpper() << toSlice("**"); - - if (!(reqsCount == 1 && reqs[0].value.getLength() == 0)) - { - // Put in a list so we can use convenience funcs - List<String> values; - for (Index i = 0; i < reqsCount; i++) + if (targetSet.second.shaderStageSets.getCount() == kCapabilityStageCount) { - if (reqs[i].value.getLength() > 0) + m_builder->append("Available in all stages.\n"); + } + else if (targetSet.second.shaderStageSets.getCount() > 1) + { + m_builder->append("Available in stages: "); + bool isFirst = true; + for (auto& stage : targetSet.second.shaderStageSets) { - values.add(reqs[i].value); + if (!isFirst) + { + m_builder->append(", "); + } + isFirst = false; + m_builder->append("`"); + m_builder->append(getCapabilityName(stage.first)); + m_builder->append("`"); } + m_builder->append(".\n"); + } + else if (targetSet.second.shaderStageSets.getCount() == 1) + { + m_builder->append("Available in `"); + m_builder->append(getCapabilityName(targetSet.second.shaderStageSets.begin()->first)); + m_builder->append("` stage only.\n"); } - out << toSlice(" "); - _appendCommaList(values, '`'); - } - - out << toSlice(" "); -} - -void DocMarkdownWriter::_appendRequirements(const List<Requirement>& requirements) -{ - Index startIndex = 0; - CodeGenTarget curTarget = CodeGenTarget::None; - - for (Index i = 0; i < requirements.getCount(); ++i) - { - const auto& req = requirements[i]; + m_builder->append("\n"); - if (req.target != curTarget) + // TODO: We should probably print the capabilities for each stage set if the requirements differ between + // different stages, but for now we'll just print the first one, assuming the rest are the same. + // This is currently true for most if not all of our stdlib decls. + // + if (targetSet.second.shaderStageSets.getCount() > 0 && + targetSet.second.shaderStageSets.begin()->second.atomSet.has_value()) { - _writeTargetRequirements(requirements.getBuffer() + startIndex, i - startIndex); - - startIndex = i; - curTarget = req.target; + List<String> capabilities; + auto atomSet = targetSet.second.shaderStageSets.begin()->second.atomSet.value().newSetWithoutImpliedAtoms(); + for (auto atom : atomSet) + { + // If the requirement atom is the target or stage atom, don't repeat ourselves. + if ((CapabilityAtom)atom == targetSet.first) + continue; + if ((CapabilityAtom)atom == targetSet.second.shaderStageSets.begin()->first) + continue; + String capabilityName = capabilityNameToString((CapabilityName)atom); + if (!capabilityName.startsWith("_")) + { + capabilities.add(capabilityName); + } + } + if (capabilities.getCount() > 1) + { + m_builder->append("Requires capabilities: "); + _appendCommaList(capabilities, '`'); + m_builder->append(".\n"); + } + else if (capabilities.getCount() == 1) + { + m_builder->append("Requires capability: `"); + m_builder->append(capabilities[0]); + m_builder->append("`"); + m_builder->append(".\n"); + } } } - - _writeTargetRequirements(requirements.getBuffer() + startIndex, requirements.getCount() - startIndex); } -void DocMarkdownWriter::_maybeAppendRequirements(const UnownedStringSlice& title, const List<List<DocMarkdownWriter::Requirement>>& uniqueRequirements) +void DocMarkdownWriter::_maybeAppendRequirements(const UnownedStringSlice& title, const List<DocMarkdownWriter::Requirement>& uniqueRequirements) { - auto& out = m_builder; + auto& out = *m_builder; const Index uniqueCount = uniqueRequirements.getCount(); if (uniqueCount <= 0) @@ -616,12 +1056,6 @@ void DocMarkdownWriter::_maybeAppendRequirements(const UnownedStringSlice& title { const auto& reqs = uniqueRequirements[0]; - // If just HLSL on own, then ignore - if (reqs.getCount() == 0 || (reqs.getCount() == 1 && reqs[0] == Requirement{ CodeGenTarget::HLSL, String() })) - { - return; - } - out << title; _appendRequirements(reqs); @@ -633,7 +1067,7 @@ void DocMarkdownWriter::_maybeAppendRequirements(const UnownedStringSlice& title for (Index i = 0; i < uniqueCount; ++i) { - out << (i + 1) << (". "); + out << "### Capability Set " << (i + 1) << ("\n\n"); _appendRequirements(uniqueRequirements[i]); out << toSlice("\n"); } @@ -642,39 +1076,16 @@ void DocMarkdownWriter::_maybeAppendRequirements(const UnownedStringSlice& title out << toSlice("\n"); } -static Decl* _getSameNameDecl(Decl* decl) +static Decl* _getSameNameDecl(ContainerDecl* parentDecl, Decl* decl) { - auto parentDecl = decl->parentDecl; - - // Sanity check: there should always be a parent declaration. - // - SLANG_ASSERT(parentDecl); - if (!parentDecl) return nullptr; - - // If the declaration is the "inner" declaration of a generic, - // then we actually want to look one level up, because the - // peers/siblings of the declaration will belong to the same - // parent as the generic, not to the generic. - // - if (auto genericParentDecl = as<GenericDecl>(parentDecl)) - { - // Note: we need to check here to be sure `newDecl` - // is the "inner" declaration and not one of the - // generic parameters, or else we will end up - // checking them at the wrong scope. - // - if (decl == genericParentDecl->inner) - { - decl = parentDecl; - } - } - - return decl; + Decl* result = nullptr; + parentDecl->getMemberDictionary().tryGetValue(decl->getName(), result); + return result; } static bool _isFirstOverridden(Decl* decl) { - decl = _getSameNameDecl(decl); + decl = _getSameNameDecl(as<ContainerDecl>(getParentDecl(decl)), decl); ContainerDecl* parentDecl = decl->parentDecl; @@ -688,47 +1099,291 @@ static bool _isFirstOverridden(Decl* decl) return false; } -void DocMarkdownWriter::writeCallableOverridable(const ASTMarkup::Entry& entry, CallableDecl* callableDecl) +void ParsedDescription::write(DocMarkdownWriter* writer, Decl* decl, StringBuilder& out) { - auto& out = m_builder; - - writePreamble(entry); - + for (auto span : spans) { - // Output the overridable path (ie without terminal generic parameters) - ASTPrinter printer(m_astBuilder); - printer.addOverridableDeclPath(DeclRef<Decl>(callableDecl)); - // Extract the name - out << toSlice("# `") << printer.getStringBuilder() << toSlice("`\n\n"); + switch (span.kind) + { + case DocumentationSpanKind::OrdinaryText: + { + out << span.text; + break; + } + case DocumentationSpanKind::InlineCode: + { + out << "<span class='code'>" << writer->translateToHTMLWithLinks(decl, span.text) << "</span>"; + break; + } + } } +} - writeDescription(entry); - - List<CallableDecl*> sigs; +void ParsedDescription::parse(UnownedStringSlice text) +{ + text = text.trim(); + ownedText = text; + List<UnownedStringSlice> lines; + StringUtil::calcLines(text, lines); + bool isInCodeBlock = false; + for (auto line : lines) { - Decl* sameNameDecl = _getSameNameDecl(callableDecl); + line = line.trim(); + if (line.startsWith("```")) + { + isInCodeBlock = !isInCodeBlock; + spans.add({ line, DocumentationSpanKind::OrdinaryText}); + spans.add({ toSlice("\n"), DocumentationSpanKind::OrdinaryText }); + continue; + } + + if (!isInCodeBlock) + { + bool isInCode = false; + const char* currentSpanStart = line.begin(); + const char* currentSpanEnd = currentSpanStart; + for (Index i = 0; i < line.getLength(); i++) + { + if (line[i] == '`') + { + if (currentSpanEnd > currentSpanStart) + { + spans.add({ + UnownedStringSlice(currentSpanStart, line.begin() + i), + isInCode ? DocumentationSpanKind::InlineCode : DocumentationSpanKind::OrdinaryText }); + currentSpanEnd = currentSpanStart = line.begin() + i + 1; + } + isInCode = !isInCode; + } + else + { + currentSpanEnd = line.begin() + i + 1; + } + } + if (currentSpanEnd > currentSpanStart) + { + spans.add({ UnownedStringSlice(currentSpanStart, currentSpanEnd), + DocumentationSpanKind::OrdinaryText }); + } + spans.add({ toSlice("\n"), DocumentationSpanKind::OrdinaryText }); + } + else + { + spans.add({ line, DocumentationSpanKind::OrdinaryText }); + spans.add({ toSlice("\n"), DocumentationSpanKind::OrdinaryText }); + } + } +} - for (Decl* curDecl = sameNameDecl; curDecl; curDecl = curDecl->nextInContainerWithSameName) +void DeclDocumentation::parse(const UnownedStringSlice& text) +{ + List<UnownedStringSlice> lines; + StringUtil::calcLines(text, lines); + DocPageSection currentSection = DocPageSection::Description; + Dictionary<DocPageSection, StringBuilder> sectionBuilders; + for (Index ptr = 0; ptr < lines.getCount(); ptr++) + { + auto line = lines[ptr].trim(); + if (line.startsWith("@param")) { - CallableDecl* sig = nullptr; - if (GenericDecl* genericDecl = as<GenericDecl>(curDecl)) + currentSection = DocPageSection::Parameter; + line = line.tail(6).trimStart(); + UnownedStringSlice paramDirection; + UnownedStringSlice paramName; + if (line.startsWith("[")) + { + auto closingIndex = line.indexOf(']'); + if (closingIndex != -1) + { + paramDirection = line.subString(1, closingIndex - 1); + line = line.tail(closingIndex + 1).trimStart(); + } + } + auto spaceIndex = line.indexOf(' '); + if (spaceIndex != -1) + { + paramName = line.subString(0, spaceIndex); + line = line.tail(spaceIndex + 1).trimStart(); + } + StringBuilder paramSB; + paramSB << line << "\n"; + ptr++; + for (; ptr < lines.getCount(); ptr++) { - sig = as<CallableDecl>(genericDecl->inner); + auto nextLine = lines[ptr].trim(); + if (nextLine.getLength() == 0 || nextLine.startsWith("@")) + { + ptr--; + break; + } + paramSB << nextLine << "\n"; + } + ParamDocumentation paramDesc; + paramDesc.description.parse(paramSB.getUnownedSlice()); + paramDesc.name = paramName; + paramDesc.direction = paramDirection; + parameters[paramDesc.name] = paramDesc; + continue; + } + else if (line.startsWith("@return")) + { + currentSection = DocPageSection::ReturnInfo; + line = line.tail(7).trim(); + } + else if (line.startsWith("@returns")) + { + currentSection = DocPageSection::ReturnInfo; + line = line.tail(8).trim(); + } + else if (line.startsWith("@remarks")) + { + currentSection = DocPageSection::Remarks; + line = line.tail(8).trim(); + } + else if (line.startsWith("@example")) + { + currentSection = DocPageSection::Example; + line = line.tail(8).trim(); + } + else if (line.startsWith("@see")) + { + currentSection = DocPageSection::SeeAlso; + line = line.tail(4).trim(); + } + else if (line.startsWith("@experimental")) + { + currentSection = DocPageSection::ExperimentalCallout; + line = line.tail(13).trim(); + } + else if (line.startsWith("@internal")) + { + currentSection = DocPageSection::InternalCallout; + line = line.tail(9).trim(); + } + else if (line.startsWith("@deprecated")) + { + currentSection = DocPageSection::DeprecatedCallout; + line = line.tail(11).trim(); + } + else if (line.startsWith("@category")) + { + line = line.tail(9).trimStart(); + auto spaceIndex = line.indexOf(' '); + if (spaceIndex != -1) + { + categoryName = line.subString(0, spaceIndex); + categoryText = line.tail(spaceIndex + 1).trim(); } else { - sig = as<CallableDecl>(curDecl); + categoryName = line.trim(); } - - if (!sig) + continue; + } + sectionBuilders[currentSection] << line << "\n"; + + // If the current directive is a callout, set currentSection back + // to Description after processing the directive line. + switch (currentSection) + { + case DocPageSection::ExperimentalCallout: + case DocPageSection::InternalCallout: + case DocPageSection::DeprecatedCallout: + currentSection = DocPageSection::Description; + break; + } + } + for (auto& kv : sectionBuilders) + { + sections[kv.first].parse(kv.second.getUnownedSlice()); + } +} + +void DocMarkdownWriter::writeCallableOverridable(DocumentPage* page, const ASTMarkup::Entry& primaryEntry, CallableDecl* callableDecl) +{ + SLANG_UNUSED(primaryEntry); + + auto& out = *m_builder; + { + // Output the overridable path (ie without terminal generic parameters) + ASTPrinter printer(m_astBuilder, ASTPrinter::OptionFlag::NoSpecializedExtensionTypeName); + printer.addOverridableDeclPath(DeclRef<Decl>(callableDecl)); + // Extract the name + out << toSlice("# ") << escapeMarkdownText(printer.getStringBuilder()) << toSlice("\n\n"); + } + + // Collect descriptions from all overloads. + StringBuilder descriptionSB, additionalDescriptionSB; + for (auto entry : page->entries) + { + auto markup = entry->m_markup.trim(); + if (markup.getLength() == 0) + continue; + if (entry->m_markup.startsWith("@")) + { + additionalDescriptionSB << markup << "\n"; + } + else if (descriptionSB.getLength() != 0) + { + // We already have a main description, so this is potentially a duplicate. + // If the content is not the same, we will report a warning. + if (!descriptionSB.toString().startsWith(markup)) { - continue; + auto decl = as<Decl>(entry->m_node); + m_sink->diagnose(decl->loc, Diagnostics::ignoredDocumentationOnOverloadCandidate, decl); } + } + else + { + descriptionSB << markup << "\n"; + } + } + + DeclDocumentation funcDoc; + funcDoc.parse(descriptionSB.getUnownedSlice()); + funcDoc.parse(additionalDescriptionSB.getUnownedSlice()); + + registerCategory(page, funcDoc); - // Want to add only the primary sig - if (sig->primaryDecl == nullptr || sig->primaryDecl == sig) + auto& descSection = funcDoc.sections[DocPageSection::Description]; + if (descSection.ownedText.getLength() > 0) + { + out << toSlice("## Description\n\n"); + descSection.write(this, callableDecl, out); + } + + // Collect all overloads from all entries on the page. + List<CallableDecl*> sigs; + List<Requirement> requirements; + HashSet<Decl*> sigSet; + { + for (auto& entry : page->entries) + { + Decl* sameNameDecl = _getSameNameDecl(as<ContainerDecl>(getParentDecl((Decl*)entry->m_node)), callableDecl); + + for (Decl* curDecl = sameNameDecl; curDecl; curDecl = curDecl->nextInContainerWithSameName) { - sigs.add(sig); + CallableDecl* sig = nullptr; + if (GenericDecl* genericDecl = as<GenericDecl>(curDecl)) + { + sig = as<CallableDecl>(genericDecl->inner); + } + else + { + sig = as<CallableDecl>(curDecl); + } + + if (!sig) + { + continue; + } + + // Want to add only the primary sig + if (sig->primaryDecl == nullptr || sig->primaryDecl == sig) + { + if (sigSet.add(sig)) + sigs.add(sig); + } } } @@ -736,8 +1391,6 @@ void DocMarkdownWriter::writeCallableOverridable(const ASTMarkup::Entry& entry, sigs.sort([](CallableDecl* a, CallableDecl* b) -> bool { return a->loc.getRaw() < b->loc.getRaw(); }); } - // The unique requirements found - List<List<Requirement>> uniqueRequirements; // Maps a sig index to a unique requirements set List<Index> requirementsMap; @@ -746,40 +1399,17 @@ void DocMarkdownWriter::writeCallableOverridable(const ASTMarkup::Entry& entry, CallableDecl* sig = sigs[i]; // Add the requirements for all the different versions - List<Requirement> requirements; for (CallableDecl* curSig = sig; curSig; curSig = curSig->nextDecl) { - _addRequirements(sig, requirements); - } - - // TODO(JS): HACK - almost everything is available for HLSL, and is generally not marked up in stdlib - // So assume here it's available - _addRequirement(Requirement{ CodeGenTarget::HLSL, String() }, requirements); - - // We want to make the requirements set unique, so we can look it up easily in the uniqueRequirements, so we sort it - // This also has the benefit of keeping the ordering consistent - requirements.sort(); - - // See if we already have this combination of requirements - Index uniqueRequirementIndex = uniqueRequirements.indexOf(requirements); - if (uniqueRequirementIndex < 0) - { - // If not add it - uniqueRequirementIndex = uniqueRequirements.getCount(); - uniqueRequirements.add(requirements); + requirementsMap.add(_addRequirements(sig, requirements)); } - - // Add the uniqueRequirement index for this sig index - requirementsMap.add(uniqueRequirementIndex); } // Output the signature { out << toSlice("## Signature \n\n"); - out << toSlice("```\n"); + out << toSlice("<pre>\n"); - Index prevRequirementsIndex = -1; - const Int sigCount = sigs.getCount(); for (Index i = 0; i < sigCount; ++i) { @@ -788,22 +1418,18 @@ void DocMarkdownWriter::writeCallableOverridable(const ASTMarkup::Entry& entry, const Index requirementsIndex = requirementsMap[i]; // Output if needs unique requirements - if (uniqueRequirements.getCount() > 1 ) + if (requirements.getCount() > 1 && requirementsIndex != -1) { - if (requirementsIndex != prevRequirementsIndex) - { - out << toSlice("/// See Availability ") << (requirementsIndex + 1) << toSlice("\n"); - prevRequirementsIndex = requirementsIndex; - } + out << toSlice("/// Requires Capability Set ") << (requirementsIndex + 1) << toSlice(":\n"); } writeSignature(sig); + + out << "\n"; } - out << "```\n\n"; + out << "</pre>\n\n"; } - _maybeAppendRequirements(toSlice("## Availability\n\n"), uniqueRequirements); - { // We will use the first documentation found for each parameter type { @@ -822,7 +1448,7 @@ void DocMarkdownWriter::writeCallableOverridable(const ASTMarkup::Entry& entry, { for (Decl* decl : genericDecl->members) { - if (as<GenericTypeParamDecl>(decl) || + if (as<GenericTypeParamDeclBase>(decl) || as<GenericValueParamDecl>(decl)) { genericDecls.add(decl); @@ -836,26 +1462,62 @@ void DocMarkdownWriter::writeCallableOverridable(const ASTMarkup::Entry& entry, } } - if (paramDecls.getCount() > 0 || paramDecls.getCount() > 0) + if (genericDecls.getCount() > 0) + { + out << "## Generic Parameters\n\n"; + + // Document generic parameters + _appendAsBullets(_getUniqueParams(genericDecls, &funcDoc), false, 0); + + out << toSlice("\n"); + } + + if (paramDecls.getCount() > 0) { out << "## Parameters\n\n"; - // Get the unique generics - _appendAsBullets(_getUniqueParams(genericDecls), '`'); - // And parameters - _appendAsBullets(_getUniqueParams(paramDecls), '`'); + // Document ordinary parameters + _appendAsBullets(_getUniqueParams(paramDecls, &funcDoc), false, 0); out << toSlice("\n"); } } } + + auto& returnsSection = funcDoc.sections[DocPageSection::ReturnInfo]; + if (returnsSection.ownedText.getLength() > 0) + { + out << toSlice("## Return value\n"); + returnsSection.write(this, callableDecl, out); + } + + auto& remarksSection = funcDoc.sections[DocPageSection::Remarks]; + if (remarksSection.ownedText.getLength() > 0) + { + out << toSlice("## Remarks\n"); + remarksSection.write(this, callableDecl, out); + } + + auto& exampleSection = funcDoc.sections[DocPageSection::Example]; + if (exampleSection.ownedText.getLength() > 0) + { + out << toSlice("## Example\n"); + exampleSection.write(this, callableDecl, out); + } + + _maybeAppendRequirements(toSlice("## Availability and Requirements\n\n"), requirements); + + auto& seeAlsoSection = funcDoc.sections[DocPageSection::SeeAlso]; + if (seeAlsoSection.ownedText.getLength() > 0) + { + out << toSlice("## See Also\n"); + seeAlsoSection.write(this, callableDecl, out); + } } void DocMarkdownWriter::writeEnum(const ASTMarkup::Entry& entry, EnumDecl* enumDecl) { - writePreamble(entry); - - auto& out = m_builder; + auto& out = *m_builder; out << toSlice("# enum "); Name* name = enumDecl->getName(); @@ -865,16 +1527,23 @@ void DocMarkdownWriter::writeEnum(const ASTMarkup::Entry& entry, EnumDecl* enumD } out << toSlice("\n\n"); + DeclDocumentation declDoc; + declDoc.parse(entry.m_markup.getUnownedSlice()); + declDoc.writeDescription(out, this, enumDecl); + registerCategory(m_currentPage, declDoc); + out << toSlice("## Values \n\n"); - _appendAsBullets(_getAsNameAndTextList(enumDecl->getMembersOfType<EnumCaseDecl>()), '_'); + _appendAsBullets(_getAsNameAndTextList(enumDecl->getMembersOfType<EnumCaseDecl>()), false, '_'); - writeDescription(entry); + declDoc.writeSection(out, this, enumDecl, DocPageSection::Remarks); + declDoc.writeSection(out, this, enumDecl, DocPageSection::Example); + declDoc.writeSection(out, this, enumDecl, DocPageSection::SeeAlso); } void DocMarkdownWriter::_appendEscaped(const UnownedStringSlice& text) { - auto& out = m_builder; + auto& out = *m_builder; const char* start = text.begin(); const char* cur = start; @@ -918,7 +1587,7 @@ void DocMarkdownWriter::_appendEscaped(const UnownedStringSlice& text) void DocMarkdownWriter::_appendDerivedFrom(const UnownedStringSlice& prefix, AggTypeDeclBase* aggTypeDecl) { - auto& out = m_builder; + auto& out = *m_builder; List<InheritanceDecl*> inheritanceDecls; _getDecls(aggTypeDecl, inheritanceDecls); @@ -934,14 +1603,42 @@ void DocMarkdownWriter::_appendDerivedFrom(const UnownedStringSlice& prefix, Agg { out << toSlice(", "); } - out << inheritanceDecl->base; + out << escapeMarkdownText(inheritanceDecl->base->toString()); } } } -void DocMarkdownWriter::_appendAggTypeName(AggTypeDeclBase* aggTypeDecl) +void DocMarkdownWriter::_appendAggTypeName(const ASTMarkup::Entry& entry, Decl* aggTypeDecl) { - auto& out = m_builder; + SLANG_UNUSED(entry); + + auto& out = *m_builder; + +#if 0 + // For extensions, try to see if the documentation defines a more readable title. + if (as<ExtensionDecl>(aggTypeDecl)) + { + auto trimStart = String(entry.m_markup.trimStart()); + if (trimStart.startsWith("@title")) + { + List<UnownedStringSlice> lines; + StringUtil::calcLines(trimStart.getUnownedSlice(), lines); + if (lines.getCount() > 0) + { + out << escapeMarkdownText(lines[0].tail(6).trim()); + + // Remove @title directive from the description markup. + StringBuilder restSB; + for (Index i = 1; i < lines.getCount(); ++i) + { + restSB << lines[i] << "\n"; + } + entry.m_markup = restSB.produceString(); + return; + } + } + } +#endif // This could be lots of different things - struct/class/extension/interface/.. @@ -950,55 +1647,86 @@ void DocMarkdownWriter::_appendAggTypeName(AggTypeDeclBase* aggTypeDecl) if (as<StructDecl>(aggTypeDecl)) { - out << toSlice("struct ") << printer.getStringBuilder(); + out << toSlice("struct ") << escapeMarkdownText(printer.getStringBuilder().produceString()); } else if (as<ClassDecl>(aggTypeDecl)) { - out << toSlice("class ") << printer.getStringBuilder(); + out << toSlice("class ") << escapeMarkdownText(printer.getStringBuilder().produceString()); } else if (as<InterfaceDecl>(aggTypeDecl)) { - out << toSlice("interface ") << printer.getStringBuilder(); + out << toSlice("interface ") << escapeMarkdownText(printer.getStringBuilder().produceString()); } else if (ExtensionDecl* extensionDecl = as<ExtensionDecl>(aggTypeDecl)) { - out << toSlice("extension ") << extensionDecl->targetType; + out << toSlice("extension ") << escapeMarkdownText(extensionDecl->targetType->toString()); _appendDerivedFrom(toSlice(" : "), extensionDecl); } + else if (as<TypeDefDecl>(aggTypeDecl)) + { + out << toSlice("typealias ") << escapeMarkdownText(printer.getStringBuilder().produceString()); + } else { out << toSlice("?"); } } -void DocMarkdownWriter::writeAggType(const ASTMarkup::Entry& entry, AggTypeDeclBase* aggTypeDecl) +void DocMarkdownWriter::writeAggType(DocumentPage* page, const ASTMarkup::Entry& primaryEntry, AggTypeDeclBase* aggTypeDecl) { - writePreamble(entry); - - auto& out = m_builder; + auto& out = *m_builder; // We can write out he name using the printer - - ASTPrinter printer(m_astBuilder); - printer.addDeclPath(DeclRef<Decl>(aggTypeDecl)); - - out << toSlice("# `"); - _appendAggTypeName(aggTypeDecl); - out << toSlice("`\n\n"); - + out << toSlice("# "); + _appendAggTypeName(primaryEntry, aggTypeDecl); + out << toSlice("\n\n"); + List<ExtensionDecl*> conditionalConformanceExts; { List<InheritanceDecl*> inheritanceDecls; _getDecls<InheritanceDecl>(aggTypeDecl, inheritanceDecls); - - if (inheritanceDecls.getCount()) + List<String> baseTypes; + HashSet<String> conditionalBaseTypes; + baseTypes = _getAsStringList(inheritanceDecls); + for (auto entry : page->entries) + { + for (auto member : as<ContainerDecl>(entry->m_node)->members) + { + if (auto inheritanceDecl = as<InheritanceDecl>(member)) + { + if (auto extDecl = as<ExtensionDecl>(entry->m_node)) + { + conditionalConformanceExts.add(extDecl); + conditionalBaseTypes.add(inheritanceDecl->base->toString()); + } + } + } + } + if (baseTypes.getCount()) { - out << "*Implements:* "; - _appendCommaList(_getAsStringList(inheritanceDecls), '`'); + if (as<InterfaceDecl>(aggTypeDecl)) + out << "*Inherits from:* "; + else + out << "*Conforms to:* "; + _appendCommaList(baseTypes, 0); + out << toSlice("\n\n"); + } + if (conditionalBaseTypes.getCount()) + { + out << "*Conditionally conforms to:* "; + List<String> list; + for (auto t : conditionalBaseTypes) + list.add(t); + _appendCommaList(list, 0); out << toSlice("\n\n"); } } - writeDescription(entry); + DeclDocumentation declDoc; + declDoc.parse(primaryEntry.m_markup.getUnownedSlice()); + declDoc.writeDescription(out, this, aggTypeDecl); + registerCategory(page, declDoc); + + declDoc.writeGenericParameters(out, this, aggTypeDecl); { List<AssocTypeDecl*> assocTypeDecls; @@ -1009,7 +1737,7 @@ void DocMarkdownWriter::writeAggType(const ASTMarkup::Entry& entry, AggTypeDeclB out << toSlice("# Associated types\n\n"); for (AssocTypeDecl* assocTypeDecl : assocTypeDecls) - { + { out << "* _" << assocTypeDecl->getName()->text << "_ "; // Look up markup @@ -1020,8 +1748,8 @@ void DocMarkdownWriter::writeAggType(const ASTMarkup::Entry& entry, AggTypeDeclB } out << toSlice("\n"); - List<InheritanceDecl*> inheritanceDecls; - _getDecls<InheritanceDecl>(assocTypeDecl, inheritanceDecls); + List<TypeConstraintDecl*> inheritanceDecls; + _getDecls<TypeConstraintDecl>(assocTypeDecl, inheritanceDecls); if (inheritanceDecls.getCount()) { @@ -1035,101 +1763,449 @@ void DocMarkdownWriter::writeAggType(const ASTMarkup::Entry& entry, AggTypeDeclB } } - if (GenericDecl* genericDecl = as<GenericDecl>(aggTypeDecl->parentDecl)) { - // The parameters, in order - List<Decl*> params; - for (Decl* decl : genericDecl->members) + List<Decl*> fields; + _getDeclsOfType<VarDecl>(this, page, fields); + if (fields.getCount()) { - if (as<GenericTypeParamDecl>(decl) || - as<GenericValueParamDecl>(decl)) - { - params.add(decl); - } + out << toSlice("## Fields\n\n"); + _appendAsBullets(_getAsNameAndTextList(fields), true, 0); + out << toSlice("\n"); } - - if (params.getCount()) + } + + { + List<Decl*> properties; + _getDeclsOfType<PropertyDecl>(this, page, properties); + if (properties.getCount()) { - out << toSlice("## Generic Parameters\n\n"); - _appendAsBullets(_getAsNameAndTextList(params), '`'); + out << toSlice("## Properties\n\n"); + _appendAsBullets(_getAsNameAndTextList(properties), true, 0); out << toSlice("\n"); } } { - List<Decl*> fields; - _getDeclsOfType<VarDecl>(aggTypeDecl, fields); - if (fields.getCount()) + List<Decl*> uniqueMethods; + _getDeclsOfType<CallableDecl>(this, page, uniqueMethods); + + if (uniqueMethods.getCount()) { - out << toSlice("## Fields\n\n"); - _appendAsBullets(_getAsNameAndTextList(fields), '`'); + // Put in source definition order + uniqueMethods.sort([](Decl* a, Decl* b) -> bool { return a->loc.getRaw() < b->loc.getRaw(); }); + + out << "## Methods\n\n"; + _appendAsBullets(_getAsStringList(uniqueMethods), 0); out << toSlice("\n"); } } + if (conditionalConformanceExts.getCount()) { - // Make sure we've got a query-able member dictionary - auto& memberDict = aggTypeDecl->getMemberDictionary(); + out << "## Conditional Conformances\n\n"; + for (auto ext : conditionalConformanceExts) + { + for (auto member : ext->members) + { + auto inheritanceDecl = as<InheritanceDecl>(member); + if (!inheritanceDecl) + continue; + out << "### Conformance to "; + out << escapeMarkdownText(inheritanceDecl->base.type->toString()); + out << "\n"; + StringBuilder sb; + writeExtensionConditions(sb, ext, "\n", false); + List<UnownedStringSlice> lines, nonEmptyLines; + StringUtil::calcLines(sb.getUnownedSlice(), lines); + for (auto line : lines) + { + if (line.trim().getLength()) + nonEmptyLines.add(line); + } + ASTPrinter printer(m_astBuilder); + printer.addDeclPath(aggTypeDecl->getDefaultDeclRef()); + out << "`" << printer.getString() << "` additionally conforms to `"; + out << escapeMarkdownText(inheritanceDecl->base.type->toString()); + if (nonEmptyLines.getCount() != 0) + { + out << "` when the following conditions are met:\n\n"; + for (auto condition : nonEmptyLines) + { + out << " * " << condition << "\n"; + } + } + else + { + out << "`.\n"; + } + } + } + } + declDoc.writeSection(out, this, aggTypeDecl, DocPageSection::Remarks); + declDoc.writeSection(out, this, aggTypeDecl, DocPageSection::Example); + declDoc.writeSection(out, this, aggTypeDecl, DocPageSection::SeeAlso); +} - List<Decl*> uniqueMethods; - for (const auto& [_, decl] : memberDict) +String DocMarkdownWriter::escapeMarkdownText(String text) +{ + StringBuilder sb; + for (auto c : text) + { + switch (c) { - if (!shouldDocumentDecl(decl)) + case '_': + case '*': + case '[': + case ']': + case '<': + case '>': + case '|': + case '.': + case '!': + case '(': + case ')': + sb << '\\'; + sb.appendChar(c); + break; + default: + sb.appendChar(c); + break; + } + } + return sb.produceString(); +} + +void DocMarkdownWriter::ensureDeclPageCreated(ASTMarkup::Entry& entry) +{ + auto page = getPage(as<Decl>(entry.m_node)); + page->entries.add(&entry); +} + +Slang::Misc::Token treatLiteralsAsIdentifier(Slang::Misc::Token token) +{ + // If the token is a literal, we want to treat it as an identifier. + if (token.Type == Slang::Misc::TokenType::StringLiteral) + { + token.Type = Slang::Misc::TokenType::Identifier; + StringBuilder stringSB; + StringEscapeUtil::appendQuoted(StringEscapeUtil::getHandler(StringEscapeUtil::Style::Cpp), token.Content.getUnownedSlice(), stringSB); + token.Content = stringSB.produceString(); + } + else if (token.Type == Slang::Misc::TokenType::IntLiteral || + token.Type == Slang::Misc::TokenType::DoubleLiteral) + { + token.Type = Slang::Misc::TokenType::Identifier; + } + return token; +} + +String DocMarkdownWriter::translateToMarkdownWithLinks(String text, bool strictChildLookup) +{ + StringBuilder sb; + List<DocumentPage*> currentPage; + currentPage.add(m_currentPage); + Slang::Misc::TokenReader reader(text); + bool requireSpaceBeforeNextToken = false; + bool isFirstToken = true; + for (; !reader.IsEnd(); ) + { + auto token = treatLiteralsAsIdentifier(reader.ReadToken()); + + + if (token.Type == Slang::Misc::TokenType::Identifier) + { + if (requireSpaceBeforeNextToken) + sb.append(' '); + auto tokenContent = token.Content; + if (tokenContent == "operator") + { + for (;;) + { + auto operatorToken = reader.ReadToken(); + tokenContent.append(operatorToken.Content); + if (operatorToken.Type != Slang::Misc::TokenType::LParent && + operatorToken.Type != Slang::Misc::TokenType::LBracket) + { + break; + } + } + } + String sectionName; + Decl* referencedDecl = nullptr; + auto page = findPageForToken(currentPage.getLast(), tokenContent, sectionName, referencedDecl); + + if (isFirstToken && strictChildLookup && page && page->parentPage != m_currentPage) + { + // If we are performing a strict child lookup (for displaying the member list of an agg type), + // then we want to ignore any lookup results that refer to a different parent page. + page = nullptr; + } + + if (page) + { + sb.append("["); + sb << escapeMarkdownText(tokenContent.getUnownedSlice()); + sb.append("]("); + sb.append(getDocPath(m_config, page->path)); + if (sectionName.getLength()) + sb << "#" << sectionName; + sb.append(")"); + currentPage.getLast() = page; continue; - CallableDecl* callableDecl = as<CallableDecl>(decl); - if (callableDecl && isVisible(callableDecl)) + } + requireSpaceBeforeNextToken = true; + isFirstToken = false; + } + else + { + switch (token.Type) { - uniqueMethods.add(callableDecl); + case Slang::Misc::TokenType::OpLess: + case Slang::Misc::TokenType::OpGreater: + case Slang::Misc::TokenType::Comma: + case Slang::Misc::TokenType::Dot: + case Slang::Misc::TokenType::IntLiteral: + case Slang::Misc::TokenType::Semicolon: + requireSpaceBeforeNextToken = false; + break; + default: + requireSpaceBeforeNextToken = true; + sb.appendChar(' '); + break; } } - - if (uniqueMethods.getCount()) + // Maintain the `currentPage` stack so we can use the correct starting page + // to lookup things like `Foo<int>.Bar`. When we look up `Bar`, we want to start + // from the same page after looking up `Foo`, so we need to push the stack when we + // see `<` and pop the stack when we see `>`. + if (token.Type == Slang::Misc::TokenType::OpLess) { - // Put in source definition order - uniqueMethods.sort([](Decl* a, Decl* b) -> bool { return a->loc.getRaw() < b->loc.getRaw(); }); + currentPage.add(currentPage.getLast()); + } + else if (token.Type == Slang::Misc::TokenType::OpGreater) + { + if (currentPage.getCount() > 1) + currentPage.removeLast(); + } + sb << escapeMarkdownText(token.Content.getUnownedSlice()); + if (token.Type == Slang::Misc::TokenType::Comma) + sb.appendChar(' '); + } + return sb.produceString(); +} - out << "## Methods\n\n"; - _appendAsBullets(_getAsStringList(uniqueMethods), '`'); - out << toSlice("\n"); +// Implemented in slang-language-server-completion.cpp +bool isDeclKeyword(const UnownedStringSlice& slice); + +bool isKeyword(const UnownedStringSlice& slice) +{ + if (isDeclKeyword(slice)) + return true; + static const char* knownTypeNames[] = { "int", "float", "half", "double", "bool", "void", "uint" }; + for (auto typeName : knownTypeNames) + { + if (slice == typeName) + return true; + } + return false; +} + +String DocMarkdownWriter::translateToHTMLWithLinks(Decl* decl, String text) +{ + SLANG_UNUSED(decl); + StringBuilder sb; + List<DocumentPage*> currentPage; + currentPage.add(m_currentPage); + Slang::Misc::TokenReader reader(text); + bool prevIsIdentifier = false; + for (; !reader.IsEnd(); ) + { + auto token = treatLiteralsAsIdentifier(reader.ReadToken()); + + if (token.Type == Slang::Misc::TokenType::Identifier) + { + if (prevIsIdentifier) + sb.append(' '); + String sectionName; + Decl* referencedDecl = nullptr; + auto page = findPageForToken(currentPage.getLast(), token.Content, sectionName, referencedDecl); + if (page) + { + sb.append("<a href=\""); + sb.append(getDocPath(m_config, page->path)); + if (sectionName.getLength()) + sb << "#" << sectionName; + sb.append("\""); + if (isKeyword(token.Content.getUnownedSlice())) + sb.append(" class=\"code_keyword\""); + else if (as<AggTypeDeclBase>(referencedDecl) || + as<SimpleTypeDecl>(referencedDecl)) + sb.append(" class=\"code_type\""); + else if (as<ParamDecl>(referencedDecl)) + sb.append(" class=\"code_param\""); + else if (as<VarDeclBase>(referencedDecl) || as<EnumCaseDecl>(referencedDecl)) + sb.append(" class=\"code_var\""); + sb.append(">"); + escapeHTMLContent(sb, token.Content.getUnownedSlice()); + sb.append("</a>"); + currentPage.getLast() = page; + continue; + } + prevIsIdentifier = true; + } + else + { + prevIsIdentifier = false; } + // Maintain the `currentPage` stack so we can use the correct starting page + // to lookup things like `Foo<int>.Bar`. When we look up `Bar`, we want to start + // from the same page after looking up `Foo`, so we need to push the stack when we + // see `<` and pop the stack when we see `>`. + if (token.Type == Slang::Misc::TokenType::OpLess) + { + currentPage.add(m_currentPage); + } + else if (token.Type == Slang::Misc::TokenType::OpGreater) + { + if (currentPage.getCount() > 1) + currentPage.removeLast(); + } + bool shouldCloseSpan = false; + if (isKeyword(token.Content.getUnownedSlice())) + { + sb.append("<span class=\"code_keyword\">"); + shouldCloseSpan = true; + } + escapeHTMLContent(sb, token.Content.getUnownedSlice()); + if (shouldCloseSpan) + sb.append("</span>"); + if (token.Type == Slang::Misc::TokenType::Comma) + sb.appendChar(' '); } + return sb.produceString(); } -void DocMarkdownWriter::writePreamble(const ASTMarkup::Entry& entry) +void DocMarkdownWriter::writePreamble() { - SLANG_UNUSED(entry); - auto& out = m_builder; + auto& out = *m_builder; + if (out.getLength() == 0) + { + out << m_config.preamble; + out << toSlice("\n"); + } +} + +const char* getSectionTitle(DocPageSection section) +{ + switch (section) + { + case DocPageSection::Description: return "Description"; + case DocPageSection::Parameter: return "Parameters"; + case DocPageSection::ReturnInfo: return "Return value"; + case DocPageSection::Remarks: return "Remarks"; + case DocPageSection::Example: return "Example"; + case DocPageSection::SeeAlso: return "See also"; + default: return ""; + } +} - //out << toSlice("\n"); +void DeclDocumentation::writeDescription(StringBuilder& out, DocMarkdownWriter* writer, Decl* decl) +{ + // Write all callout sections first. + writeSection(out, writer, decl, DocPageSection::DeprecatedCallout); + writeSection(out, writer, decl, DocPageSection::ExperimentalCallout); + writeSection(out, writer, decl, DocPageSection::InternalCallout); - out.appendRepeatedChar('-', 80); - out << toSlice("\n"); + // Write description section. + writeSection(out, writer, decl, DocPageSection::Description); } -void DocMarkdownWriter::writeDescription(const ASTMarkup::Entry& entry) +void DeclDocumentation::writeGenericParameters(StringBuilder& out, DocMarkdownWriter* writer, Decl* decl) { - auto& out = m_builder; + GenericDecl* genericDecl = as<GenericDecl>(decl->parentDecl); + if (!genericDecl) + return; - if (entry.m_markup.getLength() > 0) + // The parameters, in order + List<Decl*> params; + for (Decl* member : genericDecl->members) { - out << toSlice("## Description\n\n"); + if (as<GenericTypeParamDeclBase>(member) || + as<GenericValueParamDecl>(member)) + { + params.add(member); + } + } - out << entry.m_markup.getUnownedSlice(); -#if 0 - UnownedStringSlice text(entry.m_markup.getUnownedSlice()), line; - while (StringUtil::extractLine(text, line)) + if (params.getCount()) + { + out << toSlice("## Generic Parameters\n\n"); + auto paramList = writer->_getAsNameAndTextList(params); + + // Append names with constraints if any. + for (Index i = 0; i < paramList.getCount(); i++) { - out << line << toSlice("\n"); + auto param = params[i]; + if (paramList[i].text.getLength() == 0) + { + ParamDocumentation paramDoc; + if (parameters.tryGetValue(getText(param->getName()), paramDoc)) + { + StringBuilder sb; + sb << paramDoc.description.ownedText; + paramList[i].text = sb.produceString(); + } + } } -#endif + writer->_appendAsBullets(paramList, false, 0); out << toSlice("\n"); } } -void DocMarkdownWriter::writeDecl(const ASTMarkup::Entry& entry, Decl* decl) +void DeclDocumentation::writeSection(StringBuilder& out, DocMarkdownWriter* writer, Decl* decl, DocPageSection section) +{ + SLANG_UNUSED(decl); + ParsedDescription* sectionDoc = sections.tryGetValue(section); + if (!sectionDoc) + return; + + switch (section) + { + case DocPageSection::DeprecatedCallout: + out << "> #### Deprecated Feature\n"; + out << "> The feature described in this page is marked as deprecated, and may be removed in a future release.\n"; + out << "> Users are advised to avoid using this feature, and to migrate to a newer alternative.\n"; + out << "\n"; + return; + case DocPageSection::ExperimentalCallout: + out << "> #### Experimental Feature\n"; + out << "> The feature described in this page is marked as experimental, and may be subject to change in future releases.\n"; + out << "> Users are advised that any code that depend on this feature may not be compilable by future versions of the compiler.\n"; + out << "\n"; + return; + case DocPageSection::InternalCallout: + out << "> #### Internal Feature\n"; + out << "> The feature described in this page is marked as an internal implementation detail, and is not intended for use by end-users.\n"; + out << "> Users are advised to avoid using this declaration directly, as it may be removed or changed in future releases.\n"; + out << "\n"; + return; + } + if (sectionDoc && sectionDoc->ownedText.getLength() > 0) + { + out << "## " << getSectionTitle(section) << "\n\n"; + sectionDoc->write(writer, decl, out); + } +} + +void DocMarkdownWriter::createPage(DocMarkdownWriter::WriteDeclMode mode, ASTMarkup::Entry& entry, Decl* decl) { // Skip these they will be output as part of their respective 'containers' - if (as<ParamDecl>(decl) || as<EnumCaseDecl>(decl) || as<AssocTypeDecl>(decl) || as<InheritanceDecl>(decl) || as<ThisTypeDecl>(decl)) + if (as<ParamDecl>(decl) || + as<EnumCaseDecl>(decl) || + as<AssocTypeDecl>(decl) || + as<TypeConstraintDecl>(decl) || + as<ThisTypeDecl>(decl) || + as<AccessorDecl>(decl)) { return; } @@ -1138,26 +2214,35 @@ void DocMarkdownWriter::writeDecl(const ASTMarkup::Entry& entry, Decl* decl) { if (_isFirstOverridden(callableDecl)) { - writeCallableOverridable(entry, callableDecl); + if (mode == WriteDeclMode::Header) + ensureDeclPageCreated(entry); } } - else if (EnumDecl* enumDecl = as<EnumDecl>(decl)) + else if (as<EnumDecl>(decl)) { - writeEnum(entry, enumDecl); + if (mode == WriteDeclMode::Header) + ensureDeclPageCreated(entry); } - else if (AggTypeDeclBase* aggType = as<AggTypeDeclBase>(decl)) + else if (as<AggTypeDeclBase>(decl)) { - writeAggType(entry, aggType); + if (mode == WriteDeclMode::Header) + ensureDeclPageCreated(entry); } - else if (VarDecl* varDecl = as<VarDecl>(decl)) + else if (as<VarDecl>(decl)) { // If part of aggregate type will be output there. - if (as<AggTypeDecl>(varDecl->parentDecl)) - { - return; - } - - writeVar(entry, varDecl); + if (mode == WriteDeclMode::Header) + ensureDeclPageCreated(entry); + } + else if (as<TypeDefDecl>(decl)) + { + if (mode == WriteDeclMode::Header) + ensureDeclPageCreated(entry); + } + else if (as<PropertyDecl>(decl)) + { + if (mode == WriteDeclMode::Header) + ensureDeclPageCreated(entry); } else if (as<GenericDecl>(decl)) { @@ -1165,9 +2250,113 @@ void DocMarkdownWriter::writeDecl(const ASTMarkup::Entry& entry, Decl* decl) } } +void DocMarkdownWriter::registerCategory(DocumentPage* page, DeclDocumentation& doc) +{ + if (doc.categoryText.getLength() != 0) + { + m_categories[doc.categoryName] = doc.categoryText; + } + else if (!m_categories.containsKey(doc.categoryName)) + { + m_categories[doc.categoryName] = doc.categoryName; + } + page->category = doc.categoryName; +} + + bool DocMarkdownWriter::isVisible(const Name* name) { - return name == nullptr || !name->text.startsWith(toSlice("__")); + return name == nullptr || !name->text.startsWith(toSlice("__")) + || m_config.visibleDeclNames.contains(getText((Name*)name)); +} + +DocumentPage* DocMarkdownWriter::findPageForToken(DocumentPage* currentPage, String token, String& outSectionName, Decl*& outDecl) +{ + while (currentPage) + { + // Are there any children pages whose short title matches `token`? + // If so, return the path of that page. + if (currentPage->shortName == token) + { + outDecl = currentPage->decl; + return currentPage; + } + if (auto rs = currentPage->findChildByShortName(token.getUnownedSlice())) + { + outDecl = rs->decl; + return rs; + } + // Is `token` documented as a section on current page? + // This will be the case for parameters and generic parameters. + if (currentPage->decl) + { + for (auto entry : currentPage->entries) + { + auto containerDecl = as<ContainerDecl>(entry->m_node); + if (!containerDecl) + continue; + if (auto genericParent = as<GenericDecl>(containerDecl->parentDecl)) + { + for (auto member : genericParent->members) + { + if (getText(member->getName()) == token) + { + outDecl = member; + if (as<GenericTypeParamDeclBase>(member)) + outSectionName = String("typeparam-") + token; + else if (as<GenericValueParamDecl>(member)) + outSectionName = String("decl-") + token; + return currentPage; + } + } + } + for (auto member : containerDecl->members) + { + if (as<ParamDecl>(member) || as<EnumCaseDecl>(member)) + { + if (getText(member->getName()) == token) + { + outDecl = member; + outSectionName = String("decl-") + token; + return currentPage; + } + } + } + } + } + + currentPage = currentPage->parentPage; + } + // Otherwise, try find in global decls. + if (auto rs = m_typesPage->findChildByShortName(token.getUnownedSlice())) + { + outDecl = rs->decl; + return rs; + } + if (auto rs = m_interfacesPage->findChildByShortName(token.getUnownedSlice())) + { + outDecl = rs->decl; + return rs; + } + if (auto rs = m_globalDeclsPage->findChildByShortName(token.getUnownedSlice())) + { + outDecl = rs->decl; + return rs; + } + return nullptr; +} + +String DocMarkdownWriter::findLinkForToken(DocumentPage* currentPage, String token) +{ + String sectionName; + Decl* decl = nullptr; + if (auto page = findPageForToken(currentPage, token, sectionName, decl)) + { + if (sectionName.getLength() == 0) + return page->path; + return page->path + "#" + sectionName; + } + return String(); } bool DocMarkdownWriter::isVisible(const ASTMarkup::Entry& entry) @@ -1179,7 +2368,7 @@ bool DocMarkdownWriter::isVisible(const ASTMarkup::Entry& entry) } Decl* decl = as<Decl>(entry.m_node); - return decl == nullptr || isVisible(decl->getName()); + return decl == nullptr || isVisible(decl); } bool DocMarkdownWriter::isVisible(Decl* decl) @@ -1188,22 +2377,362 @@ bool DocMarkdownWriter::isVisible(Decl* decl) { return false; } - + bool parentIsVisible = true; + auto parent = decl; + while (parent) + { + if (auto extDecl = as<ExtensionDecl>(parent)) + { + if (auto targetDecl = isDeclRefTypeOf<Decl>(extDecl->targetType)) + { + parentIsVisible = parentIsVisible && isVisible(targetDecl.getDecl()); + parent = targetDecl.getDecl(); + } + else + { + parent = getParentDecl(parent); + } + } + else + { + parent = getParentDecl(parent); + } + if (as<AggTypeDeclBase>(parent)) + { + parentIsVisible = parentIsVisible && isVisible(parent); + } + } auto entry = m_markup->getEntry(decl); - return entry == nullptr || entry->m_visibility == MarkupVisibility::Public; + return parentIsVisible && (entry == nullptr || entry->m_visibility == MarkupVisibility::Public); } -void DocMarkdownWriter::writeAll() +void DocumentationConfig::parse(UnownedStringSlice config) { - for (const auto& entry : m_markup->getEntries()) + List<UnownedStringSlice> lines; + StringUtil::calcLines(config, lines); + Index ptr = 0; + for (;ptr < lines.getCount(); ptr++) + { + auto line = lines[ptr]; + if (line.startsWith(toSlice("@preamble:"))) + { + ptr++; + StringBuilder preambleSB; + for (; ptr < lines.getCount(); ptr++) + { + if (lines[ptr].startsWith("@end")) + break; + preambleSB << lines[ptr] << "\n"; + } + ptr++; + preamble = preambleSB.produceString(); + } + else if (line.startsWith(toSlice("@title:"))) + { + title = line.tail(7).trim(); + } + else if (line.startsWith(toSlice("@libname:"))) + { + libName = line.tail(9).trim(); + } + else if (line.startsWith(toSlice("@rootdir:"))) + { + rootDir = line.tail(9).trim(); + } + else if (line.startsWith("@includedecl:")) + { + ptr++; + for (; ptr < lines.getCount(); ptr++) + { + if (lines[ptr].startsWith("@end")) + break; + auto name = lines[ptr].trim(); + if (name.getLength()) + visibleDeclNames.add(name); + } + ptr++; + } + } +} + +void sortPages(DocumentPage* page) +{ + page->children.sort([](DocumentPage* a, DocumentPage* b) -> bool { return a->shortName < b->shortName; }); +} + +void DocMarkdownWriter::generateSectionIndexPage(DocumentPage* page) +{ + // Generate the content for meta section index page. + StringBuilder& sb = page->get(); + sb << m_config.preamble; + sb << "# " << page->title; + sb << "\n\n"; + sb << m_config.libName; + sb << " defines the following " << String(page->title).toLower() << ":\n\n"; + sortPages(page); + + for (auto child : page->children) + { + sb << "- [" << escapeMarkdownText(child->shortName) << "](" << getDocPath(m_config, child->path) << ")\n"; + } +} + +DocumentPage* DocMarkdownWriter::writeAll(UnownedStringSlice configStr) +{ + m_config.parse(configStr); + + auto addBuiltinPage = [&](DocumentPage* parent, UnownedStringSlice path, UnownedStringSlice title, UnownedStringSlice shortTitle) + { + RefPtr<DocumentPage> page = new DocumentPage(); + page->title = title; + page->path = path; + page->shortName = shortTitle; + page->decl = nullptr; + if (parent) + { + parent->children.add(page); + } + m_output[page->path] = page; + return page.get(); + }; + m_rootPage = addBuiltinPage(nullptr, toSlice("index.md"), m_config.title.getUnownedSlice(), toSlice("Standard Library Reference")); + m_rootPage->skipWrite = true; + + m_interfacesPage = addBuiltinPage(m_rootPage.get(), toSlice("interfaces/index.md"), toSlice("Interfaces"), toSlice("Interfaces")); + m_typesPage = addBuiltinPage(m_rootPage.get(), toSlice("types/index.md"), toSlice("Types"), toSlice("Types")); + m_globalDeclsPage = addBuiltinPage(m_rootPage.get(), toSlice("global-decls/index.md"), toSlice("Global Declarations"), toSlice("Global Declarations")); + + // In the first pass, we create all the pages so we can reference them + // when writing the content. + for (auto& entry : m_markup->getEntries()) { Decl* decl = as<Decl>(entry.m_node); if (decl && isVisible(entry)) { - writeDecl(entry, decl); + createPage(WriteDeclMode::Header, entry, decl); + } + } + // In the second pass, actually writes the content to each page. + writePageRecursive(m_rootPage.get()); + + generateSectionIndexPage(m_interfacesPage); + generateSectionIndexPage(m_typesPage); + generateSectionIndexPage(m_globalDeclsPage); + + return m_rootPage.get(); +} + +void DocMarkdownWriter::writePage(DocumentPage* page) +{ + if (page->skipWrite) + return; + if (page->entries.getCount() == 0) + return; + + m_currentPage = page; + m_builder = &(page->get()); + + writePreamble(); + + Decl* decl = (Decl*)page->getFirstEntry()->m_node; + if (CallableDecl* callableDecl = as<CallableDecl>(decl)) + { + writeCallableOverridable(page, *page->getFirstEntry(), callableDecl); + } + else if (EnumDecl* enumDecl = as<EnumDecl>(decl)) + { + writeEnum(*page->getFirstEntry(), enumDecl); + } + else if (AggTypeDeclBase* aggTypeDeclBase = as<AggTypeDeclBase>(decl)) + { + // Find the primary decl. + ASTMarkup::Entry* primaryEntry = page->getFirstEntry(); + AggTypeDeclBase* primaryDecl = aggTypeDeclBase; + for (auto entry : page->entries) + { + if (auto aggTypeDecl = as<AggTypeDecl>(entry->m_node)) + { + primaryEntry = entry; + primaryDecl = aggTypeDecl; + break; + } + } + writeAggType(page, *primaryEntry, primaryDecl); + } + else if (PropertyDecl* propertyDecl = as<PropertyDecl>(decl)) + { + writeProperty(*page->getFirstEntry(), propertyDecl); + } + else if (VarDecl* varDecl = as<VarDecl>(decl)) + { + writeVar(*page->getFirstEntry(), varDecl); + } + else if (TypeDefDecl* typeDefDecl = as<TypeDefDecl>(decl)) + { + writeTypeDef(*page->getFirstEntry(), typeDefDecl); + } +} + +void DocMarkdownWriter::writePageRecursive(DocumentPage* page) +{ + writePage(page); + for (auto child : page->children) + { + writePageRecursive(child); + } +} + +void writeTOCImpl(StringBuilder& sb, DocMarkdownWriter* writer, DocumentationConfig& config, DocumentPage* page); + +void writeTOCChildren(StringBuilder& sb, DocMarkdownWriter* writer, DocumentationConfig& config, DocumentPage* page) +{ + if (page->children.getCount() == 0) + return; + + sb << R"(<ul class="toc_list">)" << "\n"; + + // Don't sort the root page. + if (page->path != "index.md") + sortPages(page); + + Dictionary<String, List<DocumentPage*>> categories; + for (auto child : page->children) + { + categories[child->category].add(child); + } + List<String> categoryNames; + for (auto& kv : categories) + { + categoryNames.add(kv.first); + } + categoryNames.sort(); + auto parentPath = Path::getParentDirectory(page->path); + parentPath.append("/"); + for (auto& cat : categoryNames) + { + // Skip non-categorized pages first. + if (cat.getLength() == 0) + continue; + sb << "<li data-link=\"" << config.rootDir << parentPath << cat << "\"><span>"; + escapeHTMLContent(sb, writer->m_categories[cat].getUnownedSlice()); + sb << "</span>\n"; + sb << "<ul class=\"toc_list\">\n"; + for (auto child : categories[cat]) + { + writeTOCImpl(sb, writer, config, child); + } + sb << "</ul>\n"; + sb << "</li>\n"; + + // Create a landing page for the category. + RefPtr<DocumentPage> landingPage = new DocumentPage(); + landingPage->title = writer->m_categories[cat]; + landingPage->path = parentPath + cat + ".md"; + landingPage->shortName = writer->m_categories[cat]; + landingPage->decl = nullptr; + landingPage->parentPage = page; + landingPage->contentSB << config.preamble; + landingPage->contentSB << "# " << landingPage->title << "\n\nThis category contains the following declarations:\n\n"; + for (auto child : categories[cat]) + { + landingPage->contentSB << "#### [" << writer->escapeMarkdownText(child->title) << "](" << + child->path << ")\n\n"; } + page->children.add(landingPage); } + + for (auto child : categories[""]) + { + writeTOCImpl(sb, writer, config, child); + } + sb << "</ul>\n"; } +void writeTOCImpl(StringBuilder& sb, DocMarkdownWriter* writer, DocumentationConfig& config, DocumentPage* page) +{ + sb << R"(<li data-link=")" << getDocPath(config, page->path) << R"("><span>)"; + escapeHTMLContent(sb, page->shortName.getUnownedSlice()); + sb << "</span>\n"; + writeTOCChildren(sb, writer, config, page); + sb << "</li>"; +} + +String DocMarkdownWriter::writeTOC() +{ + StringBuilder sb; + sb << R"(<ul class="toc_root_list"><li data-link=")" + << m_config.rootDir << R"(index"><span>)" + << m_config.title << "</span>\n"; + writeTOCChildren(sb, this, m_config, m_rootPage); + sb << "</li></ul>\n"; + return sb.produceString(); +} + +DocumentPage* DocMarkdownWriter::getPage(Decl* decl) +{ + auto path = _getDocFilePath(decl); + RefPtr<DocumentPage> page; + if (m_output.tryGetValue(path, page)) + { + return page.get(); + } + page = new DocumentPage(); + page->title = _getFullName(decl); + page->path = path; + page->shortName = _getName(decl); + page->decl = decl; + m_output[path] = page; + + // If parent page exists, add this page to it's children + if (path.endsWith("index.md")) + path = Path::getParentDirectory(path); + auto parentPath = Path::getParentDirectory(path); + parentPath = parentPath + "/index.md"; + RefPtr<DocumentPage> parentPage; + if (m_output.tryGetValue(parentPath, parentPage)) + { + parentPage->children.add(page); + page->parentPage = parentPage.get(); + } + return page.get(); +} + +StringBuilder* DocMarkdownWriter::getBuilder(Decl* decl) +{ + m_currentPage = getPage(decl); + return &(m_currentPage->get()); +} + +void writePageToDisk(DocumentPage* page) +{ + if (!page->skipWrite) + { + auto dir = Path::getParentDirectory(page->path); + if (dir.getLength()) + Path::createDirectoryRecursive(dir); + File::writeAllText(page->path, page->contentSB.toString()); + } + for (auto child : page->children) + { + writePageToDisk(child); + } +} + +void DocumentPage::writeToDisk() +{ + writePageToDisk(this); +} + +DocumentPage* DocumentPage::findChildByShortName(const UnownedStringSlice& name) +{ + for (auto child : children) + { + if (child->shortName == name) + return child; + } + return nullptr; +} + + } // namespace Slang |
