gloo 6.3.1 → 6.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +13 -0
  3. data/docs/application.md +9 -0
  4. data/docs/language_objects.md +78 -0
  5. data/docs/language_scripting.md +41 -0
  6. data/docs/objects.md +1 -1
  7. data/docs/verbs.md +36 -2
  8. data/lib/VERSION +1 -1
  9. data/lib/VERSION_NOTES +13 -0
  10. data/lib/gloo/app/engine.rb +5 -1
  11. data/lib/gloo/app/log.rb +5 -5
  12. data/lib/gloo/app/settings.rb +4 -1
  13. data/lib/gloo/core/obj.rb +60 -1
  14. data/lib/gloo/core/parser.rb +30 -1
  15. data/lib/gloo/core/tokens.rb +44 -18
  16. data/lib/gloo/expr/l_string.rb +8 -4
  17. data/lib/gloo/objs/basic/script.rb +28 -2
  18. data/lib/gloo/objs/web/json.rb +4 -1
  19. data/lib/gloo/persist/comment_buffer.rb +58 -0
  20. data/lib/gloo/persist/declaration_ledger.rb +61 -0
  21. data/lib/gloo/persist/disc_mech.rb +15 -2
  22. data/lib/gloo/persist/file_loader.rb +208 -102
  23. data/lib/gloo/persist/file_saver.rb +251 -6
  24. data/lib/gloo/persist/file_storage.rb +10 -4
  25. data/lib/gloo/persist/indent_stack.rb +90 -0
  26. data/lib/gloo/persist/line_splitter.rb +18 -3
  27. data/lib/gloo/persist/persist_man.rb +193 -26
  28. data/lib/gloo/persist/script_body_collector.rb +102 -0
  29. data/lib/gloo/persist/shorthand_expander.rb +51 -0
  30. data/lib/gloo/persist/source/blank_node.rb +27 -0
  31. data/lib/gloo/persist/source/comment_node.rb +27 -0
  32. data/lib/gloo/persist/source/directive_node.rb +26 -0
  33. data/lib/gloo/persist/source/obj_node.rb +91 -0
  34. data/lib/gloo/persist/source/source_doc.rb +41 -0
  35. data/lib/gloo/verbs/list.rb +24 -2
  36. data/lib/gloo/verbs/load.rb +9 -1
  37. data/lib/gloo/verbs/reload.rb +3 -0
  38. data/lib/gloo/verbs/save.rb +57 -10
  39. data/test.gloo/ctrl/each.test.gloo +28 -30
  40. data/test.gloo/dt/date.test.gloo +69 -71
  41. data/test.gloo/dt/datetime.test.gloo +55 -57
  42. data/test.gloo/dt/time.test.gloo +70 -72
  43. data/test.gloo/lang/continuation.test.gloo +25 -27
  44. data/test.gloo/lang/convert.test.gloo +50 -52
  45. data/test.gloo/lang/dt_comparisons.test.gloo +87 -89
  46. data/test.gloo/lang/exceptions.test.gloo +42 -44
  47. data/test.gloo/lang/gloo_sys.test.gloo +100 -102
  48. data/test.gloo/lang/here.test.gloo +32 -34
  49. data/test.gloo/lang/it.test.gloo +27 -29
  50. data/test.gloo/lang/literal.test.gloo +55 -57
  51. data/test.gloo/lang/load_lib_directive.test.gloo +18 -20
  52. data/test.gloo/lang/naming.test.gloo +28 -30
  53. data/test.gloo/lang/ops.test.gloo +53 -55
  54. data/test.gloo/lang/shorthand.test.gloo +27 -0
  55. data/test.gloo/math/add.test.gloo +40 -42
  56. data/test.gloo/math/div.test.gloo +23 -25
  57. data/test.gloo/math/mult.test.gloo +23 -25
  58. data/test.gloo/math/sub.test.gloo +23 -25
  59. data/test.gloo/objs/alias.test.gloo +15 -17
  60. data/test.gloo/objs/bool.test.gloo +54 -56
  61. data/test.gloo/objs/can.test.gloo +33 -35
  62. data/test.gloo/objs/cipher.test.gloo +39 -41
  63. data/test.gloo/objs/decimal.test.gloo +27 -29
  64. data/test.gloo/objs/erb.test.gloo +47 -49
  65. data/test.gloo/objs/file.test.gloo +68 -70
  66. data/test.gloo/objs/function.test.gloo +37 -39
  67. data/test.gloo/objs/int.test.gloo +54 -56
  68. data/test.gloo/objs/json.test.gloo +57 -50
  69. data/test.gloo/objs/obj.test.gloo +68 -48
  70. data/test.gloo/objs/outline.test.gloo +48 -50
  71. data/test.gloo/objs/password.test.gloo +34 -36
  72. data/test.gloo/objs/repeat.test.gloo +42 -44
  73. data/test.gloo/objs/script.test.gloo +13 -15
  74. data/test.gloo/objs/string.test.gloo +82 -84
  75. data/test.gloo/objs/text.test.gloo +90 -92
  76. data/test.gloo/objs/untyped.test.gloo +36 -38
  77. data/test.gloo/objs/uri.test.gloo +37 -39
  78. data/test.gloo/string/str.test.gloo +15 -0
  79. data/test.gloo/verbs/break.test.gloo +12 -14
  80. data/test.gloo/verbs/check.test.gloo +22 -24
  81. data/test.gloo/verbs/context.test.gloo +11 -13
  82. data/test.gloo/verbs/create.test.gloo +7 -9
  83. data/test.gloo/verbs/eval.test.gloo +10 -12
  84. data/test.gloo/verbs/exists.test.gloo +43 -45
  85. data/test.gloo/verbs/if.test.gloo +8 -10
  86. data/test.gloo/verbs/invoke.test.gloo +73 -75
  87. data/test.gloo/verbs/list.test.gloo +21 -23
  88. data/test.gloo/verbs/load.test.gloo +27 -21
  89. data/test.gloo/verbs/log.test.gloo +7 -9
  90. data/test.gloo/verbs/move.test.gloo +12 -14
  91. data/test.gloo/verbs/put.test.gloo +10 -12
  92. data/test.gloo/verbs/reload.test.gloo +55 -0
  93. data/test.gloo/verbs/run.test.gloo +8 -10
  94. data/test.gloo/verbs/save.test.gloo +111 -0
  95. data/test.gloo/verbs/show.test.gloo +26 -28
  96. data/test.gloo/verbs/tell.test.gloo +8 -10
  97. data/test.gloo/verbs/throw.test.gloo +31 -33
  98. data/test.gloo/verbs/unless.test.gloo +13 -15
  99. data/test.gloo/verbs/unload.test.gloo +10 -12
  100. metadata +14 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 517b0f6358d83303a3e6e7536b9a16abd4a71cf9ef1649faf6abd21443fe652d
