csv_plus_plus 0.1.1 → 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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +18 -62
  4. data/lib/csv_plus_plus/benchmarked_compiler.rb +62 -0
  5. data/lib/csv_plus_plus/{code_section.rb → can_define_references.rb} +22 -35
  6. data/lib/csv_plus_plus/can_resolve_references.rb +8 -0
  7. data/lib/csv_plus_plus/cell.rb +3 -3
  8. data/lib/csv_plus_plus/cli.rb +24 -7
  9. data/lib/csv_plus_plus/color.rb +12 -6
  10. data/lib/csv_plus_plus/compiler.rb +156 -0
  11. data/lib/csv_plus_plus/data_validation.rb +138 -0
  12. data/lib/csv_plus_plus/{language → entities}/ast_builder.rb +3 -5
  13. data/lib/csv_plus_plus/entities/boolean.rb +31 -0
  14. data/lib/csv_plus_plus/{language → entities}/builtins.rb +2 -4
  15. data/lib/csv_plus_plus/entities/cell_reference.rb +60 -0
  16. data/lib/csv_plus_plus/entities/date.rb +30 -0
  17. data/lib/csv_plus_plus/entities/entity.rb +84 -0
  18. data/lib/csv_plus_plus/entities/function.rb +33 -0
  19. data/lib/csv_plus_plus/entities/function_call.rb +35 -0
  20. data/lib/csv_plus_plus/entities/number.rb +34 -0
  21. data/lib/csv_plus_plus/entities/runtime_value.rb +26 -0
  22. data/lib/csv_plus_plus/entities/string.rb +29 -0
  23. data/lib/csv_plus_plus/entities/variable.rb +25 -0
  24. data/lib/csv_plus_plus/entities.rb +33 -0
  25. data/lib/csv_plus_plus/error/error.rb +10 -0
  26. data/lib/csv_plus_plus/error/formula_syntax_error.rb +36 -0
  27. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +27 -0
  28. data/lib/csv_plus_plus/error/modifier_validation_error.rb +49 -0
  29. data/lib/csv_plus_plus/{language → error}/syntax_error.rb +6 -14
  30. data/lib/csv_plus_plus/error/writer_error.rb +9 -0
  31. data/lib/csv_plus_plus/error.rb +9 -2
  32. data/lib/csv_plus_plus/expand.rb +3 -1
  33. data/lib/csv_plus_plus/google_api_client.rb +4 -0
  34. data/lib/csv_plus_plus/lexer/lexer.rb +13 -6
  35. data/lib/csv_plus_plus/modifier/conditional_formatting.rb +17 -0
  36. data/lib/csv_plus_plus/modifier.rb +73 -65
  37. data/lib/csv_plus_plus/{language → parser}/cell_value.tab.rb +20 -20
  38. data/lib/csv_plus_plus/{language → parser}/code_section.tab.rb +83 -87
  39. data/lib/csv_plus_plus/parser/modifier.tab.rb +484 -0
  40. data/lib/csv_plus_plus/references.rb +68 -0
  41. data/lib/csv_plus_plus/row.rb +0 -3
  42. data/lib/csv_plus_plus/runtime.rb +199 -0
  43. data/lib/csv_plus_plus/scope.rb +196 -0
  44. data/lib/csv_plus_plus/template.rb +10 -10
  45. data/lib/csv_plus_plus/validated_modifier.rb +164 -0
  46. data/lib/csv_plus_plus/version.rb +1 -1
  47. data/lib/csv_plus_plus/writer/file_backer_upper.rb +6 -4
  48. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +24 -29
  49. data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +33 -12
  50. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +3 -6
  51. data/lib/csv_plus_plus.rb +19 -10
  52. metadata +34 -24
  53. data/lib/csv_plus_plus/language/benchmarked_compiler.rb +0 -65
  54. data/lib/csv_plus_plus/language/compiler.rb +0 -152
  55. data/lib/csv_plus_plus/language/entities/boolean.rb +0 -33
  56. data/lib/csv_plus_plus/language/entities/cell_reference.rb +0 -33
  57. data/lib/csv_plus_plus/language/entities/entity.rb +0 -86
  58. data/lib/csv_plus_plus/language/entities/function.rb +0 -35
  59. data/lib/csv_plus_plus/language/entities/function_call.rb +0 -37
  60. data/lib/csv_plus_plus/language/entities/number.rb +0 -36
  61. data/lib/csv_plus_plus/language/entities/runtime_value.rb +0 -28
  62. data/lib/csv_plus_plus/language/entities/string.rb +0 -31
  63. data/lib/csv_plus_plus/language/entities/variable.rb +0 -25
  64. data/lib/csv_plus_plus/language/entities.rb +0 -28
  65. data/lib/csv_plus_plus/language/references.rb +0 -70
  66. data/lib/csv_plus_plus/language/runtime.rb +0 -205
  67. data/lib/csv_plus_plus/language/scope.rb +0 -192
  68. 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: 6ef86258faa82b75114acc715e9cb68bb312dc55194d533270fce0a796e02362
