intent 0.5.5 → 0.7.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 +4 -4
- data/.gitignore +2 -0
- data/LICENSE.txt +1 -1
- data/README.md +10 -28
- data/bin/{review → intent} +1 -2
- data/bin/inventory +12 -0
- data/bin/project_doc +30 -0
- data/bin/projects +1 -1
- data/bin/projects_active +123 -0
- data/bin/todo +1 -1
- data/bin/todo_review +123 -0
- data/intent.gemspec +15 -7
- data/lib/gem_ext/todo-txt.rb +26 -2
- data/lib/intent/commands/base.rb +40 -0
- data/lib/intent/commands/errors.rb +7 -0
- data/lib/intent/commands/intent.rb +9 -0
- data/lib/intent/commands/inventory.rb +174 -0
- data/lib/intent/commands/projects.rb +34 -0
- data/lib/intent/commands/todo.rb +51 -0
- data/lib/intent/commands.rb +9 -0
- data/lib/intent/core.rb +184 -0
- data/lib/intent/desktop.rb +3 -0
- data/lib/intent/projects.rb +1 -1
- data/lib/intent/review.rb +10 -1
- data/lib/intent/text/inventory.help.txt +6 -0
- data/lib/intent/text/project.help.txt +7 -0
- data/lib/intent/text/projects.help.txt +9 -0
- data/lib/intent/text/todo.help.txt +15 -0
- data/lib/intent/todo.rb +3 -1
- data/lib/intent/ui/ttyui.rb +90 -0
- data/lib/intent/verbs/add.rb +21 -0
- data/lib/intent/verbs/cite.rb +14 -0
- data/lib/intent/verbs/install.rb +20 -0
- data/lib/intent/verbs/review.rb +131 -0
- data/lib/intent/{todo/manager.rb → verbs/todo.rb} +3 -16
- data/lib/intent/version.rb +1 -1
- data/lib/intent.rb +20 -3
- metadata +179 -32
- data/.travis.yml +0 -4
- data/CODE_OF_CONDUCT.md +0 -13
- data/lib/intent/projects/manager.rb +0 -20
- data/lib/intent/review/manager.rb +0 -17
- /data/lib/intent/{projects → verbs}/status.rb +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a80e9d9082642b1f2325b40629ff95ffb096cdba644d0f2d5aa2af25541c2d2b
|
4
|
+
data.tar.gz: 90ed06f5afbf84636fbeb70e6fab66a4868dc0d485b751178b2ff31568315237
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a4a23b8d4882af953e2d330c26388c91bb86513c1881192952998f6f74b5f49b682f4b609566ea18b0b6f0ad7517020ac15ccc22ce8756edc001ac5fc25c455b
|
7
|
+
data.tar.gz: cfc14324613fc505a6ec0a591e73f667dd3360b4ec1d8ee7f05d86813fcbaf5e645f12b917579adc6b046223dbc251f665f65b0cd70087dac39d59cc7adfdccf
|
data/.gitignore
CHANGED
data/LICENSE.txt
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
The MIT License (MIT)
|
2
2
|
|
3
|
-
Copyright (c) 2016 Mark Rickerby
|
3
|
+
Copyright (c) 2016-2024 Mark Rickerby <https://maetl.net>
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
data/README.md
CHANGED
@@ -1,41 +1,23 @@
|
|
1
1
|
# Intent
|
2
2
|
|
3
|
-
|
3
|
+
Automation engine with todo.txt
|
4
4
|
|
5
|
-
|
5
|
+
This tool is mired deep in the obscurity and pedantry of the text-based computing culture that originated in the 1970s and still continues today. You probably don’t want to use this.
|
6
6
|
|
7
7
|
## Installation
|
8
8
|
|
9
|
-
Add this line to your application's Gemfile:
|
10
|
-
|
11
|
-
```ruby
|
12
|
-
gem 'intent'
|
13
9
|
```
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
$ bundle
|
18
|
-
|
19
|
-
Or install it yourself as:
|
20
|
-
|
21
|
-
$ gem install intent
|
10
|
+
gem install intent
|
11
|
+
```
|
22
12
|
|
23
13
|
## Usage
|
24
14
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
-
|
31
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
-
|
33
|
-
## Contributing
|
34
|
-
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/intent. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
|
36
|
-
|
15
|
+
```
|
16
|
+
intent help
|
17
|
+
```
|
37
18
|
|
38
|
-
## License
|
19
|
+
## Copyright & License
|
39
20
|
|
40
|
-
|
21
|
+
Copyright 2024 Mark Rickerby <https://maetl.net>.
|
41
22
|
|
23
|
+
All documentation and modelling concepts are **CC BY-NC**. Source code and command line tools are MIT.
|
data/bin/{review → intent}
RENAMED
@@ -1,5 +1,4 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
|
3
2
|
path = __FILE__
|
4
3
|
while File.symlink?(path)
|
5
4
|
path = File.expand_path(File.readlink(path), File.dirname(path))
|
@@ -8,4 +7,4 @@ $:.unshift(File.join(File.dirname(File.expand_path(path)), '..', 'lib'))
|
|
8
7
|
|
9
8
|
require 'intent'
|
10
9
|
|
11
|
-
Intent::
|
10
|
+
Intent::Dispatcher.exec_command(:intent, ARGV.dup)
|
data/bin/inventory
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
path = __FILE__
|
3
|
+
|
4
|
+
while File.symlink?(path)
|
5
|
+
path = File.expand_path(File.readlink(path), File.dirname(path))
|
6
|
+
end
|
7
|
+
|
8
|
+
$:.unshift(File.join(File.dirname(File.expand_path(path)), '..', 'lib'))
|
9
|
+
|
10
|
+
require 'intent'
|
11
|
+
|
12
|
+
Intent::Dispatcher.exec_command(:inventory, ARGV.dup)
|
data/bin/project_doc
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "tty-markdown"
|
3
|
+
require "tty-pager"
|
4
|
+
|
5
|
+
PROJECTS_PATH = File.dirname(ENV['TODO_TXT'])
|
6
|
+
|
7
|
+
class ProjectDocCommand
|
8
|
+
def self.print_help(output)
|
9
|
+
output.puts "Pass in a project name to read its main document."
|
10
|
+
output.puts "Future iterations could display a list of available documents."
|
11
|
+
# Command idea: Add milestone to document
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.run(args, output=STDOUT)
|
15
|
+
if args.empty?
|
16
|
+
print_help(output)
|
17
|
+
else
|
18
|
+
project_name = args.first.delete("^0–9a-z\-")
|
19
|
+
project_path = "#{PROJECTS_PATH}/#{project_name}"
|
20
|
+
|
21
|
+
if Dir.exists?(project_path)
|
22
|
+
pager = TTY::Pager.new
|
23
|
+
intent = File.read("#{project_path}/#{project_name}.md")
|
24
|
+
pager.page(TTY::Markdown.parse(intent))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
ProjectDocCommand.run(ARGV.dup)
|
data/bin/projects
CHANGED
data/bin/projects_active
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "tty-box"
|
3
|
+
require "tty-screen"
|
4
|
+
require "tty-reader"
|
5
|
+
require "tty-cursor"
|
6
|
+
require "tty-pager"
|
7
|
+
require "pastel"
|
8
|
+
require "todo-txt"
|
9
|
+
|
10
|
+
list = Todo::List.new(ENV['TODO_TXT'])
|
11
|
+
|
12
|
+
box = TTY::Box.frame(
|
13
|
+
width: TTY::Screen.width,
|
14
|
+
height: TTY::Screen.height-1,
|
15
|
+
align: :center,
|
16
|
+
border: :thick,
|
17
|
+
style: {
|
18
|
+
fg: :bright_yellow,
|
19
|
+
border: {
|
20
|
+
fg: :white,
|
21
|
+
}
|
22
|
+
}
|
23
|
+
)
|
24
|
+
|
25
|
+
focus_index = 0
|
26
|
+
|
27
|
+
def truncate(str, width)
|
28
|
+
return str unless str.length >= width
|
29
|
+
|
30
|
+
"#{str.slice(0, width-3)}..."
|
31
|
+
end
|
32
|
+
|
33
|
+
def draw_card(top, item, focus)
|
34
|
+
|
35
|
+
style = unless top == focus
|
36
|
+
{ fg: :white, border: { fg: :white } }
|
37
|
+
else
|
38
|
+
{ fg: :bright_white, border: { fg: :bright_magenta, bg: :black } }
|
39
|
+
end
|
40
|
+
|
41
|
+
title = if top == focus
|
42
|
+
{bottom_right: "[x: close][enter: launch]"}
|
43
|
+
else
|
44
|
+
{}
|
45
|
+
end
|
46
|
+
|
47
|
+
#pastel = Pastel.new
|
48
|
+
width = 90
|
49
|
+
|
50
|
+
TTY::Box.frame(width: width, height: 4, left: 4, title: title, border: :thick, style: style) do
|
51
|
+
"\s#{truncate(item[:title], 88)}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
projects = [
|
56
|
+
{ title: "maetl", index: File.read(File.expand_path("~/Documents/Projects/maetl/maetl.md")) },
|
57
|
+
{ title: "calyx", index: File.read(File.expand_path("~/Documents/Projects/calyx/calyx.md")) },
|
58
|
+
{ title: "yarrow", index: File.read(File.expand_path("~/Documents/Projects/yarrow/yarrow.md")) }
|
59
|
+
]
|
60
|
+
|
61
|
+
items_per_page = (TTY::Screen.height-1) / 4
|
62
|
+
|
63
|
+
# items = list.by_not_done.by_context("@reading").slice(0, items_per_page).map do |item|
|
64
|
+
# { title: item.text, href: item.text }
|
65
|
+
# end
|
66
|
+
|
67
|
+
$cursor = TTY::Cursor
|
68
|
+
#print $cursor.hide
|
69
|
+
|
70
|
+
def draw_list(items, focus)
|
71
|
+
buffer = []
|
72
|
+
items.each_with_index do |item, i|
|
73
|
+
buffer << draw_card(i, item, focus)
|
74
|
+
end
|
75
|
+
result = buffer.join("")
|
76
|
+
|
77
|
+
print $cursor.clear_screen + $cursor.move_to(0,0)
|
78
|
+
print result
|
79
|
+
end
|
80
|
+
|
81
|
+
draw_list(projects, focus_index)
|
82
|
+
|
83
|
+
reader = TTY::Reader.new(interrupt: :exit)
|
84
|
+
|
85
|
+
reader.on(:keyctrl_x, :keyescape) do
|
86
|
+
print $cursor.clear_screen + $cursor.move_to(0,0)
|
87
|
+
print $cursor.show
|
88
|
+
exit
|
89
|
+
end
|
90
|
+
|
91
|
+
reader.on(:keytab, :keydown) do
|
92
|
+
if focus_index == projects.count - 1
|
93
|
+
focus_index = 0
|
94
|
+
else
|
95
|
+
focus_index += 1
|
96
|
+
end
|
97
|
+
draw_list(projects, focus_index)
|
98
|
+
end
|
99
|
+
|
100
|
+
reader.on(:keyup) do
|
101
|
+
if focus_index == 0
|
102
|
+
focus_index = projects.count - 1
|
103
|
+
else
|
104
|
+
focus_index -= 1
|
105
|
+
end
|
106
|
+
draw_list(projects, focus_index)
|
107
|
+
end
|
108
|
+
|
109
|
+
reader.on(:keypress) do |key|
|
110
|
+
# if key.value == "x"
|
111
|
+
# items.delete_at(focus_index)
|
112
|
+
# focus_index -= 1 if focus_index >= items.count
|
113
|
+
# draw_list(items, focus_index)
|
114
|
+
# end
|
115
|
+
end
|
116
|
+
|
117
|
+
reader.on(:keyenter) do
|
118
|
+
`chrome-cli open #{items[focus_index][:href]}`
|
119
|
+
end
|
120
|
+
|
121
|
+
loop do
|
122
|
+
reader.read_line(echo: false)
|
123
|
+
end
|
data/bin/todo
CHANGED
data/bin/todo_review
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "tty-box"
|
3
|
+
require "tty-screen"
|
4
|
+
require "tty-reader"
|
5
|
+
require "tty-cursor"
|
6
|
+
require "tty-pager"
|
7
|
+
require "pastel"
|
8
|
+
require "todo-txt"
|
9
|
+
|
10
|
+
list = Todo::List.new(ENV['TODO_TXT'])
|
11
|
+
|
12
|
+
box = TTY::Box.frame(
|
13
|
+
width: TTY::Screen.width,
|
14
|
+
height: TTY::Screen.height-1,
|
15
|
+
align: :center,
|
16
|
+
border: :thick,
|
17
|
+
style: {
|
18
|
+
fg: :bright_yellow,
|
19
|
+
border: {
|
20
|
+
fg: :white,
|
21
|
+
}
|
22
|
+
}
|
23
|
+
)
|
24
|
+
|
25
|
+
focus_index = 0
|
26
|
+
|
27
|
+
def truncate(str, width)
|
28
|
+
return str unless str.length >= width
|
29
|
+
|
30
|
+
"#{str.slice(0, width-3)}..."
|
31
|
+
end
|
32
|
+
|
33
|
+
def draw_card(top, item, focus)
|
34
|
+
|
35
|
+
style = unless top == focus
|
36
|
+
{ fg: :white, border: { fg: :white } }
|
37
|
+
else
|
38
|
+
{ fg: :bright_white, border: { fg: :bright_magenta, bg: :black } }
|
39
|
+
end
|
40
|
+
|
41
|
+
title = if top == focus
|
42
|
+
{bottom_right: "[x: close][enter: launch]"}
|
43
|
+
else
|
44
|
+
{}
|
45
|
+
end
|
46
|
+
|
47
|
+
#pastel = Pastel.new
|
48
|
+
width = 90
|
49
|
+
|
50
|
+
TTY::Box.frame(width: width, height: 4, left: 4, title: title, border: :thick, style: style) do
|
51
|
+
"\s#{truncate(item[:title], 88)}\n\s#{truncate(item[:href], 88)}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
items = [
|
56
|
+
{ title: "maetl website", href: "https://maetl.net/" },
|
57
|
+
{ title: "tty-cursor on Github", href: "https://github.com/piotrmurach/tty-cursor" },
|
58
|
+
{ title: "Tangram Design", href: "https://tangram.co.nz" },
|
59
|
+
]
|
60
|
+
|
61
|
+
items_per_page = (TTY::Screen.height-1) / 4
|
62
|
+
|
63
|
+
items = list.by_not_done.by_context("@reading").slice(0, items_per_page).map do |item|
|
64
|
+
{ title: item.text, href: item.text }
|
65
|
+
end
|
66
|
+
|
67
|
+
$cursor = TTY::Cursor
|
68
|
+
print $cursor.hide
|
69
|
+
|
70
|
+
def draw_list(items, focus)
|
71
|
+
buffer = []
|
72
|
+
items.each_with_index do |item, i|
|
73
|
+
buffer << draw_card(i, item, focus)
|
74
|
+
end
|
75
|
+
result = buffer.join("")
|
76
|
+
|
77
|
+
print $cursor.clear_screen + $cursor.move_to(0,0)
|
78
|
+
print result
|
79
|
+
end
|
80
|
+
|
81
|
+
draw_list(items, focus_index)
|
82
|
+
|
83
|
+
reader = TTY::Reader.new(interrupt: :exit)
|
84
|
+
|
85
|
+
reader.on(:keyctrl_x, :keyescape) do
|
86
|
+
print $cursor.clear_screen + $cursor.move_to(0,0)
|
87
|
+
print $cursor.show
|
88
|
+
exit
|
89
|
+
end
|
90
|
+
|
91
|
+
reader.on(:keytab, :keydown) do
|
92
|
+
if focus_index == items.count - 1
|
93
|
+
focus_index = 0
|
94
|
+
else
|
95
|
+
focus_index += 1
|
96
|
+
end
|
97
|
+
draw_list(items, focus_index)
|
98
|
+
end
|
99
|
+
|
100
|
+
reader.on(:keyup) do
|
101
|
+
if focus_index == 0
|
102
|
+
focus_index = items.count - 1
|
103
|
+
else
|
104
|
+
focus_index -= 1
|
105
|
+
end
|
106
|
+
draw_list(items, focus_index)
|
107
|
+
end
|
108
|
+
|
109
|
+
reader.on(:keypress) do |key|
|
110
|
+
if key.value == "x"
|
111
|
+
items.delete_at(focus_index)
|
112
|
+
focus_index -= 1 if focus_index >= items.count
|
113
|
+
draw_list(items, focus_index)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
reader.on(:keyenter) do
|
118
|
+
`chrome-cli open #{items[focus_index][:href]}`
|
119
|
+
end
|
120
|
+
|
121
|
+
loop do
|
122
|
+
reader.read_line(echo: false)
|
123
|
+
end
|
data/intent.gemspec
CHANGED
@@ -18,14 +18,22 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.add_runtime_dependency "todo-txt", "~> 0.
|
22
|
-
spec.add_runtime_dependency "pastel", "~> 0.
|
23
|
-
spec.add_runtime_dependency "git", "~> 1.
|
24
|
-
spec.add_runtime_dependency "terminal-notifier", "~>
|
21
|
+
spec.add_runtime_dependency "todo-txt", "~> 0.12"
|
22
|
+
spec.add_runtime_dependency "pastel", "~> 0.8"
|
23
|
+
spec.add_runtime_dependency "git", "~> 1.19.1"
|
24
|
+
spec.add_runtime_dependency "terminal-notifier", "~> 2.0"
|
25
25
|
spec.add_runtime_dependency "ghost", "~> 1.0.0"
|
26
|
+
spec.add_runtime_dependency "unicode_plot", "0.0.5"
|
27
|
+
spec.add_runtime_dependency "kdl", "1.0.3"
|
28
|
+
spec.add_runtime_dependency "sorted_set", "1.0.3"
|
29
|
+
spec.add_runtime_dependency "strings-ansi", "0.2.0"
|
30
|
+
spec.add_runtime_dependency "bibtex-ruby", "6.1.0"
|
31
|
+
spec.add_runtime_dependency "tty-prompt", "~> 0.23.1"
|
32
|
+
spec.add_runtime_dependency "tty-table", "~> 0.12.0"
|
33
|
+
spec.add_runtime_dependency "tty-tree", "~> 0.4.0"
|
34
|
+
spec.add_runtime_dependency "nanoid", "~> 2.0.0"
|
26
35
|
|
27
|
-
|
28
|
-
spec.add_development_dependency "
|
29
|
-
spec.add_development_dependency "rake", "~> 10.0"
|
36
|
+
spec.add_development_dependency "bundler"
|
37
|
+
spec.add_development_dependency "rake"
|
30
38
|
spec.add_development_dependency "rspec"
|
31
39
|
end
|
data/lib/gem_ext/todo-txt.rb
CHANGED
@@ -10,13 +10,37 @@ class Todo::Task
|
|
10
10
|
# Completed tasks are rendered with a strikethrough
|
11
11
|
pastel.strikethrough(to_s)
|
12
12
|
else
|
13
|
-
|
14
|
-
|
13
|
+
[
|
14
|
+
pastel.red(print_priority),
|
15
|
+
pastel.yellow(created_on.to_s),
|
16
|
+
text,
|
17
|
+
pastel.bold.magenta(print_contexts),
|
18
|
+
pastel.bold.blue(print_projects),
|
19
|
+
pastel.bold.cyan(print_tags)
|
20
|
+
].reject { |item| !item || item.nil? || item.empty? }.join(' ')
|
15
21
|
end
|
16
22
|
end
|
17
23
|
|
24
|
+
def highlight_as_project
|
25
|
+
return to_s unless STDOUT.tty?
|
26
|
+
|
27
|
+
pastel = Pastel.new
|
28
|
+
[
|
29
|
+
pastel.bold.cyan(print_projects),
|
30
|
+
pastel.red(print_project_context)
|
31
|
+
].reject { |item| !item || item.nil? || item.empty? }.join(' ')
|
32
|
+
end
|
33
|
+
|
34
|
+
def text=(label)
|
35
|
+
@text = label
|
36
|
+
end
|
37
|
+
|
18
38
|
private
|
19
39
|
|
40
|
+
def print_project_context
|
41
|
+
'active' if contexts.include?('@active')
|
42
|
+
end
|
43
|
+
|
20
44
|
def print_open_task(pastel)
|
21
45
|
[
|
22
46
|
pastel.red(print_priority),
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Intent
|
2
|
+
module Commands
|
3
|
+
class Base
|
4
|
+
attr_reader :identity
|
5
|
+
attr_reader :documents
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@identity = strip_classname
|
9
|
+
@documents = ::Intent::Core::Documents.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def print_help(output)
|
13
|
+
output.puts(File.read(help_txt_path))
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate_id
|
17
|
+
Nanoid.generate(size: 8, alphabet: ID_ALPHABET)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
T_CLASS_PREFIX = 'Intent::Commands::'
|
23
|
+
|
24
|
+
ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
25
|
+
|
26
|
+
def strip_classname
|
27
|
+
self.class.to_s.sub(T_CLASS_PREFIX, '').downcase
|
28
|
+
end
|
29
|
+
|
30
|
+
def help_txt_path
|
31
|
+
# TODO: does this work on Windows?
|
32
|
+
"#{__dir__}/../text/#{identity}.help.txt"
|
33
|
+
end
|
34
|
+
|
35
|
+
def create_noun(type, label, tags)
|
36
|
+
Intent::Core::Noun.new(type, label, tags)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|