4
- data.tar.gz: b43125b452393abea79914d54a2f73e7d77791891ed3e14cde8ad51e2bdb44ce
3
+ metadata.gz: c586639c9bf946ff078f68d28ac3c8fd676ae3e040ee181d7a65eb63a648feda
4
+ data.tar.gz: defbe6d74c65dfd5df9abbf3e917cdd3acb950c8d890dfab53cc37b158118772
5
5
  SHA512:
6
- metadata.gz: cb9e73f9e4d4281d3ce190258bf744150176ac09c2f0e980b920f185a583015fdd3a5b64180fa0c07b6970f04f8c0eba49840495b35291846bd86e0e57dcf3d7
7
- data.tar.gz: ac5e8e247415a01107fee4cc12358559eafc698ac2e5c1ffdd5ee6cc01e6c7cca54d5b9eb44fdc589a8ce692fd36367103125571d5cff038a10b6cabe27ca4ac
6
+ metadata.gz: 578cac3fcd5cf809fd00db651563b3a0ff8a2eef70796f9ccd4742b1086f6d4e9045ffe77db73a307be5972a46d9262f9b43a59c2fe215ecacae900fd450f478
7
+ data.tar.gz: 5248581d7000865535b0419cd6772e72de8c5ac1a09fe8668345e2ca2703ffcf28756fdd70b8b7e99f2fe04a88c64fbe5a9bb4ad7e204827e689eef05011ac4f
data/CLAUDE.md CHANGED
@@ -58,6 +58,19 @@ Base classes: inherit from `GlooTest` (defined in `test/base_test.rb`).
58
58
  ### Gloo integration tests — `test.gloo/`
59
59
  Written in gloo itself. Each file contains `[test]` objects with `on_test` scripts using `assert` and `refute`.
60
60
 
