1 /++
2 	Utility functions for string processing
3 	Modified from https://github.com/dlang-community/dmarkdown/blob/master/source/dmarkdown/string.d
4 +/
5 module md2d.util;
6 
7 import std.range,
8 std.string;
9 import std.algorithm : canFind;
10 public import std.conv : to;
11 public import tame.text.uri : asSlug;
12 
13 @safe pure:
14 /++
15 	Checks if all characters in 'str' are contained in 'chars'.
16 +/
17 bool allOf(S)(S str, S chars) {
18 	foreach (ch; str)
19 		if (!chars.canFind(ch))
20 			return false;
21 	return true;
22 }
23 
24 /++
25 	Checks if any character in 'str' is contained in 'chars'.
26 +/
27 bool anyOf(S)(S str, S chars) {
28 	foreach (ch; str)
29 		if (chars.canFind(ch))
30 			return true;
31 	return false;
32 }
33 
34 /++
35 	Finds the closing bracket (works with any of '[', '$(LPAREN)', '<', '{').
36 
37 	Params:
38 		str = input string
39 		nested = whether to skip nested brackets
40 	Returns:
41 		The index of the closing bracket or -1 for unbalanced strings
42 		and strings that don't start with a bracket.
43 +/
44 ptrdiff_t matchBracket(string str, bool nested = true) nothrow {
45 	if (str.length < 2)
46 		return -1;
47 
48 	// dfmt off
49 	char open = str[0], close = void;
50 	switch (str[0]) {
51 		case '[': close = ']'; break;
52 		case '(': close = ')'; break;
53 		case '<': close = '>'; break;
54 		case '{': close = '}'; break;
55 		default: return -1;
56 	}
57 // dfmt on
58 
59 	size_t level = 1;
60 	foreach (i, c; str[1 .. $]) {
61 		if (nested && c == open)
62 			++level;
63 		else if (c == close)
64 			--level;
65 		if (level == 0)
66 			return i + 1;
67 	}
68 	return -1;
69 }
70 
71 bool isLineBlank(in char[] ln) => allOf(ln, " \t");
72 
73 pure @safe:
74 @nogc {
75 	bool isLineIndented(in char[] ln) => ln.startsWith('\t') || ln.startsWith("    ");
76 
77 	auto unindentLine(in char[] ln) {
78 		if (ln.startsWith('\t'))
79 			return ln[1 .. $];
80 		if (ln.startsWith("    "))
81 			return ln[4 .. $];
82 		return ln;
83 	}
84 
85 	bool isAtxHeaderLine(const(char)[] ln) {
86 		ln = ln.stripLeft;
87 		size_t i = 0;
88 		while (i < ln.length && ln[i] == '#')
89 			i++;
90 		if (i < 1 || i > 6 || i >= ln.length)
91 			return false;
92 		return ln[i] == ' ';
93 	}
94 }
95 
96 bool isSetextHeaderLine(const(char)[] ln, char subHeaderChar) {
97 	ln = ln.stripLeft;
98 	if (ln.length < 1)
99 		return false;
100 	if (ln[0] == '=') {
101 		while (!ln.empty && ln.front == '=')
102 			ln.popFront();
103 		return allOf(ln, " \t");
104 	}
105 	if (ln[0] == subHeaderChar) {
106 		while (!ln.empty && ln.front == subHeaderChar)
107 			ln.popFront();
108 		return allOf(ln, " \t");
109 	}
110 	return false;
111 }
112 
113 bool isHlineLine(in char[] ln) =>
114 	(allOf(ln, " -") && count(ln, '-') >= 3) ||
115 	(allOf(ln, " *") && count(ln, '*') >= 3) ||
116 	(allOf(ln, " _") && count(ln, '_') >= 3);
117 
118 bool isQuoteLine(in char[] ln) => ln.stripLeft().startsWith(">");
119 
120 size_t getQuoteLevel(const(char)[] ln) {
121 	size_t level;
122 	ln = stripLeft(ln);
123 	while (ln.length > 0 && ln[0] == '>') {
124 		level++;
125 		ln = stripLeft(ln[1 .. $]);
126 	}
127 	return level;
128 }
129 
130 bool isUListLine(const(char)[] ln) {
131 	ln = ln.stripLeft;
132 	if (ln.length < 2)
133 		return false;
134 	if (!"*+-".canFind(ln[0]))
135 		return false;
136 	if (ln[1] != ' ' && ln[1] != '\t')
137 		return false;
138 	return true;
139 }
140 
141 bool isOListLine(const(char)[] ln) {
142 	ln = ln.stripLeft;
143 	if (!ln.length || ln[0] < '0' || ln[0] > '9')
144 		return false;
145 	ln = ln[1 .. $];
146 	while (ln.length && ln[0] >= '0' && ln[0] <= '9')
147 		ln = ln[1 .. $];
148 	if (ln.length < 2)
149 		return false;
150 	if (ln[0] != '.')
151 		return false;
152 	if (ln[1] != ' ' && ln[1] != '\t')
153 		return false;
154 	return true;
155 }
156 
157 bool isTableRowLine(bool proper = false)(in char[] ln) {
158 	static if (proper)
159 		return ln.indexOf(" | ") >= 0
160 			&& !ln.isOListLine
161 			&& !ln.isUListLine
162 			&& !ln.isAtxHeaderLine;
163 	else
164 		return ln.indexOf(" | ") >= 0;
165 }
166 
167 bool isCodeBlockDelimiter(in char[] ln) => ln.startsWith("```");