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 }