Coverage for apio/utils/jsonc.py: 98%
41 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-06 10:20 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-06 10:20 +0000
1# -*- coding: utf-8 -*-
2# -- This file is part of the Apio project
3# -- (C) 2016-2024 FPGAwars
4# -- Author Jesús Arroyo
5# -- License GPLv2
6# -- Derived from:
7# ---- Platformio project
8# ---- (C) 2014-2016 Ivan Kravets <me@ikravets.com>
9# ---- License Apache v2
12"""A simple utility to string '//' comments from a json file.
13It allows to add comments in apio resource files.
15Inspired by the state machine in
16https://github.com/xitop/jsonstrip/blob/main/jsonstrip.py
18Json5 could do the job but it's very slow, doubling the execution time of
19'apio system info'.
20"""
22from enum import Enum
23from dataclasses import dataclass
26class _State(Enum):
27 """Represents the parsing state."""
29 IDLE = 1
30 IN_STRING = 2
31 IN_STRING_ESCAPE = 3
32 IN_FIRST_SLASH = 4
33 IN_COMMENT = 5
36class _Action(Enum):
37 """Actions that are performed on the finite state transitions"""
39 NO_ACTION = 1
40 COPY_UP_TO_COMMENT = 2
41 SKIP_TO_CURRENT_LOCATION = 3
44@dataclass(frozen=True)
45class _Transition:
46 new_state: _State
47 action: _Action
50# A key for any other char.
51ELSE = "ELSE"
54# -- A dict from a state to state transitions. Each state transition is a
55# -- dict from an input char to a transition, with ELSE is the transition to
56# -- all other chars.
57STATES_TRANSITIONS = {
58 _State.IDLE: {
59 "/": _Transition(_State.IN_FIRST_SLASH, _Action.NO_ACTION),
60 '"': _Transition(_State.IN_STRING, _Action.NO_ACTION),
61 },
62 _State.IN_STRING: {
63 "\\": _Transition(_State.IN_STRING_ESCAPE, _Action.NO_ACTION),
64 '"': _Transition(_State.IDLE, _Action.NO_ACTION),
65 },
66 _State.IN_STRING_ESCAPE: {
67 ELSE: _Transition(_State.IN_STRING, _Action.NO_ACTION),
68 },
69 _State.IN_FIRST_SLASH: {
70 "/": _Transition(_State.IN_COMMENT, _Action.COPY_UP_TO_COMMENT),
71 ELSE: _Transition(_State.IDLE, _Action.NO_ACTION),
72 },
73 _State.IN_COMMENT: {
74 "\r": _Transition(_State.IDLE, _Action.SKIP_TO_CURRENT_LOCATION),
75 "\n": _Transition(_State.IDLE, _Action.SKIP_TO_CURRENT_LOCATION),
76 },
77}
80def to_json(text: str) -> str:
81 """Convert jasonc input to json by removing '//' comments. Line and
82 number and characters position are preserved to any later json parsing
83 errors meaningful to the user.
84 """
85 output = []
87 # -- Indicates the input position that is already covered by the content
88 # -- in output. It can be larger than the size of the text in output since
89 # -- we drop comment text.
90 output_pos = 0
92 # -- The current state of the state machine.
93 state = _State.IDLE
94 state_transitions = STATES_TRANSITIONS[state]
96 # -- Iterate and process the input chars.
97 for input_pos, ch in enumerate(text):
99 if ch in state_transitions:
100 # -- Found a transition for this char.
101 transition: _Transition = state_transitions[ch]
103 elif ELSE in state_transitions:
104 # -- Use the default transition if any.
105 transition: _Transition = state_transitions[ELSE]
107 else:
108 # -- Otherwise, do nothing. All of our actions are in transitions.
109 continue
111 # -- We found a transition. Apply it.
113 # -- Set the state.
114 state = transition.new_state
115 state_transitions = STATES_TRANSITIONS[state]
117 # -- Perform action, if any.
118 if transition.action is _Action.SKIP_TO_CURRENT_LOCATION:
119 # -- Move output pos to input pos. This is how we skip over
120 # -- comments.
121 output_pos = input_pos
122 continue
124 if transition.action is _Action.COPY_UP_TO_COMMENT:
125 # -- We just entered a comment, copy any pending text from
126 # -- before the '//'.
127 end = input_pos - 1
128 output.append(text[output_pos:end])
129 output_pos = input_pos - 1
130 continue
132 # -- Here when the transition doesn't have an action. Do nothing.
133 assert transition.action is _Action.NO_ACTION
135 # -- Here we reached the end of the input. Copy pending non comment text
136 # -- if any.
137 if state != _State.IN_COMMENT: 137 ↛ 141line 137 didn't jump to line 141 because the condition on line 137 was always true
138 output.append(text[output_pos:])
140 # -- Concatenates the text pieces into a string.
141 return "".join(output)