61
+ Run with `ruby lib/run.rb --test --quiet <paths...>` — **not** `bundle exec` (the Gemfile doesn't carry `gloo-test` / `gloo-md` / the other core-lib gems, so `[test]` and `load lib` fail under bundler; plain `ruby` uses the system gem env where they're installed).
62
+
63
+ - Interpreter suite only:
64
+ `ruby ~/dev/gloo/lib/run.rb --test --quiet ~/dev/gloo/test.gloo/`
65
+ - Interpreter **plus** every extension and core library:
66
+ ```
67
+ ruby ~/dev/gloo/lib/run.rb --test --quiet \
68
+ ~/dev/gloo/test.gloo/ \
69
+ ~/gloo/extensions/{alert,beep,git,ruby,slack,stats,teams}/test/ \
70
+ ~/dev/gloo_core_libraries/gloo-{beep,cli,db,email,md,mysql,pg,sqlite,test,web,yaml}/test/
71
+ ```
72
+ (brace expansion is shell sugar for the full explicit path list)
73
+
61
74
  ```
62
75
  test.gloo/
63
76
  ├── basic.test.gloo
data/docs/application.md CHANGED
@@ -30,6 +30,8 @@ gloo [global option] [file]
30
30
 
31
31
  Running gloo with a file specified will run that file. Once that file is done, gloo will quit. However, by specifying `--cli`, once the file has finished, gloo will remain open in CLI mode.
32
32
 
33
+ If a file named on the command line can't be found (a typo, the wrong folder, a missing `.gloo` extension on a full path), gloo prints `File not found: {name}` to stderr and exits with a non-zero status. Any other load or run error also exits non-zero, so a shell script calling gloo can tell a failed run from a clean one.
34
+
33
35
  When specifying a file there are a couple ways to reference the gloo file to open:
34
36
 
35
37
  - By Path
@@ -101,6 +103,13 @@ gloo:
101
103
  #
102
104
  list_levels: 3
103
105
 
106
+ #
107
+ # When listing the object tree, also show each object's doc (the
108
+ # comment declared immediately above it in its source file), if it
109
+ # has one.
110
+ #
111
+ list_docs: false
112
+
104
113
  #
105
114
  # Show debug statements in the log?
106
115
  #
@@ -3,6 +3,7 @@
3
3
  **Contents**
4
4
 
5
5
  - Object Naming
6
+ - Nested Container Shorthand
6
7
  - Keywords
7
8
  - Literals
8
9
  - Value Conversion
@@ -39,6 +40,76 @@ naming [container] :
39
40
 
40
41
  See also: Pathname.
41
42
 
43
+ ## Nested Container Shorthand
44
+
45
+ A declaration whose name is a dotted path creates a container for each
46
+ prefix segment, then declares the real object under the last one. These
47
+ two files are equivalent:
48
+
49
+ ```gloo
50
+ page [container] :
51
+ core [container] :
52
+ users [container] :
53
+ list [container] :
54
+ title [string] : Users
55
+ ```
56
+
57
+ ```gloo
58
+ page.core.users.list [container] :
59
+ title [string] : Users
60
+ ```
61
+
62
+ - Each prefix segment (`page`, `core`, `users`) becomes a container,
63
+ unless a sibling object of that name already exists — in which case
64
+ that object is used as-is.
65
+ - The shorthand works at any indent level; the prefix is resolved
66
+ relative to the current parent.
67
+ - Several shorthand lines can share a prefix — `page.core.users [...]`
68
+ and `page.core.settings [...]` both reuse the same `page` and
69
+ `page.core` containers.
70
+ - Indented lines below a shorthand declaration are children of the last
71
+ segment (`list` above), not of any prefix container.
72
+
73
+ ## Documenting an Object
74
+
75
+ A contiguous run of whole-line comments immediately above a declaration,
76
+ at the same indent and with no blank line in between, is that object's
77
+ **doc** — read it back at run time with `tell {obj} to doc` or
78
+ `check {obj} for doc` (the cleaned text lands in `it`, like any other
79
+ value-returning message):
80
+
81
+ ```gloo
82
+ #
83
+ # The user's display name. Empty until they set it in preferences.
84
+ #
85
+ name [string] :
86
+ ```
87
+
88
+ ```gloo
89
+ > check name for doc
90
+ > show it
91
+
92
+ The user's display name. Empty until they set it in preferences.
93
+
94
+ ```
95
+
96
+ Each line's own leading whitespace and `#` marker (plus one space after
97
+ it, if there is one) are stripped, and the result is dedented to its
98
+ shallowest line — but otherwise the full block is kept exactly as
99
+ written, blank `#` lines above and below the text included. An object
100
+ with no leading comment — including anything created at run time rather
101
+ than loaded from a file — has a blank doc (`''`).
102
+
103
+ A comment separated from the declaration by a blank line, or at a
104
+ different indent, is not associated with it — it's kept as its own
105
+ floating comment in the file instead. When a name is declared in more
106
+ than one loaded file, the first non-empty doc wins (same rule as the
107
+ first value).
108
+
109
+ `list` can show every documented object in a listed tree: turn on the
110
+ `list_docs` setting (off by default) — see `list` and `settings` in the
111
+ in-app help.
112
+
42
113
  ## Keywords
43
114
 
