demotape 0.0.2 → 0.0.3

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 (548) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/CHANGELOG.md +9 -0
  4. data/Makefile +4 -2
  5. data/README.md +101 -10
  6. data/demotape.gemspec +1 -1
  7. data/demotape.y +224 -505
  8. data/demotape_new.y.backup +160 -0
  9. data/demotape_simple_test.y +32 -0
  10. data/editors/sublime-text/DemoTape.sublime-completions +4 -0
  11. data/editors/sublime-text/DemoTape.sublime-syntax +24 -2
  12. data/editors/vim/syntax/demotape.vim +16 -2
  13. data/editors/vscode/demotape.tmLanguage.json +31 -3
  14. data/lib/demo_tape/cli.rb +36 -0
  15. data/lib/demo_tape/command.rb +63 -103
  16. data/lib/demo_tape/exporter.rb +3 -0
  17. data/lib/demo_tape/formatter.rb +142 -0
  18. data/lib/demo_tape/lexer.rb +118 -34
  19. data/lib/demo_tape/parser/helpers.rb +363 -0
  20. data/lib/demo_tape/parser/rules.rb +396 -0
  21. data/lib/demo_tape/parser.rb +381 -592
  22. data/lib/demo_tape/runner.rb +82 -27
  23. data/lib/demo_tape/token.rb +86 -0
  24. data/lib/demo_tape/version.rb +1 -1
  25. data/lib/demo_tape.rb +5 -2
  26. metadata +10 -527
  27. data/examples/alt.mp4 +0 -0
  28. data/examples/alt.png +0 -0
  29. data/examples/alt.tape +0 -9
  30. data/examples/arrow.mp4 +0 -0
  31. data/examples/arrow.png +0 -0
  32. data/examples/arrow.tape +0 -7
  33. data/examples/backspace.mp4 +0 -0
  34. data/examples/backspace.png +0 -0
  35. data/examples/backspace.tape +0 -4
  36. data/examples/bash.mp4 +0 -0
  37. data/examples/bash.png +0 -0
  38. data/examples/bash.tape +0 -4
  39. data/examples/bg.jpg +0 -0
  40. data/examples/clear.mp4 +0 -0
  41. data/examples/clear.png +0 -0
  42. data/examples/clear.tape +0 -6
  43. data/examples/cli_ui_interactive_prompt.mp4 +0 -0
  44. data/examples/cli_ui_interactive_prompt.png +0 -0
  45. data/examples/cli_ui_interactive_prompt.tape +0 -16
  46. data/examples/cli_ui_nested_frames.mp4 +0 -0
  47. data/examples/cli_ui_nested_frames.png +0 -0
  48. data/examples/cli_ui_nested_frames.tape +0 -15
  49. data/examples/cli_ui_progress.mp4 +0 -0
  50. data/examples/cli_ui_progress.png +0 -0
  51. data/examples/cli_ui_progress.tape +0 -6
  52. data/examples/cli_ui_spinner.mp4 +0 -0
  53. data/examples/cli_ui_spinner.png +0 -0
  54. data/examples/cli_ui_spinner.tape +0 -6
  55. data/examples/cli_ui_status_widget.mp4 +0 -0
  56. data/examples/cli_ui_status_widget.png +0 -0
  57. data/examples/cli_ui_status_widget.tape +0 -6
  58. data/examples/cli_ui_symbols.mp4 +0 -0
  59. data/examples/cli_ui_symbols.png +0 -0
  60. data/examples/cli_ui_symbols.tape +0 -6
  61. data/examples/cli_ui_text_prompt.mp4 +0 -0
  62. data/examples/cli_ui_text_prompt.png +0 -0
  63. data/examples/cli_ui_text_prompt.tape +0 -10
  64. data/examples/clipboard.mp4 +0 -0
  65. data/examples/clipboard.png +0 -0
  66. data/examples/clipboard.tape +0 -5
  67. data/examples/ctrl.mp4 +0 -0
  68. data/examples/ctrl.png +0 -0
  69. data/examples/ctrl.tape +0 -5
  70. data/examples/ctrl_arrows.mp4 +0 -0
  71. data/examples/ctrl_arrows.png +0 -0
  72. data/examples/ctrl_arrows.tape +0 -14
  73. data/examples/demotape.mp4 +0 -0
  74. data/examples/demotape.png +0 -0
  75. data/examples/demotape.tape +0 -6
  76. data/examples/demotape.txt +0 -13
  77. data/examples/enter.mp4 +0 -0
  78. data/examples/enter.png +0 -0
  79. data/examples/enter.tape +0 -3
  80. data/examples/errors/include_itself.tape +0 -1
  81. data/examples/fastfetch.gif +0 -0
  82. data/examples/fastfetch.mp4 +0 -0
  83. data/examples/fastfetch.png +0 -0
  84. data/examples/fastfetch.tape +0 -11
  85. data/examples/fish.mp4 +0 -0
  86. data/examples/fish.png +0 -0
  87. data/examples/fish.tape +0 -4
  88. data/examples/format.mp4 +0 -0
  89. data/examples/format.png +0 -0
  90. data/examples/format.tape +0 -15
  91. data/examples/gh-cli.mp4 +0 -0
  92. data/examples/gh-cli.png +0 -0
  93. data/examples/gh-cli.tape +0 -13
  94. data/examples/gif_loop.gif +0 -0
  95. data/examples/gif_loop.png +0 -0
  96. data/examples/gif_loop.tape +0 -5
  97. data/examples/include.mp4 +0 -0
  98. data/examples/include.png +0 -0
  99. data/examples/include.tape +0 -1
  100. data/examples/include_with_set.mp4 +0 -0
  101. data/examples/include_with_set.png +0 -0
  102. data/examples/include_with_set.tape +0 -2
  103. data/examples/meta/cli.tape +0 -10
  104. data/examples/nerdfonts.mp4 +0 -0
  105. data/examples/nerdfonts.png +0 -0
  106. data/examples/nerdfonts.tape +0 -9
  107. data/examples/output.avi +0 -0
  108. data/examples/output.gif +0 -0
  109. data/examples/output.mov +0 -0
  110. data/examples/output.mp4 +0 -0
  111. data/examples/output.png +0 -0
  112. data/examples/output.tape +0 -9
  113. data/examples/output.webm +0 -0
  114. data/examples/products.json +0 -1803
  115. data/examples/screenshot.mp4 +0 -0
  116. data/examples/screenshot.png +0 -0
  117. data/examples/screenshot.tape +0 -9
  118. data/examples/script.rb +0 -3
  119. data/examples/set_cursor_bar.mp4 +0 -0
  120. data/examples/set_cursor_bar.png +0 -0
  121. data/examples/set_cursor_bar.tape +0 -5
  122. data/examples/set_cursor_blink.mp4 +0 -0
  123. data/examples/set_cursor_blink.png +0 -0
  124. data/examples/set_cursor_blink.tape +0 -4
  125. data/examples/set_cursor_underline.mp4 +0 -0
  126. data/examples/set_cursor_underline.png +0 -0
  127. data/examples/set_cursor_underline.tape +0 -5
  128. data/examples/set_dimensions.mp4 +0 -0
  129. data/examples/set_dimensions.png +0 -0
  130. data/examples/set_dimensions.tape +0 -5
  131. data/examples/set_font_family.mp4 +0 -0
  132. data/examples/set_font_family.png +0 -0
  133. data/examples/set_font_family.tape +0 -4
  134. data/examples/set_font_size.mp4 +0 -0
  135. data/examples/set_font_size.png +0 -0
  136. data/examples/set_font_size.tape +0 -4
  137. data/examples/set_line_height.mp4 +0 -0
  138. data/examples/set_line_height.png +0 -0
  139. data/examples/set_line_height.tape +0 -8
  140. data/examples/set_margin.mp4 +0 -0
  141. data/examples/set_margin.png +0 -0
  142. data/examples/set_margin.tape +0 -7
  143. data/examples/set_margin_with_border_radius.mp4 +0 -0
  144. data/examples/set_margin_with_border_radius.png +0 -0
  145. data/examples/set_margin_with_border_radius.tape +0 -8
  146. data/examples/set_margin_with_image.mp4 +0 -0
  147. data/examples/set_margin_with_image.png +0 -0
  148. data/examples/set_margin_with_image.tape +0 -7
  149. data/examples/set_padding.mp4 +0 -0
  150. data/examples/set_padding.png +0 -0
  151. data/examples/set_padding.tape +0 -7
  152. data/examples/set_theme.mp4 +0 -0
  153. data/examples/set_theme.png +0 -0
  154. data/examples/set_theme.tape +0 -9
  155. data/examples/set_theme_property.mp4 +0 -0
  156. data/examples/set_theme_property.png +0 -0
  157. data/examples/set_theme_property.tape +0 -10
  158. data/examples/set_typing_speed.mp4 +0 -0
  159. data/examples/set_typing_speed.png +0 -0
  160. data/examples/set_typing_speed.tape +0 -6
  161. data/examples/slides.mp4 +0 -0
  162. data/examples/slides.png +0 -0
  163. data/examples/slides.tape +0 -14
  164. data/examples/space.mp4 +0 -0
  165. data/examples/space.png +0 -0
  166. data/examples/space.tape +0 -3
  167. data/examples/stdin.mp4 +0 -0
  168. data/examples/stdin.png +0 -0
  169. data/examples/stdin.tape +0 -7
  170. data/examples/tab.mp4 +0 -0
  171. data/examples/tab.png +0 -0
  172. data/examples/tab.tape +0 -5
  173. data/examples/themes/3024_day.png +0 -0
  174. data/examples/themes/3024_night.png +0 -0
  175. data/examples/themes/aardvark_blue.png +0 -0
  176. data/examples/themes/abernathy.png +0 -0
  177. data/examples/themes/adventure.png +0 -0
  178. data/examples/themes/adventure_time.png +0 -0
  179. data/examples/themes/afterglow.png +0 -0
  180. data/examples/themes/alabaster.png +0 -0
  181. data/examples/themes/alien_blood.png +0 -0
  182. data/examples/themes/andromeda.png +0 -0
  183. data/examples/themes/apple_classic.png +0 -0
  184. data/examples/themes/apple_system_colors.png +0 -0
  185. data/examples/themes/arcoiris.png +0 -0
  186. data/examples/themes/argonaut.png +0 -0
  187. data/examples/themes/arthur.png +0 -0
  188. data/examples/themes/atelier_sulphurpool.png +0 -0
  189. data/examples/themes/atom.png +0 -0
  190. data/examples/themes/atom_one_light.png +0 -0
  191. data/examples/themes/aurora.png +0 -0
  192. data/examples/themes/ayu.png +0 -0
  193. data/examples/themes/ayu_light.png +0 -0
  194. data/examples/themes/ayu_mirage.png +0 -0
  195. data/examples/themes/banana_blueberry.png +0 -0
  196. data/examples/themes/batman.png +0 -0
  197. data/examples/themes/belafonte_day.png +0 -0
  198. data/examples/themes/belafonte_night.png +0 -0
  199. data/examples/themes/birds_of_paradise.png +0 -0
  200. data/examples/themes/blazer.png +0 -0
  201. data/examples/themes/blue_berry_pie.png +0 -0
  202. data/examples/themes/blue_dolphin.png +0 -0
  203. data/examples/themes/blue_matrix.png +0 -0
  204. data/examples/themes/bluloco_dark.png +0 -0
  205. data/examples/themes/bluloco_light.png +0 -0
  206. data/examples/themes/borland.png +0 -0
  207. data/examples/themes/breeze.png +0 -0
  208. data/examples/themes/bright_lights.png +0 -0
  209. data/examples/themes/broadcast.png +0 -0
  210. data/examples/themes/brogrammer.png +0 -0
  211. data/examples/themes/bubbles.png +0 -0
  212. data/examples/themes/builtin_dark.png +0 -0
  213. data/examples/themes/builtin_light.png +0 -0
  214. data/examples/themes/builtin_pastel_dark.png +0 -0
  215. data/examples/themes/builtin_solarized_dark.png +0 -0
  216. data/examples/themes/builtin_solarized_light.png +0 -0
  217. data/examples/themes/builtin_tango_dark.png +0 -0
  218. data/examples/themes/builtin_tango_light.png +0 -0
  219. data/examples/themes/c64.png +0 -0
  220. data/examples/themes/calamity.png +0 -0
  221. data/examples/themes/catppuccin_frappe.png +0 -0
  222. data/examples/themes/catppuccin_latte.png +0 -0
  223. data/examples/themes/catppuccin_macchiato.png +0 -0
  224. data/examples/themes/catppuccin_mocha.png +0 -0
  225. data/examples/themes/cga.png +0 -0
  226. data/examples/themes/chalk.png +0 -0
  227. data/examples/themes/chalkboard.png +0 -0
  228. data/examples/themes/challenger_deep.png +0 -0
  229. data/examples/themes/chester.png +0 -0
  230. data/examples/themes/ciapre.png +0 -0
  231. data/examples/themes/clrs.png +0 -0
  232. data/examples/themes/cobalt2.png +0 -0
  233. data/examples/themes/cobalt_neon.png +0 -0
  234. data/examples/themes/coffee_theme.png +0 -0
  235. data/examples/themes/contrast_light.png +0 -0
  236. data/examples/themes/coolnight.png +0 -0
  237. data/examples/themes/crayon_pony_fish.png +0 -0
  238. data/examples/themes/crystal_violet.png +0 -0
  239. data/examples/themes/cutie_pro.png +0 -0
  240. data/examples/themes/cyber_cube.png +0 -0
  241. data/examples/themes/cyber_punk2077.png +0 -0
  242. data/examples/themes/cyberdyne.png +0 -0
  243. data/examples/themes/cyberpunk.png +0 -0
  244. data/examples/themes/dark_pastel.png +0 -0
  245. data/examples/themes/dark_plus.png +0 -0
  246. data/examples/themes/darkermatrix.png +0 -0
  247. data/examples/themes/darkmatrix.png +0 -0
  248. data/examples/themes/darkside.png +0 -0
  249. data/examples/themes/dayfox.png +0 -0
  250. data/examples/themes/deep.png +0 -0
  251. data/examples/themes/desert.png +0 -0
  252. data/examples/themes/dimmed_monokai.png +0 -0
  253. data/examples/themes/django.png +0 -0
  254. data/examples/themes/django_reborn_again.png +0 -0
  255. data/examples/themes/django_smooth.png +0 -0
  256. data/examples/themes/doom_one.png +0 -0
  257. data/examples/themes/doom_peacock.png +0 -0
  258. data/examples/themes/dot_gov.png +0 -0
  259. data/examples/themes/dracula.png +0 -0
  260. data/examples/themes/dracula_plus.png +0 -0
  261. data/examples/themes/duckbones.png +0 -0
  262. data/examples/themes/duotone_dark.png +0 -0
  263. data/examples/themes/earthsong.png +0 -0
  264. data/examples/themes/elemental.png +0 -0
  265. data/examples/themes/elementary.png +0 -0
  266. data/examples/themes/encom.png +0 -0
  267. data/examples/themes/espresso.png +0 -0
  268. data/examples/themes/espresso_libre.png +0 -0
  269. data/examples/themes/everblush.png +0 -0
  270. data/examples/themes/fahrenheit.png +0 -0
  271. data/examples/themes/fairyfloss.png +0 -0
  272. data/examples/themes/farmhouse_dark.png +0 -0
  273. data/examples/themes/farmhouse_light.png +0 -0
  274. data/examples/themes/fideloper.png +0 -0
  275. data/examples/themes/firefly_traditional.png +0 -0
  276. data/examples/themes/firefox_dev.png +0 -0
  277. data/examples/themes/firewatch.png +0 -0
  278. data/examples/themes/fish_tank.png +0 -0
  279. data/examples/themes/flat.png +0 -0
  280. data/examples/themes/flatland.png +0 -0
  281. data/examples/themes/flexoki_dark.png +0 -0
  282. data/examples/themes/flexoki_light.png +0 -0
  283. data/examples/themes/floraverse.png +0 -0
  284. data/examples/themes/forest_blue.png +0 -0
  285. data/examples/themes/framer.png +0 -0
  286. data/examples/themes/front_end_delight.png +0 -0
  287. data/examples/themes/fun_forrest.png +0 -0
  288. data/examples/themes/galaxy.png +0 -0
  289. data/examples/themes/galizur.png +0 -0
  290. data/examples/themes/ganyu.png +0 -0
  291. data/examples/themes/git_hub_dark.png +0 -0
  292. data/examples/themes/github.png +0 -0
  293. data/examples/themes/glacier.png +0 -0
  294. data/examples/themes/glorious.png +0 -0
  295. data/examples/themes/grape.png +0 -0
  296. data/examples/themes/grass.png +0 -0
  297. data/examples/themes/grey_green.png +0 -0
  298. data/examples/themes/gruvbox_dark.png +0 -0
  299. data/examples/themes/gruvbox_dark_hard.png +0 -0
  300. data/examples/themes/gruvbox_light.png +0 -0
  301. data/examples/themes/guezwhoz.png +0 -0
  302. data/examples/themes/h4rithd.png +0 -0
  303. data/examples/themes/h4rithd_com.png +0 -0
  304. data/examples/themes/ha_x0_r_blue.png +0 -0
  305. data/examples/themes/ha_x0_r_gr33_n.png +0 -0
  306. data/examples/themes/ha_x0_r_r3_d.png +0 -0
  307. data/examples/themes/hacktober.png +0 -0
  308. data/examples/themes/hardcore.png +0 -0
  309. data/examples/themes/harper.png +0 -0
  310. data/examples/themes/highway.png +0 -0
  311. data/examples/themes/hipster_green.png +0 -0
  312. data/examples/themes/hivacruz.png +0 -0
  313. data/examples/themes/homebrew.png +0 -0
  314. data/examples/themes/hopscotch.png +0 -0
  315. data/examples/themes/hopscotch_256.png +0 -0
  316. data/examples/themes/horizon.png +0 -0
  317. data/examples/themes/hurtado.png +0 -0
  318. data/examples/themes/hybrid.png +0 -0
  319. data/examples/themes/hyper.png +0 -0
  320. data/examples/themes/ic_green_ppl.png +0 -0
  321. data/examples/themes/ic_orange_ppl.png +0 -0
  322. data/examples/themes/iceberg_dark.png +0 -0
  323. data/examples/themes/iceberg_light.png +0 -0
  324. data/examples/themes/idea.png +0 -0
  325. data/examples/themes/idle_toes.png +0 -0
  326. data/examples/themes/ir_black.png +0 -0
  327. data/examples/themes/iterm2_dark_background.png +0 -0
  328. data/examples/themes/iterm2_default.png +0 -0
  329. data/examples/themes/iterm2_light_background.png +0 -0
  330. data/examples/themes/iterm2_pastel_dark_background.png +0 -0
  331. data/examples/themes/iterm2_smoooooth.png +0 -0
  332. data/examples/themes/iterm2_solarized_dark.png +0 -0
  333. data/examples/themes/iterm2_solarized_light.png +0 -0
  334. data/examples/themes/iterm2_tango_dark.png +0 -0
  335. data/examples/themes/iterm2_tango_light.png +0 -0
  336. data/examples/themes/jackie_brown.png +0 -0
  337. data/examples/themes/japanesque.png +0 -0
  338. data/examples/themes/jellybeans.png +0 -0
  339. data/examples/themes/jet_brains_darcula.png +0 -0
  340. data/examples/themes/jubi.png +0 -0
  341. data/examples/themes/juicy_colors.png +0 -0
  342. data/examples/themes/kanagawa.png +0 -0
  343. data/examples/themes/kanagawabones.png +0 -0
  344. data/examples/themes/kibble.png +0 -0
  345. data/examples/themes/kolorit.png +0 -0
  346. data/examples/themes/konsolas.png +0 -0
  347. data/examples/themes/lab_fox.png +0 -0
  348. data/examples/themes/laser.png +0 -0
  349. data/examples/themes/later_this_evening.png +0 -0
  350. data/examples/themes/lavandula.png +0 -0
  351. data/examples/themes/liquid_carbon.png +0 -0
  352. data/examples/themes/liquid_carbon_transparent.png +0 -0
  353. data/examples/themes/liquid_carbon_transparent_inverse.png +0 -0
  354. data/examples/themes/lovelace.png +0 -0
  355. data/examples/themes/man_page.png +0 -0
  356. data/examples/themes/mariana.png +0 -0
  357. data/examples/themes/material.png +0 -0
  358. data/examples/themes/material_dark.png +0 -0
  359. data/examples/themes/material_darker.png +0 -0
  360. data/examples/themes/material_design_colors.png +0 -0
  361. data/examples/themes/material_ocean.png +0 -0
  362. data/examples/themes/mathias.png +0 -0
  363. data/examples/themes/matrix.png +0 -0
  364. data/examples/themes/medallion.png +0 -0
  365. data/examples/themes/mellifluous.png +0 -0
  366. data/examples/themes/midnight_in_mojave.png +0 -0
  367. data/examples/themes/mirage.png +0 -0
  368. data/examples/themes/misterioso.png +0 -0
  369. data/examples/themes/molokai.png +0 -0
  370. data/examples/themes/mona_lisa.png +0 -0
  371. data/examples/themes/monokai_cmder.png +0 -0
  372. data/examples/themes/monokai_pro.png +0 -0
  373. data/examples/themes/monokai_pro_filter_octagon.png +0 -0
  374. data/examples/themes/monokai_pro_filter_ristretto.png +0 -0
  375. data/examples/themes/monokai_remastered.png +0 -0
  376. data/examples/themes/monokai_soda.png +0 -0
  377. data/examples/themes/monokai_vivid.png +0 -0
  378. data/examples/themes/moonlight_ii.png +0 -0
  379. data/examples/themes/n0tch2k.png +0 -0
  380. data/examples/themes/neobones_dark.png +0 -0
  381. data/examples/themes/neobones_light.png +0 -0
  382. data/examples/themes/neon.png +0 -0
  383. data/examples/themes/neopolitan.png +0 -0
  384. data/examples/themes/neutron.png +0 -0
  385. data/examples/themes/night_city.png +0 -0
  386. data/examples/themes/night_lion_v1.png +0 -0
  387. data/examples/themes/night_lion_v2.png +0 -0
  388. data/examples/themes/night_owlish_light.png +0 -0
  389. data/examples/themes/nightfox.png +0 -0
  390. data/examples/themes/niji.png +0 -0
  391. data/examples/themes/nocturnal_winter.png +0 -0
  392. data/examples/themes/nord.png +0 -0
  393. data/examples/themes/nord_light.png +0 -0
  394. data/examples/themes/novel.png +0 -0
  395. data/examples/themes/nvim_dark.png +0 -0
  396. data/examples/themes/nvim_light.png +0 -0
  397. data/examples/themes/obsidian.png +0 -0
  398. data/examples/themes/ocean.png +0 -0
  399. data/examples/themes/oceanic_material.png +0 -0
  400. data/examples/themes/oceanic_next.png +0 -0
  401. data/examples/themes/ollie.png +0 -0
  402. data/examples/themes/one_dark.png +0 -0
  403. data/examples/themes/one_half_dark.png +0 -0
  404. data/examples/themes/one_half_light.png +0 -0
  405. data/examples/themes/one_star.png +0 -0
  406. data/examples/themes/operator_mono_dark.png +0 -0
  407. data/examples/themes/overnight_slumber.png +0 -0
  408. data/examples/themes/pale_night_hc.png +0 -0
  409. data/examples/themes/pandora.png +0 -0
  410. data/examples/themes/paraiso_dark.png +0 -0
  411. data/examples/themes/paul_millr.png +0 -0
  412. data/examples/themes/pencil_dark.png +0 -0
  413. data/examples/themes/pencil_light.png +0 -0
  414. data/examples/themes/peppermint.png +0 -0
  415. data/examples/themes/piatto_light.png +0 -0
  416. data/examples/themes/pnevma.png +0 -0
  417. data/examples/themes/popping_and_locking.png +0 -0
  418. data/examples/themes/primary.png +0 -0
  419. data/examples/themes/primer.png +0 -0
  420. data/examples/themes/pro.png +0 -0
  421. data/examples/themes/pro_light.png +0 -0
  422. data/examples/themes/purple_rain.png +0 -0
  423. data/examples/themes/purplepeter.png +0 -0
  424. data/examples/themes/qb64_super_dark_blue.png +0 -0
  425. data/examples/themes/rapture.png +0 -0
  426. data/examples/themes/raycast_dark.png +0 -0
  427. data/examples/themes/raycast_light.png +0 -0
  428. data/examples/themes/rebecca.png +0 -0
  429. data/examples/themes/red_alert.png +0 -0
  430. data/examples/themes/red_planet.png +0 -0
  431. data/examples/themes/red_sands.png +0 -0
  432. data/examples/themes/relaxed.png +0 -0
  433. data/examples/themes/retro.png +0 -0
  434. data/examples/themes/retrowave.png +0 -0
  435. data/examples/themes/rippedcasts.png +0 -0
  436. data/examples/themes/rose_pine.png +0 -0
  437. data/examples/themes/rose_pine_dawn.png +0 -0
  438. data/examples/themes/rose_pine_moon.png +0 -0
  439. data/examples/themes/rouge_2.png +0 -0
  440. data/examples/themes/royal.png +0 -0
  441. data/examples/themes/ryuuko.png +0 -0
  442. data/examples/themes/sakura.png +0 -0
  443. data/examples/themes/scarlet_protocol.png +0 -0
  444. data/examples/themes/sea_shells.png +0 -0
  445. data/examples/themes/seafoam_pastel.png +0 -0
  446. data/examples/themes/seoulbones_dark.png +0 -0
  447. data/examples/themes/seoulbones_light.png +0 -0
  448. data/examples/themes/serendipity_midnight.png +0 -0
  449. data/examples/themes/serendipity_morning.png +0 -0
  450. data/examples/themes/serendipity_sunset.png +0 -0
  451. data/examples/themes/seti.png +0 -0
  452. data/examples/themes/shades_of_purple.png +0 -0
  453. data/examples/themes/shaman.png +0 -0
  454. data/examples/themes/slate.png +0 -0
  455. data/examples/themes/sleepy_hollow.png +0 -0
  456. data/examples/themes/smyck.png +0 -0
  457. data/examples/themes/snazzy.png +0 -0
  458. data/examples/themes/soft_server.png +0 -0
  459. data/examples/themes/solarized_darcula.png +0 -0
  460. data/examples/themes/solarized_dark_higher_contrast.png +0 -0
  461. data/examples/themes/solarized_dark_patched.png +0 -0
  462. data/examples/themes/sonoran_gothic.png +0 -0
  463. data/examples/themes/sonoran_sunrise.png +0 -0
  464. data/examples/themes/space_gray.png +0 -0
  465. data/examples/themes/space_gray_eighties.png +0 -0
  466. data/examples/themes/space_gray_eighties_dull.png +0 -0
  467. data/examples/themes/spacedust.png +0 -0
  468. data/examples/themes/spiderman.png +0 -0
  469. data/examples/themes/spring.png +0 -0
  470. data/examples/themes/square.png +0 -0
  471. data/examples/themes/sublette.png +0 -0
  472. data/examples/themes/subliminal.png +0 -0
  473. data/examples/themes/sundried.png +0 -0
  474. data/examples/themes/symfonic.png +0 -0
  475. data/examples/themes/synthwave.png +0 -0
  476. data/examples/themes/synthwave_alpha.png +0 -0
  477. data/examples/themes/synthwave_everything.png +0 -0
  478. data/examples/themes/tango_adapted.png +0 -0
  479. data/examples/themes/tango_half_adapted.png +0 -0
  480. data/examples/themes/teerb.png +0 -0
  481. data/examples/themes/terafox.png +0 -0
  482. data/examples/themes/terminal_basic.png +0 -0
  483. data/examples/themes/thayer_bright.png +0 -0
  484. data/examples/themes/the_hulk.png +0 -0
  485. data/examples/themes/theme.tape +0 -13
  486. data/examples/themes/tinacious_design_dark.png +0 -0
  487. data/examples/themes/tinacious_design_light.png +0 -0
  488. data/examples/themes/tokyo_night.png +0 -0
  489. data/examples/themes/tokyo_night_light.png +0 -0
  490. data/examples/themes/tokyo_night_storm.png +0 -0
  491. data/examples/themes/tokyonight.png +0 -0
  492. data/examples/themes/tokyonight_day.png +0 -0
  493. data/examples/themes/tokyonight_storm.png +0 -0
  494. data/examples/themes/tomorrow.png +0 -0
  495. data/examples/themes/tomorrow_night.png +0 -0
  496. data/examples/themes/tomorrow_night_blue.png +0 -0
  497. data/examples/themes/tomorrow_night_bright.png +0 -0
  498. data/examples/themes/tomorrow_night_burns.png +0 -0
  499. data/examples/themes/tomorrow_night_eighties.png +0 -0
  500. data/examples/themes/toy_chest.png +0 -0
  501. data/examples/themes/treehouse.png +0 -0
  502. data/examples/themes/twilight.png +0 -0
  503. data/examples/themes/ubuntu.png +0 -0
  504. data/examples/themes/ultra_dark.png +0 -0
  505. data/examples/themes/ultra_violent.png +0 -0
  506. data/examples/themes/under_the_sea.png +0 -0
  507. data/examples/themes/unholy.png +0 -0
  508. data/examples/themes/unikitty.png +0 -0
  509. data/examples/themes/urple.png +0 -0
  510. data/examples/themes/vaughn.png +0 -0
  511. data/examples/themes/vesper.png +0 -0
  512. data/examples/themes/vibrant_ink.png +0 -0
  513. data/examples/themes/vimbones.png +0 -0
  514. data/examples/themes/violet_dark.png +0 -0
  515. data/examples/themes/violet_light.png +0 -0
  516. data/examples/themes/warm_neon.png +0 -0
  517. data/examples/themes/wez.png +0 -0
  518. data/examples/themes/whimsy.png +0 -0
  519. data/examples/themes/wild_cherry.png +0 -0
  520. data/examples/themes/wilmersdorf.png +0 -0
  521. data/examples/themes/wombat.png +0 -0
  522. data/examples/themes/wryan.png +0 -0
  523. data/examples/themes/zenbones.png +0 -0
  524. data/examples/themes/zenbones_dark.png +0 -0
  525. data/examples/themes/zenbones_light.png +0 -0
  526. data/examples/themes/zenburn.png +0 -0
  527. data/examples/themes/zenburned.png +0 -0
  528. data/examples/themes/zenwritten_dark.png +0 -0
  529. data/examples/themes/zenwritten_light.png +0 -0
  530. data/examples/themes/zeonica.png +0 -0
  531. data/examples/tmux.mp4 +0 -0
  532. data/examples/tmux.png +0 -0
  533. data/examples/tmux.tape +0 -28
  534. data/examples/type.mp4 +0 -0
  535. data/examples/type.png +0 -0
  536. data/examples/type.tape +0 -11
  537. data/examples/type_file.mp4 +0 -0
  538. data/examples/type_file.png +0 -0
  539. data/examples/type_file.tape +0 -23
  540. data/examples/variable_typing.mp4 +0 -0
  541. data/examples/variable_typing.png +0 -0
  542. data/examples/variable_typing.tape +0 -41
  543. data/examples/wait_until.mp4 +0 -0
  544. data/examples/wait_until.png +0 -0
  545. data/examples/wait_until.tape +0 -8
  546. data/examples/zsh.mp4 +0 -0
  547. data/examples/zsh.png +0 -0
  548. data/examples/zsh.tape +0 -4