4
- data.tar.gz: 9316f43dd4169a779efc6d8f447002434c4fc5984064b1f22f1448baee0f4522
3
+ metadata.gz: d0fb79a1f7a00d60f77289518fb9c6ccac6b059024d5cf66a3f083ec92e77def
4
+ data.tar.gz: a35796b982b01171636e27b1d1a9d19d6d414249b0e4634947d5a754cb240146
5
5
  SHA512:
6
- metadata.gz: 8637e6c3dec41bf747e5fe1f6eb4d3227247e196ef530bbac96e964f6cdbe911d39bed9941ab7e116f00ea2547cf5050a609f078af8dab8f28a6e0f7746e776d
7
- data.tar.gz: 57acb6edef384a31a41ea244841d2977d31817d87735944336688f5961d6eb22c97e5b88683d753c1163239f397936411a7836eafe1aef6e9ed6a612e846fd68
6
+ metadata.gz: 45dd804f7889d65ac5f5c63ccc131d774e7ea24097bfe9449af63a48345ca0b9f16ea9e84c17a73151c9ee8d9d90ce4b01d67b7ced817e831e66781d2e7141e0
7
+ data.tar.gz: 21cda263af6d05d5ee396a9d00b4c1c78f1a043d91787dcf0d822e1adb6a97a2326da836cdc3bbc3c78e43e5c342c1bdb9da966884f1d9d8df364e574f334ded
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
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
+
1
9
  ## v0.1.1
2
10
 