44
115
  Gloo doesn't reserve words the way many languages do. A verb keyword like `put` or an object type name like `string` can also be used as an object name — there's no parser conflict, because verbs are only looked up as the first word of a statement, and object type names are only looked up where a type is expected (inside the `[ ]` on a declaration). Everywhere else, the word is just a pathname segment (see Object Naming, above).
@@ -60,6 +131,11 @@ The following rules apply to literal values:
60
131
 
61
132
  - Strings
62
133
  - Can be delimited by single or double quotes. (`"` or `'`)
134
+ - A quote of the other kind inside needs no escaping —
135
+ `'{"x":1}'` is the string `{"x":1}`, which is the usual way to
136
+ write a JSON literal.
137
+ - A quote of the same kind inside is escaped with a backslash —
138
+ `"say \"hi\""` is the string `say "hi"`.
63
139
  - Numbers
64
140
  - Integer and decimal numbers need no delimiters.
65
141
  - To refer to a decimal with no fractional value, include `.0` to indicate a decimal value.
@@ -89,6 +165,8 @@ The following rules apply to literal values:
89
165
  show ^.s
90
166
  put "You're Awesome!" into ^.s
91
167
  show ^.s
168
+ put '{"lang":"gloo"}' into ^.s
169
+ show ^.s
92
170
 
93
171
  # Number literals
94
172
  put 1 into ^.i
@@ -3,6 +3,7 @@
3
3
  **Contents**
4
4
 
5
5
  - Gloo Script Files
6
+ - Comments
6
7
  - Gloo Constants
7
8
  - Line Continuation
8
9
 
@@ -26,6 +27,46 @@ hello [can] :
26
27
  on_load [script] : show 'hello world'
27
28
  ```
28
29
 
30
+ ## Comments
31
+
32
+ A line whose first non-blank character is `#` is a comment. Whole-line comments
33
+ can appear anywhere — between objects, inside a container, or inside a script
34
+ body — and are ignored when the file runs.
35
+
36
+ ```gloo
37
+ #
38
+ # A whole-line comment.
39
+ #
40
+ demo [can] :
41
+ # a comment inside the container
42
+ msg [string] : hello
43
+ on_load [script] :
44
+ # a comment inside the script body
45
+ show demo.msg
46
+ ```
47
+
48
+ A statement line may also end with an inline `# ...` comment. It is stripped
49
+ before the statement runs, so it has no effect on execution:
50
+
51
+ ```gloo
52
+ on_load [script] :
53
+ show 3 + 4 # prints 7
54
+ check demo.msg for blank?
55
+ show it # prints false
56
+ ```
57
+
58
+ Two things are *not* treated as inline comments, and are left alone:
59
+
60
+ - a `#` inside a quoted string — `show 'a # b'`
61
+ - a `#` with no space before it — a URL fragment such as
62
+ `http://example.com/page#section`
63
+
64
+ An inline comment on an object declaration is also left alone — everything after
65
+ the `:` is the object's value, so `note [string] : see item # 5` stores the
66
+ string `see item # 5`.
67
+
68
+ ---
69
+
29
70
  ## Gloo Constants
30
71
 
31
72
  There is no gloo language construct for constants. They are simply objects. But by convention, constants are named in all caps. They might be in a container or at the root object level.
data/docs/objects.md CHANGED
@@ -4,7 +4,7 @@ Everything in gloo is an object. Strings, numbers, containers, scripts, function
4
4
 
5
5
  Gloo ships with a large set of built-in object types, and core libraries and extensions can add more. This page doesn't try to cover them all — it walks through three common ones to get a feel for how objects work. For the complete list of object types, and every message each one supports, use the in-app help: enter `help` (or `?`), then `objects` to list them all, or `object {name}` for detail on one (see Application, Help).
6
6
 
7
- An object's **type declares which messages it can receive** — `up` and `trim` for a `string`, `inc` for an `integer`, `run` for a `script`. An object doesn't have to have a type: with none it is `untyped` (short `any`), declared with a bare colon (`slot :`), with `[any]`, or by `create` with no `as`. An untyped object still takes the messages every object understands (`blank?`, `contains?`, `responds_to?`, `reload`, `unload`) — just not the type-specific ones. Untyped is the right choice when you only need to hold, compare, or show a value: a generic result slot, a config value passed straight through, or a field whose kind of value changes over its life.
7
+ An object's **type declares which messages it can receive** — `up` and `trim` for a `string`, `inc` for an `integer`, `run` for a `script`. An object doesn't have to have a type: with none it is `untyped` (short `any`), declared with a bare colon (`slot :`), with `[any]`, or by `create` with no `as`. An untyped object still takes the messages every object understands (`blank?`, `contains?`, `responds_to?`, `doc`, `reload`, `unload`) — just not the type-specific ones. Untyped is the right choice when you only need to hold, compare, or show a value: a generic result slot, a config value passed straight through, or a field whose kind of value changes over its life.
8
8
 
