2 * Ddoc documentation generation.
4 * Specification: $(LINK2 https://dlang.org/spec/ddoc.html, Documentation Generator)
6 * Copyright: Copyright (C) 1999-2023 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
= strlen(cast(char*)m
.comment
);
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
];
751 if (auto symFromId
= sc
.search(loc
, id
, null))
753 emitAnchor(buf
, symFromId
, sc
, forHeader
);
759 // For example: `public import str = core.stdc.string;`
762 auto symbolName
= imp
.aliasId
.toString();
764 buf
.printf("$(%.*s %.*s", cast(int) macroName
.length
, macroName
.ptr
,
765 cast(int) symbolName
.length
, symbolName
.ptr
);
769 buf
.printf(", %.*s", cast(int) symbolName
.length
, symbolName
.ptr
);
774 // The general case: `public import core.stdc.string;`
776 // fully qualify imports so `core.stdc.string` doesn't appear as `core`
777 void printFullyQualifiedImport()
779 foreach (const pid
; imp
.packages
)
781 buf
.printf("%s.", pid
.toChars());
783 buf
.writestring(imp
.id
.toString());
786 buf
.printf("$(%.*s ", cast(int) macroName
.length
, macroName
.ptr
);
787 printFullyQualifiedImport();
792 printFullyQualifiedImport();
801 auto symbolName
= ident
.toString();
802 buf
.printf("$(%.*s %.*s", cast(int) macroName
.length
, macroName
.ptr
,
803 cast(int) symbolName
.length
, symbolName
.ptr
);
805 // only append count once there's a duplicate
807 buf
.printf(".%u", count
);
811 Identifier shortIdent
;
814 emitAnchorName(anc
, s
, skipNonQualScopes(sc
), false);
815 shortIdent
= Identifier
.idPool(anc
[]);
818 auto shortName
= shortIdent
.toString();
819 buf
.printf(", %.*s", cast(int) shortName
.length
, shortName
.ptr
);
826 /******************************* emitComment **********************************/
828 /** Get leading indentation from 'src' which represents lines of code. */
829 size_t
getCodeIndent(const(char)* src
)
831 while (src
&& (*src
== '\r' ||
*src
== '\n'))
832 ++src
; // skip until we find the first non-empty line
833 size_t codeIndent
= 0;
834 while (src
&& (*src
== ' ' ||
*src
== '\t'))
842 /** Recursively expand template mixin member docs into the scope. */
843 void expandTemplateMixinComments(TemplateMixin tm
, ref OutBuffer buf
, Scope
* sc
)
846 tm
.dsymbolSemantic(sc
);
847 TemplateDeclaration td
= (tm
&& tm
.tempdecl
) ? tm
.tempdecl
.isTemplateDeclaration() : null;
848 if (td
&& td
.members
)
850 for (size_t i
= 0; i
< td
.members
.length
; i
++)
852 Dsymbol sm
= (*td
.members
)[i
];
853 TemplateMixin tmc
= sm
.isTemplateMixin();
854 if (tmc
&& tmc
.comment
)
855 expandTemplateMixinComments(tmc
, buf
, sc
);
857 emitComment(sm
, buf
, sc
);
862 void emitMemberComments(ScopeDsymbol sds
, ref OutBuffer buf
, Scope
* sc
)
866 //printf("ScopeDsymbol::emitMemberComments() %s\n", toChars());
867 const(char)[] m
= "$(DDOC_MEMBERS ";
868 if (sds
.isTemplateDeclaration())
869 m
= "$(DDOC_TEMPLATE_MEMBERS ";
870 else if (sds
.isClassDeclaration())
871 m
= "$(DDOC_CLASS_MEMBERS ";
872 else if (sds
.isStructDeclaration())
873 m
= "$(DDOC_STRUCT_MEMBERS ";
874 else if (sds
.isEnumDeclaration())
875 m
= "$(DDOC_ENUM_MEMBERS ";
876 else if (sds
.isModule())
877 m
= "$(DDOC_MODULE_MEMBERS ";
878 size_t offset1
= buf
.length
; // save starting offset
880 size_t offset2
= buf
.length
; // to see if we write anything
882 for (size_t i
= 0; i
< sds
.members
.length
; i
++)
884 Dsymbol s
= (*sds
.members
)[i
];
885 //printf("\ts = '%s'\n", s.toChars());
886 // only expand if parent is a non-template (semantic won't work)
887 if (s
.comment
&& s
.isTemplateMixin() && s
.parent
&& !s
.parent
.isTemplateDeclaration())
888 expandTemplateMixinComments(cast(TemplateMixin
)s
, buf
, sc
);
889 emitComment(s
, buf
, sc
);
891 emitComment(null, buf
, sc
);
893 if (buf
.length
== offset2
)
895 /* Didn't write out any members, so back out last write
897 buf
.setsize(offset1
);
900 buf
.writestring(")");
903 void emitVisibility(ref OutBuffer buf
, Import i
)
905 // imports are private by default, which is different from other declarations
906 // so they should explicitly show their visibility
907 emitVisibility(buf
, i
.visibility
);
910 void emitVisibility(ref OutBuffer buf
, Declaration d
)
912 auto vis
= d
.visibility
;
913 if (vis
.kind
!= Visibility
.Kind
.undefined
&& vis
.kind
!= Visibility
.Kind
.public_
)
915 emitVisibility(buf
, vis
);
919 void emitVisibility(ref OutBuffer buf
, Visibility vis
)
921 visibilityToBuffer(buf
, vis
);
925 void emitComment(Dsymbol s
, ref OutBuffer buf
, Scope
* sc
)
927 extern (C
++) final class EmitComment
: Visitor
929 alias visit
= Visitor
.visit
;
934 extern (D
) this(ref OutBuffer buf
, Scope
* sc
) scope
940 override void visit(Dsymbol
)
944 override void visit(InvariantDeclaration
)
948 override void visit(UnitTestDeclaration
)
952 override void visit(PostBlitDeclaration
)
956 override void visit(DtorDeclaration
)
960 override void visit(StaticCtorDeclaration
)
964 override void visit(StaticDtorDeclaration
)
968 override void visit(TypeInfoDeclaration
)
972 void emit(Scope
* sc
, Dsymbol s
, const(char)* com
)
974 if (s
&& sc
.lastdc
&& isDitto(com
))
979 // Put previous doc comment if exists
980 if (DocComment
* dc
= sc
.lastdc
)
982 assert(dc
.a
.length
> 0, "Expects at least one declaration for a" ~
983 "documentation comment");
985 auto symbol
= dc
.a
[0];
987 buf
.writestring("$(DDOC_MEMBER");
988 buf
.writestring("$(DDOC_MEMBER_HEADER");
989 emitAnchor(*buf
, symbol
, sc
, true);
992 // Put the declaration signatures as the document 'title'
993 buf
.writestring(ddoc_decl_s
);
994 for (size_t i
= 0; i
< dc
.a
.length
; i
++)
996 Dsymbol sx
= dc
.a
[i
];
997 // the added linebreaks in here make looking at multiple
998 // signatures more appealing
1001 size_t o
= buf
.length
;
1002 toDocBuffer(sx
, *buf
, sc
);
1003 highlightCode(sc
, sx
, *buf
, o
);
1004 buf
.writestring("$(DDOC_OVERLOAD_SEPARATOR)");
1007 buf
.writestring("$(DDOC_DITTO ");
1009 size_t o
= buf
.length
;
1010 toDocBuffer(sx
, *buf
, sc
);
1011 highlightCode(sc
, sx
, *buf
, o
);
1013 buf
.writestring("$(DDOC_OVERLOAD_SEPARATOR)");
1016 buf
.writestring(ddoc_decl_e
);
1017 // Put the ddoc comment as the document 'description'
1018 buf
.writestring(ddoc_decl_dd_s
);
1020 dc
.writeSections(sc
, &dc
.a
, *buf
);
1021 if (ScopeDsymbol sds
= dc
.a
[0].isScopeDsymbol())
1022 emitMemberComments(sds
, *buf
, sc
);
1024 buf
.writestring(ddoc_decl_dd_e
);
1026 //printf("buf.2 = [[%.*s]]\n", cast(int)(buf.length - o0), buf.data + o0);
1030 DocComment
* dc
= DocComment
.parse(s
, com
);
1031 dc
.pmacrotable
= &sc
._module
.macrotable
;
1036 override void visit(Import imp
)
1038 if (imp
.visible().kind
!= Visibility
.Kind
.public_
&& sc
.visibility
.kind
!= Visibility
.Kind
.export_
)
1042 emit(sc
, imp
, imp
.comment
);
1045 override void visit(Declaration d
)
1047 //printf("Declaration::emitComment(%p '%s'), comment = '%s'\n", d, d.toChars(), d.comment);
1048 //printf("type = %p\n", d.type);
1049 const(char)* com
= d
.comment
;
1050 if (TemplateDeclaration td
= getEponymousParent(d
))
1052 if (isDitto(td
.comment
))
1055 com
= Lexer
.combineComments(td
.comment
.toDString(), com
.toDString(), true);
1063 if (!d
.isCtorDeclaration() &&
1064 !d
.isAliasDeclaration() &&
1065 !d
.isVarDeclaration())
1070 if (d
.visibility
.kind
== Visibility
.Kind
.private_ || sc
.visibility
.kind
== Visibility
.Kind
.private_
)
1078 override void visit(AggregateDeclaration ad
)
1080 //printf("AggregateDeclaration::emitComment() '%s'\n", ad.toChars());
1081 const(char)* com
= ad
.comment
;
1082 if (TemplateDeclaration td
= getEponymousParent(ad
))
1084 if (isDitto(td
.comment
))
1087 com
= Lexer
.combineComments(td
.comment
.toDString(), com
.toDString(), true);
1091 if (ad
.visible().kind
== Visibility
.Kind
.private_ || sc
.visibility
.kind
== Visibility
.Kind
.private_
)
1101 override void visit(TemplateDeclaration td
)
1103 //printf("TemplateDeclaration::emitComment() '%s', kind = %s\n", td.toChars(), td.kind());
1104 if (td
.visible().kind
== Visibility
.Kind
.private_ || sc
.visibility
.kind
== Visibility
.Kind
.private_
)
1108 if (Dsymbol ss
= getEponymousMember(td
))
1113 emit(sc
, td
, td
.comment
);
1116 override void visit(EnumDeclaration ed
)
1118 if (ed
.visible().kind
== Visibility
.Kind
.private_ || sc
.visibility
.kind
== Visibility
.Kind
.private_
)
1120 if (ed
.isAnonymous() && ed
.members
)
1122 for (size_t i
= 0; i
< ed
.members
.length
; i
++)
1124 Dsymbol s
= (*ed
.members
)[i
];
1125 emitComment(s
, *buf
, sc
);
1131 if (ed
.isAnonymous())
1133 emit(sc
, ed
, ed
.comment
);
1136 override void visit(EnumMember em
)
1138 //printf("EnumMember::emitComment(%p '%s'), comment = '%s'\n", em, em.toChars(), em.comment);
1139 if (em
.visible().kind
== Visibility
.Kind
.private_ || sc
.visibility
.kind
== Visibility
.Kind
.private_
)
1143 emit(sc
, em
, em
.comment
);
1146 override void visit(AttribDeclaration ad
)
1148 //printf("AttribDeclaration::emitComment(sc = %p)\n", sc);
1149 /* A general problem with this,
1150 * illustrated by https://issues.dlang.org/show_bug.cgi?id=2516
1151 * is that attributes are not transmitted through to the underlying
1152 * member declarations for template bodies, because semantic analysis
1153 * is not done for template declaration bodies
1154 * (only template instantiations).
1155 * Hence, Ddoc omits attributes from template members.
1157 Dsymbols
* d
= ad
.include(null);
1160 for (size_t i
= 0; i
< d
.length
; i
++)
1162 Dsymbol s
= (*d
)[i
];
1163 //printf("AttribDeclaration::emitComment %s\n", s.toChars());
1164 emitComment(s
, *buf
, sc
);
1169 override void visit(VisibilityDeclaration pd
)
1175 sc
.visibility
= pd
.visibility
;
1176 visit(cast(AttribDeclaration
)pd
);
1177 scx
.lastdc
= sc
.lastdc
;
1182 override void visit(ConditionalDeclaration cd
)
1184 //printf("ConditionalDeclaration::emitComment(sc = %p)\n", sc);
1185 if (cd
.condition
.inc != Include
.notComputed
)
1187 visit(cast(AttribDeclaration
)cd
);
1190 /* If generating doc comment, be careful because if we're inside
1191 * a template, then include(null) will fail.
1193 Dsymbols
* d
= cd
.decl ? cd
.decl
: cd
.elsedecl
;
1194 for (size_t i
= 0; i
< d
.length
; i
++)
1196 Dsymbol s
= (*d
)[i
];
1197 emitComment(s
, *buf
, sc
);
1202 scope EmitComment v
= new EmitComment(buf
, sc
);
1204 v
.emit(sc
, null, null);
1209 void toDocBuffer(Dsymbol s
, ref OutBuffer buf
, Scope
* sc
)
1211 extern (C
++) final class ToDocBuffer
: Visitor
1213 alias visit
= Visitor
.visit
;
1218 extern (D
) this(ref OutBuffer buf
, Scope
* sc
) scope
1224 override void visit(Dsymbol s
)
1226 //printf("Dsymbol::toDocbuffer() %s\n", s.toChars());
1229 toCBuffer(s
, *buf
, hgs
);
1232 void prefix(Dsymbol s
)
1234 if (s
.isDeprecated())
1235 buf
.writestring("deprecated ");
1236 if (Declaration d
= s
.isDeclaration())
1238 emitVisibility(*buf
, d
);
1240 buf
.writestring("static ");
1241 else if (d
.isFinal())
1242 buf
.writestring("final ");
1243 else if (d
.isAbstract())
1244 buf
.writestring("abstract ");
1246 if (d
.isFuncDeclaration()) // functionToBufferFull handles this
1249 if (d
.isImmutable())
1250 buf
.writestring("immutable ");
1251 if (d
.storage_class
& STC
.shared_
)
1252 buf
.writestring("shared ");
1254 buf
.writestring("inout ");
1256 buf
.writestring("const ");
1258 if (d
.isSynchronized())
1259 buf
.writestring("synchronized ");
1261 if (d
.storage_class
& STC
.manifest
)
1262 buf
.writestring("enum ");
1264 // Add "auto" for the untyped variable in template members
1265 if (!d
.type
&& d
.isVarDeclaration() &&
1266 !d
.isImmutable() && !(d
.storage_class
& STC
.shared_
) && !d
.isWild() && !d
.isConst() &&
1267 !d
.isSynchronized())
1269 buf
.writestring("auto ");
1274 override void visit(Import i
)
1278 emitVisibility(*buf
, i
);
1279 toCBuffer(i
, *buf
, hgs
);
1282 override void visit(Declaration d
)
1286 TemplateDeclaration td
= getEponymousParent(d
);
1287 //printf("Declaration::toDocbuffer() %s, originalType = %s, td = %s\n", d.toChars(), d.originalType ? d.originalType.toChars() : "--", td ? td.toChars() : "--");
1290 if (d
.isDeprecated())
1291 buf
.writestring("$(DEPRECATED ");
1295 Type origType
= d
.originalType ? d
.originalType
: d
.type
;
1296 if (origType
.ty
== Tfunction
)
1298 functionToBufferFull(cast(TypeFunction
)origType
, *buf
, d
.ident
, &hgs
, td
);
1301 toCBuffer(origType
, *buf
, d
.ident
, hgs
);
1304 buf
.writestring(d
.ident
.toString());
1305 if (d
.isVarDeclaration() && td
)
1308 if (td
.origParameters
&& td
.origParameters
.length
)
1310 for (size_t i
= 0; i
< td
.origParameters
.length
; i
++)
1313 buf
.writestring(", ");
1314 toCBuffer((*td
.origParameters
)[i
], *buf
, hgs
);
1319 // emit constraints if declaration is a templated declaration
1320 if (td
&& td
.constraint
)
1322 bool noFuncDecl
= td
.isFuncDeclaration() is null;
1325 buf
.writestring("$(DDOC_CONSTRAINT ");
1328 toCBuffer(td
.constraint
, *buf
, hgs
);
1332 buf
.writestring(")");
1335 if (d
.isDeprecated())
1336 buf
.writestring(")");
1337 buf
.writestring(";\n");
1340 override void visit(AliasDeclaration ad
)
1342 //printf("AliasDeclaration::toDocbuffer() %s\n", ad.toChars());
1345 if (ad
.isDeprecated())
1346 buf
.writestring("deprecated ");
1347 emitVisibility(*buf
, ad
);
1348 buf
.printf("alias %s = ", ad
.toChars());
1349 if (Dsymbol s
= ad
.aliassym
) // ident alias
1351 prettyPrintDsymbol(s
, ad
.parent
);
1353 else if (Type type
= ad
.getType()) // type alias
1355 if (type
.ty
== Tclass || type
.ty
== Tstruct || type
.ty
== Tenum
)
1357 if (Dsymbol s
= type
.toDsymbol(null)) // elaborate type
1358 prettyPrintDsymbol(s
, ad
.parent
);
1360 buf
.writestring(type
.toChars());
1365 buf
.writestring(type
.toChars());
1368 buf
.writestring(";\n");
1371 void parentToBuffer(Dsymbol s
)
1373 if (s
&& !s
.isPackage() && !s
.isModule())
1375 parentToBuffer(s
.parent
);
1376 buf
.writestring(s
.toChars());
1377 buf
.writestring(".");
1381 static bool inSameModule(Dsymbol s
, Dsymbol p
) @safe
1383 for (; s
; s
= s
.parent
)
1388 for (; p
; p
= p
.parent
)
1396 void prettyPrintDsymbol(Dsymbol s
, Dsymbol parent
)
1398 if (s
.parent
&& (s
.parent
== parent
)) // in current scope -> naked name
1400 buf
.writestring(s
.toChars());
1402 else if (!inSameModule(s
, parent
)) // in another module -> full name
1404 buf
.writestring(s
.toPrettyChars());
1406 else // nested in a type in this module -> full name w/o module name
1408 // if alias is nested in a user-type use module-scope lookup
1409 if (!parent
.isModule() && !parent
.isPackage())
1410 buf
.writestring(".");
1411 parentToBuffer(s
.parent
);
1412 buf
.writestring(s
.toChars());
1416 override void visit(AggregateDeclaration ad
)
1422 emitVisibility(buf
, ad
);
1424 buf
.printf("%s %s", ad
.kind(), ad
.toChars());
1425 buf
.writestring(";\n");
1428 override void visit(StructDeclaration sd
)
1430 //printf("StructDeclaration::toDocbuffer() %s\n", sd.toChars());
1435 emitVisibility(buf
, sd
);
1437 if (TemplateDeclaration td
= getEponymousParent(sd
))
1439 toDocBuffer(td
, *buf
, sc
);
1443 buf
.printf("%s %s", sd
.kind(), sd
.toChars());
1445 buf
.writestring(";\n");
1448 override void visit(ClassDeclaration cd
)
1450 //printf("ClassDeclaration::toDocbuffer() %s\n", cd.toChars());
1455 emitVisibility(*buf
, cd
);
1457 if (TemplateDeclaration td
= getEponymousParent(cd
))
1459 toDocBuffer(td
, *buf
, sc
);
1463 if (!cd
.isInterfaceDeclaration() && cd
.isAbstract())
1464 buf
.writestring("abstract ");
1465 buf
.printf("%s %s", cd
.kind(), cd
.toChars());
1468 for (size_t i
= 0; i
< cd
.baseclasses
.length
; i
++)
1470 BaseClass
* bc
= (*cd
.baseclasses
)[i
];
1471 if (bc
.sym
&& bc
.sym
.ident
== Id
.Object
)
1474 buf
.writestring(", ");
1477 buf
.writestring(": ");
1483 buf
.printf("$(DDOC_PSUPER_SYMBOL %s)", bc
.sym
.toPrettyChars());
1488 toCBuffer(bc
.type
, *buf
, null, hgs
);
1491 buf
.writestring(";\n");
1494 override void visit(EnumDeclaration ed
)
1498 buf
.printf("%s %s", ed
.kind(), ed
.toChars());
1501 buf
.writestring(": $(DDOC_ENUM_BASETYPE ");
1503 toCBuffer(ed
.memtype
, *buf
, null, hgs
);
1504 buf
.writestring(")");
1506 buf
.writestring(";\n");
1509 override void visit(EnumMember em
)
1513 buf
.writestring(em
.toChars());
1517 scope ToDocBuffer v
= new ToDocBuffer(buf
, sc
);
1521 /***********************************************************
1526 Sections sections
; // Section*[]
1530 MacroTable
* pmacrotable
;
1531 Escape
* escapetable
;
1534 static DocComment
* parse(Dsymbol s
, const(char)* comment
)
1536 //printf("parse(%s): '%s'\n", s.toChars(), comment);
1537 auto dc
= new DocComment();
1541 dc
.parseSections(comment
);
1542 for (size_t i
= 0; i
< dc
.sections
.length
; i
++)
1544 Section sec
= dc
.sections
[i
];
1545 if (iequals("copyright", sec
.name
))
1549 if (iequals("macros", sec
.name
))
1557 /************************************************
1558 * Parse macros out of Macros: section.
1559 * Macros are of the form:
1564 extern(D
) static void parseMacros(
1565 Escape
* escapetable
, ref MacroTable pmacrotable
, const(char)[] m
)
1567 const(char)* p
= m
.ptr
;
1568 size_t len
= m
.length
;
1569 const(char)* pend
= p
+ len
;
1570 const(char)* tempstart
= null;
1572 const(char)* namestart
= null;
1573 size_t namelen
= 0; // !=0 if line continuation
1574 const(char)* textstart
= null;
1578 // Skip to start of macro
1597 goto Ltext
; // continuation of prev macro
1611 templen
= p
- tempstart
;
1616 if (!(*p
== ' ' ||
*p
== '\t'))
1623 goto Ltext
; // continuation of prev macro
1631 // Output existing macro
1633 //printf("macro '%.*s' = '%.*s'\n", cast(int)namelen, namestart, cast(int)textlen, textstart);
1634 if (iequals("ESCAPES", namestart
[0 .. namelen
]))
1635 parseEscapes(escapetable
, textstart
[0 .. textlen
]);
1637 pmacrotable
.define(namestart
[0 .. namelen
], textstart
[0 .. textlen
]);
1642 namestart
= tempstart
;
1644 while (p
< pend
&& (*p
== ' ' ||
*p
== '\t'))
1648 while (p
< pend
&& *p
!= '\r' && *p
!= '\n')
1650 textlen
= p
- textstart
;
1652 //printf("p = %p, pend = %p\n", p, pend);
1657 while (p
< pend
&& *p
!= '\r' && *p
!= '\n')
1662 goto L1
; // write out last one
1665 /**************************************
1666 * Parse escapes of the form:
1668 * where c is a single character.
1669 * Multiple escapes can be separated
1670 * by whitespace and/or commas.
1672 static void parseEscapes(Escape
* escapetable
, const(char)[] text
)
1676 escapetable
= new Escape();
1677 memset(escapetable
, 0, Escape
.sizeof
);
1679 //printf("parseEscapes('%.*s') pescapetable = %p\n", cast(int)text.length, text.ptr, escapetable);
1680 const(char)* p
= text
.ptr
;
1681 const(char)* pend
= p
+ text
.length
;
1688 if (!(*p
== ' ' ||
*p
== '\t' ||
*p
== '\r' ||
*p
== '\n' ||
*p
== ','))
1692 if (p
[0] != '/' || p
[2] != '/')
1696 const(char)* start
= p
;
1705 size_t len
= p
- start
;
1706 char* s
= cast(char*)memcpy(mem
.xmalloc(len
+ 1), start
, len
);
1708 escapetable
.strings
[c
] = s
[0 .. len
];
1709 //printf("\t%c = '%s'\n", c, s);
1714 /*****************************************
1715 * Parse next paragraph out of *pcomment.
1716 * Update *pcomment to point past paragraph.
1717 * Returns NULL if no more paragraphs.
1718 * If paragraph ends in 'identifier:',
1719 * then (*pcomment)[0 .. idlen] is the identifier.
1721 void parseSections(const(char)* comment
)
1724 const(char)* pstart
;
1726 const(char)* idstart
= null; // dead-store to prevent spurious warning
1728 const(char)* name
= null;
1730 //printf("parseSections('%s')\n", comment);
1734 const(char)* pstart0
= p
;
1735 p
= skipwhitespace(p
);
1739 // Undo indent if starting with a list item
1740 if ((*p
== '-' ||
*p
== '+' ||
*p
== '*') && (*(p
+1) == ' ' ||
*(p
+1) == '\t'))
1744 const(char)* pitem
= p
;
1745 while (*pitem
>= '0' && *pitem
<= '9')
1747 if (pitem
> p
&& *pitem
== '.' && (*(pitem
+1) == ' ' ||
*(pitem
+1) == '\t'))
1751 /* Find end of section, which is ended by one of:
1752 * 'identifier:' (but not inside a code section)
1759 // Check for start/end of a code section
1760 if (*p
== '-' ||
*p
== '`' ||
*p
== '~')
1769 // BUG: handle UTF PS and LS too
1770 if ((!*p ||
*p
== '\r' ||
*p
== '\n' ||
(!inCode
&& c
!= '-')) && numdash
>= 3)
1772 inCode
= inCode
== c ?
false : c
;
1775 // restore leading indentation
1776 while (pstart0
< pstart
&& isIndentWS(pstart
- 1))
1782 if (!inCode
&& isIdStart(p
))
1784 const(char)* q
= p
+ utfStride(p
);
1788 // Detected tag ends it
1789 if (*q
== ':' && isupper(*p
)
1790 && (isspace(q
[1]) || q
[1] == 0))
1794 for (pend
= p
; pend
> pstart
; pend
--)
1796 if (pend
[-1] == '\n')
1810 if (*p
== '\n' && !summary
&& !namelen
&& !inCode
)
1821 p
= skipwhitespace(p
);
1824 if (namelen || pstart
< pend
)
1827 if (iequals("Params", name
[0 .. namelen
]))
1828 s
= new ParamSection();
1829 else if (iequals("Macros", name
[0 .. namelen
]))
1830 s
= new MacroSection();
1833 s
.name
= name
[0 .. namelen
];
1834 s
.body_
= pstart
[0 .. pend
- pstart
];
1836 //printf("Section: '%.*s' = '%.*s'\n", cast(int)s.namelen, s.name, cast(int)s.bodylen, s.body);
1838 if (!summary
&& !namelen
)
1856 void writeSections(Scope
* sc
, Dsymbols
* a
, ref OutBuffer buf
)
1859 //printf("DocComment::writeSections()\n");
1860 Loc loc
= (*a
)[0].loc
;
1861 if (Module m
= (*a
)[0].isModule())
1866 size_t offset1
= buf
.length
;
1867 buf
.writestring("$(DDOC_SECTIONS ");
1868 size_t offset2
= buf
.length
;
1869 for (size_t i
= 0; i
< sections
.length
; i
++)
1871 Section sec
= sections
[i
];
1874 //printf("Section: '%.*s' = '%.*s'\n", cast(int)sec.namelen, sec.name, cast(int)sec.bodylen, sec.body);
1875 if (!sec
.name
.length
&& i
== 0)
1877 buf
.writestring("$(DDOC_SUMMARY ");
1878 size_t o
= buf
.length
;
1879 buf
.write(sec
.body_
);
1880 escapeStrayParenthesis(loc
, buf
, o
, true, sc
.eSink
);
1881 highlightText(sc
, a
, loc
, buf
, o
);
1882 buf
.writestring(")");
1885 sec
.write(loc
, &this, sc
, a
, buf
);
1887 for (size_t i
= 0; i
< a
.length
; i
++)
1889 Dsymbol s
= (*a
)[i
];
1890 if (Dsymbol td
= getEponymousParent(s
))
1892 for (UnitTestDeclaration utd
= s
.ddocUnittest
; utd
; utd
= utd
.ddocUnittest
)
1894 if (utd
.visibility
.kind
== Visibility
.Kind
.private_ ||
!utd
.comment ||
!utd
.fbody
)
1896 // Strip whitespaces to avoid showing empty summary
1897 const(char)* c
= utd
.comment
;
1898 while (*c
== ' ' ||
*c
== '\t' ||
*c
== '\n' ||
*c
== '\r')
1900 buf
.writestring("$(DDOC_EXAMPLES ");
1901 size_t o
= buf
.length
;
1902 buf
.writestring(cast(char*)c
);
1905 auto codedoc
= utd
.codedoc
.stripLeadingNewlines
;
1906 size_t n
= getCodeIndent(codedoc
);
1909 buf
.writestring("----\n");
1910 buf
.writestring(codedoc
);
1911 buf
.writestring("----\n");
1912 highlightText(sc
, a
, loc
, buf
, o
);
1914 buf
.writestring(")");
1917 if (buf
.length
== offset2
)
1919 /* Didn't write out any sections, so back out last write
1921 buf
.setsize(offset1
);
1922 buf
.writestring("\n");
1925 buf
.writestring(")");
1929 /*****************************************
1930 * Return true if comment consists entirely of "ditto".
1932 bool isDitto(const(char)* comment
)
1936 const(char)* p
= skipwhitespace(comment
);
1937 if (Port
.memicmp(p
, "ditto", 5) == 0 && *skipwhitespace(p
+ 5) == 0)
1943 /**********************************************
1946 const(char)* skipwhitespace(const(char)* p
)
1948 return skipwhitespace(p
.toDString
).ptr
;
1952 const(char)[] skipwhitespace(const(char)[] p
) @safe
1954 foreach (idx
, char c
; p
)
1969 /************************************************
1970 * Scan past all instances of the given characters.
1972 * buf = an OutBuffer containing the DDoc
1973 * i = the index within `buf` to start scanning from
1974 * chars = the characters to skip; order is unimportant
1975 * Returns: the index after skipping characters.
1977 size_t
skipChars(ref OutBuffer buf
, size_t i
, string chars
) @safe
1980 foreach (j
, c
; buf
[][i
..$])
1994 string data
= "test ---\r\n\r\nend";
1997 assert(skipChars(buf
, 0, "-") == 0);
1998 assert(skipChars(buf
, 4, "-") == 4);
1999 assert(skipChars(buf
, 4, " -") == 8);
2000 assert(skipChars(buf
, 8, "\r\n") == 12);
2001 assert(skipChars(buf
, 12, "dne") == 15);
2004 /****************************************************
2005 * Replace all instances of `c` with `r` in the given string
2007 * s = the string to do replacements in
2008 * c = the character to look for
2009 * r = the string to replace `c` with
2010 * Returns: `s` with `c` replaced with `r`
2012 inout(char)[] replaceChar(inout(char)[] s
, char c
, string r
) pure @safe
2015 foreach (char sc
; s
)
2022 result
.reserve(s
.length
- count
+ (r
.length
* count
));
2024 foreach (i
, char sc
; s
)
2028 result
~= s
[start
..i
];
2033 result
~= s
[start
..$];
2040 assert("".replaceChar(',', "$(COMMA)") == "");
2041 assert("ab".replaceChar(',', "$(COMMA)") == "ab");
2042 assert("a,b".replaceChar(',', "$(COMMA)") == "a$(COMMA)b");
2043 assert("a,,b".replaceChar(',', "$(COMMA)") == "a$(COMMA)$(COMMA)b");
2044 assert(",ab".replaceChar(',', "$(COMMA)") == "$(COMMA)ab");
2045 assert("ab,".replaceChar(',', "$(COMMA)") == "ab$(COMMA)");
2049 * Return a lowercased copy of a string.
2051 * s = the string to lowercase
2052 * Returns: the lowercase version of the string or the original if already lowercase
2054 string
toLowercase(string s
) pure @safe
2057 foreach (size_t i
; 0..s
.length
)
2060 // TODO: maybe unicode lowercase, somehow
2061 if (c
>= 'A' && c
<= 'Z')
2063 if (!lower
.length
) {
2064 lower
.reserve(s
.length
);
2066 lower
~= s
[lower
.length
..i
];
2072 lower
~= s
[lower
.length
..$];
2081 assert("".toLowercase
== "");
2082 assert("abc".toLowercase
== "abc");
2083 assert("ABC".toLowercase
== "abc");
2084 assert("aBc".toLowercase
== "abc");
2087 /************************************************
2088 * Get the indent from one index to another, counting tab stops as four spaces wide
2089 * per the Markdown spec.
2091 * buf = an OutBuffer containing the DDoc
2092 * from = the index within `buf` to start counting from, inclusive
2093 * to = the index within `buf` to stop counting at, exclusive
2094 * Returns: the indent
2096 int getMarkdownIndent(ref OutBuffer buf
, size_t from
, size_t to
) @safe
2098 const slice
= buf
[];
2099 if (to
> slice
.length
)
2102 foreach (const c
; slice
[from
..to
])
2103 indent
+= (c
== '\t') ?
4 - (indent
% 4) : 1;
2107 /************************************************
2108 * Scan forward to one of:
2109 * start of identifier
2110 * beginning of next line
2113 size_t
skiptoident(ref OutBuffer buf
, size_t i
) @safe
2115 const slice
= buf
[];
2116 while (i
< slice
.length
)
2120 if (utf_decodeChar(slice
, i
, c
))
2122 /* Ignore UTF errors, but still consume input
2131 else if (!(isalpha(c
) || c
== '_' || c
== '\n'))
2139 /************************************************
2140 * Scan forward past end of identifier.
2142 size_t
skippastident(ref OutBuffer buf
, size_t i
) @safe
2144 const slice
= buf
[];
2145 while (i
< slice
.length
)
2149 if (utf_decodeChar(slice
, i
, c
))
2151 /* Ignore UTF errors, but still consume input
2160 else if (isalnum(c
) || c
== '_')
2168 /************************************************
2169 * Scan forward past end of an identifier that might
2170 * contain dots (e.g. `abc.def`)
2172 size_t
skipPastIdentWithDots(ref OutBuffer buf
, size_t i
) @safe
2174 const slice
= buf
[];
2175 bool lastCharWasDot
;
2176 while (i
< slice
.length
)
2180 if (utf_decodeChar(slice
, i
, c
))
2182 /* Ignore UTF errors, but still consume input
2188 // We need to distinguish between `abc.def`, abc..def`, and `abc.`
2189 // Only `abc.def` is a valid identifier
2197 lastCharWasDot
= true;
2206 lastCharWasDot
= false;
2210 else if (isalnum(c
) || c
== '_')
2212 lastCharWasDot
= false;
2227 /************************************************
2228 * Scan forward past URL starting at i.
2229 * We don't want to highlight parts of a URL.
2232 * index just past it if it is a URL
2234 size_t
skippastURL(ref OutBuffer buf
, size_t i
)
2236 const slice
= buf
[][i
.. $];
2238 bool sawdot
= false;
2239 if (slice
.length
> 7 && Port
.memicmp(slice
.ptr
, "http://", 7) == 0)
2243 else if (slice
.length
> 8 && Port
.memicmp(slice
.ptr
, "https://", 8) == 0)
2249 for (; j
< slice
.length
; j
++)
2254 if (c
== '-' || c
== '_' || c
== '?' || c
== '=' || c
== '%' ||
2255 c
== '&' || c
== '/' || c
== '+' || c
== '#' || c
== '~')
2270 /****************************************************
2271 * Remove a previously-inserted blank line macro.
2273 * buf = an OutBuffer containing the DDoc
2274 * iAt = the index within `buf` of the start of the `$(DDOC_BLANKLINE)`
2275 * macro. Upon function return its value is set to `0`.
2276 * i = an index within `buf`. If `i` is after `iAt` then it gets
2277 * reduced by the length of the removed macro.
2279 void removeBlankLineMacro(ref OutBuffer buf
, ref size_t iAt
, ref size_t i
)
2284 enum macroLength
= "$(DDOC_BLANKLINE)".length
;
2285 buf
.remove(iAt
, macroLength
);
2291 /****************************************************
2292 * Attempt to detect and replace a Markdown thematic break (HR). These are three
2293 * or more of the same delimiter, optionally with spaces or tabs between any of
2294 * them, e.g. `\n- - -\n` becomes `\n$(HR)\n`
2296 * buf = an OutBuffer containing the DDoc
2297 * i = the index within `buf` of the first character of a potential
2298 * thematic break. If the replacement is made `i` changes to
2299 * point to the closing parenthesis of the `$(HR)` macro.
2300 * iLineStart = the index within `buf` that the thematic break's line starts at
2301 * loc = the current location within the file
2302 * Returns: whether a thematic break was replaced
2304 bool replaceMarkdownThematicBreak(ref OutBuffer buf
, ref size_t i
, size_t iLineStart
, const ref Loc loc
)
2307 const slice
= buf
[];
2311 for (; j
< slice
.length
; j
++)
2315 else if (buf
[j
] != ' ' && buf
[j
] != '\t')
2320 if (j
>= buf
.length || buf
[j
] == '\n' || buf
[j
] == '\r')
2322 buf
.remove(iLineStart
, j
- iLineStart
);
2323 i
= buf
.insert(iLineStart
, "$(HR)") - 1;
2330 /****************************************************
2331 * Detect the level of an ATX-style heading, e.g. `## This is a heading` would
2332 * have a level of `2`.
2334 * buf = an OutBuffer containing the DDoc
2335 * i = the index within `buf` of the first `#` character
2337 * the detected heading level from 1 to 6, or
2338 * 0 if not at an ATX heading
2340 int detectAtxHeadingLevel(ref OutBuffer buf
, const size_t i
) @safe
2342 const iHeadingStart
= i
;
2343 const iAfterHashes
= skipChars(buf
, i
, "#");
2344 const headingLevel
= cast(int) (iAfterHashes
- iHeadingStart
);
2345 if (headingLevel
> 6)
2348 const iTextStart
= skipChars(buf
, iAfterHashes
, " \t");
2349 const emptyHeading
= buf
[iTextStart
] == '\r' || buf
[iTextStart
] == '\n';
2351 // require whitespace
2352 if (!emptyHeading
&& iTextStart
== iAfterHashes
)
2355 return headingLevel
;
2358 /****************************************************
2359 * Remove any trailing `##` suffix from an ATX-style heading.
2361 * buf = an OutBuffer containing the DDoc
2362 * i = the index within `buf` to start looking for a suffix at
2364 void removeAnyAtxHeadingSuffix(ref OutBuffer buf
, size_t i
)
2367 size_t iSuffixStart
= 0;
2368 size_t iWhitespaceStart
= j
;
2369 const slice
= buf
[];
2370 for (; j
< slice
.length
; j
++)
2375 if (iWhitespaceStart
&& !iSuffixStart
)
2380 if (!iWhitespaceStart
)
2381 iWhitespaceStart
= j
;
2388 iWhitespaceStart
= 0;
2394 buf
.remove(iWhitespaceStart
, j
- iWhitespaceStart
);
2397 /****************************************************
2398 * Wrap text in a Markdown heading macro, e.g. `$(H2 heading text`).
2400 * buf = an OutBuffer containing the DDoc
2401 * iStart = the index within `buf` that the Markdown heading starts at
2402 * iEnd = the index within `buf` of the character after the last
2403 * heading character. Is incremented by the length of the
2404 * inserted heading macro when this function ends.
2405 * loc = the location of the Ddoc within the file
2406 * headingLevel = the level (1-6) of heading to end. Is set to `0` when this
2409 void endMarkdownHeading(ref OutBuffer buf
, size_t iStart
, ref size_t iEnd
, const ref Loc loc
, ref int headingLevel
)
2411 char[5] heading
= "$(H0 ";
2412 heading
[3] = cast(char) ('0' + headingLevel
);
2413 buf
.insert(iStart
, heading
);
2415 size_t iBeforeNewline
= iEnd
;
2416 while (buf
[iBeforeNewline
-1] == '\r' || buf
[iBeforeNewline
-1] == '\n')
2418 buf
.insert(iBeforeNewline
, ")");
2422 /****************************************************
2423 * End all nested Markdown quotes, if inside any.
2425 * buf = an OutBuffer containing the DDoc
2426 * i = the index within `buf` of the character after the quote text.
2427 * quoteLevel = the current quote level. Is set to `0` when this function ends.
2428 * Returns: the amount that `i` was moved
2430 size_t
endAllMarkdownQuotes(ref OutBuffer buf
, size_t i
, ref int quoteLevel
)
2432 const length
= quoteLevel
;
2433 for (; quoteLevel
> 0; --quoteLevel
)
2434 i
= buf
.insert(i
, ")");
2438 /****************************************************
2439 * Convenience function to end all Markdown lists and quotes, if inside any, and
2440 * set `quoteMacroLevel` to `0`.
2442 * buf = an OutBuffer containing the DDoc
2443 * i = the index within `buf` of the character after the list and/or
2444 * quote text. Is adjusted when this function ends if any lists
2445 * and/or quotes were ended.
2446 * nestedLists = a set of nested lists. Upon return it will be empty.
2447 * quoteLevel = the current quote level. Is set to `0` when this function ends.
2448 * quoteMacroLevel = the macro level that the quote was started at. Is set to
2449 * `0` when this function ends.
2450 * Returns: the amount that `i` was moved
2452 size_t
endAllListsAndQuotes(ref OutBuffer buf
, ref size_t i
, ref MarkdownList
[] nestedLists
, ref int quoteLevel
, out int quoteMacroLevel
)
2454 quoteMacroLevel
= 0;
2456 i
+= MarkdownList
.endAllNestedLists(buf
, i
, nestedLists
);
2457 i
+= endAllMarkdownQuotes(buf
, i
, quoteLevel
);
2461 /****************************************************
2462 * Replace Markdown emphasis with the appropriate macro,
2463 * e.g. `*very* **nice**` becomes `$(EM very) $(STRONG nice)`.
2465 * buf = an OutBuffer containing the DDoc
2466 * loc = the current location within the file
2467 * inlineDelimiters = the collection of delimiters found within a paragraph. When this function returns its length will be reduced to `downToLevel`.
2468 * downToLevel = the length within `inlineDelimiters`` to reduce emphasis to
2469 * Returns: the number of characters added to the buffer by the replacements
2471 size_t
replaceMarkdownEmphasis(ref OutBuffer buf
, const ref Loc loc
, ref MarkdownDelimiter
[] inlineDelimiters
, int downToLevel
= 0)
2473 size_t
replaceEmphasisPair(ref MarkdownDelimiter start
, ref MarkdownDelimiter end
)
2475 immutable count
= start
.count
== 1 || end
.count
== 1 ?
1 : 2;
2477 size_t iStart
= start
.iStart
;
2478 size_t iEnd
= end
.iStart
;
2480 start
.count
-= count
;
2481 iStart
+= start
.count
;
2488 buf
.remove(iStart
, count
);
2490 buf
.remove(iEnd
, count
);
2492 string macroName
= count
>= 2 ?
"$(STRONG " : "$(EM ";
2493 buf
.insert(iEnd
, ")");
2494 buf
.insert(iStart
, macroName
);
2496 const delta
= 1 + macroName
.length
- (count
+ count
);
2497 end
.iStart
+= count
;
2502 int start
= (cast(int) inlineDelimiters
.length
) - 1;
2503 while (start
>= downToLevel
)
2505 // find start emphasis
2506 while (start
>= downToLevel
&&
2507 (inlineDelimiters
[start
].type
!= '*' ||
!inlineDelimiters
[start
].leftFlanking
))
2509 if (start
< downToLevel
)
2512 // find the nearest end emphasis
2513 int end
= start
+ 1;
2514 while (end
< inlineDelimiters
.length
&&
2515 (inlineDelimiters
[end
].type
!= inlineDelimiters
[start
].type ||
2516 inlineDelimiters
[end
].macroLevel
!= inlineDelimiters
[start
].macroLevel ||
2517 !inlineDelimiters
[end
].rightFlanking
))
2519 if (end
== inlineDelimiters
.length
)
2521 // the start emphasis has no matching end; if it isn't an end itself then kill it
2522 if (!inlineDelimiters
[start
].rightFlanking
)
2523 inlineDelimiters
[start
].type
= 0;
2528 // multiple-of-3 rule
2529 if (((inlineDelimiters
[start
].leftFlanking
&& inlineDelimiters
[start
].rightFlanking
) ||
2530 (inlineDelimiters
[end
].leftFlanking
&& inlineDelimiters
[end
].rightFlanking
)) &&
2531 (inlineDelimiters
[start
].count
+ inlineDelimiters
[end
].count
) % 3 == 0)
2537 immutable delta0
= replaceEmphasisPair(inlineDelimiters
[start
], inlineDelimiters
[end
]);
2539 for (; end
< inlineDelimiters
.length
; ++end
)
2540 inlineDelimiters
[end
].iStart
+= delta0
;
2544 inlineDelimiters
.length
= downToLevel
;
2548 /****************************************************
2550 bool isIdentifier(Dsymbols
* a
, const(char)[] s
) @safe
2552 foreach (member
; *a
)
2554 if (auto imp
= member
.isImport())
2556 // For example: `public import str = core.stdc.string;`
2557 // This checks if `s` is equal to `str`
2560 if (s
== imp
.aliasId
.toString())
2565 // The general case: `public import core.stdc.string;`
2567 // fully qualify imports so `core.stdc.string` doesn't appear as `core`
2568 string fullyQualifiedImport
;
2569 foreach (const pid
; imp
.packages
)
2571 fullyQualifiedImport
~= pid
.toString() ~ ".";
2573 fullyQualifiedImport
~= imp
.id
.toString();
2575 // Check if `s` == `core.stdc.string`
2576 if (s
== fullyQualifiedImport
)
2580 else if (member
.ident
)
2582 if (s
== member
.ident
.toString())
2590 /****************************************************
2592 bool isKeyword(const(char)[] str) @safe
2594 immutable string
[3] table
= ["true", "false", "null"];
2603 /****************************************************
2605 TypeFunction
isTypeFunction(Dsymbol s
) @safe
2607 FuncDeclaration f
= s
.isFuncDeclaration();
2608 /* f.type may be NULL for template members.
2612 Type t
= f
.originalType ? f
.originalType
: f
.type
;
2613 if (t
.ty
== Tfunction
)
2614 return cast(TypeFunction
)t
;
2619 /****************************************************
2621 Parameter
isFunctionParameter(Dsymbol s
, const(char)[] str) @safe
2623 TypeFunction tf
= isTypeFunction(s
);
2624 if (tf
&& tf
.parameterList
.parameters
)
2626 foreach (fparam
; *tf
.parameterList
.parameters
)
2628 if (fparam
.ident
&& str == fparam
.ident
.toString())
2637 /****************************************************
2639 Parameter
isFunctionParameter(Dsymbols
* a
, const(char)[] p
) @safe
2641 foreach (Dsymbol sym
; *a
)
2643 Parameter fparam
= isFunctionParameter(sym
, p
);
2652 /****************************************************
2654 Parameter
isEponymousFunctionParameter(Dsymbols
*a
, const(char)[] p
) @safe
2656 foreach (Dsymbol dsym
; *a
)
2658 TemplateDeclaration td
= dsym
.isTemplateDeclaration();
2659 if (td
&& td
.onemember
)
2661 /* Case 1: we refer to a template declaration inside the template
2668 td
= td
.onemember
.isTemplateDeclaration();
2672 /* Case 2: we're an alias to a template declaration
2675 alias case2 = case1!int;
2677 AliasDeclaration ad
= dsym
.isAliasDeclaration();
2678 if (ad
&& ad
.aliassym
)
2680 td
= ad
.aliassym
.isTemplateDeclaration();
2685 Dsymbol sym
= getEponymousMember(td
);
2688 Parameter fparam
= isFunctionParameter(sym
, p
);
2700 /****************************************************
2702 TemplateParameter
isTemplateParameter(Dsymbols
* a
, const(char)* p
, size_t len
)
2704 for (size_t i
= 0; i
< a
.length
; i
++)
2706 TemplateDeclaration td
= (*a
)[i
].isTemplateDeclaration();
2707 // Check for the parent, if the current symbol is not a template declaration.
2709 td
= getEponymousParent((*a
)[i
]);
2710 if (td
&& td
.origParameters
)
2712 foreach (tp
; *td
.origParameters
)
2714 if (tp
.ident
&& p
[0 .. len
] == tp
.ident
.toString())
2724 /****************************************************
2725 * Return true if str is a reserved symbol name
2726 * that starts with a double underscore.
2728 bool isReservedName(const(char)[] str) @safe
2730 immutable string
[] table
=
2753 "__PRETTY_FUNCTION__",
2772 /****************************************************
2773 * A delimiter for Markdown inline content like emphasis and links.
2775 struct MarkdownDelimiter
2777 size_t iStart
; /// the index where this delimiter starts
2778 int count
; /// the length of this delimeter's start sequence
2779 int macroLevel
; /// the count of nested DDoc macros when the delimiter is started
2780 bool leftFlanking
; /// whether the delimiter is left-flanking, as defined by the CommonMark spec
2781 bool rightFlanking
; /// whether the delimiter is right-flanking, as defined by the CommonMark spec
2782 bool atParagraphStart
; /// whether the delimiter is at the start of a paragraph
2783 char type
; /// the type of delimiter, defined by its starting character
2785 /// whether this describes a valid delimiter
2786 @property bool isValid() const @safe { return count
!= 0; }
2788 /// flag this delimiter as invalid
2789 void invalidate() @safe { count
= 0; }
2792 /****************************************************
2793 * Info about a Markdown list.
2797 string orderedStart
; /// an optional start number--if present then the list starts at this number
2798 size_t iStart
; /// the index where the list item starts
2799 size_t iContentStart
; /// the index where the content starts after the list delimiter
2800 int delimiterIndent
; /// the level of indent the list delimiter starts at
2801 int contentIndent
; /// the level of indent the content starts at
2802 int macroLevel
; /// the count of nested DDoc macros when the list is started
2803 char type
; /// the type of list, defined by its starting character
2805 /// whether this describes a valid list
2806 @property bool isValid() const @safe { return type
!= type
.init
; }
2808 /****************************************************
2809 * Try to parse a list item, returning whether successful.
2811 * buf = an OutBuffer containing the DDoc
2812 * iLineStart = the index within `buf` of the first character of the line
2813 * i = the index within `buf` of the potential list item
2814 * Returns: the parsed list item. Its `isValid` property describes whether parsing succeeded.
2816 static MarkdownList
parseItem(ref OutBuffer buf
, size_t iLineStart
, size_t i
) @safe
2818 if (buf
[i
] == '+' || buf
[i
] == '-' || buf
[i
] == '*')
2819 return parseUnorderedListItem(buf
, iLineStart
, i
);
2821 return parseOrderedListItem(buf
, iLineStart
, i
);
2824 /****************************************************
2825 * Return whether the context is at a list item of the same type as this list.
2827 * buf = an OutBuffer containing the DDoc
2828 * iLineStart = the index within `buf` of the first character of the line
2829 * i = the index within `buf` of the list item
2830 * Returns: whether `i` is at a list item of the same type as this list
2832 private bool isAtItemInThisList(ref OutBuffer buf
, size_t iLineStart
, size_t i
) @safe
2834 MarkdownList item
= (type
== '.' || type
== ')') ?
2835 parseOrderedListItem(buf
, iLineStart
, i
) :
2836 parseUnorderedListItem(buf
, iLineStart
, i
);
2837 if (item
.type
== type
)
2838 return item
.delimiterIndent
< contentIndent
&& item
.contentIndent
> delimiterIndent
;
2842 /****************************************************
2843 * Start a Markdown list item by creating/deleting nested lists and starting the item.
2845 * buf = an OutBuffer containing the DDoc
2846 * iLineStart = the index within `buf` of the first character of the line. If this function succeeds it will be adjuested to equal `i`.
2847 * i = the index within `buf` of the list item. If this function succeeds `i` will be adjusted to fit the inserted macro.
2848 * 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`.
2849 * nestedLists = a set of nested lists. If this function succeeds it may contain a new nested list.
2850 * loc = the location of the Ddoc within the file
2851 * Returns: `true` if a list was created
2853 bool startItem(ref OutBuffer buf
, ref size_t iLineStart
, ref size_t i
, ref size_t iPrecedingBlankLine
, ref MarkdownList
[] nestedLists
, const ref Loc loc
)
2855 buf
.remove(iStart
, iContentStart
- iStart
);
2857 if (!nestedLists
.length ||
2858 delimiterIndent
>= nestedLists
[$-1].contentIndent ||
2859 buf
[iLineStart
- 4..iLineStart
] == "$(LI")
2861 // start a list macro
2862 nestedLists
~= this;
2865 if (orderedStart
.length
)
2867 iStart
= buf
.insert(iStart
, "$(OL_START ");
2868 iStart
= buf
.insert(iStart
, orderedStart
);
2869 iStart
= buf
.insert(iStart
, ",\n");
2872 iStart
= buf
.insert(iStart
, "$(OL\n");
2875 iStart
= buf
.insert(iStart
, "$(UL\n");
2877 removeBlankLineMacro(buf
, iPrecedingBlankLine
, iStart
);
2879 else if (nestedLists
.length
)
2881 nestedLists
[$-1].delimiterIndent
= delimiterIndent
;
2882 nestedLists
[$-1].contentIndent
= contentIndent
;
2885 iStart
= buf
.insert(iStart
, "$(LI\n");
2892 /****************************************************
2893 * End all nested Markdown lists.
2895 * buf = an OutBuffer containing the DDoc
2896 * i = the index within `buf` to end lists at.
2897 * nestedLists = a set of nested lists. Upon return it will be empty.
2898 * Returns: the amount that `i` changed
2900 static size_t
endAllNestedLists(ref OutBuffer buf
, size_t i
, ref MarkdownList
[] nestedLists
)
2903 for (; nestedLists
.length
; --nestedLists
.length
)
2904 i
= buf
.insert(i
, ")\n)");
2908 /****************************************************
2909 * Look for a sibling list item or the end of nested list(s).
2911 * buf = an OutBuffer containing the DDoc
2912 * 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.
2913 * iParagraphStart = the index within `buf` to start the next paragraph at at. May be adjusted upon return.
2914 * nestedLists = a set of nested lists. Some nested lists may have been removed from it upon return.
2916 static void handleSiblingOrEndingList(ref OutBuffer buf
, ref size_t i
, ref size_t iParagraphStart
, ref MarkdownList
[] nestedLists
)
2918 size_t iAfterSpaces
= skipChars(buf
, i
+ 1, " \t");
2920 if (nestedLists
[$-1].isAtItemInThisList(buf
, i
+ 1, iAfterSpaces
))
2922 // end a sibling list item
2923 i
= buf
.insert(i
, ")");
2924 iParagraphStart
= skipChars(buf
, i
, " \t\r\n");
2926 else if (iAfterSpaces
>= buf
.length ||
(buf
[iAfterSpaces
] != '\r' && buf
[iAfterSpaces
] != '\n'))
2928 // end nested lists that are indented more than this content
2929 const indent
= getMarkdownIndent(buf
, i
+ 1, iAfterSpaces
);
2930 while (nestedLists
.length
&& nestedLists
[$-1].contentIndent
> indent
)
2932 i
= buf
.insert(i
, ")\n)");
2933 --nestedLists
.length
;
2934 iParagraphStart
= skipChars(buf
, i
, " \t\r\n");
2936 if (nestedLists
.length
&& nestedLists
[$-1].isAtItemInThisList(buf
, i
+ 1, iParagraphStart
))
2938 i
= buf
.insert(i
, ")");
2946 /****************************************************
2947 * Parse an unordered list item at the current position
2949 * buf = an OutBuffer containing the DDoc
2950 * iLineStart = the index within `buf` of the first character of the line
2951 * i = the index within `buf` of the list item
2952 * Returns: the parsed list item, or a list item with type `.init` if no list item is available
2954 private static MarkdownList
parseUnorderedListItem(ref OutBuffer buf
, size_t iLineStart
, size_t i
) @safe
2956 if (i
+1 < buf
.length
&&
2965 const iContentStart
= skipChars(buf
, i
+ 1, " \t");
2966 const delimiterIndent
= getMarkdownIndent(buf
, iLineStart
, i
);
2967 const contentIndent
= getMarkdownIndent(buf
, iLineStart
, iContentStart
);
2968 auto list
= MarkdownList(null, iLineStart
, iContentStart
, delimiterIndent
, contentIndent
, 0, buf
[i
]);
2971 return MarkdownList();
2974 /****************************************************
2975 * Parse an ordered list item at the current position
2977 * buf = an OutBuffer containing the DDoc
2978 * iLineStart = the index within `buf` of the first character of the line
2979 * i = the index within `buf` of the list item
2980 * Returns: the parsed list item, or a list item with type `.init` if no list item is available
2982 private static MarkdownList
parseOrderedListItem(ref OutBuffer buf
, size_t iLineStart
, size_t i
) @safe
2984 size_t iAfterNumbers
= skipChars(buf
, i
, "0123456789");
2985 if (iAfterNumbers
- i
> 0 &&
2986 iAfterNumbers
- i
<= 9 &&
2987 iAfterNumbers
+ 1 < buf
.length
&&
2988 buf
[iAfterNumbers
] == '.' &&
2989 (buf
[iAfterNumbers
+1] == ' ' ||
2990 buf
[iAfterNumbers
+1] == '\t' ||
2991 buf
[iAfterNumbers
+1] == '\r' ||
2992 buf
[iAfterNumbers
+1] == '\n'))
2994 const iContentStart
= skipChars(buf
, iAfterNumbers
+ 1, " \t");
2995 const delimiterIndent
= getMarkdownIndent(buf
, iLineStart
, i
);
2996 const contentIndent
= getMarkdownIndent(buf
, iLineStart
, iContentStart
);
2997 size_t iNumberStart
= skipChars(buf
, i
, "0");
2998 if (iNumberStart
== iAfterNumbers
)
3000 auto orderedStart
= buf
[][iNumberStart
.. iAfterNumbers
];
3001 if (orderedStart
== "1")
3002 orderedStart
= null;
3003 return MarkdownList(orderedStart
.idup
, iLineStart
, iContentStart
, delimiterIndent
, contentIndent
, 0, buf
[iAfterNumbers
]);
3005 return MarkdownList();
3009 /****************************************************
3014 string href
; /// the link destination
3015 string title
; /// an optional title for the link
3016 string label
; /// an optional label for the link
3017 Dsymbol symbol
; /// an optional symbol to link to
3019 /****************************************************
3020 * Replace a Markdown link or link definition in the form of:
3021 * - Inline link: `[foo](url/ 'optional title')`
3022 * - Reference link: `[foo][bar]`, `[foo][]` or `[foo]`
3023 * - Link reference definition: `[bar]: url/ 'optional title'`
3025 * buf = an OutBuffer containing the DDoc
3026 * i = the index within `buf` that points to the `]` character of the potential link.
3027 * If this function succeeds it will be adjusted to fit the inserted link macro.
3028 * loc = the current location within the file
3029 * inlineDelimiters = previously parsed Markdown delimiters, including emphasis and link/image starts
3030 * delimiterIndex = the index within `inlineDelimiters` of the nearest link/image starting delimiter
3031 * linkReferences = previously parsed link references. When this function returns it may contain
3032 * additional previously unparsed references.
3033 * Returns: whether a reference link was found and replaced at `i`
3035 static bool replaceLink(ref OutBuffer buf
, ref size_t i
, const ref Loc loc
, ref MarkdownDelimiter
[] inlineDelimiters
, int delimiterIndex
, ref MarkdownLinkReferences linkReferences
)
3037 const delimiter
= inlineDelimiters
[delimiterIndex
];
3040 size_t iEnd
= link
.parseReferenceDefinition(buf
, i
, delimiter
);
3043 i
= delimiter
.iStart
;
3044 link
.storeAndReplaceDefinition(buf
, i
, iEnd
, linkReferences
, loc
);
3045 inlineDelimiters
.length
= delimiterIndex
;
3049 iEnd
= link
.parseInlineLink(buf
, i
);
3052 iEnd
= link
.parseReferenceLink(buf
, i
, delimiter
);
3055 const label
= link
.label
;
3056 link
= linkReferences
.lookupReference(label
, buf
, i
, loc
);
3057 // check rightFlanking to avoid replacing things like int[string]
3058 if (!link
.href
.length
&& !delimiter
.rightFlanking
)
3059 link
= linkReferences
.lookupSymbol(label
);
3060 if (!link
.href
.length
)
3068 immutable delta
= replaceMarkdownEmphasis(buf
, loc
, inlineDelimiters
, delimiterIndex
);
3071 link
.replaceLink(buf
, i
, iEnd
, delimiter
);
3075 /****************************************************
3076 * Replace a Markdown link definition in the form of `[bar]: url/ 'optional title'`
3078 * buf = an OutBuffer containing the DDoc
3079 * i = the index within `buf` that points to the `]` character of the potential link.
3080 * If this function succeeds it will be adjusted to fit the inserted link macro.
3081 * inlineDelimiters = previously parsed Markdown delimiters, including emphasis and link/image starts
3082 * delimiterIndex = the index within `inlineDelimiters` of the nearest link/image starting delimiter
3083 * linkReferences = previously parsed link references. When this function returns it may contain
3084 * additional previously unparsed references.
3085 * loc = the current location in the file
3086 * Returns: whether a reference link was found and replaced at `i`
3088 static bool replaceReferenceDefinition(ref OutBuffer buf
, ref size_t i
, ref MarkdownDelimiter
[] inlineDelimiters
, int delimiterIndex
, ref MarkdownLinkReferences linkReferences
, const ref Loc loc
)
3090 const delimiter
= inlineDelimiters
[delimiterIndex
];
3092 size_t iEnd
= link
.parseReferenceDefinition(buf
, i
, delimiter
);
3096 i
= delimiter
.iStart
;
3097 link
.storeAndReplaceDefinition(buf
, i
, iEnd
, linkReferences
, loc
);
3098 inlineDelimiters
.length
= delimiterIndex
;
3102 /****************************************************
3103 * Parse a Markdown inline link in the form of `[foo](url/ 'optional title')`
3105 * buf = an OutBuffer containing the DDoc
3106 * i = the index within `buf` that points to the `]` character of the inline link.
3107 * Returns: the index at the end of parsing the link, or `i` if parsing failed.
3109 private size_t
parseInlineLink(ref OutBuffer buf
, size_t i
)
3111 size_t iEnd
= i
+ 1;
3112 if (iEnd
>= buf
.length || buf
[iEnd
] != '(')
3116 if (!parseHref(buf
, iEnd
))
3119 iEnd
= skipChars(buf
, iEnd
, " \t\r\n");
3120 if (buf
[iEnd
] != ')')
3122 if (parseTitle(buf
, iEnd
))
3123 iEnd
= skipChars(buf
, iEnd
, " \t\r\n");
3126 if (buf
[iEnd
] != ')')
3132 /****************************************************
3133 * Parse a Markdown reference link in the form of `[foo][bar]`, `[foo][]` or `[foo]`
3135 * buf = an OutBuffer containing the DDoc
3136 * i = the index within `buf` that points to the `]` character of the inline link.
3137 * delimiter = the delimiter that starts this link
3138 * Returns: the index at the end of parsing the link, or `i` if parsing failed.
3140 private size_t
parseReferenceLink(ref OutBuffer buf
, size_t i
, MarkdownDelimiter delimiter
) @safe
3142 size_t iStart
= i
+ 1;
3143 size_t iEnd
= iStart
;
3144 if (iEnd
>= buf
.length || buf
[iEnd
] != '[' ||
(iEnd
+1 < buf
.length
&& buf
[iEnd
+1] == ']'))
3146 // collapsed reference [foo][] or shortcut reference [foo]
3147 iStart
= delimiter
.iStart
+ delimiter
.count
- 1;
3148 if (buf
[iEnd
] == '[')
3152 parseLabel(buf
, iStart
);
3161 /****************************************************
3162 * Parse a Markdown reference definition in the form of `[bar]: url/ 'optional title'`
3164 * buf = an OutBuffer containing the DDoc
3165 * i = the index within `buf` that points to the `]` character of the inline link.
3166 * delimiter = the delimiter that starts this link
3167 * Returns: the index at the end of parsing the link, or `i` if parsing failed.
3169 private size_t
parseReferenceDefinition(ref OutBuffer buf
, size_t i
, MarkdownDelimiter delimiter
)
3171 if (!delimiter
.atParagraphStart || delimiter
.type
!= '[' ||
3172 i
+1 >= buf
.length || buf
[i
+1] != ':')
3175 size_t iEnd
= delimiter
.iStart
;
3176 parseLabel(buf
, iEnd
);
3177 if (label
.length
== 0 || iEnd
!= i
+ 1)
3181 iEnd
= skipChars(buf
, iEnd
, " \t");
3182 skipOneNewline(buf
, iEnd
);
3184 if (!parseHref(buf
, iEnd
) || href
.length
== 0)
3187 iEnd
= skipChars(buf
, iEnd
, " \t");
3188 const requireNewline
= !skipOneNewline(buf
, iEnd
);
3189 const iBeforeTitle
= iEnd
;
3191 if (parseTitle(buf
, iEnd
))
3193 iEnd
= skipChars(buf
, iEnd
, " \t");
3194 if (iEnd
< buf
.length
&& buf
[iEnd
] != '\r' && buf
[iEnd
] != '\n')
3196 // the title must end with a newline
3198 iEnd
= iBeforeTitle
;
3202 iEnd
= skipChars(buf
, iEnd
, " \t");
3203 if (requireNewline
&& iEnd
< buf
.length
-1 && buf
[iEnd
] != '\r' && buf
[iEnd
] != '\n')
3209 /****************************************************
3210 * Parse and normalize a Markdown reference label
3212 * buf = an OutBuffer containing the DDoc
3213 * i = the index within `buf` that points to the `[` character at the start of the label.
3214 * If this function returns a non-empty label then `i` will point just after the ']' at the end of the label.
3215 * Returns: the parsed and normalized label, possibly empty
3217 private bool parseLabel(ref OutBuffer buf
, ref size_t i
) @safe
3222 const slice
= buf
[];
3225 // Some labels have already been en-symboled; handle that
3226 const inSymbol
= j
+15 < slice
.length
&& slice
[j
..j
+15] == "$(DDOC_PSYMBOL ";
3230 for (; j
< slice
.length
; ++j
)
3239 if (label
.length
&& label
[$-1] != ' ')
3243 if (inSymbol
&& j
+1 < slice
.length
&& slice
[j
+1] == ']')
3250 if (slice
[j
-1] != '\\')
3257 if (label
.length
&& label
[$-1] == ' ')
3274 /****************************************************
3275 * Parse and store a Markdown link URL, optionally enclosed in `<>` brackets
3277 * buf = an OutBuffer containing the DDoc
3278 * i = the index within `buf` that points to the first character of the URL.
3279 * If this function succeeds `i` will point just after the end of the URL.
3280 * Returns: whether a URL was found and parsed
3282 private bool parseHref(ref OutBuffer buf
, ref size_t i
)
3284 size_t j
= skipChars(buf
, i
, " \t");
3286 size_t iHrefStart
= j
;
3287 size_t parenDepth
= 1;
3288 bool inPointy
= false;
3289 const slice
= buf
[];
3290 for (; j
< slice
.length
; j
++)
3295 if (!inPointy
&& j
== iHrefStart
)
3302 if (inPointy
&& slice
[j
-1] != '\\')
3306 if (!inPointy
&& slice
[j
-1] != '\\')
3310 if (!inPointy
&& slice
[j
-1] != '\\')
3334 auto href
= slice
[iHrefStart
.. j
].dup
;
3335 this.href
= cast(string
) percentEncode(removeEscapeBackslashes(href
)).replaceChar(',', "$(COMMA)");
3342 /****************************************************
3343 * Parse and store a Markdown link title, enclosed in parentheses or `'` or `"` quotes
3345 * buf = an OutBuffer containing the DDoc
3346 * i = the index within `buf` that points to the first character of the title.
3347 * If this function succeeds `i` will point just after the end of the title.
3348 * Returns: whether a title was found and parsed
3350 private bool parseTitle(ref OutBuffer buf
, ref size_t i
)
3352 size_t j
= skipChars(buf
, i
, " \t");
3353 if (j
>= buf
.length
)
3357 if (type
!= '"' && type
!= '\'' && type
!= '(')
3362 const iTitleStart
= j
+ 1;
3363 size_t iNewline
= 0;
3364 const slice
= buf
[];
3365 for (j
= iTitleStart
; j
< slice
.length
; j
++)
3373 if (type
== c
&& slice
[j
-1] != '\\')
3384 // no blank lines in titles
3396 auto title
= slice
[iTitleStart
.. j
].dup
;
3397 this.title
= cast(string
) removeEscapeBackslashes(title
).
3398 replaceChar(',', "$(COMMA)").
3399 replaceChar('"', "$(QUOTE)");
3404 /****************************************************
3405 * Replace a Markdown link or image with the appropriate macro
3407 * buf = an OutBuffer containing the DDoc
3408 * i = the index within `buf` that points to the `]` character of the inline link.
3409 * When this function returns it will be adjusted to the end of the inserted macro.
3410 * iLinkEnd = the index within `buf` that points just after the last character of the link
3411 * delimiter = the Markdown delimiter that started the link or image
3413 private void replaceLink(ref OutBuffer buf
, ref size_t i
, size_t iLinkEnd
, MarkdownDelimiter delimiter
)
3415 size_t iAfterLink
= i
- delimiter
.count
;
3419 macroName
= "$(SYMBOL_LINK ";
3421 else if (title
.length
)
3423 if (delimiter
.type
== '[')
3424 macroName
= "$(LINK_TITLE ";
3426 macroName
= "$(IMAGE_TITLE ";
3430 if (delimiter
.type
== '[')
3431 macroName
= "$(LINK2 ";
3433 macroName
= "$(IMAGE ";
3435 buf
.remove(delimiter
.iStart
, delimiter
.count
);
3436 buf
.remove(i
- delimiter
.count
, iLinkEnd
- i
);
3437 iLinkEnd
= buf
.insert(delimiter
.iStart
, macroName
);
3438 iLinkEnd
= buf
.insert(iLinkEnd
, href
);
3439 iLinkEnd
= buf
.insert(iLinkEnd
, ", ");
3440 iAfterLink
+= macroName
.length
+ href
.length
+ 2;
3443 iLinkEnd
= buf
.insert(iLinkEnd
, title
);
3444 iLinkEnd
= buf
.insert(iLinkEnd
, ", ");
3445 iAfterLink
+= title
.length
+ 2;
3447 // Link macros with titles require escaping commas
3448 for (size_t j
= iLinkEnd
; j
< iAfterLink
; ++j
)
3452 j
= buf
.insert(j
, "$(COMMA)") - 1;
3456 // TODO: if image, remove internal macros, leaving only text
3457 buf
.insert(iAfterLink
, ")");
3461 /****************************************************
3462 * Store the Markdown link definition and remove it from `buf`
3464 * buf = an OutBuffer containing the DDoc
3465 * i = the index within `buf` that points to the `[` character at the start of the link definition.
3466 * When this function returns it will be adjusted to exclude the link definition.
3467 * iEnd = the index within `buf` that points just after the end of the definition
3468 * linkReferences = previously parsed link references. When this function returns it may contain
3469 * an additional reference.
3470 * loc = the current location in the file
3472 private void storeAndReplaceDefinition(ref OutBuffer buf
, ref size_t i
, size_t iEnd
, ref MarkdownLinkReferences linkReferences
, const ref Loc loc
)
3474 // Remove the definition and trailing whitespace
3475 iEnd
= skipChars(buf
, iEnd
, " \t\r\n");
3476 buf
.remove(i
, iEnd
- i
);
3479 string lowercaseLabel
= label
.toLowercase();
3480 if (lowercaseLabel
!in linkReferences
.references
)
3481 linkReferences
.references
[lowercaseLabel
] = this;
3484 /****************************************************
3485 * Remove Markdown escaping backslashes from the given string
3487 * s = the string to remove escaping backslashes from
3488 * Returns: `s` without escaping backslashes in it
3490 private static char[] removeEscapeBackslashes(char[] s
) @safe
3495 // avoid doing anything if there isn't anything to escape
3497 for (i
= 0; i
< s
.length
-1; ++i
)
3498 if (s
[i
] == '\\' && ispunct(s
[i
+1]))
3500 if (i
== s
.length
-1)
3503 // copy characters backwards, then truncate
3506 for (++i
, ++j
; j
< s
.length
; ++i
, ++j
)
3508 if (j
< s
.length
-1 && s
[j
] == '\\' && ispunct(s
[j
+1]))
3512 s
.length
-= (j
- i
);
3519 assert(removeEscapeBackslashes("".dup
) == "");
3520 assert(removeEscapeBackslashes(`\a`.dup
) == `\a`);
3521 assert(removeEscapeBackslashes(`.\`.dup
) == `.\`);
3522 assert(removeEscapeBackslashes(`\.\`.dup
) == `.\`);
3523 assert(removeEscapeBackslashes(`\.`.dup
) == `.`);
3524 assert(removeEscapeBackslashes(`\.\.`.dup
) == `..`);
3525 assert(removeEscapeBackslashes(`a\.b\.c`.dup
) == `a.b.c`);
3528 /****************************************************
3529 * Percent-encode (AKA URL-encode) the given string
3531 * s = the string to percent-encode
3532 * Returns: `s` with special characters percent-encoded
3534 private static inout(char)[] percentEncode(inout(char)[] s
) pure @safe
3536 static bool shouldEncode(char c
)
3538 return ((c
< '0' && c
!= '!' && c
!= '#' && c
!= '$' && c
!= '%' && c
!= '&' && c
!= '\'' && c
!= '(' &&
3539 c
!= ')' && c
!= '*' && c
!= '+' && c
!= ',' && c
!= '-' && c
!= '.' && c
!= '/')
3540 ||
(c
> '9' && c
< 'A' && c
!= ':' && c
!= ';' && c
!= '=' && c
!= '?' && c
!= '@')
3541 ||
(c
> 'Z' && c
< 'a' && c
!= '[' && c
!= ']' && c
!= '_')
3542 ||
(c
> 'z' && c
!= '~'));
3545 for (size_t i
= 0; i
< s
.length
; ++i
)
3547 if (shouldEncode(s
[i
]))
3549 immutable static hexDigits
= "0123456789ABCDEF";
3550 immutable encoded1
= hexDigits
[s
[i
] >> 4];
3551 immutable encoded2
= hexDigits
[s
[i
] & 0x0F];
3552 s
= s
[0..i
] ~ '%' ~ encoded1
~ encoded2
~ s
[i
+1..$];
3562 assert(percentEncode("") == "");
3563 assert(percentEncode("aB12-._~/?") == "aB12-._~/?");
3564 assert(percentEncode("<\n>") == "%3C%0A%3E");
3567 /**************************************************
3568 * Skip a single newline at `i`
3570 * buf = an OutBuffer containing the DDoc
3571 * i = the index within `buf` to start looking at.
3572 * If this function succeeds `i` will point after the newline.
3573 * Returns: whether a newline was skipped
3575 private static bool skipOneNewline(ref OutBuffer buf
, ref size_t i
) pure @safe
3577 if (i
< buf
.length
&& buf
[i
] == '\r')
3579 if (i
< buf
.length
&& buf
[i
] == '\n')
3588 /**************************************************
3589 * A set of Markdown link references.
3591 struct MarkdownLinkReferences
3593 MarkdownLink
[string
] references
; // link references keyed by normalized label
3594 MarkdownLink
[string
] symbols
; // link symbols keyed by name
3595 Scope
* _scope
; // the current scope
3596 bool extractedAll
; // the index into the buffer of the last-parsed reference
3598 /**************************************************
3599 * Look up a reference by label, searching through the rest of the buffer if needed.
3600 * Symbols in the current scope are searched for if the DDoc doesn't define the reference.
3602 * label = the label to find the reference for
3603 * buf = an OutBuffer containing the DDoc
3604 * i = the index within `buf` to start searching for references at
3605 * loc = the current location in the file
3606 * Returns: a link. If the `href` member has a value then the reference is valid.
3608 MarkdownLink
lookupReference(string label
, ref OutBuffer buf
, size_t i
, const ref Loc loc
)
3610 const lowercaseLabel
= label
.toLowercase();
3611 if (lowercaseLabel
!in references
)
3612 extractReferences(buf
, i
, loc
);
3614 if (lowercaseLabel
in references
)
3615 return references
[lowercaseLabel
];
3617 return MarkdownLink();
3621 * Look up the link for the D symbol with the given name.
3622 * If found, the link is cached in the `symbols` member.
3624 * name = the name of the symbol
3625 * Returns: the link for the symbol or a link with a `null` href
3627 MarkdownLink
lookupSymbol(string name
)
3629 if (name
in symbols
)
3630 return symbols
[name
];
3632 const ids
= split(name
, '.');
3635 auto id
= Identifier
.lookup(ids
[0].ptr
, ids
[0].length
);
3639 auto symbol
= _scope
.search(loc
, id
, null, IgnoreErrors
);
3640 for (size_t i
= 1; symbol
&& i
< ids
.length
; ++i
)
3642 id
= Identifier
.lookup(ids
[i
].ptr
, ids
[i
].length
);
3643 symbol
= id
!is null ? symbol
.search(loc
, id
, IgnoreErrors
) : null;
3646 link
= MarkdownLink(createHref(symbol
), null, name
, symbol
);
3649 symbols
[name
] = link
;
3653 /**************************************************
3654 * Remove and store all link references from the document, in the form of
3655 * `[label]: href "optional title"`
3657 * buf = an OutBuffer containing the DDoc
3658 * i = the index within `buf` to start looking at
3659 * loc = the current location in the file
3660 * Returns: whether a reference was extracted
3662 private void extractReferences(ref OutBuffer buf
, size_t i
, const ref Loc loc
)
3664 static bool isFollowedBySpace(ref OutBuffer buf
, size_t i
)
3666 return i
+1 < buf
.length
&& (buf
[i
+1] == ' ' || buf
[i
+1] == '\t');
3672 bool leadingBlank
= false;
3674 bool newParagraph
= true;
3675 MarkdownDelimiter
[] delimiters
;
3676 for (; i
< buf
.length
; ++i
)
3685 if (leadingBlank
&& !inCode
)
3686 newParagraph
= true;
3687 leadingBlank
= true;
3693 if (leadingBlank
&& !inCode
)
3694 newParagraph
= true;
3695 leadingBlank
= false;
3698 if (leadingBlank
&& !inCode
)
3699 newParagraph
= true;
3702 if (leadingBlank
&& !inCode
&& isFollowedBySpace(buf
, i
))
3703 newParagraph
= true;
3705 leadingBlank
= false;
3710 if (leadingBlank
&& !inCode
)
3712 i
= skipChars(buf
, i
, "0123456789");
3713 if (i
< buf
.length
&&
3714 (buf
[i
] == '.' || buf
[i
] == ')') &&
3715 isFollowedBySpace(buf
, i
))
3716 newParagraph
= true;
3718 leadingBlank
= false;
3722 if (leadingBlank
&& !inCode
)
3724 newParagraph
= true;
3725 if (!isFollowedBySpace(buf
, i
))
3726 leadingBlank
= false;
3731 if (leadingBlank
&& i
+2 < buf
.length
&& buf
[i
+1] == c
&& buf
[i
+2] == c
)
3733 inCode
= inCode
== c ?
false : c
;
3734 i
= skipChars(buf
, i
, [c
]) - 1;
3735 newParagraph
= true;
3737 leadingBlank
= false;
3740 if (leadingBlank
&& !inCode
&& isFollowedBySpace(buf
, i
))
3745 if (leadingBlank
&& !inCode
&& newParagraph
)
3746 delimiters
~= MarkdownDelimiter(i
, 1, 0, false, false, true, c
);
3749 if (delimiters
.length
&& !inCode
&&
3750 MarkdownLink
.replaceReferenceDefinition(buf
, i
, delimiters
, cast(int) delimiters
.length
- 1, this, loc
))
3755 newParagraph
= false;
3756 leadingBlank
= false;
3760 extractedAll
= true;
3764 * Split a string by a delimiter, excluding the delimiter.
3766 * s = the string to split
3767 * delimiter = the character to split by
3768 * Returns: the resulting array of strings
3770 private static string
[] split(string s
, char delimiter
) pure @safe
3774 foreach (size_t i
; 0..s
.length
)
3775 if (s
[i
] == delimiter
)
3777 result
~= s
[iStart
..i
];
3780 result
~= s
[iStart
..$];
3787 assert(split("", ',') == [""]);
3788 assert(split("ab", ',') == ["ab"]);
3789 assert(split("a,b", ',') == ["a", "b"]);
3790 assert(split("a,,b", ',') == ["a", "", "b"]);
3791 assert(split(",ab", ',') == ["", "ab"]);
3792 assert(split("ab,", ',') == ["ab", ""]);
3796 * Create a HREF for the given D symbol.
3797 * The HREF is relative to the current location if possible.
3799 * symbol = the symbol to create a HREF for.
3800 * Returns: the resulting href
3802 private string
createHref(Dsymbol symbol
)
3804 Dsymbol root
= symbol
;
3807 while (symbol
&& symbol
.ident
&& !symbol
.isModule())
3811 lref
= symbol
.ident
.toString() ~ lref
;
3812 symbol
= symbol
.parent
;
3816 if (symbol
&& symbol
.ident
&& symbol
.isModule() != _scope
._module
)
3822 // If the module has a file name, we're done
3823 if (const m
= symbol
.isModule())
3826 path
= m
.docfile
.toString();
3832 path
= symbol
.ident
.toString() ~ path
;
3833 symbol
= symbol
.parent
;
3834 } while (symbol
&& symbol
.ident
);
3836 if (!symbol
&& path
.length
)
3837 path
~= "$(DOC_EXTENSION)";
3840 // Attempt an absolute URL if not in the same package
3843 Dsymbol scopeRoot
= _scope
._module
;
3844 while (scopeRoot
.parent
)
3845 scopeRoot
= scopeRoot
.parent
;
3846 if (scopeRoot
!= root
)
3848 path
= "$(DOC_ROOT_" ~ root
.ident
.toString() ~ ')' ~ path
;
3849 lref
= '.' ~ lref
; // remote URIs like Phobos and Mir use .prefixes
3852 return cast(string
) (path
~ '#' ~ lref
);
3856 enum TableColumnAlignment
3864 /****************************************************
3865 * Parse a Markdown table delimiter row in the form of `| -- | :-- | :--: | --: |`
3866 * where the example text has four columns with the following alignments:
3867 * default, left, center, and right. The first and last pipes are optional. If a
3868 * delimiter row is found it will be removed from `buf`.
3871 * buf = an OutBuffer containing the DDoc
3872 * iStart = the index within `buf` that the delimiter row starts at
3873 * inQuote = whether the table is inside a quote
3874 * columnAlignments = alignments to populate for each column
3875 * Returns: the index of the end of the parsed delimiter, or `0` if not found
3877 size_t
parseTableDelimiterRow(ref OutBuffer buf
, const size_t iStart
, bool inQuote
, ref TableColumnAlignment
[] columnAlignments
) @safe
3879 size_t i
= skipChars(buf
, iStart
, inQuote ?
">| \t" : "| \t");
3880 while (i
< buf
.length
&& buf
[i
] != '\r' && buf
[i
] != '\n')
3882 const leftColon
= buf
[i
] == ':';
3886 if (i
>= buf
.length || buf
[i
] != '-')
3888 i
= skipChars(buf
, i
, "-");
3890 const rightColon
= i
< buf
.length
&& buf
[i
] == ':';
3891 i
= skipChars(buf
, i
, ": \t");
3893 if (i
>= buf
.length ||
(buf
[i
] != '|' && buf
[i
] != '\r' && buf
[i
] != '\n'))
3895 i
= skipChars(buf
, i
, "| \t");
3897 columnAlignments
~= (leftColon
&& rightColon
) ? TableColumnAlignment
.center
:
3898 leftColon ? TableColumnAlignment
.left
:
3899 rightColon ? TableColumnAlignment
.right
:
3900 TableColumnAlignment
.none
;
3903 if (i
< buf
.length
&& buf
[i
] != '\r' && buf
[i
] != '\n' && buf
[i
] != ')')
3905 columnAlignments
.length
= 0;
3909 if (i
< buf
.length
&& buf
[i
] == '\r') ++i
;
3910 if (i
< buf
.length
&& buf
[i
] == '\n') ++i
;
3914 /****************************************************
3915 * Look for a table delimiter row, and if found parse the previous row as a
3916 * table header row. If both exist with a matching number of columns, start a
3920 * buf = an OutBuffer containing the DDoc
3921 * iStart = the index within `buf` that the table header row starts at, inclusive
3922 * iEnd = the index within `buf` that the table header row ends at, exclusive
3923 * loc = the current location in the file
3924 * inQuote = whether the table is inside a quote
3925 * inlineDelimiters = delimiters containing columns separators and any inline emphasis
3926 * columnAlignments = the parsed alignments for each column
3927 * Returns: the number of characters added by starting the table, or `0` if unchanged
3929 size_t
startTable(ref OutBuffer buf
, size_t iStart
, size_t iEnd
, const ref Loc loc
, bool inQuote
, ref MarkdownDelimiter
[] inlineDelimiters
, out TableColumnAlignment
[] columnAlignments
)
3931 const iDelimiterRowEnd
= parseTableDelimiterRow(buf
, iEnd
+ 1, inQuote
, columnAlignments
);
3932 if (iDelimiterRowEnd
)
3935 if (replaceTableRow(buf
, iStart
, iEnd
, loc
, inlineDelimiters
, columnAlignments
, true, delta
))
3937 buf
.remove(iEnd
+ delta
, iDelimiterRowEnd
- iEnd
);
3938 buf
.insert(iEnd
+ delta
, "$(TBODY ");
3939 buf
.insert(iStart
, "$(TABLE ");
3944 columnAlignments
.length
= 0;
3948 /****************************************************
3949 * Replace a Markdown table row in the form of table cells delimited by pipes:
3950 * `| cell | cell | cell`. The first and last pipes are optional.
3953 * buf = an OutBuffer containing the DDoc
3954 * iStart = the index within `buf` that the table row starts at, inclusive
3955 * iEnd = the index within `buf` that the table row ends at, exclusive
3956 * loc = the current location in the file
3957 * inlineDelimiters = delimiters containing columns separators and any inline emphasis
3958 * columnAlignments = alignments for each column
3959 * headerRow = if `true` then the number of columns will be enforced to match
3960 * `columnAlignments.length` and the row will be surrounded by a
3962 * delta = the number of characters added by replacing the row, or `0` if unchanged
3963 * Returns: `true` if a table row was found and replaced
3965 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
)
3969 if (!columnAlignments
.length || iStart
== iEnd
)
3972 iStart
= skipChars(buf
, iStart
, " \t");
3974 foreach (delimiter
; inlineDelimiters
)
3975 if (delimiter
.type
== '|' && !delimiter
.leftFlanking
)
3977 bool ignoreLast
= inlineDelimiters
.length
> 0 && inlineDelimiters
[$-1].type
== '|';
3980 const iLast
= skipChars(buf
, inlineDelimiters
[$-1].iStart
+ inlineDelimiters
[$-1].count
, " \t");
3981 ignoreLast
= iLast
>= iEnd
;
3986 if (headerRow
&& cellCount
!= columnAlignments
.length
)
3989 void replaceTableCell(size_t iCellStart
, size_t iCellEnd
, int cellIndex
, int di)
3991 const eDelta
= replaceMarkdownEmphasis(buf
, loc
, inlineDelimiters
, di);
3995 // strip trailing whitespace and delimiter
3996 size_t i
= iCellEnd
- 1;
3997 while (i
> iCellStart
&& (buf
[i
] == '|' || buf
[i
] == ' ' || buf
[i
] == '\t'))
4000 buf
.remove(i
, iCellEnd
- i
);
4001 delta
-= iCellEnd
- i
;
4004 buf
.insert(iCellEnd
, ")");
4007 // strip initial whitespace and delimiter
4008 i
= skipChars(buf
, iCellStart
, "| \t");
4009 buf
.remove(iCellStart
, i
- iCellStart
);
4010 delta
-= i
- iCellStart
;
4012 switch (columnAlignments
[cellIndex
])
4014 case TableColumnAlignment
.none
:
4015 buf
.insert(iCellStart
, headerRow ?
"$(TH " : "$(TD ");
4018 case TableColumnAlignment
.left
:
4019 buf
.insert(iCellStart
, "left, ");
4022 case TableColumnAlignment
.center
:
4023 buf
.insert(iCellStart
, "center, ");
4026 case TableColumnAlignment
.right
:
4027 buf
.insert(iCellStart
, "right, ");
4031 buf
.insert(iCellStart
, headerRow ?
"$(TH_ALIGN " : "$(TD_ALIGN ");
4037 int cellIndex
= cellCount
- 1;
4038 size_t iCellEnd
= iEnd
;
4039 foreach_reverse (di, delimiter
; inlineDelimiters
)
4041 if (delimiter
.type
== '|')
4043 if (ignoreLast
&& di == inlineDelimiters
.length
-1)
4049 if (cellIndex
>= columnAlignments
.length
)
4051 // kill any extra cells
4052 buf
.remove(delimiter
.iStart
, iEnd
+ delta
- delimiter
.iStart
);
4053 delta
-= iEnd
+ delta
- delimiter
.iStart
;
4054 iCellEnd
= iEnd
+ delta
;
4059 replaceTableCell(delimiter
.iStart
, iCellEnd
, cellIndex
, cast(int) di);
4060 iCellEnd
= delimiter
.iStart
;
4065 // if no starting pipe, replace from the start
4067 replaceTableCell(iStart
, iCellEnd
, cellIndex
, 0);
4069 buf
.insert(iEnd
+ delta
, ")");
4070 buf
.insert(iStart
, "$(TR ");
4075 buf
.insert(iEnd
+ delta
, ")");
4076 buf
.insert(iStart
, "$(THEAD ");
4083 /****************************************************
4084 * End a table, if in one.
4087 * buf = an OutBuffer containing the DDoc
4088 * i = the index within `buf` to end the table at
4089 * columnAlignments = alignments for each column; upon return is set to length `0`
4090 * Returns: the number of characters added by ending the table, or `0` if unchanged
4092 size_t
endTable(ref OutBuffer buf
, size_t i
, ref TableColumnAlignment
[] columnAlignments
)
4094 if (!columnAlignments
.length
)
4097 buf
.insert(i
, "))");
4098 columnAlignments
.length
= 0;
4102 /****************************************************
4103 * End a table row and then the table itself.
4106 * buf = an OutBuffer containing the DDoc
4107 * iStart = the index within `buf` that the table row starts at, inclusive
4108 * iEnd = the index within `buf` that the table row ends at, exclusive
4109 * loc = the current location in the file
4110 * inlineDelimiters = delimiters containing columns separators and any inline emphasis
4111 * columnAlignments = alignments for each column; upon return is set to length `0`
4112 * Returns: the number of characters added by replacing the row, or `0` if unchanged
4114 size_t
endRowAndTable(ref OutBuffer buf
, size_t iStart
, size_t iEnd
, const ref Loc loc
, ref MarkdownDelimiter
[] inlineDelimiters
, ref TableColumnAlignment
[] columnAlignments
)
4117 replaceTableRow(buf
, iStart
, iEnd
, loc
, inlineDelimiters
, columnAlignments
, false, delta
);
4118 delta
+= endTable(buf
, iEnd
+ delta
, columnAlignments
);
4122 /**************************************************
4123 * Highlight text section.
4126 * scope = the current parse scope
4127 * a = an array of D symbols at the current scope
4128 * 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.
4129 * buf = an OutBuffer containing the DDoc
4130 * offset = the index within buf to start highlighting
4132 void highlightText(Scope
* sc
, Dsymbols
* a
, Loc loc
, ref OutBuffer buf
, size_t offset
)
4134 const incrementLoc
= loc
.linnum
== 0 ?
1 : 0;
4135 loc
.linnum
= loc
.linnum
+ incrementLoc
;
4137 //printf("highlightText()\n");
4138 bool leadingBlank
= true;
4139 size_t iParagraphStart
= offset
;
4140 size_t iPrecedingBlankLine
= 0;
4141 int headingLevel
= 0;
4142 int headingMacroLevel
= 0;
4144 bool lineQuoted
= false;
4145 int quoteMacroLevel
= 0;
4146 MarkdownList
[] nestedLists
;
4147 MarkdownDelimiter
[] inlineDelimiters
;
4148 MarkdownLinkReferences linkReferences
;
4149 TableColumnAlignment
[] columnAlignments
;
4150 bool tableRowDetected
= false;
4154 int previousMacroLevel
= 0;
4156 size_t iCodeStart
= 0; // start of code section
4157 size_t codeFenceLength
= 0;
4158 size_t codeIndent
= 0;
4159 string codeLanguage
;
4160 size_t iLineStart
= offset
;
4161 linkReferences
._scope
= sc
;
4162 for (size_t i
= offset
; i
< buf
.length
; i
++)
4174 // `inline code` is only valid if contained on a single line
4175 // otherwise, the backticks should be output literally.
4177 // This lets things like `output from the linker' display
4178 // unmolested while keeping the feature consistent with GitHub.
4180 inCode
= false; // the backtick also assumes we're in code
4181 // Nothing else is necessary since the DDOC_BACKQUOTED macro is
4182 // inserted lazily at the close quote, meaning the rest of the
4183 // text is already OK.
4187 i
+= replaceMarkdownEmphasis(buf
, loc
, inlineDelimiters
);
4188 endMarkdownHeading(buf
, iParagraphStart
, i
, loc
, headingLevel
);
4189 removeBlankLineMacro(buf
, iPrecedingBlankLine
, i
);
4191 iParagraphStart
= skipChars(buf
, i
, " \t\r\n");
4194 if (tableRowDetected
&& !columnAlignments
.length
)
4195 i
+= startTable(buf
, iLineStart
, i
, loc
, lineQuoted
, inlineDelimiters
, columnAlignments
);
4196 else if (columnAlignments
.length
)
4199 if (replaceTableRow(buf
, iLineStart
, i
, loc
, inlineDelimiters
, columnAlignments
, false, delta
))
4202 i
+= endTable(buf
, i
, columnAlignments
);
4205 if (!inCode
&& nestedLists
.length
&& !quoteLevel
)
4206 MarkdownList
.handleSiblingOrEndingList(buf
, i
, iParagraphStart
, nestedLists
);
4208 iPrecedingBlankLine
= 0;
4209 if (!inCode
&& i
== iLineStart
&& i
+ 1 < buf
.length
) // if "\n\n"
4211 i
+= endTable(buf
, i
, columnAlignments
);
4212 if (!lineQuoted
&& quoteLevel
)
4213 endAllListsAndQuotes(buf
, i
, nestedLists
, quoteLevel
, quoteMacroLevel
);
4214 i
+= replaceMarkdownEmphasis(buf
, loc
, inlineDelimiters
);
4216 // if we don't already know about this paragraph break then
4217 // insert a blank line and record the paragraph break
4218 if (iParagraphStart
<= i
)
4220 iPrecedingBlankLine
= i
;
4221 i
= buf
.insert(i
, "$(DDOC_BLANKLINE)");
4222 iParagraphStart
= i
+ 1;
4227 i
+ 1 < buf
.length
&&
4229 quoteLevel
) // if "\n\n" in quoted code
4232 i
= buf
.insert(i
, ")");
4233 i
+= endAllMarkdownQuotes(buf
, i
, quoteLevel
);
4234 quoteMacroLevel
= 0;
4236 leadingBlank
= true;
4238 tableRowDetected
= false;
4240 loc
.linnum
= loc
.linnum
+ incrementLoc
;
4242 // update the paragraph start if we just entered a macro
4243 if (previousMacroLevel
< macroLevel
&& iParagraphStart
< iLineStart
)
4244 iParagraphStart
= iLineStart
;
4245 previousMacroLevel
= macroLevel
;
4250 leadingBlank
= false;
4253 const slice
= buf
[];
4255 const se
= sc
._module
.escapetable
.escapeChar('<');
4259 // Skip over comments
4260 if (p
[1] == '!' && p
[2] == '-' && p
[3] == '-')
4266 if (j
== slice
.length
)
4268 if (p
[0] == '-' && p
[1] == '-' && p
[2] == '>')
4270 i
= j
+ 2; // place on closing '>'
4278 // Skip over HTML tag
4279 if (isalpha(p
[1]) ||
(p
[1] == '/' && isalpha(p
[2])))
4285 if (j
== slice
.length
)
4289 i
= j
; // place on closing '>'
4299 // Replace '<' with '<' character entity
4303 i
= buf
.insert(i
, se
);
4304 i
--; // point to ';'
4311 if (leadingBlank
&& (!inCode || quoteLevel
))
4314 int lineQuoteLevel
= 1;
4315 size_t iAfterDelimiters
= i
+ 1;
4316 for (; iAfterDelimiters
< buf
.length
; ++iAfterDelimiters
)
4318 const c0
= buf
[iAfterDelimiters
];
4321 else if (c0
!= ' ' && c0
!= '\t')
4324 if (!quoteMacroLevel
)
4325 quoteMacroLevel
= macroLevel
;
4326 buf
.remove(i
, iAfterDelimiters
- i
);
4328 if (quoteLevel
< lineQuoteLevel
)
4330 i
+= endRowAndTable(buf
, iLineStart
, i
, loc
, inlineDelimiters
, columnAlignments
);
4331 if (nestedLists
.length
)
4333 const indent
= getMarkdownIndent(buf
, iLineStart
, i
);
4334 if (indent
< nestedLists
[$-1].contentIndent
)
4335 i
+= MarkdownList
.endAllNestedLists(buf
, i
, nestedLists
);
4338 for (; quoteLevel
< lineQuoteLevel
; ++quoteLevel
)
4340 i
= buf
.insert(i
, "$(BLOCKQUOTE\n");
4341 iLineStart
= iParagraphStart
= i
;
4348 if (nestedLists
.length
)
4349 MarkdownList
.handleSiblingOrEndingList(buf
, i
, iParagraphStart
, nestedLists
);
4354 leadingBlank
= false;
4357 // Replace '>' with '>' character entity
4358 const se
= sc
._module
.escapetable
.escapeChar('>');
4362 i
= buf
.insert(i
, se
);
4363 i
--; // point to ';'
4370 leadingBlank
= false;
4373 char* p
= cast(char*)&buf
[].ptr
[i
];
4374 if (p
[1] == '#' ||
isalpha(p
[1]))
4376 // already a character entity
4377 // Replace '&' with '&' character entity
4378 const se
= sc
._module
.escapetable
.escapeChar('&');
4382 i
= buf
.insert(i
, se
);
4383 i
--; // point to ';'
4390 const iAfterDelimiter
= skipChars(buf
, i
, "`");
4391 const count
= iAfterDelimiter
- i
;
4393 if (inBacktick
== count
)
4398 codebuf
.write(buf
[iCodeStart
+ count
.. i
]);
4399 // escape the contents, but do not perform highlighting except for DDOC_PSYMBOL
4400 highlightCode(sc
, a
, codebuf
, 0);
4401 escapeStrayParenthesis(loc
, codebuf
, 0, false, sc
.eSink
);
4402 buf
.remove(iCodeStart
, i
- iCodeStart
+ count
); // also trimming off the current `
4403 immutable pre
= "$(DDOC_BACKQUOTED ";
4404 i
= buf
.insert(iCodeStart
, pre
);
4405 i
= buf
.insert(i
, codebuf
[]);
4406 i
= buf
.insert(i
, ")");
4407 i
--; // point to the ending ) so when the for loop does i++, it will see the next character
4411 // Perhaps we're starting or ending a Markdown code block
4412 if (leadingBlank
&& count
>= 3)
4414 bool moreBackticks
= false;
4415 for (size_t j
= iAfterDelimiter
; !moreBackticks
&& j
< buf
.length
; ++j
)
4417 moreBackticks
= true;
4418 else if (buf
[j
] == '\r' || buf
[j
] == '\n')
4427 i
= iAfterDelimiter
- 1;
4431 inBacktick
= cast(int) count
;
4432 codeIndent
= 0; // inline code is not indented
4433 // All we do here is set the code flags and record
4434 // the location. The macro will be inserted lazily
4435 // so we can easily cancel the inBacktick if we come
4436 // across a newline character.
4438 i
= iAfterDelimiter
- 1;
4444 /* A line beginning with # indicates an ATX-style heading. */
4445 if (leadingBlank
&& !inCode
)
4447 leadingBlank
= false;
4449 headingLevel
= detectAtxHeadingLevel(buf
, i
);
4453 i
+= endRowAndTable(buf
, iLineStart
, i
, loc
, inlineDelimiters
, columnAlignments
);
4454 if (!lineQuoted
&& quoteLevel
)
4455 i
+= endAllListsAndQuotes(buf
, iLineStart
, nestedLists
, quoteLevel
, quoteMacroLevel
);
4457 // remove the ### prefix, including whitespace
4458 i
= skipChars(buf
, i
+ headingLevel
, " \t");
4459 buf
.remove(iLineStart
, i
- iLineStart
);
4460 i
= iParagraphStart
= iLineStart
;
4462 removeAnyAtxHeadingSuffix(buf
, i
);
4465 headingMacroLevel
= macroLevel
;
4474 // Perhaps we're starting or ending a Markdown code block
4475 const iAfterDelimiter
= skipChars(buf
, i
, "~");
4476 if (iAfterDelimiter
- i
>= 3)
4479 leadingBlank
= false;
4484 /* A line beginning with --- delimits a code section.
4485 * inCode tells us if it is start or end of a code section.
4489 if (!inCode
&& c
== '-')
4491 const list
= MarkdownList
.parseItem(buf
, iLineStart
, i
);
4494 if (replaceMarkdownThematicBreak(buf
, i
, iLineStart
, loc
))
4496 removeBlankLineMacro(buf
, iPrecedingBlankLine
, i
);
4497 iParagraphStart
= skipChars(buf
, i
+1, " \t\r\n");
4507 leadingBlank
= false;
4508 const c0
= c
; // if we jumped here from case '`' or case '~'
4509 size_t iInfoString
= 0;
4511 codeLanguage
.length
= 0;
4515 if (i
>= buf
.length
)
4526 if (i
+ 1 >= buf
.length
)
4528 if (buf
[i
+ 1] == '\n')
4534 // BUG: handle UTF PS and LS too
4535 if (c
!= c0 || iInfoString
)
4537 if (!iInfoString
&& !inCode
&& i
- istart
>= 3)
4539 // Start a Markdown info string, like ```ruby
4540 codeFenceLength
= i
- istart
;
4541 i
= iInfoString
= skipChars(buf
, i
, " \t");
4543 else if (iInfoString
&& c
!= '`')
4545 if (!codeLanguage
.length
&& (c
== ' ' || c
== '\t'))
4546 codeLanguage
= cast(string
) buf
[iInfoString
..i
].idup
;
4555 if (i
- istart
< 3 ||
(inCode
&& (inCode
!= c0 ||
(inCode
!= '-' && i
- istart
< codeFenceLength
))))
4559 if (!codeLanguage
.length
)
4560 codeLanguage
= cast(string
) buf
[iInfoString
..i
].idup
;
4563 codeFenceLength
= i
- istart
;
4565 // We have the start/end of a code section
4566 // Remove the entire --- line, including blanks and \n
4567 buf
.remove(iLineStart
, i
- iLineStart
+ eollen
);
4570 leadingBlank
= true;
4571 if (inCode
&& (i
<= iCodeStart
))
4573 // Empty code section, just remove it completely.
4580 // The code section is from iCodeStart to i
4582 codebuf
.write(buf
[iCodeStart
.. i
]);
4583 codebuf
.writeByte(0);
4584 // Remove leading indentations from all lines
4585 bool lineStart
= true;
4586 char* endp
= cast(char*)codebuf
[].ptr
+ codebuf
.length
;
4587 for (char* p
= cast(char*)codebuf
[].ptr
; p
< endp
;)
4591 size_t j
= codeIndent
;
4593 while (j
-- > 0 && q
< endp
&& isIndentWS(q
))
4595 codebuf
.remove(p
- cast(char*)codebuf
[].ptr
, q
- p
);
4596 assert(cast(char*)codebuf
[].ptr
<= p
);
4597 assert(p
< cast(char*)codebuf
[].ptr
+ codebuf
.length
);
4599 endp
= cast(char*)codebuf
[].ptr
+ codebuf
.length
; // update
4606 if (!codeLanguage
.length || codeLanguage
== "dlang" || codeLanguage
== "d")
4607 highlightCode2(sc
, a
, codebuf
, 0);
4609 codebuf
.remove(codebuf
.length
-1, 1); // remove the trailing 0 byte
4610 escapeStrayParenthesis(loc
, codebuf
, 0, false, sc
.eSink
);
4611 buf
.remove(iCodeStart
, i
- iCodeStart
);
4612 i
= buf
.insert(iCodeStart
, codebuf
[]);
4613 i
= buf
.insert(i
, ")\n");
4614 i
-= 2; // in next loop, c should be '\n'
4618 i
+= endRowAndTable(buf
, iLineStart
, i
, loc
, inlineDelimiters
, columnAlignments
);
4619 if (!lineQuoted
&& quoteLevel
)
4621 const delta
= endAllListsAndQuotes(buf
, iLineStart
, nestedLists
, quoteLevel
, quoteMacroLevel
);
4627 codeIndent
= istart
- iLineStart
; // save indent count
4628 if (codeLanguage
.length
&& codeLanguage
!= "dlang" && codeLanguage
!= "d")
4631 for (size_t j
; j
< codeLanguage
.length
- 1; ++j
)
4632 if (codeLanguage
[j
] == '\\' && ispunct(codeLanguage
[j
+ 1]))
4633 codeLanguage
= codeLanguage
[0..j
] ~ codeLanguage
[j
+ 1..$];
4635 i
= buf
.insert(i
, "$(OTHER_CODE ");
4636 i
= buf
.insert(i
, codeLanguage
);
4637 i
= buf
.insert(i
, ",");
4640 i
= buf
.insert(i
, "$(D_CODE ");
4642 i
--; // place i on >
4643 leadingBlank
= true;
4650 if (leadingBlank
&& !inCode
&& replaceMarkdownThematicBreak(buf
, i
, iLineStart
, loc
))
4652 i
+= endRowAndTable(buf
, iLineStart
, i
, loc
, inlineDelimiters
, columnAlignments
);
4653 if (!lineQuoted
&& quoteLevel
)
4654 i
+= endAllListsAndQuotes(buf
, iLineStart
, nestedLists
, quoteLevel
, quoteMacroLevel
);
4655 removeBlankLineMacro(buf
, iPrecedingBlankLine
, i
);
4656 iParagraphStart
= skipChars(buf
, i
+1, " \t\r\n");
4667 if (leadingBlank
&& !inCode
)
4669 MarkdownList list
= MarkdownList
.parseItem(buf
, iLineStart
, i
);
4672 // Avoid starting a numbered list in the middle of a paragraph
4673 if (!nestedLists
.length
&& list
.orderedStart
.length
&&
4674 iParagraphStart
< iLineStart
)
4676 i
+= list
.orderedStart
.length
- 1;
4680 i
+= endRowAndTable(buf
, iLineStart
, i
, loc
, inlineDelimiters
, columnAlignments
);
4681 if (!lineQuoted
&& quoteLevel
)
4683 const delta
= endAllListsAndQuotes(buf
, iLineStart
, nestedLists
, quoteLevel
, quoteMacroLevel
);
4685 list
.iStart
+= delta
;
4686 list
.iContentStart
+= delta
;
4689 list
.macroLevel
= macroLevel
;
4690 list
.startItem(buf
, iLineStart
, i
, iPrecedingBlankLine
, nestedLists
, loc
);
4694 leadingBlank
= false;
4700 if (inCode || inBacktick
)
4702 leadingBlank
= false;
4708 // Check for a thematic break
4709 if (replaceMarkdownThematicBreak(buf
, i
, iLineStart
, loc
))
4711 i
+= endRowAndTable(buf
, iLineStart
, i
, loc
, inlineDelimiters
, columnAlignments
);
4712 if (!lineQuoted
&& quoteLevel
)
4713 i
+= endAllListsAndQuotes(buf
, iLineStart
, nestedLists
, quoteLevel
, quoteMacroLevel
);
4714 removeBlankLineMacro(buf
, iPrecedingBlankLine
, i
);
4715 iParagraphStart
= skipChars(buf
, i
+1, " \t\r\n");
4719 // An initial * indicates a Markdown list item
4720 const list
= MarkdownList
.parseItem(buf
, iLineStart
, i
);
4725 // Markdown emphasis
4726 const leftC
= i
> offset ? buf
[i
-1] : '\0';
4727 size_t iAfterEmphasis
= skipChars(buf
, i
+1, "*");
4728 const rightC
= iAfterEmphasis
< buf
.length ? buf
[iAfterEmphasis
] : '\0';
4729 int count
= cast(int) (iAfterEmphasis
- i
);
4730 const leftFlanking
= (rightC
!= '\0' && !isspace(rightC
)) && (!ispunct(rightC
) || leftC
== '\0' ||
isspace(leftC
) ||
ispunct(leftC
));
4731 const rightFlanking
= (leftC
!= '\0' && !isspace(leftC
)) && (!ispunct(leftC
) || rightC
== '\0' ||
isspace(rightC
) ||
ispunct(rightC
));
4732 auto emphasis
= MarkdownDelimiter(i
, count
, macroLevel
, leftFlanking
, rightFlanking
, false, c
);
4734 if (!emphasis
.leftFlanking
&& !emphasis
.rightFlanking
)
4736 i
= iAfterEmphasis
- 1;
4740 inlineDelimiters
~= emphasis
;
4741 i
+= emphasis
.count
;
4748 leadingBlank
= false;
4753 if (i
< buf
.length
-1 && buf
[i
+1] == '[')
4755 const imageStart
= MarkdownDelimiter(i
, 2, macroLevel
, false, false, false, c
);
4756 inlineDelimiters
~= imageStart
;
4765 leadingBlank
= false;
4769 const leftC
= i
> offset ? buf
[i
-1] : '\0';
4770 const rightFlanking
= leftC
!= '\0' && !isspace(leftC
) && !ispunct(leftC
);
4771 const atParagraphStart
= leadingBlank
&& iParagraphStart
>= iLineStart
;
4772 const linkStart
= MarkdownDelimiter(i
, 1, macroLevel
, false, rightFlanking
, atParagraphStart
, c
);
4773 inlineDelimiters
~= linkStart
;
4774 leadingBlank
= false;
4779 leadingBlank
= false;
4784 for (int d
= cast(int) inlineDelimiters
.length
- 1; d
>= 0; --d
)
4786 const delimiter
= inlineDelimiters
[d
];
4787 if (delimiter
.type
== '[' || delimiter
.type
== '!')
4789 if (delimiter
.isValid
&&
4790 MarkdownLink
.replaceLink(buf
, i
, loc
, inlineDelimiters
, d
, linkReferences
))
4792 // if we removed a reference link then we're at line start
4793 if (i
<= delimiter
.iStart
)
4794 leadingBlank
= true;
4797 if (delimiter
.type
== '[')
4798 for (--d
; d
>= 0; --d
)
4799 if (inlineDelimiters
[d
].type
== '[')
4800 inlineDelimiters
[d
].invalidate();
4804 // nothing found, so kill the delimiter
4805 inlineDelimiters
= inlineDelimiters
[0..d
] ~ inlineDelimiters
[d
+1..$];
4817 leadingBlank
= false;
4821 tableRowDetected
= true;
4822 inlineDelimiters
~= MarkdownDelimiter(i
, 1, macroLevel
, leadingBlank
, false, false, c
);
4823 leadingBlank
= false;
4829 leadingBlank
= false;
4830 if (inCode || i
+1 >= buf
.length
)
4833 /* Escape Markdown special characters */
4839 auto se
= sc
._module
.escapetable
.escapeChar(c1
);
4841 se
= c1
== '$' ?
"$(DOLLAR)" : c1
== ',' ?
"$(COMMA)" : null;
4845 i
= buf
.insert(i
, se
);
4846 i
--; // point to escaped char
4854 /* Look for the start of a macro, '$(Identifier'
4856 leadingBlank
= false;
4857 if (inCode || inBacktick
)
4859 const slice
= buf
[];
4861 if (p
[1] == '(' && isIdStart(&p
[2]))
4868 if (!inCode
&& i
> offset
&& buf
[i
-1] != '$')
4876 leadingBlank
= false;
4877 if (inCode || inBacktick
)
4881 else if (macroLevel
)
4883 int downToLevel
= cast(int) inlineDelimiters
.length
;
4884 while (downToLevel
> 0 && inlineDelimiters
[downToLevel
- 1].macroLevel
>= macroLevel
)
4886 if (headingLevel
&& headingMacroLevel
>= macroLevel
)
4888 endMarkdownHeading(buf
, iParagraphStart
, i
, loc
, headingLevel
);
4889 removeBlankLineMacro(buf
, iPrecedingBlankLine
, i
);
4891 i
+= endRowAndTable(buf
, iLineStart
, i
, loc
, inlineDelimiters
, columnAlignments
);
4892 while (nestedLists
.length
&& nestedLists
[$-1].macroLevel
>= macroLevel
)
4894 i
= buf
.insert(i
, ")\n)");
4895 --nestedLists
.length
;
4897 if (quoteLevel
&& quoteMacroLevel
>= macroLevel
)
4898 i
+= endAllMarkdownQuotes(buf
, i
, quoteLevel
);
4899 i
+= replaceMarkdownEmphasis(buf
, loc
, inlineDelimiters
, downToLevel
);
4902 quoteMacroLevel
= 0;
4908 leadingBlank
= false;
4909 if (sc
._module
.filetype
== FileType
.ddoc || inCode
)
4911 const start
= cast(char*)buf
[].ptr
+ i
;
4912 if (isIdStart(start
))
4914 size_t j
= skippastident(buf
, i
);
4917 size_t k
= skippastURL(buf
, i
);
4920 /* The URL is buf[i..k]
4923 /* Leave alone if already in a macro
4928 /* Replace URL with '$(DDOC_LINK_AUTODETECT URL)'
4930 i
= buf
.bracket(i
, "$(DDOC_LINK_AUTODETECT ", k
, ")") - 1;
4938 // leading '_' means no highlight unless it's a reserved symbol name
4939 if (c
== '_' && (i
== 0 ||
!isdigit(*(start
- 1))) && (i
== buf
.length
- 1 ||
!isReservedName(start
[0 .. len
])))
4942 i
= buf
.bracket(i
, "$(DDOC_AUTO_PSYMBOL_SUPPRESS ", j
- 1, ")") - 1;
4945 if (isIdentifier(a
, start
[0 .. len
]))
4947 i
= buf
.bracket(i
, "$(DDOC_AUTO_PSYMBOL ", j
, ")") - 1;
4950 if (isKeyword(start
[0 .. len
]))
4952 i
= buf
.bracket(i
, "$(DDOC_AUTO_KEYWORD ", j
, ")") - 1;
4955 if (isFunctionParameter(a
, start
[0 .. len
]))
4957 //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j);
4958 i
= buf
.bracket(i
, "$(DDOC_AUTO_PARAM ", j
, ")") - 1;
4968 sc
.eSink
.error(loc
, "unmatched `---` in DDoc comment");
4970 buf
.insert(buf
.length
, ")");
4972 size_t i
= buf
.length
;
4975 endMarkdownHeading(buf
, iParagraphStart
, i
, loc
, headingLevel
);
4976 removeBlankLineMacro(buf
, iPrecedingBlankLine
, i
);
4978 i
+= endRowAndTable(buf
, iLineStart
, i
, loc
, inlineDelimiters
, columnAlignments
);
4979 i
+= replaceMarkdownEmphasis(buf
, loc
, inlineDelimiters
);
4980 endAllListsAndQuotes(buf
, i
, nestedLists
, quoteLevel
, quoteMacroLevel
);
4983 /**************************************************
4984 * Highlight code for DDOC section.
4986 void highlightCode(Scope
* sc
, Dsymbol s
, ref OutBuffer buf
, size_t offset
)
4988 auto imp
= s
.isImport();
4989 if (imp
&& imp
.aliases
.length
> 0)
4991 // For example: `public import core.stdc.string : memcpy, memcmp;`
4992 for(int i
= 0; i
< imp
.aliases
.length
; i
++)
4994 // Need to distinguish between
4995 // `public import core.stdc.string : memcpy, memcmp;` and
4996 // `public import core.stdc.string : copy = memcpy, compare = memcmp;`
4997 auto a
= imp
.aliases
[i
];
4998 auto id
= a ? a
: imp
.names
[i
];
4999 auto loc
= Loc
.init
;
5000 if (auto symFromId
= sc
.search(loc
, id
, null))
5002 highlightCode(sc
, symFromId
, buf
, offset
);
5009 emitAnchor(ancbuf
, s
, sc
);
5010 buf
.insert(offset
, ancbuf
[]);
5011 offset
+= ancbuf
.length
;
5015 highlightCode(sc
, &a
, buf
, offset
);
5019 /****************************************************
5021 void highlightCode(Scope
* sc
, Dsymbols
* a
, ref OutBuffer buf
, size_t offset
)
5023 //printf("highlightCode(a = '%s')\n", a.toChars());
5024 bool resolvedTemplateParameters
= false;
5026 for (size_t i
= offset
; i
< buf
.length
; i
++)
5029 const se
= sc
._module
.escapetable
.escapeChar(c
);
5033 i
= buf
.insert(i
, se
);
5034 i
--; // point to ';'
5037 char* start
= cast(char*)buf
[].ptr
+ i
;
5038 if (isIdStart(start
))
5040 size_t j
= skipPastIdentWithDots(buf
, i
);
5044 if (isIdentifier(a
, start
[0 .. len
]))
5046 i
= buf
.bracket(i
, "$(DDOC_PSYMBOL ", j
, ")") - 1;
5051 j
= skippastident(buf
, i
);
5055 if (isIdentifier(a
, start
[0 .. len
]))
5057 i
= buf
.bracket(i
, "$(DDOC_PSYMBOL ", j
, ")") - 1;
5060 if (isFunctionParameter(a
, start
[0 .. len
]))
5062 //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j);
5063 i
= buf
.bracket(i
, "$(DDOC_PARAM ", j
, ")") - 1;
5069 else if (!resolvedTemplateParameters
)
5073 // hunt for template declarations:
5074 foreach (symi
; 0 .. a
.length
)
5076 FuncDeclaration fd
= (*a
)[symi
].isFuncDeclaration();
5078 if (!fd ||
!fd
.parent ||
!fd
.parent
.isTemplateDeclaration())
5083 TemplateDeclaration td
= fd
.parent
.isTemplateDeclaration();
5085 // build the template parameters
5086 Array
!(size_t
) paramLens
;
5087 paramLens
.reserve(td
.parameters
.length
);
5089 OutBuffer parametersBuf
;
5092 parametersBuf
.writeByte('(');
5094 foreach (parami
; 0 .. td
.parameters
.length
)
5096 TemplateParameter tp
= (*td
.parameters
)[parami
];
5099 parametersBuf
.writestring(", ");
5101 size_t lastOffset
= parametersBuf
.length
;
5103 toCBuffer(tp
, parametersBuf
, hgs
);
5105 paramLens
[parami
] = parametersBuf
.length
- lastOffset
;
5107 parametersBuf
.writeByte(')');
5109 const templateParams
= parametersBuf
[];
5111 //printf("templateDecl: %s\ntemplateParams: %s\nstart: %s\n", td.toChars(), templateParams, start);
5112 if (start
[0 .. templateParams
.length
] == templateParams
)
5114 immutable templateParamListMacro
= "$(DDOC_TEMPLATE_PARAM_LIST ";
5115 buf
.bracket(i
, templateParamListMacro
.ptr
, i
+ templateParams
.length
, ")");
5117 // We have the parameter list. While we're here we might
5118 // as well wrap the parameters themselves as well
5120 // + 1 here to take into account the opening paren of the
5121 // template param list
5122 i
+= templateParamListMacro
.length
+ 1;
5124 foreach (const len
; paramLens
)
5126 i
= buf
.bracket(i
, "$(DDOC_TEMPLATE_PARAM ", i
+ len
, ")");
5127 // increment two here for space + comma
5131 resolvedTemplateParameters
= true;
5132 // reset i to be positioned back before we found the template
5133 // param list this assures that anything within the template
5134 // param list that needs to be escaped or otherwise altered
5135 // has an opportunity for that to happen outside of this context
5145 /****************************************
5147 void highlightCode3(Scope
* sc
, ref OutBuffer buf
, const(char)* p
, const(char)* pend
)
5149 for (; p
< pend
; p
++)
5151 const se
= sc
._module
.escapetable
.escapeChar(*p
);
5153 buf
.writestring(se
);
5159 /**************************************************
5160 * Highlight code for CODE section.
5162 void highlightCode2(Scope
* sc
, Dsymbols
* a
, ref OutBuffer buf
, size_t offset
)
5164 scope eSinkNull
= new ErrorSinkNull();
5166 scope Lexer lex
= new Lexer(null, cast(char*)buf
[].ptr
, 0, buf
.length
- 1, 0, 1,
5167 eSinkNull
, // ignore errors
5168 &global
.compileEnv
);
5170 const(char)* lastp
= cast(char*)buf
[].ptr
;
5171 //printf("highlightCode2('%.*s')\n", cast(int)(buf.length - 1), buf[].ptr);
5172 res
.reserve(buf
.length
);
5177 highlightCode3(sc
, res
, lastp
, tok
.ptr
);
5178 string highlight
= null;
5181 case TOK
.identifier
:
5185 size_t len
= lex
.p
- tok
.ptr
;
5186 if (isIdentifier(a
, tok
.ptr
[0 .. len
]))
5188 highlight
= "$(D_PSYMBOL ";
5191 if (isFunctionParameter(a
, tok
.ptr
[0 .. len
]))
5193 //printf("highlighting arg '%s', i = %d, j = %d\n", arg.ident.toChars(), i, j);
5194 highlight
= "$(D_PARAM ";
5200 highlight
= "$(D_COMMENT ";
5203 highlight
= "$(D_STRING ";
5206 if (tok
.isKeyword())
5207 highlight
= "$(D_KEYWORD ";
5212 res
.writestring(highlight
);
5213 size_t o
= res
.length
;
5214 highlightCode3(sc
, res
, tok
.ptr
, lex
.p
);
5215 if (tok
.value
== TOK
.comment || tok
.value
== TOK
.string_
)
5216 /* https://issues.dlang.org/show_bug.cgi?id=7656
5217 * https://issues.dlang.org/show_bug.cgi?id=7715
5218 * https://issues.dlang.org/show_bug.cgi?id=10519
5220 escapeDdocString(res
, o
);
5224 highlightCode3(sc
, res
, tok
.ptr
, lex
.p
);
5225 if (tok
.value
== TOK
.endOfFile
)
5229 buf
.setsize(offset
);
5233 /****************************************
5234 * Determine if p points to the start of a "..." parameter identifier.
5236 bool isCVariadicArg(const(char)[] p
) @nogc nothrow pure @safe
5238 return p
.length
>= 3 && p
[0 .. 3] == "...";
5241 /****************************************
5242 * Determine if p points to the start of an identifier.
5245 bool isIdStart(const(char)* p
) @nogc nothrow pure
5248 if (isalpha(c
) || c
== '_')
5253 if (utf_decodeChar(p
[0 .. 4], i
, c
))
5254 return false; // ignore errors
5261 /****************************************
5262 * Determine if p points to the rest of an identifier.
5265 bool isIdTail(const(char)* p
) @nogc nothrow pure
5268 if (isalnum(c
) || c
== '_')
5273 if (utf_decodeChar(p
[0 .. 4], i
, c
))
5274 return false; // ignore errors
5281 /****************************************
5282 * Determine if p points to the indentation space.
5284 bool isIndentWS(const(char)* p
) @nogc nothrow pure @safe
5286 return (*p
== ' ') ||
(*p
== '\t');
5289 /*****************************************
5290 * Return number of bytes in UTF character.
5292 int utfStride(const(char)* p
) @nogc nothrow pure
5298 utf_decodeChar(p
[0 .. 4], i
, c
); // ignore errors, but still consume input
5302 inout(char)* stripLeadingNewlines(inout(char)* s
) @nogc nothrow pure
5304 while (s
&& *s
== '\n' ||
*s
== '\r')