@@ -62,6 +62,7 @@ module DemoTape
62
62
  "F12" => :f12,
63
63
  "Meta" => :meta,
64
64
  "Command" => :command,
65
+ "Cmd" => :command,
65
66
  "Slash" => "/",
66
67
  "BackSlash" => "\\"
67
68
  }.freeze
@@ -69,12 +70,14 @@ module DemoTape
69
70
  VALID_COMMANDS = KEY_MAPPING.keys + %w[
70
71
  Clear
71
72
  Copy
73
+ Group
72
74
  Include
73
75
  Output
74
76
  Pause
75
77
  Paste
76
78
  Require
77
79
  Resume
80
+ Run
78
81
  Screenshot
79
82
  Send
80
83
  Set
@@ -85,9 +88,10 @@ module DemoTape
85
88
  WaitUntil
86
89
  ].freeze
87
90
 
88
- META_COMMANDS = %w[Require Set Include Output].freeze
89
- COMMANDS_WITH_SPEED = KEY_MAPPING.keys + %w[Type]
90
- COMMANDS_WITH_TIMEOUT = %w[WaitUntil].freeze
91
+ META_COMMANDS = %w[Group Include Output Require Set].freeze
92
+ COMMANDS_WITH_DURATION = KEY_MAPPING.keys +
93
+ %w[Run Type TypeFile WaitUntil Wait Sleep].freeze
94
+ COMMANDS_WITH_COUNT = KEY_MAPPING.keys + %w[Wait Sleep Set]
91
95
  VALID_TIME_UNITS = %w[ms s m h].freeze
