glib-web 4.40.0 → 4.42.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd0efe0c2aa42dbfd2131c020d25f47d543796e865c775073acd3218ad966f4e
4
- data.tar.gz: dc82d410b14230f797865e9ac1d484e21345f809796cf20867e9582de4ed739d
3
+ metadata.gz: 0fd0f52f48d55fef27f49ae004e3f2c59e730963cecaddd6af5cb5fe962b6323
4
+ data.tar.gz: aebd337997b0b656fac64f72a0c53322bd58815bdf2052aef4b15243d1e06e96
5
5
  SHA512:
6
- metadata.gz: d5bebe2d5a905aa416fc6c308ac3f66b36f5b2b700a833828c0e1b582fa94627930b9fab8d5a1cf40f39f727f4b0182492b89f48c51ae91fb2fe37d8f6b20d0e
7
- data.tar.gz: 624b7523faf66fc74b11df5199f17ba580bbc405cdd5f934c56019c9480e12e1bca3f3f055f59c92250179dcd3af5b9d63b5cdcf0f0030abf4a2f5b46b92791a
6
+ metadata.gz: 7dfa32067ee2a2be6dd8095bf8fce2b5da301cff44fdc93f1e80eba7fe9562ae12bae8f7edab9902c16e6ace86af262d028052fb30b644f2bb54957d61302571
7
+ data.tar.gz: e5d068af18a7f8908ae82aac1d5f74225aacdbd58eac2db8b0b78b07f9a5cb4e037d49f32aa11fb2278bc5341ce8c374a89fac4650f5b97f4227eb618606c136
@@ -7,6 +7,7 @@ class Glib::JsonUi::ActionBuilder
7
7
  # @note Does not support markdown formatting. For markdown support, use dialogs/show with a custom body instead.
8
8
  string :message
9
9
 
10
+ action :onOpen
10
11
  action :onLoad
11
12
  action :onClose
12
13
  singleton_array :styleClass, :styleClasses
@@ -20,6 +21,7 @@ class Glib::JsonUi::ActionBuilder
20
21
  length :width
21
22
  bool :closeOnBlur
22
23
  bool :updateExisting
24
+ action :onShow
23
25
  action :onClose
24
26
  singleton_array :styleClass, :styleClasses
25
27
 
@@ -42,6 +44,7 @@ class Glib::JsonUi::ActionBuilder
42
44
  length :height
43
45
  bool :closeOnBlur
44
46
  bool :updateExisting
47
+ action :onOpen
45
48
  action :onClose
46
49
  singleton_array :styleClass, :styleClasses
47
50
  end
@@ -59,7 +59,7 @@ module Glib
59
59
  # menu :leftOuterButtons
60
60
  int :imageSize
61
61
 
62
- panels_builder :content, :header, :footer, :right
62
+ panels_builder :content, :header, :footer, :right, :left
63
63
  end
64
64
 
65
65
  class Editable < Standard
@@ -578,6 +578,7 @@ class Glib::JsonUi::ViewBuilder
578
578
 
579
579
  # To override the default behaviour
580
580
  bool :useChips
581
+ int :maxChipLines
581
582
 
582
583
  def options(value)
583
584
  @_options = value
@@ -848,6 +849,38 @@ class Glib::JsonUi::ViewBuilder
848
849
  # hash :placeholderView
849
850
  # end
850
851
 
852
+ # Supported format https://vuetifyjs.com/en/features/dates/#format-options
853
+ DATE_FORMAT_OPTIONS = %i[
854
+ fullDate
855
+ fullDateWithWeekday
856
+ normalDate
857
+ normalDateWithWeekday
858
+ shortDate
859
+ year
860
+ month
861
+ monthShort
862
+ monthAndYear
863
+ monthAndDate
864
+ weekday
865
+ weekdayShort
866
+ dayOfMonth
867
+ hours12h
868
+ hours24h
869
+ minutes
870
+ seconds
871
+ fullTime
872
+ fullTime12h
873
+ fullTime24h
874
+ fullDateTime
875
+ fullDateTime12h
876
+ fullDateTime24h
877
+ keyboardDate
878
+ keyboardDateTime
879
+ keyboardDateTime12h
880
+ keyboardDateTime24h
881
+ glibDate
882
+ ].freeze
883
+
851
884
  class Date < AbstractField
852
885
  date :min
853
886
  date :max
@@ -859,6 +892,7 @@ class Glib::JsonUi::ViewBuilder
859
892
  bool :buttonTemplate # TODO: Remove
860
893
  hash :template, required: [:type]
861
894
  string :time_zone
895
+ enum :format, options: DATE_FORMAT_OPTIONS
862
896
 
863
897
  # Override
864
898
  def value(value)
@@ -873,6 +907,7 @@ class Glib::JsonUi::ViewBuilder
873
907
  date_time :max
