1
/******************************************************************
4
* A simple Markdown lexer for scintilla.
6
* Includes highlighting for some extra features from the
7
* Pandoc implementation; strikeout, using '#.' as a default
8
* ordered list item marker, and delimited code blocks.
12
* Standard indented code blocks are not highlighted at all,
13
* as it would conflict with other indentation schemes. Use
14
* delimited code blocks for blanket highlighting of an
15
* entire code block. Embedded HTML is not highlighted either.
16
* Blanket HTML highlighting has issues, because some Markdown
17
* implementations allow Markdown markup inside of the HTML. Also,
18
* there is a following blank line issue that can't be ignored,
19
* explained in the next paragraph. Embedded HTML and code
20
* blocks would be better supported with language specific
23
* The highlighting aims to accurately reflect correct syntax,
24
* but a few restrictions are relaxed. Delimited code blocks are
25
* highlighted, even if the line following the code block is not blank.
26
* Requiring a blank line after a block, breaks the highlighting
27
* in certain cases, because of the way Scintilla ends up calling
30
* Written by Jon Strait - jstrait@moonloop.net
32
* The License.txt file describes the conditions under which this
33
* software may be distributed.
35
*****************************************************************/
47
#include "StyleContext.h"
49
#include "Scintilla.h"
53
using namespace Scintilla;
56
static inline bool IsNewline(const int ch) {
57
return (ch == '\n' || ch == '\r');
60
// True if can follow ch down to the end with possibly trailing whitespace
61
static bool FollowToLineEnd(const int ch, const int state, const unsigned int endPos, StyleContext &sc) {
63
while (sc.GetRelative(++i) == ch)
65
// Skip over whitespace
66
while (IsASpaceOrTab(sc.GetRelative(i)) && sc.currentPos + i < endPos)
68
if (IsNewline(sc.GetRelative(i)) || sc.currentPos + i == endPos) {
70
sc.ChangeState(state);
71
sc.SetState(SCE_MARKDOWN_LINE_BEGIN);
77
// Set the state on text section from current to length characters,
78
// then set the rest until the newline to default, except for any characters matching token
79
static void SetStateAndZoom(const int state, const int length, const int token, StyleContext &sc) {
82
sc.SetState(SCE_MARKDOWN_DEFAULT);
85
while (sc.More() && !IsNewline(sc.ch)) {
86
if (sc.ch == token && !started) {
90
else if (sc.ch != token) {
91
sc.SetState(SCE_MARKDOWN_DEFAULT);
96
sc.SetState(SCE_MARKDOWN_LINE_BEGIN);
99
// Does the previous line have more than spaces and tabs?
100
static bool HasPrevLineContent(StyleContext &sc) {
102
// Go back to the previous newline
103
while ((--i + (int)sc.currentPos) >= 0 && !IsNewline(sc.GetRelative(i)))
105
while ((--i + (int)sc.currentPos) >= 0) {
106
if (IsNewline(sc.GetRelative(i)))
108
if (!IsASpaceOrTab(sc.GetRelative(i)))
114
static bool IsValidHrule(const unsigned int endPos, StyleContext &sc) {
118
c = sc.GetRelative(i);
121
// hit a terminating character
122
else if (!IsASpaceOrTab(c) || sc.currentPos + i == endPos) {
123
// Are we a valid HRULE
124
if ((IsNewline(c) || sc.currentPos + i == endPos) &&
125
count >= 3 && !HasPrevLineContent(sc)) {
126
sc.SetState(SCE_MARKDOWN_HRULE);
128
sc.SetState(SCE_MARKDOWN_LINE_BEGIN);
132
sc.SetState(SCE_MARKDOWN_DEFAULT);
140
static void ColorizeMarkdownDoc(unsigned int startPos, int length, int initStyle,
141
WordList **, Accessor &styler) {
142
unsigned int endPos = startPos + length;
143
int precharCount = 0;
144
// Don't advance on a new loop iteration and retry at the same position.
145
// Useful in the corner case of having to start at the beginning file position
146
// in the default state.
147
bool freezeCursor = false;
149
StyleContext sc(startPos, length, initStyle, styler);
152
// Skip past escaped characters
158
// A blockquotes resets the line semantics
159
if (sc.state == SCE_MARKDOWN_BLOCKQUOTE)
160
sc.SetState(SCE_MARKDOWN_LINE_BEGIN);
162
// Conditional state-based actions
163
if (sc.state == SCE_MARKDOWN_CODE2) {
164
if (sc.Match("``") && sc.GetRelative(-2) != ' ') {
166
sc.SetState(SCE_MARKDOWN_DEFAULT);
169
else if (sc.state == SCE_MARKDOWN_CODE) {
170
if (sc.ch == '`' && sc.chPrev != ' ')
171
sc.ForwardSetState(SCE_MARKDOWN_DEFAULT);
173
/* De-activated because it gets in the way of other valid indentation
174
* schemes, for example multiple paragraphs inside a list item.
176
else if (sc.state == SCE_MARKDOWN_CODEBK) {
178
if (IsNewline(sc.ch)) {
179
if (sc.chNext != '\t') {
180
for (int c = 1; c < 5; ++c) {
181
if (sc.GetRelative(c) != ' ')
186
else if (sc.atLineStart) {
187
if (sc.ch != '\t' ) {
188
for (int i = 0; i < 4; ++i) {
189
if (sc.GetRelative(i) != ' ')
195
sc.SetState(SCE_MARKDOWN_LINE_BEGIN);
199
else if (sc.state == SCE_MARKDOWN_STRONG1) {
200
if (sc.Match("**") && sc.chPrev != ' ') {
202
sc.SetState(SCE_MARKDOWN_DEFAULT);
205
else if (sc.state == SCE_MARKDOWN_STRONG2) {
206
if (sc.Match("__") && sc.chPrev != ' ') {
208
sc.SetState(SCE_MARKDOWN_DEFAULT);
212
else if (sc.state == SCE_MARKDOWN_EM1) {
213
if (sc.ch == '*' && sc.chPrev != ' ')
214
sc.ForwardSetState(SCE_MARKDOWN_DEFAULT);
216
else if (sc.state == SCE_MARKDOWN_EM2) {
217
if (sc.ch == '_' && sc.chPrev != ' ')
218
sc.ForwardSetState(SCE_MARKDOWN_DEFAULT);
220
else if (sc.state == SCE_MARKDOWN_CODEBK) {
221
if (sc.atLineStart && sc.Match("~~~")) {
223
while (!IsNewline(sc.GetRelative(i)) && sc.currentPos + i < endPos)
226
sc.SetState(SCE_MARKDOWN_DEFAULT);
229
else if (sc.state == SCE_MARKDOWN_STRIKEOUT) {
230
if (sc.Match("~~") && sc.chPrev != ' ') {
232
sc.SetState(SCE_MARKDOWN_DEFAULT);
235
else if (sc.state == SCE_MARKDOWN_LINE_BEGIN) {
237
if (sc.Match("######"))
238
SetStateAndZoom(SCE_MARKDOWN_HEADER6, 6, '#', sc);
239
else if (sc.Match("#####"))
240
SetStateAndZoom(SCE_MARKDOWN_HEADER5, 5, '#', sc);
241
else if (sc.Match("####"))
242
SetStateAndZoom(SCE_MARKDOWN_HEADER4, 4, '#', sc);
243
else if (sc.Match("###"))
244
SetStateAndZoom(SCE_MARKDOWN_HEADER3, 3, '#', sc);
245
else if (sc.Match("##"))
246
SetStateAndZoom(SCE_MARKDOWN_HEADER2, 2, '#', sc);
247
else if (sc.Match("#")) {
248
// Catch the special case of an unordered list
249
if (sc.chNext == '.' && IsASpaceOrTab(sc.GetRelative(2))) {
251
sc.SetState(SCE_MARKDOWN_PRECHAR);
254
SetStateAndZoom(SCE_MARKDOWN_HEADER1, 1, '#', sc);
257
else if (sc.Match("~~~")) {
258
if (!HasPrevLineContent(sc))
259
sc.SetState(SCE_MARKDOWN_CODEBK);
261
sc.SetState(SCE_MARKDOWN_DEFAULT);
263
else if (sc.ch == '=') {
264
if (HasPrevLineContent(sc) && FollowToLineEnd('=', SCE_MARKDOWN_HEADER1, endPos, sc))
267
sc.SetState(SCE_MARKDOWN_DEFAULT);
269
else if (sc.ch == '-') {
270
if (HasPrevLineContent(sc) && FollowToLineEnd('-', SCE_MARKDOWN_HEADER2, endPos, sc))
274
sc.SetState(SCE_MARKDOWN_PRECHAR);
277
else if (IsNewline(sc.ch))
278
sc.SetState(SCE_MARKDOWN_LINE_BEGIN);
281
sc.SetState(SCE_MARKDOWN_PRECHAR);
285
// The header lasts until the newline
286
else if (sc.state == SCE_MARKDOWN_HEADER1 || sc.state == SCE_MARKDOWN_HEADER2 ||
287
sc.state == SCE_MARKDOWN_HEADER3 || sc.state == SCE_MARKDOWN_HEADER4 ||
288
sc.state == SCE_MARKDOWN_HEADER5 || sc.state == SCE_MARKDOWN_HEADER6) {
289
if (IsNewline(sc.ch))
290
sc.SetState(SCE_MARKDOWN_LINE_BEGIN);
293
// New state only within the initial whitespace
294
if (sc.state == SCE_MARKDOWN_PRECHAR) {
296
if (sc.ch == '>' && precharCount < 5)
297
sc.SetState(SCE_MARKDOWN_BLOCKQUOTE);
299
// Begin of code block
300
else if (!HasPrevLineContent(sc) && (sc.chPrev == '\t' || precharCount >= 4))
301
sc.SetState(SCE_MARKDOWN_CODEBK);
303
// HRule - Total of three or more hyphens, asterisks, or underscores
304
// on a line by themselves
305
else if ((sc.ch == '-' || sc.ch == '*' || sc.ch == '_') && IsValidHrule(endPos, sc))
308
else if ((sc.ch == '-' || sc.ch == '*' || sc.ch == '+') && IsASpaceOrTab(sc.chNext)) {
309
sc.SetState(SCE_MARKDOWN_ULIST_ITEM);
310
sc.ForwardSetState(SCE_MARKDOWN_DEFAULT);
313
else if (IsADigit(sc.ch)) {
315
while (IsADigit(sc.GetRelative(++digitCount)))
317
if (sc.GetRelative(digitCount) == '.' &&
318
IsASpaceOrTab(sc.GetRelative(digitCount + 1))) {
319
sc.SetState(SCE_MARKDOWN_OLIST_ITEM);
320
sc.Forward(digitCount + 1);
321
sc.SetState(SCE_MARKDOWN_DEFAULT);
324
// Alternate Ordered list
325
else if (sc.ch == '#' && sc.chNext == '.' && IsASpaceOrTab(sc.GetRelative(2))) {
326
sc.SetState(SCE_MARKDOWN_OLIST_ITEM);
328
sc.SetState(SCE_MARKDOWN_DEFAULT);
330
else if (sc.ch != ' ' || precharCount > 2)
331
sc.SetState(SCE_MARKDOWN_DEFAULT);
336
// New state anywhere in doc
337
if (sc.state == SCE_MARKDOWN_DEFAULT) {
338
if (sc.atLineStart && sc.ch == '#') {
339
sc.SetState(SCE_MARKDOWN_LINE_BEGIN);
343
if (sc.Match("![") || sc.ch == '[') {
344
int i = 0, j = 0, k = 0;
345
int len = endPos - sc.currentPos;
346
while (i < len && (sc.GetRelative(++i) != ']' || sc.GetRelative(i - 1) == '\\'))
348
if (sc.GetRelative(i) == ']') {
350
if (sc.GetRelative(++i) == '(') {
351
while (i < len && (sc.GetRelative(++i) != ')' || sc.GetRelative(i - 1) == '\\'))
353
if (sc.GetRelative(i) == ')')
356
else if (sc.GetRelative(i) == '[' || sc.GetRelative(++i) == '[') {
357
while (i < len && (sc.GetRelative(++i) != ']' || sc.GetRelative(i - 1) == '\\'))
359
if (sc.GetRelative(i) == ']')
363
// At least a link text
365
sc.SetState(SCE_MARKDOWN_LINK);
367
// Also has a URL or reference portion
370
sc.ForwardSetState(SCE_MARKDOWN_DEFAULT);
373
// Code - also a special case for alternate inside spacing
374
if (sc.Match("``") && sc.GetRelative(3) != ' ') {
375
sc.SetState(SCE_MARKDOWN_CODE2);
378
else if (sc.ch == '`' && sc.chNext != ' ') {
379
sc.SetState(SCE_MARKDOWN_CODE);
382
else if (sc.Match("**") && sc.GetRelative(2) != ' ') {
383
sc.SetState(SCE_MARKDOWN_STRONG1);
386
else if (sc.Match("__") && sc.GetRelative(2) != ' ') {
387
sc.SetState(SCE_MARKDOWN_STRONG2);
391
else if (sc.ch == '*' && sc.chNext != ' ')
392
sc.SetState(SCE_MARKDOWN_EM1);
393
else if (sc.ch == '_' && sc.chNext != ' ')
394
sc.SetState(SCE_MARKDOWN_EM2);
396
else if (sc.Match("~~") && sc.GetRelative(2) != ' ') {
397
sc.SetState(SCE_MARKDOWN_STRIKEOUT);
401
else if (IsNewline(sc.ch))
402
sc.SetState(SCE_MARKDOWN_LINE_BEGIN);
404
// Advance if not holding back the cursor for this iteration.
407
freezeCursor = false;
412
LexerModule lmMarkdown(SCLEX_MARKDOWN, ColorizeMarkdownDoc, "markdown");