92
96
 
93
97
  # Valid keys that can be used in key combos
@@ -103,21 +107,21 @@ module DemoTape
103
107
  typing_speed variable_typing width
104
108
  ].freeze
105
109
 
106
- attr_reader :type, :args, :options
110
+ attr_reader :type, :args, :options, :children
107
111
  attr_accessor :column, :duration_column, :file, :line, :line_content,
108
- :speed_column, :timeout_column, :tokens
112
+ :tokens
109
113
 
110
114
  def initialize(type, args = "", **options)
111
115
  @type = type
112
116
  @args = args
113
117
  @options = options
118
+ @children = options.delete(:children) || []
119
+ @group_invocation = options.delete(:group_invocation) || false
114
120
  @line = nil
115
121
  @column = nil
116
122
  @line_content = nil
117
123
  @file = nil
118
124
  @duration_column = nil
119
- @speed_column = nil
120
- @timeout_column = nil
121
125
  @tokens = []
122
126
  end
123
127
 
@@ -126,6 +130,10 @@ module DemoTape
126
130
  VALID_KEYS.include?(type)
127
131
  end
128
132
 
133
+ def group?
134
+ type == "Group"
135
+ end
136
+
129
137
  def keys
130
138
  return [] unless key?
131
139
 
@@ -153,22 +161,14 @@ module DemoTape
153
161
  .to_sym
