Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for linking to section headings #783

Merged
merged 10 commits into from
Jul 31, 2023
Merged
13 changes: 13 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
version: 2
updates:

- package-ecosystem: github-actions
directory: "/"
schedule:
interval: daily
time: '00:00'
timezone: UTC
open-pull-requests-limit: 10
commit-message:
prefix: "chore"
include: "scope"
4 changes: 4 additions & 0 deletions dub.sdl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ dependency "mustache-d" version="~>0.1"

targetPath "build"

// debugVersions "CodeHash"

dflags "-preview=shortenedMethods"

configuration "executable" {
targetType "executable"
versions "VibeDefaultMain"
Expand Down
15 changes: 8 additions & 7 deletions dub.selections.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@
"versions": {
"botan": "1.12.19",
"botan-math": "1.0.3",
"dfmt": "0.14.2",
"dfmt": "0.15.1",
"diet-ng": "1.8.1",
"dyaml": "0.9.2",
"eventcore": "0.9.22",
"eventcore": "0.9.25",
"libasync": "0.8.6",
"libdparse": "0.22.0",
"memutils": "1.0.4",
"libdparse": "0.23.2",
"memutils": "1.0.9",
"mir-linux-kernel": "1.0.1",
"mustache-d": "0.1.5",
"openssl": "3.2.2",
"openssl": "3.3.0",
"openssl-static": "1.0.2+3.0.8",
"stdx-allocator": "2.77.5",
"taggedalgebraic": "0.11.22",
"tinyendian": "0.2.0",
"vibe-core": "1.22.6",
"vibe-d": "0.9.5"
"vibe-core": "2.2.0",
"vibe-d": "0.9.6"
}
}
2 changes: 1 addition & 1 deletion public/content/de
2 changes: 1 addition & 1 deletion public/content/en
2 changes: 1 addition & 1 deletion public/content/ja
2 changes: 1 addition & 1 deletion public/content/pt-br
Submodule pt-br updated 3 files
+27 −0 .github/workflows/d.yml
+0 −19 .travis.yml
+1 −1 README.md
2 changes: 1 addition & 1 deletion public/content/tr
Submodule tr updated 3 files
+27 −0 .github/workflows/d.yml
+0 −19 .travis.yml
+1 −1 README.md
2 changes: 1 addition & 1 deletion public/content/uk
Submodule uk updated 3 files
+27 −0 .github/workflows/d.yml
+0 −19 .travis.yml
+1 −1 README.md
2 changes: 1 addition & 1 deletion public/content/zh
Submodule zh updated 63 files
+3 −3 README.md
+43 −67 basics/alias-strings.md
+18 −16 basics/arrays.md
+44 −44 basics/associative-arrays.md
+8 −7 basics/basic-types.md
+36 −48 basics/classes.md
+7 −7 basics/controlling-flow.md
+11 −13 basics/delegates.md
+25 −38 basics/exceptions.md
+20 −29 basics/foreach.md
+24 −40 basics/functions.md
+3 −3 basics/further-reading.md
+31 −7 basics/imports-and-modules.md
+1 −1 basics/index.yml
+34 −49 basics/interfaces.md
+30 −29 basics/loops.md
+4 −4 basics/memory.md
+144 −73 basics/ranges.md
+35 −55 basics/slices.md
+36 −83 basics/structs.md
+27 −45 basics/templates.md
+6 −6 basics/type-qualifiers.md
+93 −0 byexample/code-generation-parser.md
+3 −0 byexample/index.yml
+20 −0 dub/emsi_containers.md
+10 −0 dub/index.yml
+44 −0 dub/libdparse.md
+42 −0 dub/lubeck.md
+38 −0 dub/mir-algorithm.md
+30 −0 dub/mir-random.md
+42 −0 dub/mir.md
+54 −0 dub/pegged.md
+26 −0 dub/vibe-d.md
+9 −5 gems/attributes.md
+19 −19 gems/bit-manipulation.md
+7 −7 gems/compile-time-function-evaluation-ctfe.md
+15 −15 gems/contract-programming.md
+8 −8 gems/documentation.md
+16 −13 gems/functional-programming.md
+3 −3 gems/opdispatch-opapply.md
+12 −11 gems/range-algorithms.md
+5 −4 gems/scope-guards.md
+10 −7 gems/string-mixins.md
+18 −19 gems/subtyping.md
+7 −7 gems/template-meta-programming.md
+10 −8 gems/traits.md
+20 −20 gems/unicode.md
+9 −9 gems/uniform-function-call-syntax-ufcs.md
+13 −11 gems/unittesting.md
+3 −1 index.yml
+2 −2 multithreading/fibers.md
+12 −11 multithreading/message-passing.md
+10 −9 multithreading/synchronization-sharing.md
+1 −1 multithreading/thread-local-storage.md
+10 −10 vibed/basics-asynchronous-i-o.md
+2 −2 vibed/deploy-on-heroku.md
+7 −4 vibed/diet-templates.md
+7 −6 vibed/json-rest-interface.md
+15 −17 vibed/vibe-d-web-framework.md
+12 −8 vibed/web-server.md
+28 −30 welcome/install-d-locally.md
+2 −2 welcome/run-d-program-locally.md
+2 −2 welcome/welcome-to-d.md
4 changes: 4 additions & 0 deletions public/static/css/common.css
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,7 @@ pre code {
.table-hover>tbody>tr:hover {
background-color: #f5f5f5;
}

:target {
background-color: yellow;
}
8 changes: 8 additions & 0 deletions public/static/js/tour-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ if (location.origin.indexOf("run.dlang.io") >= 0 || location.pathname.startsWith
requireBase: false
});
}]);
} else {
dlangTourApp.config(['$locationProvider', function($locationProvider) {
$locationProvider.html5Mode({
enabled: true,
requireBase: false,
rewriteLinks: false,
});
}]);
}

