collie 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +12 -0
  3. data/Gemfile +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +333 -0
  6. data/Rakefile +9 -0
  7. data/collie.gemspec +37 -0
  8. data/docs/TUTORIAL.md +588 -0
  9. data/docs/index.html +56 -0
  10. data/docs/playground/README.md +134 -0
  11. data/docs/playground/build-collie-bundle.rb +85 -0
  12. data/docs/playground/css/styles.css +402 -0
  13. data/docs/playground/index.html +146 -0
  14. data/docs/playground/js/app.js +231 -0
  15. data/docs/playground/js/collie-bridge.js +186 -0
  16. data/docs/playground/js/editor.js +129 -0
  17. data/docs/playground/js/examples.js +80 -0
  18. data/docs/playground/js/ruby-runner.js +75 -0
  19. data/docs/playground/test-server.sh +18 -0
  20. data/exe/collie +15 -0
  21. data/lib/collie/analyzer/conflict.rb +114 -0
  22. data/lib/collie/analyzer/reachability.rb +83 -0
  23. data/lib/collie/analyzer/recursion.rb +96 -0
  24. data/lib/collie/analyzer/symbol_table.rb +67 -0
  25. data/lib/collie/ast.rb +183 -0
  26. data/lib/collie/cli.rb +249 -0
  27. data/lib/collie/config.rb +91 -0
  28. data/lib/collie/formatter/formatter.rb +196 -0
  29. data/lib/collie/formatter/options.rb +23 -0
  30. data/lib/collie/linter/base.rb +62 -0
  31. data/lib/collie/linter/registry.rb +34 -0
  32. data/lib/collie/linter/rules/ambiguous_precedence.rb +87 -0
  33. data/lib/collie/linter/rules/circular_reference.rb +89 -0
  34. data/lib/collie/linter/rules/consistent_tag_naming.rb +69 -0
  35. data/lib/collie/linter/rules/duplicate_token.rb +38 -0
  36. data/lib/collie/linter/rules/empty_action.rb +52 -0
  37. data/lib/collie/linter/rules/factorizable_rules.rb +67 -0
  38. data/lib/collie/linter/rules/left_recursion.rb +34 -0
  39. data/lib/collie/linter/rules/long_rule.rb +37 -0
  40. data/lib/collie/linter/rules/missing_start_symbol.rb +38 -0
  41. data/lib/collie/linter/rules/nonterminal_naming.rb +34 -0
  42. data/lib/collie/linter/rules/prec_improvement.rb +54 -0
  43. data/lib/collie/linter/rules/redundant_epsilon.rb +44 -0
  44. data/lib/collie/linter/rules/right_recursion.rb +35 -0
  45. data/lib/collie/linter/rules/token_naming.rb +39 -0
  46. data/lib/collie/linter/rules/trailing_whitespace.rb +46 -0
  47. data/lib/collie/linter/rules/undefined_symbol.rb +55 -0
  48. data/lib/collie/linter/rules/unreachable_rule.rb +49 -0
  49. data/lib/collie/linter/rules/unused_nonterminal.rb +93 -0
  50. data/lib/collie/linter/rules/unused_token.rb +82 -0
  51. data/lib/collie/parser/lexer.rb +349 -0
  52. data/lib/collie/parser/parser.rb +416 -0
  53. data/lib/collie/reporter/github.rb +35 -0
  54. data/lib/collie/reporter/json.rb +52 -0
  55. data/lib/collie/reporter/text.rb +97 -0
  56. data/lib/collie/version.rb +5 -0
  57. data/lib/collie.rb +52 -0
  58. metadata +145 -0