154
162
  end
155
163
 
156
- def validate_command!
157
- return if VALID_COMMANDS.include?(type)
158
-
159
- raise_error "Unknown command: #{type.inspect}"
164
+ def find_token(klass, list = tokens)
165
+ list.find {|token| token.is_a?(klass) }
160
166
  end
161
167
 
162
- def validate_speed!
163
- return unless options[:speed] && !COMMANDS_WITH_SPEED.include?(type)
164
-
165
- raise_error "Command #{type.inspect} does not accept speed option"
166
- end
167
-
168
- def validate_timeout!
169
- return unless options[:timeout] && !COMMANDS_WITH_TIMEOUT.include?(type)
170
-
171
- raise_error "Command #{type.inspect} does not accept timeout option"
168
+ def group_invocation?
169
+ # Group invocations are marked as such
170
+ # AND don't start with uppercase letter
171
+ @group_invocation && type[0].to_s.match?(/[^A-Z]/)
172
172
  end
173
173
 
174
174
  def validate_set_options!
@@ -180,96 +180,35 @@ module DemoTape
180
180
  return if SET_OPTIONS.include?(base_option)
181
181
 
182
182
  raise_error "Unknown option: #{options[:option].inspect} #{self}",
183
- column_override: tokens[1].column
184
- end
185
-
186
- def validate_keys!
187
- return unless options[:keys]
188
-
189
- all_keys = options[:keys] + [type]
190
-
191
- all_keys.each do |key|
192
- next if VALID_KEYS.include?(key)
193
-
194
- unless VALID_COMMANDS.include?(key)
195
- raise_error "Invalid key in combo: #{key.inspect}"
196
- end
197
-
198
- raise_error "Command #{key.inspect} doesn't support key combos"
199
- end
200
- end
201
-
202
- def validate_regex!
203
- return unless type == "WaitUntil"
204
-
205
- raise_error "WaitUntil command requires a regex pattern" if args.empty?
206
-
207
- begin
208
- options[:pattern] = Regexp.new(args)
209
- rescue RegexpError => error
210
- raise_error "Invalid regex pattern: #{error.message}"
211
- end
183
+ column_override: tokens[2].column
212
184
  end