3
11
  - Better support for the various infix operators (+,-,/,*,^,%,=,<,etc)
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 := cellref(C)
16
- quantity := cellref(D)
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]],,,"=PROFIT()",$$fees
23
+ ![[expand]],[[format=bold]],,,"=profit()",$$fees
23
24
  ```
24
25
 
25
- ## Variables
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
- foo,[[...]]bar,baz
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
- which will apply that modifier to all cells in the row.
35
+ ## Installing
53
36
 
54
- ### Examples
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
- ## Setup (Google Sheets)
41
+ or if you want the very latest changes, clone the repository and run:
76
42
 
77
- Just install it via rubygems (homebrew and debian packages are in the works):
43
+ `$ rake gem:install`
78
44
 
79
- `$ gem install csv_plus_plus`
45
+ ### [Setting Up Google Sheets](./docs/README_GOOGLE_SHEETS.md)
80
46
 
81
- ### Publishing to Google Sheets
47
+ ## Examples
82
48
 
83
- * Go to the [GCP developers console](https://console.cloud.google.com/projectselector2/apis/credentials?pli=1&supportedpurview=project), create a service account and export keys for it to `~/.config/gcloud/application_default_credentials.json`
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
@@ -1,37 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './language/code_section.tab'
4
- require_relative './language/entities'
5
-
6
3
  module CSVPlusPlus
7
- # A representation of the code section part of a template (the variable and function definitions)
8
- #
9
- # @attr variables [Hash<Symbol, Variable>] All defined variables
10
- # @attr_reader functions [Hash<Symbol, Function>] All defined functions
11
- class CodeSection
12
- attr_reader :functions
13
- attr_accessor :variables
14
-
15
- # @param variables [Hash<Symbol, Variable>] Initial variables
16
- # @param functions [Hash<Symbol, Variable>] Initial functions
17
- def initialize(variables: {}, functions: {})
18
- @variables = variables
19
- @functions = functions
20
- end
21
-
4
+ # Methods for classes that need to manage +@variables+ and +@functions+
5
+ module CanDefineReferences
22
6
  # Define a (or re-define an existing) variable
23
7
  #
24
8
  # @param id [String, Symbol] The identifier for the variable
25
9
  # @param entity [Entity] The value (entity) the variable holds
26
10
  def def_variable(id, entity)
27
- @variables[id.to_sym] = entity
11
+ variables[id.to_sym] = entity
28
12
  end
29
13
 
30
14
  # Define (or re-define existing) variables
31
15
  #
32
16
  # @param variables [Hash<Symbol, Variable>] Variables to define
33
- def def_variables(variables)
34
- variables.each { |id, entity| def_variable(id, entity) }
17
+ def def_variables(vars)
18
+ vars.each { |id, entity| def_variable(id, entity) }
35
19
  end
36
20
 
37
21
  # Define a (or re-define an existing) function
@@ -39,7 +23,7 @@ module CSVPlusPlus
39
23
  # @param id [String, Symbol] The identifier for the function
40
24
  # @param entity [Entities::Function] The defined function
41
25
  def def_function(id, entity)
42
- @functions[id.to_sym] = entity
26
+ functions[id.to_sym] = entity
43
27
  end
44
28
 
45
29
  # Is the variable defined?
@@ -48,7 +32,7 @@ module CSVPlusPlus
48
32
  #
49
33
  # @return [boolean]
50
34
  def defined_variable?(var_id)
51
- @variables.key?(var_id.to_sym)
35
+ variables.key?(var_id.to_sym)
52
36
  end
53
37
 
54
38
  # Is the function defined?
@@ -57,12 +41,7 @@ module CSVPlusPlus
57
41
  #
58
42
  # @return [boolean]
59
43
  def defined_function?(fn_id)
60
- @functions.key?(fn_id.to_sym)
61
- end
62
-
63
- # @return [String]
64
- def to_s
65
- "CodeSection(functions: #{@functions}, variables: #{@variables})"
44
+ functions.key?(fn_id.to_sym)
66
45
  end
67
46
 
68
47
  # Provide a summary of the functions and variables compiled (to show in verbose mode)
@@ -84,18 +63,26 @@ module CSVPlusPlus
84
63
 
85
64
  private
86
65
 
66
+ def variables
67
+ @variables ||= {}
68
+ end
69
+
70
+ def functions
71
+ @functions ||= {}
72
+ end
73
+
87
74
  def variable_summary
88
- return '(no variables defined)' if @variables.empty?
75
+ return '(no variables defined)' if variables.empty?
89
76
 
90
- @variables.map { |k, v| "#{k} := #{v}" }
91
- .join("\n")
77
+ variables.map { |k, v| "#{k} := #{v}" }
78
+ .join("\n")
92
79
  end
93
80
 
94
81
  def function_summary
95
- return '(no functions defined)' if @functions.empty?
82
+ return '(no functions defined)' if functions.empty?
96
83
 
97
- @functions.map { |k, f| "#{k}: #{f}" }
98
- .join("\n")
84
+ functions.map { |k, f| "#{k}: #{f}" }
85
+ .join("\n")
99
86
  end
100
87
  end
101
88
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVPlusPlus
4
+ # Methods for classes that need to resolve references
5
+ module CanResolveReferences
6
+ # TODO
7
+ end
8
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './language/cell_value.tab'
4
- require_relative './modifier'
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::Language::CellValueParser.new.parse(value, runtime)
26
+ c.ast = ::CSVPlusPlus::Parser::CellValue.new.parse(value, runtime)
27
27
  end
28
28
  end
29
29
 
@@ -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
- # TODO: more if verbose?
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
- if error.is_a?(::CSVPlusPlus::Language::SyntaxError)
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
@@ -9,13 +9,19 @@ module CSVPlusPlus
9
9
  class Color
10
10
  attr_reader :red_hex, :green_hex, :blue_hex
11
11
 
12
- # create an instance from a string like "#FFF" or "#FFFFFF"
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