csv_plus_plus 0.1.0 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|