213
185
 
214
186
  def prepare!
215
- validate_command!
216
- validate_speed!
217
- validate_timeout!
218
187
  normalize_theme_options!
219
188
  validate_set_options!
220
- validate_keys!
221
- validate_regex!
222
- validate_duration!
223
- validate_type_file!
224
-
225
189
  normalize_spacing_values!
226
190
 
227
191
  self
228
192
  end
229
193
 
230
- private def validate_type_file!
231
- return unless type == "TypeFile"
232
-
233
- raise_error "TypeFile command requires a file path" if args.empty?
234
- end
235
-
236
- private def validate_duration!
237
- [args, :duration] => [duration, source] if %w[Sleep Wait].include?(type)
238
- [options[:speed], :speed] => [duration, source] if options[:speed]
239
- [options[:timeout], :timeout] => [duration, source] if options[:timeout]
240
-
241
- return unless duration
242
-
243
- unit = duration[/[a-z]+$/i]
244
- return if VALID_TIME_UNITS.include?(unit)
245
-
246
- col = case source
247
- when :speed then speed_column
248
- when :timeout then timeout_column
249
- else duration_column
250
- end
251
-
252
- raise_error "Invalid time unit: #{unit.inspect}", column_override: col
253
- end
254
-
255
194
  private def normalize_theme_options!