874
908
  bool :clearable
875
909
  string :time_zone
910
+ enum :format, options: DATE_FORMAT_OPTIONS
876
911
 
877
912
  bool :buttonTemplate # TODO: Remove
878
913
  hash :template, required: [:type]
@@ -274,10 +274,17 @@ module Glib
274
274
  @size = value
275
275
  end
276
276
 
277
+ def family(value)
278
+ @family = value.to_s
279
+ end
280
+
277
281
  # Override
278
282
  def created
279
283
  if @name
280
- json.material do
284
+ icon_type = @family == 'fontawesome' ? 'fa' :
285
+ @family == 'custom' ? 'custom' : 'material'
286
+
287
+ json.set!(icon_type) do
281
288
  json.name @name
282
289
  json.size @size if @size
283
290
  end
@@ -5,9 +5,14 @@ end
5
5
 
6
6
  section.rows builder: ->(template) do
7
7
  template.thumbnail title: 'dialogs/alert', onClick: ->(action) do
8
- action.dialogs_alert message: 'This is an alert dialog', onClose: ->(subaction) do
9
- subaction.snackbars_alert message: 'Closed'
10
- end
8
+ action.dialogs_alert \
9
+ message: 'This is an alert dialog',
10
+ onOpen: ->(subaction) do
11
+ subaction.snackbars_alert message: 'Opened'
12
+ end,
13
+ onClose: ->(subaction) do
14
+ subaction.snackbars_alert message: 'Closed'
15
+ end
11
16
  end
12
17
 
13
18
  # markdown = '# h1 Heading' + "\n" +
@@ -60,60 +65,109 @@ section.rows builder: ->(template) do
60
65
  res.skeleton template: 'commentList'
61
66
  end
62
67
  end,
68
+ onOpen: ->(saction) do
69
+ saction.snackbars_alert message: 'Opened'
70
+ end,
63
71
  onClose: ->(saction) do
64
72
  saction.snackbars_alert message: 'dialog closed'
65
73
  end
66
74
  end
67
75
 
68
76
  template.thumbnail title: 'dialogs/open (fullscreen on mobile)', onClick: ->(action) do
69
- action.dialogs_open fullscreen: 'mobile', url: json_ui_garage_url(path: 'forms/basic', mode: 'dialog')
77
+ action.dialogs_open \
78
+ fullscreen: 'mobile',
79
+ url: json_ui_garage_url(path: 'forms/basic', mode: 'dialog'),
80
+ onOpen: ->(saction) do
81
+ saction.snackbars_alert message: 'Opened'
82
+ end
70
83
  end
71
84
 
72
85
  template.thumbnail title: 'dialogs/reload (without url)', onClick: ->(action) do
73
86
  action.runMultiple childActions: ->(saction) do
74
- saction.dialogs_open url: json_ui_garage_url(path: 'forms/dialogs_form')
75
- saction.timeouts_set interval: 1000, onTimeout: ->(ssaction) do
76
- ssaction.dialogs_reload onReload: ->(xaction) do
77
- xaction.snackbars_alert message: 'reload!'
87
+ saction.dialogs_open \
88
+ url: json_ui_garage_url(path: 'forms/dialogs_form'),
89
+ onOpen: ->(ssaction) do
90
+ ssaction.snackbars_alert message: 'Opened'
78
91
  end
92
+ saction.timeouts_set interval: 1000, onTimeout: ->(ssaction) do
93
+ ssaction.dialogs_reload \
94
+ onReload: ->(xaction) do
95
+ xaction.snackbars_alert message: 'reload!'
96
+ end
79
97
  end
80
98
  end
81
99
  end
82
100
 
83
101
  if !Rails.env.test? # Prevent test failure
84
102
  template.thumbnail title: 'dialogs/open (page with error)', onClick: ->(action) do
85
- action.dialogs_open url: json_ui_garage_url(path: 'forms/non_existent', mode: 'dialog')
103
+ action.dialogs_open \
104
+ url: json_ui_garage_url(path: 'forms/non_existent', mode: 'dialog'),
105
+ onOpen: ->(saction) do
106
+ saction.snackbars_alert message: 'Opened'
107
+ end
86
108
  end
87
109
  end
88
110
 
89
111
  template.thumbnail title: 'dialogs/open (page with dialogs/close)', onClick: ->(action) do
90
- action.dialogs_open url: json_ui_garage_url(path: 'forms/dialogs_close', mode: 'dialog')
112
+ action.dialogs_open \
113
+ url: json_ui_garage_url(path: 'forms/dialogs_close', mode: 'dialog'),
114
+ onOpen: ->(saction) do
115
+ saction.snackbars_alert message: 'Opened'
116
+ end
91
117
  end
92
118
 
93
119
  template.thumbnail title: 'dialogs/open (page with dialogs/show with updateExisting)', onClick: ->(action) do
