Skip to content

Source parser

parser

Parse static AutoCorrect2 hotstring declarations from source text.

The parser treats trigger source spelling as AutoHotkey syntax rather than as plain text. In particular, an escaped colon (`:) is part of the trigger and must not be mistaken for the closing :: delimiter. Parsed trigger text is passed to ExistingHotstring in AHK source form; the model then derives semantic and canonical source forms.

LOGGER module-attribute

LOGGER: Final[Logger] = logging.getLogger(__name__)

Module logger used for parser diagnostics.

HOTSTRING_PREFIX_PATTERN module-attribute

HOTSTRING_PREFIX_PATTERN: Final[Pattern[str]] = re.compile(
    "^[ \\t]*:([^\\r\\n:]*):"
)

Pattern capturing a static declaration's indentation and option prefix.

extract_hotstrings

extract_hotstrings(
    content: str, *, source: str | Path
) -> list[ExistingHotstring]

Extract static hotstring declarations from decoded AutoCorrect2 text.

The function performs no filesystem I/O. Each source line is checked for a leading :options: prefix. The trigger is then scanned character by character so escaped characters cannot terminate it accidentally.

Parameters:

Name Type Description Default
content str

Decoded AutoCorrect2 source text.

required
source str | Path

Source identifier stored on each extracted definition.

required

Returns:

Type Description
list[ExistingHotstring]

Existing hotstrings in declaration order.

Raises:

Type Description
TypeError

If the supplied source data has an invalid type.

ValueError

If a recognized declaration contains invalid options or invalid AHK trigger escaping.

Source code in hotstring\autocorrect2\source_loading\parser.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def extract_hotstrings(content: str, *, source: str | Path) -> list[ExistingHotstring]:
    """Extract static hotstring declarations from decoded AutoCorrect2 text.

    The function performs no filesystem I/O. Each source line is checked for a
    leading ``:options:`` prefix. The trigger is then scanned character by
    character so escaped characters cannot terminate it accidentally.

    Args:
        content:
            Decoded AutoCorrect2 source text.
        source:
            Source identifier stored on each extracted definition.

    Returns:
        Existing hotstrings in declaration order.

    Raises:
        TypeError:
            If the supplied source data has an invalid type.
        ValueError:
            If a recognized declaration contains invalid options or invalid
            AHK trigger escaping.
    """
    if not isinstance(content, str):
        raise TypeError(
            f"Hotstring source content must be a string, got {type(content).__name__}"
        )
    if not isinstance(source, (str, Path)):
        raise TypeError(
            "Hotstring source identifier must be a string or Path, "
            f"got {type(source).__name__}"
        )

    source_path = Path(source)
    LOGGER.debug("Parsing static hotstrings from source %s.", source_path)

    hotstrings: list[ExistingHotstring] = []
    for line in content.splitlines():
        prefix_match = HOTSTRING_PREFIX_PATTERN.match(line)
        if prefix_match is None:
            continue

        trigger_start = prefix_match.end()
        trigger_end = _find_trigger_delimiter(line, start=trigger_start)
        if trigger_end is None:
            continue

        hotstrings.append(
            ExistingHotstring(
                trigger=line[trigger_start:trigger_end],
                options_input=prefix_match.group(1),
                source=source_path,
            )
        )

    LOGGER.debug(
        "Parsed %d static hotstring(s) from source %s.",
        len(hotstrings),
        source_path,
    )
    return hotstrings

_find_trigger_delimiter

_find_trigger_delimiter(
    line: str, *, start: int
) -> int | None

Locate the first unescaped :: trigger delimiter on one source line.

A backtick escapes the next source character, so a colon immediately after a backtick cannot begin the delimiter. Two consecutive backticks represent one literal backtick; scanning the pair as one escaped unit naturally lets a following :: terminate the trigger.

An unescaped semicolon preceded by horizontal whitespace begins an AutoHotkey comment. If such a comment starts before a closing delimiter, the line does not contain a complete static hotstring declaration.

Parameters:

Name Type Description Default
line str

One source line without its newline terminator.

required
start int

Index immediately after the :options: prefix.

required

Returns:

Type Description
int | None

Index of the first colon in the closing delimiter, or None when no

int | None

valid delimiter occurs before the end of the source line.

Source code in hotstring\autocorrect2\source_loading\parser.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def _find_trigger_delimiter(line: str, *, start: int) -> int | None:
    """Locate the first unescaped ``::`` trigger delimiter on one source line.

    A backtick escapes the next source character, so a colon immediately after
    a backtick cannot begin the delimiter. Two consecutive backticks represent
    one literal backtick; scanning the pair as one escaped unit naturally lets
    a following ``::`` terminate the trigger.

    An unescaped semicolon preceded by horizontal whitespace begins an
    AutoHotkey comment. If such a comment starts before a closing delimiter,
    the line does not contain a complete static hotstring declaration.

    Args:
        line:
            One source line without its newline terminator.
        start:
            Index immediately after the ``:options:`` prefix.

    Returns:
        Index of the first colon in the closing delimiter, or `None` when no
        valid delimiter occurs before the end of the source line.
    """
    index = start

    while index < len(line):
        character = line[index]

        if character == "`":
            if index + 1 >= len(line):
                return None
            index += 2
            continue

        if (
            character == ";"
            and index > 0
            and line[index - 1] in {" ", "\t"}
        ):
            return None

        if character == ":" and index + 1 < len(line) and line[index + 1] == ":":
            return index

        index += 1

    return None