256
195
  return unless type == "Set"
257
196
  return unless options[:option]&.include?(".")
258
197
 
259
- option, sub_option = *options[:option].split(".", 2)
198
+ option, property = *options[:option].split(".", 2)
260
199
 
261
200
  unless option == "theme"
262
201
  raise_error "Unexpected attribute #{options[:option].inspect}",
263
- column_override: tokens[1].column
202
+ column_override: tokens[2].column
264
203
  end
265
204
 
266
- unless Theme.valid_options.include?(sub_option.to_sym)
205
+ unless Theme.valid_options.include?(property.to_sym)
267
206
  raise_error "Invalid theme property",
268
- column_override: tokens[1].column + 6 # ".theme".size
207
+ column_override: tokens[2].column + option.length + 1
269
208
  end
270
209
 
271
210
  @options[:option] = option
272
- @options[:sub_option] = sub_option
211
+ @options[:property] = property
273
212
  end
274
213
 
275
214
  private def normalize_spacing_values!
@@ -286,12 +225,23 @@ module DemoTape
286
225
 
287
226
  def raise_error(message, column_override: nil)
288
227
  col = column_override || column
228
+ line_content = tokens
229
+ # Remove "end" keyword from line content
230
+ # This is required so we can properly point to errors
231
+ # in commands that have blocks (e.g. Run do..end)
232
+ .reject { it.is_a?(Token::Keyword) && it.value == "end" }
233
+ .map(&:raw).join
289
234
 
290
- raise DemoTape::ParseError, message unless line && col && line_content
235
+ raise DemoTape::ParseError, message unless line && col
291
236
 
292
237
  error_msg = "#{message} at #{location(column_override:)}:\n"
293
- error_msg += " #{line_content}\n"
294
- error_msg += " #{' ' * (col - 1)}^"
238
+ error_msg += " #{line_content.strip}\n"
239
+
240
+ # Calculate pointer position: col is absolute position in original line
241
+ # line_content.strip removes leading spaces, so we need to adjust
242
+ leading_spaces = line_content[/^\s*/].size
243
+ pointer_col = col - leading_spaces - 1
244
+ error_msg += " #{' ' * pointer_col}^"
295
245
 
296
246
  raise DemoTape::ParseError, error_msg
297
247
  end
@@ -308,28 +258,38 @@ module DemoTape
308
258
 
309
259
  tokens.each_with_index do |token, index|
310
260
  previous_token = tokens[index - 1]
311
- preceded_by_comma = previous_token.is_a?(Token::Operator) &&
312
- previous_token.value == ","
261
+ preceded_by_number = previous_token.is_a?(Token::Number)
262
+ preceded_by_group_name = tokens[index - 2]&.value == "Group"
263
+ preceded_by_command_with_duration =
264
+ %w[Sleep Wait].include?(tokens[index - 2]&.value) # rubocop:disable Performance/CollectionLiteralInLoop
313
265
 
314
266
  case token
267
+ when Token::Duration
268
+ duration = token.value[:raw].to_s.strip
269
+ duration = "#{duration}s" if token.value[:unit].to_s.strip == ""
270
+ values << thor.set_color(duration, :cyan)
315
271
  when Token::String
316
- values << " "
317
272
  values << thor.set_color(token.raw, :yellow)
318
273
  when Token::Operator
319
274
  values << thor.set_color(token.value, :white)
320
275
  when Token::Number
321
- values << " " unless previous_token.is_a?(Token::Operator)
322
- values << " " if preceded_by_comma
323
- values << thor.set_color(token.value, :magenta)
324
- when Token::Duration
325
- values << " " unless previous_token.is_a?(Token::Operator)
326
- values << thor.set_color(token.value, :magenta)
276
+ values << if preceded_by_command_with_duration
277
+ thor.set_color("#{token.value}s", :cyan)
278
+ else
279
+ thor.set_color(token.value, :magenta)
280
+ end
327
281
  when Token::Regex
