nadesiko3 3.2.27

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 (283) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -0
  3. package/bin/cnako3 +10 -0
  4. package/bin/cnako3.bat +11 -0
  5. package/bin/nako3server.bat +6 -0
  6. package/demo/ace_editor.html +90 -0
  7. package/demo/ace_editor_tabs.html +162 -0
  8. package/demo/basic.html +71 -0
  9. package/demo/browsers.html +128 -0
  10. package/demo/css/basic.css +3 -0
  11. package/demo/css/common.css +157 -0
  12. package/demo/css/editor.css +8 -0
  13. package/demo/css/flow.css +3 -0
  14. package/demo/css/index.css +3 -0
  15. package/demo/extlib/ace@1.4.12/ace.js +17 -0
  16. package/demo/extlib/ace@1.4.12/ext-code_lens.min.js +1 -0
  17. package/demo/extlib/ace@1.4.12/ext-language_tools.min.js +1 -0
  18. package/demo/extlib/ace@1.4.12/ext-options.min.js +1 -0
  19. package/demo/extlib/ace@1.4.12/ext-settings_menu.js +8 -0
  20. package/demo/extlib/ace@1.4.12/keybinding-vscode.js +8 -0
  21. package/demo/extlib/ace@1.4.12/theme-monokai.js +8 -0
  22. package/demo/extlib/ace@1.4.12/theme-xcode.js +8 -0
  23. package/demo/extlib/chart.js@3.2.1/chart.min.js +13 -0
  24. package/demo/extlib/pure-min.css +11 -0
  25. package/demo/flow.html +97 -0
  26. package/demo/graph.html +53 -0
  27. package/demo/image/nako.png +0 -0
  28. package/demo/image/nakopad-icon256.png +0 -0
  29. package/demo/image/turtle.fla +0 -0
  30. package/demo/image/turtle.png +0 -0
  31. package/demo/index.html +134 -0
  32. package/demo/js/common.js +17 -0
  33. package/demo/js/turtle3d_test.js +44 -0
  34. package/demo/js/turtle_test.js +44 -0
  35. package/demo/runscript.html +47 -0
  36. package/demo/runscript2.html +33 -0
  37. package/demo/turtle.html +58 -0
  38. package/demo/turtle2.html +141 -0
  39. package/demo/turtle3.html +278 -0
  40. package/demo/turtle3d.html +58 -0
  41. package/demo/turtle3d2.html +107 -0
  42. package/demo/version.html +24 -0
  43. package/doc/SETUP.md +172 -0
  44. package/doc/about.md +34 -0
  45. package/doc/browsers.md +60 -0
  46. package/doc/docgen.md +21 -0
  47. package/doc/editor.md +44 -0
  48. package/doc/files.md +37 -0
  49. package/doc/plugins.md +195 -0
  50. package/doc/release.md +78 -0
  51. package/doc/win32.md +57 -0
  52. package/package.json +196 -0
  53. package/release/_hash.txt +65 -0
  54. package/release/_script-tags.txt +13 -0
  55. package/release/command.json +1 -0
  56. package/release/command.json.js +1 -0
  57. package/release/command_cnako3.json +1 -0
  58. package/release/command_list.json +1 -0
  59. package/release/editor.js +2 -0
  60. package/release/editor.js.LICENSE.txt +32 -0
  61. package/release/josi.json +48 -0
  62. package/release/nako_gen_async.js +1 -0
  63. package/release/nako_gen_async.js.LICENSE.txt +595 -0
  64. package/release/plugin_caniuse.js +1 -0
  65. package/release/plugin_caniuse.js.LICENSE.txt +411 -0
  66. package/release/plugin_csv.js +1 -0
  67. package/release/plugin_csv.js.LICENSE.txt +367 -0
  68. package/release/plugin_datetime.js +1 -0
  69. package/release/plugin_datetime.js.LICENSE.txt +471 -0
  70. package/release/plugin_kansuji.js +1 -0
  71. package/release/plugin_kansuji.js.LICENSE.txt +491 -0
  72. package/release/plugin_markup.js +1 -0
  73. package/release/plugin_markup.js.LICENSE.txt +363 -0
  74. package/release/plugin_turtle.js +1 -0
  75. package/release/plugin_turtle.js.LICENSE.txt +435 -0
  76. package/release/plugin_webworker.js +1 -0
  77. package/release/plugin_webworker.js.LICENSE.txt +491 -0
  78. package/release/version.js +2 -0
  79. package/release/version.js.LICENSE.txt +32 -0
  80. package/release/wnako3.js +2 -0
  81. package/release/wnako3.js.LICENSE.txt +1 -0
  82. package/release/wnako3webworker.js +1 -0
  83. package/release/wnako3webworker.js.LICENSE.txt +847 -0
  84. package/release/yoyakugo.json +30 -0
  85. package/src/browsers.md +60 -0
  86. package/src/cnako3.js +466 -0
  87. package/src/commander_ja.js +154 -0
  88. package/src/enako3.js +69 -0
  89. package/src/era.json +22 -0
  90. package/src/index.js +5 -0
  91. package/src/nako3.js +836 -0
  92. package/src/nako3_assert.js +37 -0
  93. package/src/nako3editorfix.sfd +106 -0
  94. package/src/nako3editorfix.woff +0 -0
  95. package/src/nako3server.js +51 -0
  96. package/src/nako_colors.js +86 -0
  97. package/src/nako_errors.js +176 -0
  98. package/src/nako_gen.js +1459 -0
  99. package/src/nako_gen_async.js +1622 -0
  100. package/src/nako_global.js +113 -0
  101. package/src/nako_indent.js +480 -0
  102. package/src/nako_josi_list.js +46 -0
  103. package/src/nako_lex_rules.js +259 -0
  104. package/src/nako_lexer.js +576 -0
  105. package/src/nako_logger.js +138 -0
  106. package/src/nako_parser3.js +1768 -0
  107. package/src/nako_parser_base.js +265 -0
  108. package/src/nako_parser_const.js +37 -0
  109. package/src/nako_prepare.js +293 -0
  110. package/src/nako_reserved_words.js +35 -0
  111. package/src/nako_source_mapping.js +251 -0
  112. package/src/nako_test.js +37 -0
  113. package/src/nako_version.js +8 -0
  114. package/src/plugin_browser.js +191 -0
  115. package/src/plugin_browser_ajax.js +352 -0
  116. package/src/plugin_browser_audio.js +109 -0
  117. package/src/plugin_browser_canvas.js +462 -0
  118. package/src/plugin_browser_chart.js +296 -0
  119. package/src/plugin_browser_color.js +49 -0
  120. package/src/plugin_browser_crypto.js +26 -0
  121. package/src/plugin_browser_dialog.js +53 -0
  122. package/src/plugin_browser_dom_basic.js +322 -0
  123. package/src/plugin_browser_dom_event.js +193 -0
  124. package/src/plugin_browser_dom_parts.js +163 -0
  125. package/src/plugin_browser_geolocation.js +51 -0
  126. package/src/plugin_browser_hotkey.js +25 -0
  127. package/src/plugin_browser_html.js +59 -0
  128. package/src/plugin_browser_in_worker.js +44 -0
  129. package/src/plugin_browser_location.js +21 -0
  130. package/src/plugin_browser_speech.js +111 -0
  131. package/src/plugin_browser_storage.js +121 -0
  132. package/src/plugin_browser_system.js +12 -0
  133. package/src/plugin_browser_websocket.js +73 -0
  134. package/src/plugin_caniuse.js +24 -0
  135. package/src/plugin_csv.js +57 -0
  136. package/src/plugin_datetime.js +414 -0
  137. package/src/plugin_express.js +212 -0
  138. package/src/plugin_kansuji.js +224 -0
  139. package/src/plugin_keigo.js +55 -0
  140. package/src/plugin_markup.js +32 -0
  141. package/src/plugin_math.js +319 -0
  142. package/src/plugin_node.js +1018 -0
  143. package/src/plugin_promise.js +94 -0
  144. package/src/plugin_system.js +2109 -0
  145. package/src/plugin_test.js +38 -0
  146. package/src/plugin_turtle.js +646 -0
  147. package/src/plugin_webworker.js +334 -0
  148. package/src/plugin_weykturtle3d.js +1216 -0
  149. package/src/plugin_worker.js +92 -0
  150. package/src/repl.nako3 +63 -0
  151. package/src/turtle-elephant.png +0 -0
  152. package/src/turtle-panda.png +0 -0
  153. package/src/turtle64.png +0 -0
  154. package/src/wnako3.js +162 -0
  155. package/src/wnako3_editor.css +215 -0
  156. package/src/wnako3_editor.js +1645 -0
  157. package/src/wnako3_editor_marker_red.png +0 -0
  158. package/src/wnako3_editor_marker_red.xcf +0 -0
  159. package/src/wnako3_editor_marker_yellow.png +0 -0
  160. package/src/wnako3_editor_marker_yellow.xcf +0 -0
  161. package/src/wnako3webworker.js +70 -0
  162. package/test/ace_editor/karma.config.js +94 -0
  163. package/test/ace_editor/test/.babelrc.json +3 -0
  164. package/test/ace_editor/test/ace_editor_test.js +178 -0
  165. package/test/ace_editor/test/html/custom_context.html +140 -0
  166. package/test/async/async_basic_test.js +124 -0
  167. package/test/browser/karma.config.js +212 -0
  168. package/test/browser/test/.babelrc.json +3 -0
  169. package/test/browser/test/compare_util.js +50 -0
  170. package/test/browser/test/html/canvas_basic.html +1 -0
  171. package/test/browser/test/html/div_basic.html +2 -0
  172. package/test/browser/test/html/event_dom_form.html +4 -0
  173. package/test/browser/test/html/event_dom_scrolldiv.html +5 -0
  174. package/test/browser/test/image/canvas_test1.png +0 -0
  175. package/test/browser/test/image/canvas_test2.png +0 -0
  176. package/test/browser/test/image/canvas_test3.png +0 -0
  177. package/test/browser/test/image/canvas_test4.png +0 -0
  178. package/test/browser/test/image/canvas_test7.png +0 -0
  179. package/test/browser/test/image/canvas_test8.png +0 -0
  180. package/test/browser/test/image/canvas_test_blank.png +0 -0
  181. package/test/browser/test/image/elephant_kana.png +0 -0
  182. package/test/browser/test/image/panda_kana.png +0 -0
  183. package/test/browser/test/image/turtle_kana.png +0 -0
  184. package/test/browser/test/import_plugin_checker.js +24 -0
  185. package/test/browser/test/plugin_browser_test.js +52 -0
  186. package/test/browser/test/plugin_browser_test_ajax.js +123 -0
  187. package/test/browser/test/plugin_browser_test_color.js +18 -0
  188. package/test/browser/test/plugin_browser_test_dialog.js +72 -0
  189. package/test/browser/test/plugin_browser_test_dom_event.js +598 -0
  190. package/test/browser/test/plugin_browser_test_dom_parts.js +125 -0
  191. package/test/browser/test/plugin_browser_test_system.js +9 -0
  192. package/test/browser/test/plugin_turtle_test.js +817 -0
  193. package/test/browser/test/plugin_webworker_test.js +87 -0
  194. package/test/browser/test/require_test.js +71 -0
  195. package/test/bundled/karma.config.base.js +117 -0
  196. package/test/bundled/karma.config.js +86 -0
  197. package/test/bundled/test/.babelrc.json +3 -0
  198. package/test/bundled/test/bundled_test.js +69 -0
  199. package/test/bundled/test/html/custom_context.html +65 -0
  200. package/test/bundled/test/html/custom_debug.html +66 -0
  201. package/test/bundled/test4b.cmd +52 -0
  202. package/test/bundled/test_base/.babelrc.json +3 -0
  203. package/test/bundled/test_base/_checktool_test.js +25 -0
  204. package/test/bundled/test_base/basic_ajax_test.js +56 -0
  205. package/test/bundled/test_base/basic_async_test.js +18 -0
  206. package/test/bundled/test_base/basic_test.js +153 -0
  207. package/test/bundled/test_base/calc_test.js +132 -0
  208. package/test/bundled/test_base/css/browsers_box.css +114 -0
  209. package/test/bundled/test_base/html/custom_context.html +69 -0
  210. package/test/bundled/test_base/html/custom_debug.html +71 -0
  211. package/test/bundled/test_base/js/browsers_box.js +72 -0
  212. package/test/bundled/test_base/plugin_csv_test.js +37 -0
  213. package/test/bundled/test_base/plugin_datetime_test.js +115 -0
  214. package/test/bundled/test_base/plugin_kansuji_test.js +49 -0
  215. package/test/bundled/test_base/plugin_system_test.js +410 -0
  216. package/test/bundled/test_base/plugin_webworker_test.js +53 -0
  217. package/test/bundled/test_base/resources/ok.txt +1 -0
  218. package/test/bundled/test_base/test_utils.js +191 -0
  219. package/test/common/array_test.js +40 -0
  220. package/test/common/basic_test.js +317 -0
  221. package/test/common/calc_test.js +139 -0
  222. package/test/common/debug_test.js +16 -0
  223. package/test/common/error_test.js +16 -0
  224. package/test/common/flow_test.js +360 -0
  225. package/test/common/func_call.js +136 -0
  226. package/test/common/func_test.js +149 -0
  227. package/test/common/indent_test.js +362 -0
  228. package/test/common/lex_test.js +146 -0
  229. package/test/common/literal_test.js +72 -0
  230. package/test/common/nako_logger_test.js +26 -0
  231. package/test/common/plugin_browser_test.js +24 -0
  232. package/test/common/plugin_browser_ut_audio_test.js +88 -0
  233. package/test/common/plugin_browser_ut_color_test.js +21 -0
  234. package/test/common/plugin_browser_ut_dialog_test.js +100 -0
  235. package/test/common/plugin_browser_ut_html_test.js +13 -0
  236. package/test/common/plugin_browser_ut_system_test.js +10 -0
  237. package/test/common/plugin_csv_test.js +39 -0
  238. package/test/common/plugin_datetime_test.js +120 -0
  239. package/test/common/plugin_kansuji_test.js +59 -0
  240. package/test/common/plugin_promise_test.js +18 -0
  241. package/test/common/plugin_system_test.js +451 -0
  242. package/test/common/prepare_test.js +93 -0
  243. package/test/common/re_test.js +20 -0
  244. package/test/common/variable_scope_test.js +105 -0
  245. package/test/jsconfig.json +19 -0
  246. package/test/karma.config.js +91 -0
  247. package/test/node/add_test.nako3 +1 -0
  248. package/test/node/async_test.js +80 -0
  249. package/test/node/commander_ja_test.js +82 -0
  250. package/test/node/error_message_test.js +244 -0
  251. package/test/node/kai_test.nako3 +6 -0
  252. package/test/node/node_test.js +43 -0
  253. package/test/node/plugin_broken.js.txt +3 -0
  254. package/test/node/plugin_browser_ut_ajax_test.js +355 -0
  255. package/test/node/plugin_browser_ut_location_test.js +32 -0
  256. package/test/node/plugin_markup_test.js +44 -0
  257. package/test/node/plugin_math_test.js +42 -0
  258. package/test/node/plugin_node_test.js +93 -0
  259. package/test/node/plugin_test.js +16 -0
  260. package/test/node/relative_import_test_1.nako3 +1 -0
  261. package/test/node/relative_import_test_2.nako3 +2 -0
  262. package/test/node/require_nako3_test.js +59 -0
  263. package/test/node/requiretest.nako3 +4 -0
  264. package/test/node/requiretest_indirect.nako3 +1 -0
  265. package/test/node/requiretest_name.nako3 +5 -0
  266. package/test/node/runtime_error.nako3 +2 -0
  267. package/test/node/side_effects_test.js +106 -0
  268. package/test/node/sjis.txt +5 -0
  269. package/test/node/syntax_error.nako3 +2 -0
  270. package/test/node/wnako3_editor_test.js +360 -0
  271. package/tools/README.md +7 -0
  272. package/tools/nako3edit/html/edit.html +83 -0
  273. package/tools/nako3edit/html/edit_plugin.js +6 -0
  274. package/tools/nako3edit/html/files.html +49 -0
  275. package/tools/nako3edit/html/nako3edit.css +66 -0
  276. package/tools/nako3edit/index.nako3 +145 -0
  277. package/tools/nako3edit/run.js +12 -0
  278. package/tools/nako3server/html/edit.html +104 -0
  279. package/tools/nako3server/html/edit_plugin.js +6 -0
  280. package/tools/nako3server/html/files.html +53 -0
  281. package/tools/nako3server/html/nako3edit.css +66 -0
  282. package/tools/nako3server/index.nako3 +129 -0
  283. package/tools/nako3server/run.js +12 -0
