1 module md2d.link;
2 
3 import md2d.util,
4 std.array,
5 std.ascii,
6 std.string;
7 
8 struct LinkRef {
9 	string id, url, title;
10 }
11 
12 struct Link {
13 	string text, url, title;
14 }
15 
16 @safe:
17 
18 bool parseLink(ref string str, ref Link dst, in LinkRef[string] linkrefs) pure {
19 	string pstr = str;
20 	if (pstr.length < 3)
21 		return false;
22 	// ignore img-link prefix
23 	if (pstr[0] == '!')
24 		pstr = pstr[1 .. $];
25 
26 	// parse the text part [text]
27 	if (pstr[0] != '[')
28 		return false;
29 	auto cidx = pstr.matchBracket();
30 	if (cidx < 1)
31 		return false;
32 	string refid;
33 	dst.text = pstr[1 .. cidx];
34 	pstr = pstr[cidx + 1 .. $];
35 
36 	// parse either (link '['"title"']') or '[' ']'[refid]
37 	if (pstr.length < 2)
38 		return false;
39 	if (pstr[0] == '(') {
40 		cidx = pstr.matchBracket();
41 		if (cidx < 1)
42 			return false;
43 		auto inner = pstr[1 .. cidx];
44 		immutable qidx = inner.indexOf('"');
45 		if (qidx > 1 && inner[qidx - 1].isWhite) {
46 			dst.url = inner[0 .. qidx].stripRight();
47 			immutable len = inner[qidx .. $].lastIndexOf('"');
48 			if (len == 0)
49 				return false;
50 			assert(len > 0);
51 			dst.title = inner[qidx + 1 .. qidx + len];
52 		} else {
53 			dst.url = inner.stripRight();
54 			dst.title = null;
55 		}
56 		if (dst.url.startsWith('<') && dst.url.endsWith('>'))
57 			dst.url = dst.url[1 .. $ - 1];
58 		pstr = pstr[cidx + 1 .. $];
59 	} else {
60 		if (pstr[0] == ' ')
61 			pstr = pstr[1 .. $];
62 		if (pstr[0] != '[')
63 			return false;
64 		pstr = pstr[1 .. $];
65 		cidx = pstr.indexOf(']');
66 		if (cidx < 0)
67 			return false;
68 		if (cidx == 0)
69 			refid = dst.text;
70 		else
71 			refid = pstr[0 .. cidx];
72 		pstr = pstr[cidx + 1 .. $];
73 	}
74 
75 	if (refid.length) {
76 		auto pr = refid.toLower in linkrefs;
77 		if (!pr) {
78 			// debug if (!__ctfe) logDebug("[LINK REF NOT FOUND: '%s'", refid);
79 			return false;
80 		}
81 		dst.url = pr.url;
82 		dst.title = pr.title;
83 	}
84 
85 	str = pstr;
86 	return true;
87 }
88 
89 unittest {
90 	void testLink(string s, Link exp, in LinkRef[string] refs) {
91 		Link link;
92 		assert(parseLink(s, link, refs), s);
93 		assert(link == exp);
94 	}
95 
96 	LinkRef[string] refs;
97 	refs["ref"] = LinkRef("ref", "target", "title");
98 
99 	testLink(`[link](target)`, Link("link", "target"), null);
100 	testLink(`[link](target "title")`, Link("link", "target", "title"), null);
101 	testLink(`[link](target  "title")`, Link("link", "target", "title"), null);
102 	testLink(`[link](target "title" )`, Link("link", "target", "title"), null);
103 
104 	testLink(`[link](target)`, Link("link", "target"), null);
105 	testLink(`[link](target "title")`, Link("link", "target", "title"), null);
106 
107 	testLink(`[link][ref]`, Link("link", "target", "title"), refs);
108 	testLink(`[ref][]`, Link("ref", "target", "title"), refs);
109 
110 	testLink(`[link[with brackets]](target)`, Link("link[with brackets]", "target"), null);
111 	testLink(`[link[with brackets]][ref]`, Link("link[with brackets]", "target", "title"), refs);
112 
113 	testLink(`[link](/target with spaces)`, Link("link", "/target with spaces"), null);
114 	testLink(`[link](/target with spaces "title")`, Link("link", "/target with spaces", "title"), null);
115 
116 	testLink(`[link](white-space  "around title")`, Link("link", "white-space", "around title"), null);
117 	testLink(`[link](tabs	"around title"	)`, Link("link", "tabs", "around title"), null);
118 
119 	testLink(`[link](target "")`, Link("link", "target", ""), null);
120 	testLink(`[link](target-no-title"foo")`, Link("link", "target-no-title\"foo\"", ""), null);
121 
122 	testLink(`[link](<target>)`, Link("link", "target"), null);
123 
124 	auto failing = [
125 		`text`, `[link](target`, `[link]target)`, `[link]`,
126 		`[link(target)`, `link](target)`, `[link] (target)`,
127 		`[link][noref]`, `[noref][]`
128 	];
129 	Link link;
130 	foreach (s; failing)
131 		assert(!parseLink(s, link, refs), s);
132 }
133 
134 bool parseAutoLink(ref string str, ref string url) pure {
135 	string pstr = str;
136 	if (pstr.length < 3 || pstr[0] != '<')
137 		return false;
138 	pstr = pstr[1 .. $];
139 	auto cidx = pstr.indexOf('>');
140 	if (cidx < 0)
141 		return false;
142 	url = pstr[0 .. cidx];
143 	if (anyOf(url, " \t") || !anyOf(url, ":@"))
144 		return false;
145 	str = pstr[cidx + 1 .. $];
146 	if (url.indexOf('@') > 0)
147 		url = "mailto:" ~ url;
148 	return true;
149 }
150 
151 LinkRef[string] scanForReferences(ref string[] lines) pure {
152 	LinkRef[string] ret;
153 	bool[size_t] reflines;
154 
155 	// search for reference definitions:
156 	//	[refid] link "opt text"
157 	//	[refid] <link> "opt text"
158 	//	"opt text", 'opt text', (opt text)
159 	//	line must not be indented
160 	foreach (lnidx, ln; lines) {
161 		if (isLineIndented(ln))
162 			continue;
163 		ln = ln.strip;
164 		if (!ln.startsWith('['))
165 			continue;
166 		ln = ln[1 .. $];
167 
168 		auto idx = ln.indexOf("]:");
169 		if (idx < 0)
170 			continue;
171 		string refid = ln[0 .. idx];
172 		ln = stripLeft(ln[idx + 2 .. $]);
173 
174 		string url;
175 		if (ln.startsWith('<')) {
176 			idx = ln.indexOf('>');
177 			if (idx < 0)
178 				continue;
179 			url = ln[1 .. idx];
180 			ln = ln[idx + 1 .. $];
181 		} else {
182 			idx = ln.indexOf(' ');
183 			if (idx > 0) {
184 				url = ln[0 .. idx];
185 				ln = ln[idx + 1 .. $];
186 			} else {
187 				idx = ln.indexOf('\t');
188 				if (idx < 0) {
189 					url = ln;
190 					ln = ln[$ .. $];
191 				} else {
192 					url = ln[0 .. idx];
193 					ln = ln[idx + 1 .. $];
194 				}
195 			}
196 		}
197 		ln = stripLeft(ln);
198 
199 		string title;
200 		if (ln.length >= 3) {
201 			if (ln[0] == '(' && ln[$ - 1] == ')' ||
202 				ln[0] == '\"' && ln[$ - 1] == '\"' ||
203 				ln[0] == '\'' && ln[$ - 1] == '\'')
204 				title = ln[1 .. $ - 1];
205 		}
206 
207 		ret[refid.toLower] = LinkRef(refid, url, title);
208 		reflines[lnidx] = true;
209 
210 		// debug if (!__ctfe) logTrace("[detected ref on line %d]", lnidx+1);
211 	}
212 
213 	// remove all lines containing references
214 	auto nonreflines = appender!(string[]);
215 	nonreflines.reserve(lines.length);
216 	foreach (i, ln; lines)
217 		if (i !in reflines)
218 			nonreflines ~= ln;
219 	lines = nonreflines[];
220 
221 	return ret;
222 }