diff --git a/locale/de/Notepad2.rc b/locale/de/Notepad2.rc index 3331f69a92..0bb12a82b3 100644 --- a/locale/de/Notepad2.rc +++ b/locale/de/Notepad2.rc @@ -2205,6 +2205,7 @@ BEGIN NP2STYLE_Bookmark "Bookmark" NP2STYLE_CallTip "CallTip" NP2STYLE_CodeFolding "Code Folding" + NP2STYLE_Link "Link" NP2STYLE_Default "Default" END diff --git a/locale/it/Notepad2.rc b/locale/it/Notepad2.rc index 0072b048c8..c899aeaf93 100644 --- a/locale/it/Notepad2.rc +++ b/locale/it/Notepad2.rc @@ -2205,6 +2205,7 @@ BEGIN NP2STYLE_Bookmark "Bookmark" NP2STYLE_CallTip "CallTip" NP2STYLE_CodeFolding "Code Folding" + NP2STYLE_Link "Link" NP2STYLE_Default "Default" END diff --git a/locale/ja/Notepad2.rc b/locale/ja/Notepad2.rc index 5a8334811f..b0df13d39d 100644 --- a/locale/ja/Notepad2.rc +++ b/locale/ja/Notepad2.rc @@ -2205,6 +2205,7 @@ BEGIN NP2STYLE_Bookmark "ブックマーク" NP2STYLE_CallTip "コールチップ" NP2STYLE_CodeFolding "コード折りたたみ" + NP2STYLE_Link "Link" NP2STYLE_Default "標準" END diff --git a/locale/ko/Notepad2.rc b/locale/ko/Notepad2.rc index a0ad20cf09..7139e65e6f 100644 --- a/locale/ko/Notepad2.rc +++ b/locale/ko/Notepad2.rc @@ -2205,6 +2205,7 @@ BEGIN NP2STYLE_Bookmark "책갈피" NP2STYLE_CallTip "호출팁" NP2STYLE_CodeFolding "코드 접기" + NP2STYLE_Link "Link" NP2STYLE_Default "기본값" END diff --git a/locale/zh-Hans/Notepad2.rc b/locale/zh-Hans/Notepad2.rc index 0a355d98e5..68ee8061a2 100644 --- a/locale/zh-Hans/Notepad2.rc +++ b/locale/zh-Hans/Notepad2.rc @@ -2204,6 +2204,7 @@ BEGIN NP2STYLE_Bookmark "书签" NP2STYLE_CallTip "调用提示" NP2STYLE_CodeFolding "代码折叠" + NP2STYLE_Link "链接" NP2STYLE_Default "默认样式" END diff --git a/locale/zh-Hant/Notepad2.rc b/locale/zh-Hant/Notepad2.rc index 546a3fb649..0f6babbfa3 100644 --- a/locale/zh-Hant/Notepad2.rc +++ b/locale/zh-Hant/Notepad2.rc @@ -2204,6 +2204,7 @@ BEGIN NP2STYLE_Bookmark "書籤" NP2STYLE_CallTip "呼叫提示" NP2STYLE_CodeFolding "程式碼折疊" + NP2STYLE_Link "連結" NP2STYLE_Default "預設樣式" END diff --git a/scintilla/include/Scintilla.h b/scintilla/include/Scintilla.h index 3fe863abb7..b19fa0a574 100644 --- a/scintilla/include/Scintilla.h +++ b/scintilla/include/Scintilla.h @@ -204,8 +204,8 @@ typedef sptr_t (*SciFnDirectStatus)(sptr_t ptr, unsigned int iMessage, uptr_t wP #define SCI_SETMARGINS 2252 #define SCI_GETMARGINS 2253 #define STYLE_DEFAULT 0 -#define STYLE_LINENUMBER 32 -#define STYLE_HOTSPOT 33 +#define STYLE_LINK 32 +#define STYLE_LINENUMBER 33 #define STYLE_BRACELIGHT 34 #define STYLE_BRACEBAD 35 #define STYLE_CONTROLCHAR 36 diff --git a/scintilla/include/Scintilla.iface b/scintilla/include/Scintilla.iface index 89db33698d..1d7e50fa4b 100644 --- a/scintilla/include/Scintilla.iface +++ b/scintilla/include/Scintilla.iface @@ -523,8 +523,8 @@ get int GetMargins=2253(,) # Styles in range 32..39 are predefined for parts of the UI and are not used as normal styles. enu StylesCommon=STYLE_ val STYLE_DEFAULT=0 -val STYLE_LINENUMBER=32 -val STYLE_HOTSPOT=33 +val STYLE_LINK=32 +val STYLE_LINENUMBER=33 val STYLE_BRACELIGHT=34 val STYLE_BRACEBAD=35 val STYLE_CONTROLCHAR=36 @@ -535,7 +535,6 @@ val STYLE_FIRSTPREDEFINED=32 val STYLE_LASTPREDEFINED=39 val STYLE_MAX=255 -ali STYLE_HOTSPOT=HOT_SPOT ali STYLE_LINENUMBER=LINE_NUMBER ali STYLE_BRACELIGHT=BRACE_LIGHT ali STYLE_BRACEBAD=BRACE_BAD diff --git a/scintilla/include/ScintillaTypes.h b/scintilla/include/ScintillaTypes.h index aa16883055..5178901aee 100644 --- a/scintilla/include/ScintillaTypes.h +++ b/scintilla/include/ScintillaTypes.h @@ -109,8 +109,8 @@ enum class MarginType { enum class StylesCommon { Default = 0, - LineNumber = 32, - HotSpot = 33, + Link = 32, + LineNumber = 33, BraceLight = 34, BraceBad = 35, ControlChar = 36, diff --git a/scintilla/lexers/LexHTML.cxx b/scintilla/lexers/LexHTML.cxx index e33ec79b4e..873c853775 100644 --- a/scintilla/lexers/LexHTML.cxx +++ b/scintilla/lexers/LexHTML.cxx @@ -525,11 +525,6 @@ constexpr bool IsAttributeContinue(int ch) noexcept { return IsAlphaNumeric(ch) || AnyOf(ch, '.', '-', '_', ':', '!', '#', '/') || ch >= 0x80; } -constexpr bool IsInvalidAttrChar(int ch) noexcept { - // characters not allowed in unquoted attribute value - return ch <= 32 || ch == 127 || AnyOf(ch, '"', '\'', '\\', '`', '=', '<', '>'); -} - constexpr bool IsOKBeforeJSRE(int ch) noexcept { // TODO: also handle + and - (except if they're part of ++ or --) and return keywords return AnyOf(ch, '(', '[', '{', '=', ',', ':', ';', '!', '%', '^', '&', '*', '|', '?', '~'); @@ -1514,7 +1509,7 @@ void ColouriseHyperTextDoc(Sci_PositionU startPos, Sci_Position length, int init } break; case SCE_H_VALUE: - if (IsInvalidAttrChar(ch)) { + if (IsHtmlInvalidAttrChar(ch)) { if (ch == '\"' && chPrev == '=') { // Should really test for being first character state = SCE_H_DOUBLESTRING; diff --git a/scintilla/lexers/LexMarkdown.cxx b/scintilla/lexers/LexMarkdown.cxx index 31077a5704..8f4a059af0 100644 --- a/scintilla/lexers/LexMarkdown.cxx +++ b/scintilla/lexers/LexMarkdown.cxx @@ -212,8 +212,8 @@ struct MarkdownLexer { HtmlTagState tagState = HtmlTagState::None; // html tag, link title int delimiterCount = 0; // code fence int bracketCount = 0; // link text - int parenCount = 0; // link destination, link title, autoLink - int periodCount = 0; // autoLink domain + int parenCount = 0; // link destination, link title + int periodCount = 0; // autoLink domain and path AutoLink autoLink = AutoLink::None; const Markdown markdown; @@ -246,7 +246,7 @@ struct MarkdownLexer { void HighlightInlineText(); void HighlightIndentedText(); - int GetCurrentDelimiterRun(DelimiterRun &delimiterRun) const noexcept; + int GetCurrentDelimiterRun(DelimiterRun &delimiterRun, bool ignoreCurrent = false) const noexcept; bool HighlightEmphasis(); bool HighlightLinkText(); @@ -480,9 +480,16 @@ void MarkdownLexer::HighlightIndentedText() { } } -int MarkdownLexer::GetCurrentDelimiterRun(DelimiterRun &delimiterRun) const noexcept { +int MarkdownLexer::GetCurrentDelimiterRun(DelimiterRun &delimiterRun, bool ignoreCurrent) const noexcept { int chPrev = sc.chPrev; + int delimiter = sc.ch; Sci_PositionU pos = sc.currentPos; + if (ignoreCurrent) { + chPrev = delimiter; + delimiter = sc.chNext; + pos += sc.width; + } + // unlike official Lexilla, for performance reason our StyleContext // for UTF-8 encoding is byte oriented instead of character oriented. if ((chPrev & 0x80) != 0 && sc.styler.Encoding() == EncodingType::unicode) { @@ -490,7 +497,8 @@ int MarkdownLexer::GetCurrentDelimiterRun(DelimiterRun &delimiterRun) const noex chPrev = sc.styler.GetCharacterAndWidth(pos - 1, &width); } - int chNext = GetCharAfterDelimiter(sc.styler, pos, sc.ch); + const Sci_PositionU startPos = pos; + int chNext = GetCharAfterDelimiter(sc.styler, pos, delimiter); if (chNext & 0x80) { chNext = sc.styler.GetCharacterAndWidth(pos); } @@ -500,7 +508,16 @@ int MarkdownLexer::GetCurrentDelimiterRun(DelimiterRun &delimiterRun) const noex delimiterRun.ccPrev = (chPrev == '_') ? CharacterClass::punctuation : sc.styler.GetCharacterClass(chPrev); delimiterRun.ccNext = (chNext == '_') ? CharacterClass::punctuation : sc.styler.GetCharacterClass(chNext); // returns length of the delimiter run - return static_cast(pos - sc.currentPos); + return static_cast(pos - startPos); +} + +constexpr bool IsEmphasisDelimiter(int ch) noexcept { + return ch == '*' || ch == '_' || ch == '~'; +} + +constexpr uint8_t GetEmphasisDelimiter(int state) noexcept { + static_assert((SCE_MARKDOWN_EM_ASTERISK & 1) == 0); + return (state == SCE_MARKDOWN_STRIKEOUT) ? '~' : ((state & 1) ? '_' : '*'); } bool MarkdownLexer::HighlightEmphasis() { @@ -511,14 +528,13 @@ bool MarkdownLexer::HighlightEmphasis() { return true; } - const int delimiter = static_cast((sc.state == SCE_MARKDOWN_STRIKEOUT) ? '~' - : ((sc.state == SCE_MARKDOWN_EM_ASTERISK || sc.state == SCE_MARKDOWN_STRONG_ASTERISK) ? '*' : '_')); - if (sc.ch == delimiter && (sc.state != SCE_MARKDOWN_STRIKEOUT || sc.chNext == '~')) { + const int current = sc.state; + const int delimiter = GetEmphasisDelimiter(current); + if (sc.ch == delimiter && (current != SCE_MARKDOWN_STRIKEOUT || sc.chNext == '~')) { DelimiterRun delimiterRun; const int length = GetCurrentDelimiterRun(delimiterRun); const bool closed = delimiterRun.CanClose(delimiter); - const int current = sc.state; if (current != SCE_MARKDOWN_STRIKEOUT) { // TODO: fix longest match failure for `***strong** in emph* t` if (length == 1 && current >= SCE_MARKDOWN_STRONG_ASTERISK) { @@ -585,17 +601,14 @@ bool MarkdownLexer::HighlightLinkText() { chNext = LexGetNextChar(sc.styler, startPos, endPos); } if (chNext != '\0') { - const int current = sc.state; - const int style = (chNext == '<') ? SCE_MARKDOWN_ANGLE_LINK - : ((sc.ch == '(') ? SCE_MARKDOWN_PAREN_LINK : SCE_MARKDOWN_PLAIN_LINK); + SaveOuterStyle(sc.state); + SaveOuterStart(sc.currentPos); tagState = (startPos == sc.currentPos) ? HtmlTagState::None : HtmlTagState::Open; parenCount = sc.ch == '('; - if (sc.ch != '(') { - sc.SetState(SCE_MARKDOWN_DELIMITER); - sc.ForwardSetState(current); - } - SaveOuterStyle(current); - SaveOuterStart(sc.currentPos); + const int style = (chNext == '<') ? SCE_MARKDOWN_ANGLE_LINK + : (parenCount ? SCE_MARKDOWN_PAREN_LINK : SCE_MARKDOWN_PLAIN_LINK); + sc.SetState(parenCount ? SCE_MARKDOWN_LINK_TEXT : SCE_MARKDOWN_DELIMITER); + sc.Forward(); sc.SetState(style); sc.Forward(); if (tagState == HtmlTagState::None) { @@ -631,8 +644,15 @@ bool MarkdownLexer::HighlightLinkText() { } // 6.3 Links +constexpr bool IsLinkTitleStyle(int state) noexcept { + return state == SCE_MARKDOWN_LINK_TITLE_PAREN + || state == SCE_MARKDOWN_LINK_TITLE_SQ + || state == SCE_MARKDOWN_LINK_TITLE_DQ; +} + bool MarkdownLexer::HighlightLinkDestination() { - if (sc.ch == '\\' || sc.ch == '&' || sc.ch == ':') { + if (sc.ch == '\\' || ((sc.ch == '&' || sc.ch == ':') && IsLinkTitleStyle(sc.state))) { + // escape sequence, entity, emoji HighlightInlineText(); return false; } @@ -734,16 +754,19 @@ bool MarkdownLexer::HighlightLinkDestination() { parenCount = 0; const int current = sc.state; const int outer = TakeOuterStyle(); - const Sci_PositionU startPos = TakeOuterStart(); - sc.ForwardSetState(outer); - if (current == SCE_MARKDOWN_LINK_TITLE_PAREN || current == SCE_MARKDOWN_LINK_TITLE_SQ || current == SCE_MARKDOWN_LINK_TITLE_DQ) { + DropOuterStart(); + if (sc.ch == ')' && current != SCE_MARKDOWN_LINK_TITLE_PAREN) { + // make brace matching work + sc.SetState(SCE_MARKDOWN_LINK_TEXT); + } + sc.Forward(); + sc.SetState(outer); + if (IsLinkTitleStyle(current)) { while (IsSpaceOrTab(sc.ch)) { sc.Forward(); } if (sc.ch == ')') { - // use same style for enclosing link parenthesis - const int style = sc.styler.StyleAtEx(startPos); - sc.SetState(style); + sc.SetState(SCE_MARKDOWN_LINK_TEXT); sc.Forward(); sc.SetState(outer); } @@ -792,60 +815,63 @@ bool MarkdownLexer::HighlightLinkDestination() { } // 6.9 Autolinks (extension) -constexpr bool IsSchemeNameChar(int ch) noexcept { - return IsAlphaNumeric(ch) || ch == '+' || ch == '-' || ch == '.'; -} - -constexpr bool IsDomainNameChar(int ch) noexcept { - return IsIdentifierChar(ch) || ch == '-'; -} - bool MarkdownLexer::DetectAutoLink() { - if (!IsAlpha(sc.ch) || IsIdentifierChar(sc.chPrev)) { - return false; - } - int offset = 0; AutoLink result = AutoLink::None; Sci_PositionU pos = sc.currentPos; - if (sc.Match('w', 'w') - && sc.styler.SafeGetCharAt(pos + 2) == 'w' - && sc.styler.SafeGetCharAt(pos + 3) == '.' - && IsDomainNameChar(sc.styler.SafeGetCharAt(pos + 4))) { - offset = 3; - result = AutoLink::Domain; - } else if (IsSchemeNameChar(sc.chNext)) { - int length = 2; - pos += 2; - while (true) { - const uint8_t ch = sc.styler.SafeGetCharAt(pos++); - if (IsSchemeNameChar(ch)) { - ++length; - if (length > 32) { + switch (sc.ch) { + case 'w': + if (sc.chNext == 'w' && !IsIdentifierChar(sc.chPrev) + && sc.styler.SafeGetCharAt(pos + 2) == 'w' + && sc.styler.SafeGetCharAt(pos + 3) == '.' + && IsDomainNameChar(sc.styler.SafeGetCharAt(pos + 4))) { + offset = 3; + result = AutoLink::Domain; + } + break; + + case ':': + if (sc.chNext == '/' && pos >= 2 && IsLowerCase(sc.chPrev) && sc.styler.SafeGetCharAt(pos + 2) == '/') { + // backtrack to find scheme name before `://`, this is more efficient than forward check every word + constexpr int kMinSchemeNameLength = 2; + constexpr int kMaxSchemeNameLength = 32; + const Sci_PositionU startPos = sc.styler.GetStartSegment(); + const Sci_PositionU endPos = pos; + pos -= 2; + uint8_t ch; + while (true) { + ch = sc.styler.SafeGetCharAt(pos); + if (pos == startPos || pos + kMaxSchemeNameLength < endPos || !IsLowerCase(ch)) { break; } - } else { - if (ch == ':' && sc.styler.SafeGetCharAt(pos) == '/' && sc.styler.SafeGetCharAt(pos + 1) == '/') { - // scheme://domain/path, file:///drive:/path - const uint8_t chNext = sc.styler.SafeGetCharAt(pos + 2); - offset = length + 3; - if (chNext == '/') { - result = AutoLink::Path; - } else if (IsDomainNameChar(chNext)) { - result = AutoLink::Domain; - } - } - break; + --pos; + } + if (sc.styler.IsLeadByte(ch)) { + ++pos; + } + + uint8_t chPrev = '\0'; + while (pos < endPos && !IsLowerCase(ch)) { + chPrev = ch; + ch = sc.styler.SafeGetCharAt(++pos); + } + + offset = static_cast(endPos - pos); + if (offset >= kMinSchemeNameLength && !IsIdentifierChar(chPrev)) { + offset += 3; + result = AutoLink::Path; + // go back to scheme start position and change style from here + sc.currentPos = pos; } } + break; } if (result != AutoLink::None) { - tagState = HtmlTagState::None; periodCount = 0; autoLink = result; SaveOuterStyle(sc.state); - sc.SetState(SCE_MARKDOWN_AUTOLINK); + sc.SetState(STYLE_LINK); sc.Advance(offset); return true; } @@ -853,7 +879,6 @@ bool MarkdownLexer::DetectAutoLink() { } bool MarkdownLexer::HighlightAutoLink() { - bool invalid = false; switch (autoLink) { case AutoLink::Angle: if (sc.ch == '>') { @@ -861,7 +886,12 @@ bool MarkdownLexer::HighlightAutoLink() { return true; } if (sc.ch == '<' || !IsGraphic(sc.ch)) { - invalid = true; + periodCount = 0; + autoLink = AutoLink::None; + sc.ChangeState(TakeOuterStyle()); + sc.Rewind(); + sc.Forward(); + return true; } break; @@ -870,49 +900,61 @@ bool MarkdownLexer::HighlightAutoLink() { ++periodCount; sc.Forward(); } else if (!IsDomainNameChar(sc.ch)) { - invalid = (periodCount == 0 && tagState == HtmlTagState::None) - || (sc.ch == ':' && !IsADigit(sc.chNext)); - tagState = HtmlTagState::None; + const bool invalid = periodCount == 0; periodCount = 0; - if (!invalid) { - if (sc.ch == ':' || ((sc.ch == '/' || sc.ch == '?') && !IsInvalidUrlChar(sc.chNext))) { - parenCount = 0; - autoLink = AutoLink::Path; + if (!invalid && ((sc.ch == ':' && IsADigit(sc.chNext)) + || ((sc.ch == '/' || sc.ch == '?' || sc.ch == '#') && !IsInvalidUrlChar(sc.chNext)))) { + autoLink = AutoLink::Path; + } + if (autoLink != AutoLink::Path) { + autoLink = AutoLink::None; + const int outer = TakeOuterStyle(); + if (invalid) { + sc.ChangeState(outer); + sc.Rewind(); + sc.Forward(); } else { if (sc.ch == '/') { sc.Forward(); } - sc.SetState(TakeOuterStyle()); - return true; + sc.SetState(outer); } + return true; } } break; default: { if (sc.ch == '(') { - ++parenCount; + ++periodCount; } else if (sc.ch == ')') { - --parenCount; + --periodCount; } - invalid = IsInvalidUrlChar(sc.chNext); + bool invalid = IsInvalidUrlChar(sc.chNext); const bool punctuation = IsPunctuation(sc.ch); if (punctuation && !invalid) { const int outer = nestedState.back(); switch (outer) { - case SCE_MARKDOWN_EM_UNDERSCORE: - case SCE_MARKDOWN_STRONG_UNDERSCORE: - if (sc.ch == '_') { - // similar to HighlightEmphasis() - DelimiterRun delimiterRun; - const int length = GetCurrentDelimiterRun(delimiterRun); - invalid = (length == 1 && sc.state == SCE_MARKDOWN_STRONG_UNDERSCORE) - || delimiterRun.CanOpen('_') || delimiterRun.CanClose('_'); + default: + if (sc.state >= SCE_MARKDOWN_HEADER1) { + const bool current = IsEmphasisDelimiter(sc.ch); + if (current || IsEmphasisDelimiter(sc.chNext)) { + // similar to HighlightEmphasis() + DelimiterRun delimiterRun; + const int length = GetCurrentDelimiterRun(delimiterRun, !current); + const int delimiter = current ? sc.ch : sc.chNext; + if ((delimiter != '~' || length >= 2) + && (delimiterRun.CanOpen(delimiter) || delimiterRun.CanClose(delimiter))) { + invalid = true; + } else { + sc.Advance(length - current); + } + } } break; - case SCE_MARKDOWN_STRIKEOUT: - invalid = sc.Match('~', '~'); + case SCE_H_SINGLESTRING: + invalid = sc.ch == '\''; break; case SCE_H_COMMENT: @@ -925,24 +967,17 @@ bool MarkdownLexer::HighlightAutoLink() { } } if (invalid) { - if (!punctuation || sc.ch == '/' || (sc.ch == ')' && parenCount == 0)) { + if (!punctuation || sc.ch == '/' || (sc.ch == ')' && periodCount == 0)) { sc.Forward(); } - parenCount = 0; + periodCount = 0; autoLink = AutoLink::None; sc.SetState(TakeOuterStyle()); return true; } } break; } - if (invalid) { - parenCount = 0; - autoLink = AutoLink::None; - sc.ChangeState(TakeOuterStyle()); - sc.Rewind(); - sc.Forward(); - return true; - } + return false; } @@ -963,11 +998,6 @@ constexpr bool IsHtmlAttrChar(int ch) noexcept { return IsIdentifierChar(ch) || ch == ':' || ch == '.' || ch == '-'; } -constexpr bool IsInvalidAttrChar(int ch) noexcept { - // characters not allowed in unquoted attribute value - return ch <= 32 || ch == 127 || AnyOf(ch, '"', '\'', '\\', '`', '=', '<', '>'); -} - constexpr bool IsHtmlBlockStartChar(int ch) noexcept { return ch == '!' || ch == '?' || ch == '/' || IsHtmlTagStart(ch); } @@ -1217,7 +1247,7 @@ void MarkdownLexer::HighlightInlineText() { sc.Forward(); } else { autoLink = AutoLink::Angle; - sc.SetState(SCE_MARKDOWN_AUTOLINK); + sc.SetState(STYLE_LINK); } } else if (sc.chNext == '?') { // @@ -1233,7 +1263,7 @@ void MarkdownLexer::HighlightInlineText() { } } else if (!IsInvalidUrlChar(sc.chNext)) { autoLink = AutoLink::Angle; - sc.SetState(SCE_MARKDOWN_AUTOLINK); + sc.SetState(STYLE_LINK); } break; @@ -1351,20 +1381,19 @@ void MarkdownLexer::HighlightInlineText() { sc.SetState(SCE_MARKDOWN_CITATION_AT); } break; - - default: - if (bracketCount == 0 && DetectAutoLink()) { - return; - } - break; } - if (handled || (current != sc.state && IsInlineStyle(sc.state))) { - SaveOuterStyle(current); + if (handled || current != sc.state) { + if (handled || IsInlineStyle(sc.state)) { + SaveOuterStyle(current); + } + } else if (bracketCount == 0) { + DetectAutoLink(); } } bool MarkdownLexer::HighlightInlineDiff() { if (sc.ch == '\\' || sc.ch == '&' || sc.ch == ':') { + // escape sequence, entity, emoji HighlightInlineText(); } else if (IsEOLChar(sc.ch) || sc.ch == '`' || (sc.ch == '<' && IsHtmlBlockStartChar(sc.chNext))) { sc.ChangeState(nestedState.back()); @@ -1611,7 +1640,7 @@ void ColouriseMarkdownDoc(Sci_PositionU startPos, Sci_Position lengthDoc, int in } break; - case SCE_MARKDOWN_AUTOLINK: + case STYLE_LINK: if (lexer.HighlightAutoLink()) { continue; } @@ -1626,6 +1655,8 @@ void ColouriseMarkdownDoc(Sci_PositionU startPos, Sci_Position lengthDoc, int in sc.ForwardSetState(lexer.TakeOuterStyle()); continue; } + } else { + lexer.DetectAutoLink(); } break; @@ -1720,7 +1751,7 @@ void ColouriseMarkdownDoc(Sci_PositionU startPos, Sci_Position lengthDoc, int in } else { lexer.tagState = HtmlTagState::None; lexer.autoLink = AutoLink::Angle; - sc.ChangeState(SCE_MARKDOWN_AUTOLINK); + sc.ChangeState(STYLE_LINK); continue; } } @@ -1734,8 +1765,6 @@ void ColouriseMarkdownDoc(Sci_PositionU startPos, Sci_Position lengthDoc, int in // https://html.spec.whatwg.org/entities.json if (!IsAlphaNumeric(sc.ch)) { sc.ChangeState(lexer.TakeOuterStyle()); - sc.Rewind(); - sc.Forward(); continue; } break; @@ -1747,7 +1776,7 @@ void ColouriseMarkdownDoc(Sci_PositionU startPos, Sci_Position lengthDoc, int in break; case SCE_H_VALUE: - if (IsInvalidAttrChar(sc.ch)) { + if (IsHtmlInvalidAttrChar(sc.ch)) { sc.SetState(SCE_MARKDOWN_DEFAULT); } break; @@ -1755,12 +1784,16 @@ void ColouriseMarkdownDoc(Sci_PositionU startPos, Sci_Position lengthDoc, int in case SCE_H_SINGLESTRING: if (sc.ch == '\'') { sc.ForwardSetState(SCE_MARKDOWN_DEFAULT); + } else { + lexer.DetectAutoLink(); } break; case SCE_H_DOUBLESTRING: if (sc.ch == '\"') { sc.ForwardSetState(SCE_MARKDOWN_DEFAULT); + } else { + lexer.DetectAutoLink(); } break; @@ -1851,7 +1884,7 @@ void ColouriseMarkdownDoc(Sci_PositionU startPos, Sci_Position lengthDoc, int in sc.SetState(SCE_H_OTHER); } else if (lexer.tagState == HtmlTagState::Open && IsHtmlAttrStart(sc.ch)) { sc.SetState(SCE_H_ATTRIBUTE); - } else if (!IsInvalidAttrChar(sc.ch)) { + } else if (!IsHtmlInvalidAttrChar(sc.ch)) { sc.SetState(SCE_H_VALUE); } if (sc.state != SCE_MARKDOWN_DEFAULT) { diff --git a/scintilla/lexers/LexRebol.cxx b/scintilla/lexers/LexRebol.cxx index 4d55a87c35..2e842c77ee 100644 --- a/scintilla/lexers/LexRebol.cxx +++ b/scintilla/lexers/LexRebol.cxx @@ -238,7 +238,7 @@ void ColouriseRebolDoc(Sci_PositionU startPos, Sci_Position lengthDoc, int initS case SCE_REBOL_FILE: case SCE_REBOL_EMAIL: case SCE_REBOL_URL: - if (sc.atLineEnd || isspacechar(sc.ch) || sc.ch == ']') { + if (sc.atLineEnd || !IsGraphic(sc.ch) || sc.ch == ']') { sc.SetState(SCE_REBOL_DEFAULT); } break; diff --git a/scintilla/lexlib/CharacterSet.h b/scintilla/lexlib/CharacterSet.h index 92d6c61460..e7e07268ee 100644 --- a/scintilla/lexlib/CharacterSet.h +++ b/scintilla/lexlib/CharacterSet.h @@ -172,6 +172,16 @@ constexpr bool IsHexDigit(int ch) noexcept { || (ch >= 'a' && ch <= 'f'); } +constexpr bool IsLowerHex(int ch) noexcept { + return (ch >= '0' && ch <= '9') + || (ch >= 'a' && ch <= 'f'); +} + +constexpr bool IsUpperHex(int ch) noexcept { + return (ch >= '0' && ch <= '9') + || (ch >= 'A' && ch <= 'F'); +} + constexpr bool IsOctalDigit(int ch) noexcept { return ch >= '0' && ch <= '7'; } @@ -323,9 +333,22 @@ constexpr bool IsCommentTagPrev(int chPrev) noexcept { return chPrev <= 32 || AnyOf(chPrev, '/', '*', '!'); } +constexpr bool IsSchemeNameChar(int ch) noexcept { + return IsAlphaNumeric(ch) || ch == '+' || ch == '-' || ch == '.'; +} + +constexpr bool IsDomainNameChar(int ch) noexcept { + return IsIdentifierChar(ch) || ch == '-'; +} + constexpr bool IsInvalidUrlChar(int ch) noexcept { - // win32 file name reserved characters: '<', '>', ':', '"', '|', '?', '*' - return ch <= 32 || AnyOf(ch, '"', '<', '>', '\\', '^', '`', '{', '|', '}', '*', 127); + // TODO: https://url.spec.whatwg.org/ and https://www.rfc-editor.org/rfc/rfc3986 + return ch <= 32 || AnyOf(ch, '"', '<', '>', '\\', '^', '`', '{', '|', '}', 127); +} + +constexpr bool IsHtmlInvalidAttrChar(int ch) noexcept { + // characters not allowed in unquoted attribute value + return ch <= 32 || AnyOf(ch, '"', '\'', '\\', '`', '=', '<', '>', 127); } // characters can follow jump `label:`, based on Swift's document Labeled Statement at diff --git a/src/Edit.c b/src/Edit.c index 14d6f209fb..7f077128e4 100644 --- a/src/Edit.c +++ b/src/Edit.c @@ -7259,6 +7259,21 @@ char* EditGetStringAroundCaret(LPCSTR delimiters) { return NULL; } + const int style = SciCall_GetStyleAt(iCurrentPos); + if (SciCall_StyleGetHotSpot(style)) { + Sci_Position iPos = iCurrentPos - 1; + while (SciCall_GetStyleAt(iPos) == style) { + --iPos; + } + iLineStart = iPos + 1; + iPos = iCurrentPos + 1; + while (SciCall_GetStyleAt(iPos) == style) { + ++iPos; + } + iLineEnd = iPos; + goto labelEnd; + } + struct Sci_TextToFind ft = { { iCurrentPos, 0 }, delimiters, { 0, 0 } }; const int findFlag = SCFIND_REGEXP | SCFIND_POSIX; @@ -7314,6 +7329,7 @@ char* EditGetStringAroundCaret(LPCSTR delimiters) { return NULL; } +labelEnd: char *mszSelection = (char *)NP2HeapAlloc(iLineEnd - iLineStart + 1); struct Sci_TextRange tr = { { iLineStart, iLineEnd }, mszSelection }; SciCall_GetTextRange(&tr); @@ -7343,7 +7359,7 @@ static DWORD EditOpenSelectionCheckFile(LPCWSTR link, LPWSTR path, int cchFilePa return dwAttributes; } -void EditOpenSelection(int type) { +void EditOpenSelection(OpenSelectionType type) { Sci_Position cchSelection = SciCall_GetSelTextLength(); char *mszSelection = NULL; if (cchSelection != 0) { @@ -7449,34 +7465,39 @@ void EditOpenSelection(int type) { } } - if (type == 4) { // containing folder + if (type == OpenSelectionType_ContainingFolder) { if (dwAttributes == INVALID_FILE_ATTRIBUTES) { - type = 0; + type = OpenSelectionType_None; } } else if (dwAttributes != INVALID_FILE_ATTRIBUTES) { if (dwAttributes & FILE_ATTRIBUTE_DIRECTORY) { - type = 3; + type = OpenSelectionType_Folder; } else { const BOOL can = line != NULL || Style_CanOpenFile(link); // open supported file in a new window - type = can ? 2 : 1; + type = can ? OpenSelectionType_File : OpenSelectionType_Link; } } else if (StrChr(link, L':')) { // link // TODO: check scheme - type = 1; + type = OpenSelectionType_Link; } else if (StrChr(link, L'@')) { // email lstrcpy(wszSelection, L"mailto:"); lstrcpy(wszSelection + CSTRLEN(L"mailto:"), link); - type = 1; + type = OpenSelectionType_Link; + link = wszSelection; + } else if (StrHasPrefix(link, L"www.")) { + lstrcpy(wszSelection, L"http://"); // browser should auto switch to https + lstrcpy(wszSelection + CSTRLEN(L"http://"), link); + type = OpenSelectionType_Link; link = wszSelection; } switch (type) { - case 1: + case OpenSelectionType_Link: ShellExecute(hwndMain, L"open", link, NULL, NULL, SW_SHOWNORMAL); break; - case 2: { + case OpenSelectionType_File: { WCHAR szModuleName[MAX_PATH]; GetModuleFileName(NULL, szModuleName, COUNTOF(szModuleName)); @@ -7509,7 +7530,7 @@ void EditOpenSelection(int type) { } break; - case 3: + case OpenSelectionType_Folder: if (bOpenFolderWithMetapath) { TryBrowseFile(hwndMain, link, FALSE); } else { @@ -7517,9 +7538,37 @@ void EditOpenSelection(int type) { } break; - case 4: + case OpenSelectionType_ContainingFolder: OpenContainingFolder(hwndMain, link, TRUE); break; + + default: + if (cchTextW > 1 && link[0] == L'#') { + // regex find link anchor in current document + // html, markdown: (id | name) = [' | "] anchor [' | "] + mszSelection = (char *)NP2HeapAlloc(2*cchSelection + 32); + strcpy(mszSelection, "name\\s*=\\s*[\'\"]?"); + char *lpstrText = mszSelection + CSTRLEN("name\\s*=\\s*[\'\"]?"); + char* const lpszArgs = lpstrText + cchSelection; + WideCharToMultiByte(cpEdit, 0, link + 1, cchTextW - 1, lpszArgs, (int)(cchSelection + 16), NULL, NULL); + EscapeRegex(lpstrText, lpszArgs); + strcat(lpstrText, "[\'\"]?"); + + struct Sci_TextToFind ft = { { 0, SciCall_GetLength() }, mszSelection, { 0, 0 } }; + Sci_Position iPos = SciCall_FindText(SCFIND_REGEXP | SCFIND_POSIX, &ft); + if (iPos < 0) { + lpstrText = mszSelection + 2; + lpstrText[0] = 'i'; + lpstrText[1] = 'd'; + ft.lpstrText = lpstrText; + iPos = SciCall_FindText(SCFIND_REGEXP | SCFIND_POSIX, &ft); + } + NP2HeapSize(mszSelection); + if (iPos >= 0) { + EditSelectEx(ft.chrgText.cpMin, ft.chrgText.cpMax); + } + } + break; } } diff --git a/src/Edit.h b/src/Edit.h index c7025ff614..add09cc9f6 100644 --- a/src/Edit.h +++ b/src/Edit.h @@ -191,7 +191,15 @@ BOOL EditSortDlg(HWND hwnd, EditSortFlag *piSortFlags); BOOL EditAlignDlg(HWND hwnd, EditAlignMode *piAlignMode); void EditSelectionAction(int action); void TryBrowseFile(HWND hwnd, LPCWSTR pszFile, BOOL bWarn); -void EditOpenSelection(int type); + +typedef enum OpenSelectionType { + OpenSelectionType_None, + OpenSelectionType_Link, + OpenSelectionType_File, + OpenSelectionType_Folder, + OpenSelectionType_ContainingFolder, +} OpenSelectionType; +void EditOpenSelection(OpenSelectionType type); // in Print.cpp #ifdef __cplusplus diff --git a/src/EditAutoC.c b/src/EditAutoC.c index ffdd9cff40..1db67c9dbd 100644 --- a/src/EditAutoC.c +++ b/src/EditAutoC.c @@ -768,7 +768,7 @@ static int GetCurrentHtmlTextBlock(void) { return GetCurrentHtmlTextBlockEx(iCurrentStyle); } -static void EscapeRegex(LPSTR pszOut, LPCSTR pszIn) { +void EscapeRegex(LPSTR pszOut, LPCSTR pszIn) { char ch; while ((ch = *pszIn++) != '\0') { if (ch == '.' // any character diff --git a/src/EditLexers/EditStyle.h b/src/EditLexers/EditStyle.h index b94cd8284d..2eca969c1e 100644 --- a/src/EditLexers/EditStyle.h +++ b/src/EditLexers/EditStyle.h @@ -20,6 +20,7 @@ #define NP2STYLE_Bookmark 63218 #define NP2STYLE_CallTip 63219 #define NP2STYLE_CodeFolding 63220 +#define NP2STYLE_Link 63221 #define NP2STYLE_Default 63226 #define NP2STYLE_Comment 63227 @@ -245,7 +246,7 @@ #define NP2STYLE_InternalFilter 63655 #define NP2STYLE_ExternalFilter 63656 #define NP2STYLE_File 63657 -#define NP2STYLE_Link 63658 + #define NP2STYLE_Money 63659 #define NP2STYLE_Issue 63660 #define NP2STYLE_MagicMethod 63661 diff --git a/src/EditLexers/EditStyleX.h b/src/EditLexers/EditStyleX.h index 093423a1c9..0c8f29949b 100644 --- a/src/EditLexers/EditStyleX.h +++ b/src/EditLexers/EditStyleX.h @@ -32,6 +32,7 @@ #define NP2StyleX_Bookmark EDITSTYLE_HOLE(Bookmark, L"Bookmark") #define NP2StyleX_CallTip EDITSTYLE_HOLE(CallTip, L"CallTip") #define NP2StyleX_CodeFolding EDITSTYLE_HOLE(CodeFolding, L"Code Folding") +#define NP2StyleX_Link EDITSTYLE_HOLE(Link, L"Link") #define NP2StyleX_Default EDITSTYLE_HOLE(Default, L"Default") #define NP2StyleX_Comment EDITSTYLE_HOLE(Comment, L"Comment") @@ -257,7 +258,7 @@ #define NP2StyleX_InternalFilter EDITSTYLE_HOLE(InternalFilter, L"Internal Filter") #define NP2StyleX_ExternalFilter EDITSTYLE_HOLE(ExternalFilter, L"External Filter") #define NP2StyleX_File EDITSTYLE_HOLE(File, L"File") -#define NP2StyleX_Link EDITSTYLE_HOLE(Link, L"Link") + #define NP2StyleX_Money EDITSTYLE_HOLE(Money, L"Money") #define NP2StyleX_Issue EDITSTYLE_HOLE(Issue, L"Issue") #define NP2StyleX_MagicMethod EDITSTYLE_HOLE(MagicMethod, L"Magic Method") diff --git a/src/EditLexers/stlDefault.c b/src/EditLexers/stlDefault.c index 2c50a801c5..184fd81927 100644 --- a/src/EditLexers/stlDefault.c +++ b/src/EditLexers/stlDefault.c @@ -28,6 +28,7 @@ static EDITSTYLE Styles_Global[] = { { 0, NP2StyleX_MarkOccurrences, L"alpha:100; outline:150" }, { 0, NP2StyleX_Bookmark, L"fore:#408040; back:#00FF00; alpha:40" }, { STYLE_CALLTIP, NP2StyleX_CallTip, L"" }, + { STYLE_LINK, NP2StyleX_Link, L"fore:#648000" }, }; static EDITSTYLE Styles_2ndGlobal[] = { @@ -51,6 +52,7 @@ static EDITSTYLE Styles_2ndGlobal[] = { { 0, NP2StyleX_MarkOccurrences, L"alpha:100; outline:150" }, { 0, NP2StyleX_Bookmark, L"fore:#408040; back:#00FF00; alpha:40" }, { STYLE_CALLTIP, NP2StyleX_CallTip, L"" }, + { STYLE_LINK, NP2StyleX_Link, L"fore:#648000" }, }; EDITLEXER lexGlobal = { @@ -196,7 +198,6 @@ static EDITSTYLE Styles_Markdown[] = { { MULTI_STYLE(SCE_MARKDOWN_DISPLAY_MATH, SCE_MARKDOWN_BACKTICK_MATH, SCE_MARKDOWN_TILDE_MATH, 0), NP2StyleX_DisplayMath, L"back:#C5C5C5; eolfilled" }, { SCE_MARKDOWN_INLINE_MATH, NP2StyleX_InlineMath, L"back:#C5C5C5" }, { MULTI_STYLE(SCE_MARKDOWN_LINK_TEXT, SCE_MARKDOWN_LINK_TITLE_SQ, SCE_MARKDOWN_LINK_TITLE_DQ, SCE_MARKDOWN_LINK_TITLE_PAREN), NP2StyleX_LinkText, L"fore:#3A6EA5" }, - { MULTI_STYLE(SCE_MARKDOWN_AUTOLINK, SCE_MARKDOWN_PLAIN_LINK, SCE_MARKDOWN_PAREN_LINK, SCE_MARKDOWN_ANGLE_LINK), NP2StyleX_Link, L"fore:#648000" }, { MULTI_STYLE(SCE_MARKDOWN_EM_ASTERISK, SCE_MARKDOWN_EM_UNDERSCORE, 0, 0), NP2StyleX_Emphasis, L"italic" }, { MULTI_STYLE(SCE_MARKDOWN_STRONG_ASTERISK, SCE_MARKDOWN_STRONG_UNDERSCORE, 0, 0), NP2StyleX_Strong, L"bold" }, { SCE_MARKDOWN_STRIKEOUT, NP2StyleX_Strikethrough, L"strike" }, diff --git a/src/EditLexers/stlRebol.c b/src/EditLexers/stlRebol.c index 5427b4117a..887dc727fd 100644 --- a/src/EditLexers/stlRebol.c +++ b/src/EditLexers/stlRebol.c @@ -82,7 +82,6 @@ static EDITSTYLE Styles_Rebol[] = { { SCE_REBOL_SYMBOL, NP2StyleX_Symbol, L"fore:#7C5AF3" }, { SCE_REBOL_OPERATOR, NP2StyleX_Operator, L"fore:#B000B0" }, { MULTI_STYLE(SCE_REBOL_FILE, SCE_REBOL_QUOTEDFILE, 0, 0), NP2StyleX_File, L"fore:#648000" }, - { MULTI_STYLE(SCE_REBOL_URL, SCE_REBOL_EMAIL, 0, 0), NP2StyleX_Link, L"fore:#648000" }, { SCE_REBOL_MONEY, NP2StyleX_Money, L"fore:#7C5AF3" }, { SCE_REBOL_ISSUE, NP2StyleX_Issue, L"fore:#9E4D2A" }, { MULTI_STYLE(SCE_REBOL_DATE, SCE_REBOL_TIME, 0, 0), NP2StyleX_DateTime, L"fore:#008080" }, diff --git a/src/Helpers.h b/src/Helpers.h index a64356c7f7..1b05e35bbb 100644 --- a/src/Helpers.h +++ b/src/Helpers.h @@ -940,6 +940,7 @@ UINT_PTR CALLBACK OpenSaveFileDlgHookProc(HWND hWnd, UINT uMsg, WPARAM wParam, L void TransformBackslashes(char *pszInput, BOOL bRegEx, UINT cpEdit); BOOL AddBackslashA(char *pszOut, const char *pszInput); BOOL AddBackslashW(LPWSTR pszOut, LPCWSTR pszInput); +void EscapeRegex(LPSTR pszOut, LPCSTR pszIn); //==== MinimizeToTray Functions - see comments in Helpers.c =================== BOOL GetDoAnimateMinimize(void); diff --git a/src/Notepad2.c b/src/Notepad2.c index f697c45eda..bfec6cb298 100644 --- a/src/Notepad2.c +++ b/src/Notepad2.c @@ -4675,10 +4675,10 @@ LRESULT MsgCommand(HWND hwnd, WPARAM wParam, LPARAM lParam) { break; case CMD_OPEN_PATH_OR_LINK: - EditOpenSelection(0); + EditOpenSelection(OpenSelectionType_None); break; case CMD_OPEN_CONTAINING_FOLDER: - EditOpenSelection(4); + EditOpenSelection(OpenSelectionType_ContainingFolder); break; case CMD_ONLINE_SEARCH_GOOGLE: @@ -5209,6 +5209,13 @@ LRESULT MsgNotify(HWND hwnd, WPARAM wParam, LPARAM lParam) { case SCN_CODEPAGECHANGED: EditOnCodePageChanged(scn->oldCodePage, bShowUnicodeControlCharacter, &efrData); break; + + case SCN_HOTSPOTCLICK: + if (scn->modifiers & SCMOD_CTRL) { + SciCall_SetSel(scn->position, scn->position); + EditOpenSelection(OpenSelectionType_None); + } + break; } break; diff --git a/src/Notepad2.rc b/src/Notepad2.rc index a453406ca4..6f3aea2ce1 100644 --- a/src/Notepad2.rc +++ b/src/Notepad2.rc @@ -2205,6 +2205,7 @@ BEGIN NP2STYLE_Bookmark "Bookmark" NP2STYLE_CallTip "CallTip" NP2STYLE_CodeFolding "Code Folding" + NP2STYLE_Link "Link" NP2STYLE_Default "Default" END diff --git a/src/SciCall.h b/src/SciCall.h index 0aa0483884..89c2bb9eeb 100644 --- a/src/SciCall.h +++ b/src/SciCall.h @@ -695,6 +695,14 @@ NP2_inline void SciCall_StyleSetCharacterSet(int style, int characterSet) { SciCall(SCI_STYLESETCHARACTERSET, style, characterSet); } +NP2_inline void SciCall_StyleSetHotSpot(int style, BOOL hotspot) { + SciCall(SCI_STYLESETHOTSPOT, style, hotspot); +} + +NP2_inline BOOL SciCall_StyleGetHotSpot(int style) { + return (BOOL)SciCall(SCI_STYLEGETHOTSPOT, style, 0); +} + NP2_inline void SciCall_StyleSetCheckMonospaced(int style, BOOL checkMonospaced) { SciCall(SCI_STYLESETCHECKMONOSPACED, style, checkMonospaced); } diff --git a/src/Styles.c b/src/Styles.c index bd31d2f3ae..d64af5c5cd 100644 --- a/src/Styles.c +++ b/src/Styles.c @@ -404,6 +404,7 @@ enum GlobalStyleIndex { GlobalStyleIndex_MarkOccurrences, // indicator style. `fore`, `alpha`, `outline` GlobalStyleIndex_Bookmark, // indicator style. `fore`, `back`, `alpha` GlobalStyleIndex_CallTip, // inherited style. + GlobalStyleIndex_Link, // inherited style. }; // styles in ANSI Art used to override global styles. @@ -1900,6 +1901,9 @@ void Style_SetLexer(PEDITLEXER pLexNew, BOOL bLexerChanged) { // CallTip Style_SetDefaultStyle(GlobalStyleIndex_CallTip); + // HotSpot + Style_SetDefaultStyle(GlobalStyleIndex_Link); + SciCall_StyleSetHotSpot(STYLE_LINK, TRUE); if (SciCall_GetIndentationGuides() != SC_IV_NONE) { Style_SetIndentGuides(TRUE); @@ -1955,10 +1959,15 @@ void Style_SetLexer(PEDITLEXER pLexNew, BOOL bLexerChanged) { #endif break; + case SCLEX_REBOL: + SciCall_CopyStyles(STYLE_LINK, MULTI_STYLE(SCE_REBOL_URL, SCE_REBOL_EMAIL, 0, 0)); + break; + case SCLEX_MARKDOWN: if (!IsStyleLoaded(&lexHTML)) { Style_LoadOne(&lexHTML); } + SciCall_CopyStyles(STYLE_LINK, MULTI_STYLE(SCE_MARKDOWN_PLAIN_LINK, SCE_MARKDOWN_PAREN_LINK, SCE_MARKDOWN_ANGLE_LINK, 0)); for (UINT i = 1; i < lexHTML.iStyleCount; i++) { const UINT iStyle = lexHTML.Styles[i].iStyle; szValue = lexHTML.Styles[i].szValue;