328
- values << " "
329
282
  values << thor.set_color(token.raw, :green)
330
283
  when Token::Identifier
331
- values << " " if previous_token.is_a?(Token::Identifier)
332
- values << thor.set_color(token.value, :blue)
284
+ values << if preceded_by_group_name
285
+ thor.set_color(token.value, :cyan)
286
+ else
287
+ thor.set_color(token.value, :blue)
288
+ end
289
+ when Token::Space
290
+ values << " " unless preceded_by_number
291
+ when Token::Keyword
292
+ # do nothing
333
293
  else
334
294
  raise "Unexpected token type: #{token.class.name}"
335
295
  end
@@ -214,6 +214,9 @@ module DemoTape
214
214
  end
215
215
 
216
216
  def get_png_dimensions(file_path)
217
+ return [0, 0] unless file_path
218
+ return [0, 0] unless File.file?(file_path)
219
+
217
220
  File.open(file_path, "rb") do |f|
218
221
  f.read(8) # Skip PNG signature
219
222
  f.read(4) # Skip chunk length
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DemoTape
4
+ class Formatter
5
+ attr_accessor :buffered_newlines
6
+
7
+ def initialize(input)
8
+ @input = input
9
+ @buffered_newlines = 0
10
+ end
11
+
12
+ def newline?(token)
13
+ token.instance_of?(Token::Newline)
14
+ end
15
+
16
+ def call
17
+ parser = Parser.new
18
+ tree = parser.parse(@input)
19
+
20
+ meta = extract_meta!(tree)
21
+
22
+ output = [
23
+ collect(meta).flatten.compact.join.strip,
24
+ collect(tree).flatten.compact.join.strip
25
+ ].reject(&:empty?).join("\n\n")
26
+
27
+ "#{output}\n"
28
+ end
29
+
30
+ def extract_meta!(tree, meta = [])
31
+ tree.each do |node|
32
+ next unless node.is_a?(Hash)
33
+
34
+ if node[:type] == :command
35
+ first_token = node[:tokens].reject(&:any_space?).first
36
+ next unless first_token.meta?
37
+
38
+ meta << node
39
+ meta << Token::Newline.new("\n")
40
+ tree.delete(node)
41
+ elsif node[:type] == :group
42
+ extract_meta!(node[:children], meta)
43
+ end
44
+ end
45
+
46
+ meta
47
+ end
48
+
49
+ def collect(tree, output = [])
50
+ query = Query.new(tree)
51
+
52
+ tree.each_with_index do |node, _index|
53
+ # Whenever we hit a non-newline, flush buffered newlines (first)
54
+ # and then process the node. Newlines are clamped to max 2.
55
+ unless newline?(node)
56
+ newlines = buffered_newlines.clamp(0, 2)
57
+ output << ("\n" * newlines)
58
+ self.buffered_newlines = 0
59
+ end
60
+
61
+ case node
62
+ when Token::Keyword
63
+ output << node.value unless node.keyword?("end")
64
+ when Token::Identifier
65
+ output << if node.group?
66
+ "\n#{node.value}"
67
+ else
68
+ node.value
69
+ end
70
+ when Token::Duration, Token::Comment
71
+ output << node.value
72
+ when Token::Newline
73
+ self.buffered_newlines += 1
74
+ when Token::Number
75
+ value = [node.value.to_s]
76
+ value << "s" if query.previous_identifier?(node, /Sleep|Wait/)
77
+ output << value.join
78
+ when Token::String
79
+ output << normalize_string(node)
80
+ when Token::MultilineString
81
+ output << %["""\n#{node.value.chomp}\n"""\n]
82
+ when Token::Space
83
+ output << " "
84
+ when Token::LeadingSpace
85
+ next if output.empty?
86
+ next unless within_group?
87
+
88
+ output << " "
89
+ when Hash
90
+ output = [output.flatten.compact.join]
91
+ output << collect(node[:tokens])
92
+
93
+ if node[:type] == :group
94
+ within_group do
95
+ output << "\n "
96
+ output << collect(node[:children]).join.strip
97
+ output << "\nend"
98
+ self.buffered_newlines += 1
99
+ end
100
+ end
101
+ else
102
+ output << node.raw
103
+ end
104
+ end
105
+
106
+ output
107
+ end
108
+
109
+ def within_group?
110
+ @within_group
111
+ end
112
+
113
+ def within_group
114
+ @within_group = true
115
+ yield
116
+ ensure
117
+ @within_group = false
118
+ end
119
+
120
+ def normalize_string(token)
121
+ %["#{token.value}"]
122
+ end
123
+
124
+ class Query
125
+ attr_reader :nodes
126
+
127
+ def initialize(nodes)
128
+ @nodes = nodes
129
+ end
130
+
131
+ def previous_identifier?(current_node, value)
132
+ index = nodes.index(current_node)
133
+ return false if index.nil? || index.zero?
134
+
135
+ previous_node = nodes[index - 2]
136
+
137
+ previous_node.is_a?(Token::Identifier) &&
138
+ previous_node.value.match?(value)
139
+ end
140
+ end
141
+ end
142
+ end
@@ -2,10 +2,6 @@
2
2
 
3
3
  module DemoTape
4
4
  class Lexer
5
- def self.tokenize(content)
6
- new.tokenize(content)
7
- end
8
-
9
5
  attr_reader :line_map
10
6
 
11
7
  def tokenize(content)
@@ -39,7 +35,7 @@ module DemoTape
39
35
  content: '"""',
40
36
  raw: '"""..."""'
41
37
  }
42
- tokens << [:STRING, multiline_text]
38
+ tokens << [:MULTILINE_STRING, multiline_text]
43
39
  token_index += 1
44
40
 
45
41
  # Add NEWLINE token after multiline string
@@ -59,25 +55,99 @@ module DemoTape
59
55
  next
60
56
  end
61
57
 