94
- action.dialogs_open url: json_ui_garage_url(path: 'forms/dialogs_update', mode: 'dialog')
120
+ action.dialogs_open \
121
+ url: json_ui_garage_url(path: 'forms/dialogs_update', mode: 'dialog'),
122
+ onOpen: ->(saction) do
123
+ saction.snackbars_alert message: 'Opened'
124
+ end
95
125
  end
96
126
 
97
127
  template.thumbnail title: 'dialogs/open (page with dialogs/open with updateExisting)', onClick: ->(action) do
98
- action.dialogs_open url: json_ui_garage_url(path: 'forms/dialogs_update2')
128
+ action.dialogs_open \
129
+ url: json_ui_garage_url(path: 'forms/dialogs_update2'),
130
+ onOpen: ->(saction) do
131
+ saction.snackbars_alert message: 'Opened'
132
+ end
99
133
  end
100
134
 
101
135
  template.thumbnail title: 'dialogs/open (update form inside dialog)', onClick: ->(action) do
102
- action.dialogs_open url: json_ui_garage_url(path: 'forms/dialogs_form')
136
+ action.dialogs_open \
137
+ url: json_ui_garage_url(path: 'forms/dialogs_form'),
138
+ onOpen: ->(saction) do
139
+ saction.snackbars_alert message: 'Opened'
140
+ end
103
141
  end
104
142
 
105
143
  template.thumbnail title: 'dialogs/open (with filePaster)', onClick: ->(action) do
106
- action.dialogs_open url: json_ui_garage_url(path: 'forms/dialogs_update3')
144
+ action.dialogs_open \
145
+ url: json_ui_garage_url(path: 'forms/dialogs_update3'),
146
+ onOpen: ->(saction) do
147
+ saction.snackbars_alert message: 'Opened'
148
+ end
107
149
  end
108
150
 
109
151
  template.thumbnail title: 'dialogs/open (update form inside dialog)', onClick: ->(action) do
110
- action.dialogs_open url: json_ui_garage_url(path: 'forms/dialogs_form')
152
+ action.dialogs_open \
153
+ url: json_ui_garage_url(path: 'forms/dialogs_form'),
154
+ onOpen: ->(saction) do
155
+ saction.snackbars_alert message: 'Opened'
156
+ end
111
157
  end
112
158
 
113
159
  template.thumbnail title: 'dialogs/closeAll', onClick: ->(action) do
114
160
  action.runMultiple childActions: ->(saction) do
115
- saction.dialogs_open url: json_ui_garage_url(path: 'forms/dialogs_form')
116
- saction.dialogs_open url: json_ui_garage_url(path: 'forms/dialogs_form')
161
+ saction.dialogs_open \
162
+ url: json_ui_garage_url(path: 'forms/dialogs_form'),
163
+ onOpen: ->(ssaction) do
164
+ ssaction.snackbars_alert message: 'Opened'
165
+ end
166
+ saction.dialogs_open \
167
+ url: json_ui_garage_url(path: 'forms/dialogs_form'),
168
+ onOpen: ->(ssaction) do
169
+ ssaction.snackbars_alert message: 'Opened'
170
+ end
117
171
  saction.timeouts_set interval: 1000, onTimeout: ->(ssaction) do
118
172
  ssaction.dialogs_closeAll
119
173
  end
@@ -32,9 +32,18 @@ if (update_existing = local_assigns[:update_existing])
32
32
  options[:updateExisting] = update_existing
33
33
  end
34
34
 
35
+ dialog_mode_sym = (local_assigns[:dialog_mode] || :show).to_sym
36
+ dialog_callback_message = dialog_mode_sym == :reload ? 'onReload' : 'onShow'
37
+
38
+ if dialog_mode_sym == :reload
39
+ options[:onReload] = ->(action) { action.snackbars_alert message: dialog_callback_message }
40
+ else
41
+ options[:onShow] = ->(action) { action.snackbars_alert message: dialog_callback_message }
42
+ end
43
+
35
44
  include_form = local_assigns[:include_form]
36
45
 
37
- action.send "dialogs_#{dialog_mode}", **options,
46
+ action.send "dialogs_#{dialog_mode_sym}", **options,
38
47
  onClose: ->(action) { action.snackbars_alert message: 'dialog closed' },
39
48
  content: ->(dialog) do
40
49
  dialog.body padding: glib_json_padding_body, childViews: ->(body) do
@@ -78,6 +78,19 @@ page.form \
78
78
  max: '2021-03-01',
79
79
  value: '2021-11'
80
80
 