@@ -0,0 +1,1645 @@
1
+ // @ts-nocheck
2
+ /* eslint-disable no-template-curly-in-string */
3
+ /** なでしこのtokenのtypeをscope(CSSのクラス名)に変換する。 */
4
+
5
+ const { OffsetToLineColumn } = require('./nako_source_mapping')
6
+ const { NakoError } = require('./nako_errors')
7
+ const { getBlockStructure, getIndent, countIndent, isIndentSyntaxEnabled } = require('./nako_indent')
8
+ const NakoPrepare = require('./nako_prepare')
9
+ const NakoLogger = require('./nako_logger')
10
+
11
+ /**
12
+ * @typedef {import('./nako3')} NakoCompiler
13
+ *
14
+ * @typedef {{
15
+ * getValue(): string
16
+ * setValue(text: string): void
17
+ * session: Session
18
+ * execCommand(command: string): void
19
+ * setReadOnly(value: boolean): void
20
+ * setOption(key: string, value: unknown): void
21
+ * getOption(key: string): unknown
22
+ * setOptions(entries: Record<string, unknown>): void
23
+ * setFontSize(px: number): void
24
+ * setKeyboardHandler(name: string): void
25
+ * setTheme(name: string): void
26
+ * container: HTMLElement
27
+ * wnako3EditorId?: number
28
+ * getCursorPosition(): { row: number, column: number }
29
+ * commands: { addCommand(data: { name: string, exec: (editor: AceEditor, args: any[]) => void }): void }
30
+ * }} AceEditor
31
+ *
32
+ * @typedef {import("./nako_lexer").TokenWithSourceMap} TokenWithSourceMap
33
+ *
34
+ * @typedef {{
35
+ * getLine(row: number): string
36
+ * getAllLines(): string[]
37
+ * getLength(): number
38
+ * insertInLine(position: { row: number, column: number }, text: string): void
39
+ * removeInLine(row: number, columnStart: number, columnEnd: number): void
40
+ * replace(range: AceRange, text: string): void
41
+ * }} AceDocument
42
+ *
43
+ * @typedef {{
44
+ * doc: AceDocument
45
+ * bgTokenizer: BackgroundTokenizer
46
+ * getScrollTop(): number
47
+ * setScrollTop(x: number): void
48
+ * getScrollLeft(): number
49
+ * setScrollLeft(x: number): void
50
+ * getUndoManager(): any
51
+ * setUndoManager(x: any): void
52
+ * selection: { getRange(): AceRange, isBackwards(): boolean, setRange(range: AceRange, reversed: boolean): void, clearSelection(): void }
53
+ * setMode(mode: string | object): void
54
+ * }} Session
55
+ *
56
+ * @typedef {{}} AceRange
57
+ *
58
+ * @typedef {new (startLine: number, startColumn: number, endLine: number, endColumn: number) => AceRange} TypeofAceRange
59
+ *
60
+ * @typedef {string} TokenType
61
+ * @typedef {{ type: TokenType, value: string, docHTML: string | null }} EditorToken
62
+ *
63
+ * @typedef {{ start: { row: number }, command: { id: string, title: string, arguments: string[] } }} CodeLens
64
+ */
65
+
66
+ /**
67
+ * シンタックスハイライトでは一般にテキストの各部分に 'comment.line' のようなラベルを付け、各エディタテーマがそのそれぞれの色を設定する。
68
+ * ace editor では例えば 'comment.line' が付いた部分はクラス .ace_comment.ace_line が付いたHTMLタグで囲まれ、各テーマはそれに対応するCSSを実装する。
69
+ * @param {TokenWithSourceMap} token
70
+ * @returns {TokenType}
71
+ */
72
+ function getScope (token) {
73
+ switch (token.type) {
74
+ case 'line_comment': return 'comment.line'
75
+ case 'range_comment': return 'comment.block'
76
+ case 'def_test': return 'keyword.control'
77
+ case 'def_func': return 'keyword.control'
78
+ case 'func': return 'entity.name.function'
79
+ case 'number': return 'constant.numeric'
80
+ // 独立した助詞
81
+ case 'とは':
82
+ case 'ならば':
83
+ case 'でなければ':
84
+ return 'keyword.control'
85
+ // 制御構文
86
+ case 'ここから':
87
+ case 'ここまで':
88
+ case 'もし':
89
+ case '違えば':
90
+ case 'require':
91
+ return 'keyword.control'
92
+ // 予約語
93
+ case '回':
94
+ case '間':
95
+ case '繰り返す':
96
+ case '反復':
97
+ case '抜ける':
98
+ case '続ける':
99
+ case '戻る':
100
+ case '先に':
101
+ case '次に':
102
+ case '代入':
103
+ case '逐次実行':
104
+ case '条件分岐':
105
+ case '取込':
106
+ case 'エラー監視':
107
+ case 'エラー':
108
+ case '変数':
109
+ case '実行速度優先':
110
+ return 'keyword.control'
111
+ case '定める':
112
+ case '定数':
113
+ return 'support.constant'
114
+ // 演算子
115
+ case 'shift_r0':
116
+ case 'shift_r':
117
+ case 'shift_l':
118
+ case 'gteq':
119
+ case 'lteq':
120
+ case 'noteq':
121
+ case 'eq':
122
+ case 'not':
123
+ case 'gt':
124
+ case 'lt':
125
+ case 'and':
126
+ case 'or':
127
+ case '@':
128
+ case '+':
129
+ case '-':
130
+ case '*':
131
+ case '/':
132
+ case '%':
133
+ case '^':
134
+ case '&':
135
+ return 'keyword.operator'
136
+ case 'string':
137
+ case 'string_ex':
138
+ return 'string.other'
139
+ case 'word':
140
+ if (['そう', 'それ', '回数', '対象キー', '対象'].includes(token.value)) {
141
+ return 'variable.language'
142
+ } else {
143
+ return 'variable.other'
144
+ }
145
+ default:
146
+ return 'markup.other'
147
+ }
148
+ }
149
+
150
+ /**
151
+ * @param {TokenWithSourceMap} compilerToken
152
+ * @param {NakoCompiler} nako3
153
+ * @param {string} value
154
+ * @param {boolean} includesLastCharacter
155
+ * @param {boolean} underlineJosi
156
+ */
157
+ function getEditorTokens (compilerToken, nako3, value, includesLastCharacter, underlineJosi) {
158
+ const type = getScope(compilerToken)
159
+ const docHTML = getDocumentationHTML(compilerToken, nako3)
160
+
161
+ // 助詞があれば助詞の部分を分割する。
162
+ // 最後の文字が現在の行に含まれないときは助詞を表示しない。そうしないと例えば `「文字列\n」を表示` の「列」の部分に下線が引かれてしまう。
163
+ if (compilerToken.rawJosi && value.length >= compilerToken.rawJosi.length && includesLastCharacter && underlineJosi) {
164
+ return [
165
+ { type, docHTML, value: value.slice(0, -compilerToken.rawJosi.length) },
166
+ { type: type + '.markup.underline', docHTML, value: value.slice(-compilerToken.josi.length) }
167
+ ]
168
+ }
169
+ return [
170
+ { type, docHTML, value }
171
+ ]
172
+ }
173
+
174
+ /**
175
+ * `name` が定義されたプラグインの名前を返す。
176
+ * @param {string} name
177
+ * @param {NakoCompiler} nako3
178
+ * @returns {string | null}
179
+ */
180
+ function findPluginName (name, nako3) {
181
+ for (const pluginName of Object.keys(nako3.__module)) {
182
+ if (Object.keys(nako3.__module[pluginName]).includes(name)) {
183
+ return pluginName
184
+ }
185
+ }
186
+ return null
187
+ }
188
+
189
+ /**
190
+ * i = 0, 1, 2, ... に対して 'A', 'B', 'C', ... 'Z', 'AA', 'AB', ... を返す。
191
+ * @param {number} i
192
+ * @returns {string}
193
+ */
194
+ function createParameterName (i) {
195
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
196
+ return i.toString(26).split('').map((v) => alphabet[parseInt(v, 26)]).join('')
197
+ }
198
+
199
+ /**
200
+ * パラメータの定義を表す文字列を生成する。例えば `[['と', 'の'], ['を']]` に対して `'(Aと|Aの、Bを)'` を返す、パラメータが無い場合、空文字列を返す。
201
+ * @param {string[][]} josi
202
+ * @retunrs {string}
203
+ */
204
+ function createParameterDeclaration (josi) {
205
+ const args = josi.map((union, i) => union.map((v) => `${createParameterName(i)}${v}`).join('|')).join('、')
206
+ if (args !== '') {
207
+ return `(${args})`
208
+ } else {
209
+ return ''
210
+ }
211
+ }
212
+
213
+ // https://stackoverflow.com/a/6234804
214
+ /** @param {string} t */
215
+ function escapeHTML (t) {
216
+ return t
217
+ .replace(/&/g, '&amp;')
218
+ .replace(/</g, '&lt;')
219
+ .replace(/>/g, '&gt;')
220
+ .replace(/"/g, '&quot;')
221
+ .replace(/'/g, '&#039;')
222
+ }
223
+
224
+ /**
225
+ * 関数のドキュメントを返す。
226
+ * @param {TokenWithSourceMap} token
227
+ * @param {NakoCompiler} nako3
228
+ * @returns {string | null}
229
+ */
230
+ function getDocumentationHTML (token, nako3) {
231
+ /** @param {string} text */
232
+ const meta = (text) => `<span class="tooltip-plugin-name">${escapeHTML(text)}</span>`
233
+ if (token.type === 'func') {
234
+ /** @type {string | null} */
235
+ const pluginName = findPluginName(token.value + '', nako3) || (token.meta && token.meta.file ? token.meta.file : null)
236
+ const josi = (token.meta && token.meta.josi) ? createParameterDeclaration(token.meta.josi) : '' // {関数} のとき token.meta.josi が存在しない
237
+ if (pluginName) {
238
+ return escapeHTML(josi + token.value) + meta(pluginName)
239
+ }
240
+ return escapeHTML(josi + token.value)
241
+ } else if (token.type === 'word') {
242
+ /** @type {string | null} */
243
+ const pluginName = findPluginName(token.value + '', nako3) || (token.meta && token.meta.file ? token.meta.file : null)
244
+ if (pluginName) {
245
+ return escapeHTML(token.value + '') + meta(pluginName)
246
+ }
247
+ }
248
+ return null
249
+ }
250
+
251
+ /**
252
+ * ace editor ではエディタの文字列の全ての部分に何らかの `type` を付けなければならない。
253
+ * なでしこのエディタでは 'markup.other' をデフォルト値として使うことにした。
254
+ * @param {number} row
255
+ * @param {AceDocument} doc
256
+ * @returns {EditorToken[]}
257
+ */
258
+ const getDefaultTokens = (row, doc) => [{ type: 'markup.other', value: doc.getLine(row), docHTML: null }]
259
+
260
+ /**
261
+ * 一時的にloggerを無効化する。そうしないとシンタックスハイライトの更新のたびにloggerへコンパイルエラーや警告が送られて、結果のボックスに行が追加されてしまう。
262
+ * @type {<T>(nako3: NakoCompiler, f: () => T) => T}
263
+ */
264
+ const withoutLogger = (nako3, f) => {
265
+ const logger = nako3.logger
266
+ try {
267
+ nako3.replaceLogger()
268
+ return f()
269
+ } finally {
270
+ nako3.logger = logger
271
+ }
272
+ }
273
+
274
+ /**
275
+ * プログラムをlexerでtokenizeした後、ace editor 用のトークン列に変換する。
276
+ * @param {string[]} lines
277
+ * @param {NakoCompiler} nako3
278
+ * @param {boolean} underlineJosi
279
+ */
280
+ function tokenize (lines, nako3, underlineJosi) {
281
+ const code = lines.join('\n')
282
+
283
+ // 取り込み文を含めてしまうと依存ファイルが大きい時に時間がかかってしまうため、
284
+ // 取り込み文を無視してトークン化してから、依存ファイルで定義された関数名と一致するトークンを関数のトークンへ変換する。
285
+ nako3.reset()
286
+ const lexerOutput = withoutLogger(nako3, () => nako3.lex(code, 'main.nako3', undefined, true))
287
+ lexerOutput.commentTokens = lexerOutput.commentTokens.filter((t) => t.file === 'main.nako3')
288
+ lexerOutput.requireTokens = lexerOutput.requireTokens.filter((t) => t.file === 'main.nako3')
289
+ lexerOutput.tokens = lexerOutput.tokens.filter((t) => t.file === 'main.nako3')
290
+
291
+ // 外部ファイルで定義された関数名に一致するトークンのtypeをfuncに変更する。
292
+ // 取り込んでいないファイルも参照される問題や、関数名の重複がある場合に正しくない情報を表示する問題がある。
293
+ // eslint-disable-next-line no-lone-blocks
294
+ {
295
+ /** @type {Record<string, object>} */
296
+ for (const [file, { funclist }] of Object.entries(nako3.dependencies)) {
297
+ for (const token of lexerOutput.tokens) {
298
+ if (token.type === 'word' && token.value !== 'それ' && funclist[token.value]) {
299
+ token.type = 'func'
300
+ // meta.file に定義元のファイル名を持たせる。
301
+ token.meta = { ...funclist[token.value + ''], file: file }
302
+ }
303
+ }
304
+ }
305
+ }
306
+
307
+ // eol、eof、長さが1未満のトークン、位置を特定できないトークンを消す
308
+ /** @type {(TokenWithSourceMap & { startOffset: number, endOffset: number })[]} */
309
+ // @ts-ignore
310
+ const tokens = [...lexerOutput.tokens, ...lexerOutput.commentTokens, ...lexerOutput.requireTokens].filter((t) =>
311
+ t.type !== 'eol' && t.type !== 'eof' &&
312
+ typeof t.startOffset === 'number' && typeof t.endOffset === 'number' &&
313
+ t.startOffset < t.endOffset)
314
+
315
+ // startOffsetでソートする
316
+ tokens.sort((a, b) => (a.startOffset || 0) - (b.startOffset || 0))
317
+
318
+ // 各行について、余る文字の無いようにエディタのトークンに変換する。
319
+ // 複数のトークンが重なることはないと仮定する。
320
+ let lineStartOffset = 0
321
+ let tokenIndex = 0
322
+ // 実際に必要なプロパティはtype, valueだけで、docは独自に追加した。
323
+ /** @type {EditorToken[][]} */
324
+ const editorTokens = [] // 各行のエディタのトークン
325
+ for (let i = 0; i < lines.length; i++) {
326
+ editorTokens.push([])
327
+ const lineEndOffset = lineStartOffset + lines[i].length
328
+ let offset = lineStartOffset
329
+
330
+ // 現在の行にかかっているトークンまで飛ばす
331
+ while (tokenIndex < tokens.length &&
332
+ tokens[tokenIndex].endOffset <= lineStartOffset) {
333
+ tokenIndex++
334
+ }
335
+
336
+ // 行全体を完全にまたがっているトークンが存在する場合
337
+ if (tokenIndex < tokens.length &&
338
+ tokens[tokenIndex].startOffset <= lineStartOffset &&
339
+ tokens[tokenIndex].endOffset >= lineEndOffset) {
340
+ editorTokens[i].push(...getEditorTokens(tokens[tokenIndex], nako3, lines[i], tokens[tokenIndex].endOffset <= lineEndOffset, underlineJosi))
341
+ } else {
342
+ // 行頭をまたがっているトークンが存在する場合
343
+ if (tokenIndex < tokens.length &&
344
+ tokens[tokenIndex].startOffset <= lineStartOffset) {
345
+ editorTokens[i].push(...getEditorTokens(tokens[tokenIndex], nako3, code.slice(offset, tokens[tokenIndex].endOffset), true, underlineJosi))
346
+ offset = tokens[tokenIndex].endOffset
347
+ tokenIndex++
348
+ }
349
+
350
+ // 行頭も行末もまたがっていないトークンを処理する
351
+ while (tokenIndex < tokens.length &&
352
+ tokens[tokenIndex].endOffset < lineEndOffset) {
353
+ // このトークンと直前のトークンの間に隙間があるなら、埋める
354
+ if (offset < tokens[tokenIndex].startOffset) {
355
+ editorTokens[i].push({
356
+ type: 'markup.other',
357
+ docHTML: null,
358
+ value: code.slice(offset, tokens[tokenIndex].startOffset)
359
+ })
360
+ offset = tokens[tokenIndex].startOffset
361
+ }
362
+
363
+ // 現在のトークンを使う
364
+ editorTokens[i].push(...getEditorTokens(tokens[tokenIndex], nako3, code.slice(offset, tokens[tokenIndex].endOffset), true, underlineJosi))
365
+ offset = tokens[tokenIndex].endOffset
366
+ tokenIndex++
367
+ }
368
+
369
+ // 行末をまたがっているトークンが存在する場合
370
+ if (tokenIndex < tokens.length &&
371
+ tokens[tokenIndex].startOffset < lineEndOffset) {
372
+ // トークンの前の隙間
373
+ if (offset < tokens[tokenIndex].startOffset) {
374
+ editorTokens[i].push({
375
+ type: 'markup.other',
376
+ docHTML: null,
377
+ value: code.slice(offset, tokens[tokenIndex].startOffset)
378
+ })
379
+ offset = tokens[tokenIndex].startOffset
380
+ }
381
+
382
+ // トークンを使う
383
+ editorTokens[i].push(...getEditorTokens(tokens[tokenIndex], nako3, code.slice(tokens[tokenIndex].startOffset, lineEndOffset), tokens[tokenIndex].endOffset <= lineEndOffset, underlineJosi))
384
+ } else {
385
+ editorTokens[i].push({
386
+ type: 'markup.other',
387
+ docHTML: null,
388
+ value: code.slice(offset, lineEndOffset)
389
+ })
390
+ }
391
+ }
392
+
393
+ lineStartOffset += lines[i].length + 1
394
+ }
395
+
396
+ return { editorTokens, lexerOutput }
397
+ }
398
+
399
+ /**
400
+ * エディタ上にエラーメッセージの波線とgutterの赤いマークとエラーメッセージのポップアップを設定するためのクラス。
401
+ */
402
+ class EditorMarkers {
403
+ /**
404
+ * @param {any} session
405
+ * @param {AceDocument} doc
406
+ * @param {TypeofAceRange} AceRange
407
+ * @param {boolean} disable
408
+ */
409
+ constructor (session, doc, AceRange, disable) {
410
+ this.session = session
411
+ this.doc = doc
412
+ this.AceRange = AceRange
413
+ /** @type {any[]} */
414
+ this.markers = []
415
+ this.hasAnnotations = false
416
+ this.disable = disable
417
+ }
418
+
419
+ /**
420
+ * @param {number} startLine
421
+ * @param {number | null} startColumn
422
+ * @param {number | null} endLine
423
+ * @param {number | null} endColumn
424
+ * @param {(row: number) => string} getLine
425
+ * @returns {[number, number, number, number]}
426
+ */
427
+ static fromNullable (startLine, startColumn, endLine, endColumn, getLine) {
428
+ if (startColumn === null) {
429
+ startColumn = 0
430
+ }
431
+ if (endLine === null) {
432
+ endLine = startLine
433
+ }
434
+ if (endColumn === null) {
435
+ endColumn = getLine(endLine).length
436
+ }
437
+
438
+ // 最低でも1文字分の長さをとる
439
+ if (startLine === endLine && startColumn === endColumn) {
440
+ endColumn++
441
+ }
442
+
443
+ return [startLine, startColumn, endLine, endColumn]
444
+ }
445
+
446
+ /**
447
+ * @param {string} code @param {number} startOffset @param {number} endOffset
448
+ * @returns {[number, number, number, number]}
449
+ */
450
+ static fromOffset (code, startOffset, endOffset) {
451
+ const offsetToLineColumn = new OffsetToLineColumn(code)
452
+ const start = offsetToLineColumn.map(startOffset, false)
453
+ const end = offsetToLineColumn.map(endOffset, false)
454
+ return [start.line, start.column, end.line, end.column]
455
+ }
456
+
457
+ /**
458
+ * @param {string} code
459
+ * @param {{ line?: number, startOffset?: number | null, endOffset?: number | null, message: string }} error
460
+ * @param {(row: number) => string} getLine
461
+ * @returns {[number, number, number, number]}
462
+ */
463
+ static fromError (code, error, getLine) {
464
+ if (typeof error.startOffset === 'number' && typeof error.endOffset === 'number') {
465
+ // 完全な位置を取得できる場合
466
+ return this.fromOffset(code, error.startOffset, error.endOffset)
467
+ } else if (typeof error.line === 'number') {
468
+ // 行全体の場合
469
+ return this.fromNullable(error.line, null, null, null, getLine)
470
+ } else {
471
+ // 位置が不明な場合
472
+ return this.fromNullable(0, null, null, null, getLine)
473
+ }
474
+ }
475
+
476
+ /**
477
+ * @param {number} startLine
478
+ * @param {number | null} startColumn
479
+ * @param {number | null} endLine
480
+ * @param {number | null} endColumn
481
+ * @param {string} message
482
+ * @param {'warn' | 'error'} type
483
+ */
484
+ add (startLine, startColumn, endLine, endColumn, message, type) {
485
+ if (this.disable) {
486
+ return
487
+ }
488
+ const range = new this.AceRange(...EditorMarkers.fromNullable(startLine, startColumn, endLine, endColumn, (row) => this.doc.getLine(row)))
489
+ this.markers.push(this.session.addMarker(range, 'marker-' + (type === 'warn' ? 'yellow' : 'red'), 'text', false))
490
+ // typeは 'error' | 'warning' | 'info'
491
+ this.session.setAnnotations([{ row: startLine, column: startColumn, text: message, type: type === 'warn' ? 'warning' : 'error' }])
492
+ this.hasAnnotations = true
493
+ }
494
+
495
+ /**
496
+ * @param {string} code
497
+ * @param {{ line?: number, startOffset?: number | null, endOffset?: number | null, message: string }} error
498
+ * @param {'warn' | 'error'} type
499
+ */
500
+ addByError (code, error, type) {
501
+ this.add(...EditorMarkers.fromError(code, error, (row) => this.doc.getLine(row)), error.message, type)
502
+ }
503
+
504
+ /**
505
+ * 全てのエラーメッセージを削除する。
506
+ */
507
+ clear () {
508
+ for (const marker of this.markers) {
509
+ this.session.removeMarker(marker)
510
+ }
511
+ this.markers.length = 0
512
+ if (this.hasAnnotations) {
513
+ this.session.clearAnnotations()
514
+ this.hasAnnotations = false
515
+ }
516
+ }
517
+ }
518
+
519
+ /**
520
+ * ace editor のBackgroundTokenizerを上書きして、シンタックスハイライトを自由に表示するためのクラス。
521
+ * ace editor ではシンタックスハイライトのために正規表現ベースのBackgroundTokenizerクラスを用意し定期的にトークン化を
522
+ * 行っているが、正規表現ではなくなでしこのコンパイラの出力を使うためにはそれを上書きする必要がある。
523
+ */
524
+ class BackgroundTokenizer {
525
+ /**
526
+ * @param {AceDocument} doc
527
+ * @param {NakoCompiler} nako3
528
+ * @param {(firstRow: number, lastRow: number, ms: number) => void} onTokenUpdate
529
+ * @param {(code: string, err: Error) => void} onCompileError
530
+ * @param {boolean} underlineJosi
531
+ */
532
+ constructor (doc, nako3, onTokenUpdate, onCompileError, underlineJosi) {
533
+ this.onUpdate = onTokenUpdate
534
+ this.doc = doc
535
+ this.dirty = true
536
+ this.nako3 = nako3
537
+ this.onCompileError = onCompileError
538
+ this.underlineJosi = underlineJosi
539
+
540
+ // オートコンプリートで使うために、直近のtokenizeの結果を保存しておく
541
+ /** @type {ReturnType<NakoCompiler['lex']> | null} */
542
+ this.lastLexerOutput = null
543
+
544
+ // 各行のパース結果。
545
+ // typeはscopeのこと。配列の全要素のvalueを結合した文字列がその行の文字列と等しくなる必要がある。
546
+ /** @type {EditorToken[][]} */
547
+ this.lines = this.doc.getAllLines().map((line) => [{ type: 'markup.other', value: line, docHTML: null }])
548
+
549
+ // this.lines は外部から勝手に編集されてしまうため、コピーを持つ
550
+ /** @type {{ code: string, lines: string } | null} */
551
+ this.cache = null
552
+
553
+ this.deleted = false
554
+
555
+ /** @public */
556
+ this.enabled = true
557
+
558
+ const update = () => {
559
+ if (this.deleted) {
560
+ return
561
+ }
562
+ if (this.dirty && this.enabled) {
563
+ const startTime = Date.now()
564
+ this.dirty = false
565
+ const code = this.doc.getAllLines().join('\n')
566
+ try {
567
+ const startTime = Date.now()
568
+ const out = tokenize(this.doc.getAllLines(), nako3, this.underlineJosi)
569
+ this.lastLexerOutput = out.lexerOutput
570
+ this.lines = out.editorTokens
571
+ this.cache = { code, lines: JSON.stringify(this.lines) }
572
+
573
+ // ファイル全体の更新を通知する。
574
+ onTokenUpdate(0, this.doc.getLength() - 1, Date.now() - startTime)
575
+ } catch (e) {
576
+ onCompileError(code, e)
577
+ }
578
+ // tokenizeに時間がかかる場合、文字を入力できるように次回の実行を遅くする。
579
+ setTimeout(update, Math.max(100, Math.min(5000, (Date.now() - startTime) * 5)))
580
+ } else {
581
+ setTimeout(update, 100)
582
+ }
583
+ }
584
+
585
+ // コンストラクタが返る前にコールバックを呼ぶのはバグの元になるため一瞬待つ。
586
+ // たとえば `const a = new BackgroundTokenizer(..., () => { /* aを使った処理 */ }, ...)` がReferenceErrorになる。
587
+ setTimeout(() => { update() }, 0)
588
+ }
589
+
590
+ dispose () {
591
+ this.deleted = true
592
+ }
593
+
594
+ /**
595
+ * テキストに変更があったときに呼ばれる。IME入力中には呼ばれない。
596
+ * @param {{ action: string, start: { row: number, column: number }, end: { row: number, column: number }, lines: string[] }} delta
597
+ */
598
+ $updateOnChange (delta) {
599
+ this.dirty = true
600
+ const startRow = delta.start.row
601
+ const endRow = delta.end.row
602
+ if (startRow === endRow) { // 1行の編集
603
+ if (delta.action === 'insert' && this.lines[startRow]) { // 行内に文字列を挿入
604
+ const columnStart = delta.start.column
605
+ // updateOnChangeはIME入力中には呼ばれない。composition_placeholder を消さないとIME確定後の表示がずれる。
606
+ const oldTokens = this.lines[startRow]
607
+ .filter((v) => v.type !== 'composition_placeholder')
608
+ /** @type {EditorToken[]} */
609
+ const newTokens = []
610
+ let i = 0
611
+ let offset = 0
612
+
613
+ // columnStartより左のトークンはそのまま保持する
614
+ while (i < oldTokens.length && offset + oldTokens[i].value.length <= columnStart) {
615
+ newTokens.push(oldTokens[i])
616
+ offset += oldTokens[i].value.length
617
+ i++
618
+ }
619
+
620
+ // columnStartに重なっているトークンがあれば、2つに分割する
621
+ if (i < oldTokens.length && offset < columnStart) {
622
+ newTokens.push({ type: oldTokens[i].type, value: oldTokens[i].value.slice(0, columnStart - offset), docHTML: null })
623
+ newTokens.push({ type: 'markup.other', value: delta.lines[0], docHTML: null })
624
+ newTokens.push({ type: oldTokens[i].type, value: oldTokens[i].value.slice(columnStart - offset), docHTML: null })
625
+ i++
626
+ } else {
627
+ newTokens.push({ type: 'markup.other', value: delta.lines[0], docHTML: null })
628
+ }
629
+
630
+ // columnStartより右のトークンもそのまま保持する
631
+ while (i < oldTokens.length) {
632
+ newTokens.push(oldTokens[i])
633
+ i++
634
+ }
635
+
636
+ this.lines[startRow] = newTokens
637
+ } else {
638
+ this.lines[startRow] = getDefaultTokens(startRow, this.doc)
639
+ }
640
+ } else if (delta.action === 'remove') { // 範囲削除
641
+ this.lines.splice(startRow, endRow - startRow + 1, getDefaultTokens(startRow, this.doc))
642
+ } else { // 行の挿入
643
+ this.lines.splice(startRow, 1, ...Array(endRow - startRow + 1).fill(null).map((_, i) => getDefaultTokens(i + startRow, this.doc)))
644
+ }
645
+ }
646
+
647
+ /**
648
+ * tokenizerの出力を返す。文字入力したときに呼ばれる。
649
+ * @param {number} row
650
+ */
651
+ getTokens (row) {
652
+ // IME入力中はthis.lines[row]に自動的にnullが設定される。その場合新しく行のトークン列を生成して返さなければならない。
653
+ // 返した配列には自動的にIMEの入力用のテキストボックスであるcomposition_placeholderが挿入される。
654
+ if (!this.lines[row]) {
655
+ let ok = false
656
+
657
+ if (this.enabled) {
658
+ // tokenizeは非常に遅いため、キャッシュを使えるならそれを使う。
659
+ const code = this.doc.getAllLines().join('\n')
660
+ if (this.cache !== null && this.cache.code === code) {
661
+ ok = true
662
+ } else {
663
+ try {
664
+ const lines = tokenize(this.doc.getAllLines(), this.nako3, this.underlineJosi)
665
+ this.cache = { code, lines: JSON.stringify(lines.editorTokens) }
666
+ ok = true
667
+ } catch (e) {
668
+ if (!(e instanceof NakoError)) {
669
+ console.error(e)
670
+ }
671
+ }
672
+ }
673
+ }
674
+
675
+ if (ok && this.cache !== null) {
676
+ this.lines[row] = JSON.parse(this.cache.lines)[row]
677
+ } else {
678
+ this.lines[row] = getDefaultTokens(row, this.doc)
679
+ }
680
+ }
681
+ return this.lines[row]
682
+ }
683
+
684
+ // ace側から呼ばれるが無視するメソッド
685
+ // @ts-ignore
686
+ start (startRow) { /* pass */ }
687
+ // @ts-ignore
688
+ fireUpdateEvent (firstRow, lastRow) { /* pass */ }
689
+ // @ts-ignore
690
+ setDocument (doc) { /* pass */ }
691
+ scheduleStart () { /* pass */ }
692
+ // @ts-ignore
693
+ setTokenizer (tokenizer) { /* pass */ }
694
+ stop () { /* pass */ }
695
+ // @ts-ignore
696
+ getState (row) { return 'start' }
697
+ }
698
+
699
+ /**
700
+ * シンタックスハイライト以外のエディタの挙動の定義。
701
+ */
702
+ class LanguageFeatures {
703
+ /**
704
+ * @param {TypeofAceRange} AceRange
705
+ * @param {NakoCompiler} nako3
706
+ */
707
+ constructor (AceRange, nako3) {
708
+ this.AceRange = AceRange
709
+ this.nako3 = nako3
710
+ }
711
+
712
+ /**
713
+ * Ctrl + / の動作の定義。
714
+ * @param {string} state
715
+ * @param {Session} session
716
+ * @param {number} startRow
717
+ * @param {number} endRow
718
+ */
719
+ static toggleCommentLines (state, { doc }, startRow, endRow) {
720
+ const prepare = new NakoPrepare(new NakoLogger())
721
+ /**
722
+ * @param {string} line
723
+ * @returns {{ type: 'blank' | 'code' } | { type: 'comment', start: number, len: number }}
724
+ */
725
+ const parseLine = (line) => {
726
+ // 先頭の空白を消す
727
+ const indent = getIndent(line)
728
+ if (indent === line) {
729
+ return { type: 'blank' }
730
+ }
731
+ line = line.substr(indent.length)
732
+
733
+ // 先頭がコメントの開始文字かどうか確認する
734
+ const ch2 = line.substr(0, 2).split('').map((c) => prepare.convert1ch(c)).join('')
735
+ if (ch2.substr(0, 1) === '#') {
736
+ return { type: 'comment', start: indent.length, len: 1 + (line.charAt(1) === ' ' ? 1 : 0) }
737
+ }
738
+ if (ch2 === '//') {
739
+ return { type: 'comment', start: indent.length, len: 2 + (line.charAt(2) === ' ' ? 1 : 0) }
740
+ }
741
+
742
+ return { type: 'code' }
743
+ }
744
+
745
+ /** @type {number[]} */
746
+ const rows = []
747
+ for (let i = startRow; i <= endRow; i++) {
748
+ rows.push(i)
749
+ }
750
+
751
+ // 全ての行が空白行ならコメントアウト、全ての行が行コメントで始まるか空白行ならアンコメント、そうでなければコメントアウト。
752
+ if (!rows.every((row) => parseLine(doc.getLine(row)).type === 'blank') &&
753
+ rows.every((row) => parseLine(doc.getLine(row)).type !== 'code')) {
754
+ // アンコメント
755
+ for (const row of rows) {
756
+ // 行コメントで始まる行ならアンコメントする。
757
+ // 行コメントの直後にスペースがあるなら、それも1文字だけ削除する。
758
+ const line = parseLine(doc.getLine(row))
759
+ if (line.type === 'comment') {
760
+ doc.removeInLine(row, line.start, line.start + line.len)
761
+ }
762
+ }
763
+ } else {
764
+ // 最もインデントの低い行のインデント数を数える
765
+ const minIndent = Math.min(...rows.map((row) => countIndent(doc.getLine(row))))
766
+
767
+ // コメントアウトする
768
+ for (const row of rows) {
769
+ const line = doc.getLine(row)
770
+ let column = line.length
771
+ for (let i = 0; i < line.length; i++) {
772
+ if (countIndent(line.slice(0, i)) >= minIndent) {
773
+ column = i
774
+ break
775
+ }
776
+ }
777
+ doc.insertInLine({ row, column }, '// ')
778
+ }
779
+ }
780
+ }
781
+
782
+ /**
783
+ * 文字を入力するたびに呼ばれる。trueを返すとautoOutdentが呼ばれる。
784
+ * @param {string} state
785
+ * @param {string} line
786
+ * @param {string} input
787
+ * @returns {boolean}
788
+ */
789
+ static checkOutdent (state, line, input) {
790
+ // 特定のキーワードの入力が終わったタイミングでインデントを自動修正する。
791
+ // '違えば'のautoOutdentは「もし」と「条件分岐」のどちらのものか見分けが付かないため諦める。
792
+ // 「ここ|ま」(縦線がカーソル)の状態で「で」を打つとtrueになってしまう問題があるが、修正するには引数が足りない。
793
+ // eslint-disable-next-line no-irregular-whitespace
794
+ return /^[  ・\t]*ここまで$/.test(line + input)
795
+ }
796
+
797
+ /**
798
+ * checkOutdentがtrueを返したときに呼ばれる。
799
+ * @param {string} state
800
+ * @param {Session} session
801
+ * @param {number} row
802
+ * @returns {void}
803
+ */
804
+ autoOutdent (state, { doc }, row) {
805
+ // 1行目なら何もしない
806
+ if (row === 0) {
807
+ return
808
+ }
809
+ const prevLine = doc.getLine(row - 1)
810
+ let indent
811
+ if (LanguageFeatures.isBlockStart(prevLine)) {
812
+ // 1つ前の行が「〜ならば」などのブロック開始行なら、その行に合わせる。
813
+ indent = getIndent(prevLine)
814
+ } else {
815
+ // そうでなければ、1つ前の行のインデントから1段階outdentした位置に合わせる。
816
+ const s = this.getBlockStructure(doc.getAllLines().join('\n'))
817
+ const parent = s.parents[row]
818
+ indent = parent !== null ? s.spaces[parent] : ''
819
+ }
820
+
821
+ // 置換する
822
+ const oldIndent = getIndent(doc.getLine(row))
823
+ doc.replace(new this.AceRange(row, 0, row, oldIndent.length), indent)
824
+ }
825
+
826
+ /**
827
+ * エンターキーを押して行が追加されたときに挿入する文字列を指定する。
828
+ * @param {string} state
829
+ * @param {string} line 改行前にカーソルがあった行の文字列
830
+ * @param {string} tab タブ文字(デフォルトでは " ")
831
+ */
832
+ static getNextLineIndent (state, line, tab) {
833
+ // ●で始まるか、特定のキーワードで終わる場合にマッチする。
834
+ if (this.isBlockStart(line)) {
835
+ return getIndent(line) + tab
836
+ }
837
+ return getIndent(line)
838
+ }
839
+
840
+ /** @param {string} line */
841
+ static isBlockStart (line) {
842
+ // eslint-disable-next-line no-irregular-whitespace
843
+ return /^[  ・\t]*●|(ならば|なければ|ここから|条件分岐|違えば|回|繰り返(す|し)|の間|反復|とは|には|エラー監視|エラーならば|実行速度優先)、?\s*$/.test(line)
844
+ }
845
+
846
+ /**
847
+ * オートコンプリート
848
+ * @param {number} row
849
+ * @param {string} prefix getCompletionPrefixの出力
850
+ * @param {NakoCompiler} nako3
851
+ * @param {BackgroundTokenizer} backgroundTokenizer
852
+ */
853
+ static getCompletionItems (row, prefix, nako3, backgroundTokenizer) {
854
+ /**
855
+ * keyはcaption。metaは候補の横に薄く表示されるテキスト。
856
+ * @type {Map<string, { value: string, meta: Set<string>, score: number }>}
857
+ */
858
+ const result = new Map()
859
+ /** 引数のリストを含まない、関数名だけのリスト @type {Set<string>} */
860
+ const values = new Set()
861
+
862
+ /**
863
+ * オートコンプリートの項目を追加する。すでに存在するならマージする。
864
+ * @param {string} caption @param {string} value @param {string} meta
865
+ */
866
+ const addItem = (caption, value, meta) => {
867
+ const item = result.get(caption)
868
+ if (item) {
869
+ item.meta.add(meta)
870
+ } else {
871
+ // 日本語の文字数は英語よりずっと多いため、ただ一致する文字数を数えるだけで十分。
872
+ const score = prefix.split('').filter((c) => value.includes(c)).length
873
+ result.set(caption, { value, meta: new Set([meta]), score })
874
+ values.add(value)
875
+ }
876
+ }
877
+
878
+ // プラグイン関数
879
+ for (const name of Object.keys(nako3.__varslist[0])) {
880
+ if (name.startsWith('!')) { // 「!PluginBrowser:初期化」などを除外
881
+ continue
882
+ }
883
+ const f = nako3.funclist[name]
884
+ if (typeof f !== 'object' || f === null) {
885
+ continue
886
+ }
887
+
888
+ const pluginName = findPluginName(name, nako3) || 'プラグイン'
889
+ if (f.type === 'func') {
890
+ addItem(createParameterDeclaration(f.josi) + name, name, pluginName)
891
+ } else {
892
+ addItem(name, name, pluginName)
893
+ }
894
+ }
895
+
896
+ // 依存ファイルが定義した関数名
897
+ for (const [file, { funclist }] of Object.entries(nako3.dependencies)) {
898
+ for (const [name, f] of Object.entries(funclist)) {
899
+ const josi = (f && f.type === 'func') ? createParameterDeclaration(f.josi) : ''
900
+ addItem(josi + name, name, file)
901
+ }
902
+ }
903
+
904
+ // 現在のファイル内に存在する名前
905
+ if (backgroundTokenizer.lastLexerOutput !== null) {
906
+ for (const token of backgroundTokenizer.lastLexerOutput.tokens) {
907
+ const name = token.value + ''
908
+ // 同じ行のトークンの場合、自分自身にマッチしている可能性が高いため除外する。
909
+ // すでに定義されている場合も、定義ではなく参照の可能性が高いため除外する。
910
+ if (token.line === row || values.has(name)) {
911
+ continue
912
+ }
913
+ if (token.type === 'word') {
914
+ addItem(name, name, '変数')
915
+ } else if (token.type === 'func') {
916
+ const f = nako3.funclist[name]
917
+ const josi = (f && f.type === 'func') ? createParameterDeclaration(f.josi) : ''
918
+ addItem(josi + name, name, '関数')
919
+ }
920
+ }
921
+ }
922
+
923
+ return Array.from(result.entries()).map(([caption, data]) => ({ caption, ...data, meta: Array.from(data.meta).join(', ') }))
924
+ }
925
+
926
+ /**
927
+ * スニペット
928
+ */
929
+ /** @param {string} text */
930
+ static getSnippets (text) {
931
+ // インデント構文が有効化されているなら「ここまで」を消す
932
+ const indentSyntax = isIndentSyntaxEnabled(text)
933
+
934
+ /** @param {string} en @param {string} jp @param {string} snippet */
935
+ const item = (en, jp, snippet) => indentSyntax
936
+ ? { caption: en, meta: `\u21E5 ${jp}`, score: 1, snippet: snippet.replace(/\t*ここまで(\n|$)/g, '').replace(/\t/g, ' ') }
937
+ : { caption: en, meta: `\u21E5 ${jp}`, score: 1, snippet: snippet.replace(/\t/g, ' ') }
938
+
939
+ return [
940
+ item('if', 'もし〜ならば', 'もし${1:1=1}ならば\n\t${2:1を表示}\n違えば\n\t${3:2を表示}\nここまで\n'),
941
+ item('times', '〜回', '${1:3}回\n\t${2:1を表示}\nここまで\n'),
942
+ item('for', '繰り返す', '${1:N}で${2:1}から${3:3}まで繰り返す\n\t${4:Nを表示}\nここまで\n'),
943
+ item('while', '〜の間', '${1:N<2の間}\n\tN=N+1\nここまで\n'),
944
+ item('foreach', '〜を反復', '${1:[1,2,3]}を反復\n\t${2:対象を表示}\nここまで\n'),
945
+ item('switch', '〜で条件分岐', '${1:N}で条件分岐\n\t${2:1}ならば\n\t\t${3:1を表示}\n\tここまで\n\t${4:2}ならば\n\t\t${5:2を表示}\n\tここまで\n\t違えば\n\t\t${6:3を表示}\n\tここまで\nここまで\n'),
946
+ item('function', '●〜とは', '●(${1:AとBを})${2:足す}とは\n\t${3:A+Bを戻す}\nここまで\n'),
947
+ item('test', '●テスト:〜とは', '●テスト:${2:足す}とは\n\t1と2を足す\n\tそれと3がASSERT等しい\nここまで\n'),
948
+ item('try', 'エラー監視', 'エラー監視\n\t${1:1のエラー発生}\nエラーならば\n\t${2:2を表示}\nここまで\n')
949
+ ]
950
+ }
951
+
952
+ /**
953
+ * @param {string} line
954
+ * @param {NakoCompiler} nako3
955
+ */
956
+ static getCompletionPrefix (line, nako3) {
957
+ /** @type {ReturnType<NakoCompiler['lex']>["tokens"] | null} */
958
+ let tokens = null
959
+
960
+ // ひらがなとアルファベットとカタカナと漢字のみオートコンプリートする。
961
+ if (line.length === 0 || !/[ぁ-んa-zA-Zァ-ヶー\u3005\u4E00-\u9FCF]/.test(line[line.length - 1])) {
962
+ return ''
963
+ }
964
+
965
+ // 現在の行のカーソルより前の部分をlexerにかける。速度を優先して1行だけ処理する。
966
+ try {
967
+ nako3.reset()
968
+ tokens = withoutLogger(nako3, () => nako3.lex(line, 'completion.nako3', undefined, true)).tokens
969
+ .filter((t) => t.type !== 'eol' && t.type !== 'eof')
970
+ } catch (e) {
971
+ if (!(e instanceof NakoError)) {
972
+ console.error(e)
973
+ }
974
+ }
975
+ if (tokens === null || tokens.length === 0 || !tokens[tokens.length - 1].value) {
976
+ return ''
977
+ }
978
+ const prefix = tokens[tokens.length - 1].value + ''
979
+
980
+ // 単語の先頭がひらがなではなく末尾がひらがなのとき、助詞を打っている可能性が高いためオートコンプリートしない。
981
+ if (/[ぁ-ん]/.test(prefix[prefix.length - 1]) && !/[ぁ-ん]/.test(prefix[0])) {
982
+ return ''
983
+ }
984
+
985
+ // 最後のトークンの値を、オートコンプリートで既に入力した部分とする。
986
+ return prefix
987
+ }
988
+
989
+ /**
990
+ * 文字を打つたびに各行についてこの関数が呼ばれる。'start'を返した行はfold可能な範囲の先頭の行になる。
991
+ * @param {Session} session
992
+ * @param {string} foldStyle
993
+ * @param {number} row
994
+ * @returns {'start' | ''}
995
+ */
996
+ getFoldWidget ({ doc }, foldStyle, row) {
997
+ // 速度が重要なため正規表現でマッチする。
998
+ return LanguageFeatures.isBlockStart(doc.getLine(row)) ? 'start' : ''
999
+ }
1000
+
1001
+ /**
1002
+ * getFoldWidgetが'start'を返した行に設置されるfold用のボタンが押されたときに呼ばれる。
1003
+ * @param {Session} session
1004
+ * @param {string} foldStyle
1005
+ * @param {number} row
1006
+ * @returns {AceRange | null} foldする範囲
1007
+ */
1008
+ getFoldWidgetRange ({ doc }, foldStyle, row) {
1009
+ const pair = this.getBlockStructure(doc.getAllLines().join('\n')).pairs.find((v) => v[0] === row)
1010
+ if (pair !== undefined) {
1011
+ return new this.AceRange(pair[0], doc.getLine(pair[0]).length, pair[1] - 1, doc.getLine(pair[1] - 1).length)
1012
+ }
1013
+ return null
1014
+ }
1015
+
1016
+ /**
1017
+ * @param {AceDocument} doc
1018
+ * @returns {CodeLens[]}
1019
+ */
1020
+ static getCodeLens (doc) {
1021
+ const results = []
1022
+ for (const [row, line] of Array.from(doc.getAllLines().entries())) {
1023
+ // eslint-disable-next-line no-irregular-whitespace
1024
+ const matches = /^[  ・\t]*●テスト:(.+?)(?:とは|$)/.exec(line)
1025
+ if (matches !== null) {
1026
+ results.push({
1027
+ start: { row },
1028
+ command: { title: 'テストを実行', id: 'runTest', arguments: [matches[1]] }
1029
+ })
1030
+ }
1031
+ }
1032
+ return results
1033
+ }
1034
+
1035
+ /**
1036
+ * @param {string} code
1037
+ * @returns {ReturnType<getBlockStructure>}
1038
+ * @private
1039
+ */
1040
+ getBlockStructure (code) {
1041
+ // キャッシュ
1042
+ if (!this.blockStructure || this.blockStructure.code !== code) {
1043
+ // @ts-ignore
1044
+ this.blockStructure = { code, data: getBlockStructure(code) }
1045
+ }
1046
+ return this.blockStructure.data
1047
+ }
1048
+ }
1049
+
1050
+ /**
1051
+ * 複数ファイルを表示するための最低限のAPIを提供する。
1052
+ * @typedef {{ content: string, cursor: { range: AceRange, reversed: boolean }, scroll: { top: number, left: number }, undoManger: any }} EditorTabState
1053
+ */
1054
+ class EditorTabs {
1055
+ /**
1056
+ * @param {AceEditor} editor
1057
+ * @param {TypeofAceRange} AceRange
1058
+ * @param {any} UndoManager
1059
+ */
1060
+ constructor (editor, AceRange, UndoManager) {
1061
+ this.editor = editor
1062
+ this.AceRange = AceRange
1063
+ this.UndoManager = UndoManager
1064
+ }
1065
+
1066
+ /** @param {string} content @returns {EditorTabState} */
1067
+ newTab (content) {
1068
+ return {
1069
+ content,
1070
+ cursor: { range: new this.AceRange(0, 0, 0, 0), reversed: false },
1071
+ scroll: { left: 0, top: 0 },
1072
+ undoManger: new this.UndoManager()
1073
+ }
1074
+ }
1075
+
1076
+ /** @returns {EditorTabState} */
1077
+ getTab () {
1078
+ return {
1079
+ content: this.editor.getValue(),
1080
+ cursor: { range: this.editor.session.selection.getRange(), reversed: this.editor.session.selection.isBackwards() },
1081
+ scroll: { left: this.editor.session.getScrollLeft(), top: this.editor.session.getScrollTop() },
1082
+ undoManger: this.editor.session.getUndoManager()
1083
+ }
1084
+ }
1085
+
1086
+ /** @param {EditorTabState} state */
1087
+ setTab (state) {
1088
+ this.editor.setValue(state.content)
1089
+ this.editor.session.selection.setRange(state.cursor.range, state.cursor.reversed)
1090
+ this.editor.session.setScrollLeft(state.scroll.left)
1091
+ this.editor.session.setScrollTop(state.scroll.top)
1092
+ this.editor.session.setUndoManager(state.undoManger)
1093
+ }
1094
+ }
1095
+
1096
+ class Options {
1097
+ /** @param {AceEditor} editor */
1098
+ static save (editor) {
1099
+ try {
1100
+ /** @type {any} */
1101
+ const obj = {}
1102
+ for (const key of ['syntaxHighlighting', 'keyboardHandler', 'theme', 'fontSize', 'wrap', 'useSoftTabs', 'tabSize', 'showInvisibles', 'enableLiveAutocompletion', 'indentedSoftWrap', 'underlineJosi']) {
1103
+ obj[key] = editor.getOption(key)
1104
+ }
1105
+ localStorage.setItem('nako3EditorOptions', JSON.stringify(obj))
1106
+ } catch (e) {
1107
+ // JSON.stringify のエラー、localStorageのエラーなど
1108
+ console.error(e)
1109
+ return null
1110
+ }
1111
+ }
1112
+
1113
+ /** @param {AceEditor} editor */
1114
+ static load (editor) {
1115
+ try {
1116
+ if (!window.localStorage) {
1117
+ return null
1118
+ }
1119
+ const text = window.localStorage.getItem('nako3EditorOptions')
1120
+ if (text === null) {
1121
+ return null
1122
+ }
1123
+ const json = JSON.parse(text)
1124
+ if (['ace/keyboard/vscode', 'ace/keyboard/emacs', 'ace/keyboard/sublime', 'ace/keyboard/vim'].includes(json.keyboardHandler)) {
1125
+ editor.setOption('keyboardHandler', json.keyboardHandler)
1126
+ }
1127
+ if (['ace/theme/xcode', 'ace/theme/monokai'].includes(json.theme)) {
1128
+ editor.setOption('theme', json.theme)
1129
+ }
1130
+ if (typeof json.fontSize === 'number') {
1131
+ editor.setOption('fontSize', Math.min(48, Math.max(6, json.fontSize)))
1132
+ }
1133
+ for (const key of ['syntaxHighlighting', 'wrap', 'useSoftTabs', 'showInvisibles', 'enableLiveAutocompletion', 'indentedSoftWrap', 'underlineJosi']) {
1134
+ if (typeof json[key] === 'boolean') {
1135
+ editor.setOption(key, json[key])
1136
+ }
1137
+ }
1138
+ if (typeof json.tabSize === 'number') {
1139
+ editor.setOption('tabSize', Math.min(16, Math.max(0, json.tabSize)))
1140
+ }
1141
+ } catch (e) {
1142
+ // JSONのパースエラー、localStorageのエラーなど
1143
+ console.error(e)
1144
+ return null
1145
+ }
1146
+ }
1147
+
1148
+ /**
1149
+ * OptionPanelクラスをなでしこ用に書き換える。
1150
+ * @param {any} OptionPanel
1151
+ * @param {AceEditor} editor
1152
+ */
1153
+ static initPanel (OptionPanel, editor) {
1154
+ const panel = new OptionPanel(editor) // editorはエラーが飛ばなければ何でも良い
1155
+
1156
+ // ページ内で一度だけ呼ぶ
1157
+ if (this.done) {
1158
+ return
1159
+ }
1160
+ this.done = true
1161
+
1162
+ // renderメソッドを呼ぶとrenderOptionGroupにoptionGroups.Main、optionGroups.More が順に渡されることを利用して、optionGroupsを書き換える。
1163
+ let isMain = true
1164
+ panel.renderOptionGroup = (group) => {
1165
+ if (isMain) { // Main
1166
+ for (const key of Object.keys(group)) {
1167
+ delete group[key]
1168
+ }
1169
+
1170
+ // スマートフォンでも見れるように、文字数は最小限にする
1171
+ group['シンタックスハイライト'] = {
1172
+ path: 'syntaxHighlighting'
1173
+ }
1174
+ group['キーバインド'] = {
1175
+ path: 'keyboardHandler',
1176
+ type: 'select',
1177
+ items: [
1178
+ { caption: 'VSCode', value: 'ace/keyboard/vscode' },
1179
+ { caption: 'Emacs', value: 'ace/keyboard/emacs' },
1180
+ { caption: 'Sublime', value: 'ace/keyboard/sublime' },
1181
+ { caption: 'Vim', value: 'ace/keyboard/vim' }
1182
+ ]
1183
+ }
1184
+ group['カラーテーマ'] = {
1185
+ path: 'theme',
1186
+ type: 'select',
1187
+ items: [
1188
+ { caption: 'ライト', value: 'ace/theme/xcode' },
1189
+ { caption: 'ダーク', value: 'ace/theme/monokai' }
1190
+ ]
1191
+ }
1192
+ group['文字サイズ'] = {
1193
+ path: 'fontSize',
1194
+ type: 'number',
1195
+ defaultValue: 16
1196
+ }
1197
+ group['行の折り返し'] = {
1198
+ path: 'wrap',
1199
+ type: 'select',
1200
+ items: [
1201
+ { caption: 'なし', value: 'off' },
1202
+ { caption: 'あり', value: 'free' }
1203
+ ]
1204
+ }
1205
+ group['ソフトタブ'] = [{
1206
+ path: 'useSoftTabs'
1207
+ }, {
1208
+ ariaLabel: 'Tab Size',
1209
+ path: 'tabSize',
1210
+ type: 'number',
1211
+ values: [2, 3, 4, 8, 16]
1212
+ }]
1213
+ group['空白文字を表示'] = {
1214
+ path: 'showInvisibles'
1215
+ }
1216
+ group['常に自動補完'] = {
1217
+ path: 'enableLiveAutocompletion'
1218
+ }
1219
+ group['折り返した行をインデント'] = {
1220
+ path: 'indentedSoftWrap'
1221
+ }
1222
+ group['助詞に下線を引く'] = {
1223
+ path: 'underlineJosi'
1224
+ }
1225
+ isMain = false
1226
+ } else { // More
1227
+ for (const key of Object.keys(group)) {
1228
+ delete group[key]
1229
+ }
1230
+ }
1231
+ }
1232
+ panel.render()
1233
+
1234
+ // 設定メニューは ace/ext/settings_menu.js の showSettingsMenu 関数によって開かれる。
1235
+ // showSettingsMenu 関数は new OptionPanel(editor).render() で新しい設定パネルのインスタンスを生成するため、
1236
+ // renderメソッドを上書きすることで、生成されたインスタンスにアクセスできる。
1237
+ const render = OptionPanel.prototype.render
1238
+ const self = this
1239
+ OptionPanel.prototype.render = function (...args) {
1240
+ render.apply(this, ...args) // 元の処理
1241
+
1242
+ // OptionPanel.setOption() で発火される setOption イベントをキャッチする
1243
+ this.on('setOption', () => {
1244
+ console.log('設定を保存しました。')
1245
+ self.save(this.editor)
1246
+ })
1247
+ }
1248
+ }
1249
+ }
1250
+
1251
+ /**
1252
+ * ace/ext/language_tools の設定がグローバル変数で保持されているため、こちら側でもグローバル変数で管理しないと、エディタが複数あるときに正しく動かない。
1253
+ * - captionはオートコンプリートの候補として表示されるテキスト
1254
+ * - metaはcaptionのテキストの右に薄く表示されるテキスト
1255
+ * - docHTMLはその更に右に独立したウィンドウで表示されるHTMLによる説明
1256
+ * - valueは決定したときに実際に挿入される文字列。プレースホルダーを配置するなら代わりにsnippetに値を設定する。
1257
+ *
1258
+ * @typedef {{
1259
+ * getCompletions(
1260
+ * editor: any,
1261
+ * session: Session,
1262
+ * pos: { row: number, column: number },
1263
+ * prefix: any,
1264
+ * callback: (
1265
+ * a: null,
1266
+ * b: { meta: string, caption: string, value?: string, score: number, docHTML?: string, snippet?: string }[]
1267
+ * ) => void
1268
+ * ): void
1269
+ * getDocTooltip?(item: any): void
1270
+ * }} Completer
1271
+ * @type {Completer[]}
1272
+ */
1273
+ const completers = []
1274
+
1275
+ let editorIdCounter = 0
1276
+ /**
1277
+ * 指定したidのHTML要素をなでしこ言語のエディタにする。
1278
+ *
1279
+ * - ace editor がグローバルに読み込まれている必要がある。
1280
+ * - wnako3_editor.css を読み込む必要がある。
1281
+ * - readonly にするには data-nako3-readonly="true" を設定する。
1282
+ * - エラー位置の表示を無効化するには data-nako3-disable-marker="true" を設定する。
1283
+ * - 縦方向にリサイズ可能にするには nako3-resizable="true" を設定する。
1284
+ * - デバイスが遅いときにシンタックスハイライトを無効化する機能を切るには nako3-force-syntax-highlighting="true" を設定する。
1285
+ *
1286
+ * @param {string | Element} idOrElement HTML要素
1287
+ * @param {import('./wnako3')} nako3
1288
+ * @param {any} ace
1289
+ */
1290
+ function setupEditor (idOrElement, nako3, ace) {
1291
+ /** @type {AceEditor} */
1292
+ const editor = ace.edit(idOrElement)
1293
+ const element = typeof idOrElement === 'string' ? document.getElementById(idOrElement) : idOrElement
1294
+ if (element === null) {
1295
+ throw new Error(`idが ${idOrElement} のHTML要素は存在しません。`)
1296
+ }
1297
+
1298
+ /** @type {TypeofAceRange} */
1299
+ const AceRange = ace.require('ace/range').Range
1300
+ const editorMarkers = new EditorMarkers(
1301
+ editor.session,
1302
+ editor.session.bgTokenizer.doc,
1303
+ AceRange,
1304
+ !!element.dataset.nako3DisableMarker
1305
+ )
1306
+
1307
+ if (element.classList.contains('nako3_ace_mounted')) {
1308
+ // 同じエディタを誤って複数回初期化すると、ace editor の挙動を書き換えているせいで
1309
+ // 意図しない動作をしたため、すでにエディタとして使われていないことを確認する。
1310
+ throw new Error('なでしこ言語のエディタの初期化処理を同一のHTML要素に対して複数回適用しました。')
1311
+ }
1312
+ // lang="ja" があると表示がずれる問題の修正 #839
1313
+ element.setAttribute('lang', 'en')
1314
+ // 以前のバージョンではnako3_editorをhtmlに直接付けていたため、互換性のためnako3_editorとは別のクラス名を使用する。
1315
+ element.classList.add('nako3_ace_mounted')
1316
+ element.classList.add('nako3_editor') // CSSのため
1317
+ const readonly = element.dataset.nako3Readonly
1318
+ // eslint-disable-next-line no-extra-boolean-cast
1319
+ if (!!readonly) {
1320
+ element.classList.add('readonly')
1321
+ editor.setReadOnly(true)
1322
+ }
1323
+
1324
+ editor.setFontSize(16)
1325
+
1326
+ /** @param {Session} session */
1327
+ const resetEditorTokens = (session) => {
1328
+ // 一旦テキスト全体を消してから、元に戻す
1329
+ /** @type {AceDocument} */
1330
+ const doc = session.doc
1331
+ const lines = doc.getAllLines()
1332
+ const range = session.selection.getRange()
1333
+ doc.removeFullLines(0, doc.getLength())
1334
+ doc.insert({ row: 0, column: 0 }, lines.join('\n'))
1335
+ session.selection.setRange(range, false)
1336
+ }
1337
+
1338
+ ace.require('ace/config').defineOptions(editor.constructor.prototype, 'editor', {
1339
+ syntaxHighlighting: {
1340
+ /** @type {(this: AceEditor, value: boolean) => void} */
1341
+ set: function (value) {
1342
+ this.session.bgTokenizer.enabled = value
1343
+ resetEditorTokens(this.session)
1344
+ },
1345
+ initialValue: true
1346
+ },
1347
+ underlineJosi: {
1348
+ /** @type {(this: AceEditor, value: boolean) => void} */
1349
+ set: function (value) {
1350
+ this.session.bgTokenizer.underlineJosi = value
1351
+ resetEditorTokens(this.session)
1352
+ },
1353
+ initialValue: true
1354
+ }
1355
+ })
1356
+
1357
+ editor.setOptions({
1358
+ wrap: 'free',
1359
+ indentedSoftWrap: false,
1360
+ showPrintMargin: false
1361
+ })
1362
+ ace.require('ace/keybindings/vscode')
1363
+ editor.setKeyboardHandler('ace/keyboard/vscode')
1364
+
1365
+ // ドキュメントのホバー
1366
+ const Tooltip = ace.require('ace/tooltip').Tooltip
1367
+ const tooltip = new Tooltip(editor.container)
1368
+ const event = ace.require('ace/lib/event')
1369
+ event.addListener(editor.renderer.content, 'mouseout', () => {
1370
+ // マウスカーソルがエディタの外に出たら、tooltipを隠す
1371
+ tooltip.hide()
1372
+ })
1373
+ editor.on('mousemove', (e) => {
1374
+ // マウスカーソルがトークンに重なったときにtooltipを表示する。モバイル端末の場合はトークンにカーソルが当たったときに表示される。
1375
+ const pos = e.getDocumentPosition()
1376
+ // getTokenAtはcolumnが行末より大きいとき行末のトークンを返してしまう。
1377
+ if (pos.column >= e.editor.session.getLine(pos.row).length) {
1378
+ tooltip.hide()
1379
+ return
1380
+ }
1381
+ // getTokenAtは実際よりも1文字右のトークンを取得してしまうため、columnに1を足している。
1382
+ /** @type {EditorToken} */
1383
+ const token = e.editor.session.getTokenAt(pos.row, pos.column + 1)
1384
+ if (token === null || !token.docHTML) {
1385
+ // ドキュメントが存在しないトークンならtooltipを表示しない
1386
+ tooltip.hide()
1387
+ return
1388
+ }
1389
+
1390
+ tooltip.setHtml(token.docHTML)
1391
+ tooltip.show(null, e.clientX, e.clientY)
1392
+ })
1393
+ editor.session.on('change', () => {
1394
+ // モバイル端末でドキュメントが存在するトークンを編集するときにツールチップが消えない問題を解消するために、文字を打ったらtooltipを隠す。
1395
+ tooltip.hide()
1396
+
1397
+ // 文字入力したらマーカーを消す
1398
+ editorMarkers.clear()
1399
+ })
1400
+
1401
+ const forceSyntaxHighlighting = !!element.dataset.nako3ForceSyntaxHighlighting
1402
+
1403
+ let isFirstTime = true
1404
+ const oldBgTokenizer = editor.session.bgTokenizer
1405
+ const backgroundTokenizer = new BackgroundTokenizer(
1406
+ editor.session.bgTokenizer.doc,
1407
+ nako3,
1408
+ (firstRow, lastRow, ms) => {
1409
+ oldBgTokenizer._signal('update', { data: { first: firstRow, last: lastRow } })
1410
+
1411
+ // 処理が遅い場合シンタックスハイライトを無効化する。
1412
+ if (ms > 220 && editor.getOption('syntaxHighlighting') && !readonly && !forceSyntaxHighlighting && isFirstTime) {
1413
+ isFirstTime = false
1414
+ slowSpeedMessage.classList.add('visible')
1415
+ editor.setOption('syntaxHighlighting', false)
1416
+ setTimeout(() => {
1417
+ slowSpeedMessage.classList.remove('visible')
1418
+ }, 13000)
1419
+ }
1420
+ },
1421
+ (code, err) => { editorMarkers.addByError(code, err, 'error') },
1422
+ /** @type {boolean} */(editor.getOption('underlineJosi'))
1423
+ )
1424
+
1425
+ // オートコンプリートを有効化する
1426
+ editor.setOptions({
1427
+ enableBasicAutocompletion: true,
1428
+ enableSnippets: true,
1429
+ enableLiveAutocompletion: true
1430
+ })
1431
+
1432
+ const editorId = editorIdCounter++
1433
+ editor.wnako3EditorId = editorId
1434
+
1435
+ // オートコンプリートのcompleterを設定する
1436
+ completers.push(
1437
+ {
1438
+ getCompletions (editor, session, pos, prefix, callback) {
1439
+ if (editor.wnako3EditorId !== editorId) {
1440
+ callback(null, [])
1441
+ } else {
1442
+ const items = LanguageFeatures.getCompletionItems(pos.row, prefix, nako3, backgroundTokenizer)
1443
+ // 完全に一致する候補があればオートコンプリートしない。(Aceエディタでの挙動が微妙なため。)
1444
+ if (items.some((v) => v.value === prefix)) {
1445
+ callback(null, [])
1446
+ return
1447
+ }
1448
+ callback(null, items)
1449
+ }
1450
+ }
1451
+ },
1452
+ { getCompletions (editor, session, pos, prefix, callback) { callback(null, (editor.wnako3EditorId !== editorId) ? [] : LanguageFeatures.getSnippets(editor.session.doc.getAllLines().join('\n'))) } }
1453
+ )
1454
+ ace.require('ace/ext/language_tools').setCompleters(completers)
1455
+
1456
+ // オートコンプリートの単語の区切りが日本語に対応していないため、メソッドを上書きして対応させる。
1457
+ // 文字を入力するたびに呼ばれ、''以外を返すとその文字列をもとにしてautocompletionが始まる。
1458
+ ace.require('ace/autocomplete/util').getCompletionPrefix = (/** @type {AceEditor} */ editor) => {
1459
+ const pos = editor.getCursorPosition()
1460
+ return LanguageFeatures.getCompletionPrefix(editor.session.doc.getLine(pos.row).slice(0, pos.column), nako3)
1461
+ }
1462
+
1463
+ // エディタの挙動の設定
1464
+ const languageFeatures = new LanguageFeatures(AceRange, nako3)
1465
+ const oop = ace.require('ace/lib/oop')
1466
+ const TextMode = ace.require('ace/mode/text').Mode
1467
+ const Mode = function () {
1468
+ this.HighlightRules = new TextMode().HighlightRules
1469
+ this.foldingRules = {
1470
+ getFoldWidget: languageFeatures.getFoldWidget.bind(languageFeatures),
1471
+ getFoldWidgetRange: languageFeatures.getFoldWidgetRange.bind(languageFeatures)
1472
+ }
1473
+ }
1474
+ oop.inherits(Mode, TextMode)
1475
+ Mode.prototype.toggleCommentLines = LanguageFeatures.toggleCommentLines.bind(LanguageFeatures)
1476
+ Mode.prototype.getNextLineIndent = LanguageFeatures.getNextLineIndent.bind(LanguageFeatures)
1477
+ Mode.prototype.checkOutdent = LanguageFeatures.checkOutdent.bind(LanguageFeatures)
1478
+ Mode.prototype.autoOutdent = languageFeatures.autoOutdent.bind(languageFeatures)
1479
+ editor.session.setMode(new Mode())
1480
+
1481
+ // tokenizer (シンタックスハイライト)の上書き
1482
+ editor.session.bgTokenizer.stop()
1483
+ editor.session.bgTokenizer = backgroundTokenizer
1484
+
1485
+ editor.setTheme('ace/theme/xcode')
1486
+
1487
+ // 設定メニューの上書き
1488
+ // なでしこ用に上書きした設定の削除やテキストの和訳をする。
1489
+ Options.load(editor)
1490
+ const OptionPanel = ace.require('ace/ext/options').OptionPanel
1491
+ Options.initPanel(OptionPanel, editor)
1492
+
1493
+ // 右下のボタン全体を囲むdiv
1494
+ const buttonContainer = document.createElement('div')
1495
+ buttonContainer.classList.add('button-container')
1496
+ editor.container.appendChild(buttonContainer)
1497
+
1498
+ // 遅い端末へのメッセージのボタン
1499
+ const slowSpeedMessage = document.createElement('span')
1500
+ slowSpeedMessage.classList.add('slow-speed-message')
1501
+ slowSpeedMessage.innerHTML = '<span>エディタの|応答速度が|低下したため|シンタックス|ハイライトを|無効化|しました。</span>'.replace(/\|/g, '</span><span>')
1502
+ buttonContainer.appendChild(slowSpeedMessage)
1503
+
1504
+ // テストの定義の上に「テストを実行」ボタンを表示する
1505
+ /** @type {{ name: 'test', callback: (testName: string | undefined) => void }[]} */
1506
+ const codeLensListeners = []
1507
+ try {
1508
+ const CodeLens = ace.require('ace/ext/code_lens')
1509
+ editor.setOption('enableCodeLens', true)
1510
+ editor.commands.addCommand({
1511
+ name: 'runTest',
1512
+ exec: (/** @type {AceEditor} */editor, /** @type {any[]} */args) => {
1513
+ codeLensListeners
1514
+ .filter((v) => v.name === 'test')
1515
+ .forEach((f) => f.callback(args[0]))
1516
+ }
1517
+ })
1518
+ CodeLens.registerCodeLensProvider(editor, {
1519
+ provideCodeLenses: (/** @type {Session} */session, /** @type {(_: null, arr: CodeLens[]) => void} */callback) => {
1520
+ callback(null, codeLensListeners.some((v) => v.name === 'test') ? LanguageFeatures.getCodeLens(session.doc) : [])
1521
+ }
1522
+ })
1523
+ } catch (e) {
1524
+ console.error(e) // ext/code_lens のscriptタグが読み込まれていない場合など。
1525
+ }
1526
+
1527
+ // 「全画面表示」ボタン
1528
+ const exitFullscreen = () => {
1529
+ editor.container.classList.remove('fullscreen')
1530
+ editor.renderer.setScrollMargin(0, 0, 0, 0) // marginを元に戻す
1531
+ }
1532
+ const fullscreenButton = document.createElement('span')
1533
+ fullscreenButton.classList.add('editor-button')
1534
+ fullscreenButton.innerText = '全画面表示'
1535
+ fullscreenButton.addEventListener('click', (e) => {
1536
+ if (editor.container.classList.contains('fullscreen')) {
1537
+ exitFullscreen()
1538
+ } else {
1539
+ editor.container.classList.add('fullscreen')
1540
+ editor.renderer.setScrollMargin(20, 20, 0, 0) // 上下に少し隙間を開ける
1541
+ }
1542
+ e.preventDefault()
1543
+ })
1544
+ buttonContainer.appendChild(fullscreenButton)
1545
+
1546
+ // 「設定を開く」ボタン
1547
+ const settingsButton = document.createElement('span')
1548
+ settingsButton.classList.add('editor-button')
1549
+ settingsButton.innerText = '設定を開く'
1550
+ settingsButton.addEventListener('click', (e) => {
1551
+ exitFullscreen()
1552
+ editor.execCommand('showSettingsMenu')
1553
+ e.preventDefault()
1554
+ })
1555
+ buttonContainer.appendChild(settingsButton)
1556
+
1557
+ // 複数ファイルの切り替え
1558
+ const UndoManager = ace.require('ace/undomanager').UndoManager
1559
+ const editorTabs = new EditorTabs(editor, AceRange, UndoManager)
1560
+
1561
+ // リサイズ可能にする
1562
+ const resizable = element.dataset.nako3Resizable
1563
+ if (resizable) {
1564
+ new MutationObserver(() => { editor.resize() }).observe(editor.container, { attributes: true })
1565
+ editor.renderer.setScrollMargin(4, 0, 4, 0)
1566
+ editor.container.classList.add('resizable')
1567
+ }
1568
+
1569
+ const retokenize = () => { backgroundTokenizer.dirty = true }
1570
+
1571
+ /**
1572
+ * プログラムを実行して、エラーがあればエディタ上に波線を表示する。出力はoutputContainerに表示する。
1573
+ * methodが'test'のとき、testNameを指定すると1つのテストだけ実行できる。
1574
+ * @param {{
1575
+ * outputContainer?: HTMLElement
1576
+ * file?: string
1577
+ * preCode?: string
1578
+ * localFiles?: Record<string, string>
1579
+ * method?: 'run' | 'test' | 'compile'
1580
+ * testName?: string
1581
+ * }} opts
1582
+ */
1583
+ const run = (opts) => {
1584
+ const code = editor.getValue()
1585
+ const preCode = opts.preCode || '' // プログラムの前に自動的に挿入されるコード
1586
+
1587
+ // loggerを新しいインスタンスに置き換える。そうしないとどのエディタで起きたエラー(や警告や出力)なのかが分からない。
1588
+ const logger = nako3.replaceLogger()
1589
+ if (opts.outputContainer) {
1590
+ const c = opts.outputContainer
1591
+ logger.addListener('info', ({ html }) => {
1592
+ if (!c) { console.log(html) }
1593
+ c.style.display = 'block'
1594
+ c.innerHTML += html
1595
+ })
1596
+ opts.outputContainer.classList.add('nako3-output-container')
1597
+ }
1598
+ const file = opts.file || 'main.nako3'
1599
+
1600
+ // 警告とエラーをエディタ上に表示する。
1601
+ logger.addListener('info', ({ position, noColor, level }) => {
1602
+ if (position.file === file && (level === 'warn' || level === 'error')) {
1603
+ editorMarkers.addByError(code, { ...position, message: noColor }, level)
1604
+ }
1605
+ })
1606
+
1607
+ // 依存ファイルを読み込む。
1608
+ const promise = nako3.loadDependencies(preCode + code, file, preCode, opts.localFiles || {})
1609
+ .then(() => {
1610
+ // プログラムを実行する。
1611
+ if (opts.method === 'test') {
1612
+ return nako3.test(preCode + code, file, preCode, opts.testName)
1613
+ } else if (opts.method === 'compile') {
1614
+ return nako3.compile(preCode + code, file, false, preCode)
1615
+ } else {
1616
+ return nako3.runReset(preCode + code, file, preCode)
1617
+ }
1618
+ })
1619
+ .catch((_) => {
1620
+ // エラーはloggerに送られるためここでは何もしなくて良い
1621
+ })
1622
+ .then(async (res) => {
1623
+ // 読み込んだ依存ファイルの情報を使って再度シンタックスハイライトする。
1624
+ retokenize()
1625
+
1626
+ // シンタックスハイライトが終わるのを待つ
1627
+ while (backgroundTokenizer.dirty) {
1628
+ await new Promise((resolve) => setTimeout(resolve, 0))
1629
+ }
1630
+ return res
1631
+ }).catch((err) => { console.error(err) })
1632
+
1633
+ return { promise, logger, code }
1634
+ }
1635
+
1636
+ return { editor, editorMarkers, editorTabs, retokenize, run, codeLensListeners }
1637
+ }
1638
+
1639
+ module.exports = {
1640
+ tokenize,
1641
+ setupEditor,
1642
+ LanguageFeatures,
1643
+ EditorMarkers,
1644
+ BackgroundTokenizer
1645
+ }