2 * Ddoc documentation generation.
4 * Specification: $(LINK2 https://dlang.org/spec/ddoc.html, Documentation Generator)
6 * Copyright: Copyright (C) 1999-2024 by The D Language Foundation, All Rights Reserved
7 * Authors: $(LINK2 https://www.digitalmars.com, Walter Bright)
8 * License: $(LINK2 https://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
9 * Source: $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/doc.d, _doc.d)
10 * Documentation: https://dlang.org/phobos/dmd_doc.html
11 * Coverage: https://codecov.io/gh/dlang/dmd/src/master/src/dmd/doc.d
16 import core
.stdc
.ctype
;
17 import core
.stdc
.stdlib
;
18 import core
.stdc
.stdio
;
19 import core
.stdc
.string
;
20 import core
.stdc
.time
;
22 import dmd
.arraytypes
;
27 import dmd
.declaration
;
35 import dmd
.dsymbolsem
;
42 import dmd
.identifier
;
46 import dmd
.root
.array
;
48 import dmd
.root
.filename
;
49 import dmd
.common
.outbuffer
;
52 import dmd
.root
.string
;
62 const(char)[][char.max
] strings
;
64 /***************************************
65 * Find character string to replace c with.
67 const(char)[] escapeChar(char c
) @safe
71 //printf("escapeChar('%c') => %p, %p\n", c, strings, strings[c].ptr);
97 /***********************************************************
105 override string
toString() const
110 void write(Loc loc
, DocComment
* dc
, Scope
* sc
, Dsymbols
* a
, ref OutBuffer buf
)
115 static immutable table
=
131 foreach (entry
; table
)
133 if (iequals(entry
, name
))
135 buf
.printf("$(DDOC_%s ", entry
.ptr
);
139 buf
.writestring("$(DDOC_SECTION ");
140 // Replace _ characters with spaces
141 buf
.writestring("$(DDOC_SECTION_H ");
142 size_t o
= buf
.length
;
143 foreach (char c
; name
)
144 buf
.writeByte((c
== '_') ?
' ' : c
);
145 escapeStrayParenthesis(loc
, buf
, o
, false, sc
.eSink
);
146 buf
.writestring(")");
150 buf
.writestring("$(DDOC_DESCRIPTION ");
153 size_t o
= buf
.length
;
155 escapeStrayParenthesis(loc
, buf
, o
, true, sc
.eSink
);
156 highlightText(sc
, a
, loc
, buf
, o
);
157 buf
.writestring(")");
161 /***********************************************************
163 final class ParamSection
: Section
165 override void write(Loc loc
, DocComment
* dc
, Scope
* sc
, Dsymbols
* a
, ref OutBuffer buf
)
168 Dsymbol s
= (*a
)[0]; // test
169 const(char)* p
= body_
.ptr
;
170 size_t len
= body_
.length
;
171 const(char)* pend
= p
+ len
;
172 const(char)* tempstart
= null;
174 const(char)* namestart
= null;
175 size_t namelen
= 0; // !=0 if line continuation
176 const(char)* textstart
= null;
178 size_t paramcount
= 0;
179 buf
.writestring("$(DDOC_PARAMS ");
182 // Skip to start of macro
195 if (isIdStart(p
) ||
isCVariadicArg(p
[0 .. cast(size_t
)(pend
- p
)]))
199 // continuation of prev macro
207 if (isCVariadicArg(p
[0 .. cast(size_t
)(pend
- p
)]))
209 templen
= p
- tempstart
;
210 while (*p
== ' ' ||
*p
== '\t')
216 // continuation of prev macro
222 // Output existing param
224 //printf("param '%.*s' = '%.*s'\n", cast(int)namelen, namestart, cast(int)textlen, textstart);
227 buf
.writestring("$(DDOC_PARAM_ROW ");
229 buf
.writestring("$(DDOC_PARAM_ID ");
231 size_t o
= buf
.length
;
232 Parameter fparam
= isFunctionParameter(a
, namestart
[0 .. namelen
]);
235 // Comments on a template might refer to function parameters within.
236 // Search the parameters of nested eponymous functions (with the same name.)
237 fparam
= isEponymousFunctionParameter(a
, namestart
[0 .. namelen
]);
239 bool isCVariadic
= isCVariadicParameter(a
, namestart
[0 .. namelen
]);
242 buf
.writestring("...");
244 else if (fparam
&& fparam
.type
&& fparam
.ident
)
246 toCBuffer(fparam
.type
, buf
, fparam
.ident
, hgs
);
250 if (isTemplateParameter(a
, namestart
, namelen
))
252 // 10236: Don't count template parameters for params check
257 sc
.eSink
.warning(s
.loc
, "Ddoc: function declaration has no parameter '%.*s'", cast(int)namelen
, namestart
);
259 buf
.write(namestart
[0 .. namelen
]);
261 escapeStrayParenthesis(loc
, buf
, o
, true, sc
.eSink
);
262 highlightCode(sc
, a
, buf
, o
);
264 buf
.writestring(")");
265 buf
.writestring("$(DDOC_PARAM_DESC ");
267 size_t o
= buf
.length
;
268 buf
.write(textstart
[0 .. textlen
]);
269 escapeStrayParenthesis(loc
, buf
, o
, true, sc
.eSink
);
270 highlightText(sc
, a
, loc
, buf
, o
);
272 buf
.writestring(")");
274 buf
.writestring(")");
279 namestart
= tempstart
;
281 while (*p
== ' ' ||
*p
== '\t')
287 textlen
= p
- textstart
;
299 // write out last one
300 buf
.writestring(")");
301 TypeFunction tf
= a
.length
== 1 ?
isTypeFunction(s
) : null;
304 size_t pcount
= (tf
.parameterList
.parameters ? tf
.parameterList
.parameters
.length
: 0) +
305 cast(int)(tf
.parameterList
.varargs
== VarArg
.variadic
);
306 if (pcount
!= paramcount
)
308 sc
.eSink
.warning(s
.loc
, "Ddoc: parameter count mismatch, expected %llu, got %llu",
309 cast(ulong) pcount
, cast(ulong) paramcount
);
312 // Chances are someone messed up the format
313 sc
.eSink
.warningSupplemental(s
.loc
, "Note that the format is `param = description`");
320 /***********************************************************
322 final class MacroSection
: Section
324 override void write(Loc loc
, DocComment
* dc
, Scope
* sc
, Dsymbols
* a
, ref OutBuffer buf
)
326 //printf("MacroSection::write()\n");
327 DocComment
.parseMacros(dc
.escapetable
, *dc
.pmacrotable
, body_
);
331 alias Sections
= Array
!(Section
);
333 // Workaround for missing Parameter instance for variadic params. (it's unnecessary to instantiate one).
334 bool isCVariadicParameter(Dsymbols
* a
, const(char)[] p
) @safe
338 TypeFunction tf
= isTypeFunction(member
);
339 if (tf
&& tf
.parameterList
.varargs
== VarArg
.variadic
&& p
== "...")
345 Dsymbol
getEponymousMember(TemplateDeclaration td
) @safe
349 if (AggregateDeclaration ad
= td
.onemember
.isAggregateDeclaration())
351 if (FuncDeclaration fd
= td
.onemember
.isFuncDeclaration())
353 if (auto em
= td
.onemember
.isEnumMember())
354 return null; // Keep backward compatibility. See compilable/ddoc9.d
355 if (VarDeclaration vd
= td
.onemember
.isVarDeclaration())
356 return td
.constraint ?
null : vd
;
360 TemplateDeclaration
getEponymousParent(Dsymbol s
) @safe
364 TemplateDeclaration td
= s
.parent
.isTemplateDeclaration();
365 return (td
&& getEponymousMember(td
)) ? td
: null;
368 immutable ddoc_default
= import("default_ddoc_theme." ~ ddoc_ext
);
369 immutable ddoc_decl_s
= "$(DDOC_DECL ";
370 immutable ddoc_decl_e
= ")\n";
371 immutable ddoc_decl_dd_s
= "$(DDOC_DECL_DD ";
372 immutable ddoc_decl_dd_e
= ")\n";
374 /****************************************************
375 * Generate Ddoc file for Module m.
378 * ddoctext_ptr = combined text of .ddoc files for macro definitions
379 * ddoctext_length = extant of ddoctext_ptr
380 * datetime = charz returned by ctime()
381 * eSink = send error messages to eSink
382 * outbuf = append the Ddoc text to this
385 extern(C
++) void gendocfile(Module m
, const char* ddoctext_ptr
, size_t ddoctext_length
, const char* datetime
, ErrorSink eSink
, ref OutBuffer outbuf
)
387 gendocfile(m
, ddoctext_ptr
[0 .. ddoctext_length
], datetime
, eSink
, outbuf
);
390 /****************************************************
391 * Generate Ddoc text for Module `m` and append it to `outbuf`.
394 * ddoctext = combined text of .ddoc files for macro definitions
395 * datetime = charz returned by ctime()
396 * eSink = send error messages to eSink
397 * outbuf = append the Ddoc text to this
400 void gendocfile(Module m
, const char[] ddoctext
, const char* datetime
, ErrorSink eSink
, ref OutBuffer outbuf
)
402 // Load internal default macros first
403 DocComment
.parseMacros(m
.escapetable
, m
.macrotable
, ddoc_default
[]);
405 // Ddoc files override default macros
406 DocComment
.parseMacros(m
.escapetable
, m
.macrotable
, ddoctext
);
408 Scope
* sc
= Scope
.createGlobal(m
, eSink
); // create root scope
409 DocComment
* dc
= DocComment
.parse(m
, m
.comment
);
410 dc
.pmacrotable
= &m
.macrotable
;
411 dc
.escapetable
= m
.escapetable
;
413 // Generate predefined macros
414 // Set the title to be the name of the module
416 const p
= m
.toPrettyChars().toDString
;
417 m
.macrotable
.define("TITLE", p
);
420 m
.macrotable
.define("DATETIME", datetime
[0 .. 26]);
421 m
.macrotable
.define("YEAR", datetime
[20 .. 20 + 4]);
423 const srcfilename
= m
.srcfile
.toString();
424 m
.macrotable
.define("SRCFILENAME", srcfilename
);
425 const docfilename
= m
.docfile
.toString();
426 m
.macrotable
.define("DOCFILENAME", docfilename
);
429 dc
.copyright
.nooutput
= 1;
430 m
.macrotable
.define("COPYRIGHT", dc
.copyright
.body_
);
434 if (m
.filetype
== FileType
.ddoc
)
436 const ploc
= m
.md ?
&m
.md
.loc
: &m
.loc
;
439 loc
.filename
= srcfilename
.ptr
;
441 size_t commentlen
= m
.comment ?
strlen(cast(char*)m
.comment
) : 0;
443 // https://issues.dlang.org/show_bug.cgi?id=9764
444 // Don't push m in a, to prevent emphasize ddoc file name.
447 commentlen
= dc
.macros
.name
.ptr
- m
.comment
;
448 dc
.macros
.write(loc
, dc
, sc
, &a
, buf
);
450 buf
.write(m
.comment
[0 .. commentlen
]);
451 highlightText(sc
, &a
, loc
, buf
, 0);
457 dc
.writeSections(sc
, &a
, buf
);
458 emitMemberComments(m
, buf
, sc
);
460 //printf("BODY= '%.*s'\n", cast(int)buf.length, buf.data);
461 m
.macrotable
.define("BODY", buf
[]);
464 buf2
.writestring("$(DDOC)");
465 size_t end
= buf2
.length
;
467 // Expand buf in place with macro expansions
468 const success
= m
.macrotable
.expand(buf2
, 0, end
, null, global
.recursionLimit
, &isIdStart
, &isIdTail
);
470 eSink
.error(Loc
.initial
, "DDoc macro expansion limit exceeded; more than %d expansions.", global
.recursionLimit
);
472 /* Remove all the escape sequences from buf,
473 * and make CR-LF the newline.
475 const slice
= buf2
[];
476 outbuf
.reserve(slice
.length
);
478 for (size_t j
= 0; j
< slice
.length
; j
++)
481 if (c
== 0xFF && j
+ 1 < slice
.length
)
487 outbuf
.writeByte('\r');
490 outbuf
.writestring("\r\n");
491 if (j
+ 1 < slice
.length
&& p
[j
+ 1] == '\n')
501 /****************************************************
502 * Having unmatched parentheses can hose the output of Ddoc,
503 * as the macros depend on properly nested parentheses.
504 * This function replaces all ( with $(LPAREN) and ) with $(RPAREN)
505 * to preserve text literally. This also means macros in the
506 * text won't be expanded.
509 void escapeDdocString(ref OutBuffer buf
, size_t start
)
511 for (size_t u
= start
; u
< buf
.length
; u
++)
518 buf
.insert(u
, "$(DOLLAR)");
522 buf
.remove(u
, 1); //remove the (
523 buf
.insert(u
, "$(LPAREN)"); //insert this instead
524 u
+= 8; //skip over newly inserted macro
527 buf
.remove(u
, 1); //remove the )
528 buf
.insert(u
, "$(RPAREN)"); //insert this instead
529 u
+= 8; //skip over newly inserted macro
537 /****************************************************
538 * Having unmatched parentheses can hose the output of Ddoc,
539 * as the macros depend on properly nested parentheses.
541 * Fix by replacing unmatched ( with $(LPAREN) and unmatched ) with $(RPAREN).
544 * loc = source location of start of text. It is a mutable copy to allow incrementing its linenum, for printing the correct line number when an error is encountered in a multiline block of ddoc.
545 * buf = an OutBuffer containing the DDoc
546 * start = the index within buf to start replacing unmatched parentheses
547 * respectBackslashEscapes = if true, always replace parentheses that are
548 * directly preceeded by a backslash with $(LPAREN) or $(RPAREN) instead of
549 * counting them as stray parentheses
551 private void escapeStrayParenthesis(Loc loc
, ref OutBuffer buf
, size_t start
, bool respectBackslashEscapes
, ErrorSink eSink
)
555 bool atLineStart
= true;
556 for (size_t u
= start
; u
< buf
.length
; u
++)
572 eSink
.warning(loc
, "Ddoc: Stray ')'. This may cause incorrect Ddoc output. Use $(RPAREN) instead for unpaired right parentheses.");
573 buf
.remove(u
, 1); //remove the )
574 buf
.insert(u
, "$(RPAREN)"); //insert this instead
575 u
+= 8; //skip over newly inserted macro
586 // For this to work, loc must be set to the beginning of the passed
587 // text which is currently not possible
588 // (loc is set to the Loc of the Dsymbol)
599 // Issue 15465: don't try to escape unbalanced parens inside code
602 for (++u
; u
< buf
.length
&& buf
[u
] == c
; ++u
)
605 if (c
== '`' ||
(atLineStart
&& numdash
>= 3))
615 // replace backslash-escaped parens with their macros
616 if (!inCode
&& respectBackslashEscapes
&& u
+1 < buf
.length
)
618 if (buf
[u
+1] == '(' || buf
[u
+1] == ')')
620 const paren
= buf
[u
+1] == '(' ?
"$(LPAREN)" : "$(RPAREN)";
621 buf
.remove(u
, 2); //remove the \)
622 buf
.insert(u
, paren
); //insert this instead
623 u
+= 8; //skip over newly inserted macro
625 else if (buf
[u
+1] == '\\')
634 if (par_open
) // if any unmatched lparens
637 for (size_t u
= buf
.length
; u
> start
;)
650 eSink
.warning(loc
, "Ddoc: Stray '('. This may cause incorrect Ddoc output. Use $(LPAREN) instead for unpaired left parentheses.");
651 buf
.remove(u
, 1); //remove the (
652 buf
.insert(u
, "$(LPAREN)"); //insert this instead
664 // Basically, this is to skip over things like private{} blocks in a struct or
665 // class definition that don't add any components to the qualified name.
666 Scope
* skipNonQualScopes(Scope
* sc
) @safe
668 while (sc
&& !sc
.scopesym
)
673 bool emitAnchorName(ref OutBuffer buf
, Dsymbol s
, Scope
* sc
, bool includeParent
)
675 if (!s || s
.isPackage() || s
.isModule())
677 // Add parent names first
679 auto eponymousParent
= getEponymousParent(s
);
680 if (includeParent
&& s
.parent || eponymousParent
)
681 dot
= emitAnchorName(buf
, s
.parent
, sc
, includeParent
);
682 else if (includeParent
&& sc
)
683 dot
= emitAnchorName(buf
, sc
.scopesym
, skipNonQualScopes(sc
.enclosing
), includeParent
);
684 // Eponymous template members can share the parent anchor name
689 // Use "this" not "__ctor"
690 TemplateDeclaration td
;
691 if (s
.isCtorDeclaration() ||
((td
= s
.isTemplateDeclaration()) !is null && td
.onemember
&& td
.onemember
.isCtorDeclaration()))
693 buf
.writestring("this");
697 /* We just want the identifier, not overloads like TemplateDeclaration::toChars.
698 * We don't want the template parameter list and constraints. */
699 buf
.writestring(s
.Dsymbol
.toChars());
704 void emitAnchor(ref OutBuffer buf
, Dsymbol s
, Scope
* sc
, bool forHeader
= false)
709 emitAnchorName(anc
, s
, skipNonQualScopes(sc
), true);
710 ident
= Identifier
.idPool(anc
[]);
713 auto pcount
= cast(void*)ident
in sc
.anchorCounts
;
714 typeof(*pcount
) count
;
720 // don't write an anchor for matching consecutive ditto symbols
721 TemplateDeclaration td
= getEponymousParent(s
);
722 if (sc
.prevAnchor
== ident
&& sc
.lastdc
&& (isDitto(s
.comment
) ||
(td
&& isDitto(td
.comment
))))
729 sc
.anchorCounts
[cast(void*)ident
] = 1;
735 sc
.prevAnchor
= ident
;
736 auto macroName
= forHeader ?
"DDOC_HEADER_ANCHOR" : "DDOC_ANCHOR";
738 if (auto imp
= s
.isImport())
740 // For example: `public import core.stdc.string : memcpy, memcmp;`
741 if (imp
.aliases
.length
> 0)
743 for(int i
= 0; i
< imp
.aliases
.length
; i
++)
745 // Need to distinguish between
746 // `public import core.stdc.string : memcpy, memcmp;` and
747 // `public import core.stdc.string : copy = memcpy, compare = memcmp;`
748 auto a
= imp
.aliases
[i
];
749 auto id
= a ? a
: imp
.names
[i
];
752 if (auto symFromId
= sc
.search(loc
, id
, pscopesym
))
754 emitAnchor(buf
, symFromId
, sc
, forHeader
);
760 // For example: `public import str = core.stdc.string;`
763 auto symbolName
= imp
.aliasId
.toString();
765 buf
.printf("$(%.*s %.*s", cast(int) macroName
.length
, macroName
.ptr
,
766 cast(int) symbolName
.length
, symbolName
.ptr
);
770 buf
.printf(", %.*s", cast(int) symbolName
.length
, symbolName
.ptr
);
775 // The general case: `public import core.stdc.string;`
777 // fully qualify imports so `core.stdc.string` doesn't appear as `core`
778 void printFullyQualifiedImport()
780 foreach (const pid
; imp
.packages
)
782 buf
.printf("%s.", pid
.toChars());
784 buf
.writestring(imp
.id
.toString());
787 buf
.printf("$(%.*s ", cast(int) macroName
.length
, macroName
.ptr
);
788 printFullyQualifiedImport();
793 printFullyQualifiedImport();
802 auto symbolName
= ident
.toString();
803 buf
.printf("$(%.*s %.*s", cast(int) macroName
.length
, macroName
.ptr
,
804 cast(int) symbolName
.length
, symbolName
.ptr
);
806 // only append count once there's a duplicate
808 buf
.printf(".%u", count
);
812 Identifier shortIdent
;
815 emitAnchorName(anc
, s
, skipNonQualScopes(sc
), false);
816 shortIdent
= Identifier
.idPool(anc
[]);
819 auto shortName
= shortIdent
.toString();
820 buf
.printf(", %.*s", cast(int) shortName
.length
, shortName
.ptr
);
827 /******************************* emitComment **********************************/
829 /** Get leading indentation from 'src' which represents lines of code. */
830 size_t
getCodeIndent(const(char)* src
)
832 while (src
&& (*src
== '\r' ||
*src
== '\n'))
833 ++src
; // skip until we find the first non-empty line
834 size_t codeIndent
= 0;
835 while (src
&& (*src
== ' ' ||
*src
== '\t'))
843 /** Recursively expand template mixin member docs into the scope. */
844 void expandTemplateMixinComments(TemplateMixin tm
, ref OutBuffer buf
, Scope
* sc
)
847 tm
.dsymbolSemantic(sc
);
848 TemplateDeclaration td
= (tm
&& tm
.tempdecl
) ? tm
.tempdecl
.isTemplateDeclaration() : null;
849 if (td
&& td
.members
)
851 for (size_t i
= 0; i
< td
.members
.length
; i
++)
853 Dsymbol sm
= (*td
.members
)[i
];
854 TemplateMixin tmc
= sm
.isTemplateMixin();
855 if (tmc
&& tmc
.comment
)
856 expandTemplateMixinComments(tmc
, buf
, sc
);
858 emitComment(sm
, buf
, sc
);
863 void emitMemberComments(ScopeDsymbol sds
, ref OutBuffer buf
, Scope
* sc
)
867 //printf("ScopeDsymbol::emitMemberComments() %s\n", toChars());
868 const(char)[] m
= "$(DDOC_MEMBERS ";
869 if (sds
.isTemplateDeclaration())
870 m
= "$(DDOC_TEMPLATE_MEMBERS ";
871 else if (sds
.isClassDeclaration())
872 m
= "$(DDOC_CLASS_MEMBERS ";
873 else if (sds
.isStructDeclaration())
874 m
= "$(DDOC_STRUCT_MEMBERS ";
875 else if (sds
.isEnumDeclaration())
876 m
= "$(DDOC_ENUM_MEMBERS ";
877 else if (sds
.isModule())
878 m
= "$(DDOC_MODULE_MEMBERS ";
879 size_t offset1
= buf
.length
; // save starting offset
881 size_t offset2
= buf
.length
; // to see if we write anything
883 for (size_t i
= 0; i
< sds
.members
.length
; i
++)
885 Dsymbol s
= (*sds
.members
)[i
];
886 //printf("\ts = '%s'\n", s.toChars());
887 // only expand if parent is a non-template (semantic won't work)
888 if (s
.comment
&& s
.isTemplateMixin() && s
.parent
&& !s
.parent
.isTemplateDeclaration())
889 expandTemplateMixinComments(cast(TemplateMixin
)s
, buf
, sc
);
890 emitComment(s
, buf
, sc
);
892 emitComment(null, buf
, sc
);
894 if (buf
.length
== offset2
)
896 /* Didn't write out any members, so back out last write
898 buf
.setsize(offset1
);
901 buf
.writestring(")");
904 void emitVisibility(ref OutBuffer buf
, Import i
)
906 // imports are private by default, which is different from other declarations
907 // so they should explicitly show their visibility
908 emitVisibility(buf
, i
.visibility
);
911 void emitVisibility(ref OutBuffer buf
, Declaration d
)
913 auto vis
= d
.visibility
;
914 if (vis
.kind
!= Visibility
.Kind
.undefined
&& vis
.kind
!= Visibility
.Kind
.public_
)
916 emitVisibility(buf
, vis
);
920 void emitVisibility(ref OutBuffer buf
, Visibility vis
)
922 visibilityToBuffer(buf
, vis
);
926 void emitComment(Dsymbol s
, ref OutBuffer buf
, Scope
* sc
)
928 extern (C
++) final class EmitComment
: Visitor
930 alias visit
= Visitor
.visit
;
935 extern (D
) this(ref OutBuffer buf
, Scope
* sc
) scope
941 override void visit(Dsymbol
)
945 override void visit(InvariantDeclaration
)
949 override void visit(UnitTestDeclaration
)
953 override void visit(PostBlitDeclaration
)
957 override void visit(DtorDeclaration
)
961 override void visit(StaticCtorDeclaration
)
965 override void visit(StaticDtorDeclaration
)
969 override void visit(TypeInfoDeclaration
)
973 void emit(Scope
* sc
, Dsymbol s
, const(char)* com
)
975 if (s
&& sc
.lastdc
&& isDitto(com
))
980 // Put previous doc comment if exists
981 if (DocComment
* dc
= sc
.lastdc
)
983 assert(dc
.a
.length
> 0, "Expects at least one declaration for a" ~
984 "documentation comment");
986 auto symbol
= dc
.a
[0];
988 buf
.writestring("$(DDOC_MEMBER");
989 buf
.writestring("$(DDOC_MEMBER_HEADER");
990 emitAnchor(*buf
, symbol
, sc
, true);
993 // Put the declaration signatures as the document 'title'
994 buf
.writestring(ddoc_decl_s
);
995 for (size_t i
= 0; i
< dc
.a
.length
; i
++)
997 Dsymbol sx
= dc
.a
[i
];
998 // the added linebreaks in here make looking at multiple
999 // signatures more appealing
1002 size_t o
= buf
.length
;
1003 toDocBuffer(sx
, *buf
, sc
);
1004 highlightCode(sc
, sx
, *buf
, o
);
1005 buf
.writestring("$(DDOC_OVERLOAD_SEPARATOR)");
1008 buf
.writestring("$(DDOC_DITTO ");
1010 size_t o
= buf
.length
;
1011 toDocBuffer(sx
, *buf
, sc
);
1012 highlightCode(sc
, sx
, *buf
, o
);
1014 buf
.writestring("$(DDOC_OVERLOAD_SEPARATOR)");
1017 buf
.writestring(ddoc_decl_e
);
1018 // Put the ddoc comment as the document 'description'
1019 buf
.writestring(ddoc_decl_dd_s
);
1021 dc
.writeSections(sc
, &dc
.a
, *buf
);
1022 if (ScopeDsymbol sds
= dc
.a
[0].isScopeDsymbol())
1023 emitMemberComments(sds
, *buf
, sc
);
1025 buf
.writestring(ddoc_decl_dd_e
);
1027 //printf("buf.2 = [[%.*s]]\n", cast(int)(buf.length - o0), buf.data + o0);
1031 DocComment
* dc
= DocComment
.parse(s
, com
);
1032 dc
.pmacrotable
= &sc
._module
.macrotable
;
1037 override void visit(Import imp
)
1039 if (imp
.visible().kind
!= Visibility
.Kind
.public_
&& sc
.visibility
.kind
!= Visibility
.Kind
.export_
)
1043 emit(sc
, imp
, imp
.comment
);
1046 override void visit(Declaration d
)
1048 //printf("Declaration::emitComment(%p '%s'), comment = '%s'\n", d, d.toChars(), d.comment);
1049 //printf("type = %p\n", d.type);
1050 const(char)* com
= d
.comment
;
1051 if (TemplateDeclaration td
= getEponymousParent(d
))
1053 if (isDitto(td
.comment
))
1056 com
= Lexer
.combineComments(td
.comment
.toDString(), com
.toDString(), true);
1064 if (!d
.isCtorDeclaration() &&
1065 !d
.isAliasDeclaration() &&
1066 !d
.isVarDeclaration())
1071 if (d
.visibility
.kind
== Visibility
.Kind
.private_ || sc
.visibility
.kind
== Visibility
.Kind
.private_
)
1079 override void visit(AggregateDeclaration ad
)
1081 //printf("AggregateDeclaration::emitComment() '%s'\n", ad.toChars());
1082 const(char)* com
= ad
.comment
;
1083 if (TemplateDeclaration td
= getEponymousParent(ad
))
1085 if (isDitto(td
.comment
))
1088 com
= Lexer
.combineComments(td
.comment
.toDString(), com
.toDString(), true);
1092 if (ad
.visible().kind
== Visibility
.Kind
.private_ || sc
.visibility
.kind
== Visibility
.Kind
.private_
)
1102 override void visit(TemplateDeclaration td
)
1104 //printf("TemplateDeclaration::emitComment() '%s', kind = %s\n", td.toChars(), td.kind());
1105 if (td
.visible().kind
== Visibility
.Kind
.private_ || sc
.visibility
.kind
== Visibility
.Kind
.private_
)
1109 if (Dsymbol ss
= getEponymousMember(td
))
1114 emit(sc
, td
, td
.comment
);
1117 override void visit(EnumDeclaration ed
)
1119 if (ed
.visible().kind
== Visibility
.Kind
.private_ || sc
.visibility
.kind
== Visibility
.Kind
.private_
)
1121 if (ed
.isAnonymous() && ed
.members
)
1123 for (size_t i
= 0; i
< ed
.members
.length
; i
++)
1125 Dsymbol s
= (*ed
.members
)[i
];
1126 emitComment(s
, *buf
, sc
);
1132 if (ed
.isAnonymous())
1134 emit(sc
, ed
, ed
.comment
);
1137 override void visit(EnumMember em
)
1139 //printf("EnumMember::emitComment(%p '%s'), comment = '%s'\n", em, em.toChars(), em.comment);
1140 if (em
.visible().kind
== Visibility
.Kind
.private_ || sc
.visibility
.kind
== Visibility
.Kind
.private_
)
1144 emit(sc
, em
, em
.comment
);
1147 override void visit(AttribDeclaration ad
)
1149 //printf("AttribDeclaration::emitComment(sc = %p)\n", sc);
1150 /* A general problem with this,
1151 * illustrated by https://issues.dlang.org/show_bug.cgi?id=2516
1152 * is that attributes are not transmitted through to the underlying
1153 * member declarations for template bodies, because semantic analysis
1154 * is not done for template declaration bodies
1155 * (only template instantiations).
1156 * Hence, Ddoc omits attributes from template members.
1158 Dsymbols
* d
= ad
.include(null);
1161 for (size_t i
= 0; i
< d
.length
; i
++)
1163 Dsymbol s
= (*d
)[i
];
1164 //printf("AttribDeclaration::emitComment %s\n", s.toChars());
1165 emitComment(s
, *buf
, sc
);
1170 override void visit(VisibilityDeclaration pd
)
1176 sc
.visibility
= pd
.visibility
;
1177 visit(cast(AttribDeclaration
)pd
);
1178 scx
.lastdc
= sc
.lastdc
;
1183 override void visit(ConditionalDeclaration cd
)
1185 //printf("ConditionalDeclaration::emitComment(sc = %p)\n", sc);
1186 if (cd
.condition
.inc != Include
.notComputed
)
1188 visit(cast(AttribDeclaration
)cd
);
1191 /* If generating doc comment, be careful because if we're inside
1192 * a template, then include(null) will fail.
1194 Dsymbols
* d
= cd
.decl ? cd
.decl
: cd
.elsedecl
;
1195 for (size_t i
= 0; i
< d
.length
; i
++)
1197 Dsymbol s
= (*d
)[i
];
1198 emitComment(s
, *buf
, sc
);
1203 scope EmitComment v
= new EmitComment(buf
, sc
);
1205 v
.emit(sc
, null, null);
1210 void toDocBuffer(Dsymbol s
, ref OutBuffer buf
, Scope
* sc
)
1212 extern (C
++) final class ToDocBuffer
: Visitor
1214 alias visit
= Visitor
.visit
;
1219 extern (D
) this(ref OutBuffer buf
, Scope
* sc
) scope
1225 override void visit(Dsymbol s
)
1227 //printf("Dsymbol::toDocbuffer() %s\n", s.toChars());
1230 toCBuffer(s
, *buf
, hgs
);
1233 void prefix(Dsymbol s
)
1235 if (s
.isDeprecated())
1236 buf
.writestring("deprecated ");
1237 if (Declaration d
= s
.isDeclaration())
1239 emitVisibility(*buf
, d
);
1241 buf
.writestring("static ");
1242 else if (d
.isFinal())
1243 buf
.writestring("final ");
1244 else if (d
.isAbstract())
1245 buf
.writestring("abstract ");
1247 if (d
.isFuncDeclaration()) // functionToBufferFull handles this
1250 if (d
.isImmutable())
1251 buf
.writestring("immutable ");
1252 if (d
.storage_class
& STC
.shared_
)
1253 buf
.writestring("shared ");
1255 buf
.writestring("inout ");
1257 buf
.writestring("const ");
1259 if (d
.isSynchronized())
1260 buf
.writestring("synchronized ");
1262 if (d
.storage_class
& STC
.manifest
)
1263 buf
.writestring("enum ");
1265 // Add "auto" for the untyped variable in template members
1266 if (!d
.type
&& d
.isVarDeclaration() &&
1267 !d
.isImmutable() && !(d
.storage_class
& STC
.shared_
) && !d
.isWild() && !d
.isConst() &&
1268 !d
.isSynchronized())
1270 buf
.writestring("auto ");
1275 override void visit(Import i
)
1279 emitVisibility(*buf
, i
);
1280 toCBuffer(i
, *buf
, hgs
);
1283 override void visit(Declaration d
)
1287 TemplateDeclaration td
= getEponymousParent(d
);
1288 //printf("Declaration::toDocbuffer() %s, originalType = %s, td = %s\n", d.toChars(), d.originalType ? d.originalType.toChars() : "--", td ? td.toChars() : "--");
1291 if (d
.isDeprecated())
1292 buf
.writestring("$(DEPRECATED ");
1296 Type origType
= d
.originalType ? d
.originalType
: d
.type
;
1297 if (origType
.ty
== Tfunction
)
1299 functionToBufferFull(cast(TypeFunction
)origType
, *buf
, d
.ident
, hgs
, td
);
1302 toCBuffer(origType
, *buf
, d
.ident
, hgs
);
1305 buf
.writestring(d
.ident
.toString());
1306 if (d
.isVarDeclaration() && td
)
1309 if (td
.origParameters
&& td
.origParameters
.length
)
1311 for (size_t i
= 0; i
< td
.origParameters
.length
; i
++)
1314 buf
.writestring(", ");
1315 toCBuffer((*td
.origParameters
)[i
], *buf
, hgs
);
1320 // emit constraints if declaration is a templated declaration
1321 if (td
&& td
.constraint
)
1323 bool noFuncDecl
= td
.isFuncDeclaration() is null;
1326 buf
.writestring("$(DDOC_CONSTRAINT ");
1329 toCBuffer(td
.constraint
, *buf
, hgs
);
1333 buf
.writestring(")");
1336 if (d
.isDeprecated())
1337 buf
.writestring(")");
1338 buf
.writestring(";\n");
1341 override void visit(AliasDeclaration ad
)
1343 //printf("AliasDeclaration::toDocbuffer() %s\n", ad.toChars());
1346 if (ad
.isDeprecated())
1347 buf
.writestring("deprecated ");
1348 emitVisibility(*buf
, ad
);
1349 buf
.printf("alias %s = ", ad
.toChars());
1350 if (Dsymbol s
= ad
.aliassym
) // ident alias
1352 prettyPrintDsymbol(s
, ad
.parent
);
1354 else if (Type type
= ad
.getType()) // type alias
1356 if (type
.ty
== Tclass || type
.ty
== Tstruct || type
.ty
== Tenum
)
1358 import dmd
.typesem
: toDsymbol
;
1359 if (Dsymbol s
= type
.toDsymbol(null)) // elaborate type
1360 prettyPrintDsymbol(s
, ad
.parent
);
1362 buf
.writestring(type
.toChars());
1367 buf
.writestring(type
.toChars());
1370 buf
.writestring(";\n");
1373 void parentToBuffer(Dsymbol s
)
1375 if (s
&& !s
.isPackage() && !s
.isModule())
1377 parentToBuffer(s
.parent
);
1378 buf
.writestring(s
.toChars());
1379 buf
.writestring(".");
1383 static bool inSameModule(Dsymbol s
, Dsymbol p
) @safe
1385 for (; s
; s
= s
.parent
)
1390 for (; p
; p
= p
.parent
)
1398 void prettyPrintDsymbol(Dsymbol s
, Dsymbol parent
)
1400 if (s
.parent
&& (s
.parent
== parent
)) // in current scope -> naked name
1402 buf
.writestring(s
.toChars());
1404 else if (!inSameModule(s
, parent
)) // in another module -> full name
1406 buf
.writestring(s
.toPrettyChars());
1408 else // nested in a type in this module -> full name w/o module name
1410 // if alias is nested in a user-type use module-scope lookup
1411 if (!parent
.isModule() && !parent
.isPackage())
1412 buf
.writestring(".");
1413 parentToBuffer(s
.parent
);
1414 buf
.writestring(s
.toChars());
1418 override void visit(AggregateDeclaration ad
)
1424 emitVisibility(buf
, ad
);
1426 buf
.printf("%s %s", ad
.kind(), ad
.toChars());
1427 buf
.writestring(";\n");
1430 override void visit(StructDeclaration sd
)
1432 //printf("StructDeclaration::toDocbuffer() %s\n", sd.toChars());
1437 emitVisibility(buf
, sd
);
1439 if (TemplateDeclaration td
= getEponymousParent(sd
))
1441 toDocBuffer(td
, *buf
, sc
);
1445 buf
.printf("%s %s", sd
.kind(), sd
.toChars());
1447 buf
.writestring(";\n");
1450 override void visit(ClassDeclaration cd
)
1452 //printf("ClassDeclaration::toDocbuffer() %s\n", cd.toChars());
1457 emitVisibility(*buf
, cd
);
1459 if (TemplateDeclaration td
= getEponymousParent(cd
))
1461 toDocBuffer(td
, *buf
, sc
);
1465 if (!cd
.isInterfaceDeclaration() && cd
.isAbstract())
1466 buf
.writestring("abstract ");
1467 buf
.printf("%s %s", cd
.kind(), cd
.toChars());
1470 for (size_t i
= 0; i
< cd
.baseclasses
.length
; i
++)
1472 BaseClass
* bc
= (*cd
.baseclasses
)[i
];
1473 if (bc
.sym
&& bc
.sym
.ident
== Id
.Object
)
1476 buf
.writestring(", ");
1479 buf
.writestring(": ");
1485 buf
.printf("$(DDOC_PSUPER_SYMBOL %s)", bc
.sym
.toPrettyChars());
1490 toCBuffer(bc
.type
, *buf
, null, hgs
);
1493 buf
.writestring(";\n");
1496 override void visit(EnumDeclaration ed
)
1500 buf
.printf("%s %s", ed
.kind(), ed
.toChars());
1503 buf
.writestring(": $(DDOC_ENUM_BASETYPE ");
1505 toCBuffer(ed
.memtype
, *buf
, null, hgs
);
1506 buf
.writestring(")");
1508 buf
.writestring(";\n");
1511 override void visit(EnumMember em
)
1515 buf
.writestring(em
.toChars());
1519 scope ToDocBuffer v
= new ToDocBuffer(buf
, sc
);
1523 /***********************************************************
1528 Sections sections
; // Section*[]
1532 MacroTable
* pmacrotable
;
1533 Escape
* escapetable
;
1536 static DocComment
* parse(Dsymbol s
, const(char)* comment
)
1538 //printf("parse(%s): '%s'\n", s.toChars(), comment);
1539 auto dc
= new DocComment();
1543 dc
.parseSections(comment
);
1544 for (size_t i
= 0; i
< dc
.sections
.length
; i
++)
1546 Section sec
= dc
.sections
[i
];
1547 if (iequals("copyright", sec
.name
))
1551 if (iequals("macros", sec
.name
))
1559 /************************************************
1560 * Parse macros out of Macros: section.
1561 * Macros are of the form:
1566 extern(D
) static void parseMacros(
1567 Escape
* escapetable
, ref MacroTable pmacrotable
, const(char)[] m
)
1569 const(char)* p
= m
.ptr
;
1570 size_t len
= m
.length
;
1571 const(char)* pend
= p
+ len
;
1572 const(char)* tempstart
= null;
1574 const(char)* namestart
= null;
1575 size_t namelen
= 0; // !=0 if line continuation
1576 const(char)* textstart
= null;
1580 // Skip to start of macro
1599 goto Ltext
; // continuation of prev macro
1613 templen
= p
- tempstart
;
1618 if (!(*p
== ' ' ||
*p
== '\t'))
1625 goto Ltext
; // continuation of prev macro
1633 // Output existing macro
1635 //printf("macro '%.*s' = '%.*s'\n", cast(int)namelen, namestart, cast(int)textlen, textstart);
1636 if (iequals("ESCAPES", namestart
[0 .. namelen
]))
1637 parseEscapes(escapetable
, textstart
[0 .. textlen
]);
1639 pmacrotable
.define(namestart
[0 .. namelen
], textstart
[0 .. textlen
]);
1644 namestart
= tempstart
;
1646 while (p
< pend
&& (*p
== ' ' ||
*p
== '\t'))
1650 while (p
< pend
&& *p
!= '\r' && *p
!= '\n')
1652 textlen
= p
- textstart
;
1654 //printf("p = %p, pend = %p\n", p, pend);
1659 while (p
< pend
&& *p
!= '\r' && *p
!= '\n')
1664 goto L1
; // write out last one
1667 /**************************************
1668 * Parse escapes of the form:
1670 * where c is a single character.
1671 * Multiple escapes can be separated
1672 * by whitespace and/or commas.
1674 static void parseEscapes(Escape
* escapetable
, const(char)[] text
)
1678 escapetable
= new Escape();
1679 memset(escapetable
, 0, Escape
.sizeof
);
1681 //printf("parseEscapes('%.*s') pescapetable = %p\n", cast(int)text.length, text.ptr, escapetable);
1682 const(char)* p
= text
.ptr
;
1683 const(char)* pend
= p
+ text
.length
;
1690 if (!(*p
== ' ' ||
*p
== '\t' ||
*p
== '\r' ||
*p
== '\n' ||
*p
== ','))
1694 if (p
[0] != '/' || p
[2] != '/')
1698 const(char)* start
= p
;
1707 size_t len
= p
- start
;
1708 char* s
= cast(char*)memcpy(mem
.xmalloc(len
+ 1), start
, len
);
1710 escapetable
.strings
[c
] = s
[0 .. len
];
1711 //printf("\t%c = '%s'\n", c, s);
1716 /*****************************************
1717 * Parse next paragraph out of *pcomment.
1718 * Update *pcomment to point past paragraph.
1719 * Returns NULL if no more paragraphs.
1720 * If paragraph ends in 'identifier:',
1721 * then (*pcomment)[0 .. idlen] is the identifier.
1723 void parseSections(const(char)* comment
)
1726 const(char)* pstart
;
1728 const(char)* idstart
= null; // dead-store to prevent spurious warning
1730 const(char)* name
= null;
1732 //printf("parseSections('%s')\n", comment);
1736 const(char)* pstart0
= p
;
1737 p
= skipwhitespace(p
);
1741 // Undo indent if starting with a list item
1742 if ((*p
== '-' ||
*p
== '+' ||
*p
== '*') && (*(p
+1) == ' ' ||
*(p
+1) == '\t'))
1746 const(char)* pitem
= p
;
1747 while (*pitem
>= '0' && *pitem
<= '9')
1749 if (pitem
> p
&& *pitem
== '.' && (*(pitem
+1) == ' ' ||
*(pitem
+1) == '\t'))
1753 /* Find end of section, which is ended by one of:
1754 * 'identifier:' (but not inside a code section)
1761 // Check for start/end of a code section
1762 if (*p
== '-' ||
*p
== '`' ||
*p
== '~')
1771 // BUG: handle UTF PS and LS too
1772 if ((!*p ||
*p
== '\r' ||
*p
== '\n' ||
(!inCode
&& c
!= '-')) && numdash
>= 3)
1774 inCode
= inCode
== c ?
false : c
;
1777 // restore leading indentation
1778 while (pstart0
< pstart
&& isIndentWS(pstart
- 1))
1784 if (!inCode
&& isIdStart(p
))
1786 const(char)* q
= p
+ utfStride(p
);
1790 // Detected tag ends it
1791 if (*q
== ':' && isupper(*p
)
1792 && (isspace(q
[1]) || q
[1] == 0))
1796 for (pend
= p
; pend
> pstart
; pend
--)
1798 if (pend
[-1] == '\n')
1812 if (*p
== '\n' && !summary
&& !namelen
&& !inCode
)
1823 p
= skipwhitespace(p
);
1826 if (namelen || pstart
< pend
)
1829 if (iequals("Params", name
[0 .. namelen
]))
1830 s
= new ParamSection();
1831 else if (iequals("Macros", name
[0 .. namelen
]))
1832 s
= new MacroSection();
1835 s
.name
= name
[0 .. namelen
];
1836 s
.body_
= pstart
[0 .. pend
- pstart
];
1838 //printf("Section: '%.*s' = '%.*s'\n", cast(int)s.namelen, s.name, cast(int)s.bodylen, s.body);
1840 if (!summary
&& !namelen
)
1858 void writeSections(Scope
* sc
, Dsymbols
* a
, ref OutBuffer buf
)
1861 //printf("DocComment::writeSections()\n");
1862 Loc loc
= (*a
)[0].loc
;
1863 if (Module m
= (*a
)[0].isModule())
1868 size_t offset1
= buf
.length
;
1869 buf
.writestring("$(DDOC_SECTIONS ");
1870 size_t offset2
= buf
.length
;
1871 for (size_t i
= 0; i
< sections
.length
; i
++)
1873 Section sec
= sections
[i
];
1876 //printf("Section: '%.*s' = '%.*s'\n", cast(int)sec.namelen, sec.name, cast(int)sec.bodylen, sec.body);
1877 if (!sec
.name
.length
&& i
== 0)
1879 buf
.writestring("$(DDOC_SUMMARY ");
1880 size_t o
= buf
.length
;
1881 buf
.write(sec
.body_
);
1882 escapeStrayParenthesis(loc
, buf
, o
, true, sc
.eSink
);
1883 highlightText(sc
, a
, loc
, buf
, o
);
1884 buf
.writestring(")");
1887 sec
.write(loc
, &this, sc
, a
, buf
);
1889 for (size_t i
= 0; i
< a
.length
; i
++)
1891 Dsymbol s
= (*a
)[i
];
1892 if (Dsymbol td
= getEponymousParent(s
))
1894 for (UnitTestDeclaration utd
= s
.ddocUnittest
; utd
; utd
= utd
.ddocUnittest
)
1896 if (utd
.visibility
.kind
== Visibility
.Kind
.private_ ||
!utd
.comment ||
!utd
.fbody
)
1898 // Strip whitespaces to avoid showing empty summary
1899 const(char)* c
= utd
.comment
;
1900 while (*c
== ' ' ||
*c
== '\t' ||
*c
== '\n' ||
*c
== '\r')
1902 buf
.writestring("$(DDOC_EXAMPLES ");
1903 size_t o
= buf
.length
;
1904 buf
.writestring(cast(char*)c
);
1907 auto codedoc
= utd
.codedoc
.stripLeadingNewlines
;
1908 size_t n
= getCodeIndent(codedoc
);
1911 buf
.writestring("----\n");
1912 buf
.writestring(codedoc
);
1913 buf
.writestring("----\n");
1914 highlightText(sc
, a
, loc
, buf
, o
);
1916 buf
.writestring(")");
1919 if (buf
.length
== offset2
)
1921 /* Didn't write out any sections, so back out last write
1923 buf
.setsize(offset1
);
1924 buf
.writestring("\n");
1927 buf
.writestring(")");
1931 /*****************************************
1932 * Return true if comment consists entirely of "ditto".
1934 bool isDitto(const(char)* comment
)
1938 const(char)* p
= skipwhitespace(comment
);
1939 if (Port
.memicmp(p
, "ditto", 5) == 0 && *skipwhitespace(p
+ 5) == 0)
1945 /**********************************************
1948 const(char)* skipwhitespace(const(char)* p
)
1950 return skipwhitespace(p
.toDString
).ptr
;
1954 const(char)[] skipwhitespace(const(char)[] p
) @safe
1956 foreach (idx
, char c
; p
)
1971 /************************************************
1972 * Scan past all instances of the given characters.
1974 * buf = an OutBuffer containing the DDoc
1975 * i = the index within `buf` to start scanning from
1976 * chars = the characters to skip; order is unimportant
1977 * Returns: the index after skipping characters.
1979 size_t
skipChars(ref OutBuffer buf
, size_t i
, string chars
) @safe
1982 foreach (j
, c
; buf
[][i
..$])
1996 string data
= "test ---\r\n\r\nend";
1999 assert(skipChars(buf
, 0, "-") == 0);
2000 assert(skipChars(buf
, 4, "-") == 4);
2001 assert(skipChars(buf
, 4, " -") == 8);
2002 assert(skipChars(buf
, 8, "\r\n") == 12);
2003 assert(skipChars(buf
, 12, "dne") == 15);
2006 /****************************************************
2007 * Replace all instances of `c` with `r` in the given string
2009 * s = the string to do replacements in
2010 * c = the character to look for
2011 * r = the string to replace `c` with
2012 * Returns: `s` with `c` replaced with `r`
2014 inout(char)[] replaceChar(inout(char)[] s
, char c
, string r
) pure @safe
2017 foreach (char sc
; s
)
2024 result
.reserve(s
.length
- count
+ (r
.length
* count
));
2026 foreach (i
, char sc
; s
)
2030 result
~= s
[start
..i
];
2035 result
~= s
[start
..$];
2042 assert("".replaceChar(',', "$(COMMA)") == "");
2043 assert("ab".replaceChar(',', "$(COMMA)") == "ab");
2044 assert("a,b".replaceChar(',', "$(COMMA)") == "a$(COMMA)b");
2045 assert("a,,b".replaceChar(',', "$(COMMA)") == "a$(COMMA)$(COMMA)b");
2046 assert(",ab".replaceChar(',', "$(COMMA)") == "$(COMMA)ab");
2047 assert("ab,".replaceChar(',', "$(COMMA)") == "ab$(COMMA)");
2051 * Return a lowercased copy of a string.
2053 * s = the string to lowercase
2054 * Returns: the lowercase version of the string or the original if already lowercase
2056 string
toLowercase(string s
) pure @safe
2059 foreach (size_t i
; 0..s
.length
)
2062 // TODO: maybe unicode lowercase, somehow
2063 if (c
>= 'A' && c
<= 'Z')
2065 if (!lower
.length
) {
2066 lower
.reserve(s
.length
);
2068 lower
~= s
[lower
.length
..i
];
2074 lower
~= s
[lower
.length
..$];
2083 assert("".toLowercase
== "");
2084 assert("abc".toLowercase
== "abc");
2085 assert("ABC".toLowercase
== "abc");
2086 assert("aBc".toLowercase
== "abc");
2089 /************************************************
2090 * Get the indent from one index to another, counting tab stops as four spaces wide
2091 * per the Markdown spec.
2093 * buf = an OutBuffer containing the DDoc
2094 * from = the index within `buf` to start counting from, inclusive
2095 * to = the index within `buf` to stop counting at, exclusive
2096 * Returns: the indent
2098 int getMarkdownIndent(ref OutBuffer buf
, size_t from
, size_t to
) @safe
2100 const slice
= buf
[];
2101 if (to
> slice
.length
)
2104 foreach (const c
; slice
[from
..to
])
2105 indent
+= (c
== '\t') ?
4 - (indent
% 4) : 1;
2109 /************************************************
2110 * Scan forward to one of:
2111 * start of identifier
2112 * beginning of next line
2115 size_t
skiptoident(ref OutBuffer buf
, size_t i
) @safe
2117 const slice
= buf
[];
2118 while (i
< slice
.length
)
2122 if (utf_decodeChar(slice
, i
, c
))
2124 /* Ignore UTF errors, but still consume input
2133 else if (!(isalpha(c
) || c
== '_' || c
== '\n'))
2141 /************************************************
2142 * Scan forward past end of identifier.
2144 size_t
skippastident(ref OutBuffer buf
, size_t i
) @safe
2146 const slice
= buf
[];
2147 while (i
< slice
.length
)
2151 if (utf_decodeChar(slice
, i
, c
))
2153 /* Ignore UTF errors, but still consume input
2162 else if (isalnum(c
) || c
== '_')
2170 /************************************************
2171 * Scan forward past end of an identifier that might
2172 * contain dots (e.g. `abc.def`)
2174 size_t
skipPastIdentWithDots(ref OutBuffer buf
, size_t i
) @safe
2176 const slice
= buf
[];
2177 bool lastCharWasDot
;
2178 while (i
< slice
.length
)
2182 if (utf_decodeChar(slice
, i
, c
))
2184 /* Ignore UTF errors, but still consume input
2190 // We need to distinguish between `abc.def`, abc..def`, and `abc.`
2191 // Only `abc.def` is a valid identifier
2199 lastCharWasDot
= true;
2208 lastCharWasDot
= false;
2212 else if (isalnum(c
) || c
== '_')
2214 lastCharWasDot
= false;
2229 /************************************************
2230 * Scan forward past URL starting at i.
2231 * We don't want to highlight parts of a URL.
2234 * index just past it if it is a URL
2236 size_t
skippastURL(ref OutBuffer buf
, size_t i
)
2238 const slice
= buf
[][i
.. $];
2240 bool sawdot
= false;
2241 if (slice
.length
> 7 && Port
.memicmp(slice
.ptr
, "http://", 7) == 0)
2245 else if (slice
.length
> 8 && Port
.memicmp(slice
.ptr
, "https://", 8) == 0)
2251 for (; j
< slice
.length
; j
++)
2256 if (c
== '-' || c
== '_' || c
== '?' || c
== '=' || c
== '%' ||
2257 c
== '&' || c
== '/' || c
== '+' || c
== '#' || c
== '~')
2272 /****************************************************
2273 * Remove a previously-inserted blank line macro.
2275 * buf = an OutBuffer containing the DDoc
2276 * iAt = the index within `buf` of the start of the `$(DDOC_BLANKLINE)`
2277 * macro. Upon function return its value is set to `0`.
2278 * i = an index within `buf`. If `i` is after `iAt` then it gets
2279 * reduced by the length of the removed macro.
2281 void removeBlankLineMacro(ref OutBuffer buf
, ref size_t iAt
, ref size_t i
)
2286 enum macroLength
= "$(DDOC_BLANKLINE)".length
;
2287 buf
.remove(iAt
, macroLength
);
2293 /****************************************************
2294 * Attempt to detect and replace a Markdown thematic break (HR). These are three
2295 * or more of the same delimiter, optionally with spaces or tabs between any of
2296 * them, e.g. `\n- - -\n` becomes `\n$(HR)\n`
2298 * buf = an OutBuffer containing the DDoc
2299 * i = the index within `buf` of the first character of a potential
2300 * thematic break. If the replacement is made `i` changes to
2301 * point to the closing parenthesis of the `$(HR)` macro.
2302 * iLineStart = the index within `buf` that the thematic break's line starts at
2303 * loc = the current location within the file
2304 * Returns: whether a thematic break was replaced
2306 bool replaceMarkdownThematicBreak(ref OutBuffer buf
, ref size_t i
, size_t iLineStart
, const ref Loc loc
)
2309 const slice
= buf
[];
2313 for (; j
< slice
.length
; j
++)
2317 else if (buf
[j
] != ' ' && buf
[j
] != '\t')
2322 if (j
>= buf
.length || buf
[j
] == '\n' || buf
[j
] == '\r')
2324 buf
.remove(iLineStart
, j
- iLineStart
);
2325 i
= buf
.insert(iLineStart
, "$(HR)") - 1;
2332 /****************************************************
2333 * Detect the level of an ATX-style heading, e.g. `## This is a heading` would
2334 * have a level of `2`.
2336 * buf = an OutBuffer containing the DDoc
2337 * i = the index within `buf` of the first `#` character
2339 * the detected heading level from 1 to 6, or
2340 * 0 if not at an ATX heading
2342 int detectAtxHeadingLevel(ref OutBuffer buf
, const size_t i
) @safe
2344 const iHeadingStart
= i
;
2345 const iAfterHashes
= skipChars(buf
, i
, "#");
2346 const headingLevel
= cast(int) (iAfterHashes
- iHeadingStart
);
2347 if (headingLevel
> 6)
2350 const iTextStart
= skipChars(buf
, iAfterHashes
, " \t");
2351 const emptyHeading
= buf
[iTextStart
] == '\r' || buf
[iTextStart
] == '\n';
2353 // require whitespace
2354 if (!emptyHeading
&& iTextStart
== iAfterHashes
)
2357 return headingLevel
;
2360 /****************************************************
2361 * Remove any trailing `##` suffix from an ATX-style heading.
2363 * buf = an OutBuffer containing the DDoc
2364 * i = the index within `buf` to start looking for a suffix at
2366 void removeAnyAtxHeadingSuffix(ref OutBuffer buf
, size_t i
)
2369 size_t iSuffixStart
= 0;
2370 size_t iWhitespaceStart
= j
;
2371 const slice
= buf
[];
2372 for (; j
< slice
.length
; j
++)
2377 if (iWhitespaceStart
&& !iSuffixStart
)
2382 if (!iWhitespaceStart
)
2383 iWhitespaceStart
= j
;
2390 iWhitespaceStart
= 0;
2396 buf
.remove(iWhitespaceStart
, j
- iWhitespaceStart
);
2399 /****************************************************
2400 * Wrap text in a Markdown heading macro, e.g. `$(H2 heading text`).
2402 * buf = an OutBuffer containing the DDoc
2403 * iStart = the index within `buf` that the Markdown heading starts at
2404 * iEnd = the index within `buf` of the character after the last
2405 * heading character. Is incremented by the length of the
2406 * inserted heading macro when this function ends.
2407 * loc = the location of the Ddoc within the file
2408 * headingLevel = the level (1-6) of heading to end. Is set to `0` when this
2411 void endMarkdownHeading(ref OutBuffer buf
, size_t iStart
, ref size_t iEnd
, const ref Loc loc
, ref int headingLevel
)
2413 char[5] heading
= "$(H0 ";
2414 heading
[3] = cast(char) ('0' + headingLevel
);
2415 buf
.insert(iStart
, heading
);
2417 size_t iBeforeNewline
= iEnd
;
2418 while (buf
[iBeforeNewline
-1] == '\r' || buf
[iBeforeNewline
-1] == '\n')
2420 buf
.insert(iBeforeNewline
, ")");
2424 /****************************************************
2425 * End all nested Markdown quotes, if inside any.
2427 * buf = an OutBuffer containing the DDoc
2428 * i = the index within `buf` of the character after the quote text.
2429 * quoteLevel = the current quote level. Is set to `0` when this function ends.
2430 * Returns: the amount that `i` was moved
2432 size_t
endAllMarkdownQuotes(ref OutBuffer buf
, size_t i
, ref int quoteLevel
)
2434 const length
= quoteLevel
;
2435 for (; quoteLevel
> 0; --quoteLevel
)
2436 i
= buf
.insert(i
, ")");
2440 /****************************************************
2441 * Convenience function to end all Markdown lists and quotes, if inside any, and
2442 * set `quoteMacroLevel` to `0`.
2444 * buf = an OutBuffer containing the DDoc
2445 * i = the index within `buf` of the character after the list and/or
2446 * quote text. Is adjusted when this function ends if any lists
2447 * and/or quotes were ended.
2448 * nestedLists = a set of nested lists. Upon return it will be empty.
2449 * quoteLevel = the current quote level. Is set to `0` when this function ends.
2450 * quoteMacroLevel = the macro level that the quote was started at. Is set to
2451 * `0` when this function ends.
2452 * Returns: the amount that `i` was moved
2454 size_t
endAllListsAndQuotes(ref OutBuffer buf
, ref size_t i
, ref MarkdownList
[] nestedLists
, ref int quoteLevel
, out int quoteMacroLevel
)
2456 quoteMacroLevel
= 0;
2458 i
+= MarkdownList
.endAllNestedLists(buf
, i
, nestedLists
);
2459 i
+= endAllMarkdownQuotes(buf
, i
, quoteLevel
);
2463 /****************************************************
2464 * Replace Markdown emphasis with the appropriate macro,
2465 * e.g. `*very* **nice**` becomes `$(EM very) $(STRONG nice)`.
2467 * buf = an OutBuffer containing the DDoc
2468 * loc = the current location within the file
2469 * inlineDelimiters = the collection of delimiters found within a paragraph. When this function returns its length will be reduced to `downToLevel`.
2470 * downToLevel = the length within `inlineDelimiters`` to reduce emphasis to
2471 * Returns: the number of characters added to the buffer by the replacements
2473 size_t
replaceMarkdownEmphasis(ref OutBuffer buf
, const ref Loc loc
, ref MarkdownDelimiter
[] inlineDelimiters
, int downToLevel
= 0)
2475 size_t
replaceEmphasisPair(ref MarkdownDelimiter start
, ref MarkdownDelimiter end
)
2477 immutable count
= start
.count
== 1 || end
.count
== 1 ?
1 : 2;
2479 size_t iStart
= start
.iStart
;
2480 size_t iEnd
= end
.iStart
;
2482 start
.count
-= count
;
2483 iStart
+= start
.count
;
2490 buf
.remove(iStart
, count
);
2492 buf
.remove(iEnd
, count
);
2494 string macroName
= count
>= 2 ?
"$(STRONG " : "$(EM ";
2495 buf
.insert(iEnd
, ")");
2496 buf
.insert(iStart
, macroName
);
2498 const delta
= 1 + macroName
.length
- (count
+ count
);
2499 end
.iStart
+= count
;
2504 int start
= (cast(int) inlineDelimiters
.length
) - 1;
2505 while (start
>= downToLevel
)
2507 // find start emphasis
2508 while (start
>= downToLevel
&&
2509 (inlineDelimiters
[start
].type
!= '*' ||
!inlineDelimiters
[start
].leftFlanking
))
2511 if (start
< downToLevel
)
2514 // find the nearest end emphasis
2515 int end
= start
+ 1;
2516 while (end
< inlineDelimiters
.length
&&
2517 (inlineDelimiters
[end
].type
!= inlineDelimiters
[start
].type ||
2518 inlineDelimiters
[end
].macroLevel
!= inlineDelimiters
[start
].macroLevel ||
2519 !inlineDelimiters
[end
].rightFlanking
))
2521 if (end
== inlineDelimiters
.length
)
2523 // the start emphasis has no matching end; if it isn't an end itself then kill it
2524 if (!inlineDelimiters
[start
].rightFlanking
)
2525 inlineDelimiters
[start
].type
= 0;
2530 // multiple-of-3 rule
2531 if (((inlineDelimiters
[start
].leftFlanking
&& inlineDelimiters
[start
].rightFlanking
) ||
2532 (inlineDelimiters
[end
].leftFlanking
&& inlineDelimiters
[end
].rightFlanking
)) &&
2533 (inlineDelimiters
[start
].count
+ inlineDelimiters
[end
].count
) % 3 == 0)
2539 immutable delta0
= replaceEmphasisPair(inlineDelimiters
[start
], inlineDelimiters
[end
]);
2541 for (; end
< inlineDelimiters
.length
; ++end
)
2542 inlineDelimiters
[end
].iStart
+= delta0
;
2546 inlineDelimiters
.length
= downToLevel
;
2550 /****************************************************
2552 bool isIdentifier(Dsymbols
* a
, const(char)[] s
) @safe
2554 foreach (member
; *a
)
2556 if (auto imp
= member
.isImport())
2558 // For example: `public import str = core.stdc.string;`
2559 // This checks if `s` is equal to `str`
2562 if (s
== imp
.aliasId
.toString())
2567 // The general case: `public import core.stdc.string;`
2569 // fully qualify imports so `core.stdc.string` doesn't appear as `core`
2570 string fullyQualifiedImport
;
2571 foreach (const pid
; imp
.packages
)
2573 fullyQualifiedImport
~= pid
.toString() ~ ".";
2575 fullyQualifiedImport
~= imp
.id
.toString();
2577 // Check if `s` == `core.stdc.string`
2578 if (s
== fullyQualifiedImport
)
2582 else if (member
.ident
)
2584 if (s
== member
.ident
.toString())
2592 /****************************************************
2594 bool isKeyword(const(char)[] str) @safe
2596 immutable string
[3] table
= ["true", "false", "null"];
2605 /****************************************************
2607 TypeFunction
isTypeFunction(Dsymbol s
) @safe
2609 FuncDeclaration f
= s
.isFuncDeclaration();
2610 /* f.type may be NULL for template members.
2614 Type t
= f
.originalType ? f
.originalType
: f
.type
;
2615 if (t
.ty
== Tfunction
)
2616 return cast(TypeFunction
)t
;
2621 /****************************************************
2623 Parameter
isFunctionParameter(Dsymbol s
, const(char)[] str) @safe
2625 TypeFunction tf
= isTypeFunction(s
);
2626 if (tf
&& tf
.parameterList
.parameters
)
2628 foreach (fparam
; *tf
.parameterList
.parameters
)
2630 if (fparam
.ident
&& str == fparam
.ident
.toString())
2639 /****************************************************
2641 Parameter
isFunctionParameter(Dsymbols
* a
, const(char)[] p
) @safe
2643 foreach (Dsymbol sym
; *a
)
2645 Parameter fparam
= isFunctionParameter(sym
, p
);
2654 /****************************************************
2656 Parameter
isEponymousFunctionParameter(Dsymbols
*a
, const(char)[] p
) @safe
2658 foreach (Dsymbol dsym
; *a
)
2660 TemplateDeclaration td
= dsym
.isTemplateDeclaration();
2661 if (td
&& td
.onemember
)
2663 /* Case 1: we refer to a template declaration inside the template
2670 td
= td
.onemember
.isTemplateDeclaration();
2674 /* Case 2: we're an alias to a template declaration
2677 alias case2 = case1!int;
2679 AliasDeclaration ad
= dsym
.isAliasDeclaration();
2680 if (ad
&& ad
.aliassym
)
2682 td
= ad
.aliassym
.isTemplateDeclaration();
2687 Dsymbol sym
= getEponymousMember(td
);
2690 Parameter fparam
= isFunctionParameter(sym
, p
);
2702 /****************************************************
2704 TemplateParameter
isTemplateParameter(Dsymbols
* a
, const(char)* p
, size_t len
)
2706 for (size_t i
= 0; i
< a
.length
; i
++)
2708 TemplateDeclaration td
= (*a
)[i
].isTemplateDeclaration();
2709 // Check for the parent, if the current symbol is not a template declaration.
2711 td
= getEponymousParent((*a
)[i
]);
2712 if (td
&& td
.origParameters
)
2714 foreach (tp
; *td
.origParameters
)
2716 if (tp
.ident
&& p
[0 .. len
] == tp
.ident
.toString())
2726 /****************************************************
2727 * Return true if str is a reserved symbol name
2728 * that starts with a double underscore.
2730 bool isReservedName(const(char)[] str) @safe
2732 immutable string
[] table
=
2755 "__PRETTY_FUNCTION__",
2774 /****************************************************
2775 * A delimiter for Markdown inline content like emphasis and links.
2777 struct MarkdownDelimiter
2779 size_t iStart
; /// the index where this delimiter starts
2780 int count
; /// the length of this delimeter's start sequence
2781 int macroLevel
; /// the count of nested DDoc macros when the delimiter is started
2782 bool leftFlanking
; /// whether the delimiter is left-flanking, as defined by the CommonMark spec
2783 bool rightFlanking
; /// whether the delimiter is right-flanking, as defined by the CommonMark spec
2784 bool atParagraphStart
; /// whether the delimiter is at the start of a paragraph
2785 char type
; /// the type of delimiter, defined by its starting character
2787 /// whether this describes a valid delimiter
2788 @property bool isValid() const @safe { return count
!= 0; }
2790 /// flag this delimiter as invalid
2791 void invalidate() @safe { count
= 0; }
2794 /****************************************************
2795 * Info about a Markdown list.
2799 string orderedStart
; /// an optional start number--if present then the list starts at this number
2800 size_t iStart
; /// the index where the list item starts
2801 size_t iContentStart
; /// the index where the content starts after the list delimiter
2802 int delimiterIndent
; /// the level of indent the list delimiter starts at
2803 int contentIndent
; /// the level of indent the content starts at
2804 int macroLevel
; /// the count of nested DDoc macros when the list is started
2805 char type
; /// the type of list, defined by its starting character
2807 /// whether this describes a valid list
2808 @property bool isValid() const @safe { return type
!= type
.init
; }
2810 /****************************************************
2811 * Try to parse a list item, returning whether successful.
2813 * buf = an OutBuffer containing the DDoc
2814 * iLineStart = the index within `buf` of the first character of the line
2815 * i = the index within `buf` of the potential list item
2816 * Returns: the parsed list item. Its `isValid` property describes whether parsing succeeded.
2818 static MarkdownList
parseItem(ref OutBuffer buf
, size_t iLineStart
, size_t i
) @safe
2820 if (buf
[i
] == '+' || buf
[i
] == '-' || buf
[i
] == '*')
2821 return parseUnorderedListItem(buf
, iLineStart
, i
);
2823 return parseOrderedListItem(buf
, iLineStart
, i
);
2826 /****************************************************
2827 * Return whether the context is at a list item of the same type as this list.
2829 * buf = an OutBuffer containing the DDoc
2830 * iLineStart = the index within `buf` of the first character of the line
2831 * i = the index within `buf` of the list item
2832 * Returns: whether `i` is at a list item of the same type as this list
2834 private bool isAtItemInThisList(ref OutBuffer buf
, size_t iLineStart
, size_t i
) @safe
2836 MarkdownList item
= (type
== '.' || type
== ')') ?
2837 parseOrderedListItem(buf
, iLineStart
, i
) :
2838 parseUnorderedListItem(buf
, iLineStart
, i
);
2839 if (item
.type
== type
)
2840 return item
.delimiterIndent
< contentIndent
&& item
.contentIndent
> delimiterIndent
;
2844 /****************************************************
2845 * Start a Markdown list item by creating/deleting nested lists and starting the item.
2847 * buf = an OutBuffer containing the DDoc
2848 * iLineStart = the index within `buf` of the first character of the line. If this function succeeds it will be adjuested to equal `i`.
2849 * i = the index within `buf` of the list item. If this function succeeds `i` will be adjusted to fit the inserted macro.
2850 * iPrecedingBlankLine = the index within `buf` of the preceeding blank line. If non-zero and a new list was started, the preceeding blank line is removed and this value is set to `0`.
2851 * nestedLists = a set of nested lists. If this function succeeds it may contain a new nested list.
2852 * loc = the location of the Ddoc within the file
2853 * Returns: `true` if a list was created
2855 bool startItem(ref OutBuffer buf
, ref size_t iLineStart
, ref size_t i
, ref size_t iPrecedingBlankLine
, ref MarkdownList
[] nestedLists
, const ref Loc loc
)
2857 buf
.remove(iStart
, iContentStart
- iStart
);
2859 if (!nestedLists
.length ||
2860 delimiterIndent
>= nestedLists
[$-1].contentIndent ||
2861 buf
[iLineStart
- 4..iLineStart
] == "$(LI")
2863 // start a list macro
2864 nestedLists
~= this;
2867 if (orderedStart
.length
)
2869 iStart
= buf
.insert(iStart
, "$(OL_START ");
2870 iStart
= buf
.insert(iStart
, orderedStart
);
2871 iStart
= buf
.insert(iStart
, ",\n");
2874 iStart
= buf
.insert(iStart
, "$(OL\n");
2877 iStart
= buf
.insert(iStart
, "$(UL\n");
2879 removeBlankLineMacro(buf
, iPrecedingBlankLine
, iStart
);
2881 else if (nestedLists
.length
)
2883 nestedLists
[$-1].delimiterIndent
= delimiterIndent
;
2884 nestedLists
[$-1].contentIndent
= contentIndent
;
2887 iStart
= buf
.insert(iStart
, "$(LI\n");
2894 /****************************************************
2895 * End all nested Markdown lists.
2897 * buf = an OutBuffer containing the DDoc
2898 * i = the index within `buf` to end lists at.
2899 * nestedLists = a set of nested lists. Upon return it will be empty.
2900 * Returns: the amount that `i` changed
2902 static size_t
endAllNestedLists(ref OutBuffer buf
, size_t i
, ref MarkdownList
[] nestedLists
)
2905 for (; nestedLists
.length
; --nestedLists
.length
)
2906 i
= buf
.insert(i
, ")\n)");
2910 /****************************************************
2911 * Look for a sibling list item or the end of nested list(s).
2913 * buf = an OutBuffer containing the DDoc
2914 * i = the index within `buf` to end lists at. If there was a sibling or ending lists `i` will be adjusted to fit the macro endings.
2915 * iParagraphStart = the index within `buf` to start the next paragraph at at. May be adjusted upon return.
2916 * nestedLists = a set of nested lists. Some nested lists may have been removed from it upon return.
2918 static void handleSiblingOrEndingList(ref OutBuffer buf
, ref size_t i
, ref size_t iParagraphStart
, ref MarkdownList
[] nestedLists
)
2920 size_t iAfterSpaces
= skipChars(buf
, i
+ 1, " \t");
2922 if (nestedLists
[$-1].isAtItemInThisList(buf
, i
+ 1, iAfterSpaces
))
2924 // end a sibling list item
2925 i
= buf
.insert(i
, ")");
2926 iParagraphStart
= skipChars(buf
, i
, " \t\r\n");
2928 else if (iAfterSpaces
>= buf
.length ||
(buf
[iAfterSpaces
] != '\r' && buf
[iAfterSpaces
] != '\n'))
2930 // end nested lists that are indented more than this content
2931 const indent
= getMarkdownIndent(buf
, i
+ 1, iAfterSpaces
);
2932 while (nestedLists
.length
&& nestedLists
[$-1].contentIndent
> indent
)
2934 i
= buf
.insert(i
, ")\n)");
2935 --nestedLists
.length
;
2936 iParagraphStart
= skipChars(buf
, i
, " \t\r\n");
2938 if (nestedLists
.length
&& nestedLists
[$-1].isAtItemInThisList(buf
, i
+ 1, iParagraphStart
))
2940 i
= buf
.insert(i
, ")");
2948 /****************************************************
2949 * Parse an unordered list item at the current position
2951 * buf = an OutBuffer containing the DDoc
2952 * iLineStart = the index within `buf` of the first character of the line
2953 * i = the index within `buf` of the list item
2954 * Returns: the parsed list item, or a list item with type `.init` if no list item is available
2956 private static MarkdownList
parseUnorderedListItem(ref OutBuffer buf
, size_t iLineStart
, size_t i
) @safe
2958 if (i
+1 < buf
.length
&&
2967 const iContentStart
= skipChars(buf
, i
+ 1, " \t");
2968 const delimiterIndent
= getMarkdownIndent(buf
, iLineStart
, i
);
2969 const contentIndent
= getMarkdownIndent(buf
, iLineStart
, iContentStart
);
2970 auto list
= MarkdownList(null, iLineStart
, iContentStart
, delimiterIndent
, contentIndent
, 0, buf
[i
]);
2973 return MarkdownList();
2976 /****************************************************
2977 * Parse an ordered list item at the current position
2979 * buf = an OutBuffer containing the DDoc
2980 * iLineStart = the index within `buf` of the first character of the line
2981 * i = the index within `buf` of the list item
2982 * Returns: the parsed list item, or a list item with type `.init` if no list item is available
2984 private static MarkdownList
parseOrderedListItem(ref OutBuffer buf
, size_t iLineStart
, size_t i
) @safe
2986 size_t iAfterNumbers
= skipChars(buf
, i
, "0123456789");
2987 if (iAfterNumbers
- i
> 0 &&
2988 iAfterNumbers
- i
<= 9 &&
2989 iAfterNumbers
+ 1 < buf
.length
&&
2990 buf
[iAfterNumbers
] == '.' &&
2991 (buf
[iAfterNumbers
+1] == ' ' ||
2992 buf
[iAfterNumbers
+1] == '\t' ||
2993 buf
[iAfterNumbers
+1] == '\r' ||
2994 buf
[iAfterNumbers
+1] == '\n'))
2996 const iContentStart
= skipChars(buf
, iAfterNumbers
+ 1, " \t");
2997 const delimiterIndent
= getMarkdownIndent(buf
, iLineStart
, i
);
2998 const contentIndent
= getMarkdownIndent(buf
, iLineStart
, iContentStart
);
2999 size_t iNumberStart
= skipChars(buf
, i
, "0");
3000 if (iNumberStart
== iAfterNumbers
)
3002 auto orderedStart
= buf
[][iNumberStart
.. iAfterNumbers
];
3003 if (orderedStart
== "1")
3004 orderedStart
= null;
3005 return MarkdownList(orderedStart
.idup
, iLineStart
, iContentStart
, delimiterIndent
, contentIndent
, 0, buf
[iAfterNumbers
]);
3007 return MarkdownList();
3011 /****************************************************
3016 string href
; /// the link destination
3017 string title
; /// an optional title for the link
3018 string label
; /// an optional label for the link
3019 Dsymbol symbol
; /// an optional symbol to link to
3021 /****************************************************
3022 * Replace a Markdown link or link definition in the form of:
3023 * - Inline link: `[foo](url/ 'optional title')`
3024 * - Reference link: `[foo][bar]`, `[foo][]` or `[foo]`
3025 * - Link reference definition: `[bar]: url/ 'optional title'`
3027 * buf = an OutBuffer containing the DDoc
3028 * i = the index within `buf` that points to the `]` character of the potential link.
3029 * If this function succeeds it will be adjusted to fit the inserted link macro.
3030 * loc = the current location within the file
3031 * inlineDelimiters = previously parsed Markdown delimiters, including emphasis and link/image starts
3032 * delimiterIndex = the index within `inlineDelimiters` of the nearest link/image starting delimiter
3033 * linkReferences = previously parsed link references. When this function returns it may contain
3034 * additional previously unparsed references.
3035 * Returns: whether a reference link was found and replaced at `i`
3037 static bool replaceLink(ref OutBuffer buf
, ref size_t i
, const ref Loc loc
, ref MarkdownDelimiter
[] inlineDelimiters
, int delimiterIndex
, ref MarkdownLinkReferences linkReferences
)
3039 const delimiter
= inlineDelimiters
[delimiterIndex
];
3042 size_t iEnd
= link
.parseReferenceDefinition(buf
, i
, delimiter
);
3045 i
= delimiter
.iStart
;
3046 link
.storeAndReplaceDefinition(buf
, i
, iEnd
, linkReferences
, loc
);
3047 inlineDelimiters
.length
= delimiterIndex
;
3051 iEnd
= link
.parseInlineLink(buf
, i
);
3054 iEnd
= link
.parseReferenceLink(buf
, i
, delimiter
);
3057 const label
= link
.label
;
3058 link
= linkReferences
.lookupReference(label
, buf
, i
, loc
);
3059 // check rightFlanking to avoid replacing things like int[string]
3060 if (!link
.href
.length
&& !delimiter
.rightFlanking
)
3061 link
= linkReferences
.lookupSymbol(label
);
3062 if (!link
.href
.length
)
3070 immutable delta
= replaceMarkdownEmphasis(buf
, loc
, inlineDelimiters
, delimiterIndex
);
3073 link
.replaceLink(buf
, i
, iEnd
, delimiter
);
3077 /****************************************************
3078 * Replace a Markdown link definition in the form of `[bar]: url/ 'optional title'`
3080 * buf = an OutBuffer containing the DDoc
3081 * i = the index within `buf` that points to the `]` character of the potential link.
3082 * If this function succeeds it will be adjusted to fit the inserted link macro.
3083 * inlineDelimiters = previously parsed Markdown delimiters, including emphasis and link/image starts
3084 * delimiterIndex = the index within `inlineDelimiters` of the nearest link/image starting delimiter
3085 * linkReferences = previously parsed link references. When this function returns it may contain
3086 * additional previously unparsed references.
3087 * loc = the current location in the file
3088 * Returns: whether a reference link was found and replaced at `i`
3090 static bool replaceReferenceDefinition(ref OutBuffer buf
, ref size_t i
, ref MarkdownDelimiter
[] inlineDelimiters
, int delimiterIndex
, ref MarkdownLinkReferences linkReferences
, const ref Loc loc
)
3092 const delimiter
= inlineDelimiters
[delimiterIndex
];
3094 size_t iEnd
= link
.parseReferenceDefinition(buf
, i
, delimiter
);
3098 i
= delimiter
.iStart
;
3099 link
.storeAndReplaceDefinition(buf
, i
, iEnd
, linkReferences
, loc
);
3100 inlineDelimiters
.length
= delimiterIndex
;
3104 /****************************************************
3105 * Parse a Markdown inline link in the form of `[foo](url/ 'optional title')`
3107 * buf = an OutBuffer containing the DDoc
3108 * i = the index within `buf` that points to the `]` character of the inline link.
3109 * Returns: the index at the end of parsing the link, or `i` if parsing failed.
3111 private size_t
parseInlineLink(ref OutBuffer buf
, size_t i
)
3113 size_t iEnd
= i
+ 1;
3114 if (iEnd
>= buf
.length || buf
[iEnd
] != '(')
3118 if (!parseHref(buf
, iEnd
))
3121 iEnd
= skipChars(buf
, iEnd
, " \t\r\n");
3122 if (buf
[iEnd
] != ')')
3124 if (parseTitle(buf
, iEnd
))
3125 iEnd
= skipChars(buf
, iEnd
, " \t\r\n");
3128 if (buf
[iEnd
] != ')')
3134 /****************************************************
3135 * Parse a Markdown reference link in the form of `[foo][bar]`, `[foo][]` or `[foo]`
3137 * buf = an OutBuffer containing the DDoc
3138 * i = the index within `buf` that points to the `]` character of the inline link.
3139 * delimiter = the delimiter that starts this link
3140 * Returns: the index at the end of parsing the link, or `i` if parsing failed.
3142 private size_t
parseReferenceLink(ref OutBuffer buf
, size_t i
, MarkdownDelimiter delimiter
) @safe
3144 size_t iStart
= i
+ 1;
3145 size_t iEnd
= iStart
;
3146 if (iEnd
>= buf
.length || buf
[iEnd
] != '[' ||
(iEnd
+1 < buf
.length
&& buf
[iEnd
+1] == ']'))
3148 // collapsed reference [foo][] or shortcut reference [foo]
3149 iStart
= delimiter
.iStart
+ delimiter
.count
- 1;
3150 if (buf
[iEnd
] == '[')
3154 parseLabel(buf
, iStart
);
3163 /****************************************************
3164 * Parse a Markdown reference definition in the form of `[bar]: url/ 'optional title'`
3166 * buf = an OutBuffer containing the DDoc
3167 * i = the index within `buf` that points to the `]` character of the inline link.
3168 * delimiter = the delimiter that starts this link
3169 * Returns: the index at the end of parsing the link, or `i` if parsing failed.
3171 private size_t
parseReferenceDefinition(ref OutBuffer buf
, size_t i
, MarkdownDelimiter delimiter
)
3173 if (!delimiter
.atParagraphStart || delimiter
.type
!= '[' ||
3174 i
+1 >= buf
.length || buf
[i
+1] != ':')
3177 size_t iEnd
= delimiter
.iStart
;
3178 parseLabel(buf
, iEnd
);
3179 if (label
.length
== 0 || iEnd
!= i
+ 1)
3183 iEnd
= skipChars(buf
, iEnd
, " \t");
3184 skipOneNewline(buf
, iEnd
);
3186 if (!parseHref(buf
, iEnd
) || href
.length
== 0)
3189 iEnd
= skipChars(buf
, iEnd
, " \t");
3190 const requireNewline
= !skipOneNewline(buf
, iEnd
);
3191 const iBeforeTitle
= iEnd
;
3193 if (parseTitle(buf
, iEnd
))
3195 iEnd
= skipChars(buf
, iEnd
, " \t");
3196 if (iEnd
< buf
.length
&& buf
[iEnd
] != '\r' && buf
[iEnd
] != '\n')
3198 // the title must end with a newline
3200 iEnd
= iBeforeTitle
;
3204 iEnd
= skipChars(buf
, iEnd
, " \t");
3205 if (requireNewline
&& iEnd
< buf
.length
-1 && buf
[iEnd
] != '\r' && buf
[iEnd
] != '\n')
3211 /****************************************************
3212 * Parse and normalize a Markdown reference label
3214 * buf = an OutBuffer containing the DDoc
3215 * i = the index within `buf` that points to the `[` character at the start of the label.
3216 * If this function returns a non-empty label then `i` will point just after the ']' at the end of the label.
3217 * Returns: the parsed and normalized label, possibly empty
3219 private bool parseLabel(ref OutBuffer buf
, ref size_t i
) @safe
3224 const slice
= buf
[];
3227 // Some labels have already been en-symboled; handle that
3228 const inSymbol
= j
+15 < slice
.length
&& slice
[j
..j
+15] == "$(DDOC_PSYMBOL ";
3232 for (; j
< slice
.length
; ++j
)
3241 if (label
.length
&& label
[$-1] != ' ')
3245 if (inSymbol
&& j
+1 < slice
.length
&& slice
[j
+1] == ']')
3252 if (slice
[j
-1] != '\\')
3259 if (label
.length
&& label
[$-1] == ' ')
3276 /****************************************************
3277 * Parse and store a Markdown link URL, optionally enclosed in `<>` brackets
3279 * buf = an OutBuffer containing the DDoc
3280 * i = the index within `buf` that points to the first character of the URL.
3281 * If this function succeeds `i` will point just after the end of the URL.
3282 * Returns: whether a URL was found and parsed
3284 private bool parseHref(ref OutBuffer buf
, ref size_t i
)
3286 size_t j
= skipChars(buf
, i
, " \t");
3288 size_t iHrefStart
= j
;
3289 size_t parenDepth
= 1;
3290 bool inPointy
= false;
3291 const slice
= buf
[];
3292 for (; j
< slice
.length
; j
++)
3297 if (!inPointy
&& j
== iHrefStart
)
3304 if (inPointy
&& slice
[j
-1] != '\\')
3308 if (!inPointy
&& slice
[j
-1] != '\\')
3312 if (!inPointy
&& slice
[j
-1] != '\\')
3336 auto href
= slice
[iHrefStart
.. j
].dup
;
3337 this.href
= cast(string
) percentEncode(removeEscapeBackslashes(href
)).replaceChar(',', "$(COMMA)");
3344 /****************************************************
3345 * Parse and store a Markdown link title, enclosed in parentheses or `'` or `"` quotes
3347 * buf = an OutBuffer containing the DDoc
3348 * i = the index within `buf` that points to the first character of the title.
3349 * If this function succeeds `i` will point just after the end of the title.
3350 * Returns: whether a title was found and parsed
3352 private bool parseTitle(ref OutBuffer buf
, ref size_t i
)
3354 size_t j
= skipChars(buf
, i
, " \t");
3355 if (j
>= buf
.length
)
3359 if (type
!= '"' && type
!= '\'' && type
!= '(')
3364 const iTitleStart
= j
+ 1;
3365 size_t iNewline
= 0;
3366 const slice
= buf
[];
3367 for (j
= iTitleStart
; j
< slice
.length
; j
++)
3375 if (type
== c
&& slice
[j
-1] != '\\')
3386 // no blank lines in titles
3398 auto title
= slice
[iTitleStart
.. j
].dup
;
3399 this.title
= cast(string
) removeEscapeBackslashes(title
).
3400 replaceChar(',', "$(COMMA)").
3401 replaceChar('"', "$(QUOTE)");
3406 /****************************************************
3407 * Replace a Markdown link or image with the appropriate macro
3409 * buf = an OutBuffer containing the DDoc
3410 * i = the index within `buf` that points to the `]` character of the inline link.
3411 * When this function returns it will be adjusted to the end of the inserted macro.
3412 * iLinkEnd = the index within `buf` that points just after the last character of the link
3413 * delimiter = the Markdown delimiter that started the link or image
3415 private void replaceLink(ref OutBuffer buf
, ref size_t i
, size_t iLinkEnd
, MarkdownDelimiter delimiter
)
3417 size_t iAfterLink
= i
- delimiter
.count
;
3421 macroName
= "$(SYMBOL_LINK ";
3423 else if (title
.length
)
3425 if (delimiter
.type
== '[')
3426 macroName
= "$(LINK_TITLE ";
3428 macroName
= "$(IMAGE_TITLE ";
3432 if (delimiter
.type
== '[')
3433 macroName
= "$(LINK2 ";
3435 macroName
= "$(IMAGE ";
3437 buf
.remove(delimiter
.iStart
, delimiter
.count
);
3438 buf
.remove(i
- delimiter
.count
, iLinkEnd
- i
);
3439 iLinkEnd
= buf
.insert(delimiter
.iStart
, macroName
);
3440 iLinkEnd
= buf
.insert(iLinkEnd
, href
);
3441 iLinkEnd
= buf
.insert(iLinkEnd
, ", ");
3442 iAfterLink
+= macroName
.length
+ href
.length
+ 2;
3445 iLinkEnd
= buf
.insert(iLinkEnd
, title
);
3446 iLinkEnd
= buf
.insert(iLinkEnd
, ", ");
3447 iAfterLink
+= title
.length
+ 2;
3449 // Link macros with titles require escaping commas
3450 for (size_t j
= iLinkEnd
; j
< iAfterLink
; ++j
)
3454 j
= buf
.insert(j
, "$(COMMA)") - 1;
3458 // TODO: if image, remove internal macros, leaving only text
3459 buf
.insert(iAfterLink
, ")");
3463 /****************************************************
3464 * Store the Markdown link definition and remove it from `buf`
3466 * buf = an OutBuffer containing the DDoc
3467 * i = the index within `buf` that points to the `[` character at the start of the link definition.
3468 * When this function returns it will be adjusted to exclude the link definition.
3469 * iEnd = the index within `buf` that points just after the end of the definition
3470 * linkReferences = previously parsed link references. When this function returns it may contain
3471 * an additional reference.
3472 * loc = the current location in the file
3474 private void storeAndReplaceDefinition(ref OutBuffer buf
, ref size_t i
, size_t iEnd
, ref MarkdownLinkReferences linkReferences
, const ref Loc loc
)
3476 // Remove the definition and trailing whitespace
3477 iEnd
= skipChars(buf
, iEnd
, " \t\r\n");
3478 buf
.remove(i
, iEnd
- i
);
3481 string lowercaseLabel
= label
.toLowercase();
3482 if (lowercaseLabel
!in linkReferences
.references
)
3483 linkReferences
.references
[lowercaseLabel
] = this;
3486 /****************************************************
3487 * Remove Markdown escaping backslashes from the given string
3489 * s = the string to remove escaping backslashes from
3490 * Returns: `s` without escaping backslashes in it
3492 private static char[] removeEscapeBackslashes(char[] s
) @safe
3497 // avoid doing anything if there isn't anything to escape
3499 for (i
= 0; i
< s
.length
-1; ++i
)
3500 if (s
[i
] == '\\' && ispunct(s
[i
+1]))
3502 if (i
== s
.length
-1)
3505 // copy characters backwards, then truncate
3508 for (++i
, ++j
; j
< s
.length
; ++i
, ++j
)
3510 if (j
< s
.length
-1 && s
[j
] == '\\' && ispunct(s
[j
+1]))
3514 s
.length
-= (j
- i
);
3521 assert(removeEscapeBackslashes("".dup
) == "");
3522 assert(removeEscapeBackslashes(`\a`.dup
) == `\a`);
3523 assert(removeEscapeBackslashes(`.\`.dup
) == `.\`);
3524 assert(removeEscapeBackslashes(`\.\`.dup
) == `.\`);
3525 assert(removeEscapeBackslashes(`\.`.dup
) == `.`);
3526 assert(removeEscapeBackslashes(`\.\.`.dup
) == `..`);
3527 assert(removeEscapeBackslashes(`a\.b\.c`.dup
) == `a.b.c`);
3530 /****************************************************
3531 * Percent-encode (AKA URL-encode) the given string
3533 * s = the string to percent-encode
3534 * Returns: `s` with special characters percent-encoded
3536 private static inout(char)[] percentEncode(inout(char)[] s
) pure @safe
3538 static bool shouldEncode(char c
)
3540 return ((c
< '0' && c
!= '!' && c
!= '#' && c
!= '$' && c
!= '%' && c
!= '&' && c
!= '\'' && c
!= '(' &&
3541 c
!= ')' && c
!= '*' && c
!= '+' && c
!= ',' && c
!= '-' && c
!= '.' && c
!= '/')
3542 ||
(c
> '9' && c
< 'A' && c
!= ':' && c
!= ';' && c
!= '=' && c
!= '?' && c
!= '@')
3543 ||
(c
> 'Z' && c
< 'a' && c
!= '[' && c
!= ']' && c
!= '_')
3544 ||
(c
> 'z' && c
!= '~'));
3547 for (size_t i
= 0; i
< s
.length
; ++i
)
3549 if (shouldEncode(s
[i
]))
3551 immutable static hexDigits
= "0123456789ABCDEF";
3552 immutable encoded1
= hexDigits
[s
[i
] >> 4];
3553 immutable encoded2
= hexDigits
[s
[i
] & 0x0F];
3554 s
= s
[0..i
] ~ '%' ~ encoded1
~ encoded2
~ s
[i
+1..$];
3564 assert(percentEncode("") == "");
3565 assert(percentEncode("aB12-._~/?") == "aB12-._~/?");
3566 assert(percentEncode("<\n>") == "%3C%0A%3E");
3569 /**************************************************
3570 * Skip a single newline at `i`
3572 * buf = an OutBuffer containing the DDoc
3573 * i = the index within `buf` to start looking at.
3574 * If this function succeeds `i` will point after the newline.
3575 * Returns: whether a newline was skipped
3577 private static bool skipOneNewline(ref OutBuffer buf
, ref size_t i
) pure @safe
3579 if (i
< buf
.length
&& buf
[i
] == '\r')
3581 if (i
< buf
.length
&& buf
[i
] == '\n')
3590 /**************************************************
3591 * A set of Markdown link references.
3593 struct MarkdownLinkReferences
3595 MarkdownLink
[string
] references
; // link references keyed by normalized label
3596 MarkdownLink
[string
] symbols
; // link symbols keyed by name
3597 Scope
* _scope
; // the current scope
3598 bool extractedAll
; // the index into the buffer of the last-parsed reference
3600 /**************************************************
3601 * Look up a reference by label, searching through the rest of the buffer if needed.
3602 * Symbols in the current scope are searched for if the DDoc doesn't define the reference.
3604 * label = the label to find the reference for
3605 * buf = an OutBuffer containing the DDoc
3606 * i = the index within `buf` to start searching for references at
3607 * loc = the current location in the file
3608 * Returns: a link. If the `href` member has a value then the reference is valid.
3610 MarkdownLink
lookupReference(string label
, ref OutBuffer buf
, size_t i
, const ref Loc loc
)
3612 const lowercaseLabel
= label
.toLowercase();
3613 if (lowercaseLabel
!in references
)
3614 extractReferences(buf
, i
, loc
);
3616 if (lowercaseLabel
in references
)
3617 return references
[lowercaseLabel
];
3619 return MarkdownLink();
3623 * Look up the link for the D symbol with the given name.
3624 * If found, the link is cached in the `symbols` member.
3626 * name = the name of the symbol
3627 * Returns: the link for the symbol or a link with a `null` href
3629 MarkdownLink
lookupSymbol(string name
)
3631 if (name
in symbols
)
3632 return symbols
[name
];
3634 const ids
= split(name
, '.');
3637 auto id
= Identifier
.lookup(ids
[0].ptr
, ids
[0].length
);
3642 auto symbol
= _scope
.search(loc
, id
, pscopesym
, SearchOpt
.ignoreErrors
);
3643 for (size_t i
= 1; symbol
&& i
< ids
.length
; ++i
)
3645 id
= Identifier
.lookup(ids
[i
].ptr
, ids
[i
].length
);
3646 symbol
= id
!is null ? symbol
.search(loc
, id
, SearchOpt
.ignoreErrors
) : null;
3649 link
= MarkdownLink(createHref(symbol
), null, name
, symbol
);
3652 symbols
[name
] = link
;
3656 /**************************************************
3657 * Remove and store all link references from the document, in the form of
3658 * `[label]: href "optional title"`
3660 * buf = an OutBuffer containing the DDoc
3661 * i = the index within `buf` to start looking at
3662 * loc = the current location in the file
3663 * Returns: whether a reference was extracted
3665 private void extractReferences(ref OutBuffer buf
, size_t i
, const ref Loc loc
)
3667 static bool isFollowedBySpace(ref OutBuffer buf
, size_t i
)
3669 return i
+1 < buf
.length
&& (buf
[i
+1] == ' ' || buf
[i
+1] == '\t');
3675 bool leadingBlank
= false;
3677 bool newParagraph
= true;
3678 MarkdownDelimiter
[] delimiters
;
3679 for (; i
< buf
.length
; ++i
)
3688 if (leadingBlank
&& !inCode
)
3689 newParagraph
= true;
3690 leadingBlank
= true;
3696 if (leadingBlank
&& !inCode
)
3697 newParagraph
= true;
3698 leadingBlank
= false;
3701 if (leadingBlank
&& !inCode
)
3702 newParagraph
= true;
3705 if (leadingBlank
&& !inCode
&& isFollowedBySpace(buf
, i
))
3706 newParagraph
= true;
3708 leadingBlank
= false;
3713 if (leadingBlank
&& !inCode
)
3715 i
= skipChars(buf
, i
, "0123456789");
3716 if (i
< buf
.length
&&
3717 (buf
[i
] == '.' || buf
[i
] == ')') &&
3718 isFollowedBySpace(buf
, i
))
3719 newParagraph
= true;
3721 leadingBlank
= false;
3725 if (leadingBlank
&& !inCode
)
3727 newParagraph
= true;
3728 if (!isFollowedBySpace(buf
, i
))
3729 leadingBlank
= false;
3734 if (leadingBlank
&& i
+2 < buf
.length
&& buf
[i
+1] == c
&& buf
[i
+2] == c
)
3736 inCode
= inCode
== c ?
false : c
;
3737 i
= skipChars(buf
, i
, [c
]) - 1;
3738 newParagraph
= true;
3740 leadingBlank
= false;
3743 if (leadingBlank
&& !inCode
&& isFollowedBySpace(buf
, i
))
3748 if (leadingBlank
&& !inCode
&& newParagraph
)
3749 delimiters
~= MarkdownDelimiter(i
, 1, 0, false, false, true, c
);
3752 if (delimiters
.length
&& !inCode
&&
3753 MarkdownLink
.replaceReferenceDefinition(buf
, i
, delimiters
, cast(int) delimiters
.length
- 1, this, loc
))
3758 newParagraph
= false;
3759 leadingBlank
= false;
3763 extractedAll
= true;
3767 * Split a string by a delimiter, excluding the delimiter.
3769 * s = the string to split
3770 * delimiter = the character to split by
3771 * Returns: the resulting array of strings
3773 private static string
[] split(string s
, char delimiter
) pure @safe
3777 foreach (size_t i
; 0..s
.length
)
3778 if (s
[i
] == delimiter
)
3780 result
~= s
[iStart
..i
];
3783 result
~= s
[iStart
..$];
3790 assert(split("", ',') == [""]);
3791 assert(split("ab", ',') == ["ab"]);
3792 assert(split("a,b", ',') == ["a", "b"]);
3793 assert(split("a,,b", ',') == ["a", "", "b"]);
3794 assert(split(",ab", ',') == ["", "ab"]);
3795 assert(split("ab,", ',') == ["ab", ""]);
3799 * Create a HREF for the given D symbol.
3800 * The HREF is relative to the current location if possible.
3802 * symbol = the symbol to create a HREF for.
3803 * Returns: the resulting href
3805 private string
createHref(Dsymbol symbol
)
3807 Dsymbol root
= symbol
;
3810 while (symbol
&& symbol
.ident
&& !symbol
.isModule())
3814 lref
= symbol
.ident
.toString() ~ lref
;
3815 symbol
= symbol
.parent
;
3819 if (symbol
&& symbol
.ident
&& symbol
.isModule() != _scope
._module
)
3825 // If the module has a file name, we're done
3826 if (const m
= symbol
.isModule())
3829 path
= m
.docfile
.toString();
3835 path
= symbol
.ident
.toString() ~ path
;
3836 symbol
= symbol
.parent
;
3837 } while (symbol
&& symbol
.ident
);
3839 if (!symbol
&& path
.length
)
3840 path
~= "$(DOC_EXTENSION)";
3843 // Attempt an absolute URL if not in the same package
3846 Dsymbol scopeRoot
= _scope
._module
;
3847 while (scopeRoot
.parent
)
3848 scopeRoot
= scopeRoot
.parent
;
3849 if (scopeRoot
!= root
)
3851 path
= "$(DOC_ROOT_" ~ root
.ident
.toString() ~ ')' ~ path
;
3852 lref
= '.' ~ lref
; // remote URIs like Phobos and Mir use .prefixes
3855 return cast(string
) (path
~ '#' ~ lref
);
3859 enum TableColumnAlignment
3867 /****************************************************
3868 * Parse a Markdown table delimiter row in the form of `| -- | :-- | :--: | --: |`
3869 * where the example text has four columns with the following alignments:
3870 * default, left, center, and right. The first and last pipes are optional. If a
3871 * delimiter row is found it will be removed from `buf`.
3874 * buf = an OutBuffer containing the DDoc
3875 * iStart = the index within `buf` that the delimiter row starts at
3876 * inQuote = whether the table is inside a quote
3877 * columnAlignments = alignments to populate for each column
3878 * Returns: the index of the end of the parsed delimiter, or `0` if not found
3880 size_t
parseTableDelimiterRow(ref OutBuffer buf
, const size_t iStart
, bool inQuote
, ref TableColumnAlignment
[] columnAlignments
) @safe
3882 size_t i
= skipChars(buf
, iStart
, inQuote ?
">| \t" : "| \t");
3883 while (i
< buf
.length
&& buf
[i
] != '\r' && buf
[i
] != '\n')
3885 const leftColon
= buf
[i
] == ':';
3889 if (i
>= buf
.length || buf
[i
] != '-')
3891 i
= skipChars(buf
, i
, "-");
3893 const rightColon
= i
< buf
.length
&& buf
[i
] == ':';
3894 i
= skipChars(buf
, i
, ": \t");
3896 if (i
>= buf
.length ||
(buf
[i
] != '|' && buf
[i
] != '\r' && buf
[i
] != '\n'))
3898 i
= skipChars(buf
, i
, "| \t");
3900 columnAlignments
~= (leftColon
&& rightColon
) ? TableColumnAlignment
.center
:
3901 leftColon ? TableColumnAlignment
.left
:
3902 rightColon ? TableColumnAlignment
.right
:
3903 TableColumnAlignment
.none
;
3906 if (i
< buf
.length
&& buf
[i
] != '\r' && buf
[i
] != '\n' && buf
[i
] != ')')
3908 columnAlignments
.length
= 0;
3912 if (i
< buf
.length
&& buf
[i
] == '\r') ++i
;
3913 if (i
< buf
.length
&& buf
[i
] == '\n') ++i
;
3917 /****************************************************
3918 * Look for a table delimiter row, and if found parse the previous row as a
3919 * table header row. If both exist with a matching number of columns, start a
3923 * buf = an OutBuffer containing the DDoc
3924 * iStart = the index within `buf` that the table header row starts at, inclusive
3925 * iEnd = the index within `buf` that the table header row ends at, exclusive
3926 * loc = the current location in the file
3927 * inQuote = whether the table is inside a quote
3928 * inlineDelimiters = delimiters containing columns separators and any inline emphasis
3929 * columnAlignments = the parsed alignments for each column
3930 * Returns: the number of characters added by starting the table, or `0` if unchanged
3932 size_t
startTable(ref OutBuffer buf
, size_t iStart
, size_t iEnd
, const ref Loc loc
, bool inQuote
, ref MarkdownDelimiter
[] inlineDelimiters
, out TableColumnAlignment
[] columnAlignments
)
3934 const iDelimiterRowEnd
= parseTableDelimiterRow(buf
, iEnd
+ 1, inQuote
, columnAlignments
);
3935 if (iDelimiterRowEnd
)
3938 if (replaceTableRow(buf
, iStart
, iEnd
, loc
, inlineDelimiters
, columnAlignments
, true, delta
))
3940 buf
.remove(iEnd
+ delta
, iDelimiterRowEnd
- iEnd
);
3941 buf
.insert(iEnd
+ delta
, "$(TBODY ");
3942 buf
.insert(iStart
, "$(TABLE ");
3947 columnAlignments
.length
= 0;
3951 /****************************************************
3952 * Replace a Markdown table row in the form of table cells delimited by pipes:
3953 * `| cell | cell | cell`. The first and last pipes are optional.
3956 * buf = an OutBuffer containing the DDoc
3957 * iStart = the index within `buf` that the table row starts at, inclusive
3958 * iEnd = the index within `buf` that the table row ends at, exclusive
3959 * loc = the current location in the file
3960 * inlineDelimiters = delimiters containing columns separators and any inline emphasis
3961 * columnAlignments = alignments for each column
3962 * headerRow = if `true` then the number of columns will be enforced to match
3963 * `columnAlignments.length` and the row will be surrounded by a
3965 * delta = the number of characters added by replacing the row, or `0` if unchanged
3966 * Returns: `true` if a table row was found and replaced
3968 bool replaceTableRow(ref OutBuffer buf
, size_t iStart
, size_t iEnd
, const ref Loc loc
, ref MarkdownDelimiter
[] inlineDelimiters
, TableColumnAlignment
[] columnAlignments
, bool headerRow
, out size_t delta
)
3972 if (!columnAlignments
.length || iStart
== iEnd
)
3975 iStart
= skipChars(buf
, iStart
, " \t");
3977 foreach (delimiter
; inlineDelimiters
)
3978 if (delimiter
.type
== '|' && !delimiter
.leftFlanking
)
3980 bool ignoreLast
= inlineDelimiters
.length
> 0 && inlineDelimiters
[$-1].type
== '|';
3983 const iLast
= skipChars(buf
, inlineDelimiters
[$-1].iStart
+ inlineDelimiters
[$-1].count
, " \t");
3984 ignoreLast
= iLast
>= iEnd
;
3989 if (headerRow
&& cellCount
!= columnAlignments
.length
)
3992 void replaceTableCell(size_t iCellStart
, size_t iCellEnd
, int cellIndex
, int di)
3994 const eDelta
= replaceMarkdownEmphasis(buf
, loc
, inlineDelimiters
, di);
3998 // strip trailing whitespace and delimiter
3999 size_t i
= iCellEnd
- 1;
4000 while (i
> iCellStart
&& (buf
[i
] == '|' || buf
[i
] == ' ' || buf
[i
] == '\t'))
4003 buf
.remove(i
, iCellEnd
- i
);
4004 delta
-= iCellEnd
- i
;
4007 buf
.insert(iCellEnd
, ")");
4010 // strip initial whitespace and delimiter
4011 i
= skipChars(buf
, iCellStart
, "| \t");
4012 buf
.remove(iCellStart
, i
- iCellStart
);
4013 delta
-= i
- iCellStart
;
4015 switch (columnAlignments
[cellIndex
])
4017 case TableColumnAlignment
.none
:
4018 buf
.insert(iCellStart
, headerRow ?
"$(TH " : "$(TD ");
4021 case TableColumnAlignment
.left
:
4022 buf
.insert(iCellStart
, "left, ");
4025 case TableColumnAlignment
.center
:
4026 buf
.insert(iCellStart
, "center, ");
4029 case TableColumnAlignment
.right
:
4030 buf
.insert(iCellStart
, "right, ");
4034 buf
.insert(iCellStart
, headerRow ?
"$(TH_ALIGN " : "$(TD_ALIGN ");
4040 int cellIndex
= cellCount
- 1;
4041 size_t iCellEnd
= iEnd
;
4042 foreach_reverse (di, delimiter
; inlineDelimiters
)
4044 if (delimiter
.type
== '|')
4046 if (ignoreLast
&& di == inlineDelimiters
.length
-1)
4052 if (cellIndex
>= columnAlignments
.length
)
4054 // kill any extra cells
4055 buf
.remove(delimiter
.iStart
, iEnd
+ delta
- delimiter
.iStart
);
4056 delta
-= iEnd
+ delta
- delimiter
.iStart
;
4057 iCellEnd
= iEnd
+ delta
;
4062 replaceTableCell(delimiter
.iStart
, iCellEnd
, cellIndex
, cast(int) di);
4063 iCellEnd
= delimiter
.iStart
;
4068 // if no starting pipe, replace from the start
4070 replaceTableCell(iStart
, iCellEnd
, cellIndex
, 0);
4072 buf
.insert(iEnd
+ delta
, ")");
4073 buf
.insert(iStart
, "$(TR ");
4078 buf
.insert(iEnd
+ delta
, ")");
4079 buf
.insert(iStart
, "$(THEAD ");
4086 /****************************************************
4087 * End a table, if in one.
4090 * buf = an OutBuffer containing the DDoc
4091 * i = the index within `buf` to end the table at
4092 * columnAlignments = alignments for each column; upon return is set to length `0`
4093 * Returns: the number of characters added by ending the table, or `0` if unchanged
4095 size_t
endTable(ref OutBuffer buf
, size_t i
, ref TableColumnAlignment
[] columnAlignments
)
4097 if (!columnAlignments
.length
)
4100 buf
.insert(i
, "))");
4101 columnAlignments
.length
= 0;
4105 /****************************************************
4106 * End a table row and then the table itself.
4109 * buf = an OutBuffer containing the DDoc
4110 * iStart = the index within `buf` that the table row starts at, inclusive
4111 * iEnd = the index within `buf` that the table row ends at, exclusive
4112 * loc = the current location in the file
4113 * inlineDelimiters = delimiters containing columns separators and any inline emphasis
4114 * columnAlignments = alignments for each column; upon return is set to length `0`
4115 * Returns: the number of characters added by replacing the row, or `0` if unchanged
4117 size_t
endRowAndTable(ref OutBuffer buf
, size_t iStart
, size_t iEnd
, const ref Loc loc
, ref MarkdownDelimiter
[] inlineDelimiters
, ref TableColumnAlignment
[] columnAlignments
)
4120 replaceTableRow(buf
, iStart
, iEnd
, loc
, inlineDelimiters
, columnAlignments
, false, delta
);
4121 delta
+= endTable(buf
, iEnd
+ delta
, columnAlignments
);
4125 /**************************************************
4126 * Highlight text section.
4129 * scope = the current parse scope
4130 * a = an array of D symbols at the current scope
4131 * loc = source location of start of text. It is a mutable copy to allow incrementing its linenum, for printing the correct line number when an error is encountered in a multiline block of ddoc.
4132 * buf = an OutBuffer containing the DDoc
4133 * offset = the index within buf to start highlighting
4135 void highlightText(Scope
* sc
, Dsymbols
* a
, Loc loc
, ref OutBuffer buf
, size_t offset
)
4137 const incrementLoc
= loc
.linnum
== 0 ?
1 : 0;
4138 loc
.linnum
= loc
.linnum
+ incrementLoc
;
4140 //printf("highlightText()\n");
4141 bool leadingBlank
= true;
4142 size_t iParagraphStart
= offset
;
4143 size_t iPrecedingBlankLine
= 0;
4144 int headingLevel
= 0;
4145 int headingMacroLevel
= 0;
4147 bool lineQuoted
= false;
4148 int quoteMacroLevel
= 0;
4149 MarkdownList
[] nestedLists
;
4150 MarkdownDelimiter
[] inlineDelimiters
;
4151 MarkdownLinkReferences linkReferences
;
4152 TableColumnAlignment
[] columnAlignments
;
4153 bool tableRowDetected
= false;
4157 int previousMacroLevel
= 0;
4159 size_t iCodeStart
= 0; // start of code section
4160 size_t codeFenceLength
= 0;
4161 size_t codeIndent
= 0;
4162 string codeLanguage
;
4163 size_t iLineStart
= offset
;
4164 linkReferences
._scope
= sc
;
4165 for (size_t i
= offset
; i
< buf
.length
; i
++)
4177 // `inline code` is only valid if contained on a single line
4178 // otherwise, the backticks should be output literally.
4180 // This lets things like `output from the linker' display
4181 // unmolested while keeping the feature consistent with GitHub.
4183 inCode
= false; // the backtick also assumes we're in code
4184 // Nothing else is necessary since the DDOC_BACKQUOTED macro is
4185 // inserted lazily at the close quote, meaning the rest of the
4186 // text is already OK.
4190 i
+= replaceMarkdownEmphasis(buf
, loc
, inlineDelimiters
);
4191 endMarkdownHeading(buf
, iParagraphStart
, i
, loc
, headingLevel
);
4192 removeBlankLineMacro(buf
, iPrecedingBlankLine
, i
);
4194 iParagraphStart
= skipChars(buf
, i
, " \t\r\n");
4197 if (tableRowDetected
&& !columnAlignments
.length
)
4198 i
+= startTable(buf
, iLineStart
, i
, loc
, lineQuoted
, inlineDelimiters
, columnAlignments
);
4199 else if (columnAlignments
.length
)
4202 if (replaceTableRow(buf
, iLineStart
, i
, loc
, inlineDelimiters
, columnAlignments
, false, delta
))
4205 i
+= endTable(buf
, i
, columnAlignments
);
4208 if (!inCode
&& nestedLists
.length
&& !quoteLevel
)
4209 MarkdownList
.handleSiblingOrEndingList(buf
, i
, iParagraphStart
, nestedLists
);
4211 iPrecedingBlankLine
= 0;
4212 if (!inCode
&& i
== iLineStart
&& i
+ 1 < buf
.length
) // if "\n\n"
4214 i
+= endTable(buf
, i
, columnAlignments
);
4215 if (!lineQuoted
&& quoteLevel
)
4216 endAllListsAndQuotes(buf
, i
, nestedLists
, quoteLevel
, quoteMacroLevel
);
4217 i
+= replaceMarkdownEmphasis(buf
, loc
, inlineDelimiters
);
4219 // if we don't already know about this paragraph break then
4220 // insert a blank line and record the paragraph break
4221 if (iParagraphStart
<= i
)
4223 iPrecedingBlankLine
= i
;
4224 i
= buf
.insert(i
, "$(DDOC_BLANKLINE)");
4225 iParagraphStart
= i
+ 1;
4230 i
+ 1 < buf
.length
&&
4232 quoteLevel
) // if "\n\n" in quoted code
4235 i
= buf
.insert(i
, ")");
4236 i
+= endAllMarkdownQuotes(buf
, i
, quoteLevel
);
4237 quoteMacroLevel
= 0;
4239 leadingBlank
= true;
4241 tableRowDetected
= false;
4243 loc
.linnum
= loc
.linnum
+ incrementLoc
;
4245 // update the paragraph start if we just entered a macro
4246 if (previousMacroLevel
< macroLevel
&& iParagraphStart
< iLineStart
)
4247 iParagraphStart
= iLineStart
;
4248 previousMacroLevel
= macroLevel
;
4253 leadingBlank
= false;
4256 const slice
= buf
[];
4258 const se
= sc
._module
.escapetable
.escapeChar('<');
4262 // Skip over comments
4263 if (p
[1] == '!' && p
[2] == '-' && p
[3] == '-')
4269 if (j
== slice
.length
)
4271 if (p
[0] == '-' && p
[1] == '-' && p
[2] == '>')
4273 i
= j
+ 2; // place on closing '>'
4281 // Skip over HTML tag
4282 if (isalpha(p
[1]) ||
(p
[1] == '/' && isalpha(p
[2])))
4288 if (j
== slice
.length
)
4292 i
= j
; // place on closing '>'
4302 // Replace '<' with '<' character entity
4306 i
= buf
.insert(i
, se
);
4307 i
--; // point to ';'
4314 if (leadingBlank
&& (!inCode || quoteLevel
))
4317 int lineQuoteLevel
= 1;
4318 size_t iAfterDelimiters
= i
+ 1;
4319 for (; iAfterDelimiters
< buf
.length
; ++iAfterDelimiters
)
4321 const c0
= buf
[iAfterDelimiters
];
4324 else if (c0
!= ' ' && c0
!= '\t')
4327 if (!quoteMacroLevel
)
4328 quoteMacroLevel
= macroLevel
;
4329 buf
.remove(i
, iAfterDelimiters
- i
);
4331 if (quoteLevel
< lineQuoteLevel
)
4333 i
+= endRowAndTable(buf
, iLineStart
, i
, loc
, inlineDelimiters
, columnAlignments
);
4334 if (nestedLists
.length
)
4336 const indent
= getMarkdownIndent(buf
, iLineStart
, i
);
4337 if (indent
< nestedLists
[$-1].contentIndent
)
4338 i
+= MarkdownList
.endAllNestedLists(buf
, i
, nestedLists
);
4341 for (; quoteLevel
< lineQuoteLevel
; ++quoteLevel
)
4343 i
= buf
.insert(i
, "$(BLOCKQUOTE\n");
4344 iLineStart
= iParagraphStart
= i
;
4351 if (nestedLists
.length
)
4352 MarkdownList
.handleSiblingOrEndingList(buf
, i
, iParagraphStart
, nestedLists
);
4357 leadingBlank
= false;
4360 // Replace '>' with '>' character entity
4361 const se
= sc
._module
.escapetable
.escapeChar('>');
4365 i
= buf
.insert(i
, se
);
4366 i
--; // point to ';'
4373 leadingBlank
= false;
4376 char* p
= cast(char*)&buf
[].ptr
[i
];
4377 if (p
[1] == '#' ||
isalpha(p
[1]))
4379 // already a character entity
4380 // Replace '&' with '&' character entity
4381 const se
= sc
._module
.escapetable
.escapeChar('&');
4385 i
= buf
.insert(i
, se
);
4386 i
--; // point to ';'
4393 const iAfterDelimiter
= skipChars(buf
, i
, "`");
4394 const count
= iAfterDelimiter
- i
;
4396 if (inBacktick
== count
)
4401 codebuf
.write(buf
[iCodeStart
+ count
.. i
]);
4402 // escape the contents, but do not perform highlighting except for DDOC_PSYMBOL
4403 highlightCode(sc
, a
, codebuf
, 0);
4404 escapeStrayParenthesis(loc
, codebuf
, 0, false, sc
.eSink
);
4405 buf
.remove(iCodeStart
, i
- iCodeStart
+ count
); // also trimming off the current `
4406 immutable pre
= "$(DDOC_BACKQUOTED ";
4407 i
= buf
.insert(iCodeStart
, pre
);
4408 i
= buf
.insert(i
, codebuf
[]);
4409 i
= buf
.insert(i
, ")");
4410 i
--; // point to the ending ) so when the for loop does i++, it will see the next character
4414 // Perhaps we're starting or ending a Markdown code block
4415 if (leadingBlank
&& count
>= 3)
4417 bool moreBackticks
= false;
4418 for (size_t j
= iAfterDelimiter
; !moreBackticks
&& j
< buf
.length
; ++j
)
4420 moreBackticks
= true;
4421 else if (buf
[j
] == '\r' || buf
[j
] == '\n')
4430 i
= iAfterDelimiter
- 1;
4434 inBacktick
= cast(int) count
;
4435 codeIndent
= 0; // inline code is not indented
4436 // All we do here is set the code flags and record
4437 // the location. The macro will be inserted lazily
4438 // so we can easily cancel the inBacktick if we come
4439 // across a newline character.
4441 i
= iAfterDelimiter
- 1;
4447 /* A line beginning with # indicates an ATX-style heading. */
4448 if (leadingBlank
&& !inCode
)
4450 leadingBlank
= false;
4452 headingLevel
= detectAtxHeadingLevel(buf
, i
);
4456 i
+= endRowAndTable(buf
, iLineStart
, i
, loc
, inlineDelimiters
, columnAlignments
);
4457 if (!lineQuoted
&& quoteLevel
)
4458 i
+= endAllListsAndQuotes(buf
, iLineStart
, nestedLists
, quoteLevel
, quoteMacroLevel
);
4460 // remove the ### prefix, including whitespace
4461 i
= skipChars(buf
, i
+ headingLevel
, " \t");
4462 buf
.remove(iLineStart
, i
- iLineStart
);
4463 i
= iParagraphStart
= iLineStart
;
4465 removeAnyAtxHeadingSuffix(buf
, i
);
4468 headingMacroLevel
= macroLevel
;
4477 // Perhaps we're starting or ending a Markdown code block
4478 const iAfterDelimiter
= skipChars(buf
, i
, "~");
4479 if (iAfterDelimiter
- i
>= 3)
4482 leadingBlank
= false;
4487 /* A line beginning with --- delimits a code section.
4488 * inCode tells us if it is start or end of a code section.
4492 if (!inCode
&& c
== '-')
4494 const list
= MarkdownList
.parseItem(buf
, iLineStart
, i
);
4497 if (replaceMarkdownThematicBreak(buf
, i
, iLineStart
, loc
))
4499 removeBlankLineMacro(buf
, iPrecedingBlankLine
, i
);
4500 iParagraphStart
= skipChars(buf
, i
+1, " \t\r\n");
4510 leadingBlank
= false;
4511 const c0
= c
; // if we jumped here from case '`' or case '~'
4512 size_t iInfoString
= 0;
4514 codeLanguage
.length
= 0;
4518 if (i
>= buf
.length
)
4529 if (i
+ 1 >= buf
.length
)
4531 if (buf
[i
+ 1] == '\n')
4537 // BUG: handle UTF PS and LS too
4538 if (c
!= c0 || iInfoString
)
4540 if (!iInfoString
&& !inCode
&& i
- istart
>= 3)
4542 // Start a Markdown info string, like ```ruby
4543 codeFenceLength
= i
- istart
;
4544 i
= iInfoString
= skipChars(buf
, i
, " \t");
4546 else if (iInfoString
&& c
!= '`')
4548 if (!codeLanguage
.length
&& (c
== ' ' || c
== '\t'))
4549 codeLanguage
= cast(string
) buf
[iInfoString
..i
].idup
;
4558 if (i
- istart
< 3 ||
(inCode
&& (inCode
!= c0 ||
(inCode
!= '-' && i
- istart
< codeFenceLength
))))
4562 if (!codeLanguage
.length
)
4563 codeLanguage
= cast(string
) buf
[iInfoString
..i
].idup
;
4566 codeFenceLength
= i
- istart
;
4568 // We have the start/end of a code section
4569 // Remove the entire --- line, including blanks and \n
4570 buf
.remove(iLineStart
, i
- iLineStart
+ eollen
);
4573 leadingBlank
= true;
4574 if (inCode
&& (i
<= iCodeStart
))
4576 // Empty code section, just remove it completely.
4583 // The code section is from iCodeStart to i
4585 codebuf
.write(buf
[iCodeStart
.. i
]);
4586 codebuf
.writeByte(0);
4587 // Remove leading indentations from all lines
4588 bool lineStart
= true;
4589 char* endp
= cast(char*)codebuf
[].ptr
+ codebuf
.length
;
4590 for (char* p
= cast(char*)codebuf
[].ptr
; p
< endp
;)
4594 size_t j
= codeIndent
;
4596 while (j
-- > 0 && q
< endp
&& isIndentWS(q
))
4598 codebuf
.remove(p
- cast(char*)codebuf
[].ptr
, q
- p
);
4599 assert(cast(char*)codebuf
[].ptr
<= p
);
4600 assert(p
< cast(char*)codebuf
[].ptr
+ codebuf
.length
);
4602 endp
= cast(char*)codebuf
[].ptr
+ codebuf
.length
; // update
4609 if (!codeLanguage
.length || codeLanguage
== "dlang" || codeLanguage
== "d")
4610 highlightCode2(sc
, a
, codebuf
, 0);
4612 codebuf
.remove(codebuf
.length
-1, 1); // remove the trailing 0 byte
4613 escapeStrayParenthesis(loc
, codebuf
, 0, false, sc
.eSink
);
4614 buf
.remove(iCodeStart
, i
- iCodeStart
);
4615 i
= buf
.insert(iCodeStart
, codebuf
[]);
4616 i
= buf
.insert(i
, ")\n");
4617 i
-= 2; // in next loop, c should be '\n'
4621 i
+= endRowAndTable(buf
, iLineStart
, i
, loc
, inlineDelimiters
, columnAlignments
);
4622 if (!lineQuoted
&& quoteLevel
)
4624 const delta
= endAllListsAndQuotes(buf
, iLineStart
, nestedLists
, quoteLevel
, quoteMacroLevel
);
4630 codeIndent
= istart
- iLineStart
; // save indent count
4631 if (codeLanguage
.length
&& codeLanguage
!= "dlang" && codeLanguage
!= "d")
4634 for (size_t j
; j
< codeLanguage
.length
- 1; ++j
)
4635 if (codeLanguage
[j
] == '\\' && ispunct(codeLanguage
[j
+ 1]))
4636 codeLanguage
= codeLanguage
[0..j
] ~ codeLanguage
[j
+ 1..$];
4638 i
= buf
.insert(i
, "$(OTHER_CODE ");
4639 i
= buf
.insert(i
, codeLanguage
);
4640 i
= buf
.insert(i
, ",");
4643 i
= buf
.insert(i
, "$(D_CODE ");
4645 i
--; // place i on >
4646 leadingBlank
= true;
4653 if (leadingBlank
&& !inCode
&& replaceMarkdownThematicBreak(buf
, i
, iLineStart
, loc
))
4655 i
+= endRowAndTable(buf
, iLineStart
, i
, loc
, inlineDelimiters
, columnAlignments
);
4656 if (!lineQuoted
&& quoteLevel
)
4657 i
+= endAllListsAndQuotes(buf
, iLineStart
, nestedLists
, quoteLevel
, quoteMacroLevel
);
4658 removeBlankLineMacro(buf
, iPrecedingBlankLine
, i
);
4659 iParagraphStart
= skipChars(buf
, i
+1, " \t\r\n");
4670 if (leadingBlank
&& !inCode
)
4672 MarkdownList list
= MarkdownList
.parseItem(buf
, iLineStart
, i
);
4675 // Avoid starting a numbered list in the middle of a paragraph
4676 if (!nestedLists
.length
&& list
.orderedStart
.length
&&
4677 iParagraphStart
< iLineStart
)
4679 i
+= list
.orderedStart
.length
- 1;
4683 i
+= endRowAndTable(buf
, iLineStart
, i
, loc
, inlineDelimiters
, columnAlignments
);
4684 if (!lineQuoted
&& quoteLevel
)
4686 const delta
= endAllListsAndQuotes(buf
, iLineStart
, nestedLists
, quoteLevel
, quoteMacroLevel
);
4688 list
.iStart
+= delta
;
4689 list
.iContentStart
+= delta
;
4692 list
.macroLevel
= macroLevel
;
4693 list
.startItem(buf
, iLineStart
, i
, iPrecedingBlankLine
, nestedLists
, loc
);
4697 leadingBlank
= false;
4703 if (inCode || inBacktick
)
4705 leadingBlank
= false;
4711 // Check for a thematic break
4712 if (replaceMarkdownThematicBreak(buf
, i
, iLineStart
, loc
))
4714 i
+= endRowAndTable(buf
, iLineStart
, i
, loc
, inlineDelimiters
, columnAlignments
);
4715 if (!lineQuoted
&& quoteLevel
)
4716 i
+= endAllListsAndQuotes(buf
, iLineStart
, nestedLists
, quoteLevel
, quoteMacroLevel
);
4717 removeBlankLineMacro(buf
, iPrecedingBlankLine
, i
);
4718 iParagraphStart
= skipChars(buf
, i
+1, " \t\r\n");
4722 // An initial * indicates a Markdown list item
4723 const list
= MarkdownList
.parseItem(buf
, iLineStart
, i
);
4728 // Markdown emphasis
4729 const leftC
= i
> offset ? buf
[i
-1] : '\0';
4730 size_t iAfterEmphasis
= skipChars(buf
, i
+1, "*");
4731 const rightC
= iAfterEmphasis
< buf
.length ? buf
[iAfterEmphasis
] : '\0';
4732 int count
= cast(int) (iAfterEmphasis
- i
);
4733 const leftFlanking
= (rightC
!= '\0' && !isspace(rightC
)) && (!ispunct(rightC
) || leftC
== '\0' ||
isspace(leftC
) ||
ispunct(leftC
));
4734 const rightFlanking
= (leftC
!= '\0' && !isspace(leftC
)) && (!ispunct(leftC
) || rightC
== '\0' ||
isspace(rightC
) ||
ispunct(rightC
));
4735 auto emphasis
= MarkdownDelimiter(i
, count
, macroLevel
, leftFlanking
, rightFlanking
, false, c
);
4737 if (!emphasis
.leftFlanking
&& !emphasis
.rightFlanking
)
4739 i
= iAfterEmphasis
- 1;
4743 inlineDelimiters
~= emphasis
;
4744 i
+= emphasis
.count
;
4751 leadingBlank
= false;
4756 if (i
< buf
.length
-1 && buf
[i
+1] == '[')
4758 const imageStart
= MarkdownDelimiter(i
, 2, macroLevel
, false, false, false, c
);
4759 inlineDelimiters
~= imageStart
;
4768 leadingBlank
= false;
4772 const leftC
= i
> offset ? buf
[i
-1] : '\0';
4773 const rightFlanking
= leftC
!= '\0' && !isspace(leftC
) && !ispunct(leftC
);
4774 const atParagraphStart
= leadingBlank
&& iParagraphStart
>= iLineStart
;
4775 const linkStart
= MarkdownDelimiter(i
, 1, macroLevel
, false, rightFlanking
, atParagraphStart
, c
);
4776 inlineDelimiters
~= linkStart
;
4777 leadingBlank
= false;
4782 leadingBlank
= false;
4787 for (int d
= cast(int) inlineDelimiters
.length
- 1; d
>= 0; --d
)
4789 const delimiter
= inlineDelimiters
[d
];
4790 if (delimiter
.type
== '[' || delimiter
.type
== '!')
4792 if (delimiter
.isValid
&&
4793 MarkdownLink
.replaceLink(buf
, i
, loc
, inlineDelimiters
, d
, linkReferences
))
4795 // if we removed a reference link then we're at line start
4796 if (i
<= delimiter
.iStart
)
4797 leadingBlank
= true;
4800 if (delimiter
.type
== '[')
4801 for (--d
; d
>= 0; --d
)
4802 if (inlineDelimiters
[d
].type
== '[')
4803 inlineDelimiters
[d
].invalidate();
4807 // nothing found, so kill the delimiter
4808 inlineDelimiters
= inlineDelimiters
[0..d
] ~ inlineDelimiters
[d
+1..$];
4820 leadingBlank
= false;
4824 tableRowDetected
= true;
4825 inlineDelimiters
~= MarkdownDelimiter(i
, 1, macroLevel
, leadingBlank
, false, false, c
);
4826 leadingBlank
= false;
4832 leadingBlank
= false;
4833 if (inCode || i
+1 >= buf
.length
)
4836 /* Escape Markdown special characters */
4842 auto se
= sc
._module
.escapetable
.escapeChar(c1
);
4844 se
= c1
== '$' ?
"$(DOLLAR)" : c1
== ',' ?
"$(COMMA)" : null;
4848 i
= buf
.insert(i
, se
);
4849 i
--; // point to escaped char
4857 /* Look for the start of a macro, '$(Identifier'
4859 leadingBlank
= false;
4860 if (inCode || inBacktick
)
4862 const slice
= buf
[];
4864 if (p
[1] == '(' && isIdStart(&p
[2]))
4871 if (!inCode
&& i
> offset
&& buf
[i
-1] != '$')
4879 leadingBlank
= false;
4880 if (inCode || inBacktick
)
4884 else if (macroLevel
)
4886 int downToLevel
= cast(int) inlineDelimiters
.length
;
4887 while (downToLevel
> 0 && inlineDelimiters
[downToLevel
- 1].macroLevel
>= macroLevel
)
4889 if (headingLevel
&& headingMacroLevel
>= macroLevel
)
4891 endMarkdownHeading(buf
, iParagraphStart
, i
, loc
, headingLevel
);
4892 removeBlankLineMacro(buf
, iPrecedingBlankLine
, i
);
4894 i
+= endRowAndTable(buf
, iLineStart
, i
, loc
, inlineDelimiters
, columnAlignments
);
4895 while (nestedLists
.length
&& nestedLists
[$-1].macroLevel
>= macroLevel
)
4897 i
= buf
.insert(i
, ")\n)");
4898 --nestedLists
.length
;
4900 if (quoteLevel
&& quoteMacroLevel
>= macroLevel
)
4901 i
+= endAllMarkdownQuotes(buf
, i
, quoteLevel
);
4902 i
+= replaceMarkdownEmphasis(buf
, loc
, inlineDelimiters
, downToLevel
);
4905 quoteMacroLevel
= 0;
4911 leadingBlank
= false;
4912 if (sc
._module
.filetype
== FileType
.ddoc || inCode
)
4914 const start
= cast(char*)buf
[].ptr
+ i
;
4915 if (isIdStart(start
))
4917 size_t j
= skippastident(buf
, i
);
4920 size_t k
= skippastURL(buf
, i
);
4923 /* The URL is buf[i..k]
4926 /* Leave alone if already in a macro
4931 /* Replace URL with '$(DDOC_LINK_AUTODETECT URL)'
4933 i
= buf
.bracket(i
, "$(DDOC_LINK_AUTODETECT ", k
, ")") - 1;
4941 // leading '_' means no highlight unless it's a reserved symbol name
4942 if (c
== '_' && (i
== 0 ||
!isdigit(*(start
- 1))) && (i
== buf
.length
- 1 ||
!isReservedName(start
[0 .. len
])))
4945 i
= buf
.bracket(i
, "$(DDOC_AUTO_PSYMBOL_SUPPRESS ", j
- 1, ")") - 1;
4948 if (isIdentifier(a
, start
[0 .. len
]))
4950 i
= buf
.bracket(i
, "$(DDOC_AUTO_PSYMBOL ", j
, ")") - 1;
4953 if (isKeyword(start
[0 .. len
]))
4955 i
= buf
.bracket(i
, "$(DDOC_AUTO_KEYWORD ", j
, ")") - 1;
4958 if (isFunctionParameter(a
, start
[0 .. len
]))
4960 //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j);
4961 i
= buf
.bracket(i
, "$(DDOC_AUTO_PARAM ", j
, ")") - 1;
4971 sc
.eSink
.error(loc
, "unmatched `---` in DDoc comment");
4973 buf
.insert(buf
.length
, ")");
4975 size_t i
= buf
.length
;
4978 endMarkdownHeading(buf
, iParagraphStart
, i
, loc
, headingLevel
);
4979 removeBlankLineMacro(buf
, iPrecedingBlankLine
, i
);
4981 i
+= endRowAndTable(buf
, iLineStart
, i
, loc
, inlineDelimiters
, columnAlignments
);
4982 i
+= replaceMarkdownEmphasis(buf
, loc
, inlineDelimiters
);
4983 endAllListsAndQuotes(buf
, i
, nestedLists
, quoteLevel
, quoteMacroLevel
);
4986 /**************************************************
4987 * Highlight code for DDOC section.
4989 void highlightCode(Scope
* sc
, Dsymbol s
, ref OutBuffer buf
, size_t offset
)
4991 auto imp
= s
.isImport();
4992 if (imp
&& imp
.aliases
.length
> 0)
4994 // For example: `public import core.stdc.string : memcpy, memcmp;`
4995 for(int i
= 0; i
< imp
.aliases
.length
; i
++)
4997 // Need to distinguish between
4998 // `public import core.stdc.string : memcpy, memcmp;` and
4999 // `public import core.stdc.string : copy = memcpy, compare = memcmp;`
5000 auto a
= imp
.aliases
[i
];
5001 auto id
= a ? a
: imp
.names
[i
];
5002 auto loc
= Loc
.init
;
5004 if (auto symFromId
= sc
.search(loc
, id
, pscopesym
))
5006 highlightCode(sc
, symFromId
, buf
, offset
);
5013 emitAnchor(ancbuf
, s
, sc
);
5014 buf
.insert(offset
, ancbuf
[]);
5015 offset
+= ancbuf
.length
;
5019 highlightCode(sc
, &a
, buf
, offset
);
5023 /****************************************************
5025 void highlightCode(Scope
* sc
, Dsymbols
* a
, ref OutBuffer buf
, size_t offset
)
5027 //printf("highlightCode(a = '%s')\n", a.toChars());
5028 bool resolvedTemplateParameters
= false;
5030 for (size_t i
= offset
; i
< buf
.length
; i
++)
5033 const se
= sc
._module
.escapetable
.escapeChar(c
);
5037 i
= buf
.insert(i
, se
);
5038 i
--; // point to ';'
5041 char* start
= cast(char*)buf
[].ptr
+ i
;
5042 if (isIdStart(start
))
5044 size_t j
= skipPastIdentWithDots(buf
, i
);
5048 if (isIdentifier(a
, start
[0 .. len
]))
5050 i
= buf
.bracket(i
, "$(DDOC_PSYMBOL ", j
, ")") - 1;
5055 j
= skippastident(buf
, i
);
5059 if (isIdentifier(a
, start
[0 .. len
]))
5061 i
= buf
.bracket(i
, "$(DDOC_PSYMBOL ", j
, ")") - 1;
5064 if (isFunctionParameter(a
, start
[0 .. len
]))
5066 //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j);
5067 i
= buf
.bracket(i
, "$(DDOC_PARAM ", j
, ")") - 1;
5073 else if (!resolvedTemplateParameters
)
5077 // hunt for template declarations:
5078 foreach (symi
; 0 .. a
.length
)
5080 FuncDeclaration fd
= (*a
)[symi
].isFuncDeclaration();
5082 if (!fd ||
!fd
.parent ||
!fd
.parent
.isTemplateDeclaration())
5087 TemplateDeclaration td
= fd
.parent
.isTemplateDeclaration();
5089 // build the template parameters
5090 Array
!(size_t
) paramLens
;
5091 paramLens
.reserve(td
.parameters
.length
);
5093 OutBuffer parametersBuf
;
5096 parametersBuf
.writeByte('(');
5098 foreach (parami
; 0 .. td
.parameters
.length
)
5100 TemplateParameter tp
= (*td
.parameters
)[parami
];
5103 parametersBuf
.writestring(", ");
5105 size_t lastOffset
= parametersBuf
.length
;
5107 toCBuffer(tp
, parametersBuf
, hgs
);
5109 paramLens
[parami
] = parametersBuf
.length
- lastOffset
;
5111 parametersBuf
.writeByte(')');
5113 const templateParams
= parametersBuf
[];
5115 //printf("templateDecl: %s\ntemplateParams: %s\nstart: %s\n", td.toChars(), templateParams, start);
5116 if (start
[0 .. templateParams
.length
] == templateParams
)
5118 immutable templateParamListMacro
= "$(DDOC_TEMPLATE_PARAM_LIST ";
5119 buf
.bracket(i
, templateParamListMacro
.ptr
, i
+ templateParams
.length
, ")");
5121 // We have the parameter list. While we're here we might
5122 // as well wrap the parameters themselves as well
5124 // + 1 here to take into account the opening paren of the
5125 // template param list
5126 i
+= templateParamListMacro
.length
+ 1;
5128 foreach (const len
; paramLens
)
5130 i
= buf
.bracket(i
, "$(DDOC_TEMPLATE_PARAM ", i
+ len
, ")");
5131 // increment two here for space + comma
5135 resolvedTemplateParameters
= true;
5136 // reset i to be positioned back before we found the template
5137 // param list this assures that anything within the template
5138 // param list that needs to be escaped or otherwise altered
5139 // has an opportunity for that to happen outside of this context
5149 /****************************************
5151 void highlightCode3(Scope
* sc
, ref OutBuffer buf
, const(char)* p
, const(char)* pend
)
5153 for (; p
< pend
; p
++)
5155 const se
= sc
._module
.escapetable
.escapeChar(*p
);
5157 buf
.writestring(se
);
5163 /**************************************************
5164 * Highlight code for CODE section.
5166 void highlightCode2(Scope
* sc
, Dsymbols
* a
, ref OutBuffer buf
, size_t offset
)
5168 scope eSinkNull
= new ErrorSinkNull();
5170 scope Lexer lex
= new Lexer(null, cast(char*)buf
[].ptr
, 0, buf
.length
- 1, 0, 1,
5171 eSinkNull
, // ignore errors
5172 &global
.compileEnv
);
5174 const(char)* lastp
= cast(char*)buf
[].ptr
;
5175 //printf("highlightCode2('%.*s')\n", cast(int)(buf.length - 1), buf[].ptr);
5176 res
.reserve(buf
.length
);
5181 highlightCode3(sc
, res
, lastp
, tok
.ptr
);
5182 string highlight
= null;
5185 case TOK
.identifier
:
5189 size_t len
= lex
.p
- tok
.ptr
;
5190 if (isIdentifier(a
, tok
.ptr
[0 .. len
]))
5192 highlight
= "$(D_PSYMBOL ";
5195 if (isFunctionParameter(a
, tok
.ptr
[0 .. len
]))
5197 //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j);
5198 highlight
= "$(D_PARAM ";
5204 highlight
= "$(D_COMMENT ";
5207 case TOK
.interpolated
:
5208 highlight
= "$(D_STRING ";
5211 if (tok
.isKeyword())
5212 highlight
= "$(D_KEYWORD ";
5217 res
.writestring(highlight
);
5218 size_t o
= res
.length
;
5219 highlightCode3(sc
, res
, tok
.ptr
, lex
.p
);
5220 if (tok
.value
== TOK
.comment || tok
.value
== TOK
.string_ || tok
.value
== TOK
.interpolated
)
5221 /* https://issues.dlang.org/show_bug.cgi?id=7656
5222 * https://issues.dlang.org/show_bug.cgi?id=7715
5223 * https://issues.dlang.org/show_bug.cgi?id=10519
5225 escapeDdocString(res
, o
);
5229 highlightCode3(sc
, res
, tok
.ptr
, lex
.p
);
5230 if (tok
.value
== TOK
.endOfFile
)
5234 buf
.setsize(offset
);
5238 /****************************************
5239 * Determine if p points to the start of a "..." parameter identifier.
5241 bool isCVariadicArg(const(char)[] p
) @nogc nothrow pure @safe
5243 return p
.length
>= 3 && p
[0 .. 3] == "...";
5246 /****************************************
5247 * Determine if p points to the start of an identifier.
5250 bool isIdStart(const(char)* p
) @nogc nothrow pure
5253 if (isalpha(c
) || c
== '_')
5258 if (utf_decodeChar(p
[0 .. 4], i
, c
))
5259 return false; // ignore errors
5266 /****************************************
5267 * Determine if p points to the rest of an identifier.
5270 bool isIdTail(const(char)* p
) @nogc nothrow pure
5273 if (isalnum(c
) || c
== '_')
5278 if (utf_decodeChar(p
[0 .. 4], i
, c
))
5279 return false; // ignore errors
5286 /****************************************
5287 * Determine if p points to the indentation space.
5289 bool isIndentWS(const(char)* p
) @nogc nothrow pure @safe
5291 return (*p
== ' ') ||
(*p
== '\t');
5294 /*****************************************
5295 * Return number of bytes in UTF character.
5297 int utfStride(const(char)* p
) @nogc nothrow pure
5303 utf_decodeChar(p
[0 .. 4], i
, c
); // ignore errors, but still consume input
5307 inout(char)* stripLeadingNewlines(inout(char)* s
) @nogc nothrow pure
5309 while (s
&& *s
== '\n' ||
*s
== '\r')