81
+ form.spacer height: 8
82
+ form.fields_date \
83
+ name: 'user[formatted_date]',
84
+ width: 'matchParent',
85
+ label: 'Date (formatted)',
86
+ placeholder: 'Select Date'
87
+ form.fields_datetime \
88
+ name: 'user[formatted_date_time]',
89
+ width: 'matchParent',
90
+ label: 'Date Time (formatted)',
91
+ format: 'keyboardDateTime12h',
92
+ placeholder: 'Select Date'
93
+
81
94
  if record.present?
82
95
  form.fields_datetime \
83
96
  prop: :created_at,
@@ -113,4 +113,8 @@ page.form \
113
113
  form.spacer height: 8
114
114
  form.fields_submit text: 'Submit'
115
115
  form.fields_submit text: 'Submit (disable if form invalid)', disableIfFormInvalid: true
116
+ form.spacer height: 8
117
+ form.button styleClass: 'link', text: 'Cancel', onClick: ->(action) do
118
+ action.http_get url: json_ui_garage_url(path: 'forms/index')
119
+ end
116
120
  end
@@ -0,0 +1,84 @@
1
+ page = json_ui_page json
2
+
3
+ json.title 'Lists'
4
+
5
+ render "#{@path_prefix}/nav_menu", json: json, page: page
6
+
7
+ tab_index = params[:tab].to_i
8
+
9
+ page.header(
10
+ childViews: ->(header) do
11
+ # Allow navigating to another "edit mode" page to test reuse issues.
12
+ header.tabBar(
13
+ buttons: ->(menu) do
14
+ ['FIRST', 'SECOND'].each_with_index do |text, index|
15
+ menu.button(
16
+ text: text,
17
+ disabled: tab_index == index,
18
+ onClick: ->(action) do
19
+ action.windows_reload url: json_ui_garage_url(path: 'lists/edit_mode', tab: index)
20
+ end
21
+ )
22
+ end
23
+ end
24
+ )
25
+ end
26
+ )
27
+
28
+ page.form(
29
+ width: 'matchParent',
30
+ url: json_ui_garage_url(path: 'forms/generic_post'),
31
+ method: 'post',
32
+ padding: glib_json_padding_body,
33
+ childViews: ->(form) do
34
+ form.panels_list(
35
+ fieldPrefix: 'user[items]',
36
+ fieldTitleName: 'name',
37
+ width: 'matchParent',
38
+ sections: [
39
+ ->(section) do
40
+ section.header(
41
+ padding: glib_json_padding_list,
42
+ childViews: ->(header) do
43
+ header.panels_horizontal(
44
+ childViews: ->(horizontal) do
45
+ horizontal.fields_check name: 'user[check_all]', label: 'All', checkValue: true
46
+ horizontal.spacer width: 20
47
+ # header.fields_text width: 'matchParent', styleClass: 'outlined', name: 'user[new_name]', label: 'Item name'
48
+ statuses = [:pending, :active]
49
+ horizontal.fields_select(
50
+ styleClass: 'outlined',
51
+ name: 'user[status]',
52
+ width: 'matchParent',
53
+ label: 'Status',
54
+ options: statuses.map { |status| { value: status, text: status.to_s.humanize } }
55
+ )
56
+ horizontal.spacer width: 20
57
+ horizontal.fields_submit text: 'Update'
58
+ end
59
+ )
60
+ end
61
+ )
62
+ section.rows(
63
+ builder: ->(row) do
64
+ batch_count = 20
65
+ batch_count.times do |index|
66
+ id = (batch_count * tab_index) + index
67
+ # row.editable title: "Item #{id}", recordId: "PK_#{id}"
68
+ row.thumbnail title: "Item #{id}", recordId: "PK_#{id}"
69
+ end
70
+ end
71
+ )
72
+ end
73
+ ],
74
+ fieldCheckValueIf: {
75
+ "==": [
76
+ {
77
+ "var": 'user[check_all]'
78
+ },
79
+ true
80
+ ]
81
+ }
82
+ )
83
+ end
84
+ )
@@ -3,52 +3,80 @@ json.title 'Views'
3
3
  page = json_ui_page json
4
4
  render "#{@path_prefix}/nav_menu", json: json, page: page
5
5
 