62
- # Skip empty lines and comments (but only when not in multiline)
63
- next if stripped_line.empty? || stripped_line.start_with?("#")
58
+ # Handle comments - emit as tokens
59
+ if stripped_line.start_with?("#")
60
+ # Emit LEADING_SPACE if there's any
61
+ leading_space = original_line[/^[ \t]*/]
62
+ if leading_space && !leading_space.empty?
63
+ @line_map[token_index] = {
64
+ line: @line_number,
65
+ column: 1,
66
+ content: original_line.chomp,
67
+ raw: leading_space
68
+ }
69
+ tokens << [:LEADING_SPACE, leading_space]
70
+ token_index += 1
71
+ end
72
+
73
+ # Emit COMMENT token
74
+ @line_map[token_index] = {
75
+ line: @line_number,
76
+ column: leading_space.length + 1,
77
+ content: original_line.chomp,
78
+ raw: stripped_line
79
+ }
80
+ tokens << [:COMMENT, stripped_line]
81
+ token_index += 1
82
+
83
+ @line_map[token_index] = {
84
+ line: @line_number,
85
+ column: 1,
86
+ content: original_line.chomp
87
+ }
88
+ tokens << [:NEWLINE, "\n"]
89
+ token_index += 1
90
+ next
91
+ end
92
+
93
+ # Handle empty lines - emit just NEWLINE
94
+ if stripped_line.empty?
95
+ @line_map[token_index] = {
96
+ line: @line_number,
97
+ column: 1,
98
+ content: ""
99
+ }
100
+ tokens << [:NEWLINE, "\n"]
101
+ token_index += 1
102
+ next
103
+ end
104
+
105
+ # Tokenize the original line (preserving all spaces)
106
+ line_tokens = tokenize_line(line.chomp)
107
+
108
+ # Convert first SPACE token to LEADING_SPACE if it exists
109
+ if line_tokens.any? && line_tokens[0][0] == :SPACE
110
+ line_tokens[0][0] = :LEADING_SPACE
111
+ end
112
+
113
+ # Special case: if the line is just whitespace followed by 'end', skip
114
+ # the LEADING_SPACE. This helps the parser avoid shift/reduce conflicts
115
+ # with group_body rules
116
+ if line_tokens.length >= 2 &&
117
+ line_tokens[0][0] == :LEADING_SPACE &&
118
+ line_tokens[1][0] == :END &&
119
+ (line_tokens.length == 2 || line_tokens[2][0] == :SPACE)
120
+ # Skip the leading space token for 'end' keyword
121
+ line_tokens.shift
122
+ end
64
123
 
65
- line_tokens = tokenize_line(stripped_line)
124
+ # Convert last SPACE token (before end) to TRAILING_SPACE if it exists
125
+ last_non_newline = line_tokens.length - 1
126
+
127
+ while last_non_newline >= 0 &&
128
+ line_tokens[last_non_newline][0] == :NEWLINE
129
+ last_non_newline -= 1
130
+ end
131
+
132
+ if last_non_newline >= 0 && line_tokens[last_non_newline][0] == :SPACE
133
+ line_tokens[last_non_newline][0] = :TRAILING_SPACE
134
+ end
66
135
 
67
136
  # Check if any token starts a multiline string
68
- multiline_idx = line_tokens.find_index do |t|
137
+ multiline_index = line_tokens.find_index do |t|
69
138
  t[0] == :TRIPLE_QUOTE_START
70
139
  end
71
- if multiline_idx
140
+
141
+ if multiline_index
72
142
  # Process tokens before the triple quote
73
- line_tokens[0...multiline_idx].each do |token|
143
+ line_tokens[0...multiline_index].each do |token|
74
144
  col = token[2] || 1
75
145
  raw = token[3] || token[1].to_s
76
146
 
77
147
  @line_map[token_index] = {
78
148
  line: @line_number,
79
149
  column: col,
80
- content: original_line.strip,
150
+ content: original_line.chomp,
81
151
  raw: raw
82
152
  }
83
153
 
@@ -86,12 +156,12 @@ module DemoTape
86
156
  token_index += 1
87
157
  end
88
158
 
89
- tokens.concat(line_tokens[0...multiline_idx])
159
+ tokens.concat(line_tokens[0...multiline_index])
90
160
 
91
161
  # Start multiline mode
92
162
  in_multiline = true
93
163
  multiline_start_line = @line_number
94
- multiline_start_col = line_tokens[multiline_idx][2] || 1
164
+ multiline_start_col = line_tokens[multiline_index][2] || 1
95
165
  multiline_content = []
96
166
  next
97
167
  end
@@ -104,7 +174,7 @@ module DemoTape
104
174
  @line_map[token_index] = {
105
175
  line: @line_number,
106
176
  column: col,
107
- content: original_line.strip,
177
+ content: original_line.chomp,
108
178
  raw: raw
109
179
  }
110
180
 
@@ -119,7 +189,7 @@ module DemoTape
119
189
  @line_map[token_index] = {
120
190
  line: @line_number,
121
191
  column: 1,
122
- content: original_line.strip
192
+ content: original_line.chomp
123
193
  }
124
194
 
125
195
  tokens << [:NEWLINE, "\n"]
@@ -157,22 +227,36 @@ module DemoTape
157
227
 
158
228
  # Regex pattern (between forward slashes)
159
229
  elsif scanner.scan(%r{/((?:[^/\\]|\\.)*)/})
160
- tokens << [:REGEX, scanner[1], col, scanner[0]]
230
+ result = begin
231
+ {pattern: Regexp.new(scanner[1])}
232
+ rescue RegexpError => error
233
+ {error: error.message.gsub("/#{scanner[1]}/", "").strip.chomp(":")}
234
+ end
235
+
236
+ tokens << [:REGEX, result, col, scanner[0]]
161
237
 
162
238
  # Duration (number + time unit - any identifier)
163
- elsif scanner.scan(/(\d+(?:\.\d+)?|\.\d+)([a-zA-Z]+)/)
239
+ elsif scanner.scan(/(\d+(?:\.\d+)?|-?\.\d+)([a-zA-Z]+)/)
240
+ number_part = scanner[1]
241
+ unit_part = scanner[2]
242
+ full_match = scanner[0]
164
243
  tokens << [
165
- :NUMBER,
166
- scanner[1].include?(".") ? scanner[1].to_f : scanner[1].to_i,
244
+ :DURATION,
245
+ {
246
+ number: if number_part.include?(".")
247
+ number_part.to_f
248
+ else
249
+ number_part.to_i
250
+ end,
251
+ unit: unit_part,
252
+ raw: full_match
253
+ },
167
254
  col,
168
- scanner[1]
255
+ full_match
169
256
  ]
170
- # TIME_UNIT starts after the number
171
- time_unit_col = col + scanner[1].length
172
- tokens << [:TIME_UNIT, scanner[2], time_unit_col, scanner[2]]
173
257
 
174
- # Number
175
- elsif scanner.scan(/\d+(?:\.\d+)?|\.\d+/)
258
+ # Number (including negative numbers)
259
+ elsif scanner.scan(/-?(?:\d+(?:\.\d+)?|\.\d+)/)
176
260
  value = scanner[0].include?(".") ? scanner[0].to_f : scanner[0].to_i
177
261
  tokens << [:NUMBER, value, col, scanner[0]]
178
262
 
@@ -190,14 +274,14 @@ module DemoTape
190
274
 
191
275
  # Identifier (including dot notation for nested options)
192
276
  elsif scanner.scan(/[a-zA-Z_][\w.]*/)
193
- tokens << [:IDENTIFIER, scanner[0], col, scanner[0]]
194
-
195
- # Word
196
- elsif scanner.scan(/\S+/)
197
- tokens << [:WORD, scanner[0], col, scanner[0]]
198
-
199
- else
200
- break
277
+ # Check for keywords
278
+ value = scanner[0]
279
+ token_type = case value
280
+ when "do" then :DO
281
+ when "end" then :END
282
+ else :IDENTIFIER
283
+ end
284
+ tokens << [token_type, value, col, value]
201
285
  end
202
286
  end
203
287