1 /// `moduleView` is a work-in-progress component used to display an overview of a module.
2 ///
3 /// Warning: This module is unstable. Significant changes may be made without prior warning.
4 ///
5 /// At the present moment, interactive playground functionality is disabled on Windows, because of how troublesome it is
6 /// to get it to run.
7 ///
8 /// This module is not enabled, unless additonal dependencies, `fluid-tree-sitter` and `fluid-tree-sitter:d` are also
9 /// compiled in.
10 module fluid.module_view;
11 
12 version (Fluid_ModuleView):
13 version (Have_fluid_tree_sitter):
14 version (Have_fluid_tree_sitter_d):
15 
16 debug (Fluid_BuildMessages) {
17     pragma(msg, "Fluid: Including moduleView");
18 }
19 
20 import lib_tree_sitter;
21 import bindbc = bindbc.loader;
22 
23 import std.conv;
24 import std.range;
25 import std.format;
26 import std.string;
27 import std.algorithm;
28 
29 import fluid.node;
30 import fluid.slot;
31 import fluid.rope;
32 import fluid.label;
33 import fluid.space;
34 import fluid.style;
35 import fluid.frame;
36 import fluid.button;
37 import fluid.structs;
38 import fluid.popup_button;
39 import fluid.code_input;
40 import fluid.tree_sitter;
41 import fluid.default_theme;
42 
43 // Disable playground functionality under Windows & macOS
44 // Hopefully this can be resolved soon
45 version (Windows)
46     version = Fluid_DisablePlayground;
47 version (OSX)
48     version = Fluid_DisablePlayground;
49 debug (Fluid_DisablePlayground)
50     version = Fluid_DisablePlayground;
51 
52 debug (Fluid_BuildMessages) {
53     version (Fluid_DisablePlayground) 
54         pragma(msg, "Fluid: moduleView: Disabling interactive playground");
55 }
56 
57 
58 @safe:
59 
60 
61 private {
62 
63     bool initialized;
64     TSQuery* documentationQuery;
65     TSQuery* dlangQuery;
66 
67 }
68 
69 static this() @system {
70 
71     // Guard to keep this only running once
72     if (initialized) return;
73     initialized = true;
74 
75     TSQueryError error;
76     uint errorOffset;
77 
78     auto language = treeSitterLanguage!"d";
79     auto query = q{
80         (
81             (comment)+ @comment
82         )
83     };
84 
85     documentationQuery = ts_query_new(language, query.ptr, cast(uint) query.length, &errorOffset, &error);
86     assert(documentationQuery, format!"%s at offset %s"(error, errorOffset));
87 
88     dlangQuery = ts_query_new(language, dQuerySource.ptr, cast(uint) dQuerySource.length, &errorOffset, &error);
89     assert(dlangQuery, format!"%s at offset %s"(error, errorOffset));
90 
91 }
92 
93 static ~this() @system {
94 
95     // Guard to keep this only running once
96     if (!initialized) return;
97     initialized = false;
98 
99     ts_query_delete(documentationQuery);
100     ts_query_delete(dlangQuery);
101 
102 }
103 
104 static immutable string[] fluidImportPaths;
105 static immutable string[] fluidExtraFlags;
106 static immutable string[] fluidExtraFlagsLDC;
107 
108 shared static this() {
109 
110     import std.path;
111     import std.process;
112     import std.file : thisExePath;
113 
114     fluidImportPaths = [
115         "source",
116         "../source",
117         environment.get("BINDBC_FREETYPE_PACKAGE_DIR").buildPath("source"),
118         environment.get("BINDBC_LOADER_PACKAGE_DIR").buildPath("source"),
119     ];
120     version (Windows) {
121         fluidExtraFlags = [
122             // scraped from LDC's linker output
123             "msvcrt.lib",
124             "vcruntime.lib",
125             "oldnames.lib",
126 
127             "fluid.lib",
128             "freetype.lib",
129             "raylib.lib",
130             "-L/LIBPATH:" ~ thisExePath.dirName,
131             "-L/LIBPATH:" ~ environment.get("FLUID_LIBPATH", "."),
132         ];
133         fluidExtraFlagsLDC = fluidExtraFlags ~ "--link-defaultlib-shared";
134     }
135     else {
136         fluidExtraFlags = [];
137         fluidExtraFlagsLDC = [
138             "-L-lfluid",
139             "-L-lfreetype",
140             "-L-L" ~ thisExePath.dirName,
141             "-L-L" ~ environment.get("FLUID_LIBPATH", "."),
142         ];
143     }
144 }
145 
146 /// Provides information about the companion D compiler to use for evaluating examples.
147 struct DlangCompiler {
148 
149     import std.regex;
150     import std.process;
151 
152     enum Type {
153         dmd,
154         ldc,
155     }
156 
157     /// Type (vendor) of the compiler. Either DMD or LDC.
158     Type type;
159 
160     /// Executable name or path to the compiler executable. If null, no compiler is available.
161     string executable;
162 
163     /// DMD frontend version of the compiler. Major version is assumed to be 2.
164     int frontendMinor;
165 
166     /// ditto
167     int frontendPatch;
168 
169     /// Import paths to pass to the compiler.
170     const(string)[] importPaths;
171 
172     /// ditto
173     enum frontendMajor = 2;
174 
175     /// Returns true if this entry points to a valid compiler.
176     bool opCast(T : bool)() const {
177 
178         return executable !is null
179             && frontendMinor != 0;
180 
181     }
182 
183     /// Find any suitable in the system.
184     static DlangCompiler findAny() {
185 
186         if (auto explicitD = environment.get("DC", null)) {
187 
188             if (explicitD.canFind("dmd"))
189                 return findDMD();
190             else
191                 return findLDC();
192 
193         }
194 
195         return either(
196             findDMD,
197             findLDC,
198         );
199 
200     }
201 
202     /// Find DMD in the system.
203     static DlangCompiler findDMD() {
204 
205         version (Fluid_DisablePlayground) {
206             return DlangCompiler.init;
207         }
208         else {
209 
210             // According to run.dlang.io, this pattern has been used since at least 2.068.2
211             auto pattern = regex(r"D Compiler v2.(\d+).(\d+)");
212             auto explicitDMD = std.process.environment.get("DMD");
213             auto candidates = explicitDMD.empty
214                 ? ["dmd"]
215                 : [explicitDMD];
216 
217             // Test the executables
218             foreach (name; candidates) {
219 
220                 try {
221 
222                     auto process = execute([name, "--version"]);
223 
224                     // Found a compatible compiler
225                     if (auto match = process.output.matchFirst(pattern)) {
226 
227                         return DlangCompiler(Type.dmd, name, match[1].to!int, match[2].to!int, fluidImportPaths);
228 
229                     }
230 
231                 }
232 
233                 catch (ProcessException) {
234                     continue;
235                 }
236 
237             }
238 
239             return DlangCompiler.init;
240 
241         }
242 
243     }
244 
245     /// Find LDC in the system.
246     static DlangCompiler findLDC() {
247 
248         version (Fluid_DisablePlayground) {
249             return DlangCompiler.init;
250         }
251         else {
252 
253             // This pattern appears to be stable as, according to the blame, hasn't changed in at least 5 years
254             auto pattern = regex(r"based on DMD v2\.(\d+)\.(\d+)");
255             auto explicitLDC = std.process.environment.get("LDC");
256             auto candidates = explicitLDC.empty
257                 ? ["ldc2", "ldc"]
258                 : [explicitLDC];
259 
260             // Test the executables
261             foreach (name; candidates) {
262 
263                 try {
264 
265                     auto process = execute([name, "--version"]);
266 
267                     // Found a compatible compiler
268                     if (auto match = process.output.matchFirst(pattern)) {
269 
270                         return DlangCompiler(Type.ldc, name, match[1].to!int, match[2].to!int, fluidImportPaths);
271 
272                     }
273 
274                 }
275 
276                 catch (ProcessException) {
277                     continue;
278                 }
279 
280             }
281 
282             return DlangCompiler.init;
283 
284         }
285 
286     }
287 
288     unittest {
289 
290         import std.stdio;
291 
292         auto dmd = findDMD();
293         auto ldc = findLDC();
294 
295         // Output search results
296         // Correctness of these tests has to be verified by the CI runner script or the programmer
297         if (dmd) {
298             writefln!"Found DMD (%s) 2.%s.%s"(dmd.executable, dmd.frontendMinor, dmd.frontendPatch);
299             assert(dmd.type == Type.dmd);
300         }
301         else
302             writefln!"DMD wasn't found";
303 
304         if (ldc) {
305             writefln!"Found LDC (%s) compatible with DMD 2.%s.%s"(ldc.executable, ldc.frontendMinor, ldc.frontendPatch);
306             assert(ldc.type == Type.ldc);
307         }
308         else
309             writefln!"LDC wasn't found";
310 
311         // Leading zeros have to be ignored
312         assert("068".to!int == 68);
313 
314         // Compare results of the compiler-agnostic and compiler-specific functions
315         if (auto compiler = findAny()) {
316 
317             final switch (compiler.type) {
318 
319                 case Type.dmd:
320                     assert(dmd);
321                     break;
322 
323                 case Type.ldc:
324                     assert(ldc);
325                     break;
326 
327             }
328 
329         }
330 
331         // No compiler found
332         else {
333 
334             assert(!dmd);
335             assert(!ldc);
336 
337         }
338 
339     }
340 
341     /// Get the flag for importing from given directory.
342     string importFlag(string directory) const {
343 
344         return "-I" ~ directory;
345 
346     }
347 
348     /// Get the flag for adding all import directories specified in compiler config.
349     string[] importPathsFlag() const {
350 
351         return importPaths
352             .map!(a => importFlag(a))
353             .array;
354 
355     }
356 
357     /// Get the flag to generate a debug binary for the current compiler.
358     const(string)[] debugFlags() const {
359 
360         static immutable flags = ["-g", "-debug"];
361 
362         final switch (type) {
363 
364             case Type.dmd: return flags[];
365             case Type.ldc: return flags[0..1];
366 
367         }
368 
369     }
370 
371     /// Get the flag to generate a shared library for the given compiler.
372     string sharedLibraryFlag() const {
373 
374         final switch (type) {
375 
376             case Type.dmd: return "-shared";
377             case Type.ldc: return "--shared";
378 
379         }
380 
381     }
382 
383     /// Get the flag to include unittests.
384     string unittestFlag() const {
385 
386         final switch (type) {
387 
388             case Type.dmd: return "-unittest";
389             case Type.ldc: return "--unittest";
390 
391         }
392 
393     }
394 
395     /// Extra, platform-specific flags needed to build a shared library.
396     const(string)[] extraFlags() const {
397 
398         final switch (type) {
399 
400             case Type.dmd: return fluidExtraFlags;
401             case Type.ldc: return fluidExtraFlagsLDC;
402 
403         }
404 
405     }
406 
407     /// Compile a shared library from given source file.
408     ///
409     /// TODO make this async
410     auto compileSharedLibrary(string source, out string outputPath) const
411     in (this)
412     do {
413 
414         import fs = std.file;
415         import random = std.random;
416         import std.path : buildPath, setExtension;
417 
418         static string path;
419 
420         // Build a path to contain the program's source
421         if (!path)
422             path = fs.tempDir.buildPath("fluid_" ~ random.uniform!uint.to!string ~ ".d");
423 
424         // Write the source
425         fs.write(path, source);
426 
427         // Find the correct extension for the system
428         version (Windows)
429             outputPath = path.setExtension(".dll");
430         else version (OSX)
431             outputPath = path.setExtension(".dylib");
432         else
433             outputPath = path.setExtension(".so");
434 
435         auto cmdline = [executable, sharedLibraryFlag, unittestFlag, path, "-of=" ~ outputPath] 
436             ~ debugFlags
437             ~ importPathsFlag
438             ~ extraFlags;
439 
440         debug (Fluid_BuildMessages) {
441             import std.stdio;
442             writefln!"Fluid: compiling $ %s"(escapeShellCommand(cmdline));
443         }
444 
445         // Compile the program
446         auto result = execute(cmdline);
447 
448         debug (Fluid_BuildMessages) {
449             import std.stdio;
450             if (result.status == 0)
451                 writefln!"Fluid: Compilation succeeded.";
452             else
453                 writefln!"Fluid: Compilation failed.\n%s"(result.output.stripRight);
454         }
455         
456         return result;
457 
458     }
459 
460 }
461 
462 /// Create an overview display of the given module.
463 Frame moduleViewSource(Params...)(Params params, DlangCompiler compiler, string source,
464     Theme contentTheme = fluidDefaultTheme) @trusted
465 do {
466 
467     auto language = treeSitterLanguage!"d";
468     auto parser = ts_parser_new();
469     scope (exit) ts_parser_delete(parser);
470 
471     ts_parser_set_language(parser, language);
472 
473     // Parse the source
474     auto tree = ts_parser_parse_string(parser, null, source.ptr, cast(uint) source.length);
475     scope (exit) ts_tree_delete(tree);
476     auto root = ts_tree_root_node(tree);
477     auto cursor = ts_query_cursor_new();
478     scope (exit) ts_query_cursor_delete(cursor);
479 
480     auto view = ModuleView(compiler, source, contentTheme);
481     auto result = vframe(params);
482 
483     if (compiler == compiler.init) {
484 
485         version (Fluid_DisablePlayground) {
486             auto message = [
487                 label("Warning: Interactive playground is disabled on this platform. See issue #182 for more details."),
488                 button("Open #182 in browser", delegate {
489                     import fluid.utils;
490                     openURL("https://git.samerion.com/Samerion/Fluid/issues/182");
491                 }),
492             ];
493         }
494         else {
495             auto message = [
496                 label("Warning: No suitable D compiler could be found; interactive playground is disabled.")
497             ];
498         }
499         
500         result ~= vframe(
501             .layout!"fill",
502             .tags!(FluidTag.warning),
503             message,
504         );
505     }
506 
507     // Perform a query to find possibly relevant comments
508     ts_query_cursor_exec(cursor, documentationQuery, root);
509     TSQueryMatch match;
510     captures: while (ts_query_cursor_next_match(cursor, &match)) {
511 
512         auto captures = match.captures[0 .. match.capture_count];
513         auto node = captures[$-1].node;
514 
515         // Load the comment
516         auto docs = readDocs(source, captures)
517             .interpretDocs;
518 
519         // Find the symbol the comment is attached to
520         while (true) {
521 
522             node = ts_node_next_named_sibling(node);
523             if (ts_node_is_null(node)) break;
524 
525             // Once found, annotate and append to result
526             if (auto annotated = view.annotate(docs, node)) {
527                 result ~= annotated;
528                 continue captures;
529             }
530 
531         }
532 
533         // Nothing relevant found, paste the documentation as-is
534         result ~= docs;
535 
536     }
537 
538     return result;
539 
540 }
541 
542 /// Load the module source from a file.
543 Frame moduleViewFile(Params...)(Params params, DlangCompiler compiler, string filename,
544     Theme contentTheme = fluidDefaultTheme)
545 do {
546 
547     import std.file : readText;
548 
549     return moduleViewSource!Params(params, compiler, filename.readText, contentTheme);
550 
551 }
552 
553 private struct ModuleView {
554 
555     DlangCompiler compiler;
556     string source;
557     Theme contentTheme;
558     int unittestNumber;
559 
560     /// Returns:
561     ///     Space to represent the node in the output, or `null` if the given TSNode doesn't correspond to any known valid
562     ///     symbol.
563     Space annotate(Space documentation, TSNode node) @trusted {
564 
565         const typeC = ts_node_type(node);
566         const type = typeC[0 .. strlen(typeC)];
567 
568         const start = ts_node_start_byte(node);
569         const end = ts_node_end_byte(node);
570         const symbolSource = source[start .. end];
571 
572         switch (type) {
573 
574             // unittest
575             case "unittest_declaration":
576 
577                 // Create the code block
578                 auto input = dlangInput();
579 
580                 // Find the surrounding context
581                 const exampleStart = start + symbolSource.countUntil("{") + 1;
582                 const exampleEnd = end - symbolSource.retro.countUntil("}") - 1;
583 
584                 // Set a constant mangle for the example by injecting it into source
585                 const injectSource = Rope(q{
586                     pragma(mangle, "fluid_moduleView_entrypoint")
587                 });
588 
589                 const prefix = source[0 .. start] ~ injectSource ~ source[start .. exampleStart];
590                 const value = source[exampleStart .. exampleEnd];
591                 const suffix = Rope(source[exampleEnd .. $]);
592                 
593                 // Append code editor to the result
594                 documentation.children ~= exampleView(compiler, prefix, value, suffix, contentTheme);
595                 return documentation;
596 
597             // Declarations that aren't implemented
598             case "module_declaration":
599             case "import_declaration":
600             case "mixin_declaration":
601             case "variable_declaration":
602             case "auto_declaration":
603             case "alias_declaration":
604             case "attribute_declaration":
605             case "pragma_declaration":
606             case "struct_declaration":
607             case "union_declaration":
608             case "invariant_declaration":
609             case "class_declaration":
610             case "interface_declaration":
611             case "enum_declaration":
612             case "anonymous_enum_declaration":
613             case "function_declaration":
614             case "template_declaration":
615             case "mixin_template_declaration":
616                 return documentation;
617 
618             // Unknown declaration, skip
619             default:
620                 return null;
621 
622         }
623 
624     }
625 
626 }
627 
628 /// Produce an example.
629 Frame exampleView(DlangCompiler compiler, CodeInput input, Theme contentTheme) {
630 
631     auto stdoutLabel = label(.layout!"fill", "");
632     auto resultCanvas = nodeSlot!Frame(.layout!"fill");
633 
634     /// Wrapper over bindbc.SharedLib to ensure proper library destruction.
635     struct SharedLib {
636 
637         bindbc.SharedLib library;
638         alias library this;
639 
640         // BUG  this crashes the tour on exit
641         //      without this, memory is leaked
642         version (none)
643         ~this() {
644             clear();
645         }
646 
647         void clear() @trusted {
648             if (library != bindbc.invalidHandle) {
649                 bindbc.unload(library);
650             }
651         }
652 
653     }
654 
655     auto library = new SharedLib;
656     auto originalValue = input.value;
657 
658     /// Compile the program
659     void compileAndRun() @trusted {
660 
661         string outputPath;
662 
663         // Unload the previous library
664         library.clear();
665 
666         auto result = compiler.compileSharedLibrary(input.sourceValue.to!string, outputPath);
667 
668         // Compiled successfully
669         if (result.status == 0) {
670 
671             // Prepare the output
672             resultCanvas.value = vframe(
673                 .layout!(1, "fill"),
674                 contentTheme,
675             );
676             resultCanvas.show();
677             stdoutLabel.hide();
678 
679             // Run the snippet and output the results
680             mockRun((node) {
681                 resultCanvas.value ~= node;
682             });
683             scope (exit) mockRun(null);
684 
685             // TODO Run the result on a separate thread to prevent the app from locking up
686             library.library = runSharedLibrary(outputPath);
687             resultCanvas.updateSize();
688 
689         }
690 
691         // Write compiler output
692         else {
693 
694             stdoutLabel.show();
695             stdoutLabel.text = result.output;
696             resultCanvas.hide();
697 
698         }
699 
700     }
701 
702     /// Restore original source code.
703     void restoreSource() {
704 
705         input.value = originalValue;
706         input.updateSize();
707 
708     }
709 
710     // Compile and run the program; do the same when submitted
711     if (compiler) {
712 
713         compileAndRun();
714         input.submitted = &compileAndRun;
715 
716         Frame root;
717 
718         return root = hframe(
719             .layout!"fill",
720             vspace(
721                 .layout!(1, "fill"),
722                 hframe(
723                     button("Run", () => compileAndRun()),
724                     popupButton("Reset",
725                         label("Reset original content? Changes\n"
726                             ~ "will not be saved."),
727                         hspace(
728                             .layout!"end",
729                             button("Cancel", delegate {
730 
731                                 root.tree.focus = null;
732 
733                             }),
734                             button("Reset content", delegate {
735 
736                                 restoreSource();
737                                 root.tree.focus = null;
738 
739                             }),
740                         ),
741                     ),
742                 ),
743                 input,
744             ),
745             vspace(
746                 .layout!(1, "fill"),
747                 resultCanvas,
748                 stdoutLabel,
749             ),
750         );
751 
752     }
753 
754     // Disable edits if there's no compiler available
755     else return hframe(
756         .layout!"fill",
757         input.disable(),
758     );
759 
760 }
761 
762 /// ditto
763 Frame exampleView(DlangCompiler compiler, Rope prefix, string value, Rope suffix, Theme contentTheme) {
764 
765     auto input = dlangInput();
766     input.prefix = prefix ~ "\n";
767     input.suffix = "\n" ~ suffix;
768     input.value = value
769         .outdent
770         .strip;
771 
772     return exampleView(compiler, input, contentTheme);
773 
774 }
775 
776 /// Run a Fluid snippet from a shared library.
777 private bindbc.SharedLib runSharedLibrary(string path) @system {
778 
779     import core.stdc.stdio;
780 
781     // Load the resulting library
782     auto library = bindbc.load(path.toStringz);
783 
784     // Failed to load
785     if (library == bindbc.invalidHandle) {
786 
787         foreach (error; bindbc.errors)
788             fprintf(stderr, "%s %s\n", error.error, error.message);
789         fflush(stderr);
790         bindbc.resetErrors();
791 
792         return library;
793 
794     }
795 
796     void function() entrypoint;
797     bindbc.bindSymbol(library, cast(void**) &entrypoint, "fluid_moduleView_entrypoint");
798     entrypoint();
799 
800     return library;
801 
802 }
803 
804 @system unittest {
805 
806     import std.path;
807 
808     auto source = q{
809         import fluid;
810 
811         pragma(mangle, "fluid_moduleView_entrypoint")
812         void entrypoint() {
813             run(label("Hello, World!"));
814         }
815     };
816 
817     // Configure the compiler
818     auto compiler = DlangCompiler.findAny();
819 
820     version (Fluid_DisablePlayground) {
821         assert(compiler == DlangCompiler.init);
822     }
823 
824     else {
825 
826         // Compile the pogram
827         string outputPath;
828         auto program = compiler.compileSharedLibrary(source, outputPath);
829         assert(program.status == 0, program.output);
830 
831         // Prepare the environment
832         Node output;
833         mockRun = delegate (node) {
834             output = node;
835         };
836         scope (exit) mockRun = null;
837     
838         // Make sure it could be loaded
839         auto library = runSharedLibrary(outputPath);
840         assert(library != bindbc.invalidHandle);
841         scope (exit) bindbc.unload(library);
842 
843         // And test the output
844         auto result = cast(Label) output;
845         assert(result.text == "Hello, World!");
846 
847     }
848 
849 }
850 
851 /// Creates a `CodeInput` with D syntax highlighting.
852 CodeInput dlangInput(void delegate() @safe submitted = null) @trusted {
853 
854     auto language = treeSitterLanguage!"d";
855     auto highlighter = new TreeSitterHighlighter(language, dlangQuery);
856 
857     return codeInput(
858         .layout!(1, "fill"),
859         highlighter,
860         submitted
861     );
862 
863 }
864 
865 private Rope readDocs(string source, TSQueryCapture[] captures) @trusted {
866 
867     import std.stdio : writefln;
868 
869     const lineFeed = Rope("\n");
870 
871     Rope result;
872 
873     // Load all comments
874     foreach (capture; captures) {
875 
876         auto start = ts_node_start_byte(capture.node);
877         auto end = ts_node_end_byte(capture.node);
878         auto commentSource = source[start .. end];
879 
880         // TODO multiline comments
881         // Filter
882         if (!commentSource.skipOver("///")) continue;
883 
884         result ~= Rope(Rope(commentSource), lineFeed);
885 
886     }
887 
888     return result;
889 
890 }
891 
892 private Space interpretDocs(Rope rope) {
893 
894     import std.conv : to;
895     import fluid.typeface : Typeface;
896 
897     const space = Rope(" ");
898     const lineFeed = Rope("\n");
899 
900     rope = rope.strip;
901 
902     // Empty comment, omit
903     if (rope == "") return vspace();
904 
905     // Ditto comment, TODO
906     if (rope == "ditto") return vspace();
907 
908     // TODO DDoc
909     CodeInput lastCode;
910     auto lastParagraph = label("");
911     auto result = vspace(
912         .layout!"fill",
913         lastParagraph
914     );
915 
916     string preformattedDelimiter;
917 
918     // Read line by line
919     foreach (line; Typeface.lineSplitter(rope)) {
920 
921         // Regular documentation line
922         if (preformattedDelimiter.empty) {
923 
924             line = line.strip();
925 
926             // Start a new paragraph if the line is blank
927             if (line.empty) {
928                 if (!lastParagraph.text.empty)
929                     result ~= lastParagraph = label("");
930             }
931 
932             // Preformatted line
933             // TODO other delimiters
934             // TODO common space (prefix)
935             else if (line == "---") {
936                 preformattedDelimiter = "---";
937                 result ~= lastCode = dlangInput().disable();
938             }
939 
940             // Append text to previous line
941             else {
942                 lastParagraph.text ~= Rope(line, space);
943             }
944 
945         }
946 
947         // Preformatted fragments/code
948         else {
949 
950             // Reached the other delimiter, turn preformatted lines off
951             if (line.strip == preformattedDelimiter) {
952                 preformattedDelimiter = null;
953                 result ~= lastParagraph = label("");
954                 lastCode.value = lastCode.value.to!string.outdent;
955             }
956 
957             /// Append text to previous line
958             else {
959                 lastCode.push(Rope(line, lineFeed));
960             }
961 
962         }
963 
964     }
965 
966     return result;
967 
968 }
969 
970 /// Outdent a rope.
971 ///
972 /// A saner wrapper over `std.string.outdent` that actually does what it should do.
973 private string outdent(string rope) {
974 
975     import std.string : outdent;
976 
977     return rope.splitLines.outdent.join("\n");
978 
979 }