9
9
  **Contents**
10
10
 
data/docs/verbs.md CHANGED
@@ -9,8 +9,9 @@ Verbs aren't just for scripts, though. They're also the interactive language of
9
9
  - Run
10
10
  - Tell
11
11
  - Put
12
+ - Load & Save
12
13
 
13
- This page walks through three of the most commonly used verbs to get a feel for how they work together. For the complete list of verbs, their full syntax, and every error they can raise, use the in-app help: enter `help` (or `?`), then `verbs` to list them all, or `verb {name}` for detail on one (see Application, Help).
14
+ This page walks through the most commonly used verbs to get a feel for how they work together. For the complete list of verbs, their full syntax, and every error they can raise, use the in-app help: enter `help` (or `?`), then `verbs` to list them all, or `verb {name}` for detail on one (see Application, Help).
14
15
 
15
16
  ## Run
16
17
 
@@ -50,6 +51,13 @@ tell {path.to.object} to {message}
50
51
  > if it then show 'it does'
51
52
  ```
52
53
 
54
+ `doc` is one such message every object responds to: it returns the comment block declared immediately above it in its source file, cleaned up (see Language, Objects — Documenting an Object).
55
+
56
+ ```gloo
57
+ > tell my.obj to doc
58
+ > show it
59
+ ```
60
+
53
61
  ## Put
54
62
 
55
63
  `put` evaluates an expression and stores the result in an object.
@@ -67,6 +75,32 @@ put {expression} into {dst.path}
67
75
 
68
76
  `it` also picks up the result of the evaluation, same as with other verbs — see It.
69
77
 
78
+ ## Load & Save
79
+
80
+ `load` reads a `.gloo` file into the heap and runs its `on_load` script. Give a path relative to the project folder (no extension needed) or a full path (extension required); `*` in place of a file name loads every `.gloo` file in a folder.
81
+
82
+ ```gloo
83
+ > load my/project/config
84
+ > load my/app/*
85
+ > load ~/.my_app/settings.gloo
86
+ ```
87
+
88
+ If the name can't be resolved to a file, `load` reports `File not found: {name}` — it fires `on_error` and, when the file was named on the `gloo` command line, prints the message to stderr and exits non-zero. A bad or missing file never fails silently.
89
+
90
+ `save` writes loaded objects back to their files. With no argument it saves every open file; with an object it saves the file (or files) that object's tree came from; with `to {path}` it saves to a new file and remembers the mapping.
91
+
92
+ ```gloo
93
+ > save # every open file
94
+ > save config # just config's file
95
+ > save config to backups/config
96
+ ```
97
+
98
+ A save is a **rewrite, not a regeneration**: comments, blank lines, and the original spacing are kept, and only the values you actually changed are re-written. A declaration you never touched comes back byte-for-byte.
99
+
100
+ Several files can contribute to one container — declare `app [container] :` in each and add different children. They merge in the heap, and each file's save only rewrites its own declarations. If two files declare the *same* object with different values, the first one loaded wins and `load` logs a warning.
101
+
102
+ An object can also save itself: `tell config to save`.
103
+
70
104
  ---
71
105
 
72
- `run`, `tell`, and `put` cover a lot of ground on their own, but there are 26 more verbs — `show`, `if`, `create`, `each`, `check`, and so on — all documented in-app. Enter `help` (or `?`), then `verbs` to browse them. (This page itself is also viewable in-app: `help> doc verbs`.)
106
+ `run`, `tell`, `put`, and `load` / `save` cover a lot of ground, but there are more than two dozen other verbs — `show`, `if`, `create`, `each`, `check`, `reload`, `unload`, and so on — all documented in-app. Enter `help` (or `?`), then `verbs` to browse them. (This page itself is also viewable in-app: `help> doc verbs`.)
data/lib/VERSION CHANGED
@@ -1 +1 @@
1
- 6.3.1
1
+ 6.5.0
data/lib/VERSION_NOTES CHANGED
@@ -1,3 +1,16 @@
1
+ 6.5.0 - 2026.09.11
2
+ - Adds option to show comments from the list verb.
3
+ - Adds message to get doc for an object.
4
+ - Report error if the file to load can't be found.
5
+ - Fixes issue with multisegment root level object.
6
+
7
+
8
+ 6.4.0 - 2026.09.06
9
+ - Persistance refresh with updates for loading and saving
10
+ - Nested container shortcut syntax
11
+ - Fixes a number of comment and quoting issues
12
+
13
+
1
14
  6.3.1 - 2026.09.01
2
15
  - Fixes issue with MD in a text block (# ignored as comments)
3
16
 
@@ -134,11 +134,15 @@ module Gloo
134
134
 
135
135
  #
136
136
  # Run files specified on the CLI.
137
- # Then quit.
137
+ # Then quit, with a non-zero exit code if any file failed to
138
+ # load (missing, unresolvable, or errored on load) so that a
139
+ # calling shell script can tell a bad run from a good one.
138
140
  #
139
141
  def run_files
140
142
  load_files
143
+ failed = error?
141
144
  quit
145
+ exit( 1 ) if failed
142
146
  end
143
147
 
144
148
  #
data/lib/gloo/app/log.rb CHANGED
@@ -167,7 +167,7 @@ module Gloo
167
167
  #
168
168
  # Write an error message to the log and set the error
169
169
  # in the engine's data heap.
170
- # Also write to the console unless quiet.
170
+ # Also write to the console (on stderr) unless quiet.
171
171
  #
172
172
  def error( msg, ex = nil, engine = nil )
173
173
  engine&.heap&.error&.set_to( msg ) if engine
@@ -176,11 +176,11 @@ module Gloo
176
176
  if ex
177
177
  @error.error ex.message
178
178
  @error.error ex.backtrace
179
- puts @theme.error( msg ) unless @quiet
180
- puts @theme.error( ex.message ) unless @quiet
181
- puts ex.backtrace unless @quiet
179
+ $stderr.puts @theme.error( msg ) unless @quiet
180
+ $stderr.puts @theme.error( ex.message ) unless @quiet
181
+ $stderr.puts ex.backtrace unless @quiet
182
182
  else
183
- puts @theme.error( msg ) unless @quiet
183
+ $stderr.puts @theme.error( msg ) unless @quiet
184
184
  end
185
185
  end
186
186
 
@@ -15,7 +15,7 @@ module Gloo
15
15
 
16
16
  attr_reader :user_root, :log_path,
17
17
  :config_path, :project_path, :ext_path,
18
- :start_with, :list_indent, :list_levels, :tmp_path,
18
+ :start_with, :list_indent, :list_levels, :list_docs, :tmp_path,
19
19
  :debug_path, :debug, :theme
20
20
 
21
21
  #
@@ -41,6 +41,7 @@ module Gloo
41
41
  puts theme.accent( ' Startup with: ' ) + theme.emphasis( @start_with.to_s )
42
42
  puts theme.accent( ' Indent in Listing: ' ) + theme.emphasis( @list_indent.to_s )
43
43
  puts theme.accent( ' List Levels: ' ) + theme.emphasis( @list_levels.to_s )
44
+ puts theme.accent( ' List Docs: ' ) + theme.emphasis( @list_docs.to_s )
44
45
  puts theme.accent( ' Debug? ' ) + theme.emphasis( @debug.to_s )
45
46
  puts theme.accent( ' Theme: ' ) + theme.emphasis( @theme )
46
47
  puts theme.accent( ' Screen Lines: ' ) + theme.emphasis( Gloo::App::Settings.lines( @engine ).to_s )
@@ -158,6 +159,7 @@ module Gloo
158
159
  @start_with = settings[ 'gloo' ][ 'start_with' ]
159
160
  @list_indent = settings[ 'gloo' ][ 'list_indent' ]
160
161
  @list_levels = settings[ 'gloo' ][ 'list_levels' ]
162
+ @list_docs = settings[ 'gloo' ][ 'list_docs' ] == true
161
163
 
162
164
  @debug = settings[ 'gloo' ][ 'debug' ]
163
165
 
@@ -192,6 +194,7 @@ module Gloo
192
194
  start_with:
193
195
  list_indent: 2
194
196
  list_levels: 3
197
+ list_docs: false
195
198
  debug: false
196
199
  theme: #{DEFAULT_THEME}
197
200
  TEXT
data/lib/gloo/core/obj.rb CHANGED
@@ -12,12 +12,21 @@ module Gloo
12
12
  attr_accessor :value
13
13
  attr_reader :children, :parent
14
14
 
15
+ # The object's documentation: the cleaned comment block declared
16
+ # immediately above it in its source file (Source::ObjNode#doc),
17
+ # copied on at load time by FileLoader. '' for an object with no
18
+ # such comment, or one created at run time. Loader-set only --
19
+ # there's no verb/message path to change it, so it can't drift
20
+ # from what the file actually says.
21
+ attr_accessor :doc
22
+
15
23
  #
16
24
  # Set up the object.
17
25
  #
18
26
  def initialize( engine )
19
27
  @engine = engine
20
28
  @value = ''
29
+ @doc = ''
21
30
  @children = []
22
31
  @parent = nil
23
32
  end
@@ -118,6 +127,29 @@ module Gloo
118
127
  return false
119
128
  end
120
129
 
130
+ #
131
+ # Serialize this object's value for saving to a file.
132
+ # Returns the text that follows "name [type]" on the declaration
133
+ # line -- including the leading ' :' and, for a multi-line string
134
+ # value, the additional indented lines and the closing END marker.
135
+ # indent is the indentation level of the declaration line itself.
136
+ #
137
+ def serialize_value( indent )
138
+ return " : #{value_display}" unless value_string? && value.include?( "\n" )
139
+
140
+ return serialize_block_value( indent )
141
+ end
142
+
143
+ #
144
+ # Serialize a multi-line string value as a BEGIN/END block.
145
+ #
146
+ def serialize_block_value( indent )
147
+ t = "\t" * indent
148
+ inner = "\t" * ( indent + 1 )
149
+ body = value.split( "\n" ).map { |line| "#{inner}#{line}" }.join( "\n" )
150
+ return " : BEGIN\n#{body}\n#{t}END"
151
+ end
152
+
121
153
  #
122
154
  # Is the value a String?
123
155
  #
@@ -306,7 +338,7 @@ module Gloo
306
338
  # Get a list of message names that this object receives.
307
339
  #
308
340
  def self.messages
309
- return %w[reload unload blank? contains? responds_to?]
341
+ return %w[save reload unload blank? contains? responds_to? doc]
310
342
  end
311
343
 
312
344
  #
@@ -356,6 +388,22 @@ module Gloo
356
388
  @engine.persist_man.unload self
357
389
  end
358
390
 
391
+ #
392
+ # Send the object the save message: tell it to save itself,
393
+ # rather than going through the save verb. Same rules as
394
+ # `save {path.to.object}` -- saves every file that owns a
395
+ # declaration in this object's root, or saves fresh to a default
396
+ # path if it isn't mapped to a file yet.
397
+ #
398
+ def msg_save
399
+ if self.root?
400
+ @engine.err 'Cannot save the root object.'
401
+ return
402
+ end
403
+
404
+ @engine.persist_man.save self.pn
405
+ end
406
+
359
407
  #
360
408
  # Send the object the reload message.
361
409
  # Note that this will only work for objects with file assoications.
@@ -406,6 +454,17 @@ module Gloo
406
454
  end
407
455
  end
408
456
 
457
+ #
458
+ # Get this object's documentation -- the comment block declared
459
+ # immediately above it in its source file, cleaned up. Puts the
460
+ # result into 'it' rather than printing, same as other
461
+ # value-returning messages (count, down, blank?, ...).
462
+ #
463
+ def msg_doc
464
+ @engine.heap.it.set_to self.doc
465
+ return self.doc
466
+ end
467
+
409
468
 
410
469
  # ---------------------------------------------------------------------
411
470
  # Render
@@ -21,7 +21,11 @@ module Gloo
21
21
  # Parse a command from the immediate execution context.
22
22
  #
23
23
  def parse_immediate( full_cmd )
24
- # Break the full command into verb and params
24
+ # Drop an inline trailing comment first, then break the
25
+ # command into verb and params.
26
+ full_cmd = strip_comment( full_cmd )
27
+ return nil if full_cmd.strip.empty?
28
+
25
29
  cmd, params = split_params full_cmd
26
30
 
27
31
  # Params are the parenthetical part of the command at the end
@@ -35,6 +39,31 @@ module Gloo
35
39
  return nil
36
40
  end
37
41
 
42
+ #
43
+ # Remove an inline trailing comment: the first '#' that starts a
44
+ # word (at the start of the command, or right after whitespace)
45
+ # and is not inside a quoted string -- and everything after it.
46
+ #
47
+ # Left alone: a '#' inside single or double quotes (show 'a # b'),
48
+ # a '#' glued to preceding text (a URL fragment like
49
+ # http://x?id=1#frag), and a command with no such '#'. Object
50
+ # declaration values in a .gloo file never reach here -- they're
51
+ # split by LineSplitter, not the parser.
52
+ #
53
+ def strip_comment( full_cmd )
54
+ quote = nil
55
+ full_cmd.each_char.with_index do |ch, i|
56
+ if quote
57
+ quote = nil if ch == quote
58
+ elsif Gloo::Core::Tokens::QUOTE_CHARS.include?( ch )
59
+ quote = ch
60
+ elsif ch == '#' && ( i.zero? || full_cmd[ i - 1 ] =~ /\s/ )
61
+ return full_cmd[ 0...i ].rstrip
62
+ end
63
+ end
64
+ return full_cmd
65
+ end
66
+
38
67
  #
39
68
  # If additional params were provided, split them out
40
69
  # from the token list.
@@ -154,8 +154,9 @@ module Gloo
154
154
  # An inline call (invoke( ... ) / ~>( ... )) is checked for
155
155
  # first since it needs to be quote-aware in its own right (a
156
156
  # call's args can include a quoted string) - see
157
- # find_call_range. Falls through to the original quote-then-
158
- # plain-split handling, unchanged, when there's no call.
157
+ # find_call_range. Otherwise the first quoted run (whichever
158
+ # quote char opens first) becomes one token, and the rest is
159
+ # split on spaces.
159
160
  #
160
161
  def tokenize( str )
161
162
  range = find_call_range( str )
@@ -163,27 +164,52 @@ module Gloo
163
164
  tokenize( str[ 0...range.first ] ) if range.first.positive?
164
165
  @tokens << str[ range ]
165
166
  tokenize( str[ range.last + 1..-1 ] ) if range.last + 1 < str.length
166
- elsif str.index( '"' )
167
- i = str.index( '"' )
168
- j = str.index( '"', i + 1 )
169
- j ||= str.length
170
-
171
- tokenize( str[ 0..i - 1 ] ) if i > 1
172
- @tokens << str[ i..j ]
173
- tokenize( str[ j + 1..-1 ] ) if j + 1 < str.length
174
- elsif str.index( "'" )
175
- i = str.index( "'" )
176
- j = str.index( "'", i + 1 )
177
- j ||= str.length
178
-
179
- tokenize( str[ 0..i - 1 ] ) if i > 1
180
- @tokens << str[ i..j ]
181
- tokenize( str[ j + 1..-1 ] ) if j + 1 < str.length
167
+ return
168
+ end
169
+
170
+ qi, qc = first_quote( str )
171
+ if qi
172
+ close = closing_quote( str, qi, qc )
173
+ tokenize( str[ 0...qi ] ) if qi.positive?
174
+ @tokens << str[ qi..close ]
175
+ tokenize( str[ close + 1..-1 ] ) if close + 1 < str.length
182
176
  else
183
177
  str.strip.split( ' ' ).each { |t| @tokens << t }
184
178
  end
185
179
  end
186
180
 
181
+ #
182
+ # Find the first quote character in the string, of either kind
183
+ # -- returns [index, char], or [nil, nil] if there is none. The
184
+ # kind that opens first wins, so a " inside a '...' literal (and
185
+ # vice versa) is treated as ordinary content.
186
+ #
187
+ def first_quote( str )
188
+ found = nil
189
+ QUOTE_CHARS.each do |q|
190
+ i = str.index( q )
191
+ found = [ i, q ] if i && ( found.nil? || i < found[ 0 ] )
192
+ end
193
+ return found || [ nil, nil ]
194
+ end
195
+
196
+ #
197
+ # Index of the quote that closes the one opened at open_index.
198
+ # A backslash-escaped quote (\" or \') does not close the
199
+ # string. Returns str.length if it is never closed.
200
+ #
201
+ def closing_quote( str, open_index, quote_char )
202
+ i = open_index + 1
203
+ while i < str.length
204
+ ch = str[ i ]
205
+ return i if ch == quote_char
206
+
207
+ i += 1
208
+ i += 1 if ch == '\\'
209
+ end
210
+ return str.length
211
+ end
212
+
187
213
  #
188
214
  # Find the char range of the first top-level (not inside a
189
215
  # quoted string) invoke(...)/~>(...) call in str. Returns nil
@@ -31,19 +31,23 @@ module Gloo
31
31
  end
32
32
 
33
33
  #
34
- # Given a string with leading and trailing quotes,
35
- # strip them out.
34
+ # Given a string with leading and trailing quotes, strip them
35
+ # out. A quote of the other kind inside needs nothing done to it
36
+ # ('{"x":1}' -> {"x":1}); an escaped quote of the same kind is
37
+ # unescaped ("say \"hi\"" -> say "hi").
36
38
  #
37
39
  def self.strip_quotes( str )
38
40
  if str.start_with?( '"' )
39
41
  str = str[ 1..-1 ]
40
42
  str = str[ 0..-2 ] if str.end_with?( '"' )
41
- return str
43
+ return str.gsub( '\\"', '"' )
42
44
  elsif str.start_with?( "'" )
43
45
  str = str[ 1..-1 ]
44
46
  str = str[ 0..-2 ] if str.end_with?( "'" )
45
- return str
47
+ return str.gsub( "\\'", "'" )
46
48
  end
49
+
50
+ return str
47
51
  end
48
52
 
49
53
  #