@@ -0,0 +1,146 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Collie Playground - Interactive Grammar Linter</title>
7
+ <link rel="stylesheet" href="css/styles.css">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.44.0/min/vs/editor/editor.main.css">
9
+ </head>
10
+ <body>
11
+ <div id="app">
12
+ <!-- Header -->
13
+ <header class="header">
14
+ <div class="container">
15
+ <h1>Collie Playground</h1>
16
+ <p>Interactive linter and formatter for Lrama Style BNF grammar files</p>
17
+ <div class="header-actions">
18
+ <a href="https://github.com/ydah/collie" target="_blank" class="btn btn-secondary">
19
+ GitHub
20
+ </a>
21
+ <a href="../TUTORIAL.html" class="btn btn-secondary">
22
+ Tutorial
23
+ </a>
24
+ </div>
25
+ </div>
26
+ </header>
27
+
28
+ <!-- Loading Screen -->
29
+ <div id="loading" class="loading">
30
+ <div class="loading-content">
31
+ <div class="spinner"></div>
32
+ <p>Loading Ruby.wasm and Collie...</p>
33
+ <p class="loading-detail">First load may take a moment</p>
34
+ </div>
35
+ </div>
36
+
37
+ <!-- Main Content -->
38
+ <div id="main-content" class="main-content" style="display: none;">
39
+ <div class="container">
40
+ <!-- Toolbar -->
41
+ <div class="toolbar">
42
+ <div class="toolbar-left">
43
+ <select id="example-select" class="select">
44
+ <option value="">Load Example...</option>
45
+ <option value="simple">Simple Calculator</option>
46
+ <option value="lrama">Lrama Features</option>
47
+ <option value="invalid">Invalid Grammar (Demo)</option>
48
+ </select>
49
+ </div>
50
+ <div class="toolbar-right">
51
+ <button id="lint-btn" class="btn btn-primary">
52
+ Lint
53
+ </button>
54
+ <button id="format-btn" class="btn btn-primary">
55
+ Format
56
+ </button>
57
+ <button id="fix-btn" class="btn btn-success">
58
+ Fix All
59
+ </button>
60
+ <button id="clear-btn" class="btn btn-secondary">
61
+ Clear
62
+ </button>
63
+ </div>
64
+ </div>
65
+
66
+ <!-- Editor and Output -->
67
+ <div class="workspace">
68
+ <!-- Editor Panel -->
69
+ <div class="panel panel-editor">
70
+ <div class="panel-header">
71
+ <h2>Grammar File</h2>
72
+ <span class="panel-info">calc.y</span>
73
+ </div>
74
+ <div id="editor" class="editor-container"></div>
75
+ </div>
76
+
77
+ <!-- Output Panel -->
78
+ <div class="panel panel-output">
79
+ <div class="panel-header">
80
+ <h2>Output</h2>
81
+ <div class="output-tabs">
82
+ <button class="tab-btn active" data-tab="diagnostics">
83
+ Diagnostics
84
+ </button>
85
+ <button class="tab-btn" data-tab="formatted">
86
+ Formatted
87
+ </button>
88
+ <button class="tab-btn" data-tab="rules">
89
+ Rules
90
+ </button>
91
+ </div>
92
+ </div>
93
+
94
+ <!-- Diagnostics Tab -->
95
+ <div id="tab-diagnostics" class="tab-content active">
96
+ <div id="diagnostics-output" class="output-content">
97
+ <div class="placeholder">
98
+ Click "Lint" to check for issues
99
+ </div>
100
+ </div>
101
+ </div>
102
+
103
+ <!-- Formatted Tab -->
104
+ <div id="tab-formatted" class="tab-content">
105
+ <pre id="formatted-output" class="output-content"><div class="placeholder">Click "Format" to see formatted output</div></pre>
106
+ </div>
107
+
108
+ <!-- Rules Tab -->
109
+ <div id="tab-rules" class="tab-content">
110
+ <div id="rules-output" class="output-content">
111
+ <div class="placeholder">
112
+ Loading rules...
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- Footer Info -->
120
+ <div class="info-panel">
121
+ <h3>About Collie</h3>
122
+ <p>
123
+ Collie is a linter and formatter for Lrama Style BNF grammar files.
124
+ It provides 18+ built-in lint rules to catch errors, enforce conventions,
125
+ and suggest optimizations.
126
+ </p>
127
+ <p>
128
+ This playground runs completely in your browser using
129
+ <a href="https://github.com/ruby/ruby.wasm" target="_blank">Ruby.wasm</a>.
130
+ Your code never leaves your browser.
131
+ </p>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </div>
136
+
137
+ <!-- Scripts -->
138
+ <script src="https://cdn.jsdelivr.net/npm/@ruby/3.3-wasm-wasi@2.6.2/dist/browser.script.iife.js"></script>
139
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.44.0/min/vs/loader.min.js"></script>
140
+ <script src="js/examples.js?v=3"></script>
141
+ <script src="js/ruby-runner.js?v=3"></script>
142
+ <script src="js/collie-bridge.js?v=3"></script>
143
+ <script src="js/editor.js?v=3"></script>
144
+ <script src="js/app.js?v=3"></script>
145
+ </body>
146
+ </html>
@@ -0,0 +1,231 @@
1
+ // Main playground application
2
+
3
+ class PlaygroundApp {
4
+ constructor() {
5
+ this.rubyRunner = new RubyRunner();
6
+ this.collieBridge = null;
7
+ this.editorManager = new EditorManager();
8
+ this.currentTab = 'diagnostics';
9
+ }
10
+
11
+ async initialize() {
12
+ try {
13
+ document.getElementById('loading').style.display = 'flex';
14
+
15
+ await this.rubyRunner.initialize();
16
+ this.collieBridge = new CollieBridge(this.rubyRunner);
17
+ await this.editorManager.initialize('editor');
18
+ await this.loadRulesList();
19
+ this.setupEventListeners();
20
+
21
+ document.getElementById('loading').style.display = 'none';
22
+ document.getElementById('main-content').style.display = 'block';
23
+ } catch (error) {
24
+ console.error('Initialization error:', error);
25
+ this.showInitError('Failed to initialize playground: ' + error.message);
26
+ }
27
+ }
28
+
29
+ showInitError(message) {
30
+ const loadingContent = document.querySelector('.loading-content');
31
+ loadingContent.innerHTML = `
32
+ <div style="color: #f5222d; max-width: 500px;">
33
+ <h2>Initialization Failed</h2>
34
+ <p>${this.escapeHtml(message)}</p>
35
+ <p style="font-size: 0.9rem; margin-top: 1rem;">
36
+ Please check the browser console for more details.
37
+ </p>
38
+ <button onclick="location.reload()" style="margin-top: 1rem; padding: 0.5rem 1rem; cursor: pointer;">
39
+ Retry
40
+ </button>
41
+ </div>
42
+ `;
43
+ }
44
+
45
+ setupEventListeners() {
46
+ // Example selector
47
+ document.getElementById('example-select').addEventListener('change', (e) => {
48
+ const exampleKey = e.target.value;
49
+ if (exampleKey && EXAMPLES[exampleKey]) {
50
+ this.editorManager.setValue(EXAMPLES[exampleKey].code);
51
+ }
52
+ });
53
+
54
+ // Lint button
55
+ document.getElementById('lint-btn').addEventListener('click', () => {
56
+ this.handleLint();
57
+ });
58
+
59
+ // Format button
60
+ document.getElementById('format-btn').addEventListener('click', () => {
61
+ this.handleFormat();
62
+ });
63
+
64
+ // Fix button
65
+ document.getElementById('fix-btn').addEventListener('click', () => {
66
+ this.handleFixAll();
67
+ });
68
+
69
+ // Clear button
70
+ document.getElementById('clear-btn').addEventListener('click', () => {
71
+ this.editorManager.setValue('');
72
+ this.editorManager.clearMarkers();
73
+ this.clearOutput();
74
+ });
75
+
76
+ // Tab switching
77
+ document.querySelectorAll('.tab-btn').forEach(btn => {
78
+ btn.addEventListener('click', (e) => {
79
+ this.switchTab(e.target.dataset.tab);
80
+ });
81
+ });
82
+ }
83
+
84
+ async handleLint() {
85
+ const source = this.editorManager.getValue();
86
+ if (!source.trim()) {
87
+ this.showMessage('diagnostics-output', 'Please enter some code first');
88
+ return;
89
+ }
90
+
91
+ this.showLoading('diagnostics-output');
92
+
93
+ try {
94
+ const diagnostics = await this.collieBridge.lint(source);
95
+ this.displayDiagnostics(diagnostics);
96
+ this.editorManager.setMarkers(diagnostics);
97
+ this.switchTab('diagnostics');
98
+ } catch (error) {
99
+ this.showError('Linting failed: ' + error.message);
100
+ }
101
+ }
102
+
103
+ async handleFormat() {
104
+ const source = this.editorManager.getValue();
105
+ if (!source.trim()) {
106
+ this.showMessage('formatted-output', 'Please enter some code first');
107
+ return;
108
+ }
109
+
110
+ this.showLoading('formatted-output');
111
+
112
+ try {
113
+ const formatted = await this.collieBridge.format(source);
114
+ document.getElementById('formatted-output').textContent = formatted;
115
+ this.switchTab('formatted');
116
+ } catch (error) {
117
+ this.showError('Formatting failed: ' + error.message);
118
+ }
119
+ }
120
+
121
+ async handleFixAll() {
122
+ const source = this.editorManager.getValue();
123
+ if (!source.trim()) {
124
+ this.showMessage('diagnostics-output', 'Please enter some code first');
125
+ return;
126
+ }
127
+
128
+ try {
129
+ const corrected = await this.collieBridge.autocorrect(source);
130
+ this.editorManager.setValue(corrected);
131
+ await this.handleLint();
132
+ } catch (error) {
133
+ this.showError('Auto-correction failed: ' + error.message);
134
+ }
135
+ }
136
+
137
+ displayDiagnostics(diagnostics) {
138
+ const container = document.getElementById('diagnostics-output');
139
+
140
+ if (diagnostics.length === 0) {
141
+ container.innerHTML = '<div class="no-offenses">✓ No offenses detected</div>';
142
+ return;
143
+ }
144
+
145
+ const html = diagnostics.map(diag => `
146
+ <div class="diagnostic-item ${diag.severity}">
147
+ <div class="diagnostic-location">
148
+ ${diag.location.file}:${diag.location.line}:${diag.location.column}
149
+ </div>
150
+ <div class="diagnostic-message">
151
+ ${this.escapeHtml(diag.message)}
152
+ </div>
153
+ <span class="diagnostic-rule">${diag.rule_name}</span>
154
+ ${diag.autocorrectable ? ' <span class="diagnostic-rule">autocorrectable</span>' : ''}
155
+ </div>
156
+ `).join('');
157
+
158
+ container.innerHTML = html;
159
+ }
160
+
161
+ async loadRulesList() {
162
+ try {
163
+ const rules = await this.collieBridge.getRules();
164
+ this.displayRules(rules);
165
+ } catch (error) {
166
+ console.error('Failed to load rules:', error);
167
+ }
168
+ }
169
+
170
+ displayRules(rules) {
171
+ const container = document.getElementById('rules-output');
172
+
173
+ const html = rules.map(rule => `
174
+ <div class="rule-item">
175
+ <div class="rule-header">
176
+ <span class="rule-name">${rule.name}</span>
177
+ <span class="rule-severity ${rule.severity}">${rule.severity}</span>
178
+ </div>
179
+ <div class="rule-description">
180
+ ${this.escapeHtml(rule.description)}
181
+ ${rule.autocorrectable ? ' (autocorrectable)' : ''}
182
+ </div>
183
+ </div>
184
+ `).join('');
185
+
186
+ container.innerHTML = html;
187
+ }
188
+
189
+ switchTab(tabName) {
190
+ // Update buttons
191
+ document.querySelectorAll('.tab-btn').forEach(btn => {
192
+ btn.classList.toggle('active', btn.dataset.tab === tabName);
193
+ });
194
+
195
+ // Update content
196
+ document.querySelectorAll('.tab-content').forEach(content => {
197
+ content.classList.toggle('active', content.id === `tab-${tabName}`);
198
+ });
199
+
200
+ this.currentTab = tabName;
201
+ }
202
+
203
+ showLoading(containerId) {
204
+ document.getElementById(containerId).innerHTML = '<div class="placeholder">Processing...</div>';
205
+ }
206
+
207
+ showMessage(containerId, message) {
208
+ document.getElementById(containerId).innerHTML = `<div class="placeholder">${message}</div>`;
209
+ }
210
+
211
+ showError(message) {
212
+ alert(message);
213
+ }
214
+
215
+ clearOutput() {
216
+ document.getElementById('diagnostics-output').innerHTML = '<div class="placeholder">Click "Lint" to check for issues</div>';
217
+ document.getElementById('formatted-output').textContent = '';
218
+ }
219
+
220
+ escapeHtml(text) {
221
+ const div = document.createElement('div');
222
+ div.textContent = text;
223
+ return div.innerHTML;
224
+ }
225
+ }
226
+
227
+ // Initialize app when page loads
228
+ window.addEventListener('DOMContentLoaded', async () => {
229
+ const app = new PlaygroundApp();
230
+ await app.initialize();
231
+ });
@@ -0,0 +1,186 @@
1
+ // Bridge between JavaScript and Ruby Collie code
2
+
3
+ class CollieBridge {
4
+ constructor(rubyRunner) {
5
+ this.ruby = rubyRunner;
6
+ }
7
+
8
+ async lint(source) {
9
+ const escapedSource = source.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
10
+
11
+ const rubyCode = `
12
+ require 'json'
13
+
14
+ source = '${escapedSource}'
15
+
16
+ begin
17
+ lexer = Collie::Parser::Lexer.new(source, filename: "playground.y")
18
+ tokens = lexer.tokenize
19
+ parser = Collie::Parser::Parser.new(tokens)
20
+ ast = parser.parse
21
+
22
+ symbol_table = Collie::Analyzer::SymbolTable.new
23
+ ast.declarations.each do |decl|
24
+ case decl
25
+ when Collie::AST::TokenDeclaration
26
+ decl.names.each do |name|
27
+ symbol_table.add_token(name, type_tag: decl.type_tag, location: decl.location)
28
+ rescue Collie::Error
29
+ # Ignore duplicate declarations
30
+ end
31
+ when Collie::AST::ParameterizedRule
32
+ symbol_table.add_nonterminal(decl.name, location: decl.location)
33
+ end
34
+ end
35
+
36
+ ast.rules.each do |rule|
37
+ symbol_table.add_nonterminal(rule.name, location: rule.location)
38
+ end
39
+
40
+ config = Collie::Config.new
41
+ Collie::Linter::Registry.load_rules
42
+ enabled_rules = Collie::Linter::Registry.enabled_rules(config)
43
+
44
+ context = { symbol_table: symbol_table, source: source, file: "playground.y" }
45
+
46
+ offenses = []
47
+ enabled_rules.each do |rule_class|
48
+ rule = rule_class.new(config.rule_config(rule_class.rule_name))
49
+ offenses.concat(rule.check(ast, context))
50
+ end
51
+
52
+ result = offenses.map do |offense|
53
+ location = offense.location || Collie::AST::Location.new(file: "playground.y", line: 1, column: 1)
54
+ {
55
+ severity: offense.severity.to_s,
56
+ rule_name: offense.rule.rule_name,
57
+ message: offense.message,
58
+ location: {
59
+ file: location.file,
60
+ line: location.line,
61
+ column: location.column
62
+ },
63
+ autocorrectable: offense.autocorrectable?
64
+ }
65
+ end
66
+
67
+ JSON.generate(result)
68
+ rescue => e
69
+ JSON.generate([{
70
+ severity: "error",
71
+ rule_name: "ParseError",
72
+ message: e.message,
73
+ location: { file: "playground.y", line: 1, column: 1 },
74
+ autocorrectable: false
75
+ }])
76
+ end
77
+ `;
78
+
79
+ const result = await this.ruby.eval(rubyCode);
80
+ return JSON.parse(result.toString());
81
+ }
82
+
83
+ async format(source) {
84
+ const escapedSource = source.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
85
+
86
+ const rubyCode = `
87
+ source = '${escapedSource}'
88
+
89
+ begin
90
+ lexer = Collie::Parser::Lexer.new(source, filename: "playground.y")
91
+ tokens = lexer.tokenize
92
+ parser = Collie::Parser::Parser.new(tokens)
93
+ ast = parser.parse
94
+
95
+ config = Collie::Config.new
96
+ formatter = Collie::Formatter::Formatter.new(
97
+ Collie::Formatter::Options.new(config.formatter_options)
98
+ )
99
+
100
+ formatter.format(ast)
101
+ rescue => e
102
+ "Error: #{e.message}"
103
+ end
104
+ `;
105
+
106
+ const result = await this.ruby.eval(rubyCode);
107
+ return result.toString();
108
+ }
109
+
110
+ async autocorrect(source) {
111
+ const escapedSource = source.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
112
+
113
+ const rubyCode = `
114
+ source = '${escapedSource}'
115
+
116
+ begin
117
+ lexer = Collie::Parser::Lexer.new(source, filename: "playground.y")
118
+ tokens = lexer.tokenize
119
+ parser = Collie::Parser::Parser.new(tokens)
120
+ ast = parser.parse
121
+
122
+ symbol_table = Collie::Analyzer::SymbolTable.new
123
+ ast.declarations.each do |decl|
124
+ case decl
125
+ when Collie::AST::TokenDeclaration
126
+ decl.names.each do |name|
127
+ symbol_table.add_token(name, type_tag: decl.type_tag, location: decl.location)
128
+ rescue Collie::Error
129
+ # Ignore
130
+ end
131
+ when Collie::AST::ParameterizedRule
132
+ symbol_table.add_nonterminal(decl.name, location: decl.location)
133
+ end
134
+ end
135
+
136
+ ast.rules.each do |rule|
137
+ symbol_table.add_nonterminal(rule.name, location: rule.location)
138
+ end
139
+
140
+ context = { symbol_table: symbol_table, source: source, file: "playground.y" }
141
+
142
+ config = Collie::Config.new
143
+ Collie::Linter::Registry.load_rules
144
+ enabled_rules = Collie::Linter::Registry.enabled_rules(config)
145
+
146
+ offenses = []
147
+ enabled_rules.each do |rule_class|
148
+ rule = rule_class.new(config.rule_config(rule_class.rule_name))
149
+ offenses.concat(rule.check(ast, context))
150
+ end
151
+
152
+ autocorrectable = offenses.select(&:autocorrectable?)
153
+ autocorrectable.each { |offense| offense.autocorrect&.call }
154
+
155
+ context[:source]
156
+ rescue => e
157
+ source
158
+ end
159
+ `;
160
+
161
+ const result = await this.ruby.eval(rubyCode);
162
+ return result.toString();
163
+ }
164
+
165
+ async getRules() {
166
+ const rubyCode = `
167
+ require 'json'
168
+
169
+ Collie::Linter::Registry.load_rules
170
+
171
+ rules = Collie::Linter::Registry.all.map do |rule_class|
172
+ {
173
+ name: rule_class.rule_name,
174
+ description: rule_class.description,
175
+ severity: rule_class.severity.to_s,
176
+ autocorrectable: rule_class.autocorrectable
177
+ }
178
+ end
179
+
180
+ JSON.generate(rules)
181
+ `;
182
+
183
+ const result = await this.ruby.eval(rubyCode);
184
+ return JSON.parse(result.toString());
185
+ }
186
+ }
@@ -0,0 +1,129 @@
1
+ // Monaco Editor integration
2
+
3
+ class EditorManager {
4
+ constructor() {
5
+ this.editor = null;
6
+ }
7
+
8
+ async initialize(containerId) {
9
+ return new Promise((resolve) => {
10
+ require.config({
11
+ paths: {
12
+ vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.44.0/min/vs'
13
+ }
14
+ });
15
+
16
+ require(['vs/editor/editor.main'], () => {
17
+ // Register Yacc language
18
+ monaco.languages.register({ id: 'yacc' });
19
+
20
+ // Define syntax highlighting
21
+ monaco.languages.setMonarchTokensProvider('yacc', {
22
+ keywords: [
23
+ 'token', 'type', 'left', 'right', 'nonassoc', 'prec', 'start',
24
+ 'union', 'rule', 'inline'
25
+ ],
26
+
27
+ tokenizer: {
28
+ root: [
29
+ // Comments
30
+ [/\/\*/, 'comment', '@comment'],
31
+ [/\/\/.*$/, 'comment'],
32
+
33
+ // Directives
34
+ [/%[a-z]+/, {
35
+ cases: {
36
+ '@keywords': 'keyword',
37
+ '@default': 'keyword'
38
+ }
39
+ }],
40
+
41
+ // Section separator
42
+ [/^%%/, 'keyword.control'],
43
+
44
+ // Tokens (uppercase)
45
+ [/\b[A-Z_][A-Z0-9_]*\b/, 'constant'],
46
+
47
+ // Nonterminals (lowercase)
48
+ [/\b[a-z_][a-z0-9_]*\b/, 'variable'],
49
+
50
+ // Strings
51
+ [/"[^"]*"/, 'string'],
52
+ [/'[^']*'/, 'string'],
53
+
54
+ // Actions
55
+ [/\{/, 'delimiter.curly', '@action'],
56
+ ],
57
+
58
+ comment: [
59
+ [/\*\//, 'comment', '@pop'],
60
+ [/./, 'comment']
61
+ ],
62
+
63
+ action: [
64
+ [/\}/, 'delimiter.curly', '@pop'],
65
+ [/./, 'embedded']
66
+ ]
67
+ }
68
+ });
69
+
70
+ // Create editor
71
+ this.editor = monaco.editor.create(document.getElementById(containerId), {
72
+ value: '// Type your grammar here or load an example\n',
73
+ language: 'yacc',
74
+ theme: 'vs',
75
+ automaticLayout: true,
76
+ minimap: { enabled: false },
77
+ fontSize: 14,
78
+ lineNumbers: 'on',
79
+ scrollBeyondLastLine: false,
80
+ wordWrap: 'on',
81
+ tabSize: 4
82
+ });
83
+
84
+ resolve(this.editor);
85
+ });
86
+ });
87
+ }
88
+
89
+ getValue() {
90
+ return this.editor.getValue();
91
+ }
92
+
93
+ setValue(value) {
94
+ this.editor.setValue(value);
95
+ }
96
+
97
+ setMarkers(diagnostics) {
98
+ const model = this.editor.getModel();
99
+ const markers = diagnostics.map(diag => ({
100
+ severity: this.severityToMonaco(diag.severity),
101
+ startLineNumber: diag.location.line,
102
+ startColumn: diag.location.column,
103
+ endLineNumber: diag.location.line,
104
+ endColumn: diag.location.column + 10,
105
+ message: `[${diag.rule_name}] ${diag.message}`
106
+ }));
107
+
108
+ monaco.editor.setModelMarkers(model, 'collie', markers);
109
+ }
110
+
111
+ severityToMonaco(severity) {
112
+ switch (severity) {
113
+ case 'error':
114
+ return monaco.MarkerSeverity.Error;
115
+ case 'warning':
116
+ return monaco.MarkerSeverity.Warning;
117
+ case 'convention':
118
+ case 'info':
119
+ return monaco.MarkerSeverity.Info;
120
+ default:
121
+ return monaco.MarkerSeverity.Hint;
122
+ }
123
+ }
124
+
125
+ clearMarkers() {
126
+ const model = this.editor.getModel();
127
+ monaco.editor.setModelMarkers(model, 'collie', []);
128
+ }
129
+ }