6
- page.form padding: glib_json_padding_body, childViews: ->(scroll) do
7
- scroll.h2 text: 'Icon'
8
- # scroll.icon spec: { name: 'home' }
9
- scroll.icon name: 'home'
10
-
11
- scroll.spacer height: 20
12
- scroll.h2 text: 'Icon with color'
13
- scroll.fields_select \
14
- name: 'input_icon',
15
- value: 'verified',
16
- options: ['pending', 'verified', 'cancel'].map { |icon_name| { text: icon_name, value: icon_name } }
17
- scroll.button text: 'update', onClick: ->(action) do
18
- action.logics_set \
19
- targetId: 'icon1',
20
- conditionalData: {
21
- 'material.name': { "var": 'input_icon' },
22
- 'tooltip.text': { "var": 'input_icon' },
6
+ page.form(
7
+ padding: glib_json_padding_body,
8
+ childViews: ->(scroll) do
9
+ scroll.h2 text: 'Icon'
10
+ # scroll.icon spec: { name: 'home' }
11
+ scroll.icon name: 'home'
12
+ scroll.spacer height: 20
13
+ scroll.h2 text: 'Icon with color'
14
+ scroll.fields_select(
15
+ name: 'input_icon',
16
+ value: 'verified',
17
+ options: ['pending', 'verified', 'cancel'].map { |icon_name| { text: icon_name, value: icon_name } }
18
+ )
19
+ scroll.button(
20
+ text: 'update',
21
+ onClick: ->(action) do
22
+ action.logics_set(
23
+ targetId: 'icon1',
24
+ conditionalData: {
25
+ 'material.name': { "var": 'input_icon' },
26
+ 'tooltip.text': { "var": 'input_icon' },
27
+ 'tooltip.placement': 'bottom'
28
+ }
29
+ )
30
+ end
31
+ )
32
+ scroll.icon id: 'icon1', name: 'home', color: '#9370DB'
33
+ scroll.icon(
34
+ name: 'home',
35
+ color: '#9370DB80',
36
+ onClick: ->(action) do
37
+ action.snackbars_alert message: 'Icon clicked!'
38
+ end
39
+ )
40
+ scroll.spacer height: 20
41
+ scroll.h2 text: 'Icon with badge'
42
+ scroll.icon name: 'home', badge: { text: '22', backgroundColor: '#00ff00' }
43
+ scroll.spacer height: 20
44
+ scroll.h2 text: 'Icon with styleClasses'
45
+ scroll.icon id: 'icon2', name: 'home', styleClasses: ['small']
46
+ scroll.button(
47
+ text: 'Hide icon',
48
+ onClick: ->(action) do
49
+ action.logics_set(
50
+ targetId: 'icon2',
51
+ data: {
52
+ displayed: false
53
+ }
54
+ )
55
+ end
56
+ )
57
+ scroll.spacer height: 20
58
+ scroll.h2 text: 'Icon with onLoad'
59
+ scroll.icon(
60
+ id: 'icon3',
61
+ name: 'home',
62
+ onLoad: ->(action) do
63
+ action.logics_set(
64
+ targetId: 'icon3',
65
+ data: {
66
+ 'material.name': 'info',
67
+ 'tooltip.text': 'TEST123',
23
68
  'tooltip.placement': 'bottom'
24
- }
25
- end
26
- scroll.icon id: 'icon1', name: 'home', color: '#9370DB'
27
- scroll.icon name: 'home', color: '#9370DB80', onClick: ->(action) do
28
- action.snackbars_alert message: 'Icon clicked!'
29
- end
30
-
31
- scroll.spacer height: 20
32
- scroll.h2 text: 'Icon with badge'
33
- scroll.icon name: 'home', badge: { text: '22', backgroundColor: '#00ff00' }
34
-
35
- scroll.spacer height: 20
36
- scroll.h2 text: 'Icon with styleClasses'
37
- scroll.icon id: 'icon2', name: 'home', styleClasses: ['small']
38
-
39
- scroll.button text: 'Hide icon', onClick: ->(action) do
40
- action.logics_set targetId: 'icon2', data: {
41
- displayed: false
42
- }
43
- end
69
+ }
70
+ )
71
+ end
72
+ )
44
73
 
45
- scroll.spacer height: 20
46
- scroll.h2 text: 'Icon with onLoad'
47
- scroll.icon id: 'icon3', name: 'home', onLoad: ->(action) do
48
- action.logics_set targetId: 'icon3', data: {
49
- 'material.name': 'info',
50
- 'tooltip.text': 'TEST123',
51
- 'tooltip.placement': 'bottom'
52
- }
74
+ # scroll.spacer height: 20
75
+ # scroll.h2 text: 'Icon using Font Awesome'
76
+ # scroll.icon(
77
+ # id: 'icon4',
78
+ # name: 'fa-solid fa-home',
79
+ # family: 'fontawesome'
80
+ # )
53
81
  end
54
- end
82
+ )
@@ -0,0 +1,386 @@
1
+ require 'parser/current'
2
+ require 'yaml'
3
+ require 'fileutils'
4
+ require 'time'
5
+
6
+ module Glib
7
+ class DocGenerator
8
+ def initialize
9
+ @parser = Parser::CurrentRuby
10
+ end
11
+
12
+ # Generate YAML documentation for a single Ruby file
13
+ def generate_for_file(file_path, output_path)
14
+ unless File.exist?(file_path)
15
+ puts "Warning: File not found: #{file_path}"
16
+ return
17
+ end
18
+
19
+ source = File.read(file_path)
20
+ ast = @parser.parse(source)
21
+
22
+ components = extract_components(ast, source)
23
+
24
+ # Generate YAML
25
+ yaml_data = {
26
+ 'meta' => {
27
+ 'source_file' => file_path,
28
+ 'generated_at' => Time.now.iso8601,
29
+ 'generator_version' => '1.0.0'
30
+ },
31
+ 'components' => components
32
+ }
33
+
34
+ # Ensure output directory exists
35
+ FileUtils.mkdir_p(File.dirname(output_path))
36
+
37
+ # Write YAML file
38
+ File.write(output_path, yaml_data.to_yaml)
39
+ end
40
+
41
+ private
42
+
43
+ # Extract component information from AST
44
+ def extract_components(node, source)
45
+ components = {}
46
+
47
+ traverse_ast(node, source) do |class_node, class_name, parent_class, comment|
48
+ next unless class_node.type == :class
49
+
50
+ # Parse YARD comment
51
+ yard_doc = parse_yard_comment(comment)
52
+
53
+ # Extract properties
54
+ properties = extract_properties(class_node, source)
55
+
56
+ # Build component hash
57
+ component_key = underscore(class_name)
58
+ components[component_key] = {
59
+ 'class_name' => class_name,
60
+ 'extends' => parent_class,
61
+ 'description' => yard_doc[:description],
62
+ 'properties' => properties,
63
+ 'examples' => yard_doc[:examples],
64
+ 'references' => yard_doc[:references],
65
+ 'notes' => yard_doc[:notes],
66
+ 'deprecated' => yard_doc[:deprecated]
67
+ }.compact
68
+ end
69
+
70
+ components
71
+ end
72
+
73
+ # Traverse AST and yield class nodes with their comments
74
+ def traverse_ast(node, source, &block)
75
+ return unless node.is_a?(Parser::AST::Node)
76
+
77
+ if node.type == :class
78
+ class_name = extract_class_name(node)
79
+ parent_class = extract_parent_class(node)
80
+ comment = extract_comment(node, source)
81
+
82
+ yield node, class_name, parent_class, comment
83
+ end
84
+
85
+ # Recursively traverse child nodes
86
+ node.children.each do |child|
87
+ traverse_ast(child, source, &block)
88
+ end
89
+ end
90
+
91
+ # Extract class name from class node
92
+ def extract_class_name(class_node)
93
+ const_node = class_node.children[0]
94
+ if const_node.type == :const
95
+ const_node.children[1].to_s
96
+ else
97
+ 'Unknown'
98
+ end
99
+ end
100
+
101
+ # Extract parent class name
102
+ def extract_parent_class(class_node)
103
+ parent_node = class_node.children[1]
104
+ return nil unless parent_node
105
+
106
+ if parent_node.type == :const
107
+ parent_node.children[1].to_s
108
+ else
109
+ nil
110
+ end
111
+ end
112
+
113
+ # Extract comment before a node
114
+ def extract_comment(node, source)
115
+ return '' unless node.location
116
+
117
+ # Get all lines before the node
118
+ lines = source.lines
119
+ node_line = node.location.line - 1
120
+
121
+ # Walk backwards to collect comment lines
122
+ comment_lines = []
123
+ (node_line - 1).downto(0) do |i|
124
+ line = lines[i].strip
125
+ break unless line.start_with?('#') || line.empty?
126
+
127
+ if line.start_with?('#')
128
+ # Remove leading # and whitespace
129
+ comment_lines.unshift(line.sub(/^#\s?/, ''))
130
+ end
131
+ end
132
+
133
+ comment_lines.join("\n")
134
+ end
135
+
136
+ # Parse YARD comment into structured data
137
+ def parse_yard_comment(comment)
138
+ return {} if comment.empty?
139
+
140
+ result = {
141
+ description: '',
142
+ examples: [],
143
+ references: [],
144
+ notes: [],
145
+ deprecated: nil
146
+ }
147
+
148
+ current_section = :description
149
+ current_example = nil
150
+ description_lines = []
151
+
152
+ comment.lines.each do |line|
153
+ line = line.chomp
154
+
155
+ # Check for YARD tags
156
+ if line =~ /^@example\s*(.*)/
157
+ # Save current example if exists
158
+ result[:examples] << current_example if current_example
159
+
160
+ current_example = {
161
+ 'label' => $1.strip,
162
+ 'code' => ''
163
+ }
164
+ current_section = :example
165
+ elsif line =~ /^@see\s+(.*)/
166
+ reference = $1.strip
167
+ # Parse URL and description
168
+ if reference =~ /^(https?:\/\/\S+)\s+(.*)/
169
+ result[:references] << { 'url' => $1, 'description' => $2 }
170
+ elsif reference =~ /^(https?:\/\/\S+)/
171
+ result[:references] << { 'url' => $1 }
172
+ else
173
+ result[:references] << { 'text' => reference }
174
+ end
175
+ current_section = :description
176
+ elsif line =~ /^@note\s+(.*)/
177
+ result[:notes] << $1.strip
178
+ current_section = :description
179
+ elsif line =~ /^@deprecated\s*(.*)/
180
+ result[:deprecated] = $1.strip
181
+ result[:deprecated] = true if result[:deprecated].empty?
182
+ current_section = :description
183
+ else
184
+ # Regular content
185
+ if current_section == :example && current_example
186
+ # Remove leading spaces from example code (preserve relative indentation)
187
+ current_example['code'] += line + "\n"
188
+ elsif current_section == :description
189
+ description_lines << line unless line.strip.empty? && description_lines.empty?
190
+ end
191
+ end
192
+ end
193
+
194
+ # Save last example
195
+ result[:examples] << current_example if current_example
196
+
197
+ # Clean up example code (remove extra leading whitespace)
198
+ result[:examples].each do |example|
199
+ example['code'] = unindent(example['code'])
200
+ end
201
+
202
+ # Join description lines
203
+ result[:description] = description_lines.join("\n").strip
204
+
205
+ # Remove empty arrays/nils
206
+ result.delete(:examples) if result[:examples].empty?
207
+ result.delete(:references) if result[:references].empty?
208
+ result.delete(:notes) if result[:notes].empty?
209
+ result.delete(:deprecated) if result[:deprecated].nil?
210
+
211
+ result
212
+ end
213
+
214
+ # Extract properties from class body
215
+ def extract_properties(class_node, source)
216
+ properties = {}
217
+
218
+ class_body = class_node.children[2]
219
+ return properties unless class_body
220
+
221
+ traverse_class_body(class_body, source) do |method_name, args, comment|
222
+ # These are the DSL property definition methods
223
+ if [:string, :int, :bool, :float, :action, :views, :array, :hash,
224
+ :icon, :color, :length, :date, :date_time, :text, :any,
225
+ :panels_builder, :menu, :singleton_array].include?(method_name)
226
+
227
+ property_name = args.first
228
+ next unless property_name
229
+
230
+ # Parse property comment
231
+ prop_doc = parse_property_comment(comment)
232
+
233
+ # Extract options if present (for hash, singleton_array, etc.)
234
+ options = extract_property_options(args)
235
+
236
+ properties[property_name] = {
237
+ 'type' => method_name.to_s,
238
+ 'description' => prop_doc[:description],
239
+ 'required' => options[:required] || false,
240
+ 'options' => options[:options],
241
+ 'examples' => prop_doc[:examples],
242
+ 'notes' => prop_doc[:notes],
243
+ 'deprecated' => prop_doc[:deprecated]
244
+ }.compact
245
+
246
+ # Remove empty arrays
247
+ properties[property_name].delete('examples') if properties[property_name]['examples']&.empty?
248
+ properties[property_name].delete('notes') if properties[property_name]['notes']&.empty?
249
+ end
250
+ end
251
+
252
+ properties
253
+ end
254
+
255
+ # Traverse class body to find method calls (property definitions)
256
+ def traverse_class_body(node, source, &block)
257
+ return unless node.is_a?(Parser::AST::Node)
258
+
259
+ if node.type == :send
260
+ receiver = node.children[0]
261
+ method_name = node.children[1]
262
+ args = node.children[2..-1].map { |arg| extract_symbol_or_string(arg) }
263
+ comment = extract_comment(node, source)
264
+
265
+ # Only process calls without a receiver (DSL methods)
266
+ yield method_name, args, comment if receiver.nil?
267
+ end
268
+
269
+ node.children.each do |child|
270
+ traverse_class_body(child, source, &block)
271
+ end
272
+ end
273
+
274
+ # Extract symbol or string value from AST node
275
+ def extract_symbol_or_string(node)
276
+ return nil unless node.is_a?(Parser::AST::Node)
277
+
278
+ case node.type
279
+ when :sym
280
+ node.children[0].to_s
281
+ when :str
282
+ node.children[0]
283
+ when :hash
284
+ extract_hash(node)
285
+ else
286
+ nil
287
+ end
288
+ end
289
+
290
+ # Extract hash from AST node
291
+ def extract_hash(node)
292
+ return nil unless node.type == :hash
293
+
294
+ result = {}
295
+ node.children.each do |pair|
296
+ next unless pair.type == :pair
297
+ key = extract_symbol_or_string(pair.children[0])
298
+ value = extract_symbol_or_string(pair.children[1]) || extract_array(pair.children[1])
299
+ result[key] = value if key
300
+ end
301
+ result
302
+ end
303
+
304
+ # Extract array from AST node
305
+ def extract_array(node)
306
+ return nil unless node.is_a?(Parser::AST::Node) && node.type == :array
307
+
308
+ node.children.map { |child| extract_symbol_or_string(child) }.compact
309
+ end
310
+
311
+ # Extract property options from arguments
312
+ def extract_property_options(args)
313
+ options = { required: false }
314
+
315
+ args.each do |arg|
316
+ if arg.is_a?(Hash)
317
+ if arg['required']
318
+ options[:required] = true
319
+ end
320
+ if arg['optional']
321
+ options[:options] = { 'optional' => arg['optional'] }
322
+ end
323
+ end
324
+ end
325
+
326
+ options
327
+ end
328
+
329
+ # Parse property comment
330
+ def parse_property_comment(comment)
331
+ return {} if comment.empty?
332
+
333
+ result = {
334
+ description: '',
335
+ examples: [],
336
+ notes: [],
337
+ deprecated: nil
338
+ }
339
+
340
+ description_lines = []
341
+
342
+ comment.lines.each do |line|
343
+ line = line.chomp
344
+
345
+ if line =~ /^@example\s+(.*)/
346
+ result[:examples] << $1.strip
347
+ elsif line =~ /^@note\s+(.*)/
348
+ result[:notes] << $1.strip
349
+ elsif line =~ /^@deprecated\s*(.*)/
350
+ result[:deprecated] = $1.strip
351
+ result[:deprecated] = true if result[:deprecated].empty?
352
+ else
353
+ description_lines << line unless line.strip.empty? && description_lines.empty?
354
+ end
355
+ end
356
+
357
+ result[:description] = description_lines.join("\n").strip
358
+
359
+ result
360
+ end
361
+
362
+ # Convert CamelCase to snake_case
363
+ def underscore(string)
364
+ string.gsub(/::/, '_')
365
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
366
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
367
+ .tr('-', '_')
368
+ .downcase
369
+ end
370
+
371
+ # Remove common leading whitespace from multi-line strings
372
+ def unindent(text)
373
+ return text if text.empty?
374
+
375
+ lines = text.lines
376
+ # Find minimum indentation (ignoring empty lines)
377
+ min_indent = lines
378
+ .reject { |line| line.strip.empty? }
379
+ .map { |line| line.match(/^(\s*)/)[1].length }
380
+ .min || 0
381
+
382
+ # Remove that amount of indentation from each line
383
+ lines.map { |line| line.strip.empty? ? line : line[min_indent..-1] }.join
384
+ end
385
+ end
386
+ end
data/lib/tasks/db.rake CHANGED
@@ -63,7 +63,7 @@ namespace :db do
63
63
 
64
64
  entries.each do |a|
65
65
  attrs = Hash[a.attributes.sort]
66
- attrs.delete_if { |k, v| v.nil? }
66
+ # attrs.delete_if { |k, v| v.nil? }
67
67
 
68
68
  id = if attrs.include?('id')
69
69
  [a.id]
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glib-web
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.40.0
4
+ version: 4.42.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
  date: 2019-10-04 00:00:00.000000000 Z
@@ -136,7 +136,7 @@ dependencies:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
138
  version: '0'
139
- description:
139
+ description:
140
140
  email: ''
141
141
  executables: []
142
142
  extensions: []
@@ -317,6 +317,7 @@ files:
317
317
  - app/views/json_ui/garage/lists/edit_mode.json.jbuilder
318
318
  - app/views/json_ui/garage/lists/fab.json.jbuilder
319
319
  - app/views/json_ui/garage/lists/index.json.jbuilder
320
+ - app/views/json_ui/garage/lists/old_edit_mode.json.jbuilder
320
321
  - app/views/json_ui/garage/lists/reordering.json.jbuilder
321
322
  - app/views/json_ui/garage/lists/templating.json.jbuilder
322
323
  - app/views/json_ui/garage/multistep_form/step1.json.jbuilder
@@ -427,6 +428,7 @@ files:
427
428
  - lib/glib-web.rb
428
429
  - lib/glib/all_helpers.rb
429
430
  - lib/glib/crypt/utils.rb
431
+ - lib/glib/doc_generator.rb
430
432
  - lib/glib/dynamic_text.rb
431
433
  - lib/glib/dynamic_text/config.rb
432
434
  - lib/glib/engine.rb
@@ -462,10 +464,10 @@ files:
462
464
  - lib/glib/version.rb
463
465
  - lib/tasks/db.rake
464
466
  - lib/tasks/docs.rake
465
- homepage:
467
+ homepage:
466
468
  licenses: []
467
469
  metadata: {}
468
- post_install_message:
470
+ post_install_message:
469
471
  rdoc_options: []
470
472
  require_paths:
471
473
  - lib
@@ -481,7 +483,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
481
483
  version: '0'
482
484
  requirements: []
483
485
  rubygems_version: 3.4.6
484
- signing_key:
486
+ signing_key:
485
487
  specification_version: 4
486
488
  summary: ''
487
489
  test_files: []