dlangTourApp.controller('DlangTourAppCtrl',
Expand Down
4 changes: 2 additions & 2 deletions source/app.d
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private IExecProvider createExecProvider(Config config,

if (config.enableExecCache) {
import std.algorithm: map;
auto allowedSources = contentProvider.getContent()
auto allowedSources = contentProvider.getAllContent()
.map!(x => x.sourceCode.idup)
.array;
return new Cache(execProvider, allowedSources);
Expand Down Expand Up @@ -110,7 +110,7 @@ private void doSanityCheck(ContentProvider contentProvider, IExecProvider execPr

import std.parallelism : parallel;

auto content = contentProvider.getContent();
auto content = contentProvider.getAllContent();

if (cast(Docker) execProvider)
runChecks(content.parallel);
Expand Down
134 changes: 84 additions & 50 deletions source/contentprovider.d
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import vibe.core.log;
import vibe.textfilter.markdown;
import vibe.core.log: logInfo, logError;
import vibe.textfilter.markdown: MarkdownFlags, MarkdownSettings, filterMarkdown;

import std.file;
import std.file: exists, readText, dirEntries, isDir, SpanMode;
import std.algorithm: splitter, filter, countUntil;
import std.array: array, empty;
import std.string: split, strip;
import std.typecons: Tuple;
import std.exception: enforce;
import std.string: format;
import std.path: baseName, buildPath;

import dyaml;
import mustache;
import dyaml: Loader;

import translator;
import translator: Translator;

alias MustacheEngine!(string) Mustache;
alias enforce = imported!"std.exception".enforce!ContentProviderException;
alias Mustache = imported!"mustache".MustacheEngine!(string);

class ContentProviderException : Exception
{
mixin imported!"std.exception".basicExceptionCtors;
}

/++
Manages the mark down files found in public/content
Expand All @@ -29,7 +33,7 @@ class ContentProvider
private immutable SourceCodeSectionTitle ="{SourceCode";
private immutable SourceCodeDisabled = ":disabled";
private immutable SourceCodeIncomplete = ":incomplete";
private immutable SourceCodeFullWidth =":fullWidth";
private immutable SourceCodeFullWidth = ":fullWidth";
private immutable SourceCodeMaxCharsPerLine = 48;

private {
Expand Down Expand Up @@ -102,19 +106,50 @@ class ContentProvider
return content;
}

private bool isValidLink(string link, string language)
private auto isValidLink(string link, string language)
{
import std.algorithm.searching : count;
if (link.count("/") != 1)
return false;

// check for existence in file system
import std.array : split;
auto parts = link.split("/");

static struct Result
{
string baseDir, path, fragment;

enum invalid = typeof(this).init;

bool opCast(T: bool)() => !!path;
string filename() => buildPath(baseDir, path ~ ".md");
string fragmentSuffix() => !fragment ? "" : '#' ~ fragment;
string routePath() => "/tour/%s/%s"
.format(baseDir.baseName, path ~ fragmentSuffix);
}

enforce(language in langWithPath, "Language hasn't been seen before.");
string fileName = buildPath(langWithPath[language], parts[0], parts[1] ~ ".md");
import std.stdio;
return exists(fileName);

auto parts = link.split("/");

if (parts.length != 2 || parts[0].empty || parts[1].empty)
return Result.invalid;

// Support links to chapter/section#heading
auto sectionAndFragment = parts[1].split("#");
parts[1] = sectionAndFragment[0];

Result r = {
baseDir: langWithPath[language],
path: buildPath(parts[0], parts[1]),
fragment: sectionAndFragment.length == 2
? sectionAndFragment[1]
: null,
};

enforce(r.fragment is null || r.fragment.length > 0,
"Fragment following # must not be empty.");

// check for existence in file system
if (!exists(r.filename))
return Result.invalid;

return r;
}

this(string contentDirectory)
Expand Down Expand Up @@ -170,8 +205,7 @@ class ContentProvider
langMeta.translator = new Translator();
}

import std.meta : AliasSeq;
foreach (attr; AliasSeq!("title", "repo"))
foreach (attr; imported!"std.meta".AliasSeq!("title", "repo"))
{
enforce(attr in root, "'" ~ attr ~ "' point required in language-specific yaml");
mixin("langMeta." ~ attr ~ " = root[attr].as!string;");
Expand Down Expand Up @@ -212,12 +246,12 @@ class ContentProvider
scope (failure) logError("lang: %s, chapter: %s, section: %s failed", language, chapter, currentSection);
foreach (ref section; splitMarkdownBySection(readText(filename))) {
if (section.title.startsWith(SourceCodeSectionTitle)) {
enforce(section.level == 2, new Exception("%s: %s section expected to be on 2nd level"
.format(filename, SourceCodeSectionTitle)));
enforce(!content.html.empty, new Exception("%s: %s section must be within existing section."
.format(filename, SourceCodeSectionTitle)));
enforce(content.sourceCode.empty, new Exception("%s: Double %s section in '%s'"
.format(filename, SourceCodeSectionTitle, content.title)));
enforce(section.level == 2, "%s: %s section expected to be on 2nd level"
.format(filename, SourceCodeSectionTitle));
enforce(!content.html.empty, "%s: %s section must be within existing section."
.format(filename, SourceCodeSectionTitle));
enforce(content.sourceCode.empty, "%s: Double %s section in '%s'"
.format(filename, SourceCodeSectionTitle, content.title));
content.filename = filename;
content.sourceCode = section.bodyOnly;
// ignore markdown code blocks
Expand All @@ -235,14 +269,14 @@ class ContentProvider
checkSourceCodeLineWidth(content.sourceCode, content.title);
} else if (section.level == 1) {
enforce(content.title.length == 0,
new Exception("%s: Just one chapter title allowed: %s".format(filename, section.title)));
"%s: Just one chapter title allowed: %s".format(filename, section.title));
content.title = section.title;
content.html = processMarkdown(section.content, language);
} else if (section.level >= 2) {
enforce(content.title.length != 0, new Exception("%s: level 3 section can't be first (%s)".format(filename, section.title)));
enforce(content.title.length != 0, "%s: level 3 section can't be first (%s)".format(filename, section.title));
content.html ~= processMarkdown(section.content, language);
} else {
throw new Exception("%s: Illegal section %s".format(filename, section.title));
throw new ContentProviderException("%s: Illegal section %s".format(filename, section.title));
}
}
return content;
Expand All @@ -258,24 +292,26 @@ class ContentProvider
+/
private string processMarkdown(string content, string language)
{
import std.array : replace;
import std.algorithm.searching : startsWith;

auto processed = expandMacros(content, mustacheContext_);
auto settings = new MarkdownSettings;
with(MarkdownFlags)
settings.flags = backtickCodeBlocks | vanillaMarkdown | tables;
settings.urlFilter = (string link, bool) {
import std.algorithm.searching : startsWith;
if (link.startsWith("http", "https", "irc", "/"))
return link;
else
{
enforce(isValidLink(link, language), "Invalid link given: " ~ link);
return "/tour/%s/%s".format(language, link);
return isValidLink(link, language)
.enforce("Invalid link given: " ~ link)
.routePath;
}
};
auto text = filterMarkdown(processed, settings);
// workaround against vibe.textfiler.markdown inserting empty newlines
// in inline code blocks
import std.array : replace;
text = text.replace("\n\n</code></pre>", "\n</code></pre>");
return text;
}
Expand All @@ -287,6 +323,8 @@ class ContentProvider
cp.addLanguage(contentDir.buildPath("en"));

assert(cp.processMarkdown("[foo](welcome/welcome-to-d)", "en") == "<p><a href=\"/tour/en/welcome/welcome-to-d\">foo</a>\n</p>\n");
assert(cp.processMarkdown("[foo](welcome/welcome-to-d#what-is-d)", "en") == "<p><a href=\"/tour/en/welcome/welcome-to-d#what-is-d\">foo</a>\n</p>\n");
assert(cp.processMarkdown("[foo](basics/delegates#anonymous-functions-lambdas)", "en") == "<p><a href=\"/tour/en/basics/delegates#anonymous-functions-lambdas\">foo</a>\n</p>\n");
assert(cp.processMarkdown("[foo](http://dlang.org)", "en") == "<p><a href=\"http://dlang.org\">foo</a>\n</p>\n");
}

Expand Down Expand Up @@ -316,18 +354,17 @@ Text after`, "en");
to the SourceCodeMaxCharsPerLine bytes per lines
restriction.

Throws: Exception when contraint doesn't apply.
Throws: ContentProviderException when contraint doesn't apply.
+/
private void checkSourceCodeLineWidth(string sourceCode, string sectionTitle)
{
import std.algorithm: all;
import std.range.primitives : walkLength;
import std.uni : byGrapheme;
auto lineNo = 0;
foreach (line; splitter(sourceCode, '\n')) {
++lineNo;
if (line.byGrapheme.walkLength(SourceCodeMaxCharsPerLine + 1) > SourceCodeMaxCharsPerLine) {
throw new Exception("Source code line length exceeds %d limit in '%s': %s"
throw new ContentProviderException("Source code line length exceeds %d limit in '%s': %s"
.format(SourceCodeMaxCharsPerLine, sectionTitle, line));
}
}
Expand Down Expand Up @@ -381,7 +418,7 @@ Text after`, "en");
Tuple!(string, "title", string, "sectionId")[] sections;
}
auto chapterMeta = language in chapter_;
enforce(chapterMeta !is null, new Exception("%s not known.".format(language)));
enforce(chapterMeta !is null, "%s not known.".format(language));
Chapter[] toc = new Chapter[content_[language].length];

foreach (chapterId, sections; content_[language]) {
Expand Down Expand Up @@ -420,18 +457,15 @@ Text after`, "en");
over the whole content, regardless of language. Content
doesn't guarantee any order.
+/
auto getContent() const
auto getAllContent() const
{
alias Element = const(Content)*;
Element[] range;
foreach(ref chapters; content_) {
foreach(ref sections; chapters) {
foreach(ref content; sections) {
range ~= &content;
}
}
}
return range;
import std.algorithm : map, joiner;
return content_
.byValue
.map!byValue
.joiner
.map!byValue
.joiner;
}

} // class ContentProvider
Expand Down Expand Up @@ -585,7 +619,7 @@ private string expandMacros(string content, Mustache.Context context)
{
Mustache.Option options;
options.handler = (string tag) {
throw new Exception("Unknown template tag " ~ tag);
throw new ContentProviderException("Unknown template tag " ~ tag);
};
auto mustache = Mustache(options);
return mustache.renderString(content, context);
Expand Down
Loading
Loading