csv_plus_plus 0.1.0 → 0.1.2
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/CHANGELOG.md +16 -1
- data/README.md +18 -62
- data/lib/csv_plus_plus/benchmarked_compiler.rb +62 -0
- data/lib/csv_plus_plus/can_define_references.rb +88 -0
- data/lib/csv_plus_plus/can_resolve_references.rb +8 -0
- data/lib/csv_plus_plus/cell.rb +3 -3
- data/lib/csv_plus_plus/cli.rb +24 -7
- data/lib/csv_plus_plus/color.rb +12 -6
- data/lib/csv_plus_plus/compiler.rb +156 -0
- data/lib/csv_plus_plus/data_validation.rb +138 -0
- data/lib/csv_plus_plus/{language → entities}/ast_builder.rb +5 -7
- data/lib/csv_plus_plus/entities/boolean.rb +31 -0
- data/lib/csv_plus_plus/{language → entities}/builtins.rb +2 -4
- data/lib/csv_plus_plus/entities/cell_reference.rb +60 -0
- data/lib/csv_plus_plus/entities/date.rb +30 -0
- data/lib/csv_plus_plus/entities/entity.rb +84 -0
- data/lib/csv_plus_plus/entities/function.rb +33 -0
- data/lib/csv_plus_plus/entities/function_call.rb +35 -0
- data/lib/csv_plus_plus/entities/number.rb +34 -0
- data/lib/csv_plus_plus/entities/runtime_value.rb +26 -0
- data/lib/csv_plus_plus/entities/string.rb +29 -0
- data/lib/csv_plus_plus/entities/variable.rb +25 -0
- data/lib/csv_plus_plus/entities.rb +33 -0
- data/lib/csv_plus_plus/error/error.rb +10 -0
- data/lib/csv_plus_plus/error/formula_syntax_error.rb +36 -0
- data/lib/csv_plus_plus/error/modifier_syntax_error.rb +27 -0
- data/lib/csv_plus_plus/error/modifier_validation_error.rb +49 -0
- data/lib/csv_plus_plus/{language → error}/syntax_error.rb +6 -14
- data/lib/csv_plus_plus/error/writer_error.rb +9 -0
- data/lib/csv_plus_plus/error.rb +9 -2
- data/lib/csv_plus_plus/expand.rb +3 -1
- data/lib/csv_plus_plus/google_api_client.rb +4 -0
- data/lib/csv_plus_plus/lexer/lexer.rb +19 -11
- data/lib/csv_plus_plus/modifier/conditional_formatting.rb +17 -0
- data/lib/csv_plus_plus/modifier.rb +73 -70
- data/lib/csv_plus_plus/options.rb +3 -0
- data/lib/csv_plus_plus/parser/cell_value.tab.rb +305 -0
- data/lib/csv_plus_plus/parser/code_section.tab.rb +410 -0
- data/lib/csv_plus_plus/parser/modifier.tab.rb +484 -0
- data/lib/csv_plus_plus/references.rb +68 -0
- data/lib/csv_plus_plus/row.rb +0 -3
- data/lib/csv_plus_plus/runtime.rb +199 -0
- data/lib/csv_plus_plus/scope.rb +196 -0
- data/lib/csv_plus_plus/template.rb +21 -5
- data/lib/csv_plus_plus/validated_modifier.rb +164 -0
- data/lib/csv_plus_plus/version.rb +1 -1
- data/lib/csv_plus_plus/writer/file_backer_upper.rb +6 -4
- data/lib/csv_plus_plus/writer/google_sheet_builder.rb +24 -29
- data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +33 -12
- data/lib/csv_plus_plus/writer/rubyxl_builder.rb +3 -6
- data/lib/csv_plus_plus.rb +41 -16
- metadata +34 -24
- data/lib/csv_plus_plus/code_section.rb +0 -68
- data/lib/csv_plus_plus/language/benchmarked_compiler.rb +0 -65
- data/lib/csv_plus_plus/language/cell_value.tab.rb +0 -332
- data/lib/csv_plus_plus/language/code_section.tab.rb +0 -442
- data/lib/csv_plus_plus/language/compiler.rb +0 -157
- data/lib/csv_plus_plus/language/entities/boolean.rb +0 -33
- data/lib/csv_plus_plus/language/entities/cell_reference.rb +0 -33
- data/lib/csv_plus_plus/language/entities/entity.rb +0 -86
- data/lib/csv_plus_plus/language/entities/function.rb +0 -35
- data/lib/csv_plus_plus/language/entities/function_call.rb +0 -26
- data/lib/csv_plus_plus/language/entities/number.rb +0 -36
- data/lib/csv_plus_plus/language/entities/runtime_value.rb +0 -28
- data/lib/csv_plus_plus/language/entities/string.rb +0 -31
- data/lib/csv_plus_plus/language/entities/variable.rb +0 -25
- data/lib/csv_plus_plus/language/entities.rb +0 -28
- data/lib/csv_plus_plus/language/references.rb +0 -70
- data/lib/csv_plus_plus/language/runtime.rb +0 -205
- data/lib/csv_plus_plus/language/scope.rb +0 -188
- data/lib/csv_plus_plus/modifier.tab.rb +0 -907
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d0fb79a1f7a00d60f77289518fb9c6ccac6b059024d5cf66a3f083ec92e77def
|
4
|
+
data.tar.gz: a35796b982b01171636e27b1d1a9d19d6d414249b0e4634947d5a754cb240146
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 45dd804f7889d65ac5f5c63ccc131d774e7ea24097bfe9449af63a48345ca0b9f16ea9e84c17a73151c9ee8d9d90ce4b01d67b7ced817e831e66781d2e7141e0
|
7
|
+
data.tar.gz: 21cda263af6d05d5ee396a9d00b4c1c78f1a043d91787dcf0d822e1adb6a97a2326da836cdc3bbc3c78e43e5c342c1bdb9da966884f1d9d8df364e574f334ded
|
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,23 @@
|
|
1
|
+
## v0.1.2
|
2
|
+
|
3
|
+
- var=... modifier which allows binding a variable to a cell
|
4
|
+
- Improved error handling and messages
|
5
|
+
- Moving in a direction that allows for the context-dependent aspects of modifiers
|
6
|
+
- Fixes a bug with creating a new excel spreadsheet
|
7
|
+
- Docs & tests
|
8
|
+
|
9
|
+
## v0.1.1
|
10
|
+
|
11
|
+
- Better support for the various infix operators (+,-,/,*,^,%,=,<,etc)
|
12
|
+
* Previously we were converting them to their prefix equivalent (multiply, minus, concat, etc) but excel doesn't support most of those. So we keep them infix
|
13
|
+
* Didn't support some infix operators (^, %, </>/<=/>=/<>)
|
14
|
+
* Proper support for operator precedence
|
15
|
+
- When in verbose mode, print a summary of compiled functions and variables
|
16
|
+
- docs & tests
|
1
17
|
|
2
18
|
## v0.1.0
|
3
19
|
|
4
20
|
- revamp of builtin functions
|
5
|
-
|
6
21
|
- docs & tests
|
7
22
|
|
8
23
|
## v0.0.5
|
data/README.md
CHANGED
@@ -9,101 +9,57 @@ A tool that allows you to programatically author spreadsheets in your favorite t
|
|
9
9
|
|
10
10
|
A `csvpp` file consists of a (optional) code section and a CSV section separated by `---`. In the code section you can define variables and functions that can be used in the CSV below it. For example:
|
11
11
|
|
12
|
+
###### **`mystocks.csvpp`**
|
12
13
|
```
|
13
14
|
fees := 0.50 # my broker charges $0.50 a trade
|
14
15
|
|
15
|
-
price :=
|
16
|
-
quantity :=
|
16
|
+
price := celladjacent(C)
|
17
|
+
quantity := celladjacent(D)
|
17
18
|
|
18
19
|
def profit() (price * quantity) - fees
|
19
20
|
|
20
21
|
---
|
21
22
|
![[format=bold/align=center]]Date,Ticker,Price,Quantity,Total,Fees
|
22
|
-
![[expand]],[[format=bold]],,,"=
|
23
|
+
![[expand]],[[format=bold]],,,"=profit()",$$fees
|
23
24
|
```
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
Variables can be defined in the code section by giving a name (a combination of letters, numbers and underscores ) the expression `:=` and followed with a value.
|
28
|
-
|
29
|
-
### Built-in Variables
|
30
|
-
|
31
|
-
* `$$rownum` - The current row number. The first row of the spreadsheet starts at 1. Can be used anywhere and it's value will evaluate to the current row being processed.
|
32
|
-
|
33
|
-
## Functions
|
34
|
-
|
35
|
-
### Built-in Functions
|
36
|
-
* `cellref(CELL)` - Returns a reference to the `CELL` relative to the current row. If the current `$$rownum` is `2`, then `CELLREF("C")` returns a reference to cell `C2`.
|
37
|
-
|
38
|
-
## Modifiers
|
39
|
-
|
40
|
-
Modifiers can change the formatting of a cell or row, apply validation, change alignment, etc. All of the normal rules of CSV apply, with the addition that each cell can have modifiers (specified in `[[`/`]]` for cells and `![[`/`]]` for rows):
|
26
|
+
And can be compiled into a `.xlsx` file by:
|
41
27
|
|
42
28
|
```
|
43
|
-
|
29
|
+
$ csv++ -n 'My Stock Tracker' -o mystocks.xlsx mystocks.csvpp
|
44
30
|
```
|
45
31
|
|
46
|
-
specifying formatting or various other modifiers to the cell. Additionally a row can start with:
|
47
32
|
|
48
|
-
|
49
|
-
![[...]]foo,bar,baz
|
50
|
-
```
|
33
|
+
See the [Language Reference](./docs/LANGUAGE_REFERENCE.md) for a full explanation of features.
|
51
34
|
|
52
|
-
|
35
|
+
## Installing
|
53
36
|
|
54
|
-
|
55
|
-
|
56
|
-
* Align the second cell left, align the last cell to the center and make it bold and italicized:
|
57
|
-
|
58
|
-
```
|
59
|
-
Date,[[align=left]]Amount,Quantity,[[align=center/format=bold italic]]Price
|
60
|
-
```
|
61
|
-
|
62
|
-
* Underline and center-align an entire row:
|
63
|
-
|
64
|
-
```
|
65
|
-
![[align=center/format=underline]]Date,Amount,Quantity,Price
|
66
|
-
```
|
67
|
-
|
68
|
-
* A header for the first row, then some formulas that repeat for each row for the rest of the spreadsheet:
|
37
|
+
Just install it via rubygems (homebrew and debian packages are in the works):
|
69
38
|
|
70
|
-
|
71
|
-
![[align=center/format=bold]]Date,Price,Quantity,Profit
|
72
|
-
![[expand=1:]],,,"=MULTIPLY(cellref(B), cellref(C))"
|
73
|
-
```
|
39
|
+
`$ gem install csv_plus_plus`
|
74
40
|
|
75
|
-
|
41
|
+
or if you want the very latest changes, clone the repository and run:
|
76
42
|
|
77
|
-
|
43
|
+
`$ rake gem:install`
|
78
44
|
|
79
|
-
|
45
|
+
### [Setting Up Google Sheets](./docs/README_GOOGLE_SHEETS.md)
|
80
46
|
|
81
|
-
|
47
|
+
## Examples
|
82
48
|
|
83
|
-
|
84
|
-
* "Share" the spreadsheet with the email associated with the service account
|
49
|
+
Take a look at the [examples](./examples/) directory for a bunch of example `.csvpp` files.
|
85
50
|
|
86
51
|
## CLI Arguments
|
87
52
|
|
88
53
|
```
|
89
54
|
Usage: csv++ [options]
|
55
|
+
-h, --help Show help information
|
90
56
|
-b, --backup Create a backup of the spreadsheet before applying changes.
|
91
|
-
-g, --google-sheet-id SHEET_ID The id of the sheet - you can extract this from the URL: https://docs.google.com/spreadsheets/d/< ... SHEET_ID ... >/edit#gid=0
|
92
57
|
-c, --create Create the sheet if it doesn't exist. It will use --sheet-name if specified
|
58
|
+
-g, --google-sheet-id SHEET_ID The id of the sheet - you can extract this from the URL: https://docs.google.com/spreadsheets/d/< ... SHEET_ID ... >/edit#gid=0
|
93
59
|
-k, --key-values KEY_VALUES A comma-separated list of key=values which will be made available to the template
|
94
60
|
-n, --sheet-name SHEET_NAME The name of the sheet to apply the template to
|
61
|
+
-o, --output OUTPUT_FILE The file to write to (must be .csv, .ods, .xls)
|
95
62
|
-v, --verbose Enable verbose output
|
96
63
|
-x, --offset-columns OFFSET Apply the template offset by OFFSET cells
|
97
64
|
-y, --offset-rows OFFSET Apply the template offset by OFFSET rows
|
98
|
-
-h, --help Show help information
|
99
|
-
```
|
100
|
-
|
101
|
-
## Usage Examples
|
102
|
-
|
103
|
-
```
|
104
|
-
# apply my_taxes_template.csvpp to an existing Google Sheet with name "Taxes 2022"
|
105
|
-
$ csv++ --sheet-name "Taxes 2022" --sheet-id "[...]" my_taxes_template.csvpp
|
106
|
-
|
107
|
-
# take input from stdin, supply a variable ($$rate = 1) and apply to the "Stocks" spreadsheet
|
108
|
-
$ cat stocks.csvpp | csv++ -k "rate=1" -n "Stocks" -i "[...]"
|
109
65
|
```
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CSVPlusPlus
|
4
|
+
# Extend a +Compiler+ class and add benchmark timings
|
5
|
+
#
|
6
|
+
# @attr_reader timings [Array<Benchmark::Tms>] +Benchmark+ timings that have been accumulated by each step of
|
7
|
+
# compilation
|
8
|
+
# @attr_reader benchmark [Benchmark] A +Benchmark+ instance
|
9
|
+
module BenchmarkedCompiler
|
10
|
+
attr_reader :benchmark, :timings
|
11
|
+
|
12
|
+
# Wrap a +Compiler+ with our instance methods that add benchmarks
|
13
|
+
def self.with_benchmarks(compiler, &block)
|
14
|
+
::Benchmark.benchmark(::Benchmark::CAPTION, 25, ::Benchmark::FORMAT, '> Total') do |x|
|
15
|
+
# compiler = new(options:, runtime:, benchmark: x)
|
16
|
+
compiler.extend(self)
|
17
|
+
compiler.benchmark = x
|
18
|
+
|
19
|
+
block.call(compiler)
|
20
|
+
|
21
|
+
[compiler.timings.reduce(:+)]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param benchmark [Benchmark] A +Benchmark+ instance
|
26
|
+
def benchmark=(benchmark)
|
27
|
+
@benchmark = benchmark
|
28
|
+
@timings = []
|
29
|
+
end
|
30
|
+
|
31
|
+
# Time the Compiler#outputting! stage
|
32
|
+
def outputting!
|
33
|
+
time_stage('Writing the spreadsheet') { super }
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def parse_code_section!
|
39
|
+
time_stage('Parsing code section') { super }
|
40
|
+
end
|
41
|
+
|
42
|
+
def parse_csv_section!
|
43
|
+
time_stage('Parsing CSV section') { super }
|
44
|
+
end
|
45
|
+
|
46
|
+
def expanding
|
47
|
+
time_stage('Expanding rows') { super }
|
48
|
+
end
|
49
|
+
|
50
|
+
def resolve_all_cells!(template)
|
51
|
+
time_stage('Resolving each cell') { super(template) }
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def time_stage(stage, &block)
|
57
|
+
ret = nil
|
58
|
+
@timings << @benchmark.report(stage) { ret = block.call }
|
59
|
+
ret
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CSVPlusPlus
|
4
|
+
# Methods for classes that need to manage +@variables+ and +@functions+
|
5
|
+
module CanDefineReferences
|
6
|
+
# Define a (or re-define an existing) variable
|
7
|
+
#
|
8
|
+
# @param id [String, Symbol] The identifier for the variable
|
9
|
+
# @param entity [Entity] The value (entity) the variable holds
|
10
|
+
def def_variable(id, entity)
|
11
|
+
variables[id.to_sym] = entity
|
12
|
+
end
|
13
|
+
|
14
|
+
# Define (or re-define existing) variables
|
15
|
+
#
|
16
|
+
# @param variables [Hash<Symbol, Variable>] Variables to define
|
17
|
+
def def_variables(vars)
|
18
|
+
vars.each { |id, entity| def_variable(id, entity) }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Define a (or re-define an existing) function
|
22
|
+
#
|
23
|
+
# @param id [String, Symbol] The identifier for the function
|
24
|
+
# @param entity [Entities::Function] The defined function
|
25
|
+
def def_function(id, entity)
|
26
|
+
functions[id.to_sym] = entity
|
27
|
+
end
|
28
|
+
|
29
|
+
# Is the variable defined?
|
30
|
+
#
|
31
|
+
# @param var_id [Symbol, String] The identifier of the variable
|
32
|
+
#
|
33
|
+
# @return [boolean]
|
34
|
+
def defined_variable?(var_id)
|
35
|
+
variables.key?(var_id.to_sym)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Is the function defined?
|
39
|
+
#
|
40
|
+
# @param fn_id [Symbol, String] The identifier of the function
|
41
|
+
#
|
42
|
+
# @return [boolean]
|
43
|
+
def defined_function?(fn_id)
|
44
|
+
functions.key?(fn_id.to_sym)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Provide a summary of the functions and variables compiled (to show in verbose mode)
|
48
|
+
#
|
49
|
+
# @return [String]
|
50
|
+
def verbose_summary
|
51
|
+
<<~SUMMARY
|
52
|
+
# Code Section Summary
|
53
|
+
|
54
|
+
## Resolved Variables
|
55
|
+
|
56
|
+
#{variable_summary}
|
57
|
+
|
58
|
+
## Functions
|
59
|
+
|
60
|
+
#{function_summary}
|
61
|
+
SUMMARY
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def variables
|
67
|
+
@variables ||= {}
|
68
|
+
end
|
69
|
+
|
70
|
+
def functions
|
71
|
+
@functions ||= {}
|
72
|
+
end
|
73
|
+
|
74
|
+
def variable_summary
|
75
|
+
return '(no variables defined)' if variables.empty?
|
76
|
+
|
77
|
+
variables.map { |k, v| "#{k} := #{v}" }
|
78
|
+
.join("\n")
|
79
|
+
end
|
80
|
+
|
81
|
+
def function_summary
|
82
|
+
return '(no functions defined)' if functions.empty?
|
83
|
+
|
84
|
+
functions.map { |k, f| "#{k}: #{f}" }
|
85
|
+
.join("\n")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/lib/csv_plus_plus/cell.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '
|
4
|
-
require_relative '
|
3
|
+
require_relative 'modifier'
|
4
|
+
require_relative 'parser/cell_value.tab'
|
5
5
|
|
6
6
|
module CSVPlusPlus
|
7
7
|
# A cell of a template
|
@@ -23,7 +23,7 @@ module CSVPlusPlus
|
|
23
23
|
# @return [Cell]
|
24
24
|
def self.parse(value, runtime:, modifier:)
|
25
25
|
new(value:, row_index: runtime.row_index, index: runtime.cell_index, modifier:).tap do |c|
|
26
|
-
c.ast = ::CSVPlusPlus::
|
26
|
+
c.ast = ::CSVPlusPlus::Parser::CellValue.new.parse(value, runtime)
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
data/lib/csv_plus_plus/cli.rb
CHANGED
@@ -31,14 +31,16 @@ module CSVPlusPlus
|
|
31
31
|
#
|
32
32
|
# @param error [CSVPlusPlus::Error, Google::Apis::ClientError, StandardError]
|
33
33
|
def handle_error(error)
|
34
|
+
# make sure that we're on a newline (verbose mode might be in the middle of printing a benchmark)
|
35
|
+
puts("\n\n") if @options.verbose
|
36
|
+
|
34
37
|
case error
|
35
|
-
when ::CSVPlusPlus::Error
|
38
|
+
when ::CSVPlusPlus::Error::Error
|
36
39
|
handle_internal_error(error)
|
37
40
|
when ::Google::Apis::ClientError
|
38
41
|
handle_google_error(error)
|
39
42
|
else
|
40
|
-
|
41
|
-
warn(error.message)
|
43
|
+
unhandled_error(error)
|
42
44
|
end
|
43
45
|
end
|
44
46
|
|
@@ -48,18 +50,33 @@ module CSVPlusPlus
|
|
48
50
|
option_parser.parse!
|
49
51
|
validate_options
|
50
52
|
rescue ::OptionParser::InvalidOption => e
|
51
|
-
raise(::CSVPlusPlus::Error, e.message)
|
53
|
+
raise(::CSVPlusPlus::Error::Error, e.message)
|
52
54
|
end
|
53
55
|
|
54
|
-
# @return [String]
|
56
|
+
# @return [::String]
|
55
57
|
def to_s
|
56
58
|
"CLI(options: #{options})"
|
57
59
|
end
|
58
60
|
|
59
61
|
private
|
60
62
|
|
63
|
+
# An error was thrown that we weren't planning on
|
64
|
+
def unhandled_error(error)
|
65
|
+
warn(
|
66
|
+
<<~ERROR_MESSAGE)
|
67
|
+
An unexpected error was encountered. Please try running again with --verbose and
|
68
|
+
reporting the error at: https://github.com/patrickomatic/csv-plus-plus/issues/new'
|
69
|
+
ERROR_MESSAGE
|
70
|
+
|
71
|
+
return unless @options.verbose
|
72
|
+
|
73
|
+
warn(error.full_message)
|
74
|
+
warn("Cause: #{error.cause}") if error.cause
|
75
|
+
end
|
76
|
+
|
61
77
|
def handle_internal_error(error)
|
62
|
-
|
78
|
+
case error
|
79
|
+
when ::CSVPlusPlus::Error::SyntaxError
|
63
80
|
warn(@options.verbose ? error.to_verbose_trace : error.to_trace)
|
64
81
|
else
|
65
82
|
warn(error.message)
|
@@ -78,7 +95,7 @@ module CSVPlusPlus
|
|
78
95
|
return if error_message.nil?
|
79
96
|
|
80
97
|
puts(option_parser)
|
81
|
-
raise(::CSVPlusPlus::Error, error_message)
|
98
|
+
raise(::CSVPlusPlus::Error::Error, error_message)
|
82
99
|
end
|
83
100
|
|
84
101
|
def option_parser
|
data/lib/csv_plus_plus/color.rb
CHANGED
@@ -9,13 +9,19 @@ module CSVPlusPlus
|
|
9
9
|
class Color
|
10
10
|
attr_reader :red_hex, :green_hex, :blue_hex
|
11
11
|
|
12
|
-
|
12
|
+
HEX_STRING_REGEXP = /^#?([0-9a-f]{1,2})([0-9a-f]{1,2})([0-9a-f]{1,2})/i
|
13
|
+
public_constant :HEX_STRING_REGEXP
|
14
|
+
|
15
|
+
# @return [boolean]
|
16
|
+
def self.valid_hex_string?(hex_string)
|
17
|
+
!(hex_string.strip =~ ::CSVPlusPlus::Color::HEX_STRING_REGEXP).nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
# Create an instance from a string like "#FFF" or "#FFFFFF"
|
13
21
|
#
|
14
22
|
# @param hex_string [String] The hex string input to parse
|
15
23
|
def initialize(hex_string)
|
16
|
-
@red_hex, @green_hex, @blue_hex = hex_string
|
17
|
-
.gsub(/^#?/, '')
|
18
|
-
.match(/([0-9a-f]{1,2})([0-9a-f]{1,2})([0-9a-f]{1,2})/i)
|
24
|
+
@red_hex, @green_hex, @blue_hex = hex_string.strip.match(::CSVPlusPlus::Color::HEX_STRING_REGEXP)
|
19
25
|
&.captures
|
20
26
|
&.map { |s| s.length == 1 ? s + s : s }
|
21
27
|
end
|
@@ -43,12 +49,12 @@ module CSVPlusPlus
|
|
43
49
|
|
44
50
|
# Create a hex representation of the color (without a '#')
|
45
51
|
#
|
46
|
-
# @return [String]
|
52
|
+
# @return [::String]
|
47
53
|
def to_hex
|
48
54
|
[@red_hex, @green_hex, @blue_hex].join
|
49
55
|
end
|
50
56
|
|
51
|
-
# @return [String]
|
57
|
+
# @return [::String]
|
52
58
|
def to_s
|
53
59
|
"Color(r: #{@red_hex}, g: #{@green_hex}, b: #{@blue_hex})"
|
54
60
|
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'benchmarked_compiler'
|
4
|
+
require_relative 'entities'
|
5
|
+
require_relative 'parser/code_section.tab'
|
6
|
+
require_relative 'runtime'
|
7
|
+
require_relative 'scope'
|
8
|
+
|
9
|
+
module CSVPlusPlus
|
10
|
+
# Encapsulates the parsing and building of objects (+Template+ -> +Row+ -> +Cell+). Variable resolution is delegated
|
11
|
+
# to the +Scope+
|
12
|
+
#
|
13
|
+
# @attr_reader options [Options] The +Options+ to compile with
|
14
|
+
# @attr_reader runtime [Runtime] The runtime execution
|
15
|
+
# @attr_reader scope [Scope] +Scope+ for variable resolution
|
16
|
+
class Compiler
|
17
|
+
attr_reader :options, :runtime, :scope
|
18
|
+
|
19
|
+
# Create a compiler and make sure it gets cleaned up
|
20
|
+
#
|
21
|
+
# @param runtime [Runtime] The initial +Runtime+ for the compiler
|
22
|
+
# @param options [Options]
|
23
|
+
def self.with_compiler(runtime:, options:, &block)
|
24
|
+
compiler = new(options:, runtime:)
|
25
|
+
if options.verbose
|
26
|
+
::CSVPlusPlus::BenchmarkedCompiler.with_benchmarks(compiler) do |c|
|
27
|
+
block.call(c)
|
28
|
+
end
|
29
|
+
else
|
30
|
+
yield(compiler)
|
31
|
+
end
|
32
|
+
ensure
|
33
|
+
runtime.cleanup!
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param runtime [Runtime]
|
37
|
+
# @param options [Options]
|
38
|
+
# @param scope [Scope, nil]
|
39
|
+
def initialize(runtime:, options:, scope: nil)
|
40
|
+
@options = options
|
41
|
+
@runtime = runtime
|
42
|
+
@scope = scope || ::CSVPlusPlus::Scope.new(runtime:)
|
43
|
+
|
44
|
+
# TODO: infer a type
|
45
|
+
# allow user-supplied key/values to override anything global or from the code section
|
46
|
+
@scope.def_variables(
|
47
|
+
options.key_values.transform_values { |v| ::CSVPlusPlus::Entities::String.new(v.to_s) }
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Write the compiled results
|
52
|
+
def outputting!
|
53
|
+
@runtime.start_at_csv!
|
54
|
+
yield
|
55
|
+
end
|
56
|
+
|
57
|
+
# Compile a template and return a +::CSVPlusPlus::Template+ instance ready to be written with a +Writer+
|
58
|
+
#
|
59
|
+
# @return [Template]
|
60
|
+
def compile_template
|
61
|
+
parse_code_section!
|
62
|
+
rows = parse_csv_section!
|
63
|
+
|
64
|
+
::CSVPlusPlus::Template.new(rows:, scope: @scope).tap do |t|
|
65
|
+
t.validate_infinite_expands(@runtime)
|
66
|
+
expanding { t.expand_rows! }
|
67
|
+
resolve_all_cells!(t)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [String]
|
72
|
+
def to_s
|
73
|
+
"Compiler(options: #{@options}, runtime: #{@runtime}, scope: #{@scope})"
|
74
|
+
end
|
75
|
+
|
76
|
+
protected
|
77
|
+
|
78
|
+
# Parses the input file and returns a +CodeSection+
|
79
|
+
#
|
80
|
+
# @return [CodeSection]
|
81
|
+
def parse_code_section!
|
82
|
+
@runtime.start!
|
83
|
+
|
84
|
+
# TODO: this flow can probably be refactored, it used to have more needs back when we had to
|
85
|
+
# parse and save the code_section
|
86
|
+
parsing_code_section do |input|
|
87
|
+
csv_section = ::CSVPlusPlus::Parser::CodeSection.new(@scope).parse(input, @runtime)
|
88
|
+
# TODO: call scope.resolve_static_variables?? or maybe it doesn't matter
|
89
|
+
|
90
|
+
# return the csv_section to the caller because they're gonna re-write input with it
|
91
|
+
next csv_section
|
92
|
+
end
|
93
|
+
# @scope.code_section
|
94
|
+
end
|
95
|
+
|
96
|
+
# Parse the CSV section and return an array of +Row+s
|
97
|
+
#
|
98
|
+
# @return [Array<Row>]
|
99
|
+
def parse_csv_section!
|
100
|
+
@runtime.start_at_csv!
|
101
|
+
@runtime.map_rows(::CSV.new(runtime.input)) do |csv_row|
|
102
|
+
parse_row(csv_row)
|
103
|
+
end
|
104
|
+
ensure
|
105
|
+
# we're done with the file and everything is in memory
|
106
|
+
@runtime.cleanup!
|
107
|
+
end
|
108
|
+
|
109
|
+
# Iterates through each cell of each row and resolves it's variable and function references.
|
110
|
+
#
|
111
|
+
# @param template [Template]
|
112
|
+
# @return [Array<Entity>]
|
113
|
+
def resolve_all_cells!(template)
|
114
|
+
@runtime.start_at_csv!
|
115
|
+
@runtime.map_rows(template.rows, cells_too: true) do |cell|
|
116
|
+
cell.ast = @scope.resolve_cell_value if cell.ast
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Expanding rows
|
121
|
+
def expanding
|
122
|
+
@runtime.start_at_csv!
|
123
|
+
yield
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def parsing_code_section
|
129
|
+
csv_section = yield(@runtime.input.read)
|
130
|
+
@runtime.rewrite_input!(csv_section)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Using the current +@runtime+ and the given +csv_row+ parse it into a +Row+ of +Cell+s
|
134
|
+
# +csv_row+ should have already been run through a CSV parser and is an array of strings
|
135
|
+
#
|
136
|
+
# @param csv_row [Array<Array<String>>]
|
137
|
+
# @return [Row]
|
138
|
+
def parse_row(csv_row)
|
139
|
+
row_modifier = ::CSVPlusPlus::ValidatedModifier.new(row_level: true)
|
140
|
+
|
141
|
+
cells = @runtime.map_row(csv_row) { |value, _cell_index| parse_cell(value, row_modifier) }
|
142
|
+
|
143
|
+
::CSVPlusPlus::Row.new(@runtime.row_index, cells, row_modifier)
|
144
|
+
end
|
145
|
+
|
146
|
+
def parse_cell(value, row_modifier)
|
147
|
+
cell_modifier = ::CSVPlusPlus::ValidatedModifier.new
|
148
|
+
parsed_value = ::CSVPlusPlus::Parser::Modifier.new(cell_modifier:, row_modifier:, scope: @scope).parse(
|
149
|
+
value,
|
150
|
+
@runtime
|
151
|
+
)
|
152
|
+
|
153
|
+
::CSVPlusPlus::Cell.parse(parsed_value, runtime:, modifier: cell_modifier)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|